◐ Shell
clean mode source ↗

sea: support ESM entry point in SEA by joyeecheung · Pull Request #61813 · nodejs/node

joyeecheung

This uses the new StartExecutionCallbackWithModule embedder
API to support ESM entrypoint in SEA via a new configuration
field `"mainFormat"`. The behavior currently aligns with the
embedder API and is mostly in sync with the CommonJS entry
point behavior, except that support for code caching and
snapshot is left for follow-ups.

addaleax

@addaleax addaleax added request-ci

Add this label to start a Jenkins CI on a PR.

author ready

PRs that have at least one approval, no pending requests for changes, and a CI started.

labels

Feb 14, 2026

@joyeecheung joyeecheung added commit-queue

Add this label to land a pull request using GitHub Actions.

semver-minor

PRs that contain new features and should be released in the next minor version.

notable-change

PRs with changes that should be highlighted in changelogs.

labels

Feb 18, 2026

aduh95 pushed a commit that referenced this pull request

Feb 19, 2026
This uses the new StartExecutionCallbackWithModule embedder
API to support ESM entrypoint in SEA via a new configuration
field `"mainFormat"`. The behavior currently aligns with the
embedder API and is mostly in sync with the CommonJS entry
point behavior, except that support for code caching and
snapshot is left for follow-ups.

PR-URL: #61813
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

@hyf0 hyf0 mentioned this pull request

Feb 21, 2026

6 tasks

nodejs-github-bot added a commit that referenced this pull request

Feb 22, 2026
Notable changes:

http2:
  * (SEMVER-MINOR) add http1Options for HTTP/1 fallback configuration (Amol Yadav) #61713
sea:
  * (SEMVER-MINOR) support ESM entry point in SEA (Joyee Cheung) #61813
sqlite:
  * mark as release candidate (Matteo Collina) #61262
stream:
  * (SEMVER-MINOR) rename `Duplex.toWeb()` type option to `readableType` (René) #61632
test_runner:
  * (SEMVER-MINOR) show interrupted test on SIGINT (Matteo Collina) #61676

PR-URL: #61922

aduh95 added a commit that referenced this pull request

Feb 22, 2026
Notable changes:

http2:
  * (SEMVER-MINOR) add http1Options for HTTP/1 fallback configuration (Amol Yadav) #61713
sea:
  * (SEMVER-MINOR) support ESM entry point in SEA (Joyee Cheung) #61813
sqlite:
  * mark as release candidate (Matteo Collina) #61262
stream:
  * (SEMVER-MINOR) rename `Duplex.toWeb()` type option to `readableType` (René) #61632
test_runner:
  * (SEMVER-MINOR) show interrupted test on SIGINT (Matteo Collina) #61676

PR-URL: #61922

aduh95 added a commit that referenced this pull request

Feb 23, 2026
Notable changes:

http2:
  * (SEMVER-MINOR) add http1Options for HTTP/1 fallback configuration (Amol Yadav) #61713
sea:
  * (SEMVER-MINOR) support ESM entry point in SEA (Joyee Cheung) #61813
sqlite:
  * mark as release candidate (Matteo Collina) #61262
stream:
  * (SEMVER-MINOR) rename `Duplex.toWeb()` type option to `readableType` (René) #61632
test_runner:
  * (SEMVER-MINOR) show interrupted test on SIGINT (Matteo Collina) #61676

PR-URL: #61922

aduh95 added a commit to aduh95/node that referenced this pull request

Feb 24, 2026
Notable changes:

http2:
  * (SEMVER-MINOR) add http1Options for HTTP/1 fallback configuration (Amol Yadav) nodejs#61713
sea:
  * (SEMVER-MINOR) support ESM entry point in SEA (Joyee Cheung) nodejs#61813
sqlite:
  * mark as release candidate (Matteo Collina) nodejs#61262
stream:
  * (SEMVER-MINOR) rename `Duplex.toWeb()` type option to `readableType` (René) nodejs#61632
test_runner:
  * (SEMVER-MINOR) show interrupted test on SIGINT (Matteo Collina) nodejs#61676

PR-URL: nodejs#61922

ruyadorno pushed a commit that referenced this pull request

Feb 24, 2026
Notable changes:

