Repository navigation
Conversation
When every onResolve callback returns nothing, the bundler resolves the import in run_resolver. That path dropped the resolver's side-effects value in two places: - enqueue_parse_task stored loader.side_effects() for the file. So an unused file from a package with "sideEffects": false stayed in the bundle. Also, a data file that an onLoad plugin turned into JavaScript lost its side effects. - The early return for an external import did not set IS_EXTERNAL_WITHOUT_SIDE_EFFECTS. So an unused import of a Node.js builtin stayed in the output. Both places now store the resolver's value, as the path without plugins does.
|
Status Reproduced on bun 1.4.1 (canary
The four new cases in |
|
Updated 10:12 AM PT - Sep 4th, 2026
✅ @robobun, your commit 58223df513c58350c912045129ac8c20f01dd32c passed in 🧪 To try this PR locally: bunx bun-pr 41380That installs a local version of the PR into your bun-41380 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review. WalkthroughThe bundler now uses resolver-provided side-effects metadata for external imports and parsed input files. New plugin tests cover undefined ChangesResolver side-effects handling
Suggested reviewers: Merge Risk: ⚪ Minimal · up to This change restores correct tree-shaking behavior while retaining side effects when loaders transform files into JavaScript. The affected resolver paths are covered by targeted bundler regression cases, with no remaining merge-blocking risk identified. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
LGTM — brings the plugin fallback path in line with the non-plugin resolve path.
What was reviewed:
- Both hunks mirror the sibling non-plugin path exactly: the external-import flag set at line 2594-2598 matches
resolve_import_recordsat 6659-6663, andside_effectsat 3739 matches the entry-point append at 2762. - Checked that
enqueue_parse_task's copy-for-bundling branch (3768) still overwrites toNoSideEffectsPureDataafter this change, so non-onLoad data files keep their prior behavior. - Tests cover both changed sites (false / array / onLoad-loader-swap / builtin-external) and are placed in the existing
bundler_plugin.test.tsper convention; no CODEOWNERS cover these paths.
Extended reasoning...
Overview
This PR fixes side-effects tracking in the bundler when a plugin's onResolve returns undefined and the import falls through to run_resolver. Two spots in src/bundler/bundle_v2.rs are changed: the external-import early return now sets IS_EXTERNAL_WITHOUT_SIDE_EFFECTS from resolve_result.primary_side_effects_data, and enqueue_parse_task now stores resolve_result.primary_side_effects_data on the input file instead of loader.side_effects(). Four itBundled tests are added to test/bundler/bundler_plugin.test.ts.
Security risks
None. This is tree-shaking metadata propagation inside the bundler; no parsing of untrusted input, no auth/crypto/permissions surface, no new external I/O.
Level of scrutiny
Low-to-moderate. The change is two lines of logic, and each is a byte-for-byte copy of the corresponding line in the non-plugin sibling path (resolve_import_records at 6659-6663 and the entry-point input_files.append at 2754-2762). REVIEW.md's "fix the whole class" guidance is satisfied — the PR author explicitly enumerated the remaining divergences on this path (jsx overwrite, ExternalRewritePath, HTML imports) and scoped them out with reasons, which is the required treatment. The loader.side_effects() → primary_side_effects_data swap could have regressed pure-data files, but the copy-for-bundling branch immediately below (line 3768) still overwrites to NoSideEffectsPureData when no onLoad plugin fires, and the third test proves the onLoad case now correctly retains side effects.
Other factors
Tests are in the right file, use itBundled, and cover a solid variant matrix: sideEffects: false, sideEffects: [array] via a barrel (deliberately chosen because the barrel pass would mask the bug for false), an onLoad loader swap, and an external Node builtin with target: "node". The PR description states all four fail on 1.4.1 and pass on the branch. No CODEOWNERS entries cover src/bundler/ or test/bundler/. Exit reason was dry_streak with no candidate findings. No prior reviewer activity on the timeline.
|
Closing in favor of #45408. It sends a declined import back through |
Problem
onResolve({ filter: /.*/ }, () => undefined),Bun.buildtree-shakes differently than without the plugin. An unused file from a package with"sideEffects": falsestays in the bundle and runs.run_resolverinsrc/bundler/bundle_v2.rs. It drops the resolver'sprimary_side_effects_datain two places.enqueue_parse_taskstoresloader.side_effects()(line 3731 on main). The early return for an external import (line 2590) does not setIS_EXTERNAL_WITHOUT_SIDE_EFFECTS.Fix
bundle_v2.rs:2754,bundle_v2.rs:6652andParseTask::init).onLoadturns into JavaScript keeps its side effects. An unused import of a Node.js builtin is dropped.test/bundler/bundler_plugin.test.tsfail on bun 1.4.1 and pass with this change. Other suites: see Notes.jsxoverwrite inenqueue_parse_taskis a different field (see Notes).Background
"sideEffects"field of the nearestpackage.jsonand stores the result inprimary_side_effects_data. It also marks a Node.js builtin as free of side effects.LinkerContext::file_has_no_side_effects). If a file has no side effects, an import with no used bindings does not keep the file.onResolvecallback returns nothing for afileimport, the bundler resolves it inrun_resolver(ResolveValue::NoMatch). That function is a separate copy ofresolve_import_records.Notes
Data files (JSON, text, TOML):
onLoadran. So whenonLoadreturned JavaScript, the bundle dropped that code. The third test covers this. The path without plugins does not have this special case.a.jsusesdata.json, and entryb.jsimports it without using it. Thenb.jsalso gets the JSON. A build without plugins does this today. esbuild marks data files as pure data afteronLoad. A separate change can do that for both paths.nodeandbun, and the"sideEffects"array form. The output is the same in each case.Not in this PR (other values on the same path):
enqueue_parse_taskreplaces the tsconfigjsxvalue from the resolver with the transpiler's value (line 3745 on main). A fix must keep the transpiler's value forfiles:entries, asresolve_import_recordsdoes.ExternalRewritePathpath rewrite.filesimports through the shared steps (HTML import manifest, url assets) #38605 and bundler: copy HTML url assets resolved through files: or an onResolve plugin #38635 cover HTML imports on this path.resolve_import_recordsand removerun_resolver.Tests:
"sideEffects": false, a re-export barrel does not show the bug: the barrel pass reads the resolver's value from theParseTask, so it never loads the unused file. The array form does not use the barrel pass, so the second test uses a barrel.bundler_plugin,esbuild/dce,bundler_barrel,bundler_plugin_chain,bundler_files,bundler_loader,bundler_splitting,bun-build-api,bake/dev/plugins,bake/dev/css,bun-serve-html, and the regression tests for Tree-shaking drops a still-referenced declaration when onResolve returns undefined #40606, segmentation fault when build.onResolve returnsundefinedin a Bun.plugin #22199 and Bun segfaults when there are both external and unresolved/missing non-external imports #29264.Related:
async () => null. It still reproduces on 1.4.1.enqueue_parse_taskhunk, together with anoptimizeImportschange. A review of that PR asked for this fix as a separate PR.sideEffects: false+ catch-allonResolveplugin causes live bindings to be tree-shaken from namespace exports #29445 passes on 1.4.1 and on this branch.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bundler_plugin.test.ts