Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(commonjs): support CJS modules re-exporting transpiled ESM modules #1165

Merged
merged 34 commits into from Apr 24, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f829b36
fix(commonjs): attach correct plugin meta-data to commonjs modules
lukastaegert Sep 21, 2021
1393c7c
feat(commonjs): reimplement dynamic import handling
lukastaegert Oct 28, 2021
35770bd
feat(commonjs): add strictRequires option to wrap modules
lukastaegert Nov 6, 2021
81628db
feat(commonjs): automatically wrap cyclic modules
lukastaegert Nov 9, 2021
93cf5fb
feat(commonjs): Infer type for unidentified modules
lukastaegert Nov 12, 2021
8d31f25
feat(commonjs): make namespace callable when requiring ESM with funct…
lukastaegert Nov 12, 2021
fb490c0
fix(commonjs): handle relative external imports
lukastaegert Nov 17, 2021
e672b42
feat(commonjs): limit ignoreTryCatch to external requires
lukastaegert Nov 19, 2021
748f8ba
feat(commonjs): auto-detect conditional requires
lukastaegert Nov 20, 2021
c399755
feat(commonjs): add dynamicRequireRoot option
lukastaegert Nov 21, 2021
4a84215
feat(commonjs): throw for dynamic requires from outside the configure…
lukastaegert Nov 22, 2021
7e2691a
refactor(commonjs): deconflict helpers only once globals are known
lukastaegert Nov 22, 2021
a16a3cd
feat(commonjs): expose plugin version
lukastaegert Nov 24, 2021
46776c6
fix(commonjs): do not transform "typeof exports" for mixed modules
lukastaegert Dec 2, 2021
373684d
fix(commonjs): inject module name into dynamic require function
lukastaegert Dec 14, 2021
b7e0d7e
fix(commonjs): validate node-resolve peer version
lukastaegert Dec 14, 2021
45a3141
fix(commonjs): use correct version and add package exports
lukastaegert Dec 14, 2021
12dbaa3
fix(commonjs): proxy all entries to not break legacy polyfill plugins
lukastaegert Jan 14, 2022
0fb0303
fix(commonjs): add heuristic to deoptimize requires after calling imp…
lukastaegert Feb 7, 2022
ebc1cb1
fix(commonjs): make sure type changes of esm imports are tracked
lukastaegert Feb 12, 2022
b61845f
fix(commonjs): handle external dependencies when using the cache
lukastaegert Feb 24, 2022
b1110d2
fix(commonjs): Do not change semantics when removing requires in if s…
lukastaegert Apr 3, 2022
f509b1a
fix(commonjs): Allow to dynamically require an empty file
lukastaegert Apr 3, 2022
347c148
Add a test to illustrate problematic re-exported module
fwouts Apr 8, 2022
2749ea9
fix(commonjs): support CJS modules re-exporting ESM modules
fwouts Apr 16, 2022
d637611
fix(commonjs): Warn when plugins do not pass options to resolveId
lukastaegert Apr 18, 2022
095491b
Merge remote-tracking branch 'upstream/commonjs/strict-require-order'…
fwouts Apr 19, 2022
e3b4b0c
Update snapshots post-merge
fwouts Apr 19, 2022
c77baef
Update tests
fwouts Apr 19, 2022
4863298
Update snapshot
fwouts Apr 19, 2022
159336a
Remove unnecessary fixtures
fwouts Apr 19, 2022
981918d
Remove comment given other tests do exactly the same
fwouts Apr 19, 2022
205b187
Update snapshots
lukastaegert Apr 24, 2022
224f904
Merge remote-tracking branch 'upstream/commonjs/strict-require-order'…
lukastaegert Apr 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 10 additions & 8 deletions packages/commonjs/src/index.js
Expand Up @@ -99,7 +99,7 @@ export default function commonjs(options = {}) {
};
};

const resolveId = getResolveId(extensions);
const { currentlyResolving, resolveId } = getResolveId(extensions);

const sourceMap = options.sourceMap !== false;

