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"; import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore"; import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill"; /* * 工作流程 文件变更检测 → 生成文件变更事件 事件处理 → 创建编译任务并加入队列 批量编译 → 处理队列中的所有任务 编译完成 → 触发服务器重启事件 服务器重启 → 清理缓存并重新启动服务 */ declare const require: NodeJS.Require; declare const process: NodeJS.Process; export type WatchConfig = { autoUpdateI18n?: boolean; /** * 是否启用polyfill */ polyfill?: { /** * 是否启用console polyfill */ console?: { /** * 是否启用 */ enable?: boolean; /** * 是否打印调用堆栈信息 */ trace?: boolean; /** * 格式化函数 */ formatter: LogFormatter; }; }; /** * 生命周期钩子 */ lifecycle?: { /** * 初始化时调用 * @returns void */ onInit?: (config: RealWatchConfig) => void; /** * 服务启动时调用 * @returns void */ onServerStart?: (config: RealWatchConfig) => void; /** * 编译前调用 * @returns void */ onBeforeCompile?: (config: RealWatchConfig) => void; /** * 编译后调用 * @returns void */ onAfterCompile?: (config: RealWatchConfig) => void; /** * 服务关闭时调用 * @returns void */ onServerShutdown?: (config: RealWatchConfig) => void; /** * 销毁监视器时调用 * @returns void */ onDispose?: (config: RealWatchConfig) => void; }; }; // 文件变更类型 export type FileChangeType = "add" | "remove" | "change"; // 文件变更事件 export type FileChangeEvent = { path: string; type: FileChangeType; timestamp: number; }; // 编译任务 export type CompileTask = { id: string; filePath: string; changeType: FileChangeType; timestamp: number; }; // 编译结果 export type CompileResult = { taskId: string; success: boolean; filePath: string; error?: string; }; // 事件类型 export type EventType = | "file-changed" | "compile-task-added" | "compile-task-completed" | "compile-batch-started" | "compile-batch-completed" | "server-restart-needed" | "server-restarted"; // 事件处理器 export type EventHandler = (data: T) => void | Promise; // 简单的事件系统 export type EventEmitter = { on: (event: EventType, handler: EventHandler) => void; emit: (event: EventType, data: T) => Promise; off: (event: EventType, handler: EventHandler) => void; }; // 创建事件发射器 const createEventEmitter = (): EventEmitter => { const listeners = new Map>(); return { on: (event: EventType, handler: EventHandler) => { if (!listeners.has(event)) { listeners.set(event, new Set()); } listeners.get(event)!.add(handler); }, emit: async (event: EventType, data: T) => { const handlers = listeners.get(event); if (handlers) { const promises = Array.from(handlers).map(handler => Promise.resolve(handler(data)) ); await Promise.all(promises); } }, off: (event: EventType, handler: EventHandler) => { const handlers = listeners.get(event); if (handlers) { handlers.delete(handler); } } }; }; // 真实config不会出现?,所以这里新增一个type表示真实的config type DeepRequiredIfPresent = { [P in keyof T]-?: NonNullable extends (...args: any) => any // Check for function type ? T[P] // Preserve the original function type : NonNullable extends (infer U)[] ? DeepRequiredIfPresent[] : NonNullable extends object ? DeepRequiredIfPresent> : T[P] extends undefined | null ? T[P] : NonNullable; }; // 创建编译任务队列 const createCompileQueue = (eventEmitter: EventEmitter) => { const queue: CompileTask[] = []; let isProcessing = false; let processingCount = 0; let taskProcessor: (task: CompileTask) => Promise = async (task) => ({ taskId: task.id, success: true, filePath: task.filePath }); const addTask = (task: CompileTask) => { // 检查是否有相同文件的任务已存在,如果有则更新时间戳 const existingIndex = queue.findIndex(t => t.filePath === task.filePath); if (existingIndex !== -1) { queue[existingIndex] = { ...queue[existingIndex], ...task }; } else { queue.push(task); } eventEmitter.emit("compile-task-added", task); processQueue(); }; const processQueue = async () => { if (isProcessing || queue.length === 0) { return; } isProcessing = true; processingCount = queue.length; await eventEmitter.emit("compile-batch-started", { count: processingCount }); const tasksToProcess = [...queue]; queue.length = 0; // 清空队列 const results: CompileResult[] = []; for (const task of tasksToProcess) { try { const result = await taskProcessor(task); results.push(result); await eventEmitter.emit("compile-task-completed", result); } catch (error) { const result: CompileResult = { taskId: task.id, success: false, filePath: task.filePath, error: error instanceof Error ? error.message : String(error) }; results.push(result); await eventEmitter.emit("compile-task-completed", result); } } isProcessing = false; processingCount = 0; await eventEmitter.emit("compile-batch-completed", { results }); // 检查是否需要重启服务器 const hasSuccessfulCompiles = results.some(r => r.success); if (hasSuccessfulCompiles) { await eventEmitter.emit("server-restart-needed", { results }); } // 如果在处理过程中又有新任务加入,继续处理 if (queue.length > 0) { processQueue(); } }; return { addTask, getQueueLength: () => queue.length, isProcessing: () => isProcessing, getProcessingCount: () => processingCount, setTaskProcessor: (processor: (task: CompileTask) => Promise) => { taskProcessor = processor; } }; }; export type RealWatchConfig = DeepRequiredIfPresent; type AliasConfig = Record; type ModuleType = import('module'); const defaultConfig: RealWatchConfig = { autoUpdateI18n: true, polyfill: { console: { enable: true, trace: true, formatter: ({ level, caller, args }: LogFormatterProp) => { 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: (config: RealWatchConfig) => { console.log("----> Watcher initialized."); }, onServerStart: (config: RealWatchConfig) => { console.log("----> Server started."); }, onBeforeCompile: (config: RealWatchConfig) => { console.log("----> Compiling......"); }, onAfterCompile: (config: RealWatchConfig) => { console.log("----> Compile completed."); }, onServerShutdown: (config: RealWatchConfig) => { console.log("----> Server shutdown."); }, onDispose: (config: RealWatchConfig) => { 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; }; // 当前运行目录 const pwd = process.cwd(); // 创建服务器管理器 const createServerManager = ( projectPath: string, eventEmitter: EventEmitter, config: RealWatchConfig ) => { let shutdown: (() => Promise) | undefined; let isRestarting = false; const restart = async () => { if (isRestarting) { return; } isRestarting = true; if (shutdown) { console.log("----> Shutting down service......"); await shutdown().then(() => config.lifecycle.onServerShutdown(config)); 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.` ); // eslint-disable-next-line @typescript-eslint/no-var-requires const simpleConnector = require(pathLib.join( projectPath, "lib/config/connector" )).default; console.warn("----> Starting service......"); // 这里注意要在require之前,因为require会触发编译 const { startup } = require('./start') as { startup: (pwd: string, connector: any) => Promise<() => Promise>; } shutdown = await startup(pwd, simpleConnector).then((shutdown) => { config.lifecycle.onServerStart(config); return shutdown; }); isRestarting = false; await eventEmitter.emit("server-restarted", {}); }; const dispose = async () => { if (shutdown) { await shutdown(); } }; return { restart, dispose, isRestarting: () => isRestarting }; }; // 创建编译器 const createCompiler = ( projectPath: string, options: CompilerOptions, projectReferences: readonly ProjectReference[] | undefined, aliasConfig: Record, config: RealWatchConfig ) => { const createProgramAndSourceFile = (path: string) => { 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 }; }; const compileTask = async (task: CompileTask): Promise => { const { filePath, changeType } = task; // 判断文件类型 if (!filePath.endsWith(".ts")) { // 处理非TypeScript文件 (如JSON文件) if (filePath.endsWith(".json")) { const targetPath = filePath.replace( pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib") ); try { if (changeType === "remove") { if (fs.existsSync(targetPath)) { fs.unlinkSync(targetPath); } console.warn(`File ${targetPath} has been removed.`); } else if (changeType === "add" || changeType === "change") { // 确保目录存在 const dir = pathLib.dirname(targetPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } if (changeType === "change" && fs.existsSync(targetPath)) { fs.unlinkSync(targetPath); } fs.copyFileSync(filePath, targetPath, fs.constants.COPYFILE_FICLONE); console.warn(`File ${filePath} has been copied to ${targetPath}.`); } return { taskId: task.id, success: true, filePath }; } catch (error) { return { taskId: task.id, success: false, filePath, error: error instanceof Error ? error.message : String(error) }; } } else { console.warn(`File ${filePath} is not [ts,json] file, skipped.`); return { taskId: task.id, success: true, filePath }; } } // 处理TypeScript文件 console.clear(); console.warn(`File ${filePath} has been ${changeType}d`); const modulePath = pathLib.resolve(filePath); const libPath = modulePath .replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib")) .replace(/\.ts$/, ".js"); if (changeType === "remove") { try { if (fs.existsSync(libPath)) { fs.unlinkSync(libPath); } console.warn(`File ${libPath} has been removed.`); return { taskId: task.id, success: true, filePath }; } catch (error) { return { taskId: task.id, success: false, filePath, error: `Error removing file: ${error instanceof Error ? error.message : String(error)}` }; } } // 编译TypeScript文件 const { program, sourceFile, diagnostics } = createProgramAndSourceFile(filePath); if (diagnostics.length) { return { taskId: task.id, success: false, filePath, error: "TypeScript compilation error" }; } // 只输出单个文件 config.lifecycle.onBeforeCompile(config); const emitResult = program.emit(sourceFile); if (emitResult.emitSkipped) { console.error(`Emit failed for ${filePath}!`); config.lifecycle.onAfterCompile(config); return { taskId: task.id, success: false, filePath, error: "TypeScript emit failed" }; } else { console.log(`Emit succeeded for ${filePath}.`); config.lifecycle.onAfterCompile(config); return { taskId: task.id, success: true, filePath }; } }; return { compileTask }; }; // 创建文件监视器 const createFileWatcher = ( projectPath: string, eventEmitter: EventEmitter ) => { const watchSourcePath = pathLib.join(projectPath, "src"); let startWatching = false; 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, }); const handleFileChange = async (path: string, type: FileChangeType) => { if (!startWatching) { return; } const event: FileChangeEvent = { path, type, timestamp: Date.now() }; await eventEmitter.emit("file-changed", event); }; watcher.on("ready", () => { console.warn("Initial scan complete. Ready for changes"); startWatching = true; }); watcher.on("error", (error) => console.log(`Watcher error: ${error}`)); watcher .on("add", async (path) => { await handleFileChange(path, "add"); }) .on("change", async (path) => { await handleFileChange(path, "change"); }) .on("unlink", async (path) => { await handleFileChange(path, "remove"); }); const dispose = async () => { await watcher.close(); }; return { dispose }; }; // 生成唯一任务ID const generateTaskId = (): string => { return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; }; /** * 根据 alias 配置表将路径中的别名替换为真实路径 * @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module" * @param aliasConfig - alias 配置表,key 为别名,value 为对应的真实路径或路径数组 * @returns 替换后的路径,如果没有匹配到 alias,则返回原始路径 */ const replaceAliasWithPath = (path: string, aliasConfig: Record): 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> => { 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: string) => v.replace("src", "lib")); }); // 输出真实的alias配置 console.debug("[DEBUG] Running Alias config:", aliasConfig); // 初始化polyfill 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 = ts.createProgram({ rootNames: [tsPath], options, projectReferences, }); const sourceFile = program.getSourceFile(tsPath); const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); 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; } }; }; // 初始化polyfill polyfillLoader(); realConfig.polyfill.console.enable && polyfillConsole("watch", enableTrace, realConfig.polyfill?.console?.formatter); // 初始编译检查 const initialCompile = () => { 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 = ts.createProgram({ rootNames: [tsFile], options, projectReferences, }); const diagnostics = ts.getPreEmitDiagnostics(program); 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(); 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) => { // 创建事件系统 const eventEmitter = createEventEmitter(); // 创建各个组件 const compileQueue = createCompileQueue(eventEmitter); const compiler = createCompiler(projectPath, options, projectReferences, aliasConfig, realConfig); const serverManager = createServerManager(projectPath, eventEmitter, realConfig); const fileWatcher = createFileWatcher(projectPath, eventEmitter); // 设置编译器处理器 compileQueue.setTaskProcessor(compiler.compileTask); // 设置事件监听器 eventEmitter.on("file-changed", (event: FileChangeEvent) => { const task: CompileTask = { id: generateTaskId(), filePath: event.path, changeType: event.type, timestamp: event.timestamp }; compileQueue.addTask(task); }); eventEmitter.on("compile-batch-started", (data: { count: number }) => { console.log(`----> Starting compilation batch (${data.count} files)...`); }); eventEmitter.on("compile-batch-completed", (data: { results: CompileResult[] }) => { const successCount = data.results.filter(r => r.success).length; console.log(`----> Compilation batch completed: ${successCount}/${data.results.length} successful`); }); // 编译成功之后,若设置的同步i18n,则触发i18n更新 if (realConfig.autoUpdateI18n) { const projectI18nPath = pathLib.join("src", "data", "i18n.ts"); eventEmitter.on("compile-task-completed", async (result: CompileResult) => { if (result.filePath == projectI18nPath && result.success) { console.log("-------------start upgrade i18n.-------------") // 这里是copy:upgradeI18n的 const { checkAndUpdateI18n } = require('oak-backend-base/lib/routines/i18n.js'); const simpleConnector = require(pathLib.join( projectPath, "lib/config/connector" )).default; // 这里注意要在require之前,因为require会触发编译 const { startup } = require('./start') as { startup: ( pwd: string, connector: any, omitWatchers?: boolean, omitTimers?: boolean, routine?: (context: AsyncContext) => Promise ) => Promise<() => Promise>; } startup(pwd, simpleConnector, true, true, checkAndUpdateI18n).then(() => { console.log("------------upgrade i18n success.------------") }).catch((err) => { console.error("------------upgrade i18n failed!------------", err) }) } }); } eventEmitter.on("server-restart-needed", async () => { if (!serverManager.isRestarting()) { console.log("----> Restarting server..."); await serverManager.restart(); } }); // 初始化 realConfig.lifecycle.onInit(realConfig); // 执行初始编译 initialCompile(); // 启动服务器 serverManager.restart() .then(() => { const dispose = async () => { await fileWatcher.dispose(); await serverManager.dispose(); realConfig.lifecycle.onDispose(realConfig); }; resolve(dispose); }) .catch(reject); }); };