diff options
author | Guy Bedford <guybedford@gmail.com> | 2020-05-15 08:40:37 +0300 |
---|---|---|
committer | Guy Bedford <guybedford@gmail.com> | 2020-09-29 04:27:25 +0300 |
commit | 1e8cb08edcbbfe01e7ef186a09d4781b33b490cc (patch) | |
tree | 3da6f851ef45c6a1a35466d616102d6fdb344f4c /lib/internal/modules | |
parent | 062b35d4e82663b50898f4f47e74d9d64c031379 (diff) |
module: named exports for CJS via static analysis
PR-URL: https://github.com/nodejs/node/pull/35249
Reviewed-By: Mary Marchini <oss@mmarchini.me>
Reviewed-By: Geoffrey Booth <webmaster@geoffreybooth.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Myles Borins <myles.borins@gmail.com>
Reviewed-By: Michaƫl Zasso <targos@protonmail.com>
Reviewed-By: Zeyu Yang <himself65@outlook.com>
Reviewed-By: Richard Lau <riclau@uk.ibm.com>
Diffstat (limited to 'lib/internal/modules')
-rw-r--r-- | lib/internal/modules/cjs/loader.js | 38 | ||||
-rw-r--r-- | lib/internal/modules/esm/module_job.js | 27 | ||||
-rw-r--r-- | lib/internal/modules/esm/translators.js | 101 |
3 files changed, 129 insertions, 37 deletions
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 34507ebdad3..a745c66c614 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -21,12 +21,6 @@ 'use strict'; -// Set first due to cycle with ESM loader functions. -module.exports = { - wrapSafe, Module, toRealPath, readPackageScope, - get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } -}; - const { ArrayIsArray, ArrayPrototypeJoin, @@ -54,6 +48,15 @@ const { StringPrototypeStartsWith, } = primordials; +// Map used to store CJS parsing data. +const cjsParseCache = new SafeWeakMap(); + +// Set first due to cycle with ESM loader functions. +module.exports = { + wrapSafe, Module, toRealPath, readPackageScope, cjsParseCache, + get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; } +}; + const { NativeModule } = require('internal/bootstrap/loaders'); const { getSourceMapsEnabled, @@ -758,16 +761,21 @@ Module._load = function(request, parent, isMain) { const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) { updateChildren(parent, cachedModule, true); - if (!cachedModule.loaded) - return getExportsForCircularRequire(cachedModule); - return cachedModule.exports; + if (!cachedModule.loaded) { + const parseCachedModule = cjsParseCache.get(cachedModule); + if (!parseCachedModule || parseCachedModule.loaded) + return getExportsForCircularRequire(cachedModule); + parseCachedModule.loaded = true; + } else { + return cachedModule.exports; + } } const mod = loadNativeModule(filename, request); if (mod && mod.canBeRequiredByUsers) return mod.exports; // Don't call updateChildren(), Module constructor already does. - const module = new Module(filename, parent); + const module = cachedModule || new Module(filename, parent); if (isMain) { process.mainModule = module; @@ -1106,7 +1114,15 @@ Module._extensions['.js'] = function(module, filename) { throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath); } } - const content = fs.readFileSync(filename, 'utf8'); + // If already analyzed the source, then it will be cached. + const cached = cjsParseCache.get(module); + let content; + if (cached && cached.source) { + content = cached.source; + cached.source = undefined; + } else { + content = fs.readFileSync(filename, 'utf8'); + } module._compile(content, filename); }; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index dedfc54e7f3..7b8f146771c 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -103,28 +103,27 @@ class ModuleJob { ' does not provide an export named')) { const splitStack = StringPrototypeSplit(e.stack, '\n'); const parentFileUrl = splitStack[0]; - const childSpecifier = StringPrototypeMatch(e.message, /module '(.*)' does/)[1]; + const [, childSpecifier, name] = StringPrototypeMatch(e.message, + /module '(.*)' does not provide an export named '(.+)'/); const childFileURL = - await this.loader.resolve(childSpecifier, parentFileUrl); + await this.loader.resolve(childSpecifier, parentFileUrl); const format = await this.loader.getFormat(childFileURL); if (format === 'commonjs') { - e.message = `The requested module '${childSpecifier}' is expected ` + - 'to be of type CommonJS, which does not support named exports. ' + - 'CommonJS modules can be imported by importing the default ' + - 'export.'; + const importStatement = splitStack[1]; // TODO(@ctavan): The original error stack only provides the single // line which causes the error. For multi-line import statements we // cannot generate an equivalent object descructuring assignment by // just parsing the error stack. - const importStatement = splitStack[1]; const oneLineNamedImports = StringPrototypeMatch(importStatement, /{.*}/); - if (oneLineNamedImports) { - const destructuringAssignment = - StringPrototypeReplace(oneLineNamedImports[0], /\s+as\s+/g, ': '); - e.message += '\nFor example:\n' + - `import pkg from '${childSpecifier}';\n` + - `const ${destructuringAssignment} = pkg;`; - } + const destructuringAssignment = oneLineNamedImports && + StringPrototypeReplace(oneLineNamedImports, /\s+as\s+/g, ': '); + e.message = `Named export '${name}' not found. The requested module` + + ` '${childSpecifier}' is a CommonJS module, which may not support` + + ' all module.exports as named exports.\nCommonJS modules can ' + + 'always be imported via the default export, for example using:' + + `\n\nimport pkg from '${childSpecifier}';\n${ + destructuringAssignment ? + `const ${destructuringAssignment} = pkg;\n` : ''}`; const newStack = StringPrototypeSplit(e.stack, '\n'); newStack[3] = `SyntaxError: ${e.message}`; e.stack = ArrayPrototypeJoin(newStack, '\n'); diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index bb095446bc2..5e4de5d5af0 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -3,11 +3,14 @@ /* global WebAssembly */ const { + Boolean, JSONParse, + ObjectPrototypeHasOwnProperty, ObjectKeys, PromisePrototypeCatch, PromiseReject, SafeMap, + SafeSet, StringPrototypeReplace, } = primordials; @@ -17,11 +20,16 @@ function lazyTypes() { return _TYPES = require('internal/util/types'); } +const { readFileSync } = require('fs'); +const { extname } = require('path'); const { stripBOM, loadNativeModule } = require('internal/modules/cjs/helpers'); -const CJSModule = require('internal/modules/cjs/loader').Module; +const { + Module: CJSModule, + cjsParseCache +} = require('internal/modules/cjs/loader'); const internalURLModule = require('internal/url'); const { defaultGetSource } = require( 'internal/modules/esm/get_source'); @@ -44,12 +52,12 @@ const { ModuleWrap } = moduleWrap; const { getOptionValue } = require('internal/options'); const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve'); +const asyncESM = require('internal/process/esm_loader'); +const cjsParse = require('internal/deps/cjs-module-lexer/lexer'); const translators = new SafeMap(); exports.translators = translators; -const asyncESM = require('internal/process/esm_loader'); - let DECODER = null; function assertBufferSource(body, allowString, hookName) { if (allowString && typeof body === 'string') { @@ -104,7 +112,7 @@ function initializeImportMeta(meta, { url }) { meta.url = url; } -// Strategy for loading a standard JavaScript module +// Strategy for loading a standard JavaScript module. translators.set('module', async function moduleStrategy(url) { let { source } = await this._getSource( url, { format: 'module' }, defaultGetSource); @@ -125,23 +133,92 @@ translators.set('module', async function moduleStrategy(url) { // Strategy for loading a node-style CommonJS module const isWindows = process.platform === 'win32'; const winSepRegEx = /\//g; -translators.set('commonjs', function commonjsStrategy(url, isMain) { +translators.set('commonjs', async function commonjsStrategy(url, isMain) { debug(`Translating CJSModule ${url}`); - return new ModuleWrap(url, undefined, ['default'], function() { + + let filename = internalURLModule.fileURLToPath(new URL(url)); + if (isWindows) + filename = StringPrototypeReplace(filename, winSepRegEx, '\\'); + + const { module, exportNames } = cjsPreparseModuleExports(filename); + const namesWithDefault = exportNames.has('default') ? + [...exportNames] : ['default', ...exportNames]; + + return new ModuleWrap(url, undefined, namesWithDefault, function() { debug(`Loading CJSModule ${url}`); - const pathname = internalURLModule.fileURLToPath(new URL(url)); + let exports; - const cachedModule = CJSModule._cache[pathname]; - if (cachedModule && asyncESM.ESMLoader.cjsCache.has(cachedModule)) { - exports = asyncESM.ESMLoader.cjsCache.get(cachedModule); - asyncESM.ESMLoader.cjsCache.delete(cachedModule); + if (asyncESM.ESMLoader.cjsCache.has(module)) { + exports = asyncESM.ESMLoader.cjsCache.get(module); + asyncESM.ESMLoader.cjsCache.delete(module); } else { - exports = CJSModule._load(pathname, undefined, isMain); + exports = CJSModule._load(filename, undefined, isMain); + } + + for (const exportName of exportNames) { + if (!ObjectPrototypeHasOwnProperty(exports, exportName) || + exportName === 'default') + continue; + // We might trigger a getter -> dont fail. + let value; + try { + value = exports[exportName]; + } catch {} + this.setExport(exportName, value); } this.setExport('default', exports); }); }); +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'); + } catch {} + + const { exports, reexports } = cjsParse(source || ''); + + const exportNames = new SafeSet(exports); + + // Set first for cycles. + cjsParseCache.set(module, { source, exportNames, loaded }); + + if (reexports.length) { + module.filename = filename; + module.paths = CJSModule._nodeModulePaths(module.path); + } + for (const reexport of reexports) { + let resolved; + try { + resolved = CJSModule._resolveFilename(reexport, module); + } catch { + continue; + } + const ext = extname(resolved); + if (ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) { + const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); + for (const name of reexportNames) + exportNames.add(name); + } + } + + return { module, exportNames }; +} + // Strategy for loading a node builtin CommonJS module that isn't // through normal resolution translators.set('builtin', async function builtinStrategy(url) { |