@flisk-sh/flisk 1.4.1 → 1.4.5

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +421 -92
  3. package/cli/dist/backend-languages/bun.js +5 -0
  4. package/cli/dist/backend-languages/go.js +3 -1
  5. package/cli/dist/cli.js +9 -0
  6. package/cli/dist/commands/completion.js +8 -1
  7. package/cli/dist/commands/config.js +130 -55
  8. package/cli/dist/commands/doctor.js +16 -15
  9. package/cli/dist/commands/logs.js +1 -0
  10. package/cli/dist/commands/project.js +1 -0
  11. package/cli/dist/commands/resource.js +39 -11
  12. package/cli/dist/commands/status.js +18 -9
  13. package/cli/dist/commands/up.js +102 -17
  14. package/cli/dist/frontend-frameworks/next.js +197 -0
  15. package/cli/dist/frontend-frameworks/react.js +81 -12
  16. package/cli/dist/frontend-frameworks/registry.js +5 -4
  17. package/cli/dist/frontend-frameworks/vue.js +82 -12
  18. package/cli/dist/generator/resource-templates.js +2 -4
  19. package/cli/dist/generator/template-engine.js +5 -3
  20. package/cli/dist/utils/cold-preflight.js +3 -2
  21. package/cli/dist/utils/config-explain.js +197 -0
  22. package/cli/dist/utils/demo-reset.js +22 -0
  23. package/cli/dist/utils/discovery-paths.js +7 -1
  24. package/cli/dist/utils/host-port-plan.js +8 -3
  25. package/cli/dist/utils/port-conflict-fix.js +16 -14
  26. package/cli/dist/utils/port-occupant.js +35 -0
  27. package/cli/dist/utils/project-features.js +7 -16
  28. package/cli/dist/utils/project-templates.js +8 -8
  29. package/cli/dist/utils/resource-features.js +5 -5
  30. package/cli/dist/utils/service-manifest.js +1 -0
  31. package/cli/dist/utils/service-urls.js +11 -0
  32. package/cli/dist/utils/startup-report.js +4 -3
  33. package/cli/dist/utils/tar.js +33 -28
  34. package/cli/dist/utils/tilt-process.js +4 -3
  35. package/cli/dist/utils/windows-doctor.js +18 -15
  36. package/cli/dist-bundle/{chunk-UJXR6MZ2.js → chunk-4J2FLFUA.js} +14 -7
  37. package/cli/dist-bundle/{chunk-SRN64OPO.js → chunk-BMHPPX2Q.js} +10 -10
  38. package/cli/dist-bundle/chunk-BP2M4OJ4.js +2 -0
  39. package/cli/dist-bundle/{chunk-TZNXL3IO.js → chunk-CGTWXWBU.js} +1 -1
  40. package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
  41. package/cli/dist-bundle/{chunk-KJOCVAMQ.js → chunk-LUUJ4JB7.js} +1 -1
  42. package/cli/dist-bundle/chunk-PEZQEZVV.js +17 -0
  43. package/cli/dist-bundle/{chunk-7HR5PVOD.js → chunk-SFOFTVYM.js} +5 -5
  44. package/cli/dist-bundle/{chunk-RX6O3PG4.js → chunk-TIYTGN5R.js} +1 -1
  45. package/cli/dist-bundle/{chunk-BB5BZ37Y.js → chunk-U42XZWXB.js} +1 -1
  46. package/cli/dist-bundle/chunk-ULMQVD7J.js +122 -0
  47. package/cli/dist-bundle/chunk-V36PT2HF.js +24 -0
  48. package/cli/dist-bundle/{chunk-7IXBBU27.js → chunk-VMVTECCK.js} +1 -1
  49. package/cli/dist-bundle/{chunk-M2R464RM.js → chunk-Z5QEE4SW.js} +1 -1
  50. package/cli/dist-bundle/cli.js +607 -375
  51. package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +2 -0
  52. package/cli/dist-bundle/{demo-stage-HIHO4VV3.js → demo-stage-G4W23BXW.js} +1 -1
  53. package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
  54. package/cli/dist-bundle/{open-W3QX35R6.js → open-HQFT6WDC.js} +1 -1
  55. package/cli/dist-bundle/project-JZO2CPZZ.js +2 -0
  56. package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-IMJ3OX5I.js} +1 -1
  57. package/cli/dist-bundle/{services-RGTQ2LXC.js → services-GLFKZBBZ.js} +1 -1
  58. package/cli/dist-bundle/{tilt-port-owner-6DO7OJRR.js → tilt-port-owner-PJN4MPYE.js} +1 -1
  59. package/cli/package.json +1 -1
  60. package/package.json +2 -1
  61. package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
  62. package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
  63. package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
  64. package/runtime/engine/topologies/platform/docker/index.star +4 -0
  65. package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
  66. package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
  67. package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
  68. package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
  69. package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
  70. package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
  71. package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
  72. package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
  73. package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -3
  74. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
  75. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
  76. package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
  77. package/cli/dist-bundle/chunk-DUNCELUK.js +0 -24
  78. package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
  79. package/cli/dist-bundle/chunk-L6U555CJ.js +0 -61
  80. package/cli/dist-bundle/chunk-VRESY4NR.js +0 -17
  81. package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
  82. package/cli/dist-bundle/cold-preflight-BBDDHWRV.js +0 -2
  83. package/cli/dist-bundle/project-FW5LL2KM.js +0 -2