http2:
  * (SEMVER-MINOR) add http1Options for HTTP/1 fallback configuration (Amol Yadav) #61713
sea:
  * (SEMVER-MINOR) support ESM entry point in SEA (Joyee Cheung) #61813
sqlite:
  * mark as release candidate (Matteo Collina) #61262
stream:
  * (SEMVER-MINOR) rename `Duplex.toWeb()` type option to `readableType` (René) #61632
test_runner:
  * (SEMVER-MINOR) show interrupted test on SIGINT (Matteo Collina) #61676

PR-URL: #61922

tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request

Mar 2, 2026

@sxzz sxzz mentioned this pull request

Mar 5, 2026

himself65 added a commit to himself65/node that referenced this pull request

Mar 10, 2026
This uses the new StartExecutionCallbackWithModule embedder
API to support ESM entrypoint in SEA via a new configuration
field `"mainFormat"`. The behavior currently aligns with the
embedder API and is mostly in sync with the CommonJS entry
point behavior, except that support for code caching and
snapshot is left for follow-ups.

PR-URL: nodejs#61813
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

himself65 added a commit to himself65/node that referenced this pull request

Mar 10, 2026
This uses the new StartExecutionCallbackWithModule embedder
API to support ESM entrypoint in SEA via a new configuration
field `"mainFormat"`. The behavior currently aligns with the
embedder API and is mostly in sync with the CommonJS entry
point behavior, except that support for code caching and
snapshot is left for follow-ups.

PR-URL: nodejs#61813
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

himself65 pushed a commit to himself65/node that referenced this pull request

Mar 10, 2026
This uses the new StartExecutionCallbackWithModule embedder
API to support ESM entrypoint in SEA via a new configuration
field `"mainFormat"`. The behavior currently aligns with the
embedder API and is mostly in sync with the CommonJS entry
point behavior, except that support for code caching and
snapshot is left for follow-ups.

PR-URL: nodejs#61813
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

nodejs-github-bot pushed a commit that referenced this pull request

Mar 10, 2026
The initial support for ESM entrypoint in SEA didn't support
code cache. This patch implements that by following a path
similar to how code cache in CJS SEA entrypoint is supported:
at build time we generate the code cache from C++ and put it
into the sea blob, and at runtime we consume it via a special
case in compilation routines - for CJS this was
CompileFunctionForCJSLoader, in the case of SourceTextModule,
it's in Module::New.

PR-URL: #62158
Refs: #61813
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

aduh95 pushed a commit that referenced this pull request

Mar 25, 2026
The initial support for ESM entrypoint in SEA didn't support
code cache. This patch implements that by following a path
similar to how code cache in CJS SEA entrypoint is supported:
at build time we generate the code cache from C++ and put it
into the sea blob, and at runtime we consume it via a special
case in compilation routines - for CJS this was
CompileFunctionForCJSLoader, in the case of SourceTextModule,
it's in Module::New.

PR-URL: #62158
Refs: #61813
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

aduh95 pushed a commit to himself65/node that referenced this pull request

Apr 5, 2026
This uses the new StartExecutionCallbackWithModule embedder
API to support ESM entrypoint in SEA via a new configuration
field `"mainFormat"`. The behavior currently aligns with the
embedder API and is mostly in sync with the CommonJS entry
point behavior, except that support for code caching and
snapshot is left for follow-ups.

PR-URL: nodejs#61813
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

aduh95 pushed a commit to himself65/node that referenced this pull request

Apr 5, 2026
The initial support for ESM entrypoint in SEA didn't support
code cache. This patch implements that by following a path
similar to how code cache in CJS SEA entrypoint is supported:
at build time we generate the code cache from C++ and put it
into the sea blob, and at runtime we consume it via a special
case in compilation routines - for CJS this was
CompileFunctionForCJSLoader, in the case of SourceTextModule,
it's in Module::New.

PR-URL: nodejs#62158
Refs: nodejs#61813
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

aduh95 pushed a commit to himself65/node that referenced this pull request

Apr 5, 2026
The initial support for ESM entrypoint in SEA didn't support
code cache. This patch implements that by following a path
similar to how code cache in CJS SEA entrypoint is supported:
at build time we generate the code cache from C++ and put it
into the sea blob, and at runtime we consume it via a special
case in compilation routines - for CJS this was
CompileFunctionForCJSLoader, in the case of SourceTextModule,
it's in Module::New.

