From a95470d1c54d87650fdcb347f669610f2eb8d040 Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Thu, 16 Jul 2026 13:51:56 +0200 Subject: [PATCH] feat(build): typecheck on TypeScript 7 (Go compiler) while tooling stays on TS6 API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopts TS7's native Go compiler for the ~10x-faster typecheck without breaking tooling that needs the TS6 programmatic API (TS7 has no stable API until 7.1; typescript-eslint + the proptest oracle crash on it). - `typescript` stays real 6.0.3 → require("typescript") gives the full 6.x API to typescript-eslint + proptest at runtime. - `@typescript/native` (= TS7) added as a packages/core DEPENDENCY, so consumers get the fast compiler too — the gate typechecks on TS7 everywhere, not just in-repo. Both packages expose a `tsc` bin, so .bin/tsc is ambiguous; TS7 is referenced by explicit package path: * typecheck script → node_modules/@typescript/native/bin/tsc * gate TSC_BIN → resolveTs7Tsc() walks up to @typescript/native (defensive fallback to plain tsc keeps the gate functional if ever absent). - Tests: gate-tool-paths.test.ts (TSC_BIN resolves TS7); gate-incremental.test.ts now runs the gate's ACTUAL resolved binary, not a hardcoded .bin/tsc. TS7 verified to honor the gate's flags (--strict --noUncheckedIndexedAccess --incremental --tsBuildInfoFile) with identical diagnostics. Typecheck: multi-sec → ~0.34s. Full `bun run validate` green. --- bun.lock | 45 ++++++++- package.json | 2 +- packages/core/package.json | 1 + packages/core/scripts/tsc7.ts | 12 +++ packages/core/src/gate/tool-paths.ts | 53 ++++++++++- packages/core/tests/gate-incremental.test.ts | 6 +- packages/core/tests/gate-tool-paths.test.ts | 96 ++++++++++++++++++++ 7 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 packages/core/scripts/tsc7.ts create mode 100644 packages/core/tests/gate-tool-paths.test.ts diff --git a/bun.lock b/bun.lock index 14170908..5ddc0e56 100644 --- a/bun.lock +++ b/bun.lock @@ -36,7 +36,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-sonarjs": "^4.0.3", "eslint-plugin-unicorn": "^64.0.0", - "globals": "^17.7.0", + "globals": "17.7.0", "playwright": "^1.60.0", "prettier": "3.9.4", "react": "^19.2.7", @@ -77,6 +77,7 @@ "@mozilla/readability": "^0.6.0", "@stylistic/eslint-plugin": "^5.10.0", "@typescript-eslint/utils": "8.60.0", + "@typescript/native": "npm:typescript@7.0.2", "cli-highlight": "2.1.11", "eslint": "10.6.0", "eslint-plugin-jsx-a11y": "^6.10.2", @@ -762,6 +763,48 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g=="], + "@typescript/native": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.12.2", "", { "os": "android", "cpu": "arm" }, "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w=="], diff --git a/package.json b/package.json index d9610364..b18cbe2a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "description": "TypeScript coding harness with a deterministic gate, stack-aware guardrails, and stream-level correction.", "scripts": { "tsforge": "bun --cwd packages/core ./src/cli.ts", - "typecheck": "tsc -p packages/core/tsconfig.json", + "typecheck": "bun packages/core/scripts/tsc7.ts -p packages/core/tsconfig.json", "lint": "eslint packages", "lint:fix": "eslint packages --fix", "format": "prettier --write \"packages/**/*.ts\"", diff --git a/packages/core/package.json b/packages/core/package.json index cae4196f..2bd89136 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -45,6 +45,7 @@ "jsdom": "^29.1.1", "prettier": "3.9.4", "turndown": "^7.2.4", + "@typescript/native": "npm:typescript@7.0.2", "typescript": "6.0.3", "typescript-eslint": "8.62.1" }, diff --git a/packages/core/scripts/tsc7.ts b/packages/core/scripts/tsc7.ts new file mode 100644 index 00000000..d456b553 --- /dev/null +++ b/packages/core/scripts/tsc7.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env bun +// Run the project typecheck on TypeScript 7's native compiler, resolved the SAME +// robust way the gate does (resolveTs7Tsc walks up for both the monorepo and +// published/hoisted layouts) — NOT a hardcoded node_modules path, which breaks +// when the package manager hoists @typescript/native somewhere else (e.g. CI). +import { spawnSync } from "node:child_process"; +import { resolveTs7Tsc } from "../src/gate/tool-paths"; + +const bin = resolveTs7Tsc(); +const res = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" }); + +process.exit(res.status ?? 1); diff --git a/packages/core/src/gate/tool-paths.ts b/packages/core/src/gate/tool-paths.ts index 16a5e7ea..da762120 100644 --- a/packages/core/src/gate/tool-paths.ts +++ b/packages/core/src/gate/tool-paths.ts @@ -37,12 +37,63 @@ function resolveToolBin(name: string): string { return name; } +// TypeScript 7 (the Go-native compiler, ~10x faster typecheck) ships as the +// `@typescript/native` package so it can coexist with the `typescript` package, +// whose 6.x programmatic API our tooling (typescript-eslint, proptest) still +// needs — TS7 has no stable programmatic API until 7.1. It is a real dependency +// of this package, so consumers get it too (the gate typechecks on TS7 +// everywhere, not just in-repo). Both packages expose a `tsc` bin, so `.bin/tsc` +// is ambiguous; resolve TS7's compiler by its package path instead. The plain +// `tsc` fallback is only a defensive safety net (should never fire given the +// dependency) that keeps the gate functional on TS6 rather than crashing. +export function resolveTs7Tsc(startDir: string = import.meta.dir): string { + let dir = startDir; + let parent = dirname(dir); + + while (parent !== dir) { + // Hoisted (monorepo): /node_modules/@typescript/native/bin/tsc. + const hoisted = join( + dir, + "node_modules", + "@typescript", + "native", + "bin", + "tsc" + ); + + if (existsSync(hoisted)) { + return hoisted; + } + + // Published/global install where `dir` is itself a `node_modules`: the + // package sits directly inside it (same double-`node_modules` case + // resolveToolBin handles). Without this, a consumer install misses TS7. + const direct = join(dir, "@typescript", "native", "bin", "tsc"); + + if (existsSync(direct)) { + return direct; + } + + dir = parent; + parent = dirname(dir); + } + + // @typescript/native is a dependency, so a miss is unexpected — say so LOUDLY + // (not a silent downgrade) before falling back to an ambient tsc so the gate + // degrades instead of crashing. + process.stderr.write( + "tsforge: @typescript/native (TypeScript 7) not found — the gate is falling back to an ambient `tsc`, which may be TS6 or another version.\n" + ); + + return resolveToolBin("tsc"); +} + // This module lives at `src/gate/`, so the package root (where the bundled eslint // configs + `scripts/` live) is TWO levels up — `import.meta.dir/../..`. const PKG_ROOT = join(import.meta.dir, "..", ".."); export const ESLINT_BIN = resolveToolBin("eslint"); -export const TSC_BIN = resolveToolBin("tsc"); +export const TSC_BIN = resolveTs7Tsc(); export const PRETTIER_BIN = resolveToolBin("prettier"); export const STRICT_CONFIG = join(PKG_ROOT, "strict.eslint.config.mjs"); export const TYPE_AWARE_CONFIG = join( diff --git a/packages/core/tests/gate-incremental.test.ts b/packages/core/tests/gate-incremental.test.ts index 88f4b9f3..6cb59fed 100644 --- a/packages/core/tests/gate-incremental.test.ts +++ b/packages/core/tests/gate-incremental.test.ts @@ -11,9 +11,9 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildGate } from "../src/gate"; - -const ROOT = join(import.meta.dir, "..", "..", ".."); -const TSC_BIN = join(ROOT, "node_modules", ".bin", "tsc"); +// Use the gate's OWN resolved compiler (TS7 via resolveTs7Tsc), not a hardcoded +// .bin/tsc — so this incremental proof runs the exact binary the gate embeds. +import { TSC_BIN } from "../src/gate/tool-paths"; const TSCONFIG = JSON.stringify({ compilerOptions: { diff --git a/packages/core/tests/gate-tool-paths.test.ts b/packages/core/tests/gate-tool-paths.test.ts new file mode 100644 index 00000000..2e766441 --- /dev/null +++ b/packages/core/tests/gate-tool-paths.test.ts @@ -0,0 +1,96 @@ +import { test, expect, describe } from "bun:test"; +import { + existsSync, + mkdirSync, + writeFileSync, + mkdtempSync, + rmSync, +} from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { TSC_BIN, resolveTs7Tsc } from "../src/gate/tool-paths"; + +describe("TSC_BIN (resolveTs7Tsc)", () => { + test("resolves the TypeScript 7 native compiler (@typescript/native)", () => { + // Deterministic: the gate references TS7 by package path, not the ambiguous + // .bin/tsc (both `typescript` and `@typescript/native` expose a `tsc` bin). + expect(TSC_BIN).toContain("@typescript/native"); + expect(existsSync(TSC_BIN)).toBe(true); + }); + + test("the resolved binary is actually TypeScript 7", () => { + // Prefix with `bun` so this works cross-platform (extensionless bin files + // aren't directly executable on Windows). + const out = spawnSync("bun", [TSC_BIN, "--version"], { encoding: "utf8" }); + + expect(out.status).toBe(0); + expect(out.stdout).toContain("Version 7."); + }); +}); + +describe("resolveTs7Tsc layout handling", () => { + function withLayout(rel: string): { dir: string; cleanup: () => void } { + const root = mkdtempSync(join(tmpdir(), "ts7-layout-")); + const binPath = join(root, rel); + + mkdirSync(join(binPath, ".."), { recursive: true }); + writeFileSync(binPath, "#!/bin/sh\n"); + + return { + dir: root, + cleanup: () => rmSync(root, { recursive: true, force: true }), + }; + } + + test("finds the hoisted (monorepo) layout: /node_modules/@typescript/native/bin/tsc", () => { + const { dir, cleanup } = withLayout( + "node_modules/@typescript/native/bin/tsc" + ); + + try { + // start from a nested dir so the walk-up has to climb + const start = join(dir, "packages", "core", "src", "gate"); + + mkdirSync(start, { recursive: true }); + + expect(resolveTs7Tsc(start)).toBe( + join(dir, "node_modules/@typescript/native/bin/tsc") + ); + } finally { + cleanup(); + } + }); + + test("finds the published layout where the start dir is itself node_modules", () => { + const { dir, cleanup } = withLayout( + "node_modules/@typescript/native/bin/tsc" + ); + + try { + // walk begins inside node_modules → the package is a DIRECT child + const start = join(dir, "node_modules", "@agjs", "tsforge", "dist"); + + mkdirSync(start, { recursive: true }); + + expect(resolveTs7Tsc(start)).toBe( + join(dir, "node_modules/@typescript/native/bin/tsc") + ); + } finally { + cleanup(); + } + }); + + test("falls back (and warns) when @typescript/native is absent", () => { + const empty = mkdtempSync(join(tmpdir(), "ts7-none-")); + + try { + // No @typescript/native anywhere under `empty` → fallback path (not a crash) + const resolved = resolveTs7Tsc(empty); + + expect(resolved).not.toContain("@typescript/native"); + } finally { + rmSync(empty, { recursive: true, force: true }); + } + }); +});