projectCompilerOptions

tcmts.core.program.projectCompilerOptions · #e24o4omi · term · runs on the server

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

used by