mirror of
https://github.com/nodejs/node.git
synced 2026-10-10 18:59:49 -04:00
sea: mount bundled assets as a virtual file system
Support "useVfs": true in the SEA configuration: mount the bundled assets as a read-only VFS and run the CommonJS or ESM main script from inside the mount. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: https://github.com/nodejs/node/pull/65675 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
1 parent
4cd3d98510
commit
4e207b1c34
28 files changed
+1059
-2
No files matched your search
@@ -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,105 @@ 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
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.0 - Early development
|
||||
|
||||
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 entry points
|
||||
|
||||
`"useVfs": true` also supports `"mainFormat": "module"`. The ESM main
|
||||
script is loaded from inside the mount through the ESM loader, so
|
||||
`import.meta.url`, `import.meta.filename`, and `import.meta.dirname`
|
||||
reflect the location of the main script in the virtual file system, and
|
||||
static and dynamic imports resolve against the bundled assets:
|
||||
|
||||
```mjs
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// import.meta.dirname is the root of the virtual file system.
|
||||
const data = fs.readFileSync(
|
||||
path.join(import.meta.dirname, 'data/file.txt'));
|
||||
|
||||
// Relative and bare specifier imports resolve inside the mount.
|
||||
import myModule from './lib/mymodule.mjs';
|
||||
const lazy = await import('./lib/lazy.mjs');
|
||||
```
|
||||
|
||||
Module format detection works the same way as on the real file
|
||||
system: name bundled ES modules with the `.mjs` extension (or provide the
|
||||
relevant `package.json` files as assets) so they are interpreted as ESM.
|
||||
|
||||
#### 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 +748,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 +762,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
|
||||
@@ -419,6 +419,37 @@ 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');
|
||||
```
|
||||
|
||||
ESM entry points (`"mainFormat": "module"`) are supported: the main module
|
||||
is loaded from inside the mount through the ESM loader, and
|
||||
`import.meta.dirname` points at the mount root.
|
||||
|
||||
`"useVfs"` cannot be used together with `"useSnapshot"` or `"useCodeCache"`.
|
||||
The SEA configuration parser will error if either combination is detected.
|
||||
|
||||
See the [Single Executable Application][] documentation for more information
|
||||
on creating SEA builds with assets.
|
||||
|
||||
## Class: `VirtualProvider`
|
||||
|
||||
<!-- YAML
|
||||
@@ -591,6 +622,7 @@ fields use synthetic but stable values:
|
||||
[CommonJS resolution algorithm]: modules.md#all-together
|
||||
[ES modules resolution algorithm]: esm.md#resolution-algorithm
|
||||
[Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
|
||||
[Single Executable Application]: single-executable-applications.md
|
||||
[`MemoryProvider`]: #class-memoryprovider
|
||||
[`RealFSProvider`]: #class-realfsprovider
|
||||
[`VirtualFileSystem`]: #class-virtualfilesystem
|
||||
|
||||
@@ -12,10 +12,15 @@
|
||||
const {
|
||||
prepareMainThreadExecution,
|
||||
} = require('internal/process/pre_execution');
|
||||
const { isExperimentalSeaWarningNeeded, isSea } = internalBinding('sea');
|
||||
const {
|
||||
isExperimentalSeaWarningNeeded,
|
||||
isSea,
|
||||
isVfsEnabled,
|
||||
mainCodePath: seaMainCodePath,
|
||||
} = internalBinding('sea');
|
||||
const { emitExperimentalWarning } = require('internal/util');
|
||||
const { emitWarningSync } = require('internal/process/warning');
|
||||
const { Module } = require('internal/modules/cjs/loader');
|
||||
const { Module, wrapModuleLoad } = require('internal/modules/cjs/loader');
|
||||
const { compileFunctionForCJSLoader } = internalBinding('contextify');
|
||||
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
|
||||
const { pathToFileURL } = require('internal/url');
|
||||
@@ -120,10 +125,54 @@ function embedderRunESM(content, filename) {
|
||||
return wrap.getNamespace();
|
||||
}
|
||||
|
||||
/* c8 ignore start -- only reachable in an actual SEA binary */
|
||||
/**
|
||||
* Mounts the SEA virtual file system with the main script placed at the
|
||||
* mount point root, and returns the path of the main script inside the
|
||||
* mount, or null when the VFS could not be set up.
|
||||
* @param {string} content The source of the SEA main script
|
||||
* @returns {string|null} The VFS path of the main script
|
||||
*/
|
||||
function setUpSeaVfs(content) {
|
||||
const mainName = path.basename(seaMainCodePath || process.execPath);
|
||||
const { initSeaVfs } = require('internal/vfs/sea');
|
||||
const seaVfs = initSeaVfs({ extraFiles: { [mainName]: content } });
|
||||
if (seaVfs === null) {
|
||||
return null;
|
||||
}
|
||||
return path.join(seaVfs.mountPoint, mainName);
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
function embedderRunEntryPoint(content, format, filename) {
|
||||
format ||= moduleFormats.kCommonJS;
|
||||
filename ||= process.execPath;
|
||||
|
||||
/* c8 ignore start -- only reachable in an actual SEA binary */
|
||||
if (isLoadingSea && isVfsEnabled()) {
|
||||
// Run the main script from inside the SEA VFS mount so that
|
||||
// `__filename`, `__dirname`, `import.meta`, relative requires and
|
||||
// imports, and `node_modules` lookups all resolve against the
|
||||
// bundled assets.
|
||||
const vfsMain = setUpSeaVfs(content);
|
||||
if (vfsMain !== null) {
|
||||
if (format === moduleFormats.kCommonJS) {
|
||||
return wrapModuleLoad(vfsMain, null, true);
|
||||
} else if (format === moduleFormats.kModule) {
|
||||
const { runEntryPointWithESMLoader } =
|
||||
require('internal/modules/run_main');
|
||||
const mainURL = pathToFileURL(vfsMain);
|
||||
return runEntryPointWithESMLoader((cascadedLoader) => {
|
||||
// Note that if the graph contains unsettled TLA, this may never
|
||||
// resolve even after the event loop stops running.
|
||||
return cascadedLoader.import(
|
||||
mainURL, undefined, { __proto__: null }, undefined, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
|
||||
if (format === moduleFormats.kCommonJS) {
|
||||
return embedderRunCjs(content, filename);
|
||||
} else if (format === moduleFormats.kModule) {
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayFrom,
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypePop,
|
||||
ArrayPrototypePush,
|
||||
Boolean,
|
||||
ObjectKeys,
|
||||
SafeMap,
|
||||
SafeSet,
|
||||
StringPrototypeReplaceAll,
|
||||
StringPrototypeSplit,
|
||||
StringPrototypeStartsWith,
|
||||
Symbol,
|
||||
Uint8Array,
|
||||
} = primordials;
|
||||
|
||||
const { Buffer } = require('buffer');
|
||||
const { posix: pathPosix } = require('path');
|
||||
const { VirtualProvider } = require('internal/vfs/provider');
|
||||
const { MemoryFileHandle } = require('internal/vfs/file_handle');
|
||||
const {
|
||||
codes: {
|
||||
ERR_INVALID_STATE,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
const {
|
||||
createENOENT,
|
||||
createENOTDIR,
|
||||
createEISDIR,
|
||||
createEROFS,
|
||||
} = require('internal/vfs/errors');
|
||||
const {
|
||||
createFileStats,
|
||||
createDirectoryStats,
|
||||
} = require('internal/vfs/stats');
|
||||
const { Dirent } = require('internal/fs/utils');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const {
|
||||
fs: {
|
||||
UV_DIRENT_FILE,
|
||||
UV_DIRENT_DIR,
|
||||
},
|
||||
} = internalBinding('constants');
|
||||
|
||||
// Private symbols
|
||||
const kAssets = Symbol('kAssets');
|
||||
const kExtraFiles = Symbol('kExtraFiles');
|
||||
const kDirectories = Symbol('kDirectories');
|
||||
const kGetAsset = Symbol('kGetAsset');
|
||||
const kSizes = Symbol('kSizes');
|
||||
|
||||
/* c8 ignore start -- the SEA provider requires an actual SEA binary to run */
|
||||
|
||||
/**
|
||||
* Read-only provider serving the assets bundled into a Single Executable
|
||||
* Application. Asset content stays in the executable's SEA blob and is
|
||||
* copied into JS memory only when a file is opened.
|
||||
*/
|
||||
class SEAProvider extends VirtualProvider {
|
||||
/**
|
||||
* @param {object} [options] Options
|
||||
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
|
||||
* files to serve alongside the assets, keyed by path (used for the SEA
|
||||
* main script, whose source lives in the SEA blob but not in the assets)
|
||||
*/
|
||||
constructor(options = kEmptyObject) {
|
||||
super();
|
||||
|
||||
const { isSea, getAsset, getAssetKeys } = internalBinding('sea');
|
||||
|
||||
if (!isSea()) {
|
||||
throw new ERR_INVALID_STATE(
|
||||
'SEAProvider can only be used in a Single Executable Application');
|
||||
}
|
||||
|
||||
this[kGetAsset] = getAsset;
|
||||
// Map of normalized path -> asset key
|
||||
this[kAssets] = new SafeMap();
|
||||
// Map of normalized path -> Buffer content
|
||||
this[kExtraFiles] = new SafeMap();
|
||||
// Map of directory path -> SafeSet of child names
|
||||
this[kDirectories] = new SafeMap();
|
||||
// Cache of file sizes so stat does not have to copy asset content
|
||||
this[kSizes] = new SafeMap();
|
||||
|
||||
// Root directory always exists
|
||||
this[kDirectories].set('/', new SafeSet());
|
||||
|
||||
const keys = getAssetKeys() || [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
this[kAssets].set(this.#normalizePath(key), key);
|
||||
}
|
||||
const extraFiles = options.extraFiles;
|
||||
if (extraFiles !== undefined) {
|
||||
const paths = ObjectKeys(extraFiles);
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
const content = extraFiles[path];
|
||||
const buffer = typeof content === 'string' ?
|
||||
Buffer.from(content) : content;
|
||||
const normalized = this.#normalizePath(path);
|
||||
this[kExtraFiles].set(normalized, buffer);
|
||||
this[kSizes].set(normalized, buffer.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Derive the directory tree from the file paths
|
||||
for (const path of this.#filePaths()) {
|
||||
const parts = ArrayPrototypeFilter(
|
||||
StringPrototypeSplit(path, '/'), Boolean);
|
||||
let currentPath = '/';
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const parentPath = currentPath;
|
||||
currentPath = pathPosix.join(currentPath, parts[i]);
|
||||
if (!this[kDirectories].has(currentPath)) {
|
||||
this[kDirectories].set(currentPath, new SafeSet());
|
||||
}
|
||||
this[kDirectories].get(parentPath).add(parts[i]);
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
this[kDirectories].get(pathPosix.dirname(path)).add(
|
||||
pathPosix.basename(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get supportsSymlinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the normalized paths of all files.
|
||||
* @yields {string} The normalized path of each file
|
||||
*/
|
||||
* #filePaths() {
|
||||
yield* this[kAssets].keys();
|
||||
yield* this[kExtraFiles].keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a path to an absolute posix-style path.
|
||||
* @param {string} path The path
|
||||
* @returns {string} Normalized path
|
||||
*/
|
||||
#normalizePath(path) {
|
||||
let normalized = StringPrototypeReplaceAll(path, '\\', '/');
|
||||
if (!StringPrototypeStartsWith(normalized, '/')) {
|
||||
normalized = '/' + normalized;
|
||||
}
|
||||
return pathPosix.normalize(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a normalized path is a file.
|
||||
* @param {string} path Normalized path
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isFile(path) {
|
||||
return this[kAssets].has(path) || this[kExtraFiles].has(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a normalized path is a directory.
|
||||
* @param {string} path Normalized path
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#isDirectory(path) {
|
||||
return this[kDirectories].has(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file content as an independently mutable Buffer.
|
||||
* @param {string} path Normalized path
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
#getContent(path) {
|
||||
const extra = this[kExtraFiles].get(path);
|
||||
if (extra !== undefined) {
|
||||
return Buffer.from(extra);
|
||||
}
|
||||
const key = this[kAssets].get(path);
|
||||
if (key === undefined) {
|
||||
throw createENOENT('open', path);
|
||||
}
|
||||
// getAsset returns a zero-copy ArrayBuffer over the (possibly read-only)
|
||||
// SEA blob in the executable; copy it so the handle owns mutable memory.
|
||||
const content = Buffer.from(new Uint8Array(this[kGetAsset](key)));
|
||||
this[kSizes].set(path, content.length);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of a file, loading the content only on first access.
|
||||
* @param {string} path Normalized path
|
||||
* @returns {number}
|
||||
*/
|
||||
#getSize(path) {
|
||||
let size = this[kSizes].get(path);
|
||||
if (size === undefined) {
|
||||
size = this.#getContent(path).length;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
openSync(path, flags, mode) {
|
||||
// Normalize numeric flags (O_RDONLY === 0) to a string
|
||||
const normalizedFlags = typeof flags === 'number' ?
|
||||
(flags === 0 ? 'r' : null) : flags;
|
||||
if (normalizedFlags !== 'r') {
|
||||
throw createEROFS('open', path);
|
||||
}
|
||||
|
||||
const normalized = this.#normalizePath(path);
|
||||
|
||||
if (this.#isDirectory(normalized)) {
|
||||
throw createEISDIR('open', path);
|
||||
}
|
||||
if (!this.#isFile(normalized)) {
|
||||
throw createENOENT('open', path);
|
||||
}
|
||||
|
||||
const content = this.#getContent(normalized);
|
||||
const getStats = () => createFileStats(content.length, { mode: 0o444 });
|
||||
return new MemoryFileHandle(normalized, 'r', 0o444, content, null,
|
||||
getStats);
|
||||
}
|
||||
|
||||
async open(path, flags, mode) {
|
||||
return this.openSync(path, flags, mode);
|
||||
}
|
||||
|
||||
statSync(path, options) {
|
||||
const normalized = this.#normalizePath(path);
|
||||
|
||||
if (this.#isDirectory(normalized)) {
|
||||
return createDirectoryStats({ mode: 0o555, bigint: options?.bigint });
|
||||
}
|
||||
if (this.#isFile(normalized)) {
|
||||
return createFileStats(this.#getSize(normalized),
|
||||
{ mode: 0o444, bigint: options?.bigint });
|
||||
}
|
||||
throw createENOENT('stat', path);
|
||||
}
|
||||
|
||||
async stat(path, options) {
|
||||
return this.statSync(path, options);
|
||||
}
|
||||
|
||||
readdirSync(path, options) {
|
||||
const normalized = this.#normalizePath(path);
|
||||
|
||||
if (!this.#isDirectory(normalized)) {
|
||||
if (this.#isFile(normalized)) {
|
||||
throw createENOTDIR('scandir', path);
|
||||
}
|
||||
throw createENOENT('scandir', path);
|
||||
}
|
||||
|
||||
const withFileTypes = options?.withFileTypes === true;
|
||||
const recursive = options?.recursive === true;
|
||||
|
||||
if (recursive) {
|
||||
return this.#readdirRecursive(normalized, withFileTypes);
|
||||
}
|
||||
|
||||
const names = ArrayFrom(this[kDirectories].get(normalized));
|
||||
if (!withFileTypes) {
|
||||
return names;
|
||||
}
|
||||
|
||||
const dirents = [];
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const childPath = pathPosix.join(normalized, names[i]);
|
||||
const type = this.#isDirectory(childPath) ?
|
||||
UV_DIRENT_DIR : UV_DIRENT_FILE;
|
||||
ArrayPrototypePush(dirents, new Dirent(names[i], type, normalized));
|
||||
}
|
||||
return dirents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively reads directory contents.
|
||||
* @param {string} dirPath The normalized directory path
|
||||
* @param {boolean} withFileTypes Whether to return Dirent objects
|
||||
* @returns {string[]|Dirent[]}
|
||||
*/
|
||||
#readdirRecursive(dirPath, withFileTypes) {
|
||||
const results = [];
|
||||
|
||||
// Traverse depth-first in preorder with an explicit frame stack, so a
|
||||
// deeply nested asset tree cannot exhaust the call stack.
|
||||
const stack = [{
|
||||
path: dirPath,
|
||||
relative: '',
|
||||
children: ArrayFrom(this[kDirectories].get(dirPath)),
|
||||
index: 0,
|
||||
}];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const frame = stack[stack.length - 1];
|
||||
if (frame.index >= frame.children.length) {
|
||||
ArrayPrototypePop(stack);
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = frame.children[frame.index++];
|
||||
const childPath = pathPosix.join(frame.path, name);
|
||||
const childRelative = frame.relative ?
|
||||
`${frame.relative}/${name}` : name;
|
||||
const isDir = this.#isDirectory(childPath);
|
||||
|
||||
if (withFileTypes) {
|
||||
const type = isDir ? UV_DIRENT_DIR : UV_DIRENT_FILE;
|
||||
ArrayPrototypePush(results,
|
||||
new Dirent(childRelative, type, dirPath));
|
||||
} else {
|
||||
ArrayPrototypePush(results, childRelative);
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
ArrayPrototypePush(stack, {
|
||||
path: childPath,
|
||||
relative: childRelative,
|
||||
children: ArrayFrom(this[kDirectories].get(childPath)),
|
||||
index: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async readdir(path, options) {
|
||||
return this.readdirSync(path, options);
|
||||
}
|
||||
}
|
||||
|
||||
/* c8 ignore stop */
|
||||
|
||||
module.exports = {
|
||||
SEAProvider,
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const { isSea, isVfsEnabled } = internalBinding('sea');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const {
|
||||
codes: {
|
||||
ERR_INVALID_STATE,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
|
||||
let initialized = false;
|
||||
|
||||
/* c8 ignore start -- SEA VFS initialization requires an actual SEA binary */
|
||||
|
||||
/**
|
||||
* Initializes the SEA virtual file system: a read-only VFS serving the
|
||||
* assets bundled into the executable, mounted at its reserved mount point.
|
||||
* Because a VFS never shadows the real file system, the assets live under
|
||||
* the mount point returned by `vfs.mountPoint`, not at a fixed path; the
|
||||
* SEA main script is placed at the mount point root so bundled code can
|
||||
* reach the assets through `__dirname`-relative paths and relative
|
||||
* `require()` calls.
|
||||
* @param {object} [options] Configuration options
|
||||
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
|
||||
* files to serve alongside the assets (used for the SEA main script)
|
||||
* @returns {VirtualFileSystem|null} The mounted VFS, or null if not running
|
||||
* as a SEA or VFS is not enabled in the SEA configuration
|
||||
* @throws {ERR_INVALID_STATE} If already initialized
|
||||
*/
|
||||
function initSeaVfs(options = kEmptyObject) {
|
||||
if (initialized) {
|
||||
throw new ERR_INVALID_STATE('SEA VFS is already initialized');
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
if (!isSea() || !isVfsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { VirtualFileSystem } = require('internal/vfs/file_system');
|
||||
const { SEAProvider } = require('internal/vfs/providers/sea');
|
||||
|
||||
const provider = new SEAProvider({ extraFiles: options.extraFiles });
|
||||
// The SEA warning already covers the feature; don't emit the
|
||||
// VirtualFileSystem experimental warning for the implicit SEA mount.
|
||||
const vfs = new VirtualFileSystem(provider, {
|
||||
emitExperimentalWarning: false,
|
||||
});
|
||||
vfs.mount();
|
||||
|
||||
return vfs;
|
||||
}
|
||||
|
||||
/* c8 ignore stop */
|
||||
|
||||
module.exports = {
|
||||
initSeaVfs,
|
||||
};
|
||||
@@ -263,6 +263,15 @@ void IsSea(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(IsSingleExecutable());
|
||||
}
|
||||
|
||||
void IsVfsEnabled(const FunctionCallbackInfo<Value>& args) {
|
||||
bool enabled = false;
|
||||
if (IsSingleExecutable()) {
|
||||
SeaResource sea_resource = FindSingleExecutableResource();
|
||||
enabled = static_cast<bool>(sea_resource.flags & SeaFlags::kEnableVfs);
|
||||
}
|
||||
args.GetReturnValue().Set(enabled);
|
||||
}
|
||||
|
||||
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
|
||||
bool is_building_sea =
|
||||
!per_process::cli_options->experimental_sea_config.empty();
|
||||
@@ -447,6 +456,16 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
|
||||
if (use_code_cache_value) {
|
||||
result.flags |= SeaFlags::kUseCodeCache;
|
||||
}
|
||||
} else if (key == "useVfs") {
|
||||
bool use_vfs;
|
||||
if (field.value().get_bool().get(use_vfs)) {
|
||||
FPrintF(
|
||||
stderr, "\"useVfs\" field of %s is not a Boolean\n", config_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (use_vfs) {
|
||||
result.flags |= SeaFlags::kEnableVfs;
|
||||
}
|
||||
} else if (key == "assets") {
|
||||
simdjson::ondemand::object assets_object;
|
||||
if (field.value().get_object().get(assets_object)) {
|
||||
@@ -565,6 +584,19 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (static_cast<bool>(result.flags & SeaFlags::kEnableVfs)) {
|
||||
if (static_cast<bool>(result.flags & SeaFlags::kUseSnapshot)) {
|
||||
FPrintF(stderr,
|
||||
"\"useVfs\" is not supported when \"useSnapshot\" is true\n");
|
||||
return std::nullopt;
|
||||
}
|
||||
if (static_cast<bool>(result.flags & SeaFlags::kUseCodeCache)) {
|
||||
FPrintF(stderr,
|
||||
"\"useVfs\" is not supported when \"useCodeCache\" is true\n");
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.main_path.empty()) {
|
||||
FPrintF(stderr,
|
||||
"\"main\" field of %s is not a non-empty string\n",
|
||||
@@ -924,7 +956,31 @@ void Initialize(Local<Object> target,
|
||||
Local<Value> unused,
|
||||
Local<Context> context,
|
||||
void* priv) {
|
||||
Environment* env = Environment::GetCurrent(context);
|
||||
Isolate* isolate = env->isolate();
|
||||
|
||||
if (IsSingleExecutable()) {
|
||||
SeaResource sea_resource = FindSingleExecutableResource();
|
||||
// Expose the main script path recorded in the SEA config so the VFS
|
||||
// integration can place the main script at the mount point root.
|
||||
if (static_cast<bool>(sea_resource.flags & SeaFlags::kEnableVfs)) {
|
||||
Local<String> code_path_str;
|
||||
if (String::NewFromUtf8(isolate,
|
||||
sea_resource.code_path.data(),
|
||||
NewStringType::kNormal,
|
||||
sea_resource.code_path.length())
|
||||
.ToLocal(&code_path_str)) {
|
||||
target
|
||||
->Set(context,
|
||||
FIXED_ONE_BYTE_STRING(isolate, "mainCodePath"),
|
||||
code_path_str)
|
||||
.Check();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetMethod(context, target, "isSea", IsSea);
|
||||
SetMethod(context, target, "isVfsEnabled", IsVfsEnabled);
|
||||
SetMethod(context,
|
||||
target,
|
||||
"isExperimentalSeaWarningNeeded",
|
||||
@@ -935,6 +991,7 @@ void Initialize(Local<Object> target,
|
||||
|
||||
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(IsSea);
|
||||
registry->Register(IsVfsEnabled);
|
||||
registry->Register(IsExperimentalSeaWarningNeeded);
|
||||
registry->Register(GetAsset);
|
||||
registry->Register(GetAssetKeys);
|
||||
|
||||
@@ -30,6 +30,7 @@ enum class SeaFlags : uint32_t {
|
||||
kUseCodeCache = 1 << 2,
|
||||
kIncludeAssets = 1 << 3,
|
||||
kIncludeExecArgv = 1 << 4,
|
||||
kEnableVfs = 1 << 5,
|
||||
};
|
||||
|
||||
enum class SeaExecArgvExtension : uint8_t {
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
// CommonJS module imported from the ESM main to test interop inside the VFS.
|
||||
module.exports = {
|
||||
sum: (a, b) => a + b,
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"name":"test-app","version":"1.0.0"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
Hello from SEA VFS!
|
||||
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
// The ESM SEA main script runs from inside the VFS mount: import.meta
|
||||
// reflects the mount, and static imports, dynamic imports, and bare
|
||||
// specifier lookups all resolve against the bundled assets.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import assert from 'node:assert';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
// Static import of a relative ESM module from the VFS.
|
||||
import { add, multiply } from './modules/math.mjs';
|
||||
|
||||
// Static import of a CommonJS module from the VFS (interop).
|
||||
import calculator from './modules/calculator.cjs';
|
||||
|
||||
// Static import of a bare specifier resolved via the in-VFS node_modules.
|
||||
import { name as pkgName, greet } from 'test-esm-pkg';
|
||||
|
||||
// The main script runs from inside the VFS, not from the executable.
|
||||
assert.ok(import.meta.url.startsWith('file:'));
|
||||
assert.strictEqual(path.basename(import.meta.filename), 'main.mjs');
|
||||
assert.notStrictEqual(import.meta.filename, process.execPath);
|
||||
console.log('main module runs from', import.meta.url);
|
||||
|
||||
// import.meta.dirname is the root of the mounted assets.
|
||||
const config = JSON.parse(
|
||||
fs.readFileSync(path.join(import.meta.dirname, 'config.json'), 'utf8'));
|
||||
assert.strictEqual(config.name, 'test-app');
|
||||
const greeting = fs.readFileSync(
|
||||
path.join(import.meta.dirname, 'data', 'greeting.txt'), 'utf8');
|
||||
assert.strictEqual(greeting, 'Hello from SEA VFS!');
|
||||
console.log('fs access through import.meta.dirname passed');
|
||||
|
||||
// Static ESM import from the VFS.
|
||||
assert.strictEqual(add(2, 3), 5);
|
||||
assert.strictEqual(multiply(4, 5), 20);
|
||||
console.log('static relative import passed');
|
||||
|
||||
// CommonJS interop from the VFS.
|
||||
assert.strictEqual(calculator.sum(10, 20), 30);
|
||||
console.log('static import of CommonJS module passed');
|
||||
|
||||
// Bare specifier resolution confined to the in-VFS node_modules.
|
||||
assert.strictEqual(pkgName, 'test-esm-pkg');
|
||||
assert.strictEqual(greet('World'), 'Hello, World!');
|
||||
console.log('bare specifier import passed');
|
||||
|
||||
// Dynamic import from the VFS.
|
||||
const dynamicMath = await import('./modules/math.mjs');
|
||||
assert.strictEqual(dynamicMath.add(1, 1), 2);
|
||||
console.log('dynamic import passed');
|
||||
|
||||
// createRequire against the VFS main URL.
|
||||
const require = createRequire(import.meta.url);
|
||||
const requiredCalculator = require('./modules/calculator.cjs');
|
||||
assert.strictEqual(requiredCalculator.sum(3, 4), 7);
|
||||
console.log('createRequire from import.meta.url passed');
|
||||
|
||||
// node:sea and the VFS serve the same content.
|
||||
const { getAsset } = await import('node:sea');
|
||||
assert.strictEqual(getAsset('data/greeting.txt', 'utf8'), greeting);
|
||||
console.log('node:sea API and VFS coexistence passed');
|
||||
|
||||
console.log('All SEA VFS ESM tests passed!');
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
export function multiply(a, b) {
|
||||
return a * b;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"main": "main.mjs",
|
||||
"mainFormat": "module",
|
||||
"output": "sea-prep.blob",
|
||||
"useVfs": true,
|
||||
"assets": {
|
||||
"config.json": "config.json",
|
||||
"data/greeting.txt": "greeting.txt",
|
||||
"modules/math.mjs": "math.mjs",
|
||||
"modules/calculator.cjs": "calculator.cjs",
|
||||
"node_modules/test-esm-pkg/package.json": "test-esm-pkg-package.json",
|
||||
"node_modules/test-esm-pkg/index.mjs": "test-esm-pkg-index.mjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const name = 'test-esm-pkg';
|
||||
export function greet(who) {
|
||||
return `Hello, ${who}!`;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "test-esm-pkg",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": { ".": "./index.mjs" }
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
// This module tests transitive requires - it requires math.js using a
|
||||
// relative path, which tests that module hooks resolve correctly.
|
||||
const math = require('./math.js');
|
||||
|
||||
module.exports = {
|
||||
sum: (a, b) => math.add(a, b),
|
||||
product: (a, b) => math.multiply(a, b),
|
||||
};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"name":"test-app","version":"1.0.0"}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
Hello from SEA VFS!
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
add: (a, b) => a + b,
|
||||
multiply: (a, b) => a * b,
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"main": "sea.js",
|
||||
"output": "sea-prep.blob",
|
||||
"useVfs": true,
|
||||
"assets": {
|
||||
"config.json": "config.json",
|
||||
"data/greeting.txt": "greeting.txt",
|
||||
"modules/math.js": "math.js",
|
||||
"modules/calculator.js": "calculator.js",
|
||||
"node_modules/test-pkg/package.json": "test-pkg-package.json",
|
||||
"node_modules/test-pkg/index.js": "test-pkg-index.js",
|
||||
"node_modules/test-exports-pkg/package.json": "test-exports-pkg-package.json",
|
||||
"node_modules/test-exports-pkg/lib/entry.js": "test-exports-pkg-entry.js"
|
||||
}
|
||||
}
|
||||
Vendored
+106
@@ -0,0 +1,106 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
// The SEA VFS never shadows the real file system: the assets are mounted at
|
||||
// a reserved mount point and the main script runs from inside the mount, so
|
||||
// everything is reachable through __dirname-relative paths and relative
|
||||
// requires.
|
||||
|
||||
// The main script runs from inside the VFS, not from the executable.
|
||||
assert.notStrictEqual(__filename, process.execPath);
|
||||
assert.strictEqual(path.basename(__filename), 'sea.js');
|
||||
assert.strictEqual(require.main, module);
|
||||
console.log('main script runs from', __filename);
|
||||
|
||||
// The main script itself is readable through fs.
|
||||
assert.strictEqual(fs.existsSync(__filename), true);
|
||||
|
||||
// Read the config file through standard fs (via VFS hooks)
|
||||
const configContent = fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8');
|
||||
const config = JSON.parse(configContent);
|
||||
assert.strictEqual(config.name, 'test-app', 'config.name should match');
|
||||
assert.strictEqual(config.version, '1.0.0', 'config.version should match');
|
||||
console.log('Read config.json:', config);
|
||||
|
||||
// Read a text file
|
||||
const greetingPath = path.join(__dirname, 'data', 'greeting.txt');
|
||||
const greeting = fs.readFileSync(greetingPath, 'utf8');
|
||||
assert.strictEqual(greeting, 'Hello from SEA VFS!', 'greeting should match');
|
||||
console.log('Read greeting.txt:', greeting);
|
||||
|
||||
// Test existsSync
|
||||
assert.strictEqual(fs.existsSync(path.join(__dirname, 'config.json')), true);
|
||||
assert.strictEqual(fs.existsSync(greetingPath), true);
|
||||
assert.strictEqual(fs.existsSync(path.join(__dirname, 'nonexistent.txt')), false);
|
||||
console.log('existsSync tests passed');
|
||||
|
||||
// Test statSync
|
||||
const configStat = fs.statSync(path.join(__dirname, 'config.json'));
|
||||
assert.strictEqual(configStat.isFile(), true);
|
||||
assert.strictEqual(configStat.isDirectory(), false);
|
||||
const dirStat = fs.statSync(path.join(__dirname, 'data'));
|
||||
assert.strictEqual(dirStat.isDirectory(), true);
|
||||
console.log('statSync tests passed');
|
||||
|
||||
// Test readdirSync - the mount root lists the assets and the main script
|
||||
const entries = fs.readdirSync(__dirname);
|
||||
assert.ok(entries.includes('config.json'), 'Should include config.json');
|
||||
assert.ok(entries.includes('data'), 'Should include data directory');
|
||||
assert.ok(entries.includes('sea.js'), 'Should include the main script');
|
||||
console.log('readdirSync tests passed, entries:', entries);
|
||||
|
||||
// The VFS is read-only
|
||||
assert.throws(() => {
|
||||
fs.writeFileSync(path.join(__dirname, 'new-file.txt'), 'nope');
|
||||
}, { code: 'EROFS' });
|
||||
console.log('read-only test passed');
|
||||
|
||||
// Test relative require from main script - __filename is inside the mount so
|
||||
// relative paths resolve against the bundled assets via module hooks
|
||||
const mathModule = require('./modules/math.js');
|
||||
assert.strictEqual(mathModule.add(2, 3), 5, 'math.add should work');
|
||||
assert.strictEqual(mathModule.multiply(4, 5), 20, 'math.multiply should work');
|
||||
console.log('relative require from main script passed');
|
||||
|
||||
// Test transitive requires: calculator.js requires ./math.js internally
|
||||
const calculator = require('./modules/calculator.js');
|
||||
assert.strictEqual(calculator.sum(10, 20), 30, 'calculator.sum should work');
|
||||
assert.strictEqual(calculator.product(3, 7), 21, 'calculator.product should work');
|
||||
console.log('transitive require from VFS tests passed');
|
||||
|
||||
// Module lookup paths are confined to the mount
|
||||
assert.deepStrictEqual(module.paths, [path.join(__dirname, 'node_modules')]);
|
||||
console.log('module.paths confinement test passed');
|
||||
|
||||
// Test that node:sea API and VFS can load the same asset
|
||||
const sea = require('node:sea');
|
||||
const seaAsset = sea.getAsset('data/greeting.txt', 'utf8');
|
||||
const vfsAsset = fs.readFileSync(greetingPath, 'utf8');
|
||||
assert.strictEqual(seaAsset, vfsAsset, 'node:sea and VFS should return the same content');
|
||||
console.log('node:sea API and VFS coexistence test passed');
|
||||
|
||||
// Test buffer independence: multiple reads return independent copies
|
||||
const buf1 = fs.readFileSync(greetingPath);
|
||||
const buf2 = fs.readFileSync(greetingPath);
|
||||
const original = buf1[0];
|
||||
buf1[0] = 0xFF;
|
||||
assert.strictEqual(buf2[0], original, 'buf2 should be unaffected by buf1 mutation');
|
||||
assert.strictEqual(buf1[0], 0xFF, 'buf1 mutation should persist');
|
||||
console.log('buffer independence test passed');
|
||||
|
||||
// Test node_modules package lookup via VFS (resolved through "exports" field)
|
||||
const testPkg = require('test-pkg');
|
||||
assert.strictEqual(testPkg.name, 'test-pkg', 'package name should match');
|
||||
assert.strictEqual(testPkg.greet('World'), 'Hello, World!', 'package function should work');
|
||||
console.log('node_modules package lookup test passed');
|
||||
|
||||
// Test exports-only package (no "main" field, entry in subdirectory)
|
||||
// This proves the package.json reader is VFS-aware - without it,
|
||||
// "exports" would not be consulted and resolution would fail.
|
||||
const exportsPkg = require('test-exports-pkg');
|
||||
assert.strictEqual(exportsPkg.fromExports, true, 'exports-only package should resolve');
|
||||
console.log('exports-only package lookup test passed');
|
||||
|
||||
console.log('All SEA VFS tests passed!');
|
||||
@@ -0,0 +1,2 @@
|
||||
'use strict';
|
||||
module.exports = { fromExports: true };
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "test-exports-pkg",
|
||||
"version": "1.0.0",
|
||||
"exports": { ".": "./lib/entry.js" }
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
module.exports = {
|
||||
name: 'test-pkg',
|
||||
greet: (name) => `Hello, ${name}!`,
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "test-pkg",
|
||||
"version": "1.0.0",
|
||||
"exports": { ".": "./index.js" }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// This tests that --build-sea rejects "useVfs" combined with options it
|
||||
// does not support.
|
||||
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { skipIfBuildSEAIsNotSupported } = require('../common/sea');
|
||||
const { writeFileSync } = require('fs');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
|
||||
skipIfBuildSEAIsNotSupported();
|
||||
|
||||
// Test: "useVfs" is not a Boolean
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = tmpdir.resolve('invalid-useVfs.json');
|
||||
writeFileSync(config, `
|
||||
{
|
||||
"main": "bundle.js",
|
||||
"output": "sea",
|
||||
"useVfs": "true"
|
||||
}
|
||||
`, 'utf8');
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--build-sea', config], {
|
||||
cwd: tmpdir.path,
|
||||
}, {
|
||||
status: 1,
|
||||
stderr: /"useVfs" field of .*invalid-useVfs\.json is not a Boolean/,
|
||||
});
|
||||
}
|
||||
|
||||
// Test: "useVfs" with "useSnapshot"
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = tmpdir.resolve('vfs-snapshot.json');
|
||||
writeFileSync(config, `
|
||||
{
|
||||
"main": "bundle.js",
|
||||
"output": "sea",
|
||||
"useVfs": true,
|
||||
"useSnapshot": true
|
||||
}
|
||||
`, 'utf8');
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--build-sea', config], {
|
||||
cwd: tmpdir.path,
|
||||
}, {
|
||||
status: 1,
|
||||
stderr: /"useVfs" is not supported when "useSnapshot" is true/,
|
||||
});
|
||||
}
|
||||
|
||||
// Test: "useVfs" with "useCodeCache"
|
||||
{
|
||||
tmpdir.refresh();
|
||||
const config = tmpdir.resolve('vfs-code-cache.json');
|
||||
writeFileSync(config, `
|
||||
{
|
||||
"main": "bundle.js",
|
||||
"output": "sea",
|
||||
"useVfs": true,
|
||||
"useCodeCache": true
|
||||
}
|
||||
`, 'utf8');
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--build-sea', config], {
|
||||
cwd: tmpdir.path,
|
||||
}, {
|
||||
status: 1,
|
||||
stderr: /"useVfs" is not supported when "useCodeCache" is true/,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
// This tests the SEA VFS integration with an ESM entry point - the bundled
|
||||
// assets are mounted as a virtual file system and the ESM main script runs
|
||||
// from inside the mount through the ESM loader.
|
||||
|
||||
require('../common');
|
||||
|
||||
const {
|
||||
buildSEA,
|
||||
skipIfBuildSEAIsNotSupported,
|
||||
} = require('../common/sea');
|
||||
|
||||
skipIfBuildSEAIsNotSupported();
|
||||
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
tmpdir.refresh();
|
||||
const outputFile = buildSEA(fixtures.path('sea', 'vfs-esm'));
|
||||
|
||||
spawnSyncAndAssert(
|
||||
outputFile,
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
stdout: /All SEA VFS ESM tests passed!/,
|
||||
stderr(stderr) {
|
||||
if (/ExperimentalWarning: VirtualFileSystem/.test(stderr)) {
|
||||
throw new Error('SEA VFS should not emit the public VirtualFileSystem warning');
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
// This tests the SEA VFS integration - the bundled assets are mounted as a
|
||||
// virtual file system and the main script runs from inside the mount.
|
||||
|
||||
require('../common');
|
||||
|
||||
const {
|
||||
buildSEA,
|
||||
skipIfBuildSEAIsNotSupported,
|
||||
} = require('../common/sea');
|
||||
|
||||
skipIfBuildSEAIsNotSupported();
|
||||
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
tmpdir.refresh();
|
||||
const outputFile = buildSEA(fixtures.path('sea', 'vfs'));
|
||||
|
||||
spawnSyncAndAssert(
|
||||
outputFile,
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_DEBUG_NATIVE: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
stdout: /All SEA VFS tests passed!/,
|
||||
stderr(stderr) {
|
||||
if (/ExperimentalWarning: VirtualFileSystem/.test(stderr)) {
|
||||
throw new Error('SEA VFS should not emit the public VirtualFileSystem warning');
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user