一个代码仓库里的 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、文件存在或整个打包生命周期。本文只运行临时反例,没有执行应用打包或声称更改这些配置后必然成功。