diff --git a/lib/server/start.d.ts b/lib/server/start.d.ts index 5f38b10..2e45380 100644 --- a/lib/server/start.d.ts +++ b/lib/server/start.d.ts @@ -4,4 +4,4 @@ import { Connector, EntityDict } from 'oak-domain/lib/types'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; -export declare function startup, Cxt extends BackendRuntimeContext>(path: string, connector: Connector, omitWatchers?: boolean, omitTimers?: boolean, routine?: (context: AsyncContext) => Promise): Promise<(() => void) | undefined>; +export declare function startup, Cxt extends BackendRuntimeContext>(path: string, connector: Connector, omitWatchers?: boolean, omitTimers?: boolean, routine?: (context: AsyncContext) => Promise): Promise<(() => Promise) | undefined>; diff --git a/lib/server/start.js b/lib/server/start.js index b8a5e1c..97ea8ce 100644 --- a/lib/server/start.js +++ b/lib/server/start.js @@ -235,8 +235,8 @@ async function startup(path, connector, omitWatchers, omitTimers, routine) { process.exit(0); }); const shutdown = async () => { - httpServer.close(); - koa.removeAllListeners(); + await httpServer.close(); + await koa.removeAllListeners(); await appLoader.unmount(); }; return shutdown; diff --git a/lib/server/watch.d.ts b/lib/server/watch.d.ts new file mode 100644 index 0000000..cc96d31 --- /dev/null +++ b/lib/server/watch.d.ts @@ -0,0 +1 @@ +export declare const watch: (projectPath: string) => void; diff --git a/lib/server/watch.js b/lib/server/watch.js new file mode 100644 index 0000000..62d0e5a --- /dev/null +++ b/lib/server/watch.js @@ -0,0 +1,259 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.watch = void 0; +const tslib_1 = require("tslib"); +const chokidar_1 = tslib_1.__importDefault(require("chokidar")); +const path_1 = require("path"); +const typescript_1 = tslib_1.__importDefault(require("typescript")); +const path_2 = tslib_1.__importDefault(require("path")); +const start_1 = require("./start"); +const dayjs_1 = tslib_1.__importDefault(require("dayjs")); +const fs_1 = tslib_1.__importDefault(require("fs")); +const lodash_1 = require("lodash"); +const watch = (projectPath) => { + const enableTrace = !!process.env.ENABLE_TRACE; + //polyfill console.log 添加时间 + const polyfill = (trace) => { + const getTime = () => (0, dayjs_1.default)().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 = trace ? "]\x1B[0m\n" : "]\x1B[0m"; + // 获取调用堆栈信息 + const getCallerInfo = () => { + const originalFunc = Error.prepareStackTrace; + let callerInfo = null; + try { + const err = new Error(); + Error.prepareStackTrace = (err, stack) => stack; + const stack = err.stack; // Type assertion here + const currentFile = stack[0].getFileName(); + for (let i = 1; i < stack.length; i++) { + // Start from index 1 + const callSite = stack[i]; + if (currentFile !== callSite.getFileName()) { + callerInfo = callSite; + break; + } + } + } + catch (e) { + console.error(e); + } + Error.prepareStackTrace = originalFunc; + return callerInfo; + }; + const getFileInfo = () => { + if (!trace) { + return ""; + } + const callerInfo = getCallerInfo(); + const fileInfo = callerInfo + ? `${callerInfo.getFileName()}:${callerInfo.getLineNumber()}` + : ""; + return fileInfo.trim(); + }; + // polyfill console.log 添加时间和文件位置 + const oldLog = console.log; + console.log = function (...args) { + oldLog(infoStart, getTime(), getFileInfo(), clearColor, ...args); + }; + const oldWarn = console.warn; + console.warn = function (...args) { + oldWarn(warnStart, getTime(), getFileInfo(), clearColor, ...args); + }; + const oldError = console.error; + console.error = function (...args) { + oldError(errorStart, getTime(), getFileInfo(), clearColor, ...args); + }; + }; + polyfill(enableTrace); + let shutdown; + const restart = async () => { + if (shutdown) { + await shutdown(); + } + 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((0, path_1.join)(projectPath, 'lib/config/connector')).default; + console.warn("----> Starting service......"); + shutdown = await (0, start_1.startup)(pwd, simpleConnector); + }; + const watchSourcePath = (0, path_1.join)(projectPath, "src"); + console.log("Watching for changes in", watchSourcePath); + // 查找配置文件 + const configFileName = typescript_1.default.findConfigFile(projectPath, typescript_1.default.sys.fileExists, "tsconfig.build.json"); + if (!configFileName) { + throw new Error("Could not find a valid 'tsconfig.build.json'."); + } + // 读取配置文件 + const configFile = typescript_1.default.readConfigFile(configFileName, typescript_1.default.sys.readFile); + // 解析配置文件内容 + const { options, projectReferences } = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, path_2.default.dirname(configFileName)); + const watcher = chokidar_1.default.watch(watchSourcePath, { + persistent: true, + ignored: (file) => file.endsWith(".tsx") || + 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 isProcessing = false; + const fileChangeHandler = async (path, type) => { + // 判断一下是不是以下扩展名:ts,tsx + if (!path.endsWith(".ts")) { + // 如果是json或者xml文件,复制或者删除 + if (path.endsWith(".json") || path.endsWith(".xml")) { + const targetPath = path.replace("src", "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,xml] file, skiped.`); + } + return; + } + // 控制台清空 + console.clear(); + console.warn(`File ${path} has been ${type}d`); + // 先判断一下这个文件在不在require.cache里面 + const modulePath = (0, path_1.resolve)(path); + // 将src替换为lib + const libPath = modulePath + .replace("\\src\\", "\\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; + } + else { + // 如果是删除,则需要发出警告,文件正在被进程使用 + if (type === "remove") { + console.error(`File ${libPath} is being used, skiped.`); + return; + } + } + 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; + } + } + // 只输出单个文件 + const emitResult = program.emit(sourceFile); + // 是否成功 + const result = emitResult.emitSkipped; + if (result) { + console.error(`Emit failed for ${path}!`); + } + else { + console.log(`Emit succeeded. ${compileOnly ? "" : "reload service......"}`); + if (compileOnly) { + return; + } + await restart(); + } + }; + const onChangeDebounced = (0, lodash_1.debounce)(async (path, type) => { + if (isProcessing) { + console.log("Processing, please wait..."); + return; + } + try { + isProcessing = true; + await fileChangeHandler(path, type); + } + catch (e) { + console.clear(); + console.error(e); + } + finally { + isProcessing = false; + } + }, 100); + watcher + .on("add", (path) => { + if (startWatching) { + onChangeDebounced(path, "add"); + } + }) + .on("change", (path) => { + if (startWatching) { + onChangeDebounced(path, "change"); + } + }) + .on("unlink", (path) => { + if (startWatching) { + onChangeDebounced(path, "remove"); + } + }); + restart(); +}; +exports.watch = watch; diff --git a/lib/template.js b/lib/template.js index a55cd27..3cd23b4 100644 --- a/lib/template.js +++ b/lib/template.js @@ -303,7 +303,10 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i "readable-stream": "3.6.2" }, "_moduleAliases": { - "@project": "./lib" + "@project": "./lib", + "@oak-general-business": "./node_modules/oak-general-business/lib", + "@oak-frontend-base": "./node_modules/oak-frontend-base/lib", + "@oak-app-domain": "./lib/oak-app-domain" } } `;