Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ The configuration currently reads the following top-level fields:
{
"main": "/path/to/bundled/script.js",
"mainFormat": "commonjs", // Default: "commonjs", options: "commonjs", "module"
"allowDynamicImportFromFileSystem": false, // Default: false
"executable": "/path/to/node/binary", // Optional, if not specified, uses the current Node.js binary
"output": "/path/to/write/the/generated/executable",
"disableExperimentalSEAWarning": true, // Default: false
Expand Down Expand Up @@ -451,9 +452,12 @@ injected main script with the following properties:

<!-- TODO(joyeecheung): support and document module.registerHooks -->

When using `"mainFormat": "module"`, `import()` can be used to dynamically
load built-in modules. Attempting to use `import()` to load modules from
the file system will throw an error.
By default, `import()` in the injected main script can only load built-in
modules. When `allowDynamicImportFromFileSystem` is set to `true` in the SEA
configuration, `import()` can also load modules from the file system. This
works for both `"mainFormat": "commonjs"` and `"mainFormat": "module"`
entry points. Relative specifiers are resolved relative to the directory of the
executable.

### Using native addons in the injected main script

Expand Down
21 changes: 18 additions & 3 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const {
} = require('internal/modules/helpers');

let defaultConditions;
let seaBinding;
/**
* Returns the default conditions for ES module loading.
* @returns {object}
Expand All @@ -68,6 +69,10 @@ function getDefaultConditionsSet() {
return defaultConditionsSet;
}

function getSeaBinding() {
return seaBinding ??= internalBinding('sea');
}

/**
* Initializes the default conditions for ESM module loading.
* This function is called during pre-execution, before any user code is run.
Expand Down Expand Up @@ -238,15 +243,25 @@ function getBuiltinModuleWrapForEmbedder(specifier) {
}

/**
* Get the built-in module dynamically for embedder ESM.
* Handle dynamic import for embedder entry points.
* SEA can opt into the default loader via explicit configuration; other embedder
* entry points continue to be limited to built-in modules.
* @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase. Ignored for now.
* @param {Record<string, string>} attributes - The import attributes object. Ignored for now.
* @param {string|null|undefined} referrerName - name of the referrer.
* @returns {import('internal/modules/esm/loader.js').ModuleExports} - The imported module object.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
*/
function importModuleDynamicallyForEmbedder(specifier, phase, attributes, referrerName) {
// Ignore phase and attributes for embedder ESM for now, because this only supports loading builtins.
const { isSeaDynamicImportFromFileSystemEnabled } = getSeaBinding();
if (isSeaDynamicImportFromFileSystemEnabled()) {
return defaultImportModuleDynamicallyForScript(
specifier,
phase,
attributes,
referrerName,
);
}
return getBuiltinModuleWrapForEmbedder(specifier).getNamespace();
}

Expand Down
29 changes: 29 additions & 0 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,18 @@ void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
}

void IsSeaDynamicImportFromFileSystemEnabled(
const FunctionCallbackInfo<Value>& args) {
if (!IsSingleExecutable()) {
args.GetReturnValue().Set(false);
return;
}

SeaResource sea_resource = FindSingleExecutableResource();
args.GetReturnValue().Set(static_cast<bool>(
sea_resource.flags & SeaFlags::kAllowDynamicImportFromFileSystem));
}

std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
// Repeats argv[0] at position 1 on argv as a replacement for the missing
// entry point file path.
Expand Down Expand Up @@ -444,6 +456,18 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
if (use_code_cache_value) {
result.flags |= SeaFlags::kUseCodeCache;
}
} else if (key == "allowDynamicImportFromFileSystem") {
bool allow_dynamic_import_from_file_system;
if (field.value().get_bool().get(allow_dynamic_import_from_file_system)) {
FPrintF(stderr,
"\"allowDynamicImportFromFileSystem\" field of %s is not a "
"Boolean\n",
config_path);
return std::nullopt;
}
if (allow_dynamic_import_from_file_system) {
result.flags |= SeaFlags::kAllowDynamicImportFromFileSystem;
}
} else if (key == "assets") {
simdjson::ondemand::object assets_object;
if (field.value().get_object().get(assets_object)) {
Expand Down Expand Up @@ -918,13 +942,18 @@ void Initialize(Local<Object> target,
target,
"isExperimentalSeaWarningNeeded",
IsExperimentalSeaWarningNeeded);
SetMethod(context,
target,
"isSeaDynamicImportFromFileSystemEnabled",
IsSeaDynamicImportFromFileSystemEnabled);
SetMethod(context, target, "getAsset", GetAsset);
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsSea);
registry->Register(IsExperimentalSeaWarningNeeded);
registry->Register(IsSeaDynamicImportFromFileSystemEnabled);
registry->Register(GetAsset);
registry->Register(GetAssetKeys);
}
Expand Down
1 change: 1 addition & 0 deletions src/node_sea.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum class SeaFlags : uint32_t {
kUseCodeCache = 1 << 2,
kIncludeAssets = 1 << 3,
kIncludeExecArgv = 1 << 4,
kAllowDynamicImportFromFileSystem = 1 << 5,
};

