Skip to content

Commit c583aaf

Browse files
committed
fix(commonjs): add heuristic to deoptimize requires after calling imported function (requires [email protected]) (#1038)
BREAKING CHANGES: Requires at least [email protected]
1 parent 7434b0f commit c583aaf

File tree

23 files changed

+660
-180
lines changed

23 files changed

+660
-180
lines changed

packages/commonjs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"require"
5353
],
5454
"peerDependencies": {
55-
"rollup": "^2.67.0"
55+
"rollup": "^2.68.0"
5656
},
5757
"dependencies": {
5858
"@rollup/pluginutils": "^3.1.0",
@@ -68,7 +68,7 @@
6868
"@rollup/plugin-node-resolve": "^13.1.0",
6969
"locate-character": "^2.0.5",
7070
"require-relative": "^0.8.7",
71-
"rollup": "^2.67.3",
71+
"rollup": "^2.68.0",
7272
"shx": "^0.3.2",
7373
"source-map": "^0.7.3",
7474
"source-map-support": "^0.5.19",

packages/commonjs/src/proxies.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ export function getEsImportProxy(id, defaultIsModuleExports) {
7373
}
7474
return {
7575
code,
76-
syntheticNamedExports: '__moduleExports',
77-
meta: { commonjs: { isCommonJS: false } }
76+
syntheticNamedExports: '__moduleExports'
7877
};
7978
}

packages/commonjs/src/resolve-id.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export default function getResolveId(extensions) {
121121
meta: { commonjs: commonjsMeta }
122122
} = moduleInfo;
123123
if (commonjsMeta && commonjsMeta.isCommonJS === IS_WRAPPED_COMMONJS) {
124-
return wrapId(resolved.id, ES_IMPORT_SUFFIX);
124+
return { id: wrapId(resolved.id, ES_IMPORT_SUFFIX), meta: { commonjs: { resolved } } };
125125
}
126126
return resolved;
127127
};

packages/commonjs/src/resolve-require-sources.js

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
2+
ES_IMPORT_SUFFIX,
23
EXTERNAL_SUFFIX,
34
IS_WRAPPED_COMMONJS,
5+
isWrappedId,
46
PROXY_SUFFIX,
57
wrapId,
68
WRAPPED_SUFFIX
@@ -27,28 +29,28 @@ export function getRequireResolver(extensions, detectCyclesAndConditional) {
2729
return false;
2830
};
2931

32+
// Once a module is listed here, its type (wrapped or not) is fixed and may
33+
// not change for the rest of the current build, to not break already
34+
// transformed modules.
3035
const fullyAnalyzedModules = Object.create(null);
3136

3237
const getTypeForFullyAnalyzedModule = (id) => {
3338
const knownType = knownCjsModuleTypes[id];
3439
if (knownType !== true || !detectCyclesAndConditional || fullyAnalyzedModules[id]) {
3540
return knownType;
3641
}
37-
fullyAnalyzedModules[id] = true;
3842
if (isCyclic(id)) {
3943
return (knownCjsModuleTypes[id] = IS_WRAPPED_COMMONJS);
4044
}
4145
return knownType;
4246
};
4347

