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 } 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.