Skip to content

feat: move TypeScript client codegen into the SDK module#9

Draft
TomChv wants to merge 7 commits into
mainfrom
feat/extract-client-codegen-in-typescript-sdk
Draft

feat: move TypeScript client codegen into the SDK module#9
TomChv wants to merge 7 commits into
mainfrom
feat/extract-client-codegen-in-typescript-sdk

Conversation

@TomChv

@TomChv TomChv commented Jul 6, 2026

Copy link
Copy Markdown
Member

Move TypeScript client codegen into the SDK module

Moves TypeScript client generation out of dagger/dagger's engine runtime and into this .dang SDK module. The module now generates a typed client for a bound module itself — receiving the schema + module metadata from the engine as plain data and running codegen in an ordinary container, with no nested engine session.

Important

Depends on two engine PRs, both gated v1.0.0-0:

This PR requires an engine that ships both.

What's in it

helpers/codegen — the TypeScript client generator ported from dagger/dagger's cmd/codegen as a standalone, engine-free Go module (no dagger.io/dagger dependency). Reads a pre-computed introspection schema + a meta.json (module name, engine version, bound module) and emits the client bindings. The upstream live-engine query is replaced by the meta.json input. Upstream golden tests ported to guard fidelity.

helpers/config-updator — new client config writers producing a scoped npm package: package.json (@dagger.io/<module>-client name, @dagger.io/dagger pinned to the engine version, type:module, typescript), plus tsconfig.json / deno.json.

typescript-sdk.dang — the dang wiring:

  • generateClient(ws, module, path) — the workspace analogue of mod(ws, path).generate(ws).
  • generateAllClient(ws) @generate — regenerates every registered client (currentModule.asSDK.clients), resolving each bound module via client.moduleSource and generating from clientSchemaIntrospectionJSON.
  • Renamed generateAllgenerateAllModule for symmetry.
  • codegenBuilder / configUpdatorBuilder container builders (compiled like the existing helpers).

e2egenerateClientCheck, generateAllClientCheck, generateClientRespectsExistingCheck, generateClientCompilesCheck, and helperTestsCheck (runs the Go helper tests inside dagger check), plus dependency-bearing fixtures (client/appclient/dep).

Client output layout

