diff --git a/packages/commonjs/package.json b/packages/commonjs/package.json index 6f3bb2e2a..ff706eeb6 100644 --- a/packages/commonjs/package.json +++ b/packages/commonjs/package.json @@ -52,7 +52,7 @@ "require" ], "peerDependencies": { - "rollup": "^2.61.1" + "rollup": "^2.67.0" }, "dependencies": { "@rollup/pluginutils": "^3.1.0", diff --git a/packages/commonjs/src/generate-imports.js b/packages/commonjs/src/generate-imports.js index 9c3aaa02e..a9cc478fe 100644 --- a/packages/commonjs/src/generate-imports.js +++ b/packages/commonjs/src/generate-imports.js @@ -90,11 +90,12 @@ export function getRequireHandlers() { exportsName, id, exportMode, - resolveRequireSourcesAndGetMeta, + resolveRequireSourcesAndUpdateMeta, needsRequireWrapper, isEsModule, isDynamicRequireModulesEnabled, - getIgnoreTryCatchRequireStatementMode + getIgnoreTryCatchRequireStatementMode, + commonjsMeta ) { const imports = []; imports.push(`import * as ${helpersName} from "${HELPERS_ID}";`); @@ -117,9 +118,10 @@ export function getRequireHandlers() { ); } const requiresBySource = collectSources(requireExpressions); - const { requireTargets, usesRequireWrapper } = await resolveRequireSourcesAndGetMeta( + const requireTargets = await resolveRequireSourcesAndUpdateMeta( id, needsRequireWrapper ? IS_WRAPPED_COMMONJS : !isEsModule, + commonjsMeta, Object.keys(requiresBySource).map((source) => { return { source, @@ -134,10 +136,7 @@ export function getRequireHandlers() { getIgnoreTryCatchRequireStatementMode, magicString ); - return { - importBlock: imports.length ? `${imports.join('\n')}\n\n` : '', - usesRequireWrapper - }; + return imports.length ? `${imports.join('\n')}\n\n` : ''; } return { diff --git a/packages/commonjs/src/helpers.js b/packages/commonjs/src/helpers.js index d2906ecf5..94d770d38 100644 --- a/packages/commonjs/src/helpers.js +++ b/packages/commonjs/src/helpers.js @@ -7,7 +7,8 @@ export const WRAPPED_SUFFIX = '?commonjs-wrapped'; export const EXTERNAL_SUFFIX = '?commonjs-external'; export const EXPORTS_SUFFIX = '?commonjs-exports'; export const MODULE_SUFFIX = '?commonjs-module'; -export const ES_IMPORT_SUFFIX = '?es-import'; +export const ENTRY_SUFFIX = '?commonjs-entry'; +export const ES_IMPORT_SUFFIX = '?commonjs-es-import'; export const DYNAMIC_MODULES_ID = '\0commonjs-dynamic-modules'; export const HELPERS_ID = '\0commonjsHelpers.js'; diff --git a/packages/commonjs/src/index.js b/packages/commonjs/src/index.js index 639f9e92d..b53fa7fd4 100644 --- a/packages/commonjs/src/index.js +++ b/packages/commonjs/src/index.js @@ -9,6 +9,7 @@ import { getDynamicModuleRegistry, getDynamicRequireModules } from './dynamic-mo import { DYNAMIC_MODULES_ID, + ENTRY_SUFFIX, ES_IMPORT_SUFFIX, EXPORTS_SUFFIX, EXTERNAL_SUFFIX, @@ -20,13 +21,20 @@ import { unwrapId } from './helpers'; import { hasCjsKeywords } from './parse'; -import { getEsImportProxy, getStaticRequireProxy, getUnknownRequireProxy } from './proxies'; +import { + getEntryProxy, + getEsImportProxy, + getStaticRequireProxy, + getUnknownRequireProxy +} from './proxies'; import getResolveId from './resolve-id'; -import { getResolveRequireSourcesAndGetMeta } from './resolve-require-sources'; +import { getRequireResolver } from './resolve-require-sources'; import validateVersion from './rollup-version'; import transformCommonjs from './transform-commonjs'; import { getName, getStrictRequiresFilter, normalizePathSlashes } from './utils'; +const PLUGIN_NAME = 'commonjs'; + export default function commonjs(options = {}) { const { ignoreGlobal, @@ -58,11 +66,6 @@ export default function commonjs(options = {}) { : () => typeof defaultIsModuleExportsOption === 'boolean' ? defaultIsModuleExportsOption : 'auto'; - const { - resolveRequireSourcesAndGetMeta, - getWrappedIds, - isRequiredId - } = getResolveRequireSourcesAndGetMeta(extensions, detectCyclesAndConditional); const dynamicRequireRoot = typeof options.dynamicRequireRoot === 'string' ? resolve(options.dynamicRequireRoot) @@ -73,9 +76,6 @@ export default function commonjs(options = {}) { ); const isDynamicRequireModulesEnabled = dynamicRequireModules.size > 0; - const esModulesWithDefaultExport = new Set(); - const esModulesWithNamedExports = new Set(); - const ignoreRequire = typeof options.ignore === 'function' ? options.ignore @@ -103,25 +103,31 @@ export default function commonjs(options = {}) { const sourceMap = options.sourceMap !== false; + // Initialized in buildStart + let requireResolver; + function transformAndCheckExports(code, id) { const { isEsModule, hasDefaultExport, hasNamedExports, ast } = analyzeTopLevelStatements( this.parse, code, id ); + + const commonjsMeta = this.getModuleInfo(id).meta.commonjs || {}; if (hasDefaultExport) { - esModulesWithDefaultExport.add(id); + commonjsMeta.hasDefaultExport = true; } if (hasNamedExports) { - esModulesWithNamedExports.add(id); + commonjsMeta.hasNamedExports = true; } if ( !dynamicRequireModules.has(normalizePathSlashes(id)) && - (!(hasCjsKeywords(code, ignoreGlobal) || isRequiredId(id)) || + (!(hasCjsKeywords(code, ignoreGlobal) || requireResolver.isRequiredId(id)) || (isEsModule && !options.transformMixedEsModules)) ) { - return { meta: { commonjs: { isCommonJS: false } } }; + commonjsMeta.isCommonJS = false; + return { meta: { commonjs: commonjsMeta } }; } const needsRequireWrapper = @@ -160,14 +166,15 @@ export default function commonjs(options = {}) { ast, getDefaultIsModuleExports(id), needsRequireWrapper, - resolveRequireSourcesAndGetMeta(this), - isRequiredId(id), - checkDynamicRequire + requireResolver.resolveRequireSourcesAndUpdateMeta(this), + requireResolver.isRequiredId(id), + checkDynamicRequire, + commonjsMeta ); } return { - name: 'commonjs', + name: PLUGIN_NAME, version, @@ -175,7 +182,7 @@ export default function commonjs(options = {}) { // We inject the resolver in the beginning so that "catch-all-resolver" like node-resolver // do not prevent our plugin from resolving entry points ot proxies. const plugins = Array.isArray(rawOptions.plugins) - ? rawOptions.plugins + ? [...rawOptions.plugins] : rawOptions.plugins ? [rawOptions.plugins] : []; @@ -197,11 +204,12 @@ export default function commonjs(options = {}) { 'The namedExports option from "@rollup/plugin-commonjs" is deprecated. Named exports are now handled automatically.' ); } + requireResolver = getRequireResolver(extensions, detectCyclesAndConditional); }, buildEnd() { if (options.strictRequires === 'debug') { - const wrappedIds = getWrappedIds(); + const wrappedIds = requireResolver.getWrappedIds(); if (wrappedIds.length) { this.warn({ code: 'WRAPPED_IDS', @@ -250,6 +258,15 @@ export default function commonjs(options = {}) { ); } + // entry suffix is just appended to not mess up relative external resolution + if (id.endsWith(ENTRY_SUFFIX)) { + return getEntryProxy( + id.slice(0, -ENTRY_SUFFIX.length), + defaultIsModuleExports, + this.getModuleInfo + ); + } + if (isWrappedId(id, ES_IMPORT_SUFFIX)) { return getEsImportProxy(unwrapId(id, ES_IMPORT_SUFFIX), defaultIsModuleExports); } @@ -265,18 +282,16 @@ export default function commonjs(options = {}) { if (isWrappedId(id, PROXY_SUFFIX)) { const actualId = unwrapId(id, PROXY_SUFFIX); - return getStaticRequireProxy( - actualId, - getRequireReturnsDefault(actualId), - esModulesWithDefaultExport, - esModulesWithNamedExports, - this.load - ); + return getStaticRequireProxy(actualId, getRequireReturnsDefault(actualId), this.load); } return null; }, + shouldTransformCachedModule(...args) { + return requireResolver.shouldTransformCachedModule.call(this, ...args); + }, + transform(code, id) { const extName = extname(id); if (extName !== '.cjs' && (!filter(id) || !extensions.includes(extName))) { diff --git a/packages/commonjs/src/proxies.js b/packages/commonjs/src/proxies.js index 2935612ac..10e9d0894 100644 --- a/packages/commonjs/src/proxies.js +++ b/packages/commonjs/src/proxies.js @@ -1,4 +1,4 @@ -import { HELPERS_ID } from './helpers'; +import { HELPERS_ID, IS_WRAPPED_COMMONJS } from './helpers'; import { capitalize, getName } from './utils'; export function getUnknownRequireProxy(id, requireReturnsDefault) { @@ -17,21 +17,15 @@ export function getUnknownRequireProxy(id, requireReturnsDefault) { return `import * as ${name} from ${JSON.stringify(id)}; ${exported}`; } -export async function getStaticRequireProxy( - id, - requireReturnsDefault, - esModulesWithDefaultExport, - esModulesWithNamedExports, - loadModule -) { +export async function getStaticRequireProxy(id, requireReturnsDefault, loadModule) { const name = getName(id); const { meta: { commonjs: commonjsMeta } } = await loadModule({ id }); - if (commonjsMeta && commonjsMeta.isCommonJS) { - return `export { __moduleExports as default } from ${JSON.stringify(id)};`; - } else if (!commonjsMeta) { + if (!commonjsMeta) { return getUnknownRequireProxy(id, requireReturnsDefault); + } else if (commonjsMeta.isCommonJS) { + return `export { __moduleExports as default } from ${JSON.stringify(id)};`; } else if (!requireReturnsDefault) { return `import { getAugmentedNamespace } from "${HELPERS_ID}"; import * as ${name} from ${JSON.stringify( id @@ -39,14 +33,30 @@ export async function getStaticRequireProxy( } else if ( requireReturnsDefault !== true && (requireReturnsDefault === 'namespace' || - !esModulesWithDefaultExport.has(id) || - (requireReturnsDefault === 'auto' && esModulesWithNamedExports.has(id))) + !commonjsMeta.hasDefaultExport || + (requireReturnsDefault === 'auto' && commonjsMeta.hasNamedExports)) ) { return `import * as ${name} from ${JSON.stringify(id)}; export default ${name};`; } return `export { default } from ${JSON.stringify(id)};`; } +export function getEntryProxy(id, defaultIsModuleExports, getModuleInfo) { + const { + meta: { commonjs: commonjsMeta }, + hasDefaultExport + } = getModuleInfo(id); + if (!commonjsMeta || commonjsMeta.isCommonJS !== IS_WRAPPED_COMMONJS) { + const stringifiedId = JSON.stringify(id); + let code = `export * from ${stringifiedId};`; + if (hasDefaultExport) { + code += `export { default } from ${stringifiedId};`; + } + return code; + } + return getEsImportProxy(id, defaultIsModuleExports); +} + export function getEsImportProxy(id, defaultIsModuleExports) { const name = getName(id); const exportsName = `${name}Exports`; diff --git a/packages/commonjs/src/resolve-id.js b/packages/commonjs/src/resolve-id.js index 74d209e5b..1f6c874a3 100644 --- a/packages/commonjs/src/resolve-id.js +++ b/packages/commonjs/src/resolve-id.js @@ -5,6 +5,7 @@ import { dirname, resolve, sep } from 'path'; import { DYNAMIC_MODULES_ID, + ENTRY_SUFFIX, ES_IMPORT_SUFFIX, EXPORTS_SUFFIX, EXTERNAL_SUFFIX, @@ -51,11 +52,8 @@ export function resolveExtensions(importee, importer, extensions) { export default function getResolveId(extensions) { return async function resolveId(importee, importer, resolveOptions) { // We assume that all requires are pre-resolved - if ( - resolveOptions.custom && - resolveOptions.custom['node-resolve'] && - resolveOptions.custom['node-resolve'].isRequire - ) { + const customOptions = resolveOptions.custom; + if (customOptions && customOptions['node-resolve'] && customOptions['node-resolve'].isRequire) { return null; } if (isWrappedId(importee, WRAPPED_SUFFIX)) { @@ -63,6 +61,7 @@ export default function getResolveId(extensions) { } if ( + importee.endsWith(ENTRY_SUFFIX) || isWrappedId(importee, MODULE_SUFFIX) || isWrappedId(importee, EXPORTS_SUFFIX) || isWrappedId(importee, PROXY_SUFFIX) || @@ -79,7 +78,8 @@ export default function getResolveId(extensions) { importer === DYNAMIC_MODULES_ID || // Proxies are only importing resolved ids, no need to resolve again isWrappedId(importer, PROXY_SUFFIX) || - isWrappedId(importer, ES_IMPORT_SUFFIX) + isWrappedId(importer, ES_IMPORT_SUFFIX) || + importer.endsWith(ENTRY_SUFFIX) ) { return importee; } @@ -99,22 +99,27 @@ export default function getResolveId(extensions) { // If this is an entry point or ESM import, we need to figure out if the importee is wrapped and // if that is the case, we need to add a proxy. - const customOptions = resolveOptions.custom; - - // If this is a require, we do not need a proxy - if (customOptions && customOptions['node-resolve'] && customOptions['node-resolve'].isRequire) { - return null; - } - const resolved = (await this.resolve(importee, importer, Object.assign({ skipSelf: true }, resolveOptions))) || resolveExtensions(importee, importer, extensions); - if (!resolved || resolved.external) { + // Make sure that even if other plugins resolve again, we ignore our own proxies + if ( + !resolved || + resolved.external || + resolved.id.endsWith(ENTRY_SUFFIX) || + isWrappedId(resolved.id, ES_IMPORT_SUFFIX) + ) { return resolved; } + const moduleInfo = await this.load(resolved); + if (resolveOptions.isEntry) { + moduleInfo.moduleSideEffects = true; + // We must not precede entry proxies with a `\0` as that will mess up relative external resolution + return resolved.id + ENTRY_SUFFIX; + } const { meta: { commonjs: commonjsMeta } - } = await this.load(resolved); + } = moduleInfo; if (commonjsMeta && commonjsMeta.isCommonJS === IS_WRAPPED_COMMONJS) { return wrapId(resolved.id, ES_IMPORT_SUFFIX); } diff --git a/packages/commonjs/src/resolve-require-sources.js b/packages/commonjs/src/resolve-require-sources.js index 76fae132e..49dc0dc2a 100644 --- a/packages/commonjs/src/resolve-require-sources.js +++ b/packages/commonjs/src/resolve-require-sources.js @@ -7,7 +7,7 @@ import { } from './helpers'; import { resolveExtensions } from './resolve-id'; -export function getResolveRequireSourcesAndGetMeta(extensions, detectCyclesAndConditional) { +export function getRequireResolver(extensions, detectCyclesAndConditional) { const knownCjsModuleTypes = Object.create(null); const requiredIds = Object.create(null); const unconditionallyRequiredIds = Object.create(null); @@ -31,11 +31,7 @@ export function getResolveRequireSourcesAndGetMeta(extensions, detectCyclesAndCo const getTypeForFullyAnalyzedModule = (id) => { const knownType = knownCjsModuleTypes[id]; - if ( - knownType === IS_WRAPPED_COMMONJS || - !detectCyclesAndConditional || - fullyAnalyzedModules[id] - ) { + if (knownType !== true || !detectCyclesAndConditional || fullyAnalyzedModules[id]) { return knownType; } fullyAnalyzedModules[id] = true; @@ -45,26 +41,80 @@ export function getResolveRequireSourcesAndGetMeta(extensions, detectCyclesAndCo return knownType; }; + const setInitialParentType = (id, initialCommonJSType) => { + // It is possible a transformed module is already fully analyzed when using + // the cache and one dependency introduces a new cycle. Then transform is + // run for a fully analzyed module again. Fully analyzed modules may never + // change their type as importers already trust their type. + knownCjsModuleTypes[id] = fullyAnalyzedModules[id] + ? knownCjsModuleTypes[id] + : initialCommonJSType; + if ( + detectCyclesAndConditional && + knownCjsModuleTypes[id] === true && + requiredIds[id] && + !unconditionallyRequiredIds[id] + ) { + knownCjsModuleTypes[id] = IS_WRAPPED_COMMONJS; + } + }; + + const setTypesForRequiredModules = async (parentId, resolved, isConditional, loadModule) => { + const childId = resolved.id; + requiredIds[childId] = true; + if (!(isConditional || knownCjsModuleTypes[parentId] === IS_WRAPPED_COMMONJS)) { + unconditionallyRequiredIds[childId] = true; + } + + getDependencies(parentId).add(childId); + if (!isCyclic(childId)) { + // This makes sure the current transform handler waits for all direct dependencies to be + // loaded and transformed and therefore for all transitive CommonJS dependencies to be + // loaded as well so that all cycles have been found and knownCjsModuleTypes is reliable. + await loadModule(resolved); + } + }; + return { getWrappedIds: () => Object.keys(knownCjsModuleTypes).filter( (id) => knownCjsModuleTypes[id] === IS_WRAPPED_COMMONJS ), isRequiredId: (id) => requiredIds[id], - resolveRequireSourcesAndGetMeta: (rollupContext) => async ( + async shouldTransformCachedModule({ id: parentId, meta: { commonjs: parentMeta } }) { + // Ignore modules that did not pass through the original transformer in a previous build + if (!(parentMeta && parentMeta.requires)) { + return false; + } + setInitialParentType(parentId, parentMeta.initialCommonJSType); + await Promise.all( + parentMeta.requires.map(({ resolved, isConditional }) => + setTypesForRequiredModules(parentId, resolved, isConditional, this.load) + ) + ); + if (getTypeForFullyAnalyzedModule(parentId) !== parentMeta.isCommonJS) { + return true; + } + for (const { + resolved: { id } + } of parentMeta.requires) { + if (getTypeForFullyAnalyzedModule(id) !== parentMeta.isRequiredCommonJS[id]) { + return true; + } + } + return false; + }, + /* eslint-disable no-param-reassign */ + resolveRequireSourcesAndUpdateMeta: (rollupContext) => async ( parentId, isParentCommonJS, + parentMeta, sources ) => { - knownCjsModuleTypes[parentId] = isParentCommonJS; - if ( - detectCyclesAndConditional && - knownCjsModuleTypes[parentId] && - requiredIds[parentId] && - !unconditionallyRequiredIds[parentId] - ) { - knownCjsModuleTypes[parentId] = IS_WRAPPED_COMMONJS; - } + parentMeta.initialCommonJSType = isParentCommonJS; + parentMeta.requires = []; + parentMeta.isRequiredCommonJS = Object.create(null); + setInitialParentType(parentId, isParentCommonJS); const requireTargets = await Promise.all( sources.map(async ({ source, isConditional }) => { // Never analyze or proxy internal modules @@ -82,38 +132,27 @@ export function getResolveRequireSourcesAndGetMeta(extensions, detectCyclesAndCo if (resolved.external) { return { id: wrapId(childId, EXTERNAL_SUFFIX), allowProxy: false }; } - requiredIds[childId] = true; - if (!(isConditional || knownCjsModuleTypes[parentId] === IS_WRAPPED_COMMONJS)) { - unconditionallyRequiredIds[childId] = true; - } - - getDependencies(parentId).add(childId); - if (!isCyclic(childId)) { - // This makes sure the current transform handler waits for all direct dependencies to be - // loaded and transformed and therefore for all transitive CommonJS dependencies to be - // loaded as well so that all cycles have been found and knownCjsModuleTypes is reliable. - await rollupContext.load(resolved); - } else if (detectCyclesAndConditional && knownCjsModuleTypes[parentId]) { - knownCjsModuleTypes[parentId] = IS_WRAPPED_COMMONJS; - } + parentMeta.requires.push({ resolved, isConditional }); + await setTypesForRequiredModules(parentId, resolved, isConditional, rollupContext.load); return { id: childId, allowProxy: true }; }) ); - return { - requireTargets: requireTargets.map(({ id: dependencyId, allowProxy }, index) => { - const isCommonJS = getTypeForFullyAnalyzedModule(dependencyId); - return { - source: sources[index].source, - id: allowProxy - ? isCommonJS === IS_WRAPPED_COMMONJS - ? wrapId(dependencyId, WRAPPED_SUFFIX) - : wrapId(dependencyId, PROXY_SUFFIX) - : dependencyId, - isCommonJS - }; - }), - usesRequireWrapper: getTypeForFullyAnalyzedModule(parentId) === IS_WRAPPED_COMMONJS - }; + parentMeta.isCommonJS = getTypeForFullyAnalyzedModule(parentId); + return requireTargets.map(({ id: dependencyId, allowProxy }, index) => { + // eslint-disable-next-line no-multi-assign + const isCommonJS = (parentMeta.isRequiredCommonJS[ + dependencyId + ] = getTypeForFullyAnalyzedModule(dependencyId)); + return { + source: sources[index].source, + id: allowProxy + ? isCommonJS === IS_WRAPPED_COMMONJS + ? wrapId(dependencyId, WRAPPED_SUFFIX) + : wrapId(dependencyId, PROXY_SUFFIX) + : dependencyId, + isCommonJS + }; + }); } }; } diff --git a/packages/commonjs/src/transform-commonjs.js b/packages/commonjs/src/transform-commonjs.js index 851115b47..5a71d0517 100644 --- a/packages/commonjs/src/transform-commonjs.js +++ b/packages/commonjs/src/transform-commonjs.js @@ -51,9 +51,10 @@ export default async function transformCommonjs( astCache, defaultIsModuleExports, needsRequireWrapper, - resolveRequireSourcesAndGetMeta, + resolveRequireSourcesAndUpdateMeta, isRequired, - checkDynamicRequire + checkDynamicRequire, + commonjsMeta ) { const ast = astCache || tryParse(parse, code, id); const magicString = new MagicString(code); @@ -437,7 +438,7 @@ export default async function transformCommonjs( ) && (ignoreGlobal || !uses.global) ) { - return { meta: { commonjs: { isCommonJS: false, isMixedModule: false } } }; + return { meta: { commonjs: { isCommonJS: false } } }; } let leadingComment = ''; @@ -459,7 +460,7 @@ export default async function transformCommonjs( ? 'exports' : 'module'; - const { importBlock, usesRequireWrapper } = await rewriteRequireExpressionsAndGetImportBlock( + const importBlock = await rewriteRequireExpressionsAndGetImportBlock( magicString, topLevelDeclarations, reassignedNames, @@ -469,12 +470,14 @@ export default async function transformCommonjs( exportsName, id, exportMode, - resolveRequireSourcesAndGetMeta, + resolveRequireSourcesAndUpdateMeta, needsRequireWrapper, isEsModule, isDynamicRequireModulesEnabled, - getIgnoreTryCatchRequireStatementMode + getIgnoreTryCatchRequireStatementMode, + commonjsMeta ); + const usesRequireWrapper = commonjsMeta.isCommonJS === IS_WRAPPED_COMMONJS; const exportBlock = isEsModule ? '' : rewriteExportsAndGetExportsBlock( @@ -527,11 +530,6 @@ function ${requireName} () { code: magicString.toString(), map: sourceMap ? magicString.generateMap() : null, syntheticNamedExports: isEsModule || usesRequireWrapper ? false : '__moduleExports', - meta: { - commonjs: { - isCommonJS: !isEsModule && (usesRequireWrapper ? IS_WRAPPED_COMMONJS : true), - isMixedModule: isEsModule - } - } + meta: { commonjs: commonjsMeta } }; } diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/_config.js b/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/_config.js new file mode 100644 index 000000000..8c1d053bd --- /dev/null +++ b/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/_config.js @@ -0,0 +1,22 @@ +const path = require('path'); + +module.exports = { + description: + 'use correct side-effects flags for files that become entry points after they are loaded', + options: { + treeshake: { moduleSideEffects: false }, + plugins: [ + { + load(id) { + if (id.endsWith('foo.js')) { + this.emitFile({ type: 'chunk', id: path.join(__dirname, 'foo.js') }); + } + } + } + ], + output: { chunkFileNames: 'generated-[name].js' } + }, + global: (global, t) => { + t.is(global.foo, 'foo'); + } +}; diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/foo.js b/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/foo.js new file mode 100644 index 000000000..53d1fab39 --- /dev/null +++ b/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/foo.js @@ -0,0 +1,2 @@ +// This side-effect will only be respected if this is an entry point +global.foo = 'foo'; diff --git a/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/main.js b/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/main.js new file mode 100644 index 000000000..60af16024 --- /dev/null +++ b/packages/commonjs/test/fixtures/function/module-side-effects-late-entry/main.js @@ -0,0 +1,3 @@ +import './foo.js'; + +export default 'main'; diff --git a/packages/commonjs/test/fixtures/function/plugin-isentry/_config.js b/packages/commonjs/test/fixtures/function/plugin-isentry/_config.js new file mode 100644 index 000000000..156176ad9 --- /dev/null +++ b/packages/commonjs/test/fixtures/function/plugin-isentry/_config.js @@ -0,0 +1,36 @@ +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +const ID_MAIN = path.join(__dirname, 'main.js'); +const ID_OTHER = path.join(__dirname, 'other.js'); + +module.exports = { + description: 'provides correct values for ModuleInfo.isEntry to not break legacy plugins', + options: { + input: [ID_MAIN, ID_OTHER], + output: { + chunkFileNames: '[name].js' + }, + plugins: [ + { + transform(code, id) { + if (this.getModuleInfo(id).isEntry) { + return `import "polyfill";\n${code}`; + } + return null; + }, + resolveId(id) { + if (id === 'polyfill') return id; + return null; + }, + load(id) { + if (id === 'polyfill') { + return `global.entryDetected = true;`; + } + return null; + } + } + ] + } +}; diff --git a/packages/commonjs/test/fixtures/function/plugin-isentry/dep.js b/packages/commonjs/test/fixtures/function/plugin-isentry/dep.js new file mode 100644 index 000000000..2f5dc9fbe --- /dev/null +++ b/packages/commonjs/test/fixtures/function/plugin-isentry/dep.js @@ -0,0 +1,2 @@ +t.is(global.entryDetected, true); +module.exports = 'dep'; diff --git a/packages/commonjs/test/fixtures/function/plugin-isentry/main.js b/packages/commonjs/test/fixtures/function/plugin-isentry/main.js new file mode 100644 index 000000000..811a39ee5 --- /dev/null +++ b/packages/commonjs/test/fixtures/function/plugin-isentry/main.js @@ -0,0 +1,2 @@ +t.is(global.entryDetected, true); +module.exports = require('./dep.js'); diff --git a/packages/commonjs/test/fixtures/function/plugin-isentry/other.js b/packages/commonjs/test/fixtures/function/plugin-isentry/other.js new file mode 100644 index 000000000..661ce2f2e --- /dev/null +++ b/packages/commonjs/test/fixtures/function/plugin-isentry/other.js @@ -0,0 +1 @@ +export const other = true; diff --git a/packages/commonjs/test/fixtures/function/preserve-modules/_config.js b/packages/commonjs/test/fixtures/function/preserve-modules/_config.js new file mode 100644 index 000000000..dbca5a506 --- /dev/null +++ b/packages/commonjs/test/fixtures/function/preserve-modules/_config.js @@ -0,0 +1,14 @@ +module.exports = { + description: 'uses correct entry files names when preserving modules', + options: { + preserveModules: true + }, + pluginOptions: { + // Our entry is wrapped, so it will not be functional without a proper proxy + strictRequires: true + }, + // This will only work if "main" corresponds to the actual entry point that unwraps main.js + global: (global, t) => { + t.is(global.main, 'main'); + } +}; diff --git a/packages/commonjs/test/fixtures/function/preserve-modules/main.js b/packages/commonjs/test/fixtures/function/preserve-modules/main.js new file mode 100644 index 000000000..7696217a1 --- /dev/null +++ b/packages/commonjs/test/fixtures/function/preserve-modules/main.js @@ -0,0 +1,4 @@ +global.main = 'main'; +console.log('main'); + +module.exports = 'main'; diff --git a/packages/commonjs/test/form.js b/packages/commonjs/test/form.js index 270d27ccf..182498e8b 100644 --- a/packages/commonjs/test/form.js +++ b/packages/commonjs/test/form.js @@ -54,15 +54,16 @@ if (path.sep === '/') { (config.solo ? test.only : test)(dir, (t) => Promise.all( inputEntries.map(async ([outputName, id]) => { - const { transform } = commonjs(config.options); - + const { buildStart, transform } = commonjs(config.options); + buildStart.call({ meta: { rollupVersion: '99.0.0' } }, { plugins: [] }); transformContext.getModuleInfo = (moduleId) => { return { isEntry: config.entry && moduleId === id, importers: config.importers && config.importers[outputName] ? config.importers[outputName].map((x) => `fixtures/form/${dir}/${x}`) - : [] + : [], + meta: {} }; }; const input = fs.readFileSync(id, 'utf-8'); diff --git a/packages/commonjs/test/snapshots/function.js.md b/packages/commonjs/test/snapshots/function.js.md index 1e80780c8..d928b3a85 100644 --- a/packages/commonjs/test/snapshots/function.js.md +++ b/packages/commonjs/test/snapshots/function.js.md @@ -3668,15 +3668,23 @@ Generated by [AVA](https://avajs.dev). { 'main.js': `'use strict';␊ ␊ - var other = require('./other.js');␊ + var other = require('./other-aeb2ae1d.js');␊ ␊ - t.is(other, 'foo');␊ + t.is(other.other, 'foo');␊ `, - 'other.js': `'use strict';␊ + 'other-aeb2ae1d.js': `'use strict';␊ ␊ var other = 'foo';␊ ␊ - module.exports = other;␊ + exports.other = other;␊ + `, + 'other.js': `'use strict';␊ + ␊ + var other = require('./other-aeb2ae1d.js');␊ + ␊ + ␊ + ␊ + module.exports = other.other;␊ `, } @@ -4772,11 +4780,11 @@ Generated by [AVA](https://avajs.dev). { 'main.js': `'use strict';␊ ␊ - var other = require('./other.js');␊ + var other = require('./other2.js');␊ ␊ var main = {};␊ ␊ - const foo = other;␊ + const foo = other.other;␊ ␊ t.is(foo, 'foo');␊ ␊ @@ -4784,9 +4792,17 @@ Generated by [AVA](https://avajs.dev). `, 'other.js': `'use strict';␊ ␊ + var other = require('./other2.js');␊ + ␊ + ␊ + ␊ + module.exports = other.other;␊ + `, + 'other2.js': `'use strict';␊ + ␊ var other = 'foo';␊ ␊ - module.exports = other;␊ + exports.other = other;␊ `, } @@ -4895,6 +4911,36 @@ Generated by [AVA](https://avajs.dev). `, } +## module-side-effects-late-entry + +> Snapshot 1 + + { + 'generated-foo.js': `'use strict';␊ + ␊ + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};␊ + ␊ + // This side-effect will only be respected if this is an entry point␊ + commonjsGlobal.foo = 'foo';␊ + `, + 'generated-foo2.js': `'use strict';␊ + ␊ + require('./generated-foo.js');␊ + ␊ + var foo = {};␊ + ␊ + module.exports = foo;␊ + `, + 'main.js': `'use strict';␊ + ␊ + require('./generated-foo.js');␊ + ␊ + var main = 'main';␊ + ␊ + module.exports = main;␊ + `, + } + ## module_require > Snapshot 1 @@ -5292,6 +5338,85 @@ Generated by [AVA](https://avajs.dev). `, } +## plugin-isentry + +> Snapshot 1 + + { + 'main.js': `'use strict';␊ + ␊ + require('./polyfill.js');␊ + ␊ + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};␊ + ␊ + t.is(commonjsGlobal.entryDetected, true);␊ + var dep = 'dep';␊ + ␊ + t.is(commonjsGlobal.entryDetected, true);␊ + var main = dep;␊ + ␊ + module.exports = main;␊ + `, + 'other.js': `'use strict';␊ + ␊ + Object.defineProperty(exports, '__esModule', { value: true });␊ + ␊ + require('./polyfill.js');␊ + ␊ + const other = true;␊ + ␊ + exports.other = other;␊ + `, + 'polyfill.js': `'use strict';␊ + ␊ + global.entryDetected = true;␊ + `, + } + +## preserve-modules + +> Snapshot 1 + + { + '_virtual/_commonjsHelpers.js': `'use strict';␊ + ␊ + Object.defineProperty(exports, '__esModule', { value: true });␊ + ␊ + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};␊ + ␊ + exports.commonjsGlobal = commonjsGlobal;␊ + `, + 'main.js': `'use strict';␊ + ␊ + var main = require('./main2.js');␊ + ␊ + var mainExports = main.__require();␊ + ␊ + module.exports = mainExports;␊ + `, + 'main2.js': `'use strict';␊ + ␊ + Object.defineProperty(exports, '__esModule', { value: true });␊ + ␊ + var _commonjsHelpers = require('./_virtual/_commonjsHelpers.js');␊ + ␊ + var main;␊ + var hasRequiredMain;␊ + ␊ + function requireMain () {␊ + if (hasRequiredMain) return main;␊ + hasRequiredMain = 1;␊ + _commonjsHelpers.commonjsGlobal.main = 'main';␊ + console.log('main');␊ + ␊ + main = 'main';␊ + return main;␊ + }␊ + ␊ + exports.__require = requireMain;␊ + `, + } + ## react-apollo > Snapshot 1 diff --git a/packages/commonjs/test/snapshots/function.js.snap b/packages/commonjs/test/snapshots/function.js.snap index 56d991917..f12cfa25e 100644 Binary files a/packages/commonjs/test/snapshots/function.js.snap and b/packages/commonjs/test/snapshots/function.js.snap differ diff --git a/packages/commonjs/test/snapshots/test.js.md b/packages/commonjs/test/snapshots/test.js.md index 1b1ebe464..d565de262 100644 --- a/packages/commonjs/test/snapshots/test.js.md +++ b/packages/commonjs/test/snapshots/test.js.md @@ -115,3 +115,173 @@ Generated by [AVA](https://avajs.dev). ␊ export { foo as default };␊ ` + +## updates all relevant modules when using the cache and the wrapping of a module changes + +> Snapshot 1 + + `'use strict';␊ + ␊ + var first = {};␊ + ␊ + var second = {};␊ + ␊ + second.second = 'second';␊ + ␊ + first.first = 'first'; first.first += second.second;␊ + ␊ + var main = 'main' + first.first;␊ + ␊ + module.exports = main;␊ + ` + +> Snapshot 2 + + `'use strict';␊ + ␊ + var first = {};␊ + ␊ + var second = {};␊ + ␊ + var hasRequiredSecond;␊ + ␊ + function requireSecond () {␊ + if (hasRequiredSecond) return second;␊ + hasRequiredSecond = 1;␊ + second.second = 'second'; second.second += requireFirst().first;␊ + return second;␊ + }␊ + ␊ + var hasRequiredFirst;␊ + ␊ + function requireFirst () {␊ + if (hasRequiredFirst) return first;␊ + hasRequiredFirst = 1;␊ + first.first = 'first'; first.first += requireSecond().second;␊ + return first;␊ + }␊ + ␊ + var main = 'main' + requireFirst().first;␊ + ␊ + module.exports = main;␊ + ` + +> Snapshot 3 + + `'use strict';␊ + ␊ + function getAugmentedNamespace(n) {␊ + var f = n.default;␊ + if (typeof f == "function") {␊ + var a = function () {␊ + return f.apply(this, arguments);␊ + };␊ + a.prototype = f.prototype;␊ + } else a = {};␊ + Object.defineProperty(a, '__esModule', {value: true});␊ + Object.keys(n).forEach(function (k) {␊ + var d = Object.getOwnPropertyDescriptor(n, k);␊ + Object.defineProperty(a, k, d.get ? d : {␊ + enumerable: true,␊ + get: function () {␊ + return n[k];␊ + }␊ + });␊ + });␊ + return a;␊ + }␊ + ␊ + var second = {};␊ + ␊ + var hasRequiredSecond;␊ + ␊ + function requireSecond () {␊ + if (hasRequiredSecond) return second;␊ + hasRequiredSecond = 1;␊ + second.second = 'second';␊ + return second;␊ + }␊ + ␊ + let first = 'first'; if (Math.random() < 1) first += requireSecond().second;␊ + ␊ + var first$1 = /*#__PURE__*/Object.freeze({␊ + __proto__: null,␊ + get first () { return first; }␊ + });␊ + ␊ + var require$$0 = /*@__PURE__*/getAugmentedNamespace(first$1);␊ + ␊ + var main = 'main' + require$$0.first;␊ + ␊ + module.exports = main;␊ + ` + +## updates mixed modules when using the cache and the wrapping of a module changes + +> Snapshot 1 + + `'use strict';␊ + ␊ + Object.defineProperty(exports, '__esModule', { value: true });␊ + ␊ + var first = {};␊ + ␊ + first.first = 'first';␊ + ␊ + const main = 'main'; const result = first.first;␊ + ␊ + exports.main = main;␊ + exports.result = result;␊ + ` + +> Snapshot 2 + + `'use strict';␊ + ␊ + Object.defineProperty(exports, '__esModule', { value: true });␊ + ␊ + var main$1 = /*#__PURE__*/Object.freeze({␊ + __proto__: null,␊ + get main () { return main; },␊ + get result () { return result; }␊ + });␊ + ␊ + function getAugmentedNamespace(n) {␊ + var f = n.default;␊ + if (typeof f == "function") {␊ + var a = function () {␊ + return f.apply(this, arguments);␊ + };␊ + a.prototype = f.prototype;␊ + } else a = {};␊ + Object.defineProperty(a, '__esModule', {value: true});␊ + Object.keys(n).forEach(function (k) {␊ + var d = Object.getOwnPropertyDescriptor(n, k);␊ + Object.defineProperty(a, k, d.get ? d : {␊ + enumerable: true,␊ + get: function () {␊ + return n[k];␊ + }␊ + });␊ + });␊ + return a;␊ + }␊ + ␊ + var first = {};␊ + ␊ + var require$$0 = /*@__PURE__*/getAugmentedNamespace(main$1);␊ + ␊ + var hasRequiredFirst;␊ + ␊ + function requireFirst () {␊ + if (hasRequiredFirst) return first;␊ + hasRequiredFirst = 1;␊ + first.first = 'first' + require$$0.main;␊ + return first;␊ + }␊ + ␊ + const main = 'main'; const result = requireFirst().first;␊ + ␊ + exports.main = main;␊ + exports.result = result;␊ + ` diff --git a/packages/commonjs/test/snapshots/test.js.snap b/packages/commonjs/test/snapshots/test.js.snap index 685297264..f5ee342d7 100644 Binary files a/packages/commonjs/test/snapshots/test.js.snap and b/packages/commonjs/test/snapshots/test.js.snap differ diff --git a/packages/commonjs/test/test.js b/packages/commonjs/test/test.js index 4a297a301..2f86e9f17 100644 --- a/packages/commonjs/test/test.js +++ b/packages/commonjs/test/test.js @@ -1,11 +1,13 @@ /* eslint-disable line-comment-position, no-new-func, no-undefined */ -import * as path from 'path'; import os from 'os'; +import * as path from 'path'; + import nodeResolve from '@rollup/plugin-node-resolve'; import test from 'ava'; import { getLocator } from 'locate-character'; + import { rollup } from 'rollup'; import { SourceMapConsumer } from 'source-map'; import { install } from 'source-map-support'; @@ -19,6 +21,23 @@ install(); process.chdir(__dirname); +const loader = (modules) => { + return { + load(id) { + if (Object.hasOwnProperty.call(modules, id)) { + return modules[id]; + } + return null; + }, + resolveId(id) { + if (Object.hasOwnProperty.call(modules, id)) { + return id; + } + return null; + } + }; +}; + test('Rollup peer dependency has correct format', (t) => { t.regex(peerDependencies.rollup, /^\^\d+\.\d+\.\d+(\|\|\^\d+\.\d+\.\d+)*$/); }); @@ -449,20 +468,18 @@ test('does not warn even if the ES module does not export "default"', async (t) }); test('compiles with cache', async (t) => { - // specific commonjs require() to ensure same instance is used - // eslint-disable-next-line global-require - const commonjsInstance = require('..'); + const plugin = commonjs(); - const bundle = await rollup({ + const { cache } = await rollup({ input: 'fixtures/function/index/main.js', - plugins: [commonjsInstance()] + plugins: [plugin] }); await t.notThrowsAsync( rollup({ input: 'fixtures/function/index/main.js', - plugins: [commonjsInstance()], - cache: bundle + plugins: [plugin], + cache }) ); }); @@ -547,20 +564,10 @@ test('produces optimized code when importing esm with a known default export', a input: 'main.js', plugins: [ commonjs({ requireReturnsDefault: true }), - { - load(id) { - if (id === 'main.js') { - return 'module.exports = require("esm.js")'; - } - if (id === 'esm.js') { - return 'export const ignored = "ignored"; export default "default"'; - } - return null; - }, - resolveId(id) { - return id; - } - } + loader({ + 'main.js': 'module.exports = require("esm.js")', + 'esm.js': 'export const ignored = "ignored"; export default "default"' + }) ] }); t.snapshot(await getCodeFromBundle(bundle)); @@ -571,20 +578,10 @@ test('produces optimized code when importing esm without a default export', asyn input: 'main.js', plugins: [ commonjs(), - { - load(id) { - if (id === 'main.js') { - return 'module.exports = require("esm.js")'; - } - if (id === 'esm.js') { - return 'export const value = "value";'; - } - return null; - }, - resolveId(id) { - return id; - } - } + loader({ + 'main.js': 'module.exports = require("esm.js")', + 'esm.js': 'export const value = "value";' + }) ] }); t.snapshot(await getCodeFromBundle(bundle)); @@ -763,3 +760,108 @@ test('throws when using an inadequate node_resolve version', async (t) => { 'Insufficient @rollup/plugin-node-resolve version: "@rollup/plugin-commonjs" requires at least @rollup/plugin-node-resolve@13.0.6 but found @rollup/plugin-node-resolve@13.0.5.' }); }); + +test('updates all relevant modules when using the cache and the wrapping of a module changes', async (t) => { + const modules = {}; + const resetModules = () => { + modules['main.js'] = "module.exports = 'main' + require('first.js').first;"; + modules['first.js'] = "exports.first = 'first'; exports.first += require('second.js').second;"; + modules['second.js'] = "exports.second = 'second';"; + }; + const options = { + input: 'main.js', + plugins: [commonjs({ transformMixedEsModules: true }), loader(modules)], + onwarn(warning) { + if (warning.code !== 'CIRCULAR_DEPENDENCY') { + throw new Error(warning.message); + } + } + }; + + resetModules(); + let bundle = await rollup(options); + t.is((await executeBundle(bundle, t)).exports, 'mainfirstsecond'); + const firstCode = await getCodeFromBundle(bundle); + t.snapshot(firstCode); + + options.cache = bundle.cache; + modules['second.js'] = "exports.second = 'second'; exports.second += require('first.js').first;"; + bundle = await rollup(options); + t.is((await executeBundle(bundle, t)).exports, 'mainfirstsecondfirst'); + t.snapshot(await getCodeFromBundle(bundle)); + + options.cache = bundle.cache; + resetModules(); + bundle = await rollup(options); + t.is(await getCodeFromBundle(bundle), firstCode); + + options.cache = bundle.cache; + modules['first.js'] = + "export let first = 'first'; if (Math.random() < 1) first += require('second.js').second;"; + bundle = await rollup(options); + t.is((await executeBundle(bundle, t)).exports, 'mainfirstsecond'); + t.snapshot(await getCodeFromBundle(bundle)); + + options.cache = bundle.cache; + resetModules(); + bundle = await rollup(options); + t.is(await getCodeFromBundle(bundle), firstCode); +}); + +test('updates mixed modules when using the cache and the wrapping of a module changes', async (t) => { + const modules = {}; + const resetModules = () => { + modules['main.js'] = + "export const main = 'main'; export const result = require('first.js').first;"; + modules['first.js'] = "exports.first = 'first';"; + }; + const options = { + input: 'main.js', + plugins: [commonjs({ transformMixedEsModules: true }), loader(modules)], + onwarn(warning) { + if (warning.code !== 'CIRCULAR_DEPENDENCY') { + throw new Error(warning.message); + } + } + }; + + resetModules(); + let bundle = await rollup(options); + t.deepEqual((await executeBundle(bundle, t)).exports, { main: 'main', result: 'first' }); + const firstCode = await getCodeFromBundle(bundle); + t.snapshot(firstCode); + + options.cache = bundle.cache; + modules['first.js'] = "exports.first = 'first' + require('main.js').main"; + bundle = await rollup(options); + t.deepEqual((await executeBundle(bundle, t)).exports, { main: 'main', result: 'firstmain' }); + t.snapshot(await getCodeFromBundle(bundle)); + + options.cache = bundle.cache; + resetModules(); + bundle = await rollup(options); + t.is(await getCodeFromBundle(bundle), firstCode); +}); + +test('allows the config to be reused', async (t) => { + const config = { + preserveModules: true, + plugins: [ + commonjs({ requireReturnsDefault: true }), + loader({ + 'foo.js': "console.log('foo')", + 'bar.js': "console.log('bar')" + }) + ] + }; + let bundle = await rollup({ input: 'foo.js', ...config }); + t.deepEqual( + bundle.cache.modules.map(({ id }) => id), + ['foo.js', 'foo.js?commonjs-entry'] + ); + bundle = await rollup({ input: 'bar.js', ...config }); + t.deepEqual( + bundle.cache.modules.map(({ id }) => id), + ['bar.js', 'bar.js?commonjs-entry'] + ); +});