Skip to content

Commit

Permalink
esm: add --import flag
Browse files Browse the repository at this point in the history
PR-URL: #43942
Backport-PR-URL: #49539
Fixes: #40110
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
MoLow authored and ruyadorno committed Sep 12, 2023
1 parent c224e1b commit c39b7c2
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 66 deletions.
25 changes: 22 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,18 @@ added: v0.11.15

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

### `--import=module`

<!-- YAML
added: REPLACEME
-->

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 +1752,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 +1909,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 +2330,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 +2350,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
Loading

0 comments on commit c39b7c2

Please sign in to comment.