Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuy Bedford <guybedford@gmail.com>2020-05-15 08:40:37 +0300
committerGuy Bedford <guybedford@gmail.com>2020-09-29 04:27:25 +0300
commit1e8cb08edcbbfe01e7ef186a09d4781b33b490cc (patch)
tree3da6f851ef45c6a1a35466d616102d6fdb344f4c /lib/internal/modules
parent062b35d4e82663b50898f4f47e74d9d64c031379 (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.js38
-rw-r--r--lib/internal/modules/esm/module_job.js27
-rw-r--r--lib/internal/modules/esm/translators.js101
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) {