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