enum class SeaExecArgvExtension : uint8_t {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"main": "sea.js",
"output": "sea",
"allowDynamicImportFromFileSystem": true,
"useCodeCache": true,
"disableExperimentalSEAWarning": true
}
6 changes: 6 additions & 0 deletions test/fixtures/sea/dynamic-import-cjs/sea-config-opt-in.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"main": "sea.js",
"output": "sea",
"allowDynamicImportFromFileSystem": true,
"disableExperimentalSEAWarning": true
}
5 changes: 5 additions & 0 deletions test/fixtures/sea/dynamic-import-cjs/sea-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"main": "sea.js",
"output": "sea",
"disableExperimentalSEAWarning": true
}
15 changes: 15 additions & 0 deletions test/fixtures/sea/dynamic-import-cjs/sea.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { pathToFileURL } = require('node:url');
const { dirname, join } = require('node:path');

const userURL = pathToFileURL(
join(dirname(process.execPath), 'user.mjs'),
).href;

import(userURL)
.then(({ message }) => {
console.log(message);
})
.catch((err) => {
console.error(err);
process.exit(1);
});
3 changes: 3 additions & 0 deletions test/fixtures/sea/dynamic-import-cjs/user.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
await Promise.resolve();

export const message = 'CJS SEA dynamic import executed successfully';
7 changes: 7 additions & 0 deletions test/fixtures/sea/dynamic-import-esm/sea-config-opt-in.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"main": "sea.mjs",
"output": "sea",
"mainFormat": "module",
"allowDynamicImportFromFileSystem": true,
"disableExperimentalSEAWarning": true
}
6 changes: 6 additions & 0 deletions test/fixtures/sea/dynamic-import-esm/sea-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"main": "sea.mjs",
"output": "sea",
"mainFormat": "module",
"disableExperimentalSEAWarning": true
}
2 changes: 2 additions & 0 deletions test/fixtures/sea/dynamic-import-esm/sea.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { message } = await import('./user.mjs');
console.log(message);
3 changes: 3 additions & 0 deletions test/fixtures/sea/dynamic-import-esm/user.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
await Promise.resolve();

export const message = 'ESM SEA dynamic import executed successfully';
21 changes: 21 additions & 0 deletions test/sea/test-build-sea-invalid-boolean-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,24 @@ skipIfBuildSEAIsNotSupported();
stderr: /"useCodeCache" field of .*invalid-useCodeCache\.json is not a Boolean/,
});
}

// Test: Invalid "allowDynamicImportFromFileSystem" type (should be Boolean)
{
tmpdir.refresh();
const config = tmpdir.resolve('invalid-allowDynamicImportFromFileSystem.json');
writeFileSync(config, `
{
"main": "bundle.js",
"output": "sea",
"allowDynamicImportFromFileSystem": "true"
}
`, 'utf8');
spawnSyncAndAssert(
process.execPath,
['--build-sea', config], {
cwd: tmpdir.path,
}, {
status: 1,
stderr: /"allowDynamicImportFromFileSystem" field of .*invalid-allowDynamicImportFromFileSystem\.json is not a Boolean/,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

require('../common');

// This tests that a CommonJS SEA entry point can dynamically import a user
// module from the file system when explicitly enabled, even with code cache.

const {
buildSEA,
skipIfBuildSEAIsNotSupported,
} = require('../common/sea');

skipIfBuildSEAIsNotSupported();

const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');

tmpdir.refresh();

const outputFile = buildSEA(fixtures.path('sea', 'dynamic-import-cjs'), {
configPath: 'sea-config-opt-in-code-cache.json',
});

spawnSyncAndExitWithoutError(
outputFile,
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
},
{
stdout: /CJS SEA dynamic import executed successfully/,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

require('../common');

// This tests that a CommonJS SEA entry point cannot dynamically import a user
// module from the file system without explicit configuration.

const {
buildSEA,
skipIfBuildSEAIsNotSupported,
} = require('../common/sea');

skipIfBuildSEAIsNotSupported();

const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const { spawnSyncAndAssert } = require('../common/child_process');

tmpdir.refresh();

const outputFile = buildSEA(fixtures.path('sea', 'dynamic-import-cjs'));

spawnSyncAndAssert(
outputFile,
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
},
{
status: 1,
stderr: /ERR_UNKNOWN_BUILTIN_MODULE/,
});
35 changes: 35 additions & 0 deletions test/sea/test-single-executable-application-dynamic-import-cjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

require('../common');

// This tests that a CommonJS SEA entry point can use dynamic import() to
// load a user module from the file system.

const {
buildSEA,
skipIfBuildSEAIsNotSupported,
} = require('../common/sea');

skipIfBuildSEAIsNotSupported();

const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');

tmpdir.refresh();

const outputFile = buildSEA(fixtures.path('sea', 'dynamic-import-cjs'), {
configPath: 'sea-config-opt-in.json',
});

spawnSyncAndExitWithoutError(
outputFile,
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
},
{
stdout: /CJS SEA dynamic import executed successfully/,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

require('../common');

// This tests that an ESM SEA entry point cannot dynamically import a user
// module from the file system without explicit configuration.

const {
buildSEA,
skipIfBuildSEAIsNotSupported,
} = require('../common/sea');

skipIfBuildSEAIsNotSupported();

const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const { spawnSyncAndAssert } = require('../common/child_process');

tmpdir.refresh();

const outputFile = buildSEA(fixtures.path('sea', 'dynamic-import-esm'));

spawnSyncAndAssert(
outputFile,
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
},
{
status: 1,
stderr: /ERR_UNKNOWN_BUILTIN_MODULE/,
});
Loading
Loading