@tdk-landscape/tdk-cli-core 1.3.86 → 1.3.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +20 -1
- package/cli/dist/backend-languages/bun.js +53 -0
- package/cli/dist/backend-languages/python.js +89 -0
- package/cli/dist/backend-languages/registry.js +30 -0
- package/cli/dist/backend-languages/types.js +2 -0
- package/cli/dist/commands/resource.js +41 -47
- package/cli/dist/commands/up.js +1 -1
- package/cli/dist/utils/doctor-wiring.js +9 -1
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/package.json +1 -1
- package/engine/schemas/service-schema.json +8 -0
- package/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +62 -0
- package/engine/topologies/platform/docker/index.star +7 -0
- package/engine/topologies/tilt/generators/validators.star +7 -3
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +31 -4
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +25 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- Add an opt-in Python backend: `tdk resource <name> --type backend --language python` scaffolds a FastAPI service (`pyproject.toml`, `src/main.py`, a pytest smoke test) and TDK generates its Docker image (`python:3.12-slim`, with `test` and `production` stages). The choice is saved as `language` in `service.json` only when you pass the flag; omitting it keeps the existing Bun + Hono output byte for byte. Unknown ids and `--language` on non-backend types fail before any file is written. A runnable example is in `examples/one-backend-python`, and the provider contract is in `docs/backend-language-providers.md`. Node.js is not included.
|
|
8
|
+
- `tdk doctor` no longer reports "resource without a package.json" for a Python backend, and a Python service missing `pyproject.toml` or `src/main.py` now fails early with the missing file named.
|
|
9
|
+
- Live reload now works: Tilt `live_update` sync paths were relative to the project root while Tilt resolves them against `.tdk/.tdk-out`, so they never matched and every source edit rebuilt the image ("Found file(s) not matching any sync"). An edit under `src/` is now copied into the running container without a rebuild, for Bun and Python services. Dependency file changes (`package.json`, `pyproject.toml`) still rebuild the image.
|
|
10
|
+
- The service schema (`https://tdk-landscape.github.io/schema.service.json`) gains the `language` field (`bun`, `python`).
|
|
11
|
+
- Fix the routed health URL in the `one-backend` example README.
|
|
12
|
+
|
|
7
13
|
## 1.3.80 (2026-09-30)
|
|
8
14
|
|
|
9
15
|
- Add `tdk doctor --json` with schema-versioned readiness, checks, and structured errors; diagnostics remain on stderr.
|
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
TDK CLI starts your services on your laptop. It is not a deploy and not a Compose file: define each service in `service.json`, then run `tdk up`. No Kubernetes is needed on the machine.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Stability: 1.x local dev. Generated files are a contract; verify with `tdk config verify`. Core CLI is MIT and needs no key. Premium is optional.
|
|
6
|
+
|
|
7
|
+
Docker runs the containers. Tilt watches services and live-updates containers while you code. TDK CLI writes the configuration Tilt uses. Production deployment stays with Helm, Argo CD, or Kustomize.
|
|
6
8
|
|
|
7
9
|
[Website](https://tdk-landscape.github.io/tdk-website/) · [Quickstart](https://tdk-landscape.github.io/tdk-website/docs/quickstart/) · [Examples](https://tdk-landscape.github.io/tdk-website/docs/examples/) · [Awesome TDK](https://github.com/tdk-landscape/awesome-tdk-framework) · [Demo](https://tdk-landscape.github.io/tdk-demo-animation/) · [Report a bug](https://github.com/tdk-landscape/tdk-cli-core/issues)
|
|
8
10
|
|
|
@@ -20,6 +22,8 @@ curl http://api.shop.localhost/api/orders-api/health
|
|
|
20
22
|
|
|
21
23
|
*Scaffolding a backend and frontend, then listing the stack.*
|
|
22
24
|
|
|
25
|
+

|
|
26
|
+
|
|
23
27
|
If Helm, Compose, or your existing Tilt setup already gives you a working local environment, keep using it. TDK CLI is for engineers managing several services who want a clear local service contract and one command to start the stack.
|
|
24
28
|
|
|
25
29
|
See [how TDK CLI works alongside Helm](https://tdk-landscape.github.io/tdk-website/docs/with-helm/), the [service schema](engine/schemas/service-schema.json), and the [project configuration schema](engine/schemas/project-schema.json).
|
|
@@ -57,9 +61,12 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
|
|
|
57
61
|
- [Working alongside Helm](docs/with-helm.md)
|
|
58
62
|
- [Configuration and editor schemas](docs/configuration.md)
|
|
59
63
|
- [Runnable one-backend example](examples/one-backend/README.md)
|
|
64
|
+
- [Runnable Python backend example](examples/one-backend-python/README.md)
|
|
60
65
|
- [Full multi-service example](examples/tdk-example/README.md)
|
|
61
66
|
- [Features and license limits](docs/FEATURES.md)
|
|
62
67
|
- [Honest comparison and known limits](docs/compare-honest.md)
|
|
68
|
+
- [Public claims registry](docs/claims.md)
|
|
69
|
+
- [Show HN draft](docs/show-hn.md)
|
|
63
70
|
- [Architecture and repository map](docs/project-overview.md)
|
|
64
71
|
- [Scale fixture measurements and caveats](docs/scale-bench.md)
|
|
65
72
|
|
|
@@ -67,6 +74,18 @@ curl -fsSL https://tdk-landscape.github.io/install.sh | sh
|
|
|
67
74
|
|
|
68
75
|
For the local runtime, install Docker (Desktop, OrbStack, or Colima; Engine 25+, Compose 2.20+) and [Tilt](https://docs.tilt.dev/install.html). Bun 1.2+ is used by the default generated services. TDK selects host ports from bounded fallback ranges for HTTP, HTTPS, and Postgres; set `TDK_HTTP_PORT`, `TDK_HTTPS_PORT`, or `TDK_POSTGRES_PORT` to override them. TDK supports macOS, Linux, and Windows through WSL2 Ubuntu; native Windows supports CLI inspection only. Run `tdk doctor` to check local readiness. See [WSL2 setup](docs/wsl2.md).
|
|
69
76
|
|
|
77
|
+
On native Windows, `tdk --version`, `tdk doctor`, and `tdk up --dry-run` are inspect-only commands. `tdk up` exits 2 with “Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.”
|
|
78
|
+
|
|
79
|
+
## License matrix
|
|
80
|
+
|
|
81
|
+
| Capability | Free | Premium |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `tdk up`, scaffold, Traefik, Postgres, Tilt live update, golden layers | yes | yes |
|
|
84
|
+
| Verdaccio, DDD scaffold, Sablier idle stop | no | key |
|
|
85
|
+
| Playwright, C4, AGENTS.md | free if generated in-repo | only if the implementation is downloaded with a key |
|
|
86
|
+
|
|
87
|
+
Core stays free. Premium is a separate key for the extras above; no key required to run `tdk up`.
|
|
88
|
+
|
|
70
89
|
**Ecosystem map:** examples, articles, related tools, and notes live in [awesome-tdk-framework](https://github.com/tdk-landscape/awesome-tdk-framework). Star this repo (`tdk-cli-core`) if the CLI is what you run; use the awesome list to browse the rest.
|
|
71
90
|
|
|
72
91
|
## Contributing and license
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function getBackendIndexTemplate(name) {
|
|
2
|
+
return `import { Hono } from 'hono';
|
|
3
|
+
|
|
4
|
+
const app = new Hono();
|
|
5
|
+
|
|
6
|
+
// Health check endpoint (required by TILT_RESOURCE_DEFAULTS.star)
|
|
7
|
+
app.get('/health', (c) => {
|
|
8
|
+
return c.json({ status: 'ok', service: '${name}' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
app.get('/health/live', (c) => {
|
|
12
|
+
return c.json({ status: 'alive', timestamp: Date.now() });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.get('/health/ready', async (c) => {
|
|
16
|
+
const dependencies: Record<string, string> = {};
|
|
17
|
+
let allReady = true;
|
|
18
|
+
|
|
19
|
+
// Add dependency checks here (database, cache, etc.)
|
|
20
|
+
// Mark allReady = false if any dependency is unhealthy
|
|
21
|
+
|
|
22
|
+
const status = allReady ? 'ready' : 'not_ready';
|
|
23
|
+
return c.json({ status, dependencies }, allReady ? 200 : 503);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.get('/', (c) => {
|
|
27
|
+
return c.json({
|
|
28
|
+
service: '${name}',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
endpoints: ['/health', '/health/live', '/health/ready']
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const port = process.env.PORT || 3000;
|
|
35
|
+
console.log('\\n🚀 ${name} running on http://localhost:' + port);
|
|
36
|
+
console.log('📊 Health check: http://localhost:' + port + '/health\\n');
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
port,
|
|
40
|
+
fetch: app.fetch,
|
|
41
|
+
};
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The historical default. Package scripts, tsconfig, Dockerfile, and the vitest smoke test stay in
|
|
46
|
+
* the shared scaffold so omitted-language output is byte-identical to before providers existed.
|
|
47
|
+
*/
|
|
48
|
+
export const bunBackendProvider = {
|
|
49
|
+
id: "bun",
|
|
50
|
+
label: "Bun + Hono",
|
|
51
|
+
installHint: "bun install",
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=bun.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const PYTHON_DEV_COMMAND = "uvicorn main:app --app-dir src --host 0.0.0.0 --port ${PORT:-4000} --reload --reload-dir src";
|
|
2
|
+
export const pythonBackendProvider = {
|
|
3
|
+
id: "python",
|
|
4
|
+
label: "Python 3.12 + FastAPI",
|
|
5
|
+
devCommand: PYTHON_DEV_COMMAND,
|
|
6
|
+
watch: ["src/**/*"],
|
|
7
|
+
installHint: 'pip install -e ".[dev]" # optional: Docker builds the image for you',
|
|
8
|
+
createFiles(name) {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
filename: "pyproject.toml",
|
|
12
|
+
content: `[project]
|
|
13
|
+
name = "${name}"
|
|
14
|
+
version = "0.0.1"
|
|
15
|
+
requires-python = ">=3.12"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"fastapi==0.115.6",
|
|
18
|
+
"uvicorn[standard]==0.34.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest==8.3.4",
|
|
24
|
+
"httpx==0.28.1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=68"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
# Only the dependencies are installed into the image; the source is synced for live reload.
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
py-modules = []
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
pythonpath = ["src"]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
`,
|
|
39
|
+
description: "Generating pyproject.toml",
|
|
40
|
+
emoji: "📦",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
filename: "src/main.py",
|
|
44
|
+
content: `import os
|
|
45
|
+
|
|
46
|
+
from fastapi import FastAPI
|
|
47
|
+
|
|
48
|
+
app = FastAPI(title="${name}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.get("/health")
|
|
52
|
+
def health() -> dict[str, str]:
|
|
53
|
+
return {"status": "ok", "service": "${name}"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.get("/")
|
|
57
|
+
def root() -> dict[str, object]:
|
|
58
|
+
return {"service": "${name}", "version": "1.0.0", "endpoints": ["/health"]}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
import uvicorn
|
|
63
|
+
|
|
64
|
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000")))
|
|
65
|
+
`,
|
|
66
|
+
description: "Generating backend source",
|
|
67
|
+
emoji: "💻",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
filename: "tests/test_health.py",
|
|
71
|
+
content: `from fastapi.testclient import TestClient
|
|
72
|
+
|
|
73
|
+
from main import app
|
|
74
|
+
|
|
75
|
+
client = TestClient(app)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_health() -> None:
|
|
79
|
+
response = client.get("/health")
|
|
80
|
+
assert response.status_code == 200
|
|
81
|
+
assert response.json() == {"status": "ok", "service": "${name}"}
|
|
82
|
+
`,
|
|
83
|
+
description: "Generating test file",
|
|
84
|
+
emoji: "🧪",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=python.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { TdkError } from "../utils/errors.js";
|
|
2
|
+
import { bunBackendProvider } from "./bun.js";
|
|
3
|
+
import { pythonBackendProvider } from "./python.js";
|
|
4
|
+
export const DEFAULT_BACKEND_LANGUAGE = "bun";
|
|
5
|
+
export const BACKEND_LANGUAGES = {
|
|
6
|
+
bun: bunBackendProvider,
|
|
7
|
+
python: pythonBackendProvider,
|
|
8
|
+
};
|
|
9
|
+
export function getBackendLanguage(languageId) {
|
|
10
|
+
const id = (languageId ?? DEFAULT_BACKEND_LANGUAGE).trim().toLowerCase();
|
|
11
|
+
const provider = Object.hasOwn(BACKEND_LANGUAGES, id) ? BACKEND_LANGUAGES[id] : undefined;
|
|
12
|
+
if (!provider) {
|
|
13
|
+
const supportedIds = Object.keys(BACKEND_LANGUAGES).join(", ");
|
|
14
|
+
throw new TdkError(`Unknown backend language "${languageId}". Supported languages: ${supportedIds}.`, [`Use one of: ${supportedIds}`, `Omit --language to use ${DEFAULT_BACKEND_LANGUAGE}`]);
|
|
15
|
+
}
|
|
16
|
+
return provider;
|
|
17
|
+
}
|
|
18
|
+
/** Returns the provider for a backend, or undefined for other types. Rejects --language elsewhere. */
|
|
19
|
+
export function resolveBackendLanguage(resourceType, languageId) {
|
|
20
|
+
if (resourceType !== "backend") {
|
|
21
|
+
if (languageId !== undefined) {
|
|
22
|
+
throw new TdkError("--language can only be used with --type backend.", [
|
|
23
|
+
"Add --type backend, or drop --language for this resource type",
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return getBackendLanguage(languageId);
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { getBackendIndexTemplate } from "../backend-languages/bun.js";
|
|
6
|
+
import { resolveBackendLanguage } from "../backend-languages/registry.js";
|
|
5
7
|
import { resolveFrontendFramework } from "../frontend-frameworks/registry.js";
|
|
6
8
|
import { hasDddLicense } from "../generator/extension-fetch.js";
|
|
7
9
|
import { CREATABLE_RESOURCE_TYPES } from "../types/index.js";
|
|
@@ -63,8 +65,10 @@ export function resolveByoPort(value, assignedPort, resources) {
|
|
|
63
65
|
}
|
|
64
66
|
return port;
|
|
65
67
|
}
|
|
66
|
-
export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId) {
|
|
68
|
+
export function createServiceJson(name, type, stack, port, extraFeatures = [], frameworkId, languageId) {
|
|
67
69
|
const framework = resolveFrontendFramework(type, frameworkId);
|
|
70
|
+
// Only an explicit selection is persisted: a missing `language` has always meant Bun.
|
|
71
|
+
const language = languageId === undefined ? undefined : resolveBackendLanguage(type, languageId);
|
|
68
72
|
const typeSpecific = TYPE_SPECIFIC[type];
|
|
69
73
|
const base = JSON.parse(JSON.stringify(BASE_TEMPLATE));
|
|
70
74
|
for (const [key, value] of Object.entries(typeSpecific)) {
|
|
@@ -75,18 +79,33 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
|
|
|
75
79
|
base[key] = value;
|
|
76
80
|
}
|
|
77
81
|
}
|
|
82
|
+
if (language?.devCommand) {
|
|
83
|
+
base.dev = { command: language.devCommand, watch: language.watch ?? base.dev.watch };
|
|
84
|
+
// Provider-owned runtimes declare the probe path the engine reads; Bun keeps its historical key.
|
|
85
|
+
base.healthCheckPath = base.healthCheck;
|
|
86
|
+
delete base.healthCheck;
|
|
87
|
+
// The engine generates the image (.autogenerated/Dockerfile.app.autogenerated); a build
|
|
88
|
+
// block would point at a Dockerfile that does not exist.
|
|
89
|
+
delete base.build;
|
|
90
|
+
}
|
|
78
91
|
return {
|
|
79
92
|
...base,
|
|
80
93
|
$schema: SERVICE_MANIFEST_SCHEMA_URL,
|
|
81
94
|
schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
|
|
82
95
|
appName: name,
|
|
83
96
|
appType: type,
|
|
84
|
-
featuresEnabled: [
|
|
97
|
+
featuresEnabled: [
|
|
98
|
+
...getDefaultFeaturesForResourceType(type).filter(
|
|
99
|
+
// Prisma wiring is Bun-only; the Python provider owns their data layer.
|
|
100
|
+
(feature) => !(language?.createFiles && feature === "prisma")),
|
|
101
|
+
...extraFeatures,
|
|
102
|
+
],
|
|
85
103
|
name,
|
|
86
104
|
type,
|
|
87
105
|
stack,
|
|
88
106
|
port,
|
|
89
107
|
...(framework ? { framework: framework.id } : {}),
|
|
108
|
+
...(language ? { language: language.id } : {}),
|
|
90
109
|
};
|
|
91
110
|
}
|
|
92
111
|
export function createPackageJson(name, type, frameworkId) {
|
|
@@ -178,49 +197,7 @@ EXPOSE 3000
|
|
|
178
197
|
|
|
179
198
|
CMD ["bun", "run", "start"]
|
|
180
199
|
`;
|
|
181
|
-
export
|
|
182
|
-
return `import { Hono } from 'hono';
|
|
183
|
-
|
|
184
|
-
const app = new Hono();
|
|
185
|
-
|
|
186
|
-
// Health check endpoint (required by TILT_RESOURCE_DEFAULTS.star)
|
|
187
|
-
app.get('/health', (c) => {
|
|
188
|
-
return c.json({ status: 'ok', service: '${name}' });
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
app.get('/health/live', (c) => {
|
|
192
|
-
return c.json({ status: 'alive', timestamp: Date.now() });
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
app.get('/health/ready', async (c) => {
|
|
196
|
-
const dependencies: Record<string, string> = {};
|
|
197
|
-
let allReady = true;
|
|
198
|
-
|
|
199
|
-
// Add dependency checks here (database, cache, etc.)
|
|
200
|
-
// Mark allReady = false if any dependency is unhealthy
|
|
201
|
-
|
|
202
|
-
const status = allReady ? 'ready' : 'not_ready';
|
|
203
|
-
return c.json({ status, dependencies }, allReady ? 200 : 503);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
app.get('/', (c) => {
|
|
207
|
-
return c.json({
|
|
208
|
-
service: '${name}',
|
|
209
|
-
version: '1.0.0',
|
|
210
|
-
endpoints: ['/health', '/health/live', '/health/ready']
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const port = process.env.PORT || 3000;
|
|
215
|
-
console.log('\\n🚀 ${name} running on http://localhost:' + port);
|
|
216
|
-
console.log('📊 Health check: http://localhost:' + port + '/health\\n');
|
|
217
|
-
|
|
218
|
-
export default {
|
|
219
|
-
port,
|
|
220
|
-
fetch: app.fetch,
|
|
221
|
-
};
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
200
|
+
export { getBackendIndexTemplate };
|
|
224
201
|
function getShutdownHandlerTemplate(signal) {
|
|
225
202
|
return `process.on('${signal}', () => {
|
|
226
203
|
console.log('[Worker] ${signal} received, shutting down gracefully...');
|
|
@@ -312,6 +289,7 @@ export const resourceCommand = new Command("resource")
|
|
|
312
289
|
.argument("[name]", "Resource name (kebab-case)")
|
|
313
290
|
.option("-t, --type <type>", "Resource type: backend, frontend, worker, bring-your-own, sdk", "backend")
|
|
314
291
|
.option("--framework <id>", "Frontend framework: react (default), vue")
|
|
292
|
+
.option("--language <id>", "Backend language: bun (default), python")
|
|
315
293
|
.option("-s, --stack <stack>", "Stack to assign resource to", "default")
|
|
316
294
|
.option("-p, --path <path>", "Custom path for resource directory")
|
|
317
295
|
.option("--resource-path <path>", "Alias for --path (for backward compatibility)")
|
|
@@ -371,6 +349,7 @@ export const resourceCommand = new Command("resource")
|
|
|
371
349
|
resourceType = normalizedType;
|
|
372
350
|
}
|
|
373
351
|
const frontendFramework = resolveFrontendFramework(resourceType, options.framework);
|
|
352
|
+
const backendLanguage = resolveBackendLanguage(resourceType, options.language);
|
|
374
353
|
let dddEnabled = false;
|
|
375
354
|
if (options.ddd) {
|
|
376
355
|
if (resourceType !== "backend" && resourceType !== "worker") {
|
|
@@ -594,7 +573,7 @@ This file contains the resource configuration for TDK.
|
|
|
594
573
|
}
|
|
595
574
|
}
|
|
596
575
|
// Prepare file generation tasks
|
|
597
|
-
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], frontendFramework?.id);
|
|
576
|
+
const serviceJson = createServiceJson(resourceName, resourceType, stackName, assignedPort, dddEnabled ? ["ddd"] : [], frontendFramework?.id, options.language === undefined ? undefined : backendLanguage?.id);
|
|
598
577
|
const packageJson = createPackageJson(resourceName, resourceType, frontendFramework?.id);
|
|
599
578
|
const tasks = [
|
|
600
579
|
{
|
|
@@ -669,6 +648,21 @@ This file contains the resource configuration for TDK.
|
|
|
669
648
|
description: "Generating test file",
|
|
670
649
|
emoji: "🧪",
|
|
671
650
|
});
|
|
651
|
+
// A language provider that owns its runtime files replaces the shared Bun/TypeScript
|
|
652
|
+
// scaffold (package.json, tsconfig, source, test, and the Dockerfile, which the engine generates); service.json stays shared.
|
|
653
|
+
const languageFiles = backendLanguage?.createFiles?.(resourceName);
|
|
654
|
+
if (languageFiles) {
|
|
655
|
+
const owned = new Set(languageFiles.map((file) => file.filename));
|
|
656
|
+
const sharedTasks = tasks.filter((task) => task.filename === "service.json" ||
|
|
657
|
+
!(owned.has(task.filename) ||
|
|
658
|
+
task.filename === "Dockerfile" ||
|
|
659
|
+
task.filename === "src/index.ts" ||
|
|
660
|
+
task.filename === "package.json" ||
|
|
661
|
+
task.filename === "tsconfig.json" ||
|
|
662
|
+
task.filename.endsWith(".test.ts")));
|
|
663
|
+
tasks.length = 0;
|
|
664
|
+
tasks.push(...sharedTasks, ...languageFiles.map((file) => ({ ...file, type: "text" })));
|
|
665
|
+
}
|
|
672
666
|
// Execute all file writes with progress
|
|
673
667
|
console.log(chalk.blue("💻 Generating source files..."));
|
|
674
668
|
writeFilesWithProgress(fullPath, tasks);
|
|
@@ -688,7 +682,7 @@ This file contains the resource configuration for TDK.
|
|
|
688
682
|
}
|
|
689
683
|
console.log(chalk.gray(`\nNext steps:`));
|
|
690
684
|
console.log(chalk.gray(` cd ${finalResourcePath}`));
|
|
691
|
-
console.log(chalk.gray(` bun install`));
|
|
685
|
+
console.log(chalk.gray(` ${backendLanguage?.installHint ?? "bun install"}`));
|
|
692
686
|
console.log(chalk.gray(` tdk up ${stackName}`));
|
|
693
687
|
});
|
|
694
688
|
});
|
package/cli/dist/commands/up.js
CHANGED
|
@@ -28,7 +28,7 @@ export function formatUpSuccess(port, appUrls = []) {
|
|
|
28
28
|
export function nativeWindowsUpRefusal(platform, allowNativeWindows) {
|
|
29
29
|
if (platform !== "win32" || allowNativeWindows === "1")
|
|
30
30
|
return null;
|
|
31
|
-
return "
|
|
31
|
+
return "Landscape startup needs Ubuntu on WSL2. Native Windows is inspect-only.";
|
|
32
32
|
}
|
|
33
33
|
function waitForTiltUi(port, timeoutMs = 30_000) {
|
|
34
34
|
return new Promise((resolve) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join, relative } from "node:path";
|
|
4
|
+
import { BACKEND_LANGUAGES } from "../backend-languages/registry.js";
|
|
4
5
|
import { execAsync } from "./exec-async.js";
|
|
5
6
|
import { formatCount } from "./formatting.js";
|
|
6
7
|
import { findProjectRoot } from "./paths.js";
|
|
@@ -44,9 +45,16 @@ function backendPorts(resources) {
|
|
|
44
45
|
}
|
|
45
46
|
return ports;
|
|
46
47
|
}
|
|
48
|
+
/** A backend whose language provider scaffolds its own runtime files (Python) has no package.json. */
|
|
49
|
+
function ownsRuntimeFiles(resource) {
|
|
50
|
+
const language = resource.config?.language;
|
|
51
|
+
return (typeof language === "string" &&
|
|
52
|
+
Object.hasOwn(BACKEND_LANGUAGES, language) &&
|
|
53
|
+
BACKEND_LANGUAGES[language].createFiles !== undefined);
|
|
54
|
+
}
|
|
47
55
|
export function checkResourcePackageJson(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
48
56
|
const missing = discoverResourcesFromRoot(projectRoot)
|
|
49
|
-
.filter((resource) => !existsSync(join(resource.path, "package.json")))
|
|
57
|
+
.filter((resource) => !ownsRuntimeFiles(resource) && !existsSync(join(resource.path, "package.json")))
|
|
50
58
|
.map((resource) => resource.name);
|
|
51
59
|
if (missing.length === 0) {
|
|
52
60
|
return {
|
package/cli/package.json
CHANGED
|
@@ -222,6 +222,14 @@
|
|
|
222
222
|
],
|
|
223
223
|
"type": "string"
|
|
224
224
|
},
|
|
225
|
+
"language": {
|
|
226
|
+
"description": "Backend language provider used to scaffold and run the service. Only meaningful for backend resources; omitted means bun (legacy manifests).",
|
|
227
|
+
"enum": [
|
|
228
|
+
"bun",
|
|
229
|
+
"python"
|
|
230
|
+
],
|
|
231
|
+
"type": "string"
|
|
232
|
+
},
|
|
225
233
|
"healthCheckPath": {
|
|
226
234
|
"default": "/health",
|
|
227
235
|
"description": "HTTP endpoint for health checks. Must return 200 OK.",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Dockerfiles for backends whose `language` is not the Bun default.
|
|
2
|
+
#
|
|
3
|
+
# Bun backends use the golden-image generator. Python backends are a small
|
|
4
|
+
# standalone image: language-specific base image and reload command, while the build
|
|
5
|
+
# context (project root), `production` target, health check, and PORT contract stay the
|
|
6
|
+
# same as every other backend.
|
|
7
|
+
|
|
8
|
+
BASE_PORT_BACKEND = 4000
|
|
9
|
+
|
|
10
|
+
# Languages whose runtime files are owned by the provider (no Vite/tsconfig/Bun config).
|
|
11
|
+
PROVIDER_OWNED_LANGUAGES = ["python"]
|
|
12
|
+
|
|
13
|
+
_CURL = """RUN apt-get update && apt-get install -y --no-install-recommends curl \\
|
|
14
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_HEALTHCHECK = """HEALTHCHECK --interval=10s --timeout=5s --retries=3 \\
|
|
18
|
+
CMD curl -f http://localhost:${{PORT:-{port}}}/health || exit 1
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_PYTHON = """# Generated by TDK for a Python backend. Do not edit; change service.json instead.
|
|
22
|
+
FROM python:3.12-slim AS base
|
|
23
|
+
|
|
24
|
+
WORKDIR /app/{res_path}
|
|
25
|
+
|
|
26
|
+
{curl}
|
|
27
|
+
COPY {res_path}/pyproject.toml ./
|
|
28
|
+
RUN pip install --no-cache-dir .
|
|
29
|
+
|
|
30
|
+
COPY {res_path}/src ./src
|
|
31
|
+
|
|
32
|
+
FROM base AS test
|
|
33
|
+
RUN pip install --no-cache-dir ".[dev]"
|
|
34
|
+
COPY {res_path}/tests ./tests
|
|
35
|
+
RUN pytest
|
|
36
|
+
|
|
37
|
+
FROM base AS production
|
|
38
|
+
|
|
39
|
+
{healthcheck}
|
|
40
|
+
EXPOSE {port}
|
|
41
|
+
|
|
42
|
+
CMD ["sh", "-c", "uvicorn main:app --app-dir src --host 0.0.0.0 --port ${{PORT:-{port}}} --reload --reload-dir src"]
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def generate_language_dockerfile(res_path, language, port = BASE_PORT_BACKEND):
|
|
46
|
+
"""Dockerfile text for a provider-owned backend language, or fail for an unknown one."""
|
|
47
|
+
template = {"python": _PYTHON}.get(language)
|
|
48
|
+
if template == None:
|
|
49
|
+
fail("No Dockerfile generator for backend language '{}'".format(language))
|
|
50
|
+
return template.format(
|
|
51
|
+
res_path = res_path,
|
|
52
|
+
port = port,
|
|
53
|
+
curl = _CURL,
|
|
54
|
+
healthcheck = _HEALTHCHECK.format(port = port),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_provider_owned_language(manifest):
|
|
59
|
+
"""True when the manifest selects Python (a missing language means Bun)."""
|
|
60
|
+
if not manifest:
|
|
61
|
+
return False
|
|
62
|
+
return manifest.get("appType", "backend") == "backend" and manifest.get("language", "bun") in PROVIDER_OWNED_LANGUAGES
|
|
@@ -4,6 +4,11 @@ load(
|
|
|
4
4
|
"generate_app_dockerfile",
|
|
5
5
|
"generate_migrator_dockerfile",
|
|
6
6
|
)
|
|
7
|
+
load(
|
|
8
|
+
"./dockerfile/language_dockerfile.star",
|
|
9
|
+
"generate_language_dockerfile",
|
|
10
|
+
"is_provider_owned_language",
|
|
11
|
+
)
|
|
7
12
|
load("./config/dockerignore.star", "generate_dockerignore")
|
|
8
13
|
load(
|
|
9
14
|
"./compose/compose.star",
|
|
@@ -18,6 +23,8 @@ Docker = struct(
|
|
|
18
23
|
frontend = generate_frontend_dockerfile,
|
|
19
24
|
backend = generate_app_dockerfile,
|
|
20
25
|
migrator = generate_migrator_dockerfile,
|
|
26
|
+
backend_language = generate_language_dockerfile,
|
|
27
|
+
is_provider_owned_language = is_provider_owned_language,
|
|
21
28
|
generate_dockerignore = generate_dockerignore,
|
|
22
29
|
frontend_compose = generate_frontend_compose,
|
|
23
30
|
backend_compose = generate_backend_compose_entry,
|
|
@@ -556,8 +556,11 @@ def generate_resource_health_check(resource_name, resource_path, manifest, check
|
|
|
556
556
|
|
|
557
557
|
# Check 1: Required files exist
|
|
558
558
|
frontend_entry = "src/main.ts" if manifest.get("framework", "react") == "vue" else "src/main.tsx"
|
|
559
|
+
backend_files = {
|
|
560
|
+
"python": ["pyproject.toml", "src/main.py"],
|
|
561
|
+
}.get(manifest.get("language", "bun"), ["package.json", "prisma/schema.prisma"])
|
|
559
562
|
required_files = {
|
|
560
|
-
"backend":
|
|
563
|
+
"backend": backend_files,
|
|
561
564
|
"frontend": ["package.json", "index.html", frontend_entry],
|
|
562
565
|
"library": ["package.json"],
|
|
563
566
|
}.get(app_type, ["package.json"])
|
|
@@ -570,8 +573,9 @@ if [ ! -f "{}" ]; then
|
|
|
570
573
|
fi
|
|
571
574
|
'''.format(req_file, req_file))
|
|
572
575
|
|
|
573
|
-
# Check 2: Package.json is valid JSON
|
|
574
|
-
|
|
576
|
+
# Check 2: Package.json is valid JSON (Python backends have no package.json)
|
|
577
|
+
if manifest.get("language", "bun") != "python":
|
|
578
|
+
health_checks.append('''
|
|
575
579
|
if ! cat package.json | head -1 > /dev/null 2>&1; then
|
|
576
580
|
echo "❌ package.json is not valid"
|
|
577
581
|
exit 1
|
package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star
CHANGED
|
@@ -201,14 +201,39 @@ def _register_replicas(config, auto_init_apps):
|
|
|
201
201
|
)
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
def
|
|
205
|
-
"""
|
|
204
|
+
def _live_update_sync_source(project_root, full_res_path, sync_path):
|
|
205
|
+
"""Local path Tilt watches for a live-update sync.
|
|
206
|
+
|
|
207
|
+
Tilt resolves sync() local paths against the Tiltfile directory (.tdk/.tdk-out), not
|
|
208
|
+
the docker_build context, so a project-relative path never matches the changed files
|
|
209
|
+
and every edit falls back to a full image rebuild ("Found file(s) not matching any
|
|
210
|
+
sync"). Anchor it at the project root, like the Dockerfile and build context.
|
|
211
|
+
"""
|
|
212
|
+
relative_path = full_res_path + '/' + sync_path
|
|
213
|
+
if project_root:
|
|
214
|
+
return project_root.rstrip('/') + '/' + relative_path
|
|
215
|
+
return relative_path
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _build_live_update_rules(res_path, full_res_path, syncs, project_root='', language='bun'):
|
|
219
|
+
"""Build live update sync and run rules for a resource.
|
|
220
|
+
|
|
221
|
+
Python backends reload their own process after a sync (uvicorn --reload), so they have
|
|
222
|
+
no Bun install step. A changed pyproject.toml is not synced, so Tilt rebuilds the image instead.
|
|
223
|
+
"""
|
|
206
224
|
live_update_rules = []
|
|
207
225
|
for sync_path in syncs:
|
|
208
|
-
|
|
226
|
+
# Python images never read the npm/Bun registry config, and the engine does not
|
|
227
|
+
# generate it for provider-owned languages, so there is nothing to sync.
|
|
228
|
+
if language == 'python' and (sync_path.endswith('.npmrc.docker.autogenerated') or sync_path.endswith('bunfig.docker.autogenerated.toml')):
|
|
229
|
+
continue
|
|
230
|
+
full_sync_path = _live_update_sync_source(project_root, full_res_path, sync_path)
|
|
209
231
|
dest = '/app/' + full_res_path + '/' + sync_path
|
|
210
232
|
live_update_rules.append(sync(full_sync_path, dest))
|
|
211
233
|
|
|
234
|
+
if language == 'python':
|
|
235
|
+
return live_update_rules
|
|
236
|
+
|
|
212
237
|
package_json_path = '/app/' + full_res_path + '/package.json'
|
|
213
238
|
bun_lock_path = '/app/' + full_res_path + '/bun.lock'
|
|
214
239
|
live_update_rules.append(run('cd /app/' + full_res_path + ' && bun install',
|
|
@@ -373,7 +398,9 @@ def _register_single_resource(config, resource_path, compose_project_name, auto_
|
|
|
373
398
|
live_update_rules = _build_live_update_rules(
|
|
374
399
|
config['res_path'],
|
|
375
400
|
config['res_path'],
|
|
376
|
-
config['syncs']
|
|
401
|
+
config['syncs'],
|
|
402
|
+
project_root=config.get('project_root', ''),
|
|
403
|
+
language=config.get('manifest', {}).get('language', 'bun'),
|
|
377
404
|
)
|
|
378
405
|
_register_docker_build(config, live_update_rules)
|
|
379
406
|
if defers_start:
|
|
@@ -310,6 +310,18 @@ def _generate_all_configs_for_resource(
|
|
|
310
310
|
|
|
311
311
|
app_type = manifest.get('appType', 'backend')
|
|
312
312
|
is_frontend = resource_config.get('frontend', False) or app_type == 'frontend'
|
|
313
|
+
# Python backends own their runtime files (package/pyproject, Dockerfile,
|
|
314
|
+
# reload command). Bun-only configs below (Vite, tsconfig, Prisma, bunfig) are skipped.
|
|
315
|
+
provider_owned = Docker.is_provider_owned_language(manifest)
|
|
316
|
+
if provider_owned:
|
|
317
|
+
# Fail before generating anything, with the missing file named, instead of a Docker
|
|
318
|
+
# COPY error halfway through the image build.
|
|
319
|
+
root = ctx.get('project_root', '')
|
|
320
|
+
prefix = (root.rstrip('/') + '/' if root else '') + resource_path + '/'
|
|
321
|
+
for required in ['pyproject.toml', 'src/main.py']:
|
|
322
|
+
if not os.path.exists(prefix + required):
|
|
323
|
+
fail("{}: language '{}' requires {} but {}{} does not exist".format(
|
|
324
|
+
resource_name, manifest.get('language'), required, resource_path + '/', required))
|
|
313
325
|
|
|
314
326
|
# ==========================================================================
|
|
315
327
|
# 1. VITE CONFIG
|
|
@@ -319,7 +331,7 @@ def _generate_all_configs_for_resource(
|
|
|
319
331
|
Vite.for_manifest(manifest, backend_manifest, write_file)
|
|
320
332
|
else:
|
|
321
333
|
Vite.for_manifest(manifest, None, write_file)
|
|
322
|
-
elif app_type == 'backend':
|
|
334
|
+
elif app_type == 'backend' and not provider_owned:
|
|
323
335
|
Vite.for_manifest(manifest, None, write_file)
|
|
324
336
|
elif app_type == 'sdk':
|
|
325
337
|
Vite.for_manifest(manifest, None, write_file)
|
|
@@ -365,6 +377,12 @@ def _generate_all_configs_for_resource(
|
|
|
365
377
|
use_nginx=use_nginx,
|
|
366
378
|
use_golden=should_enable('golden-image')
|
|
367
379
|
)
|
|
380
|
+
elif provider_owned:
|
|
381
|
+
dockerfile_content = Docker.backend_language(
|
|
382
|
+
res_path=resource_path,
|
|
383
|
+
language=manifest.get('language'),
|
|
384
|
+
port=manifest.get('port', 4000),
|
|
385
|
+
)
|
|
368
386
|
else:
|
|
369
387
|
dockerfile_content = Docker.backend(
|
|
370
388
|
res_path=resource_path,
|
|
@@ -419,7 +437,7 @@ def _generate_all_configs_for_resource(
|
|
|
419
437
|
resource_path, write_file, internal_deps=internal_deps_map, is_docker=False,
|
|
420
438
|
framework=manifest.get('framework', 'react'),
|
|
421
439
|
)
|
|
422
|
-
|
|
440
|
+
elif not provider_owned:
|
|
423
441
|
prisma_path = resource_config.get('prisma_client_path', './node_modules/.prisma/client')
|
|
424
442
|
resource_features = manifest.get('featuresEnabled', [])
|
|
425
443
|
TSConfig.backend(resource_path, prisma_path, write_file, internal_deps=internal_deps_map, is_docker=True, features=resource_features)
|
|
@@ -434,10 +452,11 @@ def _generate_all_configs_for_resource(
|
|
|
434
452
|
# 8. PACKAGE CONFIGS (npmrc, bunfig)
|
|
435
453
|
# ==========================================================================
|
|
436
454
|
verdaccio_enabled = global_config.get('verdaccio_enabled', True)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
455
|
+
if not provider_owned:
|
|
456
|
+
PackageConfig.npmrc(resource_path=resource_path, registry_url=global_config['verdaccio_url_docker'], is_docker=True, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
|
|
457
|
+
PackageConfig.bunfig(resource_path=resource_path, registry_url=global_config['verdaccio_url_docker'], is_docker=True, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
|
|
458
|
+
PackageConfig.npmrc(resource_path=resource_path, registry_url=global_config['verdaccio_url_local'], is_docker=False, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
|
|
459
|
+
PackageConfig.bunfig(resource_path=resource_path, registry_url=global_config['verdaccio_url_local'], is_docker=False, write_fn=write_file, verdaccio_enabled=verdaccio_enabled)
|
|
441
460
|
|
|
442
461
|
# ==========================================================================
|
|
443
462
|
# 9. YAML MANIFEST (for verification)
|