@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.
@@ -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
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.74",
4
+ "version": "1.3.76",
5
5
  "description": "TDK CLI - Tilt Development Kit command line interface",
6
6
  "keywords": [
7
7
  "tdk",
@@ -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
- for _INFRA in ['init-networks', 'postgres', 'traefik', 'golden-layers-build']:
283
- if _INFRA in _KNOWN_TILT_RESOURCES:
284
- _FILTERED_RESOURCES.append(_INFRA)
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.docker.ts (Docker build)
206
- # - vite.config.build.ts (production build)
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/React application\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library",
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", "src/main.tsx"],
561
+ "frontend": ["package.json", "index.html", frontend_entry],
561
562
  "library": ["package.json"],
562
563
  }.get(app_type, ["package.json"])
563
564