@ainc/script
Version:
Script compiler for typescript
491 lines (395 loc) • 12.5 kB
text/typescript
/**
*****************************************
* 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);
}