import fs from 'fs-extra'; import kleur from 'kleur'; import type { CompilerOptions, Diagnostic, Program, SourceFile, } from 'typescript'; import ts from 'typescript'; import upath from 'upath'; import { log, LogLevel, reportDiagnosticByLine } from '../../../utils/log.js'; import { isUndefined } from '../../../utils/unit.js'; /** * The most general version of compiler options. */ const defaultOptions: CompilerOptions = { noEmitOnError: false, allowJs: true, experimentalDecorators: true, target: ts.ScriptTarget.ES2019, downlevelIteration: true, module: ts.ModuleKind.ESNext, strictNullChecks: true, moduleResolution: ts.ModuleResolutionKind.NodeJs, esModuleInterop: true, noEmit: true, pretty: true, allowSyntheticDefaultImports: true, allowUnreachableCode: true, allowUnusedLabels: true, skipLibCheck: true, }; export function readTsConfigFile(root: string): CompilerOptions | undefined { const configPath = ts.findConfigFile( root, ts.sys.fileExists, 'tsconfig.json', ); log( () => !isUndefined(configPath) ? `Using TS config file: ${configPath}` : `Could not find TS config file from: ${root} [using default]`, LogLevel.Info, ); const tsConfig = !isUndefined(configPath) ? ts.readConfigFile(configPath!, ts.sys.readFile).config : undefined; return tsConfig; } export interface CompileResult { program: Program; files: SourceFile[]; } export function compileOnce( filePaths: string[], options: CompilerOptions = defaultOptions, ): Program { return ts.createProgram(filePaths, options); } const tsDiagnosticCategoryToLogLevel: Record = { [ts.DiagnosticCategory.Warning]: LogLevel.Warn, [ts.DiagnosticCategory.Error]: LogLevel.Error, [ts.DiagnosticCategory.Message]: LogLevel.Info, [ts.DiagnosticCategory.Suggestion]: LogLevel.Info, }; function reportDiagnostic(diagnostic: Diagnostic) { const sourceFile = diagnostic.file; const message = ts.flattenDiagnosticMessageText( diagnostic.messageText, ts.sys.newLine, ); const logLevel = tsDiagnosticCategoryToLogLevel[diagnostic.category]; const pos = diagnostic.start ? sourceFile?.getLineAndCharacterOfPosition(diagnostic.start) : undefined; if (isUndefined(sourceFile) || isUndefined(pos)) { log(message, logLevel); } else { reportDiagnosticByLine(message, sourceFile, pos.line + 1, logLevel); } } function reportWatchStatusChanged(diagnostic: Diagnostic) { log( `[${kleur.yellow(diagnostic.code)}] ${diagnostic.messageText}`, LogLevel.Info, ); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function compileAndWatch( root: string, configFileName: string, options: CompilerOptions = defaultOptions, onProgramCreate: (program: Program) => void | Promise, ) { const host = ts.createWatchCompilerHost( configFileName, { ...options, baseUrl: root, }, ts.sys, ts.createSemanticDiagnosticsBuilderProgram, reportDiagnostic, reportWatchStatusChanged, ); const postProgramCreateRef = host.afterProgramCreate; host.afterProgramCreate = builderProgram => { const program = builderProgram.getProgram(); onProgramCreate(program); postProgramCreateRef!(builderProgram); }; return ts.createWatchProgram(host); } export async function transpileModuleOnce(filePath: string): Promise { const sourceText = (await fs.readFile(filePath)).toString(); const transpiledResult = ts.transpileModule(sourceText, { compilerOptions: defaultOptions, }); const tmpDir = upath.resolve( process.cwd(), 'node_modules/@celement/cli/.temp', ); if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir); } const transpiledFilePath = upath.resolve(tmpDir, 'config.mjs'); await fs.writeFile(transpiledFilePath, transpiledResult.outputText); try { return ( (await import(transpiledFilePath + `?t=${Date.now()}`))?.default ?? [] ); } catch (e) { return {}; } }