@flisk-sh/flisk 1.4.2 → 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.
- package/CHANGELOG.md +4 -0
- package/README.md +119 -43
- package/cli/dist/backend-languages/bun.js +5 -0
- package/cli/dist/backend-languages/go.js +3 -1
- package/cli/dist/cli.js +9 -0
- package/cli/dist/commands/completion.js +8 -1
- package/cli/dist/commands/config.js +130 -55
- package/cli/dist/commands/doctor.js +16 -15
- package/cli/dist/commands/logs.js +1 -0
- package/cli/dist/commands/project.js +1 -0
- package/cli/dist/commands/resource.js +39 -11
- package/cli/dist/commands/status.js +18 -9
- package/cli/dist/commands/up.js +102 -17
- package/cli/dist/frontend-frameworks/next.js +197 -0
- package/cli/dist/frontend-frameworks/react.js +81 -12
- package/cli/dist/frontend-frameworks/registry.js +5 -4
- package/cli/dist/frontend-frameworks/vue.js +82 -12
- package/cli/dist/generator/resource-templates.js +2 -4
- package/cli/dist/generator/template-engine.js +5 -3
- package/cli/dist/utils/cold-preflight.js +3 -2
- package/cli/dist/utils/config-explain.js +197 -0
- package/cli/dist/utils/demo-reset.js +22 -0
- package/cli/dist/utils/discovery-paths.js +7 -1
- package/cli/dist/utils/host-port-plan.js +8 -3
- package/cli/dist/utils/port-conflict-fix.js +16 -14
- package/cli/dist/utils/port-occupant.js +35 -0
- package/cli/dist/utils/project-features.js +7 -16
- package/cli/dist/utils/project-templates.js +8 -8
- package/cli/dist/utils/resource-features.js +5 -5
- package/cli/dist/utils/service-manifest.js +1 -0
- package/cli/dist/utils/service-urls.js +11 -0
- package/cli/dist/utils/startup-report.js +4 -3
- package/cli/dist/utils/tar.js +33 -28
- package/cli/dist/utils/tilt-process.js +4 -3
- package/cli/dist/utils/windows-doctor.js +18 -15
- package/cli/dist-bundle/{chunk-IGN5UC3P.js → chunk-4J2FLFUA.js} +14 -7
- package/cli/dist-bundle/{chunk-ETKUVKJ2.js → chunk-BMHPPX2Q.js} +10 -10
- package/cli/dist-bundle/chunk-BP2M4OJ4.js +2 -0
- package/cli/dist-bundle/{chunk-53L2JN7G.js → chunk-CGTWXWBU.js} +1 -1
- package/cli/dist-bundle/chunk-GPUJ55MI.js +3 -0
- package/cli/dist-bundle/{chunk-5QBYM33Q.js → chunk-LUUJ4JB7.js} +1 -1
- package/cli/dist-bundle/chunk-PEZQEZVV.js +17 -0
- package/cli/dist-bundle/{chunk-MWOVYIBB.js → chunk-SFOFTVYM.js} +5 -5
- package/cli/dist-bundle/{chunk-3XODDLRJ.js → chunk-TIYTGN5R.js} +1 -1
- package/cli/dist-bundle/{chunk-YNCBBJ4V.js → chunk-U42XZWXB.js} +1 -1
- package/cli/dist-bundle/chunk-ULMQVD7J.js +122 -0
- package/cli/dist-bundle/chunk-V36PT2HF.js +24 -0
- package/cli/dist-bundle/{chunk-I7TGIK5N.js → chunk-VMVTECCK.js} +1 -1
- package/cli/dist-bundle/{chunk-DU2BN46C.js → chunk-Z5QEE4SW.js} +1 -1
- package/cli/dist-bundle/cli.js +607 -375
- package/cli/dist-bundle/cold-preflight-3BNDOLGS.js +2 -0
- package/cli/dist-bundle/{demo-stage-OHNTP2UJ.js → demo-stage-G4W23BXW.js} +1 -1
- package/cli/dist-bundle/{extension-fetch-VEFSCG4B.js → extension-fetch-KIPYDIRM.js} +1 -1
- package/cli/dist-bundle/{open-XROJ6YEY.js → open-HQFT6WDC.js} +1 -1
- package/cli/dist-bundle/project-JZO2CPZZ.js +2 -0
- package/cli/dist-bundle/{project-templates-EYSQG2EO.js → project-templates-IMJ3OX5I.js} +1 -1
- package/cli/dist-bundle/{services-BIMF6MVU.js → services-GLFKZBBZ.js} +1 -1
- package/cli/dist-bundle/{tilt-port-owner-ZY5M2JC4.js → tilt-port-owner-PJN4MPYE.js} +1 -1
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/runtime/engine/topologies/platform/docker/compose/compose.star +40 -4
- package/runtime/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +6 -0
- package/runtime/engine/topologies/platform/docker/dockerfile/next_dockerfile.star +83 -0
- package/runtime/engine/topologies/platform/docker/index.star +4 -0
- package/runtime/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +7 -0
- package/runtime/engine/topologies/platform/docker/networking/traefik.star +85 -46
- package/runtime/engine/topologies/tilt/common/utils_output.star +1 -0
- package/runtime/engine/topologies/tilt/generators/validators.star +1 -1
- package/runtime/engine/topologies/tilt/generators/vite/frontend.star +2 -2
- package/runtime/engine/topologies/tilt/generators/vite/generators.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/constants.star +3 -0
- package/runtime/engine/topologies/tilt/manifest/parser.star +9 -3
- package/runtime/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +8 -3
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +50 -24
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/env.star +11 -2
- package/runtime/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +23 -9
- package/cli/dist-bundle/chunk-43XWL2PB.js +0 -61
- package/cli/dist-bundle/chunk-5F7YP5IR.js +0 -17
- package/cli/dist-bundle/chunk-K6SOCO6V.js +0 -3
- package/cli/dist-bundle/chunk-YHYRGWUH.js +0 -24
- package/cli/dist-bundle/chunk-YL56J2WX.js +0 -2
- package/cli/dist-bundle/cold-preflight-ZOPR7PWJ.js +0 -2
- package/cli/dist-bundle/project-7PTIOHGF.js +0 -2
|
@@ -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:
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
//
|
|
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:
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
125
|
+
return `import { describe, it } from 'vitest';
|
|
126
126
|
|
|
127
127
|
describe('${name}', () => {
|
|
128
|
-
it('
|
|
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 (
|
|
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 = [];
|
|
@@ -61,7 +61,7 @@ export function requireVerifiedDockerVersions(result) {
|
|
|
61
61
|
fix: result.fix ?? "Start Docker and ensure Docker Engine 25+ and Compose 2.20.2+ are installed.",
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
function usesPrisma(projectRoot) {
|
|
64
|
+
export function usesPrisma(projectRoot) {
|
|
65
65
|
try {
|
|
66
66
|
return discoverResourcesFromRoot(projectRoot).some((resource) => {
|
|
67
67
|
return resource.config?.featuresEnabled?.includes("prisma") ?? false;
|
|
@@ -120,9 +120,10 @@ export async function runColdPreflight(opts = {}) {
|
|
|
120
120
|
const bunPath = bunNeeded ? findOnPath("bun") : undefined;
|
|
121
121
|
if (bunPath) {
|
|
122
122
|
try {
|
|
123
|
+
// A cold start of bun can pass 3s on a busy machine; a timeout there reads as "not installed".
|
|
123
124
|
const version = execFileSync(bunPath, ["--version"], {
|
|
124
125
|
encoding: "utf-8",
|
|
125
|
-
timeout:
|
|
126
|
+
timeout: 10_000,
|
|
126
127
|
}).trim();
|
|
127
128
|
bun = bunMeetsFloor(version);
|
|
128
129
|
}
|