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% |
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`.
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.
## 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](/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.
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.
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>