UNPKG

@ainc/script

Version:

Script compiler for typescript

491 lines (395 loc) 12.5 kB
/** ***************************************** * Created by edonet@163.com * Created on 2021-06-28 22:47:29 ***************************************** */ 'use strict'; /** ***************************************** * 加载依赖 ***************************************** */ import * as ts from 'typescript'; import { resolve } from 'path'; import { log, error } from '@ainc/stdout'; import { stat } from '@ainc/fs/dist/stat'; import { rmdir } from '@ainc/fs/dist/rmdir'; import { dirname } from '@ainc/fs/dist/dirname'; import { isBuiltinModule } from './helpers/isBuiltinModule'; import { transformer as transformModulePaths } from './helpers/module-paths-transformer'; import { transformer as transformModuleExport} from './helpers/module-export-transformer'; /** ***************************************** * 清空屏幕 ***************************************** */ const clearScreen = ts.sys.clearScreen || console.clear; /** ***************************************** * 编译环境变量 ***************************************** */ const env = { watch: false }; /** ***************************************** * 诊断信息格式服务 ***************************************** */ const formatHost: ts.FormatDiagnosticsHost = { getCanonicalFileName: path => path, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine, }; /** ***************************************** * 格式化信息 ***************************************** */ function printMessage(message: unknown): void { // 测试环境不打印 if (process.env.NODE_ENV === 'test') { return; } // 打印信息 if (env.watch) { log(`\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m`, message); } else { log(message); } } /** ***************************************** * 打印错误信息 ***************************************** */ function printDiagnostic(diagnostic: ts.Diagnostic): void { printDiagnostics([diagnostic]); } /** ***************************************** * 打印多条错误信息 ***************************************** */ function printDiagnostics(diagnostics: readonly ts.Diagnostic[]): void { error(ts.formatDiagnosticsWithColorAndContext(diagnostics, formatHost)); } /** ***************************************** * 打印错误信息且展示总结信息 ***************************************** */ function printDiagnosticsAndSummaryInfo(diagnostics: readonly ts.Diagnostic[]): void { const diags = ts.sortAndDeduplicateDiagnostics(diagnostics); // 打印错误 printDiagnostics(diags); // 打印错误总数 printMessage(`Found ${diags.length} errors.`); } /** ***************************************** * 打印监听状态 ***************************************** */ function printWatchHostStatus(diagnostic: ts.Diagnostic): void { const { code } = diagnostic; // 清空屏幕 if (code === 6031 || code === 6032) { clearScreen(); } // 打印信息 diagnostic.file ? printDiagnostic(diagnostic) : printMessage(diagnostic.messageText || ''); } /** ***************************************** * 配置 ***************************************** */ interface Configuration { context: string; file: string; source: string; optionsToExtend: ts.CompilerOptions; watchOptionsToExtend?: ts.WatchOptions; } /** ***************************************** * 解析配置文件 ***************************************** */ function resolveConfigFile(dir: string): void | string { while (dir) { const stats = stat(resolve(dir, 'tsconfig.json')); // 存在文件 if (stats && stats.isFile()) { return stats.path; } // 更新目录 dir = dirname(dir); } } /** ***************************************** * 解析配置 ***************************************** */ function parseConfiguration(opts: Options): Configuration { const context = resolve(opts.context || './'); const include = opts.include.length ? opts.include : undefined; const exclude = opts.exclude; const configFile = resolveConfigFile(context); // 返回配置 return { context, file: resolve(context, '.esconfig.json'), source: JSON.stringify({ include, exclude, extends: configFile }), optionsToExtend: { noEmitOnError: true }, }; } /** ***************************************** * 解析命令行参数 ***************************************** */ function parseCommandLine(argv?: string[]): ts.ParsedCommandLine { // 未指定参数 if (!argv || !argv.length) { return { errors: [], options: {}, fileNames: [], }; } // 执行解析 return ts.parseCommandLine(argv); } /** ***************************************** * 创建编译文件系统 ***************************************** */ function createFileSystem(config: Configuration): ts.System { const fs = ts.sys; const sys: ts.System = Object.create(fs); // 获取当前文件目录 sys.getCurrentDirectory = () => config.context; // 更新文件存在接口 sys.fileExists = (path) => { return path === config.file ? true : fs.fileExists(path); }; // 更新读取文件接口 sys.readFile = (path, encoding) => { return path === config.file ? config.source : fs.readFile(path, encoding); }; // 返回结果 return sys; } /** ***************************************** * 创建写入回调 ***************************************** */ function createWriteFileCallback(options: ts.CompilerOptions): ts.WriteFileCallback { let writeFile: ts.WriteFileCallback = (path, data, writeByteOrderMark) => { // 清空目录 options.outDir && rmdir(options.outDir); // 更新写入函数 writeFile = ts.sys.writeFile; // 执行写入 return writeFile(path, data, writeByteOrderMark); }; // 返回函数 return (path, data, writeByteOrderMark) => writeFile(path, data, writeByteOrderMark); } /** ***************************************** * 解析模块列表 ***************************************** */ function resolveModuleNames(this: ts.CompilerHost, moduleNames: string[], containingFile: string, _?: string[], redirectedReference?: ts.ResolvedProjectReference, options?: ts.CompilerOptions): (ts.ResolvedModule | undefined)[] { return moduleNames.map(moduleName => { if (!isBuiltinModule(moduleName)) { const resolved = ts.resolveModuleName( moduleName, containingFile, options || {}, this, undefined, redirectedReference, ); // 返回解析结果 return resolved.resolvedModule; } }); } /** ***************************************** * 编译配置 ***************************************** */ interface CompilerConfiguration extends ts.ParsedCommandLine { context: string; file: string; sys: ts.System; } /** ***************************************** * 解析配置 ***************************************** */ function parseCompilerConfiguration(config: Configuration): CompilerConfiguration | void { const sys = createFileSystem(config); const host = Object.create(sys) as ts.ParseConfigFileHost; // 添加错误捕捉方法 host.onUnRecoverableConfigFileDiagnostic = printDiagnostic; // 执行解析 const result = ts.getParsedCommandLineOfConfigFile( config.file, config.optionsToExtend, host, undefined, config.watchOptionsToExtend, undefined, ); // 解析失败 if (!result) { return; } // 获取配置 const opts = result.options; // 格式化输出目录 if (opts && opts.outDir) { opts.outDir = resolve(config.context, opts.outDir); } // 更新写入函数 sys.writeFile = createWriteFileCallback(opts); // 返回结果 return Object.assign(result, { sys, file: config.file, context: config.context }); } /** ***************************************** * 分发应用文件 ***************************************** */ function emitProgramSourceFile(program: ts.Program | ts.BuilderProgram, sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): void { const compilerOptions = program.getCompilerOptions(); const diagnostics: ts.Diagnostic[] = []; // 获取诊断信息 diagnostics.push(...program.getConfigFileParsingDiagnostics()); diagnostics.push(...program.getOptionsDiagnostics(cancellationToken)); diagnostics.push(...program.getSyntacticDiagnostics(sourceFile, cancellationToken)); diagnostics.push(...program.getGlobalDiagnostics(cancellationToken)); diagnostics.push(...program.getSemanticDiagnostics(sourceFile, cancellationToken)); diagnostics.push(...program.getDeclarationDiagnostics(sourceFile, cancellationToken)); // 存在诊断信息 if (diagnostics.length) { return printDiagnosticsAndSummaryInfo(diagnostics); } // 分发文件 const result = program.emit( sourceFile, undefined, cancellationToken, compilerOptions.emitDeclarationOnly, { before: [transformModulePaths], after: [transformModuleExport], }, ); // 显示诊断信息 if (result.diagnostics.length) { return printDiagnosticsAndSummaryInfo(result.diagnostics); } // 打印日志 printMessage('Found 0 errors.'); } /** ***************************************** * 监听文件服务器 ***************************************** */ interface WatchCompilerHost extends ts.WatchCompilerHostOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> { configFileParsingResult?: ts.ParsedCommandLine; } /** ***************************************** * 监听文件变更 ***************************************** */ function watch(config: CompilerConfiguration): void { const host: WatchCompilerHost = ts.createWatchCompilerHost( config.file, config.options, config.sys, ts.createSemanticDiagnosticsBuilderProgram, printDiagnostic, printWatchHostStatus ); // 配置配置结果 host.configFileParsingResult = config; // 更新模块解析函数 host.resolveModuleNames = resolveModuleNames; // 重写应用函数 host.afterProgramCreate = emitProgramSourceFile; // 创建监听应用 ts.createWatchProgram(host); } /** ***************************************** * 编译文件 ***************************************** */ function build(config: CompilerConfiguration): void { const host = ts.createCompilerHost(config.options); // 更新模块解析函数 host.resolveModuleNames = resolveModuleNames; // 更新定入函数 host.writeFile = config.sys.writeFile; // 更新获取当前目录 host.getCurrentDirectory = config.sys.getCurrentDirectory; // 打印信息 printMessage('Starting compilation...'); // 创建应用并分发文件 emitProgramSourceFile(ts.createProgram(config.fileNames, config.options, host)); } /** ***************************************** * 配置 ***************************************** */ export interface Options { context?: string; watch?: boolean; argv?: string[]; include: string[]; exclude?: string[]; } /** ***************************************** * 编译项目 ***************************************** */ export function compile(options: Options): void { const opts: Configuration = parseConfiguration(options); const argv = parseCommandLine(options.argv); // 解析命令行失败 if (argv.errors.length) { return printDiagnostics(argv.errors); } // 合并配置 opts.optionsToExtend = { ...argv.options, noEmitOnError: true }; opts.watchOptionsToExtend = argv.watchOptions; // 创建文件系统 const config = parseCompilerConfiguration(opts); // 解析失败 if (!config) { return; } // 存在解析错误 if (config.errors.length) { return printDiagnostics(config.errors); } // 启动服务 env.watch = !!options.watch; env.watch ? watch(config) : build(config); }