swallowkit 1.1.2 → 2.0.0-alpha.2

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 (88) hide show
  1. package/README.ja.md +61 -7
  2. package/README.md +61 -7
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +66 -21
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/dev.d.ts +4 -1
  7. package/dist/cli/commands/dev.d.ts.map +1 -1
  8. package/dist/cli/commands/dev.js +108 -44
  9. package/dist/cli/commands/dev.js.map +1 -1
  10. package/dist/cli/commands/init.d.ts +6 -5
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +83 -39
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/commands/scaffold.d.ts +1 -1
  15. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  16. package/dist/cli/commands/scaffold.js +38 -15
  17. package/dist/cli/commands/scaffold.js.map +1 -1
  18. package/dist/cli/index.d.ts.map +1 -1
  19. package/dist/cli/index.js +6 -3
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/core/config.d.ts +2 -1
  22. package/dist/core/config.d.ts.map +1 -1
  23. package/dist/core/config.js +19 -0
  24. package/dist/core/config.js.map +1 -1
  25. package/dist/core/project/agent-instructions.d.ts +2 -1
  26. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  27. package/dist/core/project/agent-instructions.js +12 -0
  28. package/dist/core/project/agent-instructions.js.map +1 -1
  29. package/dist/core/project/capabilities.d.ts +7 -0
  30. package/dist/core/project/capabilities.d.ts.map +1 -1
  31. package/dist/core/project/capabilities.js +7 -0
  32. package/dist/core/project/capabilities.js.map +1 -1
  33. package/dist/core/project/manifest.d.ts +3 -1
  34. package/dist/core/project/manifest.d.ts.map +1 -1
  35. package/dist/core/project/manifest.js +47 -25
  36. package/dist/core/project/manifest.js.map +1 -1
  37. package/dist/core/project/react-project.d.ts +26 -0
  38. package/dist/core/project/react-project.d.ts.map +1 -0
  39. package/dist/core/project/react-project.js +422 -0
  40. package/dist/core/project/react-project.js.map +1 -0
  41. package/dist/core/project/validation.d.ts.map +1 -1
  42. package/dist/core/project/validation.js +32 -10
  43. package/dist/core/project/validation.js.map +1 -1
  44. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  45. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  46. package/dist/core/scaffold/react-bff-generator.js +252 -0
  47. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  48. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  49. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  50. package/dist/core/scaffold/react-ui-generator.js +41 -0
  51. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  52. package/dist/index.d.ts +3 -3
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/machine/index.js +2 -2
  57. package/dist/machine/index.js.map +1 -1
  58. package/dist/types/index.d.ts +4 -0
  59. package/dist/types/index.d.ts.map +1 -1
  60. package/dist/utils/package-manager.d.ts +2 -2
  61. package/dist/utils/package-manager.d.ts.map +1 -1
  62. package/dist/utils/package-manager.js +5 -2
  63. package/dist/utils/package-manager.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/__tests__/agent-instructions.test.ts +1 -1
  66. package/src/__tests__/init.test.ts +29 -5
  67. package/src/__tests__/package-manager.test.ts +15 -0
  68. package/src/__tests__/react.test.ts +819 -0
  69. package/src/__tests__/release-metadata.test.ts +121 -0
  70. package/src/cli/commands/add-auth.ts +67 -23
  71. package/src/cli/commands/dev.ts +109 -46
  72. package/src/cli/commands/init.ts +75 -35
  73. package/src/cli/commands/scaffold.ts +40 -19
  74. package/src/cli/index.ts +6 -3
  75. package/src/core/config.ts +21 -1
  76. package/src/core/project/agent-instructions.ts +14 -1
  77. package/src/core/project/agent-templates/AGENTS.md +24 -9
  78. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  79. package/src/core/project/capabilities.ts +14 -0
  80. package/src/core/project/manifest.ts +52 -27
  81. package/src/core/project/react-project.ts +507 -0
  82. package/src/core/project/validation.ts +32 -10
  83. package/src/core/scaffold/react-bff-generator.ts +292 -0
  84. package/src/core/scaffold/react-ui-generator.ts +51 -0
  85. package/src/index.ts +3 -1
  86. package/src/machine/index.ts +2 -2
  87. package/src/types/index.ts +4 -0
  88. package/src/utils/package-manager.ts +6 -3
@@ -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: "BFF helper lib/api/call-function.ts is missing.",
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: "Keep app/api as a pure BFF layer and call the backend through generated helpers instead.",
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 app/, functions/, or lib/api/ layers.",
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") {