projectCompilerOptions
uses node:fs
Compiler options for a project: its tsconfig.json if there is one (lib, strictness, types, ... are the
project's choice), with the options tcm requires forced on top. Warnings name the settings that were overridden.
import { compilerOptions, ENUM_NAMES, REQUIRED, REQUIRED_FOR_IMPORT, tcmNodeTypes } from "#codebase/tcmts/core/program"; import type { CompilerOptions } from "#codebase/tcmts/core/program"; import { compiler } from "#codebase/tcmts/core/ts"; import fs from "node:fs"; import path from "node:path"; import { JsxEmit as ts_JsxEmit } from "typescript/sync"; import type { Diagnostic as ts_Diagnostic } from "typescript/sync"; export function projectCompilerOptions(tsconfig: string | undefined, mode: "scratch" | "import" = "scratch"): { options: CompilerOptions; warnings: string[] } { const required = mode === "import" ? REQUIRED_FOR_IMPORT : REQUIRED; if (!tsconfig || !fs.existsSync(tsconfig)) return { options: { ...compilerOptions(), ...(mode === "import" ? required : {}) }, warnings: [] }; const warnings: string[] = []; let parsed: { options: CompilerOptions; errors?: readonly ts_Diagnostic[] }; try { parsed = compiler().parseConfigFile(tsconfig) as unknown as typeof parsed; } catch (e) { return { options: compilerOptions(), warnings: [`${tsconfig}: ${(e as Error).message.split("\n")[0]}; using tcm's defaults`] }; } // "no inputs were found" is about the project's own build, not tcm's for (const e of (parsed.errors ?? []).filter((e) => e.code !== 18003)) warnings.push(`${tsconfig}: ${e.text}`); const options: CompilerOptions = { skipLibCheck: true, ...parsed.options }; delete options.configFilePath; // without the config file, the compiler would look for @types from its working directory: look from the // project's, as TypeScript does (node_modules/@types in it and each directory above) if (options.typeRoots === undefined) { const roots: string[] = []; for (let d = path.dirname(path.resolve(tsconfig)); ; d = path.dirname(d)) { roots.push(path.join(d, "node_modules", "@types")); if (path.dirname(d) === d) break; } options.typeRoots = roots; } // without its own `types`, a project gets tcm's @types/node, so scratch files typecheck without installing it if (options.types === undefined) Object.assign(options, tcmNodeTypes()); for (const [k, v] of Object.entries(required)) { // in import mode, resolution settings are tcm's business (it renders imports itself): no need to mention them const quiet = mode === "import" && ["module", "moduleResolution", "verbatimModuleSyntax", "erasableSyntaxOnly"].includes(k); if (!quiet && parsed.options[k] !== undefined && parsed.options[k] !== v) { warnings.push(`${path.basename(tsconfig)}: tcm requires \`${k}\` = ${ENUM_NAMES[k] ? ENUM_NAMES[k][v as number] : String(v)}, ignoring the project's setting`); } options[k] = v; } // JSX compiles to the automatic runtime (`react/jsx-runtime`, or the project's jsxImportSource), which generated // modules import by pinned version if (options.jsx !== ts_JsxEmit.ReactJSX && options.jsx !== ts_JsxEmit.ReactJSXDev) { if (options.jsx !== undefined) warnings.push(`${path.basename(tsconfig)}: tcm compiles JSX with the automatic runtime (\`jsx\` = react-jsx), ignoring the project's setting`); options.jsx = ts_JsxEmit.ReactJSX; } // emit options mean nothing here (nothing is emitted), and some are invalid without the others for (const k of ["outDir", "declaration", "declarationMap", "emitDeclarationOnly", "isolatedDeclarations", "composite", "incremental", "tsBuildInfoFile", "sourceMap"]) delete options[k]; return { options, warnings }; }
uses
- tcmts.core.program.CompilerOptions
- tcmts.core.program.ENUM_NAMES
- tcmts.core.program.REQUIRED
- tcmts.core.program.REQUIRED_FOR_IMPORT
- tcmts.core.program.compilerOptions
- tcmts.core.program.tcmNodeTypes
- tcmts.core.ts.compiler