LangChain.js 构建系统实战:@langchain/build 如何用 tsdown 标准化所有包的构建
2026/9/13 15:46:03 网站建设 项目流程

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 可以看到它的两个关键设计:

  • maintypes都直接指向./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-festPackageJson类型。

插件实现还依赖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 中新增包的完整流程:

  1. 在合适的 workspace 下创建包目录;
  2. 在包根目录添加tsdown.config.ts
  3. 按包的需求配置构建配置——重点是向entry数组添加本包的各个入口文件;tsdown 会自动把package.jsonexports字段编译为对应的入口点;
  4. package.jsonbuild脚本设置为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):

  1. 从每个入口的import值出发,把./dist前缀换回./src得到输入路径;
  2. 全程使用path.posix拼接,注释明确指出这是为了避免 Windows 反斜杠破坏 exports 字段和 publint 校验;
  3. context.pkg(原始 package.json)中读取手写的额外 export 条件(例如"browser"),这些条件 tsdown 不知道,必须手动带过来;
  4. input/require/import三项固定生成,不可被额外条件覆盖。

这意味着包作者无需手写.d.cts/.cjs映射,构建后package.json的 exports 自动符合 Node16 模块解析的完整条件结构。

三道内置校验

attwpublintunusedlevel全部设为"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 统一导出四个插件:cjsCompatPluginimportConstantsPluginimportMapPluginlcSecretsPlugin。它们全部是 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,工作过程分五步:

  1. 确定扫描范围:不是盲目遍历目录,而是解析包根目录的tsconfig.json(通过ts.parseJsonConfigFileContent),只扫描其中列出的文件,并过滤掉excludePatterns命中的文件(scanForSecrets);
  2. AST 扫描:用 TypeScript 编译器 API 遍历每个源文件,找出class声明/表达式中名为lc_secrets的 getter,提取其return语句中对象字面量里"字符串字面量值"——即环境变量名,并记录文件与行号(scanSourceFile);
  3. 命名校验:环境变量名必须全大写、不得含空白,违反时收集错误并打印file:linestrict模式(默认开启)下直接抛错让构建失败(validateSecrets);
  4. 去重排序后生成:输出文件包含两个接口,secret 名按字典序排序,保证产物稳定不产生无意义的 git diff;
  5. 格式化写入:经 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; }

可配置项(含源码中的默认值):

选项默认值说明
enabledtrue是否启用扫描
stricttrue校验失败时是否抛错中断构建
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.jsonname,为配置的每个入口生成带包名前缀的导入路径字符串,写入一个导出数组文件(默认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):

  1. buildStartinput参数读取 tsdown 的入口集合;
  2. 过滤:跳过特殊的load入口,跳过nodeOnlyimportsOptionalDependenciesomitFromImportMap三个列表命中的入口,再按键排序以保证输出确定性(避免排序漂移造成 git diff);
  3. 别名转换:入口路径中的/替换为双下划线__tools/calculatortools__calculator),以/index结尾的键会先去掉/index后缀;
  4. 路径转换:入口的相对路径(去./src/前缀)映射为指向编译输出的.js文件,生成export * as <alias> from "../<path>.js"语句;
  5. extraEntries支持额外的导入映射:modules"*"时生成命名空间导出,否则生成import {...}语句加一个聚合常量对象导出;相同path的条目会被按 path 去重合并。

可配置项:enabled(默认true)、outputPath(默认"src/load/import_map.ts")、nodeOnlyimportsOptionalDependenciesomitFromImportMap(均为入口键列表)、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/indexcallbacks/base),再按深度生成./dist/...../../dist/...的导入路径;根级桶(barrelPath === ".")被跳过(cjs-compat.ts);
  • 选择性生成shouldGeneratedcts/cjs/dts/esm四项默认全为true,可单独关闭某类文件;
  • 路径安全护栏:写入与删除都经过isSafeProjectPath校验,Windows 下先做toPosixPath归一化,防止反斜杠路径导致解析错乱;
  • 自动维护 package.json 的 files 字段:在buildEnd钩子中,把本次生成(或清理)过的顶层目录合并进files数组(前面拼接用户传入的files白名单,如["dist/", "CHANGELOG.md", ...]),保证桶文件确实会被npm publish带上(cjs-compat.ts)。

可配置项:enabled(默认true)、modeshouldGenerate(四项布尔,默认全开)、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数组显式列出每个需要发布子路径的入口文件,getBuildConfigcustomExports会据此自动重写 exports 的 require/import/types 条件;
  • define__PKG_VERSION__注入为当前包版本,供运行时代码读取;
  • cjsCompatPlugin通过files声明dist/、CHANGELOG、README、LICENSE 随包发布,插件再把各桶目录补进files
  • importMapPluginomitFromImportMap把内部模块(如load/indexcallbacks/dispatch/*)排除在动态导入图之外;
  • lcSecretsPluginimportConstantsPlugin均使用默认参数,生成物落在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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询