Skip to content

Commit

Permalink
esm: add --import flag
Browse files Browse the repository at this point in the history
PR-URL: #43942
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 committed Jul 31, 2022
1 parent 659dc12 commit af0921d
Show file tree
Hide file tree
Showing 15 changed files with 346 additions and 64 deletions.
25 changes: 22 additions & 3 deletions doc/api/cli.md
Expand Up @@ -445,7 +445,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 @@ -594,6 +595,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 @@ -1527,8 +1540,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 @@ -1682,6 +1696,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 @@ -2086,7 +2101,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 @@ -2103,8 +2120,10 @@ done
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
[`--heap-prof-dir`]: #--heap-prof-dir
[`--import`]: #--importmodule
[`--openssl-config`]: #--openssl-configfile
[`--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/man1.1.0/man3/CRYPTO_secure_malloc_init.html
Expand Down
27 changes: 21 additions & 6 deletions lib/internal/main/check_syntax.js
Expand Up @@ -3,6 +3,7 @@
// If user passed `-c` or `--check` arguments to Node, check its syntax
// instead of actually running the file.

const { getOptionValue } = require('internal/options');
const {
prepareMainThreadExecution
} = require('internal/bootstrap/pre_execution');
Expand Down Expand Up @@ -36,18 +37,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 @@ -57,11 +69,14 @@ async function checkSyntax(source, filename) {
const format = await defaultGetFormat(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
Expand Up @@ -23,11 +23,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
Expand Up @@ -21,10 +21,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
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
@@ -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
61 changes: 35 additions & 26 deletions lib/internal/process/execution.js
Expand Up @@ -48,7 +48,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 @@ -60,35 +60,44 @@ 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;
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;
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
Expand Up @@ -608,10 +608,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,
kAllowedInEnvironment);
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
Expand Up @@ -188,7 +188,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

0 comments on commit af0921d

Please sign in to comment.