Conversation
WalkthroughPaths and sideEffects patterns are normalized and converted to absolute patterns for cross-platform matching; sideEffects checks are centralized; JS/JSC testing hooks added; unit and end-to-end tests for Windows path/glob scenarios and DCE cases were introduced or expanded. ChangesCross-Platform sideEffects Matching & Tests
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
I didn't find any bugs, but this swaps r.fs.join → r.fs.abs in the resolver's sideEffects pattern construction, which subtly changes path-joining semantics on all platforms (not just Windows) and directly affects tree-shaking decisions — worth a human sanity-check, especially with Windows CI as the only real discriminator.
Extended reasoning...
Overview
This PR fixes Windows-only sideEffects glob/exact matching by (1) building stored patterns with r.fs.abs instead of r.fs.join so they go through the same _joinAbsStringBufWindows joiner as runtime paths, (2) running exact-match map keys through normalizePathForGlob at both store and lookup time so \\ vs / doesn't break the hash, and (3) collapsing the .map/.glob/.mixed arms in resolver.zig to all route through hasSideEffects(). Four todo: isWindows markers are removed from existing dce tests, and a 4-case regression test is added.
Security risks
None. This is internal bundler path-normalization logic for tree-shaking decisions; no auth, crypto, or untrusted-input parsing is involved.
Level of scrutiny
Medium-high. The diff is small and the reasoning in the PR description is thorough, but:
r.fs.join→r.fs.abschanges which path-joining codepath runs on every platform (joinStringBuf→joinAbsStringwithtop_level_diras cwd). On posix the first part (dirWithTrailingSlash()) is already absolute so the result should be identical, but this is exactly the kind of subtle path-handling change where edge cases (symlinks,./prefixes, trailing slashes) bite.- Tree-shaking correctness is high-stakes: a false negative silently drops side-effectful code (the original bug); a false positive silently bloats bundles.
- The fix is only verifiable on Windows CI, which the author acknowledges — the new regression tests pass on Linux both before and after.
Other factors
r.fs.absreturns a pointer into a shared threadlocal buffer (parser_join_input_buffer); the result is immediately duped bynormalizePathForGlob, so lifetime is fine in the success path. Thecatch patternfallback would store a temp-buffer pointer, but that's pre-existing behavior and only triggers on OOM.- The robobun CI comment shows widespread
build-zigfailures on commit4f5f0f81; an autofix commit landed afterward, so this may already be resolved, but it's worth confirming green CI before merge. - No CODEOWNERS match for
src/resolver/.
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
test/regression/issue/30320.test.ts:155-158— nit: perCLAUDE.md("Writing Tests"),expect(stdout)...should come beforeexpect(exitCode).toBe(0)so a failure shows the missing output rather than just an exit-code mismatch. Moveexpect(exitCode).toBe(0)after the twoexpect(stdout).toContain(...)lines.Extended reasoning...
What this is
CLAUDE.mdline 128 documents an explicit repo convention for spawned-process tests:When spawning processes, tests should
expect(stdout).toBe(...)BEFOREexpect(exitCode).toBe(0). This gives you a more useful error message on test failure.test/CLAUDE.mdlines 48–50 show the canonical ordering:stderr→stdout→exitCode. In the new end-to-end test attest/regression/issue/30320.test.ts:155-158, the assertions are orderedstderr→exitCode→stdout, so theexitCodecheck runs before the twostdoutcontent checks.Why it matters (and why it mostly doesn't)
The point of the convention is diagnostic quality: when a spawned process fails, you want the assertion that prints the content (stdout/stderr) to fire first, so the test failure message shows what actually went wrong instead of just
expected 0, received 1.To be fair, this test already gets most of that benefit —
expect(stderr).toBe("")is asserted first, so abun buildfailure that writes to stderr will surface the diagnostic before eitherexitCodeorstdoutis checked. And in the specific regression this test guards against (sideEffects glob fails to match → side-effect imports tree-shaken), the build still succeeds withexitCode === 0and empty stderr, so execution reaches thestdoutassertions regardless of where theexitCodeline sits. The current ordering does not affect test correctness.The remaining gap is the (admittedly narrow) failure mode where
bun buildexits non-zero with nothing on stderr. With the current ordering you'd getexpected 0, received 1and never see what was on stdout; with the documented ordering you'd see the stdout content (or its absence) first.Step-by-step
- Suppose
bun buildexits with code1, writes nothing to stderr, and writes a diagnostic to stdout (rare but possible). - Line 155
expect(stderr).toBe("")passes. - Line 156
expect(exitCode).toBe(0)fails withexpected: 0, received: 1— test stops here. - The
stdoutcontent that would have explained the failure is never printed in the assertion diff.
With the documented order (stdout before exitCode), step 3 would instead be
expect(stdout).toContain("foo adapter registered"), which fails showing the actual stdout content.Fix
Swap the lines so the order is
stderr→stdout→stdout→exitCode:expect(stderr).toBe(""); expect(stdout).toContain("foo adapter registered"); expect(stdout).toContain("bar adapter registered"); expect(exitCode).toBe(0);
Severity
Nit. This is a documented style convention rather than a functional bug, and the practical diagnostic benefit is small here because
stderris already checked first. Flagging only because it's an explicitCLAUDE.mdrule and the fix is a trivial one-line move. - Suppose
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/regression/issue/30320.test.ts`:
- Around line 141-146: The test currently asserts stderr is exactly empty which
is flaky on ASAN; update the test around the spawn result (variables stdout,
stderr, exitCode) to remove expect(stderr).toBe(""), keep the stdout assertions
(expect(stdout).toContain("foo adapter registered") and
expect(stdout).toContain("bar adapter registered")), then assert the exit code
last (expect(exitCode).toBe(0)); if the exit code is non‑zero, dump or assert on
stderr to aid debugging (e.g., fail the test with stderr content) so stderr is
only examined when the process fails.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d0e472fe-b1d1-42b4-80a9-945503408f88
📒 Files selected for processing (2)
src/resolver_jsc/package_json_jsc.zigtest/regression/issue/30320.test.ts
|
@robobun rebase this for the new rust version of bun |
The Rust port of the package.json resolver inherited the same bug PR #30322 fixed in the Zig implementation: patterns stored via `r_fs.join` with .loose produce `/C:/pkg/...` on Windows, while runtime paths from `r_fs.abs` produce `C:\pkg\...` — after normalizePathForGlob (\\ -> /) the pattern still carries a leading slash the path doesn't, so no file ever matches. - Build patterns via `r_fs.abs` so they share the joiner used for runtime paths (\_joinAbsStringBuf). - Normalize map keys at parse time. - Route Map/Glob/Mixed branches in finalize_result through has_side_effects so the same normalization applies to lookups. - Drop the Zig TestingAPI scaffolding (too expensive to Rust-port for a testing-only helper); rely on the end-to-end bundler test and Windows CI as the real regression verifier.
68d88d6 to
9c05c92
Compare
|
Rebased onto the Rust port — applied the same fix to Dropped the Zig |
|
@robobun Thank you for your Rust port. It works on my Win11. Looking forward to see it merged. |
|
@Jarred-Sumner When you have time, could you please review this PR? |
54665b9 to
a9b618f
Compare
The Rust port of the package.json resolver inherited the same bug PR #30322 fixed in the Zig implementation: patterns stored via `r_fs.join` with .loose produce `/C:/pkg/...` on Windows, while runtime paths from `r_fs.abs` produce `C:\pkg\...` — after normalizePathForGlob (\\ -> /) the pattern still carries a leading slash the path doesn't, so no file ever matches. - Build patterns via `r_fs.abs` so they share the joiner used for runtime paths (\_joinAbsStringBuf). - Normalize map keys at parse time. - Route Map/Glob/Mixed branches in finalize_result through has_side_effects so the same normalization applies to lookups. - Drop the Zig TestingAPI scaffolding (too expensive to Rust-port for a testing-only helper); rely on the end-to-end bundler test and Windows CI as the real regression verifier.
a9b618f to
0ae4cb3
Compare
The Rust port of the package.json resolver inherited the same bug PR #30322 fixed in the Zig implementation: patterns stored via `r_fs.join` with .loose produce `/C:/pkg/...` on Windows, while runtime paths from `r_fs.abs` produce `C:\pkg\...` — after normalizePathForGlob (\\ -> /) the pattern still carries a leading slash the path doesn't, so no file ever matches. - Build patterns via `r_fs.abs` so they share the joiner used for runtime paths (\_joinAbsStringBuf). - Normalize map keys at parse time. - Route Map/Glob/Mixed branches in finalize_result through has_side_effects so the same normalization applies to lookups. - Drop the Zig TestingAPI scaffolding (too expensive to Rust-port for a testing-only helper); rely on the end-to-end bundler test and Windows CI as the real regression verifier.
0ae4cb3 to
1d8e6bc
Compare
|
Rebased onto main. The rebase was non-trivial: main finished the Zig port and deleted No behavior change from the last reviewed version. Fail-before re-verified locally: reverting just the resolver diff makes the exact + mixed Windows-path cases fail (2/6), restoring it makes all 6 pass. |
b88fadb to
7d4cf0f
Compare
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟣
src/resolver/package_json.rs:1436-1444— Pre-existing, non-blocking: this exact-only branch has no.cssskip, while the mixed (line 1379) and glob-only (line 1419) branches bothcontinueonbun_paths::extension(name) == b".css", so["styles.css"]keeps the entry but["styles.css", "lib/*.js"]silently drops it. The exact-only branch is arguably the correct one (esbuild doesn't special-case CSS insideEffects, and dropping an entry the author explicitly listed is backwards), so the follow-up is probably to remove the skip from the other two arms rather than add it here — just flagging since this PR rewrites all three.Extended reasoning...
What this is
A pre-existing inconsistency between the three sibling
sideEffectsparse arms inPackageJSON::parse, surfaced because this PR rewrites all three. It is not introduced by this PR and should not block it.- Mixed branch (line 1379) and glob-only branch (line 1419) both have:
if bun_paths::extension(name) == b".css" { continue; }
- Exact-only branch (lines 1436–1444, this hunk) has no such skip.
The first-pass classifier at lines 1353–1365 does not look at extensions, so a
.cssentry with no glob characters routes to the exact-only or mixed arm purely based on whether any other entry in the array contains a glob character.Step-by-step proof of the asymmetry
Take a package at
/p/node_modules/pkg/.Case A —
"sideEffects": ["styles.css"]:- First pass:
styles.csshas no*?[{→has_exact = true,has_globs = false. - Falls into the exact-only
elsearm at line 1432. - No CSS check →
r_fs.abs([dir, "styles.css"])→ normalized → inserted intomap. SideEffects::Mapcontains/p/node_modules/pkg/styles.css;has_side_effects("/p/node_modules/pkg/styles.css")→ true.
Case B —
"sideEffects": ["styles.css", "lib/*.js"]:- First pass:
styles.css→has_exact = true;lib/*.js→has_globs = true. - Falls into the mixed arm at line 1370.
- Iteration hits
styles.css→bun_paths::extension(b"styles.css") == b".css"→continue(line 1379–1381). Entry dropped. SideEffects::Mixedhas an emptyexactmap (only the JS glob inglobs);has_side_effects("/p/node_modules/pkg/styles.css")→ false.
So the same explicit
styles.cssentry is honoured or silently discarded depending on whether an unrelated sibling entry happens to contain a*.Which arm is correct?
This branch — the one without the skip — is almost certainly the right one, and the comment is filed here only because the PR rewrites these exact lines:
- esbuild (the reference implementation this code ports) does not special-case CSS in
sideEffectsparsing at all; it stores every array entry. The exact-only arm matches that. - Packages list
*.cssinsideEffectsprecisely because CSS imports are side-effectful (they inject styles) and must survive tree-shaking. Dropping an entry the author explicitly listed inverts the field's purpose. - Adding the skip here (i.e. "fixing" the inconsistency in the wrong direction) would turn
["styles.css"]into an empty map →has_side_effectsreturnsfalse→ the explicitly-listed CSS file is marked side-effect-free. That would be a regression, not a fix.
So if anything is wrong, it's the skip at lines 1379 and 1419, not the absence of one here.
Why this may be moot in practice
Bun's bundler appears to route CSS through a separate chunking pipeline (
css_entry_points/ast.cssguards inParseTask), so it's plausiblehas_side_effects()is never consulted for a.csspath and the dropped map entry is dead weight either way. I haven't traced that exhaustively; if it holds, the asymmetry is harmless storage noise rather than an observable tree-shaking difference. That uncertainty is another reason this is filed as pre-existing/non-blocking rather than a defect.Why mention it on this PR at all
The diff context shows the pre-PR exact-only branch also lacked the CSS skip, so the asymmetry predates this change entirely and is orthogonal to the Windows path-shape fix (#30320). It's raised only because this PR mechanically rewrites the bodies of all three arms in the same commit, so the author is already looking at every line involved — a cheap moment to either drop the two
continues for esbuild parity or leave a note explaining why CSS is intentionally skipped there.Suggested follow-up (not for this PR)
Remove the
if bun_paths::extension(name) == b".css" { continue; }from the mixed and glob-only arms so all three agree with esbuild and with each other. Do not add the skip to the exact-only arm. - Mixed branch (line 1379) and glob-only branch (line 1419) both have:
|
Re the Agree with your read that the exact-only arm (no skip) matches esbuild, so the correct follow-up would be to drop the |
7d4cf0f to
c44c1e7
Compare
On Windows the resolver stored `sideEffects` patterns built with `r_fs.join(dir, name)`, which for an absolute input routes through `normalize_string_node_t` and prepends a leading `/` before the drive letter: `/C:/proj/node_modules/my-lib/adapters/**/*.js`. Runtime paths come from `r_fs.abs` instead, which emits `C:\proj\...\foo.js` (no leading `/`). After `normalize_path_for_glob` (`\` -> `/`) the stored pattern still carried the leading `/` the path never had, so neither glob nor exact-match keys ever matched and every file was treated as side-effect-free. prebid.js (`"sideEffects": ["dist/src/modules/**/*.js"]`) silently lost every bid adapter on Windows. Fix: - Build patterns with `r_fs.abs` so they share the joiner the runtime path uses (no spurious leading `/`). - Strip any package-root-relative leading `/` or `\` from each entry before joining, so `r_fs.abs` joins against the package dir instead of discarding it (path.resolve treats an absolute later component as a new root; the strip loops so `//x` is handled too). Without this, entries like `"/index.js"` would regress on POSIX, where `r_fs.join` previously concatenated them correctly. - Normalize exact-match map keys at parse time, and normalize the lookup path in `has_side_effects` for the Map/Mixed variants so `\` vs `/` no longer matters. - Route the Map/Glob/Mixed branches in `finalize_result` through `has_side_effects` so the lookup-side normalization always applies. The bug is Windows-only (POSIX paths never gain the leading `/`), so the regression test drives `SideEffects::has_side_effects` through `bun:internal-for-testing` with synthetic `C:\pkg\...` strings on any host; `usePreFix: true` reproduces the pre-fix `r_fs.join` shape to prove fail-before. End-to-end `bun build` cases guard the real bundler path, and the four `PackageJsonSideEffectsArray*` bundler cases in dce.test.ts drop their `todo: isWindows` markers. Closes #30320 Closes #22598
c44c1e7 to
7ea056d
Compare
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
Bun's bundler mis-handles package.json sideEffects paths on Windows (oven-sh/bun#30322, unfixed in 1.3.14). POSIX-style entries like ./dist/solid.mjs never match Windows-resolved paths, so the side-effect import "import opentui-spinner/solid" gets tree-shaken on Windows builds. At runtime, the opentui reconciler throws "[Reconciler] Unknown component type: spinner" when the TUI renders the spinner. Official CI builds on Linux and cross-compiles to Windows, so the bug is invisible there. Local Windows builds hit it because path separators do not match. This adds a small onResolve plugin that explicitly marks opentui-spinner/solid and /react as having side effects via the official OnResolveResult.sideEffects API, restoring correct behavior on every host. Remove once Bun ships the fix for oven-sh/bun#30322.
|
Stale PR review: closing. Main already has this fix. 49b9a62 (merged in #34552, shipped in v1.4.1) builds the Reopen if this evidence is wrong. |
What does this PR do?
Fixes #30320: on Windows,
sideEffectsglobs never matched and every file in a package was treated as side-effect-free.prebid.js@10.29.0("sideEffects": ["dist/src/modules/**/*.js"]) silently lost every bid adapter on Windows.Cause. Patterns were built with
r_fs.join({package_dir, name})(.looseplatform). That routes throughjoin_string_buf→normalize_string_node_t, which prepends a leading/for absolute inputs, so a Windows package dir produced/C:/proj/node_modules/my-lib/adapters/**/*.js. Runtime paths instead go throughr_fs.abs→_join_abs_string_buf_windows, emittingC:\proj\...\foo.js— no leading/. After the shared\→/pass innormalize_path_for_glob, the stored pattern kept a leading/the subject never had, so neither glob nor exact-match keys matched. The same mismatch broke exact-match keys, which is why four existing tests were markedtodo: isWindows.Fix.
r_fs.absinstead ofr_fs.joinso it shares the joiner the runtime path uses./or\from each entry before joining, sor_fs.absjoins against the package dir instead of discarding it (path.resolvetreats an absolute later component as a new root). Without this,"/index.js"-style entries would regress on POSIX wherer_fs.joinpreviously concatenated them; matches esbuild'sfilepath.Join.has_side_effects(Map/Mixed), so\vs/no longer affects the hash.Map/Glob/Mixedbranches infinalize_resultthroughhas_side_effectsso the lookup-side normalization always applies.todo: isWindowsmarkers on the fourPackageJsonSideEffectsArray*bundler tests this now unsticks.How did you verify your code works?
The bug is Windows-only (POSIX paths never gain the leading
/), sotest/regression/issue/30320.test.tsdrivesSideEffects::has_side_effectsthroughbun:internal-for-testingwith syntheticC:\pkg\...strings on any host.usePreFix: truereproduces the pre-fixr_fs.joinshape, so the exact and mixed cases fail without the src fix and pass with it on Linux (verified locally: reverting the resolver diff makes 2/6 fail, restoring makes all pass). Two end-to-endbun buildcases guard the real bundler path (glob entries, and a leading-slash entry). Windows CI exercises the original repro directly, and @brynne8 confirmed the build works on Win11.Closes #30320
Closes #22598