738 lines
21 KiB
TypeScript
738 lines
21 KiB
TypeScript
import chokidar from "chokidar";
|
||
import ts, { CompilerOptions, ProjectReference } from "typescript";
|
||
import pathLib from "path";
|
||
import dayjs from "dayjs";
|
||
import fs from "fs";
|
||
import { cloneDeep } from "lodash";
|
||
|
||
declare const require: NodeRequire;
|
||
declare const process: NodeJS.Process;
|
||
|
||
export type LogFormatterProp = {
|
||
level: "info" | "warn" | "error";
|
||
caller: NodeJS.CallSite | null;
|
||
args: any[];
|
||
};
|
||
|
||
export type LogFormatter = (props: LogFormatterProp) => any[];
|
||
|
||
export type WatchConfig = {
|
||
/**
|
||
* 是否启用polyfill
|
||
*/
|
||
polyfill?: {
|
||
/**
|
||
* 是否启用console polyfill
|
||
*/
|
||
console?: {
|
||
/**
|
||
* 是否启用
|
||
*/
|
||
enable?: boolean;
|
||
/**
|
||
* 是否打印调用堆栈信息
|
||
*/
|
||
trace?: boolean;
|
||
/**
|
||
* 格式化函数
|
||
*/
|
||
formatter: LogFormatter;
|
||
};
|
||
};
|
||
/**
|
||
* 生命周期钩子
|
||
*/
|
||
lifecycle?: {
|
||
/**
|
||
* 初始化时调用
|
||
* @returns void
|
||
*/
|
||
onInit?: () => void;
|
||
/**
|
||
* 服务启动时调用
|
||
* @returns void
|
||
*/
|
||
onServerStart?: () => void;
|
||
/**
|
||
* 编译前调用
|
||
* @returns void
|
||
*/
|
||
onBeforeCompile?: () => void;
|
||
/**
|
||
* 编译后调用
|
||
* @returns void
|
||
*/
|
||
onAfterCompile?: () => void;
|
||
/**
|
||
* 服务关闭时调用
|
||
* @returns void
|
||
*/
|
||
onServerShutdown?: () => void;
|
||
/**
|
||
* 销毁监视器时调用
|
||
* @returns void
|
||
*/
|
||
onDispose?: () => void;
|
||
};
|
||
};
|
||
|
||
// 真实config不会出现?,所以这里新增一个type表示真实的config
|
||
type DeepRequiredIfPresent<T> = {
|
||
[P in keyof T]-?: NonNullable<T[P]> extends (...args: any) => any // Check for function type
|
||
? T[P] // Preserve the original function type
|
||
: NonNullable<T[P]> extends (infer U)[]
|
||
? DeepRequiredIfPresent<U>[]
|
||
: NonNullable<T[P]> extends object
|
||
? DeepRequiredIfPresent<NonNullable<T[P]>>
|
||
: T[P] extends undefined | null
|
||
? T[P]
|
||
: NonNullable<T[P]>;
|
||
};
|
||
|
||
export type RealWatchConfig = DeepRequiredIfPresent<WatchConfig>;
|
||
|
||
const defaultConfig: RealWatchConfig = {
|
||
polyfill: {
|
||
console: {
|
||
enable: true,
|
||
trace: true,
|
||
formatter: ({ level, caller, args }) => {
|
||
const getFileInfo = () => {
|
||
if (!caller) {
|
||
return "";
|
||
}
|
||
const fileInfo = caller
|
||
? `${caller.getFileName()}:${caller.getLineNumber()}`
|
||
: "";
|
||
return fileInfo.trim();
|
||
};
|
||
const getTime = () => dayjs().format("YYYY-MM-DD HH:mm:ss.SSS");
|
||
const infoStart = "\x1B[36m[ Info";
|
||
const warnStart = "\x1B[33m[ Warn";
|
||
const errorStart = "\x1B[31m[ Error";
|
||
const clearColor = caller ? "]\x1B[0m\n" : "]\x1B[0m";
|
||
const levelStart =
|
||
level === "info"
|
||
? infoStart
|
||
: level === "warn"
|
||
? warnStart
|
||
: errorStart;
|
||
return [
|
||
levelStart,
|
||
getTime(),
|
||
getFileInfo(),
|
||
clearColor,
|
||
...args,
|
||
];
|
||
},
|
||
},
|
||
},
|
||
lifecycle: {
|
||
onInit: () => {
|
||
console.log("----> Watcher initialized.");
|
||
},
|
||
onServerStart: () => {
|
||
console.log("----> Server started.");
|
||
},
|
||
onBeforeCompile: () => {
|
||
console.log("----> Compiling......");
|
||
},
|
||
onAfterCompile: () => {
|
||
console.log("----> Compile completed.");
|
||
},
|
||
onServerShutdown: () => {
|
||
console.log("----> Server shutdown.");
|
||
},
|
||
onDispose: () => {
|
||
console.log("----> Watcher disposed.");
|
||
},
|
||
},
|
||
};
|
||
|
||
const getOverrideConfig = (config?: WatchConfig): RealWatchConfig => {
|
||
// 遍历default里面的每一个key,如果config里面有,就覆盖,没有就用default的
|
||
const overrideInner = (obj: any, dobj: any): any => {
|
||
const result: any = {};
|
||
Object.keys(dobj).forEach((key) => {
|
||
const value = dobj[key];
|
||
if (typeof value === "object") {
|
||
const v = obj[key];
|
||
if (v === undefined) {
|
||
result[key] = value;
|
||
} else {
|
||
result[key] = overrideInner(v, value);
|
||
}
|
||
} else {
|
||
if (obj && obj[key] !== undefined) {
|
||
result[key] = obj[key];
|
||
} else {
|
||
result[key] = value;
|
||
}
|
||
}
|
||
});
|
||
return result;
|
||
};
|
||
return config
|
||
? (overrideInner(config, defaultConfig) as RealWatchConfig)
|
||
: defaultConfig;
|
||
};
|
||
|
||
type AliasConfig = Record<string, string | string[]>;
|
||
type ModuleType = import('module')
|
||
|
||
/**
|
||
* 根据 alias 配置表将路径中的别名替换为真实路径
|
||
* @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
|
||
* @param aliasConfig - alias 配置表,key 为别名,value 为对应的真实路径或路径数组
|
||
* @returns 替换后的路径,如果没有匹配到 alias,则返回原始路径
|
||
*/
|
||
function replaceAliasWithPath(path: string, aliasConfig: Record<string, string | string[]>): string {
|
||
for (const [alias, targets] of Object.entries(aliasConfig)) {
|
||
// If alias ends with "*", handle it as a dynamic alias.
|
||
if (alias.endsWith('*')) {
|
||
// Create a regex pattern that matches paths starting with the alias, followed by any characters
|
||
const aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)'
|
||
|
||
const match = path.match(aliasPattern);
|
||
if (match) {
|
||
// Replace the alias with the target path, appending the matched part from the original path
|
||
const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
|
||
// Ensure that the target path ends with a slash if it's not already
|
||
const replacedPath = target.replace(/\/\*$/, "/") + match[1];
|
||
return replacedPath;
|
||
}
|
||
} else {
|
||
// Handle static alias without "*" by directly matching the path
|
||
if (path.startsWith(alias)) {
|
||
const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
|
||
// Replace the alias part with the target path
|
||
return path.replace(alias, target);
|
||
}
|
||
}
|
||
}
|
||
|
||
// If no alias matches, return the original path
|
||
return path;
|
||
}
|
||
|
||
export const watch = (
|
||
projectPath: string,
|
||
config?: WatchConfig
|
||
): Promise<() => Promise<void>> => {
|
||
const realConfig = getOverrideConfig(config);
|
||
const enableTrace = !!process.env.ENABLE_TRACE;
|
||
|
||
// 查找配置文件
|
||
const configFileName = ts.findConfigFile(
|
||
projectPath,
|
||
ts.sys.fileExists,
|
||
"tsconfig.build.json"
|
||
);
|
||
|
||
if (!configFileName) {
|
||
throw new Error("Could not find a valid 'tsconfig.build.json'.");
|
||
}
|
||
|
||
// 读取配置文件
|
||
const configFile = ts.readConfigFile(configFileName, ts.sys.readFile);
|
||
|
||
// 解析配置文件内容
|
||
const { options, projectReferences } = ts.parseJsonConfigFileContent(
|
||
configFile.config,
|
||
ts.sys,
|
||
pathLib.dirname(configFileName)
|
||
);
|
||
|
||
const aliasConfig: AliasConfig = cloneDeep(options.paths) || {};
|
||
|
||
// 输出原始配置
|
||
// console.log("[DEBUG] Original alias config:", aliasConfig);
|
||
|
||
Object.keys(aliasConfig).forEach((key) => {
|
||
const value = aliasConfig[key];
|
||
// 替换src
|
||
aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v) => v.replace("src", "lib"));
|
||
});
|
||
|
||
// 输出真实的alias配置
|
||
console.debug("[DEBUG] Running Alias config:", aliasConfig);
|
||
|
||
const createProgramAndSourceFile = (path: string, options: CompilerOptions, projectReferences: readonly ProjectReference[] | undefined) => {
|
||
const program = ts.createProgram({
|
||
rootNames: [path],
|
||
options,
|
||
projectReferences,
|
||
});
|
||
const sourceFile = program.getSourceFile(path);
|
||
// 是否有语法错误
|
||
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
|
||
if (diagnostics.length) {
|
||
const syntaxErrors = diagnostics.filter(
|
||
(diagnostic) =>
|
||
diagnostic.category === ts.DiagnosticCategory.Error
|
||
);
|
||
|
||
if (syntaxErrors.length) {
|
||
console.error(`Error in ${path}`);
|
||
syntaxErrors.forEach((diagnostic) => {
|
||
console.error(
|
||
`${ts.flattenDiagnosticMessageText(
|
||
diagnostic.messageText,
|
||
"\n"
|
||
)}`
|
||
);
|
||
});
|
||
console.error(`文件存在语法错误,请检查修复后重试!`);
|
||
return { program, sourceFile, diagnostics };
|
||
}
|
||
}
|
||
|
||
return { program, sourceFile, diagnostics };
|
||
}
|
||
|
||
// 这个函数用于解决require的时候,如果文件不存在,会尝试编译ts文件
|
||
// 测试:在src下新建一个ts文件,然后删除lib下的js文件,然后require这个文件,会发现会自动编译ts文件
|
||
const polyfillLoader = () => {
|
||
const BuiltinModule = require("module");
|
||
// 模拟环境下的 module 构造函数
|
||
const Module = module.constructor.length > 1 ? module.constructor : BuiltinModule;
|
||
|
||
// 保存原始 _resolveFilename 方法
|
||
const oldResolveFilename = Module._resolveFilename;
|
||
|
||
// 通用的路径检查与编译函数
|
||
function resolveAndCompile(requestPath: string, parent: ModuleType, options: CompilerOptions, projectReferences: readonly ts.ProjectReference[] | undefined) {
|
||
const jsPath = pathLib.resolve(requestPath + ".js");
|
||
const tsPath = jsPath.replace(/\.js$/, ".ts").replace(
|
||
pathLib.join(projectPath, "lib"),
|
||
pathLib.join(projectPath, "src")
|
||
);
|
||
|
||
// 检查并编译 .ts 文件
|
||
if (fs.existsSync(tsPath)) {
|
||
console.log(`[resolve] Found TypeScript source file: ${tsPath}, attempting to compile...`);
|
||
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(
|
||
tsPath,
|
||
options,
|
||
projectReferences
|
||
);
|
||
|
||
if (diagnostics.length) {
|
||
console.error(`[resolve] Compilation failed for: ${tsPath}`);
|
||
throw new Error("TypeScript compilation error");
|
||
}
|
||
|
||
const emitResult = program.emit(sourceFile);
|
||
|
||
if (emitResult.emitSkipped) {
|
||
console.error(`[resolve] Emit skipped for: ${tsPath}`);
|
||
throw new Error("TypeScript emit skipped");
|
||
}
|
||
|
||
console.log(`[resolve] Successfully compiled: ${tsPath}`);
|
||
return jsPath;
|
||
}
|
||
|
||
// 如果没有找到对应的 .ts 文件
|
||
if (fs.existsSync(jsPath)) {
|
||
return jsPath;
|
||
}
|
||
|
||
throw new Error(`[resolve] Unable to find module: ${requestPath}`);
|
||
}
|
||
|
||
// 处理文件夹导入的情况
|
||
function resolveDirectory(requestPath: string, parent: ModuleType, options: CompilerOptions, projectReferences: readonly ts.ProjectReference[] | undefined) {
|
||
const indexJs = pathLib.join(requestPath, "index.js");
|
||
const indexTs = pathLib.join(requestPath, "index.ts").replace(
|
||
pathLib.join(projectPath, "lib"),
|
||
pathLib.join(projectPath, "src")
|
||
);
|
||
|
||
if (fs.existsSync(indexTs)) {
|
||
console.log(`[resolve] Found TypeScript index file: ${indexTs}, attempting to compile...`);
|
||
return resolveAndCompile(indexTs, parent, options, projectReferences);
|
||
}
|
||
|
||
if (fs.existsSync(indexJs)) {
|
||
return indexJs;
|
||
}
|
||
|
||
throw new Error(`[resolve] No index file found in directory: ${requestPath}`);
|
||
}
|
||
|
||
// 重写 _resolveFilename 方法
|
||
Module._resolveFilename = function (
|
||
request: string, // 模块请求路径
|
||
parent: ModuleType, // 调用方模块
|
||
isMain: boolean, // 是否是主模块
|
||
rFoptions: object | undefined // 解析选项
|
||
) {
|
||
let resolvedRequest = request;
|
||
const replacedPath = replaceAliasWithPath(request, aliasConfig);
|
||
|
||
if (replacedPath !== request) {
|
||
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
|
||
resolvedRequest = pathLib.join(projectPath, replacedPath);
|
||
}
|
||
|
||
try {
|
||
return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
|
||
} catch (error: any) {
|
||
if (error.code === "MODULE_NOT_FOUND") {
|
||
const requestPath = pathLib.resolve(pathLib.dirname(parent.filename), resolvedRequest);
|
||
|
||
// 处理文件夹导入
|
||
if (fs.existsSync(requestPath) && fs.statSync(requestPath).isDirectory()) {
|
||
return resolveDirectory(requestPath, parent, options, projectReferences);
|
||
}
|
||
|
||
// 处理单文件导入
|
||
return resolveAndCompile(requestPath, parent, options, projectReferences);
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
};
|
||
};
|
||
|
||
polyfillLoader();
|
||
|
||
//polyfill console.log 添加时间
|
||
const polyfillConsole = (trace: boolean) => {
|
||
// 获取调用堆栈信息
|
||
const getCallerInfo = (): NodeJS.CallSite | null => {
|
||
if (!trace) {
|
||
return null;
|
||
}
|
||
const originalFunc = Error.prepareStackTrace;
|
||
let callerInfo: NodeJS.CallSite | null = null;
|
||
try {
|
||
const err = new Error();
|
||
Error.prepareStackTrace = (err, stack) => stack;
|
||
const stack = err.stack as unknown as NodeJS.CallSite[]; // Type assertion here
|
||
const currentFile = stack[0].getFileName();
|
||
|
||
for (let i = 1; i < stack.length; i++) {
|
||
// Start from index 1
|
||
const callSite = stack[i];
|
||
if (currentFile !== callSite.getFileName()) {
|
||
callerInfo = callSite;
|
||
break;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
Error.prepareStackTrace = originalFunc;
|
||
return callerInfo;
|
||
};
|
||
// polyfill console.log 添加时间和文件位置
|
||
["info", "warn", "error"].forEach((level: string) => {
|
||
const levelStr = level as "info" | "warn" | "error";
|
||
const oldFunc = console[levelStr];
|
||
console[levelStr] = function (...args) {
|
||
oldFunc(
|
||
...(defaultConfig.polyfill?.console?.formatter({
|
||
level: levelStr,
|
||
caller: getCallerInfo(),
|
||
args,
|
||
}) || [])
|
||
);
|
||
};
|
||
});
|
||
};
|
||
|
||
realConfig.polyfill.console.enable && polyfillConsole(enableTrace);
|
||
|
||
// 这里注意要在require之前,因为require会触发编译
|
||
const { startup } = require('./start') as {
|
||
startup: (pwd: string, connector: any) => Promise<() => Promise<void>>;
|
||
}
|
||
|
||
// 如果lib目录是空的,则直接编译所有的ts文件
|
||
const serverConfigFile = pathLib.join(projectPath, "lib/configuration/server.js");
|
||
if (!fs.existsSync(serverConfigFile)) {
|
||
// 尝试编译src/configuration/server.ts
|
||
console.log(`[watch] Server configuration file not found, attempting to compile the project......`);
|
||
const tryCompile = (tsFile: string) => {
|
||
console.log(`[watch] Compiling: ${tsFile}`);
|
||
if (fs.existsSync(tsFile)) {
|
||
const { program, diagnostics } = createProgramAndSourceFile(tsFile, options, projectReferences);
|
||
if (diagnostics.length) {
|
||
console.error(`Error in ${tsFile}`);
|
||
diagnostics.forEach((diagnostic) => {
|
||
console.error(
|
||
`${ts.flattenDiagnosticMessageText(
|
||
diagnostic.messageText,
|
||
"\n"
|
||
)}`
|
||
);
|
||
});
|
||
console.error(`文件存在语法错误,请检查修复后重试!`);
|
||
process.exit(1);
|
||
}
|
||
// const emitResult = program.emit(sourceFile);
|
||
// 编译所有的文件
|
||
const emitResult = program.emit();
|
||
|
||
if (emitResult.emitSkipped) {
|
||
console.error(`Emit failed for ${tsFile}!`);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(`Emit succeeded. ${tsFile} has been compiled.`);
|
||
}
|
||
}
|
||
// 所有要编译的目录
|
||
// 其他涉及到的目录会在运行的时候自动编译,这里主要处理的是动态require的文件
|
||
const compileFiles = [
|
||
"src/configuration/index.ts",
|
||
"src/aspects/index.ts",
|
||
"src/checkers/index.ts",
|
||
"src/triggers/index.ts",
|
||
"src/timers/index.ts",
|
||
"src/routines/start.ts",
|
||
"src/watchers/index.ts",
|
||
"src/endpoints/index.ts",
|
||
"src/data/index.ts",
|
||
"src/ports/index.ts",
|
||
];
|
||
compileFiles.forEach(tryCompile);
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
realConfig.lifecycle.onInit();
|
||
let shutdown: (() => Promise<void>) | undefined;
|
||
|
||
const restart = async () => {
|
||
if (shutdown) {
|
||
console.log("----> Shutting down service......");
|
||
await shutdown().then(realConfig.lifecycle.onServerShutdown);
|
||
// reset shutdown
|
||
shutdown = undefined;
|
||
}
|
||
console.warn("----> Clearing require cache of project......");
|
||
let deleteCount = 0;
|
||
// 清空lib以下目录的缓存
|
||
Object.keys(require.cache).forEach((key) => {
|
||
// 如果不是项目目录下的文件,不删除
|
||
if (!key.startsWith(projectPath)) {
|
||
return;
|
||
} else if (
|
||
key.includes("lib") &&
|
||
!key.includes("node_modules")
|
||
) {
|
||
delete require.cache[key];
|
||
deleteCount++;
|
||
}
|
||
});
|
||
console.warn(
|
||
`----> ${deleteCount} modules has been removed from require.cache.`
|
||
);
|
||
const pwd = process.cwd();
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||
const simpleConnector = require(pathLib.join(
|
||
projectPath,
|
||
"lib/config/connector"
|
||
)).default;
|
||
console.warn("----> Starting service......");
|
||
shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
|
||
realConfig.lifecycle.onServerStart();
|
||
return shutdown;
|
||
});
|
||
};
|
||
|
||
const watchSourcePath = pathLib.join(projectPath, "src");
|
||
|
||
console.log("Watching for changes in", watchSourcePath);
|
||
|
||
|
||
|
||
const watcher = chokidar.watch(watchSourcePath, {
|
||
persistent: true,
|
||
ignored: (file: string) =>
|
||
file.endsWith(".tsx") ||
|
||
file.endsWith(".xml") ||
|
||
file.includes("components") ||
|
||
file.includes("pages") ||
|
||
file.includes("hooks"),
|
||
interval: 100,
|
||
binaryInterval: 100,
|
||
cwd: projectPath,
|
||
depth: 99,
|
||
followSymlinks: true,
|
||
ignoreInitial: false,
|
||
ignorePermissionErrors: false,
|
||
usePolling: false,
|
||
alwaysStat: false,
|
||
});
|
||
|
||
let startWatching = false;
|
||
|
||
watcher.on("ready", () => {
|
||
console.warn("Initial scan complete. Ready for changes");
|
||
startWatching = true;
|
||
});
|
||
|
||
watcher.on("error", (error) => console.log(`Watcher error: ${error}`));
|
||
|
||
let processingQueue: string[] = [];
|
||
const fileChangeHandler = async (
|
||
path: string,
|
||
type: "add" | "remove" | "change"
|
||
) => {
|
||
// 判断一下是不是以下扩展名:ts
|
||
if (!path.endsWith(".ts")) {
|
||
// 如果是json文件,复制或者删除
|
||
if (path.endsWith(".json")) {
|
||
// const targetPath = path.replace("src", "lib"); // 这里直接替换不对,应该是拿到项目目录,把项目目录/src 换成项目目录/lib
|
||
const targetPath = path.replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib"));
|
||
if (type === "remove") {
|
||
fs.unlinkSync(targetPath);
|
||
console.warn(`File ${targetPath} has been removed.`);
|
||
} else if (type === "add") {
|
||
fs.copyFileSync(
|
||
path,
|
||
targetPath,
|
||
fs.constants.COPYFILE_FICLONE
|
||
);
|
||
console.warn(
|
||
`File ${path} has been created at ${targetPath}.`
|
||
);
|
||
} else if (type === "change") {
|
||
// 强制覆盖文件
|
||
if (fs.existsSync(targetPath)) {
|
||
fs.unlinkSync(targetPath);
|
||
}
|
||
fs.copyFileSync(
|
||
path,
|
||
targetPath,
|
||
fs.constants.COPYFILE_FICLONE
|
||
);
|
||
console.warn(
|
||
`File ${path} has been copied to ${targetPath}.`
|
||
);
|
||
}
|
||
} else {
|
||
console.warn(`File ${path} is not [ts,json] file, skiped.`);
|
||
}
|
||
return;
|
||
}
|
||
// 控制台清空
|
||
console.clear();
|
||
console.warn(`File ${path} has been ${type}d`);
|
||
// 先判断一下这个文件在不在require.cache里面
|
||
const modulePath = pathLib.resolve(path);
|
||
// 将src替换为lib
|
||
const libPath = modulePath
|
||
.replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib"))
|
||
.replace(/\.ts$/, ".js");
|
||
let compileOnly = false;
|
||
if (!require.cache[libPath]) {
|
||
// 如果是删除的话,直接尝试删除lib下的文件
|
||
if (type === "remove") {
|
||
try {
|
||
fs.unlinkSync(libPath);
|
||
} catch (e) {
|
||
console.error(`Error in delete ${libPath}`, e);
|
||
}
|
||
console.warn(`File ${libPath} has been removed.`);
|
||
return;
|
||
}
|
||
// console.warn(
|
||
// `File ${libPath} is not in module cache, will compile only.`
|
||
// );
|
||
// compileOnly = true;
|
||
// 这里现在不需要仅编译了,因为在require的时候会自动编译ts文件
|
||
} else {
|
||
// 如果是删除,则需要发出警告,文件正在被进程使用
|
||
if (type === "remove") {
|
||
console.error(`File ${libPath} is being used, skiped.`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(path, options, projectReferences);
|
||
|
||
if (diagnostics.length) {
|
||
return;
|
||
}
|
||
|
||
// 只输出单个文件
|
||
realConfig.lifecycle.onBeforeCompile();
|
||
const emitResult = program.emit(sourceFile);
|
||
// 是否成功
|
||
const result = emitResult.emitSkipped;
|
||
if (result) {
|
||
console.error(`Emit failed for ${path}!`);
|
||
realConfig.lifecycle.onAfterCompile();
|
||
} else {
|
||
console.log(
|
||
`Emit succeeded. ${compileOnly ? "" : "reload service......"
|
||
}`
|
||
);
|
||
realConfig.lifecycle.onAfterCompile();
|
||
if (compileOnly) {
|
||
return;
|
||
}
|
||
// await restart(); // 只有在队列里面的最后一个文件编译完成后才会重启服务
|
||
if (processingQueue.length === 1) {
|
||
await restart();
|
||
} else {
|
||
console.log("Waiting for other operations to complete...");
|
||
}
|
||
}
|
||
};
|
||
|
||
const onChangeDebounced =
|
||
async (path: string, type: "add" | "remove" | "change") => {
|
||
if (processingQueue.includes(path)) {
|
||
console.log("Processing, please wait...");
|
||
return;
|
||
}
|
||
try {
|
||
processingQueue.push(path);
|
||
await fileChangeHandler(path, type);
|
||
} catch (e) {
|
||
console.clear();
|
||
console.error(e);
|
||
process.exit(1);
|
||
} finally {
|
||
processingQueue = processingQueue.filter((p) => p !== path);
|
||
}
|
||
}
|
||
|
||
watcher
|
||
.on("add", async (path) => {
|
||
if (startWatching) {
|
||
await onChangeDebounced(path, "add");
|
||
}
|
||
})
|
||
.on("change", async (path) => {
|
||
if (startWatching) {
|
||
await onChangeDebounced(path, "change");
|
||
}
|
||
})
|
||
.on("unlink", async (path) => {
|
||
if (startWatching) {
|
||
await onChangeDebounced(path, "remove");
|
||
}
|
||
});
|
||
|
||
const dispose = async () => {
|
||
if (shutdown) {
|
||
await shutdown();
|
||
}
|
||
await watcher.close();
|
||
realConfig.lifecycle.onDispose();
|
||
};
|
||
|
||
restart()
|
||
.then(() => {
|
||
resolve(dispose);
|
||
})
|
||
.catch(reject);
|
||
});
|
||
};
|