A TDK project has two kinds of generated output. The first is the starter code and project files the CLI writes when you create something. The second is the runtime configuration that Tilt writes when it loads the project. The second kind is produced by the topologies and generators, which this article describes.
Two stages, two languages
TDK generates in two stages, and they are connected but not interchangeable.
Stage one runs in the CLI, in TypeScript. When you run tdk project or tdk resource, the CLI copies a starter and writes your first source files and service.json. Project-level files come from Handlebars templates in cli/templates/, filled from .tdk/project.json.
Stage two runs in Tilt, in Starlark. When Tilt loads the project's Tiltfile, Starlark discovery reads every service.json, works out the resources, and the engine writes per-service configuration under .autogenerated/. It then declares the Tilt resources and how they depend on each other.
The practical rule follows from this split: the CLI sets up the project, and the engine derives the runtime. A bug in a generated Dockerfile is almost always a bug in the engine, not in a template.
Starlark is the generator, not a template
This is the detail that surprises people. Starlark is the generator implementation language, and it is not a template-file extension. There is no Dockerfile.hbs for a service. Instead, a Starlark module builds the Dockerfile text as a string and writes it through Tilt's filesystem helpers.
The same holds for the Vite configs, the TypeScript configs, the Prisma config, the package-manager config, the frontend API helpers, and the environment files. Some outputs are TypeScript or JSON, but they are still produced by Starlark code.
Where things live
The maintainer guide maps each responsibility to a folder:
-
cli/src/commands/holds the user commands, including project and resource scaffolding and config regeneration. -
cli/templates/holds Handlebars templates for the project root, such as the project Tiltfile template and the resource defaults template. -
cli/src/frontend-frameworks/andcli/src/backend-languages/hold the TypeScript providers that write framework and language starter files. -
discovery/scans theservice.jsonfiles, validates and normalises them, and builds the registry and indexes. -
engine/topologies/tilt/generators/holds the per-file generators. Start withindex.starthere. -
engine/topologies/tilt/resources/declares the Tilt resources, their start order, dependencies, and triggers. -
engine/topologies/tilt/manifest/loads and maps manifests, and holds the schema and constants. -
engine/topologies/platform/holds shared building blocks, such as Docker and networking. - The root
Tiltfilewires discovery, the engine, and the extensions together.
The topology idea
A topology is the shape of the stack: which resources exist, how they connect, and what infrastructure they need. The platform folder holds the parts every stack shares, and the tilt folder holds the parts that turn a discovered service into Tilt resources. The split lets shared infrastructure stay in one place while per-service behaviour lives beside its generator.
Example: how a Dockerfile is produced
Take the Dockerfile for a Rust backend. The CLI's provider writes the starter files: Cargo.toml and src/main.rs, with a health route and a unit test. The CLI does not write a Dockerfile, because the engine generates the image.
At Tilt time, the language Dockerfile module in engine/topologies/platform/docker/dockerfile/ builds the image text. For Rust that means a build stage, a test stage that runs cargo test, and a small production stage with a health check. The output goes to .autogenerated/Dockerfile.app.autogenerated, and its header names the generator that wrote it.
The Go language follows the same pattern, with an opt-in development target and a watcher script. Python relies on its own process reload, and so has no Bun install step.
Provider-owned and shared
Some behaviour is shared by every language and framework: the app name, type, stack, port range, health check path, dependencies, the Traefik hostname, health-gated startup order, and database provisioning.
Other behaviour belongs to the provider: the dependency manifest, the source entry, the smoke test, the base image, and the reload command. Keeping that line clear stops one language from leaking into another. A Python service should not get a Bun install step, and a Go service should not get a second Compose file.
Generators that only make sense for Bun, such as Vite, tsconfig, Prisma, and bunfig, are skipped for provider-owned languages. The manifest resource generator makes that decision.
Where to change things
If you want to change the output for a file, find its generator under engine/topologies/tilt/generators/ and change it there. Do not patch the generated file, because the next generation overwrites it. If the change should apply to one service only, change that service's service.json instead.
If you want to add a language, follow the provider recipe: add the starter provider in cli/src/backend-languages/, register it, add the image in the platform Dockerfile module, and add any live-update rules the language needs. Keep each language in its own change, so review stays manageable.
Checking your change
Run the change through the same path a user would. tdk up main --dry-run shows the plan without starting containers. For generation detail, set TDK_DEBUG=1 and run tdk up --verbose, which includes Starlark debug output in the Tilt log. The contributor guide has the checks the repo expects before a change is merged.
Summary
TDK's topologies describe the shape of a stack, and its generators write the runtime files that realise it. The CLI scaffolds starter code in TypeScript, and the engine generates runtime configuration in Starlark. Change the generator, not the output, and keep the provider-owned parts of each language in the provider.
Top comments (0)