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

esm: add --import flag #43942

Merged
merged 3 commits into from Jul 31, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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.
MoLow marked this conversation as resolved.
Show resolved Hide resolved

### `--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][].
MoLow marked this conversation as resolved.
Show resolved Hide resolved
modules preloaded with `--require` will run before modules preloaded with `--import`
MoLow marked this conversation as resolved.
Show resolved Hide resolved

### `--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`
MoLow marked this conversation as resolved.
Show resolved Hide resolved

### `-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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you have two links to the same page here

[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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not sure about the name. It sounds like it would be the primary function here (or check a lot more than it does), but it's a helper.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any suggestions?

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 = loadModulesInIsolation(customLoaders);

if (customLoaders.length === 0) return;
// Hooks must then be added to external/public loader
// (so they're triggered in userland)
MoLow marked this conversation as resolved.
Show resolved Hide resolved
esmLoader.addCustomLoaders(loaders);

// Preload after loaders are added so they can be used
if (preloadModules?.length) {
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,
MoLow marked this conversation as resolved.
Show resolved Hide resolved
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 = () => {
MoLow marked this conversation as resolved.
Show resolved Hide resolved
// 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