Latest Results
feat(es/parser): add opt-in parser-only TSRX lowering (#12120)
**Description:**
This rewrites the initial TSRX implementation as an opt-in, parser-only
lowering instead of extending `swc_ecma_ast`.
The parser recognizes TSRX through the default-disabled
`swc_ecma_parser/tsrx` Cargo feature and immediately lowers the syntax
to existing TypeScript/JSX AST nodes. As a result, the shared AST,
generated visitors, codegen, transforms, JavaScript bindings, WASM
bindings, configuration schema, and filename-based defaults remain
unchanged.
Rust facade exposure is intentionally limited to:
- `swc_ecmascript/tsrx-parser`
- `swc_core/ecma_parser_tsrx`
Most TSRX-specific behavior lives in
`swc_ecma_parser/src/parser/tsrx.rs`, including parser-private IR,
validation, standard AST construction, React helper tracking,
collision-safe generated identifiers, conditional import injection, and
JSX directive handling. The lexer and existing parser modules contain
only feature-gated delegation and JSX rescanning hooks. With the feature
disabled, the TSRX parser state and the JSX `@` scan path are not
compiled.
TSRX is defined as module-only syntax. `parse_module` and
`parse_program` are supported, and `parse_program` always returns a
module when TSRX is enabled. Script/CommonJS and standalone-expression
entry points report dedicated diagnostics when TSRX syntax is used.
The lowering covers:
- direct function `@{ setup; render }` bodies and expression `@{}` IIFEs
- `@if` conditionals, using branch IIFEs only when setup statements
require them
- classic `for`, `for-in`, `for-of`, and `for-await` loops with
preserved `break`/`continue`, per-iteration index bindings, key
handling, and entered-state-based `@empty`
- `@switch` with fallthrough and explicit break behavior preserved
- `@try` content components with optional React `Suspense`,
`TsrxErrorBoundary`, catch/reset patterns, setup statements, and
synchronous JavaScript `finally`
- single-evaluation dynamic tag aliases
- JSX prop shorthand normalization
- raw `<style>` bodies represented as ordinary JSX string expression
children
Required React helpers are inserted after directive prologues and only
when used. Generated bindings avoid source-level name collisions and use
a separate `SyntaxContext`.
This deliberately does not implement hook analysis, helper/static
hoisting, scoped CSS extraction, or optimization parity with the
canonical `@tsrx/react` compiler. In particular, `@finally` follows
synchronous JavaScript `try/finally` semantics and does not model a
Suspense asynchronous lifetime.
Validation includes feature-gated fixture snapshots for the full syntax
surface, comments/trivia, nested JSX, all loop forms, switch behavior,
try/pending/catch/reset/finally, helper import placement, name
collisions, module-only diagnostics, and invalid syntax. Lowered TSX
snapshots are reparsed by the regular TSX parser, and serialized AST
checks ensure no TSRX-specific public node types are emitted.
Checks run locally:
- `cargo fmt --all -- --check`
- `cargo clippy --all --all-targets -- -D warnings`
- `cargo clippy -p swc_ecma_parser --no-default-features --features tsrx
--all-targets -- -D warnings`
- TSRX fixture tests with and without snapshot updates
- feature-disabled JSX fixture tests
- `cargo test -p swc_ecmascript --no-default-features --features
tsrx-parser`
- `cargo test -p swc_core --no-default-features --features
ecma_parser_tsrx`
- parser benchmark comparison against the PR base with the TSRX feature
disabled; Criterion detected no performance change
The full parser suite was also run with both `typescript` and `tsrx`.
The existing
`errors_tests__typescript_errors__type_only_import_specifier__invalid_type_only__input_ts`
fixture fails identically in both configurations; all TSRX-specific
tests pass.
**Related issue (if exists):**
N/A
---------
Co-authored-by: DongYun Kang <kdy.1997.dev@gmail.com> fix(es/minifier): drop spans of cached `globals` values (#12129)
**Related issue:** none filed yet — happy to open one if you would like
the report separately.
Rewritten after your review: the fix moved from `inline_globals` to the
cache that produces the values, and the PR now leads with a failing
test.
## Description
`GlobalPassOption::build` parses each
`jsc.transform.optimizer.globals.vars` value into an anonymous file of
the `SourceMap` it is handed, and memoizes the parsed `Expr` in a
process-wide `static CACHE`. A span is only meaningful to the
`SourceMap` that produced it, so the cache outlives what its contents
point at.
For a host that builds a fresh `SourceMap` per file — which is what
`swc::Compiler` does, and what a bundler does — the emitted source map
depends on whether the call hit the cache:
- on the call that parses the value, the mappings point into the
anonymous file. With `source_file_name` set, `map_file_name_to_source`
reports that file under the name of the file being compiled, so the
position is silently attributed to the user's source;
- on every call that hits the cache, the position belongs to a
`SourceMap` that no longer exists: it is dropped when it is past the end
of the file being compiled, and points at an unrelated line when that
file is longer.
Three things that cannot affect the semantics of `globals` currently
affect its output:
1. **the order the host passes the defines in** — the cache key is a
`Vec<(Atom, Atom)>` built from `vars`, so a host that holds the defines
in a `std::collections::HashMap` (whose iteration order differs per
instance) produces a different key per call, and therefore a different
hit/miss pattern;
2. **whether the call hits the process-wide cache** — a cache should be
semantically a no-op;
3. **which file was compiled first in the process** — the cached
position is derived from the size of that file, so in a parallel build
it depends on scheduling.
That is how this surfaced: in a Rspack build the source map is part of
the module hash, so the chunk hash moved between builds of unchanged
sources, and with minification on every mangled name in the chunk moved
with it.
## Fix
Drop the spans before the value is cached.
Parsing still happens in the caller's `SourceMap`, so a syntax error in
a define value is still reported with a position — but nothing
`SourceMap`-dependent survives into the cache. Substituted values then
carry `DUMMY_SP`, which `srcmap!` skips, so no mapping is emitted for
them on any call, whether it parsed the value or got it from the cache.
The previous revision of this PR overwrote the spans at the four
substitution sites in `inline_globals` instead. It left the cache
poisoned for any other consumer, added a subtree visit per substitution
on a hot path, and gave synthesized nodes the span of real source. This
one is a single `drop_span` at the point the value is created, which is
also what `inline_globals`' own tests already assume — they build their
globals map with `DropSpan`.
If you would rather keep a mapping for substituted values (`__DEV__`
mapping back to `__DEV__` is nicer than no mapping at all), that can be
layered on top cheaply now: with the cached children already dummy, the
substitution site only needs `value.set_span(ident.span)`, no visitor. I
left it out because it is a separate change.
## Tests
`crates/swc/tests/source_map.rs` gets a test that compiles the same
input twice, each in a fresh `SourceMap`, and compares the emitted maps.
It is committed before the fix and fails there:
```
assertion failed: `(left == right)`: the same input produced two different source maps
< {"version":3,"sources":["input.js","<anon>"],"sourcesContent":[...,"true"],...}
> {"version":3,"sources":["input.js"],"sourcesContent":[...],...}
```
The first compilation leaks the define value itself into the map as a
source file; the second, which hits the cache, does not.
- `cargo test -p swc_ecma_transforms_optimization` — pass
- `cargo test -p swc --test projects` — 889 pass
- `cargo test -p swc --test rust_api` — pass
- `cargo test -p swc --test source_map` — pass except `issue_622` and
the `stacktrace` fixtures, which fail on `main` on my machine too (Node
version)
The end-to-end numbers in the original description (400 transforms → 1
distinct source map, 40 builds → 1 distinct chunk hash) were measured
with the previous revision. I have not rebuilt the downstream binding
against this revision; the invariant is the one the new test asserts,
and this revision removes the mappings entirely rather than rewriting
them. Latest Branches
0%
fix/minifier-jsx-sequence-progress 0%
fix/flow-component-type-semantics 0%
trueadm:codex/tsrx-parser © 2026 CodSpeed Technology