PR-URL: nodejs#62158
Refs: nodejs#61813
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>

This was referenced

Apr 7, 2026

zkochan added a commit to pnpm/pnpm that referenced this pull request

Apr 21, 2026
## Summary

`@pnpm/exe@11.0.0-rc.4` aborts on every invocation with:

```
node::sea::(anonymous namespace)::SeaDeserializer::Read() at ../src/node_sea.cc:174
Assertion failed: (format_value) <= (static_cast<uint8_t>(ModuleFormat::kModule))
```

Two independent Node.js v25.7+ SEA regressions are responsible, both surfaced by the rc.4 bump of the embedded runtime from 25.6.1 to 25.9.0. This PR fixes both and adds a prepublish smoke test so a broken binary can't reach npm again.

## Root cause

**1. SEA blob format changed in Node.js v25.7.0** ([nodejs/node#61813](nodejs/node#61813) added ESM-entry-point support and inserted a new `ModuleFormat` byte into the blob header). SEA blobs carry no version marker, so a blob written by one Node.js version can only be deserialized by a matching one. In rc.4, the CI host Node.js (25.6.1, pre-change) wrote the blob and it was embedded in a 25.9.0 runtime (post-change) — the deserializer reads a misaligned byte as `format_value`, exceeds `kModule`, `CHECK_LE` fires, `SIGABRT`. `resolveBuilderBinary()` was preferring `process.execPath` whenever the running Node supported `--build-sea`, never checking that its version matched the embedded runtime.

**2. Node.js v25.7+ replaces the ambient `require` and `import()` inside a CJS SEA entry with embedder hooks** that only resolve built-in module names. The `pnpm.cjs` shim loaded `dist/pnpm.mjs` via `await import(pathToFileURL(...).href)`, which after the fix to (1) reached the CJS entry and then blew up with:

```
ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: file:///.../dist/pnpm.mjs
    at loadBuiltinModuleForEmbedder
    at importModuleDynamicallyForEmbedder
```

## Changes

- **`releasing/commands/src/pack-app/packApp.ts`** — `resolveBuilderBinary` now takes the resolved target runtime version and only reuses `process.execPath` when `process.version` exactly matches; otherwise it downloads a host-arch Node of the target version via the existing `ensureNodeRuntime` path. Added `PACK_APP_RUNTIME_TOO_OLD` for runtimes older than v25.5 (no `--build-sea`). Removed the now-unused `DEFAULT_BUILDER_SPEC` and the stale `fetch`/`nodeDownloadMirrors` args on the builder resolver. Help text / examples refreshed to drop `node@22` / `node@lts` references that would now be rejected.
- **`pnpm/pnpm.cjs`** — loads `dist/pnpm.mjs` through `Module.createRequire(process.execPath)` instead of `await import(fileURL)`. `createRequire` returns a regular CJS loader that bypasses the SEA embedder hooks, and the pnpm bundle has no top-level await so synchronous `require` of ESM (Node 22+) loads it cleanly. No build-time paths are baked in — `process.execPath` is evaluated at runtime, verified by relocation-testing the darwin-arm64 SEA under `/tmp/`.
- **`pnpm/artifacts/verify-binary.mjs`** (new) + `prepublishOnly` on every platform artifact — replaces the existence-only `test -f pnpm` gate with:
  1. A **relocation-sensitivity check**: run the binary without `dist/` staged and confirm the failure mentions a path derived from `process.execPath`, not a build-time constant. Catches any future regression of (2).
  2. A **smoke test**: stage a `dist → ../exe/dist` symlink (using `symlink-dir` so Windows junctions are handled transparently), exec `./pnpm -v`, assert the output is a SemVer 2 string.
  - Cross-platform targets (darwin/win32 artifacts on a Linux CI, or a libc mismatch) skip the exec with a log line and fall back to existence-only, so a musl artifact published from a glibc host still goes through.
  - Real `dist/` dirs (developer layout) are preserved; stale symlinks from aborted runs are replaced; created symlinks are cleaned up on exit.
- **`pnpm/artifacts/exe/test/setup.test.ts`** — new `pnpm -v` execution test gated on both the platform binary and the staged bundle being present, so ordinary `pn compile` test runs skip cleanly instead of failing on a missing `dist/`.

kairosci pushed a commit to kairosci/pnpm that referenced this pull request

Apr 22, 2026
## Summary

`@pnpm/exe@11.0.0-rc.4` aborts on every invocation with:

```
node::sea::(anonymous namespace)::SeaDeserializer::Read() at ../src/node_sea.cc:174
Assertion failed: (format_value) <= (static_cast<uint8_t>(ModuleFormat::kModule))
```

Two independent Node.js v25.7+ SEA regressions are responsible, both surfaced by the rc.4 bump of the embedded runtime from 25.6.1 to 25.9.0. This PR fixes both and adds a prepublish smoke test so a broken binary can't reach npm again.

## Root cause

**1. SEA blob format changed in Node.js v25.7.0** ([nodejs/node#61813](nodejs/node#61813) added ESM-entry-point support and inserted a new `ModuleFormat` byte into the blob header). SEA blobs carry no version marker, so a blob written by one Node.js version can only be deserialized by a matching one. In rc.4, the CI host Node.js (25.6.1, pre-change) wrote the blob and it was embedded in a 25.9.0 runtime (post-change) — the deserializer reads a misaligned byte as `format_value`, exceeds `kModule`, `CHECK_LE` fires, `SIGABRT`. `resolveBuilderBinary()` was preferring `process.execPath` whenever the running Node supported `--build-sea`, never checking that its version matched the embedded runtime.

**2. Node.js v25.7+ replaces the ambient `require` and `import()` inside a CJS SEA entry with embedder hooks** that only resolve built-in module names. The `pnpm.cjs` shim loaded `dist/pnpm.mjs` via `await import(pathToFileURL(...).href)`, which after the fix to (1) reached the CJS entry and then blew up with:

```
ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: file:///.../dist/pnpm.mjs
    at loadBuiltinModuleForEmbedder
    at importModuleDynamicallyForEmbedder
```

## Changes

- **`releasing/commands/src/pack-app/packApp.ts`** — `resolveBuilderBinary` now takes the resolved target runtime version and only reuses `process.execPath` when `process.version` exactly matches; otherwise it downloads a host-arch Node of the target version via the existing `ensureNodeRuntime` path. Added `PACK_APP_RUNTIME_TOO_OLD` for runtimes older than v25.5 (no `--build-sea`). Removed the now-unused `DEFAULT_BUILDER_SPEC` and the stale `fetch`/`nodeDownloadMirrors` args on the builder resolver. Help text / examples refreshed to drop `node@22` / `node@lts` references that would now be rejected.
- **`pnpm/pnpm.cjs`** — loads `dist/pnpm.mjs` through `Module.createRequire(process.execPath)` instead of `await import(fileURL)`. `createRequire` returns a regular CJS loader that bypasses the SEA embedder hooks, and the pnpm bundle has no top-level await so synchronous `require` of ESM (Node 22+) loads it cleanly. No build-time paths are baked in — `process.execPath` is evaluated at runtime, verified by relocation-testing the darwin-arm64 SEA under `/tmp/`.
- **`pnpm/artifacts/verify-binary.mjs`** (new) + `prepublishOnly` on every platform artifact — replaces the existence-only `test -f pnpm` gate with:
  1. A **relocation-sensitivity check**: run the binary without `dist/` staged and confirm the failure mentions a path derived from `process.execPath`, not a build-time constant. Catches any future regression of (2).
  2. A **smoke test**: stage a `dist → ../exe/dist` symlink (using `symlink-dir` so Windows junctions are handled transparently), exec `./pnpm -v`, assert the output is a SemVer 2 string.
  - Cross-platform targets (darwin/win32 artifacts on a Linux CI, or a libc mismatch) skip the exec with a log line and fall back to existence-only, so a musl artifact published from a glibc host still goes through.
  - Real `dist/` dirs (developer layout) are preserved; stale symlinks from aborted runs are replaced; created symlinks are cleaned up on exit.
- **`pnpm/artifacts/exe/test/setup.test.ts`** — new `pnpm -v` execution test gated on both the platform binary and the staged bundle being present, so ordinary `pn compile` test runs skip cleanly instead of failing on a missing `dist/`.