# Toolchain

The build pipeline is Rust-first (see [the rust toolchain post](/blog/rust-owns-the-javascript-toolchain-in-2026) for the rationale and [the benchmark post](/blog/benchmarking-the-rust-javascript-toolchain-in-2026) for the before/after numbers). This file is the migration log plus a day-to-day command reference.

## Commands

```sh
# install (bun blocks postinstalls by default; see bunfig.toml)
bun install

# one-time after a fresh clone: install the pre-commit hook
bash scripts/install-hooks.sh

# dev (Turbopack, ready in ~350ms locally)
bun run dev

# build (Turbopack)
bun run build

# lint
bun run lint        # biome 2 (lint, format check, and import-sort)
bun run lint:fast   # oxlint (CI speed gate, ~8ms across 88 files)
bun run lint:rust   # cargo clippy on cli/

# format (also fixes import order)
bun run format      # biome check --write

# typecheck
bun run typecheck   # tsgo --noEmit (TypeScript 7 beta, Go-based)

# benchmark the toolchain (see BENCHMARK.md)
bash scripts/bench.sh         # quick (3 samples, no install)
bash scripts/bench.sh --full  # cold install + 5 samples
```

## CI status

GitHub Actions auto-triggers are currently OFF (see `.github/workflows/ci.yml` `on: workflow_dispatch:`). The pre-commit hook in `.githooks/pre-commit` covers oxlint + biome + tsgo locally before each commit (~500 ms). Run the workflow manually with `gh workflow run CI` if you want the full audit + rust jobs against `main`.

To re-enable auto CI:

```yaml
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
```

## Status

| Phase | Scope | Status |
|---|---|---|
| 1 | Turbopack default, bun lockfile, `.nvmrc`, `bunfig.toml`, Dependabot | Done |
| 2 | Biome 2 + oxlint (lint, format, import-sort; ESLint dropped) | Done |
| 3 | GH Actions CI (lint + typecheck + build + supply-chain audit) | Done |
| 4 | Tailwind v4 + Lightning CSS | Done |
| 5 | cargo-deny + clippy enforcement on the Rust CLI | Done |
| 6 | tsgo (TypeScript 7 beta, Go-based) for type checking | Done |

## What was changed

### Phase 1: free wins

- `package.json` scripts no longer pass `--webpack`. Turbopack is the Next.js 16 default and is now active.
- Lockfile: deleted `package-lock.json` and `bun.lockb`, replaced with text-format `bun.lock`. Vercel auto-detects bun for installs.
- `.nvmrc` pins Node to `24.11.1`.
- `bunfig.toml` sets `[install] exact = true` and `saveTextLockfile = true`. Bun's default policy of blocking postinstall scripts (except for entries listed in `trustedDependencies` in `package.json`) is what gives us the supply-chain posture the blog post argues for. Two postinstalls are currently blocked and confirmed safe to leave blocked: `@vercel/speed-insights` (analytics-id sanity check) and `@coinbase/x402` (TOS notice).
- `.github/dependabot.yml` schedules weekly updates for npm, cargo, and GitHub Actions, with grouping rules so related package bumps land together.

### Phase 2: lint + format toolchain

- Added `@biomejs/biome` and `oxlint` as dev deps.
- `biome.json` was produced by `bunx biome migrate eslint --write` against the existing `eslint.config.mjs`. The formatter is disabled for this round (enabling it would emit a 1000+ line cosmetic diff); import sorting is also off. The CSS parser has `tailwindDirectives: true` so `@apply` does not trip the lint.
- `.oxlintrc.json` ignores the same paths as biome.
- `next.config.ts`: MDX plugins are now passed as string-form (`["rehype-pretty-code", { theme: "github-dark" }]`) so Turbopack's worker can resolve them on its side. Function refs cannot cross the worker boundary in Turbopack. The original `onVisitLine`/`onVisitHighlightedLine`/`onVisitHighlightedWord` callbacks were doing redundant work (the CSS in `globals.css` already targets the `data-line` and `data-highlighted-line` attributes that `rehype-pretty-code` adds natively).
- `src/app/api/og/route.tsx`: replaced `catch (e: any)` with `catch (e: unknown)` + `instanceof Error` narrowing.

### Phase 3: GitHub Actions CI

`.github/workflows/ci.yml` runs three parallel jobs on push and pull request to `main`:

- **js**: bun install (frozen lockfile), then oxlint, biome check, `tsgo --noEmit`, `next build`.
- **audit**: `npm audit signatures` (provenance attestation check the blog post discusses) plus OSV-Scanner against `bun.lock` and `cli/Cargo.lock`.
- **rust**: `cargo clippy -- -D warnings`, `cargo build --release`, RustSec audit, cargo-deny.

### Phase 6: tsgo (TypeScript 7 beta)

- `bun add -d @typescript/native-preview@beta` adds the Go-based TypeScript port. Binary is `tsgo` (parity with `tsc`, ~6x faster on this codebase: 0.44s vs 2.64s wall time for `--noEmit`).
- `package.json` `typecheck` script now runs `tsgo --noEmit`.
- The `typescript@5.9.3` package stays installed because Next.js's tsconfig has a `{ "name": "next" }` language-server plugin that wants the regular `typescript` peer, and editor IntelliSense falls back to it for anyone not running the `TypeScript Native Preview` VS Code extension.
- Microsoft's announcement explicitly green-lights production use ("you can probably start using this in your day-to-day work immediately"). Bloomberg, Canva, and Figma have shipped it on multi-million-line codebases.

