diff --git a/doc/api/esm.md b/doc/api/esm.md index d2277044a9..6987c7bf55 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -55,10 +55,6 @@ property: ## Notable differences between `import` and `require` -### Only Support for .mjs - -ESM must have the `.mjs` extension. - ### Mandatory file extensions You must provide a file extension when using the `import` keyword. @@ -157,37 +153,128 @@ The resolver has the following properties: ### Resolver Algorithm -The algorithm to resolve an ES module specifier is provided through -_ESM_RESOLVE_: +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. + +The _"esm"_ format is returned for an ECMAScript Module, while the +_"legacy"_ format is used to indicate loading through the legacy +CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be +extended in future updates. -**ESM_RESOLVE**(_specifier_, _parentURL_) -> 1. Let _resolvedURL_ be _undefined_. -> 1. If _specifier_ is a valid URL then, +In the following algorithms, all subroutine errors are propogated as errors +of these top-level routines. + +_isMain_ is **true** when resolving the Node.js application entry point. + +**ESM_RESOLVE(_specifier_, _parentURL_, _isMain_)** +> 1. Let _resolvedURL_ be **undefined**. +> 1. If _specifier_ is a valid URL, then > 1. Set _resolvedURL_ to the result of parsing and reserializing > _specifier_ as a URL. -> 1. Otherwise, if _specifier_ starts with _"/"_, _"./"_ or _"../"_ then, +> 1. Otherwise, if _specifier_ starts with _"/"_, then +> 1. Throw an _Invalid Specifier_ error. +> 1. Otherwise, if _specifier_ starts with _"./"_ or _"../"_, then > 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to > _parentURL_. > 1. Otherwise, > 1. Note: _specifier_ is now a bare specifier. > 1. Set _resolvedURL_ the result of > **PACKAGE_RESOLVE**(_specifier_, _parentURL_). -> 1. If the file at _resolvedURL_ does not exist then, +> 1. If the file at _resolvedURL_ does not exist, then > 1. Throw a _Module Not Found_ error. -> 1. Return _resolvedURL_. - -**PACKAGE_RESOLVE**(_packageSpecifier_, _parentURL_) -> 1. Assert: _packageSpecifier_ is a bare specifier. -> 1. If _packageSpecifier_ is a Node.js builtin module then, +> 1. Let _format_ be the result of **ESM_FORMAT**(_url_, _isMain_). +> 1. Load _resolvedURL_ as module format, _format_. + +PACKAGE_RESOLVE(_packageSpecifier_, _parentURL_) +> 1. Let _packageName_ be *undefined*. +> 1. Let _packageSubpath_ be *undefined*. +> 1. If _packageSpecifier_ is an empty string, then +> 1. Throw an _Invalid Specifier_ error. +> 1. If _packageSpecifier_ does not start with _"@"_, then +> 1. Set _packageName_ to the substring of _packageSpecifier_ until the +> first _"/"_ separator or the end of the string. +> 1. Otherwise, +> 1. If _packageSpecifier_ does not contain a _"/"_ separator, then +> 1. Throw an _Invalid Specifier_ error. +> 1. Set _packageName_ to the substring of _packageSpecifier_ +> until the second _"/"_ separator or the end of the string. +> 1. Let _packageSubpath_ be the substring of _packageSpecifier_ from the +> position at the length of _packageName_ plus one, if any. +> 1. Assert: _packageName_ is a valid package name or scoped package name. +> 1. Assert: _packageSubpath_ is either empty, or a path without a leading +> separator. +> 1. If _packageSubpath_ contains any _"."_ or _".."_ segments or percent +> encoded strings for _"/"_ or _"\"_ then, +> 1. Throw an _Invalid Specifier_ error. +> 1. If _packageSubpath_ is empty and _packageName_ is a Node.js builtin +> module, then > 1. Return the string _"node:"_ concatenated with _packageSpecifier_. -> 1. While _parentURL_ contains a non-empty _pathname_, +> 1. While _parentURL_ is not the file system root, > 1. Set _parentURL_ to the parent folder URL of _parentURL_. > 1. Let _packageURL_ be the URL resolution of the string concatenation of -> _parentURL_, _"/node_modules/"_ and _"packageSpecifier"_. -> 1. If the file at _packageURL_ exists then, -> 1. Return _packageURL_. +> _parentURL_, _"/node_modules/"_ and _packageSpecifier_. +> 1. If the folder at _packageURL_ does not exist, then +> 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 empty, then +> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, +> _pjson_). +> 1. Otherwise, +> 1. Return the URL resolution of _packageSubpath_ in _packageURL_. > 1. Throw a _Module Not Found_ error. +PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_) +> 1. If _pjson_ is **null**, then +> 1. Throw a _Module Not Found_ error. +> 1. If _pjson.main_ is a String, then +> 1. Let _resolvedMain_ be the concatenation of _packageURL_, "/", and +> _pjson.main_. +> 1. If the file at _resolvedMain_ exists, then +> 1. Return _resolvedMain_. +> 1. If _pjson.type_ is equal to _"esm"_, then +> 1. Throw a _Module Not Found_ error. +> 1. Let _legacyMainURL_ be the result applying the legacy +> **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a +> _Module Not Found_ error for no resolution. +> 1. If _legacyMainURL_ does not end in _".js"_ then, +> 1. Throw an _Unsupported File Extension_ error. +> 1. Return _legacyMainURL_. + +**ESM_FORMAT(_url_, _isMain_)** +> 1. Assert: _url_ corresponds to an existing file. +> 1. Let _pjson_ be the result of **READ_PACKAGE_BOUNDARY**(_url_). +> 1. If _pjson_ is **null** and _isMain_ is **true**, then +> 1. Return _"legacy"_. +> 1. If _pjson.type_ exists and is _"esm"_, then +> 1. If _url_ does not end in _".js"_ or _".mjs"_, then +> 1. Throw an _Unsupported File Extension_ error. +> 1. Return _"esm"_. +> 1. Otherwise, +> 1. If _url_ ends in _".mjs"_, then +> 1. Return _"esm"_. +> 1. Otherwise, +> 1. Return _"legacy"_. + +READ_PACKAGE_BOUNDARY(_url_) +> 1. Let _boundaryURL_ be _url_. +> 1. While _boundaryURL_ is not the file system root, +> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_boundaryURL_). +> 1. If _pjson_ is not **null**, then +> 1. Return _pjson_. +> 1. Set _boundaryURL_ to the parent URL of _boundaryURL_. +> 1. Return **null**. + +READ_PACKAGE_JSON(_packageURL_) +> 1. Let _pjsonURL_ be the resolution of _"package.json"_ within _packageURL_. +> 1. If the file at _pjsonURL_ does not exist, then +> 1. Return **null**. +> 1. If the file at _packageURL_ does not parse as valid JSON, then +> 1. Throw an _Invalid Package Configuration_ error. +> 1. Return the parsed JSON source of the file at _pjsonURL_. + [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md [`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename [ESM Minimal Kernel]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 31e64e4a8c..f1a9805032 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -864,13 +864,6 @@ E('ERR_MISSING_ARGS', E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', 'The ES Module loader may not return a format of \'dynamic\' when no ' + 'dynamicInstantiate function was provided', Error); -E('ERR_MODULE_NOT_FOUND', (module, base, legacyResolution) => { - let msg = `Cannot find module '${module}' imported from ${base}.`; - if (legacyResolution) - msg += ' Legacy behavior in require() would have found it at ' + - legacyResolution; - return msg; -}, Error); E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); E('ERR_NAPI_INVALID_DATAVIEW_ARGS', @@ -973,10 +966,10 @@ E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error); E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError); // This should probably be a `TypeError`. -E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension: \'%s\' imported ' + - 'from %s', Error); E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError); E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); +E('ERR_UNSUPPORTED_FILE_EXTENSION', 'Unsupported file extension: \'%s\' ' + + 'imported from %s', Error); E('ERR_V8BREAKITERATOR', 'Full ICU data not installed. See https://github.com/nodejs/node/wiki/Intl', diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js index 188313f304..1e784e710a 100644 --- a/lib/internal/modules/esm/default_resolve.js +++ b/lib/internal/modules/esm/default_resolve.js @@ -1,7 +1,5 @@ 'use strict'; -const { URL } = require('url'); -const CJSmodule = require('internal/modules/cjs/loader'); const internalFS = require('internal/fs/utils'); const { NativeModule } = require('internal/bootstrap/loaders'); const { extname } = require('path'); @@ -9,38 +7,24 @@ const { realpathSync } = require('fs'); const { getOptionValue } = require('internal/options'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); -const { - ERR_MODULE_NOT_FOUND, - ERR_UNKNOWN_FILE_EXTENSION -} = require('internal/errors').codes; +const { ERR_UNSUPPORTED_FILE_EXTENSION } = require('internal/errors').codes; const { resolve: moduleWrapResolve } = internalBinding('module_wrap'); const { pathToFileURL, fileURLToPath } = require('internal/url'); const realpathCache = new Map(); -function search(target, base) { - try { - return moduleWrapResolve(target, base); - } catch (e) { - e.stack; // cause V8 to generate stack before rethrow - let error = e; - try { - const questionedBase = new URL(base); - const tmpMod = new CJSmodule(questionedBase.pathname, null); - tmpMod.paths = CJSmodule._nodeModulePaths( - new URL('./', questionedBase).pathname); - const found = CJSmodule._resolveFilename(target, tmpMod); - error = new ERR_MODULE_NOT_FOUND(target, fileURLToPath(base), found); - } catch { - // ignore - } - throw error; - } -} - const extensionFormatMap = { '__proto__': null, - '.mjs': 'esm' + '.mjs': 'esm', + '.js': 'esm' +}; + +const legacyExtensionFormatMap = { + '__proto__': null, + '.js': 'cjs', + '.json': 'cjs', + '.mjs': 'esm', + '.node': 'cjs' }; function resolve(specifier, parentURL) { @@ -51,10 +35,14 @@ function resolve(specifier, parentURL) { }; } - let url = search(specifier, - parentURL || pathToFileURL(`${process.cwd()}/`).href); - const isMain = parentURL === undefined; + if (isMain) + parentURL = pathToFileURL(`${process.cwd()}/`).href; + + const resolved = moduleWrapResolve(specifier, parentURL, isMain); + + let url = resolved.url; + const legacy = resolved.legacy; if (isMain ? !preserveSymlinksMain : !preserveSymlinks) { const real = realpathSync(fileURLToPath(url), { @@ -67,14 +55,14 @@ function resolve(specifier, parentURL) { } const ext = extname(url.pathname); + let format = (legacy ? legacyExtensionFormatMap : extensionFormatMap)[ext]; - let format = extensionFormatMap[ext]; if (!format) { if (isMain) - format = 'cjs'; + format = legacy ? 'cjs' : 'esm'; else - throw new ERR_UNKNOWN_FILE_EXTENSION(url.pathname, - fileURLToPath(parentURL)); + throw new ERR_UNSUPPORTED_FILE_EXTENSION(fileURLToPath(url), + fileURLToPath(parentURL)); } return { url: `${url}`, format }; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index a1a1621909..1776d3da48 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -14,7 +14,7 @@ const ModuleJob = require('internal/modules/esm/module_job'); const defaultResolve = require('internal/modules/esm/default_resolve'); const createDynamicModule = require( 'internal/modules/esm/create_dynamic_module'); -const translators = require('internal/modules/esm/translators'); +const { translators } = require('internal/modules/esm/translators'); const FunctionBind = Function.call.bind(Function.prototype.bind); @@ -32,6 +32,9 @@ class Loader { // Registry of loaded modules, akin to `require.cache` this.moduleMap = new ModuleMap(); + // map of already-loaded CJS modules to use + this.cjsCache = new Map(); + // The resolver has the signature // (specifier : string, parentURL : string, defaultResolve) // -> Promise<{ url : string, format: string }> diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index e900babefd..e0b1d00303 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -23,7 +23,7 @@ class ModuleJob { // This is a Promise<{ module, reflect }>, whose fields will be copied // onto `this` by `link()` below once it has been resolved. - this.modulePromise = moduleProvider(url, isMain); + this.modulePromise = moduleProvider.call(loader, url, isMain); this.module = undefined; this.reflect = undefined; diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 46e3bdd998..4fc0d70b53 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -23,7 +23,7 @@ const StringReplace = Function.call.bind(String.prototype.replace); const debug = debuglog('esm'); const translators = new SafeMap(); -module.exports = translators; +exports.translators = translators; function initializeImportMeta(meta, { url }) { meta.url = url; @@ -35,7 +35,7 @@ async function importModuleDynamically(specifier, { url }) { } // Strategy for loading a standard JavaScript module -translators.set('esm', async (url) => { +translators.set('esm', async function(url) { const source = `${await readFileAsync(new URL(url))}`; debug(`Translating StandardModule ${url}`); const module = new ModuleWrap(stripShebang(source), url); @@ -52,9 +52,14 @@ translators.set('esm', async (url) => { // Strategy for loading a node-style CommonJS module const isWindows = process.platform === 'win32'; const winSepRegEx = /\//g; -translators.set('cjs', async (url, isMain) => { +translators.set('cjs', async function(url, isMain) { debug(`Translating CJSModule ${url}`); const pathname = internalURLModule.fileURLToPath(new URL(url)); + const cached = this.cjsCache.get(url); + if (cached) { + this.cjsCache.delete(url); + return cached; + } const module = CJSModule._cache[ isWindows ? StringReplace(pathname, winSepRegEx, '\\') : pathname]; if (module && module.loaded) { @@ -74,7 +79,7 @@ translators.set('cjs', async (url, isMain) => { // Strategy for loading a node builtin CommonJS module that isn't // through normal resolution -translators.set('builtin', async (url) => { +translators.set('builtin', async function(url) { debug(`Translating BuiltinModule ${url}`); // slice 'node:' scheme const id = url.slice(5); diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 6ed43970cb..98e1f26d94 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -33,9 +33,7 @@ exports.importModuleDynamicallyCallback = async function(wrap, specifier) { }; let loaderResolve; -exports.loaderPromise = new Promise((resolve, reject) => { - loaderResolve = resolve; -}); +exports.loaderPromise = new Promise((resolve) => loaderResolve = resolve); exports.ESMLoader = undefined; diff --git a/src/env.h b/src/env.h index 40bd0797a2..ddd8dae0ac 100644 --- a/src/env.h +++ b/src/env.h @@ -73,14 +73,23 @@ namespace loader { class ModuleWrap; struct PackageConfig { - enum class Exists { Yes, No }; - enum class IsValid { Yes, No }; - enum class HasMain { Yes, No }; - - Exists exists; - IsValid is_valid; - HasMain has_main; - std::string main; + struct Exists { + enum Bool { No, Yes }; + }; + struct IsValid { + enum Bool { No, Yes }; + }; + struct HasMain { + enum Bool { No, Yes }; + }; + struct IsESM { + enum Bool { No, Yes }; + }; + const Exists::Bool exists; + const IsValid::Bool is_valid; + const HasMain::Bool has_main; + const std::string main; + const IsESM::Bool esm; }; } // namespace loader @@ -168,6 +177,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(env_var_settings_string, "envVarSettings") \ V(errno_string, "errno") \ V(error_string, "error") \ + V(esm_string, "esm") \ V(exchange_string, "exchange") \ V(exit_code_string, "exitCode") \ V(expire_string, "expire") \ @@ -206,6 +216,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(kill_signal_string, "killSignal") \ V(kind_string, "kind") \ V(library_string, "library") \ + V(legacy_string, "legacy") \ V(mac_string, "mac") \ V(main_string, "main") \ V(max_buffer_string, "maxBuffer") \ diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 656cf82074..ed6795bd8e 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -43,8 +43,6 @@ using v8::String; using v8::Undefined; using v8::Value; -static const char* const EXTENSIONS[] = {".mjs"}; - ModuleWrap::ModuleWrap(Environment* env, Local object, Local module, @@ -467,14 +465,30 @@ std::string ReadFile(uv_file file) { } enum DescriptorType { - NONE, FILE, - DIRECTORY + DIRECTORY, + NONE }; -DescriptorType CheckDescriptor(const std::string& path) { +// When DescriptorType cache is added, this can also return +// Nothing for the "null" cache entries. +inline Maybe OpenDescriptor(const std::string& path) { + uv_fs_t fs_req; + uv_file fd = uv_fs_open(nullptr, &fs_req, path.c_str(), O_RDONLY, 0, nullptr); + uv_fs_req_cleanup(&fs_req); + if (fd < 0) return Nothing(); + return Just(fd); +} + +inline void CloseDescriptor(uv_file fd) { + uv_fs_t fs_req; + uv_fs_close(nullptr, &fs_req, fd, nullptr); + uv_fs_req_cleanup(&fs_req); +} + +inline DescriptorType CheckDescriptorAtFile(uv_file fd) { uv_fs_t fs_req; - int rc = uv_fs_stat(nullptr, &fs_req, path.c_str(), nullptr); + int rc = uv_fs_fstat(nullptr, &fs_req, fd, nullptr); if (rc == 0) { uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; uv_fs_req_cleanup(&fs_req); @@ -484,27 +498,320 @@ DescriptorType CheckDescriptor(const std::string& path) { return NONE; } -Maybe PackageResolve(Environment* env, +// TODO(@guybedford): Add a DescriptorType cache layer here. +// Should be directory based -> if path/to/dir doesn't exist +// then the cache should early-fail any path/to/dir/file check. +DescriptorType CheckDescriptorAtPath(const std::string& path) { + Maybe fd = OpenDescriptor(path); + if (fd.IsNothing()) return NONE; + DescriptorType type = CheckDescriptorAtFile(fd.FromJust()); + CloseDescriptor(fd.FromJust()); + return type; +} + +Maybe ReadIfFile(const std::string& path) { + Maybe fd = OpenDescriptor(path); + if (fd.IsNothing()) return Nothing(); + DescriptorType type = CheckDescriptorAtFile(fd.FromJust()); + if (type != FILE) return Nothing(); + std::string source = ReadFile(fd.FromJust()); + CloseDescriptor(fd.FromJust()); + return Just(source); +} + +using Exists = PackageConfig::Exists; +using IsValid = PackageConfig::IsValid; +using HasMain = PackageConfig::HasMain; +using IsESM = PackageConfig::IsESM; + +Maybe GetPackageConfig(Environment* env, + const std::string& path, + const URL& base) { + auto existing = env->package_json_cache.find(path); + if (existing != env->package_json_cache.end()) { + return Just(&existing->second); + } + + Maybe source = ReadIfFile(path); + + if (source.IsNothing()) { + auto entry = env->package_json_cache.emplace(path, + PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", + IsESM::No }); + return Just(&entry.first->second); + } + + std::string pkg_src = source.FromJust(); + + Isolate* isolate = env->isolate(); + v8::HandleScope handle_scope(isolate); + + bool parsed = false; + Local pkg_json; + { + Local src; + Local pkg_json_v; + if (String::NewFromUtf8(isolate, + pkg_src.c_str(), + v8::NewStringType::kNormal, + pkg_src.length()).ToLocal(&src) && + v8::JSON::Parse(env->context(), src).ToLocal(&pkg_json_v) && + pkg_json_v->ToObject(env->context()).ToLocal(&pkg_json)) { + parsed = true; + } + } + + if (!parsed) { + (void)env->package_json_cache.emplace(path, + PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "", + IsESM::No }); + std::string msg = "Invalid JSON in '" + path + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); + return Nothing(); + } + + Local pkg_main; + HasMain::Bool has_main = HasMain::No; + std::string main_std; + if (pkg_json->Get(env->context(), env->main_string()).ToLocal(&pkg_main)) { + if (pkg_main->IsString()) { + has_main = HasMain::Yes; + } + Utf8Value main_utf8(isolate, pkg_main); + main_std.assign(std::string(*main_utf8, main_utf8.length())); + } + + IsESM::Bool esm = IsESM::No; + Local type_v; + if (pkg_json->Get(env->context(), env->type_string()).ToLocal(&type_v)) { + if (type_v->StrictEquals(env->esm_string())) { + esm = IsESM::Yes; + } + } + + Local exports_v; + if (pkg_json->Get(env->context(), + env->exports_string()).ToLocal(&exports_v) && + (exports_v->IsObject() || exports_v->IsString() || + exports_v->IsBoolean())) { + Persistent exports; + // esm = IsESM::Yes; + exports.Reset(env->isolate(), exports_v); + + auto entry = env->package_json_cache.emplace(path, + PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std, + esm }); + return Just(&entry.first->second); + } + + auto entry = env->package_json_cache.emplace(path, + PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std, + esm }); + return Just(&entry.first->second); +} + +Maybe GetPackageBoundaryConfig(Environment* env, + const URL& search, + const URL& base) { + URL pjson_url("package.json", &search); + while (true) { + Maybe pkg_cfg = + GetPackageConfig(env, pjson_url.ToFilePath(), base); + if (pkg_cfg.IsNothing()) return pkg_cfg; + if (pkg_cfg.FromJust()->exists == Exists::Yes) return pkg_cfg; + + URL last_pjson_url = pjson_url; + pjson_url = URL("../package.json", pjson_url); + + // Terminates at root where ../package.json equals ../../package.json + // (can't just check "/package.json" for Windows support). + if (pjson_url.path() == last_pjson_url.path()) { + auto entry = env->package_json_cache.emplace(pjson_url.ToFilePath(), + PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", + IsESM::No }); + return Just(&entry.first->second); + } + } +} + +/* + * Legacy CommonJS main resolution: + * 1. let M = pkg_url + (json main field) + * 2. TRY(M, M.js, M.json, M.node) + * 3. TRY(M/index.js, M/index.json, M/index.node) + * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) + * 5. NOT_FOUND + */ +inline bool FileExists(const URL& url) { + return CheckDescriptorAtPath(url.ToFilePath()) == FILE; +} +Maybe LegacyMainResolve(const URL& pjson_url, + const PackageConfig& pcfg) { + URL guess; + if (pcfg.has_main == HasMain::Yes) { + // Note: fs check redundances will be handled by Descriptor cache here. + if (FileExists(guess = URL("./" + pcfg.main, pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + ".js", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + ".json", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + ".node", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + "/index.js", pjson_url))) { + return Just(guess); + } + // Such stat. + if (FileExists(guess = URL("./" + pcfg.main + "/index.json", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + "/index.node", pjson_url))) { + return Just(guess); + } + // Fallthrough. + } + if (FileExists(guess = URL("./index.js", pjson_url))) { + return Just(guess); + } + // So fs. + if (FileExists(guess = URL("./index.json", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./index.node", pjson_url))) { + return Just(guess); + } + // Not found. + return Nothing(); +} + +Maybe FinalizeResolution(Environment* env, + const URL& resolved, + const URL& base, + bool check_exists, + bool is_main) { + const std::string& path = resolved.ToFilePath(); + + if (check_exists && CheckDescriptorAtPath(path) != FILE) { + std::string msg = "Cannot find module '" + path + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + return Nothing(); + } + + Maybe pcfg = + GetPackageBoundaryConfig(env, resolved, base); + if (pcfg.IsNothing()) return Nothing(); + + if (pcfg.FromJust()->exists == Exists::No) { + return Just(ModuleResolution { resolved, true }); + } + + return Just(ModuleResolution { + resolved, pcfg.FromJust()->esm == IsESM::No }); +} + +Maybe PackageMainResolve(Environment* env, + const URL& pjson_url, + const PackageConfig& pcfg, + const URL& base) { + if (pcfg.exists == Exists::No || ( + pcfg.esm == IsESM::Yes && pcfg.has_main == HasMain::No)) { + std::string msg = "Cannot find main entry point for '" + + URL(".", pjson_url).ToFilePath() + "' imported from " + + base.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + return Nothing(); + } + if (pcfg.has_main == HasMain::Yes && + pcfg.main.substr(pcfg.main.length() - 4, 4) == ".mjs") { + return FinalizeResolution(env, URL(pcfg.main, pjson_url), base, true, + true); + } + if (pcfg.esm == IsESM::Yes && + pcfg.main.substr(pcfg.main.length() - 3, 3) == ".js") { + return FinalizeResolution(env, URL(pcfg.main, pjson_url), base, true, + true); + } + + Maybe resolved = LegacyMainResolve(pjson_url, pcfg); + // Legacy main resolution error + if (resolved.IsNothing()) { + return Nothing(); + } + return FinalizeResolution(env, resolved.FromJust(), base, false, true); +} + +Maybe PackageResolve(Environment* env, const std::string& specifier, const URL& base) { - URL parent(".", base); + size_t sep_index = specifier.find('/'); + if (specifier[0] == '@' && (sep_index == std::string::npos || + specifier.length() == 0)) { + std::string msg = "Invalid package name '" + specifier + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_INVALID_MODULE_SPECIFIER(env, msg.c_str()); + return Nothing(); + } + bool scope = false; + if (specifier[0] == '@') { + scope = true; + sep_index = specifier.find('/', sep_index + 1); + } + std::string pkg_name = specifier.substr(0, + sep_index == std::string::npos ? std::string::npos : sep_index); + std::string pkg_subpath; + if ((sep_index == std::string::npos || + sep_index == specifier.length() - 1)) { + pkg_subpath = ""; + } else { + pkg_subpath = "." + specifier.substr(sep_index); + } + URL pjson_url("./node_modules/" + pkg_name + "/package.json", &base); + std::string pjson_path = pjson_url.ToFilePath(); std::string last_path; do { - URL pkg_url("./node_modules/" + specifier, &parent); - DescriptorType check = CheckDescriptor(pkg_url.ToFilePath()); - if (check == FILE) return Just(pkg_url); - last_path = parent.path(); - parent = URL("..", &parent); - // cross-platform root check - } while (parent.path() != last_path); - return Nothing(); + DescriptorType check = + CheckDescriptorAtPath(pjson_path.substr(0, pjson_path.length() - 13)); + if (check != DIRECTORY) { + last_path = pjson_path; + pjson_url = URL((scope ? + "../../../../node_modules/" : "../../../node_modules/") + + pkg_name + "/package.json", &pjson_url); + pjson_path = pjson_url.ToFilePath(); + continue; + } + + // Package match. + Maybe pcfg = GetPackageConfig(env, pjson_path, base); + // Invalid package configuration error. + if (pcfg.IsNothing()) return Nothing(); + if (!pkg_subpath.length()) { + return PackageMainResolve(env, pjson_url, *pcfg.FromJust(), base); + } else { + return FinalizeResolution(env, URL(pkg_subpath, pjson_url), + base, true, false); + } + CHECK(false); + // Cross-platform root check. + } while (pjson_url.path().length() != last_path.length()); + + std::string msg = "Cannot find package '" + pkg_name + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + return Nothing(); } } // anonymous namespace -Maybe Resolve(Environment* env, - const std::string& specifier, - const URL& base) { +Maybe Resolve(Environment* env, + const std::string& specifier, + const URL& base, + bool is_main) { // Order swapped from spec for minor perf gain. // Ok since relative URLs cannot parse as URLs. URL resolved; @@ -518,21 +825,14 @@ Maybe Resolve(Environment* env, return PackageResolve(env, specifier, base); } } - DescriptorType check = CheckDescriptor(resolved.ToFilePath()); - if (check != FILE) { - std::string msg = "Cannot find module '" + resolved.ToFilePath() + - "' imported from " + base.ToFilePath(); - node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); - return Nothing(); - } - return Just(resolved); + return FinalizeResolution(env, resolved, base, true, is_main); } void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - // module.resolve(specifier, url) - CHECK_EQ(args.Length(), 2); + // module.resolve(specifier, url, is_main) + CHECK_EQ(args.Length(), 3); CHECK(args[0]->IsString()); Utf8Value specifier_utf8(env->isolate(), args[0]); @@ -542,28 +842,41 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { Utf8Value url_utf8(env->isolate(), args[1]); URL url(*url_utf8, url_utf8.length()); + CHECK(args[2]->IsBoolean()); + if (url.flags() & URL_FLAGS_FAILED) { return node::THROW_ERR_INVALID_ARG_TYPE( env, "second argument is not a URL string"); } TryCatchScope try_catch(env); - Maybe result = node::loader::Resolve(env, specifier_std, url); - if (try_catch.HasCaught()) { - try_catch.ReThrow(); - return; - } else if (result.IsNothing() || - (result.FromJust().flags() & URL_FLAGS_FAILED)) { - std::string msg = "Cannot find module '" + specifier_std + - "' imported from " + url.ToFilePath(); - node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + Maybe result = + node::loader::Resolve(env, specifier_std, url, args[2]->IsTrue()); + if (result.IsNothing()) { + CHECK(try_catch.HasCaught()); try_catch.ReThrow(); return; } + CHECK(!try_catch.HasCaught()); + + ModuleResolution resolution = result.FromJust(); + CHECK(!(resolution.url.flags() & URL_FLAGS_FAILED)); + + Local resolved = Object::New(env->isolate()); + + resolved->DefineOwnProperty( + env->context(), + env->url_string(), + resolution.url.ToObject(env).ToLocalChecked(), + v8::ReadOnly).FromJust(); + + resolved->DefineOwnProperty( + env->context(), + env->legacy_string(), + v8::Boolean::New(env->isolate(), resolution.legacy), + v8::ReadOnly).FromJust(); - MaybeLocal obj = result.FromJust().ToObject(env); - if (!obj.IsEmpty()) - args.GetReturnValue().Set(obj.ToLocalChecked()); + args.GetReturnValue().Set(resolved); } static MaybeLocal ImportModuleDynamically( diff --git a/src/module_wrap.h b/src/module_wrap.h index 8a5592d3f2..f5e6eef94e 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -23,9 +23,14 @@ enum HostDefinedOptions : int { kLength = 10, }; -v8::Maybe Resolve(Environment* env, - const std::string& specifier, - const url::URL& base); +struct ModuleResolution { + url::URL url; + bool legacy; +}; + +v8::Maybe Resolve(Environment* env, + const std::string& specifier, + const url::URL& base); class ModuleWrap : public BaseObject { public: diff --git a/src/node_errors.h b/src/node_errors.h index 3010ddf594..3b849abed4 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -59,6 +59,8 @@ void FatalException(const v8::FunctionCallbackInfo& args); V(ERR_CONSTRUCT_CALL_REQUIRED, Error) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ + V(ERR_INVALID_MODULE_SPECIFIER, TypeError) \ + V(ERR_INVALID_PACKAGE_CONFIG, SyntaxError) \ V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ V(ERR_MISSING_ARGS, TypeError) \ diff --git a/test/message/esm_display_syntax_error.out b/test/message/esm_display_syntax_error.out index ed2e928eb1..2700fd894c 100644 --- a/test/message/esm_display_syntax_error.out +++ b/test/message/esm_display_syntax_error.out @@ -3,4 +3,4 @@ file:///*/test/message/esm_display_syntax_error.mjs:3 await async () => 0; ^^^^^ SyntaxError: Unexpected reserved word - at translators.set (internal/modules/esm/translators.js:*:*) + at Loader. (internal/modules/esm/translators.js:*:*)