Avatar for the swc-project user
swc-project
swc
BlogDocsChangelog

Performance History

Latest Results

fix(es/minifier): always run hygiene before the mangler The minify pipeline skipped `hygiene()` whenever the mangler was enabled, on the assumption that the mangler renames everything hygiene would. That assumption is false: the mangler preserves names it is not allowed to rename — most importantly top-level bindings, which with the default `mangle.top_level = false` are never renamed at all. It therefore cannot deconflict a synthesized binding that collides with a preserved one. IIFE inlining hoists a parameter (or body-level `var`) binding into the enclosing scope, reusing its original nested syntax context. When that lands at the top level next to a user binding of the same name, the two collide: SyntaxError: Identifier 'x' has already been declared `hygiene()` renames one of them (e.g. to `x1`) and resolves the collision, but it was being skipped precisely in the configuration that needs it (compress + mangle, the default `@swc/core` script setup). SWC's parser does not enforce lexical redeclaration, so the invalid output shipped silently; Node rejects it at load. This surfaced in Turbopack/Next.js output, whose top-level `eval('require')` shim also disables the dead-parameter elimination that would otherwise remove the hoisted binding. Fix: run `hygiene()` unconditionally after `optimize()` in every minify entry point (`Compiler::minify`, the `transform()` minifier pass, and the node minifier binding), and mirror this in the minifier test harness. Hygiene runs after all compressor passes, so unlike a mid-pipeline rename it does not desync usage data or regress optimization. A handful of fixtures update: hygiene now disambiguates shadowed names (e.g. a nested `function n()` that the mangler had left shadowing an outer `n`) — latent bugs the same misconfiguration was producing without an error. The wasm minifier binding inherits the fix through `Compiler::minify`. Closes: https://github.com/swc-project/swc/issues/11977 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lukesandberg:fix/issue-11977-iife-mangle-collision
5 hours ago
chore: Add changeset
Chastrlove:fix-react-compiler-spans
18 hours ago
fix(es/minifier): deconflict IIFE-hoisted bindings under mangle IIFE inlining hoists parameter (and body-level `var`) bindings into the enclosing scope while reusing their original nested syntax context. When the hoist target is the top level and a same-symbol binding already lives there, the two bindings collide. The non-mangle pipeline is saved by `hygiene()`, which renames the hoisted binding (e.g. to `x1`). But `hygiene()` is skipped once mangling is enabled, and the mangler cannot rename the hoisted binding: its context is a descendant of the top-level mark, so the preserver keeps its name, and with the default `top_level: false` the top level is never renamed. The result was two top-level `x` declarations: SyntaxError: Identifier 'x' has already been declared (SWC's parser does not enforce lexical redeclaration, so the invalid output was emitted silently; Node rejects it at load time.) Fix: when mangling is enabled, remap each hoisted binding's identifier to a fresh synthesized syntax context (a mark whose parent is the root, like `private_ident!`). Such a context is not a descendant of the top-level mark, so the mangler's existing "force rename synthesized names" path deconflicts it exactly as `hygiene()` would. The remap is applied to the hoisted declarators and to every reference (assignment LHS and body uses), which all share the same original id. The react-pdf-renderer fixture output improves as a side effect: the fresh context unblocks a UMD-wrapper simplification the optimizer previously could not perform. Closes: https://github.com/swc-project/swc/issues/11977 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lukesandberg:fix/issue-11977-iife-mangle-collision
19 hours ago
fix(react-compiler): move index.ts/binding.js to package root Reported by an automated reviewer: the previous layout (index.ts under src/, binding.js/binding.d.ts also under src/, tsc's rootDir "./src" + outDir "./") had `tsc -d` (running before `napi build` in build:dev, same order as CI) generate its OWN weak, type-inferred root binding.d.ts from src/binding.js (a plain JS file with no real type annotations -- roughly `export = nativeBinding; declare let nativeBinding: any`), since tsc treats already-existing .d.ts files as ambient declarations it never copies to outDir, and only emits declarations for .js/.ts source files it actually compiles. napi build's own --dts flag only ever wrote the real, richly-typed declarations to src/binding.d.ts, never to the root file that actually ships. So `export type { Diagnostic, ... } from './binding'` and `lint`/`lintSync`'s `binding.Diagnostic[]` return types resolved fine while developing (tsc checks src/index.ts against its real, correctly-located sibling src/binding.d.ts) but were broken for any real downstream consumer of the published package, who only gets the root-level files. Confirmed by simulating a consumer against only the final built root/index.d.ts + root/binding.d.ts (no access to a src/ directory): `Cannot find module '../binding'` before this fix, clean after. @swc/core avoids this because its binding.js/binding.d.ts live at the package root already, not under src/ -- an initial attempt to copy that exact pattern here (keep index.ts under src/, use a type-only `typeof import("../binding")` + separate runtime `require("./binding")` split, matching @swc/core's own src/index.ts) turned out to have the same latent bug: tsc emits the *literal* import-specifier text into the compiled .d.ts without rewriting it for the new location, so `../binding` ends up wrong in the emitted root index.d.ts too, once a public signature (unlike @swc/core's internal-only use of that pattern) actually needs to resolve it. Verified this by re-running the same consumer simulation against that version -- still broken. The actual fix: index.ts now lives at the package root too. It was the only real source file under src/ (binding.js/binding.d.ts are napi build's output, not authored TS/JS) once those moved out for the same reason, so the src/ subdirectory no longer serves any purpose -- having rootDir/outDir differ was the entire source of the bug, and with only one file, there's no reason to keep that split. `./binding` is now unambiguously correct everywhere: same directory, before and after compilation, for the source file, the compiled JS's require call, and the emitted .d.ts's import specifier alike. Re-verified the consumer simulation passes clean, and the runtime test suite (tests/lint.test.cjs) still passes 4/4 against the new layout. @swc/minifier has the identical structural bug (binding.js/d.ts under src/, minifySync/minify return binding.TransformOutput) and is already shipping it in production -- confirmed separately, left as a distinct out-of-scope issue for this PR.
imjordanxd:feat/react-compiler-lint-diagnostics
1 day ago
fix(react-compiler): convert diagnostic positions to UTF-16 code units swc_ecma_react_compiler's Position (loc.column/index) is intentionally byte-based -- it also feeds DiagnosticMessage.span, which @swc/core's existing react-compiler transform-error path turns into a real swc_common::Span/BytePos for terminal error rendering. That consumer genuinely needs byte offsets, so the shared representation must stay byte-based. lint/lintSync are a different consumer exposing these positions to JS/ESLint, whose string indexing is UTF-16-based. For source containing any non-ASCII character before a diagnostic's location, returning raw byte offsets there produces wrong positions for any JS consumer. Fixes this at this package's own boundary instead of the shared upstream representation: diagnostic_from_message (bindings/ binding_react_compiler_node/src/diagnostics.rs) now takes the original source text and re-derives column/index as UTF-16 code units, using the byte-based line/column as a starting point. For ASCII-only source this is a no-op. Verified against the real built binary: for source with an emoji before a violation, source.slice(loc.start.index, loc.end.index) (JS's own UTF-16 string slicing) now correctly extracts just the violating expression. An earlier version of this fix changed swc_ecma_react_compiler's shared position-computation function directly, which would have fixed this for lint/lintSync but broken the unrelated, already-shipped transform-error span rendering for the same class of input. Caught before landing.
imjordanxd:feat/react-compiler-lint-diagnostics
2 days ago

Latest Branches

CodSpeed Performance Gauge
-4%
fix(es/minifier): always run hygiene before the mangler#11978
5 hours ago
6e221ee
lukesandberg:fix/issue-11977-iife-mangle-collision
CodSpeed Performance Gauge
0%
18 hours ago
d6e90d0
Chastrlove:fix-react-compiler-spans
CodSpeed Performance Gauge
0%
1 day ago
d07e188
imjordanxd:feat/react-compiler-lint-diagnostics
© 2026 CodSpeed Technology
Home Terms Privacy Docs