### Phase 5: Rust CLI hygiene

- `cli/deny.toml`: cargo-deny configuration. License allowlist (MIT, Apache-2.0, BSD-2/3-Clause, ISC, Unicode-3.0, Zlib, CC0-1.0, MPL-2.0). Bans wildcard versions, warns on unknown registries and yanked crates.
- Fixed four pre-existing clippy issues that surfaced once `-D warnings` was enforced in CI:
  - `cli/src/types.rs`: added `#[allow(clippy::enum_variant_names)]` on the `Message` enum (variants intentionally share the `Loaded` suffix because they represent async-load results).
  - `cli/src/ui/about.rs`: switched `(x / 5) % 2 == 0` to `(x / 5).is_multiple_of(2)`.
  - `cli/src/ui/about_art.rs`: dead-code bug fix where `let dim = if invert { '\u{2592}' } else { '\u{2592}' };` returned the same character either way. Collapsed to `let dim = '\u{2592}';` (preserves the existing visual output).
  - Same file: same `is_multiple_of` cleanup.

## What's left

#### Phase 4: Tailwind v4 + Lightning CSS

- `tailwindcss@4.3.0` and `@tailwindcss/postcss@4.3.0` replace `tailwindcss@3.4.19`. `tw-animate-css@1.4.0` replaces `tailwindcss-animate@1.0.7`. `tailwind-merge` bumped from 2.6.1 to 3.6.0.
- `tailwind.config.ts` deleted. The shadcn HSL theme now lives in `src/app/globals.css` as `:root` + `.dark` blocks containing wrapped `hsl(...)` values, plus an `@theme inline { --color-*: var(--*); }` mapping. Variable names (`--background`, `--foreground`, etc.) are preserved so no component edits were required.
- Class-based dark mode is expressed via `@custom-variant dark (&:where(.dark, .dark *));`.
- Radix accordion keyframes are inlined in `globals.css` and registered as `--animate-accordion-down` / `--animate-accordion-up` tokens inside `@theme inline`. Radix dropdown animations (`animate-in`, `fade-in-0`, `zoom-in-95`, `slide-in-from-*`) come from `tw-animate-css`.
- `rehype-pretty-code` runtime classes (`data-line`, `data-highlighted-line`, `data-line-numbers`) are safelisted via `@source inline(...)` since v4 auto-detection cannot see classes generated at render time.
- `postcss.config.mjs` switched to a single `@tailwindcss/postcss` plugin.
- Pre-existing `hsl(var(--*))` patterns in `src/app/the-middle-of-the-web-dies-in-2027/talk.css` were replaced with bare `var(--*)` (since the variable now contains the full `hsl(...)` value). Slash-alpha patterns like `hsl(var(--x) / 0.25)` were rewritten as `color-mix(in srgb, var(--x) 25%, transparent)`. Same treatment for the `shadow-[...]` arbitrary value in `ScreenFrame.tsx`.
- `components.json`, `eslint.config.mjs`, and `biome.json` had their stale `tailwind.config.ts` references removed.

#### Codemod note

`bunx @tailwindcss/upgrade@latest` crashed mid-flight on `Cannot apply unknown utility class 'border-border'` because the v3 `@apply border-border` inside `@layer base` could not resolve before the new `@theme inline` block existed. Phase C (CSS rewrite) and Phase D (config delete) were finished manually. v4.3 keeps backwards-compatible aliases for `shadow-sm`, `outline-none`, and `bg-gradient-*`, so template renames did not need to be applied.


### Known advisories (surfaced by `bun audit` / `cargo audit` / OSV-Scanner, non-blocking in CI)

All audit steps run with `continue-on-error: true` in `.github/workflows/ci.yml` so they surface issues without breaking builds. As of the last audit:

**npm (run locally with `bun audit`):**

| Severity | Package | Path | Notes |
|---|---|---|---|
| high | `ws@>=7.0.0 <7.5.10` | `@coinbase/x402` and `@x402/*` → viem → isows → ws | Transitive in the web3 stack. GHSA-3h5v-q93c-6h6q (DoS on many HTTP headers). Resolution: upstream `viem` / `isows` bump. |
| moderate | `postcss@<8.5.10` | Transitive after Tailwind v4 migration: `@tailwindcss/postcss` and `next` both ship internal copies | GHSA-qx2v-qp2m-jg93 (XSS via unescaped `</style>`). Resolution: upstream bump from `@tailwindcss/postcss` and Next.js. |
| moderate | `bn.js@<4.12.3` | `@x402/paywall` → `@walletconnect/*` → `bn.js` | GHSA-378v-28hj-76wf (infinite loop). Resolution: upstream `@walletconnect` bump. |

**cargo (run locally with `cargo audit --manifest-path cli/Cargo.toml`):**

| Severity | Crate | Notes |
|---|---|---|
| moderate | `lru@0.12.5` | RUSTSEC-2026-0002 / GHSA-rhfx-m35p-ff5j. Pulled in via the russh / quinn dep tree. Resolution: upstream bump in russh. |
| moderate | `paste@1.0.15` | RUSTSEC-2024-0436 (crate is unmaintained). Same path. |
| moderate | `rsa@0.10.0-rc.16` | RUSTSEC-2023-0071 (Marvin attack). Pre-release version pinned by russh. Resolution: russh upstream. |

Re-run locally with `bun audit` and `cargo audit --manifest-path cli/Cargo.toml`. Add `bun update` first to pull in any compatible patches that may have landed.

### Future tightening (out of scope for now)

(Nothing tracked here for now. tsgo migration completed early.)
