watch不再需要单独配置module-alias,直接从tsconfig读取,并使用队列来维护待编译文件,如果发现import的文件不存在,现在会尝试编译
This commit is contained in:
parent
3779a8c2de
commit
1d1f9bb8b2
|
|
@ -5,7 +5,6 @@ const tslib_1 = require("tslib");
|
||||||
const chokidar_1 = tslib_1.__importDefault(require("chokidar"));
|
const chokidar_1 = tslib_1.__importDefault(require("chokidar"));
|
||||||
const typescript_1 = tslib_1.__importDefault(require("typescript"));
|
const typescript_1 = tslib_1.__importDefault(require("typescript"));
|
||||||
const path_1 = tslib_1.__importDefault(require("path"));
|
const path_1 = tslib_1.__importDefault(require("path"));
|
||||||
const start_1 = require("./start");
|
|
||||||
const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
|
const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
|
||||||
const fs_1 = tslib_1.__importDefault(require("fs"));
|
const fs_1 = tslib_1.__importDefault(require("fs"));
|
||||||
const lodash_1 = require("lodash");
|
const lodash_1 = require("lodash");
|
||||||
|
|
@ -95,9 +94,156 @@ const getOverrideConfig = (config) => {
|
||||||
? overrideInner(config, defaultConfig)
|
? overrideInner(config, defaultConfig)
|
||||||
: 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 watch = (projectPath, config) => {
|
||||||
const realConfig = getOverrideConfig(config);
|
const realConfig = getOverrideConfig(config);
|
||||||
const enableTrace = !!process.env.ENABLE_TRACE;
|
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 添加时间
|
//polyfill console.log 添加时间
|
||||||
const polyfillConsole = (trace) => {
|
const polyfillConsole = (trace) => {
|
||||||
// 获取调用堆栈信息
|
// 获取调用堆栈信息
|
||||||
|
|
@ -141,6 +287,8 @@ const watch = (projectPath, config) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
realConfig.polyfill.console.enable && polyfillConsole(enableTrace);
|
realConfig.polyfill.console.enable && polyfillConsole(enableTrace);
|
||||||
|
// 这里注意要在require之前,因为require会触发编译
|
||||||
|
const { startup } = require('./start');
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
realConfig.lifecycle.onInit();
|
realConfig.lifecycle.onInit();
|
||||||
let shutdown;
|
let shutdown;
|
||||||
|
|
@ -170,22 +318,13 @@ const watch = (projectPath, config) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
|
const simpleConnector = require(path_1.default.join(projectPath, "lib/config/connector")).default;
|
||||||
console.warn("----> Starting service......");
|
console.warn("----> Starting service......");
|
||||||
shutdown = await (0, start_1.startup)(pwd, simpleConnector).then((shutdown) => {
|
shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
|
||||||
realConfig.lifecycle.onServerStart();
|
realConfig.lifecycle.onServerStart();
|
||||||
return shutdown;
|
return shutdown;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const watchSourcePath = path_1.default.join(projectPath, "src");
|
const watchSourcePath = path_1.default.join(projectPath, "src");
|
||||||
console.log("Watching for changes in", watchSourcePath);
|
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_1.default.dirname(configFileName));
|
|
||||||
const watcher = chokidar_1.default.watch(watchSourcePath, {
|
const watcher = chokidar_1.default.watch(watchSourcePath, {
|
||||||
persistent: true,
|
persistent: true,
|
||||||
ignored: (file) => file.endsWith(".tsx") ||
|
ignored: (file) => file.endsWith(".tsx") ||
|
||||||
|
|
@ -209,13 +348,14 @@ const watch = (projectPath, config) => {
|
||||||
startWatching = true;
|
startWatching = true;
|
||||||
});
|
});
|
||||||
watcher.on("error", (error) => console.log(`Watcher error: ${error}`));
|
watcher.on("error", (error) => console.log(`Watcher error: ${error}`));
|
||||||
let isProcessing = false;
|
let processingQueue = [];
|
||||||
const fileChangeHandler = async (path, type) => {
|
const fileChangeHandler = async (path, type) => {
|
||||||
// 判断一下是不是以下扩展名:ts
|
// 判断一下是不是以下扩展名:ts
|
||||||
if (!path.endsWith(".ts")) {
|
if (!path.endsWith(".ts")) {
|
||||||
// 如果是json文件,复制或者删除
|
// 如果是json文件,复制或者删除
|
||||||
if (path.endsWith(".json")) {
|
if (path.endsWith(".json")) {
|
||||||
const targetPath = path.replace("src", "lib");
|
// 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") {
|
if (type === "remove") {
|
||||||
fs_1.default.unlinkSync(targetPath);
|
fs_1.default.unlinkSync(targetPath);
|
||||||
console.warn(`File ${targetPath} has been removed.`);
|
console.warn(`File ${targetPath} has been removed.`);
|
||||||
|
|
@ -245,8 +385,8 @@ const watch = (projectPath, config) => {
|
||||||
const modulePath = path_1.default.resolve(path);
|
const modulePath = path_1.default.resolve(path);
|
||||||
// 将src替换为lib
|
// 将src替换为lib
|
||||||
const libPath = modulePath
|
const libPath = modulePath
|
||||||
.replace("\\src\\", "\\lib\\")
|
.replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib"))
|
||||||
.replace(".ts", ".js");
|
.replace(/\.ts$/, ".js");
|
||||||
let compileOnly = false;
|
let compileOnly = false;
|
||||||
if (!require.cache[libPath]) {
|
if (!require.cache[libPath]) {
|
||||||
// 如果是删除的话,直接尝试删除lib下的文件
|
// 如果是删除的话,直接尝试删除lib下的文件
|
||||||
|
|
@ -260,8 +400,11 @@ const watch = (projectPath, config) => {
|
||||||
console.warn(`File ${libPath} has been removed.`);
|
console.warn(`File ${libPath} has been removed.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.warn(`File ${libPath} is not in module cache, will compile only.`);
|
// console.warn(
|
||||||
compileOnly = true;
|
// `File ${libPath} is not in module cache, will compile only.`
|
||||||
|
// );
|
||||||
|
// compileOnly = true;
|
||||||
|
// 这里现在不需要仅编译了,因为在require的时候会自动编译ts文件
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 如果是删除,则需要发出警告,文件正在被进程使用
|
// 如果是删除,则需要发出警告,文件正在被进程使用
|
||||||
|
|
@ -270,24 +413,9 @@ const watch = (projectPath, config) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const program = typescript_1.default.createProgram({
|
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(path, options, projectReferences);
|
||||||
rootNames: [path],
|
|
||||||
options,
|
|
||||||
projectReferences,
|
|
||||||
});
|
|
||||||
const sourceFile = program.getSourceFile(path);
|
|
||||||
// 是否有语法错误
|
|
||||||
const diagnostics = typescript_1.default.getPreEmitDiagnostics(program, sourceFile);
|
|
||||||
if (diagnostics.length) {
|
if (diagnostics.length) {
|
||||||
const syntaxErrors = diagnostics.filter((diagnostic) => diagnostic.category === typescript_1.default.DiagnosticCategory.Error);
|
return;
|
||||||
if (syntaxErrors.length) {
|
|
||||||
console.error(`Error in ${path}`);
|
|
||||||
syntaxErrors.forEach((diagnostic) => {
|
|
||||||
console.error(`${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`);
|
|
||||||
});
|
|
||||||
console.error(`文件存在语法错误,请检查修复后重试!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 只输出单个文件
|
// 只输出单个文件
|
||||||
realConfig.lifecycle.onBeforeCompile();
|
realConfig.lifecycle.onBeforeCompile();
|
||||||
|
|
@ -304,40 +432,47 @@ const watch = (projectPath, config) => {
|
||||||
if (compileOnly) {
|
if (compileOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await restart();
|
// await restart(); // 只有在队列里面的最后一个文件编译完成后才会重启服务
|
||||||
|
if (processingQueue.length === 1) {
|
||||||
|
await restart();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("Waiting for other operations to complete...");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onChangeDebounced = (0, lodash_1.debounce)(async (path, type) => {
|
const onChangeDebounced = async (path, type) => {
|
||||||
if (isProcessing) {
|
if (processingQueue.includes(path)) {
|
||||||
console.log("Processing, please wait...");
|
console.log("Processing, please wait...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
isProcessing = true;
|
processingQueue.push(path);
|
||||||
await fileChangeHandler(path, type);
|
await fileChangeHandler(path, type);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.clear();
|
console.clear();
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
isProcessing = false;
|
processingQueue = processingQueue.filter((p) => p !== path);
|
||||||
}
|
}
|
||||||
}, 100);
|
};
|
||||||
watcher
|
watcher
|
||||||
.on("add", (path) => {
|
.on("add", async (path) => {
|
||||||
if (startWatching) {
|
if (startWatching) {
|
||||||
onChangeDebounced(path, "add");
|
await onChangeDebounced(path, "add");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("change", (path) => {
|
.on("change", async (path) => {
|
||||||
if (startWatching) {
|
if (startWatching) {
|
||||||
onChangeDebounced(path, "change");
|
await onChangeDebounced(path, "change");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("unlink", (path) => {
|
.on("unlink", async (path) => {
|
||||||
if (startWatching) {
|
if (startWatching) {
|
||||||
onChangeDebounced(path, "remove");
|
await onChangeDebounced(path, "remove");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const dispose = async () => {
|
const dispose = async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import chokidar from "chokidar";
|
import chokidar from "chokidar";
|
||||||
import ts from "typescript";
|
import ts, { CompilerOptions, ProjectReference } from "typescript";
|
||||||
import pathLib from "path";
|
import pathLib from "path";
|
||||||
import { startup } from "./start";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { debounce } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
declare const require: NodeRequire;
|
declare const require: NodeRequire;
|
||||||
declare const process: NodeJS.Process;
|
declare const process: NodeJS.Process;
|
||||||
|
|
@ -80,14 +79,14 @@ export type WatchConfig = {
|
||||||
// 真实config不会出现?,所以这里新增一个type表示真实的config
|
// 真实config不会出现?,所以这里新增一个type表示真实的config
|
||||||
type DeepRequiredIfPresent<T> = {
|
type DeepRequiredIfPresent<T> = {
|
||||||
[P in keyof T]-?: NonNullable<T[P]> extends (...args: any) => any // Check for function type
|
[P in keyof T]-?: NonNullable<T[P]> extends (...args: any) => any // Check for function type
|
||||||
? T[P] // Preserve the original function type
|
? T[P] // Preserve the original function type
|
||||||
: NonNullable<T[P]> extends (infer U)[]
|
: NonNullable<T[P]> extends (infer U)[]
|
||||||
? DeepRequiredIfPresent<U>[]
|
? DeepRequiredIfPresent<U>[]
|
||||||
: NonNullable<T[P]> extends object
|
: NonNullable<T[P]> extends object
|
||||||
? DeepRequiredIfPresent<NonNullable<T[P]>>
|
? DeepRequiredIfPresent<NonNullable<T[P]>>
|
||||||
: T[P] extends undefined | null
|
: T[P] extends undefined | null
|
||||||
? T[P]
|
? T[P]
|
||||||
: NonNullable<T[P]>;
|
: NonNullable<T[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RealWatchConfig = DeepRequiredIfPresent<WatchConfig>;
|
export type RealWatchConfig = DeepRequiredIfPresent<WatchConfig>;
|
||||||
|
|
@ -116,8 +115,8 @@ const defaultConfig: RealWatchConfig = {
|
||||||
level === "info"
|
level === "info"
|
||||||
? infoStart
|
? infoStart
|
||||||
: level === "warn"
|
: level === "warn"
|
||||||
? warnStart
|
? warnStart
|
||||||
: errorStart;
|
: errorStart;
|
||||||
return [
|
return [
|
||||||
levelStart,
|
levelStart,
|
||||||
getTime(),
|
getTime(),
|
||||||
|
|
@ -178,6 +177,43 @@ const getOverrideConfig = (config?: WatchConfig): RealWatchConfig => {
|
||||||
: defaultConfig;
|
: defaultConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AliasConfig = Record<string, string | string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 alias 配置表将路径中的别名替换为真实路径
|
||||||
|
* @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
|
||||||
|
* @param aliasConfig - alias 配置表,key 为别名,value 为对应的真实路径或路径数组
|
||||||
|
* @returns 替换后的路径,如果没有匹配到 alias,则返回原始路径
|
||||||
|
*/
|
||||||
|
function 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;
|
||||||
|
}
|
||||||
|
|
||||||
export const watch = (
|
export const watch = (
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
config?: WatchConfig
|
config?: WatchConfig
|
||||||
|
|
@ -185,6 +221,169 @@ export const watch = (
|
||||||
const realConfig = getOverrideConfig(config);
|
const realConfig = getOverrideConfig(config);
|
||||||
const enableTrace = !!process.env.ENABLE_TRACE;
|
const enableTrace = !!process.env.ENABLE_TRACE;
|
||||||
|
|
||||||
|
// 查找配置文件
|
||||||
|
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,
|
||||||
|
pathLib.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) => 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文件
|
||||||
|
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: string, // 模块请求路径
|
||||||
|
parent: typeof Module, // 调用方模块
|
||||||
|
isMain: boolean, // 是否是主模块
|
||||||
|
rFoptions: object | undefined // 解析选项
|
||||||
|
) {
|
||||||
|
let resolvedRequest = request; // 用于存储替换后的路径
|
||||||
|
|
||||||
|
// 使用 replaceAliasWithPath 进行 alias 路径替换
|
||||||
|
const replacedPath = replaceAliasWithPath(request, aliasConfig);
|
||||||
|
if (replacedPath !== request) {
|
||||||
|
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`); // 记录替换日志
|
||||||
|
resolvedRequest = pathLib.join(projectPath, replacedPath); // 更新解析路径
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename; // 用于存储最终解析的文件名
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用原始的 _resolveFilename 方法尝试解析文件
|
||||||
|
filename = oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(`[resolve] Failed to resolve: ${resolvedRequest}`); // 打印解析失败信息
|
||||||
|
|
||||||
|
// 如果解析失败,尝试处理 .ts 源文件的动态编译
|
||||||
|
if (error.code === "MODULE_NOT_FOUND") {
|
||||||
|
// 构造 .js 文件路径
|
||||||
|
const jsPath = pathLib.resolve(pathLib.dirname(parent.filename), resolvedRequest + ".js");
|
||||||
|
|
||||||
|
// 替换为对应的 .ts 文件路径
|
||||||
|
const tsPath = jsPath
|
||||||
|
.replace(/\.js$/, ".ts")
|
||||||
|
.replace(
|
||||||
|
pathLib.join(projectPath, "lib"), // 替换 lib 为 src
|
||||||
|
pathLib.join(projectPath, "src")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查对应的 .ts 文件是否存在
|
||||||
|
if (fs.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 添加时间
|
//polyfill console.log 添加时间
|
||||||
const polyfillConsole = (trace: boolean) => {
|
const polyfillConsole = (trace: boolean) => {
|
||||||
// 获取调用堆栈信息
|
// 获取调用堆栈信息
|
||||||
|
|
@ -232,6 +431,11 @@ export const watch = (
|
||||||
|
|
||||||
realConfig.polyfill.console.enable && polyfillConsole(enableTrace);
|
realConfig.polyfill.console.enable && polyfillConsole(enableTrace);
|
||||||
|
|
||||||
|
// 这里注意要在require之前,因为require会触发编译
|
||||||
|
const { startup } = require('./start') as {
|
||||||
|
startup: (pwd: string, connector: any) => Promise<() => Promise<void>>;
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
realConfig.lifecycle.onInit();
|
realConfig.lifecycle.onInit();
|
||||||
let shutdown: (() => Promise<void>) | undefined;
|
let shutdown: (() => Promise<void>) | undefined;
|
||||||
|
|
@ -278,26 +482,7 @@ export const watch = (
|
||||||
|
|
||||||
console.log("Watching for changes in", watchSourcePath);
|
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,
|
|
||||||
pathLib.dirname(configFileName)
|
|
||||||
);
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(watchSourcePath, {
|
const watcher = chokidar.watch(watchSourcePath, {
|
||||||
persistent: true,
|
persistent: true,
|
||||||
|
|
@ -327,7 +512,7 @@ export const watch = (
|
||||||
|
|
||||||
watcher.on("error", (error) => console.log(`Watcher error: ${error}`));
|
watcher.on("error", (error) => console.log(`Watcher error: ${error}`));
|
||||||
|
|
||||||
let isProcessing = false;
|
let processingQueue: string[] = [];
|
||||||
const fileChangeHandler = async (
|
const fileChangeHandler = async (
|
||||||
path: string,
|
path: string,
|
||||||
type: "add" | "remove" | "change"
|
type: "add" | "remove" | "change"
|
||||||
|
|
@ -336,7 +521,8 @@ export const watch = (
|
||||||
if (!path.endsWith(".ts")) {
|
if (!path.endsWith(".ts")) {
|
||||||
// 如果是json文件,复制或者删除
|
// 如果是json文件,复制或者删除
|
||||||
if (path.endsWith(".json")) {
|
if (path.endsWith(".json")) {
|
||||||
const targetPath = path.replace("src", "lib");
|
// const targetPath = path.replace("src", "lib"); // 这里直接替换不对,应该是拿到项目目录,把项目目录/src 换成项目目录/lib
|
||||||
|
const targetPath = path.replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib"));
|
||||||
if (type === "remove") {
|
if (type === "remove") {
|
||||||
fs.unlinkSync(targetPath);
|
fs.unlinkSync(targetPath);
|
||||||
console.warn(`File ${targetPath} has been removed.`);
|
console.warn(`File ${targetPath} has been removed.`);
|
||||||
|
|
@ -375,8 +561,8 @@ export const watch = (
|
||||||
const modulePath = pathLib.resolve(path);
|
const modulePath = pathLib.resolve(path);
|
||||||
// 将src替换为lib
|
// 将src替换为lib
|
||||||
const libPath = modulePath
|
const libPath = modulePath
|
||||||
.replace("\\src\\", "\\lib\\")
|
.replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib"))
|
||||||
.replace(".ts", ".js");
|
.replace(/\.ts$/, ".js");
|
||||||
let compileOnly = false;
|
let compileOnly = false;
|
||||||
if (!require.cache[libPath]) {
|
if (!require.cache[libPath]) {
|
||||||
// 如果是删除的话,直接尝试删除lib下的文件
|
// 如果是删除的话,直接尝试删除lib下的文件
|
||||||
|
|
@ -389,10 +575,11 @@ export const watch = (
|
||||||
console.warn(`File ${libPath} has been removed.`);
|
console.warn(`File ${libPath} has been removed.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.warn(
|
// console.warn(
|
||||||
`File ${libPath} is not in module cache, will compile only.`
|
// `File ${libPath} is not in module cache, will compile only.`
|
||||||
);
|
// );
|
||||||
compileOnly = true;
|
// compileOnly = true;
|
||||||
|
// 这里现在不需要仅编译了,因为在require的时候会自动编译ts文件
|
||||||
} else {
|
} else {
|
||||||
// 如果是删除,则需要发出警告,文件正在被进程使用
|
// 如果是删除,则需要发出警告,文件正在被进程使用
|
||||||
if (type === "remove") {
|
if (type === "remove") {
|
||||||
|
|
@ -400,34 +587,13 @@ export const watch = (
|
||||||
return;
|
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) {
|
const { program, sourceFile, diagnostics } = createProgramAndSourceFile(path, options, projectReferences);
|
||||||
console.error(`Error in ${path}`);
|
|
||||||
syntaxErrors.forEach((diagnostic) => {
|
if (diagnostics.length) {
|
||||||
console.error(
|
return;
|
||||||
`${ts.flattenDiagnosticMessageText(
|
|
||||||
diagnostic.messageText,
|
|
||||||
"\n"
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
console.error(`文件存在语法错误,请检查修复后重试!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只输出单个文件
|
// 只输出单个文件
|
||||||
realConfig.lifecycle.onBeforeCompile();
|
realConfig.lifecycle.onBeforeCompile();
|
||||||
const emitResult = program.emit(sourceFile);
|
const emitResult = program.emit(sourceFile);
|
||||||
|
|
@ -438,51 +604,54 @@ export const watch = (
|
||||||
realConfig.lifecycle.onAfterCompile();
|
realConfig.lifecycle.onAfterCompile();
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`Emit succeeded. ${
|
`Emit succeeded. ${compileOnly ? "" : "reload service......"
|
||||||
compileOnly ? "" : "reload service......"
|
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
realConfig.lifecycle.onAfterCompile();
|
realConfig.lifecycle.onAfterCompile();
|
||||||
if (compileOnly) {
|
if (compileOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await restart();
|
// await restart(); // 只有在队列里面的最后一个文件编译完成后才会重启服务
|
||||||
|
if (processingQueue.length === 1) {
|
||||||
|
await restart();
|
||||||
|
} else {
|
||||||
|
console.log("Waiting for other operations to complete...");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeDebounced = debounce(
|
const onChangeDebounced =
|
||||||
async (path: string, type: "add" | "remove" | "change") => {
|
async (path: string, type: "add" | "remove" | "change") => {
|
||||||
if (isProcessing) {
|
if (processingQueue.includes(path)) {
|
||||||
console.log("Processing, please wait...");
|
console.log("Processing, please wait...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
isProcessing = true;
|
processingQueue.push(path);
|
||||||
await fileChangeHandler(path, type);
|
await fileChangeHandler(path, type);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.clear();
|
console.clear();
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
isProcessing = false;
|
processingQueue = processingQueue.filter((p) => p !== path);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
100
|
|
||||||
);
|
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on("add", (path) => {
|
.on("add", async (path) => {
|
||||||
if (startWatching) {
|
if (startWatching) {
|
||||||
onChangeDebounced(path, "add");
|
await onChangeDebounced(path, "add");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("change", (path) => {
|
.on("change", async (path) => {
|
||||||
if (startWatching) {
|
if (startWatching) {
|
||||||
onChangeDebounced(path, "change");
|
await onChangeDebounced(path, "change");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("unlink", (path) => {
|
.on("unlink", async (path) => {
|
||||||
if (startWatching) {
|
if (startWatching) {
|
||||||
onChangeDebounced(path, "remove");
|
await onChangeDebounced(path, "remove");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue