Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/commonjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,34 @@ You can also provide a [picomatch pattern](https://github.com/micromatch/picomat

`"debug"` works like `"auto"` but after bundling, it will display a warning containing a list of ids that have been wrapped which can be used as picomatch pattern for fine-tuning or to avoid the potential race conditions mentioned for `"auto"`.

### `requireNodeBuiltins`

Type: `boolean`<br>
Default: `false`

When enabled, external Node built-ins (e.g., `node:fs`, `node:path`) required from wrapped CommonJS modules will use `createRequire(import.meta.url)` instead of being hoisted as ESM imports. This prevents eager loading of Node built-ins at module initialization time and preserves the lazy execution semantics of `require()`.

**Important:** Enabling this option adds a dependency on `node:module` in the output bundle, which may not be available in some environments like edge runtimes (Cloudflare Workers, Vercel Edge Runtime). Only enable this option if you are targeting Node.js environments and need the lazy loading behavior for Node built-ins.

Example:

```js
commonjs({
strictRequires: true,
requireNodeBuiltins: true
});
```

With `requireNodeBuiltins: true`, code like:

```js
if (condition) {
require('node:fs');
}
```

will generate output using `createRequire` instead of hoisting the import to the top of the file.

### `dynamicRequireTargets`

Type: `string | string[]`<br>
Expand Down
8 changes: 5 additions & 3 deletions packages/commonjs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export default function commonjs(options = {}) {
ignoreDynamicRequires,
requireReturnsDefault: requireReturnsDefaultOption,
defaultIsModuleExports: defaultIsModuleExportsOption,
esmExternals
esmExternals,
requireNodeBuiltins = false
} = options;
const extensions = options.extensions || ['.js'];
const filter = createFilter(options.include, options.exclude);
Expand Down Expand Up @@ -215,7 +216,8 @@ export default function commonjs(options = {}) {
requireResolver = getRequireResolver(
extensions,
detectCyclesAndConditional,
currentlyResolving
currentlyResolving,
requireNodeBuiltins
);
},

Expand Down Expand Up @@ -263,7 +265,7 @@ export default function commonjs(options = {}) {

if (isWrappedId(id, EXTERNAL_SUFFIX)) {
const actualId = unwrapId(id, EXTERNAL_SUFFIX);
if (actualId.startsWith('node:')) {
if (requireNodeBuiltins === true && actualId.startsWith('node:')) {
return getExternalBuiltinRequireProxy(actualId);
}
return getUnknownRequireProxy(
Expand Down
36 changes: 22 additions & 14 deletions packages/commonjs/src/resolve-require-sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
} from './helpers';
import { resolveExtensions } from './resolve-id';

export function getRequireResolver(extensions, detectCyclesAndConditional, currentlyResolving) {
export function getRequireResolver(
extensions,
detectCyclesAndConditional,
currentlyResolving,
requireNodeBuiltins
) {
const knownCjsModuleTypes = Object.create(null);
const requiredIds = Object.create(null);
const unconditionallyRequiredIds = Object.create(null);
Expand Down Expand Up @@ -195,21 +200,24 @@ export function getRequireResolver(extensions, detectCyclesAndConditional, curre
getTypeForFullyAnalyzedModule(dependencyId));
// Special-case external Node built-ins to be handled via a lazy __require
// helper instead of hoisted ESM imports when strict wrapping is used.
// Only apply this when requireNodeBuiltins option is enabled.
const isExternalWrapped = isWrappedId(dependencyId, EXTERNAL_SUFFIX);
let resolvedDependencyId = dependencyId;
if (parentMeta.isCommonJS === IS_WRAPPED_COMMONJS && !allowProxy && isExternalWrapped) {
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
if (actualExternalId.startsWith('node:')) {
isCommonJS = IS_WRAPPED_COMMONJS;
parentMeta.isRequiredCommonJS[dependencyId] = isCommonJS;
}
} else if (isExternalWrapped && !allowProxy) {
// If the parent is not wrapped but the dependency is a node: builtin external,
// unwrap the EXTERNAL_SUFFIX so it's treated as a normal external.
// This avoids trying to load the lazy __require proxy for non-wrapped contexts.
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
if (actualExternalId.startsWith('node:')) {
resolvedDependencyId = actualExternalId;
if (requireNodeBuiltins === true) {
if (parentMeta.isCommonJS === IS_WRAPPED_COMMONJS && !allowProxy && isExternalWrapped) {
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
if (actualExternalId.startsWith('node:')) {
isCommonJS = IS_WRAPPED_COMMONJS;
parentMeta.isRequiredCommonJS[dependencyId] = isCommonJS;
}
} else if (isExternalWrapped && !allowProxy) {
// If the parent is not wrapped but the dependency is a node: builtin external,
// unwrap the EXTERNAL_SUFFIX so it's treated as a normal external.
// This avoids trying to load the lazy __require proxy for non-wrapped contexts.
const actualExternalId = unwrapId(dependencyId, EXTERNAL_SUFFIX);
if (actualExternalId.startsWith('node:')) {
resolvedDependencyId = actualExternalId;
}
}
}
const isWrappedCommonJS = isCommonJS === IS_WRAPPED_COMMONJS;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
description:
'does not crash and does not mark external node: builtins as pure when strictRequires is true and requireNodeBuiltins is false (default)',
pluginOptions: {
strictRequires: true,
requireNodeBuiltins: false
},
context: {
__filename: __filename
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Top-level require of a Node builtin ensures the transform computes
// wrappedModuleSideEffects for an external wrapped dependency.
function unused() {
// External Node builtin require; not executed at runtime
require('node:crypto');
}

module.exports = 1;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ module.exports = {
description:
'does not crash and does not mark external node: builtins as pure when strictRequires is true',
pluginOptions: {
strictRequires: true
strictRequires: true,
requireNodeBuiltins: true
},
context: {
__filename: __filename
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
description: 'handles node: builtins correctly with strictRequires: auto and requireNodeBuiltins: false (default)',
pluginOptions: {
strictRequires: 'auto',
requireNodeBuiltins: false
},
exports: (exports, t) => {
// Should be able to access properties of node:stream
t.truthy(exports.Readable);
t.is(typeof exports.Readable, 'function');
// Should be able to instantiate
t.truthy(exports.readable);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const stream = require('node:stream');
const readable = new stream.Readable({});

module.exports = { Readable: stream.Readable, readable };
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module.exports = {
description: 'handles node: builtins correctly with strictRequires: auto',
pluginOptions: {
strictRequires: 'auto'
strictRequires: 'auto',
requireNodeBuiltins: true
},
exports: (exports, t) => {
// Should be able to access properties of node:stream
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
description: "hoists external node built-in requires when requireNodeBuiltins is false (default)",
pluginOptions: {
strictRequires: true,
requireNodeBuiltins: false
},
exports: (exports, t) => {
t.is(exports, 42);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
if (false) {
require('node:sqlite');
}
module.exports = 42;
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module.exports = {
description: "does not hoist external node built-in requires when strictRequires is true",
pluginOptions: {
strictRequires: true
strictRequires: true,
requireNodeBuiltins: true
},
exports: (exports, t) => {
t.is(exports, 42);
Expand Down
87 changes: 87 additions & 0 deletions packages/commonjs/test/snapshots/function.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -6731,6 +6731,37 @@ Generated by [AVA](https://avajs.dev).
`,
}

## module-side-effects-external-node-builtin-wrapped-default

> Snapshot 1
{
'main.js': `'use strict';␊
require('node:crypto');␊
function getDefaultExportFromCjs (x) {␊
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊
}␊
var main$1;␊
var hasRequiredMain;␊
function requireMain () {␊
if (hasRequiredMain) return main$1;␊
hasRequiredMain = 1;␊
main$1 = 1;␊
return main$1;␊
}␊
var mainExports = requireMain();␊
var main = /*@__PURE__*/getDefaultExportFromCjs(mainExports);␊
module.exports = main;␊
`,
}

## module-side-effects-import-wrapped

> Snapshot 1
Expand Down Expand Up @@ -8717,6 +8748,34 @@ Generated by [AVA](https://avajs.dev).

## strict-requires-auto-external-node-builtin

> Snapshot 1
{
'main.js': `'use strict';␊
var require$$0 = require('node:stream');␊
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }␊
var require$$0__default = /*#__PURE__*/_interopDefaultCompat(require$$0);␊
function getDefaultExportFromCjs (x) {␊
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊
}␊
const stream = require$$0__default.default;␊
const readable = new stream.Readable({});␊
var main = { Readable: stream.Readable, readable };␊
var main$1 = /*@__PURE__*/getDefaultExportFromCjs(main);␊
module.exports = main$1;␊
`,
}

## strict-requires-auto-external-node-builtin-default

> Snapshot 1
{
Expand Down Expand Up @@ -9258,6 +9317,34 @@ Generated by [AVA](https://avajs.dev).

## strict-requires-external-node-builtin

> Snapshot 1
{
'main.js': `'use strict';␊
function getDefaultExportFromCjs (x) {␊
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;␊
}␊
var main$1;␊
var hasRequiredMain;␊
function requireMain () {␊
if (hasRequiredMain) return main$1;␊
hasRequiredMain = 1;␊
main$1 = 42;␊
return main$1;␊
}␊
var mainExports = requireMain();␊
var main = /*@__PURE__*/getDefaultExportFromCjs(mainExports);␊
module.exports = main;␊
`,
}

## strict-requires-external-node-builtin-default

> Snapshot 1
{
Expand Down
Binary file modified packages/commonjs/test/snapshots/function.js.snap
Binary file not shown.
11 changes: 11 additions & 0 deletions packages/commonjs/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,17 @@ interface RollupCommonJSOptions {
* home directory name. By default, it uses the current working directory.
*/
dynamicRequireRoot?: string;
/**
* When enabled, external Node built-ins (e.g., `node:fs`) required from wrapped CommonJS modules
* will use `createRequire(import.meta.url)` instead of being hoisted as ESM imports. This prevents
* eager loading of Node built-ins at module initialization time.
*
* Note: This option adds a dependency on `node:module` in the output, which may not be available
* in some environments like edge runtimes (Cloudflare Workers, Vercel Edge Runtime).
*
* @default false
*/
requireNodeBuiltins?: boolean;
}

/**
Expand Down
Loading