Skip to content

fix(build): bundle the Lambda handler as CommonJS - #281

Merged
alexholtz merged 4 commits into
mainfrom
fix/esm-bundle-package-json
Oct 16, 2026
Merged

alexholtz merged 4 commits into
mainfrom
fix/esm-bundle-package-json

Conversation

@alexholtz

@alexholtz alexholtz commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Since v2.3.0 (#191, switch to Bun), bun build emits build/proxy.js as an ES module (import … / export { handler }). The repo's package.json sets "type": "module", but publish.yaml only zips build and node_modules, so the released proxy-lambda.zip has no package.json. Lambda's Node runtime therefore loads build/proxy.js as CommonJS and fails during init on every invocation:

Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module

API Gateway returns 502 {"message": "Internal server error"} in well under a second for every webhook. Every release from v2.3.0 to v2.4.3 is affected for anyone deploying the published zip. We hit it in production after upgrading an internal deployment from v2.1.0 (still built with tsc as CommonJS) to v2.4.3.

Fix

  • bun build … --format cjs emits build/proxy.js as CommonJS, which is how Lambda loads it by default. That matches the format v2.1.0 shipped. It doesn't depend on a sidecar package.json, and the handler path (build/proxy.handler), runtime and Terraform don't change. (An earlier commit in this PR generated build/package.json with "type": "module". Review feedback suggested fixing this in the bundler instead, and the later commits replace it.)
  • New package script, used by both publish.yaml and CI, so they zip the same contents.
  • New CI step Smoke Test Lambda Bundle: unzips the packaged proxy-lambda.zip and requires build/proxy.js on Node 20 (the lambda.tf runtime). It runs with --no-experimental-require-module --no-experimental-detect-module. Node 20.19+ can require() ESM by default, which would let a broken bundle pass, so the flags ensure only a real CommonJS bundle loads.

Verification

Each step was pushed separately so CI shows both sides:

Commit Result
test(ci): smoke test loading the Lambda bundle as Lambda does ❌ failed on the ESM bundle
fix(build): mark Lambda bundle as an ES module ✅ passed (superseded)
test(ci): smoke test the packaged zip by requiring the handler as CommonJS ❌ failed: ERR_REQUIRE_ESM
fix(build): bundle the Lambda handler as CommonJS ✅ passed

Out of scope (follow-up)

  • Upgrading to nodejs24.x and setting a minimum AWS provider version, which needs a major release.
  • Dropping node_modules from the zip, since the bundle inlines its dependencies. Left out to avoid changing the artifact's contents in an outage-fix patch.

Releasing as fix: gives a v2.4.4 patch.

🤖 Generated with Claude Code

alholtz and others added 2 commits October 15, 2026 18:11
Load build/proxy.js with Node 20 from a directory containing only the
files publish.yaml zips, so the repo-root package.json cannot mark it as
an ES module, and with module syntax detection disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bun build emits build/proxy.js as ESM, but proxy-lambda.zip contains only
build/ and node_modules/, so Lambda loads it as CommonJS and fails init
with "Cannot use import statement outside a module". Write
build/package.json with "type": "module" so it ships in the zip.

Affects releases v2.3.0 through v2.4.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexholtz
alexholtz marked this pull request as ready for review October 15, 2026 23:19
@alexholtz
alexholtz requested a review from a team as a code owner October 15, 2026 23:19
@danadajian

Copy link
Copy Markdown
Contributor

Thanks for tracking this down, and for pushing the failing test and the fix as separate commits so CI shows both sides. I'd suggest fixing this in the bundler instead of shipping a generated build/package.json:

"build": "bun build lambda/proxy.ts --outdir build/ --target node --format cjs"

This emits build/proxy.js as CommonJS, which is what Lambda loads by default. The bundle doesn't depend on a sidecar file, and the handler path in lambda.tf doesn't change. I checked this locally on Node 20.20.2 and 24.15.0, with require(esm) turned off, loading from a directory with no package.json:

Build Result
Current (ESM proxy.js) SyntaxError: Cannot use import statement outside a module
--format cjs handler is exported as a function

Some related suggestions:

1. Upgrade the runtime to Node 24. Node 20 is past end-of-life. nodejs24.x drops callback-style handlers, but handler is already async, so the code doesn't need to change.

# lambda.tf
runtime = "nodejs24.x"

The AWS provider added nodejs24.x in 6.21.0. versions.tf doesn't constrain the provider today, so adding a minimum version makes consumers on an older provider fail at terraform init with a clear message, instead of hitting an invalid-runtime error at plan:

# versions.tf
aws = {
  source  = "hashicorp/aws"
  version = ">= 6.21.0"
}

This requires consumers still on provider 5.x to upgrade, so the runtime change may be better released as a new major version, separate from the fix: patch. The CJS bundle runs on both nodejs20.x and nodejs24.x, so consumers can upload the new zip and update the module in either order.

2. Drop node_modules from the zip. bun build inlines all dependencies. The bundle only imports Node built-ins (crypto, https, zlib, etc.), so node_modules is dead weight in proxy-lambda.zip. A shared script keeps publishing and CI in sync:

"package": "zip -r -qq proxy-lambda.zip build"
# publish.yaml
- name: Zip Lambda
  run: bun run package

3. Smoke test the real artifact on the Lambda Node version. Unzipping the release zip, instead of copying build and node_modules by hand, means the test can't drift from what gets published. Node 24 turns on require(esm) by default, so a plain require() loads the broken ESM bundle and the test would pass. --no-experimental-require-module makes the test require a real CommonJS bundle.

- name: Setup Node
  uses: actions/setup-node@v7
  with:
    # Keep in sync with the Lambda runtime in lambda.tf
    node-version: 24

- name: Smoke Test Lambda Bundle
  run: |
    bun run package
    unzip -q proxy-lambda.zip -d "$RUNNER_TEMP/lambda"
    cd "$RUNNER_TEMP/lambda"
    node --no-experimental-require-module -e "
      if (typeof require('./build/proxy.js').handler !== 'function') throw new Error('build/proxy.js does not export handler');
    "

alholtz and others added 2 commits October 16, 2026 12:42
…monJS

Packages through a shared package script (also used by publish.yaml), unzips it, and requires build/proxy.js on Node 20 with require(esm) and module syntax detection turned off. This tests the real artifact and requires the bundle itself to be CommonJS, so it fails against the current ESM bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the generated build/package.json with a CommonJS bundle, so build/proxy.js loads on Lambda without depending on a sidecar file. This matches the format v2.1.0 shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexholtz alexholtz changed the title fix(build): mark Lambda bundle as an ES module fix(build): bundle the Lambda handler as CommonJS Oct 16, 2026

@danadajian danadajian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The CJS bundle, shared package script, and smoke test against the packaged zip all landed as suggested. Dropping node_modules from the zip and the Node 24 runtime upgrade (with the minimum AWS provider version) will be handled in a follow-up.

@alexholtz
alexholtz merged commit ac9d7fc into main Oct 16, 2026
4 checks passed
@alexholtz
alexholtz deleted the fix/esm-bundle-package-json branch October 16, 2026 17:58
@eg-oss-ci

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.4.4 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants