Mastra 类型构建器 @internal/types-builder:跨包捆绑类型的内联、校验与结构性边界规则
【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra
本篇技术指南以 Mastra 仓库中的内部工具包 packages/_types-builder/AGENTS.md 为骨架,深入讲解@internal/types-builder如何在发布.d.ts声明文件时内联@internal/*依赖类型、校验声明文件的运行时依赖合法性,并解释其核心设计约束——"被捆绑的类型必须是结构性的(structural)"。读完本文,你将掌握generateTypes()的完整流水线、dist/_types/内联机制、IMastraAuthProvider结构接口的由来,以及 #18682 这类跨包名义类型(nominal typing)陷阱的规避方法。
一、工具定位:解决 monorepo 内部类型依赖的发布难题
Mastra 是一个基于 pnpm workspace 的 TypeScript monorepo,packages/core等公共包会通过deps.alwaysBundle(见 packages/core/tsdown.config.ts)将@internal/*系列内部包直接打进产物。这带来一个连锁问题:运行时依赖被捆绑后,声明文件(.d.ts)中的import ... from '@internal/...'在发布后就无法解析了。
@internal/types-builder正是为处理这一场景而生的构建期工具。按 packages/_types-builder/AGENTS.md 的定位描述,它负责:
- 为捆绑了
@internal/*(及其他)类型依赖的包生成可发布的.d.ts产物; - 通过
generateTypes()编译声明; - 将捆绑的包类型内联进
dist/_types/目录; - 校验声明文件只引用运行时依赖或已捆绑的包。
工具本身是一个private: true的内部包(package.json),依赖@microsoft/api-extractor(声明折叠/回滚)、ts-morph(AST 改写)、tinyglobby(文件扫描)、local-pkg与resolve.exports(包解析)、typescript,并通过 exports 暴露./embed-types与./compile-zod两个子入口。
二、generateTypes():一次发布类型构建的完整流水线
generateTypes(rootDir, bundledPackages)是工具的主入口(src/index.js),接受两个参数:
| 参数 | 类型 | 含义 |
|---|---|---|
rootDir | string | 目标包的根目录(如process.cwd()) |
bundledPackages | Set<string> | 被捆绑进产物的包名集合,支持'@ai-sdk/*'这样的通配符前缀 |
整个流程分四步,每一步都有对应的源码实现:
2.1 调用 tsc 编译声明
const tscProcess = spawn('npx', ['tsc', '-p', 'tsconfig.build.json'], { cwd: rootDir, stdio: 'inherit', shell: true, env: getFilteredEnv(), });这里刻意使用spawn+stdio: 'inherit'(而非exec)让 TypeScript 的编译错误直接透传到终端,并使用shell: true保证跨平台兼容。值得注意的细节是getFilteredEnv()(src/index.js):它会剔除一批 pnpm 特有的环境变量(如npm_config_catalog、pnpm_config_patched-dependencies等),避免这些变量被透传给npx/npm时产生 "Unknown env config" 警告。
2.2 内联捆绑类型到 dist/_types/
编译完成后,工具用 tinyglobby 扫描dist/**/*.d.ts,对每个声明文件调用replaceTypes(fullPath, rootDir, bundledPackages)(src/index.js),其核心逻辑在 src/replace-types.js:
- 用 ts-morph 解析声明文件的 import/export/
import()语句,找出所有匹配bundledPackages的模块说明符; - 通过
local-pkg的getPackageInfo+resolve.exports(conditions: ['types'])解析出源包的声明入口;若解析不到,则回退尝试@types/*包; - 调用
copyDeclarationGraph将源包的声明文件(连同其相对依赖的整个声明图)递归复制到dist/_types/<pkg名(/ 替换为 _)>/下; - 把原声明文件中对捆绑包的 import 改写为指向
dist/_types/的相对路径。
copyDeclarationGraph还会用visitedSet 做去重,避免同一个声明文件被反复复制。另一个跨平台细节是路径分隔符:path.relative()在 Windows 上会返回反斜杠,而.d.ts中的模块说明符必须使用 POSIX 分隔符/,否则moduleResolution: "bundler"下会解析失败——因此代码中显式执行了.split(sep).join('/')(src/replace-types.js)。
2.3 重写相对导入以兼容 ESM
对于每个.d.ts,工具还会用正则重写形如from './foo'的相对导入(src/index.js):
- 若目标是一个目录,则追加
/index.js; - 否则追加
.js后缀; - 已经以
.js结尾或本身就是.d.ts/.d.mts/.d.cts的导入保持不变。
这一步骤让生成的声明文件在 NodeNext / ESM 环境下也能正确解析。
2.4 校验声明文件的运行时依赖
validateDeclarationRuntimeImports(rootDir, bundledPackages)(src/index.js)是最后一道质量闸门。它读取目标包的package.json,收集dependencies、peerDependencies、optionalDependencies与bundleDependencies/bundledDependencies作为"合法运行时依赖集合",然后扫描dist/**/*.d.ts(跳过_types/目录)中的所有导入说明符,判断其包名是否:
- 等于当前包自身,或
- 在运行时依赖集合中(含
@types/*变体),或 - 匹配
bundledPackages中的某项(支持pkg/*通配)。
任何不满足条件的导入都会被标记为devDependency或undeclared dependency,最终抛出带完整清单的错误:
Generated declaration files reference packages that are not runtime dependencies. Add the package to generateTypes(..., bundledPackages), move it to dependencies/peerDependencies, or remove it from the public types.这条错误信息本身就是一套明确的修复指引:把包加入bundledPackages、移到dependencies/peerDependencies,或从公开类型中移除。
三、边界规则:被捆绑的类型必须保持结构性
这是 AGENTS.md 中最核心、也最具工程价值的部分。背景是:
捆绑的声明文件是副本——多个已发布包各自携带同一份
@internal/*声明的拷贝(例如@mastra/core和每个 auth 提供方各自内嵌了MastraAuthProvider/MastraBase)。
TypeScript 对#private与protected成员采用名义性(nominal)检查:两个结构完全相同、但各自带#private字段的类副本,彼此之间是不可赋值的。这直接导致了用户侧的编译失败——server.auth = new MastraAuthWorkos()报错(issue #18682),因为MastraAuthWorkos实例携带的是 auth 提供方自己拷贝的那份MastraAuthProvider声明,而Mastra的server.auth期望的是@mastra/core内部那份。
3.1 三条具体规则
文档给出了可操作的边界规则:
- 跨已发布包边界传递的类型必须是结构性的。不要依赖
#private字段、protected成员或instanceof检查来建立跨边界的身份识别——应在公共契约点上暴露结构性接口(如IMastraAuthProvider),运行时使用鸭子类型(duck-typing)。 - 被捆绑的类声明本身保留其品牌(brand),因此名义性的类类型(如
MastraAuthProvider)绝不能出现在接收另一个已发布包实例的位置上。应该改收结构性接口,并让类声明implements该接口,让编译器在两份声明之间保持同步。 - 回归测试覆盖在
packages/core/src/server/server.test-d.ts(接口可赋值性,含模拟的捆绑副本)与e2e-tests/type-check/template/core/auth.test-d.ts(针对打包产物,在exactOptionalPropertyTypes下运行)。
3.2 源码佐证:IMastraAuthProvider 与模拟捆绑副本
在 packages/core/src/server/server.test-d.ts 中,"IMastraAuthProvider structural boundary (#18682)" 测试块原样印证了文档描述:
- 第一个用例验证
SimpleAuth实例可以赋值给IMastraAuthProvider、可以直接塞进new CompositeAuth([...])和new Mastra({ server: { auth: ... } }); - 第二个用例手工构造了一个
BundledCopyProvider类,模拟 auth 提供方在自己dist/中携带的声明副本:拥有相同的公开表面(component、name、toRawConfig()、authenticateToken()、authorizeUser()、mapUserToResourceId?),却带着独立的#rawConfig与protected logger——即名义上互不兼容的两份类。测试断言这个副本实例依然可以赋值给IMastraAuthProvider并注入CompositeAuth与Mastra。这就是文档所说的"如果IMastraAuthProvider将来引入任何重新触发名义检查的成员,此赋值就会断裂"的防护网。
3.3 e2e 层验证:打包产物 + 严格选项
packages/core/src/server/server.test-d.ts 属于仓库内的类型测试;而 e2e-tests/type-check/template/core/auth.test-d.ts 走的是更接近用户真实体验的路径:测试从本地 registry 安装打包后的产物(@mastra/core、@mastra/auth、@mastra/auth-workos),在exactOptionalPropertyTypes: true(见模板内tsconfig.exact-optional.json)这一曾暴露 bug 的严格开关下编译,验证:
new Mastra({ server: { auth: workos } })与new Mastra({ studio: { auth: workos } })均可编译;MastraAuthWorkos实例可赋值给来自@mastra/core/server和@mastra/auth两个来源的IMastraAuthProvider<any>;- 提供方实例可进入
CompositeAuth再注入Mastra。
这条 e2e 链路把"声明文件被打包内联"与"用户在 userland 编译"两件事真正打通,是第二节流水线的端到端验收。
四、配套子模块:embed-types 与 compile-zod
除generateTypes()主入口外,工具包还通过 package.json 的 exports 暴露两个子模块:
4.1 embed-types:基于 API Extractor 的声明内联
src/embed-types.js 提供了embedTypes(file, rootDir, bundledPackages),实现更彻底的"内联"方案:先用流式读取判断声明文件是否包含捆绑包引用(避免无谓地跑 API Extractor),确认后调用@microsoft/api-extractor,以该声明文件为主入口、以tsconfig.build.json为编译配置做dtsRollup(publicTrimmedFilePath指向原文件),把捆绑包类型真正折叠进单个声明文件。期间通过messageCallback捕获ae-forgotten-export警告,把遗漏导出的符号用 ts-morph 以export声明补回,确保回滚后公开类型不缺失。该模块是replaceTypes的 API-Extractor 版替代路径,适合需要声明折叠的发布场景。
4.2 compile-zod:构建期编译 zod 模式为 JSON Schema
src/compile-zod.js 导出一个 esbuild 插件esbuildCompileZod()与配套的compileSchema(schema)标识函数(类型声明见 src/compile-zod.d.ts)。其思路是:在源码里用compileSchema(z.object({...}))标记 schema,构建时在 esbuild 的onLoad钩子中把该调用原地替换为JSON.stringify后的 JSON Schema 字符串,并删除对@internal/types-builder/compile-zod的 import,从而让运行时不再携带 zod 本体。
实现上有两个值得关注的细节:
- 通过 zod 的 Standard Schema 接口
schema['~standard'].jsonSchema.input()求值 JSON Schema(需要 zod v4); - 替换前会用 TypeScript AST 做作用域分析(
isImportBinding,src/compile-zod.js),确保compileSchema这个局部名没有被函数参数、块级声明或顶层声明遮蔽,避免误替换同名标识符。
五、在仓库中的实际接线方式
以@mastra/core为例,packages/core/tsdown.config.ts 在 tsdown 构建的onSuccess钩子中调用:
onSuccess: async () => { await new Promise(resolve => setTimeout(resolve, 1000)); await generateTypes( process.cwd(), new Set([ '@ai-sdk/*', 'eventsource-parser', '@internal/ai-sdk-v4', '@internal/ai-sdk-v5', '@internal/ai-v6', '@internal/ai-v7', '@internal/external-types', '@internal/core', '@internal/voice', 'hono', 'hono-openapi', '@internal/auth', ]), ); // ... 复制 provider-registry.json 与 capabilities/ 到 dist/ },这里传入的bundledPackages集合恰好与 tsdown 配置中deps.alwaysBundle列表(packages/core/tsdown.config.ts)一一对应,两者共同保证了"运行时被捆绑、声明也被内联"的一致性。任何新增的alwaysBundle依赖都必须同步出现在generateTypes的bundledPackages中,否则会被第三节的validateDeclarationRuntimeImports拦下——这正是该工具在 CI 中扮演的"防漂移"角色。
六、实践要点与避坑清单
结合文档与源码,在为 Mastra 这样的多包仓库维护类型发布时,需要记住:
- 跨包边界的公开契约用接口,不用带品牌的类:凡是要接收其他已发布包实例的位置(如
server.auth、CompositeAuth构造参数),一律收IMastraAuthProvider这样的结构性接口,类侧通过implements与接口保持同步。 #private/protected/instanceof只在单一包内可信:它们引入名义检查,在内联副本场景下会静默制造不可赋值错误,且往往只在exactOptionalPropertyTypes等严格配置下暴露。- 任何新捆绑的
@internal/*依赖都要加入bundledPackages:generateTypes(process.cwd(), new Set([...]))中的集合必须覆盖 tsdownalwaysBundle的全部条目,并让校验器兜底。 - 声明产物要在"打包后"状态验证:仓库内类型测试(如 server.test-d.ts)与基于本地 registry 打包产物的 e2e 类型测试(如 auth.test-d.ts)缺一不可,后者才真正等价于用户安装后的编译体验。
- 构建脚本内联的声明要兼容 ESM 与 Windows:相对导入需补
.js后缀、目录导入需指向/index.js、路径分隔符必须归一化为/,这些细节(src/index.js 与 src/replace-types.js)直接影响moduleResolution: bundler下的解析成败。
七、总结
@internal/types-builder以一套四步流水线(tsc 编译 → 类型内联到dist/_types/→ ESM 相对导入重写 → 运行时依赖校验)解决了 monorepo 内部包被捆绑后声明文件"悬空"的问题,而其最深的工程洞见在于:当声明文件作为副本被分发给多个已发布包时,类型系统必须退回到结构性检查的地基上。IMastraAuthProvider正是这一理念的落地产物,它把 #18682 从一次用户侧编译事故,转化为一条可被仓库内与 e2e 两层类型测试持续守护的边界规则。
【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考