Skip to content

build: Run the build steps in one process - #131

Merged
BYK merged 1 commit into
mainfrom
refactor/build-in-process
Nov 5, 2026
Merged

BYK merged 1 commit into
mainfrom
refactor/build-in-process

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to this review comment on #130. It's the same refactor as getsentry/toolkit#1939, which made the CLI's generators run in one process.

Before this PR, build.mjs ran each of the 12 scripts in scripts/ as its own node process with execSync. Six of them loaded TypeScript and five parsed the 6 MB spec, each one starting over. Several also had their own copies of the same patch helpers.

What changes

  • One process: each script exports emit() and does no work at import time. build.mjs lists the steps in their old order, imports each one and runs its emit.
  • Specs parsed once: normalizeSpec now also returns the spec it writes. build.mjs passes { normalizedSpec, sourceSpec } to every step, so no step reads a spec from disk.
    • Steps that used to fall back to openapi-derefed.json when openapi-normalized.json was missing don't need to anymore; that fallback was only for running a script on its own.
    • No step changes either spec.
  • Shared helpers: the new lib/generated-source.mjs has replaceOnce, update, parse, edit and srcPath. The scripts import them instead of keeping their own copies.
    • replaceOnce is the stricter version from generate-response-contract.mjs: it takes a string or a RegExp and reports how many matches it found.
    • make-tree-shakeable.mjs now uses it instead of its own loop.

Most of each script's diff is the new indentation from wrapping its body in emit; hide whitespace to see the actual changes. I didn't indent lines inside template literals, so the generated text didn't change.

Verification

  • With a clean pnpm build, every file in src/ and dist/ is byte-identical to main's (diff -r).
  • pnpm typecheck passes, and so do all 632 tests in pnpm test.
  • The step phase (from the first patch through make-tree-shakeable) took 19.1 s and 19.5 s on main, and 16.1 s and 13.5 s on this branch, in one sandbox. The whole build is about 54 s, and Hey API code generation and tsc --emitDeclarationOnly still take most of it.

via @BYK.

--

View Session in Sentry

build.mjs ran each script in scripts/ as its own `node` process, and each
one loaded TypeScript, parsed the 6 MB spec, or both again. Each script
now exports `emit()` and does no work at import time; build.mjs imports
and runs them in order and passes in the specs it already parsed. The
scripts share lib/generated-source.mjs instead of their own copies of
replaceOnce, update, parse and edit.

The generated src/ and dist/ are byte-identical to main's.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also see if we can benefit from any parallelization or optimizations in Hey API code

@BYK
BYK merged commit f8a0095 into main Nov 5, 2026
11 checks passed
@BYK
BYK deleted the refactor/build-in-process branch November 5, 2026 13:11
@sentry-junior

sentry-junior Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Followed up on the parallelization ask in #132; the build drops from about 48 s to about 25 s.

  • Biggest win: edit in lib/generated-source.mjs rebuilt the 2.6 MB valibot.gen.ts once per edit, about 6,500 times, which took 13 s. It now builds the result in one pass.
  • Declarations: tsc --emitDeclarationOnly took 30 s. It now runs as three programs in worker threads (Valibot, Zod, and the rest) and takes about 17 s.
  • Hey API: it took 5.4 s. The client, Valibot and Zod plugin groups now run in worker threads, and their files are byte-identical to the combined run. It now takes 4.0 s.

src/ is byte-identical to main's. In dist/, the only changes are the order of union members in the two validator .d.ts files.

BYK added a commit that referenced this pull request Nov 5, 2026
Follow-up to your approval comment on #131 ("see if we can benefit from
any parallelization or optimizations in Hey API code"). This cuts `pnpm
build` from about 48 s to about 25 s in my sandbox (4 vCPUs).

## Where the time went (on `main`)

| Phase | Before | After |
| --- | --- | --- |
| Hey API `createClient` | 5.4 s | 4.0 s |
| Build steps in `scripts/` | 14.6 s | 1.5 s |
| esbuild bundles + `tsc --emitDeclarationOnly` | 30.4 s | 17.4 s |

## What changes

- **`edit` is linear:** `describe-valibot-schemas` spent 13 s of the
14.6 s step phase in `lib/generated-source.mjs`'s `edit`. It sliced the
whole 2.6 MB `valibot.gen.ts` once per edit, about 6,500 times. `edit`
now builds the result in one pass.
  - It keeps the old order for insertions at the same position.
- It now throws on overlapping edits, which the doc comment already
ruled out.
  - New `test/generated-source.test.ts` covers both.
- **Declarations in parallel:** `tsc --emitDeclarationOnly` spent 25 of
its 30 s checking, mostly on the Valibot and Zod schemas. Neither of
those files imports anything else from `src/`.
- New `lib/emit-declarations.mjs` reads `tsconfig.json` and builds three
programs in worker threads: Valibot, Zod, and everything else.
- Each worker reports its diagnostics and writes its declarations, and a
type error still fails the build. I checked that with a file that has a
deliberate error.
- The workers run alongside the esbuild bundles. Valibot is now the
slowest step, at about 15 s.
- **Hey API in parallel:** the Valibot and Zod plugins read nothing the
TypeScript and SDK plugins write. New `lib/hey-api.mjs` runs the three
plugin groups in worker threads.
- Each group produces byte-identical files to the combined run; I
compared them file by file.
- Hey API empties its output directory first, so the validator groups
write to a temp directory and their files are copied into `src/`.
- The Hey API config (`describeNullableValues`, `validatorOptions`)
moves from `build.mjs` into this module.

## Output

- `src/` is byte-identical to `main`'s.
- In `dist/`, only `valibot.gen.d.ts` and `zod.gen.d.ts` change, and
only in the order of union members and enum keys. For example, `"GET" |
"HEAD" | "OPTIONS" | …` becomes `"GET" | "POST" | "HEAD" | …`.
TypeScript orders unions by when it first created each literal type, and
that depends on what else is in the program. The types themselves don't
change. Every other `.d.ts` file is byte-identical.

## Cost

Peak memory of the build process goes from about 1.4 GB (plus the
separate `tsc` process, about 1.6 GB) to about 2.3 GB in one process.

## Verification

- `pnpm build` passes.
- `pnpm typecheck` passes, and so do all 635 tests in `pnpm test`.
- With `diff -r` against `main`'s build, `src/` is byte-identical. In
`dist/`, only the two files above differ.

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3Aad43917b182be622bccdd8f7 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3Aad43917b182be622bccdd8f7/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant