Skip to content

Commit 8f02987

Browse files
bluwylukastaegert
andauthored
feat(commonjs)!: default strictRequires to true (#1639)
* feat(commonjs)!: default strictRequires to true * test: update fixtures * test: update different values * test: update some tests to use auto * test: adapt assertion While the error message has a different variable name, the point of the test is that we get an error. * test: correctly pass options in test Unfortunately, I did not find a way to detect this case from the plugin and show a warning. * fix(commonjs): respect defaultIsModuleExports:false for ES imports from wrapped CommonJS * test: adapt assertions for wrapped CommonJS * docs: improve readme to explain about CommonJS entry points * test: allow tests to specify an entry point for running the code * test: refine late-entry tests * fix(commonjs): treat moduleSideEffects as __PURE__ comments for wrapped modules * fix(commonjs): treat moduleSideEffects as __PURE__ comments for proxied wrapped modules --------- Co-authored-by: Lukas Taegert-Atkinson <[email protected]>
1 parent 274b72c commit 8f02987

File tree

75 files changed

+4787
-1744
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+4787
-1744
lines changed

packages/commonjs/README.md

+28-4
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ When used together with the node-resolve plugin
4949
### `strictRequires`
5050

5151
Type: `"auto" | boolean | "debug" | string[]`<br>
52-
Default: `"auto"`
52+
Default: `true`
5353

54-
By default, this plugin will try to hoist `require` statements as imports to the top of each file. While this works well for many code bases and allows for very efficient ESM output, it does not perfectly capture CommonJS semantics as the initialisation order of required modules will be different. The resultant side effects can include log statements being emitted in a different order, and some code that is dependent on the initialisation order of polyfills in require statements may not work. But it is especially problematic when there are circular `require` calls between CommonJS modules as those often rely on the lazy execution of nested `require` calls.
54+
Historically, this plugin tried to hoist `require` statements as imports to the top of each file. While this works well for many code bases and allows for very efficient ESM output, it does not perfectly capture CommonJS semantics as the initialisation order of required modules will be different. The resultant side effects can include log statements being emitted in a different order, and some code that is dependent on the initialisation order of polyfills in require statements may not work. But it is especially problematic when there are circular `require` calls between CommonJS modules as those often rely on the lazy execution of nested `require` calls.
5555

56-
Setting this option to `true` will wrap all CommonJS files in functions which are executed when they are required for the first time, preserving NodeJS semantics. This is the safest setting and should be used if the generated code does not work correctly with `"auto"`. Note that `strictRequires: true` can have a small impact on the size and performance of generated code, but less so if the code is minified.
56+
The default value of `true` will wrap all CommonJS files in functions which are executed when they are required for the first time, preserving NodeJS semantics. This is the safest setting and should be used if the generated code does not work correctly with `"auto"`. Note that `strictRequires: true` can have a small impact on the size and performance of generated code, but less so if the code is minified.
5757

58-
The default value of `"auto"` will only wrap CommonJS files when they are part of a CommonJS dependency cycle, e.g. an index file that is required by some of its dependencies, or if they are only required in a potentially "conditional" way like from within an if-statement or a function. All other CommonJS files are hoisted. This is the recommended setting for most code bases. Note that the detection of conditional requires can be subject to race conditions if there are both conditional and unconditional requires of the same file, which in edge cases may result in inconsistencies between builds. If you think this is a problem for you, you can avoid this by using any value other than `"auto"` or `"debug"`.
58+
Setting this option to `"auto"` will only wrap CommonJS files when they are part of a CommonJS dependency cycle, e.g. an index file that is required by some of its dependencies, or if they are only required in a potentially "conditional" way like from within an if-statement or a function. All other CommonJS files are hoisted. This is the recommended setting for most code bases. Note that the detection of conditional requires can be subject to race conditions if there are both conditional and unconditional requires of the same file, which in edge cases may result in inconsistencies between builds. If you think this is a problem for you, you can avoid this by using any value other than `"auto"` or `"debug"`.
5959

6060
`false` will entirely prevent wrapping and hoist all files. This may still work depending on the nature of cyclic dependencies but will often cause problems.
6161

@@ -386,6 +386,30 @@ For these situations, you can change Rollup's behaviour either globally or per m
386386

387387
To change this for individual modules, you can supply a function for `requireReturnsDefault` instead. This function will then be called once for each required ES module or external dependency with the corresponding id and allows you to return different values for different modules.
388388

389+
## Using CommonJS files as entry points
390+
391+
With this plugin, you can also use CommonJS files as entry points. This means, however, that when you are bundling to an ES module, your bundle will only have a default export. If you want named exports instead, you should use an ES module entry point instead that reexports from your CommonJS entry point, e.g.
392+
393+
```js
394+
// main.cjs, the CommonJS entry
395+
exports.foo = 'foo';
396+
exports.bar = 'bar';
397+
398+
// main.mjs, the ES module entry
399+
export { foo, bar } from './main.cjs';
400+
401+
// rollup.config.mjs
402+
export default {
403+
input: 'main.mjs',
404+
output: {
405+
format: 'es',
406+
file: 'bundle.mjs'
407+
}
408+
};
409+
```
410+
411+
When bundling to CommonJS, i.e `output.format === 'cjs'`, make sure that you do not set `output.exports` to `'named'`. The default value of `'auto'` will usually work, but you can also set it explicitly to `'default'`. That makes sure that Rollup assigns the default export that was generated for your CommonJS entry point to `module.exports`, and semantics do not change.
412+
389413
## Using with @rollup/plugin-node-resolve
390414

391415
Since most CommonJS packages you are importing are probably dependencies in `node_modules`, you may need to use [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/master/packages/node-resolve):

packages/commonjs/src/generate-imports.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ function processRequireExpressions(
165165
magicString
166166
) {
167167
const generateRequireName = getGenerateRequireName();
168-
for (const { source, id: resolvedId, isCommonJS } of requireTargets) {
168+
for (const { source, id: resolvedId, isCommonJS, wrappedModuleSideEffects } of requireTargets) {
169169
const requires = requiresBySource[source];
170170
const name = generateRequireName(requires);
171171
let usesRequired = false;
@@ -184,7 +184,11 @@ function processRequireExpressions(
184184
} else if (canConvertRequire) {
185185
needsImport = true;
186186
if (isCommonJS === IS_WRAPPED_COMMONJS) {
187-
magicString.overwrite(node.start, node.end, `${name}()`);
187+
magicString.overwrite(
188+
node.start,
189+
node.end,
190+
`${wrappedModuleSideEffects ? '' : '/*@__PURE__*/ '}${name}()`
191+
);
188192
} else if (usesReturnValue) {
189193
usesRequired = true;
190194
magicString.overwrite(node.start, node.end, name);

packages/commonjs/src/helpers.js

+7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ export const isWrappedId = (id, suffix) => id.endsWith(suffix);
22
export const wrapId = (id, suffix) => `\0${id}${suffix}`;
33
export const unwrapId = (wrappedId, suffix) => wrappedId.slice(1, -suffix.length);
44

5+
// A proxy module when a module is required from non-wrapped CommonJS. Is different for ESM and CommonJS requires.
56
export const PROXY_SUFFIX = '?commonjs-proxy';
7+
// Indicates that a required module is wrapped commonjs and needs special handling.
68
export const WRAPPED_SUFFIX = '?commonjs-wrapped';
9+
// Indicates that a required module is external
710
export const EXTERNAL_SUFFIX = '?commonjs-external';
11+
// A helper module that contains the exports object of a module
812
export const EXPORTS_SUFFIX = '?commonjs-exports';
13+
// A helper module that contains the module object of a module, e.g. when module.exports is reassigned
914
export const MODULE_SUFFIX = '?commonjs-module';
15+
// A special proxy for CommonJS entry points
1016
export const ENTRY_SUFFIX = '?commonjs-entry';
17+
// A proxy when wrapped ESM is required from CommonJS
1118
export const ES_IMPORT_SUFFIX = '?commonjs-es-import';
1219

1320
export const DYNAMIC_MODULES_ID = '\0commonjs-dynamic-modules';

packages/commonjs/src/index.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extname, relative, resolve, dirname } from 'path';
1+
import { dirname, extname, relative, resolve } from 'path';
22

33
import { createFilter } from '@rollup/pluginutils';
44

@@ -239,7 +239,7 @@ export default function commonjs(options = {}) {
239239
}
240240
},
241241

242-
load(id) {
242+
async load(id) {
243243
if (id === HELPERS_ID) {
244244
return getHelpersModule();
245245
}
@@ -285,7 +285,11 @@ export default function commonjs(options = {}) {
285285

286286
if (isWrappedId(id, ES_IMPORT_SUFFIX)) {
287287
const actualId = unwrapId(id, ES_IMPORT_SUFFIX);
288-
return getEsImportProxy(actualId, getDefaultIsModuleExports(actualId));
288+
return getEsImportProxy(
289+
actualId,
290+
getDefaultIsModuleExports(actualId),
291+
(await this.load({ id: actualId })).moduleSideEffects
292+
);
289293
}
290294

291295
if (id === DYNAMIC_MODULES_ID) {

packages/commonjs/src/proxies.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,28 @@ export function getEntryProxy(id, defaultIsModuleExports, getModuleInfo, shebang
5757
}
5858
return shebang + code;
5959
}
60-
const result = getEsImportProxy(id, defaultIsModuleExports);
60+
const result = getEsImportProxy(id, defaultIsModuleExports, true);
6161
return {
6262
...result,
6363
code: shebang + result.code
6464
};
6565
}
6666

67-
export function getEsImportProxy(id, defaultIsModuleExports) {
67+
export function getEsImportProxy(id, defaultIsModuleExports, moduleSideEffects) {
6868
const name = getName(id);
6969
const exportsName = `${name}Exports`;
7070
const requireModule = `require${capitalize(name)}`;
7171
let code =
7272
`import { getDefaultExportFromCjs } from "${HELPERS_ID}";\n` +
7373
`import { __require as ${requireModule} } from ${JSON.stringify(id)};\n` +
74-
`var ${exportsName} = ${requireModule}();\n` +
74+
`var ${exportsName} = ${moduleSideEffects ? '' : '/*@__PURE__*/ '}${requireModule}();\n` +
7575
`export { ${exportsName} as __moduleExports };`;
7676
if (defaultIsModuleExports === true) {
7777
code += `\nexport { ${exportsName} as default };`;
78+
} else if (defaultIsModuleExports === false) {
79+
code += `\nexport default ${exportsName}.default;`;
7880
} else {
79-
code += `export default /*@__PURE__*/getDefaultExportFromCjs(${exportsName});`;
81+
code += `\nexport default /*@__PURE__*/getDefaultExportFromCjs(${exportsName});`;
8082
}
8183
return {
8284
code,

packages/commonjs/src/resolve-id.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,7 @@ export default function getResolveId(extensions, isPossibleCjsId) {
6363
// All logic below is specific to ES imports.
6464
// Also, if we do not skip this logic for requires that are resolved while
6565
// transforming a commonjs file, it can easily lead to deadlocks.
66-
if (
67-
customOptions &&
68-
customOptions['node-resolve'] &&
69-
customOptions['node-resolve'].isRequire
70-
) {
66+
if (customOptions?.['node-resolve']?.isRequire) {
7167
return null;
7268
}
7369
const currentlyResolvingForParent = currentlyResolving.get(importer);

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,14 @@ export function getRequireResolver(extensions, detectCyclesAndConditional, curre
192192
// eslint-disable-next-line no-multi-assign
193193
const isCommonJS = (parentMeta.isRequiredCommonJS[dependencyId] =
194194
getTypeForFullyAnalyzedModule(dependencyId));
195+
const isWrappedCommonJS = isCommonJS === IS_WRAPPED_COMMONJS;
195196
fullyAnalyzedModules[dependencyId] = true;
196197
return {
198+
wrappedModuleSideEffects:
199+
isWrappedCommonJS && rollupContext.getModuleInfo(dependencyId).moduleSideEffects,
197200
source: sources[index].source,
198201
id: allowProxy
199-
? isCommonJS === IS_WRAPPED_COMMONJS
200-
? wrapId(dependencyId, WRAPPED_SUFFIX)
201-
: wrapId(dependencyId, PROXY_SUFFIX)
202+
? wrapId(dependencyId, isWrappedCommonJS ? WRAPPED_SUFFIX : PROXY_SUFFIX)
202203
: dependencyId,
203204
isCommonJS
204205
};

packages/commonjs/src/utils.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ export function capitalize(name) {
4343

4444
export function getStrictRequiresFilter({ strictRequires }) {
4545
switch (strictRequires) {
46-
case true:
47-
return { strictRequiresFilter: () => true, detectCyclesAndConditional: false };
4846
// eslint-disable-next-line no-undefined
4947
case undefined:
48+
case true:
49+
return { strictRequiresFilter: () => true, detectCyclesAndConditional: false };
5050
case 'auto':
5151
case 'debug':
5252
case null:
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22

3-
var input = async function () {
4-
// TODO
5-
};
3+
var input;
4+
var hasRequiredInput;
65

7-
export default /*@__PURE__*/commonjsHelpers.getDefaultExportFromCjs(input);
8-
export { input as __moduleExports };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
input = async function () {
10+
// TODO
11+
};
12+
return input;
13+
}
14+
15+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-assign-exports/input.js?commonjs-exports";
33

4-
input.__esModule = true;
5-
var _default = input.default = 'x';
6-
var foo = input.foo = 'foo';
4+
var hasRequiredInput;
75

8-
export { input as __moduleExports, foo, _default as default };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
input.__esModule = true;
10+
input.default = 'x';
11+
input.foo = 'foo';
12+
return input;
13+
}
14+
15+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-assign-module/input.js?commonjs-exports";
33

4-
input.__esModule = true;
5-
var _default = input.default = 'x';
6-
var foo = input.foo = 'foo';
4+
var hasRequiredInput;
75

8-
export { input as __moduleExports, foo, _default as default };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
input.__esModule = true;
10+
input.default = 'x';
11+
input.foo = 'foo';
12+
return input;
13+
}
14+
15+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-deconflict/input.js?commonjs-exports";
33

4-
Object.defineProperty(input, '__esModule', { value: true });
5-
var foo_1 = input.foo = 'bar';
4+
var hasRequiredInput;
65

7-
const foo = 'also bar';
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
Object.defineProperty(input, '__esModule', { value: true });
10+
input.foo = 'bar';
811

9-
export { input as __moduleExports, foo_1 as foo, input as default };
12+
const foo = 'also bar';
13+
return input;
14+
}
15+
16+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-define-exports-empty/input.js?commonjs-exports";
33

4-
Object.defineProperty(input, "__esModule", { value: true });
4+
var hasRequiredInput;
55

6-
export { input as __moduleExports, input as default };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
Object.defineProperty(input, "__esModule", { value: true });
10+
return input;
11+
}
12+
13+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-define-exports/input.js?commonjs-exports";
33

4-
Object.defineProperty(input, '__esModule', { value: true });
5-
var _default = input.default = 'x';
6-
var foo = input.foo = 'foo';
4+
var hasRequiredInput;
75

8-
export { input as __moduleExports, foo, _default as default };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
Object.defineProperty(input, '__esModule', { value: true });
10+
input.default = 'x';
11+
input.foo = 'foo';
12+
return input;
13+
}
14+
15+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-define-module/input.js?commonjs-exports";
33

4-
Object.defineProperty(input, '__esModule', { value: true });
5-
var _default = input.default = 'x';
6-
var foo = input.foo = 'foo';
4+
var hasRequiredInput;
75

8-
export { input as __moduleExports, foo, _default as default };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
Object.defineProperty(input, '__esModule', { value: true });
10+
input.default = 'x';
11+
input.foo = 'foo';
12+
return input;
13+
}
14+
15+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-minified/input.js?commonjs-exports";
33

4-
Object.defineProperty(input, '__esModule', { value: !0 });
5-
var foo = input.foo = 'foo';
4+
var hasRequiredInput;
65

7-
export { input as __moduleExports, foo, input as default };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
Object.defineProperty(input, '__esModule', { value: !0 });
10+
input.foo = 'foo';
11+
return input;
12+
}
13+
14+
export { requireInput as __require };
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __exports as input } from "\u0000fixtures/form/compiled-esm-only-named/input.js?commonjs-exports";
33

4-
Object.defineProperty(input, '__esModule', { value: true });
5-
var foo = input.foo = 'bar';
6-
var bar = input.bar = 'foo';
4+
var hasRequiredInput;
75

8-
export { input as __moduleExports, foo, bar, input as default };
6+
function requireInput () {
7+
if (hasRequiredInput) return input;
8+
hasRequiredInput = 1;
9+
Object.defineProperty(input, '__esModule', { value: true });
10+
input.foo = 'bar';
11+
input.bar = 'foo';
12+
return input;
13+
}
14+
15+
export { requireInput as __require };

packages/commonjs/test/fixtures/form/compiled-esm-reassign-exports/output.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import * as commonjsHelpers from "_commonjsHelpers.js";
22
import { __module as inputModule } from "\u0000fixtures/form/compiled-esm-reassign-exports/input.js?commonjs-module";
33
var input = inputModule.exports;
44

5-
Object.defineProperty(input, '__esModule', { value: true });
6-
inputModule.exports = { foo: 'bar' };
5+
var hasRequiredInput;
76

8-
var inputExports = inputModule.exports;
9-
export default /*@__PURE__*/commonjsHelpers.getDefaultExportFromCjs(inputExports);
10-
export { inputExports as __moduleExports };
7+
function requireInput () {
8+
if (hasRequiredInput) return inputModule.exports;
9+
hasRequiredInput = 1;
10+
Object.defineProperty(input, '__esModule', { value: true });
11+
inputModule.exports = { foo: 'bar' };
12+
return inputModule.exports;
13+
}
14+
15+
export { requireInput as __require };

0 commit comments

Comments
 (0)