From f6392e9fde507db0b9cbb5dc00e5939135a22d64 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 19 Dec 2019 16:25:52 -0500 Subject: [PATCH] esm: import.meta.resolve with nodejs: builtins PR-URL: https://github.com/nodejs/node/pull/31032 Reviewed-By: Jan Krems Reviewed-By: Myles Borins --- doc/api/cli.md | 8 +++ doc/api/esm.md | 50 ++++++++++++++++--- doc/node.1 | 3 ++ lib/internal/modules/esm/get_format.js | 4 +- lib/internal/modules/esm/loader.js | 10 ++-- lib/internal/modules/esm/resolve.js | 10 ++-- lib/internal/modules/esm/translators.js | 33 ++++++++---- src/module_wrap.cc | 15 ++++-- src/node_options.cc | 4 ++ src/node_options.h | 1 + test/es-module/test-esm-dynamic-import.js | 5 +- .../test-esm-import-meta-resolve.mjs | 24 +++++++++ .../es-module-loaders/example-loader.mjs | 4 +- .../loader-unknown-builtin-module.mjs | 4 +- .../not-found-assert-loader.mjs | 3 +- 15 files changed, 141 insertions(+), 37 deletions(-) create mode 100644 test/es-module/test-esm-import-meta-resolve.mjs diff --git a/doc/api/cli.md b/doc/api/cli.md index 42ef76031eb0eb..9d62ac9ba31154 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -156,6 +156,13 @@ Enable experimental Source Map V3 support for stack traces. Currently, overriding `Error.prepareStackTrace` is ignored when the `--enable-source-maps` flag is set. +### `--experimental-import-meta-resolve` + + +Enable experimental `import.meta.resolve()` support. + ### `--experimental-json-modules` * `--enable-fips` * `--enable-source-maps` +* `--experimental-import-meta-resolve` * `--experimental-json-modules` * `--experimental-loader` * `--experimental-modules` diff --git a/doc/api/esm.md b/doc/api/esm.md index a09fffccfce454..f1378b59ad10a2 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -786,6 +786,32 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); ``` +### No `require.resolve` + +Former use cases relying on `require.resolve` to determine the resolved path +of a module can be supported via `import.meta.resolve`, which is experimental +and supported via the `--experimental-import-meta-resolve` flag: + +```js +(async () => { + const dependencyAsset = await import.meta.resolve('component-lib/asset.css'); +})(); +``` + +`import.meta.resolve` also accepts a second argument which is the parent module +from which to resolve from: + +```js +(async () => { + // Equivalent to import.meta.resolve('./dep') + await import.meta.resolve('./dep', import.meta.url); +})(); +``` + +This function is asynchronous since the ES module resolver in Node.js is +asynchronous. With the introduction of [Top-Level Await][], these use cases +will be easier as they won't require an async function wrapper. + ### No `require.extensions` `require.extensions` is not used by `import`. The expectation is that loader @@ -1350,13 +1376,14 @@ The resolver has the following properties: The algorithm to load an ES module specifier is given through the **ESM_RESOLVE** method below. It returns the resolved URL for a -module specifier relative to a parentURL, in addition to the unique module -format for that resolved URL given by the **ESM_FORMAT** routine. +module specifier relative to a parentURL. -The _"module"_ format is returned for an ECMAScript Module, while the -_"commonjs"_ format is used to indicate loading through the legacy -CommonJS loader. Additional formats such as _"addon"_ can be extended in future -updates. +The algorithm to determine the module format of a resolved URL is +provided by **ESM_FORMAT**, which returns the unique module +format for any file. The _"module"_ format is returned for an ECMAScript +Module, while the _"commonjs"_ format is used to indicate loading through the +legacy CommonJS loader. Additional formats such as _"addon"_ can be extended in +future updates. In the following algorithms, all subroutine errors are propagated as errors of these top-level routines unless stated otherwise. @@ -1385,11 +1412,13 @@ _defaultEnv_ is the conditional environment name priority array, > 1. If _resolvedURL_ contains any percent encodings of _"/"_ or _"\\"_ (_"%2f"_ > and _"%5C"_ respectively), then > 1. Throw an _Invalid Specifier_ error. -> 1. If the file at _resolvedURL_ does not exist, then +> 1. If _resolvedURL_ does not end with a trailing _"/"_ and the file at +> _resolvedURL_ does not exist, then > 1. Throw a _Module Not Found_ error. > 1. Set _resolvedURL_ to the real path of _resolvedURL_. > 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_). > 1. Load _resolvedURL_ as module format, _format_. +> 1. Return _resolvedURL_. **PACKAGE_RESOLVE**(_packageSpecifier_, _parentURL_) @@ -1417,7 +1446,7 @@ _defaultEnv_ is the conditional environment name priority array, > 1. If _selfUrl_ isn't empty, return _selfUrl_. > 1. If _packageSubpath_ is _undefined_ and _packageName_ is a Node.js builtin > module, then -> 1. Return the string _"node:"_ concatenated with _packageSpecifier_. +> 1. Return the string _"nodejs:"_ concatenated with _packageSpecifier_. > 1. While _parentURL_ is not the file system root, > 1. Let _packageURL_ be the URL resolution of _"node_modules/"_ > concatenated with _packageSpecifier_, relative to _parentURL_. @@ -1426,6 +1455,8 @@ _defaultEnv_ is the conditional environment name priority array, > 1. Set _parentURL_ to the parent URL path of _parentURL_. > 1. Continue the next loop iteration. > 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_). +> 1. If _packageSubpath_ is equal to _"./"_, then +> 1. Return _packageURL_ + _"/"_. > 1. If _packageSubpath_ is _undefined__, then > 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, > _pjson_). @@ -1447,6 +1478,8 @@ _defaultEnv_ is the conditional environment name priority array, > 1. If _pjson_ does not include an _"exports"_ property, then > 1. Return **undefined**. > 1. If _pjson.name_ is equal to _packageName_, then +> 1. If _packageSubpath_ is equal to _"./"_, then +> 1. Return _packageURL_ + _"/"_. > 1. If _packageSubpath_ is _undefined_, then > 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, _pjson_). > 1. Otherwise, @@ -1625,3 +1658,4 @@ success! [the official standard format]: https://tc39.github.io/ecma262/#sec-modules [transpiler loader example]: #esm_transpiler_loader [6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index +[Top-Level Await]: https://github.com/tc39/proposal-top-level-await diff --git a/doc/node.1 b/doc/node.1 index 0227bd383a5118..f251eab8dff2b5 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -113,6 +113,9 @@ Requires Node.js to be built with .It Fl -enable-source-maps Enable experimental Source Map V3 support for stack traces. . +.It Fl -experimental-import-meta-resolve +Enable experimental ES modules support for import.meta.resolve(). +. .It Fl -experimental-json-modules Enable experimental JSON interop support for the ES Module loader. . diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 69ba2398129908..9815077c3a6dcb 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -1,6 +1,5 @@ 'use strict'; -const { NativeModule } = require('internal/bootstrap/loaders'); const { extname } = require('path'); const { getOptionValue } = require('internal/options'); @@ -39,7 +38,7 @@ if (experimentalJsonModules) extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json'; function defaultGetFormat(url, context, defaultGetFormat) { - if (NativeModule.canBeRequiredByUsers(url)) { + if (url.startsWith('nodejs:')) { return { format: 'builtin' }; } const parsed = new URL(url); @@ -73,5 +72,6 @@ function defaultGetFormat(url, context, defaultGetFormat) { } return { format: format || null }; } + return { format: null }; } exports.defaultGetFormat = defaultGetFormat; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 6d9b267ffe5d67..5a27f4be7c9d1a 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -94,7 +94,10 @@ class Loader { throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 'string', 'loader resolve', 'url', url); } + return url; + } + async getFormat(url) { const getFormatResponse = await this._getFormat( url, {}, defaultGetFormat); if (typeof getFormatResponse !== 'object') { @@ -109,7 +112,7 @@ class Loader { } if (format === 'builtin') { - return { url: `node:${url}`, format }; + return format; } if (this._resolve !== defaultResolve) { @@ -132,7 +135,7 @@ class Loader { ); } - return { url, format }; + return format; } async eval( @@ -185,7 +188,8 @@ class Loader { } async getModuleJob(specifier, parentURL) { - const { url, format } = await this.resolve(specifier, parentURL); + const url = await this.resolve(specifier, parentURL); + const format = await this.getFormat(url); let job = this.moduleMap.get(url); // CommonJS will set functions for lazy job evaluation. if (typeof job === 'function') diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index f1045871dddb72..ec2e681e621d0d 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -8,6 +8,7 @@ const internalFS = require('internal/fs/utils'); const { NativeModule } = require('internal/bootstrap/loaders'); const { realpathSync } = require('fs'); const { getOptionValue } = require('internal/options'); +const { sep } = require('path'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); @@ -29,11 +30,13 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolve) { }; } } catch {} + if (parsed && parsed.protocol === 'nodejs:') + return { url: specifier }; if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:') throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(); if (NativeModule.canBeRequiredByUsers(specifier)) { return { - url: specifier + url: 'nodejs:' + specifier }; } if (parentURL && parentURL.startsWith('data:')) { @@ -58,11 +61,12 @@ function defaultResolve(specifier, { parentURL } = {}, defaultResolve) { let url = moduleWrapResolve(specifier, parentURL); if (isMain ? !preserveSymlinksMain : !preserveSymlinks) { - const real = realpathSync(fileURLToPath(url), { + const urlPath = fileURLToPath(url); + const real = realpathSync(urlPath, { [internalFS.realpathCacheKey]: realpathCache }); const old = url; - url = pathToFileURL(real); + url = pathToFileURL(real + (urlPath.endsWith(sep) ? '/' : '')); url.search = old.search; url.hash = old.hash; } diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 9f3bcfb8e7db9d..497f90ed94475b 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -28,6 +28,9 @@ const { ERR_UNKNOWN_BUILTIN_MODULE } = require('internal/errors').codes; const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); const moduleWrap = internalBinding('module_wrap'); const { ModuleWrap } = moduleWrap; +const { getOptionValue } = require('internal/options'); +const experimentalImportMetaResolve = + getOptionValue('--experimental-import-meta-resolve'); const debug = debuglog('esm'); @@ -42,16 +45,28 @@ function errPath(url) { return url; } -function initializeImportMeta(meta, { url }) { - meta.url = url; -} - let esmLoader; async function importModuleDynamically(specifier, { url }) { if (!esmLoader) { - esmLoader = require('internal/process/esm_loader'); + esmLoader = require('internal/process/esm_loader').ESMLoader; } - return esmLoader.ESMLoader.import(specifier, url); + return esmLoader.import(specifier, url); +} + +function createImportMetaResolve(defaultParentUrl) { + return async function resolve(specifier, parentUrl = defaultParentUrl) { + if (!esmLoader) { + esmLoader = require('internal/process/esm_loader').ESMLoader; + } + return esmLoader.resolve(specifier, parentUrl); + }; +} + +function initializeImportMeta(meta, { url }) { + // Alphabetical + if (experimentalImportMetaResolve) + meta.resolve = createImportMetaResolve(url); + meta.url = url; } // Strategy for loading a standard JavaScript module @@ -104,10 +119,10 @@ translators.set('commonjs', function commonjsStrategy(url, isMain) { // through normal resolution translators.set('builtin', async function builtinStrategy(url) { debug(`Translating BuiltinModule ${url}`); - // Slice 'node:' scheme - const id = url.slice(5); + // Slice 'nodejs:' scheme + const id = url.slice(7); const module = loadNativeModule(id, url, true); - if (!module) { + if (!url.startsWith('nodejs:') || !module) { throw new ERR_UNKNOWN_BUILTIN_MODULE(id); } debug(`Loading BuiltinModule ${url}`); diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 2368279d8e640a..30a2bf96366bcf 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -836,6 +836,10 @@ Maybe FinalizeResolution(Environment* env, return Nothing(); } + if (resolved.path().back() == '/') { + return Just(resolved); + } + const std::string& path = resolved.ToFilePath(); if (CheckDescriptorAtPath(path) != FILE) { std::string msg = "Cannot find module " + @@ -1221,7 +1225,9 @@ Maybe ResolveSelf(Environment* env, } if (!found_pjson || pcfg->name != pkg_name) return Nothing(); if (pcfg->exports.IsEmpty()) return Nothing(); - if (!pkg_subpath.length()) { + if (pkg_subpath == "./") { + return Just(URL("./", pjson_url)); + } else if (!pkg_subpath.length()) { return PackageMainResolve(env, pjson_url, *pcfg, base); } else { return PackageExportsResolve(env, pjson_url, pkg_subpath, *pcfg, base); @@ -1265,8 +1271,7 @@ Maybe PackageResolve(Environment* env, return Nothing(); } std::string pkg_subpath; - if ((sep_index == std::string::npos || - sep_index == specifier.length() - 1)) { + if (sep_index == std::string::npos) { pkg_subpath = ""; } else { pkg_subpath = "." + specifier.substr(sep_index); @@ -1297,7 +1302,9 @@ Maybe PackageResolve(Environment* env, Maybe pcfg = GetPackageConfig(env, pjson_path, base); // Invalid package configuration error. if (pcfg.IsNothing()) return Nothing(); - if (!pkg_subpath.length()) { + if (pkg_subpath == "./") { + return Just(URL("./", pjson_url)); + } else if (!pkg_subpath.length()) { return PackageMainResolve(env, pjson_url, *pcfg.FromJust(), base); } else { if (!pcfg.FromJust()->exports.IsEmpty()) { diff --git a/src/node_options.cc b/src/node_options.cc index 4928170a24b1a0..243e3a40cfaa4f 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -333,6 +333,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "experimental ES Module support for webassembly modules", &EnvironmentOptions::experimental_wasm_modules, kAllowedInEnvironment); + AddOption("--experimental-import-meta-resolve", + "experimental ES Module import.meta.resolve() support", + &EnvironmentOptions::experimental_import_meta_resolve, + kAllowedInEnvironment); AddOption("--experimental-policy", "use the specified file as a " "security policy", diff --git a/src/node_options.h b/src/node_options.h index afb12c393cbd0f..f49d35cedad058 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -106,6 +106,7 @@ class EnvironmentOptions : public Options { std::string experimental_specifier_resolution; std::string es_module_specifier_resolution; bool experimental_wasm_modules = false; + bool experimental_import_meta_resolve = false; std::string module_type; std::string experimental_policy; std::string experimental_policy_integrity; diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index e60309fe886cf9..e01b86eed143ed 100644 --- a/test/es-module/test-esm-dynamic-import.js +++ b/test/es-module/test-esm-dynamic-import.js @@ -55,11 +55,12 @@ function expectFsNamespace(result) { expectFsNamespace(import('fs')); expectFsNamespace(eval('import("fs")')); expectFsNamespace(eval('import("fs")')); + expectFsNamespace(import('nodejs:fs')); + expectModuleError(import('nodejs:unknown'), + 'ERR_UNKNOWN_BUILTIN_MODULE'); expectModuleError(import('./not-an-existing-module.mjs'), 'ERR_MODULE_NOT_FOUND'); - expectModuleError(import('node:fs'), - 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); expectModuleError(import('http://example.com/foo.js'), 'ERR_UNSUPPORTED_ESM_URL_SCHEME'); })(); diff --git a/test/es-module/test-esm-import-meta-resolve.mjs b/test/es-module/test-esm-import-meta-resolve.mjs new file mode 100644 index 00000000000000..faf9320a82745a --- /dev/null +++ b/test/es-module/test-esm-import-meta-resolve.mjs @@ -0,0 +1,24 @@ +// Flags: --experimental-import-meta-resolve +import '../common/index.mjs'; +import assert from 'assert'; + +const dirname = import.meta.url.slice(0, import.meta.url.lastIndexOf('/') + 1); +const fixtures = dirname.slice(0, dirname.lastIndexOf('/', dirname.length - 2) + + 1) + 'fixtures/'; + +(async () => { + assert.strictEqual(await import.meta.resolve('./test-esm-import-meta.mjs'), + dirname + 'test-esm-import-meta.mjs'); + try { + await import.meta.resolve('./notfound.mjs'); + assert.fail(); + } catch (e) { + assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND'); + } + assert.strictEqual( + await import.meta.resolve('../fixtures/empty-with-bom.txt'), + fixtures + 'empty-with-bom.txt'); + assert.strictEqual(await import.meta.resolve('../fixtures/'), fixtures); + assert.strictEqual(await import.meta.resolve('baz/', fixtures), + fixtures + 'node_modules/baz/'); +})(); diff --git a/test/fixtures/es-module-loaders/example-loader.mjs b/test/fixtures/es-module-loaders/example-loader.mjs index 70f9f28f08e742..1ed18bda51070d 100644 --- a/test/fixtures/es-module-loaders/example-loader.mjs +++ b/test/fixtures/es-module-loaders/example-loader.mjs @@ -11,7 +11,7 @@ baseURL.pathname = process.cwd() + '/'; export function resolve(specifier, { parentURL = baseURL }, defaultResolve) { if (builtinModules.includes(specifier)) { return { - url: specifier + url: 'nodejs:' + specifier }; } if (/^\.{1,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) { @@ -27,7 +27,7 @@ export function resolve(specifier, { parentURL = baseURL }, defaultResolve) { } export function getFormat(url, context, defaultGetFormat) { - if (builtinModules.includes(url)) { + if (url.startsWith('nodejs:') && builtinModules.includes(url.slice(7))) { return { format: 'builtin' }; diff --git a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs index 1a48231966ce5b..e976343e47e9bc 100644 --- a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs +++ b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs @@ -1,14 +1,14 @@ export async function resolve(specifier, { parentURL }, defaultResolve) { if (specifier === 'unknown-builtin-module') { return { - url: 'unknown-builtin-module' + url: 'nodejs:unknown-builtin-module' }; } return defaultResolve(specifier, {parentURL}, defaultResolve); } export async function getFormat(url, context, defaultGetFormat) { - if (url === 'unknown-builtin-module') { + if (url === 'nodejs:unknown-builtin-module') { return { format: 'builtin' }; diff --git a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs index 7b1d176e4537f6..2130bad5f52698 100644 --- a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs +++ b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs @@ -14,8 +14,7 @@ export async function resolve(specifier, { parentURL }, defaultResolve) { catch (e) { assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND'); return { - format: 'builtin', - url: 'fs' + url: 'nodejs:fs' }; } assert.fail(`Module resolution for ${specifier} should be throw ERR_MODULE_NOT_FOUND`);