diff --git a/lib/server/analysis.d.ts b/lib/server/analysis.d.ts new file mode 100644 index 0000000..5e6a7de --- /dev/null +++ b/lib/server/analysis.d.ts @@ -0,0 +1,11 @@ +type AnalysisOptions = { + outputDir: string; + includeNodeModules: boolean; +}; +/** + * 分析项目中的模块 + * @param dir 项目目录 + * @param output 输出目录 + */ +export declare const analysis: (dir: string, config: AnalysisOptions) => void; +export {}; diff --git a/lib/server/analysis.js b/lib/server/analysis.js new file mode 100644 index 0000000..59568d7 --- /dev/null +++ b/lib/server/analysis.js @@ -0,0 +1,76 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.analysis = void 0; +const tslib_1 = require("tslib"); +const fs_1 = tslib_1.__importDefault(require("fs")); +const path_1 = tslib_1.__importDefault(require("path")); +/** + * 分析项目中的模块 + * @param dir 项目目录 + * @param output 输出目录 + */ +const analysis = (dir, config) => { + const BuiltinModule = require("module"); + // 兼容一些模拟环境下的 module 构造函数 + const Module = module.constructor.length > 1 ? module.constructor : BuiltinModule; + // 保存原始的 _resolveFilename 方法 + const oldResolveFilename = Module._resolveFilename; + const successImported = new Set(); + // 重写 _resolveFilename 方法 + Module._resolveFilename = function (request, // 模块请求路径 + parent, // 调用方模块 + isMain, // 是否是主模块 + rFoptions // 解析选项 + ) { + // 调用原始的 _resolveFilename 方法 + const filename = oldResolveFilename.call(this, request, parent, isMain, rFoptions); + // 记录成功导入的模块 + successImported.add(filename); + // 返回解析后的模块路径 + return filename; + }; + const filterProjectModules = (modulePath) => { + if (config.includeNodeModules) { + return modulePath.includes(dir); + } + return modulePath.includes(dir) && !modulePath.includes('node_modules'); + }; + /** + * 将所有的文件复制到指定目录,并保持目录结构 + * @param files 文件列表 string[] + * @param dest 目标目录 + * @param root 原目录的根目录 + */ + const copyFiles = (files, dest, root) => { + files.forEach(file => { + const relativePath = path_1.default.relative(root, file); + const destPath = path_1.default.join(dest, relativePath); + fs_1.default.mkdirSync(path_1.default.dirname(destPath), { recursive: true }); + fs_1.default.copyFileSync(file, destPath); + }); + }; + const { watch } = require("./watch"); + watch(dir).then(s => { + setTimeout(() => { + console.log('shutting down...'); + s().then(() => { + console.log('server stoped'); + // 把导入成功的输出 + const project = Array.from(successImported).filter(filterProjectModules); + // 添加本地的scripts下的所有文件和package.json + const scriptsDir = path_1.default.join(dir, 'scripts'); + const pkgPath = path_1.default.join(dir, 'package.json'); + const files = fs_1.default.readdirSync(scriptsDir); + const scriptFiles = files.map(file => path_1.default.join(scriptsDir, file)); + project.push(pkgPath, ...scriptFiles); + // 复制文件 + const dest = path_1.default.join(dir, config.outputDir); + fs_1.default.mkdirSync(dest, { recursive: true }); + copyFiles(project, dest, dir); + console.warn('分析结果以项目启动所需依赖为准,如果涉及动态导入,请设计routine,在项目启动时进行加载!'); + process.exit(0); + }); + }, 1000); + }); +}; +exports.analysis = analysis; diff --git a/lib/server/watch.js b/lib/server/watch.js index 2c493e9..f8d9d0a 100644 --- a/lib/server/watch.js +++ b/lib/server/watch.js @@ -5,7 +5,6 @@ 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 start_1 = require("./start"); const dayjs_1 = tslib_1.__importDefault(require("dayjs")); const fs_1 = tslib_1.__importDefault(require("fs")); const lodash_1 = require("lodash"); @@ -95,9 +94,160 @@ const getOverrideConfig = (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; + // 通用的路径检查与编译函数 + 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, sourceFile, diagnostics } = createProgramAndSourceFile(tsPath, options, projectReferences); + 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"); + } + 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 = path_1.default.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; + } + }; + }; + polyfillLoader(); //polyfill console.log 添加时间 const polyfillConsole = (trace) => { // 获取调用堆栈信息 @@ -141,6 +291,51 @@ const watch = (projectPath, config) => { }); }; realConfig.polyfill.console.enable && polyfillConsole(enableTrace); + // 这里注意要在require之前,因为require会触发编译 + const { startup } = require('./start'); + // 如果lib目录是空的,则直接编译所有的ts文件 + 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, diagnostics } = createProgramAndSourceFile(tsFile, options, projectReferences); + 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(sourceFile); + // 编译所有的文件 + 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); + } return new Promise((resolve, reject) => { realConfig.lifecycle.onInit(); let shutdown; @@ -170,22 +365,13 @@ const watch = (projectPath, config) => { // 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 (0, start_1.startup)(pwd, simpleConnector).then((shutdown) => { + 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 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, { persistent: true, ignored: (file) => file.endsWith(".tsx") || @@ -209,13 +395,14 @@ const watch = (projectPath, config) => { startWatching = true; }); watcher.on("error", (error) => console.log(`Watcher error: ${error}`)); - let isProcessing = false; + let processingQueue = []; const fileChangeHandler = async (path, type) => { // 判断一下是不是以下扩展名:ts if (!path.endsWith(".ts")) { // 如果是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") { fs_1.default.unlinkSync(targetPath); console.warn(`File ${targetPath} has been removed.`); @@ -245,8 +432,8 @@ const watch = (projectPath, config) => { const modulePath = path_1.default.resolve(path); // 将src替换为lib const libPath = modulePath - .replace("\\src\\", "\\lib\\") - .replace(".ts", ".js"); + .replace(path_1.default.join(projectPath, "src"), path_1.default.join(projectPath, "lib")) + .replace(/\.ts$/, ".js"); let compileOnly = false; if (!require.cache[libPath]) { // 如果是删除的话,直接尝试删除lib下的文件 @@ -260,8 +447,11 @@ const watch = (projectPath, config) => { console.warn(`File ${libPath} has been removed.`); return; } - console.warn(`File ${libPath} is not in module cache, will compile only.`); - compileOnly = true; + // console.warn( + // `File ${libPath} is not in module cache, will compile only.` + // ); + // compileOnly = true; + // 这里现在不需要仅编译了,因为在require的时候会自动编译ts文件 } else { // 如果是删除,则需要发出警告,文件正在被进程使用 @@ -270,24 +460,9 @@ const watch = (projectPath, config) => { return; } } - const program = typescript_1.default.createProgram({ - rootNames: [path], - options, - projectReferences, - }); - const sourceFile = program.getSourceFile(path); - // 是否有语法错误 - const diagnostics = typescript_1.default.getPreEmitDiagnostics(program, sourceFile); + const { program, sourceFile, diagnostics } = createProgramAndSourceFile(path, options, projectReferences); 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; - } + return; } // 只输出单个文件 realConfig.lifecycle.onBeforeCompile(); @@ -304,40 +479,47 @@ const watch = (projectPath, config) => { if (compileOnly) { 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) => { - if (isProcessing) { + const onChangeDebounced = async (path, type) => { + if (processingQueue.includes(path)) { console.log("Processing, please wait..."); return; } try { - isProcessing = true; + processingQueue.push(path); await fileChangeHandler(path, type); } catch (e) { console.clear(); console.error(e); + process.exit(1); } finally { - isProcessing = false; + processingQueue = processingQueue.filter((p) => p !== path); } - }, 100); + }; watcher - .on("add", (path) => { + .on("add", async (path) => { if (startWatching) { - onChangeDebounced(path, "add"); + await onChangeDebounced(path, "add"); } }) - .on("change", (path) => { + .on("change", async (path) => { if (startWatching) { - onChangeDebounced(path, "change"); + await onChangeDebounced(path, "change"); } }) - .on("unlink", (path) => { + .on("unlink", async (path) => { if (startWatching) { - onChangeDebounced(path, "remove"); + await onChangeDebounced(path, "remove"); } }); const dispose = async () => { diff --git a/lib/template.js b/lib/template.js index 8480c34..aaa0f43 100644 --- a/lib/template.js +++ b/lib/template.js @@ -103,6 +103,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i "run:android": "oak-cli run -p android", "server:init": "${serverInitScript}", "server:start": "${serverStartWatchScript}", + "server:ana": "cross-env ENABLE_TRACE=true cross-env NODE_ENV=development cross-env OAK_PLATFORM=server node --stack-size=65500 scripts/analysis.js", "postinstall": "npm run make:dep" }, "keywords": [], @@ -272,8 +273,7 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i "webpack-dev-server": "^4.15.1", "webpack-manifest-plugin": "^4.0.2", "workbox-webpack-plugin": "^6.4.1", - "chokidar": "^4.0.1", - "module-alias": "^2.2.3" + "chokidar": "^4.0.1" }, "browserslist": { "production": [ @@ -305,16 +305,9 @@ function packageJsonContent({ name, version, description, cliName, cliBinName, i }, "resolutions": { "readable-stream": "3.6.2" - }, - "_moduleAliases": { - "@project": "./lib", - "@oak-general-business": "./node_modules/oak-general-business/lib", - "@oak-frontend-base": "./node_modules/oak-frontend-base/lib", - "@oak-app-domain": "./lib/oak-app-domain" } } `; - // _moduleAliases用于lib内运行时的模块声明,重载之后require的路径还会保留@project,需要这样的方式来进行路径alias } function tsConfigJsonContent() { return `{ diff --git a/src/server/analysis.ts b/src/server/analysis.ts new file mode 100644 index 0000000..aefc35d --- /dev/null +++ b/src/server/analysis.ts @@ -0,0 +1,86 @@ +import fs from 'fs' +import path from 'path' + +type AnalysisOptions = { + outputDir: string + includeNodeModules: boolean +} + +/** + * 分析项目中的模块 + * @param dir 项目目录 + * @param output 输出目录 + */ +export const analysis = (dir: string, config: AnalysisOptions) => { + const BuiltinModule = require("module"); + // 兼容一些模拟环境下的 module 构造函数 + const Module = module.constructor.length > 1 ? module.constructor : BuiltinModule; + // 保存原始的 _resolveFilename 方法 + const oldResolveFilename = Module._resolveFilename; + const successImported = new Set(); + // 重写 _resolveFilename 方法 + Module._resolveFilename = function ( + request: string, // 模块请求路径 + parent: typeof Module, // 调用方模块 + isMain: boolean, // 是否是主模块 + rFoptions: object | undefined // 解析选项 + ) { + // 调用原始的 _resolveFilename 方法 + const filename = oldResolveFilename.call(this, request, parent, isMain, rFoptions); + + // 记录成功导入的模块 + successImported.add(filename); + + // 返回解析后的模块路径 + return filename; + } + + const filterProjectModules = (modulePath: string) => { + if (config.includeNodeModules) { + return modulePath.includes(dir) + } + return modulePath.includes(dir) && !modulePath.includes('node_modules'); + } + + /** + * 将所有的文件复制到指定目录,并保持目录结构 + * @param files 文件列表 string[] + * @param dest 目标目录 + * @param root 原目录的根目录 + */ + const copyFiles = (files: string[], dest: string, root: string) => { + files.forEach(file => { + const relativePath = path.relative(root, file) + const destPath = path.join(dest, relativePath) + fs.mkdirSync(path.dirname(destPath), { recursive: true }) + fs.copyFileSync(file, destPath) + }) + } + + const { watch } = require("./watch") as { watch: (pwd: string) => Promise<() => Promise> } + + watch(dir).then(s => { + setTimeout(() => { + console.log('shutting down...') + s().then(() => { + console.log('server stoped') + // 把导入成功的输出 + const project = Array.from(successImported).filter(filterProjectModules) + + // 添加本地的scripts下的所有文件和package.json + const scriptsDir = path.join(dir, 'scripts') + const pkgPath = path.join(dir, 'package.json') + const files = fs.readdirSync(scriptsDir) + const scriptFiles = files.map(file => path.join(scriptsDir, file)) + project.push(pkgPath, ...scriptFiles) + + // 复制文件 + const dest = path.join(dir, config.outputDir) + fs.mkdirSync(dest, { recursive: true }); + copyFiles(project, dest, dir) + console.warn('分析结果以项目启动所需依赖为准,如果涉及动态导入,请设计routine,在项目启动时进行加载!'); + process.exit(0) + }) + }, 1000) + }) +} diff --git a/src/server/watch.ts b/src/server/watch.ts index 1e3357b..bec0a12 100644 --- a/src/server/watch.ts +++ b/src/server/watch.ts @@ -1,10 +1,9 @@ import chokidar from "chokidar"; -import ts from "typescript"; +import ts, { CompilerOptions, ProjectReference } from "typescript"; import pathLib from "path"; -import { startup } from "./start"; import dayjs from "dayjs"; import fs from "fs"; -import { debounce } from "lodash"; +import { cloneDeep } from "lodash"; declare const require: NodeRequire; declare const process: NodeJS.Process; @@ -80,14 +79,14 @@ export type WatchConfig = { // 真实config不会出现?,所以这里新增一个type表示真实的config type DeepRequiredIfPresent = { [P in keyof T]-?: NonNullable extends (...args: any) => any // Check for function type - ? T[P] // Preserve the original function type - : NonNullable extends (infer U)[] - ? DeepRequiredIfPresent[] - : NonNullable extends object - ? DeepRequiredIfPresent> - : T[P] extends undefined | null - ? T[P] - : NonNullable; + ? T[P] // Preserve the original function type + : NonNullable extends (infer U)[] + ? DeepRequiredIfPresent[] + : NonNullable extends object + ? DeepRequiredIfPresent> + : T[P] extends undefined | null + ? T[P] + : NonNullable; }; export type RealWatchConfig = DeepRequiredIfPresent; @@ -116,8 +115,8 @@ const defaultConfig: RealWatchConfig = { level === "info" ? infoStart : level === "warn" - ? warnStart - : errorStart; + ? warnStart + : errorStart; return [ levelStart, getTime(), @@ -178,6 +177,44 @@ const getOverrideConfig = (config?: WatchConfig): RealWatchConfig => { : defaultConfig; }; +type AliasConfig = Record; +type ModuleType = import('module') + +/** + * 根据 alias 配置表将路径中的别名替换为真实路径 + * @param path - 输入的路径字符串,例如 "@project/file" 或 "@oak-app-domain/some-module" + * @param aliasConfig - alias 配置表,key 为别名,value 为对应的真实路径或路径数组 + * @returns 替换后的路径,如果没有匹配到 alias,则返回原始路径 + */ +function replaceAliasWithPath(path: string, aliasConfig: Record): 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 = ( projectPath: string, config?: WatchConfig @@ -185,6 +222,182 @@ export const watch = ( const realConfig = getOverrideConfig(config); 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; + + // 通用的路径检查与编译函数 + function resolveAndCompile(requestPath: string, parent: ModuleType, options: CompilerOptions, projectReferences: readonly ts.ProjectReference[] | undefined) { + const jsPath = pathLib.resolve(requestPath + ".js"); + const tsPath = jsPath.replace(/\.js$/, ".ts").replace( + pathLib.join(projectPath, "lib"), + pathLib.join(projectPath, "src") + ); + + // 检查并编译 .ts 文件 + if (fs.existsSync(tsPath)) { + console.log(`[resolve] Found TypeScript source file: ${tsPath}, attempting to compile...`); + const { program, sourceFile, diagnostics } = createProgramAndSourceFile( + tsPath, + options, + projectReferences + ); + + 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"); + } + + console.log(`[resolve] Successfully compiled: ${tsPath}`); + return jsPath; + } + + // 如果没有找到对应的 .ts 文件 + if (fs.existsSync(jsPath)) { + return jsPath; + } + + throw new Error(`[resolve] Unable to find module: ${requestPath}`); + } + + // 处理文件夹导入的情况 + function resolveDirectory(requestPath: string, parent: ModuleType, options: CompilerOptions, projectReferences: readonly ts.ProjectReference[] | undefined) { + const indexJs = pathLib.join(requestPath, "index.js"); + const indexTs = pathLib.join(requestPath, "index.ts").replace( + pathLib.join(projectPath, "lib"), + pathLib.join(projectPath, "src") + ); + + if (fs.existsSync(indexTs)) { + console.log(`[resolve] Found TypeScript index file: ${indexTs}, attempting to compile...`); + return resolveAndCompile(indexTs, parent, options, projectReferences); + } + + if (fs.existsSync(indexJs)) { + return indexJs; + } + + throw new Error(`[resolve] No index file found in directory: ${requestPath}`); + } + + // 重写 _resolveFilename 方法 + Module._resolveFilename = function ( + request: string, // 模块请求路径 + parent: ModuleType, // 调用方模块 + isMain: boolean, // 是否是主模块 + rFoptions: object | undefined // 解析选项 + ) { + 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: any) { + if (error.code === "MODULE_NOT_FOUND") { + const requestPath = pathLib.resolve(pathLib.dirname(parent.filename), resolvedRequest); + + // 处理文件夹导入 + if (fs.existsSync(requestPath) && fs.statSync(requestPath).isDirectory()) { + return resolveDirectory(requestPath, parent, options, projectReferences); + } + + // 处理单文件导入 + return resolveAndCompile(requestPath, parent, options, projectReferences); + } + + throw error; + } + }; + }; + + polyfillLoader(); + //polyfill console.log 添加时间 const polyfillConsole = (trace: boolean) => { // 获取调用堆栈信息 @@ -232,6 +445,62 @@ export const watch = ( realConfig.polyfill.console.enable && polyfillConsole(enableTrace); + // 这里注意要在require之前,因为require会触发编译 + const { startup } = require('./start') as { + startup: (pwd: string, connector: any) => Promise<() => Promise>; + } + + // 如果lib目录是空的,则直接编译所有的ts文件 + const serverConfigFile = pathLib.join(projectPath, "lib/configuration/server.js"); + if (!fs.existsSync(serverConfigFile)) { + // 尝试编译src/configuration/server.ts + console.log(`[watch] Server configuration file not found, attempting to compile the project......`); + const tryCompile = (tsFile: string) => { + console.log(`[watch] Compiling: ${tsFile}`); + if (fs.existsSync(tsFile)) { + const { program, diagnostics } = createProgramAndSourceFile(tsFile, options, projectReferences); + if (diagnostics.length) { + console.error(`Error in ${tsFile}`); + diagnostics.forEach((diagnostic) => { + console.error( + `${ts.flattenDiagnosticMessageText( + diagnostic.messageText, + "\n" + )}` + ); + }); + console.error(`文件存在语法错误,请检查修复后重试!`); + process.exit(1); + } + // const emitResult = program.emit(sourceFile); + // 编译所有的文件 + 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); + } + return new Promise((resolve, reject) => { realConfig.lifecycle.onInit(); let shutdown: (() => Promise) | undefined; @@ -278,26 +547,7 @@ export const watch = ( 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, { persistent: true, @@ -327,7 +577,7 @@ export const watch = ( watcher.on("error", (error) => console.log(`Watcher error: ${error}`)); - let isProcessing = false; + let processingQueue: string[] = []; const fileChangeHandler = async ( path: string, type: "add" | "remove" | "change" @@ -336,7 +586,8 @@ export const watch = ( if (!path.endsWith(".ts")) { // 如果是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") { fs.unlinkSync(targetPath); console.warn(`File ${targetPath} has been removed.`); @@ -375,8 +626,8 @@ export const watch = ( const modulePath = pathLib.resolve(path); // 将src替换为lib const libPath = modulePath - .replace("\\src\\", "\\lib\\") - .replace(".ts", ".js"); + .replace(pathLib.join(projectPath, "src"), pathLib.join(projectPath, "lib")) + .replace(/\.ts$/, ".js"); let compileOnly = false; if (!require.cache[libPath]) { // 如果是删除的话,直接尝试删除lib下的文件 @@ -389,10 +640,11 @@ export const watch = ( console.warn(`File ${libPath} has been removed.`); return; } - console.warn( - `File ${libPath} is not in module cache, will compile only.` - ); - compileOnly = true; + // console.warn( + // `File ${libPath} is not in module cache, will compile only.` + // ); + // compileOnly = true; + // 这里现在不需要仅编译了,因为在require的时候会自动编译ts文件 } else { // 如果是删除,则需要发出警告,文件正在被进程使用 if (type === "remove") { @@ -400,34 +652,13 @@ export const watch = ( return; } } - const program = ts.createProgram({ - rootNames: [path], - options, - projectReferences, - }); - const sourceFile = program.getSourceFile(path); - // 是否有语法错误 - const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); - if (diagnostics.length) { - const syntaxErrors = diagnostics.filter( - (diagnostic) => - diagnostic.category === ts.DiagnosticCategory.Error - ); - if (syntaxErrors.length) { - console.error(`Error in ${path}`); - syntaxErrors.forEach((diagnostic) => { - console.error( - `${ts.flattenDiagnosticMessageText( - diagnostic.messageText, - "\n" - )}` - ); - }); - console.error(`文件存在语法错误,请检查修复后重试!`); - return; - } + const { program, sourceFile, diagnostics } = createProgramAndSourceFile(path, options, projectReferences); + + if (diagnostics.length) { + return; } + // 只输出单个文件 realConfig.lifecycle.onBeforeCompile(); const emitResult = program.emit(sourceFile); @@ -438,51 +669,54 @@ export const watch = ( realConfig.lifecycle.onAfterCompile(); } else { console.log( - `Emit succeeded. ${ - compileOnly ? "" : "reload service......" + `Emit succeeded. ${compileOnly ? "" : "reload service......" }` ); realConfig.lifecycle.onAfterCompile(); if (compileOnly) { 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") => { - if (isProcessing) { + if (processingQueue.includes(path)) { console.log("Processing, please wait..."); return; } try { - isProcessing = true; + processingQueue.push(path); await fileChangeHandler(path, type); } catch (e) { console.clear(); console.error(e); + process.exit(1); } finally { - isProcessing = false; + processingQueue = processingQueue.filter((p) => p !== path); } - }, - 100 - ); + } watcher - .on("add", (path) => { + .on("add", async (path) => { if (startWatching) { - onChangeDebounced(path, "add"); + await onChangeDebounced(path, "add"); } }) - .on("change", (path) => { + .on("change", async (path) => { if (startWatching) { - onChangeDebounced(path, "change"); + await onChangeDebounced(path, "change"); } }) - .on("unlink", (path) => { + .on("unlink", async (path) => { if (startWatching) { - onChangeDebounced(path, "remove"); + await onChangeDebounced(path, "remove"); } }); diff --git a/src/template.ts b/src/template.ts index e875ee3..2fb6671 100644 --- a/src/template.ts +++ b/src/template.ts @@ -110,6 +110,7 @@ export function packageJsonContent({ "run:android": "oak-cli run -p android", "server:init": "${serverInitScript}", "server:start": "${serverStartWatchScript}", + "server:ana": "cross-env ENABLE_TRACE=true cross-env NODE_ENV=development cross-env OAK_PLATFORM=server node --stack-size=65500 scripts/analysis.js", "postinstall": "npm run make:dep" }, "keywords": [], @@ -279,8 +280,7 @@ export function packageJsonContent({ "webpack-dev-server": "^4.15.1", "webpack-manifest-plugin": "^4.0.2", "workbox-webpack-plugin": "^6.4.1", - "chokidar": "^4.0.1", - "module-alias": "^2.2.3" + "chokidar": "^4.0.1" }, "browserslist": { "production": [ @@ -312,16 +312,9 @@ export function packageJsonContent({ }, "resolutions": { "readable-stream": "3.6.2" - }, - "_moduleAliases": { - "@project": "./lib", - "@oak-general-business": "./node_modules/oak-general-business/lib", - "@oak-frontend-base": "./node_modules/oak-frontend-base/lib", - "@oak-app-domain": "./lib/oak-app-domain" } } `; - // _moduleAliases用于lib内运行时的模块声明,重载之后require的路径还会保留@project,需要这样的方式来进行路径alias } export function tsConfigJsonContent() { diff --git a/template/scripts/analysis.js b/template/scripts/analysis.js new file mode 100644 index 0000000..174cdb8 --- /dev/null +++ b/template/scripts/analysis.js @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const { analysis } = require('@xuchangzju/oak-cli/lib/server/analysis'); +const pwd = process.cwd(); + +analysis(pwd, { + outputDir: 'backend_gen', + includeNodeModules: true, +}); diff --git a/template/scripts/startServer.js b/template/scripts/startServer.js index c0b22e7..d72b81c 100644 --- a/template/scripts/startServer.js +++ b/template/scripts/startServer.js @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-require-imports */ -require('module-alias/register'); const { startup } = require('@xuchangzju/oak-cli/lib/server/start'); const simpleConnector = require('../lib/config/connector').default; const pwd = process.cwd(); diff --git a/template/scripts/watchServer.js b/template/scripts/watchServer.js index f99a024..da792fb 100644 --- a/template/scripts/watchServer.js +++ b/template/scripts/watchServer.js @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-require-imports */ -require('module-alias/register'); const { watch } = require('@xuchangzju/oak-cli/lib/server/watch'); const pwd = process.cwd();