diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 4dbe00a2..d5f44bd4 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/enums.ts b/packages/compiler/src/frontend/ts7/enums.ts index 238a65af..018abe8d 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 f319016f..8238f6b5 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,6 +71,29 @@ function serializeOptions(options: Ts7CompilerOptions): Record lib.startsWith("lib.") && lib.endsWith(".d.ts") ? lib.slice(4, -5) : lib, ); break; + // 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}'`);