From 61705bf9e14431446b49f5f49fea06a16aebffbf Mon Sep 17 00:00:00 2001 From: pqcqaq <905739777@qq.com> Date: Mon, 18 Aug 2025 16:21:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=85=A8=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BA=86server=20watch=EF=BC=8C?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E7=BC=96=E8=AF=91=E9=93=BE=E4=B8=8E=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E6=9C=BA=E5=88=B6=E8=A7=A3=E5=86=B3=E5=A4=9A=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=BF=AE=E6=94=B9=E7=9A=84=E6=83=85=E5=86=B5=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E4=B8=94=E6=94=AF=E6=8C=81=E4=BA=86=E5=9C=A8i18n?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=9B=B4=E6=96=B0=E7=9A=84=E6=83=85=E5=86=B5?= =?UTF-8?q?=E4=B8=8B=EF=BC=8C=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=E5=88=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=88=E5=88=A9=E7=94=A8=E4=BA=86?= =?UTF-8?q?routine=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/build.js | 2 +- lib/clean.js | 2 +- lib/create.js | 7 +- lib/createConfig.js | 3 +- lib/file-handle.d.ts | 2 - lib/file-handle.js | 21 +- lib/makeDependency.js | 2 +- lib/makeDomain.js | 2 +- lib/makeLocale.js | 2 +- lib/makeRouter.js | 2 +- lib/rename.js | 5 +- lib/run.js | 2 +- lib/server/initialize.d.ts | 1 - lib/server/initialize.js | 3 +- lib/server/start.d.ts | 1 - lib/server/start.js | 7 +- lib/server/watch.d.ts | 39 +- lib/server/watch.js | 742 ++++++++++++++++++---------- lib/template.js | 23 +- lib/utils.js | 19 +- src/server/watch.ts | 965 ++++++++++++++++++++++++------------- 21 files changed, 1194 insertions(+), 658 deletions(-) diff --git a/lib/build.js b/lib/build.js index 6fee0ef..2db5ec8 100644 --- a/lib/build.js +++ b/lib/build.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = build; const tslib_1 = require("tslib"); const tip_style_1 = require("./tip-style"); const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn")); @@ -184,4 +185,3 @@ async function build(cmd) { (0, tip_style_1.Error)(`${(0, tip_style_1.error)(`target could only be web or mp(wechatMp) or rn(native)`)}`); } } -exports.default = build; diff --git a/lib/clean.js b/lib/clean.js index b6b057e..5e0e07f 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = run; const tslib_1 = require("tslib"); const tip_style_1 = require("./tip-style"); const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn")); @@ -48,4 +49,3 @@ async function run(options) { process.exit(-1); } } -exports.default = run; diff --git a/lib/create.js b/lib/create.js index 71ea8dd..b30d4c1 100644 --- a/lib/create.js +++ b/lib/create.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.update = exports.create = void 0; +exports.create = create; +exports.update = update; const tslib_1 = require("tslib"); const ts = tslib_1.__importStar(require("typescript")); const fs_1 = require("fs"); @@ -242,7 +243,7 @@ async function create(dirName, cmd) { } // 获取package.json内容 const packageJson = (0, template_1.packageJsonContent)({ - name: DEFAULT_PROJECT_NAME, + name: DEFAULT_PROJECT_NAME, // 后面再统一rename version, description, cliName: config_1.CLI_NAME, @@ -265,7 +266,6 @@ async function create(dirName, cmd) { (0, tip_style_1.Error)((0, tip_style_1.error)(err)); } } -exports.create = create; async function update(dirName, subDirName, cmd) { const isDev = cmd.dev ? true : false; try { @@ -292,4 +292,3 @@ async function update(dirName, subDirName, cmd) { console.error((0, tip_style_1.error)(err.message)); } } -exports.update = update; diff --git a/lib/createConfig.js b/lib/createConfig.js index 71233f6..4c8e467 100644 --- a/lib/createConfig.js +++ b/lib/createConfig.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.CreateCompilerConfig = void 0; +exports.CreateCompilerConfig = CreateCompilerConfig; /** * 创建一个oak编译器配置 * @param raw 原始配置 @@ -11,7 +11,6 @@ function CreateCompilerConfig(raw) { // 在这里可以做配置的预处理 return raw; } -exports.CreateCompilerConfig = CreateCompilerConfig; /** * 将compiler.js中的模块导出使用以下形式: * module.exports = CreateComilerConfig({}) diff --git a/lib/file-handle.d.ts b/lib/file-handle.d.ts index 906763d..8c3c1bf 100644 --- a/lib/file-handle.d.ts +++ b/lib/file-handle.d.ts @@ -1,5 +1,3 @@ -/// -/// import { PathLike } from 'fs'; import { checkFileExistsAndCreateType } from './enum'; /** diff --git a/lib/file-handle.js b/lib/file-handle.js index 14f0c2f..3e5b805 100644 --- a/lib/file-handle.js +++ b/lib/file-handle.js @@ -1,6 +1,15 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkFileExistsAndCreate = exports.checkFileExists = exports.copyFolder = exports.readFile = exports.writeFile = exports.deleteFolderRecursive = exports.parseJsonFile = exports.parseJsonFiles = exports.readDirGetFile = exports.readDirPath = void 0; +exports.readDirPath = readDirPath; +exports.readDirGetFile = readDirGetFile; +exports.parseJsonFiles = parseJsonFiles; +exports.parseJsonFile = parseJsonFile; +exports.deleteFolderRecursive = deleteFolderRecursive; +exports.writeFile = writeFile; +exports.readFile = readFile; +exports.copyFolder = copyFolder; +exports.checkFileExists = checkFileExists; +exports.checkFileExistsAndCreate = checkFileExistsAndCreate; const fs_1 = require("fs"); const path_1 = require("path"); const enum_1 = require("./enum"); @@ -25,7 +34,6 @@ function readDirPath(entry) { } return pathList; } -exports.readDirPath = readDirPath; /** * @name 读取指定目录的文件(不进行深度遍历,只获取根目录) * @export @@ -36,7 +44,6 @@ function readDirGetFile(entry) { const dirInfo = (0, fs_1.readdirSync)(entry); return dirInfo; } -exports.readDirGetFile = readDirGetFile; /** * @name 解析json文件(数组) * @export @@ -51,7 +58,6 @@ function parseJsonFiles(arr) { } return result; } -exports.parseJsonFiles = parseJsonFiles; /** * @name 解析单个文件json * @export @@ -67,7 +73,6 @@ function parseJsonFile(file) { return; } } -exports.parseJsonFile = parseJsonFile; /** * @name 删除文件夹 * @export @@ -97,7 +102,6 @@ function deleteFolderRecursive(entry) { // console.log("文件夹不存在"); } } -exports.deleteFolderRecursive = deleteFolderRecursive; ; function writeFile(path, data) { try { @@ -108,7 +112,6 @@ function writeFile(path, data) { (0, tip_style_1.Error)((0, tip_style_1.error)('文件写入失败')); } } -exports.writeFile = writeFile; function readFile(path, options) { try { const data = (0, fs_1.readFileSync)(path, options); @@ -119,7 +122,6 @@ function readFile(path, options) { (0, tip_style_1.Error)((0, tip_style_1.error)('文件读取失败')); } } -exports.readFile = readFile; /** * @name 拷贝文件夹 * @export @@ -181,7 +183,6 @@ function copyFolder(currentDir, targetDir, overwrite = false) { throw new global.Error(`需要copy的文件夹不存在: ${currentDir}`); } } -exports.copyFolder = copyFolder; /** * @name 检测文件/文件夹是否存在 * @export @@ -191,7 +192,6 @@ exports.copyFolder = copyFolder; function checkFileExists(path) { return (0, fs_1.existsSync)(path); } -exports.checkFileExists = checkFileExists; /** * @name 检测文件/文件夹是否存在,不存在则创建 * @export @@ -217,4 +217,3 @@ function checkFileExistsAndCreate(path, data, type = enum_1.checkFileExistsAndCr throw new global.Error(`${path} already exists!`); } } -exports.checkFileExistsAndCreate = checkFileExistsAndCreate; diff --git a/lib/makeDependency.js b/lib/makeDependency.js index 271c321..0812d9a 100644 --- a/lib/makeDependency.js +++ b/lib/makeDependency.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = make; const tslib_1 = require("tslib"); const tip_style_1 = require("./tip-style"); const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn")); @@ -21,4 +22,3 @@ async function make(cmd) { process.exit(-1); } } -exports.default = make; diff --git a/lib/makeDomain.js b/lib/makeDomain.js index c5c89e4..eb140b2 100644 --- a/lib/makeDomain.js +++ b/lib/makeDomain.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = make; const tslib_1 = require("tslib"); const tip_style_1 = require("./tip-style"); const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn")); @@ -20,4 +21,3 @@ async function make() { process.exit(-1); } } -exports.default = make; diff --git a/lib/makeLocale.js b/lib/makeLocale.js index 7da9a71..f27f7fa 100644 --- a/lib/makeLocale.js +++ b/lib/makeLocale.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = make; const tslib_1 = require("tslib"); const tip_style_1 = require("./tip-style"); const localeBuilder_1 = tslib_1.__importDefault(require("oak-domain/lib/compiler/localeBuilder")); @@ -18,4 +19,3 @@ async function make(cmd) { process.exit(-1); } } -exports.default = make; diff --git a/lib/makeRouter.js b/lib/makeRouter.js index 61eda2b..d3a5196 100644 --- a/lib/makeRouter.js +++ b/lib/makeRouter.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = make; const tslib_1 = require("tslib"); const tip_style_1 = require("./tip-style"); const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn")); @@ -30,4 +31,3 @@ async function make(cmd, watch) { process.exit(-1); } } -exports.default = make; diff --git a/lib/rename.js b/lib/rename.js index 62b30e3..71af610 100644 --- a/lib/rename.js +++ b/lib/rename.js @@ -1,6 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.rename = exports.renameProject = void 0; +exports.renameProject = renameProject; +exports.rename = rename; const path_1 = require("path"); const fs_1 = require("fs"); const editTemplate_1 = require("@react-native-community/cli/build/commands/init/editTemplate"); @@ -44,7 +45,6 @@ async function renameProject(dir, name, title, placeholderName, placeholderTitle process.chdir(cwd); (0, tip_style_1.Success)(`${(0, tip_style_1.success)(`Change project name to ${(0, tip_style_1.primary)(name)}, project title to ${(0, tip_style_1.primary)(title)}`)}`); } -exports.renameProject = renameProject; async function rename(cmd) { const { oldName, newName, oldTitle, newTitle } = cmd; if (!oldName) { @@ -65,4 +65,3 @@ async function rename(cmd) { } renameProject(process.cwd(), newName, newTitle, oldName, oldTitle); } -exports.rename = rename; diff --git a/lib/run.js b/lib/run.js index 4d431e1..bfb8be2 100644 --- a/lib/run.js +++ b/lib/run.js @@ -1,5 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = run; const tslib_1 = require("tslib"); const tip_style_1 = require("./tip-style"); const cross_spawn_1 = tslib_1.__importDefault(require("cross-spawn")); @@ -59,4 +60,3 @@ async function run(options) { process.exit(-1); } } -exports.default = run; diff --git a/lib/server/initialize.d.ts b/lib/server/initialize.d.ts index 964ab37..b13884d 100644 --- a/lib/server/initialize.d.ts +++ b/lib/server/initialize.d.ts @@ -1,4 +1,3 @@ -/// import { EntityDict } from 'oak-domain/lib/types'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext'; diff --git a/lib/server/initialize.js b/lib/server/initialize.js index 90f692b..acdfa85 100644 --- a/lib/server/initialize.js +++ b/lib/server/initialize.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.initialize = void 0; +exports.initialize = initialize; /// const oak_backend_base_1 = require("oak-backend-base"); async function initialize(path) { @@ -10,4 +10,3 @@ async function initialize(path) { await appLoader.unmount(); console.log('data initialized'); } -exports.initialize = initialize; diff --git a/lib/server/start.d.ts b/lib/server/start.d.ts index eb29256..ab1c9ef 100644 --- a/lib/server/start.d.ts +++ b/lib/server/start.d.ts @@ -1,4 +1,3 @@ -/// import './polyfill'; import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext'; import { Connector, EntityDict } from 'oak-domain/lib/types'; diff --git a/lib/server/start.js b/lib/server/start.js index 2302949..b55f511 100644 --- a/lib/server/start.js +++ b/lib/server/start.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.startup = void 0; +exports.startup = startup; const tslib_1 = require("tslib"); /// require("./polyfill"); @@ -64,7 +64,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) { } : serverConfiguration.cors ? { - origin: serverConfiguration.cors.origin, + origin: serverConfiguration.cors.origin, //socket.io配置cors origin是支持数组和字符串 allowedHeaders: [ ...corsHeaders.concat(connector.getCorsHeader()), ...(serverConfiguration.cors.headers || []), @@ -146,7 +146,7 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) { if (!ui?.disable) { (0, admin_ui_1.instrument)(io, { auth: { - type: "basic", + type: "basic", // 使用基本认证,生产建议关闭或换成自定义 auth username: ui?.username || "admin", password: bcryptjs_1.default.hashSync(passwordForAdminUI, 10), // 必须使用 bcrypt 加密之后的密码 }, @@ -368,4 +368,3 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) { }; return shutdown; } -exports.startup = startup; diff --git a/lib/server/watch.d.ts b/lib/server/watch.d.ts index 45b0b46..632ca93 100644 --- a/lib/server/watch.d.ts +++ b/lib/server/watch.d.ts @@ -1,4 +1,3 @@ -/// export type LogFormatterProp = { level: "info" | "warn" | "error"; caller: NodeJS.CallSite | null; @@ -6,6 +5,7 @@ export type LogFormatterProp = { }; export type LogFormatter = (props: LogFormatterProp) => any[]; export type WatchConfig = { + autoUpdateI18n?: boolean; /** * 是否启用polyfill */ @@ -36,34 +36,59 @@ export type WatchConfig = { * 初始化时调用 * @returns void */ - onInit?: () => void; + onInit?: (config: RealWatchConfig) => void; /** * 服务启动时调用 * @returns void */ - onServerStart?: () => void; + onServerStart?: (config: RealWatchConfig) => void; /** * 编译前调用 * @returns void */ - onBeforeCompile?: () => void; + onBeforeCompile?: (config: RealWatchConfig) => void; /** * 编译后调用 * @returns void */ - onAfterCompile?: () => void; + onAfterCompile?: (config: RealWatchConfig) => void; /** * 服务关闭时调用 * @returns void */ - onServerShutdown?: () => void; + onServerShutdown?: (config: RealWatchConfig) => void; /** * 销毁监视器时调用 * @returns void */ - onDispose?: () => 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; +}; type DeepRequiredIfPresent = { [P in keyof T]-?: NonNullable extends (...args: any) => any ? T[P] : NonNullable extends (infer U)[] ? DeepRequiredIfPresent[] : NonNullable extends object ? DeepRequiredIfPresent> : T[P] extends undefined | null ? T[P] : NonNullable; }; diff --git a/lib/server/watch.js b/lib/server/watch.js index f8d9d0a..62883cb 100644 --- a/lib/server/watch.js +++ b/lib/server/watch.js @@ -8,7 +8,105 @@ const path_1 = tslib_1.__importDefault(require("path")); const dayjs_1 = tslib_1.__importDefault(require("dayjs")); const fs_1 = tslib_1.__importDefault(require("fs")); const lodash_1 = require("lodash"); +// 创建事件发射器 +const createEventEmitter = () => { + const listeners = new Map(); + return { + on: (event, handler) => { + if (!listeners.has(event)) { + listeners.set(event, new Set()); + } + listeners.get(event).add(handler); + }, + emit: async (event, data) => { + const handlers = listeners.get(event); + if (handlers) { + const promises = Array.from(handlers).map(handler => Promise.resolve(handler(data))); + await Promise.all(promises); + } + }, + off: (event, handler) => { + const handlers = listeners.get(event); + if (handlers) { + handlers.delete(handler); + } + } + }; +}; +// 创建编译任务队列 +const createCompileQueue = (eventEmitter) => { + const queue = []; + let isProcessing = false; + let processingCount = 0; + let taskProcessor = async (task) => ({ + taskId: task.id, + success: true, + filePath: task.filePath + }); + const addTask = (task) => { + // 检查是否有相同文件的任务已存在,如果有则更新时间戳 + 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 = []; + 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 = { + 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) => { + taskProcessor = processor; + } + }; +}; const defaultConfig = { + autoUpdateI18n: true, polyfill: { console: { enable: true, @@ -44,22 +142,22 @@ const defaultConfig = { }, }, lifecycle: { - onInit: () => { + onInit: (config) => { console.log("----> Watcher initialized."); }, - onServerStart: () => { + onServerStart: (config) => { console.log("----> Server started."); }, - onBeforeCompile: () => { + onBeforeCompile: (config) => { console.log("----> Compiling......"); }, - onAfterCompile: () => { + onAfterCompile: (config) => { console.log("----> Compile completed."); }, - onServerShutdown: () => { + onServerShutdown: (config) => { console.log("----> Server shutdown."); }, - onDispose: () => { + onDispose: (config) => { console.log("----> Watcher disposed."); }, }, @@ -94,13 +192,265 @@ const getOverrideConfig = (config) => { ? overrideInner(config, defaultConfig) : defaultConfig; }; +// 当前运行目录 +const pwd = process.cwd(); +// 创建服务器管理器 +const createServerManager = (projectPath, eventEmitter, config) => { + let shutdown; + 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(path_1.default.join(projectPath, "lib/config/connector")).default; + console.warn("----> Starting service......"); + // 这里注意要在require之前,因为require会触发编译 + const { startup } = require('./start'); + 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, options, projectReferences, aliasConfig, config) => { + const createProgramAndSourceFile = (path) => { + const program = typescript_1.default.createProgram({ + rootNames: [path], + options, + projectReferences, + }); + const sourceFile = program.getSourceFile(path); + // 是否有语法错误 + const diagnostics = typescript_1.default.getPreEmitDiagnostics(program, sourceFile); + if (diagnostics.length) { + const syntaxErrors = diagnostics.filter((diagnostic) => diagnostic.category === typescript_1.default.DiagnosticCategory.Error); + if (syntaxErrors.length) { + console.error(`Error in ${path}`); + syntaxErrors.forEach((diagnostic) => { + console.error(`${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`); + }); + console.error(`文件存在语法错误,请检查修复后重试!`); + return { program, sourceFile, diagnostics }; + } + } + return { program, sourceFile, diagnostics }; + }; + const compileTask = async (task) => { + const { filePath, changeType } = task; + // 判断文件类型 + if (!filePath.endsWith(".ts")) { + // 处理非TypeScript文件 (如JSON文件) + if (filePath.endsWith(".json")) { + const targetPath = filePath.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")); + try { + if (changeType === "remove") { + if (fs_1.default.existsSync(targetPath)) { + fs_1.default.unlinkSync(targetPath); + } + console.warn(`File ${targetPath} has been removed.`); + } + else if (changeType === "add" || changeType === "change") { + // 确保目录存在 + const dir = path_1.default.dirname(targetPath); + if (!fs_1.default.existsSync(dir)) { + fs_1.default.mkdirSync(dir, { recursive: true }); + } + if (changeType === "change" && fs_1.default.existsSync(targetPath)) { + fs_1.default.unlinkSync(targetPath); + } + fs_1.default.copyFileSync(filePath, targetPath, fs_1.default.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 = path_1.default.resolve(filePath); + const libPath = modulePath + .replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")) + .replace(/\.ts$/, ".js"); + if (changeType === "remove") { + try { + if (fs_1.default.existsSync(libPath)) { + fs_1.default.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, eventEmitter) => { + const watchSourcePath = path_1.default.join(projectPath, "src"); + let startWatching = false; + console.log("Watching for changes in", watchSourcePath); + const watcher = chokidar_1.default.watch(watchSourcePath, { + persistent: true, + ignored: (file) => 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, type) => { + if (!startWatching) { + return; + } + const event = { + 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 = () => { + 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,则返回原始路径 */ -function replaceAliasWithPath(path, aliasConfig) { +const replaceAliasWithPath = (path, aliasConfig) => { for (const [alias, targets] of Object.entries(aliasConfig)) { // If alias ends with "*", handle it as a dynamic alias. if (alias.endsWith('*')) { @@ -126,7 +476,7 @@ function replaceAliasWithPath(path, aliasConfig) { } // If no alias matches, return the original path return path; -} +}; const watch = (projectPath, config) => { const realConfig = getOverrideConfig(config); const enableTrace = !!process.env.ENABLE_TRACE; @@ -149,30 +499,7 @@ const watch = (projectPath, config) => { }); // 输出真实的alias配置 console.debug("[DEBUG] Running Alias config:", aliasConfig); - const createProgramAndSourceFile = (path, options, projectReferences) => { - const program = typescript_1.default.createProgram({ - rootNames: [path], - options, - projectReferences, - }); - const sourceFile = program.getSourceFile(path); - // 是否有语法错误 - const diagnostics = typescript_1.default.getPreEmitDiagnostics(program, sourceFile); - if (diagnostics.length) { - const syntaxErrors = diagnostics.filter((diagnostic) => diagnostic.category === typescript_1.default.DiagnosticCategory.Error); - if (syntaxErrors.length) { - console.error(`Error in ${path}`); - syntaxErrors.forEach((diagnostic) => { - console.error(`${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`); - }); - console.error(`文件存在语法错误,请检查修复后重试!`); - return { program, sourceFile, diagnostics }; - } - } - return { program, sourceFile, diagnostics }; - }; - // 这个函数用于解决require的时候,如果文件不存在,会尝试编译ts文件 - // 测试:在src下新建一个ts文件,然后删除lib下的js文件,然后require这个文件,会发现会自动编译ts文件 + // 初始化polyfill const polyfillLoader = () => { const BuiltinModule = require("module"); // 模拟环境下的 module 构造函数 @@ -186,7 +513,13 @@ const watch = (projectPath, config) => { // 检查并编译 .ts 文件 if (fs_1.default.existsSync(tsPath)) { console.log(`[resolve] Found TypeScript source file: ${tsPath}, attempting to compile...`); - const { program, sourceFile, diagnostics } = createProgramAndSourceFile(tsPath, options, projectReferences); + const program = typescript_1.default.createProgram({ + rootNames: [tsPath], + options, + projectReferences, + }); + const sourceFile = program.getSourceFile(tsPath); + const diagnostics = typescript_1.default.getPreEmitDiagnostics(program, sourceFile); if (diagnostics.length) { console.error(`[resolve] Compilation failed for: ${tsPath}`); throw new Error("TypeScript compilation error"); @@ -247,8 +580,6 @@ const watch = (projectPath, config) => { } }; }; - polyfillLoader(); - //polyfill console.log 添加时间 const polyfillConsole = (trace) => { // 获取调用堆栈信息 const getCallerInfo = () => { @@ -282,7 +613,7 @@ const watch = (projectPath, config) => { const levelStr = level; const oldFunc = console[levelStr]; console[levelStr] = function (...args) { - oldFunc(...(defaultConfig.polyfill?.console?.formatter({ + oldFunc(...(realConfig.polyfill?.console?.formatter({ level: levelStr, caller: getCallerInfo(), args, @@ -290,247 +621,122 @@ const watch = (projectPath, config) => { }; }); }; + // 初始化polyfill + polyfillLoader(); realConfig.polyfill.console.enable && polyfillConsole(enableTrace); - // 这里注意要在require之前,因为require会触发编译 - const { startup } = require('./start'); - // 如果lib目录是空的,则直接编译所有的ts文件 - const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js"); - if (!fs_1.default.existsSync(serverConfigFile)) { - // 尝试编译src/configuration/server.ts - console.log(`[watch] Server configuration file not found, attempting to compile the project......`); - const tryCompile = (tsFile) => { - console.log(`[watch] Compiling: ${tsFile}`); - if (fs_1.default.existsSync(tsFile)) { - const { program, diagnostics } = createProgramAndSourceFile(tsFile, options, projectReferences); - if (diagnostics.length) { - console.error(`Error in ${tsFile}`); - diagnostics.forEach((diagnostic) => { - console.error(`${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`); + // 初始编译检查 + const initialCompile = () => { + const serverConfigFile = path_1.default.join(projectPath, "lib/configuration/server.js"); + if (!fs_1.default.existsSync(serverConfigFile)) { + // 尝试编译src/configuration/server.ts + console.log(`[watch] Server configuration file not found, attempting to compile the project......`); + const tryCompile = (tsFile) => { + console.log(`[watch] Compiling: ${tsFile}`); + if (fs_1.default.existsSync(tsFile)) { + const program = typescript_1.default.createProgram({ + rootNames: [tsFile], + options, + projectReferences, }); - console.error(`文件存在语法错误,请检查修复后重试!`); - process.exit(1); + const diagnostics = typescript_1.default.getPreEmitDiagnostics(program); + if (diagnostics.length) { + console.error(`Error in ${tsFile}`); + diagnostics.forEach((diagnostic) => { + console.error(`${typescript_1.default.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.`); } - // 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); - } + }; + // 所有要编译的目录 + // 其他涉及到的目录会在运行的时候自动编译,这里主要处理的是动态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; - 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++; + // 创建事件系统 + 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) => { + const task = { + id: generateTaskId(), + filePath: event.path, + changeType: event.type, + timestamp: event.timestamp + }; + compileQueue.addTask(task); + }); + eventEmitter.on("compile-batch-started", (data) => { + console.log(`----> Starting compilation batch (${data.count} files)...`); + }); + eventEmitter.on("compile-batch-completed", (data) => { + 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 = path_1.default.join("src", "data", "i18n.ts"); + eventEmitter.on("compile-task-completed", async (result) => { + 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(path_1.default.join(projectPath, "lib/config/connector")).default; + // 这里注意要在require之前,因为require会触发编译 + const { startup } = require('./start'); + startup(pwd, simpleConnector, true, true, checkAndUpdateI18n).then(() => { + console.log("------------upgrade i18n success.------------"); + }).catch((err) => { + console.error("------------upgrade i18n failed!------------", err); + }); } }); - 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(path_1.default.join(projectPath, "lib/config/connector")).default; - console.warn("----> Starting service......"); - shutdown = await startup(pwd, simpleConnector).then((shutdown) => { - realConfig.lifecycle.onServerStart(); - return shutdown; - }); - }; - const watchSourcePath = path_1.default.join(projectPath, "src"); - console.log("Watching for changes in", watchSourcePath); - const watcher = chokidar_1.default.watch(watchSourcePath, { - persistent: true, - ignored: (file) => 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 = []; - const fileChangeHandler = async (path, type) => { - // 判断一下是不是以下扩展名:ts - if (!path.endsWith(".ts")) { - // 如果是json文件,复制或者删除 - if (path.endsWith(".json")) { - // const targetPath = path.replace("src", "lib"); // 这里直接替换不对,应该是拿到项目目录,把项目目录/src 换成项目目录/lib - const targetPath = path.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")); - if (type === "remove") { - fs_1.default.unlinkSync(targetPath); - console.warn(`File ${targetPath} has been removed.`); - } - else if (type === "add") { - fs_1.default.copyFileSync(path, targetPath, fs_1.default.constants.COPYFILE_FICLONE); - console.warn(`File ${path} has been created at ${targetPath}.`); - } - else if (type === "change") { - // 强制覆盖文件 - if (fs_1.default.existsSync(targetPath)) { - fs_1.default.unlinkSync(targetPath); - } - fs_1.default.copyFileSync(path, targetPath, fs_1.default.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 = path_1.default.resolve(path); - // 将src替换为lib - const libPath = modulePath - .replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")) - .replace(/\.ts$/, ".js"); - let compileOnly = false; - if (!require.cache[libPath]) { - // 如果是删除的话,直接尝试删除lib下的文件 - if (type === "remove") { - try { - fs_1.default.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, type) => { - 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"); + } + eventEmitter.on("server-restart-needed", async () => { + if (!serverManager.isRestarting()) { + console.log("----> Restarting server..."); + await serverManager.restart(); } }); - const dispose = async () => { - if (shutdown) { - await shutdown(); - } - await watcher.close(); - realConfig.lifecycle.onDispose(); - }; - 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); diff --git a/lib/template.js b/lib/template.js index 23a61b4..e1e16cc 100644 --- a/lib/template.js +++ b/lib/template.js @@ -1,6 +1,16 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.updateCompilerJsContent = exports.oakConfigContentWithWeb = exports.oakConfigContentWithWeChatMp = exports.appJsonContentWithWeChatMp = exports.projectConfigContentWithWeChatMp = exports.tsConfigWebJsonContent = exports.tsConfigMpJsonContent = exports.tsConfigPathsJsonContent = exports.tsConfigBuildJsonContent = exports.tsConfigJsonContent = exports.packageJsonContent = void 0; +exports.packageJsonContent = packageJsonContent; +exports.tsConfigJsonContent = tsConfigJsonContent; +exports.tsConfigBuildJsonContent = tsConfigBuildJsonContent; +exports.tsConfigPathsJsonContent = tsConfigPathsJsonContent; +exports.tsConfigMpJsonContent = tsConfigMpJsonContent; +exports.tsConfigWebJsonContent = tsConfigWebJsonContent; +exports.projectConfigContentWithWeChatMp = projectConfigContentWithWeChatMp; +exports.appJsonContentWithWeChatMp = appJsonContentWithWeChatMp; +exports.oakConfigContentWithWeChatMp = oakConfigContentWithWeChatMp; +exports.oakConfigContentWithWeb = oakConfigContentWithWeb; +exports.updateCompilerJsContent = updateCompilerJsContent; const tslib_1 = require("tslib"); const child_process_1 = require("child_process"); const fs_1 = require("fs"); @@ -300,7 +310,6 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i } `; } -exports.packageJsonContent = packageJsonContent; function tsConfigJsonContent() { return `{ "extends": "./tsconfig.paths.json", @@ -352,7 +361,6 @@ function tsConfigJsonContent() { ] }`; } -exports.tsConfigJsonContent = tsConfigJsonContent; function tsConfigBuildJsonContent() { return `{ "extends": "./tsconfig.build.paths.json", @@ -392,7 +400,6 @@ function tsConfigBuildJsonContent() { ] }`; } -exports.tsConfigBuildJsonContent = tsConfigBuildJsonContent; function tsConfigPathsJsonContent(deps) { const paths = { "@project/*": [ @@ -421,7 +428,6 @@ function tsConfigPathsJsonContent(deps) { } }, null, '\t'); } -exports.tsConfigPathsJsonContent = tsConfigPathsJsonContent; function tsConfigMpJsonContent() { return `{ "extends": "./tsconfig.paths.json", @@ -469,7 +475,6 @@ function tsConfigMpJsonContent() { ] }`; } -exports.tsConfigMpJsonContent = tsConfigMpJsonContent; function tsConfigWebJsonContent() { return `{ "extends": "./tsconfig.paths.json", @@ -519,7 +524,6 @@ function tsConfigWebJsonContent() { ] }`; } -exports.tsConfigWebJsonContent = tsConfigWebJsonContent; function projectConfigContentWithWeChatMp(oakConfigName, projectname, miniVersion) { return `{ "description": "项目配置文件", @@ -595,7 +599,6 @@ function projectConfigContentWithWeChatMp(oakConfigName, projectname, miniVersio } }`; } -exports.projectConfigContentWithWeChatMp = projectConfigContentWithWeChatMp; function appJsonContentWithWeChatMp(isDev) { const pages = [ '@project/pages/store/list/index', @@ -619,21 +622,18 @@ function appJsonContentWithWeChatMp(isDev) { "sitemapLocation": "sitemap.json" }`; } -exports.appJsonContentWithWeChatMp = appJsonContentWithWeChatMp; function oakConfigContentWithWeChatMp() { return `{ "theme": { } }`; } -exports.oakConfigContentWithWeChatMp = oakConfigContentWithWeChatMp; function oakConfigContentWithWeb() { return `{ "theme": { } }`; } -exports.oakConfigContentWithWeb = oakConfigContentWithWeb; function updateCompilerJsContent(directory, deps) { const compilerJsPath = (0, path_1.join)(directory, 'configuration', 'compiler.js'); (0, assert_1.default)((0, fs_1.existsSync)(compilerJsPath)); @@ -692,4 +692,3 @@ function updateCompilerJsContent(directory, deps) { (0, fs_1.writeFileSync)(compilerJsPath, code); } } -exports.updateCompilerJsContent = updateCompilerJsContent; diff --git a/lib/utils.js b/lib/utils.js index 389c73a..3694c23 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,14 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkNodeVersion = exports.randomString = exports.deWeight = exports.formatJsonByFile = exports.union = exports.intersect = exports.difference = exports.getStr = exports.findJson = void 0; +exports.findJson = findJson; +exports.getStr = getStr; +exports.difference = difference; +exports.intersect = intersect; +exports.union = union; +exports.formatJsonByFile = formatJsonByFile; +exports.deWeight = deWeight; +exports.randomString = randomString; +exports.checkNodeVersion = checkNodeVersion; const tslib_1 = require("tslib"); const chalk_1 = tslib_1.__importDefault(require("chalk")); /** @@ -20,7 +28,6 @@ function findJson(pathArr) { } return result; } -exports.findJson = findJson; /** * @name 已知前后文取中间文本 * @export @@ -34,7 +41,6 @@ function getStr(str, start, end) { let res = str.match(reg); return res ? res[1] : null; } -exports.getStr = getStr; /** * @name 差集 * @export @@ -46,7 +52,6 @@ exports.getStr = getStr; function difference(current, target) { return new Set([...target].filter(x => !current.has(x))); } -exports.difference = difference; /** * @name 获取交集 * @export @@ -58,7 +63,6 @@ exports.difference = difference; function intersect(current, target) { return new Set([...target].filter(x => current.has(x))); } -exports.intersect = intersect; /** * @name 获取并集 * @export @@ -70,7 +74,6 @@ exports.intersect = intersect; function union(current, target) { return new Set([...current, ...target]); } -exports.union = union; /** * @name 格式化json * @export @@ -81,7 +84,6 @@ exports.union = union; function formatJsonByFile(data) { return JSON.stringify(data, null, 2); } -exports.formatJsonByFile = formatJsonByFile; /** * @name 数组对象去重 * @export @@ -98,7 +100,6 @@ function deWeight(arr, type) { } return new Set([...map.values()]); } -exports.deWeight = deWeight; /** * @name 随机字符串 * @export @@ -114,7 +115,6 @@ function randomString(length) { } return result; } -exports.randomString = randomString; /** * @name 检查当前nodejs运行时版本 * @export @@ -135,4 +135,3 @@ function checkNodeVersion() { process.exit(-1); } } -exports.checkNodeVersion = checkNodeVersion; diff --git a/src/server/watch.ts b/src/server/watch.ts index bec0a12..964058a 100644 --- a/src/server/watch.ts +++ b/src/server/watch.ts @@ -4,6 +4,16 @@ import pathLib from "path"; import dayjs from "dayjs"; import fs from "fs"; import { cloneDeep } from "lodash"; +import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore"; + +/* + * 工作流程 + 文件变更检测 → 生成文件变更事件 + 事件处理 → 创建编译任务并加入队列 + 批量编译 → 处理队列中的所有任务 + 编译完成 → 触发服务器重启事件 + 服务器重启 → 清理缓存并重新启动服务 + */ declare const require: NodeRequire; declare const process: NodeJS.Process; @@ -17,6 +27,7 @@ export type LogFormatterProp = { export type LogFormatter = (props: LogFormatterProp) => any[]; export type WatchConfig = { + autoUpdateI18n?: boolean; /** * 是否启用polyfill */ @@ -47,32 +58,109 @@ export type WatchConfig = { * 初始化时调用 * @returns void */ - onInit?: () => void; + onInit?: (config: RealWatchConfig) => void; /** * 服务启动时调用 * @returns void */ - onServerStart?: () => void; + onServerStart?: (config: RealWatchConfig) => void; /** * 编译前调用 * @returns void */ - onBeforeCompile?: () => void; + onBeforeCompile?: (config: RealWatchConfig) => void; /** * 编译后调用 * @returns void */ - onAfterCompile?: () => void; + onAfterCompile?: (config: RealWatchConfig) => void; /** * 服务关闭时调用 * @returns void */ - onServerShutdown?: () => void; + onServerShutdown?: (config: RealWatchConfig) => void; /** * 销毁监视器时调用 * @returns void */ - onDispose?: () => 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); + } + } }; }; @@ -89,14 +177,99 @@ type DeepRequiredIfPresent = { : 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 }) => { + formatter: ({ level, caller, args }: LogFormatterProp) => { const getFileInfo = () => { if (!caller) { return ""; @@ -128,22 +301,22 @@ const defaultConfig: RealWatchConfig = { }, }, lifecycle: { - onInit: () => { + onInit: (config: RealWatchConfig) => { console.log("----> Watcher initialized."); }, - onServerStart: () => { + onServerStart: (config: RealWatchConfig) => { console.log("----> Server started."); }, - onBeforeCompile: () => { + onBeforeCompile: (config: RealWatchConfig) => { console.log("----> Compiling......"); }, - onAfterCompile: () => { + onAfterCompile: (config: RealWatchConfig) => { console.log("----> Compile completed."); }, - onServerShutdown: () => { + onServerShutdown: (config: RealWatchConfig) => { console.log("----> Server shutdown."); }, - onDispose: () => { + onDispose: (config: RealWatchConfig) => { console.log("----> Watcher disposed."); }, }, @@ -177,8 +350,327 @@ const getOverrideConfig = (config?: WatchConfig): RealWatchConfig => { : defaultConfig; }; -type AliasConfig = Record; -type ModuleType = import('module') +// 当前运行目录 +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 配置表将路径中的别名替换为真实路径 @@ -186,7 +678,7 @@ type ModuleType = import('module') * @param aliasConfig - alias 配置表,key 为别名,value 为对应的真实路径或路径数组 * @returns 替换后的路径,如果没有匹配到 alias,则返回原始路径 */ -function replaceAliasWithPath(path: string, aliasConfig: Record): string { +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('*')) { @@ -213,7 +705,7 @@ function replaceAliasWithPath(path: string, aliasConfig: Record { const value = aliasConfig[key]; // 替换src - aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v) => v.replace("src", "lib")); + 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); - 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文件 + // 初始化polyfill const polyfillLoader = () => { const BuiltinModule = require("module"); // 模拟环境下的 module 构造函数 @@ -311,11 +769,13 @@ export const watch = ( // 检查并编译 .ts 文件 if (fs.existsSync(tsPath)) { console.log(`[resolve] Found TypeScript source file: ${tsPath}, attempting to compile...`); - const { program, sourceFile, diagnostics } = createProgramAndSourceFile( - tsPath, + const program = ts.createProgram({ + rootNames: [tsPath], options, - projectReferences - ); + projectReferences, + }); + const sourceFile = program.getSourceFile(tsPath); + const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); if (diagnostics.length) { console.error(`[resolve] Compilation failed for: ${tsPath}`); @@ -396,9 +856,6 @@ export const watch = ( }; }; - polyfillLoader(); - - //polyfill console.log 添加时间 const polyfillConsole = (trace: boolean) => { // 获取调用堆栈信息 const getCallerInfo = (): NodeJS.CallSite | null => { @@ -433,7 +890,7 @@ export const watch = ( const oldFunc = console[levelStr]; console[levelStr] = function (...args) { oldFunc( - ...(defaultConfig.polyfill?.console?.formatter({ + ...(realConfig.polyfill?.console?.formatter({ level: levelStr, caller: getCallerInfo(), args, @@ -443,293 +900,153 @@ export const watch = ( }); }; + // 初始化polyfill + polyfillLoader(); realConfig.polyfill.console.enable && polyfillConsole(enableTrace); - // 这里注意要在require之前,因为require会触发编译 - const { startup } = require('./start') as { - startup: (pwd: string, connector: any) => Promise<() => Promise>; - } - - // 如果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" - )}` - ); + // 初始编译检查 + 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, }); - console.error(`文件存在语法错误,请检查修复后重试!`); - process.exit(1); + 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.`); } - // 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); } - // 所有要编译的目录 - // 其他涉及到的目录会在运行的时候自动编译,这里主要处理的是动态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) | undefined; + // 创建事件系统 + const eventEmitter = createEventEmitter(); - 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 compileQueue = createCompileQueue(eventEmitter); + const compiler = createCompiler(projectPath, options, projectReferences, aliasConfig, realConfig); + const serverManager = createServerManager(projectPath, eventEmitter, realConfig); + const fileWatcher = createFileWatcher(projectPath, eventEmitter); - const watchSourcePath = pathLib.join(projectPath, "src"); + // 设置编译器处理器 + compileQueue.setTaskProcessor(compiler.compileTask); - 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, + // 设置事件监听器 + eventEmitter.on("file-changed", (event: FileChangeEvent) => { + const task: CompileTask = { + id: generateTaskId(), + filePath: event.path, + changeType: event.type, + timestamp: event.timestamp + }; + compileQueue.addTask(task); }); - let startWatching = false; - - watcher.on("ready", () => { - console.warn("Initial scan complete. Ready for changes"); - startWatching = true; + eventEmitter.on("compile-batch-started", (data: { count: number }) => { + console.log(`----> Starting compilation batch (${data.count} files)...`); }); - watcher.on("error", (error) => console.log(`Watcher error: ${error}`)); + 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`); + }); - 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}.` - ); + // 编译成功之后,若设置的同步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>; } - } 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"); + startup(pwd, simpleConnector, true, true, checkAndUpdateI18n).then(() => { + console.log("------------upgrade i18n success.------------") + }).catch((err) => { + console.error("------------upgrade i18n failed!------------", err) + }) } }); + } - const dispose = async () => { - if (shutdown) { - await shutdown(); + eventEmitter.on("server-restart-needed", async () => { + if (!serverManager.isRestarting()) { + console.log("----> Restarting server..."); + await serverManager.restart(); } - await watcher.close(); - realConfig.lifecycle.onDispose(); - }; + }); - 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);