From 4991e64624f75b407145eb2ed522eaa73b193740 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 31 Aug 2026 11:14:34 +0200 Subject: [PATCH 1/2] sea: mount bundled assets as a virtual file system 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 --- doc/api/single-executable-applications.md | 85 +++++ doc/api/vfs.md | 29 ++ lib/internal/main/embedding.js | 41 +- lib/internal/vfs/providers/sea.js | 349 ++++++++++++++++++ lib/internal/vfs/sea.js | 58 +++ src/node_sea.cc | 64 ++++ src/node_sea.h | 1 + test/fixtures/sea/vfs/calculator.js | 9 + test/fixtures/sea/vfs/config.json | 1 + test/fixtures/sea/vfs/greeting.txt | 1 + test/fixtures/sea/vfs/math.js | 4 + test/fixtures/sea/vfs/sea-config.json | 15 + test/fixtures/sea/vfs/sea.js | 106 ++++++ .../sea/vfs/test-exports-pkg-entry.js | 2 + .../sea/vfs/test-exports-pkg-package.json | 5 + test/fixtures/sea/vfs/test-pkg-index.js | 5 + test/fixtures/sea/vfs/test-pkg-package.json | 5 + ...test-build-sea-vfs-incompatible-options.js | 99 +++++ .../test-single-executable-application-vfs.js | 38 ++ 19 files changed, 915 insertions(+), 2 deletions(-) create mode 100644 lib/internal/vfs/providers/sea.js create mode 100644 lib/internal/vfs/sea.js create mode 100644 test/fixtures/sea/vfs/calculator.js create mode 100644 test/fixtures/sea/vfs/config.json create mode 100644 test/fixtures/sea/vfs/greeting.txt create mode 100644 test/fixtures/sea/vfs/math.js create mode 100644 test/fixtures/sea/vfs/sea-config.json create mode 100644 test/fixtures/sea/vfs/sea.js create mode 100644 test/fixtures/sea/vfs/test-exports-pkg-entry.js create mode 100644 test/fixtures/sea/vfs/test-exports-pkg-package.json create mode 100644 test/fixtures/sea/vfs/test-pkg-index.js create mode 100644 test/fixtures/sea/vfs/test-pkg-package.json create mode 100644 test/sea/test-build-sea-vfs-incompatible-options.js create mode 100644 test/sea/test-single-executable-application-vfs.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index be5667cbd499..0ced6d9744b4 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -116,6 +116,7 @@ The configuration currently reads the following top-level fields: "disableExperimentalSEAWarning": true, // Default: false "useSnapshot": false, // Default: false "useCodeCache": true, // Default: false + "useVfs": true, // Default: false "execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional "execArgvExtension": "env", // Default: "env", options: "none", "env", "cli" "assets": { // Optional @@ -175,6 +176,86 @@ const raw = getRawAsset('a.jpg'); See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][], [`sea.getRawAsset()`][] and [`sea.getAssetKeys()`][] APIs for more information. +### Virtual file system (VFS) for assets + + + +> Stability: 1 - Experimental + +In addition to using the `node:sea` API to access individual assets, the +bundled assets can be exposed as a read-only [virtual file system][] and +accessed through standard `node:fs` APIs. To enable this, set +`"useVfs": true` in the SEA configuration. + +A virtual file system never shadows the real file system: it is mounted at a +reserved mount point that cannot exist on the real file system, and the mount +point is chosen at runtime rather than being a fixed path. When `useVfs` is +enabled, the injected main script itself is placed at the root of the mount +and executed from there, so `__filename` and `__dirname` point inside the +virtual file system instead of reflecting [`process.execPath`][]. Bundled +code therefore reaches the assets through `__dirname`-relative paths and +relative [`require()`][] calls, without having to know the mount point: + +```cjs +const fs = require('node:fs'); +const path = require('node:path'); + +// __dirname is the root of the virtual file system holding the assets. +const rawConfig = fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8'); +const data = fs.readFileSync(path.join(__dirname, 'data/file.txt')); + +// Directory operations work too. +const files = fs.readdirSync(path.join(__dirname, 'assets')); + +// Check if a bundled file exists. +if (fs.existsSync(path.join(__dirname, 'optional.json'))) { + // ... +} +``` + +The VFS supports the `node:fs` operations for reading files and directories. +Since the SEA VFS is read-only, write operations fail with `EROFS`. See the +[VFS documentation][] for the full list of supported operations. + +#### Loading modules from the VFS in a SEA + +When `useVfs` is enabled, the main script is executed from inside the +virtual file system, and `require()` uses the [module loader +integration][] of the VFS to load modules from the bundled assets. This +supports relative requires (e.g. `require('./helper.js')`) as well as +`node_modules` package lookups, which are confined to the mount: + +```cjs +// Require bundled modules using relative paths. +const myModule = require('./lib/mymodule.js'); + +// Packages bundled under the node_modules asset prefix also resolve. +const dep = require('some-package'); +``` + +#### ESM limitations + +The `useVfs` option does not currently support ESM entry points. Using +`"useVfs": true` together with `"mainFormat": "module"` is not supported. +The main script must use CommonJS (`require()`) when VFS is enabled. + +#### Snapshot and code caching limitations + +`"useVfs": true` cannot be used together with `"useSnapshot": true` or +`"useCodeCache": true`. The code cache limitation is due to incomplete +implementation, not a technical impossibility. Consider bundling the +application if startup performance matters and do not rely on module loading +from the VFS in that case. + +#### Native addon limitations + +Native addons (`.node` files) cannot be loaded directly from the VFS because +`process.dlopen()` requires files on the real file system. To use native +addons in a SEA with VFS, write the asset to a temporary file first. See +[Using native addons in the injected main script][] for an example. + ### Startup snapshot support The `useSnapshot` field can be used to enable startup snapshot support. In this @@ -648,6 +729,8 @@ to help us document them. [Generating single executable preparation blobs]: #1-generating-single-executable-preparation-blobs [Mach-O]: https://en.wikipedia.org/wiki/Mach-O [PE]: https://en.wikipedia.org/wiki/Portable_Executable +[Using native addons in the injected main script]: #using-native-addons-in-the-injected-main-script +[VFS documentation]: vfs.md [Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ [`process.execPath`]: process.md#processexecpath [`require()`]: modules.md#requireid @@ -660,8 +743,10 @@ to help us document them. [`v8.startupSnapshot` API]: v8.md#startup-snapshot-api [documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot [fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses +[module loader integration]: vfs.md#module-loader-integration [postject]: https://github.com/nodejs/postject [postject-linux-arm64-issue]: https://github.com/nodejs/postject/issues/105 [signtool]: https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool [single executable applications]: https://github.com/nodejs/single-executable [supported by Node.js]: https://github.com/nodejs/node/blob/main/BUILDING.md#platform-list +[virtual file system]: vfs.md diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 2070efdac238..1b1efa0afa22 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -417,6 +417,34 @@ system, the callers are responsible for avoiding removal or invalidation of modules in the virtual file system while they are being loaded. +## Use with Single Executable Applications + +When running as a [Single Executable Application][] built with +`"useVfs": true` in the SEA configuration, the bundled assets are +automatically mounted as a read-only virtual file system and the injected +main script is executed from the root of the mount. No additional setup is +required. Since the mount point is reserved and chosen at runtime, bundled +code accesses the assets through `__dirname`-relative paths and relative +`require()` calls rather than through a fixed path: + +```cjs +// In the SEA main script, __dirname is the root of the mounted assets. +const fs = require('node:fs'); +const path = require('node:path'); + +const config = JSON.parse( + fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8')); +const template = fs.readFileSync( + path.join(__dirname, 'templates/index.html'), 'utf8'); +``` + +`"useVfs"` cannot be used together with `"useSnapshot"`, `"useCodeCache"`, or +`"mainFormat": "module"`. The SEA configuration parser will error if any of +these combinations are detected. + +See the [Single Executable Application][] documentation for more information +on creating SEA builds with assets. + ## Class: `VirtualProvider`