From 7797444494c355c4dd59ba93c5053d869aaf103d Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 5 Aug 2023 17:19:26 +0000 Subject: [PATCH 1/8] esm: unflag import.meta.resolve --- doc/api/cli.md | 11 ++++++++++- lib/internal/modules/esm/initialize_import_meta.js | 11 +++++++---- src/node_options.cc | 2 +- test/es-module/test-esm-import-meta-resolve.mjs | 4 ---- test/es-module/test-esm-import-meta.mjs | 2 +- test/es-module/test-esm-loader-hooks.mjs | 3 --- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index cf509503dc838a..2e7d56031eb330 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -512,9 +512,18 @@ of `--enable-source-maps`. added: - v13.9.0 - v12.16.2 +changes: + - version: X.X.X + pr-url: incoming + description: synchronous import.meta.resolve made available by default, with + the flag retained for enabling the experimental second argument + as previously supported. --> -Enable experimental `import.meta.resolve()` support. +Enable experimental `import.meta.resolve()` parent URL support, which allows +passing a second `parentURL` argument for contextual resolution. + +Previously gated the entire `import.meta.resolve` feature. ### `--experimental-loader=module` diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index c19050ade018d0..1c08b5bc7bfa83 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -7,11 +7,14 @@ const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta * Generate a function to be used as import.meta.resolve for a particular module. * @param {string} defaultParentURL The default base to use for resolution * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader + * @param {bool} allowParentURL Whether to permit parentURL second argument for contextual resolution * @returns {(specifier: string, parentURL?: string) => string} Function to assign to import.meta.resolve */ -function createImportMetaResolve(defaultParentURL, loader) { - return function resolve(specifier, parentURL = defaultParentURL) { +function createImportMetaResolve(defaultParentURL, loader, allowParentURL) { + return function resolve(specifier) { let url; + const parentURL = allowParentURL ? arguments[1] ?? defaultParentURL : defaultParentURL; + try { ({ url } = loader.resolveSync(specifier, parentURL)); return url; @@ -40,8 +43,8 @@ function initializeImportMeta(meta, context, loader) { const { url } = context; // Alphabetical - if (experimentalImportMetaResolve && loader.allowImportMetaResolve) { - meta.resolve = createImportMetaResolve(url, loader); + if (!loader || loader.allowImportMetaResolve) { + meta.resolve = createImportMetaResolve(url, loader, experimentalImportMetaResolve); } meta.url = url; diff --git a/src/node_options.cc b/src/node_options.cc index c02752464c4ab5..f9dc78abffa48c 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -396,7 +396,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_wasm_modules, kAllowedInEnvvar); AddOption("--experimental-import-meta-resolve", - "experimental ES Module import.meta.resolve() support", + "experimental ES Module import.meta.resolve() parentURL support", &EnvironmentOptions::experimental_import_meta_resolve, kAllowedInEnvvar); AddOption("--experimental-permission", diff --git a/test/es-module/test-esm-import-meta-resolve.mjs b/test/es-module/test-esm-import-meta-resolve.mjs index 5ae4d256886a1f..8495c161312822 100644 --- a/test/es-module/test-esm-import-meta-resolve.mjs +++ b/test/es-module/test-esm-import-meta-resolve.mjs @@ -41,7 +41,6 @@ assert.strictEqual(import.meta.resolve('baz/', fixtures), { const cp = spawn(execPath, [ - '--experimental-import-meta-resolve', '--input-type=module', '--eval', 'console.log(typeof import.meta.resolve)', ]); @@ -50,7 +49,6 @@ assert.strictEqual(import.meta.resolve('baz/', fixtures), { const cp = spawn(execPath, [ - '--experimental-import-meta-resolve', '--input-type=module', ]); cp.stdin.end('console.log(typeof import.meta.resolve)'); @@ -59,7 +57,6 @@ assert.strictEqual(import.meta.resolve('baz/', fixtures), { const cp = spawn(execPath, [ - '--experimental-import-meta-resolve', '--input-type=module', '--eval', 'import "data:text/javascript,console.log(import.meta.resolve(%22node:os%22))"', ]); @@ -68,7 +65,6 @@ assert.strictEqual(import.meta.resolve('baz/', fixtures), { const cp = spawn(execPath, [ - '--experimental-import-meta-resolve', '--input-type=module', ]); cp.stdin.end('import "data:text/javascript,console.log(import.meta.resolve(%22node:os%22))"'); diff --git a/test/es-module/test-esm-import-meta.mjs b/test/es-module/test-esm-import-meta.mjs index 0151177b21c302..4c5aa902d4a970 100644 --- a/test/es-module/test-esm-import-meta.mjs +++ b/test/es-module/test-esm-import-meta.mjs @@ -3,7 +3,7 @@ import assert from 'assert'; assert.strictEqual(Object.getPrototypeOf(import.meta), null); -const keys = ['url']; +const keys = ['resolve', 'url']; assert.deepStrictEqual(Reflect.ownKeys(import.meta), keys); const descriptors = Object.getOwnPropertyDescriptors(import.meta); diff --git a/test/es-module/test-esm-loader-hooks.mjs b/test/es-module/test-esm-loader-hooks.mjs index ecb429d4593a3a..b3ac31c60e1f38 100644 --- a/test/es-module/test-esm-loader-hooks.mjs +++ b/test/es-module/test-esm-loader-hooks.mjs @@ -94,7 +94,6 @@ describe('Loader hooks', { concurrency: true }, () => { it('import.meta.resolve of a never-settling resolve', async () => { const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ '--no-warnings', - '--experimental-import-meta-resolve', '--experimental-loader', fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), fixtures.path('es-module-loaders/never-settling-resolve-step/import.meta.never-resolve.mjs'), @@ -207,7 +206,6 @@ describe('Loader hooks', { concurrency: true }, () => { it('should not leak internals or expose import.meta.resolve', async () => { const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ '--no-warnings', - '--experimental-import-meta-resolve', '--experimental-loader', fixtures.fileURL('es-module-loaders/loader-edge-cases.mjs'), fixtures.path('empty.js'), @@ -222,7 +220,6 @@ describe('Loader hooks', { concurrency: true }, () => { it('should be fine to call `process.exit` from a custom async hook', async () => { const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ '--no-warnings', - '--experimental-import-meta-resolve', '--experimental-loader', 'data:text/javascript,export function load(a,b,next){if(a==="data:exit")process.exit(42);return next(a,b)}', '--input-type=module', From 719c41800847c3ec2e4c36785b7a9c392587f754 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 5 Aug 2023 17:45:03 +0000 Subject: [PATCH 2/8] update pr url --- doc/api/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 2e7d56031eb330..3673863016fda5 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -514,7 +514,7 @@ added: - v12.16.2 changes: - version: X.X.X - pr-url: incoming + pr-url: https://github.com/nodejs/node/pull/49028 description: synchronous import.meta.resolve made available by default, with the flag retained for enabling the experimental second argument as previously supported. From 1c5c2d8a84100935e81770d00b57d8c9a1394fe0 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 5 Aug 2023 17:51:34 +0000 Subject: [PATCH 3/8] replaceme --- doc/api/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 3673863016fda5..b2d860ba500166 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -513,7 +513,7 @@ added: - v13.9.0 - v12.16.2 changes: - - version: X.X.X + - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/49028 description: synchronous import.meta.resolve made available by default, with the flag retained for enabling the experimental second argument From f865a1d36d8fd8f4330a456aabaca0fe91523747 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sat, 5 Aug 2023 18:15:19 +0000 Subject: [PATCH 4/8] docs changes --- doc/api/esm.md | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 6d510d0d0b00b5..c44b5e538406c3 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -322,7 +322,7 @@ import { readFileSync } from 'node:fs'; const buffer = readFileSync(new URL('./data.proto', import.meta.url)); ``` -### `import.meta.resolve(specifier[, parent])` +### `import.meta.resolve(specifier)` -> Stability: 1 - Experimental - -This feature is only available with the `--experimental-import-meta-resolve` -command flag enabled. - * `specifier` {string} The module specifier to resolve relative to `parent`. -* `parent` {string|URL} The absolute parent module URL to resolve from. If none - is specified, the value of `import.meta.url` is used as the default. -* Returns: {string} - -Provides a module-relative resolution function scoped to each module, returning -the URL string. In alignment with browser behavior, this now returns -synchronously. +* Returns: {string} The absolute (`file:`) URL string for the resolved module. -> **Caveat** This can result in synchronous file-system operations, which -> can impact performance similarly to `require.resolve`. +[`import.meta.resolve`][] is a module-relative resolution function scoped to +each module, returning the URL string. ```js const dependencyAsset = import.meta.resolve('component-lib/asset.css'); +// file:///app/node_modules/component-lib/asset.css ``` -`import.meta.resolve` also accepts a second argument which is the parent module -from which to resolve: +All features of the Node.js module resolution are supported. Dependency +resolutions are subject to the permitted exports resolutions within the package. ```js import.meta.resolve('./dep', import.meta.url); +// file:///app/dep ``` +> **Caveat** This can result in synchronous file-system operations, which +> can impact performance similarly to `require.resolve`. + +Previously, Node.js implemented an asynchonous resolver which also permitted +a second contextual argument. The implementation has since been updated to be +synchronous, with the second contextual `parent` URL still accessible behind the +`--experimental-import-meta-resolve` flag: + +* `parent` {string|URL} An optional absolute parent module URL to resolve from. + ## Interoperability with CommonJS ### `import` statements @@ -501,8 +507,8 @@ They can instead be loaded with [`module.createRequire()`][] or Relative resolution can be handled via `new URL('./local', import.meta.url)`. -For a complete `require.resolve` replacement, there is a flagged experimental -[`import.meta.resolve`][] API. +For a complete `require.resolve` replacement, there is the +[import.meta.resolve][] API. Alternatively `module.createRequire()` can be used. @@ -1672,7 +1678,7 @@ for ESM specifiers is [commonjs-extension-resolution-loader][]. [`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs [`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export [`import()`]: #import-expressions -[`import.meta.resolve`]: #importmetaresolvespecifier-parent +[`import.meta.resolve`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve [`import.meta.url`]: #importmetaurl [`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import [`initialize`]: #initialize @@ -1690,6 +1696,7 @@ for ESM specifiers is [commonjs-extension-resolution-loader][]. [cjs-module-lexer]: https://github.com/nodejs/cjs-module-lexer/tree/1.2.2 [commonjs-extension-resolution-loader]: https://github.com/nodejs/loaders-test/tree/main/commonjs-extension-resolution-loader [custom https loader]: #https-loader +[import.meta.resolve]: #importmetaresolvespecifier [load hook]: #loadurl-context-nextload [percent-encoded]: url.md#percent-encoding-in-urls [special scheme]: https://url.spec.whatwg.org/#special-scheme From 69796fd8e387976c2e5ff3d7eca7dacc0e8164a4 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Sun, 6 Aug 2023 17:12:40 +0000 Subject: [PATCH 5/8] pr feedback --- lib/internal/modules/esm/initialize_import_meta.js | 7 +++++-- test/es-module/test-esm-loader-hooks.mjs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index 1c08b5bc7bfa83..e3784c132974c6 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -8,12 +8,15 @@ const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta * @param {string} defaultParentURL The default base to use for resolution * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader * @param {bool} allowParentURL Whether to permit parentURL second argument for contextual resolution - * @returns {(specifier: string, parentURL?: string) => string} Function to assign to import.meta.resolve + * @returns {(specifier: string) => string} Function to assign to import.meta.resolve */ function createImportMetaResolve(defaultParentURL, loader, allowParentURL) { return function resolve(specifier) { let url; - const parentURL = allowParentURL ? arguments[1] ?? defaultParentURL : defaultParentURL; + + const parentURL = allowParentURL ? + arguments[1] ?? defaultParentURL : + defaultParentURL; try { ({ url } = loader.resolveSync(specifier, parentURL)); diff --git a/test/es-module/test-esm-loader-hooks.mjs b/test/es-module/test-esm-loader-hooks.mjs index b3ac31c60e1f38..984ba8833080a0 100644 --- a/test/es-module/test-esm-loader-hooks.mjs +++ b/test/es-module/test-esm-loader-hooks.mjs @@ -236,7 +236,6 @@ describe('Loader hooks', { concurrency: true }, () => { it('should be fine to call `process.exit` from a custom sync hook', async () => { const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ '--no-warnings', - '--experimental-import-meta-resolve', '--experimental-loader', 'data:text/javascript,export function resolve(a,b,next){if(a==="exit:")process.exit(42);return next(a,b)}', '--input-type=module', From 09f2bef5c7481a756f89723931be29d2949f005f Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 7 Aug 2023 10:43:59 -0700 Subject: [PATCH 6/8] Update lib/internal/modules/esm/initialize_import_meta.js Co-authored-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- lib/internal/modules/esm/initialize_import_meta.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index e3784c132974c6..2637b58697d130 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -11,6 +11,11 @@ const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta * @returns {(specifier: string) => string} Function to assign to import.meta.resolve */ function createImportMetaResolve(defaultParentURL, loader, allowParentURL) { + /** + * @param {string} specifier + * @param {URL['href']} [parentURL] When `--experimental-import-meta-resolve` is specified, a + * second argument can be provided. + */ return function resolve(specifier) { let url; From c94c64ea8ee509bd19fed79ac7e4b03135901028 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 7 Aug 2023 18:10:21 +0000 Subject: [PATCH 7/8] fixup default arg --- lib/internal/modules/esm/initialize_import_meta.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index 2637b58697d130..f55f60a5b7647a 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -16,12 +16,12 @@ function createImportMetaResolve(defaultParentURL, loader, allowParentURL) { * @param {URL['href']} [parentURL] When `--experimental-import-meta-resolve` is specified, a * second argument can be provided. */ - return function resolve(specifier) { + return function resolve(specifier, parentURL = defaultParentURL) { let url; - const parentURL = allowParentURL ? - arguments[1] ?? defaultParentURL : - defaultParentURL; + if (!allowParentURL) { + parentURL = defaultParentURL; + } try { ({ url } = loader.resolveSync(specifier, parentURL)); From a0ab43dbcbf6983580ad1e0e37b1ca2a9592a615 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Sat, 12 Aug 2023 19:53:39 -0700 Subject: [PATCH 8/8] add stability, fix typos --- doc/api/esm.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index c44b5e538406c3..867522e0a604c2 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -344,7 +344,10 @@ changes: flagged. --> -* `specifier` {string} The module specifier to resolve relative to `parent`. +> Stability: 1.2 - Release candidate + +* `specifier` {string} The module specifier to resolve relative to the + current module. * Returns: {string} The absolute (`file:`) URL string for the resolved module. [`import.meta.resolve`][] is a module-relative resolution function scoped to @@ -366,10 +369,10 @@ import.meta.resolve('./dep', import.meta.url); > **Caveat** This can result in synchronous file-system operations, which > can impact performance similarly to `require.resolve`. -Previously, Node.js implemented an asynchonous resolver which also permitted +Previously, Node.js implemented an asynchronous resolver which also permitted a second contextual argument. The implementation has since been updated to be -synchronous, with the second contextual `parent` URL still accessible behind the -`--experimental-import-meta-resolve` flag: +synchronous, with the second contextual `parent` argument still accessible +behind the `--experimental-import-meta-resolve` flag: * `parent` {string|URL} An optional absolute parent module URL to resolve from.