Skip to content

esm: add import.meta.node.resolveURL #49246

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

Closed
wants to merge 2 commits into from
Closed
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
33 changes: 30 additions & 3 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,34 @@ behind the `--experimental-import-meta-resolve` flag:
* `parent` {string|URL} An optional absolute parent module URL to resolve from.

> **Caveat** This feature is not available within custom loaders (it would
> create a deadlock).
> create a deadlock). Use [`import.meta.node.resolveURL`][] instead.

### `import.meta.node.resolveURL(specifier[, parentURL])`

<!--
added: REPLACEME
-->

> Stability: 1 – Experimental
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
> Stability: 1 – Experimental
> Stability: 1.1 – Active development


* `specifier` {string|URL} The module specifier to resolve relative to the
`parentURL`.
* `parentURL` {string|URL} The URL to resolve the specifier. **Default:** `import.meta.url`.
* Returns: {Promise} Fulfills with a {URL} representing the absolute URL of the
resolved specifier.
Comment on lines +393 to +394
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it would be confusing that this is async while import.meta.resolve is sync.

Copy link
Member

Choose a reason for hiding this comment

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

perhaps name it import.meta.node.resolveAsync? we have a lot of APIs with a/sync in the name to differentiate that.


> **Caveat**: This is a Node.js specific API, consider using
> [`import.meta.resolve`](#importmetaresolvespecifier) for a more portable code
> if you don't need the second argument and do not intend your module to run in
> a custom loader.

```js
import { readFile } from 'node:fs/promises';

const data = await readFile(await import.meta.node.resolveURL('./data.txt'), 'utf-8');
// This is equivalent to (but also available in custom loaders):
const data2 = await readFile(new URL(import.meta.resolve('./data.txt')), 'utf-8');
Comment on lines +404 to +406
Copy link
Member

Choose a reason for hiding this comment

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

The comment makes it sound like the data2 line, with import.meta.resolve, is available in custom loaders.

Suggested change
const data = await readFile(await import.meta.node.resolveURL('./data.txt'), 'utf-8');
// This is equivalent to (but also available in custom loaders):
const data2 = await readFile(new URL(import.meta.resolve('./data.txt')), 'utf-8');
// This works both in application code and in custom loaders, but is specific to Node.js:
const data1 = await readFile(await import.meta.node.resolveURL('./data.txt'), 'utf-8');
// This is equivalent, and portable to other runtimes, but only works in application code
// (not in custom loaders):
const data2 = await readFile(new URL(import.meta.resolve('./data.txt')), 'utf-8');

```

## Interoperability with CommonJS

Expand Down Expand Up @@ -514,7 +541,7 @@ 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 the
[import.meta.resolve][] API.
[`import.meta.node.resolveURL`][] API.

Alternatively `module.createRequire()` can be used.

Expand Down Expand Up @@ -1710,6 +1737,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.node.resolveURL`]: #importmetanoderesolveurlspecifier-parenturl
[`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
Expand All @@ -1728,7 +1756,6 @@ 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
27 changes: 27 additions & 0 deletions lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

const { URL } = require('internal/url');
const { getOptionValue } = require('internal/options');
const { kEmptyObject } = require('internal/util');
const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve');

/**
Expand All @@ -15,6 +17,7 @@ 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 {URL['href']}
*/
return function resolve(specifier, parentURL = defaultParentURL) {
let url;
Expand Down Expand Up @@ -56,6 +59,30 @@ function initializeImportMeta(meta, context, loader) {
}

meta.url = url;
meta.node = {
/**
* @param {string | URL} specifier
* @param {URL['href'] | URL} [parentURL]
* @return {Promise<URL>}
*/
async resolveURL(specifier, parentURL = url) {
try {
const { url: resolved } = await loader.resolve(specifier, parentURL, kEmptyObject);
return new URL(resolved);
} catch (error) {
switch (error?.code) {
case 'ERR_UNSUPPORTED_DIR_IMPORT':
case 'ERR_MODULE_NOT_FOUND': {
const { url: resolved } = error;
if (resolved) {
return new URL(resolved);
Comment on lines +74 to +78
Copy link
Member

Choose a reason for hiding this comment

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

Does import.meta.resolve allow this? I feel like it perhaps should, like it should produce a URL regardless of whether or not the resource exists, but does it? If it’s an intentional variance, and we don’t plan to update import.meta.resolve to match, then we should document this difference.

}
}
}
throw error;
}
},
};

return meta;
}
Expand Down
105 changes: 105 additions & 0 deletions test/es-module/test-esm-import-meta-node-resolveURL.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Flags: --experimental-import-meta-resolve
Copy link
Member

Choose a reason for hiding this comment

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

I think this is unnecessary?

Suggested change
// Flags: --experimental-import-meta-resolve

import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
import { spawn } from 'node:child_process';
import { execPath } from 'node:process';

assert.deepStrictEqual(await import.meta.node.resolveURL('./test-esm-import-meta.mjs'),
new URL('./test-esm-import-meta.mjs', import.meta.url));
assert.deepStrictEqual(await import.meta.node.resolveURL(new URL(import.meta.url)),
new URL(import.meta.url));
{
// Testing with specifiers that does not lead to actual modules:
const notFound = await import.meta.node.resolveURL('./notfound.mjs');
assert.deepStrictEqual(notFound, new URL('./notfound.mjs', import.meta.url));
const noExtension = await import.meta.node.resolveURL('./asset');
assert.deepStrictEqual(noExtension, new URL('./asset', import.meta.url));
await assert.rejects(import.meta.node.resolveURL('does-not-exist'), { code: 'ERR_MODULE_NOT_FOUND' });
}
assert.strictEqual(
`${await import.meta.node.resolveURL('../fixtures/empty-with-bom.txt')}`,
import.meta.resolve('../fixtures/empty-with-bom.txt'));
assert.deepStrictEqual(
await import.meta.node.resolveURL('../fixtures/empty-with-bom.txt'),
fixtures.fileURL('empty-with-bom.txt'));
assert.deepStrictEqual(
await import.meta.node.resolveURL('./empty-with-bom.txt', fixtures.fileURL('./')),
fixtures.fileURL('empty-with-bom.txt'));
assert.deepStrictEqual(
await import.meta.node.resolveURL('./empty-with-bom.txt', fixtures.fileURL('./').href),
fixtures.fileURL('empty-with-bom.txt'));
await [[], {}, Symbol(), 0, 1, 1n, 1.1, () => {}, true, false].map((arg) =>
assert.rejects(import.meta.node.resolveURL('../fixtures/', arg), {
code: 'ERR_INVALID_ARG_TYPE',
})
);
assert.deepStrictEqual(await import.meta.node.resolveURL('http://some-absolute/url'), new URL('http://some-absolute/url'));
assert.deepStrictEqual(await import.meta.node.resolveURL('some://weird/protocol'), new URL('some://weird/protocol'));
assert.deepStrictEqual(await import.meta.node.resolveURL('baz/', fixtures.fileURL('./')),
fixtures.fileURL('node_modules/baz/'));

await Promise.all([

async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'console.log(typeof import.meta.node.resolveURL)',
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, 'function\n');
assert.strictEqual(code, 0);
},

async () => {
const cp = spawn(execPath, [
'--input-type=module',
]);
cp.stdin.end('console.log(typeof import.meta.node.resolveURL)');
assert.match((await cp.stdout.toArray()).toString(), /^function\r?\n$/);
},

async () => {
// Should return a Promise.
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "data:text/javascript,console.log(import.meta.node.resolveURL(%22node:os%22))"',
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, 'Promise { <pending> }\n');
assert.strictEqual(code, 0);
},

async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "data:text/javascript,console.log(`%24{await import.meta.node.resolveURL(%22node:os%22)}`)"',
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, 'node:os\n');
assert.strictEqual(code, 0);
},

async () => {
// Should be available in custom loaders.
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,console.log(`%24{await import.meta.node.resolveURL(%22node:os%22)}`)',
'--eval',
'setTimeout(()=>{}, 99)',
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, 'node:os\n');
assert.strictEqual(code, 0);
},

async () => {
const cp = spawn(execPath, [
'--input-type=module',
]);
cp.stdin.end('import "data:text/javascript,console.log(`%24{await import.meta.node.resolveURL(%22node:os%22)}`)"');
assert.match((await cp.stdout.toArray()).toString(), /^node:os\r?\n$/);
},

].map((fn) => fn()));