From 126b6869f9d9ee1eb14261969af3023f61b22683 Mon Sep 17 00:00:00 2001 From: RedYetiDev <38299977+RedYetiDev@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:26:17 -0400 Subject: [PATCH 1/2] module: support loading entrypoint as url Co-Authored-By: Antoine du Hamel --- doc/api/cli.md | 24 +++++ doc/node.1 | 3 + lib/internal/main/run_main_module.js | 8 +- lib/internal/modules/run_main.js | 9 +- src/node_options.cc | 4 + src/node_options.h | 1 + test/es-module/test-esm-loader-entry-url.mjs | 97 +++++++++++++++++++ test/fixtures/es-modules/print-entrypoint.mjs | 1 + 8 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 test/es-module/test-esm-loader-entry-url.mjs create mode 100644 test/fixtures/es-modules/print-entrypoint.mjs diff --git a/doc/api/cli.md b/doc/api/cli.md index 4028e0d374b179..b613075ec5dab1 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -805,6 +805,28 @@ when `Error.stack` is accessed. If you access `Error.stack` frequently in your application, take into account the performance implications of `--enable-source-maps`. +### `--entry-url` + + + +> Stability: 1 - Experimental + +When present, Node.js will interpret the entry point as a URL, rather than a +path. + +Follows [ECMAScript module][] resolution rules. + +Any query parameter or hash in the URL will be accessible via [`import.meta.url`][]. + +```bash +node --entry-url 'file:///path/to/file.js?queryparams=work#and-hashes-too' +node --entry-url --experimental-strip-types 'file.ts?query#hash' +node --entry-url 'data:text/javascript,console.log("Hello")' +``` + ### `--env-file=config` > Stability: 1.1 - Active development @@ -2981,6 +3003,7 @@ one is included in the list below. * `--enable-fips` * `--enable-network-family-autoselection` * `--enable-source-maps` +* `--entry-url` * `--experimental-abortcontroller` * `--experimental-async-context-frame` * `--experimental-default-type` @@ -3571,6 +3594,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [`dns.lookup()`]: dns.md#dnslookuphostname-options-callback [`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder [`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options +[`import.meta.url`]: esm.md#importmetaurl [`import` specifier]: esm.md#import-specifiers [`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: net.md#netgetdefaultautoselectfamilyattempttimeout [`node:sqlite`]: sqlite.md diff --git a/doc/node.1 b/doc/node.1 index 2a8cd86433d7ed..3d92fcd7858b98 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -160,6 +160,9 @@ Requires Node.js to be built with .It Fl -enable-source-maps Enable Source Map V3 support for stack traces. . +.It Fl -entry-url +Interpret the entry point as a URL. +. .It Fl -experimental-default-type Ns = Ns Ar type Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified; .js or extensionless files with no sibling or parent package.json; diff --git a/lib/internal/main/run_main_module.js b/lib/internal/main/run_main_module.js index ccab1595685ada..9b79410bdad88d 100644 --- a/lib/internal/main/run_main_module.js +++ b/lib/internal/main/run_main_module.js @@ -9,14 +9,20 @@ const { markBootstrapComplete, } = require('internal/process/pre_execution'); const { getOptionValue } = require('internal/options'); +const { emitExperimentalWarning } = require('internal/util'); -const mainEntry = prepareMainThreadExecution(true); +const isEntryURL = getOptionValue('--entry-url'); +const mainEntry = prepareMainThreadExecution(!isEntryURL); markBootstrapComplete(); // Necessary to reset RegExp statics before user code runs. RegExpPrototypeExec(/^/, ''); +if (isEntryURL) { + emitExperimentalWarning('--entry-url'); +} + if (getOptionValue('--experimental-default-type') === 'module') { require('internal/modules/run_main').executeUserEntryPoint(mainEntry); } else { diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 1e1a1ea46fc6c1..2b7842f551179b 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -8,7 +8,7 @@ const { const { getNearestParentPackageJSONType } = internalBinding('modules'); const { getOptionValue } = require('internal/options'); const path = require('path'); -const { pathToFileURL } = require('internal/url'); +const { pathToFileURL, URL } = require('internal/url'); const { kEmptyObject, getCWDURL } = require('internal/util'); const { hasUncaughtExceptionCaptureCallback, @@ -63,7 +63,7 @@ function resolveMainPath(main) { * @param {string} mainPath - Absolute path to the main entry point */ function shouldUseESMLoader(mainPath) { - if (getOptionValue('--experimental-default-type') === 'module') { return true; } + if (getOptionValue('--entry-url') || getOptionValue('--experimental-default-type') === 'module') { return true; } /** * @type {string[]} userLoaders A list of custom loaders registered by the user @@ -157,7 +157,6 @@ function runEntryPointWithESMLoader(callback) { function executeUserEntryPoint(main = process.argv[1]) { const resolvedMain = resolveMainPath(main); const useESMLoader = shouldUseESMLoader(resolvedMain); - let mainURL; // Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first // try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM. if (!useESMLoader) { @@ -166,9 +165,7 @@ function executeUserEntryPoint(main = process.argv[1]) { wrapModuleLoad(main, null, true); } else { const mainPath = resolvedMain || main; - if (mainURL === undefined) { - mainURL = pathToFileURL(mainPath).href; - } + const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath); runEntryPointWithESMLoader((cascadedLoader) => { // Note that if the graph contains unsettled TLA, this may never resolve diff --git a/src/node_options.cc b/src/node_options.cc index cc290661ec3b76..3a8dde1cafc877 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -406,6 +406,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "Source Map V3 support for stack traces", &EnvironmentOptions::enable_source_maps, kAllowedInEnvvar); + AddOption("--entry-url", + "Treat the entrypoint as a URL", + &EnvironmentOptions::entry_is_url, + kAllowedInEnvvar); AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar); AddOption("--experimental-eventsource", "experimental EventSource API", diff --git a/src/node_options.h b/src/node_options.h index 5f186ae86f0bcc..d8520e806fd7a1 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -132,6 +132,7 @@ class EnvironmentOptions : public Options { bool experimental_import_meta_resolve = false; std::string input_type; // Value of --input-type std::string type; // Value of --experimental-default-type + bool entry_is_url = false; bool experimental_permission = false; std::vector allow_fs_read; std::vector allow_fs_write; diff --git a/test/es-module/test-esm-loader-entry-url.mjs b/test/es-module/test-esm-loader-entry-url.mjs new file mode 100644 index 00000000000000..370871113239f1 --- /dev/null +++ b/test/es-module/test-esm-loader-entry-url.mjs @@ -0,0 +1,97 @@ +import { spawnPromisified } from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert'; +import { execPath } from 'node:process'; +import { describe, it } from 'node:test'; + +// Helper function to assert the spawned process +async function assertSpawnedProcess(args, options = {}, expected = {}) { + const { code, signal, stderr, stdout } = await spawnPromisified(execPath, args, options); + + if (expected.stderr) { + assert.match(stderr, expected.stderr); + } + + if (expected.stdout) { + assert.match(stdout, expected.stdout); + } + + assert.strictEqual(code, expected.code ?? 0); + assert.strictEqual(signal, expected.signal ?? null); +} + +// Common expectation for experimental feature warning in stderr +const experimentalFeatureWarning = { stderr: /--entry-url is an experimental feature/ }; + +describe('--entry-url', { concurrency: true }, () => { + it('should reject loading a path that contains %', async () => { + await assertSpawnedProcess( + ['--entry-url', './test-esm-double-encoding-native%20.mjs'], + { cwd: fixtures.fileURL('es-modules') }, + { + code: 1, + stderr: /ERR_MODULE_NOT_FOUND/, + } + ); + }); + + it('should support loading properly encoded Unix path', async () => { + await assertSpawnedProcess( + ['--entry-url', fixtures.fileURL('es-modules/test-esm-double-encoding-native%20.mjs').pathname], + {}, + experimentalFeatureWarning + ); + }); + + it('should support loading absolute URLs', async () => { + await assertSpawnedProcess( + ['--entry-url', fixtures.fileURL('printA.js')], + {}, + { + ...experimentalFeatureWarning, + stdout: /^A\r?\n$/, + } + ); + }); + + it('should support loading relative URLs', async () => { + await assertSpawnedProcess( + ['--entry-url', 'es-modules/print-entrypoint.mjs?key=value#hash'], + { cwd: fixtures.fileURL('./') }, + { + ...experimentalFeatureWarning, + stdout: /print-entrypoint\.mjs\?key=value#hash\r?\n$/, + } + ); + }); + + it('should support loading `data:` URLs', async () => { + await assertSpawnedProcess( + ['--entry-url', 'data:text/javascript,console.log(import.meta.url)'], + {}, + { + ...experimentalFeatureWarning, + stdout: /^data:text\/javascript,console\.log\(import\.meta\.url\)\r?\n$/, + } + ); + }); + + it('should support loading TypeScript URLs', async () => { + const typescriptUrls = [ + 'typescript/cts/test-require-ts-file.cts', + 'typescript/mts/test-import-ts-file.mts', + ]; + + for (const url of typescriptUrls) { + await assertSpawnedProcess( + ['--entry-url', '--experimental-strip-types', fixtures.fileURL(url)], + {}, + { + ...experimentalFeatureWarning, + stdout: /Hello, TypeScript!/, + } + ); + } + }); + +}); diff --git a/test/fixtures/es-modules/print-entrypoint.mjs b/test/fixtures/es-modules/print-entrypoint.mjs new file mode 100644 index 00000000000000..d9536a69b3f87d --- /dev/null +++ b/test/fixtures/es-modules/print-entrypoint.mjs @@ -0,0 +1 @@ +console.log(import.meta.url); From 0226b3041e16c566d1ef1ccacfe791bdb3395620 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Thu, 26 Sep 2024 12:41:46 -0400 Subject: [PATCH 2/2] fixup! Don't resolve `main` when `--entry-rul` --- lib/internal/modules/run_main.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 2b7842f551179b..2ce5711aaa63f6 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -63,7 +63,7 @@ function resolveMainPath(main) { * @param {string} mainPath - Absolute path to the main entry point */ function shouldUseESMLoader(mainPath) { - if (getOptionValue('--entry-url') || getOptionValue('--experimental-default-type') === 'module') { return true; } + if (getOptionValue('--experimental-default-type') === 'module') { return true; } /** * @type {string[]} userLoaders A list of custom loaders registered by the user @@ -155,8 +155,14 @@ function runEntryPointWithESMLoader(callback) { * @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js` */ function executeUserEntryPoint(main = process.argv[1]) { - const resolvedMain = resolveMainPath(main); - const useESMLoader = shouldUseESMLoader(resolvedMain); + let useESMLoader; + let resolvedMain; + if (getOptionValue('--entry-url')) { + useESMLoader = true; + } else { + resolvedMain = resolveMainPath(main); + useESMLoader = shouldUseESMLoader(resolvedMain); + } // Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first // try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM. if (!useESMLoader) {