import path from 'node:path' import { inspect } from 'node:util' import type { createProgram, parseJsonConfigFileContent, readConfigFile, sys, CompilerOptions as TSCompilerOptions, } from 'typescript' export type { TSCompilerOptions } interface TSModule { readConfigFile: typeof readConfigFile parseJsonConfigFileContent: typeof parseJsonConfigFileContent sys: typeof sys createProgram: typeof createProgram } /** * TypeScript helpers. This is a class so that we can dynamically import the TypeScript module * to reduce runx start time for commands that don't use the typescript package. * * @example * * ```ts * const tsHelpers = await new TSHelpers().init() * const { ts } = tsHelpers * const tsConfig = tsHelpers.getTSConfig() * ts.createProgram(entryPoints, tsConfig).emit() * ``` */ export class TSHelpers { #ts: TSModule | undefined public get ts(): TSModule { if (!this.#ts) { throw new Error('TSHelpers not initialized. Call init() first.') } return this.#ts } async init(): Promise { this.#ts = (await import('typescript')) as TSModule return this } getTSConfig(configPath = 'tsconfig.json'): TSCompilerOptions { const absolutePath = path.resolve(configPath) const configFile = this.ts.readConfigFile(absolutePath, (p) => this.ts.sys.readFile(p)) if (configFile.error) { throw new Error(`Failed to read tsconfig: ${inspect(configFile.error)}`) } const parsed = this.ts.parseJsonConfigFileContent( configFile.config, this.ts.sys, path.dirname(absolutePath) ) if (parsed.errors.length > 0) { throw new Error(`Failed to parse tsconfig: ${inspect(parsed.errors)}`) } return parsed.options } }