From 7e81976f4fa26ad9af5f271c8f77732cfeb9d2da Mon Sep 17 00:00:00 2001 From: Max B Date: Sat, 22 Aug 2026 11:08:58 +0200 Subject: [PATCH 1/2] fix(compiler): adopt jsx and lib from the project's tsconfig in ts7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both bugs share one root cause: the ts7 tsconfig-adoption mechanism in adoptProjectConfig7() only lets a fixed allowlist of options through, forcing everything else — including jsx and lib — to scriptc's own defaults with no override. jsx wasn't adopted at all, so any .tsx file failed type-checking with tsgo's "--jsx is not set" even when the project's tsconfig.json sets jsx. Worse, jsx also wasn't handled by serializeOptions()'s enum-to- string switch (which converts TypeScript's numeric enum-valued compiler options back to tsconfig string form), so naively adopting it crashed with "unhandled enum-valued compiler option 'jsx'" instead of just failing to type-check. lib was unconditionally FORCED to ["lib.es2025.d.ts"] with no way to widen it, so a project whose tsconfig sets "lib": ["ES2025", "DOM"] could never get DOM globals into scope, even for code reachable only through type-only imports. Fix: move the lib default from FORCED_OPTIONS (never overridable) to BASE_OPTIONS (overridden by adopted config), adopt jsx/jsxImportSource/ lib from the project's tsconfig when set, and add the missing jsx case to serializeOptions()'s enum-reverse-mapping switch. --- packages/compiler/src/frontend/program.ts | 23 ++++++++++++++++++- packages/compiler/src/frontend/ts7/program.ts | 9 ++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 4dbe00a28..d5f44bd43 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -79,6 +79,11 @@ import { trackedFileExists } from "./input-tracker.js"; const BASE_OPTIONS: ts.Ts7CompilerOptions = { strict: true, + // Default lib surface — overridden below by the project's own tsconfig + // `lib` when it sets one (BASE_OPTIONS loses to `adopted` in the merge). + // A project that never declares `lib` keeps exactly this narrow, no-DOM + // default. + lib: ["lib.es2025.d.ts"], }; const FORCED_OPTIONS: ts.Ts7CompilerOptions = { @@ -93,7 +98,6 @@ const FORCED_OPTIONS: ts.Ts7CompilerOptions = { // classification is intentionally independent below; this option is only // the binder's scope decision. moduleDetection: ts.ModuleDetectionKind.Force, - lib: ["lib.es2025.d.ts"], types: [], allowImportingTsExtensions: true, allowJs: true, @@ -168,6 +172,23 @@ function adoptProjectConfig7( const value = parsed.options[key]; if (value !== undefined) adopted[key] = value; } + const rawJsx = parsed.options["jsx"]; + if (typeof rawJsx === "number") adopted["jsx"] = rawJsx; + const rawJsxImportSource = parsed.options["jsxImportSource"]; + if (typeof rawJsxImportSource === "string") adopted["jsxImportSource"] = rawJsxImportSource; + // A project's own `lib` choice (e.g. `dom` for code that reuses browser + // component prop types via `import type`, even where the runtime path is + // dead for a given compile target) was previously unreachable: `lib` was + // FORCED to a narrow no-DOM default with no override. Adopting it here + // (BASE_OPTIONS still supplies that narrow default for projects that never + // set `lib`) lets a whole-program compile satisfy type-only-imported code + // outside its own reachable surface without every such project needing to + // avoid `import type` reuse across platform-specific implementations. + const rawLib = parsed.options["lib"]; + if (Array.isArray(rawLib)) { + const lib = rawLib.filter((v): v is string => typeof v === "string"); + if (lib.length > 0) adopted["lib"] = lib; + } const nullChecks = adopted["strictNullChecks"] ?? adopted["strict"] ?? false; if (nullChecks !== true) { diags.push(strictNullChecksFloorDiag(configFile)); diff --git a/packages/compiler/src/frontend/ts7/program.ts b/packages/compiler/src/frontend/ts7/program.ts index f319016f9..2230a4053 100644 --- a/packages/compiler/src/frontend/ts7/program.ts +++ b/packages/compiler/src/frontend/ts7/program.ts @@ -71,6 +71,15 @@ function serializeOptions(options: Ts7CompilerOptions): Record lib.startsWith("lib.") && lib.endsWith(".d.ts") ? lib.slice(4, -5) : lib, ); break; + // TypeScript's JsxEmit enum: None=0, Preserve=1, React=2, + // ReactNative=3, ReactJSX=4, ReactJSXDev=5 — same reverse-mapping + // need as target/module/moduleResolution/moduleDetection above, but + // JsxEmit isn't one of this adapter's mirrored enums, so the string + // table is spelled out directly instead of going through enumKeyOf. + case "jsx": + out[key] = + ["none", "preserve", "react", "react-native", "react-jsx", "react-jsxdev"][value as number] ?? value; + break; default: { if (typeof value === "number" && key !== "maxNodeModuleJsDepth") { throw new Error(`ts7 createProgram: unhandled enum-valued compiler option '${key}'`); From 763ed246865bea5b19b3fcf689526c29466468c7 Mon Sep 17 00:00:00 2001 From: Max B Date: Sat, 22 Aug 2026 11:32:16 +0200 Subject: [PATCH 2/2] fix: load JsxEmit as a hidden TS7 enum instead of a hardcoded table vercel[bot]'s review caught that the hardcoded jsx string table used TypeScript 5.9.3's JsxEmit ordering (React=2/ReactNative=3), but 7.0.2 renumbers it (ReactNative=2/React=3) - silently swapping "react" and "react-native". Verified against the real dist/enums/jsxEmit.js module. Fixed properly rather than just correcting the numbers: JsxEmit now goes through the same loadHiddenEnum + enumKeyOf symbolic reverse- mapping as ModuleResolutionKind/ModuleDetectionKind, so no numeric enum value is ever hardcoded in this file again (matching enums.ts's own stated invariant). Only the enum-key-name -> tsconfig-spelling step (ReactNative -> "react-native", etc.) stays a fixed table, since that's a spelling convention, not a value that could renumber. --- packages/compiler/src/frontend/ts7/enums.ts | 21 ++++++++++++ packages/compiler/src/frontend/ts7/program.ts | 32 +++++++++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/frontend/ts7/enums.ts b/packages/compiler/src/frontend/ts7/enums.ts index 238a65af2..018abe8d6 100644 --- a/packages/compiler/src/frontend/ts7/enums.ts +++ b/packages/compiler/src/frontend/ts7/enums.ts @@ -87,6 +87,27 @@ export const ModuleDetectionKind: ModuleDetectionKindEnum = loadHiddenEnum("moduleDetectionKind", "ModuleDetectionKind"); export type ModuleDetectionKind = number; +/* JsxEmit isn't re-exported from unstable/ast or unstable/sync either — same + * hidden dist/enums placement as ModuleResolutionKind/ModuleDetectionKind + * above. Worth calling out by name: 7.0.2 renumbers React/ReactNative + * relative to 5.9.3 (None=0 Preserve=1 ReactNative=2 React=3 ReactJSX=4 + * ReactJSXDev=5, vs 5.9.3's React=2/ReactNative=3) — exactly the silent-lie + * risk this file's own top comment warns about, so this goes through the + * same symbolic reverse-mapping as everything else here rather than a + * hardcoded positional table. */ +interface JsxEmitEnum { + readonly None: number; + readonly Preserve: number; + readonly React: number; + readonly ReactNative: number; + readonly ReactJSX: number; + readonly ReactJSXDev: number; + readonly [key: string | number]: string | number; +} + +export const JsxEmit: JsxEmitEnum = loadHiddenEnum("jsxEmit", "JsxEmit"); +export type JsxEmit = number; + /** Reverse-maps a numeric enum value to its TS7 key name ("ESNext", * "Bundler") — the spelling tsgo's tsconfig JSON parser accepts (lowercased * by the caller where needed). Symbolic by construction: the name comes from diff --git a/packages/compiler/src/frontend/ts7/program.ts b/packages/compiler/src/frontend/ts7/program.ts index 2230a4053..8238f6b50 100644 --- a/packages/compiler/src/frontend/ts7/program.ts +++ b/packages/compiler/src/frontend/ts7/program.ts @@ -26,7 +26,7 @@ import type { } from "typescript/unstable/sync"; import type { SourceFile } from "typescript/unstable/ast"; import { CheckerFacade } from "./checker.js"; -import { enumKeyOf, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js"; +import { enumKeyOf, JsxEmit, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js"; import { tsgoPath } from "../shared.js"; import { trackedAccessibleEntries, trackedDirectoryExists, trackedFileExists, trackedReadFile, trackedRealpath } from "../input-tracker.js"; @@ -71,15 +71,29 @@ function serializeOptions(options: Ts7CompilerOptions): Record lib.startsWith("lib.") && lib.endsWith(".d.ts") ? lib.slice(4, -5) : lib, ); break; - // TypeScript's JsxEmit enum: None=0, Preserve=1, React=2, - // ReactNative=3, ReactJSX=4, ReactJSXDev=5 — same reverse-mapping - // need as target/module/moduleResolution/moduleDetection above, but - // JsxEmit isn't one of this adapter's mirrored enums, so the string - // table is spelled out directly instead of going through enumKeyOf. - case "jsx": - out[key] = - ["none", "preserve", "react", "react-native", "react-jsx", "react-jsxdev"][value as number] ?? value; + // Same reverse-mapping need as target/module/moduleResolution/ + // moduleDetection above — JsxEmit is one more hidden enum (enums.ts), + // reverse-mapped the same symbolic way rather than a hardcoded + // positional table (7.0.2 renumbers React/ReactNative relative to + // 5.9.3; see enums.ts's JsxEmit comment). Unlike the other enums here, + // JsxEmit's tsconfig spelling isn't a plain lowercase of its key + // (ReactNative -> "react-native", ReactJSX -> "react-jsx", ReactJSXDev + // -> "react-jsxdev") — the enumKeyOf lookup still comes from the + // enum's own symbolic reverse mapping (never a hardcoded number), only + // the KEY-NAME-TO-SPELLING step below is a fixed table. + case "jsx": { + const jsxKey = enumKeyOf(JsxEmit as never, value as number); + const jsxSpelling: Record = { + None: "none", + Preserve: "preserve", + React: "react", + ReactNative: "react-native", + ReactJSX: "react-jsx", + ReactJSXDev: "react-jsxdev", + }; + out[key] = (jsxKey && jsxSpelling[jsxKey]) ?? value; break; + } default: { if (typeof value === "number" && key !== "maxNodeModuleJsDepth") { throw new Error(`ts7 createProgram: unhandled enum-valued compiler option '${key}'`);