Dagger CurrentModule 深度解析:在 TypeScript 模块运行时反射模块自身 API
【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger
CurrentModule 是 Dagger 暴露给模块函数运行时的"反射式模块 API",它让运行中的模块代码能够查询自身的名称与依赖、访问源码目录与生成代码、读写模块执行时的 scratch 工作目录。本文将基于 Dagger 0.20 版本 TypeScript SDK 的 API 参考文档,结合仓库中的客户端生成源码与引擎实现,逐一拆解 CurrentModule 的每个方法、参数语义与底层调用链,帮助你写出真正了解"我是谁、我在哪"的自省型 Dagger 模块。
一、CurrentModule 是什么
在 TypeScript API 参考文档 中,CurrentModule被定义为:
Reflective module API provided to functions at runtime.
即"在运行时提供给函数的反射式模块 API"。它继承自BaseClient,是一棵惰性求值的查询树根节点——调用其方法不会立即执行,而是构建 Dagger GraphQL 查询,直到最终await时才真正与引擎通信。
从引擎侧看,CurrentModule在核心 GraphQL Schema 中被定义为一个实现了Node接口的对象类型:
"""Reflective module API provided to functions at runtime.""" type CurrentModule implements Node { dependencies: [Module!]! generatedContextDirectory: Directory! generators(include: [String!]): GeneratorGroup! id: ID! name: String! source: Directory! workdir(path: String!, exclude: [String!] = [], include: [String!] = [], gitignore: Boolean = false): Directory! workdirFile(path: String!): File! } scalar CurrentModuleID该定义完整记录在 core/schema/testdata/base_schema.graphqls 中,是与版本化 API 文档一一对应的"黄金事实源"。
构造函数:仅供内部使用
CurrentModule的构造函数签名为:
new CurrentModule(ctx?, _id?, _name?): CurrentModulectx?:Context,查询上下文;_id?:CurrentModuleID,当前模块的唯一标识;_name?:string,模块名称。
文档与源码(api/client.gen.ts)都明确标注:"Constructor is used for internal usage only, do not create object from it."(构造函数仅供内部使用,请勿自行创建实例)。在 SDK 生成的 Go 运行时客户端中同样如此(见 dagger.gen.go)。你不需要也不应该new CurrentModule(),正确获取它的方式是调用查询根上的入口方法:
const cur = client.currentModule()currentModule()在 api/client.gen.ts 中实现,它只是简单地创建了一个绑定到currentModuleGraphQL 选择器的CurrentModule实例。引擎侧入口定义在 core/schema/module.go,最终返回一个包着当前执行模块的*core.CurrentModule对象。
二、身份查询:id() 与 name()
这两个方法回答模块运行时最基本的问题:"我是谁?"
id()
id(): Promise<CurrentModuleID>返回当前模块在本次会话中的唯一标识符CurrentModuleID。在 TypeScript 客户端中,id是惰性求值的:
id = async (): Promise<ID> => { if (this._id) { return this._id } const ctx = this._ctx.select("id") const response: Awaited<ID> = await ctx.execute() return response }注意其"短路"优化:如果构造时已经注入了_id,则直接返回而不再发起网络查询(api/client.gen.ts)。由于CurrentModule implements Node,它同时支持通过loadCurrentModuleFromID(id: CurrentModuleID!): CurrentModule!从 ID 重新加载对象(见 base_schema.graphqls)。
name()
name(): Promise<string>返回"正在执行的模块的名称"(The name of the module being executed in)。引擎侧实现非常直接——它直接读取模块对象的名称字段:
func (s *moduleSchema) currentModuleName(...) (string, error) { return curMod.Module.Self().NameField, nil }见 core/schema/module.go。也就是说,name()返回的正是dagger.json中配置的模块名。与id()相同,name()也带有_name短路缓存(api/client.gen.ts)。
三、目录与文件访问:source() 与 generatedContextDirectory()
模块代码经常需要读取自身源码或生成代码,这两组方法提供了完整的访问路径。
source()
source(): Directory返回"加载进引擎的模块源码目录,并叠加了可能已生成的代码"(The directory containing the module's source code loaded into the engine (plus any generated code that may have been created))。
这是理解 Dagger 模块工作方式的关键:source()不是原始的裸源码目录,而是"上下文目录 + 生成代码补丁"的合成结果。引擎侧实现清晰地展示了这一叠加过程(core/schema/module.go):
- 取出模块的
Source.Value与SourceSubpath(若为空则回退到SourceRootSubpath); - 先选择
generatedContextDirectory拿到生成内容的 diff; - 再在
ContextDirectory上执行withDirectory(path: "/", source: generatedDiff)将生成代码叠加到根目录; - 最后按
srcSubpath定位到模块源码所在的子目录并返回。
因此,当你在模块函数里执行dag.currentModule().source()时,得到的是"能直接看到dagger.json、模块源码以及 SDK 生成文件"的完整视图。
generatedContextDirectory()
generatedContextDirectory(): Directory返回"在模块源码的上下文目录之上生成的文件与目录"(The generated files and directories made on top of the module source's context directory)。它就是上一步叠加到源码上的那层"生成补丁"本身。引擎实现同样清晰(core/schema/module.go):直接在模块源对象上选择generatedContextDirectory字段。
对比小结:
| 方法 | 返回内容 | 典型用途 |
|---|---|---|
source() | 上下文目录 + 生成代码(合成后) | 读取模块整体源码、遍历文件 |
generatedContextDirectory() | 仅生成的那一层目录 | 查看 SDK 生成了什么、对比生成差异 |
四、依赖查询:dependencies()
dependencies(): Promise<Module_[]>返回当前模块的全部依赖模块,每个元素是Module_对象数组。引擎侧遍历模块的依赖树(core/schema/module.go):
depMods := make([]*core.Module, 0, len(mod.Module.Self().Deps.Mods())) for _, dep := range mod.Module.Self().Deps.Mods() { if depInst := dep.ModuleResult(); depInst.Self() != nil { depMods = append(depMods, depInst.Self()) continue } switch dep.(type) { case *CoreMod: // skip 核心内置模块 default: return nil, fmt.Errorf("unexpected mod dependency type %T", dep) } }从源码可以看到两个实现细节:
- 跳过
CoreMod:Dagger 核心内置模块(如core依赖)不会出现在结果里,返回的是用户声明的模块依赖; - 类型校验:遇到无法识别的依赖类型会直接报错,保证返回结构的一致性。
TypeScript 客户端收到结果后,会用selectNode把每个 ID 重新绑定为Module_对象(api/client.gen.ts),因此你可以继续对每个依赖模块做进一步调用。
五、工作目录访问:workdir() 与 workdirFile()
这是CurrentModule中最"动态"的能力:读取模块执行期间对 scratch 工作目录所做的修改。
workdir(path, opts?)
workdir(path: string, opts?: CurrentModuleWorkdirOpts): DirectoryLoad a directory from the module's scratch working directory, including any changes that may have been made to it during module function execution.
从模块的 scratch 工作目录加载一个目录,包括模块函数执行期间对其做出的任何修改。path参数是相对位置,例如"."表示工作目录根。
CurrentModuleWorkdirOpts的三个可选参数(定义于 api/client.gen.ts):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
exclude | string[] | [] | 排除匹配指定模式的文件/目录,如["node_modules/", ".git*"] |
include | string[] | [] | 仅包含匹配指定模式的文件/目录,如["app/", "package.*"] |
gitignore | boolean | false | 是否在目录内应用.gitignore过滤规则 |
这些过滤模式(exclude/include/gitignore)由引擎透传给host.directory选择器执行(core/schema/module.go),与Directory对象上的过滤语义一致,可以放心复用你熟悉的 glob 模式知识。
引擎侧有一个值得注意的安全校验:
if !filepath.IsLocal(args.Path) { return inst, fmt.Errorf("workdir path %q escapes workdir", args.Path) } args.Path = filepath.Join(sdk.RuntimeWorkdirPath, args.Path)见 core/schema/module.go。它使用filepath.IsLocal拒绝一切可能逃逸出工作目录的路径(如../、绝对路径),然后把合法路径拼接到 SDK 运行时的工作目录前缀下。这意味着workdir()只允许访问工作目录内部,从引擎层面杜绝了路径穿越。
workdirFile(path)
workdirFile(path: string): FileLoad a file from the module's scratch working directory, including any changes that may have been made to it during module function execution.
加载工作目录中的单个文件,同样包含执行期间的修改。path示例:"README.md"。其实现与workdir()共享同一套路径校验逻辑(core/schema/module.go),安全性等价。
典型场景:模块函数在容器内生成产物(如构建出的二进制、渲染出的文档),随后用workdirFile("output.json")或workdir("dist")把结果读取出来交给调用方。
六、实验性能力:generators()
generators(opts?: CurrentModuleGeneratorsOpts): GeneratorGroupReturn all generators defined by the module(返回模块定义的全部生成器)。
该方法在文档与源码中都被标记为Experimental,引擎侧注解为 "This API is highly experimental and may be removed or replaced entirely."(见 core/schema/module.go 与 base_schema.graphqls),因此在正式生产代码中应谨慎使用,API 可能随时变动。
其唯一参数CurrentModuleGeneratorsOpts:
export type CurrentModuleGeneratorsOpts = { /** * Only include generators matching the specified patterns */ include?: string[] }include用于只返回匹配指定模式的生成器,例如按名称模式筛选。方法返回一个GeneratorGroup对象(api/client.gen.ts),可以继续对其调用进行更细粒度的查询。
七、实战:组合使用 CurrentModule
下面把各方法组合起来,展示一个典型的自省型 TypeScript Dagger 模块函数:
import { dag, Directory, Container } from "@dagger.io/dagger" /** * 自省模块自身:打印名称与依赖,并把"源码+生成代码"与 * "运行期间工作目录中的产物"一起返回给调用方。 */ export function introspectSelf(workdir: Directory): Container { const cur = dag.currentModule() // 1) 身份:模块名(惰性求值,await 时才真正查询) const moduleName = await cur.name() // 2) 依赖:获取模块声明的全部依赖(核心内置模块会被跳过) const deps = await cur.dependencies() console.log(`module=${moduleName}, deps=${deps.length}`) // 3) 源码视图:上下文目录 + 生成代码的合成结果 const sourceWithGenerated = cur.source() // 4) 生成层:只查看 SDK 生成的那一层 const generatedLayer = cur.generatedContextDirectory() // 5) 工作目录:读取函数执行期间产生的文件,支持过滤 const buildOutput = cur.workdir("out", { exclude: ["node_modules/", ".git*"], gitignore: true, }) const manifest = cur.workdirFile("out/manifest.json") // ... 继续用 sourceWithGenerated / buildOutput / manifest 组装容器等 return dag.container() }要点回顾:
name()与dependencies()返回 Promise,需要await;source()、workdir()等返回惰性对象,可先构建查询再统一执行;workdir()只允许工作目录内部路径,../或绝对路径会被引擎拒绝;- 需要同时拿到"源码"与"生成代码"时,直接使用
source();只想检查生成内容时用generatedContextDirectory()。
八、结语
CurrentModule是 Dagger 模块运行时自省的入口:name()/id()回答"我是谁",dependencies()回答"我依赖谁",source()/generatedContextDirectory()回答"我的代码在哪",workdir()/workdirFile()回答"我运行时改了什么"。通过文档、TypeScript 客户端源码(api/client.gen.ts)与引擎实现(core/schema/module.go)三者的对照,你可以准确掌握每个方法的语义与边界,写出更健壮、更动态的 Dagger 模块。
【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考