oak-cli/lib/server/watch.js

493 lines
22 KiB
JavaScript
Raw 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 lodash_1 = require("lodash");
const defaultConfig = {
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: () => {
console.log("----> Watcher initialized.");
},
onServerStart: () => {
console.log("----> Server started.");
},
onBeforeCompile: () => {
console.log("----> Compiling......");
},
onAfterCompile: () => {
console.log("----> Compile completed.");
},
onServerShutdown: () => {
console.log("----> Server shutdown.");
},
onDispose: () => {
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;
};
/**
* 根据 alias 配置表将路径中的别名替换为真实路径
* @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
* @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组
* @returns 替换后的路径,如果没有匹配到 alias则返回原始路径
*/
function replaceAliasWithPath(path, aliasConfig) {
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 = (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 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 = (0, lodash_1.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) => v.replace("src", "lib"));
});
// 输出真实的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文件
const polyfillLoader = () => {
const BuiltinModule = require("module");
// 兼容一些模拟环境下的 module 构造函数
const Module = module.constructor.length > 1 ? module.constructor : BuiltinModule;
// 保存原始的 _resolveFilename 方法
const oldResolveFilename = Module._resolveFilename;
// 重写 _resolveFilename 方法
Module._resolveFilename = function (request, // 模块请求路径
parent, // 调用方模块
isMain, // 是否是主模块
rFoptions // 解析选项
) {
let resolvedRequest = request; // 用于存储替换后的路径
// 使用 replaceAliasWithPath 进行 alias 路径替换
const replacedPath = replaceAliasWithPath(request, aliasConfig);
if (replacedPath !== request) {
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`); // 记录替换日志
resolvedRequest = path_1.default.join(projectPath, replacedPath); // 更新解析路径
}
let filename; // 用于存储最终解析的文件名
try {
// 调用原始的 _resolveFilename 方法尝试解析文件
filename = oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
}
catch (error) {
console.log(`[resolve] Failed to resolve: ${resolvedRequest}`); // 打印解析失败信息
// 如果解析失败,尝试处理 .ts 源文件的动态编译
if (error.code === "MODULE_NOT_FOUND") {
// 构造 .js 文件路径
const jsPath = path_1.default.resolve(path_1.default.dirname(parent.filename), resolvedRequest + ".js");
// 替换为对应的 .ts 文件路径
const tsPath = jsPath
.replace(/\.js$/, ".ts")
.replace(path_1.default.join(projectPath, "lib"), // 替换 lib 为 src
path_1.default.join(projectPath, "src"));
// 检查对应的 .ts 文件是否存在
if (fs_1.default.existsSync(tsPath)) {
console.log(`[resolve] Found TypeScript source file: ${tsPath}, attempting to compile...`);
// 调用自定义函数进行 TypeScript 编译
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(tsPath, options, projectReferences);
// 如果存在编译错误,抛出异常
if (diagnostics.length) {
console.error(`[resolve] Compilation failed for: ${tsPath}`);
throw error;
}
// 执行编译,并检查是否成功
const emitResult = program.emit(sourceFile);
if (emitResult.emitSkipped) {
console.error(`[resolve] Emit skipped for: ${tsPath}`);
throw error;
}
console.log(`[resolve] Successfully compiled: ${tsPath}`);
// 将解析路径更新为对应的 .js 文件路径
filename = jsPath;
}
else {
// 如果 .ts 文件不存在,打印错误并抛出异常
console.error(`[resolve] ${tsPath} does not exist. Unable to resolve module.`);
throw error;
}
}
else {
// 如果不是 MODULE_NOT_FOUND 异常,直接抛出
throw error;
}
}
// 返回最终解析的文件名
return filename;
};
};
polyfillLoader();
//polyfill console.log 添加时间
const polyfillConsole = (trace) => {
// 获取调用堆栈信息
const getCallerInfo = () => {
if (!trace) {
return null;
}
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;
};
// polyfill console.log 添加时间和文件位置
["info", "warn", "error"].forEach((level) => {
const levelStr = level;
const oldFunc = console[levelStr];
console[levelStr] = function (...args) {
oldFunc(...(defaultConfig.polyfill?.console?.formatter({
level: levelStr,
caller: getCallerInfo(),
args,
}) || []));
};
});
};
realConfig.polyfill.console.enable && polyfillConsole(enableTrace);
// 这里注意要在require之前因为require会触发编译
const { startup } = require('./start');
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++;
}
});
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");
}
});
const dispose = async () => {
if (shutdown) {
await shutdown();
}
await watcher.close();
realConfig.lifecycle.onDispose();
};
restart()
.then(() => {
resolve(dispose);
})
.catch(reject);
});
};
exports.watch = watch;