Preserve parentheses around TypeScript instantiation expressions in postfix contexts. Previously, `(f<T>).x` and `(f<T>)[x]` were emitted as `f<T>.x` and `f<T>[x]`, producing invalid syntax or changing how the expression is parsed.
The printer now respects the surrounding precedence while retaining grouping for lower-precedence operands.
`packages/codegen` drops required parentheses around the right operand of a private-in expression when that operand is another relational expression. This changes which value the private-field check receives and can change observable behavior.
For example, this input throws a `TypeError`:
```js
class C {
#x;
test(a, b) {
return #x in (a instanceof b);
}
}
new C().test({}, Object);
```
`a instanceof b` evaluates to `true`, but a private-field check requires an object on its right-hand side.
The JavaScript printer previously emitted:
```js
return #x in a instanceof b;
```
Relational operators associate to the left, so that output is parsed as `(#x in a) instanceof b`. The private-field check on `{}` produces `false`, and `false instanceof Object` returns `false` instead of throwing.
The printer now preserves the original grouping:
```js
return #x in (a instanceof b);
```
`printPrivateInExpression` now prints its right operand with `PREC_COMPARE` instead of `PREC_EQUALS`. The expression printers wrap operands whose precedence is equal to or lower than the requested precedence, so the comparison threshold retains parentheses around `<`, `<=`, `>`, `>=`, `instanceof`, and nested private-in expressions. The now-unused `PREC_EQUALS` import is removed.
Equality and lower-precedence operands continue to retain their required grouping. Shift, arithmetic, and exponentiation operands can still omit unnecessary parentheses. The existing `CTX_FORBID_IN` handling remains in place.
This applies to both ordinary and source-map-enabled printer builds and brings the JavaScript implementation in line with the Rust fix in #26411. It addresses grouping of the private-in right operand, separately from the left-operand correction in #26403.
`packages/codegen` can turn valid JavaScript into unparseable output when a `yield` expression appears in a classic `for` loop initializer and its argument contains an `in` operator.
For example, this input is valid:
```js
function* g(o) {
for (yield (1 in o); false;);
}
```
The JavaScript printer previously dropped the required grouping:
```js
function* g(o) {
for (yield 1 in o; false;);
}
```
An unparenthesized `in` is not allowed at this position in a classic `for` initializer. The same problem affects `yield*`, nested yields, and assignments inside the yield argument.
The printer now preserves the parentheses:
```js
function* g(o) {
for (yield (1 in o); false;);
}
```
The `for` printer already marks its initializer with `CTX_FORBID_IN`, but `printYieldExpression` discarded that context and printed its argument with `CTX_NONE`. The yield dispatch now passes its incoming context to `printYieldExpression`, which forwards only `CTX_FORBID_IN` to the argument when the yield expression is unwrapped. The argument retains its existing `PREC_YIELD` precedence.
When precedence requires parentheses around the entire yield expression, those parentheses already permit `in` inside. The argument context is cleared in that case, avoiding redundant inner parentheses:
```js
// Input
function* g(o) {
for ((yield (1 in o)) + 1; false;);
}
// Output
function* g(o) {
for ((yield 1 in o) + 1; false;);
}
```
Both `yield` and `yield*` use this path. Outside a restricted initializer, ordinary yields continue to omit unnecessary grouping: `yield (1 in o)` prints as `yield 1 in o`.
The correction applies to both ordinary and source-map-enabled builds and brings the JavaScript printer in line with the Rust fix in #26413.
Oxc codegen npm package emits invalid JavaScript when a quoted imported name matches its local binding. The printer compares the decoded names and omits the alias, but leaves the imported name quoted. A quoted import name requires an explicit `as` binding.
For this valid input:
```js
import { "foo" as foo } from "m";
```
The JavaScript printer previously emitted:
```js
import { "foo" } from "m";
```
It now prints the matching local binding as identifier shorthand:
```js
import { foo } from "m";
```
`printImportDeclaration` checks for a string-literal imported name whose decoded value equals the local binding name before printing the imported name. In that case, it prints the local identifier directly, including its source-map mapping. The local binding already supplies a valid identifier, so this requires neither a separate identifier-validity check nor an AST mutation.
Comparing the decoded value also handles escaped and Unicode import names:
```js
// Input
import { "a\u0062" as ab } from "m";
import { "π" as π } from "m";
// Output
import { ab } from "m";
import { π } from "m";
```
Declaration-level and inline TypeScript type imports use the same shorthand:
```ts
// Input
import type { "Foo" as Foo } from "m";
import { type "Bar" as Bar } from "m";
// Output
import type { Foo } from "m";
import { type Bar } from "m";
```
Quoted names that differ from their local bindings retain their quotes and aliases, including `"foo-bar" as foo`, `"" as foo`, and `"default" as foo`. Ordinary identifier imports keep their existing shorthand behavior. The correction applies to both the ordinary and source-map-enabled builds and brings the JavaScript printer in line with the Rust codegen fix in #26386.
The codegen npm package drops required parentheses when a private-in expression is the left operand of a higher-precedence binary operator. The generated JavaScript can evaluate a different expression and throw a runtime error.
For example, this input returns `2`:
```js
class C {
#x;
test(o) {
return (#x in o) + 1;
}
}
console.log(new C().test(new C()));
```
The JavaScript printer previously emitted the return expression as:
```js
return #x in o + 1;
```
Because `+` binds more tightly than `in`, that output is parsed as `#x in (o + 1)`. Adding `1` to the object produces a primitive, so the private-field check throws a `TypeError` instead of producing the boolean used by the original addition.
The printer now preserves the grouping:
```js
return (#x in o) + 1;
```
The iterative binary-expression printer already computes the precedence required for each left operand. Its private-in special case discarded that value and passed `PREC_LOWEST` to `printPrivateInExpression`. Passing `v.leftPrecedence` instead lets the existing private-in printer insert the required parentheses.
This preserves grouping for arithmetic, exponentiation, and shift operators. Equal- and lower-precedence operators, such as `<`, `===`, and `&&`, continue to omit unnecessary parentheses. The correction applies to both the ordinary and source-map-enabled JavaScript printer builds and brings this path in line with the Rust codegen fix in #26383.
`packages/codegen` emits invalid TypeScript for abstract accessors with an explicit accessibility modifier. The JavaScript printer writes `abstract` before `public` or `protected`, but TypeScript requires accessibility to come first and reports TS1029 for the generated declaration.
For example, this valid input:
```ts
abstract class C {
public abstract accessor x: number;
protected abstract accessor y: string;
}
```
was printed as:
```ts
abstract class C {
abstract public accessor x: number;
abstract protected accessor y: string;
}
```
The printer now preserves the valid modifier order:
```ts
abstract class C {
public abstract accessor x: number;
protected abstract accessor y: string;
}
```
`printAccessorProperty` now emits accessibility before `abstract`, matching the neighboring method and property printers. The same ordering also handles declarations such as `protected abstract override accessor x: number` and applies to both the ordinary and source-map-enabled builds.
This brings the JavaScript printer in line with the Rust codegen correction in #26392. The change is confined to accessor modifier ordering; subsequent `static`, `override`, and `accessor` emission and source-map anchoring retain their existing behavior.
Continuation of #26108.
Previously we built the whole output as a single rope, and then flattened it in one go at the end. This is fine for smaller files, but for larger files it has 2 problems:
1. Each `output += segment` append allocates a 32-byte cons string cell. When printing a large AST, so many of these are allocated that they can fill "new space" and trigger garbage collection. When that happens, all these little cons strings have to be copied into the other half of new space, which is slow. Even worse, if they survive 2 rounds of GC, they get promoted into "old space" where (a) they may get scattered across memory, ruining cache coherence, and (b) they sit there, taking up memory, until the next major GC round.
2. Flattening a rope string which contains any segments which are 2-byte strings (non-Latin1 characters) is way more expensive than flattening a rope which is pure Latin1/ASCII. If a single segment contains Unicode, the entire flatten operation deopts and becomes several times slower.
This PR attacks both these problems by flattening a large output in chunks.
- When output reaches 16 KiB, flatten that as a chunk and store it in an array.
- This leaves all the little cons string cells unreferenced, so garbage collection will discard them all, instead of copying them across to keep them alive.
- If file contains some non-Latin1 characters, that does still heavily impact the performance of flattening the chunk they're in, but only that chunk - other chunks which are pure ASCII still take the fast path.
The 16 KiB limit is a heuristic, chosen after experimentation with different values.
### Benchmarks
The effect of this change is minimal on small files, but dramatic on large ones (negative is faster):
| Fixture | Bytes | Change |
|:---| ---:| ---:|
| `tiny.js` | 26 | \+3.2% |
| `RadixUIAdoptionSection.jsx` | 2,424 | \-0.1% |
| `react.development.js` | 50,496 | \+2.0% |
| `binder.ts` | 126,212 | \-1.9% |
| `lodash.js` | 182,262 | \-32.5% |
| `App.tsx` | 298,130 | \-26.4% |
| `kitchen-sink.tsx` | 662,560 | \-14.2% |
| `antd.js` | 5,100,647 | \-28.4% |
### Total cost of flattening
After this PR, compared to where we were prior to #26106 (positive is slower):
| Fixture | Bytes | Change |
|:---| ---:| ---:|
| `tiny.js` | 26 | \+51.5% |
| `RadixUIAdoptionSection.jsx` | 2,424 | \+134.2% |
| `react.development.js` | 50,496 | \+35.5% |
| `binder.ts` | 126,212 | \+39.0% |
| `lodash.js` | 182,262 | \+45.3% |
| `App.tsx` | 298,130 | \+55.0% |
| `kitchen-sink.tsx` | 662,560 | \+51.3% |
| `antd.js` | 5,100,647 | \+2.2% |
The effect of flattening is still very negative, but (as mentioned in #26106) it was always a cost, just one that we were ignoring. At least we've now managed to mitigate it.
In the case of really large files (`antd.js`), benchmark is pretty much unchanged from before flattening came into the picture. But now our measure _includes_ the large flattening cost which was previously _on top of_ the cost of printing - so the real cost for the user is much reduced from where it was previously.
### Sourcemap builds
Printing with sourcemaps on large files actually gets faster after this change. It was already paying the cost of string flattening, now it pays less. Printing `lodash.js` with sourcemaps is about 15% faster now.
As noted in #26106, user code will end up flattening the output of `printSync`, and it's slow.
Since that cost is inevitable, we might as well make flattening the string part of `printSync`s responsibility, and make sure the cost is as low as possible.
V8 has 2 different methods of flattening a string. One of them is optimized for for exactly the shape of string that codegen produces - a rope which has been generated by repeated `output += segment` appends. The other (which would be more likely to be triggered by user code) is about 2x slower for ropes of this shape.
So here we use the faster one, which we trigger with `output.indexOf(" ")`. Except in the weirdest of weird cases, a space will appear early in the output, so `indexOf` won't have to search far. Even if it _does_ have to search the whole string, that's still faster than the slower flatten path.
The change to benchmarks made in #26106 flattened the output string with `output.charCodeAt(0)` which is the slower flatten path. In comparison, using the faster flatten, has a large effect (smaller is faster):
| Fixture | Bytes | Change |
|:---| ---:| ---:|
| `tiny.js` | 26 | \-23.5% |
| `RadixUIAdoptionSection.jsx` | 2,424 | \-41.8% |
| `react.development.js` | 50,496 | \-24.2% |
| `binder.ts` | 126,212 | \-23.4% |
| `lodash.js` | 182,262 | \-17.6% |
| `App.tsx` | 298,130 | \-14.9% |
| `kitchen-sink.tsx` | 662,560 | \-15.6% |
| `antd.js` | 5,100,647 | \-16.8% |
Codegen pre-prepares strings for writing indentation and caches them.
This PR ensure that these strings are properly flattened. They already were - `indent.charCodeAt(0)` does that. But that's only the case as long as `growIndents` doesn't get tiered up to TurboFan. If it does, TurboFan treats `indent.charCodeAt(0)` as a no-op, and removes it as dead code, which removes the flattening.
`growIndents` is a cold path, so it's unlikely to get tiered up, but make sure that even if it does, we don't lose the flattening by using `indent = [indent, indentString].join("");` instead.
No effect on benchmarks, because TurboFan wasn't kicking in here.
Perf optimization to sourcemap builds.
Use an `Int32Array` to store sourcemap positions, instead of a plain `Array`.
This `Int32Array` is held in a top-level var and reused for every file. It is replaced by a larger one when required, but will soon reach steady state - a size large enough to service all files being printed - and then ceases growing. Once it reaches that steady state, all the "does it need to grow?" branches become perfectly predictable.
`Int32Array` has 3 advantages over plain arrays:
1. 4 bytes per entry, instead of 8 - lower memory usage and less cache misses.
2. Low garbage collection cost - during garbage collection, GC has to loop through every item in an `Array` checking if it contains heap values (e.g. an `Object`) which the array references, and therefore can't be garbage-collected. `Int32Array`s are statically known to only contain small integers, which don't live on the heap, so this scan is skipped entirely.
3. Recycling - due to not having to worry about GC burden of keeping a typed array live across a GC run, a single `Int32Array` can be kept live and reused over and over for each file.
This change produces a decent perf bump:
| Fixture | Maps end-to-end | Recording pass only |
| --- | --- | --- |
| tiny.js | −7.3% | −34.5% |
| radix | −5.4% | −10.5% |
| react | −2.9% | −6.6% |
| App.tsx | −2.4% | −6.2% |
| binder.ts | −3.3% | −5.4% |
| kitchen-sink | −2.9% | −4.9% |
| lodash | −2.1% | −5.8% |
| antd | −5.1% | −5.3% |
### Why `Int32Array` not `Uint32Array`?
V8's native small integer type ("SMI") is a _signed_ 32-bit integer. Generally speaking, it's preferable to store numbers in this same format, as when reading from an `Int32Array`, V8 statically knows that the value is an SMI, and skips conversion logic and "does this number fit in an SMI?" checks. Ditto when writing - no conversion required to write a known SMI into an `Int32Array`.
Pure refactor. Move the category consts (`CAT_IDENT` etc) into a separate file.
2 reasons:
1. `write.ts` had got too large.
2. `index.ts`, which is not part of printer builds, imports `CAT_OTHER`. Importing it from a file containing only consts avoids any problems with minifier not being able to recognise dead code and remove it.
Optimization to sourcemap builds. This is what the rest of this stack has been working towards.
All the `writeWithMap*` and `markMap*` functions received a `node` param which they're extract `start` and `end` from. The problem is that these functions receive a range of different node types, so accessing `node.start` / `node.end` are megamorphic, involving an expensive dispatch.
Instead, extract `start` and `end` at call sites, where the type of `node` is already known (monomorphic), and pass them in to `writeWithMap*` / `markMap*` functions.
These functions still keep their `node` param, as it's needed for debug asserts, but it's never touched in release builds. #25989 removes it entirely from release builds too.
This has quite a large effect on perf. The cumulative perf gain of this stack (#25968 onwards) in the "without generation" sourcemaps benchmark is:
<table>
<tr>
<th align="left">Fixture</th>
<th align="right">Bytes</th>
<th align="right">Before</th>
<th align="right">After</th>
<th align="right">Faster by</th>
</tr>
<tr>
<td align="left">
`RadixUIAdoptionSection.jsx`
</td>
<td align="right">2,518</td>
<td align="right">0.0077 ms</td>
<td align="right">0.0059 ms</td>
<td align="right">+31.5%</td>
</tr>
<tr>
<td align="left">
`react.development.js`
</td>
<td align="right">72,141</td>
<td align="right">0.2549 ms</td>
<td align="right">0.2015 ms</td>
<td align="right">+26.9%</td>
</tr>
<tr>
<td align="left">
`lodash.js`
</td>
<td align="right">544,096</td>
<td align="right">1.14 ms</td>
<td align="right">0.9289 ms</td>
<td align="right">+23.5%</td>
</tr>
<tr>
<td align="left">
`App.tsx`
</td>
<td align="right">415,340</td>
<td align="right">1.84 ms</td>
<td align="right">1.51 ms</td>
<td align="right">+21.2%</td>
</tr>
<tr>
<td align="left">
`binder.ts`
</td>
<td align="right">193,077</td>
<td align="right">0.6598 ms</td>
<td align="right">0.5483 ms</td>
<td align="right">+20.6%</td>
</tr>
<tr>
<td align="left">
`kitchen-sink.tsx`
</td>
<td align="right">732,222</td>
<td align="right">5.12 ms</td>
<td align="right">4.33 ms</td>
<td align="right">+17.7%</td>
</tr>
<tr>
<td align="left">
`antd.js`
</td>
<td align="right">6,683,633</td>
<td align="right">23.52 ms</td>
<td align="right">20.30 ms</td>
<td align="right">+15.7%</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
The vast majority of that gain is down to this PR, and the ones preceding it which also remove polymorphic/megamorphic accesses.
---
Note: Actually this change is unnecessary for `writeWithMapNamed`, `writeWithMapNamedPrivate`, and `writeWithMapNamedJSXNoLast` - they only receive a single type of node so are already monomorphic. But it's easier to be consistent, and this uniformity enables the further optimization in #25989. We could pare this back in a later PR, so we only pass in `start` and `end` where it's beneficial to do so.
Optimization to sourcemap and no-sourcemap builds.
#25983 introduced a `writeIdent` function. Use it everywhere which was previously calling `write` with `CAT_IDENT`. This has the same advantages as #25983 - avoids using a register just to pass a static value, and makes setting `last` cheaper in this hot path.
Optimization to sourcemap and no-sourcemap builds.
`writeWithMapNamed` now always receives an `Identifier`, so all call sites pass `last` as `CAT_IDENT`. Remove the param and just write `state.last = CAT_IDENT` directly. This avoids using a register to pass a value which is always the same, and makes setting `last` cheaper too.
Add a new `writeIdent` function which `writeWithMapNamed` is converted to in no-sourcemap builds, which also doesn't have a `last` param.
Pure refactor. After #25981, `writeWithMapNamed` and `writeWithMapNamedNoLast` only receive `Identifier`s, so rename `code` param to the more specific `name`.
Continuation of #25980.
Add a specialized function `writeWithMapNamedPrivate` for writing a `PrivateIdentifier`. It feeds `hashLength: 1` to `markMapNamed`, which allows removing polymoprhic `node.type` access from `markMapNamed`.
The code being printed in paths that lead to `markMapNamed` is always the same as `node.name`, so just pass it in to `markMapNamed`, which removes a polymorphic `node.name` read there.
Additionally, `writeWithMapNamedNoLast` now only receives `Identifier` nodes, so becomes monomorphic.
Writing mappings for JSX identifiers requires different logic than for normal `Identifiers`.
- Add specialized function `writeWithMapNamedJSXNoLast` for writing `JSXIdentifier`s.
- Move the logic for getting original name of a `JSXIdentifier` into a separate function.
- Branch on an `isJSXIdentifier` param passed in to `markMapNamed`, to decide which function to use to get original identifier name.
The main motivation is to avoid having to pass `node` into `originalNameFromSource`, where reading `node.type` is a polymorphic access (because `node` could be an `Identifier`, an `PrivateIdentifier` or `JSXIdentifier`). This is part of a broader move to remove the reliance of all `write` and `markMap` functions on receiving polymorphic/megamorphic `node` params (later PRs in this stack).
Previously `mapNames` property of state was lazily initialized - it started as `null`, and an array is only created when a named mapping is written.
Often lazy initialization is a good strategy, but in this case IMO it's a poor trade-off - it adds a check for every named mapping that's written in return for saving a single allocation per file, and only in files which produce a source map with no names. Allocating 1 empty array is very cheap.
Optimization to sourcemap builds.
Previously one `recordSourceMapping` function handled all types of mappings, and branched on `location` to decide how to behave.
Instead, break it into separate `markMapStart`, `markMapEnd`, `markMapAfter`, `markMapAtStartOffset`, and `markMapNamed` functions which each select the right point to make the mapping. The main functionality that was in `recordSourceMapping` is moved into 2 helper functions `hasMappableSpan` and `recordMapping`.
This removes branches, and removes the heavy code for named mappings from all the paths which don't produce named mappings. `markMapStart` and the rest become minimal and are more likely to be inlined.
Small optimization to sourcemap builds.
Add functions for writing with a source mapping, where it's known from what's being written that the mapping cannot need a name, because `node.name` is never a string, and change all call sites to use the appropriate function.
This allows removing 2 checks from ``recordSourceMapping``:
- `typeof node.name === "string"` - because that's now statically known.
- `printedName.length > 0` - because identifiers cannot be length.
All `write` and `markMap` functions take a `NamedMappableNode` or `UnnamedMappableNode`, which makes it a type error to call the wrong function.
`markMapNamed` becomes dead code, and is removed.
Pure refactor. Just rename functions:
- `markWithMap` -> `markMap` for brevity.
- All functions which can produce a named mapping called `*Named`.
This mainly prepares the way for next PR #25975, which introduces separate `write` functions for where a mapping can never be named.
Small optimization to sourcemap builds.
Merge a `markWithMapAtStartOffset` call with offset 0 with a `write` call immediately following it into a single `writeWithMap` call. This is functionally equivalent.
Alter how functions where are not used in no-sourcemap builds get removed from the build.
Previously, the plugin explicitly replaced calls to these functions with `void 0`. Instead:
1\. Add `if (SOURCEMAPS)` guards to these functions, so minifier sees that they're no-ops and removes the calls.
2\. Remove arguments from their call sites, so nothing gets left over by minifier.
Additionally, remove the params from the function declarations themselves.
The table in plugin specifies how many arguments to remove for each function.
These change does not alter the output code - the same stuff gets removed, just by a different mechanism. But this change enables the next PR #25971.
Small perf optimization and code simplification.
`printNonNegativeFloat` previously passed the `node` parameter through a long chain of helper functions, so that they could call `writeWithMap` with `node` on their first write.
Instead create the sourcemapping at the start of the chain, so there's no need to pass `node` down and helpers can just use `write` / `writeLast`.
Only changes comments.
- Fix some inaccuracies in JSDoc comments on `write*` and `mark*` functions.
- Add missing `@param`s to JSDoc comments.
- Expand some comments.
Follow-on after #25957. Not sure how this crazily-formatted comment snuck by me. Also clarify the wording - what's this weird obsession with the word "spelling" Mr Claude? Speak English bruv!
A `PrivateIdentifier`'s span includes the leading `#`, but the `name` the AST holds does not.
The source map name was read from the whole span and then compared to `name` from AST, so the check which omits the mapping's `name` when it wasn't renamed compared `#foo` against `foo`. It never matched, so a named mapping was created for _every_ private identifier, renamed or not.
This PR fixes that in both Rust `oxc_codegen` and JS `oxc-codegen`, by comparing correctly including the `#`.
This also makes mapping names correct when a normal `Identifier` has been replaced with a `PrivateIdentifier` (`this.foo` -> `this.#foo)`, or vice-versa (`this.#foo` -> `this.foo`). New tests cover both directions.
Pure refactor.
`recordSourceMapping`'s logic for recording named mappings was convoluted, and was hard to follow. Refactor it to clarify what's going on, and add comments explaining it.
In the process, it became apparent there were unnecessary branches and operations:
* `name = printedName;` always set `name` to `undefined`, which it was already.
* `if (name !== undefined) { ... }` block was present in multiple code paths where `name` is always `undefined`.
This PR removes those unnecessary code paths, so may have a small perf benefit, but main motivation is greater clarity.
`TSPropertySignature` and `TSMethodSignature` were printing a `PrivateIdentifier` key as `key.name`, dropping the `#`, so `interface I { #x: number }` was printed as `interface I { x: number }`.
This PR switches `oxc_codegen` to delegating to the `PrivateIdentifier` printer, as every other property key site already does. It also applies the same thing to JS printer `oxc-codegen`.
This is not legal syntax anyway - TS rejects it with TS18016, and Oxc parser with "Private identifier '#x' is not allowed in property names" - so this is only reachable from a recovered parse or a hand-built AST. But, since we have this code, it might as well do what you'd expect it to.
Follow-on after #25585.
Previously `printExpression` checked whether last character written was a `)` or `]` by consulting `lastWasPostfixClose`. Every write kept `lastWasPostfixClose` up to date by reading the last character that was written (`updatePostfixClose`).
It's statically known everywhere that writes a `)` or `]`, so instead add a category `CAT_CLOSE_BRACKET`, and write that as `state.last` using the existing machinery. `printExpression` then does its check with `state.last === CAT_CLOSE_BRACKET`.
This has a large effect on perf because checking the last character that was written in `updatePostfixClose` was expensive and on a hot path.
Benchmark without sourcemap generation:
| Fixture | Bytes | Before | After | Faster by |
|:---| ---:| ---:| ---:| ---:|
| `RadixUIAdoptionSection.jsx` | 2,518 | 0\.01221ms | 0\.00716ms | \+41.4% |
| `react.development.js` | 72,141 | 0\.38326ms | 0\.25377ms | \+33.8% |
| `binder.ts` | 193,077 | 0\.6824ms | 0\.6458ms | \+5.4% |
| `App.tsx` | 415,340 | 1\.8782ms | 1\.7846ms | \+5.0% |
| `lodash.js` | 544,096 | 1\.6802ms | 1\.0902ms | \+35.1% |
| `kitchen-sink.tsx` | 732,222 | 5\.2654ms | 4\.7936ms | \+9.0% |
| `antd.js` | 6,683,633 | 32\.0742ms | 22\.4061ms | \+30.1% |
With sourcemap generation included:
| Fixture | Bytes | Before | After | Faster by |
|:---| ---:| ---:| ---:| ---:|
| `RadixUIAdoptionSection.jsx` | 2,518 | 0\.02126ms | 0\.01616ms | \+24.0% |
| `react.development.js` | 72,141 | 0\.52684ms | 0\.39368ms | \+25.3% |
| `binder.ts` | 193,077 | 1\.0708ms | 1\.0335ms | \+3.5% |
| `App.tsx` | 415,340 | 3\.8221ms | 3\.7476ms | \+2.0% |
| `lodash.js` | 544,096 | 2\.9815ms | 2\.4294ms | \+18.5% |
| `kitchen-sink.tsx` | 732,222 | 9\.8293ms | 9\.5644ms | \+2.7% |
| `antd.js` | 6,683,633 | 54\.3088ms | 44\.3745ms | \+18.3% |
The main reason for the large impact on JS/JSX fixtures is that the JS build gets poisoned by JSX text. Most strings that `updatePostfixClose` sees are interned strings (source literals like `"function "`) but in JSX files it was also seeing dynamic strings (JSX text) which turned it polymorphic. Once it's gone polymorphic, it never goes back, affecting all following JS fixtures (which use the JS build).
Removing the string accesses removes this cost.
## Summary
- replace Node.js `Buffer` storage with `Uint8Array` in the source-map encoder
- use `TextDecoder` to create the final mappings string
- use `Uint8Array#set` when growing the mappings buffer
- remove the `node:buffer` runtime dependency
This makes source-map generation runtime-neutral and removes a blocker for browser support for the codegen package.
## Performance
Indexed access, copying, and ASCII decoding have comparable performance. `Uint8Array` allocation requires zero-initialization, unlike `Buffer.allocUnsafe`, but the expected end-to-end impact is small.
## Summary
Replace the `Buffer`-backed base64 character-code table with a `Uint8Array`.
`BASE64_CODES` is only used as a lookup table mapping VLQ digits from `0–63` to ASCII byte values. It does not require any `Buffer`-specific APIs, and `Uint8Array` provides the same indexed byte semantics in both Node.js and browsers.
This reduces unnecessary Node-specific usage and is a small step toward making `oxc-codegen` browser-compatible. The hot encoding loop remains unchanged; only the one-time lookup-table initialization differs.
This does not remove the remaining `Buffer` dependency used for the source-map output buffer.
Follow-on after #25585. Small optimization.
Sourcemaps can't be generated unless `options.sourceText` is provided. This is already checked at the start. So everywhere else can then rely on `state.sourceText != null`, rather than re-checking it on each write.
Follow-on after #25585. Small optimization.
Simplify `growMappingBuffer` by relying on existing invariants. Buffer always has at least 64 bytes capacity, and no caller needs to reserve more than 64 bytes. So doubling capacity is always sufficient.
Add a debug assert to ensure this invariant continues to hold.
Follow-on after #25585. Small optimization.
`location` passed to `recordSourceMapping` was either `false` or a small number. Swap `false` for `0`, so its always passed a number (SMI), removing the need for type-checks.
Follow-on after #25585. Small optimization.
`name` can only be `!== undefined` when `location === false`. So move that logic into that block, removing a branch from the fast path.
Follow-on after #25585. Small optimization.
No point setting `last` here, since in both cases its immediately overwritten before anything else can read it.
Follow-on after #25585. Small optimization.
It's redundant to call `markWithMapNoName` with `decorators[0]` as `printDecorators` marks the same mapping immediately after.
Add benchmarks for source maps in `oxc-codegen`.
2 benchmarks:
1. Full end-to-end.
2. Without the actual sourcemap generation at the end.
The 2nd is to measure the overhead that pushing to the sourcemap arrays adds while printing in isolation.