diff --git a/package.json b/package.json index 45583ca..292f964 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "up-hooks": "npx simple-git-hooks", "prepare": "pnpm up-hooks", "build": "tsup", - "watch": "nodemon ./src/compiler.ts", + "watch": "nodemon ./src/program/index.ts", "check": "tsc -p tsconfig.check.json", "format": "prettier . --write --ignore-path .gitignore", "lint": "eslint . --fix --ext .js,.cjs,.ts --ignore-path .gitignore", diff --git a/src/program/config.ts b/src/program/config.ts new file mode 100644 index 0000000..5cd4216 --- /dev/null +++ b/src/program/config.ts @@ -0,0 +1,89 @@ +import path from 'path'; +import ts from 'typescript'; +import { errorMessage, ErrorCode } from './error'; +import { formatDiagnostics } from './diagnostic'; +import { getCurrentDirectory, fileExists, readFile } from './util'; + +export interface Options { + configName?: string; + searchPath?: string; + compilerOptions?: ts.CompilerOptions; +} + +export function findConfigFile(options: Options = {}) { + const configName = options.configName ?? 'tsconfig.json'; + const searchPath = options.searchPath ?? getCurrentDirectory(); + const filePath = ts.findConfigFile(searchPath, fileExists, configName); + + // TODO: allow fallback to default config with a warning? + if (!filePath) { + return { + error: errorMessage(ErrorCode.TSCONFIG_FILE_NOT_FOUND, { searchPath, configName }), + }; + } + + return { filePath }; +} + +export function readConfigFile(filePath: string) { + try { + const jsonText = readFile(filePath); + + if (!jsonText) { + return { + error: errorMessage(ErrorCode.TSCONFIG_FILE_NOT_READABLE, { filePath }), + }; + } + + return { filePath, jsonText }; + } catch (_err) { + return { + error: errorMessage(ErrorCode.TSCONFIG_FILE_NOT_READABLE, { filePath }), + }; + } +} + +export function parseConfigFile(fileName: string, jsonText: string, compilerOptions?: ts.CompilerOptions) { + const configObject = ts.parseConfigFileTextToJson(fileName, jsonText); + + if (configObject.error) { + return { error: new Error(formatDiagnostics([configObject.error])) }; + } + + const config = ts.parseJsonConfigFileContent(configObject.config, ts.sys, path.dirname(fileName), compilerOptions); + + if (config.errors.length > 0) { + return { error: new Error(formatDiagnostics(config.errors)) }; + } + + return { fileName, config }; +} +export function loadConfig(options?: Options) { + try { + const configFile = findConfigFile(options); + + if (configFile.error) { + throw configFile.error; + } + + const configFileContent = readConfigFile(configFile.filePath); + + if (configFileContent.error) { + throw configFileContent.error; + } + + const parsedConfig = parseConfigFile( + configFileContent.filePath, + configFileContent.jsonText, + options?.compilerOptions, + ); + + if (parsedConfig.error) { + throw parsedConfig.error; + } + + return { config: parsedConfig }; + } catch (error) { + return { error: error as Error }; + } +} diff --git a/src/program/diagnostic.ts b/src/program/diagnostic.ts new file mode 100644 index 0000000..724860b --- /dev/null +++ b/src/program/diagnostic.ts @@ -0,0 +1,12 @@ +import ts from 'typescript'; +import { getCurrentDirectory, newLine } from './util'; + +export const diagnosticsHost: ts.FormatDiagnosticsHost = { + getNewLine: () => newLine, + getCurrentDirectory: () => getCurrentDirectory(), + getCanonicalFileName: (fileName: string) => fileName, +}; + +export function formatDiagnostics(diagnostics: ts.Diagnostic[]) { + return ts.formatDiagnostics(diagnostics, diagnosticsHost); +} diff --git a/src/program/error.ts b/src/program/error.ts new file mode 100644 index 0000000..1307beb --- /dev/null +++ b/src/program/error.ts @@ -0,0 +1,19 @@ +export enum ErrorCode { + TSCONFIG_FILE_NOT_FOUND, + TSCONFIG_FILE_NOT_READABLE, +} + +export const errorMessages: Record = { + [ErrorCode.TSCONFIG_FILE_NOT_FOUND]: 'TS config file not found. File name: {configName} - Search path: {searchPath}', + [ErrorCode.TSCONFIG_FILE_NOT_READABLE]: 'TS config file not readable or empty. File path: {filePath}', +}; + +export function errorMessage(code: ErrorCode, data?: Record) { + let message = errorMessages[code]; + + Object.entries(data ?? {}).forEach(([index, value]) => { + message = message.replace(new RegExp(`{${index}}`, 'g'), String(value)); + }); + + return new Error(message); +} diff --git a/src/program/index.ts b/src/program/index.ts new file mode 100644 index 0000000..7afc310 --- /dev/null +++ b/src/program/index.ts @@ -0,0 +1,8 @@ +import { loadConfig } from './config'; + +const config = loadConfig({ + configName: 'tsconfig.check.json', +}); + +// eslint-disable-next-line no-console +console.log(config); diff --git a/src/program/util.ts b/src/program/util.ts new file mode 100644 index 0000000..4f8dd28 --- /dev/null +++ b/src/program/util.ts @@ -0,0 +1,15 @@ +import ts from 'typescript'; + +export const newLine = '\n'; + +export function getCurrentDirectory() { + return ts.sys.getCurrentDirectory(); +} + +export function fileExists(fileName: string) { + return ts.sys.fileExists(fileName); +} + +export function readFile(fileName: string) { + return ts.sys.readFile(fileName); +}