swallowkit 1.1.1 → 2.0.0-alpha.1
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/README.ja.md +47 -7
- package/README.md +47 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +66 -21
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +4 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +108 -44
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -5
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +83 -39
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +44 -16
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +6 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +22 -0
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts +2 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +12 -0
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +7 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +7 -0
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/manifest.d.ts +3 -1
- package/dist/core/project/manifest.d.ts.map +1 -1
- package/dist/core/project/manifest.js +47 -25
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/react-project.d.ts +26 -0
- package/dist/core/project/react-project.d.ts.map +1 -0
- package/dist/core/project/react-project.js +422 -0
- package/dist/core/project/react-project.js.map +1 -0
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +32 -10
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
- package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-bff-generator.js +252 -0
- package/dist/core/scaffold/react-bff-generator.js.map +1 -0
- package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
- package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-ui-generator.js +41 -0
- package/dist/core/scaffold/react-ui-generator.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine/index.js +2 -2
- package/dist/machine/index.js.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +2 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +4 -1
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/agent-instructions.test.ts +1 -1
- package/src/__tests__/init.test.ts +29 -5
- package/src/__tests__/plan-apply.test.ts +101 -0
- package/src/__tests__/react.test.ts +819 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/cli/commands/add-auth.ts +67 -23
- package/src/cli/commands/dev.ts +109 -46
- package/src/cli/commands/init.ts +75 -35
- package/src/cli/commands/scaffold.ts +45 -20
- package/src/cli/index.ts +6 -3
- package/src/core/config.ts +21 -1
- package/src/core/operations/migration-generator.ts +35 -1
- package/src/core/project/agent-instructions.ts +14 -1
- package/src/core/project/agent-templates/AGENTS.md +24 -9
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- package/src/core/project/capabilities.ts +14 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/react-project.ts +507 -0
- package/src/core/project/validation.ts +32 -10
- package/src/core/scaffold/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/index.ts +3 -1
- package/src/machine/index.ts +2 -2
- package/src/types/index.ts +4 -0
- package/src/utils/package-manager.ts +5 -2
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { BackendLanguage } from "../../types";
|
|
4
|
+
import {
|
|
5
|
+
PackageManager,
|
|
6
|
+
getCommands,
|
|
7
|
+
getCiSetupStep,
|
|
8
|
+
getAzurePipelinesSetup,
|
|
9
|
+
} from "../../utils/package-manager";
|
|
10
|
+
import {
|
|
11
|
+
generateManagedFunctionConfig,
|
|
12
|
+
generateReactBffHelper,
|
|
13
|
+
generateReactGreetBff,
|
|
14
|
+
} from "../scaffold/react-bff-generator";
|
|
15
|
+
|
|
16
|
+
function write(
|
|
17
|
+
projectDir: string,
|
|
18
|
+
relativePath: string,
|
|
19
|
+
content: string
|
|
20
|
+
): void {
|
|
21
|
+
const destination = path.join(projectDir, ...relativePath.split(/[\\/]/));
|
|
22
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
23
|
+
fs.writeFileSync(destination, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildReactSwaConfig() {
|
|
27
|
+
return {
|
|
28
|
+
platform: { apiRuntime: "node:20" },
|
|
29
|
+
navigationFallback: {
|
|
30
|
+
rewrite: "/index.html",
|
|
31
|
+
exclude: [
|
|
32
|
+
"/api/*",
|
|
33
|
+
"/.auth/*",
|
|
34
|
+
"/assets/*",
|
|
35
|
+
"/*.{css,js,json,ico,png,svg,txt}",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
routes: [{ route: "/*", allowedRoles: ["anonymous"] }],
|
|
39
|
+
globalHeaders: {
|
|
40
|
+
"X-Content-Type-Options": "nosniff",
|
|
41
|
+
"X-Frame-Options": "DENY",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createReactProject(
|
|
47
|
+
projectDir: string,
|
|
48
|
+
projectName: string
|
|
49
|
+
): void {
|
|
50
|
+
write(
|
|
51
|
+
projectDir,
|
|
52
|
+
"package.json",
|
|
53
|
+
JSON.stringify(
|
|
54
|
+
{
|
|
55
|
+
name: projectName,
|
|
56
|
+
version: "0.1.0",
|
|
57
|
+
private: true,
|
|
58
|
+
scripts: { dev: "vite --port 3000", preview: "vite preview" },
|
|
59
|
+
dependencies: {
|
|
60
|
+
react: "^19.0.0",
|
|
61
|
+
"react-dom": "^19.0.0",
|
|
62
|
+
"react-router-dom": "^7.0.0",
|
|
63
|
+
},
|
|
64
|
+
devDependencies: {
|
|
65
|
+
"@types/node": "^22.0.0",
|
|
66
|
+
"@types/react": "^19.0.0",
|
|
67
|
+
"@types/react-dom": "^19.0.0",
|
|
68
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
69
|
+
vite: "^7.0.0",
|
|
70
|
+
typescript: "^5.9.0",
|
|
71
|
+
tailwindcss: "^4.0.0",
|
|
72
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
73
|
+
"@azure/static-web-apps-cli": "^2.0.0",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
write(
|
|
81
|
+
projectDir,
|
|
82
|
+
"tsconfig.json",
|
|
83
|
+
JSON.stringify(
|
|
84
|
+
{
|
|
85
|
+
compilerOptions: {
|
|
86
|
+
target: "ES2022",
|
|
87
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
88
|
+
module: "ESNext",
|
|
89
|
+
moduleResolution: "bundler",
|
|
90
|
+
jsx: "react-jsx",
|
|
91
|
+
strict: true,
|
|
92
|
+
noEmit: true,
|
|
93
|
+
esModuleInterop: true,
|
|
94
|
+
skipLibCheck: true,
|
|
95
|
+
resolveJsonModule: true,
|
|
96
|
+
paths: { "@/*": ["./*"] },
|
|
97
|
+
types: ["vite/client"],
|
|
98
|
+
},
|
|
99
|
+
include: ["src", "lib"],
|
|
100
|
+
exclude: ["node_modules", "functions", "bff", "shared", "dist"],
|
|
101
|
+
},
|
|
102
|
+
null,
|
|
103
|
+
2
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
write(
|
|
107
|
+
projectDir,
|
|
108
|
+
"vite.config.mts",
|
|
109
|
+
`import { defineConfig } from 'vite';
|
|
110
|
+
import react from '@vitejs/plugin-react';
|
|
111
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
112
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
113
|
+
|
|
114
|
+
export default defineConfig({
|
|
115
|
+
plugins: [react(), tailwindcss()],
|
|
116
|
+
resolve: { alias: {
|
|
117
|
+
'@': fileURLToPath(new URL('.', import.meta.url)),
|
|
118
|
+
'@${projectName}/shared': fileURLToPath(new URL('./shared/index.ts', import.meta.url)),
|
|
119
|
+
} },
|
|
120
|
+
server: { port: 3000, strictPort: true },
|
|
121
|
+
build: { outDir: 'dist' },
|
|
122
|
+
});
|
|
123
|
+
`
|
|
124
|
+
);
|
|
125
|
+
write(
|
|
126
|
+
projectDir,
|
|
127
|
+
"index.html",
|
|
128
|
+
`<!doctype html>
|
|
129
|
+
<html lang="en">
|
|
130
|
+
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>SwallowKit</title></head>
|
|
131
|
+
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
|
|
132
|
+
</html>
|
|
133
|
+
`
|
|
134
|
+
);
|
|
135
|
+
write(
|
|
136
|
+
projectDir,
|
|
137
|
+
"src\\main.tsx",
|
|
138
|
+
`import { StrictMode } from 'react';
|
|
139
|
+
import { createRoot } from 'react-dom/client';
|
|
140
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
141
|
+
import App from './App';
|
|
142
|
+
import './style.css';
|
|
143
|
+
|
|
144
|
+
createRoot(document.getElementById('root')!).render(
|
|
145
|
+
<StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>,
|
|
146
|
+
);
|
|
147
|
+
`
|
|
148
|
+
);
|
|
149
|
+
write(
|
|
150
|
+
projectDir,
|
|
151
|
+
"src\\App.tsx",
|
|
152
|
+
`import { useRoutes, type RouteObject } from 'react-router-dom';
|
|
153
|
+
import Home from './Home';
|
|
154
|
+
|
|
155
|
+
const modules = import.meta.glob<RouteObject[]>('./pages/**/routes.tsx', { eager: true, import: 'default' });
|
|
156
|
+
const pages = import.meta.glob<{ default: React.ComponentType }>('./pages/login.tsx', { eager: true });
|
|
157
|
+
const Login = pages['./pages/login.tsx']?.default;
|
|
158
|
+
|
|
159
|
+
export default function App() {
|
|
160
|
+
return useRoutes([
|
|
161
|
+
{ path: '/', element: <Home /> },
|
|
162
|
+
...(Login ? [{ path: '/login', element: <Login /> }] : []),
|
|
163
|
+
...Object.values(modules).flat(),
|
|
164
|
+
{ path: '*', element: <main>Page not found</main> },
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
`
|
|
168
|
+
);
|
|
169
|
+
write(
|
|
170
|
+
projectDir,
|
|
171
|
+
"src\\Home.tsx",
|
|
172
|
+
`import { useState } from 'react';
|
|
173
|
+
import { Link } from 'react-router-dom';
|
|
174
|
+
import { scaffoldConfig } from '@/lib/scaffold-config';
|
|
175
|
+
|
|
176
|
+
export default function Home() {
|
|
177
|
+
const [message, setMessage] = useState('');
|
|
178
|
+
async function testConnection() {
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch('/api/greet?name=SwallowKit');
|
|
181
|
+
if (!response.ok) throw new Error('BFF request failed (' + response.status + ')');
|
|
182
|
+
const data = await response.json();
|
|
183
|
+
setMessage(data.message);
|
|
184
|
+
} catch (error) { setMessage(error instanceof Error ? error.message : String(error)); }
|
|
185
|
+
}
|
|
186
|
+
return <main className="container mx-auto p-8">
|
|
187
|
+
<h1 className="text-3xl font-bold mb-4">SwallowKit + React</h1>
|
|
188
|
+
<p>React SPA → SWA managed Functions BFF → standalone Functions → Cosmos DB</p>
|
|
189
|
+
<button className="my-4 bg-blue-600 text-white rounded px-4 py-2" onClick={testConnection}>Test backend connection</button>
|
|
190
|
+
<p role="status">{message}</p>
|
|
191
|
+
<nav>{scaffoldConfig.models.map(model => <p key={model.name}><Link to={model.path}>{model.label}</Link></p>)}</nav>
|
|
192
|
+
</main>;
|
|
193
|
+
}
|
|
194
|
+
`
|
|
195
|
+
);
|
|
196
|
+
write(projectDir, "src\\style.css", '@import "tailwindcss";\n');
|
|
197
|
+
write(
|
|
198
|
+
projectDir,
|
|
199
|
+
"lib\\scaffold-config.ts",
|
|
200
|
+
`export interface ScaffoldModel { name: string; path: string; label: string; }
|
|
201
|
+
export const scaffoldConfig = {
|
|
202
|
+
models: [] as ScaffoldModel[]
|
|
203
|
+
};
|
|
204
|
+
`
|
|
205
|
+
);
|
|
206
|
+
write(
|
|
207
|
+
projectDir,
|
|
208
|
+
".gitignore",
|
|
209
|
+
"node_modules/\ndist/\n.env*.local\nlocal.settings.json\n"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function createReactBffProject(
|
|
214
|
+
projectDir: string,
|
|
215
|
+
projectName: string,
|
|
216
|
+
pm: PackageManager
|
|
217
|
+
): void {
|
|
218
|
+
write(
|
|
219
|
+
projectDir,
|
|
220
|
+
"bff\\package.json",
|
|
221
|
+
JSON.stringify(
|
|
222
|
+
{
|
|
223
|
+
name: "bff",
|
|
224
|
+
private: true,
|
|
225
|
+
version: "1.0.0",
|
|
226
|
+
description:
|
|
227
|
+
"SWA managed HTTP-only Functions BFF (not the standalone backend)",
|
|
228
|
+
engines: { node: ">=20" },
|
|
229
|
+
scripts: {
|
|
230
|
+
build: "tsc --noEmit && node build.cjs",
|
|
231
|
+
start: "func start --port 7073",
|
|
232
|
+
prestart: `${pm} run build`,
|
|
233
|
+
},
|
|
234
|
+
devDependencies: {
|
|
235
|
+
"@azure/functions": "^3.5.0",
|
|
236
|
+
"@types/node": "^22.0.0",
|
|
237
|
+
typescript: "^5.9.0",
|
|
238
|
+
esbuild: "^0.25.0",
|
|
239
|
+
[`@${projectName}/shared`]:
|
|
240
|
+
pm === "pnpm" ? "workspace:*" : "file:../shared",
|
|
241
|
+
zod: "^4.0.0",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
null,
|
|
245
|
+
2
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
write(projectDir, "bff\\build.cjs", buildReactBffBuildSource());
|
|
249
|
+
write(
|
|
250
|
+
projectDir,
|
|
251
|
+
"bff\\tsconfig.json",
|
|
252
|
+
JSON.stringify(
|
|
253
|
+
{
|
|
254
|
+
compilerOptions: {
|
|
255
|
+
target: "ES2022",
|
|
256
|
+
lib: ["ES2022", "DOM"],
|
|
257
|
+
module: "Node16",
|
|
258
|
+
moduleResolution: "Node16",
|
|
259
|
+
strict: true,
|
|
260
|
+
esModuleInterop: true,
|
|
261
|
+
skipLibCheck: true,
|
|
262
|
+
noEmit: true,
|
|
263
|
+
},
|
|
264
|
+
include: ["src/**/*.ts"],
|
|
265
|
+
},
|
|
266
|
+
null,
|
|
267
|
+
2
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
// Classic function.json registrations work with SWA's managed Functions runtime.
|
|
271
|
+
write(
|
|
272
|
+
projectDir,
|
|
273
|
+
"bff\\host.json",
|
|
274
|
+
JSON.stringify(
|
|
275
|
+
{ version: "2.0", extensions: { http: { routePrefix: "api" } } },
|
|
276
|
+
null,
|
|
277
|
+
2
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
write(
|
|
281
|
+
projectDir,
|
|
282
|
+
"bff\\local.settings.json",
|
|
283
|
+
JSON.stringify(
|
|
284
|
+
{
|
|
285
|
+
IsEncrypted: false,
|
|
286
|
+
Values: {
|
|
287
|
+
FUNCTIONS_WORKER_RUNTIME: "node",
|
|
288
|
+
BACKEND_FUNCTIONS_BASE_URL: "http://localhost:7071",
|
|
289
|
+
BACKEND_FUNCTIONS_KEY: "",
|
|
290
|
+
},
|
|
291
|
+
Host: { LocalHttpPort: 7073, CORS: "http://localhost:4280" },
|
|
292
|
+
},
|
|
293
|
+
null,
|
|
294
|
+
2
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
write(
|
|
298
|
+
projectDir,
|
|
299
|
+
"bff\\.gitignore",
|
|
300
|
+
"node_modules/\ndist/\nlocal.settings.json\n"
|
|
301
|
+
);
|
|
302
|
+
write(
|
|
303
|
+
projectDir,
|
|
304
|
+
"bff\\.funcignore",
|
|
305
|
+
"src/\nnode_modules/\nlocal.settings.json\ntsconfig.json\nbuild.cjs\n.gitignore\n"
|
|
306
|
+
);
|
|
307
|
+
write(
|
|
308
|
+
projectDir,
|
|
309
|
+
"bff\\src\\lib\\call-function.ts",
|
|
310
|
+
generateReactBffHelper(projectName)
|
|
311
|
+
);
|
|
312
|
+
write(projectDir, "bff\\src\\greet\\index.ts", generateReactGreetBff());
|
|
313
|
+
write(
|
|
314
|
+
projectDir,
|
|
315
|
+
"bff\\greet\\function.json",
|
|
316
|
+
generateManagedFunctionConfig("greet", ["GET", "POST"], "greet")
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function buildReactBffBuildSource(): string {
|
|
321
|
+
return `const fs = require('node:fs');
|
|
322
|
+
const path = require('node:path');
|
|
323
|
+
const { buildSync } = require('esbuild');
|
|
324
|
+
|
|
325
|
+
const entryPoints = fs.readdirSync(path.join(__dirname, 'src'), { withFileTypes: true })
|
|
326
|
+
.filter(entry => entry.isDirectory() && fs.existsSync(path.join(__dirname, 'src', entry.name, 'index.ts')))
|
|
327
|
+
.map(entry => path.join('src', entry.name, 'index.ts'));
|
|
328
|
+
if (!entryPoints.length) throw new Error('No managed BFF handlers found');
|
|
329
|
+
buildSync({
|
|
330
|
+
absWorkingDir: __dirname, entryPoints, bundle: true,
|
|
331
|
+
platform: 'node', target: 'node20', outbase: 'src', outdir: 'dist', logLevel: 'info',
|
|
332
|
+
});
|
|
333
|
+
for (const entry of entryPoints) {
|
|
334
|
+
const output = path.join(__dirname, 'dist', path.basename(path.dirname(entry)), 'index.js');
|
|
335
|
+
if (!fs.existsSync(output)) throw new Error('Missing managed BFF bundle: ' + output);
|
|
336
|
+
}
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function buildReactReadme(
|
|
341
|
+
projectName: string,
|
|
342
|
+
pm: PackageManager,
|
|
343
|
+
backend: BackendLanguage
|
|
344
|
+
): string {
|
|
345
|
+
const cmd = getCommands(pm);
|
|
346
|
+
return `# ${projectName}
|
|
347
|
+
|
|
348
|
+
Generated by SwallowKit: React + Vite + TypeScript on Azure Static Web Apps.
|
|
349
|
+
|
|
350
|
+
## Architecture
|
|
351
|
+
|
|
352
|
+
Browser → React SPA → /api (SWA managed Functions BFF) → standalone ${backend} Functions → Cosmos DB.
|
|
353
|
+
|
|
354
|
+
- \`src/\`: React Router UI. No Next.js runtime.
|
|
355
|
+
- \`bff/\`: HTTP-only SWA managed Functions. Validates shared schemas and forwards requests; no database/business logic.
|
|
356
|
+
- \`functions/\`: independently deployed backend, NOT linked to SWA.
|
|
357
|
+
- \`shared/models/\`: Zod source of truth.
|
|
358
|
+
- \`infra/\`: Bicep infrastructure.
|
|
359
|
+
|
|
360
|
+
Only the BFF reads BACKEND_FUNCTIONS_BASE_URL and BACKEND_FUNCTIONS_KEY.
|
|
361
|
+
Never put those values in VITE_* variables or client code.
|
|
362
|
+
|
|
363
|
+
## Develop
|
|
364
|
+
|
|
365
|
+
\`\`\`sh
|
|
366
|
+
${cmd.exec} swallowkit create-model todo
|
|
367
|
+
${cmd.exec} swallowkit scaffold todo
|
|
368
|
+
${cmd.exec} swallowkit dev
|
|
369
|
+
\`\`\`
|
|
370
|
+
|
|
371
|
+
Use http://localhost:4280, not Vite's direct port, for same-origin UI/BFF requests.
|
|
372
|
+
Dev starts Vite (3000), the managed BFF (7073), the standalone Functions (7071),
|
|
373
|
+
and SWA CLI (4280). \`--bff-port\`, \`--port\`, \`--functions-port\`, and \`--swa-port\` override ports.
|
|
374
|
+
The BFF stays separate even with \`--no-functions\`; \`--no-swa\` starts only the
|
|
375
|
+
direct Vite/BFF servers (no same-origin API routing or SWA authentication).
|
|
376
|
+
Core Tools v4 must already be available. Start Cosmos DB Emulator manually if
|
|
377
|
+
this application needs Cosmos; SwallowKit never starts it.
|
|
378
|
+
|
|
379
|
+
Local BFF settings live in \`bff/local.settings.json\` (gitignored). Backend settings
|
|
380
|
+
live in \`functions/local.settings.json\`. Keep their secrets out of source control.
|
|
381
|
+
\`--mock-connectors\` still routes through the BFF and mocks only connector data.
|
|
382
|
+
|
|
383
|
+
## Verify
|
|
384
|
+
|
|
385
|
+
\`\`\`sh
|
|
386
|
+
${pm} run build
|
|
387
|
+
${cmd.exec} swallowkit machine verify project
|
|
388
|
+
${cmd.exec} swallowkit machine verify project --checks structure,drift,typecheck,build
|
|
389
|
+
\`\`\`
|
|
390
|
+
|
|
391
|
+
The build compiles shared schemas, checks the SPA, builds static assets, and
|
|
392
|
+
typechecks/bundles the managed BFF. Exercise CRUD and authentication through
|
|
393
|
+
the SWA port as well; structural verification is not an end-to-end test.
|
|
394
|
+
|
|
395
|
+
## Deploy
|
|
396
|
+
|
|
397
|
+
Use the generated GitHub Actions / Azure Pipelines workflows when selected.
|
|
398
|
+
Deploy \`dist/\` as the static app and \`bff/\` as its managed API, with
|
|
399
|
+
\`skip_app_build: true\` and \`skip_api_build: true\` after \`${pm} run build\`.
|
|
400
|
+
BFF bundles include shared schemas and Zod, so no workspace install is needed
|
|
401
|
+
in SWA's API build. Deploy \`functions/\` independently, never as SWA's api_location.
|
|
402
|
+
Do NOT link the standalone Functions app to SWA.
|
|
403
|
+
|
|
404
|
+
After approved provisioning and backend deployment, apply the generated SWA
|
|
405
|
+
appsettings module with the backend URL/key. The key is required for remote
|
|
406
|
+
Functions calls, and stays server-side. SPA deep links use navigationFallback;
|
|
407
|
+
/api and /.auth are excluded so missing APIs never become index.html.
|
|
408
|
+
|
|
409
|
+
See AGENTS.md for ownership, plan/apply, authentication, and verification guidance.
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const REACT_DEPLOY_PATHS = [
|
|
414
|
+
"src/**",
|
|
415
|
+
"bff/**",
|
|
416
|
+
"lib/**",
|
|
417
|
+
"shared/**",
|
|
418
|
+
"public/**",
|
|
419
|
+
"index.html",
|
|
420
|
+
"vite.config.mts",
|
|
421
|
+
"tsconfig.json",
|
|
422
|
+
"staticwebapp.config.json",
|
|
423
|
+
"package.json",
|
|
424
|
+
"pnpm-workspace.yaml",
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
export function buildReactGitHubSwaWorkflow(pm: PackageManager): string {
|
|
428
|
+
const paths = [
|
|
429
|
+
...REACT_DEPLOY_PATHS,
|
|
430
|
+
pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json",
|
|
431
|
+
".github/workflows/deploy-swa.yml",
|
|
432
|
+
];
|
|
433
|
+
return `name: Deploy React SPA and managed BFF
|
|
434
|
+
on:
|
|
435
|
+
push:
|
|
436
|
+
branches: [main]
|
|
437
|
+
paths:
|
|
438
|
+
${paths.map((entry) => ` - '${entry}'`).join("\n")}
|
|
439
|
+
pull_request:
|
|
440
|
+
branches: [main]
|
|
441
|
+
paths:
|
|
442
|
+
${paths.map((entry) => ` - '${entry}'`).join("\n")}
|
|
443
|
+
workflow_dispatch:
|
|
444
|
+
jobs:
|
|
445
|
+
build-and-deploy:
|
|
446
|
+
runs-on: ubuntu-latest
|
|
447
|
+
steps:
|
|
448
|
+
- uses: actions/checkout@v4
|
|
449
|
+
${getCiSetupStep(pm)}
|
|
450
|
+
- uses: actions/setup-node@v4
|
|
451
|
+
with:
|
|
452
|
+
node-version: '22'
|
|
453
|
+
cache: ${pm}
|
|
454
|
+
- name: Build SPA and managed BFF
|
|
455
|
+
run: |
|
|
456
|
+
${getCommands(pm).ci}
|
|
457
|
+
${pm} run build
|
|
458
|
+
- name: Deploy SPA and managed BFF (standalone Functions are NOT linked)
|
|
459
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
460
|
+
uses: Azure/static-web-apps-deploy@v1
|
|
461
|
+
with:
|
|
462
|
+
azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
|
463
|
+
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
464
|
+
action: upload
|
|
465
|
+
app_location: dist
|
|
466
|
+
api_location: bff
|
|
467
|
+
output_location: ''
|
|
468
|
+
skip_app_build: true
|
|
469
|
+
skip_api_build: true
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function buildReactAzureSwaPipeline(pm: PackageManager): string {
|
|
474
|
+
return `trigger:
|
|
475
|
+
branches:
|
|
476
|
+
include: [main]
|
|
477
|
+
paths:
|
|
478
|
+
include:
|
|
479
|
+
${[...REACT_DEPLOY_PATHS, pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json", "pipelines/swa.yml"].map((entry) => ` - '${entry}'`).join("\n")}
|
|
480
|
+
pr:
|
|
481
|
+
branches:
|
|
482
|
+
include: [main]
|
|
483
|
+
pool:
|
|
484
|
+
vmImage: ubuntu-latest
|
|
485
|
+
variables:
|
|
486
|
+
- group: azure-deployment
|
|
487
|
+
steps:
|
|
488
|
+
- task: NodeTool@0
|
|
489
|
+
inputs:
|
|
490
|
+
versionSpec: '22.x'
|
|
491
|
+
${getAzurePipelinesSetup(pm)}
|
|
492
|
+
- script: |
|
|
493
|
+
${getCommands(pm).ci}
|
|
494
|
+
${pm} run build
|
|
495
|
+
displayName: Build React SPA and managed BFF
|
|
496
|
+
- task: AzureStaticWebApp@0
|
|
497
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
498
|
+
inputs:
|
|
499
|
+
app_location: dist
|
|
500
|
+
api_location: bff
|
|
501
|
+
output_location: ''
|
|
502
|
+
skip_app_build: true
|
|
503
|
+
skip_api_build: true
|
|
504
|
+
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
505
|
+
displayName: Deploy SPA and managed BFF (standalone Functions are NOT linked)
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
@@ -55,7 +55,7 @@ function validateRequiredPaths(manifest: ProjectManifest, violations: ProjectVio
|
|
|
55
55
|
pushViolation(violations, {
|
|
56
56
|
code: "missing-call-function-helper",
|
|
57
57
|
severity: "warning",
|
|
58
|
-
message:
|
|
58
|
+
message: `BFF helper ${manifest.artifacts.callFunctionHelperPath} is missing.`,
|
|
59
59
|
location: { path: manifest.artifacts.callFunctionHelperPath },
|
|
60
60
|
suggestedFix: "Run scaffold to regenerate the BFF helper.",
|
|
61
61
|
});
|
|
@@ -117,8 +117,8 @@ function validateGeneratedArtifacts(manifest: ProjectManifest, violations: Proje
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function validateLayeringViolations(projectRoot: string, violations: ProjectViolation[]): void {
|
|
121
|
-
const apiDir = path.join(projectRoot, "app", "api");
|
|
120
|
+
function validateLayeringViolations(projectRoot: string, manifest: ProjectManifest, violations: ProjectViolation[]): void {
|
|
121
|
+
const apiDir = path.join(projectRoot, ...(manifest.frontendFramework === "react" ? ["bff", "src"] : ["app", "api"]));
|
|
122
122
|
const modelsDir = path.join(projectRoot, "shared", "models");
|
|
123
123
|
|
|
124
124
|
if (fs.existsSync(apiDir)) {
|
|
@@ -130,7 +130,7 @@ function validateLayeringViolations(projectRoot: string, violations: ProjectViol
|
|
|
130
130
|
severity: "error",
|
|
131
131
|
message: "BFF files must not depend directly on Azure Cosmos or Functions implementation paths.",
|
|
132
132
|
location: { path: path.relative(projectRoot, filePath).replace(/\\/g, "/") },
|
|
133
|
-
suggestedFix:
|
|
133
|
+
suggestedFix: `Keep ${manifest.frontendFramework === "react" ? "bff/src" : "app/api"} as a pure BFF layer and call the backend through generated helpers instead.`,
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -139,16 +139,37 @@ function validateLayeringViolations(projectRoot: string, violations: ProjectViol
|
|
|
139
139
|
if (fs.existsSync(modelsDir)) {
|
|
140
140
|
for (const filePath of walkFiles(modelsDir, [".ts", ".tsx"])) {
|
|
141
141
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
142
|
-
if (content.match(/from\s+['"].*(app\/|functions\/|lib\/api\/)/)) {
|
|
142
|
+
if (content.match(/from\s+['"].*(app\/|functions\/|bff\/|src\/|lib\/api\/)/)) {
|
|
143
143
|
pushViolation(violations, {
|
|
144
144
|
code: "forbidden-model-layer-import",
|
|
145
145
|
severity: "error",
|
|
146
|
-
message: "Shared models must not import from
|
|
146
|
+
message: "Shared models must not import from frontend, BFF, Functions, or lib/api/ layers.",
|
|
147
147
|
location: { path: path.relative(projectRoot, filePath).replace(/\\/g, "/") },
|
|
148
148
|
suggestedFix: "Keep shared/models framework-agnostic so generators can reuse them across all layers.",
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
if (manifest.frontendFramework === "react") {
|
|
154
|
+
for (const directory of ["src", "lib"]) {
|
|
155
|
+
const root = path.join(projectRoot, directory);
|
|
156
|
+
if (!fs.existsSync(root)) continue;
|
|
157
|
+
for (const filePath of walkFiles(root, [".ts", ".tsx"])) {
|
|
158
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
159
|
+
const serverImport = /(?:from\s*|import\s*\(|require\s*\()\s*['"](?:@azure\/cosmos|[^'"]*(?:bff\/|functions\/|lib\/api\/call-function))/;
|
|
160
|
+
const serverSetting = /\b(?:process\.env|import\.meta\.env)\s*(?:\.\s*(?:VITE_)?BACKEND_FUNCTIONS_(?:KEY|BASE_URL)\b|\[\s*['"](?:VITE_)?BACKEND_FUNCTIONS_(?:KEY|BASE_URL)['"]\s*\])/;
|
|
161
|
+
if (serverImport.test(content) || serverSetting.test(content)) {
|
|
162
|
+
pushViolation(violations, {
|
|
163
|
+
code: "forbidden-client-backend-dependency",
|
|
164
|
+
severity: "error",
|
|
165
|
+
message: "React client code must not import server implementation code or read server-side backend settings.",
|
|
166
|
+
location: { path: path.relative(projectRoot, filePath).replace(/\\/g, "/") },
|
|
167
|
+
suggestedFix: "Call same-origin /api from the SPA. Keep backend URLs, keys, and Functions/Cosmos dependencies in the managed BFF or standalone backend.",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
175
|
|
|
@@ -160,10 +181,11 @@ function validateManifestDrift(
|
|
|
160
181
|
): void {
|
|
161
182
|
const sameEntitySet = JSON.stringify(loadedManifest.entities.map((entity) => entity.name).sort()) ===
|
|
162
183
|
JSON.stringify(rebuiltManifest.entities.map((entity) => entity.name).sort());
|
|
163
|
-
const sameRouteSet = JSON.stringify(loadedManifest.routes.map((route) => route.name).sort()) ===
|
|
164
|
-
JSON.stringify(rebuiltManifest.routes.map((route) => route.name).sort());
|
|
184
|
+
const sameRouteSet = JSON.stringify(loadedManifest.routes.map((route) => `${route.name}:${route.filePath}`).sort()) ===
|
|
185
|
+
JSON.stringify(rebuiltManifest.routes.map((route) => `${route.name}:${route.filePath}`).sort());
|
|
186
|
+
const sameFrontend = (loadedManifest.frontendFramework ?? "nextjs") === (rebuiltManifest.frontendFramework ?? "nextjs");
|
|
165
187
|
|
|
166
|
-
if (!sameEntitySet || !sameRouteSet) {
|
|
188
|
+
if (!sameEntitySet || !sameRouteSet || !sameFrontend) {
|
|
167
189
|
pushViolation(violations, {
|
|
168
190
|
code: "manifest-out-of-date",
|
|
169
191
|
severity: "warning",
|
|
@@ -279,7 +301,7 @@ export async function validateProject(projectRoot: string = process.cwd()): Prom
|
|
|
279
301
|
}
|
|
280
302
|
|
|
281
303
|
validateGeneratedArtifacts(loaded.manifest, violations);
|
|
282
|
-
validateLayeringViolations(projectRoot, violations);
|
|
304
|
+
validateLayeringViolations(projectRoot, loaded.manifest, violations);
|
|
283
305
|
validateExternalTokenVerifierStubs(projectRoot, loaded.manifest, violations);
|
|
284
306
|
|
|
285
307
|
if (loaded.source === "file") {
|