Commit Graph

17 Commits

Author SHA1 Message Date
overlookmotel 20711f4995 perf(packages/codegen): remove node param from write and markMap functions in sourcemap builds (#25989)
Optimization to sourcemaps builds.

#25986 made the `node` param passed to `writeWithMap` etc redundant in sourcemap release builds.

Alter the TSDown plugin to remove the param from these functions in sourcemap release builds, and also remove the `node` argument from all call sites.

It was already removed in no-sourcemap builds - this only affects sourcemaps builds.
2026-08-24 09:20:19 +00:00
overlookmotel 6253b2ccc5 refactor(packages/codegen): convert unmap_writes TSDown plugin to a function (#25988)
Pure refactor, in preparation for #25989.

Turn `unmap_writes` TSDown plugin into a factory function. This allows passing it arguments to control the plugin's behavior.

This change split into a separate PR to avoid a massive diff in #25989.
2026-08-24 09:20:18 +00:00
overlookmotel b6c60361d2 refactor(packages/codegen): TSDown plugin strip all debugAssert* calls (#25987)
Refactor in preparation for #25989.

`remove_asserts` TSDown plugin removes debug assertions in release builds, but only when the assertion functions are imported from `asserts.ts`.

We have various other `debugAssert*` functions which are defined elsewhere. Make the plugin remove them too in release builds.

The reason this is needed is that many of these `debugAssert` functions receive `node`. #25989 removes `node` param from functions containing these assertions, which means `node` becomes a global. Minifier has to assume accessing a global may have side-effects and therefore does not remove these debug assertions. We need the plugin to do it.
2026-08-24 09:20:18 +00:00
overlookmotel 29a774930a perf(packages/codegen): pass start and end to write and mark functions (#25986)
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.
2026-08-24 09:20:18 +00:00
overlookmotel a78ed30bbf perf(packages/codegen): remove last param from writeWithMapNamed (#25983)
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.
2026-08-24 09:20:17 +00:00
overlookmotel c1bcac3be0 refactor(packages/codegen): add writeWithMapNamedPrivate (#25981)
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.
2026-08-24 09:20:16 +00:00
overlookmotel c96a69fff8 refactor(packages/codegen): add writeWithMapNamedJSXNoLast (#25980)
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).
2026-08-24 09:05:20 +00:00
overlookmotel 215a6a51ad perf(packages/codegen): add functions for writing/marking unnamed mappings (#25975)
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.
2026-08-24 07:24:06 +00:00
overlookmotel 3216cbb04c refactor(packages/codegen): rename write* and mark* functions (#25973)
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.
2026-08-22 13:24:58 +00:00
overlookmotel 94c8a34376 perf(packages/codegen): remove node param from printString and printNonNegativeFloat (#25971)
Small optimization to no-sourcemap builds.

`printString` and `printNonNegativeFloat` receive `node` as their last param, but in no-sourcemap builds, all the code which uses `node` is removed anyway.

Use the mechanism added in #25970 to remove the `node` param from the function declarations, and the corresponding argument from all call sites. This avoids passing data into these functions pointlessly, freeing a register.
2026-08-22 13:24:57 +00:00
overlookmotel 4c650372cd refactor(packages/codegen): specify in TSDown plugin how many args to remove from call sites (#25970)
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.
2026-08-22 10:23:43 +00:00
overlookmotel a0708f3950 refactor(packages/codegen): simplify TSDown plugin (#25969)
The TSDown `unmap_writes` plugin was a bit sloppy. It was over-engineered, doing multiple passes over the AST, printing it and re-parsing it. Condense this all down to a single AST pass.

In addition:

1\. Check the declarations of listed functions to make sure that `arity` accurately matches the actual declarations.
2\. Rename `plain` to `rename` - that better describes what this property is.

This does not alter the output code.
2026-08-22 10:11:53 +00:00
Boshen 1193c9dbaf chore(oxfmt): dogfood operator position at line start (#25937)
dogfood
2026-08-20 14:36:47 +00:00
camc314 b016583d78 refactor(codegen): use magicString to preserve mappings in strip-ts (#25870)
## Summary

Preserve source maps when `strip-ts` removes TypeScript-only printer regions from JS builds.

The plugin now uses Rolldown’s `magicString` transform context instead of a plain string replacement, so removed `/* IF TS */` regions retain correct mappings back to the original printer source.
2026-08-19 08:24:14 +00:00
overlookmotel 58f7ab9c1d docs(packages/codegen): reformat docs and comments (#25848)
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.
2026-08-18 12:24:56 +00:00
camc314 300763f328 feat(packages/codegen): add full source map support (#25585)
## Summary

Align `oxc-codegen` source maps with Rust `oxc_codegen`, using the Rust implementation as the conformance oracle.

This PR:

- changes `printSync` to return `{ code, map? }`;
- returns a standard Source Map v3 object;
- exposes Rust’s generated source map through `codegen_conformance`;
- compares the complete JS and Rust source maps;
- supports Oxc `start`/`end` offsets and TS-ESTree `range` offsets;
- uses `sourceText` to convert UTF-16 offsets and recover original identifier spellings;
- validates both maps and no-maps builds in both `preserveParens` modes;
- aligns mapping placement across JavaScript, TypeScript, JSX, templates, imports, destructuring, delimiters, and postfix chains;
- handles astral characters and all ECMAScript line terminators.
`loc`-only ASTs are not supported for source maps.

## API change

```ts
const { code, map } = printSync(program, {
  sourcemap: true,
  sourceFileName: "input.js",
  sourceText,
});
```

`map` is a Source Map v3 object:

```ts
interface SourceMap {
  version: 3;
  mappings: string;
  names: string[];
  sources: string[];
  sourcesContent?: string[];
}
```

## Implementation

Mapped writes collect only:

- the generated UTF-16 offset;
- the original source offset;
- the optional original identifier name.

After printing, the generator builds line tables for the generated output and `sourceText`, converts the offsets to line/column positions, suppresses duplicate source positions, and encodes the result as base64 VLQ.

The no-maps builds continue to remove mapping-specific writes at build time.

## Validation flow

```mermaid
flowchart TB
    fixture["Fixture<br/>filename + sourceText + parse options"]

    fixture --> rustParse
    fixture --> jsParse

    subgraph rust["Rust oracle"]
        direction TB
        rustParse["Parse with oxc_parser"]
        normalize["Normalize Rust-only AST information"]
        rustPrint["Print with oxc_codegen<br/>source maps enabled"]
        rustResult["Expected result<br/>{ code, map }"]

        rustParse --> normalize
        normalize --> rustPrint
        rustPrint --> rustResult
    end

    subgraph js["JavaScript candidate"]
        direction TB
        jsParse["Parse ESTree with oxc-parser<br/>start/end or range offsets"]

        plainPrint["Print with no-maps build"]
        plainResult["Actual code"]

        mappedPrint["Print with maps-enabled build"]
        deferred["Record generated offsets,<br/>source offsets, and names"]
        encode["Convert UTF-16 offsets to positions<br/>deduplicate and encode VLQ"]
        mappedResult["Actual result<br/>{ code, map }"]

        jsParse --> plainPrint
        plainPrint --> plainResult

        jsParse --> mappedPrint
        mappedPrint --> deferred
        deferred --> encode
        encode --> mappedResult
    end

    rustParse --> parseGate
    jsParse --> parseGate

    rustResult --> codeGate
    plainResult --> codeGate
    mappedResult --> codeGate

    rustResult --> mapGate
    mappedResult --> mapGate

    parseGate["Parse agreement<br/>Rust success requires no JS errors"]
    codeGate["Code equality<br/>both JS builds equal Rust"]
    mapGate["Source Map v3 equality<br/>mappings + names + sources + content"]

    parseGate --> pass["Fixture passes"]
    codeGate --> pass
    mapGate --> pass
```

Each fixture runs with both `preserveParens: false` and `preserveParens: true`.

## AST normalization

Rust’s AST contains information that ESTree cannot represent. The Rust AST is normalized before printing so the comparison measures code generation rather than representation differences.

This includes:

- parenthesized function and arrow-expression flags;
- `import "m"` versus `import {} from "m"`;
- absent versus empty import-attribute clauses;
- `assert` versus `with` attribute keywords;
- the unavailable ESTree span for a complete `with` clause.
2026-08-18 11:41:34 +00:00
overlookmotel a4478e960a feat(codegen): add oxc-codegen package (#25488)
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>
2026-08-12 13:41:31 +00:00