default strictRequires to true
The strictRequires property is a configuration property of the @rollup/plugin-commonjs plugin, used to control the behavior when translating commonjs modules to esm modules.
Introduction to strictRequires Property
Type:
"auto" | boolean | "debug" | string[]Default value:true
"auto": Only wrapscommonjsfiles when they are part of acommonjsdependency cycle, for example, when an index file is required by some of its dependencies, or when they are only required in a potentially "conditional" way, such as from anifstatement or function. All othercommonjsfiles will be hoisted. This is the recommended setting for most codebases. Note that detecting conditionalrequiremay have race condition issues - if the same file has both conditional and unconditionalrequire, it may lead to inconsistencies between builds in extreme cases. If you believe this is causing problems for you, you can avoid this by using any value other than"auto"or"debug".true: Default value. Wraps allcommonjsfiles in functions that execute on firstrequire, preservingnodejssemantics. This is the safest setting and should be used if the generated code is incompatible with"auto". Note thatstrictRequires: truemay have some impact on generated code size and performance, but the impact is minimal if the code is minified.false: Does not wrap anycommonjsfiles and performsrequirestatement hoisting. This may still work depending on the nature of circular dependencies, but usually causes issues."debug":"debug"works similarly to"auto", but after bundling, it will display a warning containing a list ofcommonjsfileids that have been wrapped, which can be used as picomatch patterns for fine-tuning or avoiding the potential race conditions mentioned with"auto".string[]: Provides a picomatch pattern or array of patterns to specify whichcommonjsmodules need to be wrapped in functions.
Historical Defects in CommonJS to ESM Translation
Historically, the @rollup/plugin-commonjs plugin would attempt to hoist require statements to the top of each file, loading them as import exports in the esm module style. While this worked well for many codebases, and the esm modules translated from commonjs possessed features similar to native esm, there were certain defects. Due to the inherent dynamic nature of commonjs, there were edge cases where the translated esm would be semantically inconsistent with the original commonjs.
Execution Order Discrepancy
To illustrate this, consider the following example, where
index.mjsis the entry module.jsexport * from './a.cjs';jsconsole.log('Module A is being loaded'); const b = require('./b.cjs'); console.log('Module A has loaded B'); module.exports = 'This is A';jsconsole.log('Module B is being loaded'); const a = require('./a.cjs'); console.log('Module B has loaded A'); module.exports = 'This is B';The execution behavior of the products from
nodeandrollupcan be observed. The execution order ofnodeis as expected, but the execution order ofrollupis not as expected.bashModule A is being loaded Module B is being loaded Module A has loaded B Module B has loaded AbashModule B is being loaded Module B has loaded A Module A is being loaded Module A has loaded BThis is because
rollupautomatically hoistsrequirestatements to the top of the outermost scope when translatingcommonjsmodules toesmmodules (strictRequires=false).jsconsole.log('Module A is being loaded'); const b = require('./b.cjs'); console.log('Module A has loaded B'); module.exports = 'This is A';jsimport * as commonjsHelpers from 'commonjsHelpers.js'; import require$$0 from '\u0000/Users/Project/vrite/examples/commonjs/src/strict-requires/b.cjs?commonjs-proxy'; console.log('Module A is being loaded'); const b = require$$0; console.log('Module A has loaded B'); var a = 'This is A'; export default /*@__PURE__*/ commonjsHelpers.getDefaultExportFromCjs(a); export { a as __moduleExports };It is clear that the execution behavior of the translated
ESMis completely different from the originalcommonjsmodule. The translatedESMwill first executeb.cjsmodule, then executea.cjsmodule, while the originalcommonjsmodule will first executea.cjsmodule, then executeb.cjsmodule.In
CommonJS,requireis lazy executed, which means that the code of a module will start executing when the module is first required. If a moduleArequires moduleAagain (directly or indirectly) in its loading process (before the moduleAis resolved), it will return an uninitialized moduleAobject (module.export = {} inCommonJS).In
ESM,importstatements are hoisted and preloaded when the module is parsed, which means that the execution order is statically determined. If we try to replacerequirewithimport, the initial execution order will be changed, and we cannot handle circular imports inCommonJSstyle, becauseimportis parsed at load time, not lazy load at runtime.Due to the static nature of
ESMrequired, preload module behavior for lazy load scenarios that heavily rely oncommonjsmodules will cause certain side effects. For example, similar log statements emitted in different orders, some code depending on the initialization order ofpolyfillsinrequirestatements.Circular Dependencies
When
commonjsmodules have circularrequirecalls, due to these usually relying on nestedrequirecalls delayed execution, there will be problems.The following example is typical in
commonjssyntax, whereindex.mjsis the entry module.jsexport * from './a.cjs';jsconsole.log('Module A is being loaded'); exports.value = 1; const b = require('./b.cjs'); console.log('In Module A, b.value =', b.value); const sum = exports.value + b.value; console.log('In Module A, sum =', sum); module.exports = { sum };jsconsole.log('Module B is being loaded'); const a = require('./a.cjs'); console.log('In Module B, a.value =', a.value); module.exports = { value: 2 };The above example is a typical scenario of circular dependency usage in
commonjssyntax, which is commonly used incommonjssyntax. In the above example,a.cjsandb.cjstwo modules are run-time strongly related circular dependency relationships. In this scenario, the translatedESMcode from@rollup/plugin-commonjsplugin (strictRequires=false) usually has problems.The following analyzes the translated code of
a.cjsandb.cjsmodules.jsconsole.log('Module A is being loaded'); exports.value = 1; const b = require('./b.cjs'); console.log('In Module A, b.value =', b.value); const sum = exports.value + b.value; console.log('In Module A, sum =', sum); module.exports = { sum };jsimport * as commonjsHelpers from 'commonjsHelpers.js'; import { __module as aModule } from '\u0000/Users/Project/vrite/examples/commonjs/src/strict-requires/case2/a.cjs?commonjs-module'; var a = aModule.exports; import require$$0 from '\u0000/Users/Project/vrite/examples/commonjs/src/strict-requires/case2/b.cjs?commonjs-proxy'; (function (module, exports) { console.log('Module A is being loaded'); exports.value = 1; const b = require$$0; console.log('In Module A, b.value =', b.value); const sum = exports.value + b.value; console.log('In Module A, sum =', sum); module.exports = { sum }; })(aModule, aModule.exports); var aExports = aModule.exports; export default /*@__PURE__*/ commonjsHelpers.getDefaultExportFromCjs( aExports ); export { aExports as __moduleExports };jsconsole.log('Module B is being loaded'); const a = require('./a.cjs'); console.log('In Module B, a.value =', a.value); module.exports = { value: 2 };jsimport * as commonjsHelpers from 'commonjsHelpers.js'; import require$$0 from '\u0000/Users/Project/vrite/examples/commonjs/src/strict-requires/case2/a.cjs?commonjs-proxy'; console.log('Module B is being loaded'); const a = require$$0; console.log('In Module B, a.value =', a.value); var b = { value: 2 }; export default /*@__PURE__*/ commonjsHelpers.getDefaultExportFromCjs(b); export { b as __moduleExports };Due to the dynamic nature of
commonjssyntax, we can useexports.value = 1execution part to consume the exported value fromb.cjsmodule, which is commonly used incommonjssyntax.However, unfortunately,
ESMsyntax has static nature, which cannot consume the exported attributes from other modules likecommonjssyntax. If we use it like above, that is, referencinga.cjsmodule inb.cjsmodule before it is parsed completed, execution will show:ReferenceError: Cannot access 'XXX' before initialization
That is, in
esm, if we directly access the referenced module before it is parsed completed, it will prompt access to uninitialized, causing errors (similar toconstdeclaration).jsconsole.log(a); const a = 1;Of course, there is a way to solve the above problem in
esm, that is, throughnamespace objectway to temporarily store the referenced module before it is parsed completed, then access it inb.cjsmodule after the module is parsed completed.js// index.mjs import a from './a.mjs'; a();js// a.mjs import b from './b.mjs'; export const a = 'a'; export default b;js// b.mjs import * as temporaryNamespace from './a.mjs'; export default function () { return temporaryNamespace; }Essentially, it is to delay access to the referenced module before it is parsed completed, then access it after the module is parsed completed, which is the characteristic of
esmmodule in static load. Unlikecommonjssyntax, which can access the referenced module before it is parsed completed incommonjs.However, it should be noted that
esmmodulenamespace objectway still cannot consume the exported attributes from other modules likecommonjssyntax. This is the limitation ofesmsyntax.
strictRequires Default Setting to auto
Due to the above two reasons, the @rollup/plugin-commonjs plugin defaults strictRequires to auto to solve the boundary problem. When strictRequires is set to "auto", only wraps commonjs files when they are part of a commonjs dependency cycle, similar to nodejs handling commonjs syntax behavior, executing module on first require, preserving nodejs semantics. Other cases will still be hoisted, which is the recommended setting for most codebases.
Conditional require Detection May Have Race Condition Issues
However, this creates a problem, conditional require detection may have race condition issues, if the same file has both conditional and unconditional require, it may lead to inconsistencies between builds in extreme cases.
This can be found from @rollup/plugin-commonjs sometimes fails to detect circular dependency issue.
Prerequisite Knowledge
@rollup/plugin-commonjs is loaded by await loadModule(resolved) way to load sub-dependency modules, loadModule is essentially calling rollup exposed plugin context load method, which is explained in rollup plugin context section.
this.load:
rollup 为每一个插件提供插件执行上下文,也就是在插件中可以通过 this 来访问到插件上下文中的方法。this.load 方法就是插件上下文中的方法之一,当在插件中调用 this.load 方法时,会加载指定路径(resolvedId)的模块实例,不过需要注意的是:
- 当
resolvedId.resolveDependencies = true时,意味着完成当前模块的依赖项模块的路径解析。this.load方法会返回已经解析完成的当前模块,同时该模块的所有依赖项模块的路径(resolvedId)也已经解析完成(即moduleParsed钩子已经触发)。 - 当
未指定 resolvedId.resolveDependencies = false时(默认),意味着还未开始解析当前模块的依赖项模块的路径。this.load方法会返回解析完成的当前模块实例,但其所有依赖项模块的路径(resolvedId)均还未解析完成。
这有助于避免在 resolveId 钩子中等待 this.load 时出现死锁情况。预先加载指定模块,如果指定模块稍后成为图的一部分,则使用此上下文函数并不会带来额外的开销,因为由于缓存,指定模块不会再次解析。
Then, when executing await this.load(resolvedId) by default (resolvedId.resolveDependencies = false), it will return the parsed current module instance, but all dependency module paths (resolvedId) are not parsed completed.
Understanding the above prerequisite knowledge, then let's analyze the example in Issue 1425.
The following is the module dependency graph, main module is the entry module.
5 <-- 4 <-- 11 <-- 10
| ^ ^
V | |
6 1 <-- main 9
| | ^
V V |
7 --> 2 <=> 3 ----> 8The execution timing of each module executing await this.load(resolvedId) is as follows:
main -> 1 -> 2 -> 4 -> 3 -> 5 -> 8 -> 6 -> 9 -> 10 -> 7 -> 11In the backtracking phase, the @rollup/plugin-commonjs plugin will mark the module whether to wrap in functions through IS_WRAPPED_COMMONJS.
const isCyclic = id => {
const dependenciesToCheck = new Set(getDependencies(id));
for (const dependency of dependenciesToCheck) {
if (dependency === id) {
return true;
}
for (const childDependency of getDependencies(dependency)) {
dependenciesToCheck.add(childDependency);
}
}
return false;
};
const getTypeForFullyAnalyzedModule = id => {
const knownType = knownCjsModuleTypes[id];
if (
knownType !== true ||
!detectCyclesAndConditional ||
fullyAnalyzedModules[id]
) {
return knownType;
}
if (isCyclic(id)) {
return (knownCjsModuleTypes[id] = IS_WRAPPED_COMMONJS);
}
return knownType;
};
const isCommonJS = (parentMeta.isRequiredCommonJS[dependencyId] =
getTypeForFullyAnalyzedModule(dependencyId));The judgment logic has a key function isCyclic, if the current module is in a circular dependency, then the module will be marked whether to wrap in functions through IS_WRAPPED_COMMONJS.
The following is the backtracking timing of each module:
`7` -> 6 -> 5 -> 11 -> 4 -> 10 -> 9 -> 8 -> 3 -> 2 -> 11 -> mainComparing the execution timing of each module:
main -> 1 -> 2 -> 4 -> 3 -> 5 -> 8 -> 6 -> 9 -> 10 -> `7` -> 11It can be found that when 7 module executes completed and needs to backtrack, since 11 module has not started parsing, executing getDependencies(10) will not get any dependency. Then for 7 module, the decision path of isCyclic function is as follows:
`7` -> 2 -> 3 -> 8 -> 9 -> 10 -> X(getDependencies(10) value is empty)When judging 10 module, it is found that there are no dependencies, which means that 7 module is not in a circular dependency, then 7 module will not be wrapped in functions.
This is a wrong decision, the fact is not like this. 7 module is actually in a circular dependency, it needs to be wrapped in functions. await this.load(X) method only means that X module has been parsed completed, but it does not mean that all dependency modules of X module are completed parsed, this is the reason why circular import detection fails.
strictRequires Default Setting to true
Due to the above reasons, the @rollup/plugin-commonjs plugin defaults strictRequires to true. That is the modification proposed in default strictRequires to true PR, defaults strictRequires to true to solve the boundary problem of @rollup/plugin-commonjs plugin when translating commonjs modules to esm modules, that is, helping to solve the circular import detection failed race condition and ensuring build hash consistency.
When strictRequires default value is true, the @rollup/plugin-commonjs plugin will all commonjs files in functions, executing behavior similar to nodejs, this is the safest setting. This helps to solve the circular import detection failed race condition and ensure build hash consistency when setting "auto".
Impact on Product
The downside is that when strictRequires is set to true, it means that the @rollup/plugin-commonjs plugin ensures the accuracy of commonjs semantics, preserving the dynamic nature of commonjs syntax while giving up the static nature of commonjs syntax as much as possible for subsequent module tree-shaking optimization and product execution performance. Of course, if the code is executed minified, then the impact is minimal.
Special Example
If commonjs module is in unwrapped state (i.e., strictRequires = false or `strictRequires = 'auto' && commonjs module is not in a circular dependency)
import { a } from './commonjs.cjs';
console.log(a);exports.a = 1234;
exports.b = 4567;import commonjs from '@rollup/plugin-commonjs';
export default {
input: './main.js',
output: {
dir: 'dist',
format: 'esm'
},
plugins: [
commonjs({
// or (auto && the target module is not in a circular dependency).
strictRequires: false
})
]
};According to the above way to pack, since commonjs.cjs module conforms to unwrapped scenario, and commonjs uses exports to export attributes, then rollup will perform tree-shaking processing on the above commonjs module, that is, the b attribute that is not used in the final product.
This approach is usually expected, but due to the dynamic nature of commonjs language, that is, the exports.a attribute in commonjs does not necessarily exist after running.
exports.a = 1234;
exports.b = 4567;
delete exports.a;Then it does not apply to tree-shaking. Fortunately, the @rollup/plugin-commonjs plugin is smart, even if it sets the strictRequires: false flag, but if it is like the above example, the @rollup/plugin-commonjs plugin will wrap commonjs, which avoids rollup from executing tree-shaking operation on it.
Further Optimization (object tree-shaking)
Since the @rollup/plugin-commonjs plugin defaults strictRequires to true, rollup currently supports tree-shaking optimization for static esm modules, then for commonjs modules tree-shaking optimization needs further optimization. Detailed content can refer to object tree-shaking section.