diff --git a/lib/internal/loader/ModuleWrap.js b/lib/internal/loader/CreateDynamicModule.js similarity index 90% rename from lib/internal/loader/ModuleWrap.js rename to lib/internal/loader/CreateDynamicModule.js index f0b9e2f757a7bd..f2596de04bfcb3 100644 --- a/lib/internal/loader/ModuleWrap.js +++ b/lib/internal/loader/CreateDynamicModule.js @@ -1,9 +1,6 @@ 'use strict'; -const { - ModuleWrap, - setImportModuleDynamicallyCallback -} = internalBinding('module_wrap'); +const { ModuleWrap } = internalBinding('module_wrap'); const debug = require('util').debuglog('esm'); const ArrayJoin = Function.call.bind(Array.prototype.join); const ArrayMap = Function.call.bind(Array.prototype.map); @@ -60,8 +57,4 @@ const createDynamicModule = (exports, url = '', evaluate) => { }; }; -module.exports = { - createDynamicModule, - setImportModuleDynamicallyCallback, - ModuleWrap -}; +module.exports = createDynamicModule; diff --git a/lib/internal/loader/DefaultResolve.js b/lib/internal/loader/DefaultResolve.js new file mode 100644 index 00000000000000..faadf0bc99ef95 --- /dev/null +++ b/lib/internal/loader/DefaultResolve.js @@ -0,0 +1,84 @@ +'use strict'; + +const { URL } = require('url'); +const CJSmodule = require('module'); +const internalURLModule = require('internal/url'); +const internalFS = require('internal/fs'); +const NativeModule = require('native_module'); +const { extname } = require('path'); +const { realpathSync } = require('fs'); +const preserveSymlinks = !!process.binding('config').preserveSymlinks; +const errors = require('internal/errors'); +const { resolve: moduleWrapResolve } = internalBinding('module_wrap'); +const StringStartsWith = Function.call.bind(String.prototype.startsWith); + +const realpathCache = new Map(); + +function search(target, base) { + if (base === undefined) { + // We cannot search without a base. + throw new errors.Error('ERR_MISSING_MODULE', target); + } + 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 errors.Error('ERR_MODULE_RESOLUTION_LEGACY', target, + base, found); + } catch (problemChecking) { + // ignore + } + throw error; + } +} + +const extensionFormatMap = { + __proto__: null, + '.mjs': 'esm', + '.json': 'json', + '.node': 'addon', + '.js': 'commonjs' +}; + +function resolve(specifier, parentURL) { + if (NativeModule.nonInternalExists(specifier)) { + return { + url: specifier, + format: 'builtin' + }; + } + + let url; + try { + url = search(specifier, parentURL); + } catch (e) { + if (typeof e.message === 'string' && + StringStartsWith(e.message, 'Cannot find module')) + e.code = 'MODULE_NOT_FOUND'; + throw e; + } + + if (!preserveSymlinks) { + const real = realpathSync(internalURLModule.getPathFromURL(url), { + [internalFS.realpathCacheKey]: realpathCache + }); + const old = url; + url = internalURLModule.getURLFromFilePath(real); + url.search = old.search; + url.hash = old.hash; + } + + const ext = extname(url.pathname); + return { url: `${url}`, format: extensionFormatMap[ext] || ext }; +} + +module.exports = resolve; +// exported for tests +module.exports.search = search; diff --git a/lib/internal/loader/Loader.js b/lib/internal/loader/Loader.js index b8fe9ccfc614b8..2e6a69af316261 100644 --- a/lib/internal/loader/Loader.js +++ b/lib/internal/loader/Loader.js @@ -2,16 +2,16 @@ const path = require('path'); const { getURLFromFilePath, URL } = require('internal/url'); - -const { - createDynamicModule, - setImportModuleDynamicallyCallback -} = require('internal/loader/ModuleWrap'); +const errors = require('internal/errors'); const ModuleMap = require('internal/loader/ModuleMap'); const ModuleJob = require('internal/loader/ModuleJob'); -const ModuleRequest = require('internal/loader/ModuleRequest'); -const errors = require('internal/errors'); +const defaultResolve = require('internal/loader/DefaultResolve'); +const createDynamicModule = require('internal/loader/CreateDynamicModule'); +const translators = require('internal/loader/Translators'); +const { setImportModuleDynamicallyCallback } = internalBinding('module_wrap'); +const FunctionBind = Function.call.bind(Function.prototype.bind); + const debug = require('util').debuglog('esm'); // Returns a file URL for the current working directory. @@ -40,105 +40,101 @@ function normalizeReferrerURL(referrer) { * the main module and everything in its dependency graph. */ class Loader { constructor(base = getURLStringForCwd()) { - if (typeof base !== 'string') { + if (typeof base !== 'string') throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string'); - } - this.moduleMap = new ModuleMap(); this.base = base; + + // methods which translate input code or other information + // into es modules + this.translators = translators; + + // registry of loaded modules, akin to `require.cache` + this.moduleMap = new ModuleMap(); + // The resolver has the signature // (specifier : string, parentURL : string, defaultResolve) - // -> Promise<{ url : string, - // format: anything in Loader.validFormats }> + // -> Promise<{ url : string, format: string }> // where defaultResolve is ModuleRequest.resolve (having the same // signature itself). // If `.format` on the returned value is 'dynamic', .dynamicInstantiate // will be used as described below. - this.resolver = ModuleRequest.resolve; - // This hook is only called when resolve(...).format is 'dynamic' and has - // the signature + this._resolve = defaultResolve; + // This hook is only called when resolve(...).format is 'dynamic' and + // has the signature // (url : string) -> Promise<{ exports: { ... }, execute: function }> // Where `exports` is an object whose property names define the exported // names of the generated module. `execute` is a function that receives // an object with the same keys as `exports`, whose values are get/set // functions for the actual exported values. - this.dynamicInstantiate = undefined; - } - - hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) { - // Use .bind() to avoid giving access to the Loader instance when it is - // called as this.resolver(...); - this.resolver = resolve.bind(null); - this.dynamicInstantiate = dynamicInstantiate; + this._dynamicInstantiate = undefined; } - // Typechecking wrapper around .resolver(). async resolve(specifier, parentURL = this.base) { - if (typeof parentURL !== 'string') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'parentURL', 'string'); - } - - const { url, format } = await this.resolver(specifier, parentURL, - ModuleRequest.resolve); + if (typeof parentURL !== 'string') + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'parentURL', 'string'); - if (!Loader.validFormats.includes(format)) { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format', - Loader.validFormats); - } + const { url, format } = + await this._resolve(specifier, parentURL, defaultResolve); - if (typeof url !== 'string') { + if (typeof url !== 'string') throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'url', 'string'); - } - if (format === 'builtin') { + if (typeof format !== 'string') + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'format', 'string'); + + if (format === 'builtin') return { url: `node:${url}`, format }; - } - if (format !== 'dynamic') { - if (!ModuleRequest.loaders.has(format)) { - throw new errors.Error('ERR_UNKNOWN_MODULE_FORMAT', format); - } - if (!url.startsWith('file:')) { - throw new errors.Error('ERR_INVALID_PROTOCOL', url, 'file:'); - } - } + if (format !== 'dynamic' && !url.startsWith('file:')) + throw new errors.Error('ERR_INVALID_PROTOCOL', url, 'file:'); return { url, format }; } - // May create a new ModuleJob instance if one did not already exist. + async import(specifier, parent = this.base) { + const job = await this.getModuleJob(specifier, parent); + const module = await job.run(); + return module.namespace(); + } + + hook({ resolve, dynamicInstantiate }) { + // Use .bind() to avoid giving access to the Loader instance when called. + if (resolve !== undefined) + this._resolve = FunctionBind(resolve, null); + if (dynamicInstantiate !== undefined) + this._dynamicInstantiate = FunctionBind(dynamicInstantiate, null); + } + async getModuleJob(specifier, parentURL = this.base) { const { url, format } = await this.resolve(specifier, parentURL); let job = this.moduleMap.get(url); - if (job === undefined) { - let loaderInstance; - if (format === 'dynamic') { - const { dynamicInstantiate } = this; - if (typeof dynamicInstantiate !== 'function') { - throw new errors.Error('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK'); - } - - loaderInstance = async (url) => { - const { exports, execute } = await dynamicInstantiate(url); - return createDynamicModule(exports, url, (reflect) => { - debug(`Loading custom loader ${url}`); - execute(reflect.exports); - }); - }; - } else { - loaderInstance = ModuleRequest.loaders.get(format); - } - job = new ModuleJob(this, url, loaderInstance); - this.moduleMap.set(url, job); + if (job !== undefined) + return job; + + let loaderInstance; + if (format === 'dynamic') { + if (typeof this._dynamicInstantiate !== 'function') + throw new errors.Error('ERR_MISSING_DYNAMIC_INTSTANTIATE_HOOK'); + + loaderInstance = async (url) => { + debug(`Translating dynamic ${url}`); + const { exports, execute } = await this._dynamicInstantiate(url); + return createDynamicModule(exports, url, (reflect) => { + debug(`Loading dynamic ${url}`); + execute(reflect.exports); + }); + }; + } else { + if (!translators.has(format)) + throw new errors.RangeError('ERR_UNKNOWN_MODULE_FORMAT', format); + + loaderInstance = translators.get(format); } - return job; - } - async import(specifier, parentURL = this.base) { - const job = await this.getModuleJob(specifier, parentURL); - const module = await job.run(); - return module.namespace(); + job = new ModuleJob(this, url, loaderInstance); + this.moduleMap.set(url, job); + return job; } static registerImportDynamicallyCallback(loader) { @@ -147,6 +143,6 @@ class Loader { }); } } -Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic']; + Object.setPrototypeOf(Loader.prototype, null); module.exports = Loader; diff --git a/lib/internal/loader/ModuleRequest.js b/lib/internal/loader/ModuleRequest.js deleted file mode 100644 index 93baf2a597fc1a..00000000000000 --- a/lib/internal/loader/ModuleRequest.js +++ /dev/null @@ -1,138 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const internalCJSModule = require('internal/module'); -const CJSModule = require('module'); -const internalURLModule = require('internal/url'); -const internalFS = require('internal/fs'); -const NativeModule = require('native_module'); -const { extname, _makeLong } = require('path'); -const { URL } = require('url'); -const { realpathSync } = require('fs'); -const preserveSymlinks = !!process.binding('config').preserveSymlinks; -const { - ModuleWrap, - createDynamicModule -} = require('internal/loader/ModuleWrap'); -const errors = require('internal/errors'); - -const search = require('internal/loader/search'); -const asyncReadFile = require('util').promisify(require('fs').readFile); -const debug = require('util').debuglog('esm'); - -const realpathCache = new Map(); - -const loaders = new Map(); -exports.loaders = loaders; - -// Strategy for loading a standard JavaScript module -loaders.set('esm', async (url) => { - const source = `${await asyncReadFile(new URL(url))}`; - debug(`Loading StandardModule ${url}`); - return { - module: new ModuleWrap(internalCJSModule.stripShebang(source), url), - reflect: undefined - }; -}); - -// Strategy for loading a node-style CommonJS module -const isWindows = process.platform === 'win32'; -const winSepRegEx = /\//g; -loaders.set('cjs', async (url) => { - const pathname = internalURLModule.getPathFromURL(new URL(url)); - const module = CJSModule._cache[ - isWindows ? pathname.replace(winSepRegEx, '\\') : pathname]; - if (module && module.loaded) { - const ctx = createDynamicModule(['default'], url, undefined); - ctx.reflect.exports.default.set(module.exports); - return ctx; - } - return createDynamicModule(['default'], url, (reflect) => { - debug(`Loading CJSModule ${url}`); - CJSModule._load(pathname); - }); -}); - -// Strategy for loading a node builtin CommonJS module that isn't -// through normal resolution -loaders.set('builtin', async (url) => { - return createDynamicModule(['default'], url, (reflect) => { - debug(`Loading BuiltinModule ${url}`); - const exports = NativeModule.require(url.substr(5)); - reflect.exports.default.set(exports); - }); -}); - -loaders.set('addon', async (url) => { - const ctx = createDynamicModule(['default'], url, (reflect) => { - debug(`Loading NativeModule ${url}`); - const module = { exports: {} }; - const pathname = internalURLModule.getPathFromURL(new URL(url)); - process.dlopen(module, _makeLong(pathname)); - reflect.exports.default.set(module.exports); - }); - return ctx; -}); - -loaders.set('json', async (url) => { - return createDynamicModule(['default'], url, (reflect) => { - debug(`Loading JSONModule ${url}`); - const pathname = internalURLModule.getPathFromURL(new URL(url)); - const content = fs.readFileSync(pathname, 'utf8'); - try { - const exports = JSON.parse(internalCJSModule.stripBOM(content)); - reflect.exports.default.set(exports); - } catch (err) { - err.message = pathname + ': ' + err.message; - throw err; - } - }); -}); - -exports.resolve = (specifier, parentURL) => { - if (NativeModule.nonInternalExists(specifier)) { - return { - url: specifier, - format: 'builtin' - }; - } - - let url; - try { - url = search(specifier, parentURL); - } catch (e) { - if (e.message && e.message.startsWith('Cannot find module')) - e.code = 'MODULE_NOT_FOUND'; - throw e; - } - - if (url.protocol !== 'file:') { - throw new errors.Error('ERR_INVALID_PROTOCOL', - url.protocol, 'file:'); - } - - if (!preserveSymlinks) { - const real = realpathSync(internalURLModule.getPathFromURL(url), { - [internalFS.realpathCacheKey]: realpathCache - }); - const old = url; - url = internalURLModule.getURLFromFilePath(real); - url.search = old.search; - url.hash = old.hash; - } - - const ext = extname(url.pathname); - switch (ext) { - case '.mjs': - return { url: `${url}`, format: 'esm' }; - case '.json': - return { url: `${url}`, format: 'json' }; - case '.node': - return { url: `${url}`, format: 'addon' }; - case '.js': - return { url: `${url}`, format: 'cjs' }; - default: - throw new errors.Error('ERR_UNKNOWN_FILE_EXTENSION', - internalURLModule.getPathFromURL(url)); - } -}; diff --git a/lib/internal/loader/Translators.js b/lib/internal/loader/Translators.js new file mode 100644 index 00000000000000..f1db8334dd5eea --- /dev/null +++ b/lib/internal/loader/Translators.js @@ -0,0 +1,92 @@ +'use strict'; + +const { ModuleWrap } = internalBinding('module_wrap'); +const NativeModule = require('native_module'); +const internalCJSModule = require('internal/module'); +const CJSModule = require('module'); +const internalURLModule = require('internal/url'); +const createDynamicModule = require('internal/loader/CreateDynamicModule'); +const fs = require('fs'); +const { _makeLong } = require('path'); +const { SafeMap } = require('internal/safe_globals'); +const { URL } = require('url'); +const debug = require('util').debuglog('esm'); +const readFileAsync = require('util').promisify(fs.readFile); +const readFileSync = fs.readFileSync; +const StringReplace = Function.call.bind(String.prototype.replace); +const JsonParse = JSON.parse; + +const translators = new SafeMap(); +module.exports = translators; + +// Stragety for loading a standard JavaScript module +translators.set('esm', async (url) => { + const source = `${await readFileAsync(new URL(url))}`; + debug(`Translating StandardModule ${url}`); + return { + module: new ModuleWrap(internalCJSModule.stripShebang(source), url), + reflect: undefined + }; +}); + +// Strategy for loading a node-style CommonJS module +const isWindows = process.platform === 'win32'; +const winSepRegEx = /\//g; +translators.set('commonjs', async (url) => { + debug(`Translating CJSModule ${url}`); + const pathname = internalURLModule.getPathFromURL(new URL(url)); + const module = CJSModule._cache[ + isWindows ? StringReplace(pathname, winSepRegEx, '\\') : pathname]; + if (module && module.loaded) { + const ctx = createDynamicModule(['default'], url); + ctx.reflect.exports.default.set(module.exports); + return ctx; + } + return createDynamicModule(['default'], url, () => { + debug(`Loading CJSModule ${url}`); + // we don't care about the return val of _load here because Module#load + // will handle it for us by checking the loader registry and filling the + // exports like above + CJSModule._load(pathname); + }); +}); + +// Strategy for loading a node builtin CommonJS module that isn't +// through normal resolution +translators.set('builtin', async (url) => { + debug(`Translating BuiltinModule ${url}`); + return createDynamicModule(['default'], url, (reflect) => { + debug(`Loading BuiltinModule ${url}`); + const exports = NativeModule.require(url.slice(5)); + reflect.exports.default.set(exports); + }); +}); + +// Stragety for loading a node native module +translators.set('addon', async (url) => { + debug(`Translating NativeModule ${url}`); + return createDynamicModule(['default'], url, (reflect) => { + debug(`Loading NativeModule ${url}`); + const module = { exports: {} }; + const pathname = internalURLModule.getPathFromURL(new URL(url)); + process.dlopen(module, _makeLong(pathname)); + reflect.exports.default.set(module.exports); + }); +}); + +// Stragety for loading a JSON file +translators.set('json', async (url) => { + debug(`Translating JSONModule ${url}`); + return createDynamicModule(['default'], url, (reflect) => { + debug(`Loading JSONModule ${url}`); + const pathname = internalURLModule.getPathFromURL(new URL(url)); + const content = readFileSync(pathname, 'utf8'); + try { + const exports = JsonParse(internalCJSModule.stripBOM(content)); + reflect.exports.default.set(exports); + } catch (err) { + err.message = pathname + ': ' + err.message; + throw err; + } + }); +}); diff --git a/lib/internal/loader/search.js b/lib/internal/loader/search.js deleted file mode 100644 index ea46f92dfe8244..00000000000000 --- a/lib/internal/loader/search.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const { URL } = require('url'); -const CJSmodule = require('module'); -const errors = require('internal/errors'); -const { resolve } = internalBinding('module_wrap'); - -module.exports = (target, base) => { - if (base === undefined) { - // We cannot search without a base. - throw new errors.Error('ERR_MISSING_MODULE', target); - } - try { - return resolve(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 errors.Error('ERR_MODULE_RESOLUTION_LEGACY', target, - base, found); - } catch (problemChecking) { - // ignore - } - throw error; - } -}; diff --git a/lib/module.js b/lib/module.js index e6082cb262c3f0..63e878b17e0f4c 100644 --- a/lib/module.js +++ b/lib/module.js @@ -45,7 +45,7 @@ module.exports = Module; // these are below module.exports for the circular reference const Loader = require('internal/loader/Loader'); const ModuleJob = require('internal/loader/ModuleJob'); -const { createDynamicModule } = require('internal/loader/ModuleWrap'); +const createDynamicModule = require('internal/loader/CreateDynamicModule'); let ESMLoader; function stat(filename) { diff --git a/node.gyp b/node.gyp index ca5eb730129224..f79aee554c6bca 100644 --- a/node.gyp +++ b/node.gyp @@ -104,11 +104,11 @@ 'lib/internal/inspector_async_hook.js', 'lib/internal/linkedlist.js', 'lib/internal/loader/Loader.js', - 'lib/internal/loader/ModuleMap.js', + 'lib/internal/loader/CreateDynamicModule.js', + 'lib/internal/loader/DefaultResolve.js', 'lib/internal/loader/ModuleJob.js', - 'lib/internal/loader/ModuleWrap.js', - 'lib/internal/loader/ModuleRequest.js', - 'lib/internal/loader/search.js', + 'lib/internal/loader/ModuleMap.js', + 'lib/internal/loader/Translators.js', 'lib/internal/safe_globals.js', 'lib/internal/net.js', 'lib/internal/module.js', diff --git a/test/es-module/test-esm-loader-modulemap.js b/test/es-module/test-esm-loader-modulemap.js index 58c76ce960657c..1c1623b680e7bd 100644 --- a/test/es-module/test-esm-loader-modulemap.js +++ b/test/es-module/test-esm-loader-modulemap.js @@ -10,7 +10,7 @@ const { URL } = require('url'); const Loader = require('internal/loader/Loader'); const ModuleMap = require('internal/loader/ModuleMap'); const ModuleJob = require('internal/loader/ModuleJob'); -const { createDynamicModule } = require('internal/loader/ModuleWrap'); +const createDynamicModule = require('internal/loader/CreateDynamicModule'); const stubModuleUrl = new URL('file://tmp/test'); const stubModule = createDynamicModule(['default'], stubModuleUrl); diff --git a/test/es-module/test-esm-loader-search.js b/test/es-module/test-esm-loader-search.js index c8c24f1deb80ae..4eb6b9fd4b3889 100644 --- a/test/es-module/test-esm-loader-search.js +++ b/test/es-module/test-esm-loader-search.js @@ -5,7 +5,7 @@ const common = require('../common'); -const search = require('internal/loader/search'); +const { search } = require('internal/loader/DefaultResolve'); const errors = require('internal/errors'); common.expectsError( diff --git a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs index 0734003802e3bb..28ccd6ecf2076f 100644 --- a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs +++ b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs @@ -3,19 +3,9 @@ import module from 'module'; const builtins = new Set( Object.keys(process.binding('natives')).filter(str => /^(?!(?:internal|node|v8)\/)/.test(str)) -) +); -export function resolve (specifier, base, defaultResolver) { - if (builtins.has(specifier)) { - return { - url: `node:${specifier}`, - format: 'dynamic' - }; - } - return defaultResolver(specifier, base); -} - -export async function dynamicInstantiate (url) { +export function dynamicInstantiate(url) { const builtinInstance = module._load(url.substr(5)); const builtinExports = ['default', ...Object.keys(builtinInstance)]; return { @@ -27,3 +17,13 @@ export async function dynamicInstantiate (url) { } }; } + +export function resolve(specifier, base, defaultResolver) { + if (builtins.has(specifier)) { + return { + url: `node:${specifier}`, + format: 'dynamic' + }; + } + return defaultResolver(specifier, base); +} diff --git a/test/message/esm_display_syntax_error.out b/test/message/esm_display_syntax_error.out index 0ca2bba5494470..a47a7e1339273f 100644 --- a/test/message/esm_display_syntax_error.out +++ b/test/message/esm_display_syntax_error.out @@ -3,5 +3,5 @@ file:///*/test/message/esm_display_syntax_error.mjs:3 await async () => 0; ^^^^^ SyntaxError: Unexpected reserved word - at loaders.set (internal/loader/ModuleRequest.js:*:*) + at translators.set (internal/loader/Translators.js:*:*) at diff --git a/test/message/esm_display_syntax_error_module.out b/test/message/esm_display_syntax_error_module.out index a76b72bdb69b63..23da9f350e5efe 100644 --- a/test/message/esm_display_syntax_error_module.out +++ b/test/message/esm_display_syntax_error_module.out @@ -3,5 +3,5 @@ file:///*/test/fixtures/es-module-loaders/syntax-error.mjs:2 await async () => 0; ^^^^^ SyntaxError: Unexpected reserved word - at loaders.set (internal/loader/ModuleRequest.js:*:*) + at translators.set (internal/loader/Translators.js:*:*) at diff --git a/test/parallel/test-internal-module-map-asserts.js b/test/parallel/test-internal-module-map-asserts.js index 99be2efedd2de9..1160c910421b80 100644 --- a/test/parallel/test-internal-module-map-asserts.js +++ b/test/parallel/test-internal-module-map-asserts.js @@ -5,7 +5,7 @@ const common = require('../common'); const assert = require('assert'); const ModuleMap = require('internal/loader/ModuleMap'); -// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string +// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string // values as url argument. { const errorReg = common.expectsError({