Skip to content

Fix 14585 — unminified identifier collisions - #17930

Merged
Jarred-Sumner merged 2 commits into
oven-sh:mainfrom
jbarrieault:jb/issue-14585
Mar 6, 2025
Merged

Jarred-Sumner merged 2 commits into
oven-sh:mainfrom
jbarrieault:jb/issue-14585

Conversation

@jbarrieault

@jbarrieault jbarrieault commented Mar 6, 2025 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #14585

This fixes a bundling bug in which the parsing step fails to hoist block-scoped vars, which can result in identifier collisions.

Implementation

I did a fair amount of debugging in bundler_v2.zig and renamer.zig to understand the issue well enough to identify the issue being in the parsing step. Namely, the discovery that at identifier rename time, d was not a top-level symbol in any of foo.js's chunks. It was instead in a child scope. Thus, the performing renaming on bar.js wasn't detecting that the identifier d was already taken by the block-scoped var in foo.js.

That said, I spent much less time stepping through the js_parser.zig stack frames and have a very limited understanding of it. The implementation may be less than ideal for all I know.

How did you verify your code works?

  • I included a test for the new code, or existing tests cover it
  • I ran my tests locally and they pass (bun-debug test test-file-name.test)

I added the test @Jarred-Sumner provided based on my initial reproduction of the issue. I observed the test failing initially, and passing after the the change. Additionally, manually verifying the output looks as expected:
image

Zig change PR template checkboxes

I don't know if these apply or how to do/answer them.

  • I checked the lifetime of memory allocated to verify it's (1) freed and (2) only freed when it should be
  • I included a test for the new code, or an existing test covers it
  • JSValue used outside outside of the stack is either wrapped in a JSC.Strong or is JSValueProtect'ed
  • I wrote TypeScript/JavaScript tests and they pass locally (bun-debug test test-file-name.test)

Identifier collision from block scope var when not minifying identifiers
…This allows bundle_v2's renameSymbolsInChunk function to tell its renamer about hoisted block-scope var declarations so its child scopes do not create collisions
Comment thread src/js_parser.zig
.binding = p.b(B.Identifier{ .ref = ref }, local.loc),
};
try partStmts.append(p.s(S.Local{ .decls = G.Decl.List.init(decls) }, local.loc));
try p.declared_symbols.append(p.allocator, .{ .ref = ref, .is_top_level = true });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is used a few lines below, which is then used by the bundler immediately prior to renaming.

@paperclover paperclover 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.

This is not the correct fix. It is a workaround, and actually the same bug and workaround I ran into somewhere else.

                // TODO(paperclover): fix the renamer bug. this bug
                // theoretically affects all usages of temp refs, but i cannot
                // find another example of it breaking (like with `using`)
                p.declared_symbols.append(p.allocator, .{
                    .is_top_level = true,
                    .ref = ctx_storage.*.?.signature_cb,
                }) catch bun.outOfMemory();

It may be worthwhile to compare esbuild's code around this point. Note how it does not append to declared_symbols in their code.

If this PR is blocking something important, i'm ok with adding a TODO comment here. but the real bug is in renaming, hoisting, or symbol merging code. I haven't investigated enough to know.

},
});
// https://github.com/oven-sh/bun/issues/14585
itBundled("identifiers/SameNameDifferentModulesWithMinifyIdentifiersDisabled", {

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.

by the way this is a good and correct test

@Jarred-Sumner
Jarred-Sumner merged commit 1570d4f into oven-sh:main Mar 6, 2025
@jbarrieault
jbarrieault deleted the jb/issue-14585 branch March 7, 2025 22:32
BYK added a commit to getsentry/cli that referenced this pull request Apr 1, 2026
… bug (#619)

## Summary

- Replace `Bun.build()` with esbuild in the binary build's Step 1 (TS →
JS bundling)
- Restores full identifier minification that was disabled in #617
- Fixes sourcemap quality: esbuild produces 27,860 names vs Bun's empty
array

## Problem

Bun's identifier minification has an unfixed collision bug
([oven-sh/bun#14585](oven-sh/bun#14585)) where
minified output produces name collisions across module scopes, causing
runtime `X is not a function` errors. PR #617 worked around this by
disabling identifier minification (`identifiers: false`), but that loses
~22% bundle size reduction and still uses Bun's sourcemaps which have an
empty `names` array.

The upstream fix
([oven-sh/bun#17930](oven-sh/bun#17930)) was
acknowledged as a workaround — the real bug in Bun's renamer/hoisting
code was never properly fixed. We still hit collisions on Bun 1.3.11.

## Solution

Use esbuild (already a devDependency for the npm bundle) for the binary
build's bundling step. The compile step still uses `Bun.build({ compile:
true })` since that's the only way to produce native Bun binaries.
esbuild's output is standard ESM that `Bun.build({ compile })` accepts
without issues.

Config: `platform: "node"`, `target: "esnext"`, `format: "esm"`,
`external: ["bun:*"]`, `sourcemap: "external"`, `minify: true`

## Size comparison

| Metric | PR #617 (identifiers:false) | This PR (esbuild full minify) |
|--------|---------------------------|-------------------------------|
| JS bundle | 3.64 MB | 2.82 MB (**-22%**) |
| Binary (gzip) | ~28.4 MB | ~28.4 MB |
| Sourcemap names | 0 (empty!) | 27,860 |
| Sourcemap sources | N/A | 1,006 |

## Verification

- `auth status` (PR #617 crash scenario): renders markdown correctly
- E2E tests: 122 pass, 0 fail, 3 skip
- Debug ID injection: works (UUID generated)
- `Bun.build({ compile })` accepts esbuild ESM output
- Lint: clean

## Changes

Single file: `script/build.ts`
- Added esbuild import, replaced `Bun.build()` in `bundleJs()` with
`esbuild()`
- Added `mkdirSync("dist-bin")` since esbuild doesn't auto-create output
dirs
- Updated JSDoc

Made with [OpenCode](https://opencode.ai)
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.

reference collision issue with Bun.build

3 participants