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: unflag import.meta.resolve #49028

Merged
merged 8 commits into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,18 @@ of `--enable-source-maps`.
added:
- v13.9.0
- v12.16.2
changes:
- 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
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`

Expand Down
50 changes: 30 additions & 20 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

<!--
added:
Expand All @@ -337,36 +337,45 @@ changes:
- v14.18.0
pr-url: https://github.com/nodejs/node/pull/38587
description: Add support for WHATWG `URL` object to `parentURL` parameter.
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/49028
description: Unflag import.meta.resolve, with `parentURL` parameter still
flagged.
-->

> Stability: 1 - Experimental
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

This feature is only available with the `--experimental-import-meta-resolve`
command flag enabled.
> Stability: 1.2 - Release candidate

* `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}
* `specifier` {string} The module specifier to resolve relative to the
current module.
* Returns: {string} The absolute (`file:`) URL string for the resolved module.

Provides a module-relative resolution function scoped to each module, returning
the URL string. In alignment with browser behavior, this now returns
synchronously.

> **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 asynchronous resolver which also permitted
a second contextual argument. The implementation has since been updated to be
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.

## Interoperability with CommonJS

### `import` statements
Expand Down Expand Up @@ -501,8 +510,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.

Expand Down Expand Up @@ -1672,7 +1681,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
Expand All @@ -1690,6 +1699,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
Expand Down
19 changes: 15 additions & 4 deletions lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ 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
* @returns {(specifier: string, parentURL?: string) => string} Function to assign to import.meta.resolve
* @param {bool} allowParentURL Whether to permit parentURL second argument for contextual resolution
* @returns {(specifier: string) => string} Function to assign to import.meta.resolve
*/
function createImportMetaResolve(defaultParentURL, loader) {
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, parentURL = defaultParentURL) {
let url;

if (!allowParentURL) {
parentURL = defaultParentURL;
}

try {
({ url } = loader.resolveSync(specifier, parentURL));
return url;
Expand Down Expand Up @@ -40,8 +51,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;
Expand Down
2 changes: 1 addition & 1 deletion src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions test/es-module/test-esm-import-meta-resolve.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
]);
Expand All @@ -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)');
Expand All @@ -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))"',
]);
Expand All @@ -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))"');
Expand Down
2 changes: 1 addition & 1 deletion test/es-module/test-esm-import-meta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 0 additions & 4 deletions test/es-module/test-esm-loader-hooks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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'),
Expand All @@ -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',
Expand All @@ -239,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',
Expand Down