Skip to content

fix(code-splitting): keep CommonJS modules wrapped under strict execution order#10405

Merged
graphite-app[bot] merged 1 commit into
mainfrom
fix/strict-cjs-entry-wrap
Jul 25, 2026
Merged

fix(code-splitting): keep CommonJS modules wrapped under strict execution order#10405
graphite-app[bot] merged 1 commit into
mainfrom
fix/strict-cjs-entry-wrap

Conversation

@hyfdev

@hyfdev hyfdev commented Jul 23, 2026

Copy link
Copy Markdown
Member

Bug

Two mutually unreachable CommonJS entries, co-located by a codeSplitting group, in cjs output with strictExecutionOrder: true:

// effect.js (entry A)
console.log('effect');
module.exports = { name: 'effect' };

// silent.js (entry B, no relation to A)
module.exports = { name: 'silent' };
// rolldown.config
{
  input: { effect: './effect.js', silent: './silent.js' },
  output: {
    format: 'cjs',
    strictExecutionOrder: true,
    codeSplitting: { groups: [{ name: 'common' }] },
  },
}

Current main emits both entry bodies raw in the group chunk:

// common.js
console.log('effect');               // entry A's top level
module.exports = { name: 'effect' };
module.exports = { name: 'silent' }; // clobbers the line above

// silent.js (entry facade)
require("./common.js");              // runs A's side effects; exports lost

require('./silent.js') executes entry A and returns {}. This build is byte-identical to the strictExecutionOrder: false output — strict silently stops engaging. v1.2.0 is correct:

// common.js (v1.2.0)
var require_effect = __commonJSMin((exports, module) => { console.log('effect'); module.exports = { name: 'effect' }; });
var require_silent = __commonJSMin((exports, module) => { module.exports = { name: 'silent' }; });
Object.defineProperty(exports, "require_effect", { get: () => require_effect });
Object.defineProperty(exports, "require_silent", { get: () => require_silent });

// silent.js (v1.2.0 entry facade)
module.exports = require("./common.js").require_silent();

Unreleased regression, bisected with the execution-order fuzzer's directed control (cross-entry-cjs-output-strict):

build result
v1.2.0 (npm) pass
ce98ae798 (parent of the #10104 squash) pass
1d86ffe06 (#10104) events-reordered
current main events-reordered

Cause

A CommonJS module has exactly one deferral mechanism: its __commonJS wrapper. Two rules decide who gets one, and after #10104 a never-imported CommonJS module falls through both:

  • Interop wrapping (link stage) wraps a CJS module only when something imports or requires it. A CJS entry in cjs output has no importer, keeps WrapKind::None, and renders raw.
  • Order wrapping (generate stage) is ESM-only: is_order_wrap_eligible requires ExportsKind::Esm | None, and the lowering synthesizes only init_*/__esm wrappers.

Pre-#10104, wrap_modules closed this gap — under strictExecutionOrder every CommonJS module was forced to WrapKind::Cjs. #10104 deleted that override together with the ESM wrap-all logic it replaced, although its own stated non-goal was "Changing CJS or require-of-ESM interop output when no order wrapper is selected". This restores the CJS half only; the ESM half is genuinely owned by order analysis now.

Why this must live at the link stage, not in order lowering

The natural question is why order lowering doesn't just handle the case itself — it runs post-chunking and can see the hazardous placement. Detection is not the problem; repair is:

  • For CommonJS, representation is deferral. An ESM order wrapper is a side-effect trigger (init_x()); a CJS wrapper must materialize and return module.exports, with module/exports rebound to closure parameters, a module.exports = require_x() entry facade, and live getter exports. That is the interop WrapKind::Cjs pathway — order lowering has no CJS-shaped tool.
  • wrap_kind() is immutable after linking, by feat(code-splitting): wrap strict execution order modules on demand #10104's own design. OrderWrapState holds all late state explicitly without changing link-stage WrapKind, and the module finalizer, render_chunk_exports, and compute_cross_chunk_links all dispatch on wrap_kind(). Wrapping CJS late would either mutate that sealed fact or thread a second wrap-kind channel through every consumer — duplicating existing interop machinery.
  • The gate — strict plus configured codeSplitting groups — is the semantically correct condition. Without strict, a lone CJS entry rendering raw is the intended minimal output. Under strict, the hazard still needs adverse placement: the only mechanism that can move a never-imported CommonJS module out of its own entry chunk is a group capture (standard chunk assignment keeps an entry with its exclusive dependencies, and every chunk-optimizer merge path either requires the entry chunk to have been emptied by a capture first or degenerates to "loading the chunk is executing that entry"). Placement itself is invisible to the link stage, but the groups config is not. With groups configured, strictExecutionOrder is exactly the user's opt-in to "stay correct under any placement, pay the wrapper bytes" — the same reasoning as wrap-all deferring hazard-free ESM modules.

Without groups the raw body is load-bearing, not just byte-smaller: review surfaced that the wrapper's synthetic module/exports parameters break a standalone CJS entry's Node module contract — exports: 'named' output collapses to { default: getter }, module.filename reads undefined, module.parent loses its value, and require.main === module turns false — all of which the raw form preserves. The first revision of this PR wrapped unconditionally under strict and regressed exactly that; the gate restores main's behavior for every no-groups config.

Cost: under strict with groups configured, a never-imported CJS entry is wrapped even when no group actually captures it — identical to every released version's output under groups. If that ever matters, the refinement is a generate-stage render downgrade backed by a placement proof (the same conservative-first pattern as the planned init_* hoisted-form narrowing), not a late wrap channel.

Tests

function/experimental/strict_execution_order/cjs_entry_group_isolation — the shape above, wrap-all and on-demand cells. Snapshot pins the wrapped shape; _test.mjs executes the output: requiring the silent entry must run no foreign top level and both entries must keep their own module.exports. Red on main without the fix. No existing fixture combined CommonJS-source entries with cjs-format strict output, which is why #10104's snapshot audit could not see this.

standalone_cjs_entry_named_exports and standalone_cjs_entry_node_module (from review, thanks @IWANABETHATGUY) pin the no-groups arm in the same wrap-all and on-demand cells: a standalone CJS entry under strict must keep rendering raw, preserving its named-exports shape and Node module identity (module.filename, module.parent, require.main === module, both required and run as the CLI). Red on this PR's first revision, green on main and on the gated fix.

Validation

  • Full integration fixture suite and workspace unit tests pass with no snapshot changes outside the new fixture; clippy -D warnings and cargo fmt --check clean.
  • Execution-order fuzzer current-target audit against this branch: cross-entry-cjs-output-strict flips to pass. The two cjs-output-facade-cycle strict controls (uninitialized-wrapper-export finding from the same fuzzer list) also flip to pass — a wrapped entry is reached through a live getter instead of a not-yet-installed export. That finding's strictExecutionOrder: false arm is out of scope here. family-a-existing-seed stays known-red (separate bug).
  • A four-cell matrix ({cjs,esm} × {seo:true,false}) reproduces v1.2.0 behavior exactly: cjs+strict isolated with correct exports, esm cells clean, cjs+seo:false unchanged (Manual codeSplitting groups leak cross-entry execution without strictExecutionOrder (#9463 follow-up) #9998).
  • After gating on groups: full suite green again with the cjs_entry_group_isolation snapshot byte-identical, so every groups-present config keeps the first revision's output; no-groups configs are byte-identical to main by construction (the force-wrap block is skipped entirely). The fuzzer results above all exercise group configs and are unaffected.

Tracked in #10294 (execution-order fuzzer findings, CJS-output strict cross-entry leak). Related: #10104 (introducing change), #9998 (the pre-existing seo:false arm of the same co-location root, unchanged here), #9997 (the original strict isolation guarantee), #10401 (closed chunking-side pin for the facade-cycle shape; this change is the "wrap captured entries" direction recorded there).

@netlify

netlify Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 959002b
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a64932f91af3500086051f5
😎 Deploy Preview https://deploy-preview-10405--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@hyfdev
hyfdev marked this pull request as ready for review July 23, 2026 08:16
@hyfdev
hyfdev requested a review from IWANABETHATGUY as a code owner July 23, 2026 08:16
Copilot AI review requested due to automatic review settings July 23, 2026 08:16
@hyfdev
hyfdev requested a review from shulaoda as a code owner July 23, 2026 08:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codspeed-hq

codspeed-hq Bot commented Jul 23, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 7 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing fix/strict-cjs-entry-wrap (d513322) with main (c698d18)2

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (4fbb0c9) during the generation of this report, so c698d18 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@IWANABETHATGUY

Copy link
Copy Markdown
Member

Really nice fix — the writeup made the root cause easy to follow, and I like that you documented the strict-mode exception in implementation.md so the "link stage doesn't touch WrapKind" invariant stays honest. Traced the logic and it holds up: the only modules that actually change are the previously-unwrapped lone CJS entries, which matches the old v1.2.0 behavior.

Just two cosmetic things, neither blocking:

  • module.exports_kind == ExportsKind::CommonJs reads a little out of place next to the rest of the file, which uses matches!(module.exports_kind, ExportsKind::CommonJs) (e.g. lines 64 and 128). Might be worth matching the local idiom.
  • The .filter(|(module_idx, _)| *module_idx != self.runtime.id()) isn't really load-bearing — the runtime is ESM, so the exports_kind == CommonJs check below already skips it. Totally fine to keep it as a defensive mirror of wrap_module_recursively's guard, just flagging that it's belt-and-suspenders. (If you do keep it, self.runtime.id() gets re-evaluated per module; a local binding would be a touch cleaner, though it's cheap enough not to matter.)

@IWANABETHATGUY

IWANABETHATGUY commented Jul 24, 2026

Copy link
Copy Markdown
Member

The regression coverage should be two separate Rust integration fixtures, one per finding.

1. standalone_cjs_entry_named_exports

_config.json

{
  "snapshot": false,
  "configName": "wrap-all",
  "config": {
    "format": "cjs",
    "exports": "named",
    "strictExecutionOrder": true,
    "codeSplitting": false
  },
  "configVariants": [
    {
      "_configName": "on-demand",
      "onDemandWrapping": true
    }
  ]
}

main.js

exports.foo = 1;

_test.mjs

import assert from 'node:assert/strict';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const bundle = require('./dist/main.js');

assert.deepStrictEqual(bundle, { foo: 1 });

2. standalone_cjs_entry_node_module

_config.json

{
  "snapshot": false,
  "configName": "wrap-all",
  "config": {
    "platform": "node",
    "format": "cjs",
    "strictExecutionOrder": true,
    "codeSplitting": false
  },
  "configVariants": [
    {
      "_configName": "on-demand",
      "onDemandWrapping": true
    }
  ]
}

main.js

exports.filename = module.filename;
exports.hasParent = module.parent !== null;
exports.isMain = require.main === module;

console.log(
  JSON.stringify({
    filename: module.filename,
    parentIsNull: module.parent === null,
    isMain: require.main === module,
  }),
);

_test.mjs

import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';

const require = createRequire(import.meta.url);
const bundlePath = fileURLToPath(new URL('./dist/main.js', import.meta.url));
const bundle = require(bundlePath);
const cli = spawnSync(process.execPath, [bundlePath], { encoding: 'utf8' });

assert.deepStrictEqual(
  {
    required: bundle,
    cli: {
      status: cli.status,
      state: JSON.parse(cli.stdout),
    },
  },
  {
    required: {
      filename: bundlePath,
      hasParent: true,
      isMain: false,
    },
    cli: {
      status: 0,
      state: {
        filename: bundlePath,
        parentIsNull: true,
        isMain: true,
      },
    },
  },
);

Each fixture runs independently in both wrap-all and on-demand modes, so one failure cannot mask the other.

@IWANABETHATGUY

IWANABETHATGUY commented Jul 24, 2026

Copy link
Copy Markdown
Member

Verification on the freshly fetched latest main:

$ git rev-parse --short HEAD
cd742e701

$ just t-run crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/standalone_cjs_entry_named_exports/_config.json
# exit 0: wrap-all passed; on-demand passed

$ just t-run crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/standalone_cjs_entry_node_module/_config.json
# exit 0: wrap-all passed; on-demand passed

The same fixtures fail independently on the current PR implementation (26802d422):

# standalone_cjs_entry_named_exports
- expected: { foo: 1 }
+ actual:   { default: { foo: 1 } }

# standalone_cjs_entry_node_module, executed as the CLI
- expected: { filename: "<dist>/main.js", parentIsNull: true, isMain: true }
+ actual:   {                          parentIsNull: false, isMain: false }

@hyfdev

hyfdev commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Thanks for catching this — both fixtures were red on the previous revision exactly as you showed. The unconditional wrap traded away a standalone CJS entry's Node module contract (named-exports shape, module.filename, module.parent, require.main === module) for isolation it doesn't need when nothing can co-locate it.

Fixed in aeab531 by gating the link-stage force-wrap on strictExecutionOrder and configured codeSplitting groups (has_manual_code_splitting_groups(), mirroring apply_manual_code_splitting's no-op conditions). A group capture is the only chunking mechanism that can move a never-imported CommonJS module out of its own entry chunk, so with no groups the block is skipped entirely and output is byte-identical to main; with groups it behaves exactly like the previous revision (cjs_entry_group_isolation snapshot unchanged). Both your fixtures are added verbatim and are green in the wrap-all and on-demand cells; full workspace suite green.

One remaining coarseness, documented in implementation.md: with groups configured, a CJS entry no group actually captures is still wrapped (same as every released version under groups). Sharpening that needs placement knowledge — capture is decided after wrap_kind is sealed — so it stays recorded as the generate-stage render-downgrade refinement.

Also took both cosmetic points: matches! it is, and the runtime-id filter is dropped rather than kept — the runtime module is hardcoded ESM, so the exports_kind check already skips it; a comment notes that instead.

@IWANABETHATGUY

Copy link
Copy Markdown
Member

Thanks for the follow-up. The two original codeSplitting: false regressions are fixed, but re-review found that both return when a manual group is configured but matches no modules.

I extended both Rust integration fixtures:

  • standalone_cjs_entry_named_exports/_config.json
  • standalone_cjs_entry_node_module/_config.json

with the following variants:

 "configVariants": [
   {
     "_configName": "on-demand",
     "onDemandWrapping": true
+  },
+  {
+    "_configName": "unmatched-group-wrap-all",
+    "codeSplitting": {
+      "groups": [
+        {
+          "name": "unused",
+          "test": "never-match-entry\\.js$"
+        }
+      ]
+    }
+  },
+  {
+    "_configName": "unmatched-group-on-demand",
+    "codeSplitting": {
+      "groups": [
+        {
+          "name": "unused",
+          "test": "never-match-entry\\.js$"
+        }
+      ]
+    },
+    "onDemandWrapping": true
   }
 ]

Finding

[P1] The force-wrap condition checks whether any group is configured, rather than whether the CJS entry is actually captured:

if self.options.is_strict_execution_order_enabled()
  && self.options.has_manual_code_splitting_groups()

For the configuration above, the group matches nothing and the entry remains in its original entry chunk. It is nevertheless forced through WrapKind::Cjs, reproducing both original regressions:

# standalone_cjs_entry_named_exports
- expected: { foo: 1 }
+ actual:   { default: { foo: 1 } }

# standalone_cjs_entry_node_module
- expected module.filename: "<dist>/main.js"
+ actual module.filename: undefined

- expected CLI: { parentIsNull: true, isMain: true }
+ actual CLI:   { parentIsNull: false, isMain: false }

Verification

The exact expanded fixtures pass on latest main (4fbb0c915):

standalone_cjs_entry_named_exports
  wrap-all                  PASS
  on-demand                 PASS
  unmatched-group-wrap-all  PASS
  unmatched-group-on-demand PASS

standalone_cjs_entry_node_module
  wrap-all                  PASS
  on-demand                 PASS
  unmatched-group-wrap-all  PASS
  unmatched-group-on-demand PASS

On the current PR branch (d51332296), both fixtures pass their no-group cells and fail at unmatched-group-wrap-all.

The wrapping decision therefore needs actual entry-capture information, or an equivalent post-placement downgrade for entries that remain in their own chunks. Group presence alone is not sufficient.

@hyfdev

hyfdev commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

a manual group is configured but matches no modules.

This is expected behavior. Currently I don't want to introduce a "sound" detection on "if manual group captures modules". It's a bit of complicated and I tend avoid it for now.

hyfdev commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Merge activity

  • Jul 25, 10:42 AM UTC: The merge label 'graphite: merge-when-ready' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.
  • Jul 25, 10:42 AM UTC: hyfdev added this pull request to the Graphite merge queue.
  • Jul 25, 10:48 AM UTC: Merged by the Graphite merge queue.

…tion order (#10405)

## Bug

Two mutually unreachable CommonJS entries, co-located by a `codeSplitting` group, in cjs output with `strictExecutionOrder: true`:

```js
// effect.js (entry A)
console.log('effect');
module.exports = { name: 'effect' };

// silent.js (entry B, no relation to A)
module.exports = { name: 'silent' };
```

```js
// rolldown.config
{
  input: { effect: './effect.js', silent: './silent.js' },
  output: {
    format: 'cjs',
    strictExecutionOrder: true,
    codeSplitting: { groups: [{ name: 'common' }] },
  },
}
```

Current main emits both entry bodies raw in the group chunk:

```js
// common.js
console.log('effect');               // entry A's top level
module.exports = { name: 'effect' };
module.exports = { name: 'silent' }; // clobbers the line above

// silent.js (entry facade)
require("./common.js");              // runs A's side effects; exports lost
```

`require('./silent.js')` executes entry A and returns `{}`. This build is byte-identical to the `strictExecutionOrder: false` output — strict silently stops engaging. v1.2.0 is correct:

```js
// common.js (v1.2.0)
var require_effect = __commonJSMin((exports, module) => { console.log('effect'); module.exports = { name: 'effect' }; });
var require_silent = __commonJSMin((exports, module) => { module.exports = { name: 'silent' }; });
Object.defineProperty(exports, "require_effect", { get: () => require_effect });
Object.defineProperty(exports, "require_silent", { get: () => require_silent });

// silent.js (v1.2.0 entry facade)
module.exports = require("./common.js").require_silent();
```

Unreleased regression, bisected with the execution-order fuzzer's directed control (`cross-entry-cjs-output-strict`):

| build | result |
| --- | --- |
| v1.2.0 (npm) | pass |
| `ce98ae798` (parent of the #10104 squash) | pass |
| `1d86ffe06` (#10104) | events-reordered |
| current `main` | events-reordered |

## Cause

A CommonJS module has exactly one deferral mechanism: its `__commonJS` wrapper. Two rules decide who gets one, and after #10104 a never-imported CommonJS module falls through both:

- **Interop wrapping (link stage)** wraps a CJS module only when something imports or requires it. A CJS *entry* in cjs output has no importer, keeps `WrapKind::None`, and renders raw.
- **Order wrapping (generate stage)** is ESM-only: `is_order_wrap_eligible` requires `ExportsKind::Esm | None`, and the lowering synthesizes only `init_*`/`__esm` wrappers.

Pre-#10104, `wrap_modules` closed this gap — under `strictExecutionOrder` every CommonJS module was forced to `WrapKind::Cjs`. #10104 deleted that override together with the ESM wrap-all logic it replaced, although its own stated non-goal was "Changing CJS or require-of-ESM interop output when no order wrapper is selected". This restores the CJS half only; the ESM half is genuinely owned by order analysis now.

## Why this must live at the link stage, not in order lowering

The natural question is why order lowering doesn't just handle the case itself — it runs post-chunking and can see the hazardous placement. Detection is not the problem; repair is:

- **For CommonJS, representation *is* deferral.** An ESM order wrapper is a side-effect trigger (`init_x()`); a CJS wrapper must materialize and return `module.exports`, with `module`/`exports` rebound to closure parameters, a `module.exports = require_x()` entry facade, and live getter exports. That is the interop `WrapKind::Cjs` pathway — order lowering has no CJS-shaped tool.
- **`wrap_kind()` is immutable after linking, by #10104's own design.** `OrderWrapState` holds all late state explicitly *without changing link-stage `WrapKind`*, and the module finalizer, `render_chunk_exports`, and `compute_cross_chunk_links` all dispatch on `wrap_kind()`. Wrapping CJS late would either mutate that sealed fact or thread a second wrap-kind channel through every consumer — duplicating existing interop machinery.
- **The gate — strict plus configured `codeSplitting` groups — is the semantically correct condition.** Without strict, a lone CJS entry rendering raw is the intended minimal output. Under strict, the hazard still needs adverse placement: the only mechanism that can move a never-imported CommonJS module out of its own entry chunk is a group capture (standard chunk assignment keeps an entry with its exclusive dependencies, and every chunk-optimizer merge path either requires the entry chunk to have been emptied by a capture first or degenerates to "loading the chunk *is* executing that entry"). Placement itself is invisible to the link stage, but the groups config is not. With groups configured, `strictExecutionOrder` is exactly the user's opt-in to "stay correct under any placement, pay the wrapper bytes" — the same reasoning as wrap-all deferring hazard-free ESM modules.

Without groups the raw body is load-bearing, not just byte-smaller: review surfaced that the wrapper's synthetic `module`/`exports` parameters break a standalone CJS entry's Node module contract — `exports: 'named'` output collapses to `{ default: getter }`, `module.filename` reads undefined, `module.parent` loses its value, and `require.main === module` turns false — all of which the raw form preserves. The first revision of this PR wrapped unconditionally under strict and regressed exactly that; the gate restores main's behavior for every no-groups config.

Cost: under strict with groups configured, a never-imported CJS entry is wrapped even when no group actually captures it — identical to every released version's output under groups. If that ever matters, the refinement is a generate-stage *render* downgrade backed by a placement proof (the same conservative-first pattern as the planned `init_*` hoisted-form narrowing), not a late wrap channel.

## Tests

`function/experimental/strict_execution_order/cjs_entry_group_isolation` — the shape above, wrap-all and on-demand cells. Snapshot pins the wrapped shape; `_test.mjs` executes the output: requiring the silent entry must run no foreign top level and both entries must keep their own `module.exports`. Red on main without the fix. No existing fixture combined CommonJS-source entries with cjs-format strict output, which is why #10104's snapshot audit could not see this.

`standalone_cjs_entry_named_exports` and `standalone_cjs_entry_node_module` (from review, thanks @IWANABETHATGUY) pin the no-groups arm in the same wrap-all and on-demand cells: a standalone CJS entry under strict must keep rendering raw, preserving its named-exports shape and Node module identity (`module.filename`, `module.parent`, `require.main === module`, both required and run as the CLI). Red on this PR's first revision, green on main and on the gated fix.

## Validation

- Full integration fixture suite and workspace unit tests pass with no snapshot changes outside the new fixture; clippy `-D warnings` and `cargo fmt --check` clean.
- Execution-order fuzzer current-target audit against this branch: `cross-entry-cjs-output-strict` flips to pass. The two `cjs-output-facade-cycle` strict controls (uninitialized-wrapper-export finding from the same fuzzer list) also flip to pass — a wrapped entry is reached through a live getter instead of a not-yet-installed export. That finding's `strictExecutionOrder: false` arm is out of scope here. `family-a-existing-seed` stays known-red (separate bug).
- A four-cell matrix ({cjs,esm} × {seo:true,false}) reproduces v1.2.0 behavior exactly: cjs+strict isolated with correct exports, esm cells clean, cjs+seo:false unchanged (#9998).
- After gating on groups: full suite green again with the `cjs_entry_group_isolation` snapshot byte-identical, so every groups-present config keeps the first revision's output; no-groups configs are byte-identical to main by construction (the force-wrap block is skipped entirely). The fuzzer results above all exercise group configs and are unaffected.

Tracked in #10294 (execution-order fuzzer findings, CJS-output strict cross-entry leak). Related: #10104 (introducing change), #9998 (the pre-existing seo:false arm of the same co-location root, unchanged here), #9997 (the original strict isolation guarantee), #10401 (closed chunking-side pin for the facade-cycle shape; this change is the "wrap captured entries" direction recorded there).
@graphite-app
graphite-app Bot force-pushed the fix/strict-cjs-entry-wrap branch from d513322 to 959002b Compare July 25, 2026 10:42
@graphite-app
graphite-app Bot merged commit 959002b into main Jul 25, 2026
35 checks passed
@graphite-app
graphite-app Bot deleted the fix/strict-cjs-entry-wrap branch July 25, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants