Skip to content

fix: prune a departed client's dependency on the legacy runtime layout - #103

Open
TomChv wants to merge 1 commit into
mainfrom
fix/legacy-client-rm-prunes
Open

TomChv wants to merge 1 commit into
mainfrom
fix/legacy-client-rm-prunes

Conversation

@TomChv

@TomChv TomChv commented Oct 13, 2026

Copy link
Copy Markdown
Member

Problem

On the --legacy-runtime layout, dagger module client rm removed the record from dagger.toml and nothing else. After client rm ./dep:

  • app/dagger-module.toml kept [[dependencies]] name = "dep"
  • app/tsconfig.json kept the @dagger.io/dep path alias
  • app/sdk/compat.gen.ts still installed dag.dep()
  • app/sdk/dep.gen.ts was not deleted — it was rewritten with the serve wiring stripped
  • the call still returned, exit 0

A following dagger generate reported no changes to apply, so the state was permanent: the user kept shipping a dependency they had removed, with nothing in the workspace config to show it. The entrypoint layout prunes all of this correctly, because it declares no [[dependencies]] at all.

Worse than the code claimed. typescript-sdk.dang said dropping a client "leaves its dependency behind, serving a module whose bindings are gone" — the bindings were not gone either, so there was no symptom to notice.

Root cause

typescript-sdk.dang:765 (withClientDependencies) upserted the scope's clients into the manifest and never pruned, on the stated grounds that a manifest may carry entries nobody here wrote — by hand, or migrated from a pre-1.0 dagger.json — and that those are indistinguishable from the SDK's own.

Everything else followed from the entry that stayed: the manifest is what a module's session serves, so the target stayed in the module's schema, and the bindings, the tsconfig alias and the compat shim are all rendered from that schema. Codegen emits <module>.gen.ts per manifest dependency as well as per recorded target (helpers/codegen/generator/typescript/generator.go:146), which is why the client file was rewritten rather than pruned — the post-rm tree is the correct rendering of a module that declares dep as a dependency.

Fix

Option 1 of the three on the table: tell the two apart, and prune only ours. The distinction the docstring said was impossible is already recorded on disk, by codegen:

The source each module client serves on use […] A module with no entry (a manifest dependency the engine serves) renders no serve hook.
— helpers/codegen/generator/typescript/generator.go:146

A client the scope records as a target is handed to codegen in meta.json and gets an __serveModule hook; a plain manifest dependency, which the engine already serves into the session, never does. So a sdk/<name>.gen.ts carrying that hook is the previous run's record that <name> was a client. Absent from clients now, it has been removed, and its [[dependencies]] entry is the SDK's to take (departedClients).

Reading the previous run's output is also what bounds the claim: the run that acts on the hook is the run that rewrites the file without it, so an entry can only ever look like a departed client once. Every later run sees an ordinary manifest dependency and leaves it alone — the same as one the module never had a client for.

Removing the manifest entry is the whole fix: the module's schema then loses the target, so its bindings, its tsconfig alias and its compat entry are no longer rendered and the existing pruning (prunedBindings, syncModuleAliases) takes them on the same run. No new pruning paths.

withoutLegacyRuntimeDependency is a no-op for a name no [[dependencies]] declares (verified against sdk-helpers@v1.0.6), so the module's own client — which carries a hook and is never in clients — needs no special case.

Verification

Engine v1.0.0-beta.17, through a scratch workspace built from .dagger/modules/engine-e2e.

Red without the fix. The same branch with typescript-sdk.dang restored from main:

✘ client:remove-module-client-prunes-dependency-check 14m13s ERROR
  ! dropping a client should take the dependency that serves it

Green with it — client:* plus the three neighbours asked for:

== CHECKS ==  ✔ 17 passed
✔ client/client-vendors-library-check                     ✔ client/remove-clients-check
✔ client/deno-client-runs-check                           ✔ client/remove-clients-keeps-user-files-check
✔ client/generate-client-check                            ✔ client/remove-deno-client-check
✔ client/generate-client-compiles-check                   ✔ client/remove-module-client-prunes-dependency-check
✔ client/generate-client-keeps-package-type-check         ✔ generate/generate-dependency-check
✔ client/generate-client-on-synthetic-workspace-check     ✔ generate/generate-idempotent-check
✔ client/generate-client-respects-existing-check          ✔ generate/generate-prunes-stale-bindings-check
✔ client/generate-deno-client-check
✔ client/module-scope-client-bindings-check
✔ client/multiple-clients-check

and, in a run of their own, the new check beside the counterpart it mirrors:

== CHECKS ==  ✔ 2 passed
✔ client/remove-module-client-prunes-dependency-check
✔ generate/generate-dependency-bindings-check

The two checks are the two halves of the decision:

  • client:remove-module-client-prunes-dependency-check records a client for a [runtime] module, regenerates with the complete desired set no longer holding it, and asserts the manifest entry, the bindings, the tsconfig alias and the compat shim's dag.<target>() all go.
  • generate:generate-dependency-bindings-check gains the counterpart that must not break: the generate-deps/app fixture's [[dependencies]] gendep was written by hand and never recorded as a client, so a generation that sees its client file on disk must leave the entry alone. The planted file stands in for a previous run's, the way the neighbouring stale-bindings check plants one — without one on disk the run is not deciding anything. It also pins the invariant the fix reads: a manifest dependency's client carries no __serveModule.

Not covered

  • A dependency that is both hand-written and recorded as a client is treated as the SDK's once a generation has upserted it — removing the client removes the entry. The upsert already replaced whatever was there, so by then the two are the same entry; this is a deliberate call, not an oversight.
  • A module whose sdk/ is gitignored or otherwise absent has no record to read, so its departed client's dependency stays — today's behaviour, unchanged. The next generation with the bindings present does not recover it either: the file it would read is written without the hook. Graceful, but it is a limit.
  • Module names that are not already kebab-case. Candidates come from the generated filenames, which codegen kebab-cases, and are matched against the manifest's dependency names. A name the two spell differently is simply never a candidate, so such a dependency is kept.
  • No CLI-level run. The repro is at the generateScope boundary the engine calls, not through dagger module client rm; the issue's own repro established the CLI half on beta.16.
  • The entrypoint layout is untouched — it never writes [[dependencies]], and withClientDependencies is skipped there.

Fixes #92

@TomChv
TomChv force-pushed the fix/legacy-client-rm-prunes branch from 0af3eb3 to 08ad7ba Compare October 13, 2026 15:54
On the `--legacy-runtime` layout `dagger module client rm` removed the record
from dagger.toml and nothing else. withClientDependencies upserted and never
pruned, so the module kept its `[[dependencies]]` entry — which kept the target
in the module's schema, so the bindings, the tsconfig alias and the compat
shim's `dag.<target>()` all regenerated from it and the call went on working
with nothing in the workspace config to say it should.

A manifest may carry entries nobody here wrote, which is why pruning everything
missing from `clients` was never an option. What separates the two was already
on disk: codegen renders a serve hook into a client it is handed as a target and
never into one for a manifest dependency the engine already serves, so a
`sdk/<name>.gen.ts` carrying one is the last run's record of a client. Absent
from `clients` now, it has been removed, and its dependency comes off the
manifest — everything derived from it then goes with the existing pruning.

Fixes #92

Signed-off-by: Tom Chauveau <tom@dagger.io>
@TomChv
TomChv force-pushed the fix/legacy-client-rm-prunes branch from 08ad7ba to 35f4a1e Compare October 14, 2026 16:10
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.

client rm is a functional no-op on the --legacy-runtime layout: the dependency stays live

1 participant