4448
const setInitialParentType = (id, initialCommonJSType) => {
45-
// It is possible a transformed module is already fully analyzed when using
46-
// the cache and one dependency introduces a new cycle. Then transform is
47-
// run for a fully analzyed module again. Fully analyzed modules may never
48-
// change their type as importers already trust their type.
49-
knownCjsModuleTypes[id] = fullyAnalyzedModules[id]
50-
? knownCjsModuleTypes[id]
51-
: initialCommonJSType;
49+
// Fully analyzed modules may never change type
50+
if (fullyAnalyzedModules[id]) {
51+
return;
52+
}
53+
knownCjsModuleTypes[id] = initialCommonJSType;
5254
if (
5355
detectCyclesAndConditional &&
5456
knownCjsModuleTypes[id] === true &&
@@ -59,7 +61,7 @@ export function getRequireResolver(extensions, detectCyclesAndConditional) {
5961
}
6062
};
6163

62-
const setTypesForRequiredModules = async (parentId, resolved, isConditional, loadModule) => {
64+
const analyzeRequiredModule = async (parentId, resolved, isConditional, loadModule) => {
6365
const childId = resolved.id;
6466
requiredIds[childId] = true;
6567
if (!(isConditional || knownCjsModuleTypes[parentId] === IS_WRAPPED_COMMONJS)) {
@@ -68,41 +70,85 @@ export function getRequireResolver(extensions, detectCyclesAndConditional) {
6870

6971
getDependencies(parentId).add(childId);
7072
if (!isCyclic(childId)) {
71-
// This makes sure the current transform handler waits for all direct dependencies to be
72-
// loaded and transformed and therefore for all transitive CommonJS dependencies to be
73-
// loaded as well so that all cycles have been found and knownCjsModuleTypes is reliable.
73+
// This makes sure the current transform handler waits for all direct
74+
// dependencies to be loaded and transformed and therefore for all
75+
// transitive CommonJS dependencies to be loaded as well so that all
76+
// cycles have been found and knownCjsModuleTypes is reliable.
7477
await loadModule(resolved);
7578
}
7679
};
7780

81+
const getTypeForImportedModule = async (resolved, loadModule) => {
82+
if (resolved.id in knownCjsModuleTypes) {
83+
// This handles cyclic ES dependencies
84+
return knownCjsModuleTypes[resolved.id];
85+
}
86+
const {
87+
meta: { commonjs }
88+
} = await loadModule(resolved);
89+
return (commonjs && commonjs.isCommonJS) || false;
90+
};
91+
7892
return {
7993
getWrappedIds: () =>
8094
Object.keys(knownCjsModuleTypes).filter(
8195
(id) => knownCjsModuleTypes[id] === IS_WRAPPED_COMMONJS
8296
),
8397
isRequiredId: (id) => requiredIds[id],
84-
async shouldTransformCachedModule({ id: parentId, meta: { commonjs: parentMeta } }) {
85-
// Ignore modules that did not pass through the original transformer in a previous build
86-
if (!(parentMeta && parentMeta.requires)) {
87-
return false;
88-
}
89-
setInitialParentType(parentId, parentMeta.initialCommonJSType);
90-
await Promise.all(
91-
parentMeta.requires.map(({ resolved, isConditional }) =>
92-
setTypesForRequiredModules(parentId, resolved, isConditional, this.load)
93-
)
94-
);
95-
if (getTypeForFullyAnalyzedModule(parentId) !== parentMeta.isCommonJS) {
96-
return true;
97-
}
98-
for (const {
99-
resolved: { id }
100-
} of parentMeta.requires) {
101-
if (getTypeForFullyAnalyzedModule(id) !== parentMeta.isRequiredCommonJS[id]) {
98+
async shouldTransformCachedModule({
99+
id: parentId,
100+
resolvedSources,
101+
meta: { commonjs: parentMeta }
102+
}) {
103+
// We explicitly track ES modules to handle ciruclar imports
104+
if (!(parentMeta && parentMeta.isCommonJS)) knownCjsModuleTypes[parentId] = false;
105+
if (isWrappedId(parentId, ES_IMPORT_SUFFIX)) return false;
106+
const parentRequires = parentMeta && parentMeta.requires;
107+
if (parentRequires) {
108+
setInitialParentType(parentId, parentMeta.initialCommonJSType);
109+
await Promise.all(
110+
parentRequires.map(({ resolved, isConditional }) =>
111+
analyzeRequiredModule(parentId, resolved, isConditional, this.load)
112+
)
113+
);
114+
if (getTypeForFullyAnalyzedModule(parentId) !== parentMeta.isCommonJS) {
102115
return true;
103116
}
117+
for (const {
118+
resolved: { id }
119+
} of parentRequires) {
120+
if (getTypeForFullyAnalyzedModule(id) !== parentMeta.isRequiredCommonJS[id]) {
121+
return true;
122+
}
123+
}
124+
// Now that we decided to go with the cached copy, neither the parent
125+
// module nor any of its children may change types anymore
126+
fullyAnalyzedModules[parentId] = true;
127+
for (const {
128+
resolved: { id }
129+
} of parentRequires) {
130+
fullyAnalyzedModules[id] = true;
131+
}
104132
}
105-
return false;
133+
const parentRequireSet = new Set((parentRequires || []).map(({ resolved: { id } }) => id));
134+
return (
135+
await Promise.all(
136+
Object.keys(resolvedSources)
137+
.map((source) => resolvedSources[source])
138+
.filter(({ id }) => !parentRequireSet.has(id))
139+
.map(async (resolved) => {
140+
if (isWrappedId(resolved.id, ES_IMPORT_SUFFIX)) {
141+
return (
142+
(await getTypeForImportedModule(
143+
(await this.load({ id: resolved.id })).meta.commonjs.resolved,
144+
this.load
145+
)) !== IS_WRAPPED_COMMONJS
146+
);
147+
}
148+
return (await getTypeForImportedModule(resolved, this.load)) === IS_WRAPPED_COMMONJS;
149+
})
150+
)
151+
).some((shouldTransform) => shouldTransform);
106152
},
107153
/* eslint-disable no-param-reassign */
108154
resolveRequireSourcesAndUpdateMeta: (rollupContext) => async (
@@ -133,16 +179,18 @@ export function getRequireResolver(extensions, detectCyclesAndConditional) {
133179
return { id: wrapId(childId, EXTERNAL_SUFFIX), allowProxy: false };
134180
}
135181
parentMeta.requires.push({ resolved, isConditional });
136-
await setTypesForRequiredModules(parentId, resolved, isConditional, rollupContext.load);
182+
await analyzeRequiredModule(parentId, resolved, isConditional, rollupContext.load);
137183
return { id: childId, allowProxy: true };
138184
})
139185
);
140186
parentMeta.isCommonJS = getTypeForFullyAnalyzedModule(parentId);
187+
fullyAnalyzedModules[parentId] = true;
141188
return requireTargets.map(({ id: dependencyId, allowProxy }, index) => {
142189
// eslint-disable-next-line no-multi-assign
143190
const isCommonJS = (parentMeta.isRequiredCommonJS[
144191
dependencyId
145192
] = getTypeForFullyAnalyzedModule(dependencyId));
193+
fullyAnalyzedModules[dependencyId] = true;
146194
return {
147195
source: sources[index].source,
148196
id: allowProxy

packages/commonjs/src/transform-commonjs.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default async function transformCommonjs(
9595
const topLevelDefineCompiledEsmExpressions = [];
9696
const replacedGlobal = [];
9797
const replacedDynamicRequires = [];
98+
const importedVariables = new Set();
9899

99100
walk(ast, {
100101
enter(node, parent) {
@@ -208,6 +209,11 @@ export default async function transformCommonjs(
208209
}
209210

210211
if (!isRequireExpression(node, scope)) {
212+
const keypath = getKeypath(node.callee);
213+
if (keypath && importedVariables.has(keypath.name)) {
214+
// Heuristic to deoptimize requires after a required function has been called
215+
currentConditionalNodeEnd = Infinity;
216+
}
211217
return;
212218
}
213219

@@ -236,6 +242,11 @@ export default async function transformCommonjs(
236242
currentConditionalNodeEnd !== null,
237243
parent.type === 'ExpressionStatement' ? parent : node
238244
);
245+
if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
246+
for (const name of extractAssignedNames(parent.id)) {
247+
importedVariables.add(name);
248+
}
249+
}
239250
}
240251
return;
241252
}
@@ -448,7 +459,9 @@ export default async function transformCommonjs(
448459
magicString.remove(0, commentEnd).trim();
449460
}
450461

451-
const exportMode = shouldWrap
462+
const exportMode = isEsModule
463+
? 'none'
464+
: shouldWrap
452465
? uses.module
453466
? 'module'
454467
: 'exports'
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
2-
import { __exports as input } from "\u0000fixtures/form/unambiguous-with-default-export/input.js?commonjs-exports"
32
import "\u0000CWD/fixtures/form/unambiguous-with-default-export/foo.js?commonjs-proxy";
43

54
export default {};
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
2-
import { __exports as input } from "\u0000fixtures/form/unambiguous-with-import/input.js?commonjs-exports"
32
import "\u0000CWD/fixtures/form/unambiguous-with-import/foo.js?commonjs-proxy";
43

54
import './bar.js';
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
2-
import { __exports as input } from "\u0000fixtures/form/unambiguous-with-named-export/input.js?commonjs-exports"
32
import "\u0000CWD/fixtures/form/unambiguous-with-named-export/foo.js?commonjs-proxy";
43

54
export {};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
description:
3+
'uses strict require semantics for all modules that are required after an imported function is called'
4+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'browser';

0 commit comments

Comments
 (0)