Skip to content

Commit 0f96dc2

Browse files
committed
esm: import.meta.resolve with nodejs: builtins
PR-URL: #31032 Reviewed-By: Jan Krems <[email protected]> Reviewed-By: Myles Borins <[email protected]>
1 parent 875a4d1 commit 0f96dc2

15 files changed

+141
-37
lines changed

doc/api/cli.md

+8
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ Enable experimental Source Map V3 support for stack traces.
156156
Currently, overriding `Error.prepareStackTrace` is ignored when the
157157
`--enable-source-maps` flag is set.
158158

159+
### `--experimental-import-meta-resolve`
160+
<!-- YAML
161+
added: REPLACEME
162+
-->
163+
164+
Enable experimental `import.meta.resolve()` support.
165+
159166
### `--experimental-json-modules`
160167
<!-- YAML
161168
added: v12.9.0
@@ -1073,6 +1080,7 @@ Node.js options that are allowed are:
10731080
<!-- node-options-node start -->
10741081
* `--enable-fips`
10751082
* `--enable-source-maps`
1083+
* `--experimental-import-meta-resolve`
10761084
* `--experimental-json-modules`
10771085
* `--experimental-loader`
10781086
* `--experimental-modules`

doc/api/esm.md

+42-8
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,32 @@ const __filename = fileURLToPath(import.meta.url);
786786
const __dirname = dirname(__filename);
787787
```
788788

789+
### No `require.resolve`
790+
791+
Former use cases relying on `require.resolve` to determine the resolved path
792+
of a module can be supported via `import.meta.resolve`, which is experimental
793+
and supported via the `--experimental-import-meta-resolve` flag:
794+
795+
```js
796+
(async () => {
797+
const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
798+
})();
799+
```
800+
801+
`import.meta.resolve` also accepts a second argument which is the parent module
802+
from which to resolve from:
803+
804+
```js
805+
(async () => {
806+
// Equivalent to import.meta.resolve('./dep')
807+
await import.meta.resolve('./dep', import.meta.url);
808+
})();
809+
```
810+
811+
This function is asynchronous since the ES module resolver in Node.js is
812+
asynchronous. With the introduction of [Top-Level Await][], these use cases
813+
will be easier as they won't require an async function wrapper.
814+
789815
### No `require.extensions`
790816

791817
`require.extensions` is not used by `import`. The expectation is that loader
@@ -1350,13 +1376,14 @@ The resolver has the following properties:
13501376
13511377
The algorithm to load an ES module specifier is given through the
13521378
**ESM_RESOLVE** method below. It returns the resolved URL for a
1353-
module specifier relative to a parentURL, in addition to the unique module
1354-
format for that resolved URL given by the **ESM_FORMAT** routine.
1379+
module specifier relative to a parentURL.
13551380
1356-
The _"module"_ format is returned for an ECMAScript Module, while the
1357-
_"commonjs"_ format is used to indicate loading through the legacy
1358-
CommonJS loader. Additional formats such as _"addon"_ can be extended in future
1359-
updates.
1381+
The algorithm to determine the module format of a resolved URL is
1382+
provided by **ESM_FORMAT**, which returns the unique module
1383+
format for any file. The _"module"_ format is returned for an ECMAScript
1384+
Module, while the _"commonjs"_ format is used to indicate loading through the
1385+
legacy CommonJS loader. Additional formats such as _"addon"_ can be extended in
1386+
future updates.
13601387
13611388
In the following algorithms, all subroutine errors are propagated as errors
13621389
of these top-level routines unless stated otherwise.
@@ -1385,11 +1412,13 @@ _defaultEnv_ is the conditional environment name priority array,
13851412
> 1. If _resolvedURL_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2f"_
13861413
> and _"%5C"_ respectively), then
13871414
> 1. Throw an _Invalid Specifier_ error.
1388-
> 1. If the file at _resolvedURL_ does not exist, then
1415+
> 1. If _resolvedURL_ does not end with a trailing _"/"_ and the file at
1416+
> _resolvedURL_ does not exist, then
13891417
> 1. Throw a _Module Not Found_ error.
13901418
> 1. Set _resolvedURL_ to the real path of _resolvedURL_.
13911419
> 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_).
13921420
> 1. Load _resolvedURL_ as module format, _format_.
1421+
> 1. Return _resolvedURL_.
13931422
13941423
**PACKAGE_RESOLVE**(_packageSpecifier_, _parentURL_)
13951424
@@ -1417,7 +1446,7 @@ _defaultEnv_ is the conditional environment name priority array,
14171446
> 1. If _selfUrl_ isn't empty, return _selfUrl_.
14181447
> 1. If _packageSubpath_ is _undefined_ and _packageName_ is a Node.js builtin
14191448
> module, then
1420-
> 1. Return the string _"node:"_ concatenated with _packageSpecifier_.
1449+
> 1. Return the string _"nodejs:"_ concatenated with _packageSpecifier_.
14211450
> 1. While _parentURL_ is not the file system root,
14221451
> 1. Let _packageURL_ be the URL resolution of _"node_modules/"_
14231452
> concatenated with _packageSpecifier_, relative to _parentURL_.
@@ -1426,6 +1455,8 @@ _defaultEnv_ is the conditional environment name priority array,
14261455
> 1. Set _parentURL_ to the parent URL path of _parentURL_.
14271456
> 1. Continue the next loop iteration.
14281457
> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_).
1458+
> 1. If _packageSubpath_ is equal to _"./"_, then
1459+
> 1. Return _packageURL_ + _"/"_.
14291460
> 1. If _packageSubpath_ is _undefined__, then
14301461
> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_,
14311462
> _pjson_).
@@ -1447,6 +1478,8 @@ _defaultEnv_ is the conditional environment name priority array,
14471478
> 1. If _pjson_ does not include an _"exports"_ property, then
14481479
> 1. Return **undefined**.
14491480
> 1. If _pjson.name_ is equal to _packageName_, then
1481+
> 1. If _packageSubpath_ is equal to _"./"_, then
1482+
> 1. Return _packageURL_ + _"/"_.
14501483
> 1. If _packageSubpath_ is _undefined_, then
14511484
> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, _pjson_).
14521485
> 1. Otherwise,
@@ -1625,3 +1658,4 @@ success!
16251658
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
16261659
[transpiler loader example]: #esm_transpiler_loader
16271660
[6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index
1661+
[Top-Level Await]: https://github.com./tc39/proposal-top-level-await

doc/node.1

+3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ Requires Node.js to be built with
113113
.It Fl -enable-source-maps
114114
Enable experimental Source Map V3 support for stack traces.
115115
.
116+
.It Fl -experimental-import-meta-resolve
117+
Enable experimental ES modules support for import.meta.resolve().
118+
.
116119
.It Fl -experimental-json-modules
117120
Enable experimental JSON interop support for the ES Module loader.
118121
.

lib/internal/modules/esm/get_format.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
const { NativeModule } = require('internal/bootstrap/loaders');
43
const { extname } = require('path');
54
const { getOptionValue } = require('internal/options');
65

@@ -39,7 +38,7 @@ if (experimentalJsonModules)
3938
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
4039

4140
function defaultGetFormat(url, context, defaultGetFormat) {
42-
if (NativeModule.canBeRequiredByUsers(url)) {
41+
if (url.startsWith('nodejs:')) {
4342
return { format: 'builtin' };
4443
}
4544
const parsed = new URL(url);
@@ -73,5 +72,6 @@ function defaultGetFormat(url, context, defaultGetFormat) {
7372
}
7473
return { format: format || null };
7574
}
75+
return { format: null };
7676
}
7777
exports.defaultGetFormat = defaultGetFormat;

lib/internal/modules/esm/loader.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ class Loader {
9494
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
9595
'string', 'loader resolve', 'url', url);
9696
}
97+
return url;
98+
}
9799

100+
async getFormat(url) {
98101
const getFormatResponse = await this._getFormat(
99102
url, {}, defaultGetFormat);
100103
if (typeof getFormatResponse !== 'object') {
@@ -109,7 +112,7 @@ class Loader {
109112
}
110113

111114
if (format === 'builtin') {
112-
return { url: `node:${url}`, format };
115+
return format;
113116
}
114117

115118
if (this._resolve !== defaultResolve) {
@@ -132,7 +135,7 @@ class Loader {
132135
);
133136
}
134137

135-
return { url, format };
138+
return format;
136139
}
137140

138141
async eval(
@@ -185,7 +188,8 @@ class Loader {
185188
}
186189

187190
async getModuleJob(specifier, parentURL) {
188-
const { url, format } = await this.resolve(specifier, parentURL);
191+
const url = await this.resolve(specifier, parentURL);
192+
const format = await this.getFormat(url);
189193
let job = this.moduleMap.get(url);
190194
// CommonJS will set functions for lazy job evaluation.
191195
if (typeof job === 'function')

lib/internal/modules/esm/resolve.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const internalFS = require('internal/fs/utils');
88
const { NativeModule } = require('internal/bootstrap/loaders');
99
const { realpathSync } = require('fs');
1010
const { getOptionValue } = require('internal/options');
11+
const { sep } = require('path');
1112

1213
const preserveSymlinks = getOptionValue('--preserve-symlinks');
1314
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
@@ -29,11 +30,13 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolve) {
2930
};
3031
}
3132
} catch {}
33+
if (parsed && parsed.protocol === 'nodejs:')
34+
return { url: specifier };
3235
if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:')
3336
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME();
3437
if (NativeModule.canBeRequiredByUsers(specifier)) {
3538
return {
36-
url: specifier
39+
url: 'nodejs:' + specifier
3740
};
3841
}
3942
if (parentURL && parentURL.startsWith('data:')) {
@@ -58,11 +61,12 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolve) {
5861
let url = moduleWrapResolve(specifier, parentURL);
5962

6063
if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
61-
const real = realpathSync(fileURLToPath(url), {
64+
const urlPath = fileURLToPath(url);
65+
const real = realpathSync(urlPath, {
6266
[internalFS.realpathCacheKey]: realpathCache
6367
});
6468
const old = url;
65-
url = pathToFileURL(real);
69+
url = pathToFileURL(real + (urlPath.endsWith(sep) ? '/' : ''));
6670
url.search = old.search;
6771
url.hash = old.hash;
6872
}

lib/internal/modules/esm/translators.js

+24-9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const { ERR_UNKNOWN_BUILTIN_MODULE } = require('internal/errors').codes;
2828
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
2929
const moduleWrap = internalBinding('module_wrap');
3030
const { ModuleWrap } = moduleWrap;
31+
const { getOptionValue } = require('internal/options');
32+
const experimentalImportMetaResolve =
33+
getOptionValue('--experimental-import-meta-resolve');
3134

3235
const debug = debuglog('esm');
3336

@@ -42,16 +45,28 @@ function errPath(url) {
4245
return url;
4346
}
4447

45-
function initializeImportMeta(meta, { url }) {
46-
meta.url = url;
47-
}
48-
4948
let esmLoader;
5049
async function importModuleDynamically(specifier, { url }) {
5150
if (!esmLoader) {
52-
esmLoader = require('internal/process/esm_loader');
51+
esmLoader = require('internal/process/esm_loader').ESMLoader;
5352
}
54-
return esmLoader.ESMLoader.import(specifier, url);
53+
return esmLoader.import(specifier, url);
54+
}
55+
56+
function createImportMetaResolve(defaultParentUrl) {
57+
return async function resolve(specifier, parentUrl = defaultParentUrl) {
58+
if (!esmLoader) {
59+
esmLoader = require('internal/process/esm_loader').ESMLoader;
60+
}
61+
return esmLoader.resolve(specifier, parentUrl);
62+
};
63+
}
64+
65+
function initializeImportMeta(meta, { url }) {
66+
// Alphabetical
67+
if (experimentalImportMetaResolve)
68+
meta.resolve = createImportMetaResolve(url);
69+
meta.url = url;
5570
}
5671

5772
// Strategy for loading a standard JavaScript module
@@ -104,10 +119,10 @@ translators.set('commonjs', function commonjsStrategy(url, isMain) {
104119
// through normal resolution
105120
translators.set('builtin', async function builtinStrategy(url) {
106121
debug(`Translating BuiltinModule ${url}`);
107-
// Slice 'node:' scheme
108-
const id = url.slice(5);
122+
// Slice 'nodejs:' scheme
123+
const id = url.slice(7);
109124
const module = loadNativeModule(id, url, true);
110-
if (!module) {
125+
if (!url.startsWith('nodejs:') || !module) {
111126
throw new ERR_UNKNOWN_BUILTIN_MODULE(id);
112127
}
113128
debug(`Loading BuiltinModule ${url}`);

src/module_wrap.cc

+11-4
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,10 @@ Maybe<URL> FinalizeResolution(Environment* env,
836836
return Nothing<URL>();
837837
}
838838

839+
if (resolved.path().back() == '/') {
840+
return Just(resolved);
841+
}
842+
839843
const std::string& path = resolved.ToFilePath();
840844
if (CheckDescriptorAtPath(path) != FILE) {
841845
std::string msg = "Cannot find module " +
@@ -1221,7 +1225,9 @@ Maybe<URL> ResolveSelf(Environment* env,
12211225
}
12221226
if (!found_pjson || pcfg->name != pkg_name) return Nothing<URL>();
12231227
if (pcfg->exports.IsEmpty()) return Nothing<URL>();
1224-
if (!pkg_subpath.length()) {
1228+
if (pkg_subpath == "./") {
1229+
return Just(URL("./", pjson_url));
1230+
} else if (!pkg_subpath.length()) {
12251231
return PackageMainResolve(env, pjson_url, *pcfg, base);
12261232
} else {
12271233
return PackageExportsResolve(env, pjson_url, pkg_subpath, *pcfg, base);
@@ -1265,8 +1271,7 @@ Maybe<URL> PackageResolve(Environment* env,
12651271
return Nothing<URL>();
12661272
}
12671273
std::string pkg_subpath;
1268-
if ((sep_index == std::string::npos ||
1269-
sep_index == specifier.length() - 1)) {
1274+
if (sep_index == std::string::npos) {
12701275
pkg_subpath = "";
12711276
} else {
12721277
pkg_subpath = "." + specifier.substr(sep_index);
@@ -1297,7 +1302,9 @@ Maybe<URL> PackageResolve(Environment* env,
12971302
Maybe<const PackageConfig*> pcfg = GetPackageConfig(env, pjson_path, base);
12981303
// Invalid package configuration error.
12991304
if (pcfg.IsNothing()) return Nothing<URL>();
1300-
if (!pkg_subpath.length()) {
1305+
if (pkg_subpath == "./") {
1306+
return Just(URL("./", pjson_url));
1307+
} else if (!pkg_subpath.length()) {
13011308
return PackageMainResolve(env, pjson_url, *pcfg.FromJust(), base);
13021309
} else {
13031310
if (!pcfg.FromJust()->exports.IsEmpty()) {

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
333333
"experimental ES Module support for webassembly modules",
334334
&EnvironmentOptions::experimental_wasm_modules,
335335
kAllowedInEnvironment);
336+
AddOption("--experimental-import-meta-resolve",
337+
"experimental ES Module import.meta.resolve() support",
338+
&EnvironmentOptions::experimental_import_meta_resolve,
339+
kAllowedInEnvironment);
336340
AddOption("--experimental-policy",
337341
"use the specified file as a "
338342
"security policy",

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class EnvironmentOptions : public Options {
106106
std::string experimental_specifier_resolution;
107107
std::string es_module_specifier_resolution;
108108
bool experimental_wasm_modules = false;
109+
bool experimental_import_meta_resolve = false;
109110
std::string module_type;
110111
std::string experimental_policy;
111112
std::string experimental_policy_integrity;

test/es-module/test-esm-dynamic-import.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ function expectFsNamespace(result) {
5555
expectFsNamespace(import('fs'));
5656
expectFsNamespace(eval('import("fs")'));
5757
expectFsNamespace(eval('import("fs")'));
58+
expectFsNamespace(import('nodejs:fs'));
5859

60+
expectModuleError(import('nodejs:unknown'),
61+
'ERR_UNKNOWN_BUILTIN_MODULE');
5962
expectModuleError(import('./not-an-existing-module.mjs'),
6063
'ERR_MODULE_NOT_FOUND');
61-
expectModuleError(import('node:fs'),
62-
'ERR_UNSUPPORTED_ESM_URL_SCHEME');
6364
expectModuleError(import('http://example.com/foo.js'),
6465
'ERR_UNSUPPORTED_ESM_URL_SCHEME');
6566
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Flags: --experimental-import-meta-resolve
2+
import '../common/index.mjs';
3+
import assert from 'assert';
4+
5+
const dirname = import.meta.url.slice(0, import.meta.url.lastIndexOf('/') + 1);
6+
const fixtures = dirname.slice(0, dirname.lastIndexOf('/', dirname.length - 2) +
7+
1) + 'fixtures/';
8+
9+
(async () => {
10+
assert.strictEqual(await import.meta.resolve('./test-esm-import-meta.mjs'),
11+
dirname + 'test-esm-import-meta.mjs');
12+
try {
13+
await import.meta.resolve('./notfound.mjs');
14+
assert.fail();
15+
} catch (e) {
16+
assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND');
17+
}
18+
assert.strictEqual(
19+
await import.meta.resolve('../fixtures/empty-with-bom.txt'),
20+
fixtures + 'empty-with-bom.txt');
21+
assert.strictEqual(await import.meta.resolve('../fixtures/'), fixtures);
22+
assert.strictEqual(await import.meta.resolve('baz/', fixtures),
23+
fixtures + 'node_modules/baz/');
24+
})();

0 commit comments

Comments
 (0)