@@ -24,10 +24,11 @@ import { nativeWindowsStackRefusal } from "../utils/native-windows.js";
24
24
  import { findProjectRoot, getPackageVersion } from "../utils/paths.js";
25
25
  import { findAvailablePort } from "../utils/port-assignment.js";
26
26
  import { formatPortFallbackNotice } from "../utils/port-fallback-notice.js";
27
+ import { findPortOccupant } from "../utils/port-occupant.js";
27
28
  import { isApiServiceType } from "../utils/resource-kind.js";
28
29
  import { enforceSchemaVersionGate } from "../utils/schema-version-gate.js";
29
30
  import { enforceServiceConfigGate, warnSchemaVersions } from "../utils/service-config-checks.js";
30
- import { appendHealthPath, resolveSubdomainBases } from "../utils/service-urls.js";
31
+ import { appendHealthPath, resolveServiceUrl, resolveSubdomainBases, } from "../utils/service-urls.js";
31
32
  import { discoverResourcesStrict, discoverResourcesWithProblems, discoverStacks, getResourcesForStack, stackExists, warnAboutSkippedFiles, } from "../utils/services.js";
32
33
  import { buildSmokePlans, formatSmokeFailure, runSmokePlans, safeSmokeDeps, } from "../utils/smoke.js";
33
34
  import { buildStartupReport, formatStartupReport, isStartupStalled, } from "../utils/startup-report.js";
@@ -38,16 +39,53 @@ import { parseTiltPort, resolveTiltPort, secondUpAction, stopTiltForUp, } from "
38
39
  import { findUnknownServices, resolveOnlySelection } from "../utils/up-only.js";
39
40
  import { tiltGetUiResources, waitForTiltResourcesReady } from "../utils/up-readiness.js";
40
41
  import { enableDiscoveredStacks } from "./project.js";
