feat: 完善watch编译器,现编译完成之后会自动进行alias替换,防止运行时问题,并修复server启动失败导致整体进程退出的问题

This commit is contained in:
Pan Qiancheng 2025-11-26 17:09:10 +08:00
parent 1440147f3e
commit ef0d846f0a
3 changed files with 211 additions and 124 deletions

View File

@ -7,8 +7,8 @@ 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 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 polyfill_1 = require("./polyfill"); const polyfill_1 = require("./polyfill");
const tsc_alias_1 = require("tsc-alias");
// 创建事件发射器 // 创建事件发射器
const createEventEmitter = () => { const createEventEmitter = () => {
const listeners = new Map(); const listeners = new Map();
@ -227,12 +227,19 @@ const createServerManager = (projectPath, eventEmitter, 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......");
// 这里注意要在require之前因为require会触发编译 try {
const { startup } = require('./start'); // 这里注意要在require之前因为require会触发编译
shutdown = await startup(pwd, simpleConnector).then((shutdown) => { const { startup } = require('./start');
config.lifecycle.onServerStart(config); shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
return shutdown; config.lifecycle.onServerStart(config);
}); return shutdown;
});
}
catch (error) {
console.error("----> Failed to start service:", error);
isRestarting = false;
return;
}
isRestarting = false; isRestarting = false;
await eventEmitter.emit("server-restarted", {}); await eventEmitter.emit("server-restarted", {});
}; };
@ -248,7 +255,7 @@ const createServerManager = (projectPath, eventEmitter, config) => {
}; };
}; };
// 创建编译器 // 创建编译器
const createCompiler = (projectPath, options, projectReferences, aliasConfig, config) => { const createCompiler = async (projectPath, options, projectReferences, treatFile, config) => {
const createProgramAndSourceFile = (path) => { const createProgramAndSourceFile = (path) => {
const program = typescript_1.default.createProgram({ const program = typescript_1.default.createProgram({
rootNames: [path], rootNames: [path],
@ -375,6 +382,8 @@ const createCompiler = (projectPath, options, projectReferences, aliasConfig, co
else { else {
console.log(`Emit succeeded for ${filePath}.`); console.log(`Emit succeeded for ${filePath}.`);
config.lifecycle.onAfterCompile(config); config.lifecycle.onAfterCompile(config);
const jsFilePath = libPath;
treatFile(jsFilePath);
return { return {
taskId: task.id, taskId: task.id,
success: true, success: true,
@ -445,40 +454,39 @@ const createFileWatcher = (projectPath, eventEmitter) => {
const generateTaskId = () => { const generateTaskId = () => {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}; };
/** // /**
* 根据 alias 配置表将路径中的别名替换为真实路径 // * 根据 alias 配置表将路径中的别名替换为真实路径
* @param path - 输入的路径字符串例如 "@project/file" "@oak-app-domain/some-module" // * @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
* @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组 // * @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组
* @returns 替换后的路径如果没有匹配到 alias则返回原始路径 // * @returns 替换后的路径,如果没有匹配到 alias则返回原始路径
*/ // */
const replaceAliasWithPath = (path, aliasConfig) => { // const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => {
for (const [alias, targets] of Object.entries(aliasConfig)) { // for (const [alias, targets] of Object.entries(aliasConfig)) {
// If alias ends with "*", handle it as a dynamic alias. // // If alias ends with "*", handle it as a dynamic alias.
if (alias.endsWith('*')) { // if (alias.endsWith('*')) {
// Create a regex pattern that matches paths starting with the alias, followed by any characters // // 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 aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)'
const match = path.match(aliasPattern); // const match = path.match(aliasPattern);
if (match) { // if (match) {
// Replace the alias with the target path, appending the matched part from the original path // // 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) // 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 // // Ensure that the target path ends with a slash if it's not already
const replacedPath = target.replace(/\/\*$/, "/") + match[1]; // const replacedPath = target.replace(/\/\*$/, "/") + match[1];
return replacedPath; // return replacedPath;
} // }
} // } else {
else { // // Handle static alias without "*" by directly matching the path
// Handle static alias without "*" by directly matching the path // if (path.startsWith(alias)) {
if (path.startsWith(alias)) { // const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array)
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
// Replace the alias part with the target path // return path.replace(alias, target);
return path.replace(alias, target); // }
} // }
} // }
} // // If no alias matches, return the original path
// If no alias matches, return the original path // return path;
return path; // };
}; const watch = async (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;
// 查找配置文件 // 查找配置文件
@ -486,20 +494,30 @@ const watch = (projectPath, config) => {
if (!configFileName) { if (!configFileName) {
throw new Error("Could not find a valid 'tsconfig.build.json'."); throw new Error("Could not find a valid 'tsconfig.build.json'.");
} }
// 读取配置文件 const runFile = await (0, tsc_alias_1.prepareSingleFileReplaceTscAliasPaths)({
const configFile = typescript_1.default.readConfigFile(configFileName, typescript_1.default.sys.readFile); configFile: path_1.default.join(projectPath, "tsconfig.build.json"),
// 解析配置文件内容 resolveFullPaths: true,
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"));
}); });
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配置 // 输出真实的alias配置
console.debug("[DEBUG] Running Alias config:", aliasConfig); // console.debug("[DEBUG] Running Alias config:", aliasConfig);
// 初始化polyfill // 初始化polyfill
const polyfillLoader = () => { const polyfillLoader = () => {
const BuiltinModule = require("module"); const BuiltinModule = require("module");
@ -530,6 +548,9 @@ const watch = (projectPath, config) => {
console.error(`[resolve] Emit skipped for: ${tsPath}`); console.error(`[resolve] Emit skipped for: ${tsPath}`);
throw new Error("TypeScript emit skipped"); throw new Error("TypeScript emit skipped");
} }
else {
treatFile(jsPath);
}
console.log(`[resolve] Successfully compiled: ${tsPath}`); console.log(`[resolve] Successfully compiled: ${tsPath}`);
return jsPath; return jsPath;
} }
@ -559,11 +580,11 @@ const watch = (projectPath, config) => {
rFoptions // 解析选项 rFoptions // 解析选项
) { ) {
let resolvedRequest = request; let resolvedRequest = request;
const replacedPath = replaceAliasWithPath(request, aliasConfig); // const replacedPath = replaceAliasWithPath(request, aliasConfig);
if (replacedPath !== request) { // if (replacedPath !== request) {
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`); // console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
resolvedRequest = path_1.default.join(projectPath, replacedPath); // resolvedRequest = pathLib.join(projectPath, replacedPath);
} // }
try { try {
return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions); return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
} }
@ -631,14 +652,34 @@ const watch = (projectPath, config) => {
"src/ports/index.ts", "src/ports/index.ts",
]; ];
compileFiles.forEach(tryCompile); 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((resolve, reject) => { return new Promise(async (resolve, reject) => {
// 创建事件系统 // 创建事件系统
const eventEmitter = createEventEmitter(); const eventEmitter = createEventEmitter();
// 创建各个组件 // 创建各个组件
const compileQueue = createCompileQueue(eventEmitter); const compileQueue = createCompileQueue(eventEmitter);
const compiler = createCompiler(projectPath, options, projectReferences, aliasConfig, realConfig); const compiler = await createCompiler(projectPath, options, projectReferences, treatFile, realConfig);
const serverManager = createServerManager(projectPath, eventEmitter, realConfig); const serverManager = createServerManager(projectPath, eventEmitter, realConfig);
const fileWatcher = createFileWatcher(projectPath, eventEmitter); const fileWatcher = createFileWatcher(projectPath, eventEmitter);
// 设置编译器处理器 // 设置编译器处理器

View File

@ -145,6 +145,7 @@
"stylelint-webpack-plugin": "^3.2.0", "stylelint-webpack-plugin": "^3.2.0",
"tailwindcss": "^3.0.2", "tailwindcss": "^3.0.2",
"terser-webpack-plugin": "^5.2.5", "terser-webpack-plugin": "^5.2.5",
"tsc-alias": "^1.8.16",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"ui-extract-webpack-plugin": "^1.0.0", "ui-extract-webpack-plugin": "^1.0.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",

View File

@ -3,9 +3,10 @@ import ts, { CompilerOptions, ProjectReference } from "typescript";
import pathLib from "path"; import pathLib from "path";
import dayjs from "dayjs"; import dayjs from "dayjs";
import fs from "fs"; import fs from "fs";
import { cloneDeep } from "lodash"; // import { cloneDeep } from "lodash";
import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore"; import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore";
import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill"; import { LogFormatter, LogFormatterProp, polyfillConsole } from "./polyfill";
import { prepareSingleFileReplaceTscAliasPaths, SingleFileReplacer } from 'tsc-alias';
/* /*
* *
@ -397,15 +398,22 @@ const createServerManager = (
console.warn("----> Starting service......"); console.warn("----> Starting service......");
// 这里注意要在require之前因为require会触发编译 try {
const { startup } = require('./start') as { // 这里注意要在require之前因为require会触发编译
startup: (pwd: string, connector: any) => Promise<() => Promise<void>>; const { startup } = require('./start') as {
} startup: (pwd: string, connector: any) => Promise<() => Promise<void>>;
}
shutdown = await startup(pwd, simpleConnector).then((shutdown) => { shutdown = await startup(pwd, simpleConnector).then((shutdown) => {
config.lifecycle.onServerStart(config); config.lifecycle.onServerStart(config);
return shutdown; return shutdown;
}); });
} catch (error) {
console.error("----> Failed to start service:", error);
isRestarting = false;
return;
}
isRestarting = false; isRestarting = false;
await eventEmitter.emit("server-restarted", {}); await eventEmitter.emit("server-restarted", {});
@ -425,13 +433,14 @@ const createServerManager = (
}; };
// 创建编译器 // 创建编译器
const createCompiler = ( const createCompiler = async (
projectPath: string, projectPath: string,
options: CompilerOptions, options: CompilerOptions,
projectReferences: readonly ProjectReference[] | undefined, projectReferences: readonly ProjectReference[] | undefined,
aliasConfig: Record<string, string | string[]>, treatFile: (filePath: string) => void,
config: RealWatchConfig config: RealWatchConfig
) => { ) => {
const createProgramAndSourceFile = (path: string) => { const createProgramAndSourceFile = (path: string) => {
const program = ts.createProgram({ const program = ts.createProgram({
rootNames: [path], rootNames: [path],
@ -578,6 +587,8 @@ const createCompiler = (
} else { } else {
console.log(`Emit succeeded for ${filePath}.`); console.log(`Emit succeeded for ${filePath}.`);
config.lifecycle.onAfterCompile(config); config.lifecycle.onAfterCompile(config);
const jsFilePath = libPath;
treatFile(jsFilePath);
return { return {
taskId: task.id, taskId: task.id,
success: true, success: true,
@ -666,42 +677,42 @@ const generateTaskId = (): string => {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}; };
/** // /**
* alias // * 根据 alias 配置表将路径中的别名替换为真实路径
* @param path - "@project/file" "@oak-app-domain/some-module" // * @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module"
* @param aliasConfig - alias key value // * @param aliasConfig - alias 配置表key 为别名value 为对应的真实路径或路径数组
* @returns alias // * @returns 替换后的路径,如果没有匹配到 alias则返回原始路径
*/ // */
const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => { // const replaceAliasWithPath = (path: string, aliasConfig: Record<string, string | string[]>): string => {
for (const [alias, targets] of Object.entries(aliasConfig)) { // for (const [alias, targets] of Object.entries(aliasConfig)) {
// If alias ends with "*", handle it as a dynamic alias. // // If alias ends with "*", handle it as a dynamic alias.
if (alias.endsWith('*')) { // if (alias.endsWith('*')) {
// Create a regex pattern that matches paths starting with the alias, followed by any characters // // 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 aliasPattern = new RegExp(`^${alias.replace(/\*$/, "")}(.*)`); // e.g., '@project/*' becomes '@project/(.*)'
const match = path.match(aliasPattern); // const match = path.match(aliasPattern);
if (match) { // if (match) {
// Replace the alias with the target path, appending the matched part from the original path // // 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) // 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 // // Ensure that the target path ends with a slash if it's not already
const replacedPath = target.replace(/\/\*$/, "/") + match[1]; // const replacedPath = target.replace(/\/\*$/, "/") + match[1];
return replacedPath; // return replacedPath;
} // }
} else { // } else {
// Handle static alias without "*" by directly matching the path // // Handle static alias without "*" by directly matching the path
if (path.startsWith(alias)) { // if (path.startsWith(alias)) {
const target = Array.isArray(targets) ? targets[0] : targets; // Take the first target (if it's an array) // 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 // // Replace the alias part with the target path
return path.replace(alias, target); // return path.replace(alias, target);
} // }
} // }
} // }
// If no alias matches, return the original path // // If no alias matches, return the original path
return path; // return path;
}; // };
export const watch = ( export const watch = async (
projectPath: string, projectPath: string,
config?: WatchConfig config?: WatchConfig
): Promise<() => Promise<void>> => { ): Promise<() => Promise<void>> => {
@ -719,29 +730,42 @@ export const watch = (
throw new Error("Could not find a valid 'tsconfig.build.json'."); throw new Error("Could not find a valid 'tsconfig.build.json'.");
} }
// 读取配置文件 const runFile: SingleFileReplacer = await prepareSingleFileReplaceTscAliasPaths({
configFile: pathLib.join(projectPath, "tsconfig.build.json"),
resolveFullPaths: true,
});
function treatFile(filePath: string) {
console.log(`Processing file for alias replacement: ${filePath}`);
const fileContents = fs.readFileSync(filePath, 'utf8');
const newContents = runFile({ fileContents, filePath });
// do stuff with newContents
fs.writeFileSync(filePath, newContents, 'utf8');
}
// // 读取配置文件
const configFile = ts.readConfigFile(configFileName, ts.sys.readFile); const configFile = ts.readConfigFile(configFileName, ts.sys.readFile);
// 解析配置文件内容 // // 解析配置文件内容
const { options, projectReferences } = ts.parseJsonConfigFileContent( const { options, projectReferences } = ts.parseJsonConfigFileContent(
configFile.config, configFile.config,
ts.sys, ts.sys,
pathLib.dirname(configFileName) pathLib.dirname(configFileName)
); );
const aliasConfig: AliasConfig = cloneDeep(options.paths) || {}; // const aliasConfig: AliasConfig = cloneDeep(options.paths) || {};
// 输出原始配置 // // 输出原始配置
// console.log("[DEBUG] Original alias config:", aliasConfig); // // console.log("[DEBUG] Original alias config:", aliasConfig);
Object.keys(aliasConfig).forEach((key) => { // Object.keys(aliasConfig).forEach((key) => {
const value = aliasConfig[key]; // const value = aliasConfig[key];
// 替换src // // 替换src
aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v: string) => v.replace("src", "lib")); // aliasConfig[key] = typeof value === "string" ? value.replace("src", "lib") : value.map((v: string) => v.replace("src", "lib"));
}); // });
// 输出真实的alias配置 // 输出真实的alias配置
console.debug("[DEBUG] Running Alias config:", aliasConfig); // console.debug("[DEBUG] Running Alias config:", aliasConfig);
// 初始化polyfill // 初始化polyfill
const polyfillLoader = () => { const polyfillLoader = () => {
@ -781,6 +805,8 @@ export const watch = (
if (emitResult.emitSkipped) { if (emitResult.emitSkipped) {
console.error(`[resolve] Emit skipped for: ${tsPath}`); console.error(`[resolve] Emit skipped for: ${tsPath}`);
throw new Error("TypeScript emit skipped"); throw new Error("TypeScript emit skipped");
} else {
treatFile(jsPath)
} }
console.log(`[resolve] Successfully compiled: ${tsPath}`); console.log(`[resolve] Successfully compiled: ${tsPath}`);
@ -823,13 +849,12 @@ export const watch = (
rFoptions: object | undefined // 解析选项 rFoptions: object | undefined // 解析选项
) { ) {
let resolvedRequest = request; let resolvedRequest = request;
const replacedPath = replaceAliasWithPath(request, aliasConfig); // const replacedPath = replaceAliasWithPath(request, aliasConfig);
if (replacedPath !== request) {
console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
resolvedRequest = pathLib.join(projectPath, replacedPath);
}
// if (replacedPath !== request) {
// console.log(`[resolve] Alias resolved: ${request} -> ${replacedPath}`);
// resolvedRequest = pathLib.join(projectPath, replacedPath);
// }
try { try {
return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions); return oldResolveFilename.call(this, resolvedRequest, parent, isMain, rFoptions);
} catch (error: any) { } catch (error: any) {
@ -908,16 +933,36 @@ export const watch = (
"src/ports/index.ts", "src/ports/index.ts",
]; ];
compileFiles.forEach(tryCompile); compileFiles.forEach(tryCompile);
// 最后替换lib目录下所有的路径别名
console.log(`[watch] Replacing path aliases in lib directory......`);
const libDir = pathLib.join(projectPath, "lib");
const walkDir = (dir: string) => {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const fullPath = pathLib.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walkDir(fullPath);
}
else if (stat.isFile() && fullPath.endsWith(".js")) {
const fileContents = fs.readFileSync(fullPath, 'utf8');
const newContents = runFile({ fileContents, filePath: fullPath });
fs.writeFileSync(fullPath, newContents, 'utf8');
}
});
};
walkDir(libDir);
console.log(`[watch] Path alias replacement completed.`);
} }
}; };
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
// 创建事件系统 // 创建事件系统
const eventEmitter = createEventEmitter(); const eventEmitter = createEventEmitter();
// 创建各个组件 // 创建各个组件
const compileQueue = createCompileQueue(eventEmitter); const compileQueue = createCompileQueue(eventEmitter);
const compiler = createCompiler(projectPath, options, projectReferences, aliasConfig, realConfig); const compiler = await createCompiler(projectPath, options, projectReferences, treatFile, realConfig);
const serverManager = createServerManager(projectPath, eventEmitter, realConfig); const serverManager = createServerManager(projectPath, eventEmitter, realConfig);
const fileWatcher = createFileWatcher(projectPath, eventEmitter); const fileWatcher = createFileWatcher(projectPath, eventEmitter);