Conversation
…Effects When a package has both "module" and "main", the resolver records the "main" path as path_pair.secondary for CJS interop. finalize_result iterated both paths and wrote primary_side_effects_data for each, so the secondary's result overwrote the primary's. A bare 'import "pkg"' where the package lists its "module" entry in "sideEffects" (but not the "main" entry, which is the common shape for packages that ship a pre-bundled CJS main alongside ESM sources, e.g. @tensorflow/tfjs-*) was therefore marked side-effect-free and tree-shaken out of the bundle. Restrict the primary_side_effects_data computation to the primary path. Fixes #8993
|
Status: diff is green; CI red on unrelated lanes only. Ready for a maintainer to merge. Reproduced with Fail-before / pass-after via CI runs (both builds: only unrelated failures):
Neither touches the resolver or bundler; both reported to main-break triage. Re: the bot findings:
|
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 1:22 AM PT - Aug 1st, 2026
⏳ @robobun, your commit 6d2172f is still building in
|
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
LGTM — narrow, well-tested fix; the only finding is a comment-verbosity nit.
Note: the bot flagged #33883 as a possible duplicate; may want to close/reconcile that one.
Extended reasoning...
Overview
Two files changed. In src/resolver/resolver.rs, finalize_result now gates the two writes to result.primary_side_effects_data on is_primary (true only for the first path yielded by path_pair.iter()), so the CJS-interop main fallback recorded as path_pair.secondary no longer overwrites the module entry's side-effects verdict. The Glob(_) and Mixed(_) match arms in the first block are also merged — their bodies were byte-identical. In test/bundler/esbuild/dce.test.ts, three new itBundled cases cover Map-form sideEffects, glob-form, and the inverse (module NOT listed → still dropped).
Security risks
None. This is bundler tree-shaking metadata; no untrusted-input parsing, syscalls, or auth surface is touched.
Level of scrutiny
Medium. The resolver is a hot/critical path, but the change is a two-line guard whose correctness follows directly from PathPairIter::next_ (index 0 → primary, index 1 → secondary). I verified the iterator yields primary first, so core::mem::replace(&mut is_primary_path, false) is true exactly on the primary iteration. The secondary iteration still runs for tsconfig/module-type/symlink handling — only the side-effects write is skipped, matching the field's name (primary_side_effects_data). If the primary is is_disabled and skipped by the iterator, the secondary is treated as first — same effective behavior as before the PR (where secondary always wrote last), so no regression there.
Other factors
- Tests follow neighboring conventions:
todo: isWindowsmatches the adjacentPackageJsonSideEffectsArrayKeep*tests (path-separator handling insideEffectsarrays is a known Windows todo). The inverse test omits it, which is fine — a non-match on Windows still yields the expected drop. - The PR description explains why existing
PackageJsonSideEffectsArrayKeepModule*tests didn't catch this (fixturepackage.jsonlacks"name", soenclosing_package_jsonis unset and the second iteration's write path is skipped); the new fixtures include"name"and comment that it's load-bearing. - Fail-before/pass-after was verified per the description (
dce.test.ts81 pass / 0 fail;packagejson.test.ts78 pass / 0 fail) and the tfjs repro now runs. - The single inline finding is a REVIEW.md comment-style nit (bug-history narration in the resolver comment and test header). It doesn't affect correctness and can be trimmed at the author's discretion.
There was a problem hiding this comment.
LGTM — the comment-trim nit from my earlier pass is addressed in 6d2172f, and the is_primary guard is the right fix (the field is literally named primary_side_effects_data).
What was reviewed:
PathPair::iter()yieldsprimaryat index 0 thensecondary, somem::replace(&mut is_primary_path, false)correctly tags only the first iteration; thecontinueon a failedread_dir_infofor the primary still consumes the flag, which is fine (falling back to the secondary's sideEffects would just reintroduce the bug).- Both write sites of
primary_side_effects_dataare gated; the collapsedGlob | Mixedarm is byte-identical to the two arms it replaces. - New tests follow the neighboring
PackageJsonSideEffectsArrayKeepModule*shape (includingtodo: isWindows), and the inverse-guard test keeps the drop path covered.
Extended reasoning...
Overview
Two-file change: src/resolver/resolver.rs gates both assignments to result.primary_side_effects_data inside finalize_result on is_primary (true only for the first path yielded by path_pair.iter()), and test/bundler/esbuild/dce.test.ts adds three itBundled fixtures covering the array, glob, and inverse cases with a "name"-bearing package.json (which is what makes the secondary iteration reach the write site).
The Rust change is a re-indent of the existing match under an if is_primary plus && is_primary on the second block; it also merges the Glob(_) and Mixed(_) arms of the first match, whose bodies were identical. No other behavior in the loop (symlink resolution, tsconfig merge, module_type, result.package_json update) is touched.
Security risks
None. This is bundler tree-shaking metadata; the change makes DCE strictly less aggressive for the affected shape (keeps a bare import that was previously dropped). No untrusted-input parsing, no allocation, no FFI/GC.
Level of scrutiny
Low-to-moderate. finalize_result is on the resolver hot path, but the diff is a boolean guard around existing code with no new control flow. I confirmed PathPairIter yields primary at index 0 (result.rs:44-77), so core::mem::replace on a true-initialized flag is the correct idiom. Full dce.test.ts and packagejson.test.ts reportedly pass; the earlier CI build's only failure was an unrelated setInterval ASAN timeout.
Other factors
My previous review's only finding was a comment-style nit, which the author addressed in 6d2172f (resolver comment removed, test comment trimmed to issue URL + the load-bearing "name" invariant). All inline threads are resolved. The overlap with open PR #33883 is a maintainer coordination question, not a correctness concern — this PR is the narrow slice and stands on its own.
|
Closing in favor of #33883, which includes the same |
What does this PR do?
Fixes #8993.
When a package has both
"module"and"main", the resolver records the"main"path aspath_pair.secondaryso the bundler can fall back to it forrequire()interop.finalize_resultiterates both paths and was writingprimary_side_effects_datafor each, so the secondary'ssideEffectslookup overwrote the primary's.A bare
import "pkg"where the package lists its"module"entry in"sideEffects"(but not the"main"entry) was therefore marked side-effect-free and tree-shaken out of the bundle. This is the common shape for packages that ship a pre-bundled CJSmainalongside ESM sources, e.g.@tensorflow/tfjs-backend-cpu:{ "name": "@tensorflow/tfjs-backend-cpu", "main": "dist/tf-backend-cpu.node.js", "module": "dist/index.js", "sideEffects": ["./dist/index.js", "./dist/base.js", "./dist/register_all_kernels.js"] }Repro
bun build index.ts --outdir dist --target bun bun dist/index.js # TypeError: undefined is not an object (evaluating 'env().platform.isTypedArray')The existing
dce/PackageJsonSideEffectsArrayKeepModule*tests intest/bundler/esbuild/dce.test.tshappen to avoid this because their fixturepackage.jsonfiles omit"name", which prevents the package from being recorded asenclosing_package_json, soresult.package_jsonisNoneon the second iteration and the overwrite is skipped. Real packages always have"name".Fix
Compute
primary_side_effects_dataonly from the first (primary) path yielded bypath_pair.iter(). The secondary iteration still runs for symlink/tsconfig/module-type handling but no longer touches the side-effects field.How did you verify your code works?
New tests in
test/bundler/esbuild/dce.test.ts(all fail onmain, pass with this change):dce/PackageJsonSideEffectsArrayModuleMainBareImport: package withname+module+main,sideEffectslists only the module entry; bare import must be kept.dce/PackageJsonSideEffectsGlobModuleMainBareImport: same with a glob pattern (matches the@tensorflow/tfjs-coreshape).dce/PackageJsonSideEffectsArrayModuleMainBareImportRemove: inverse guard;sideEffectslists only themainentry, bare import resolves tomoduleand must still be dropped.Full
dce.test.ts(81 pass / 17 todo / 0 fail) andpackagejson.test.ts(78 pass / 9 todo / 0 fail) pass with no regressions. The original tfjs repro now bundles to 570 KB (258 KB minified) and runs correctly.no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/esbuild/dce.test.ts