From a7325cc38d9c97c0e71b3a1936121e9ea0994c8a Mon Sep 17 00:00:00 2001 From: Vasek - Tom C Date: Tue, 11 Aug 2026 15:08:43 +0200 Subject: [PATCH 1/2] fix(codegen): sync the generator with the engine it targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generating a fixture both ways and diffing turned up three places where our generator — ported with the client codegen, from an older upstream commit than the engine we pin — produces different output from the engine's: - Enum members were named with strcase.ToCamel, which mangles consecutive capitals: EStarGZ became EstarGz where the engine emits EStarGz. Module code written against the engine's bindings would not compile against ours. Upstream had already fixed this by routing formatEnum through the same PascalCase rule as the rest of the generator; the corrected helper was sitting unused in our copy. - `arguments` was missing from the JS keyword list, so a field of that name came out unescaped instead of as `arguments_`. - The entrypoint emitted no source maps. The scan reports a location for every declaration and our typedef structs dropped all but the object's on the floor; the renderer had no sourceMapExpr at all. Taken from the beta.9 tag along with the register template that calls it — our copies had no local changes to preserve, unlike the binding templates, which carry the client work developed here. Regenerating the library bindings and rebuilding the bundle propagates the enum fix into core.d.ts, which had inherited the old casing. With this, every generated file matches the engine's byte-for-byte for the same fixture, except core.js — ours is built from the vendored lockfile and is 1.1MB smaller than the engine's, which builds without one. Signed-off-by: Tom Chauveau --- .../templates/entrypoint_functions.go | 90 ++++++++++++++++++- .../templates/entrypoint_typedef.go | 48 +++++----- .../typescript/templates/functions.go | 33 ++++--- .../templates/src/entrypoint/register.ts.gtpl | 20 ++--- .../type_test_enum_with_directive_want.ts | 8 +- .../testdata/entrypoint_smoke_want.ts | 16 ++-- library/bundle/core.d.ts | 6 +- library/bundle/core.js | 10 +-- library/bundle/introspector.js | 4 +- library/src/api/client.gen.ts | 12 +-- 10 files changed, 168 insertions(+), 79 deletions(-) diff --git a/helpers/codegen/generator/typescript/templates/entrypoint_functions.go b/helpers/codegen/generator/typescript/templates/entrypoint_functions.go index c7ba731..a0e3fa5 100644 --- a/helpers/codegen/generator/typescript/templates/entrypoint_functions.go +++ b/helpers/codegen/generator/typescript/templates/entrypoint_functions.go @@ -24,6 +24,11 @@ func EntrypointTemplateFuncs(module *TypedefModule, opts EntrypointOptions) temp "renderTypeDef": c.renderTypeDef, "renderArgCall": c.renderArgCall, "renderFunctionExpr": c.renderFunctionExpr, + "renderObjectDef": c.renderObjectDef, + "renderFieldCall": c.renderFieldCall, + "renderEnumDef": c.renderEnumDef, + "renderEnumMemberCall": c.renderEnumMemberCall, + "renderInterfaceDef": c.renderInterfaceDef, "classRuntimeRef": c.classRuntimeRef, "classTypeRef": c.classTypeRef, "isExportedClass": c.isExportedClass, @@ -241,7 +246,9 @@ func (c *entrypointFuncCtx) renderArgCall(arg *TypedefArgument) string { if arg.IsOptional { td += ".withOptional(true)" } - if hasDefault(arg) { + // An explicit `null` default must still be registered as the arg's default + // (unlike the coercion path's hasDefault, which excludes null on purpose). + if len(arg.DefaultValue) > 0 { dv, ok := c.resolveDefaultValue(arg) if !ok { if !arg.IsOptional { @@ -252,9 +259,87 @@ func (c *entrypointFuncCtx) renderArgCall(arg *TypedefArgument) string { opts["defaultValue"] = fmt.Sprintf("JSON.stringify(%s) as string & { __JSON: never }", string(b)) } } + if sm := sourceMapExpr(arg.Location); sm != "" { + opts["sourceMap"] = sm + } return fmt.Sprintf(".withArg(%s, %s%s)", jsString(arg.Name), td, optsLit(opts)) } +// sourceMapExpr renders a `dag.sourceMap(path, line, col)` expression for a +// location captured at codegen time, or "" when absent. The static entrypoint +// replays these baked values at runtime so no-codegen modules keep the same +// source-map comments in dependents' bindings as codegen-at-runtime modules. +func sourceMapExpr(loc *TypedefLocation) string { + if loc == nil { + return "" + } + return fmt.Sprintf("dag.sourceMap(%s, %d, %d)", jsString(loc.Filepath), loc.Line, loc.Column) +} + +func (c *entrypointFuncCtx) renderObjectDef(obj *TypedefObject) string { + opts := map[string]string{} + if obj.Description != "" { + opts["description"] = jsString(obj.Description) + } + if obj.Deprecated != "" { + opts["deprecated"] = jsString(obj.Deprecated) + } + if sm := sourceMapExpr(obj.Location); sm != "" { + opts["sourceMap"] = sm + } + return fmt.Sprintf("dag.typeDef().withObject(%s%s)", jsString(obj.Name), optsLit(opts)) +} + +func (c *entrypointFuncCtx) renderFieldCall(prop *TypedefProperty) string { + opts := map[string]string{} + if prop.Description != "" { + opts["description"] = jsString(prop.Description) + } + if prop.Deprecated != "" { + opts["deprecated"] = jsString(prop.Deprecated) + } + if sm := sourceMapExpr(prop.Location); sm != "" { + opts["sourceMap"] = sm + } + return fmt.Sprintf(".withField(%s, %s%s)", jsString(propFieldName(prop)), c.renderTypeDef(prop.Type), optsLit(opts)) +} + +func (c *entrypointFuncCtx) renderEnumDef(e *TypedefEnum) string { + opts := map[string]string{} + if e.Description != "" { + opts["description"] = jsString(e.Description) + } + if sm := sourceMapExpr(e.Location); sm != "" { + opts["sourceMap"] = sm + } + return fmt.Sprintf("dag.typeDef().withEnum(%s%s)", jsString(e.Name), optsLit(opts)) +} + +func (c *entrypointFuncCtx) renderEnumMemberCall(v *TypedefEnumValue) string { + opts := map[string]string{"value": jsString(v.Value)} + if v.Description != "" { + opts["description"] = jsString(v.Description) + } + if v.Deprecated != "" { + opts["deprecated"] = jsString(v.Deprecated) + } + if sm := sourceMapExpr(v.Location); sm != "" { + opts["sourceMap"] = sm + } + return fmt.Sprintf(".withEnumMember(%s%s)", jsString(v.Name), optsLit(opts)) +} + +func (c *entrypointFuncCtx) renderInterfaceDef(iface *TypedefInterface) string { + opts := map[string]string{} + if iface.Description != "" { + opts["description"] = jsString(iface.Description) + } + if sm := sourceMapExpr(iface.Location); sm != "" { + opts["sourceMap"] = sm + } + return fmt.Sprintf("dag.typeDef().withInterface(%s%s)", jsString(iface.Name), optsLit(opts)) +} + func (c *entrypointFuncCtx) resolveDefaultValue(arg *TypedefArgument) (any, bool) { if !isPrimitive(arg.Type) { return nil, false @@ -288,6 +373,9 @@ func (c *entrypointFuncCtx) renderFunctionExpr(fn *TypedefFunction) string { if fn.Description != "" { parts = append(parts, fmt.Sprintf(".withDescription(%s)", jsString(fn.Description))) } + if sm := sourceMapExpr(fn.Location); sm != "" { + parts = append(parts, fmt.Sprintf(".withSourceMap(%s)", sm)) + } for _, arg := range fn.Arguments { parts = append(parts, c.renderArgCall(arg)) } diff --git a/helpers/codegen/generator/typescript/templates/entrypoint_typedef.go b/helpers/codegen/generator/typescript/templates/entrypoint_typedef.go index 4145b7e..8ece612 100644 --- a/helpers/codegen/generator/typescript/templates/entrypoint_typedef.go +++ b/helpers/codegen/generator/typescript/templates/entrypoint_typedef.go @@ -43,49 +43,55 @@ type TypedefFunction struct { IsCheck bool `json:"isCheck"` IsGenerator bool `json:"isGenerator"` IsUp bool `json:"isUp"` + Location *TypedefLocation `json:"location,omitempty"` ReturnType *TypedefType `json:"returnType,omitempty"` Arguments []*TypedefArgument `json:"arguments"` } type TypedefArgument struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Deprecated string `json:"deprecated,omitempty"` - Type *TypedefType `json:"type,omitempty"` - IsVariadic bool `json:"isVariadic"` - IsNullable bool `json:"isNullable"` - IsOptional bool `json:"isOptional"` - DefaultValue json.RawMessage `json:"defaultValue,omitempty"` - DefaultPath string `json:"defaultPath,omitempty"` - DefaultAddress string `json:"defaultAddress,omitempty"` - Ignore []string `json:"ignore,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Deprecated string `json:"deprecated,omitempty"` + Type *TypedefType `json:"type,omitempty"` + IsVariadic bool `json:"isVariadic"` + IsNullable bool `json:"isNullable"` + IsOptional bool `json:"isOptional"` + DefaultValue json.RawMessage `json:"defaultValue,omitempty"` + DefaultPath string `json:"defaultPath,omitempty"` + DefaultAddress string `json:"defaultAddress,omitempty"` + Ignore []string `json:"ignore,omitempty"` + Location *TypedefLocation `json:"location,omitempty"` } type TypedefProperty struct { - Name string `json:"name"` - Alias string `json:"alias,omitempty"` - Description string `json:"description,omitempty"` - Deprecated string `json:"deprecated,omitempty"` - IsExposed bool `json:"isExposed"` - Type *TypedefType `json:"type,omitempty"` + Name string `json:"name"` + Alias string `json:"alias,omitempty"` + Description string `json:"description,omitempty"` + Deprecated string `json:"deprecated,omitempty"` + IsExposed bool `json:"isExposed"` + Type *TypedefType `json:"type,omitempty"` + Location *TypedefLocation `json:"location,omitempty"` } type TypedefEnum struct { Name string `json:"name"` Description string `json:"description"` + Location *TypedefLocation `json:"location,omitempty"` Values map[string]*TypedefEnumValue `json:"values"` } type TypedefEnumValue struct { - Name string `json:"name"` - Value string `json:"value"` - Description string `json:"description"` - Deprecated string `json:"deprecated,omitempty"` + Name string `json:"name"` + Value string `json:"value"` + Description string `json:"description"` + Deprecated string `json:"deprecated,omitempty"` + Location *TypedefLocation `json:"location,omitempty"` } type TypedefInterface struct { Name string `json:"name"` Description string `json:"description"` + Location *TypedefLocation `json:"location,omitempty"` Functions map[string]*TypedefFunction `json:"functions"` } diff --git a/helpers/codegen/generator/typescript/templates/functions.go b/helpers/codegen/generator/typescript/templates/functions.go index 6185ae6..889e448 100644 --- a/helpers/codegen/generator/typescript/templates/functions.go +++ b/helpers/codegen/generator/typescript/templates/functions.go @@ -426,19 +426,20 @@ func (funcs typescriptTemplateFuncs) queryToClient(s string) string { // in practice, many of these work just fine as e.g. method // names, like 'export' and 'from'. var jsKeywords = map[string]struct{}{ - "await": {}, - "break": {}, - "case": {}, - "catch": {}, - "class": {}, - "const": {}, - "continue": {}, - "debugger": {}, - "default": {}, - "delete": {}, - "do": {}, - "else": {}, - "enum": {}, + "arguments": {}, + "await": {}, + "break": {}, + "case": {}, + "catch": {}, + "class": {}, + "const": {}, + "continue": {}, + "debugger": {}, + "default": {}, + "delete": {}, + "do": {}, + "else": {}, + "enum": {}, // "export": {}, // containr.export "extends": {}, "false": {}, @@ -492,8 +493,12 @@ var jsKeywords = map[string]struct{}{ } // formatEnum formats a GraphQL enum into a TS equivalent +// formatEnum names an enum member. It uses the same PascalCase rule as the rest +// of the generator rather than strcase.ToCamel, which mangles consecutive +// capitals ("EStarGZ" -> "EstarGz" instead of "EStarGz") and would leave a +// module's bindings incompatible with code written against the engine's. func (funcs typescriptTemplateFuncs) formatEnum(s string) string { - return strcase.ToCamel(s) + return toPascalCase(s) } // isArgOptional checks if some arg are optional. diff --git a/helpers/codegen/generator/typescript/templates/src/entrypoint/register.ts.gtpl b/helpers/codegen/generator/typescript/templates/src/entrypoint/register.ts.gtpl index b8a8bdd..d52cd49 100644 --- a/helpers/codegen/generator/typescript/templates/src/entrypoint/register.ts.gtpl +++ b/helpers/codegen/generator/typescript/templates/src/entrypoint/register.ts.gtpl @@ -7,10 +7,7 @@ async function register(): Promise { {{- end }} {{- range $name := sortedKeysObjects $module.Objects }} {{- $obj := index $module.Objects $name }} - let obj_{{ $obj.Name }} = dag.typeDef().withObject({{ jsString $obj.Name }} - {{- if $obj.Description }}, { description: {{ jsString $obj.Description }}{{ if $obj.Deprecated }}, deprecated: {{ jsString $obj.Deprecated }}{{ end }} } - {{- else if $obj.Deprecated }}, { deprecated: {{ jsString $obj.Deprecated }} } - {{- end }}) + let obj_{{ $obj.Name }} = {{ renderObjectDef $obj }} {{- range $mName := sortedKeysMethods $obj.Methods }} {{- $fn := index $obj.Methods $mName }} obj_{{ $obj.Name }} = obj_{{ $obj.Name }}.withFunction({{ renderFunctionExpr $fn }}) @@ -18,10 +15,7 @@ async function register(): Promise { {{- range $pName := sortedKeysProps $obj.Properties }} {{- $prop := index $obj.Properties $pName }} {{- if $prop.IsExposed }} - obj_{{ $obj.Name }} = obj_{{ $obj.Name }}.withField({{ jsString (propFieldName $prop) }}, {{ renderTypeDef $prop.Type }} - {{- if $prop.Description }}, { description: {{ jsString $prop.Description }}{{ if $prop.Deprecated }}, deprecated: {{ jsString $prop.Deprecated }}{{ end }} } - {{- else if $prop.Deprecated }}, { deprecated: {{ jsString $prop.Deprecated }} } - {{- end }}) + obj_{{ $obj.Name }} = obj_{{ $obj.Name }}{{ renderFieldCall $prop }} {{- end }} {{- end }} {{- if $obj.Constructor }} @@ -32,20 +26,16 @@ async function register(): Promise { {{- end }} {{- range $name := sortedKeysEnums $module.Enums }} {{- $e := index $module.Enums $name }} - let enum_{{ $e.Name }} = dag.typeDef().withEnum({{ jsString $e.Name }} - {{- if $e.Description }}, { description: {{ jsString $e.Description }} }{{ end }}) + let enum_{{ $e.Name }} = {{ renderEnumDef $e }} {{- range $vName := sortedKeysEnumValues $e.Values }} {{- $v := index $e.Values $vName }} - enum_{{ $e.Name }} = enum_{{ $e.Name }}.withEnumMember({{ jsString $v.Name }}, { value: {{ jsString $v.Value }} - {{- if $v.Description }}, description: {{ jsString $v.Description }}{{ end }} - {{- if $v.Deprecated }}, deprecated: {{ jsString $v.Deprecated }}{{ end }} }) + enum_{{ $e.Name }} = enum_{{ $e.Name }}{{ renderEnumMemberCall $v }} {{- end }} mod = mod.withEnum(enum_{{ $e.Name }}) {{- end }} {{- range $name := sortedKeysIfaces $module.Interfaces }} {{- $iface := index $module.Interfaces $name }} - let iface_{{ $iface.Name }} = dag.typeDef().withInterface({{ jsString $iface.Name }} - {{- if $iface.Description }}, { description: {{ jsString $iface.Description }} }{{ end }}) + let iface_{{ $iface.Name }} = {{ renderInterfaceDef $iface }} {{- range $fnName := sortedKeysMethods $iface.Functions }} {{- $fn := index $iface.Functions $fnName }} iface_{{ $iface.Name }} = iface_{{ $iface.Name }}.withFunction({{ renderFunctionExpr $fn }}) diff --git a/helpers/codegen/generator/typescript/templates/src/testdata/type_test_enum_with_directive_want.ts b/helpers/codegen/generator/typescript/templates/src/testdata/type_test_enum_with_directive_want.ts index 76cbd8c..2cd73f8 100644 --- a/helpers/codegen/generator/typescript/templates/src/testdata/type_test_enum_with_directive_want.ts +++ b/helpers/codegen/generator/typescript/templates/src/testdata/type_test_enum_with_directive_want.ts @@ -3,8 +3,8 @@ * Compression algorithm to use for image layers. */ export enum ImageLayerCompression { - EstarGz = "EStarGZ", - Estargz = ImageLayerCompression.EstarGz, + EStarGz = "EStarGZ", + Estargz = ImageLayerCompression.EStarGz, Gzip = "Gzip", Uncompressed = "Uncompressed", Zstd = "Zstd", @@ -16,7 +16,7 @@ export enum ImageLayerCompression { */ export function ImageLayerCompressionValueToName(value: ImageLayerCompression): string { switch (value) { - case ImageLayerCompression.EstarGz: + case ImageLayerCompression.EStarGz: return "EStarGZ" case ImageLayerCompression.Gzip: return "Gzip" @@ -36,7 +36,7 @@ export function ImageLayerCompressionValueToName(value: ImageLayerCompression): export function ImageLayerCompressionNameToValue(name: string): ImageLayerCompression { switch (name) { case "EStarGZ": - return ImageLayerCompression.EstarGz + return ImageLayerCompression.EStarGz case "Gzip": return ImageLayerCompression.Gzip case "Uncompressed": diff --git a/helpers/codegen/generator/typescript/testdata/entrypoint_smoke_want.ts b/helpers/codegen/generator/typescript/testdata/entrypoint_smoke_want.ts index de5bbf5..b12e3eb 100644 --- a/helpers/codegen/generator/typescript/testdata/entrypoint_smoke_want.ts +++ b/helpers/codegen/generator/typescript/testdata/entrypoint_smoke_want.ts @@ -65,14 +65,14 @@ async function serializeSmoke(__obj: Smoke): Promise { async function register(): Promise { let mod = dag.module_() - let obj_Smoke = dag.typeDef().withObject("Smoke", { description: "A module covering the shapes the entrypoint has to dispatch." }) - obj_Smoke = obj_Smoke.withFunction(dag.function_("ctr", dag.typeDef().withObject("Container"))) - obj_Smoke = obj_Smoke.withFunction(dag.function_("greet", dag.typeDef().withKind(TypeDefKind.StringKind)).withDescription("Greet someone.").withArg("name", dag.typeDef().withKind(TypeDefKind.StringKind)).withArg("loud", dag.typeDef().withKind(TypeDefKind.BooleanKind).withOptional(true))) - obj_Smoke = obj_Smoke.withFunction(dag.function_("nothing", dag.typeDef().withKind(TypeDefKind.VoidKind).withOptional(true))) - obj_Smoke = obj_Smoke.withFunction(dag.function_("read", dag.typeDef().withKind(TypeDefKind.StringKind)).withArg("path", dag.typeDef().withKind(TypeDefKind.StringKind))) - obj_Smoke = obj_Smoke.withField("greeting", dag.typeDef().withKind(TypeDefKind.StringKind)) - obj_Smoke = obj_Smoke.withField("source", dag.typeDef().withObject("Directory"), { description: "Where the source lives." }) - obj_Smoke = obj_Smoke.withConstructor(dag.function_("", obj_Smoke).withArg("source", dag.typeDef().withObject("Directory")).withArg("greeting", dag.typeDef().withKind(TypeDefKind.StringKind), { defaultValue: JSON.stringify("hello") as string & { __JSON: never } })) + let obj_Smoke = dag.typeDef().withObject("Smoke", { description: "A module covering the shapes the entrypoint has to dispatch.", sourceMap: dag.sourceMap("src/index.ts", 7, 14) }) + obj_Smoke = obj_Smoke.withFunction(dag.function_("ctr", dag.typeDef().withObject("Container")).withSourceMap(dag.sourceMap("src/index.ts", 31, 3))) + obj_Smoke = obj_Smoke.withFunction(dag.function_("greet", dag.typeDef().withKind(TypeDefKind.StringKind)).withDescription("Greet someone.").withSourceMap(dag.sourceMap("src/index.ts", 26, 3)).withArg("name", dag.typeDef().withKind(TypeDefKind.StringKind), { sourceMap: dag.sourceMap("src/index.ts", 26, 9) }).withArg("loud", dag.typeDef().withKind(TypeDefKind.BooleanKind).withOptional(true), { sourceMap: dag.sourceMap("src/index.ts", 26, 23) })) + obj_Smoke = obj_Smoke.withFunction(dag.function_("nothing", dag.typeDef().withKind(TypeDefKind.VoidKind).withOptional(true)).withSourceMap(dag.sourceMap("src/index.ts", 41, 3))) + obj_Smoke = obj_Smoke.withFunction(dag.function_("read", dag.typeDef().withKind(TypeDefKind.StringKind)).withSourceMap(dag.sourceMap("src/index.ts", 36, 9)).withArg("path", dag.typeDef().withKind(TypeDefKind.StringKind), { sourceMap: dag.sourceMap("src/index.ts", 36, 14) })) + obj_Smoke = obj_Smoke.withField("greeting", dag.typeDef().withKind(TypeDefKind.StringKind), { sourceMap: dag.sourceMap("src/index.ts", 15, 3) }) + obj_Smoke = obj_Smoke.withField("source", dag.typeDef().withObject("Directory"), { description: "Where the source lives.", sourceMap: dag.sourceMap("src/index.ts", 12, 3) }) + obj_Smoke = obj_Smoke.withConstructor(dag.function_("", obj_Smoke).withArg("source", dag.typeDef().withObject("Directory"), { sourceMap: dag.sourceMap("src/index.ts", 17, 15) }).withArg("greeting", dag.typeDef().withKind(TypeDefKind.StringKind), { defaultValue: JSON.stringify("hello") as string & { __JSON: never }, sourceMap: dag.sourceMap("src/index.ts", 17, 34) })) mod = mod.withObject(obj_Smoke) return await mod.id() } diff --git a/library/bundle/core.d.ts b/library/bundle/core.d.ts index e1cd1c0..36d9e1a 100644 --- a/library/bundle/core.d.ts +++ b/library/bundle/core.d.ts @@ -1523,7 +1523,7 @@ type ID = string & { * Compression algorithm to use for image layers. */ declare enum ImageLayerCompression { - EstarGz = "EStarGZ", + EStarGz = "EStarGZ", Estargz = "EStarGZ", Gzip = "Gzip", Uncompressed = "Uncompressed", @@ -1546,7 +1546,7 @@ declare enum ImageMediaTypes { Docker = "DockerMediaTypes", DockerMediaTypes = "DockerMediaTypes", Oci = "OCIMediaTypes", - OcimediaTypes = "OCIMediaTypes" + OciMediaTypes = "OCIMediaTypes" } /** * Utility function to convert a ImageMediaTypes value to its name so @@ -6294,7 +6294,7 @@ declare class LLMContentBlock extends BaseClient { /** * The arguments passed to the tool, JSON-encoded (for TOOL_CALL kind). */ - arguments: () => Promise; + arguments_: () => Promise; /** * The unique ID of a tool call (for TOOL_CALL or TOOL_RESULT kinds). */ diff --git a/library/bundle/core.js b/library/bundle/core.js index 125f279..308ea72 100644 --- a/library/bundle/core.js +++ b/library/bundle/core.js @@ -99993,7 +99993,7 @@ function FunctionCachePolicyNameToValue(name) { } var ImageLayerCompression; ((ImageLayerCompression2) => { - ImageLayerCompression2["EstarGz"] = "EStarGZ"; + ImageLayerCompression2["EStarGz"] = "EStarGZ"; ImageLayerCompression2["Estargz"] = "EStarGZ"; ImageLayerCompression2["Gzip"] = "Gzip"; ImageLayerCompression2["Uncompressed"] = "Uncompressed"; @@ -100001,7 +100001,7 @@ var ImageLayerCompression; })(ImageLayerCompression ||= {}); function ImageLayerCompressionValueToName(value) { switch (value) { - case "EStarGZ" /* EstarGz */: + case "EStarGZ" /* EStarGz */: return "EStarGZ"; case "Gzip" /* Gzip */: return "Gzip"; @@ -100016,7 +100016,7 @@ function ImageLayerCompressionValueToName(value) { function ImageLayerCompressionNameToValue(name) { switch (name) { case "EStarGZ": - return "EStarGZ" /* EstarGz */; + return "EStarGZ" /* EStarGz */; case "Gzip": return "Gzip" /* Gzip */; case "Uncompressed": @@ -100032,7 +100032,7 @@ var ImageMediaTypes; ImageMediaTypes2["Docker"] = "DockerMediaTypes"; ImageMediaTypes2["DockerMediaTypes"] = "DockerMediaTypes"; ImageMediaTypes2["Oci"] = "OCIMediaTypes"; - ImageMediaTypes2["OcimediaTypes"] = "OCIMediaTypes"; + ImageMediaTypes2["OciMediaTypes"] = "OCIMediaTypes"; })(ImageMediaTypes ||= {}); function ImageMediaTypesValueToName(value) { switch (value) { @@ -104128,7 +104128,7 @@ class LLMContentBlock extends BaseClient { const response = await ctx.execute(); return response; }; - arguments = async () => { + arguments_ = async () => { if (this._arguments) { return this._arguments; } diff --git a/library/bundle/introspector.js b/library/bundle/introspector.js index c2b9730..563831a 100644 --- a/library/bundle/introspector.js +++ b/library/bundle/introspector.js @@ -99585,7 +99585,7 @@ function FunctionCachePolicyValueToName(value) { } function ImageLayerCompressionValueToName(value) { switch (value) { - case "EStarGZ" /* EstarGz */: + case "EStarGZ" /* EStarGz */: return "EStarGZ"; case "Gzip" /* Gzip */: return "Gzip"; @@ -103506,7 +103506,7 @@ class LLMContentBlock extends BaseClient { const response = await ctx.execute(); return response; }; - arguments = async () => { + arguments_ = async () => { if (this._arguments) { return this._arguments; } diff --git a/library/src/api/client.gen.ts b/library/src/api/client.gen.ts index 7449b1f..778c640 100644 --- a/library/src/api/client.gen.ts +++ b/library/src/api/client.gen.ts @@ -1867,8 +1867,8 @@ export type ID = string & {__ID: never} * Compression algorithm to use for image layers. */ export enum ImageLayerCompression { - EstarGz = "EStarGZ", - Estargz = ImageLayerCompression.EstarGz, + EStarGz = "EStarGZ", + Estargz = ImageLayerCompression.EStarGz, Gzip = "Gzip", Uncompressed = "Uncompressed", Zstd = "Zstd", @@ -1880,7 +1880,7 @@ export enum ImageLayerCompression { */ export function ImageLayerCompressionValueToName(value: ImageLayerCompression): string { switch (value) { - case ImageLayerCompression.EstarGz: + case ImageLayerCompression.EStarGz: return "EStarGZ" case ImageLayerCompression.Gzip: return "Gzip" @@ -1900,7 +1900,7 @@ export function ImageLayerCompressionValueToName(value: ImageLayerCompression): export function ImageLayerCompressionNameToValue(name: string): ImageLayerCompression { switch (name) { case "EStarGZ": - return ImageLayerCompression.EstarGz + return ImageLayerCompression.EStarGz case "Gzip": return ImageLayerCompression.Gzip case "Uncompressed": @@ -1918,7 +1918,7 @@ export enum ImageMediaTypes { Docker = "DockerMediaTypes", DockerMediaTypes = ImageMediaTypes.Docker, Oci = "OCIMediaTypes", - OcimediaTypes = ImageMediaTypes.Oci, + OciMediaTypes = ImageMediaTypes.Oci, } /** @@ -13668,7 +13668,7 @@ export class LLMContentBlock extends BaseClient { /** * The arguments passed to the tool, JSON-encoded (for TOOL_CALL kind). */ - arguments = async (): Promise => { + arguments_ = async (): Promise => { if (this._arguments) { return this._arguments } From 63b2b957b814cc27aaa1dbd59ace5f30a32d5cb9 Mon Sep 17 00:00:00 2001 From: Vasek - Tom C Date: Tue, 11 Aug 2026 15:14:35 +0200 Subject: [PATCH 2/2] feat(module): generate a module's files in the SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assemble a module's generated tree here rather than routing through the engine's built-in TypeScript runtime: bindings from the module-facing schema, the sdk/ directory the runtime mounts as @dagger.io/dagger, the dispatch entrypoint, and the config files. The schema is ModuleSource.introspectionSchemaJSON — the same builder call the engine makes before handing the file to Codegen, so this is the same input, not an approximation. The scan runs with the compiler installed beside the introspector rather than in the module tree: a bare import resolves from the importing file's directory, so the module's own node_modules is not somewhere the scanner would look. Exposed as generateModuleLocal and left unwired: Mod.generate still delegates to the engine while the two are compared. Verified for the generate/app fixture by generating it both ways — every file matches byte-for-byte except core.js (§ the lockfile difference) — and by generating the generate-deps fixtures and calling a function on the result, which exercises the whole tree, dependency bindings included, through the engine runtime. ModConfig.runtime becomes public: codegen needs to know which config files a module wants, and the detection already lived there. Signed-off-by: Tom Chauveau --- mod-config.dang | 2 +- typescript-sdk.dang | 174 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/mod-config.dang b/mod-config.dang index afc2591..bc7ce14 100644 --- a/mod-config.dang +++ b/mod-config.dang @@ -132,7 +132,7 @@ type ModConfig { newer Bun writes the text bun.lock, but pre-existing modules may still carry the binary bun.lockb. """ - let runtime: Runtime! { + runtime: Runtime! { if (hasFile(denoJsonPath)) { if (hasFile(packageJsonPath)) { # Contradictory layout: deno.json selects the Deno runtime, but diff --git a/typescript-sdk.dang b/typescript-sdk.dang index f48c9d1..12ca451 100644 --- a/typescript-sdk.dang +++ b/typescript-sdk.dang @@ -449,6 +449,180 @@ type TypescriptSdk { .withExec(["go", "build", "-o", "/usr/local/bin/config-updator", "."]) } + """ + The prebuilt TypeScript library this SDK ships: core.js and its declarations, + the module-facing index.ts/telemetry.ts wrappers, the module introspector, and + the compiler version the introspector must be run with. + + Built from the vendored library sources by .dagger/modules/packager and + committed, so generating a module needs no bun toolchain and no network. + """ + let tsBundle: Directory! { + currentModule.source.directory("library/bundle") + } + + """ + Generate a module's own bindings (client.gen.ts plus one .gen.ts per + dependency) from its module-facing introspection schema. + + Engine-free: the schema arrives as data, so this is a plain container exec. + """ + let generateModuleBindings(schemaJSON: String!, moduleName: String!): Directory! { + codegenBuilder + .withNewFile("/schema.json", schemaJSON) + .withExec([ + "codegen", "module", + "--introspection-json-path", "/schema.json", + "--module-name", moduleName, + "--output", "/out", + ]) + .directory("/out") + } + + """ + Assemble a module's sdk/ directory: the shipped bundle plus the module's + generated bindings. + + This directory is what the engine runtime mounts as + node_modules/@dagger.io/dagger, so its layout is the runtime's contract, not + ours: index.ts re-exports the decorators from core.js and `export *`s + client.gen.ts, which has to sit beside it. + """ + let moduleSdkDirectory(bindings: Directory!): Directory! { + directory + .withFile("core.js", tsBundle.file("core.js")) + .withFile("core.d.ts", tsBundle.file("core.d.ts")) + .withFile("index.ts", tsBundle.file("index.ts")) + .withFile("telemetry.ts", tsBundle.file("telemetry.ts")) + .withDirectory(".", bindings) + } + + """ + Render a module's static dispatch entrypoint. + + Two steps: the introspector scans the module's own TypeScript into a typedef + JSON, then codegen renders the dispatcher from it. The scan is the only part + of module generation that reads the user's source rather than the schema — + the dispatcher imports each @object class from the file it was declared in, + which only the scan knows. + + The compiler is installed beside the introspector rather than in the module + tree: a bare import resolves from the importing file's directory, so putting + it next to the module would leave the scanner unable to find it. + """ + let generateModuleEntrypoint(moduleName: String!, source: Directory!, sdk: Directory!): File! { + let tsVersion = tsBundle.file("typescript-version.txt").contents.trimSuffix("\n") + + let typedef = container + .from("oven/bun:1.3.0-alpine@sha256:37e6b1cbe053939bccf6ae4507977ed957eaa6e7f275670b72ad6348e0d2c11f") + .withoutEntrypoint + .withMountedCache("/root/.bun/install/cache", cacheVolume("bun-install")) + .withWorkdir("/introspector") + .withFile("/introspector/introspector.js", tsBundle.file("introspector.js")) + .withExec(["bun", "add", "--no-save", "typescript@" + tsVersion]) + .withWorkdir("/work") + .withDirectory("/work/src", source) + .withDirectory("/work/sdk", sdk) + .withEnvVariable("EMIT_TYPEDEF_JSON_FILE", "/work/typedef.json") + .withExec(["bun", "/introspector/introspector.js", moduleName, "src", "sdk/client.gen.ts"]) + .file("/work/typedef.json") + .contents + + codegenBuilder + .withNewFile("/typedef.json", typedef) + .withExec([ + "codegen", "entrypoint", + "--typedef-json-path", "/typedef.json", + "--output", "/out", + "--module-root", "/work", + "--source-dir", "src", + ]) + .file("/out/__dagger.entrypoint.ts") + } + + """ + Build the complete set of generated files for a module: its sdk/ directory, + the dispatch entrypoint, and the config the runtime reads. + + `existing` supplies the module's current config files so user settings + survive; `runtime` selects which config files are written. The result is + rooted at the module's source directory. + """ + let moduleDirectory( + schemaJSON: String!, + moduleName: String!, + source: Directory!, + existing: Directory!, + runtime: Runtime!, + ): Directory! { + let sdk = moduleSdkDirectory(generateModuleBindings(schemaJSON, moduleName)) + let entrypoint = generateModuleEntrypoint(moduleName, source, sdk) + + let configUpdator = configUpdatorBuilder + .withDirectory("/existing", existing) + .withDirectory("/out", directory) + + let config = if (runtime == Runtime.DENO) { + configUpdator + .withExec(["config-updator", "deno-config", "/existing/deno.json", "/out/deno.json"]) + .directory("/out") + } else { + configUpdator + .withExec(["config-updator", "package-json", "/existing/package.json", "/out/package.json"]) + .withExec(["config-updator", "tsconfig", "/existing/tsconfig.json", "/out/tsconfig.json"]) + .directory("/out") + } + + directory + .withDirectory("sdk", sdk) + .withFile("__dagger.entrypoint.ts", entrypoint) + .withDirectory(".", config) + } + + """ + Read a module's current config files so regeneration preserves user settings + rather than resetting them. Filtered from the workspace root so a module + without any of them simply yields an empty directory. + """ + let existingModuleConfig(ws: Workspace!, sourcePath: String!): Directory! { + let prefix = if (sourcePath == ".") { "" } else { sourcePath + "/" } + let filtered = ws.directory("/", include: [ + prefix + "package.json", + prefix + "tsconfig.json", + prefix + "deno.json", + ]) + if (sourcePath == ".") { + filtered + } else if (filtered.exists(sourcePath)) { + filtered.directory(sourcePath) + } else { + directory + } + } + + """ + Generate a module's SDK files in this SDK rather than through the engine's + built-in TypeScript runtime. + + Staged behind `Mod.generate` while the two are compared: see the + generate:local-matches-engine check, which generates a fixture both ways and + diffs the result. + """ + generateModuleLocal(ws: Workspace!, path: String!): Changeset! { + let mod = mod(ws, path: path, findUp: false) + let modSrc = polyfill.workspace(ws).moduleSource("/" + mod.rootPath).core + let sourcePath = mod.sourcePath + let sourcePrefix = if (sourcePath == ".") { "" } else { sourcePath + "/" } + + polyfill.workspace(ws).fork.withDirectory(sourcePath, moduleDirectory( + modSrc.introspectionSchemaJSON.contents, + modSrc.moduleOriginalName, + ws.directory("/" + sourcePrefix + "src"), + existingModuleConfig(ws, sourcePath), + mod.config.runtime, + )).changes + } + """ Generate the raw client bindings (dagger.gen.ts plus one .gen.ts per module) from an introspection schema.