@tdk-landscape/tdk-cli-core 1.3.74 → 1.3.76
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 +5 -2
- package/cli/dist/commands/doctor.js +67 -2
- package/cli/dist/commands/resource.js +62 -62
- package/cli/dist/commands/ui.js +27 -24
- package/cli/dist/frontend-frameworks/react.js +70 -0
- package/cli/dist/frontend-frameworks/registry.js +29 -0
- package/cli/dist/frontend-frameworks/types.js +2 -0
- package/cli/dist/frontend-frameworks/vue.js +73 -0
- package/cli/dist/utils/discovery-paths.js +67 -0
- package/cli/dist/utils/doctor-wiring.js +261 -0
- package/cli/dist/utils/errors.js +4 -1
- package/cli/dist/utils/project-templates.js +4 -0
- package/cli/package.json +1 -1
- package/cli/templates/Tiltfile.hbs +24 -3
- package/engine/AGENTS.md +5 -4
- package/engine/schemas/service-schema.json +9 -1
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +26 -19
- package/engine/topologies/tilt/generators/validators.star +2 -1
- package/engine/topologies/tilt/generators/vite/frameworks/react.star +126 -0
- package/engine/topologies/tilt/generators/vite/frameworks/vue.star +92 -0
- package/engine/topologies/tilt/generators/vite/frontend.star +21 -4
- package/engine/topologies/tilt/generators/vite/templates.star +0 -283
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +8 -2
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const vueFrontendProvider = {
|
|
2
|
+
id: "vue",
|
|
3
|
+
label: "Vue + Vite",
|
|
4
|
+
dependencies: {
|
|
5
|
+
vue: "^3.5.0",
|
|
6
|
+
},
|
|
7
|
+
devDependencies: {
|
|
8
|
+
"@vitejs/plugin-vue": "^5.2.4",
|
|
9
|
+
},
|
|
10
|
+
compilerOptions: {},
|
|
11
|
+
createFiles(name) {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
filename: "index.html",
|
|
15
|
+
content: `<!DOCTYPE html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
20
|
+
<title>${name}</title>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<div id="app"></div>
|
|
24
|
+
<script type="module" src="/src/main.ts"></script>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
27
|
+
`,
|
|
28
|
+
description: "Generating HTML template",
|
|
29
|
+
emoji: "💻",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
filename: "src/main.ts",
|
|
33
|
+
content: `import { createApp } from 'vue';
|
|
34
|
+
import App from './App.vue';
|
|
35
|
+
|
|
36
|
+
createApp(App).mount('#app');
|
|
37
|
+
`,
|
|
38
|
+
description: "Generating Vue entry",
|
|
39
|
+
emoji: "💻",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
filename: "src/App.vue",
|
|
43
|
+
content: `<script setup lang="ts">
|
|
44
|
+
const message = 'Frontend resource created with TDK';
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<main style="padding: 2rem; font-family: system-ui">
|
|
49
|
+
<h1>${name}</h1>
|
|
50
|
+
<p>{{ message }}</p>
|
|
51
|
+
</main>
|
|
52
|
+
</template>
|
|
53
|
+
`,
|
|
54
|
+
description: "Generating Vue app",
|
|
55
|
+
emoji: "💻",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
filename: "src/vite-env.d.ts",
|
|
59
|
+
content: `/// <reference types="vite/client" />
|
|
60
|
+
|
|
61
|
+
declare module '*.vue' {
|
|
62
|
+
import type { Component } from 'vue';
|
|
63
|
+
const component: Component;
|
|
64
|
+
export default component;
|
|
65
|
+
}
|
|
66
|
+
`,
|
|
67
|
+
description: "Generating Vue module types",
|
|
68
|
+
emoji: "⚙️",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=vue.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
// Mirrors discovery/json_manifest_scanner.star: every `discovery.paths` entry is a shell
|
|
4
|
+
// glob of directories, and `find -maxdepth 3` looks for service.json under each match. So a
|
|
5
|
+
// resource is only seen by Tilt if some directory matching a pattern is the resource
|
|
6
|
+
// directory itself or at most this many levels above it.
|
|
7
|
+
const MAX_LEVELS_BELOW_MATCH = 2;
|
|
8
|
+
const DEFAULT_DISCOVERY_PATHS = ["services/*/*"];
|
|
9
|
+
function segments(path) {
|
|
10
|
+
return path.split(sep).filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
function segmentMatches(pattern, name) {
|
|
13
|
+
const source = pattern
|
|
14
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
15
|
+
.replace(/\*/g, "[^/]*")
|
|
16
|
+
.replace(/\?/g, "[^/]");
|
|
17
|
+
return new RegExp(`^${source}$`).test(name);
|
|
18
|
+
}
|
|
19
|
+
function globMatchesDir(patternSegments, dirSegments) {
|
|
20
|
+
return (patternSegments.length === dirSegments.length &&
|
|
21
|
+
patternSegments.every((segment, index) => segmentMatches(segment, dirSegments[index])));
|
|
22
|
+
}
|
|
23
|
+
/** `discovery.paths` from .tdk/project.json, or the generic default when unset/unreadable. */
|
|
24
|
+
export function readDiscoveryPaths(projectRoot) {
|
|
25
|
+
const configPath = join(projectRoot, ".tdk", "project.json");
|
|
26
|
+
if (!existsSync(configPath))
|
|
27
|
+
return DEFAULT_DISCOVERY_PATHS;
|
|
28
|
+
try {
|
|
29
|
+
const paths = JSON.parse(readFileSync(configPath, "utf-8"))?.discovery?.paths;
|
|
30
|
+
if (Array.isArray(paths) && paths.every((p) => typeof p === "string"))
|
|
31
|
+
return paths;
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
return DEFAULT_DISCOVERY_PATHS;
|
|
35
|
+
}
|
|
36
|
+
/** Whether Tilt's discovery would find a service.json in `resourceDir` given `patterns`. */
|
|
37
|
+
export function isPathDiscovered(projectRoot, resourceDir, patterns) {
|
|
38
|
+
const resourceSegments = segments(relative(projectRoot, resolve(projectRoot, resourceDir)));
|
|
39
|
+
return patterns.some((pattern) => {
|
|
40
|
+
const patternSegments = segments(relative(projectRoot, resolve(projectRoot, pattern)));
|
|
41
|
+
for (let levels = 0; levels <= MAX_LEVELS_BELOW_MATCH; levels++) {
|
|
42
|
+
const depth = resourceSegments.length - levels;
|
|
43
|
+
if (depth > 0 && globMatchesDir(patternSegments, resourceSegments.slice(0, depth))) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Picks the folder for a new resource without an explicit --path. The conventional folder
|
|
52
|
+
* per type (apps/, workers/, packages/) is kept when discovery covers it; otherwise the
|
|
53
|
+
* resource goes under services/<stack>/, which the default discovery paths cover, because a
|
|
54
|
+
* resource outside every discovery path is scaffolded but never started by `tdk up`.
|
|
55
|
+
*/
|
|
56
|
+
export function chooseResourcePath(projectRoot, conventionalPath, stack, name) {
|
|
57
|
+
const patterns = readDiscoveryPaths(projectRoot);
|
|
58
|
+
if (isPathDiscovered(projectRoot, conventionalPath, patterns)) {
|
|
59
|
+
return { path: conventionalPath };
|
|
60
|
+
}
|
|
61
|
+
const fallback = `services/${stack}/${name}`;
|
|
62
|
+
if (isPathDiscovered(projectRoot, fallback, patterns)) {
|
|
63
|
+
return { path: fallback, adjustedFrom: conventionalPath };
|
|
64
|
+
}
|
|
65
|
+
return { path: conventionalPath };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=discovery-paths.js.map
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { formatCount } from "./formatting.js";
|
|
5
|
+
import { findProjectRoot } from "./paths.js";
|
|
6
|
+
import { discoverResourcesFromRoot } from "./services.js";
|
|
7
|
+
/**
|
|
8
|
+
* Checks for wiring mistakes that only show up minutes into `tdk up`: an image
|
|
9
|
+
* build that cannot find package.json, a token check that cannot reach its
|
|
10
|
+
* identity provider, a frontend calling a port nothing publishes, a queue
|
|
11
|
+
* feature with no broker. Each one is decidable from the files on disk.
|
|
12
|
+
*/
|
|
13
|
+
const EXEC_TIMEOUT_MS = 10_000;
|
|
14
|
+
const SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue)$/;
|
|
15
|
+
const TEST_FILE = /(?:\.test\.|\.spec\.|\/tests?\/|\/__tests__\/)/;
|
|
16
|
+
const SKIPPED_DIRS = new Set(["node_modules", ".autogenerated", "dist", "tests", "__tests__"]);
|
|
17
|
+
function projectName(projectRoot) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(readFileSync(join(projectRoot, ".tdk", "project.json"), "utf-8"));
|
|
20
|
+
return typeof parsed?.project?.name === "string" ? parsed.project.name : undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function readParams(configPath) {
|
|
27
|
+
try {
|
|
28
|
+
const params = JSON.parse(readFileSync(configPath, "utf-8")).params;
|
|
29
|
+
return params && typeof params === "object" ? params : {};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Backends are the only resources that listen on their `port`. */
|
|
36
|
+
function backendPorts(resources) {
|
|
37
|
+
const ports = new Map();
|
|
38
|
+
for (const resource of resources) {
|
|
39
|
+
const port = resource.config?.port;
|
|
40
|
+
if (resource.config?.appType === "backend" && typeof port === "number") {
|
|
41
|
+
ports.set(resource.name, port);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return ports;
|
|
45
|
+
}
|
|
46
|
+
export function checkResourcePackageJson(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
47
|
+
const missing = discoverResourcesFromRoot(projectRoot)
|
|
48
|
+
.filter((resource) => !existsSync(join(resource.path, "package.json")))
|
|
49
|
+
.map((resource) => resource.name);
|
|
50
|
+
if (missing.length === 0) {
|
|
51
|
+
return {
|
|
52
|
+
name: "Resource package.json",
|
|
53
|
+
didPass: true,
|
|
54
|
+
message: "Every resource has a package.json",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
name: "Resource package.json",
|
|
59
|
+
didPass: false,
|
|
60
|
+
message: `${formatCount(missing.length, "resource")} without a package.json: ${missing.join(", ")}. The image build stops at "COPY .../package.json: not found"`,
|
|
61
|
+
fix: "Add a package.json next to each service.json (copy one from a working resource), or delete the folder's service.json if it is not a service",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const CONTAINER_URL = /https?:\/\/([a-z][a-z0-9-]*):(\d+)/g;
|
|
65
|
+
export function checkServiceUrlPorts(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
66
|
+
const resources = discoverResourcesFromRoot(projectRoot);
|
|
67
|
+
const ports = backendPorts(resources);
|
|
68
|
+
const byPort = new Map([...ports].map(([name, port]) => [port, name]));
|
|
69
|
+
const problems = [];
|
|
70
|
+
for (const resource of resources) {
|
|
71
|
+
for (const [key, value] of Object.entries(readParams(resource.configPath))) {
|
|
72
|
+
for (const [, host, portText] of String(value).matchAll(CONTAINER_URL)) {
|
|
73
|
+
const port = Number(portText);
|
|
74
|
+
const expected = ports.get(host);
|
|
75
|
+
if (expected !== undefined && expected !== port) {
|
|
76
|
+
problems.push(`${resource.name} param ${key}: ${host}:${port}, but ${host} listens on ${expected}`);
|
|
77
|
+
}
|
|
78
|
+
else if ((host === "localhost" || host === "127.0.0.1") &&
|
|
79
|
+
byPort.has(port) &&
|
|
80
|
+
byPort.get(port) !== resource.name) {
|
|
81
|
+
problems.push(`${resource.name} param ${key}: localhost:${port} is ${resource.name} itself inside its container; ${byPort.get(port)} is reached as ${byPort.get(port)}:${port}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (problems.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
name: "Service URL ports",
|
|
89
|
+
didPass: true,
|
|
90
|
+
message: "Service URLs in params point at the ports the services listen on",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
name: "Service URL ports",
|
|
95
|
+
didPass: false,
|
|
96
|
+
message: `${formatCount(problems.length, "service URL")} in params that cannot connect:\n ${problems.join("\n ")}`,
|
|
97
|
+
fix: 'TDK runs each backend on the "port" in its service.json, and containers reach each other by resource name (http://<resource>:<port>). Use that port in the URL',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function listSourceFiles(dir) {
|
|
101
|
+
const files = [];
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return files;
|
|
108
|
+
}
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const path = join(dir, entry.name);
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
if (!SKIPPED_DIRS.has(entry.name))
|
|
113
|
+
files.push(...listSourceFiles(path));
|
|
114
|
+
}
|
|
115
|
+
else if (SOURCE_EXTENSIONS.test(entry.name) && !TEST_FILE.test(path)) {
|
|
116
|
+
files.push(path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
const LOCALHOST_PORT = /(?:localhost|127\.0\.0\.1):(\d{2,5})\b/g;
|
|
122
|
+
const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*|<!--|#)/;
|
|
123
|
+
export function checkFrontendBackendUrls(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
124
|
+
const resources = discoverResourcesFromRoot(projectRoot);
|
|
125
|
+
const byPort = new Map([...backendPorts(resources)].map(([name, port]) => [port, name]));
|
|
126
|
+
const problems = [];
|
|
127
|
+
for (const resource of resources.filter((r) => r.config?.appType === "frontend")) {
|
|
128
|
+
for (const file of listSourceFiles(join(resource.path, "src"))) {
|
|
129
|
+
readFileSync(file, "utf-8")
|
|
130
|
+
.split("\n")
|
|
131
|
+
.forEach((line, index) => {
|
|
132
|
+
if (COMMENT_LINE.test(line))
|
|
133
|
+
return;
|
|
134
|
+
for (const [, portText] of line.matchAll(LOCALHOST_PORT)) {
|
|
135
|
+
const backend = byPort.get(Number(portText));
|
|
136
|
+
if (backend) {
|
|
137
|
+
problems.push(`${relative(projectRoot, file)}:${index + 1} calls localhost:${portText} (${backend})`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (problems.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
name: "Frontend backend URLs",
|
|
146
|
+
didPass: true,
|
|
147
|
+
message: "No frontend calls a backend through a localhost port",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const project = projectName(projectRoot) ?? "<project>";
|
|
151
|
+
return {
|
|
152
|
+
name: "Frontend backend URLs",
|
|
153
|
+
didPass: false,
|
|
154
|
+
message: `${formatCount(problems.length, "frontend call")} to a backend port that TDK does not publish on localhost:\n ${problems.join("\n ")}`,
|
|
155
|
+
fix: `Backends are only reachable through Traefik: http://api.${project}.localhost/api/<name without -api>/... (for example catalog-api is /api/catalog). Use that URL, or the generated .autogenerated/env.ts`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function checkNatsBroker(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
159
|
+
const users = discoverResourcesFromRoot(projectRoot)
|
|
160
|
+
.filter((resource) => resource.config?.featuresEnabled?.includes("nats"))
|
|
161
|
+
.map((resource) => resource.name);
|
|
162
|
+
if (users.length === 0) {
|
|
163
|
+
return {
|
|
164
|
+
name: "NATS broker",
|
|
165
|
+
didPass: true,
|
|
166
|
+
message: "No resource enables the nats feature",
|
|
167
|
+
isSkipped: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (existsSync(join(projectRoot, "services", "platform", "messaging", "docker-compose.yml"))) {
|
|
171
|
+
return {
|
|
172
|
+
name: "NATS broker",
|
|
173
|
+
didPass: true,
|
|
174
|
+
message: `${formatCount(users.length, "resource")} enable nats and services/platform/messaging/docker-compose.yml is present`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const name = (projectName(projectRoot) ?? "project").replace(/-/g, "_");
|
|
178
|
+
return {
|
|
179
|
+
name: "NATS broker",
|
|
180
|
+
didPass: false,
|
|
181
|
+
message: `${users.join(", ")} enable the nats feature, but no NATS server will start: TDK only starts one when services/platform/messaging/docker-compose.yml exists, and it does not generate that file`,
|
|
182
|
+
fix: `Create it with a "nats" service (image nats with -js, container_name ${name}_nats, network ${name}_backend, external) and a "redis" service, which TDK registers alongside it`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function parseTiltProcesses(psOutput) {
|
|
186
|
+
const processes = [];
|
|
187
|
+
for (const line of psOutput.split("\n")) {
|
|
188
|
+
const match = line.match(/^\s*(\d+)\s+.*\btilt up\b.*?\s-f\s+(\S+)/);
|
|
189
|
+
if (!match)
|
|
190
|
+
continue;
|
|
191
|
+
processes.push({ pid: Number(match[1]), root: match[2].split("/.tdk/")[0] });
|
|
192
|
+
}
|
|
193
|
+
return processes;
|
|
194
|
+
}
|
|
195
|
+
export function checkTiltInstances(projectRoot = findProjectRoot() ?? process.cwd(), exec = execSync) {
|
|
196
|
+
let output;
|
|
197
|
+
try {
|
|
198
|
+
output = exec("ps -Ao pid=,command=", { encoding: "utf-8", timeout: EXEC_TIMEOUT_MS });
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return {
|
|
202
|
+
name: "Tilt processes",
|
|
203
|
+
didPass: true,
|
|
204
|
+
isSkipped: true,
|
|
205
|
+
message: "Could not list processes",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const running = parseTiltProcesses(output);
|
|
209
|
+
const mine = running.filter((p) => p.root === projectRoot);
|
|
210
|
+
const others = running.filter((p) => p.root !== projectRoot);
|
|
211
|
+
if (mine.length > 1) {
|
|
212
|
+
return {
|
|
213
|
+
name: "Tilt processes",
|
|
214
|
+
didPass: false,
|
|
215
|
+
message: `${mine.length} Tilt processes are running for this project (pids ${mine.map((p) => p.pid).join(", ")}). They fight over ports and containers`,
|
|
216
|
+
fix: `Stop them all, then start one: tdk down && kill ${mine.map((p) => p.pid).join(" ")} && tdk up`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (others.length > 0) {
|
|
220
|
+
const list = others.map((p) => `${p.root} (pid ${p.pid})`).join(", ");
|
|
221
|
+
return {
|
|
222
|
+
name: "Tilt processes",
|
|
223
|
+
didPass: true,
|
|
224
|
+
isSkipped: true,
|
|
225
|
+
message: `Another project's Tilt is running: ${list}`,
|
|
226
|
+
fix: "tdk up will use the next free Tilt UI port (10351, ...). If it is a leftover, run tdk down in that project",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return { name: "Tilt processes", didPass: true, message: "No stray Tilt processes" };
|
|
230
|
+
}
|
|
231
|
+
export function checkDockerNetworkCapacity(exec = execSync) {
|
|
232
|
+
const probe = `tdk_doctor_probe_${process.pid}`;
|
|
233
|
+
try {
|
|
234
|
+
exec(`docker network create ${probe}`, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const stderr = String(error.stderr ?? error);
|
|
238
|
+
if (/address pools/i.test(stderr)) {
|
|
239
|
+
return {
|
|
240
|
+
name: "Docker networks",
|
|
241
|
+
didPass: false,
|
|
242
|
+
message: "Docker cannot create another network: all predefined address pools are in use. Each TDK project needs several",
|
|
243
|
+
fix: "docker network prune (removes unused networks), or run tdk down in projects you are not using",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
name: "Docker networks",
|
|
248
|
+
didPass: true,
|
|
249
|
+
isSkipped: true,
|
|
250
|
+
message: "Could not test network creation",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
exec(`docker network rm ${probe}`, { stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// A leftover probe network is harmless and tdk_doctor_probe_* is easy to prune.
|
|
258
|
+
}
|
|
259
|
+
return { name: "Docker networks", didPass: true, message: "Docker can create networks" };
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=doctor-wiring.js.map
|
package/cli/dist/utils/errors.js
CHANGED
|
@@ -12,7 +12,7 @@ export function logVerbose(message, err) {
|
|
|
12
12
|
console.warn(chalk.gray(`${message}${errMsg}`));
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
class TdkError extends Error {
|
|
15
|
+
export class TdkError extends Error {
|
|
16
16
|
suggestions;
|
|
17
17
|
exitCode;
|
|
18
18
|
constructor(message, suggestions = [], exitCode = 1) {
|
|
@@ -78,6 +78,9 @@ export function requireProjectRoot() {
|
|
|
78
78
|
return projectRoot;
|
|
79
79
|
}
|
|
80
80
|
function handleCommandError(err) {
|
|
81
|
+
if (err instanceof TdkError) {
|
|
82
|
+
err.exit();
|
|
83
|
+
}
|
|
81
84
|
console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
|
|
82
85
|
process.exit(1);
|
|
83
86
|
}
|
|
@@ -16,6 +16,10 @@ export const PROJECT_TEMPLATES = {
|
|
|
16
16
|
repo: "https://github.com/tdk-landscape/tdk-user-management.git",
|
|
17
17
|
description: "User/auth management services",
|
|
18
18
|
},
|
|
19
|
+
ecommerce: {
|
|
20
|
+
repo: "https://github.com/tdk-landscape/tdk-ecommerce-example.git",
|
|
21
|
+
description: "Vue 3 storefront and Hono catalog API (needs TDK 1.3.75+ for --framework vue)",
|
|
22
|
+
},
|
|
19
23
|
example: {
|
|
20
24
|
repo: "https://github.com/tdk-landscape/tdk-example.git",
|
|
21
25
|
description: "Minimal Project-Stack-Resource (PSR) demonstration",
|
package/cli/package.json
CHANGED
|
@@ -279,9 +279,30 @@ if FOCUS_MODE and FOCUS_ENABLED_RESOURCES:
|
|
|
279
279
|
else:
|
|
280
280
|
_MATCHED = [kr for kr in _KNOWN_TILT_RESOURCES if kr != 'service-discovery-daemon' and kr.startswith(_R + '-')]
|
|
281
281
|
_FILTERED_RESOURCES.extend(_MATCHED)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
# NATS is gated by the same 'database-management' should_enable() check as
|
|
283
|
+
# postgres (see infra-loader.star), so it belongs with the infra that is
|
|
284
|
+
# always kept enabled here -- otherwise config.set_enabled_resources()
|
|
285
|
+
# below disables it on every out-of-the-box `tdk up` (pre-alpha always
|
|
286
|
+
# activates this focus-filter branch), and any resource that enables the
|
|
287
|
+
# 'nats' feature never gets a broker to connect to.
|
|
288
|
+
#
|
|
289
|
+
# services/platform/messaging/docker-compose.yml is user-authored (TDK
|
|
290
|
+
# does not generate it) and may declare more than nats/redis -- e.g. a
|
|
291
|
+
# local mail catcher standing in for a managed email provider. Every
|
|
292
|
+
# service it declares is loaded via docker_compose() in infra-loader.star,
|
|
293
|
+
# so it must be enabled here too, or it is silently disabled the same way.
|
|
294
|
+
_ALWAYS_ENABLED_INFRA_RESOURCES = ['init-networks', 'postgres', 'traefik', 'golden-layers-build']
|
|
295
|
+
_messaging_compose_path = PROJECT_ROOT + '/services/platform/messaging/docker-compose.yml'
|
|
296
|
+
if str(local("test -f '" + _messaging_compose_path + "' && echo yes || echo no", quiet=True, echo_off=True)).strip() == 'yes':
|
|
297
|
+
# infra-loader.star only creates these Tilt resources when this file
|
|
298
|
+
# exists, so only add them here when they actually exist to add.
|
|
299
|
+
_messaging_compose_doc = read_yaml(_messaging_compose_path, default={})
|
|
300
|
+
for _messaging_svc_name in _messaging_compose_doc.get('services', {}).keys():
|
|
301
|
+
_ALWAYS_ENABLED_INFRA_RESOURCES.append(_messaging_svc_name)
|
|
302
|
+
if 'nats' not in _ALWAYS_ENABLED_INFRA_RESOURCES:
|
|
303
|
+
_ALWAYS_ENABLED_INFRA_RESOURCES.append('nats')
|
|
304
|
+
for _INFRA in _ALWAYS_ENABLED_INFRA_RESOURCES:
|
|
305
|
+
_FILTERED_RESOURCES.append(_INFRA)
|
|
285
306
|
for _KR in _KNOWN_TILT_RESOURCES:
|
|
286
307
|
if _KR.startswith('provision-db-'):
|
|
287
308
|
_FILTERED_RESOURCES.append(_KR)
|
package/engine/AGENTS.md
CHANGED
|
@@ -200,10 +200,11 @@ def generate_for_manifest(manifest, backend_manifest, write_fn):
|
|
|
200
200
|
elif app_type == 'sdk':
|
|
201
201
|
return _generate_sdk(manifest, write_fn)
|
|
202
202
|
|
|
203
|
-
# Generates:
|
|
204
|
-
# - vite.config.ts (local development)
|
|
205
|
-
# - vite.config.
|
|
206
|
-
#
|
|
203
|
+
# Generates under each frontend resource's .autogenerated/ directory:
|
|
204
|
+
# - vite.config.frontend.autogenerated.ts (local development)
|
|
205
|
+
# - vite.config.build.autogenerated.ts (production build)
|
|
206
|
+
# The CLI scaffolds root index.html; nginx generates nginx.autogenerated.conf
|
|
207
|
+
# and serves the built index.html.
|
|
207
208
|
```
|
|
208
209
|
|
|
209
210
|
**TSConfig Generator:**
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"type": "string"
|
|
87
87
|
},
|
|
88
88
|
"appType": {
|
|
89
|
-
"description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite
|
|
89
|
+
"description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite single-page application (React or Vue, see framework)\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library",
|
|
90
90
|
"enum": [
|
|
91
91
|
"backend",
|
|
92
92
|
"frontend",
|
|
@@ -199,6 +199,14 @@
|
|
|
199
199
|
"type": "array",
|
|
200
200
|
"uniqueItems": true
|
|
201
201
|
},
|
|
202
|
+
"framework": {
|
|
203
|
+
"description": "Frontend SPA provider used to generate Vite and TypeScript config. Only meaningful for frontend resources; omitted means react (legacy manifests).",
|
|
204
|
+
"enum": [
|
|
205
|
+
"react",
|
|
206
|
+
"vue"
|
|
207
|
+
],
|
|
208
|
+
"type": "string"
|
|
209
|
+
},
|
|
202
210
|
"healthCheckPath": {
|
|
203
211
|
"default": "/health",
|
|
204
212
|
"description": "HTTP endpoint for health checks. Must return 200 OK.",
|
|
@@ -18,7 +18,7 @@ if _is_verbose:
|
|
|
18
18
|
# Frontend TSConfig Generator
|
|
19
19
|
# =============================================================================
|
|
20
20
|
|
|
21
|
-
def generate_frontend_tsconfig(resource_path, write_fn, internal_deps=None, is_docker=True):
|
|
21
|
+
def generate_frontend_tsconfig(resource_path, write_fn, internal_deps=None, is_docker=True, framework='react'):
|
|
22
22
|
"""
|
|
23
23
|
Generate tsconfig for frontend services.
|
|
24
24
|
|
|
@@ -27,6 +27,7 @@ def generate_frontend_tsconfig(resource_path, write_fn, internal_deps=None, is_d
|
|
|
27
27
|
write_fn: Write function for file output
|
|
28
28
|
internal_deps: Optional internal dependencies map
|
|
29
29
|
is_docker: Whether generating for Docker context
|
|
30
|
+
framework: Frontend framework id; omitted in legacy manifests means React
|
|
30
31
|
"""
|
|
31
32
|
# Check verbose mode
|
|
32
33
|
is_verbose = os.environ.get('TILT_LOG_LEVEL') == 'verbose'
|
|
@@ -76,25 +77,31 @@ def generate_frontend_tsconfig(resource_path, write_fn, internal_deps=None, is_d
|
|
|
76
77
|
resource_path
|
|
77
78
|
))
|
|
78
79
|
print(" Docker build will resolve @{npm_scope}/* from node_modules")
|
|
80
|
+
if framework not in ['react', 'vue']:
|
|
81
|
+
fail("Unknown frontend framework for TypeScript generation: " + str(framework))
|
|
82
|
+
|
|
83
|
+
compiler_options = {
|
|
84
|
+
"target": "ES2022",
|
|
85
|
+
"module": "ES2022",
|
|
86
|
+
"moduleResolution": "bundler",
|
|
87
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
88
|
+
"paths": paths,
|
|
89
|
+
"skipLibCheck": True,
|
|
90
|
+
"strict": True,
|
|
91
|
+
"esModuleInterop": True,
|
|
92
|
+
"allowSyntheticDefaultImports": True,
|
|
93
|
+
# "bun" (not "node"): matches backend_tsconfig.star - @types/bun is a direct
|
|
94
|
+
# devDependency so it's always linked at the top level under any linker mode
|
|
95
|
+
# (including Docker's isolated linker), while @types/node (when only pulled in
|
|
96
|
+
# transitively, e.g. via bun-types) isn't hoisted under the isolated linker and
|
|
97
|
+
# "types": ["node"] fails to resolve there even though it works locally.
|
|
98
|
+
"types": ["node"],
|
|
99
|
+
}
|
|
100
|
+
if framework == 'react':
|
|
101
|
+
compiler_options['jsx'] = 'react-jsx'
|
|
102
|
+
|
|
79
103
|
content = {
|
|
80
|
-
"compilerOptions":
|
|
81
|
-
"target": "ES2022",
|
|
82
|
-
"module": "ES2022",
|
|
83
|
-
"moduleResolution": "node",
|
|
84
|
-
"lib": ["dom", "dom.iterable", "esnext"],
|
|
85
|
-
"jsx": "react-jsx",
|
|
86
|
-
"paths": paths,
|
|
87
|
-
"skipLibCheck": True,
|
|
88
|
-
"strict": True,
|
|
89
|
-
"esModuleInterop": True,
|
|
90
|
-
"allowSyntheticDefaultImports": True,
|
|
91
|
-
# "bun" (not "node"): matches backend_tsconfig.star - @types/bun is a direct
|
|
92
|
-
# devDependency so it's always linked at the top level under any linker mode
|
|
93
|
-
# (including Docker's isolated linker), while @types/node (when only pulled in
|
|
94
|
-
# transitively, e.g. via bun-types) isn't hoisted under the isolated linker and
|
|
95
|
-
# "types": ["node"] fails to resolve there even though it works locally.
|
|
96
|
-
"types": ["node"],
|
|
97
|
-
},
|
|
104
|
+
"compilerOptions": compiler_options,
|
|
98
105
|
"exclude": [
|
|
99
106
|
"node_modules",
|
|
100
107
|
"dist",
|
|
@@ -555,9 +555,10 @@ def generate_resource_health_check(resource_name, resource_path, manifest, check
|
|
|
555
555
|
health_checks = []
|
|
556
556
|
|
|
557
557
|
# Check 1: Required files exist
|
|
558
|
+
frontend_entry = "src/main.ts" if manifest.get("framework", "react") == "vue" else "src/main.tsx"
|
|
558
559
|
required_files = {
|
|
559
560
|
"backend": ["package.json", "prisma/schema.prisma"],
|
|
560
|
-
"frontend": ["package.json", "index.html",
|
|
561
|
+
"frontend": ["package.json", "index.html", frontend_entry],
|
|
561
562
|
"library": ["package.json"],
|
|
562
563
|
}.get(app_type, ["package.json"])
|
|
563
564
|
|