41
- export function formatUpSuccess(port, appUrls = []) {
42
- return [
43
- "Flisk is up.",
44
- `Tilt UI: http://localhost:${port}`,
45
- "App URLs:",
46
- ...(appUrls.length > 0
47
- ? appUrls.slice(0, 5).map((url) => ` ${url}`)
48
- : [" run: flisk networks"]),
49
- "Stop: flisk down",
50
- ];
42
+ export function formatUpSuccess(port, options = {}) {
43
+ const { appUrls = [], serviceHealth = [], showLogsHint = serviceHealth.length > 0 } = options;
44
+ const lines = [];
45
+ lines.push("Flisk is up.");
46
+ lines.push(`Tilt UI: http://localhost:${port}`);
47
+ if (serviceHealth.length > 0) {
48
+ lines.push("Services:");
49
+ for (const svc of serviceHealth.slice(0, 10)) {
50
+ const icon = svc.status === "ready" ? "✓" : svc.status === "failed" ? "✗" : "⟳";
51
+ const urlPart = svc.url ? ` → ${svc.url}` : "";
52
+ lines.push(` ${icon} ${svc.name}${urlPart}`);
53
+ }
54
+ if (serviceHealth.length > 10) {
55
+ lines.push(` … and ${serviceHealth.length - 10} more`);
56
+ }
57
+ // Health check URLs for test infrastructure and monitoring
58
+ lines.push("Health checks:");
59
+ for (const svc of serviceHealth.slice(0, 10)) {
60
+ const icon = svc.status === "ready" ? "✓" : svc.status === "failed" ? "✗" : "⟳";
61
+ if (svc.url) {
62
+ const healthUrl = svc.url.endsWith("/health") ? svc.url : `${svc.url}/health`;
63
+ lines.push(` ${icon} ${svc.name}: ${healthUrl}`);
64
+ }
65
+ }
66
+ if (serviceHealth.length > 10) {
67
+ lines.push(` … and ${serviceHealth.length - 10} more`);
68
+ }
69
+ }
70
+ else if (appUrls.length > 0) {
71
+ lines.push("App URLs:");
72
+ for (const url of appUrls.slice(0, 5)) {
73
+ lines.push(` ${url}`);
74
+ }
75
+ if (appUrls.length > 5) {
76
+ lines.push(` … and ${appUrls.length - 5} more`);
77
+ }
78
+ }
79
+ else {
80
+ lines.push("App URLs:");
81
+ lines.push(" run: flisk networks");
82
+ }
83
+ lines.push("Stop: flisk down");
84
+ if (showLogsHint) {
85
+ lines.push("Logs: flisk logs <service>");
86
+ }
87
+ lines.push("Next: flisk help # or visit https://flisk.sh/docs");
88
+ return lines;
51
89
  }
52
90
  /**
53
91
  * The native Windows boundary for a command that starts a stack.
@@ -366,8 +404,13 @@ export const upCommand = new Command("up")
366
404
  console.log(chalk.gray(`Refreshed runtime assets: ${copiedAssets.join(", ")}`));
367
405
  }
368
406
  const newlyEnabledStacks = enableDiscoveredStacks(projectRoot, discoveredResources);
369
- if (newlyEnabledStacks.length > 0 && !options.quiet) {
370
- console.log(chalk.gray(`Added ${newlyEnabledStacks.join(", ")} to the pre_alpha phase in .flisk/project.json`));
407
+ if (newlyEnabledStacks.length > 0) {
408
+ // project.json just changed, so the generated spec.master is now stale. Regenerate it here, or the next
409
+ // `flisk config verify` reports it out of sync.
410
+ await generateMasterConfigs(projectRoot, { quiet: options.quiet });
411
+ if (!options.quiet) {
412
+ console.log(chalk.gray(`Added ${newlyEnabledStacks.join(", ")} to the pre_alpha phase in .flisk/project.json`));
413
+ }
371
414
  }
372
415
  }
373
416
  let servicesToStart;
@@ -413,7 +456,7 @@ export const upCommand = new Command("up")
413
456
  console.log(chalk.blue("\n🌍 Frontend URLs:"));
414
457
  for (const svc of frontends) {
415
458
  const basePath = svc.config?.basePath ?? `/${svc.name}`;
416
- console.log(chalk.gray(` - ${svc.name}: ${appBase}${chalk.cyan(appendHealthPath(basePath))}`));
459
+ console.log(chalk.gray(` - ${svc.name}: ${appBase}${chalk.cyan(basePath)}`));
417
460
  }
418
461
  }
419
462
  if (backends.length > 0) {
@@ -605,14 +648,35 @@ export const upCommand = new Command("up")
605
648
  blocked: [],
606
649
  starting: [],
607
650
  };
608
- const reportLines = formatStartupReport(report, readiness.timedOut);
651
+ const reportLines = formatStartupReport(report, readiness.timedOut, (port) => findPortOccupant(port));
609
652
  if (reportLines.length > 0) {
610
653
  showError(reportLines.join("\n"));
611
654
  }
612
655
  }
613
656
  }
614
657
  if (uiReady && !options.quiet && !startupFailed) {
615
- for (const line of formatUpSuccess(port))
658
+ const deferred = getDeferredResourceNames(discoveredResources);
659
+ const dependsOn = Object.fromEntries(discoveredResources.map((r) => [r.name, r.config?.dependsOn ?? []]));
660
+ const readiness = await waitForTiltResourcesReady(port, {
661
+ deferred,
662
+ isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
663
+ });
664
+ const serviceHealth = readiness.enabled.map((name) => {
665
+ const failed = readiness.failures.find((f) => f.name === name);
666
+ const svc = discoveredResources.find((r) => r.name === name);
667
+ const isReady = !failed && !readiness.failures.some((f) => f.name === name);
668
+ const isStarting = !isReady && !failed;
669
+ const url = svc ? resolveServiceUrl(svc, hostPortPlan.ingressHttp) : undefined;
670
+ return {
671
+ name,
672
+ status: failed ? "failed" : isStarting ? "starting" : "ready",
673
+ url,
674
+ };
675
+ });
676
+ for (const line of formatUpSuccess(port, {
677
+ serviceHealth,
678
+ showLogsHint: true,
679
+ }))
616
680
  console.log(chalk.blue(line));
617
681
  printedSuccess = true;
618
682
  }
@@ -673,7 +737,28 @@ export const upCommand = new Command("up")
673
737
  process.exit(1);
674
738
  }
675
739
  if (!options.quiet && result.exitCode === 0 && !printedSuccess) {
676
- for (const line of formatUpSuccess(port))
740
+ const deferred = getDeferredResourceNames(discoveredResources);
741
+ const dependsOn = Object.fromEntries(discoveredResources.map((r) => [r.name, r.config?.dependsOn ?? []]));
742
+ const readiness = await waitForTiltResourcesReady(port, {
743
+ deferred,
744
+ isStalled: (text) => isStartupStalled(text, dependsOn, deferred),
745
+ });
746
+ const serviceHealth = readiness.enabled.map((name) => {
747
+ const failed = readiness.failures.find((f) => f.name === name);
748
+ const svc = discoveredResources.find((r) => r.name === name);
749
+ const isReady = !failed && !readiness.failures.some((f) => f.name === name);
750
+ const isStarting = !isReady && !failed;
751
+ const url = svc ? resolveServiceUrl(svc, hostPortPlan.ingressHttp) : undefined;
752
+ return {
753
+ name,
754
+ status: failed ? "failed" : isStarting ? "starting" : "ready",
755
+ url,
756
+ };
757
+ });
758
+ for (const line of formatUpSuccess(port, {
759
+ serviceHealth,
760
+ showLogsHint: true,
761
+ }))
677
762
  console.log(chalk.blue(line));
678
763
  }
679
764
  };
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Next.js (App Router) is a `server` provider: it owns its build and dev server, so there is no Vite config and no nginx.
3
+ * Under `flisk up` the engine runs `next dev` in a Node image (live reload through Tilt), served under `/<name>/` by Traefik.
4
+ * The engine writes `.autogenerated/.env.frontend.autogenerated`; the generated Dockerfile copies it to `.env`, so
5
+ * `next.config.mjs` and the page read `FLISK_BASE_PATH` and `NEXT_PUBLIC_API_URL` without any wiring in the app.
6
+ */
7
+ const NEXT_CONFIG = `// FLISK_BASE_PATH is set by flisk up (the route this app is served under, for example /web). On the host it is unset,
8
+ // so \`next dev\` serves from the root.
9
+ const basePath = process.env.FLISK_BASE_PATH ?? '';
10
+
11
+ /** @type {import('next').NextConfig} */
12
+ const nextConfig = {
13
+ basePath,
14
+ // Flisk's route is /<name>/; with trailingSlash the page answers there instead of redirecting.
15
+ trailingSlash: true,
16
+ output: 'standalone',
17
+ reactStrictMode: true,
18
+ };
19
+
20
+ export default nextConfig;
21
+ `;
22
+ const STARTER_STYLES = `const page = { maxWidth: 640, margin: '0 auto', padding: '3rem 1.5rem', fontFamily: 'system-ui, sans-serif', color: '#1f2933' };
23
+ const muted = { color: '#616e7c', marginTop: 0 };
24
+ `;
25
+ function renderPage(name, backendName) {
26
+ return `${backendName ? "import HealthCard from './health-card';\n\n" : ""}${STARTER_STYLES}
27
+ export default function Home() {
28
+ return (
29
+ <main style={page}>
30
+ <h1>${name}</h1>
31
+ <p style={muted}>Flisk frontend (Next.js)</p>
32
+ ${backendName ? "\n <HealthCard />\n" : ""}
33
+ <p>Edit <code>src/app/page.tsx</code> and save. It live-reloads.</p>
34
+ <ul>
35
+ <li><a href="http://localhost:10350">Tilt UI</a>: logs and resource status</li>
36
+ <li><code>flisk status</code>: every resource in this project</li>
37
+ </ul>
38
+ </main>
39
+ );
40
+ }
41
+ `;
42
+ }
43
+ function renderHealthCard(backendName) {
44
+ return `'use client';
45
+
46
+ import { useEffect, useState } from 'react';
47
+
48
+ // Set by flisk up from the backend's route; empty on the host, where the browser calls the same origin.
49
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? '';
50
+ const BACKEND_NAME = '${backendName}';
51
+
52
+ const card = { border: '1px solid #d9e2ec', borderRadius: 8, padding: '1rem 1.25rem', margin: '1.5rem 0' };
53
+ const cardHeader = { display: 'flex', justifyContent: 'space-between', alignItems: 'center' };
54
+ const pre = { background: '#f5f7fa', borderRadius: 6, padding: '0.75rem', overflowX: 'auto' as const };
55
+
56
+ interface Health {
57
+ ok: boolean;
58
+ body: unknown;
59
+ }
60
+
61
+ export default function HealthCard() {
62
+ const [health, setHealth] = useState<Health | null>(null);
63
+
64
+ useEffect(() => {
65
+ const controller = new AbortController();
66
+ fetch(API_URL + '/health', { signal: controller.signal })
67
+ .then(async (response) => {
68
+ // Read the body as text first: a non-JSON error page must show its real status, not an unreachable backend.
69
+ const text = await response.text();
70
+ let body: unknown = text;
71
+ try {
72
+ body = JSON.parse(text);
73
+ } catch {
74
+ // keep the raw text
75
+ }
76
+ setHealth({ ok: response.ok, body: { status: response.status, body } });
77
+ })
78
+ .catch((error: unknown) => {
79
+ if (!controller.signal.aborted) setHealth({ ok: false, body: { error: String(error) } });
80
+ });
81
+ return () => controller.abort();
82
+ }, []);
83
+
84
+ return (
85
+ <section style={card}>
86
+ <div style={cardHeader}>
87
+ <strong>Backend: {BACKEND_NAME}</strong>
88
+ {health === null && <span>checking…</span>}
89
+ {health?.ok === true && <span style={{ color: '#0a7f3f' }}>ok</span>}
90
+ {health?.ok === false && <span style={{ color: '#b42318' }}>unreachable</span>}
91
+ </div>
92
+ {health && <pre style={pre}>{JSON.stringify(health.body, null, 2)}</pre>}
93
+ </section>
94
+ );
95
+ }
96
+ `;
97
+ }
98
+ export const nextFrontendProvider = {
99
+ id: "next",
100
+ label: "Next.js (App Router)",
101
+ kind: "server",
102
+ dependencies: {
103
+ next: "^15.5.0",
104
+ react: "^19.0.0",
105
+ "react-dom": "^19.0.0",
106
+ },
107
+ devDependencies: {
108
+ "@types/react": "^19.0.0",
109
+ "@types/react-dom": "^19.0.0",
110
+ // `next build` type-checks through the TypeScript JavaScript API.
111
+ typescript: "^5.9.0",
112
+ // The image installs with npm, which enforces peer ranges: vitest 5 peers on @types/node ^22 || >=24, and the
113
+ // scaffold's ^20 otherwise fails `npm install` with ERESOLVE.
114
+ "@types/node": "^22.0.0",
115
+ },
116
+ compilerOptions: {},
117
+ scripts: {
118
+ dev: "next dev",
119
+ build: "next build",
120
+ start: "next start",
121
+ },
122
+ tsconfig: {
123
+ compilerOptions: {
124
+ target: "ES2022",
125
+ lib: ["dom", "dom.iterable", "esnext"],
126
+ allowJs: true,
127
+ skipLibCheck: true,
128
+ strict: true,
129
+ noEmit: true,
130
+ esModuleInterop: true,
131
+ module: "esnext",
132
+ moduleResolution: "bundler",
133
+ resolveJsonModule: true,
134
+ isolatedModules: true,
135
+ jsx: "preserve",
136
+ incremental: true,
137
+ plugins: [{ name: "next" }],
138
+ },
139
+ include: ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts"],
140
+ exclude: ["node_modules"],
141
+ },
142
+ createFiles(name, options = {}) {
143
+ return [
144
+ {
145
+ filename: "next.config.mjs",
146
+ content: NEXT_CONFIG,
147
+ description: "Generating Next.js config",
148
+ emoji: "⚙️",
149
+ },
150
+ {
151
+ filename: "src/app/layout.tsx",
152
+ content: `import type { ReactNode } from 'react';
153
+
154
+ export const metadata = { title: '${name}' };
155
+
156
+ export default function RootLayout({ children }: { children: ReactNode }) {
157
+ return (
158
+ <html lang="en">
159
+ <body>{children}</body>
160
+ </html>
161
+ );
162
+ }
163
+ `,
164
+ description: "Generating Next.js root layout",
165
+ emoji: "💻",
166
+ },
167
+ {
168
+ filename: "src/app/page.tsx",
169
+ content: renderPage(name, options.backendName),
170
+ description: "Generating Next.js home page",
171
+ emoji: "💻",
172
+ },
173
+ {
174
+ filename: "src/app/health/route.ts",
175
+ content: `// The engine probes /<name>/health; the base path puts this route under the same prefix.
176
+ export const dynamic = 'force-static';
177
+
178
+ export function GET() {
179
+ return new Response('ok');
180
+ }
181
+ `,
182
+ description: "Generating health route",
183
+ emoji: "🩺",
184
+ },
185
+ ...(options.backendName
186
+ ? [
187
+ {
188
+ filename: "src/app/health-card.tsx",
189
+ content: renderHealthCard(options.backendName),
190
+ description: "Generating backend health card",
191
+ emoji: "💻",
192
+ },
193
+ ]
194
+ : []),
195
+ ];
196
+ },
197
+ };
@@ -1,3 +1,80 @@
1
+ const STARTER_STYLES = `const page = { maxWidth: 640, margin: '0 auto', padding: '3rem 1.5rem', fontFamily: 'system-ui, sans-serif', color: '#1f2933' };
2
+ const muted = { color: '#616e7c', marginTop: 0 };
3
+ const card = { border: '1px solid #d9e2ec', borderRadius: 8, padding: '1rem 1.25rem', margin: '1.5rem 0' };
4
+ const cardHeader = { display: 'flex', justifyContent: 'space-between', alignItems: 'center' };
5
+ const pre = { background: '#f5f7fa', borderRadius: 6, padding: '0.75rem', overflowX: 'auto' };
6
+ `;
7
+ /** Hints and links shared by both starters. They need no backend, so they appear in every frontend. */
8
+ const STARTER_HINTS = ` <p>Edit <code>src/App.tsx</code> and save. It live-reloads.</p>
9
+ <ul>
10
+ <li><a href="http://localhost:10350">Tilt UI</a>: logs and resource status</li>
11
+ <li><code>flisk status</code>: every resource in this project</li>
12
+ </ul>`;
13
+ function renderAppWithBackend(name, backendName) {
14
+ return `/// <reference types="vite/client" />
15
+ import { useEffect, useState } from 'react';
16
+
17
+ const API_URL = import.meta.env.VITE_API_URL ?? '';
18
+ const BACKEND_NAME = '${backendName}';
19
+
20
+ ${STARTER_STYLES}
21
+ interface Health {
22
+ ok: boolean;
23
+ body: unknown;
24
+ }
25
+
26
+ function App() {
27
+ const [health, setHealth] = useState<Health | null>(null);
28
+
29
+ useEffect(() => {
30
+ const controller = new AbortController();
31
+ fetch(API_URL + '/health', { signal: controller.signal })
32
+ .then(async (response) => setHealth({ ok: response.ok, body: await response.json() }))
33
+ .catch((error: unknown) => {
34
+ if (!controller.signal.aborted) setHealth({ ok: false, body: { error: String(error) } });
35
+ });
36
+ return () => controller.abort();
37
+ }, []);
38
+
39
+ return (
40
+ <main style={page}>
41
+ <h1>${name}</h1>
42
+ <p style={muted}>Flisk frontend</p>
43
+
44
+ <section style={card}>
45
+ <div style={cardHeader}>
46
+ <strong>Backend: {BACKEND_NAME}</strong>
47
+ {health === null && <span>checking…</span>}
48
+ {health?.ok === true && <span style={{ color: '#0a7f3f' }}>ok</span>}
49
+ {health?.ok === false && <span style={{ color: '#b42318' }}>unreachable</span>}
50
+ </div>
51
+ {health && <pre>{JSON.stringify(health.body, null, 2)}</pre>}
52
+ </section>
53
+
54
+ ${STARTER_HINTS}
55
+ </main>
56
+ );
57
+ }
58
+
59
+ export default App;
60
+ `;
61
+ }
62
+ function renderAppWithoutBackend(name) {
63
+ return `${STARTER_STYLES}
64
+ function App() {
65
+ return (
66
+ <main style={page}>
67
+ <h1>${name}</h1>
68
+ <p style={muted}>Flisk frontend</p>
69
+
70
+ ${STARTER_HINTS}
71
+ </main>
72
+ );
73
+ }
74
+
75
+ export default App;
76
+ `;
77
+ }
1
78
  export const reactFrontendProvider = {
2
79
  id: "react",
3
80
  label: "React + Vite",
@@ -13,7 +90,7 @@ export const reactFrontendProvider = {
13
90
  compilerOptions: {
14
91
  jsx: "react-jsx",
15
92
  },
16
- createFiles(name) {
93
+ createFiles(name, options = {}) {
17
94
  return [
18
95
  {
19
96
  filename: "index.html",
@@ -50,17 +127,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
50
127
  },
51
128
  {
52
129
  filename: "src/App.tsx",
53
- content: `function App() {
54
- return (
55
- <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
56
- <h1>${name}</h1>
57
- <p>Frontend resource created with Flisk</p>
58
- </div>
59
- );
60
- }
61
-
62
- export default App;
63
- `,
130
+ content: options.backendName
131
+ ? renderAppWithBackend(name, options.backendName)
132
+ : renderAppWithoutBackend(name),
64
133
  description: "Generating React app",
65
134
  emoji: "💻",
66
135
  },
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { FliskError } from "../utils/errors.js";
4
4
  import { litFrontendProvider } from "./lit.js";
5
+ import { nextFrontendProvider } from "./next.js";
5
6
  import { preactFrontendProvider } from "./preact.js";
6
7
  import { qwikFrontendProvider } from "./qwik.js";
7
8
  import { reactFrontendProvider } from "./react.js";
@@ -21,11 +22,11 @@ export const FRONTEND_FRAMEWORKS = {
21
22
  qwik: qwikFrontendProvider,
22
23
  vanilla: vanillaFrontendProvider,
23
24
  "tanstack-router": tanstackRouterFrontendProvider,
25
+ next: nextFrontendProvider,
24
26
  };
25
- // Frameworks that own their own server or build config. They are never scaffolded as Vite SPA providers; they go through
26
- // `flisk resource <name> --type bring-your-own` instead.
27
+ // Meta-frameworks that are not registered providers. They are never scaffolded as Vite SPA providers; they go through
28
+ // `flisk resource <name> --type bring-your-own` instead. Next.js is a registered `server` provider, so it is not listed here.
27
29
  export const META_FRAMEWORK_IDS = [
28
- "next",
29
30
  "nuxt",
30
31
  "sveltekit",
31
32
  "astro",
@@ -49,7 +50,7 @@ export function listFrontendFrameworks() {
49
50
  return Object.entries(FRONTEND_FRAMEWORKS).map(([id, provider]) => ({
50
51
  id,
51
52
  label: provider.label,
52
- kind: "vite-spa",
53
+ kind: provider.kind ?? "vite-spa",
53
54
  verified: VERIFIED_FRONTEND_FRAMEWORKS.includes(id),
54
55
  command: `flisk resource <name> --type frontend --framework ${id}`,
55
56
  }));
@@ -1,3 +1,81 @@
1
+ const STARTER_STYLE = `<style scoped>
2
+ main { max-width: 640px; margin: 0 auto; padding: 3rem 1.5rem; font-family: system-ui, sans-serif; color: #1f2933; }
3
+ .muted { color: #616e7c; margin-top: 0; }
4
+ .card { border: 1px solid #d9e2ec; border-radius: 8px; padding: 1rem 1.25rem; margin: 1.5rem 0; }
5
+ .card-header { display: flex; justify-content: space-between; align-items: center; }
6
+ .ok { color: #0a7f3f; }
7
+ .unreachable { color: #b42318; }
8
+ pre { background: #f5f7fa; border-radius: 6px; padding: 0.75rem; overflow-x: auto; }
9
+ </style>`;
10
+ /** Hints and links shared by both starters. They need no backend, so they appear in every frontend. */
11
+ const STARTER_HINTS = ` <p>Edit <code>src/App.vue</code> and save. It live-reloads.</p>
12
+ <ul>
13
+ <li><a href="http://localhost:10350">Tilt UI</a>: logs and resource status</li>
14
+ <li><code>flisk status</code>: every resource in this project</li>
15
+ </ul>`;
16
+ function renderAppWithBackend(name, backendName) {
17
+ return `<script setup lang="ts">
18
+ import { onMounted, ref } from 'vue';
19
+
20
+ const API_URL = import.meta.env.VITE_API_URL ?? '';
21
+ const BACKEND_NAME = '${backendName}';
22
+
23
+ interface Health {
24
+ ok: boolean;
25
+ body: unknown;
26
+ }
27
+
28
+ const health = ref<Health | null>(null);
29
+
30
+ onMounted(async () => {
31
+ try {
32
+ const response = await fetch(API_URL + '/health');
33
+ health.value = { ok: response.ok, body: await response.json() };
34
+ } catch (error) {
35
+ health.value = { ok: false, body: { error: String(error) } };
36
+ }
37
+ });
38
+ </script>
39
+
40
+ <template>
41
+ <main>
42
+ <h1>${name}</h1>
43
+ <p class="muted">Flisk frontend</p>
44
+
45
+ <section class="card">
46
+ <div class="card-header">
47
+ <strong>Backend: {{ BACKEND_NAME }}</strong>
48
+ <span v-if="health === null">checking…</span>
49
+ <span v-else-if="health.ok" class="ok">ok</span>
50
+ <span v-else class="unreachable">unreachable</span>
51
+ </div>
52
+ <pre v-if="health">{{ JSON.stringify(health.body, null, 2) }}</pre>
53
+ </section>
54
+
55
+ ${STARTER_HINTS}
56
+ </main>
57
+ </template>
58
+
59
+ ${STARTER_STYLE}
60
+ `;
61
+ }
62
+ function renderAppWithoutBackend(name) {
63
+ return `<script setup lang="ts">
64
+ const appName = '${name}';
65
+ </script>
66
+
67
+ <template>
68
+ <main>
69
+ <h1>{{ appName }}</h1>
70
+ <p class="muted">Flisk frontend</p>
71
+
72
+ ${STARTER_HINTS}
73
+ </main>
74
+ </template>
75
+
76
+ ${STARTER_STYLE}
77
+ `;
78
+ }
1
79
  export const vueFrontendProvider = {
2
80
  id: "vue",
3
81
  label: "Vue + Vite",
@@ -8,7 +86,7 @@ export const vueFrontendProvider = {
8
86
  "@vitejs/plugin-vue": "^5.2.4",
9
87
  },
10
88
  compilerOptions: {},
11
- createFiles(name) {
89
+ createFiles(name, options = {}) {
12
90
  return [
13
91
  {
14
92
  filename: "index.html",
@@ -40,17 +118,9 @@ createApp(App).mount('#app');
40
118
  },
41
119
  {
42
120
  filename: "src/App.vue",
43
- content: `<script setup lang="ts">
44
- const message = 'Frontend resource created with Flisk';
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
- `,
121
+ content: options.backendName
122
+ ? renderAppWithBackend(name, options.backendName)
123
+ : renderAppWithoutBackend(name),
54
124
  description: "Generating Vue app",
55
125
  emoji: "💻",
56
126
  },
@@ -122,12 +122,10 @@ main().catch((err) => {
122
122
  `;
123
123
  }
124
124
  export function getTestTemplate(name) {
125
- return `import { describe, it, expect } from 'vitest';
125
+ return `import { describe, it } from 'vitest';
126
126
 
127
127
  describe('${name}', () => {
128
- it('should pass a basic test', () => {
129
- expect(true).toBe(true);
130
- });
128
+ it.todo('add a real test for ${name}');
131
129
  });
132
130
  `;
133
131
  }
@@ -34,11 +34,13 @@ export function loadTemplate(filename) {
34
34
  const triedPaths = [];
35
35
  for (const templateDir of templateDirs) {
36
36
  const templatePath = path.join(templateDir, filename);
37
- triedPaths.push(templatePath);
38
37
  try {
39
38
  return fs.readFileSync(templatePath, "utf-8");
40
39
  }
41
- catch (_error) { }
40
+ catch (error) {
41
+ const reason = error instanceof Error ? error.message : String(error);
42
+ triedPaths.push(`${templatePath} (${reason})`);
43
+ }
42
44
  }
43
45
  throw new Error(`Failed to load template ${filename} from any of these paths: ${triedPaths.join(", ")}`);
44
46
  }
@@ -707,7 +709,7 @@ async function vendorFliskExtension(projectRoot, options = {}) {
707
709
  console.warn(`⚠️ Premium overlay skipped: ${err.message}`);
708
710
  }
709
711
  }
710
- const FLISK_GENERATED_MARKER = "SYSTEM-GENERATED BY Flisk CLI";
712
+ export const FLISK_GENERATED_MARKER = "SYSTEM-GENERATED BY Flisk CLI";
711
713
  export function verifyMasterConfigs(projectRoot, options) {
712
714
  const errors = [];
713
715
  const warnings = [];