Skip to content

perf(lint): load the TypeScript compiler lazily in validateReactPageProps#2544

Merged
os-zhuang merged 1 commit into
mainfrom
perf/lint-lazy-typescript
Jul 3, 2026
Merged

perf(lint): load the TypeScript compiler lazily in validateReactPageProps#2544
os-zhuang merged 1 commit into
mainfrom
perf/lint-lazy-typescript

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Why

@objectstack/lint is loaded on the kernel boot path, but the react-source prop gate (validate-react-page-props.ts, ADR-0081 Phase 2, #2482) only runs when a kind:'react' page is actually validated — a rare, trusted-tier case. Its top-level import ts from 'typescript' meant:

  • every kernel boot eagerly parses the ~9 MB compiler — measured ~70–85 ms on a warm M-series laptop, worse on CF container cold starts where cold-start is already a known pain;
  • boot hard-crashes if the package is pruned from the image — cloud's Docker pruner stripped typescript and container boot died (worked around cloud-side in cloud#728 by keeping the package).

What

  • The compiler now loads lazily, on the first validated react-source page, via a deferred createRequire — the same bundling-safe pattern as driver-sqlite-wasm's knex-wasm-dialect (static node:module import survives esbuild/tsup; the createRequire(...) call is deferred because import.meta.url is an empty stub in the CJS build, falling back to __filename).
  • Public API unchanged and still synchronous ((stack) => ReactPropFinding[] — the package-wide contract); no caller changes, no api-surface impact.
  • typescript stays a regular dependency in packages/lint/package.json.
  • If the package is missing at call time, validation throws an actionable error naming the package and the fix, instead of killing boot. The load call sits outside the parse try so a missing compiler can't be swallowed as "unparseable source".
  • packages/lint/tsconfig.json gains "types": ["node"] (needed for node:module/__filename in the dts build; same as driver-sqlite-wasm).

Verification

  • packages/lint: 116/116 tests green ×3 runs; eslint clean; turbo build --filter=...@objectstack/lint (lint + all dependents incl. CLI) green.
  • New lazy-typescript.test.ts guards the contract at three levels, because vitest inlines static imports through its transform so an in-worker require.cache probe alone cannot catch a reintroduced eager import (found out the hard way — the first cut of the test stayed green under a red-check):
    1. structural — no src file may eagerly import ... from 'typescript' (only import type);
    2. built dist — child node processes import both dist formats and assert the compiler is absent until a react page is validated (auto-skips if dist isn't built);
    3. behavioral — the lazy path loads on demand and the gate still yields findings.
      Red-check: reintroducing the eager import turns exactly these three tests red.
  • Pruned-image simulation (resolution poisoning of typescript only): boot import OK → non-react stacks validate fine → react-page validation fails with the clear message. Measured on ESM dist: importing @objectstack/lint 85 ms with typescript not loaded; first react-page validation pays a one-time +66 ms.
  • Repo audit: no other package source has an eager typescript import.

🤖 Generated with Claude Code

…rops

@objectstack/lint sits on the kernel boot path, but the react-props gate
(ADR-0081 Phase 2, #2482) only runs when a kind:'react' page is actually
validated. The top-level `import ts from 'typescript'` made every boot
parse the ~9 MB compiler (~70 ms+ warm, worse on container cold starts)
and hard-crashed boot when a deployment pruned the package from the image
(cloud's Docker pruner did; worked around in cloud#728).

The compiler now loads on the first validated react-source page via a
deferred createRequire (the bundling-safe pattern from driver-sqlite-wasm's
knex-wasm-dialect); the public API stays synchronous and unchanged, and
`typescript` stays a regular dependency. If the package is missing at call
time, validation fails with an actionable error instead of killing boot.

Guarded by lazy-typescript.test.ts at three levels (structural no-eager-
import scan over src, child-process probes of both dist formats, in-process
lazy-load behavior) — verified to go red when the eager import is
reintroduced. An in-worker require.cache probe alone cannot catch it:
vitest inlines static imports through its transform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 3, 2026 1:30pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 1 changed package(s). ✅

@os-zhuang
os-zhuang merged commit e82a495 into main Jul 3, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the perf/lint-lazy-typescript branch July 3, 2026 13:37
os-zhuang added a commit that referenced this pull request Jul 3, 2026
@objectstack/lint sits on the kernel boot path, but the react syntax
gate (ADR-0081) only runs when a kind:'react' page is actually
validated. The top-level `import { transform } from 'sucrase'` made
every boot parse ~1.5 MB of transpiler (~16 ms cold require) — the same
boot-path problem as the TypeScript compiler in validateReactPageProps,
fixed in #2544.

Sucrase now loads on the first validated react-source page via the same
deferred createRequire (anchor chain import.meta.url → __filename →
cwd); the public API stays synchronous and unchanged, `sucrase` stays a
regular dependency, and a missing package at call time fails validation
with an actionable error instead of killing boot.

The guard test is generalized from lazy-typescript.test.ts to
lazy-deps.test.ts, covering both deps at all three levels (structural
no-eager-import scan over src, child-process probes of both dist
formats, in-process lazy-load behavior) — verified to go red for each
dep separately when its eager import is reintroduced. An in-worker
require.cache probe alone cannot catch it: vitest inlines static
imports through its transform.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant