Skip to content

Commit af0921d

Browse files
authored
esm: add --import flag
PR-URL: #43942 Fixes: #40110 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 659dc12 commit af0921d

15 files changed

+346
-64
lines changed

doc/api/cli.md

+22-3
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,8 @@ Only the root context is supported. There is no guarantee that
445445
`globalThis.Array` is indeed the default intrinsic reference. Code may break
446446
under this flag.
447447

448-
To allow polyfills to be added, `--require` runs before freezing intrinsics.
448+
To allow polyfills to be added,
449+
[`--require`][] and [`--import`][] both run before freezing intrinsics.
449450

450451
### `--force-node-api-uncaught-exceptions-policy`
451452

@@ -594,6 +595,18 @@ added: v0.11.15
594595

595596
Specify ICU data load path. (Overrides `NODE_ICU_DATA`.)
596597

598+
### `--import=module`
599+
600+
<!-- YAML
601+
added: REPLACEME
602+
-->
603+
604+
Preload the specified module at startup.
605+
606+
Follows [ECMAScript module][] resolution rules.
607+
Use [`--require`][] to load a [CommonJS module][].
608+
Modules preloaded with `--require` will run before modules preloaded with `--import`.
609+
597610
### `--input-type=type`
598611

599612
<!-- YAML
@@ -1527,8 +1540,9 @@ Preload the specified module at startup.
15271540
Follows `require()`'s module resolution
15281541
rules. `module` may be either a path to a file, or a node module name.
15291542

1530-
Only CommonJS modules are supported. Attempting to preload a
1531-
ES6 Module using `--require` will fail with an error.
1543+
Only CommonJS modules are supported.
1544+
Use [`--import`][] to preload an [ECMAScript module][].
1545+
Modules preloaded with `--require` will run before modules preloaded with `--import`.
15321546

15331547
### `-v`, `--version`
15341548

@@ -1682,6 +1696,7 @@ Node.js options that are allowed are:
16821696
* `--heapsnapshot-signal`
16831697
* `--http-parser`
16841698
* `--icu-data-dir`
1699+
* `--import`
16851700
* `--input-type`
16861701
* `--insecure-http-parser`
16871702
* `--inspect-brk`
@@ -2086,7 +2101,9 @@ done
20862101
[#42511]: https://github.com/nodejs/node/issues/42511
20872102
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
20882103
[CommonJS]: modules.md
2104+
[CommonJS module]: modules.md
20892105
[CustomEvent Web API]: https://dom.spec.whatwg.org/#customevent
2106+
[ECMAScript module]: esm.md#modules-ecmascript-modules
20902107
[ECMAScript module loader]: esm.md#loaders
20912108
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
20922109
[Modules loaders]: packages.md#modules-loaders
@@ -2103,8 +2120,10 @@ done
21032120
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
21042121
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
21052122
[`--heap-prof-dir`]: #--heap-prof-dir
2123+
[`--import`]: #--importmodule
21062124
[`--openssl-config`]: #--openssl-configfile
21072125
[`--redirect-warnings`]: #--redirect-warningsfile
2126+
[`--require`]: #-r---require-module
21082127
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
21092128
[`Buffer`]: buffer.md#class-buffer
21102129
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man1.1.0/man3/CRYPTO_secure_malloc_init.html

lib/internal/main/check_syntax.js

+21-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// If user passed `-c` or `--check` arguments to Node, check its syntax
44
// instead of actually running the file.
55

6+
const { getOptionValue } = require('internal/options');
67
const {
78
prepareMainThreadExecution
89
} = require('internal/bootstrap/pre_execution');
@@ -36,18 +37,29 @@ if (process.argv[1] && process.argv[1] !== '-') {
3637

3738
markBootstrapComplete();
3839

39-
checkSyntax(source, filename);
40+
loadESMIfNeeded(() => checkSyntax(source, filename));
4041
} else {
4142
markBootstrapComplete();
4243

43-
readStdin((code) => {
44+
loadESMIfNeeded(() => readStdin((code) => {
4445
checkSyntax(code, '[stdin]');
45-
});
46+
}));
4647
}
4748

48-
async function checkSyntax(source, filename) {
49+
function loadESMIfNeeded(cb) {
4950
const { getOptionValue } = require('internal/options');
50-
let isModule = false;
51+
const hasModulePreImport = getOptionValue('--import').length > 0;
52+
53+
if (hasModulePreImport) {
54+
const { loadESM } = require('internal/process/esm_loader');
55+
loadESM(cb);
56+
return;
57+
}
58+
cb();
59+
}
60+
61+
async function checkSyntax(source, filename) {
62+
let isModule = true;
5163
if (filename === '[stdin]' || filename === '[eval]') {
5264
isModule = getOptionValue('--input-type') === 'module';
5365
} else {
@@ -57,11 +69,14 @@ async function checkSyntax(source, filename) {
5769
const format = await defaultGetFormat(url);
5870
isModule = format === 'module';
5971
}
72+
6073
if (isModule) {
6174
const { ModuleWrap } = internalBinding('module_wrap');
6275
new ModuleWrap(filename, undefined, source, 0, 0);
6376
return;
6477
}
6578

66-
wrapSafe(filename, source);
79+
const { loadESM } = require('internal/process/esm_loader');
80+
const { handleMainPromise } = require('internal/modules/run_main');
81+
handleMainPromise(loadESM((loader) => wrapSafe(filename, source)));
6782
}

lib/internal/main/eval_stdin.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ readStdin((code) => {
2323
process._eval = code;
2424

2525
const print = getOptionValue('--print');
26+
const loadESM = getOptionValue('--import').length > 0;
2627
if (getOptionValue('--input-type') === 'module')
2728
evalModule(code, print);
2829
else
2930
evalScript('[stdin]',
3031
code,
3132
getOptionValue('--inspect-brk'),
32-
print);
33+
print,
34+
loadESM);
3335
});

lib/internal/main/eval_string.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ markBootstrapComplete();
2121

2222
const source = getOptionValue('--eval');
2323
const print = getOptionValue('--print');
24+
const loadESM = getOptionValue('--import').length > 0;
2425
if (getOptionValue('--input-type') === 'module')
2526
evalModule(source, print);
2627
else
2728
evalScript('[eval]',
2829
source,
2930
getOptionValue('--inspect-brk'),
30-
print);
31+
print,
32+
loadESM);

lib/internal/modules/run_main.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ function shouldUseESMLoader(mainPath) {
3333
* (or an empty list when none have been registered).
3434
*/
3535
const userLoaders = getOptionValue('--experimental-loader');
36-
if (userLoaders.length > 0)
36+
/**
37+
* @type {string[]} userImports A list of preloaded modules registered by the user
38+
* (or an empty list when none have been registered).
39+
*/
40+
const userImports = getOptionValue('--import');
41+
if (userLoaders.length > 0 || userImports.length > 0)
3742
return true;
3843
const esModuleSpecifierResolution =
3944
getOptionValue('--experimental-specifier-resolution');

lib/internal/process/esm_loader.js

+20-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayIsArray,
45
ObjectCreate,
56
} = primordials;
67

@@ -56,8 +57,23 @@ async function initializeLoader() {
5657

5758
const { getOptionValue } = require('internal/options');
5859
const customLoaders = getOptionValue('--experimental-loader');
60+
const preloadModules = getOptionValue('--import');
61+
const loaders = await loadModulesInIsolation(customLoaders);
5962

60-
if (customLoaders.length === 0) return;
63+
// Hooks must then be added to external/public loader
64+
// (so they're triggered in userland)
65+
esmLoader.addCustomLoaders(loaders);
66+
67+
// Preload after loaders are added so they can be used
68+
if (preloadModules?.length) {
69+
await loadModulesInIsolation(preloadModules, loaders);
70+
}
71+
72+
isESMInitialized = true;
73+
}
74+
75+
function loadModulesInIsolation(specifiers, loaders = []) {
76+
if (!ArrayIsArray(specifiers) || specifiers.length === 0) { return; }
6177

6278
let cwd;
6379
try {
@@ -70,19 +86,14 @@ async function initializeLoader() {
7086
// between internal Node.js and userland. For example, a module with internal
7187
// state (such as a counter) should be independent.
7288
const internalEsmLoader = new ESMLoader();
89+
internalEsmLoader.addCustomLoaders(loaders);
7390

7491
// Importation must be handled by internal loader to avoid poluting userland
75-
const keyedExportsList = await internalEsmLoader.import(
76-
customLoaders,
92+
return internalEsmLoader.import(
93+
specifiers,
7794
pathToFileURL(cwd).href,
7895
ObjectCreate(null),
7996
);
80-
81-
// Hooks must then be added to external/public loader
82-
// (so they're triggered in userland)
83-
await esmLoader.addCustomLoaders(keyedExportsList);
84-
85-
isESMInitialized = true;
8697
}
8798

8899
exports.loadESM = async function loadESM(callback) {

lib/internal/process/execution.js

+35-26
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function evalModule(source, print) {
4848
return handleMainPromise(loadESM((loader) => loader.eval(source)));
4949
}
5050

51-
function evalScript(name, body, breakFirstLine, print) {
51+
function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
5252
const CJSModule = require('internal/modules/cjs/loader').Module;
5353
const { kVmBreakFirstLineSymbol } = require('internal/util');
5454
const { pathToFileURL } = require('url');
@@ -60,35 +60,44 @@ function evalScript(name, body, breakFirstLine, print) {
6060
module.filename = path.join(cwd, name);
6161
module.paths = CJSModule._nodeModulePaths(cwd);
6262

63+
const { handleMainPromise } = require('internal/modules/run_main');
6364
const asyncESM = require('internal/process/esm_loader');
6465
const baseUrl = pathToFileURL(module.filename).href;
66+
const { loadESM } = asyncESM;
67+
68+
const runScript = () => {
69+
// Create wrapper for cache entry
70+
const script = `
71+
globalThis.module = module;
72+
globalThis.exports = exports;
73+
globalThis.__dirname = __dirname;
74+
globalThis.require = require;
75+
return (main) => main();
76+
`;
77+
globalThis.__filename = name;
78+
const result = module._compile(script, `${name}-wrapper`)(() =>
79+
require('vm').runInThisContext(body, {
80+
filename: name,
81+
displayErrors: true,
82+
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
83+
importModuleDynamically(specifier, _, importAssertions) {
84+
const loader = asyncESM.esmLoader;
85+
return loader.import(specifier, baseUrl, importAssertions);
86+
}
87+
}));
88+
if (print) {
89+
const { log } = require('internal/console/global');
90+
log(result);
91+
}
6592

66-
// Create wrapper for cache entry
67-
const script = `
68-
globalThis.module = module;
69-
globalThis.exports = exports;
70-
globalThis.__dirname = __dirname;
71-
globalThis.require = require;
72-
return (main) => main();
73-
`;
74-
globalThis.__filename = name;
75-
const result = module._compile(script, `${name}-wrapper`)(() =>
76-
require('vm').runInThisContext(body, {
77-
filename: name,
78-
displayErrors: true,
79-
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
80-
importModuleDynamically(specifier, _, importAssertions) {
81-
const loader = asyncESM.esmLoader;
82-
return loader.import(specifier, baseUrl, importAssertions);
83-
}
84-
}));
85-
if (print) {
86-
const { log } = require('internal/console/global');
87-
log(result);
88-
}
93+
if (origModule !== undefined)
94+
globalThis.module = origModule;
95+
};
8996

90-
if (origModule !== undefined)
91-
globalThis.module = origModule;
97+
if (shouldLoadESM) {
98+
return handleMainPromise(loadESM(runScript));
99+
}
100+
return runScript();
92101
}
93102

94103
const exceptionHandlerState = {

src/node_options.cc

+6-2
Original file line numberDiff line numberDiff line change
@@ -608,10 +608,14 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
608608
AddAlias("-pe", { "--print", "--eval" });
609609
AddAlias("-p", "--print");
610610
AddOption("--require",
611-
"module to preload (option can be repeated)",
612-
&EnvironmentOptions::preload_modules,
611+
"CommonJS module to preload (option can be repeated)",
612+
&EnvironmentOptions::preload_cjs_modules,
613613
kAllowedInEnvironment);
614614
AddAlias("-r", "--require");
615+
AddOption("--import",
616+
"ES module to preload (option can be repeated)",
617+
&EnvironmentOptions::preload_esm_modules,
618+
kAllowedInEnvironment);
615619
AddOption("--interactive",
616620
"always enter the REPL even if stdin does not appear "
617621
"to be a terminal",

src/node_options.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ class EnvironmentOptions : public Options {
188188
bool tls_max_v1_3 = false;
189189
std::string tls_keylog;
190190

191-
std::vector<std::string> preload_modules;
191+
std::vector<std::string> preload_cjs_modules;
192+
193+
std::vector<std::string> preload_esm_modules;
192194

193195
std::vector<std::string> user_argv;
194196

0 commit comments

Comments
 (0)