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.
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).
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.
## Summary
Add direct public-API tests for `printSync(statement)`:
- JavaScript/TypeScript statement output
- source-map metadata for a JavaScript statement
## Summary
- replace manual test-registration loops with Vitest parameterized APIs
- preserve per-fixture skip behavior with `.for` and parameterized suite options
- retain imperative loops that exercise fixture contents rather than register tests
Follow-on after [#25585](/github/pr-or-issue/oxc-project/oxc/25585).
Rename `options.sourceFileName` to `sourceFilename`. We use both capitalizations internal Rust code, but always `filename` in external JS APIs (Oxlint plugins etc).
NOTE: this is not marked as a breaking change as it was merged before the feature was released.
Follow-on after #25585.
Nit. Make `printSync` return an object with consistent shape. With `sourcemap: false` it now returns `{ code: string, map: null }` instead of just `{ code: string }`.
Co-authored-by: Cameron <cameron.clark@hey.com>
Follow-on after #25585.
Pure style change. Similar to #25848 - just remove the worst excesses of LLM coding style where it produces dense blocks of code with no line breaks, which makes code hard to read.
No substantive changes, just adds line breaks.
Follow-on after #25585.
Pure style change. I hate the way LLMs put line breaks in weird places like after "the" - it makes comments hard to read. And what's with all the colons and semicolons?
No substantive changes, just reformatting comments and docs to avoid this unreadable formatting.
## Summary
- throw a TypeError when indent is empty, contains characters other than spaces or tabs, or is not a string
- preserve the existing default tab indentation and unchanged printIndent hot path
- update API documentation and validation coverage
`oxc-codegen`'s current API requires passing in a `SourceMapGenerator` as an option to generate source maps. This is currently really slow, and I imagine we'll want to generate source maps internally (perhaps in Rust), and return a source map string from `printSync` instead.
Alter `printSync`'s return type now, to support that without a breaking change later.
- Before: `function printSync(node: ESTree.Node, options?: Options): string`
- After: `function printSync(node: ESTree.Node, options?: Options): { code: string }`
Later on, it can return `{ code: string, map: string }` and that won't break existing users.
## Summary
- Detect actual case-insensitive `</script` sequences in the initial string-escape scan.
- Keep strings containing only harmless `<` on the bulk-write fast path.
- Add regression coverage for script closers, controls, paired and lone surrogates, and template quasis.
## Benchmark methodology
No permanent microbenchmark is added. Measurements used release bundles built with `pnpm --filter oxc-codegen run build` on Node v26.5.0, a one-off focused harness over representative string-literal ASTs, and the existing `packages/codegen/bench.bench.ts` parsed-AST fixtures. The focused harness used ten warmup batches and 20 measured batches per case; Vitest fixtures used 1s warmup and 3s measurement windows (50/20 iteration minima).
The removed one-off harness is retained for reference at [bench.string.bench.ts](https://github.com/oxc-project/oxc/blob/49e7a9d43bdc55a8f01075363bb8701ca5a9380c/packages/codegen/bench.string.bench.ts).
Focused release results (ms/print, mean ± standard deviation):
| Case | Before | After | Change |
| --- | ---: | ---: | ---: |
| Short ordinary string | 0.00003 ± 0.00000 | 0.00003 ± 0.00000 | no material change |
| Long string without `<` | 0.02048 ± 0.00114 | 0.01906 ± 0.00038 | within noise / slightly faster |
| Dense harmless HTML/SVG-like `<` (15,360 chars) | 0.05625 ± 0.00152 | 0.01283 ± 0.00036 | 77% faster |
| Mixed text with harmless `<` | 0.04766 ± 0.00210 | 0.02371 ± 0.00072 | 50% faster |
| Frequent mixed-case `</script` closers | 0.04350 ± 0.00074 | 0.04366 ± 0.00079 | within noise |
Existing parsed-AST benchmark fixtures (Vitest RME 0.15–0.79%): Radix 0.0028 -> 0.0027 ms, React development 0.0981 -> 0.0976 ms. App.tsx varied from 0.6261 to 0.6657 ms after versus 0.6415 ms before, so this PR makes no throughput claim for that fixture and shows no consistent regression.
A release CPU profile of the dense harmless case reduced `printEscapedStringContents` from 1,099 of 1,459 samples, plus 124 `^</script` regex samples, to zero; the after profile spends 321 of 383 samples in the single initial scan.
## Summary
Validate `startingIndentLevel` before printing to prevent unbounded indentation-cache growth.
- Reject non-finite, fractional, negative, and unsafe integer values with a `RangeError`.
- Document the option as a non-negative integer.
Add `oxc-codegen` package - a printer written in TS.
### Features
- Prints JS, JSX, TS, TSX.
- Matches `oxc_codegen` exactly on all Test262, TypeScript, Acorn-JSX fixtures.
- Very fast.
### Deficiencies
- Pretty-printing only (no compact/minified output).
- No support for printing comments.
- Source map support is semi-there, but largely untested, and poor API (design doc has some ideas on how to improve it).
### This PR adds
- The printer code.
- Conformance testing infra.
- Further tests for edge cases.
- README.
- Design doc - how it differs from `oxc_codegen` and why, what produces the perf.
### This PR doesn't add
- Not integrated into CI or release pipeline.
### Notes
This started as a slop port of `oxc_codegen`. I've iterated on that base, cleaned it up a lot, and at least skimmed through all of it. I think it's in fairly decent shape.
The design doc is I think worth a read. It explains the rationale for the choices, in particular the "1 codebase, 4 builds" approach.
I've put it in a new top-level directory called `packages`. It's not an app, so didn't seem to me to fit alongside `apps/oxlint` and `apps/oxfmt`. It's pure JS, so not like e.g. `napi/parser`. And it felt too heavyweight to put into `npm` dir. But maybe there's a better place for it.
Co-authored-by: Cameron <cameron.clark@hey.com>