Skip to content

feat: enhanced SEA mode with walker integration and VFS#229

Merged
robertsLando merged 49 commits intomainfrom
feat/sea-enhanced-vfs
Apr 14, 2026
Merged

feat: enhanced SEA mode with walker integration and VFS#229
robertsLando merged 49 commits intomainfrom
feat/sea-enhanced-vfs

Conversation

@robertsLando
Copy link
Copy Markdown
Member

@robertsLando robertsLando commented Apr 7, 2026

Summary

Implements #204 — evolves the --sea flag from a simple single-file wrapper into a full packaging pipeline with walker integration and VFS support.

Enhanced SEA mode

  • Walker SEA mode — reuses the existing dependency walker with a seaMode flag that skips bytecode compilation and ESM→CJS transform while still discovering all dependencies via stepDetect. In seaMode, stepStrip is a true no-op (preserves record.body Buffer identity) and the walker guarantees that the in-memory body equals the shippable bytes — the asset generator trusts this invariant and skips a disk re-read.

  • SEA asset generator (lib/sea-assets.ts) — concatenates all discovered files into a single __pkg_archive__ blob and emits a __pkg_manifest__.json with directory listings, stats, symlinks, native-addon list, entryIsESM flag, and byte offsets into the archive for zero-copy extraction. Race-safe against mid-build source edits (uses the walker's in-memory body rather than re-reading from disk).

  • Runtime bootstrap — self-contained bootstrap bundled with @roberts_lando/vfs that provides transparent fs/fs/promises/require/import support via a lazy SEAProvider, native addon extraction, and process.pkg compatibility. Split into:

    • prelude/sea-bootstrap-core.js — shared setup (VFS mount, shared patches, worker interception, diagnostics, perf.start('module loading')). Exports { manifest, entrypoint, perf } without running the entry.
    • prelude/sea-bootstrap.js — single CJS wrapper used for every entry format. Dispatches based on manifest.entryIsESM:
      • CJS entryModule.runMain() (goes through the real CJS loader; require(esm) on Node 22.12+ transparently handles trivial ESM).
      • ESM entry → compiles import("file:///path/to/entry") via vm.Script with importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER. Dynamic import() inside that script is routed to the default ESM loader, so file URLs resolve and top-level await in the main module works on every Node ≥ 22 target. The ExperimentalWarning for USE_MAIN_CONTEXT_DEFAULT_LOADER is filtered out via a process.emitWarning shim so it doesn't leak into packaged-app stderr.
    • prelude/sea-vfs-setup.jsSEAProvider, archive loading, VFS mount, Windows path normalization. Shared by main thread and workers.
  • ESM entrypoints with top-level await (uniform path) — native ESM SEA main (mainFormat: "module", Node 25.7+) is not used. On Node 25.5+, the embedder importModuleDynamicallyForEmbedder callback only resolves builtin modules (nodejs/node#62726), so a native ESM main cannot dynamically import the user entrypoint at all. Instead, the CJS wrapper handles ESM entries through the vm.Script + USE_MAIN_CONTEXT_DEFAULT_LOADER path, which supports TLA on every Node ≥ 22 target — no version split, no build-time warning, no fallback tier.

    Entry Min target Node Bootstrap Dispatch
    CJS any (>= 22) sea-bootstrap.bundle.js Module.runMain()
    ESM any (>= 22) sea-bootstrap.bundle.js vm.Script with USE_MAIN_CONTEXT_DEFAULT_LOADER → dynamic import()

    perf.finalize() is moved into each dispatcher's finally block so module-loading timings reflect real entrypoint completion (including async / TLA apps) and still print when the entry throws. Errors on the ESM path set process.exitCode = 1 instead of calling process.exit() so the finally runs.

  • Worker thread support — monkey-patches Worker so /snapshot/... workers get the same SEAProvider + @roberts_lando/vfs mount as the main thread (same archive blob, same Buffer.subarray() extraction, same 164+ fs intercepts). Worker VFS setup is reused from sea-vfs-setup.js, no code duplication.

  • DEBUG_PKG diagnostics — the existing DEBUG_PKG / SIZE_LIMIT_PKG / FOLDER_LIMIT_PKG diagnostics now work in SEA mode too. Diagnostic code is only invoked when built with --debug, so release SEA builds can't be coerced into dumping the VFS tree via environment variables.

  • Enhanced orchestrator (seaEnhanced() in lib/sea.ts) — full pipeline: walker → refiner → asset generator → blob generation → bake → optional macOS signing.

    • Always uses node --experimental-sea-config to generate the prep blob. --build-sea (Node 25.5+) is intentionally not used: it produces a finished executable and bypasses the prep-blob + postject flow that pkg needs for multi-target injection.
    • Blob generator binary selection is done by host-vs-target major: if host major === minTargetMajor, pkg uses process.execPath (always executable regardless of target platform/arch — this is the only path that works for cross-platform same-major SEA builds, e.g. Linux x64 host producing a Windows x64 SEA). Otherwise it falls back to the downloaded target-platform binary (works cross-major when the host can exec it, e.g. same platform/arch or via QEMU/Rosetta).
    • Host vs target version checks are split: assertHostSeaNodeVersion() validates the pkg host, resolveMinTargetMajor() validates targets separately.
    • useSnapshot: true is rejected — incompatible with the VFS bootstrap (SEA snapshot mode runs the main script at build time inside a V8 startup snapshot context and expects a v8.startupSnapshot.setDeserializeMainFunction() entry; the pkg bootstrap doesn't do that and sea.getRawAsset('__pkg_archive__') does not exist at build time). useCodeCache is still forwarded — it only caches V8 bytecode for the bootstrap script and doesn't touch the runtime VFS path.
  • CLI routing--sea automatically uses enhanced mode when the input has package.json and all targets are Node >= 22; falls back to the existing simple mode otherwise. Public / no-dict / publicPackages WalkerParams are now shared between the SEA and traditional pipelines in lib/index.ts.

  • Backward compatible — existing simple --sea mode (single pre-bundled file) works unchanged.

Drop Node.js 20 support

  • Node.js 20 reaches EOL in April 2026.
  • Minimum engine bumped to >=22.0.0 in package.json.
  • Removed Node 20 from CI matrix (ci.yml, test.yml) and test:20 script.
  • Updated update-dep.yml workflow to use Node 22.
  • @roberts_lando/vfs (requires Node 22+) added as a regular dependency.
  • Updated README examples and target documentation.
  • @types/node bumped to ^22.0.0.

Bug fixes

  • Walker — skip stepDetect (Babel parser) for non-JS files in SEA mode. Previously produced thousands of spurious warnings on .json, .d.ts, .md, .node files.
  • Walker — file-before-directory resolution, built-in module shadowing, require.resolve() interception, trailing-slash specifiers, and main-pointing-to-directory. See platformatic/vfs#9 for upstream VFS fixes.
  • cpRecursive in bootstrap-shared.js — now uses lstatSync and recreates symlinks at the destination instead of dereferencing them. Fixes infinite recursion and unwanted content escape when addon packages contain symlinks (npm dedup, nested node_modules). Also reads each file once as a Buffer, hashes the buffer directly, and reuses it for writeFileSync — halves I/O on the hash-mismatch path and drops the unnecessary 'binary' encoding conversion.
  • SEAProvider._resolveSymlink — throws ELOOP instead of returning the last hop on broken symlink cycles, matching real-fs semantics.
  • insideSnapshot — refactored to a prefix table (behavior unchanged, faster check).

Documentation

A full technical comparison of traditional mode vs enhanced SEA mode lives in docs/ARCHITECTURE.md — covers build pipelines, binary formats, runtime bootstrap, VFS provider architecture, worker thread support, performance characteristics, code protection tradeoffs, and the Node.js ecosystem dependencies (including the node:vfs migration path). Updated for the single-bootstrap architecture.

Node.js ecosystem

  • Uses @roberts_lando/vfs as VFS polyfill (Node 22+), with built-in migration path to node:vfs when nodejs/node#61478 lands.
  • Blob generation uses node --experimental-sea-config. --build-sea and sea-config mainFormat: "module" are deliberately not used (see nodejs/node#62726).

Upstream dependencies

  • platformatic/vfs#9 — fixes 5 module resolution issues in @platformatic/vfs module hooks (built-in shadowing, resolution order, require.resolve, trailing slash, main-as-directory).

Test plan

  • test-00-sea — simple SEA backward compat (no regression)
  • test-85-sea-enhanced — multi-file CJS project with walker integration
  • test-86-sea-assets — non-JS asset access via fs.readFileSync
  • test-89-sea-fs-ops — VFS ops (readdir, stat, existsSync)
  • test-90-sea-worker-threads — workers spawned with /snapshot/... paths
  • test-91-sea-esm-entry — ESM entrypoint with relative .mjs imports
  • test-92-sea-tla — ESM entrypoint using top-level await (uniform vm.Script path)
  • Lint and build pass

🤖 Generated with Claude Code

robertsLando and others added 6 commits April 7, 2026 11:00
Evolves the --sea flag from a simple single-file wrapper into a full
packaging pipeline that reuses the walker for dependency discovery,
maps files as SEA assets, and provides a runtime VFS bootstrap using
@platformatic/vfs for transparent fs/require/import support.

- Add seaMode to walker: skips bytecode compilation and ESM-to-CJS
  transform, but still discovers all dependencies via stepDetect
- Add sea-assets.ts: generates SEA asset map and manifest JSON from
  walker output (directories, stats, symlinks, native addons)
- Add sea-bootstrap.js: runtime bootstrap with lazy SEAProvider,
  native addon extraction, and process.pkg compatibility
- Add seaEnhanced() to sea.ts: walker → refiner → asset gen → blob →
  bake pipeline with Node 25.5+ --build-sea detection
- Route --sea to enhanced mode when input has package.json and target
  Node >= 22; falls back to simple mode otherwise
- Add 4 test suites: multi-file project, asset access, ESM (skipped
  until node:vfs lands), and VFS fs operations

Closes #204

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mpat

@platformatic/vfs requires Node >= 22 but CI runs on Node 20. Since the
package is only needed at build time (esbuild bundles it into the
sea-bootstrap) and the bundle step is skipped on Node < 22, making it
optional prevents yarn install failures on older Node versions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node.js 20 reached EOL in April 2026. This simplifies the project by
removing the Node 20 CI matrix, test scripts, and engine check. It also
allows @platformatic/vfs to be a regular dependency (requires Node 22+)
and removes the conditional build script for sea-bootstrap.

- Update engines to >=22.0.0 in package.json
- Remove node20 from CI matrix (ci.yml, test.yml)
- Update update-dep.yml to use Node 22
- Move @platformatic/vfs from optionalDependencies to dependencies
- Simplify build:sea-bootstrap script (no Node version check)
- Update README examples and SEA requirements
- Update @types/node to ^22.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simple SEA mode was introduced in Node 20, not Node 22. The guard
should reflect when the Node.js feature was added, not the project
minimum.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shared assertSeaNodeVersion() is used by both simple and enhanced
SEA modes. Simple SEA was introduced in Node 20 — the check should
reflect that. Enhanced mode's Node 22 requirement is enforced
separately by the routing logic in index.ts.

Also fixes README --sea description to explain both modes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR upgrades pkg’s experimental --sea flag from a single-file wrapper into an “enhanced SEA” pipeline that reuses the existing walker/refiner and embeds a VFS-backed runtime bootstrap, while also raising the project’s minimum supported Node.js version to 22.

Changes:

  • Add enhanced SEA orchestration (seaEnhanced) that walks dependencies, generates SEA assets + a manifest, builds a SEA blob, and bakes it into target Node executables.
  • Introduce SEA VFS runtime bootstrap (prelude/sea-bootstrap.js) bundled via esbuild, and add @platformatic/vfs as a dependency.
  • Drop Node 20 from engines/CI/scripts and add new SEA-focused tests.

Reviewed changes

Copilot reviewed 32 out of 34 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
yarn.lock Adds lock entry for @platformatic/vfs.
package.json Adds @platformatic/vfs, bumps @types/node to 22, builds SEA bootstrap bundle, removes Node 20 test script, bumps engines to Node >= 22.
lib/types.ts Centralizes Marker/WalkerParams types and adds SeaEnhancedOptions.
lib/walker.ts Adds seaMode behavior to skip bytecode/ESM transforms and keep dependency detection.
lib/sea-assets.ts New: converts walker/refiner output into SEA asset map + manifest JSON.
lib/sea.ts Adds seaEnhanced(), shared tempdir helper, execFile usage, and shared macOS signing helper.
lib/index.ts Routes --sea to enhanced mode when appropriate; factors buildMarker(); reuses macOS signing helper.
prelude/sea-bootstrap.js New: SEA runtime bootstrap that mounts a VFS from SEA assets and supports native addon extraction.
eslint.config.js Ignores generated prelude/sea-bootstrap.bundle.js.
.gitignore Ignores generated prelude/sea-bootstrap.bundle.js.
README.md Updates SEA docs and examples to reflect enhanced mode and Node >= 22 focus.
.github/workflows/ci.yml Removes Node 20 from matrix; updates lint run condition to Node 22.
.github/workflows/test.yml Removes Node 20 from test matrix.
.github/workflows/update-dep.yml Updates workflow to use Node 22.
test/utils.js Adds assertSeaOutput() helper for SEA tests.
test/test-85-sea-enhanced/* New enhanced SEA “multi-file project” test fixture.
test/test-86-sea-assets/* New enhanced SEA “fs.readFileSync assets” test fixture.
test/test-87-sea-esm/* New enhanced SEA ESM test fixture (currently skipped).
test/test-89-sea-fs-ops/* New enhanced SEA filesystem ops test fixture.
plans/SEA_VFS_IMPLEMENTATION_PLAN.md Adds design/analysis and implementation plan document.

robertsLando and others added 4 commits April 7, 2026 11:43
Move dlopen patching, child_process patching, and process.pkg setup
into a shared module used by both the traditional and SEA bootstraps.
This eliminates duplication and ensures both modes handle native
addons, subprocess spawning, and process.pkg identically.

- Replace copyFolderRecursiveSync with fs.cpSync (Node >= 22)
- Add REQUIRE_SHARED parameter to packer wrapper for traditional mode
- SEA bootstrap consumes shared module via esbuild bundling
- Remove dead code (ARGV0, homedir import, copyFolderRecursiveSync)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Handle non-numeric nodeRange (e.g. "latest") in enhanced SEA gating
- Bump assertSeaNodeVersion to require Node >= 22 matching engines
- Guard stepStrip in walker to only strip JS/ESM files in SEA mode,
  preventing binary corruption of .node addons
- Replace blanket eslint-disable in sea-bootstrap.js with targeted
  no-unused-vars override in eslint config
- Wire symlinks through to SEA manifest and bootstrap provider
- Document --build-sea Node 25 gating assumption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of silently falling back to simple SEA mode when the input is a
package.json/config but targets are below Node 22, throw a clear error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
robertsLando and others added 3 commits April 7, 2026 13:55
Detailed comparison of traditional pkg mode vs enhanced SEA mode
covering build pipelines, binary formats, runtime bootstraps, VFS
provider architecture, shared code, performance, and code protection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SEA manifest uses POSIX keys but VFS passes platform-native paths on
Windows. Normalize all paths to POSIX in the SEAProvider before manifest
lookups, SEA asset lookups, and MemoryProvider storage. Remove the now-
redundant symlink normalization block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explains that traditional mode with --no-bytecode produces a similar
code protection profile to enhanced SEA (plaintext source), while
still retaining compression and custom VFS format advantages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 38 changed files in this pull request and generated 3 comments.

- Replace fs.cpSync with recursive copy using patched fs primitives
  so native addon extraction works through VFS in SEA mode
- Use toPosixKey instead of snapshotify for symlink manifest keys
  to match the key format used by directories/stats/assets
- Make assertSeaOutput log explicit skip on unsupported platforms
  instead of silently passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 38 changed files in this pull request and generated 3 comments.

robertsLando and others added 5 commits April 7, 2026 14:44
Move VFS tree dump and fs call tracing from prelude/diagnostic.js into
bootstrap-shared.js as installDiagnostic(). Both bootstraps now share
the same diagnostic implementation.

Security: diagnostics are only available when built with --debug / -d.
In SEA mode this is controlled via manifest.debug flag set at build
time; without it, installDiagnostic is never called.

- Delete prelude/diagnostic.js (replaced by shared installDiagnostic)
- Packer injects small DICT-dump snippet + shared call when --debug
- SEA bootstrap gates on manifest.debug before calling installDiagnostic
- Fix assertSeaNodeVersion: restore check to Node >= 20 (simple SEA)
- Fix withSeaTmpDir: restore tmpDir cleanup (was commented out)
- Update architecture docs with diagnostic usage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump assertSeaNodeVersion minimum from Node 20 to 22 (consistent with engines)
- Extract generateSeaBlob() helper with --build-sea fallback for Node 25.x
- Accept separate defaultEntrypoint in setupProcessPkg for process.pkg compat
- Update ARCHITECTURE.md Simple SEA min Node from 20 to 22

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Worker thread support:
- Extract VFS setup into shared sea-vfs-setup.js (used by both main and
  worker threads, no duplication)
- Bundle sea-worker-entry.js separately via esbuild — workers get the
  same @platformatic/vfs module hooks as the main thread
- Monkey-patch Worker constructor to intercept /snapshot/ paths and
  inject the bundled VFS bootstrap via eval mode
- Add test-90-sea-worker-threads test

Walker fix:
- Skip stepDetect (Babel parser) for non-JS files in SEA mode — only
  run on .js/.cjs/.mjs files, matching the existing stepStrip guard.
  Eliminates thousands of spurious warnings on .json, .d.ts, .md, .node
  files in real-world projects.

Build:
- Extract build:sea-bootstrap into scripts/build-sea-bootstrap.js for
  two-step bundling (worker entry → string, then main bootstrap)

TODO: Remove node_modules/@platformatic/vfs patches once
platformatic/vfs#9 is merged and released.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The VFS mount point must always be POSIX '/snapshot' because
@platformatic/vfs internally uses '/' as the path separator.
The Windows prototype patches convert C:\snapshot\... and V:\snapshot\...
to /snapshot/... before they reach the VFS.

This was broken during the sea-vfs-setup.js extraction where
SNAPSHOT_PREFIX was incorrectly set to 'C:\snapshot' on Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@robertsLando robertsLando force-pushed the feat/sea-enhanced-vfs branch from fc574b6 to 394deb1 Compare April 8, 2026 15:01
@Niek
Copy link
Copy Markdown

Niek commented Apr 8, 2026

So, comparing SEA with bundle vs Standard PKG (with bundle) = 44% slower startup, 30% larger binary, and you lose source code protection. No offence, but that sounds like a very hard sell. Additionally, if people prefer that route they would rather go with something like fossilize I think, which is a repo dedicated to creating Node SEA executables.

@robertsLando
Copy link
Copy Markdown
Member Author

robertsLando commented Apr 8, 2026

Thanks for the feedback! There are some important differences between this PR and fossilize that are worth highlighting:

1. Virtual Filesystem (VFS) — transparent fs/require/import support

Fossilize bundles everything into a single file with esbuild and requires you to use sea.getAsset() to access embedded files. There's no filesystem patching — code that does fs.readFileSync(path.join(__dirname, 'config.json')) or dynamically requires modules simply won't work without refactoring your application.

Our enhanced SEA mode uses @platformatic/vfs (and will migrate to node:vfs when nodejs/node#61478 lands) to provide a full VFS layer that transparently patches fs, require, and import. Existing code works without modification — just like traditional pkg mode.

2. Native addon support

Fossilize cannot handle .node native addons at all (esbuild limitation). Our implementation detects native addons via the walker, embeds them as SEA assets, and extracts them at runtime — same as traditional pkg.

3. Worker thread support

We monkey-patch the Worker constructor to intercept /snapshot/... paths and inject a VFS-aware bootstrap, so worker threads work transparently. Fossilize has no worker thread support.

4. Dependency walker integration

Fossilize relies entirely on esbuild's bundling, which means apps must be fully bundleable. Our approach reuses pkg's battle-tested dependency walker (stepDetect) to discover all dependencies — including edge cases handled by our dictionary of package-specific configs. It then generates a SEA asset map with directory listings, stats, and symlinks.

5. Stock Node.js binaries — no more patched builds

This is actually a shared advantage with fossilize, and one of the main motivations for this PR. Traditional pkg requires patched Node.js binaries built and released via the yao-pkg/pkg-fetch project. These patches are hard to create, time-consuming to build for every Node.js release, and can lag behind upstream — users often have to wait for new builds before they can use a new Node.js version. With enhanced SEA mode, users can use official Node.js binaries directly from nodejs.org — no patches, no waiting for pkg-fetch releases, no maintenance burden.

6. On the performance comparison

You're right about the numbers — SEA with bundle vs Standard PKG with bundle is 261ms vs 381ms startup and 111 MB vs 145 MB binary. That's a meaningful but not dramatic difference, and in return you gain stock Node.js binaries (no pkg-fetch dependency), ESM support, and a much simpler maintenance story.

The tradeoff for losing bytecode/source protection is real, but for many use cases (CLI tools, internal services, IoT deployments) it's not a concern — and the elimination of the patched-binary dependency is a significant operational win.

…ck, CRLF normalization

- Restore unconditional native addon copy in patchDlopen with per-file
  SHA-256 checksums, matching original vercel/pkg behavior (PRs vercel#1492, vercel#1611).
  The existsSync guard on destFolder was a regression — OS cleanup can
  delete files inside the cache while leaving the directory intact.
- Narrow generateSeaBlob fallback to only catch unsupported --build-sea
  flag errors (exit code 9 / "bad option"), rethrow real failures.
- Normalize CRLF in assertSeaOutput and test-00-sea for Windows compat.
  Uncomment previously disabled Windows SEA assertion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 54 out of 58 changed files in this pull request and generated 5 comments.

robertsLando and others added 3 commits April 13, 2026 14:42
- bootstrap-shared.js: standalone addon branch now uses writeFileSync
  with the already-read moduleContent (reuses hash too), so extraction
  works through VFS in SEA mode instead of calling copyFileSync.
- bootstrap-shared.js: extract cpRecursive helper out of patchDlopen
  for readability.
- docs/ARCHITECTURE.md: drop stale diagnostic.js reference and clarify
  that installDiagnostic ships in the bundle but is only invoked when
  the binary is built with --debug.
- .prettierignore: ignore generated prelude/sea-bootstrap.bundle.js so
  post-build lint stays clean.
Split the SEA bootstrap into a shared core plus two thin wrappers so ESM
entrypoints work on any supported Node.js target:

- `sea-bootstrap-core.js` — VFS mount, shared patches, worker interception,
  diagnostics. Exports `{ manifest, entrypoint }` with no entry execution.
- `sea-bootstrap.js` (CJS) — `Module.runMain()` for CJS entries; dynamic
  `import(pathToFileURL(entry))` fallback for ESM entries on Node < 25.7.
- `sea-bootstrap-esm.js` (ESM) — static `import core` + top-level
  `await import(entry)` for Node >= 25.7 with `mainFormat: "module"`.

`lib/sea.ts` picks the right bundle and sea-config based on entry format
and the smallest target Node major. When the ESM-on-old-Node fallback is
used, a build-time `log.warn` explains the limitations (one microtask
delay, no sync require of ESM-with-TLA deps) and points at Node 25.7+ as
the fix. No runtime warning is emitted.

Also refresh the symlink + read-once-Buffer improvements to `cpRecursive`
in bootstrap-shared.js (prevents infinite recursion and unwanted content
escape on addon packages with symlinks; halves I/O on the hash-mismatch
path).

Add tests:
- `test-91-sea-esm-entry` — ESM entry with relative `.mjs` imports
- `test-92-sea-tla`       — ESM entry using top-level await

Update `docs/ARCHITECTURE.md` with the dual-wrapper layout, the
bootstrap-selection table, the TLA section, and the 3-step esbuild flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clarify the enhanced SEA bullet list: ESM entries work on any supported
Node target (native on Node >= 25.7 via mainFormat:"module", dynamic
import() fallback on Node 22-25.6 with a build-time warning). Also note
that DEBUG_PKG diagnostics are available but only when built with --debug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 65 out of 69 changed files in this pull request and generated 1 comment.

robertsLando and others added 3 commits April 14, 2026 11:26
…ator selection, cross-plat

- Always use CJS bootstrap: Node 25.5+ embedder dynamic-import callback
  only resolves builtins (nodejs/node#62726), so native ESM SEA main
  cannot import the user entrypoint. ESM entries are dispatched through
  a vm.Script compiled with USE_MAIN_CONTEXT_DEFAULT_LOADER, which routes
  dynamic import() to the default ESM loader and supports top-level await.
  Drop the now-unused sea-bootstrap-esm.js and its bundle step.
- Pick SEA blob generator binary by host/target major: use process.execPath
  when host major matches minTargetMajor (works for cross-platform builds),
  else the downloaded target binary (works cross-major when host can exec
  it). Restores cross-platform same-major SEA builds.
- Split host/target node version checks: assertHostSeaNodeVersion covers
  the pkg host, resolveMinTargetMajor validates targets separately.
- Reject useSnapshot:true in enhanced SEA mode — incompatible with the VFS
  bootstrap (snapshot build has no __pkg_archive__ asset).
- Trust in-memory body in sea-assets when present: walker invariant
  guarantees body equals shippable bytes in seaMode (ESM→CJS and
  type:module rewrites are gated on !seaMode). Removes bodyModified flag
  and a redundant disk re-read, and makes the build race-safe against
  mid-build source edits.
- stepStrip leaves record.body untouched on no-op so Buffer identity is
  preserved for the sea-assets reuse path.
- perf.finalize() moved into each bootstrap dispatcher's finally block so
  module-loading timings reflect real entrypoint completion (including
  async / top-level-await apps) and still print on thrown entrypoints.
  Error paths set process.exitCode instead of process.exit() so the
  finally runs.
- _resolveSymlink now throws ELOOP instead of returning the last hop,
  matching fs semantics for broken symlink cycles.
- insideSnapshot refactored to a prefix table; behavior unchanged.
- Share public / no-dict / publicPackages WalkerParams between the SEA
  and traditional pipelines in lib/index.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-write loop

- bootstrap-shared.js: cpRecursive() falls back to recursive copy when
  symlinkSync throws EPERM/EACCES on Windows (no admin / dev mode), so
  native addon extraction no longer aborts silently.
- sea-vfs-setup.js: validate manifest offsets before Buffer.subarray() —
  corrupt or truncated manifests now throw instead of returning silently
  truncated bytes. Also hoist MAX_SYMLINK_DEPTH to a named const and stop
  reaching the module-scope provider from perf.finalize().
- sea-assets.ts: writeAll() helper loops on FileHandle.write bytesWritten
  for both buffer and stream paths, keeping manifest offsets byte-exact
  even under filesystem backpressure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ESM/TLA now runs through one CJS wrapper using vm.Script +
USE_MAIN_CONTEXT_DEFAULT_LOADER on every Node >= 22 target. Native
ESM SEA main (mainFormat:"module") is not used — nodejs/node#62726
blocks embedder dynamic-import of the user entry. Also documents
useSnapshot incompatibility, --experimental-sea-config-only blob
generation, and the host/target blob generator picker. File
reference + ecosystem tables refreshed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 68 changed files in this pull request and generated 1 comment.

bake() previously reloaded the prep blob from disk per target inside
Promise.all, causing N redundant reads and N peak buffer copies on
multi-target builds. Read once before the loop and pass the shared
Buffer into bake().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 68 changed files in this pull request and generated 11 comments.

…k, test cleanup

- lib/sea.ts: add assertSingleTargetMajor() and call from both sea() and
  seaEnhanced(). SEA prep blobs are Node-major specific, so mixing majors
  in one run (e.g. -t node22-linux-x64,node24-linux-x64) silently produced
  broken executables. Reject up front instead.
- prelude/sea-bootstrap.js: restore the original process.emitWarning as
  soon as the SEA loader warning is suppressed, so user code does not
  observe a permanently wrapped emitWarning.
- test/utils.js: filesAfter() gains { tolerateWindowsEbusy } option that
  only swallows EBUSY on win32 during cleanup.
- test/test-85..92: drop copy-pasted try/catch noop around filesAfter and
  use the new option. test-91/92 also switch from inlined spawn+CRLF
  logic to the shared assertSeaOutput helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@robertsLando robertsLando merged commit daf76c5 into main Apr 14, 2026
24 checks passed
@robertsLando robertsLando deleted the feat/sea-enhanced-vfs branch April 14, 2026 13:14
robertsLando added a commit that referenced this pull request Apr 15, 2026
package-lock.json was created by accident in #229 alongside the yarn
lockfile. pkg's canonical package manager is yarn (see yarn.lock); the
accidental npm lockfile drifts silently and confuses contributors.

- Remove package-lock.json and gitignore it at the repo root so it
  cannot reappear.
- Rewrite the dev-command references in CLAUDE.md, .claude/rules/*,
  .github/copilot-instructions.md and docs-site/development.md from
  "npm run <x>" to "yarn <x>", and spell out the split: pkg uses yarn,
  docs-site is the only place npm is used.

User-facing install instructions (README, guide/getting-started, guide/
migration, guide/api) are unchanged — end users still "npm install -g
@yao-pkg/pkg" and the CI recipe example still shows npm, since that
reflects how consumers package their own projects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants