oak-cli/lib/server/watch.js

747 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 typescript_1 = tslib_1.__importDefault(require("typescript"));
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 polyfill_1 = require("./polyfill");
const tsc_alias_1 = require("tsc-alias");
// 创建事件发射器
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,
trace: true,
formatter: ({ level, caller, args }) => {
const getFileInfo = () => {
if (!caller) {
return "";
}
const fileInfo = caller
? `${caller.getFileName()}:${caller.getLineNumber()}`
: "";
return fileInfo.trim();
};
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 = caller ? "]\x1B[0m\n" : "]\x1B[0m";
const levelStart = level === "info"
? infoStart
: level === "warn"
? warnStart
: errorStart;
return [
levelStart,
getTime(),
getFileInfo(),
clearColor,
...args,
];
},
},
},
lifecycle: {
onInit: (config) => {
console.log("----> Watcher initialized.");
},
onServerStart: (config) => {
console.log("----> Server started.");
},
onBeforeCompile: (config) => {
console.log("----> Compiling......");
},
onAfterCompile: (config) => {
console.log("----> Compile completed.");
},
onServerShutdown: (config) => {
console.log("----> Server shutdown.");
},
onDispose: (config) => {
console.log("----> Watcher disposed.");
},
},
};
const getOverrideConfig = (config) => {
// 遍历default里面的每一个key如果config里面有就覆盖没有就用default的
const overrideInner = (obj, dobj) => {
const result = {};
Object.keys(dobj).forEach((key) => {
const value = dobj[key];
if (typeof value === "object") {
const v = obj[key];
if (v === undefined) {
result[key] = value;
}
else {
result[key] = overrideInner(v, value);
}
}
else {
if (obj && obj[key] !== undefined) {
result[key] = obj[key];
}
else {
result[key] = value;
}
}
});
return result;
};
return config
? overrideInner(config, defaultConfig)
: 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......");
try {
// 这里注意要在require之前因为require会触发编译
const { startup } = require('./start');
shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
config.lifecycle.onServerStart(config);
return shutdown;
});
}
catch (error) {
console.error("----> Failed to start service:", error);
isRestarting = false;
return;
}
isRestarting = false;
await eventEmitter.emit("server-restarted", {});
};
const dispose = async () => {
if (shutdown) {
await shutdown();
}
};
return {
restart,
dispose,
isRestarting: () => isRestarting
};
};
// 创建编译器
const createCompiler = async (projectPath, options, projectReferences, treatFile, 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);
const jsFilePath = libPath;
treatFile(jsFilePath);
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则返回原始路径
// */
// const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => {
// for (const [alias, targets] of Object.entries(aliasConfig)) {
// // If alias ends with "*", handle it as a dynamic alias.
// if (alias.endsWith('*')) {
// // Create a regex pattern that matches paths starting with the alias, followed by any characters
// const aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)'
// const match = path.match(aliasPattern);
// if (match) {
// // Replace the alias with the target path, appending the matched part from the original path
// const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
// // Ensure that the target path ends with a slash if it's not already
// const replacedPath = target.replace(/\/\*$/, "/") + match[1];
// return replacedPath;
// }
// } else {
// // Handle static alias without "*" by directly matching the path
// if (path.startsWith(alias)) {
// const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
// // Replace the alias part with the target path
// return path.replace(alias, target);
// }
// }
// }
// // If no alias matches, return the original path
// return path;
// };
const watch = async (projectPath, config) => {
const realConfig = getOverrideConfig(config);
const enableTrace = !!process.env.ENABLE_TRACE;
// 查找配置文件
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 runFile = await (0, tsc_alias_1.prepareSingleFileReplaceTscAliasPaths)({
configFile: path_1.default.join(projectPath, "tsconfig.build.json"),
resolveFullPaths: true,
});
function treatFile(filePath) {
const fileContents = fs_1.default.readFileSync(filePath, 'utf8');
const newContents = runFile({ fileContents, filePath });
// do stuff with newContents
fs_1.default.writeFileSync(filePath, newContents, 'utf8');
}
// // 读取配置文件
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_1.default.dirname(configFileName));
// const aliasConfig: AliasConfig = cloneDeep(options.paths) || {};
// // 输出原始配置
// // console.log("[DEBUG] Original alias config:", aliasConfig);
// Object.keys(aliasConfig).forEach((key) => {
// const value = aliasConfig[key];
// // 替换src
// aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v: string) => v.replace("src", "lib"));
// });
// 输出真实的alias配置
// console.debug("[DEBUG] Running Alias config:", aliasConfig);
// 初始化polyfill
const polyfillLoader = () => {
const BuiltinModule = require("module");
// 模拟环境下的 module 构造函数
const Module = module.constructor.length > 1 ? module.constructor : BuiltinModule;
// 保存原始 _resolveFilename 方法
const oldResolveFilename = Module._resolveFilename;
// 通用的路径检查与编译函数
function resolveAndCompile(requestPath, parent, options, projectReferences) {
const jsPath = path_1.default.resolve(requestPath + ".js");
const tsPath = jsPath.replace(/\.js$/, ".ts").replace(path_1.default.join(projectPath, "lib"), path_1.default.join(projectPath, "src"));
// 检查并编译 .ts 文件
if (fs_1.default.existsSync(tsPath)) {
console.log(`[resolve] Found TypeScript source file: ${tsPath}, attempting to compile...`);
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");
}
const emitResult = program.emit(sourceFile);
if (emitResult.emitSkipped) {
console.error(`[resolve] Emit skipped for: ${tsPath}`);
throw new Error("TypeScript emit skipped");
}
else {
treatFile(jsPath);
}
console.log(`[resolve] Successfully compiled: ${tsPath}`);
return jsPath;
}
// 如果没有找到对应的 .ts 文件
if (fs_1.default.existsSync(jsPath)) {
return jsPath;
}
throw new Error(`[resolve] Unable to find module: ${requestPath}`);
}
// 处理文件夹导入的情况
function resolveDirectory(requestPath, parent, options, projectReferences) {
const indexJs = path_1.default.join(requestPath, "index.js");
const indexTs = path_1.default.join(requestPath, "index.ts").replace(path_1.default.join(projectPath, "lib"), path_1.default.join(projectPath, "src"));
if (fs_1.default.existsSync(indexTs)) {
console.log(`[resolve] Found TypeScript index file: ${indexTs}, attempting to compile...`);
return resolveAndCompile(indexTs, parent, options, projectReferences);
}
if (fs_1.default.existsSync(indexJs)) {
return indexJs;
}
throw new Error(`[resolve] No index file found in directory: ${requestPath}`);
}
// 重写 _resolveFilename 方法
Module._resolveFilename = function (request, // 模块请求路径
parent, // 调用方模块
isMain, // 是否是主模块
rFoptions // 解析选项
) {
let resolvedRequest = request;
// const replacedPath = replaceAliasWithPath(request, aliasConfig);
// if (replacedPath !== request) {
// console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
// resolvedRequest = pathLib.join(projectPath, replacedPath);
// }
try {
return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
}
catch (error) {
if (error.code === "MODULE_NOT_FOUND") {
const requestPath = path_1.default.resolve(path_1.default.dirname(parent.filename), resolvedRequest);
// 处理文件夹导入
if (fs_1.default.existsSync(requestPath) && fs_1.default.statSync(requestPath).isDirectory()) {
return resolveDirectory(requestPath, parent, options, projectReferences);
}
// 处理单文件导入
return resolveAndCompile(requestPath, parent, options, projectReferences);
}
throw error;
}
};
};
// 初始化polyfill
polyfillLoader();
realConfig.polyfill.console.enable && (0, polyfill_1.polyfillConsole)("watch", enableTrace, realConfig.polyfill?.console?.formatter);
// 初始编译检查
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,
});
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.`);
}
};
// 所有要编译的目录
// 其他涉及到的目录会在运行的时候自动编译这里主要处理的是动态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);
// 最后替换lib目录下所有的路径别名
console.log(`[watch] Replacing path aliases in lib directory......`);
const libDir = path_1.default.join(projectPath, "lib");
const walkDir = (dir) => {
const files = fs_1.default.readdirSync(dir);
files.forEach((file) => {
const fullPath = path_1.default.join(dir, file);
const stat = fs_1.default.statSync(fullPath);
if (stat.isDirectory()) {
walkDir(fullPath);
}
else if (stat.isFile() && fullPath.endsWith(".js")) {
const fileContents = fs_1.default.readFileSync(fullPath, 'utf8');
const newContents = runFile({ fileContents, filePath: fullPath });
fs_1.default.writeFileSync(fullPath, newContents, 'utf8');
}
});
};
walkDir(libDir);
console.log(`[watch] Path alias replacement completed.`);
}
};
return new Promise(async (resolve, reject) => {
// 创建事件系统
const eventEmitter = createEventEmitter();
// 创建各个组件
const compileQueue = createCompileQueue(eventEmitter);
const compiler = await createCompiler(projectPath, options, projectReferences, treatFile, 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.-------------");
// 这里是copyupgradeI18n的
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);
});
}
});
}
eventEmitter.on("server-restart-needed", async () => {
if (!serverManager.isRestarting()) {
console.log("----> Restarting server...");
await serverManager.restart();
}
});
// 初始化
realConfig.lifecycle.onInit(realConfig);
// 执行初始编译
initialCompile();
// 启动服务器
serverManager.restart()
.then(() => {
const dispose = async () => {
await fileWatcher.dispose();
await serverManager.dispose();
realConfig.lifecycle.onDispose(realConfig);
};
resolve(dispose);
})
.catch(reject);
});
};
exports.watch = watch;