/** * DevCompiler — esbuild 预编译器(Phase 2A 核心组件) * * 所有用户源码(TS、ESM、CJS)通过 esbuild 统一编译为 CJS `.js` 文件, * 解决了 ESM `import` 无法清除缓存的根本问题。 * * 核心职责: * * | 职责 | 方法 | 说明 | * |-------------------|------------------------------|------------------------------------------| * | 首次全量编译 | `start()` | 扫描 `src/` 下所有源文件,编译到 `.vext/dev/` | * | Tier 1 单文件编译 | `compileSingle()` / `compileFiles()` | 代码修改时 O(1) 编译 | * | Tier 2 全量重编译 | `rebuildWithNewEntryPoints()`| 文件增删时重建 context | * | 路径映射 | `resolveCompiled()` / `resolveSource()` | 源文件 ↔ 编译产物 | * * 编译产物结构: * src/routes/user.ts → .vext/dev/routes/user.js * src/services/auth.ts → .vext/dev/services/auth.js * src/config/default.ts → .vext/dev/config/default.js * * 分级编译策略: * - Tier 1(~95% 场景):文件内容修改 → `compileSingle()` ~1-5ms/文件(O(changed)) * - Tier 2(~5% 场景):文件新增/删除 → `rebuildWithNewEntryPoints()` ~50-600ms(O(all)) * * tsconfig 处理(v2.2): * - `esbuild.context()` 的 `tsconfig` 参数接受文件路径,能自动解析 `extends` 链 * - `esbuild.transform()` 的 `tsconfigRaw` 只接受 JSON 字符串,不解析 `extends` * - 为保证 Tier 1 和 Tier 2 行为一致,`start()` 时预解析 tsconfig(展平 extends 链), * 缓存结果供 `compileSingle()` 使用 * * @module lib/dev/compiler * @see 11a-dev-compiler.md(完整设计文档) * @see 09a-build.md §2.1(与 BuildCompiler 共享配置) * @see IMPLEMENTATION-PLAN.md 任务 2.1 */ /** * DevCompiler 构造选项 */ export interface DevCompilerOptions { /** 源码目录(绝对路径),通常是 `/src` */ srcDir: string; /** 编译输出目录(绝对路径),通常是 `/.vext/dev` */ outDir: string; /** tsconfig.json 路径(可选,绝对路径)。未提供时 esbuild 使用默认设置 */ tsconfig?: string; } /** * DevCompiler 编译统计信息 * * 由 `start()` 和 `rebuildWithNewEntryPoints()` 返回, * 用于 CLI 打印编译耗时和文件数等信息。 */ export interface CompileStats { /** 编译的入口文件数量 */ fileCount: number; /** 编译耗时(毫秒) */ elapsed: number; /** 是否复用了已有 .vext/dev 输出目录 */ cacheHit?: boolean; } export declare class DevCompiler { /** * esbuild 增量编译上下文 * * 通过 `esbuild.context()` 创建,支持 `rebuild()` 增量编译。 * 当文件新增/删除时需要重建(`rebuildWithNewEntryPoints()`), * 因为 entryPoints 发生了变化。 */ private ctx; private readonly srcDir; private readonly outDir; private readonly tsconfig; /** * v2.2:预解析并展平的 tsconfig 内容(字符串形式)。 * * esbuild.context() 的 `tsconfig` 参数接受文件路径,能自动解析 `extends` 链。 * 但 esbuild.transform() 的 `tsconfigRaw` 只接受 JSON 字符串,不解析 `extends`。 * * 为保证 Tier 1 (transform) 和 Tier 2 (context.rebuild) 行为一致, * 在 start() 时预解析 tsconfig(展平 extends 链),缓存结果供 compileSingle() 使用。 */ private resolvedTsconfigRaw; /** * 经过验证的 tsconfig 路径(文件确实存在时才有值)。 * * 用户传入的 tsconfig 路径可能不存在(如项目初始化阶段), * esbuild.context() 会直接报错 "Cannot find tsconfig file"。 * 因此在 resolveTsconfig() 中验证文件存在性,不存在时置为 undefined, * 后续 esbuild.context() 和 esbuild.transform() 均使用默认设置。 */ private validatedTsconfig; constructor(options: DevCompilerOptions); /** * start — 初始化 esbuild context 并执行首次全量编译 * * 只应调用一次(在 dev bootstrap 启动时)。 * * 流程: * 1. 清空输出目录(.vext/dev/)确保干净状态 * 2. 预解析 tsconfig(展平 extends 链) * 3. 扫描 src/ 下所有源文件作为 entryPoints * 4. 创建 esbuild.context(增量编译上下文) * 5. 执行首次 rebuild(全量编译) * * @returns 编译统计信息 * @throws esbuild 编译错误(语法错误等)会透传 */ start(): Promise; /** * rebuild — 使用现有 context 执行全量增量重编译 * * 当新增/删除文件时需要使用 `rebuildWithNewEntryPoints()` 而非此方法, * 因为 entry points 可能变化。 * * 此方法用于 entry points 不变但需要重编译的场景(通常较少使用)。 * * 典型耗时: * - 50 文件项目: ~10-30ms * - 500 文件项目: ~50-200ms * - 2000 文件项目: ~200-500ms * * @throws DevCompiler 未启动时抛出错误 */ rebuild(): Promise; /** * rebuildWithNewEntryPoints — 文件增删时重建 esbuild context * * 当检测到文件新增或删除时调用(因为 entryPoints 发生了变化)。 * * 流程: * 1. 释放旧的 esbuild context * 2. 重新扫描 src/ 下的源文件 * 3. 创建新的 esbuild context(新 entryPoints) * 4. 执行全量编译 * * 典型耗时: * - 50 文件项目: ~20-50ms * - 500 文件项目: ~80-250ms * - 2000 文件项目: ~250-600ms * * 仅在文件增删时触发(约 5% 场景),95% 的代码变更走 compileSingle() 路径。 * * @returns 编译统计信息 */ rebuildWithNewEntryPoints(): Promise; /** * compileSingle — 单文件编译(Tier 1:代码变更时使用) * * 使用 esbuild.transform() 只编译单个变更文件,不涉及其他文件。 * 由于 Vext 不使用 bundle 模式(每个文件独立编译),transform() 的 * 输出与 context.rebuild() 对同一文件的输出完全等价。 * * 典型耗时:~1-5ms/文件,与项目总文件数无关(O(1) 编译) * * v2.2 修复:使用预解析的 resolvedTsconfigRaw 替代直接读取 tsconfig 文件, * 确保 extends 链被正确展平,与 context.rebuild() 行为一致。 * * @param srcFile 变更的源文件绝对路径 * @returns 编译产物的绝对路径 * @throws 文件读取失败、esbuild 编译错误时抛出 */ compileSingle(srcFile: string): Promise; /** * compileFiles — 批量单文件编译(多文件同时变更时并行编译) * * 当防抖窗口内多个文件同时变更时,并行调用 compileSingle。 * 每个文件的编译是独立的,适合 Promise.all 并行。 * * @param srcFiles 变更的源文件绝对路径列表 * @returns 编译产物的绝对路径列表 */ compileFiles(srcFiles: string[]): Promise; /** * resolveCompiled — 将源文件路径映射为编译后的产物路径 * * 支持两种输入: * - 绝对路径: /project/src/routes/user.ts → /project/.vext/dev/routes/user.js * - 相对于项目根目录的路径: src/routes/user.ts → /project/.vext/dev/routes/user.js * * 映射规则: * 1. 去掉 srcDir 前缀,得到相对路径 * 2. 将 .ts / .mts / .cts / .mjs / .cjs 扩展名替换为 .js * 3. 拼接到 outDir * * @param srcFile 源文件路径(绝对路径或相对于项目根目录) * @returns 编译产物的绝对路径 */ resolveCompiled(srcFile: string): string; /** * resolveSource — 将编译产物路径映射回源文件路径(反向映射) * * 用于错误堆栈显示、日志中将编译产物路径转换回用户可理解的源文件路径。 * * 注意:反向映射不恢复原始扩展名(.ts / .mjs 等), * 因为编译产物只有 .js 扩展名,无法确定原始扩展名。 * 返回的路径使用 .js 扩展名,调用方如需精确匹配可自行检查。 * * @param compiledFile 编译产物的绝对路径 * @returns 对应的源文件路径(使用 .js 扩展名) */ resolveSource(compiledFile: string): string; /** 获取源码目录(绝对路径) */ getSrcDir(): string; /** 获取编译输出目录(绝对路径) */ getOutDir(): string; /** 获取项目根目录(srcDir 的父目录) */ getProjectRoot(): string; /** * dispose — 释放 esbuild 资源 * * 关闭 esbuild context(释放子进程/内存)。 * 在 dev 服务器关闭时调用(graceful shutdown)。 * 调用后不可再使用 rebuild / compileSingle 等方法。 */ dispose(): Promise; /** * scanEntryPoints — 扫描 src/ 目录下的所有可编译源文件 * * 使用 fast-glob 扫描,返回相对于 srcDir 的路径列表。 * 排除 .d.ts 声明文件和测试文件。 * * @returns 相对于 srcDir 的源文件路径列表 */ private scanEntryPoints; private isCompileCacheValid; private readCompileCache; private writeCompileCache; private getCompileCachePath; /** * resolveTsconfig — 预解析 tsconfig.json,展平 extends 链(v2.2) * * 读取 tsconfig.json,如果包含 extends,递归合并父配置, * 最终输出一个不含 extends 的纯 JSON 字符串。 * 这样 esbuild.transform() 的 tsconfigRaw 能获得与 esbuild.context() * 的 tsconfig(路径参数)完全一致的编译行为。 * * 注意:只提取 esbuild 实际使用的字段(compilerOptions 中的 * target、jsx、jsxFactory、jsxFragment、useDefineForClassFields、 * importsNotUsedAsValues、preserveValueImports、experimentalDecorators、 * verbatimModuleSyntax 等),其他字段不影响 transform 行为。 * * 错误处理:tsconfig 不存在或解析失败时静默降级, * transform 将使用 esbuild 默认设置。 */ private resolveTsconfig; /** * flattenTsconfig — 递归展平 tsconfig,合并 extends 链 * * 递归处理 tsconfig 的 extends 字段: * 1. 读取当前 tsconfig 文件 * 2. 移除 JSON 中的注释(tsconfig 允许单行和多行注释) * 3. 解析 JSON * 4. 如果有 extends → 递归加载父配置 * 5. 子配置的 compilerOptions 覆盖父配置(浅合并) * * @param tsconfigPath tsconfig 文件的绝对路径 * @returns 展平后的配置对象(仅包含 compilerOptions) */ private flattenTsconfig; }