Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v18.x backport] esm: add --import flag #49539

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 24 additions & 3 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,8 @@ Only the root context is supported. There is no guarantee that
`globalThis.Array` is indeed the default intrinsic reference. Code may break
under this flag.

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

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

Expand Down Expand Up @@ -679,6 +680,20 @@ added: v0.11.15

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

### `--import=module`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Preload the specified module at startup.

Follows [ECMAScript module][] resolution rules.
Use [`--require`][] to load a [CommonJS module][].
Modules preloaded with `--require` will run before modules preloaded with `--import`.

### `--input-type=type`

<!-- YAML
Expand Down Expand Up @@ -1739,8 +1754,9 @@ Preload the specified module at startup.
Follows `require()`'s module resolution
rules. `module` may be either a path to a file, or a node module name.

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

### `-v`, `--version`

Expand Down Expand Up @@ -1895,6 +1911,7 @@ Node.js options that are allowed are:
* `--heapsnapshot-signal`
* `--http-parser`
* `--icu-data-dir`
* `--import`
* `--input-type`
* `--insecure-http-parser`
* `--inspect-brk`
Expand Down Expand Up @@ -2315,7 +2332,9 @@ done
[#42511]: https://github.com/nodejs/node/issues/42511
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
[CommonJS]: modules.md
[CommonJS module]: modules.md
[CustomEvent Web API]: https://dom.spec.whatwg.org/#customevent
[ECMAScript module]: esm.md#modules-ecmascript-modules
[ECMAScript module loader]: esm.md#loaders
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
[Modules loaders]: packages.md#modules-loaders
Expand All @@ -2333,9 +2352,11 @@ done
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
[`--heap-prof-dir`]: #--heap-prof-dir
[`--import`]: #--importmodule
[`--openssl-config`]: #--openssl-configfile
[`--preserve-symlinks`]: #--preserve-symlinks
[`--redirect-warnings`]: #--redirect-warningsfile
[`--require`]: #-r---require-module
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
[`Buffer`]: buffer.md#class-buffer
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
Expand Down
27 changes: 21 additions & 6 deletions lib/internal/main/check_syntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// instead of actually running the file.

const { URL } = require('internal/url');
const { getOptionValue } = require('internal/options');
const {
prepareMainThreadExecution,
markBootstrapComplete,
Expand Down Expand Up @@ -38,18 +39,29 @@ if (process.argv[1] && process.argv[1] !== '-') {

markBootstrapComplete();

checkSyntax(source, filename);
loadESMIfNeeded(() => checkSyntax(source, filename));
} else {
markBootstrapComplete();

readStdin((code) => {
loadESMIfNeeded(() => readStdin((code) => {
checkSyntax(code, '[stdin]');
});
}));
}

async function checkSyntax(source, filename) {
function loadESMIfNeeded(cb) {
const { getOptionValue } = require('internal/options');
let isModule = false;
const hasModulePreImport = getOptionValue('--import').length > 0;

if (hasModulePreImport) {
const { loadESM } = require('internal/process/esm_loader');
loadESM(cb);
return;
}
cb();
}

async function checkSyntax(source, filename) {
let isModule = true;
if (filename === '[stdin]' || filename === '[eval]') {
isModule = getOptionValue('--input-type') === 'module';
} else {
Expand All @@ -59,11 +71,14 @@ async function checkSyntax(source, filename) {
const format = await defaultGetFormat(new URL(url));
isModule = format === 'module';
}

if (isModule) {
const { ModuleWrap } = internalBinding('module_wrap');
new ModuleWrap(filename, undefined, source, 0, 0);
return;
}

wrapSafe(filename, source);
const { loadESM } = require('internal/process/esm_loader');
const { handleMainPromise } = require('internal/modules/run_main');
handleMainPromise(loadESM((loader) => wrapSafe(filename, source)));
}
4 changes: 3 additions & 1 deletion lib/internal/main/eval_stdin.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ readStdin((code) => {
process._eval = code;

const print = getOptionValue('--print');
const loadESM = getOptionValue('--import').length > 0;
if (getOptionValue('--input-type') === 'module')
evalModule(code, print);
else
evalScript('[stdin]',
code,
getOptionValue('--inspect-brk'),
print);
print,
loadESM);
});
4 changes: 3 additions & 1 deletion lib/internal/main/eval_string.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ markBootstrapComplete();

const source = getOptionValue('--eval');
const print = getOptionValue('--print');
const loadESM = getOptionValue('--import').length > 0;
if (getOptionValue('--input-type') === 'module')
evalModule(source, print);
else
evalScript('[eval]',
source,
getOptionValue('--inspect-brk'),
print);
print,
loadESM);
7 changes: 6 additions & 1 deletion lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ function shouldUseESMLoader(mainPath) {
* (or an empty list when none have been registered).
*/
const userLoaders = getOptionValue('--experimental-loader');
if (userLoaders.length > 0)
/**
* @type {string[]} userImports A list of preloaded modules registered by the user
* (or an empty list when none have been registered).
*/
const userImports = getOptionValue('--import');
if (userLoaders.length > 0 || userImports.length > 0)
return true;
const esModuleSpecifierResolution =
getOptionValue('--experimental-specifier-resolution');
Expand Down
29 changes: 20 additions & 9 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayIsArray,
ObjectCreate,
} = primordials;

Expand Down Expand Up @@ -56,8 +57,23 @@ async function initializeLoader() {

const { getOptionValue } = require('internal/options');
const customLoaders = getOptionValue('--experimental-loader');
const preloadModules = getOptionValue('--import');
const loaders = await loadModulesInIsolation(customLoaders);

if (customLoaders.length === 0) return;
// Hooks must then be added to external/public loader
// (so they're triggered in userland)
esmLoader.addCustomLoaders(loaders);

// Preload after loaders are added so they can be used
if (preloadModules?.length) {
await loadModulesInIsolation(preloadModules, loaders);
}

isESMInitialized = true;
}

function loadModulesInIsolation(specifiers, loaders = []) {
if (!ArrayIsArray(specifiers) || specifiers.length === 0) { return; }

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

// Importation must be handled by internal loader to avoid poluting userland
const keyedExportsList = await internalEsmLoader.import(
customLoaders,
return internalEsmLoader.import(
specifiers,
pathToFileURL(cwd).href,
ObjectCreate(null),
);

// Hooks must then be added to external/public loader
// (so they're triggered in userland)
await esmLoader.addCustomLoaders(keyedExportsList);

isESMInitialized = true;
}

exports.loadESM = async function loadESM(callback) {
Expand Down
63 changes: 36 additions & 27 deletions lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function evalModule(source, print) {
return handleMainPromise(loadESM((loader) => loader.eval(source)));
}

function evalScript(name, body, breakFirstLine, print) {
function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
const CJSModule = require('internal/modules/cjs/loader').Module;
const { kVmBreakFirstLineSymbol } = require('internal/util');
const { pathToFileURL } = require('url');
Expand All @@ -62,36 +62,45 @@ function evalScript(name, body, breakFirstLine, print) {
module.filename = path.join(cwd, name);
module.paths = CJSModule._nodeModulePaths(cwd);

const { handleMainPromise } = require('internal/modules/run_main');
const asyncESM = require('internal/process/esm_loader');
const baseUrl = pathToFileURL(module.filename).href;
const { loadESM } = asyncESM;

const runScript = () => {
// Create wrapper for cache entry
const script = `
globalThis.module = module;
globalThis.exports = exports;
globalThis.__dirname = __dirname;
globalThis.require = require;
return (main) => main();
`;
globalThis.__filename = name;
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
const result = module._compile(script, `${name}-wrapper`)(() =>
require('vm').runInThisContext(body, {
filename: name,
displayErrors: true,
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, baseUrl, importAssertions);
},
}));
if (print) {
const { log } = require('internal/console/global');
log(result);
}

// Create wrapper for cache entry
const script = `
globalThis.module = module;
globalThis.exports = exports;
globalThis.__dirname = __dirname;
globalThis.require = require;
return (main) => main();
`;
globalThis.__filename = name;
RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs.
const result = module._compile(script, `${name}-wrapper`)(() =>
require('vm').runInThisContext(body, {
filename: name,
displayErrors: true,
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, baseUrl, importAssertions);
},
}));
if (print) {
const { log } = require('internal/console/global');
log(result);
}
if (origModule !== undefined)
globalThis.module = origModule;
};

if (origModule !== undefined)
globalThis.module = origModule;
if (shouldLoadESM) {
return handleMainPromise(loadESM(runScript));
}
return runScript();
}

const exceptionHandlerState = {
Expand Down
8 changes: 6 additions & 2 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -666,10 +666,14 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
AddAlias("-pe", { "--print", "--eval" });
AddAlias("-p", "--print");
AddOption("--require",
"module to preload (option can be repeated)",
&EnvironmentOptions::preload_modules,
"CommonJS module to preload (option can be repeated)",
&EnvironmentOptions::preload_cjs_modules,
kAllowedInEnvvar);
AddAlias("-r", "--require");
AddOption("--import",
"ES module to preload (option can be repeated)",
&EnvironmentOptions::preload_esm_modules,
kAllowedInEnvironment);
AddOption("--interactive",
"always enter the REPL even if stdin does not appear "
"to be a terminal",
Expand Down
4 changes: 3 additions & 1 deletion src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ class EnvironmentOptions : public Options {
bool tls_max_v1_3 = false;
std::string tls_keylog;

std::vector<std::string> preload_modules;
std::vector<std::string> preload_cjs_modules;

std::vector<std::string> preload_esm_modules;

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

Expand Down