diff --git a/packages/commonjs/src/generate-imports.js b/packages/commonjs/src/generate-imports.js index 7f3cedf38..22a59027d 100644 --- a/packages/commonjs/src/generate-imports.js +++ b/packages/commonjs/src/generate-imports.js @@ -113,6 +113,7 @@ export function getRequireHandlers() { exportMode, resolveRequireSourcesAndGetMeta, usesRequireWrapper, + isEsModule, usesRequire ) { const imports = []; @@ -134,38 +135,12 @@ export function getRequireHandlers() { ); } const requiresBySource = collectSources(requireExpressions); - // TODO Lukas consider extracting stuff - const result = await resolveRequireSourcesAndGetMeta( - usesRequireWrapper ? IS_WRAPPED_COMMONJS : true, + const requireTargets = await resolveRequireSourcesAndGetMeta( + id, + usesRequireWrapper ? IS_WRAPPED_COMMONJS : !isEsModule, Object.keys(requiresBySource) ); - let uid = 0; - for (const { source, id: resolveId, isCommonJS } of result) { - const requires = requiresBySource[source]; - let usesRequired = false; - let name; - const hasNameConflict = ({ scope }) => scope.contains(name); - do { - name = `require$$${uid}`; - uid += 1; - } while (requires.some(hasNameConflict)); - if (isCommonJS === IS_WRAPPED_COMMONJS) { - for (const { node } of requires) { - magicString.overwrite(node.start, node.end, `${name}()`); - } - imports.push(`import { __require as ${name} } from ${JSON.stringify(resolveId)};`); - } else { - for (const { node, usesReturnValue, toBeRemoved } of requires) { - if (usesReturnValue) { - usesRequired = true; - magicString.overwrite(node.start, node.end, name); - } else { - magicString.remove(toBeRemoved.start, toBeRemoved.end); - } - } - imports.push(`import ${usesRequired ? `${name} from ` : ''}${JSON.stringify(resolveId)};`); - } - } + processRequireExpressions(imports, requireTargets, requiresBySource, magicString); return imports.length ? `${imports.join('\n')}\n\n` : ''; } @@ -186,3 +161,41 @@ function collectSources(requireExpressions) { } return requiresBySource; } + +function processRequireExpressions(imports, requireTargets, requiresBySource, magicString) { + const generateRequireName = getGenerateRequireName(); + for (const { source, id: resolveId, isCommonJS } of requireTargets) { + const requires = requiresBySource[source]; + const name = generateRequireName(requires); + if (isCommonJS === IS_WRAPPED_COMMONJS) { + for (const { node } of requires) { + magicString.overwrite(node.start, node.end, `${name}()`); + } + imports.push(`import { __require as ${name} } from ${JSON.stringify(resolveId)};`); + } else { + let usesRequired = false; + for (const { node, usesReturnValue, toBeRemoved } of requires) { + if (usesReturnValue) { + usesRequired = true; + magicString.overwrite(node.start, node.end, name); + } else { + magicString.remove(toBeRemoved.start, toBeRemoved.end); + } + } + imports.push(`import ${usesRequired ? `${name} from ` : ''}${JSON.stringify(resolveId)};`); + } + } +} + +function getGenerateRequireName() { + let uid = 0; + return (requires) => { + let name; + const hasNameConflict = ({ scope }) => scope.contains(name); + do { + name = `require$$${uid}`; + uid += 1; + } while (requires.some(hasNameConflict)); + return name; + }; +} diff --git a/packages/commonjs/src/index.js b/packages/commonjs/src/index.js index 5ab3f15da..49e8287ac 100644 --- a/packages/commonjs/src/index.js +++ b/packages/commonjs/src/index.js @@ -16,21 +16,26 @@ import { EXTERNAL_SUFFIX, getHelpersModule, HELPERS_ID, - IS_WRAPPED_COMMONJS, isWrappedId, MODULE_SUFFIX, PROXY_SUFFIX, - unwrapId, - wrapId + unwrapId } from './helpers'; import { hasCjsKeywords } from './parse'; -import { getStaticRequireProxy, getUnknownRequireProxy } from './proxies'; -import getResolveId, { resolveExtensions } from './resolve-id'; +import { getEsImportProxy, getStaticRequireProxy, getUnknownRequireProxy } from './proxies'; +import getResolveId from './resolve-id'; +import { getResolveRequireSourcesAndGetMeta } from './resolve-require-sources'; import validateRollupVersion from './rollup-version'; import transformCommonjs from './transform-commonjs'; -import { capitalize, getName, normalizePathSlashes } from './utils'; +import { getName, normalizePathSlashes } from './utils'; export default function commonjs(options = {}) { + const { + ignoreGlobal, + ignoreDynamicRequires, + requireReturnsDefault: requireReturnsDefaultOption, + esmExternals + } = options; const extensions = options.extensions || ['.js']; const filter = createFilter(options.include, options.exclude); const strictRequireSemanticFilter = @@ -39,16 +44,12 @@ export default function commonjs(options = {}) { : !options.strictRequireSemantic ? () => false : createFilter(options.strictRequireSemantic); - const { - ignoreGlobal, - ignoreDynamicRequires, - requireReturnsDefault: requireReturnsDefaultOption, - esmExternals - } = options; + const getRequireReturnsDefault = typeof requireReturnsDefaultOption === 'function' ? requireReturnsDefaultOption : () => requireReturnsDefaultOption; + let esmExternalIds; const isEsmExternal = typeof esmExternals === 'function' @@ -56,10 +57,11 @@ export default function commonjs(options = {}) { : Array.isArray(esmExternals) ? ((esmExternalIds = new Set(esmExternals)), (id) => esmExternalIds.has(id)) : () => esmExternals; + const defaultIsModuleExports = typeof options.defaultIsModuleExports === 'boolean' ? options.defaultIsModuleExports : 'auto'; - const knownCjsModuleTypes = Object.create(null); + const resolveRequireSourcesAndGetMeta = getResolveRequireSourcesAndGetMeta(extensions); const dynamicRequireModuleSet = getDynamicRequireModuleSet(options.dynamicRequireTargets); const isDynamicRequireModulesEnabled = dynamicRequireModuleSet.size > 0; const commonDir = isDynamicRequireModulesEnabled @@ -120,8 +122,6 @@ export default function commonjs(options = {}) { !isEsModule && (dynamicRequireModuleSet.has(normalizePathSlashes(id)) || strictRequireSemanticFilter(id)); - // TODO Lukas auto-detect circular dependencies - // TODO Lukas extract helpers return transformCommonjs( this.parse, code, @@ -138,53 +138,7 @@ export default function commonjs(options = {}) { ast, defaultIsModuleExports, usesRequireWrapper, - // TODO Lukas extract - (isParentCommonJS, sources) => { - knownCjsModuleTypes[id] = isParentCommonJS; - return Promise.all( - sources.map(async (source) => { - // Never analyze or proxy internal modules - if (source.startsWith('\0')) { - return { source, id: source, isCommonJS: false }; - } - const resolved = - (await this.resolve(source, id, { - skipSelf: true, - custom: { - 'node-resolve': { isRequire: true } - } - })) || resolveExtensions(source, id, extensions); - if (!resolved) { - return { source, id: wrapId(source, EXTERNAL_SUFFIX), isCommonJS: false }; - } - if (resolved.external) { - return { source, id: wrapId(resolved.id, EXTERNAL_SUFFIX), isCommonJS: false }; - } - if (resolved.id in knownCjsModuleTypes) { - return { - source, - id: - knownCjsModuleTypes[resolved.id] === true - ? wrapId(resolved.id, PROXY_SUFFIX) - : resolved.id, - isCommonJS: knownCjsModuleTypes[resolved.id] - }; - } - const { - meta: { commonjs: commonjsMeta } - } = await this.load(resolved); - const isCommonJS = commonjsMeta && commonjsMeta.isCommonJS; - return { - source, - id: - isCommonJS === IS_WRAPPED_COMMONJS - ? resolved.id - : wrapId(resolved.id, PROXY_SUFFIX), - isCommonJS - }; - }) - ); - } + resolveRequireSourcesAndGetMeta(this) ); } @@ -232,40 +186,17 @@ export default function commonjs(options = {}) { ); } - // TODO Lukas extract if (isWrappedId(id, ES_IMPORT_SUFFIX)) { - const actualId = unwrapId(id, ES_IMPORT_SUFFIX); - const name = getName(actualId); - const exportsName = `${name}Exports`; - const requireModule = `require${capitalize(name)}`; - // TODO Lukas the ES wrapper might also just forward the exports object - let code = - `import { getDefaultExportFromCjs } from "${HELPERS_ID}";\n` + - `import { __require as ${requireModule} } from ${JSON.stringify(actualId)};\n` + - `var ${exportsName} = ${requireModule}();\n` + - `export { ${exportsName} as __moduleExports };`; - if (defaultIsModuleExports) { - code += `\nexport { ${exportsName} as default };`; - } else { - code += `export default /*@__PURE__*/getDefaultExportFromCjs(${exportsName});`; - } - return { - code, - syntheticNamedExports: '__moduleExports', - meta: { commonjs: { isCommonJS: false } } - }; + return getEsImportProxy(unwrapId(id, ES_IMPORT_SUFFIX), defaultIsModuleExports); } if (id === DYNAMIC_MODULES_ID) { - return { - code: getDynamicRequireModules( - isDynamicRequireModulesEnabled, - dynamicRequireModuleSet, - commonDir, - ignoreDynamicRequires - ), - meta: { commonjs: { isCommonJS: false } } - }; + return getDynamicRequireModules( + isDynamicRequireModulesEnabled, + dynamicRequireModuleSet, + commonDir, + ignoreDynamicRequires + ); } if (isWrappedId(id, PROXY_SUFFIX)) { diff --git a/packages/commonjs/src/proxies.js b/packages/commonjs/src/proxies.js index 7fd0b0234..2935612ac 100644 --- a/packages/commonjs/src/proxies.js +++ b/packages/commonjs/src/proxies.js @@ -1,5 +1,5 @@ import { HELPERS_ID } from './helpers'; -import { getName } from './utils'; +import { capitalize, getName } from './utils'; export function getUnknownRequireProxy(id, requireReturnsDefault) { if (requireReturnsDefault === true || id.endsWith('.json')) { @@ -46,3 +46,24 @@ export async function getStaticRequireProxy( } return `export { default } from ${JSON.stringify(id)};`; } + +export function getEsImportProxy(id, defaultIsModuleExports) { + const name = getName(id); + const exportsName = `${name}Exports`; + const requireModule = `require${capitalize(name)}`; + let code = + `import { getDefaultExportFromCjs } from "${HELPERS_ID}";\n` + + `import { __require as ${requireModule} } from ${JSON.stringify(id)};\n` + + `var ${exportsName} = ${requireModule}();\n` + + `export { ${exportsName} as __moduleExports };`; + if (defaultIsModuleExports) { + code += `\nexport { ${exportsName} as default };`; + } else { + code += `export default /*@__PURE__*/getDefaultExportFromCjs(${exportsName});`; + } + return { + code, + syntheticNamedExports: '__moduleExports', + meta: { commonjs: { isCommonJS: false } } + }; +} diff --git a/packages/commonjs/src/resolve-require-sources.js b/packages/commonjs/src/resolve-require-sources.js new file mode 100644 index 000000000..5be46a0c9 --- /dev/null +++ b/packages/commonjs/src/resolve-require-sources.js @@ -0,0 +1,54 @@ +import { EXTERNAL_SUFFIX, IS_WRAPPED_COMMONJS, PROXY_SUFFIX, wrapId } from './helpers'; +import { resolveExtensions } from './resolve-id'; + +// TODO Lukas auto-detect circular dependencies +// * only return once all dependencies have been analyzed +// * wait for this.load dependencies unless they already have an entry in knownCjsModuleTypes to avoid deadlocks +// as those have already started being processed +// * only analyze cycles if we do not have an explicit config +export function getResolveRequireSourcesAndGetMeta(extensions) { + const knownCjsModuleTypes = Object.create(null); + return (rollupContext) => (id, isParentCommonJS, sources) => { + knownCjsModuleTypes[id] = isParentCommonJS; + return Promise.all( + sources.map(async (source) => { + // Never analyze or proxy internal modules + if (source.startsWith('\0')) { + return { source, id: source, isCommonJS: false }; + } + const resolved = + (await rollupContext.resolve(source, id, { + skipSelf: true, + custom: { + 'node-resolve': { isRequire: true } + } + })) || resolveExtensions(source, id, extensions); + if (!resolved) { + return { source, id: wrapId(source, EXTERNAL_SUFFIX), isCommonJS: false }; + } + if (resolved.external) { + return { source, id: wrapId(resolved.id, EXTERNAL_SUFFIX), isCommonJS: false }; + } + if (resolved.id in knownCjsModuleTypes) { + return { + source, + id: + knownCjsModuleTypes[resolved.id] === IS_WRAPPED_COMMONJS + ? resolved.id + : wrapId(resolved.id, PROXY_SUFFIX), + isCommonJS: knownCjsModuleTypes[resolved.id] + }; + } + const { + meta: { commonjs: commonjsMeta } + } = await rollupContext.load(resolved); + const isCommonJS = commonjsMeta && commonjsMeta.isCommonJS; + return { + source, + id: isCommonJS === IS_WRAPPED_COMMONJS ? resolved.id : wrapId(resolved.id, PROXY_SUFFIX), + isCommonJS + }; + }) + ); + }; +} diff --git a/packages/commonjs/src/transform-commonjs.js b/packages/commonjs/src/transform-commonjs.js index 15c8a25dd..3716e6f35 100644 --- a/packages/commonjs/src/transform-commonjs.js +++ b/packages/commonjs/src/transform-commonjs.js @@ -27,7 +27,7 @@ import { isRequireStatement, isStaticRequireStatement } from './generate-imports'; -import { IS_WRAPPED_COMMONJS } from './helpers.js'; +import { IS_WRAPPED_COMMONJS } from './helpers'; import { tryParse } from './parse'; import { capitalize, deconflict, getName, getVirtualPathForDynamicRequirePath } from './utils'; @@ -196,7 +196,7 @@ export default async function transformCommonjs( node.callee.property.name === 'resolve' && hasDynamicModuleForPath(id, '/', dynamicRequireModuleSet) ) { - // TODO Lukas reimplement + // TODO Lukas reimplement? uses.require = true; const requireNode = node.callee.object; magicString.appendLeft( @@ -220,7 +220,6 @@ export default async function transformCommonjs( skippedNodes.add(node.callee); uses.require = true; - // TODO Lukas can the remaining logic be moved to generate-imports? if (!isIgnoredRequireStatement(node, ignoreRequire)) { const usesReturnValue = parent.type !== 'ExpressionStatement'; @@ -316,10 +315,6 @@ export default async function transformCommonjs( } } return; - // TODO Lukas instead of wrapping, we rewrite everything - // module.exports -> exportsVar, except if it is an assignment, then it is moduleVar.exports - // module -> moduleVar - // only exceptions: Direct assignments to module or exports. Would work with new logic, though. case 'module': case 'exports': shouldWrap = true; @@ -465,6 +460,7 @@ export default async function transformCommonjs( exportMode, resolveRequireSourcesAndGetMeta, usesRequireWrapper, + isEsModule, uses.require ); const exportBlock = isEsModule diff --git a/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/lib.js b/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/lib.js index 657003611..46d1bf449 100644 --- a/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/lib.js +++ b/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/lib.js @@ -1,4 +1,6 @@ -module.exports = { - "default": () => { console.log('beep') }, - __esModule: true -}; \ No newline at end of file +module.exports = { + default: () => { + t.is('beep', 'beep'); + }, + __esModule: true +}; diff --git a/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/main.js b/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/main.js index aed3937f1..e97281701 100644 --- a/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/main.js +++ b/packages/commonjs/test/fixtures/samples/cjs-with-esm-property/main.js @@ -1,3 +1,3 @@ -import fn from './lib' +import fn from './lib'; -fn() \ No newline at end of file +fn(); diff --git a/packages/commonjs/test/snapshots/function.js.md b/packages/commonjs/test/snapshots/function.js.md index 5c9362f6a..4e76b1ec1 100644 --- a/packages/commonjs/test/snapshots/function.js.md +++ b/packages/commonjs/test/snapshots/function.js.md @@ -6578,3 +6578,32 @@ Generated by [AVA](https://avajs.dev). module.exports = main;␊ `, } + +## require-mixed-module + +> Snapshot 1 + + { + 'main.js': `'use strict';␊ + ␊ + var main = {};␊ + ␊ + var other = 'foo';␊ + ␊ + const foo = other;␊ + ␊ + var dep$1 = 'default';␊ + ␊ + var dep$2 = /*#__PURE__*/Object.freeze({␊ + __proto__: null,␊ + foo: foo,␊ + 'default': dep$1␊ + });␊ + ␊ + const dep = dep$2;␊ + ␊ + t.deepEqual(dep, { default: 'default', ns: { default: 'bar', foo: 'foo' } });␊ + ␊ + module.exports = main;␊ + `, + } diff --git a/packages/commonjs/test/snapshots/function.js.snap b/packages/commonjs/test/snapshots/function.js.snap index 306da936b..ab054b8b0 100644 Binary files a/packages/commonjs/test/snapshots/function.js.snap and b/packages/commonjs/test/snapshots/function.js.snap differ