LangChain.js 构建系统实战:@langchain/build 如何用 tsdown 标准化所有包的构建
【免费下载链接】langchainjsThe agent engineering platform项目地址: https://gitcode.com/GitHub_Trending/la/langchainjs
在 LangChain.js 这个包含数十个包的大型 pnpm monorepo 中,如何保证每个包都产出格式一致(CJS + ESM 双格式)、类型声明完整、且通过包质量校验的构建产物?@langchain/build就是答案:它是一个基于 tsdown 的预配置构建系统,通过一个getBuildConfig()工厂函数和四个内置插件,把 LangChain 全部仓库包的tsdown.config.ts收敛到同一套默认基线上。读完本文,你可以理解该包每个默认配置项的作用、四个插件的 AST 扫描与代码生成机制,并能参照langchain-core的真实配置为自己的包接入这套构建体系。
一、@langchain/build 是什么
@langchain/build位于仓库的 internal/build 目录,是一个private: true的内部包(当前版本 0.1.1),不参与 npm 发布,只被 monorepo 内的各包以 workspace 依赖方式消费。
从 package.json 可以看到它的两个关键设计:
main和types都直接指向./src/index.ts——即消费方直接引用它的 TypeScript 源码,而不是先构建再引用;- 它的
build脚本是exit 0——因为它本身没有需要编译的产物,只需要被 tsdown 配置文件导入。
其核心依赖在 package.json 中声明:
- tsdown
^0.22.14:由 Rolldown 驱动的 TypeScript 打包/转译工具(底层为rolldown^1.2.6); - @typescript/native-preview(即 tsgo,TypeScript 原生编译器预览版):用于并行生成类型声明;
- @arethetypeswrong/core(ATTW):类型定义包质量校验;
- publint:package.json exports 结构校验;
- unplugin-unused:未使用依赖检测;
- type-fest:
PackageJson类型。
插件实现还依赖typescript编译器 API 来做 AST 扫描,并对生成文件做格式化(各插件均从 src/utils.ts 导入formatWithOxfmt包装函数)。
仓库的 AGENTS.md 中同样引用了@langchain/build,说明它是整个 monorepo 约定构建方式的基础设施。其变更历史可参见 CHANGELOG.md:0.1.1 版本修复了moduleResolution: "node"的兼容性问题。
二、快速上手:让一个包接入标准构建
按照 internal/build/README.md 的 Usage 章节,接入分两步。
第一步,在包的根目录创建tsdown.config.ts:
import { getBuildConfig } from "@langchain/build"; export default getBuildConfig();第二步,把该包package.json中的build脚本设为tsdown,然后运行tsdown完成构建(CLI 选项以 tsdown 官方文档为准)。
README 的 Development 章节还给出了在 monorepo 中新增包的完整流程:
- 在合适的 workspace 下创建包目录;
- 在包根目录添加
tsdown.config.ts; - 按包的需求配置构建配置——重点是向
entry数组添加本包的各个入口文件;tsdown 会自动把package.json的exports字段编译为对应的入口点; - 把
package.json的build脚本设置为tsdown。
下面逐层拆解getBuildConfig到底固化了哪些默认值,以及四个插件各自解决了什么问题。
三、getBuildConfig:标准化构建基线
getBuildConfig(options?)的实现在 src/index.ts。它接收一个可选的Partial<BuildOptions>,返回完整的 tsdown 配置。按 README 归纳,默认值包括:
- Formats:CommonJS 和 ESM 双格式;
- Target:ES2022;
- Platform:Node.js;
- 类型声明:使用 tsgo 并行生成;
- Source maps:开启;
- 校验:ATTW(node16 profile)、publint(strict)、未使用依赖检查。
源码中这些默认值对应的完整写法是:
return { format: ["cjs", "esm"], target: "es2022", platform: "node", fixedExtension: false, dts: { parallel: true, tsgo: true, build: true }, sourcemap: true, unbundle: true, inlineOnly: false, exports: { customExports: async (exports, context) => { /* 见下文 */ } }, attw: { profile: "node16", level: "error" }, publint: { level: "error", strict: true }, unused: { level: "error" }, ignoreWatch: [".turbo", "dist", "node_modules"], ...options, // 用户传入的部分配置在最后展开,可覆盖以上任何默认值 };(上述代码结构依据 src/index.ts 整理。)
有几个默认值在 README 中未展开,值得结合源码说明:
fixedExtension: false 与 unbundle: true
fixedExtension: false:源码注释解释,rolldown/tsdown 在开启fixedExtension时可能为 ESM 输出.mjs,而 LangChain 的包都是"type": "module",需要稳定的.jsESM 输出,因此显式关闭。unbundle: true:即 transpile-only 模式——依赖保持为 external import 而不被打包。配合inlineOnly: false,可以抑制"bundled dependency"告警;个别包如有特殊需求可以覆盖inlineOnly传入白名单。
exports 字段的自动重写
这是getBuildConfig中最核心的机制。tsdown 构建后会调用exports.customExports钩子,把每个入口重写成标准的require/import双条件结构,并同时挂上对应的类型声明路径:
acc[key] = { ...extraConditions, // 保留手写的 "browser" 等额外条件 input: `./${inputPath}`, // 如 ./src/tools/calculator.ts require: { types: `./${outputPath}.d.cts`, default: `./${outputPath}.cjs`, }, import: { types: `./${outputPath}.d.ts`, default: `./${outputPath}.js`, }, };具体规则(见 src/index.ts):
- 从每个入口的
import值出发,把./dist前缀换回./src得到输入路径; - 全程使用
path.posix拼接,注释明确指出这是为了避免 Windows 反斜杠破坏 exports 字段和 publint 校验; - 从
context.pkg(原始 package.json)中读取手写的额外 export 条件(例如"browser"),这些条件 tsdown 不知道,必须手动带过来; input/require/import三项固定生成,不可被额外条件覆盖。
这意味着包作者无需手写.d.cts/.cjs映射,构建后package.json的 exports 自动符合 Node16 模块解析的完整条件结构。
三道内置校验
attw、publint、unused的level全部设为"error",且 publint 开启strict: true。也就是说类型声明包结构、exports 结构、未使用依赖三类问题会直接让构建失败,而不是仅给出警告——这是保证所有发布包质量一致的硬约束。ignoreWatch: [".turbo", "dist", "node_modules"]则在开发态的 watch 模式下忽略产物目录,配合 monorepo 的 turbo 流水线使用。
README 给出的覆写方式是把部分配置传入getBuildConfig:
export default getBuildConfig({ target: "es2020", plugins: [myPlugin()], });由于...options位于默认值之后,任何传入的顶层字段都会覆盖同名默认值。
四、四个内置插件:面向 LangChain 模式的代码生成
该包从 src/index.ts 统一导出四个插件:cjsCompatPlugin、importConstantsPlugin、importMapPlugin、lcSecretsPlugin。它们全部是 Rolldown 插件,核心逻辑都挂在buildStart钩子上,且都依赖process.env.INIT_CWD定位包根目录(由 pnpm 在包目录下执行脚本时注入)。
1. lcSecretsPlugin:扫描 lc_secrets 约定并生成 SecretMap
LangChain 的约定是:需要敏感配置(API key、token)的类通过lc_secretsgetter 声明"实例属性 → 环境变量名"的映射:
class OpenAIProvider { get lc_secrets(): { [key: string]: string } { return { apiKey: "OPENAI_API_KEY", // Maps this.apiKey -> process.env.OPENAI_API_KEY organization: "OPENAI_ORG_ID", // Maps this.organization -> process.env.OPENAI_ORG_ID }; } }lcSecretsPlugin(options?)的实现位于 src/plugins/lc-secrets.ts,工作过程分五步:
- 确定扫描范围:不是盲目遍历目录,而是解析包根目录的
tsconfig.json(通过ts.parseJsonConfigFileContent),只扫描其中列出的文件,并过滤掉excludePatterns命中的文件(scanForSecrets); - AST 扫描:用 TypeScript 编译器 API 遍历每个源文件,找出
class声明/表达式中名为lc_secrets的 getter,提取其return语句中对象字面量里"字符串字面量值"——即环境变量名,并记录文件与行号(scanSourceFile); - 命名校验:环境变量名必须全大写、不得含空白,违反时收集错误并打印
file:line;strict模式(默认开启)下直接抛错让构建失败(validateSecrets); - 去重排序后生成:输出文件包含两个接口,secret 名按字典序排序,保证产物稳定不产生无意义的 git diff;
- 格式化写入:经 oxfmt 格式化后写入
outputPath。
生成产物(默认路径src/load/import_type.ts):
/** Auto-generated by lc-secrets plugin. Do not edit manually */ export interface OptionalImportMap {} export interface SecretMap { OPENAI_API_KEY?: string; OPENAI_ORG_ID?: string; }可配置项(含源码中的默认值):
| 选项 | 默认值 | 说明 |
|---|---|---|
enabled | true | 是否启用扫描 |
strict | true | 校验失败时是否抛错中断构建 |
outputPath | "src/load/import_type.ts" | 生成的 SecretMap 文件路径(相对包根) |
excludePatterns | [".test.ts", "test.ts", ".spec.ts", "spec.ts"] | 扫描排除模式 |
使用示例:
import { getBuildConfig, lcSecretsPlugin } from "@langchain/build"; export default getBuildConfig({ plugins: [ lcSecretsPlugin({ outputPath: "src/load/import_type.ts", excludePatterns: [".test.ts"], }), ], });它的价值在于:所有包用到的环境变量被集中到一份类型化清单中,环境变量命名错误会在构建期而不是运行期暴露。
2. importConstantsPlugin:生成可选导入入口清单
importConstantsPlugin(options?)位于 src/plugins/import-constants.ts。它读取包package.json的name,为配置的每个入口生成带包名前缀的导入路径字符串,写入一个导出数组文件(默认src/load/import_constants.ts):
/** Auto-generated by import-constants plugin. Do not edit manually */ export const optionalImportEntrypoints: string[] = [ "langchain_openai/tools/calculator", "langchain_openai/embeddings/openai", ];包名前缀规则在 import-constants.ts 中实现:
@langchain/core→ 生成langchain/...导入;@langchain/openai→ 生成langchain_openai/...导入;- 非作用域的
langchain→ 生成langchain/...导入。
这个数组在运行期可用于判断哪些可选模块可用、或做动态加载。可配置项:enabled(默认true)、outputPath(默认"src/load/import_constants.ts")、entrypoints(相对包根、不带包名前缀的入口列表)。使用示例:
import { getBuildConfig, importConstantsPlugin } from "@langchain/build"; export default getBuildConfig({ plugins: [ importConstantsPlugin({ entrypoints: ["tools/calculator", "embeddings/openai", "llms/anthropic"], }), ], });3. importMapPlugin:生成带命名空间别名的聚合导入图
importMapPlugin(options?)位于 src/plugins/import-map.ts,为所有包入口生成"命名空间再导出"文件,便于批量导入、测试和按字符串键动态加载(默认输出到src/load/import_map.ts):
/** Auto-generated by import-map plugin. Do not edit manually */ export * as index from "../index.js"; export * as tools__calculator from "../tools/calculator.js"; export * as providers__openai from "../providers/openai.js";实现细节(import-map.ts):
- 从
buildStart的input参数读取 tsdown 的入口集合; - 过滤:跳过特殊的
load入口,跳过nodeOnly、importsOptionalDependencies、omitFromImportMap三个列表命中的入口,再按键排序以保证输出确定性(避免排序漂移造成 git diff); - 别名转换:入口路径中的
/替换为双下划线__(tools/calculator→tools__calculator),以/index结尾的键会先去掉/index后缀; - 路径转换:入口的相对路径(去
./与src/前缀)映射为指向编译输出的.js文件,生成export * as <alias> from "../<path>.js"语句; extraEntries支持额外的导入映射:modules含"*"时生成命名空间导出,否则生成import {...}语句加一个聚合常量对象导出;相同path的条目会被按 path 去重合并。
可配置项:enabled(默认true)、outputPath(默认"src/load/import_map.ts")、nodeOnly、importsOptionalDependencies、omitFromImportMap(均为入口键列表)、extraEntries({ modules, alias, path }数组)。使用示例:
import { getBuildConfig, importMapPlugin } from "@langchain/build"; export default getBuildConfig({ plugins: [ importMapPlugin({ nodeOnly: ["node-specific-tool"], importsOptionalDependencies: ["openai", "anthropic"], extraEntries: [ { modules: ["*"], alias: ["utils", "helpers"], path: "./utils/helpers.js", }, ], }), ], });4. cjsCompatPlugin:双格式包的 CJS 兼容桶文件
cjsCompatPlugin(options?)位于 src/plugins/cjs-compat.ts,为双格式包的每个入口在包根目录生成指向dist/的再导出桶文件,确保 CommonJS 与 ESM 环境下的模块解析都正确。以入口tools/calculator为例,生成四组文件:
// tools/calculator.cjs module.exports = require("../dist/tools/calculator.cjs"); // tools/calculator.d.cts export * from "../dist/tools/calculator.js"; // tools/calculator.d.ts export * from "../dist/tools/calculator.js"; // tools/calculator.js export * from "../dist/tools/calculator.js";其关键机制(源码级):
- 双模式:
mode取"generate"或"clean"。默认值由环境变量决定——BUILD_MODE === "prerelease"时为"generate",否则为"clean"(cjs-compat.ts)。也就是说常规构建走清理路径,预发布流程才把桶文件写入包根并纳入发布内容; - 桶路径推导:以
src目录为基准计算入口的相对路径,若入口名是index则桶文件落到所在目录(callbacks/base/index→callbacks/base),再按深度生成./dist/...或../../dist/...的导入路径;根级桶(barrelPath === ".")被跳过(cjs-compat.ts); - 选择性生成:
shouldGenerate的dcts/cjs/dts/esm四项默认全为true,可单独关闭某类文件; - 路径安全护栏:写入与删除都经过
isSafeProjectPath校验,Windows 下先做toPosixPath归一化,防止反斜杠路径导致解析错乱; - 自动维护 package.json 的 files 字段:在
buildEnd钩子中,把本次生成(或清理)过的顶层目录合并进files数组(前面拼接用户传入的files白名单,如["dist/", "CHANGELOG.md", ...]),保证桶文件确实会被npm publish带上(cjs-compat.ts)。
可配置项:enabled(默认true)、mode、shouldGenerate(四项布尔,默认全开)、files(追加进files数组的额外条目)。使用示例:
import { getBuildConfig, cjsCompatPlugin } from "@langchain/build"; export default getBuildConfig({ plugins: [ cjsCompatPlugin({ mode: "generate", shouldGenerate: { dcts: true, cjs: true, dts: true, esm: true, }, }), ], });五、真实用法:langchain-core 的完整构建配置
最完整的消费示例是 libs/langchain-core/tsdown.config.ts——它一次性启用了全部四个插件:
import { getBuildConfig, cjsCompatPlugin, lcSecretsPlugin, importMapPlugin, importConstantsPlugin, } from "@langchain/build"; import pkg from "./package.json" with { type: "json" }; export default getBuildConfig({ entry: [ "./src/index.ts", "./src/agents.ts", "./src/caches/index.ts", "./src/callbacks/base.ts", /* ...数十个 src 入口,与 package.json 的 exports 一一对应 ... */ ], define: { __PKG_VERSION__: JSON.stringify(pkg.version) }, plugins: [ cjsCompatPlugin({ files: ["dist/", "CHANGELOG.md", "README.md", "LICENSE"], }), lcSecretsPlugin(), importMapPlugin({ omitFromImportMap: [ "load/index", "context", "callbacks/dispatch/web", "callbacks/dispatch/index", ], }), importConstantsPlugin(), ], });这份配置印证了前文的几个要点:
entry数组显式列出每个需要发布子路径的入口文件,getBuildConfig的customExports会据此自动重写 exports 的 require/import/types 条件;define把__PKG_VERSION__注入为当前包版本,供运行时代码读取;cjsCompatPlugin通过files声明dist/、CHANGELOG、README、LICENSE 随包发布,插件再把各桶目录补进files;importMapPlugin用omitFromImportMap把内部模块(如load/index、callbacks/dispatch/*)排除在动态导入图之外;lcSecretsPlugin与importConstantsPlugin均使用默认参数,生成物落在src/load/下。
其他几十个包(如 langchain、langchain-openai、langchain-anthropic 等)的tsdown.config.ts都是同一模式的不同组合,可对照参考。
六、小结
@langchain/build用约两百行的 getBuildConfig 把"双格式 + ES2022 + Node 平台 + tsgo 并行类型声明 + source map + exports 自动重写 + ATTW/publint/unused 三道 error 级校验"固化为所有包的构建基线,再用四个buildStart阶段的代码生成插件分别解决环境变量清单(lcSecretsPlugin)、可选入口常量(importConstantsPlugin)、动态导入图(importMapPlugin)和 CJS 兼容桶文件(cjsCompatPlugin)四个 LangChain 特有的工程问题。新包接入时只需照抄 README 的四步流程、按需声明entry与插件参数,即可得到与langchain-core同等质量标准的构建产物。
【免费下载链接】langchainjsThe agent engineering platform项目地址: https://gitcode.com/GitHub_Trending/la/langchainjs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考