跳到正文
曾亮懿 ZengLiangYi Blog

TypeScript 多包配置:类型检查与运行环境的分界

核对 ChatCrystal 的共享类型、服务端、Vite 客户端和 Electron 配置,用可复现的别名反例解释编译通过为何不等于运行成功。

曾亮懿

曾亮懿

阅读约 4 分钟
目录 展开 收起

一个代码仓库里的 TypeScript 文件可能由完全不同的程序执行:Node 服务端、Vite 浏览器构建和 Electron 主进程。共享类型不代表共享所有编译选项。本文检查 ChatCrystal 的实际配置继承、输出目录和模块解析,再用一个临时项目证明路径别名的运行边界。

先画出真实配置关系

当前 基础配置设置 ES2022、ESNext 模块、bundler 解析、strict 和 isolatedModules 等。只有直接或间接 extends 的配置继承它;不能看到根目录有 base 文件就宣布所有子项目都使用同一配置。

范围 实际配置关系 主要运行边界
shared extends 基础配置,composite 为 true 包入口指向 types/index.ts,共享类型契约
server extends 基础配置,rootDir 为上级目录 tsc 输出 JS,生产由 Node 执行
client 根配置引用 app 与 node,两者不 extends 基础配置 app 面向浏览器,node 面向 Vite 配置;均 noEmit
electron 独立配置,module 与解析均为 Node16 主进程编译输出,再由打包流程使用

根 package.json的工作区是 shared、server、client,electron 不在这份工作区清单里。客户端的项目引用指向 app 和 node,不是引用整个 shared 项目。shared 的 composite 也不会自动让所有消费者形成一个 project references 构建图。

服务端开发执行与生产输出分开看

server/tsconfig.json同时包含 src 和上级 shared/types,因此 rootDir 是上级目录,outDir 是 server/dist。由此形成 dist/server/src 等路径,不是简单把 src 平移为 dist。

服务端脚本开发时使用 tsx watch;生产构建先清理 dist,再 tsc,最后复制种子数据,启动入口是 dist/server/src/index.js。仅运行 tsc 不能替代整条构建命令。JSON 等运行资源也不能假定总会由类型编译器自动放到正确位置。

server 包声明 type:module;基础 module:ESNext 控制发出语法,包的 type 与 Node 解析共同决定如何执行文件。开发时能被 tsx 加载的路径,仍应在生产输出中验证,不能只以开发服务器能启动作为验收。

客户端别名同时存在于两层

客户端 app 配置使用 ES2023、DOM 类型、react-jsx、bundler 解析和 noEmit。Vite 配置的 node 项目使用 Node 类型,不是把 DOM 配置整份复制过去。两者还包含 verbatimModuleSyntax 等检查;只看旧版 base 配置会漏掉这些实际选项。

@/* 在 TypeScript paths 中映射到 src,在 vite.config.ts中另有运行构建别名,使用 import.meta.dirname 定位路径。类型解析和打包解析分别有配置,不能只修改其中一处就认为另一处跟随。paths 的含义见官方说明:它不会改写发出的 import 路径。

用临时项目验证别名反例

客户端将 typescript 指向 @typescript/typescript6 6.0.2 包装器,其底层依赖在锁文件中解析为真正的 TypeScript 6.0.3。下面直接安装这个检查器版本,避免把包装器版本当作编译器版本。练习不运行 ChatCrystal 的构建或发布脚本;在新目录创建以下文件,用 Node 24+ 执行。

package.json 声明教学项目使用 ESM:

{ "private": true, "type": "module" }

安装固定版本的编译器:

npm install --ignore-scripts --no-audit --no-fund typescript@6.0.3

保存 tsconfig.json。rootDir 显式决定输出布局;这个小项目自行发出 JS,与客户端的 noEmit 配置不同。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "paths": { "@lesson/*": ["./src/*"] }
  },
  "include": ["src"]
}

创建 src 目录,保存 src/values.ts:

export const value = 42;

保存 src/main.ts,先保留刻意存在的别名:

import { value } from '@lesson/values.js';
console.log(value);

先编译,再运行:

node node_modules/typescript/bin/tsc --project tsconfig.json
node dist/main.js

编译应通过,发出的 main.js 仍保留 @lesson/values.js。普通 Node 不认识这个自定义别名,第二条命令预期以 ERR_MODULE_NOT_FOUND 失败。这里没有配置打包器、加载器或 import 映射。

现在把 src/main.ts 的导入改为下面的相对路径,重新执行同样两条命令,应编译通过并输出 42:

import { value } from './values.js';
console.log(value);

先失败再恢复的证据说明的是路径解析边界,不是证明相对导入是所有项目的唯一方案。也可以使用构建器或运行环境支持的映射,但需要分别配置并验证。

Electron 的 Node16 模式不是一个通用 CommonJS 开关

electron/tsconfig.json使用 Node16 模块和解析,带 Node 与 Electron 类型、strict、sourceMap 等。Node16 会结合扩展名和包作用域决定模块形式。当前 electron 下没有自己的 package.json,根包也没有 type:module,所以现有普通 .ts 主进程文件处于 CommonJS 包作用域;不能把这个观察概括成 Node16 永远只发出 CommonJS。

新增选项前先问:谁检查类型,谁发出文件,谁处理路径,谁在生产执行?然后核对共享类型、资源复制和真实启动入口。strict 能发现部分类型错误,不能验证外部 JSON、文件存在或整个打包生命周期。本文只运行临时反例,没有执行应用打包或声称更改这些配置后必然成功。

分享:

相关文章

五种 AI 对话解析器:字段保留与损失边界

从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵,检查工具标记、思考文本、顺序与快照覆盖范围。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #TypeScript #Node.js

AI SDK Provider 抽象:能力表与协议边界

核对 ChatCrystal 的七项 Provider 注册、AI SDK 7 模型工厂和结构化输出,用无网络 mock 验证 Responses、Chat Completions、Embedding 与 schema 失败。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #TypeScript

SourceAdapter 内部链路:身份、内容哈希与重新导入

对照 ChatCrystal 的注册表、导入器和远端载荷,验证会话身份、内容哈希、元数据捷径与 parserVersion 的实际边界。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #TypeScript #Node.js

从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录,欢迎交流实现细节。