--- name: build description: "Configure and run @savvy-web/bundler builds (and its rspress-builder sibling): the build() front door, the BuildConfig option surface, the build:dev/build:prod/types:check/prepare package.json script contract, the Turborepo build task graph, SEA executables, and the API Extractor meta pass. Use on savvy.build.ts, and on package.json or turbo.json of a bundler-built package. Triggered by \"set up savvy.build.ts\", \"configure the bundler\", \"build:dev vs build:prod\", \"why does this package need prepare\", \"wire turbo build tasks\", \"dual-format esm cjs\", \"externals vs bundledPackages\", \"build a single executable\", \"suppress api extractor warning\", \"rspress plugin build\"." --- # Building with @savvy-web/bundler ## Is this file actually a bundler package? `package.json` and `turbo.json` exist in nearly every package in every repo — most of them have nothing to do with `@savvy-web/bundler`. Before applying anything below, check the file (or its directory) for ONE of: - a sibling `savvy.build.ts` - `publishConfig.directory` set to `dist/dev/pkg` - a `build:dev` script running `node savvy.build.ts --target dev` (`tsdown-plugins` runs `tsx savvy.build.ts --target dev` instead — it self-hosts before `node`'s own build target exists, but the contract is the same) None present? This skill's content does not apply to that file — move on. ## savvy.build.ts: the per-package build entry Run it with `node savvy.build.ts --target dev|prod`. It builds JS + `.d.ts`, transforms the manifest, and — on `prod` — runs the API Extractor meta pass. ### Front door (preferred) ```ts import { build } from "@savvy-web/bundler"; await build(); ``` Zero-config `build()` reads `package.json` `exports`/`bin`, derives the target from `process.argv` (`--target dev|prod|exe`, `--watch`, `--no-exe`, `--verbose`), and builds. Pass overrides as `build({ … })` using any `BuildConfig` field — see `references/options.md` for the full surface. ### Escape hatch (secondary) ```ts import { defineBuild, runBuild } from "@savvy-web/bundler"; const config = defineBuild({ /* … */ }); // inspect / snapshot / transform `config` here await runBuild(config, { cwd: process.cwd(), argv: process.argv.slice(2) }); ``` Use only when you must inspect or programmatically transform the resolved config, or inject `RunOptions` IO hooks (testing/self-host). Default builds use `build()`. ## Reading a build as evidence Two different failures produce a build log that reads exactly like a clean gate. Both matter most when the build is the evidence for a claim — "the warnings are fixed", "the surface is clean", "the change is in the artifact". **A direct `node savvy.build.ts --target prod` is not the `build:prod` task.** The task graph runs `types:check` and `build:dev` first; invoking the script by hand skips both. The prod pass then runs against whatever `dist/dev` happens to hold, may emit no `.d.ts` at all, and can leave a truncated `issues.json` whose empty diagnostic buckets are indistinguishable from a clean one. Run the task, not the script: `pnpm turbo run build:prod --filter `. **A turbo cache hit replays the previous run's output verbatim.** `FULL TURBO`, the same file count, the same `suppressed` figure — a stale artifact and a fresh one read identically in the log. An agent that edits source, builds, and reads a clean log has no evidence from that log that the build ever saw the edit. The tell for both is `dist//issues.json`'s `generatedAt` — the timestamp the build *wrote into* the artifact. It must postdate your newest source edit. ```bash node -pe "require('./dist/prod/issues.json').generatedAt" ``` **Do not reach for the file's mtime instead.** A cache restore writes the artifact with the *current* time, so mtime is refreshed on every replay while `generatedAt` keeps the original build's value. Measured on this repo: a `FULL TURBO` hit restored `dist/dev/issues.json` at `06:23:05` with `generatedAt` still reading `02:18:04`. An mtime comparison — `find src -newer dist/prod/issues.json` or any equivalent — therefore reports "fresh" for every replayed artifact no matter how stale, which is precisely the case you are trying to catch. `generatedAt` is the authority; mtime is worse than useless here because it looks like corroboration. A `generatedAt` predating your edit means one of two things, and they are worth telling apart: the build never saw the edit, or the edit is not an input to that task's hash. `--force` settles it — a replay against genuinely unchanged inputs is legitimate and needs no rebuild, which is why `/silk:tsdoc`'s verification recipe passes `--force` rather than trusting a cached gate. `issues.json` also carries a `buildOk` stamp — read it before the diagnostic buckets, since the artifact is written on every terminal path including a crash. `/silk:tsdoc` owns that recipe. ## package.json script contract Every bundler-built package declares `publishConfig.directory: dist/dev/pkg` and three scripts: `build:dev` (`node savvy.build.ts --target dev`), `build:prod` (`node savvy.build.ts --target prod`), `types:check` (`tsc --noEmit`). Other supporting scripts may exist alongside these. ### `prepare` — read this before touching it A package ALSO needs `"prepare": "turbo run build:dev"` whenever it is a `workspace:*` dependency of ANY OTHER `package.json` in the repo — the root, a sibling package, or an `e2e/*` fixture all count. Its consumer resolves it through a `link:` into `dist/dev/pkg`, and that link has to resolve at **install time**, before and independently of Turborepo's task graph — turbo hasn't run anything yet at that point. **Do not delete a `prepare` script on the theory that turbo's `dependsOn` already covers the ordering — it does not, and this is a known, repeating mistake.** A package that happens to build fine without `prepare` in one session may only be working by accident of *that run's* orchestration order, not by design; absence of breakage is not evidence the script is unnecessary. This has cost real repairs: deleting `@savvy-web/changelog`'s `prepare` as "redundant with turbo" broke `changeset version`/`changeset_preview` with `Cannot find package '@savvy-web/changelog'`, because it's resolved at install time via `link:` from the root, not by turbo. Before adding, removing, or reasoning about a `prepare` script, find who actually consumes the package rather than guessing: ```bash grep -rl '"@savvy-web/": "workspace:\*"' **/package.json ``` Any hit — anywhere in the repo — means it needs `prepare`. Zero hits means it doesn't yet; add one the moment something starts depending on it. See `references/workspace-setup.md` for the current roster and the two valid `prepare` forms. ## turbo.json: what it orders, and what it doesn't `build:dev` depends on `^build:dev` (a package's workspace deps build first); `build:prod` depends on `types:check` + `build:dev`; `types:check` depends on `^build:dev`. This `dependsOn` graph governs build order **whenever turbo actually runs** — `pnpm build`, CI, or a package's own `prepare: turbo run build:dev`. It's why `prepare` is written as `turbo run build:dev` rather than a bare `node savvy.build.ts`: invoked from inside the package, turbo scopes to that package plus its whole upstream `^build:dev` chain, so one `prepare` script builds everything it needs in the right order. What `dependsOn` does NOT do is decide *whether* a package's `prepare` runs at all — that's `pnpm install`'s own install-time linking, entirely outside turbo. `pnpm install` triggers a workspace package's `prepare` because that package is resolved as a `workspace:*` dependency somewhere in the repo, full stop; it does not skip a package's `prepare` on the theory that some consumer's `prepare` would transitively rebuild it via `dependsOn` anyway. `dependsOn` orders builds that are already triggered — it never triggers one itself, and it never reaches outside a `turbo run`. That gap is exactly what makes the "turbo already orders it" argument for deleting `prepare` wrong. Full root/per-package `turbo.json` shape and sentinels (`$TURBO_ROOT$`, `$TURBO_DEFAULT$`, `$TURBO_EXTENDS$`) are in `references/workspace-setup.md`. ## Which reference do I need | Reference | Covers | | --- | --- | | `references/options.md` | Every `BuildConfig` field | | `references/workspace-setup.md` | `build:dev`/`build:prod`/`types:check`/`prepare` scripts, the current with/without-`prepare` roster, and root + per-package `turbo.json` wiring | | `references/sea.md` | Single Executable Application (SEA) binaries | | `references/api-extractor.md` | The meta pass + a bundling-knob decision guide | | `references/rspress-builder.md` | Building RSPress plugins | For TSDoc release tags and fixing `ae-*`/`tsdoc-*` diagnostics, use `/silk:tsdoc` — this skill owns build *config*, `tsdoc` owns doc *comments*.