diff --git a/AGENTS.md b/AGENTS.md index 2ba03009389..822700b696d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -195,7 +195,7 @@ it binds you regardless of which mechanism is within reach. ```bash bun install -bun run typecheck # bun x tsc --noEmit (strict) +bun run typecheck # bun check --no-script --noEmit (strict) bun run test:changed # import-graph tests against the resolved `dev` merge base bun run test # full tests/ suite (default before review) bun run lint:gui # GUI eslint diff --git a/docs-site/src/content/docs/contributing.md b/docs-site/src/content/docs/contributing.md index d0021b7f12a..6c1929381e6 100644 --- a/docs-site/src/content/docs/contributing.md +++ b/docs-site/src/content/docs/contributing.md @@ -15,7 +15,7 @@ bun install bun run setup:hooks # retire the managed pre-push and post-merge hooks bun run dev:proxy # proxy API in dev mode bun run dev:gui # dashboard dev server (another terminal) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # full suite (default) ``` @@ -33,10 +33,11 @@ yourself — the dev server is unaffected because it rebuilds on demand. ## Build and test commands The root package is Bun-native TypeScript; there is no separate server compile step. Use the checked-in -scripts so local commands match CI: +scripts for local verification: ```bash -bun run typecheck # strict TypeScript check +bun run typecheck # strict Bun-native type check +bun run typecheck:tsc # reference check with the installed TypeScript compiler bun run test:changed # import-graph tests against the resolved dev merge base bun run test # full suite (default) bun test tests/routing/router.test.ts # focused test file @@ -45,6 +46,14 @@ bun run privacy:scan # credential/privacy scan used by CI bun run prepare:package # refresh package launchers/assets ``` +`typecheck` uses Bun's native checker with the root `tsconfig.json`, preserving its `src/` scope +and strict compiler options without emitting files. `--no-script` keeps it on the type checker +if a `check` package script is added later. The installed Bun dependency supplies the checker; +`typecheck:tsc` retains the TypeScript compiler for comparisons and fallback. CI and release preflight +continue to invoke `tsc` directly, and the GUI retains its own TypeScript build check. +The `prepublishOnly` hook also uses `typecheck:tsc`, between the audit and GUI build steps; +an audit or compiler failure stops the remaining steps. + The suite uses Bun 1.4.4 (`package.json`'s `testRunnerBun`) while the shipped runtime is 1.4.4. `scripts/test.ts` finds a 1.4.4 binary on PATH or in `~/.bun/bin`, or uses `OCX_TEST_RUNNER_BUN`. Install that version locally if it is missing; the resolver never downloads it. @@ -181,7 +190,7 @@ does not change `main`/`preview` review rules or allow direct pushes, force-push ## Conventions -- **ES Modules only** (`import`/`export`), TypeScript, `strict` mode. Keep `bun x tsc --noEmit` clean. +- **ES Modules only** (`import`/`export`), TypeScript, `strict` mode. Keep `bun run typecheck` clean. - **~500 lines per file max** — split by responsibility (the `web-search/` and `vision/` sidecars are good examples of small, focused modules behind a single `index.ts`). - **Handle async errors at boundaries** — sidecars never throw into the request path; they degrade to diff --git a/docs-site/src/content/docs/fr/contributing.md b/docs-site/src/content/docs/fr/contributing.md index 279b582c754..78c5ba4ae7f 100644 --- a/docs-site/src/content/docs/fr/contributing.md +++ b/docs-site/src/content/docs/fr/contributing.md @@ -15,7 +15,7 @@ bun install bun run setup:hooks # retirer les anciens hooks gérés pre-push et post-merge bun run dev:proxy # proxy API in dev mode bun run dev:gui # dashboard dev server (another terminal) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # suite complète (par défaut) ``` @@ -163,7 +163,7 @@ du dépôt et des chemins sensibles du point de vue de la sécurité est déclar ## Conventions -- **Modules ES uniquement** (`import`/`export`), TypeScript, mode `strict`. Gardez `bun x tsc --noEmit` propre. +- **Modules ES uniquement** (`import`/`export`), TypeScript, mode `strict`. Gardez `bun run typecheck` propre. - **Environ 500 lignes au maximum par fichier** — répartissez le code par responsabilité. Les services auxiliaires `web-search/` et `vision/` sont bons exemples de petits modules ciblés derrière un seul `index.ts`). - **Gérer les erreurs asynchrones aux frontières** — les services auxiliaires ne propagent jamais d’exception dans le diff --git a/docs-site/src/content/docs/ja/contributing.md b/docs-site/src/content/docs/ja/contributing.md index 9ad672176ec..ba08d50ca25 100644 --- a/docs-site/src/content/docs/ja/contributing.md +++ b/docs-site/src/content/docs/ja/contributing.md @@ -12,7 +12,7 @@ bun install bun run setup:hooks # 旧管理対象 pre-push・post-merge の削除 bun run dev:proxy # 開発モードのプロキシ API bun run dev:gui # ダッシュボード dev サーバー(別ターミナル) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # 全テストスイート(既定) ``` @@ -122,7 +122,7 @@ Go ネイティブポートを担っていた `dev2-go` は廃止し、2 本の ## 規約 -- **ES Modules のみ**(`import`/`export`)、TypeScript、`strict` モード。`bun x tsc --noEmit` をクリーンに +- **ES Modules のみ**(`import`/`export`)、TypeScript、`strict` モード。`bun run typecheck` をクリーンに 保ってください。 - **ファイルあたり最大約 500 行** — 責任ごとに分割してください。単一の `index.ts` の後に小さく集中したモジュールを置いた `web-search/` と `vision/` サイドカーが良い例です。 diff --git a/docs-site/src/content/docs/ko/contributing.md b/docs-site/src/content/docs/ko/contributing.md index cb8ba43379e..bcaaa4610e1 100644 --- a/docs-site/src/content/docs/ko/contributing.md +++ b/docs-site/src/content/docs/ko/contributing.md @@ -12,7 +12,7 @@ bun install bun run setup:hooks # 기존 관리형 pre-push·post-merge 제거 bun run dev:proxy # 개발 모드 프록시 API bun run dev:gui # 대시보드 dev 서버(다른 터미널) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # 전체 테스트 스위트 (기본) ``` @@ -121,7 +121,7 @@ Go 네이티브 포트를 담당했던 `dev2-go`는 정리했고, 두 라인을 ## 컨벤션 -- **ES Modules 전용**(`import`/`export`), TypeScript, `strict` 모드. `bun x tsc --noEmit`을 깨끗하게 +- **ES Modules 전용**(`import`/`export`), TypeScript, `strict` 모드. `bun run typecheck`을 깨끗하게 유지하세요. - **파일당 최대 약 500줄** — 책임별로 나누세요. 단일 `index.ts` 뒤에 작고 집중된 모듈을 둔 `web-search/`와 `vision/` 사이드카가 좋은 예입니다. diff --git a/docs-site/src/content/docs/ru/contributing.md b/docs-site/src/content/docs/ru/contributing.md index 84da5ac93e5..f8e9e58f2cf 100644 --- a/docs-site/src/content/docs/ru/contributing.md +++ b/docs-site/src/content/docs/ru/contributing.md @@ -12,7 +12,7 @@ bun install bun run setup:hooks # удалить прежние управляемые pre-push и post-merge bun run dev:proxy # прокси-API в режиме разработки bun run dev:gui # dev-сервер дашборда (другой терминал) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # полный набор тестов (по умолчанию) ``` @@ -123,7 +123,7 @@ Pull request'ы с ребейзом приветствуются: ребейз ## Конвенции -- **Только ES Modules** (`import`/`export`), TypeScript, режим `strict`. Держите `bun x tsc --noEmit` +- **Только ES Modules** (`import`/`export`), TypeScript, режим `strict`. Держите `bun run typecheck` без ошибок. - **Не более ~500 строк на файл** — разделяйте по ответственности (сайдкары `web-search/` и `vision/` — хорошие примеры небольших сфокусированных модулей за единым `index.ts`). diff --git a/docs-site/src/content/docs/tr/contributing.md b/docs-site/src/content/docs/tr/contributing.md index ff5c86f2cb3..02e00b8e397 100644 --- a/docs-site/src/content/docs/tr/contributing.md +++ b/docs-site/src/content/docs/tr/contributing.md @@ -17,7 +17,7 @@ bun install bun run setup:hooks # eski yönetilen pre-push ve post-merge kancalarını kaldır bun run dev:proxy # geliştirme modunda proxy API bun run dev:gui # kontrol paneli geliştirme sunucusu (başka bir terminalde) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # tam test paketi (varsayılan) ``` diff --git a/docs-site/src/content/docs/zh-cn/contributing.md b/docs-site/src/content/docs/zh-cn/contributing.md index 8aa8bb4cef5..3f5e3c898cb 100644 --- a/docs-site/src/content/docs/zh-cn/contributing.md +++ b/docs-site/src/content/docs/zh-cn/contributing.md @@ -12,7 +12,7 @@ bun install bun run setup:hooks # 移除旧的托管 pre-push 和 post-merge bun run dev:proxy # 开发模式代理 API bun run dev:gui # 仪表盘 dev 服务器(另一个终端) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # 完整测试套件(默认) ``` @@ -112,7 +112,7 @@ bun run release:watch # 观察最新的 Release workflow run ## 约定 - **仅使用 ES Modules**(`import`/`export`)、TypeScript 和 `strict` mode。保持 - `bun x tsc --noEmit` 无报错。 + `bun run typecheck` 无报错。 - **每个文件最多约 500 行** —— 按职责拆分。`web-search/` 和 `vision/` sidecar 是很好的例子: 小而专注的 module 位于单一 `index.ts` 之后。 - **在边界处理异步错误** —— sidecar 不会把异常抛进请求路径,而会降级成合适的 marker。 diff --git a/docs-site/src/content/docs/zh-tw/contributing.md b/docs-site/src/content/docs/zh-tw/contributing.md index 1180a1c3208..b027688293b 100644 --- a/docs-site/src/content/docs/zh-tw/contributing.md +++ b/docs-site/src/content/docs/zh-tw/contributing.md @@ -12,7 +12,7 @@ bun install bun run setup:hooks # 移除舊的受管理 pre-push 與 post-merge bun run dev:proxy # 開發模式代理 API bun run dev:gui # 儀表板 dev 伺服器(另一個終端) -bun run typecheck # bun x tsc --noEmit +bun run typecheck # bun check --no-script --noEmit bun run test # 完整測試套件(預設) ``` @@ -132,7 +132,7 @@ bun run release:watch # 觀察最新的 Release workflow run ## 約定 - **僅使用 ES Modules**(`import`/`export`)、TypeScript 和 `strict` mode。保持 - `bun x tsc --noEmit` 無報錯。 + `bun run typecheck` 無報錯。 - **每個檔案最多約 500 行** —— 按職責拆分。`web-search/` 和 `vision/` sidecar 是很好的例子: 小而專注的 module 位於單一 `index.ts` 之後。 - **在邊界處理非同步錯誤** —— sidecar 不會把例外拋進請求路徑,而會降級成合適的 marker。 diff --git a/package.json b/package.json index af8f7c9c50d..2a11e7a93ec 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "start": "bun run src/cli/index.ts start", "test": "bun scripts/test.ts", "test:changed": "bun scripts/test.ts --changed=dev", - "typecheck": "bun x tsc --noEmit", + "typecheck": "bun check --no-script --noEmit", + "typecheck:tsc": "bun x tsc --noEmit", "typecheck:gui:if-changed": "bun scripts/typecheck-gui-if-changed.ts", "audit:high": "bun audit --audit-level=high && cd gui && bun audit --audit-level=high", "privacy:scan": "bun scripts/privacy-scan.ts", @@ -67,7 +68,7 @@ "test:macos": "swift run --package-path app MenuBarCoreTests", "prepare:package": "bun scripts/prepare-package.ts", "prepack": "bun run prepare:package", - "prepublishOnly": "bun run audit:high && bun run typecheck && bun run build:gui", + "prepublishOnly": "bun run audit:high && bun run typecheck:tsc && bun run build:gui", "release": "bun scripts/release.ts", "release:watch": "bun scripts/release.ts watch", "prepush": "bun run typecheck && bun run typecheck:gui:if-changed && bun run lint:gui:if-changed && bun run test && bun run privacy:scan && bun run doctor:gui:if-changed", diff --git a/structure/ops/cross-platform-ci.md b/structure/ops/cross-platform-ci.md index 080aaaa5146..7f61301dc25 100644 --- a/structure/ops/cross-platform-ci.md +++ b/structure/ops/cross-platform-ci.md @@ -102,6 +102,16 @@ already `bun-` of the runtime being started it is kept unchanged, and a nests one more `bun-` level beneath it. Startup ACL reads use .NET, never module-autoloaded `Get-Acl`/`Set-Acl` (`tests/ci-workflows/ci-review-lanes.test.ts` scans `src/`). +Local `bun run typecheck` uses the installed Bun runtime's native checker with the root +`tsconfig.json`, strict options and `src/` scope. `--no-script` prevents a future package `check` +script from replacing type checking, and the command emits no files. `bun run typecheck:tsc` +retains the installed TypeScript compiler for comparison and fallback. CI and release preflight +invoke `tsc` directly; GUI build checks and the separate `testRunnerBun` pin keep their own roles. +The `prepublishOnly` hook runs `typecheck:tsc` after `audit:high` and before `build:gui`, +stopping on either audit or compiler failure. +`tests/ci-workflows/ci-gui-typecheck-gate.test.ts` exercises both package commands against the root +compiler options, including strict failures in unimported source files and a conflicting `check` script, +and runs the publish hook with safe audit/build recorders to verify compiler selection and fail-fast order. Across the jobs, the workflow runs: ```bash diff --git a/tests/ci-workflows/ci-gui-typecheck-gate.test.ts b/tests/ci-workflows/ci-gui-typecheck-gate.test.ts index ef22cc92bb0..55d3def3f70 100644 --- a/tests/ci-workflows/ci-gui-typecheck-gate.test.ts +++ b/tests/ci-workflows/ci-gui-typecheck-gate.test.ts @@ -1,10 +1,11 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { delimiter, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { repoPath } from "../helpers/repo-root"; -import { INTERNAL_DEADLINE_MS } from "../helpers/test-budget"; +import { INTERNAL_DEADLINE_MS, SPAWN_BUDGET_MS } from "../helpers/test-budget"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; /** * Every spawnSync below is bounded by the shared spawned-child deadline: Bun's test @@ -185,3 +186,156 @@ describe("gui typecheck if-changed gate", () => { expect((guiBuild!.run ?? "").split(/\r?\n/).map(line => line.trim()).filter(Boolean)).toEqual(["cd gui", "bun run build"]); }); }); + +const scripts = JSON.parse(readFileSync(repoPath("package.json"), "utf8")).scripts as Record; +const config = readFileSync(repoPath("tsconfig.json"), "utf8"); +const roots: string[] = []; +const compilerLinks: string[] = []; +afterEach(() => { + for (const path of compilerLinks.splice(0)) unlinkSync(path); + for (const root of roots.splice(0)) removeTreeWithRetry(root); +}); + +function nativeTypecheckFixture(): string { + const root = mkdtempSync(join(tmpdir(), "ocx-native-typecheck-")); + roots.push(root); + writeFileSync(join(root, "tsconfig.json"), config); + writeFileSync(join(root, "package.json"), JSON.stringify({ + type: "module", + scripts: { typecheck: scripts.typecheck, "typecheck:tsc": scripts["typecheck:tsc"], check: "bun check-trap.ts" }, + })); + writeFileSync(join(root, "check-trap.ts"), "process.exit(23);\n"); + // Package scripts must select the installed runtime, even on the older test-runner Bun. + symlinkSync(repoPath("node_modules"), join(root, "node_modules"), process.platform === "win32" ? "junction" : "dir"); + const files = { + "src/config.json": '{"port":8080}', + "src/marker.d.mts": "export interface Marker { id: string }\n", + "src/main.ts": [ + 'import { z } from "zod";', + 'import defaults from "./config.json";', + 'import type { Marker } from "./marker.mjs";', + 'const Schema = z.object({ port: z.number(), kind: z.literal("port") });', + 'export const parsed: z.infer = { port: defaults.port, kind: "port" };', + 'export const marker: Marker = { id: "marker" };', + 'export const pending: Promise = Bun.file("fixture.txt").text();', + ].join("\n"), + "gui/ignored.ts": 'export const intentionallyExcluded: number = "bad";\n', + }; + for (const [path, source] of Object.entries(files)) { + const target = join(root, path); + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, source); + } + return root; +} + +function runTypecheck(root: string, script: string) { + const result = Bun.spawnSync([process.execPath, "run", script], { + cwd: root, timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL", stdin: "ignore", + }); + return { exitCode: result.exitCode, output: result.stdout.toString() + result.stderr.toString() }; +} + +test("native and reference typecheck honor root scope and declarations without running a check script or emitting files", () => { + const root = nativeTypecheckFixture(); + for (const script of ["typecheck", "typecheck:tsc"]) { + const result = runTypecheck(root, script); + expect(result.exitCode, result.output).toBe(0); + expect(existsSync(join(root, "dist"))).toBe(false); + expect(existsSync(join(root, "src", "main.js"))).toBe(false); + } +}, SPAWN_BUDGET_MS); + +test("native and reference typecheck reject strict errors in an unimported source file", () => { + const root = nativeTypecheckFixture(); + writeFileSync(join(root, "src", "broken.ts"), [ + "export function untyped(value) { return value; }", + 'export const count: number = "text";', + "const candidate: { port: number } | undefined = Math.random() ? { port: 1 } : undefined;", + "export const port: number = candidate.port;", + 'export const labels: Record<"ready" | "missing", string> = { ready: "Ready" };', + "// @ts-expect-error", + "export const okay: number = 42;", + ].join("\n")); + for (const script of ["typecheck", "typecheck:tsc"]) { + const result = runTypecheck(root, script); + expect(result.exitCode, result.output).not.toBe(0); + const codes = [...new Set([...result.output.matchAll(/\bTS(\d+)\b/g)].map(match => Number(match[1])))]; + expect(codes.sort((left, right) => left - right), result.output).toEqual([2322, 2578, 2741, 7006, 18048]); + expect(existsSync(join(root, "dist"))).toBe(false); + } +}, SPAWN_BUDGET_MS); + +function prepublishFixture(): string { + const root = mkdtempSync(join(tmpdir(), "ocx-prepublish-typecheck-")); + roots.push(root); + mkdirSync(join(root, "node_modules", ".bin"), { recursive: true }); + const compiler = join(root, "node_modules", "typescript"); + symlinkSync(repoPath("node_modules", "typescript"), compiler, process.platform === "win32" ? "junction" : "dir"); + compilerLinks.push(compiler); + if (process.platform === "win32") { + for (const name of ["tsc.exe", "tsc.bunx"]) { + copyFileSync(repoPath("node_modules", ".bin", name), join(root, "node_modules", ".bin", name)); + } + } else { + const shim = join(root, "node_modules", ".bin", "tsc"); + symlinkSync(repoPath("node_modules", "typescript", "bin", "tsc"), shim); + compilerLinks.push(shim); + } + writeFileSync(join(root, "package.json"), JSON.stringify({ type: "module", scripts: { + // Exercise the real hook and compiler; only audit/build side effects are replaced. + prepublishOnly: scripts.prepublishOnly, + "typecheck:tsc": `${scripts["typecheck:tsc"]} && bun phase.ts compiler`, + typecheck: `${scripts.typecheck} && bun phase.ts native`, + "audit:high": "bun phase.ts audit", + "build:gui": "bun phase.ts build", + } })); + writeFileSync(join(root, "phase.ts"), [ + 'import { appendFileSync } from "node:fs";', + 'appendFileSync("phases.log", process.argv[2] + "\\n");', + 'if (process.env.FAIL_PHASE === process.argv[2]) process.exit(29);', + ].join("\n")); + writeFileSync(join(root, "tsconfig.json"), JSON.stringify({ + compilerOptions: { strict: true, types: [], outDir: "dist" }, files: ["main.ts"], + })); + writeFileSync(join(root, "main.ts"), "export const count: number = 1;\n"); + return root; +} + +function runPrepublish(root: string, failPhase?: string) { + // Reuse the caller runtime, without the repository's installed Bun shadowing it. + const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== "path")); + const result = Bun.spawnSync([process.execPath, "run", "prepublishOnly"], { + cwd: root, timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL", stdin: "ignore", + env: { ...env, PATH: `${dirname(process.execPath)}${delimiter}${process.env.PATH ?? ""}`, FAIL_PHASE: failPhase ?? "" }, + }); + return { + exitCode: result.exitCode, + output: result.stdout.toString() + result.stderr.toString(), + phases: readFileSync(join(root, "phases.log"), "utf8").trim().split(/\r?\n/), + }; +} + +test("prepublishOnly runs the reference compiler between audit and build without emitting files", () => { + const root = prepublishFixture(); + const result = runPrepublish(root); + expect(result.exitCode, result.output).toBe(0); + expect(result.phases).toEqual(["audit", "compiler", "build"]); + expect(existsSync(join(root, "dist"))).toBe(false); + expect(existsSync(join(root, "main.js"))).toBe(false); +}, SPAWN_BUDGET_MS); + +test("prepublishOnly stops before typecheck and build when audit fails", () => { + const result = runPrepublish(prepublishFixture(), "audit"); + expect(result.exitCode, result.output).toBe(29); + expect(result.phases).toEqual(["audit"]); +}, SPAWN_BUDGET_MS); + +test("prepublishOnly stops before build when the reference compiler rejects strict errors", () => { + const root = prepublishFixture(); + writeFileSync(join(root, "main.ts"), 'export const count: number = "bad";\n'); + const result = runPrepublish(root); + expect(result.exitCode, result.output).not.toBe(0); + expect(result.output).toContain("TS2322"); + expect(result.phases).toEqual(["audit"]); +}, SPAWN_BUDGET_MS); diff --git a/tests/ci-workflows/cold-spawn-warmup.test.ts b/tests/ci-workflows/cold-spawn-warmup.test.ts index 4410a2f110a..2b14659612e 100644 --- a/tests/ci-workflows/cold-spawn-warmup.test.ts +++ b/tests/ci-workflows/cold-spawn-warmup.test.ts @@ -60,8 +60,8 @@ const DISPOSITIONS: Readonly> = { warmed: false, why: "Its children run scripts/typecheck-gui-if-changed.ts, which imports only node builtins, " - + "or git itself, so an import scan has no repository module graph to warm and a registered " - + "warm-up would fail closed. The spawnSync bounds exist because an unbounded child pinned " + + "git itself, or type checkers on synthetic projects, so there is no repository graph to warm. " + + "The spawnSync bounds exist because an unbounded child pinned " + "Linux test 1/4 batch 4 until the 120s shard deadline cut it (run 37730984813).", }, "tests/ci-workflows/test-runner.test.ts": {