A generated client is a scoped package:

  • dagger.gen.ts — core Dagger types (named to match the Go SDK's dagger.gen.go)
  • <module>.gen.ts — one per module: the bound module (e.g. hello.gen.ts) and each dependency; each carries its own types plus the prototype augmentations it contributes to Query/Client/Binding/Env.

Key design decisions

  • Always Remote / scoped package — the client depends on the npm @dagger.io/dagger; no TypeScript library bundle is vendored here.
  • Serve the one bound module — the client schema is core + the bound module only, so the generated serveBoundModule bootstrap serves exactly that module (no dependency loop, no includeDependencies). A LOCAL_SOURCE/DIR_SOURCE module is resolved against the workspace by its workspace-root-relative path (dag.currentWorkspace().moduleSource(path)) — cwd-independent, so it works anywhere in the project tree; a GIT_SOURCE module serves from its canonical ref + pin (dag.moduleSource(ref, { refPin })), which resolves from anywhere. A local-bound client shipped away from the project tree is unservable by design (remote-only) — see hack/designs/generated-client-module-loading.md.

Full rationale in design/client-gen.md.

Testing

  • Go unit + golden tests: go test ./... in each helper (also run inside dagger check via helperTestsCheck), including a golden test for the serveBoundModule bootstrap (local path vs. git ref + pin).
  • e2e checks green on the #13646 dev engine, except generateClientCompilesCheck, which additionally requires #13663: it type-checks the generated dag.currentWorkspace().moduleSource(...) against the engine's Workspace.moduleSource, so it only passes once the engine ships that field.

Known limitations / follow-ups

  • @dagger.io/dagger version on a dev/unreleased engine isn't on npm. generateClientCompilesCheck builds it from a pinned dagger/dagger commit; the client's package.json respects an existing local ./sdk ref (see design/client-bundle.md).
  • Deno client config exists in config-updator but isn't wired into clientDirectory yet.

Add plumbing to handle client generation directly inside the typescript-sdk module.
Move the client codegen binary as a helper of this codegen.

Signed-off-by: Vasek - Tom C <tom@dagger.io>
Add the workspace-oriented client generators and validate them end-to-end
against a v1.0.0-beta.6 engine:

- generateClient(ws, module, path) and generateAllClient(ws) @generate, plus
  the codegen/config-updator container builders and the clientDirectory helper.
- Serve the one bound module, not its dependencies: a client binds exactly one
  module, so serveModuleDependencies serves that module (its closure comes along
  via includeDependencies). GIT_SOURCE embeds source+refPin (portable);
  LOCAL_SOURCE/DIR_SOURCE take the workspace-only guarded serve.
- Allow DIR_SOURCE in the codegen guard: workspace-local modules resolve to
  DIR_SOURCE at runtime, so fail-closed was wrong.
- e2e: dependency-bearing fixtures (client/app -> client/dep), client checks
  asserting client.gen.ts + <dep>.gen.ts + package.json + tsconfig.json, and
  helperTestsCheck so `dagger check` also runs the Go helper tests.

Two beta.6 workarounds remain until the engine ships the phase-1 primitives:
schema uses module-facing introspectionSchemaJSON and resolution uses the
polyfill. Remote (GIT_SOURCE) bound-module introspection is blocked on a
requester-session issue; see design/client-gen.md.

Signed-off-by: Tom Chauveau <tom@dagger.io>
@TomChv TomChv force-pushed the feat/extract-client-codegen-in-typescript-sdk branch from daae756 to 71b3bbb Compare July 15, 2026 14:31
Wire client generation to the phase-1 engine primitives and verify end-to-end
against the dev engine (dagger-dev2, v1.0.0):

- Generate from ModuleSource.clientSchemaIntrospectionJSON (the client-facing
  schema) instead of the module-facing introspectionSchemaJSON workaround.
- Resolve each registered client's bound module through
  CurrentModuleAsSDKClient.moduleSource (honors pin/remote refs) instead of
  polyfill path resolution.

Dang consumption details: iterate the client list with a nested projection
`.{{ path, moduleSource.{{ ... }} }}`. A projected object field is a narrowed
record, not a live handle, and the schema File can't be projected; so select
`clientSchemaIntrospectionJSON.{{ contents }}` and write it in the codegen
container via withNewFile. clientDirectory now takes the schema contents +
provenance pieces rather than a ModuleSource handle.

All 14 checks green on dagger-dev2.

Signed-off-by: Tom Chauveau <tom@dagger.io>
@TomChv TomChv force-pushed the feat/extract-client-codegen-in-typescript-sdk branch from 71b3bbb to 8d31ef1 Compare July 15, 2026 14:44
A standalone client now generates:
  - dagger.gen.ts   — core Dagger types only (named to match the Go SDK's
    dagger.gen.go)
  - <module>.gen.ts — one per module, including the bound module (e.g. hello)
    and its dependencies; each carries its own types plus the prototype
    augmentations it contributes to the extendable types (Client/Binding/Env).

Previously the bound module's types were folded into client.gen.ts alongside
core; only dependencies were split. client.gen.ts is no longer produced for a
client. Module codegen is unchanged (keeps its own types in client.gen.ts).

- generator: for ClientConfig, split all modules (schema.DependencyNames) and
  render the core file as dagger.gen.ts.
- templates: per-module files import the core file by name via a new CoreFile
  func ("dagger.gen" for clients, "client.gen" for module codegen), so module
  golden tests are unaffected.
- e2e: client checks assert dagger.gen.ts + client-app.gen.ts + client-dep.gen.ts.

Verified: Go golden tests + all 14 checks green on the dev engine (dagger-dev2).

Signed-off-by: Tom Chauveau <tom@dagger.io>
@TomChv

TomChv commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

PR needs dagger/dagger#13646 merged and released in order to load

TomChv added 3 commits July 15, 2026 19:03
Regenerating a client no longer clobbers the user's package.json:

- Generation now reads the client dir's current config (package.json,
  tsconfig.json, deno.json) instead of starting from an empty directory, so
  config-updator sees what the user already has.
- config-updator preserves a @dagger.io/dagger dependency the user has pointed
  at a local ref (./sdk, file:, link:, workspace:, git/URL) instead of
  overwriting it with the engine-version pin — so a dev/unreleased engine can
  use a local bundle. A plain version pin is still refreshed to track the engine.
- Client config files are written pretty-printed (2-space indent, key order
  preserved) rather than a single minified line. Module config edits keep
  sjson's in-place format.

e2e: generateClientRespectsExistingCheck binds a client dir that already pins
@dagger.io/dagger to ./sdk and asserts it survives regeneration, pretty-printed.

Verified: 15/15 checks green on the dev engine (dagger-dev2).

Signed-off-by: Tom Chauveau <tom@dagger.io>
Replace the remaining toJSON(...) calls with JSON.encode(...) so the module uses
one JSON-serialization builtin throughout.

Signed-off-by: Tom Chauveau <tom@dagger.io>
…ate hygiene

The engine's client-facing schema now exposes the bound module as a namespaced
dependency (dag.<module>()) rather than promoting its functions to Query, and it
no longer pulls the bound module's *internal* dependencies into the client
schema. So a client for a single module is dagger.gen.ts + <module>.gen.ts only
(a module cannot even return an external dependency's type). Drop the
client-dep.gen.ts assertion accordingly and simplify the fixture.

Also make the generation checks survive a prior `dagger generate`: they now
assert a path is generated (added OR modified) instead of strictly added, since
`dagger generate` writes real output into the fixture/client dirs. Gitignore the
client output dir so it doesn't dirty the tree.

All 16 checks green on the dev engine, including generate-client-compiles-check.

Signed-off-by: Tom Chauveau <tom@dagger.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant