From 712964f288b5cf3a159c95faf85e6d67fe1968b2 Mon Sep 17 00:00:00 2001 From: QCQCQC <1220204124@zust.edu.cn> Date: Wed, 4 Dec 2024 16:47:08 +0800 Subject: [PATCH] =?UTF-8?q?server=20watch=E7=A7=BB=E8=87=B3cli=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/watch.ts | 314 ++++++++++++++++++++++++++++++++ template/scripts/watchServer.js | 309 +------------------------------ 2 files changed, 317 insertions(+), 306 deletions(-) create mode 100644 src/server/watch.ts diff --git a/src/server/watch.ts b/src/server/watch.ts new file mode 100644 index 0000000..d54cc77 --- /dev/null +++ b/src/server/watch.ts @@ -0,0 +1,314 @@ +import chokidar from "chokidar"; +import { join, resolve } from "path"; +import ts from "typescript"; +import path from "path"; +import { startup } from "./start"; +import dayjs from "dayjs"; +import fs from "fs"; +import { debounce } from "lodash"; + +declare const require: NodeRequire; +declare const process: NodeJS.Process; + +export const watch = (projectPath: string) => { + const enableTrace = !!process.env.ENABLE_TRACE; + + //polyfill console.log 添加时间 + const polyfill = (trace: boolean) => { + 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 = trace ? "]\x1B[0m\n" : "]\x1B[0m"; + + // 获取调用堆栈信息 + const getCallerInfo = (): NodeJS.CallSite | null => { + const originalFunc = Error.prepareStackTrace; + let callerInfo: NodeJS.CallSite | null = null; + try { + const err = new Error(); + Error.prepareStackTrace = (err, stack) => stack; + const stack = err.stack as unknown as NodeJS.CallSite[]; // Type assertion here + const currentFile = stack[0].getFileName(); + + for (let i = 1; i < stack.length; i++) { + // Start from index 1 + const callSite = stack[i]; + if (currentFile !== callSite.getFileName()) { + callerInfo = callSite; + break; + } + } + } catch (e) { + console.error(e); + } + Error.prepareStackTrace = originalFunc; + return callerInfo; + }; + + 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: (() => Promise) | undefined; + + + 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(join(projectPath, 'lib/config/connector')).default; + console.warn("----> Starting service......"); + shutdown = await startup(pwd, simpleConnector); + }; + + const watchSourcePath = join(projectPath, "src"); + + console.log("Watching for changes in", watchSourcePath); + + // 查找配置文件 + 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, + path.dirname(configFileName) + ); + + const watcher = chokidar.watch(watchSourcePath, { + persistent: true, + ignored: (file: string) => + 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: string, + type: "add" | "remove" | "change" + ) => { + // 判断一下是不是以下扩展名: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.unlinkSync(targetPath); + console.warn(`File ${targetPath} has been removed.`); + } else if (type === "add") { + fs.copyFileSync( + path, + targetPath, + fs.constants.COPYFILE_FICLONE + ); + console.warn( + `File ${path} has been created at ${targetPath}.` + ); + } else if (type === "change") { + // 强制覆盖文件 + if (fs.existsSync(targetPath)) { + fs.unlinkSync(targetPath); + } + fs.copyFileSync( + path, + targetPath, + fs.constants.COPYFILE_FICLONE + ); + console.warn( + `File ${path} has been copied to ${targetPath}.` + ); + } + } else { + console.warn(`File ${path} is not [ts,json,xml] file, skiped.`); + } + return; + } + // 控制台清空 + console.clear(); + console.warn(`File ${path} has been ${type}d`); + // 先判断一下这个文件在不在require.cache里面 + const modulePath = 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.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 = 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; + } + } + // 只输出单个文件 + 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 = debounce( + async (path: string, type: "add" | "remove" | "change") => { + 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(); +}; diff --git a/template/scripts/watchServer.js b/template/scripts/watchServer.js index c684aad..f99a024 100644 --- a/template/scripts/watchServer.js +++ b/template/scripts/watchServer.js @@ -1,309 +1,6 @@ /* eslint-disable @typescript-eslint/no-require-imports */ require('module-alias/register'); -const chokidar = require('chokidar'); -const { join } = require('path'); -const ts = require('typescript'); -const path = require('path'); -const { resolve } = require('path'); -const _ = require('lodash'); -const { startup } = require('@xuchangzju/oak-cli/lib/server/start'); -const projectPath = join(__dirname, '..'); -const dayjs = require('dayjs'); -const fs = require('fs'); +const { watch } = require('@xuchangzju/oak-cli/lib/server/watch'); +const pwd = process.cwd(); -const enableTrace = !!process.env.ENABLE_TRACE; - -//polyfill console.log 添加时间 -const polyfill = (trace) => { - const getTime = () => { - return 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 = trace ? ']\x1B[0m\n' : ']\x1B[0m'; - // 获取调用堆栈信息 - const getCallerInfo = () => { - const originalFunc = Error.prepareStackTrace; - let callerInfo = null; - try { - const err = new Error(); - Error.prepareStackTrace = (err, stack) => { - return stack; - }; - const currentFile = err.stack.shift().getFileName(); - while (err.stack.length) { - callerInfo = err.stack.shift(); - if (currentFile !== callerInfo.getFileName()) { - 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 () { - oldLog(infoStart, getTime(), getFileInfo(), clearColor, ...arguments); - }; - - const oldWarn = console.warn; - console.warn = function () { - oldWarn(warnStart, getTime(), getFileInfo(), clearColor, ...arguments); - }; - - const oldError = console.error; - console.error = function () { - oldError( - errorStart, - getTime(), - getFileInfo(), - clearColor, - ...arguments - ); - }; -}; - -polyfill(enableTrace); - -let shutdown; - -const restart = async () => { - if (shutdown) { - await shutdown(); - } - // 清空lib以下目录的缓存 - // 删除所有模块的缓存 - Object.keys(require.cache).forEach(function (key) { - // 如果不是项目目录下的文件,不删除 - if (!key.startsWith(projectPath)) { - return; - } else if (key.includes('lib') && !key.includes('node_modules')) { - console.log('delete module cache:', key); - delete require.cache[key]; - } - }); - const pwd = process.cwd(); - const simpleConnector = require('../lib/config/connector').default; - console.warn('----> Starting service......'); - shutdown = await startup(pwd, simpleConnector); -}; - -const watchSourcePath = join(projectPath, 'src'); - -console.log('Watching for changes in', watchSourcePath); - -// 查找配置文件 -const configFileName = ts.findConfigFile( - /*searchPath*/ 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, - path.dirname(configFileName) -); - -const watcher = chokidar.watch(watchSourcePath, { - persistent: true, - // ignore render and i18n files - ignored: (file) => { - return ( - file.endsWith('.tsx') || - file.includes('components') || - file.includes('pages') || - file.includes('hooks') - ); - }, - // awaitWriteFinish: true, // emit single event when chunked writes are completed - // atomic: true, // emit proper events when "atomic writes" (mv _tmp file) are used - 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.unlinkSync(targetPath); - console.warn(`File ${targetPath} has been removed.`); - } else if (type === 'add') { - fs.copyFileSync( - path, - targetPath, - fs.constants.COPYFILE_FICLONE - ); - console.warn(`File ${path} has been created at ${targetPath}.`); - } else if (type === 'change') { - // 强制覆盖文件 - if (fs.existsSync(targetPath)) { - fs.unlinkSync(targetPath); - } - fs.copyFileSync( - path, - targetPath, - fs.constants.COPYFILE_FICLONE - ); - console.warn(`File ${path} has been copied to ${targetPath}.`); - } - } else { - console.warn(`File ${path} is not [ts,json,xml] file, skiped.`); - } - return; - } - // 控制台清空 - console.clear(); - console.warn(`File ${path} has been ${type}d`); - // 先判断一下这个文件在不在require.cache里面 - const modulePath = 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.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 = 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; - } - } - // 只输出单个文件 - 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 = _.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(); +watch(pwd);