Skip to content

sea: mount bundled assets as a virtual file system - #65675

Open
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:vfs-sea-integration
Open

sea: mount bundled assets as a virtual file system#65675
mcollina wants to merge 2 commits into
nodejs:mainfrom
mcollina:vfs-sea-integration

Conversation

@mcollina

@mcollina mcollina commented Aug 31, 2026

Copy link
Copy Markdown
Member

This adds the SEA integration for the virtual file system landed in #63653: setting "useVfs": true in the SEA configuration mounts the bundled assets as a read-only VFS and runs the injected main script from inside the mount.

Since a VFS never shadows the real file system and its mount point is reserved and chosen at runtime, there is no fixed /sea path. Instead, the main script is placed at the root of the mount and executed from there, so:

  • __filename and __dirname point inside the virtual file system;
  • assets are reached through __dirname-relative paths with regular node:fs APIs;
  • relative require() calls and node_modules lookups resolve against the bundled assets, confined to the mount by the module loader integration.

Implementation notes:

  • The main script is not duplicated into the assets at build time; its source already lives in the SEA blob and is injected into the provider at runtime.
  • SEAProvider is read-only and lazy: asset content stays in the executable's SEA blob and is copied into JS memory only when a file is opened, with sizes cached for stat.
  • The implicit SEA mount does not emit the VirtualFileSystem experimental warning; the SEA warning already covers it.
  • ESM entry points ("mainFormat": "module") are supported: the ESM main is loaded from inside the mount through the ESM loader, so import.meta.url/filename/dirname reflect the mount and static imports, dynamic import(), and bare specifier lookups resolve against the bundled assets.
  • "useVfs" is rejected together with "useSnapshot" and "useCodeCache".

This PR was prepared with the help of AI. I've reviewed all changes myself.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/single-executable

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Aug 31, 2026
@mcollina
mcollina force-pushed the vfs-sea-integration branch 2 times, most recently from 7e3ed4b to 148a0d7 Compare August 31, 2026 09:13
Add a "useVfs" boolean to the SEA configuration. When enabled, the
bundled assets are mounted as a read-only virtual file system before
the main script runs, and the main script is placed at the mount point
root and executed from there via wrapModuleLoad. __filename, __dirname,
relative require() calls, and node_modules lookups then all resolve
against the bundled assets, confined to the mount. Since a VFS never
shadows the real file system, bundled code reaches the assets through
__dirname-relative paths instead of a fixed mount location.

The new SEAProvider derives the directory tree from the asset keys and
keeps asset content in the executable's SEA blob, copying it into JS
memory only when a file is opened. The main script is not duplicated
into the assets at build time; its source already lives in the blob and
is injected into the provider at runtime. The implicit SEA mount does
not emit the VirtualFileSystem experimental warning, which is already
covered by the SEA warning.

"useVfs" is rejected together with "useSnapshot", "useCodeCache",
and "mainFormat": "module"; ESM entry points are left as future
work.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina force-pushed the vfs-sea-integration branch from 148a0d7 to 4991e64 Compare August 31, 2026 09:14
Allow "mainFormat": "module" together with "useVfs": true. The
ESM main script is placed at the mount point root like the CommonJS
one, and loaded through the ESM loader via runEntryPointWithESMLoader,
so import.meta.url, import.meta.filename, and import.meta.dirname
reflect the location of the main module inside the virtual file
system, and static imports, dynamic import(), and bare specifier
lookups all resolve against the bundled assets.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina
mcollina requested review from Qard and joyeecheung and removed request for Qard August 31, 2026 10:46
pipobscure added a commit to pipobscure/bundles that referenced this pull request Sep 1, 2026
The tool was one directory of JavaScript that doubled as a library by
accident. This makes the library the point: `src/` is TypeScript compiled
to ESM in `dist/`, every capability has a declared entry point, and the
CLI is a thin wrapper over the same functions an embedder gets.

Four things are now separately importable, which is the shape the rest of
the design needs:

  .           create / sign / verify / inspect / run, from code (api.ts)
  ./record    the -r preload that writes down what a run reads
  ./register  the -r preload that mounts only what is signed
  ./sea       build and boot a self-validating executable

plus ./provider, ./recorder, ./cli, ./manifest, ./archive, ./files,
./skill, ./sigstore and ./oidc for the layers underneath. The package root
deliberately does not re-export the two providers: importing either needs
node:vfs, and creating or verifying an archive does not, so a plain
`import '@pipobscure/bundle'` must not drag `--experimental-vfs` in.
There is a test that holds that line.

The sources are erasable-syntax-only, so `node src/main.ts` runs them
directly under node's type stripping. That is what lets the tests import
the sources rather than the build, and a preload be `-r ./src/register.ts`.
@types/node carries neither the node:zlib ZIP API nor node:vfs's provider
registry, so `src/types/` declares them; the shapes were checked against
the running runtime rather than transcribed from documentation.

sea.js is replaced by src/sea.ts. The old bootstrap was a copy of
manifest.js inlined into a CommonJS file, which had drifted: its signature
marker regex was still the two-field form, so it read every
sigstore-signed container as unsigned. The replacement mounts this package
out of the SEA blob with node:vfs and requires the real library from
there — the userland form of nodejs/node#65675, with the difference that
the mount running the application is the signed archive appended to the
file. Nothing is duplicated, and the verifier a container runs is the one
the test suite tests. `bundle sea` builds one.

Three bugs the type checker and the new tests turned up, all real:

  * --identity and BUNDLE_IDENTITY were only consulted on the sigstore
    path, so an archive signed against an ordinary CA — which carries no
    identity claim at all — satisfied a policy demanding one. A machine
    configured to run only releases from a workflow would have mounted
    anything key-signed. It now reports valid-untrusted.

  * Arguments beginning with `--` never reached a mounted application;
    node claimed them as its own flags. mountArgv() now ends with `--`.

  * Verifying from a Buffer crashed, and sigstore signing was written
    against @sigstore/bundle v2 while v3 is installed. Both fixed, and the
    Buffer path now has a test.

131 tests over the format, both providers, the API, the CLI, the SEA and
the published package's own shape, replacing three JavaScript suites.
@mcollina
mcollina marked this pull request as ready for review September 1, 2026 15:04
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.40476% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.07%. Comparing base (4d9cb71) to head (6ee9698).
⚠️ Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
src/node_sea.cc 93.47% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65675      +/-   ##
==========================================
+ Coverage   90.05%   90.07%   +0.02%     
==========================================
  Files         754      756       +2     
  Lines      256308   256897     +589     
  Branches    48464    48563      +99     
==========================================
+ Hits       230817   231405     +588     
- Misses      16593    16615      +22     
+ Partials     8898     8877      -21     
Files with missing lines Coverage Δ
lib/internal/main/embedding.js 91.89% <100.00%> (+2.92%) ⬆️
lib/internal/vfs/providers/sea.js 100.00% <100.00%> (ø)
lib/internal/vfs/sea.js 100.00% <100.00%> (ø)
src/node_sea.h 100.00% <ø> (ø)
src/node_sea.cc 89.77% <93.47%> (+0.27%) ⬆️

... and 46 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants