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 wrapscommonjs
files when they are part of acommonjs
dependency 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 anif
statement or function. All othercommonjs
files will be hoisted. This is the recommended setting for most codebases. Note that detecting conditionalrequire
may 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 allcommonjs
files in functions that execute on firstrequire
, preservingnodejs
semantics. This is the safest setting and should be used if the generated code is incompatible with"auto"
. Note thatstrictRequires: true
may have some impact on generated code size and performance, but the impact is minimal if the code is minified.false
: Does not wrap anycommonjs
files and performsrequire
statement 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 ofcommonjs
fileid
s 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 whichcommonjs
modules 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.mjs
is 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
node
androllup
can be observed. The execution order ofnode
is as expected, but the execution order ofrollup
is not as expected.bashModule A is being loaded Module B is being loaded Module A has loaded B Module B has loaded A
bashModule B is being loaded Module B has loaded A Module A is being loaded Module A has loaded B
This is because
rollup
automatically hoistsrequire
statements to the top of the outermost scope when translatingcommonjs
modules toesm
modules (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
ESM
is completely different from the originalcommonjs
module. The translatedESM
will first executeb.cjs
module, then executea.cjs
module, while the originalcommonjs
module will first executea.cjs
module, then executeb.cjs
module.In
CommonJS
,require
is lazy executed, which means that the code of a module will start executing when the module is first required. If a moduleA
requires moduleA
again (directly or indirectly) in its loading process (before the moduleA
is resolved), it will return an uninitialized moduleA
object (module.export = {} inCommonJS
).In
ESM
,import
statements are hoisted and preloaded when the module is parsed, which means that the execution order is statically determined. If we try to replacerequire
withimport
, the initial execution order will be changed, and we cannot handle circular imports inCommonJS
style, becauseimport
is parsed at load time, not lazy load at runtime.Due to the static nature of
ESM
required, preload module behavior for lazy load scenarios that heavily rely oncommonjs
modules will cause certain side effects. For example, similar log statements emitted in different orders, some code depending on the initialization order ofpolyfills
inrequire
statements.Circular Dependencies
When
commonjs
modules have circularrequire
calls, due to these usually relying on nestedrequire
calls delayed execution, there will be problems.The following example is typical in
commonjs
syntax, whereindex.mjs
is 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
commonjs
syntax, which is commonly used incommonjs
syntax. In the above example,a.cjs
andb.cjs
two modules are run-time strongly related circular dependency relationships. In this scenario, the translatedESM
code from@rollup/plugin-commonjs
plugin (strictRequires
=false
) usually has problems.The following analyzes the translated code of
a.cjs
andb.cjs
modules.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
commonjs
syntax, we can useexports.value = 1
execution part to consume the exported value fromb.cjs
module, which is commonly used incommonjs
syntax.However, unfortunately,
ESM
syntax has static nature, which cannot consume the exported attributes from other modules likecommonjs
syntax. If we use it like above, that is, referencinga.cjs
module inb.cjs
module 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 toconst
declaration).jsconsole.log(a); const a = 1;
Of course, there is a way to solve the above problem in
esm
, that is, throughnamespace object
way to temporarily store the referenced module before it is parsed completed, then access it inb.cjs
module 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
esm
module in static load. Unlikecommonjs
syntax, which can access the referenced module before it is parsed completed incommonjs
.However, it should be noted that
esm
modulenamespace object
way still cannot consume the exported attributes from other modules likecommonjs
syntax. This is the limitation ofesm
syntax.
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 ----> 8
The execution timing of each module executing await this.load(resolvedId)
is as follows:
main -> 1 -> 2 -> 4 -> 3 -> 5 -> 8 -> 6 -> 9 -> 10 -> 7 -> 11
In 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 -> main
Comparing the execution timing of each module:
main -> 1 -> 2 -> 4 -> 3 -> 5 -> 8 -> 6 -> 9 -> 10 -> `7` -> 11
It 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.