Skip to content

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 wraps commonjs files when they are part of a commonjs 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 an if statement or function. All other commonjs files will be hoisted. This is the recommended setting for most codebases. Note that detecting conditional require 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. 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 all commonjs files in functions that execute on first require, preserving nodejs semantics. This is the safest setting and should be used if the generated code is incompatible with "auto". Note that strictRequires: 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 any commonjs files and performs require 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 of commonjs file ids 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 which commonjs 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.

  1. Execution Order Discrepancy

    To illustrate this, consider the following example, where index.mjs is the entry module.

    js
    export * from './a.cjs';
    js
    console.log('Module A is being loaded');
    const b = require('./b.cjs');
    console.log('Module A has loaded B');
    module.exports = 'This is A';
    js
    console.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 and rollup can be observed. The execution order of node is as expected, but the execution order of rollup is not as expected.

    bash
    Module A is being loaded
    Module B is being loaded
    Module A has loaded B
    Module B has loaded A
    bash
    Module B is being loaded
    Module B has loaded A
    Module A is being loaded
    Module A has loaded B

    This is because rollup automatically hoists require statements to the top of the outermost scope when translating commonjs modules to esm modules (strictRequires = false).

    js
    console.log('Module A is being loaded');
    const b = require('./b.cjs');
    console.log('Module A has loaded B');
    module.exports = 'This is A';
    js
    import * 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 original commonjs module. The translated ESM will first execute b.cjs module, then execute a.cjs module, while the original commonjs module will first execute a.cjs module, then execute b.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 module A requires module A again (directly or indirectly) in its loading process (before the module A is resolved), it will return an uninitialized module A object (module.export = {} in CommonJS).

    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 replace require with import, the initial execution order will be changed, and we cannot handle circular imports in CommonJS style, because import 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 on commonjs modules will cause certain side effects. For example, similar log statements emitted in different orders, some code depending on the initialization order of polyfills in require statements.

  2. Circular Dependencies

    When commonjs modules have circular require calls, due to these usually relying on nested require calls delayed execution, there will be problems.

    The following example is typical in commonjs syntax, where index.mjs is the entry module.

    js
    export * from './a.cjs';
    js
    console.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 };
    js
    console.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 in commonjs syntax. In the above example, a.cjs and b.cjs two modules are run-time strongly related circular dependency relationships. In this scenario, the translated ESM code from @rollup/plugin-commonjs plugin (strictRequires = false) usually has problems.

    The following analyzes the translated code of a.cjs and b.cjs modules.

    js
    console.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 };
    js
    import * 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 };
    js
    console.log('Module B is being loaded');
    const a = require('./a.cjs');
    console.log('In Module B, a.value =', a.value);
    
    module.exports = {
      value: 2
    };
    js
    import * 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 use exports.value = 1 execution part to consume the exported value from b.cjs module, which is commonly used in commonjs syntax.

    However, unfortunately, ESM syntax has static nature, which cannot consume the exported attributes from other modules like commonjs syntax. If we use it like above, that is, referencing a.cjs module in b.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 to const declaration).

    js
    console.log(a);
    
    const a = 1;

    Of course, there is a way to solve the above problem in esm, that is, through namespace object way to temporarily store the referenced module before it is parsed completed, then access it in b.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. Unlike commonjs syntax, which can access the referenced module before it is parsed completed in commonjs.

    However, it should be noted that esm module namespace object way still cannot consume the exported attributes from other modules like commonjs syntax. This is the limitation of esm 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)的模块实例,不过需要注意的是:

  1. resolvedId.resolveDependencies = true 时,意味着完成当前模块的依赖项模块的路径解析。this.load 方法会返回已经解析完成的当前模块,同时该模块的所有依赖项模块的路径(resolvedId)也已经解析完成(即 moduleParsed 钩子已经触发)。
  2. 未指定 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.

bash
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:

md
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.

js
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:

md
`7` -> 6 -> 5 -> 11 -> 4 -> 10 -> 9 -> 8 -> 3 -> 2 -> 11 -> main

Comparing the execution timing of each module:

md
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:

md
`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)

js
import { a } from './commonjs.cjs';
console.log(a);
js
exports.a = 1234;
exports.b = 4567;
js
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.

js
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.

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)