diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index 9ab6f18f3fdda9..d0485963da4d9d 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -64,6 +64,33 @@ async function getSource(url, context) { return { __proto__: null, responseURL, source }; } +function getSourceSync(url, context) { + const parsed = new URL(url); + const responseURL = url; + let source; + if (parsed.protocol === 'file:') { + const { readFileSync } = require('fs'); + source = readFileSync(parsed); + } else if (parsed.protocol === 'data:') { + const match = RegExpPrototypeExec(DATA_URL_PATTERN, parsed.pathname); + if (!match) { + throw new ERR_INVALID_URL(url); + } + const { 1: base64, 2: body } = match; + source = BufferFrom(decodeURIComponent(body), base64 ? 'base64' : 'utf8'); + } else { + const supportedSchemes = ['file', 'data']; + if (experimentalNetworkImports) { + ArrayPrototypePush(supportedSchemes, 'http', 'https'); + } + throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, supportedSchemes); + } + if (policy?.manifest) { + policy.manifest.assertIntegrity(parsed, source); + } + return { __proto__: null, responseURL, source }; +} + /** * Node.js default load hook. @@ -82,15 +109,12 @@ async function defaultLoad(url, context = kEmptyObject) { throwIfUnsupportedURLScheme(new URL(url), experimentalNetworkImports); if (format == null) { - format = await defaultGetFormat(url, context); + format = defaultGetFormat(url, context); } validateAssertions(url, format, importAssertions); - if ( - format === 'builtin' || - format === 'commonjs' - ) { + if (format === 'builtin') { source = null; } else if (source == null) { ({ responseURL, source } = await getSource(url, context)); @@ -104,6 +128,41 @@ async function defaultLoad(url, context = kEmptyObject) { }; } +/** + * Node.js default load hook. + * @param {string} url + * @param {object} context + * @returns {object} + */ +function defaultLoadSync(url, context = kEmptyObject) { + let responseURL = url; + const { importAssertions } = context; + let { + format, + source, + } = context; + + if (format == null) { + format = defaultGetFormat(url, context); + } + + validateAssertions(url, format, importAssertions); + + if (format === 'builtin') { + source = null; + } else if (source == null) { + ({ responseURL, source } = getSourceSync(url, context)); + } + + return { + __proto__: null, + format, + responseURL, + source, + }; +} + + /** * throws an error if the protocol is not one of the protocols * that can be loaded in the default loader @@ -155,5 +214,6 @@ function throwUnknownModuleFormat(url, format) { module.exports = { defaultLoad, + defaultLoadSync, throwUnknownModuleFormat, }; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 9d94b7275fdbdd..7610e6276c7963 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -22,7 +22,7 @@ const { emitExperimentalWarning } = require('internal/util'); const { getDefaultConditions, } = require('internal/modules/esm/utils'); -let defaultResolve, defaultLoad, importMetaInitializer; +let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer; function newModuleMap() { const ModuleMap = require('internal/modules/esm/module_map'); @@ -152,12 +152,12 @@ class DefaultModuleLoader { * module import. * @returns {ModuleJob} The (possibly pending) module job */ - getModuleJob(specifier, parentURL, importAssertions) { + getModuleJob(specifier, parentURL, importAssertions, sync = false) { const resolveResult = this.resolve(specifier, parentURL, importAssertions); - return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); + return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions, sync); } - getJobFromResolveResult(resolveResult, parentURL, importAssertions) { + getJobFromResolveResult(resolveResult, parentURL, importAssertions, sync) { const { url, format } = resolveResult; const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions; @@ -169,7 +169,7 @@ class DefaultModuleLoader { } if (job === undefined) { - job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format); + job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format, sync); } return job; @@ -186,8 +186,25 @@ class DefaultModuleLoader { * `resolve` hook * @returns {Promise} The (possibly pending) module job */ - #createModuleJob(url, importAssertions, parentURL, format) { - const moduleProvider = async (url, isMain) => { + #createModuleJob(url, importAssertions, parentURL, format, sync) { + const moduleProvider = sync ? (url, isMain) => { + const { + format: finalFormat, + responseURL, + source, + } = this.loadSync(url, { + format, + importAssertions, + }); + + const translator = getTranslators().get(finalFormat); + + if (!translator) { + throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); + } + + return FunctionPrototypeCall(translator, this, responseURL, source, isMain); + } : async (url, isMain) => { const { format: finalFormat, responseURL, @@ -319,6 +336,15 @@ class DefaultModuleLoader { return result; } + loadSync(url, context) { + defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync; + + const result = defaultLoadSync(url, context); + result.format = `require-${result?.format}`; + this.validateLoadResult(url, result?.format); + return result; + } + validateLoadResult(url, format) { if (format == null) { require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); @@ -393,6 +419,12 @@ class CustomizedModuleLoader extends DefaultModuleLoader { const result = await hooksProxy.makeAsyncRequest('load', url, context); this.validateLoadResult(url, result?.format); + return result; + } + loadSync(url, context) { + const result = hooksProxy.makeSyncRequest('load', url, context); + this.validateLoadResult(url, result?.format); + return result; } } diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 2cf2813a6dcf7f..a7f767ff008bc6 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -57,6 +57,8 @@ class ModuleJob { this.isMain = isMain; this.inspectBrk = inspectBrk; + this.url = url; + this.module = undefined; // Expose the promise to the ModuleWrap directly for linking below. // `this.module` is also filled in below. @@ -184,6 +186,17 @@ class ModuleJob { } } + runSync() { + assert(this.modulePromise instanceof ModuleWrap); + + // TODO: better handle errors + this.modulePromise.instantiate(); + const timeout = -1; + const breakOnSigint = false; + this.modulePromise.evaluate(timeout, breakOnSigint); + return { __proto__: null, module: this.modulePromise }; + } + async run() { await this.instantiate(); const timeout = -1; diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 267d89f1d44730..eafde2aa07e9f3 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -1,13 +1,13 @@ 'use strict'; const { - ArrayPrototypeForEach, ArrayPrototypeMap, - Boolean, + Function, JSONParse, ObjectGetPrototypeOf, ObjectPrototypeHasOwnProperty, ObjectKeys, + ReflectApply, SafeArrayIterator, SafeMap, SafeSet, @@ -25,7 +25,7 @@ function lazyTypes() { } const { readFileSync } = require('fs'); -const { extname, isAbsolute } = require('path'); +const { dirname, join } = require('path'); const { hasEsmSyntax, loadBuiltinModule, @@ -33,13 +33,12 @@ const { } = require('internal/modules/helpers'); const { Module: CJSModule, - cjsParseCache, } = require('internal/modules/cjs/loader'); -const { fileURLToPath, URL } = require('internal/url'); +const { fileURLToPath, pathToFileURL, URL } = require('internal/url'); let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { debug = fn; }); -const { emitExperimentalWarning } = require('internal/util'); +const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); const { ERR_UNKNOWN_BUILTIN_MODULE, ERR_INVALID_RETURN_PROPERTY_VALUE, @@ -140,6 +139,45 @@ function enrichCJSError(err, content, filename) { } } +const cjsCache = new Map(); +function loadCJS(url, source) { + if (cjsCache.has(url)) return cjsCache.get(url).exports; + + debug(`Loading CJSModule ${url}`); + + const compiledWrapper = Function('exports', 'require', 'module', '__filename', '__dirname', source); + + // TODO: make module object compatible with CJS Module class + const module = { exports: {} }; + const filename = fileURLToPath(new URL(url)); + const __dirname = dirname(filename); + // TODO: add missing properties on the require function + // eslint-disable-next-line func-name-matching,func-style + const requireFn = function require(spec) { + // TODO: add support for absolute paths + if (spec[0] === '.') { + spec = pathToFileURL(join(__dirname, spec)); + } + // TODO: add support for JSON imports + const job = asyncESM.esmLoader.getModuleJob(spec, url, kEmptyObject, true); + job.runSync(); + return cjsCache.get(job.url).exports; + }; + + cjsCache.set(url, module); + ReflectApply(compiledWrapper, module.exports, + [module.exports, requireFn, module, filename, __dirname]); + + return module.exports; +} + +translators.set('require-commonjs', (url, source, isMain) => { + return new ModuleWrap(url, undefined, ['default'], function() { + const exports = loadCJS(url, source); + this.setExport('default', exports); + }); +}); + // Strategy for loading a node-style CommonJS module const isWindows = process.platform === 'win32'; translators.set('commonjs', async function commonjsStrategy(url, source, @@ -149,25 +187,12 @@ translators.set('commonjs', async function commonjsStrategy(url, source, const filename = fileURLToPath(new URL(url)); if (!cjsParse) await initCJSParse(); - const { module, exportNames } = cjsPreparseModuleExports(filename); + const { exportNames } = cjsPreparseModuleExports(filename); const namesWithDefault = exportNames.has('default') ? [...exportNames] : ['default', ...exportNames]; return new ModuleWrap(url, undefined, namesWithDefault, function() { - debug(`Loading CJSModule ${url}`); - - let exports; - if (asyncESM.esmLoader.cjsCache.has(module)) { - exports = asyncESM.esmLoader.cjsCache.get(module); - asyncESM.esmLoader.cjsCache.delete(module); - } else { - try { - exports = CJSModule._load(filename, undefined, isMain); - } catch (err) { - enrichCJSError(err, undefined, filename); - throw err; - } - } + const exports = loadCJS(url, source); for (const exportName of exportNames) { if (!ObjectPrototypeHasOwnProperty(exports, exportName) || @@ -187,20 +212,6 @@ translators.set('commonjs', async function commonjsStrategy(url, source, }); function cjsPreparseModuleExports(filename) { - let module = CJSModule._cache[filename]; - if (module) { - const cached = cjsParseCache.get(module); - if (cached) - return { module, exportNames: cached.exportNames }; - } - const loaded = Boolean(module); - if (!loaded) { - module = new CJSModule(filename); - module.filename = filename; - module.paths = CJSModule._nodeModulePaths(module.path); - CJSModule._cache[filename] = module; - } - let source; try { source = readFileSync(filename, 'utf8'); @@ -219,29 +230,30 @@ function cjsPreparseModuleExports(filename) { const exportNames = new SafeSet(new SafeArrayIterator(exports)); // Set first for cycles. - cjsParseCache.set(module, { source, exportNames, loaded }); - - if (reexports.length) { - module.filename = filename; - module.paths = CJSModule._nodeModulePaths(module.path); - } - ArrayPrototypeForEach(reexports, (reexport) => { - let resolved; - try { - resolved = CJSModule._resolveFilename(reexport, module); - } catch { - return; - } - const ext = extname(resolved); - if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) && - isAbsolute(resolved)) { - const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); - for (const name of reexportNames) - exportNames.add(name); - } - }); - - return { module, exportNames }; + // cjsParseCache.set(module, { source, exportNames }); + + // TODO: add back support for reexports + // if (reexports.length) { + // module.filename = filename; + // module.paths = CJSModule._nodeModulePaths(module.path); + // } + // ArrayPrototypeForEach(reexports, (reexport) => { + // let resolved; + // try { + // resolved = CJSModule._resolveFilename(reexport, module); + // } catch { + // return; + // } + // const ext = extname(resolved); + // if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) && + // isAbsolute(resolved)) { + // const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); + // for (const name of reexportNames) + // exportNames.add(name); + // } + // }); + + return { exportNames }; } // Strategy for loading a node builtin CommonJS module that isn't @@ -283,6 +295,7 @@ translators.set('json', async function jsonStrategy(url, source) { // A require call could have been called on the same file during loading and // that resolves synchronously. To make sure we always return the identical // export, we have to check again if the module already exists or not. + // TODO: remove CJS loader from here as well. module = CJSModule._cache[modulePath]; if (module && module.loaded) { const exports = module.exports;