Expand Down Expand Up @@ -204,7 +204,11 @@ export default function commonjs(options = {}) {
'The namedExports option from "@rollup/plugin-commonjs" is deprecated. Named exports are now handled automatically.'
);
}
requireResolver = getRequireResolver(extensions, detectCyclesAndConditional);
requireResolver = getRequireResolver(
extensions,
detectCyclesAndConditional,
currentlyResolving
);
},

buildEnd() {
Expand Down Expand Up @@ -260,15 +264,13 @@ 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
);
const acutalId = id.slice(0, -ENTRY_SUFFIX.length);
return getEntryProxy(acutalId, getDefaultIsModuleExports(acutalId), this.getModuleInfo);
}

if (isWrappedId(id, ES_IMPORT_SUFFIX)) {
return getEsImportProxy(unwrapId(id, ES_IMPORT_SUFFIX), defaultIsModuleExports);
const actualId = unwrapId(id, ES_IMPORT_SUFFIX);
return getEsImportProxy(actualId, getDefaultIsModuleExports(actualId));
}

if (id === DYNAMIC_MODULES_ID) {
Expand Down
159 changes: 97 additions & 62 deletions packages/commonjs/src/resolve-id.js
Expand Up @@ -50,79 +50,114 @@ 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
const customOptions = resolveOptions.custom;
if (customOptions && customOptions['node-resolve'] && customOptions['node-resolve'].isRequire) {
return null;
}
if (isWrappedId(importee, WRAPPED_SUFFIX)) {
return unwrapId(importee, WRAPPED_SUFFIX);
}
const currentlyResolving = new Map();

if (
importee.endsWith(ENTRY_SUFFIX) ||
isWrappedId(importee, MODULE_SUFFIX) ||
isWrappedId(importee, EXPORTS_SUFFIX) ||
isWrappedId(importee, PROXY_SUFFIX) ||
isWrappedId(importee, ES_IMPORT_SUFFIX) ||
isWrappedId(importee, EXTERNAL_SUFFIX) ||
importee.startsWith(HELPERS_ID) ||
importee === DYNAMIC_MODULES_ID
) {
return importee;
}
return {
/**
* This is a Maps of importers to Sets of require sources being resolved at
* the moment by resolveRequireSourcesAndUpdateMeta
*/
currentlyResolving,
async resolveId(importee, importer, resolveOptions) {
const customOptions = resolveOptions.custom;
// All logic below is specific to ES imports.
// Also, if we do not skip this logic for requires that are resolved while
// transforming a commonjs file, it can easily lead to deadlocks.
if (
customOptions &&
customOptions['node-resolve'] &&
customOptions['node-resolve'].isRequire
) {
return null;
}
const currentlyResolvingForParent = currentlyResolving.get(importer);
if (currentlyResolvingForParent && currentlyResolvingForParent.has(importee)) {
this.warn({
code: 'THIS_RESOLVE_WITHOUT_OPTIONS',
message:
'It appears a plugin has implemented a "resolveId" hook that uses "this.resolve" without forwarding the third "options" parameter of "resolveId". This is problematic as it can lead to wrong module resolutions especially for the node-resolve plugin and in certain cases cause early exit errors for the commonjs plugin.\nIn rare cases, this warning can appear if the same file is both imported and required from the same mixed ES/CommonJS module, in which case it can be ignored.',
url: 'https://rollupjs.org/guide/en/#resolveid'
});
return null;
}

if (isWrappedId(importee, WRAPPED_SUFFIX)) {
return unwrapId(importee, WRAPPED_SUFFIX);
}

if (importer) {
if (
importer === DYNAMIC_MODULES_ID ||
// Proxies are only importing resolved ids, no need to resolve again
isWrappedId(importer, PROXY_SUFFIX) ||
isWrappedId(importer, ES_IMPORT_SUFFIX) ||
importer.endsWith(ENTRY_SUFFIX)
importee.endsWith(ENTRY_SUFFIX) ||
isWrappedId(importee, MODULE_SUFFIX) ||
isWrappedId(importee, EXPORTS_SUFFIX) ||
isWrappedId(importee, PROXY_SUFFIX) ||
isWrappedId(importee, ES_IMPORT_SUFFIX) ||
isWrappedId(importee, EXTERNAL_SUFFIX) ||
importee.startsWith(HELPERS_ID) ||
importee === DYNAMIC_MODULES_ID
) {
return importee;
}
if (isWrappedId(importer, EXTERNAL_SUFFIX)) {
// We need to return null for unresolved imports so that the proper warning is shown
if (!(await this.resolve(importee, importer, { skipSelf: true }))) {
return null;

if (importer) {
if (
importer === DYNAMIC_MODULES_ID ||
// Proxies are only importing resolved ids, no need to resolve again
isWrappedId(importer, PROXY_SUFFIX) ||
isWrappedId(importer, ES_IMPORT_SUFFIX) ||
importer.endsWith(ENTRY_SUFFIX)
) {
return importee;
}
if (isWrappedId(importer, EXTERNAL_SUFFIX)) {
// We need to return null for unresolved imports so that the proper warning is shown
if (
!(await this.resolve(
importee,
importer,
Object.assign({ skipSelf: true }, resolveOptions)
))
) {
return null;
}
// For other external imports, we need to make sure they are handled as external
return { id: importee, external: true };
}
// For other external imports, we need to make sure they are handled as external
return { id: importee, external: true };
}
}

if (importee.startsWith('\0')) {
return null;
}
if (importee.startsWith('\0')) {
return null;
}

// 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 resolved =
(await this.resolve(importee, importer, Object.assign({ skipSelf: true }, resolveOptions))) ||
resolveExtensions(importee, importer, extensions);
// 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)
) {
// 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 resolved =
(await this.resolve(
importee,
importer,
Object.assign({ skipSelf: true }, resolveOptions)
)) || resolveExtensions(importee, importer, extensions);
// 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 }
} = moduleInfo;
if (commonjsMeta && commonjsMeta.isCommonJS === IS_WRAPPED_COMMONJS) {
return { id: wrapId(resolved.id, ES_IMPORT_SUFFIX), meta: { commonjs: { resolved } } };
}
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 }
} = moduleInfo;
if (commonjsMeta && commonjsMeta.isCommonJS === IS_WRAPPED_COMMONJS) {
return { id: wrapId(resolved.id, ES_IMPORT_SUFFIX), meta: { commonjs: { resolved } } };
}
return resolved;
};
}
10 changes: 9 additions & 1 deletion packages/commonjs/src/resolve-require-sources.js
Expand Up @@ -9,7 +9,7 @@ import {
} from './helpers';
import { resolveExtensions } from './resolve-id';

export function getRequireResolver(extensions, detectCyclesAndConditional) {
export function getRequireResolver(extensions, detectCyclesAndConditional, currentlyResolving) {
const knownCjsModuleTypes = Object.create(null);
const requiredIds = Object.create(null);
const unconditionallyRequiredIds = Object.create(null);
Expand Down Expand Up @@ -161,16 +161,20 @@ export function getRequireResolver(extensions, detectCyclesAndConditional) {
parentMeta.requires = [];
parentMeta.isRequiredCommonJS = Object.create(null);
setInitialParentType(parentId, isParentCommonJS);
const currentlyResolvingForParent = currentlyResolving.get(parentId) || new Set();
currentlyResolving.set(parentId, currentlyResolvingForParent);
const requireTargets = await Promise.all(
sources.map(async ({ source, isConditional }) => {
// Never analyze or proxy internal modules
if (source.startsWith('\0')) {
return { id: source, allowProxy: false };
}
currentlyResolvingForParent.add(source);
const resolved =
(await rollupContext.resolve(source, parentId, {
custom: { 'node-resolve': { isRequire: true } }
})) || resolveExtensions(source, parentId, extensions);
currentlyResolvingForParent.delete(source);
if (!resolved) {
return { id: wrapId(source, EXTERNAL_SUFFIX), allowProxy: false };
}
Expand Down Expand Up @@ -201,6 +205,10 @@ export function getRequireResolver(extensions, detectCyclesAndConditional) {
isCommonJS
};
});
},
isCurrentlyResolving(source, parentId) {
const currentlyResolvingForParent = currentlyResolving.get(parentId);
return currentlyResolvingForParent && currentlyResolvingForParent.has(source);
}
};
}
8 changes: 6 additions & 2 deletions packages/commonjs/src/transform-commonjs.js
Expand Up @@ -71,6 +71,7 @@ export default async function transformCommonjs(
let programDepth = 0;
let currentTryBlockEnd = null;
let shouldWrap = false;
let reexports = false;

const globals = new Set();
// A conditionalNode is a node for which execution is not guaranteed. If such a node is a require
Expand Down Expand Up @@ -151,8 +152,9 @@ export default async function transformCommonjs(
if (hasDefineEsmProperty(node.right)) {
shouldWrap = true;
}
} else if (defaultIsModuleExports === false) {
} else if (isRequireExpression(node.right, scope)) {
shouldWrap = true;
reexports = true;
}
}
} else if (exportName === KEY_COMPILED_ESM) {
Expand Down Expand Up @@ -444,7 +446,9 @@ export default async function transformCommonjs(
shouldWrap = !isEsModule && (shouldWrap || (uses.exports && moduleExportsAssignments.length > 0));
const detectWrappedDefault =
shouldWrap &&
(topLevelDefineCompiledEsmExpressions.length > 0 || code.indexOf('__esModule') >= 0);
(reexports ||
topLevelDefineCompiledEsmExpressions.length > 0 ||
code.indexOf('__esModule') >= 0);

if (
!(
Expand Down
@@ -0,0 +1,3 @@
module.exports = {
description: 'creates the correct exports from CJS module re-exporting a transpiled ES module',
};
@@ -0,0 +1,2 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'default';
@@ -0,0 +1,3 @@
import dep from './proxy';

t.is(dep, 'default');
@@ -0,0 +1 @@
module.exports = require('./dep');
@@ -0,0 +1,3 @@
module.exports = {
description: 'creates the correct exports from CJS module re-exporting a transpiled ES module',
};
@@ -0,0 +1,2 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'default';
@@ -0,0 +1,3 @@
import * as entry from './proxy';

t.deepEqual(entry, { default: 'default' });
@@ -0,0 +1 @@
module.exports = require('./entry');
@@ -0,0 +1,3 @@
module.exports = {
description: 'creates the correct exports from CJS module re-exporting a transpiled ES module',
};
@@ -0,0 +1,3 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'default';
exports.named = 'named';
@@ -0,0 +1,3 @@
import * as entry from './proxy';

t.deepEqual(entry, { default: 'default', named: 'named' });
@@ -0,0 +1 @@
module.exports = require('./entry');
@@ -0,0 +1,3 @@
module.exports = {
description: 'creates the correct exports from CJS module re-exporting a transpiled ES module',
};
@@ -0,0 +1,2 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.named = 'named';
@@ -0,0 +1,8 @@
import * as entry from './proxy';

t.deepEqual(entry, {
default: {
named: 'named',
},
named: 'named'
});
@@ -0,0 +1 @@
module.exports = require('./entry');
@@ -0,0 +1,3 @@
module.exports = {
description: 'creates the correct exports from CJS module re-exporting a transpiled ES module',
};
@@ -0,0 +1,3 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.named = 'named';
exports.default = 'default';
@@ -0,0 +1,4 @@
import dep, { named } from './proxy';

t.is(dep, 'default');
t.is(named, 'named');
@@ -0,0 +1 @@
module.exports = require('./dep');
@@ -0,0 +1,3 @@
module.exports = {
description: 'creates the correct exports from CJS module re-exporting a transpiled ES module',
};
@@ -0,0 +1,2 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.named = 'named';
@@ -0,0 +1,3 @@
import { named } from './proxy';

t.is(named, 'named');
@@ -0,0 +1 @@
module.exports = require('./dep');