From 75b9174ced97a455eb4c101c257bfbafc868d76d Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 21 Jan 2024 18:19:49 -0800 Subject: [PATCH 1/4] esm: implement import.meta.command --- lib/internal/main/eval_stdin.js | 2 +- lib/internal/main/eval_string.js | 2 +- lib/internal/main/run_main_module.js | 5 ++- lib/internal/main/worker_thread.js | 2 +- .../modules/esm/initialize_import_meta.js | 6 +++- lib/internal/modules/esm/loader.js | 25 ++++++++++++--- lib/internal/modules/run_main.js | 32 +++++++++++++++---- lib/internal/process/esm_loader.js | 7 ++++ lib/internal/process/execution.js | 12 +++++-- test/es-module/test-esm-command.mjs | 31 ++++++++++++++++++ test/es-module/test-esm-import-meta.mjs | 3 ++ test/fixtures/es-modules/command-main.mjs | 3 ++ 12 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 test/es-module/test-esm-command.mjs create mode 100755 test/fixtures/es-modules/command-main.mjs diff --git a/lib/internal/main/eval_stdin.js b/lib/internal/main/eval_stdin.js index d71751e781b9b5..058d53c276c7bb 100644 --- a/lib/internal/main/eval_stdin.js +++ b/lib/internal/main/eval_stdin.js @@ -27,7 +27,7 @@ readStdin((code) => { const loadESM = getOptionValue('--import').length > 0; if (getOptionValue('--input-type') === 'module' || (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { - evalModule(code, print); + evalModule(code, print, true); } else { evalScript('[stdin]', code, diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index 908532b0b1865a..a51b360ccde487 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -27,7 +27,7 @@ const print = getOptionValue('--print'); const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; if (getOptionValue('--input-type') === 'module' || (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { - evalModule(source, print); + evalModule(source, print, true); } else { // For backward compatibility, we want the identifier crypto to be the // `node:crypto` module rather than WebCrypto. diff --git a/lib/internal/main/run_main_module.js b/lib/internal/main/run_main_module.js index 5d09203b8c27ee..5418d889f9364a 100644 --- a/lib/internal/main/run_main_module.js +++ b/lib/internal/main/run_main_module.js @@ -15,8 +15,11 @@ markBootstrapComplete(); // Necessary to reset RegExp statics before user code runs. RegExpPrototypeExec(/^/, ''); +const runMain = require('internal/modules/run_main'); +runMain.userEntryPointIsCommandMain(); + if (getOptionValue('--experimental-default-type') === 'module') { - require('internal/modules/run_main').executeUserEntryPoint(mainEntry); + runMain.executeUserEntryPoint(mainEntry); } else { /** * To support legacy monkey-patching of `Module.runMain`, we call `runMain` here to have the CommonJS loader begin diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index c14091ffe09ca7..ae0e3a72613f19 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -171,7 +171,7 @@ port.on('message', (message) => { case 'module': { const { evalModule } = require('internal/process/execution'); - PromisePrototypeThen(evalModule(filename), undefined, (e) => { + PromisePrototypeThen(evalModule(filename, false, false), undefined, (e) => { workerOnGlobalUncaughtException(e, true); }); break; diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index 818c99479cd068..e8f08c1feca944 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -47,13 +47,17 @@ function createImportMetaResolve(defaultParentURL, loader, allowParentURL) { * Create the `import.meta` object for a module. * @param {object} meta * @param {{url: string}} context - * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader + * @param {ReturnType} loader Reference to the current module loader * @returns {{dirname?: string, filename?: string, url: string, resolve?: Function}} */ function initializeImportMeta(meta, context, loader) { const { url } = context; // Alphabetical + if (loader && loader.resolvedCommandMain === url) { + meta.command = true; + } + if (StringPrototypeStartsWith(url, 'file:') === true) { // These only make sense for locally loaded modules, // i.e. network modules are not supported. diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index c0e3cdb36e1c02..af1274c673d3f2 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -14,7 +14,7 @@ const { encodeURIComponent, hardenRegExp, } = primordials; - +const assert = require('internal/assert'); const { ERR_REQUIRE_ESM, ERR_UNKNOWN_MODULE_FORMAT, @@ -112,6 +112,12 @@ class ModuleLoader { */ allowImportMetaResolve; + /** + * @type {string | undefined} Resolved command main when the process + * top-level entry point. + */ + resolvedCommandMain; + /** * Customizations to pass requests to. * @@ -187,10 +193,19 @@ class ModuleLoader { } } - async eval( - source, - url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href, - ) { + /** + * @param {string} specifier + */ + setCommandMain(specifier) { + assert(this.resolvedCommandMain === undefined, 'only one command main permitted'); + this.resolvedCommandMain = specifier; + } + + generateEvalUrl() { + return pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href; + } + + async eval(source, url = this.nextEvalUrl()) { const evalInstance = (url) => { const { ModuleWrap } = internalBinding('module_wrap'); const { registerModule } = require('internal/modules/esm/utils'); diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 23268637e4fd58..2b9804a952412a 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -95,14 +95,17 @@ function shouldUseESMLoader(mainPath) { /** * Run the main entry point through the ESM Loader. - * @param {string} mainPath - Absolute path for the main entry point + * @param {URL} mainPath - Absolute path for the main entry point + * @param {bool} isCommandMain - whether the main is also the process command main entry point */ -function runMainESM(mainPath) { +function runMainESM(mainPath, isCommandMain) { const { loadESM } = require('internal/process/esm_loader'); const { pathToFileURL } = require('internal/url'); const main = pathToFileURL(mainPath).href; - handleMainPromise(loadESM((esmLoader) => { + if (isCommandMain) { + esmLoader.setCommandMain(main); + } return esmLoader.import(main, undefined, { __proto__: null }); })); } @@ -131,19 +134,36 @@ async function handleMainPromise(promise) { * Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`. * @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js` */ +let isCommandMain = false; function executeUserEntryPoint(main = process.argv[1]) { - const resolvedMain = resolveMainPath(main); - const useESMLoader = shouldUseESMLoader(resolvedMain); + const mainPath = resolveMainPath(main); + const useESMLoader = shouldUseESMLoader(mainPath); if (useESMLoader) { - runMainESM(resolvedMain || main); + runMainESM(mainPath || main, isCommandMain); } else { // Module._load is the monkey-patchable CJS module loader. const { Module } = require('internal/modules/cjs/loader'); Module._load(main, null, true); } + isCommandMain = false; +} + +/* + * This is a special function that can be called before executeUserEntryPoint + * to note that the coming entry point call is a command main. + * + * This should really just be implemented as a parameter, but executeUserEntryPoint is + * exposed publicly as `runMain` which has backwards-compatibility requirements, hence + * this approach. + * + * Since this ONLY applies to the ESM loader, future simplifications should remain possible here. + */ +function userEntryPointIsCommandMain() { + isCommandMain = true; } module.exports = { executeUserEntryPoint, + userEntryPointIsCommandMain, handleMainPromise, }; diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 0865d7ceef66b7..aa447fc5c18231 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -7,12 +7,19 @@ const { } = require('internal/process/execution'); const { kEmptyObject, getCWDURL } = require('internal/util'); +/** @typedef {ReturnType} ModuleLoader */ + +/** @type {ModuleLoader} */ let esmLoader; module.exports = { get esmLoader() { return esmLoader ??= createModuleLoader(); }, + /** + * @param {(esmLoader: ModuleLoader) => Promise} callback + * @returns {Promise} + */ async loadESM(callback) { esmLoader ??= createModuleLoader(); try { diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 5de5edfb2d5524..a2e33f403f734e 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -46,14 +46,20 @@ function tryGetCwd() { } } -function evalModule(source, print) { +function evalModule(source, print, isCommandMain) { if (print) { throw new ERR_EVAL_ESM_CANNOT_PRINT(); } const { loadESM } = require('internal/process/esm_loader'); const { handleMainPromise } = require('internal/modules/run_main'); RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs. - return handleMainPromise(loadESM((loader) => loader.eval(source))); + return handleMainPromise(loadESM((loader) => { + const evalUrl = loader.generateEvalUrl(); + if (isCommandMain) { + loader.setCommandMain(evalUrl); + } + return loader.eval(source, evalUrl); + })); } function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { @@ -75,7 +81,7 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { if (getOptionValue('--experimental-detect-module') && getOptionValue('--input-type') === '' && getOptionValue('--experimental-default-type') === '' && containsModuleSyntax(body, name)) { - return evalModule(body, print); + return evalModule(body, print, true); } const runScript = () => { diff --git a/test/es-module/test-esm-command.mjs b/test/es-module/test-esm-command.mjs new file mode 100644 index 00000000000000..e9c6a4656fbbe8 --- /dev/null +++ b/test/es-module/test-esm-command.mjs @@ -0,0 +1,31 @@ +import '../common/index.mjs'; +import { spawnSync } from 'node:child_process'; +import { strictEqual } from 'node:assert'; +import { fileURLToPath } from 'node:url'; + +{ + const child = spawnSync(process.execPath, [ + '--input-type', + 'module', + '-e', + 'console.log(import.meta.command)', + ]); + + if (child.status !== 0) { + console.error(child.stderr.toString()); + } + + strictEqual(child.stdout.toString().trim(), 'true'); +} + +{ + const child = spawnSync(process.execPath, [ + fileURLToPath(new URL('../fixtures/es-modules/command-main.mjs', import.meta.url)), + ]); + + if (child.status !== 0) { + console.error(child.stderr?.toString() ?? child.error); + } + + strictEqual(child.stdout.toString().trim(), 'ok'); +} diff --git a/test/es-module/test-esm-import-meta.mjs b/test/es-module/test-esm-import-meta.mjs index 50d16a3438a851..b31f6fa2a1e8e6 100644 --- a/test/es-module/test-esm-import-meta.mjs +++ b/test/es-module/test-esm-import-meta.mjs @@ -32,3 +32,6 @@ assert.match(import.meta.filename, fileReg); // Verify that `data:` imports do not behave like `file:` imports. import dataDirname from 'data:text/javascript,export default "dirname" in import.meta'; assert.strictEqual(dataDirname, false); + +// Verify that command is never set (property only exists and is truthy for command main) +assert(!('command' in import.meta)); diff --git a/test/fixtures/es-modules/command-main.mjs b/test/fixtures/es-modules/command-main.mjs new file mode 100755 index 00000000000000..8179ac7d7e4379 --- /dev/null +++ b/test/fixtures/es-modules/command-main.mjs @@ -0,0 +1,3 @@ +if (import.meta.command) { + console.log('ok'); +} From c5ec2aaebee3cc83408a6d0675d631e4d2549885 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 21 Jan 2024 21:07:11 -0800 Subject: [PATCH 2/4] pr feedback --- lib/internal/modules/run_main.js | 12 +++++++----- test/es-module/test-esm-command.mjs | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 2b9804a952412a..3cb7bc90f665b2 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -95,7 +95,7 @@ function shouldUseESMLoader(mainPath) { /** * Run the main entry point through the ESM Loader. - * @param {URL} mainPath - Absolute path for the main entry point + * @param {string} mainPath - Absolute path for the main entry point * @param {bool} isCommandMain - whether the main is also the process command main entry point */ function runMainESM(mainPath, isCommandMain) { @@ -134,7 +134,6 @@ async function handleMainPromise(promise) { * Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`. * @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js` */ -let isCommandMain = false; function executeUserEntryPoint(main = process.argv[1]) { const mainPath = resolveMainPath(main); const useESMLoader = shouldUseESMLoader(mainPath); @@ -145,7 +144,6 @@ function executeUserEntryPoint(main = process.argv[1]) { const { Module } = require('internal/modules/cjs/loader'); Module._load(main, null, true); } - isCommandMain = false; } /* @@ -153,8 +151,8 @@ function executeUserEntryPoint(main = process.argv[1]) { * to note that the coming entry point call is a command main. * * This should really just be implemented as a parameter, but executeUserEntryPoint is - * exposed publicly as `runMain` which has backwards-compatibility requirements, hence - * this approach. + * exposed publicly as `runMain`, and we don't want to expose this functionality to userland + * as setting the command main is an internal-only capability. * * Since this ONLY applies to the ESM loader, future simplifications should remain possible here. */ @@ -162,6 +160,10 @@ function userEntryPointIsCommandMain() { isCommandMain = true; } +function executeUserEntryPointCommand () { + +} + module.exports = { executeUserEntryPoint, userEntryPointIsCommandMain, diff --git a/test/es-module/test-esm-command.mjs b/test/es-module/test-esm-command.mjs index e9c6a4656fbbe8..12ff654caf37f7 100644 --- a/test/es-module/test-esm-command.mjs +++ b/test/es-module/test-esm-command.mjs @@ -1,10 +1,10 @@ -import '../common/index.mjs'; +import { spawnPromisified } from '../common/index.mjs'; import { spawnSync } from 'node:child_process'; import { strictEqual } from 'node:assert'; import { fileURLToPath } from 'node:url'; { - const child = spawnSync(process.execPath, [ + const { code, stderr, stdout } = await spawnPromisified(process.execPath, [ '--input-type', 'module', '-e', @@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url'; } { - const child = spawnSync(process.execPath, [ + const { code, stderr, stdout } = await spawnPromisified(process.execPath, [ fileURLToPath(new URL('../fixtures/es-modules/command-main.mjs', import.meta.url)), ]); From 3241da2dcddff1a0c2de91482507447a9bd95c79 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 21 Jan 2024 21:10:27 -0800 Subject: [PATCH 3/4] fixup test --- test/es-module/test-esm-command.mjs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/es-module/test-esm-command.mjs b/test/es-module/test-esm-command.mjs index 12ff654caf37f7..3ba914678d4869 100644 --- a/test/es-module/test-esm-command.mjs +++ b/test/es-module/test-esm-command.mjs @@ -1,5 +1,4 @@ import { spawnPromisified } from '../common/index.mjs'; -import { spawnSync } from 'node:child_process'; import { strictEqual } from 'node:assert'; import { fileURLToPath } from 'node:url'; @@ -11,11 +10,11 @@ import { fileURLToPath } from 'node:url'; 'console.log(import.meta.command)', ]); - if (child.status !== 0) { - console.error(child.stderr.toString()); + if (code !== 0) { + console.error(stderr.toString()); } - strictEqual(child.stdout.toString().trim(), 'true'); + strictEqual(stdout.toString().trim(), 'true'); } { @@ -23,9 +22,9 @@ import { fileURLToPath } from 'node:url'; fileURLToPath(new URL('../fixtures/es-modules/command-main.mjs', import.meta.url)), ]); - if (child.status !== 0) { - console.error(child.stderr?.toString() ?? child.error); + if (code !== 0) { + console.error(stderr.toString()); } - strictEqual(child.stdout.toString().trim(), 'ok'); + strictEqual(stdout.toString().trim(), 'ok'); } From dd034fa733cad3e33ca895650ec0597794433443 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 21 Jan 2024 21:19:11 -0800 Subject: [PATCH 4/4] fixup --- lib/internal/modules/run_main.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 3cb7bc90f665b2..8dfddd424d7ac9 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -134,6 +134,7 @@ async function handleMainPromise(promise) { * Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`. * @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js` */ +let isCommandMain = false; function executeUserEntryPoint(main = process.argv[1]) { const mainPath = resolveMainPath(main); const useESMLoader = shouldUseESMLoader(mainPath); @@ -144,6 +145,7 @@ function executeUserEntryPoint(main = process.argv[1]) { const { Module } = require('internal/modules/cjs/loader'); Module._load(main, null, true); } + isCommandMain = false; } /* @@ -160,10 +162,6 @@ function userEntryPointIsCommandMain() { isCommandMain = true; } -function executeUserEntryPointCommand () { - -} - module.exports = { executeUserEntryPoint, userEntryPointIsCommandMain,