swallowkit 1.1.2 → 2.0.0-alpha.10

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 (262) 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 +103 -27
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  7. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  8. package/dist/cli/commands/configure-mcp.js +42 -0
  9. package/dist/cli/commands/configure-mcp.js.map +1 -0
  10. package/dist/cli/commands/dev.d.ts +5 -2
  11. package/dist/cli/commands/dev.d.ts.map +1 -1
  12. package/dist/cli/commands/dev.js +143 -67
  13. package/dist/cli/commands/dev.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts +14 -7
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +149 -170
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/migrations.d.ts.map +1 -1
  19. package/dist/cli/commands/migrations.js +25 -0
  20. package/dist/cli/commands/migrations.js.map +1 -1
  21. package/dist/cli/commands/provision.d.ts +5 -0
  22. package/dist/cli/commands/provision.d.ts.map +1 -1
  23. package/dist/cli/commands/provision.js +278 -55
  24. package/dist/cli/commands/provision.js.map +1 -1
  25. package/dist/cli/commands/scaffold.d.ts +5 -1
  26. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  27. package/dist/cli/commands/scaffold.js +123 -18
  28. package/dist/cli/commands/scaffold.js.map +1 -1
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +49 -8
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/core/config.d.ts +2 -1
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +19 -0
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/operations/create-model.d.ts +2 -0
  38. package/dist/core/operations/create-model.d.ts.map +1 -1
  39. package/dist/core/operations/create-model.js +60 -8
  40. package/dist/core/operations/create-model.js.map +1 -1
  41. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  42. package/dist/core/operations/migration-generator.js +12 -1
  43. package/dist/core/operations/migration-generator.js.map +1 -1
  44. package/dist/core/operations/model-migration.d.ts +22 -0
  45. package/dist/core/operations/model-migration.d.ts.map +1 -0
  46. package/dist/core/operations/model-migration.js +36 -0
  47. package/dist/core/operations/model-migration.js.map +1 -0
  48. package/dist/core/operations/provision-deployment.d.ts +52 -0
  49. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  50. package/dist/core/operations/provision-deployment.js +164 -0
  51. package/dist/core/operations/provision-deployment.js.map +1 -0
  52. package/dist/core/operations/provision-operations.d.ts +62 -6
  53. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  54. package/dist/core/operations/provision-operations.js +653 -115
  55. package/dist/core/operations/provision-operations.js.map +1 -1
  56. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  57. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  58. package/dist/core/operations/scaffold-apply.js +1 -0
  59. package/dist/core/operations/scaffold-apply.js.map +1 -1
  60. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  61. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  62. package/dist/core/operations/scaffold-machine.js.map +1 -1
  63. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  64. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  65. package/dist/core/operations/scaffold-plan.js +3 -1
  66. package/dist/core/operations/scaffold-plan.js.map +1 -1
  67. package/dist/core/project/agent-instructions.d.ts +2 -1
  68. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  69. package/dist/core/project/agent-instructions.js +15 -0
  70. package/dist/core/project/agent-instructions.js.map +1 -1
  71. package/dist/core/project/boundaries.d.ts +5 -2
  72. package/dist/core/project/boundaries.d.ts.map +1 -1
  73. package/dist/core/project/boundaries.js +51 -20
  74. package/dist/core/project/boundaries.js.map +1 -1
  75. package/dist/core/project/capabilities.d.ts +17 -0
  76. package/dist/core/project/capabilities.d.ts.map +1 -1
  77. package/dist/core/project/capabilities.js +45 -6
  78. package/dist/core/project/capabilities.js.map +1 -1
  79. package/dist/core/project/home-page.d.ts +4 -0
  80. package/dist/core/project/home-page.d.ts.map +1 -0
  81. package/dist/core/project/home-page.js +113 -0
  82. package/dist/core/project/home-page.js.map +1 -0
  83. package/dist/core/project/manifest.d.ts +3 -1
  84. package/dist/core/project/manifest.d.ts.map +1 -1
  85. package/dist/core/project/manifest.js +47 -25
  86. package/dist/core/project/manifest.js.map +1 -1
  87. package/dist/core/project/mcp-config.d.ts +26 -0
  88. package/dist/core/project/mcp-config.d.ts.map +1 -0
  89. package/dist/core/project/mcp-config.js +117 -0
  90. package/dist/core/project/mcp-config.js.map +1 -0
  91. package/dist/core/project/react-project.d.ts +26 -0
  92. package/dist/core/project/react-project.d.ts.map +1 -0
  93. package/dist/core/project/react-project.js +443 -0
  94. package/dist/core/project/react-project.js.map +1 -0
  95. package/dist/core/project/state.d.ts +7 -0
  96. package/dist/core/project/state.d.ts.map +1 -1
  97. package/dist/core/project/state.js +86 -0
  98. package/dist/core/project/state.js.map +1 -1
  99. package/dist/core/project/validation.d.ts.map +1 -1
  100. package/dist/core/project/validation.js +32 -10
  101. package/dist/core/project/validation.js.map +1 -1
  102. package/dist/core/project/workflows.d.ts.map +1 -1
  103. package/dist/core/project/workflows.js +53 -7
  104. package/dist/core/project/workflows.js.map +1 -1
  105. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/auth-generator.js +26 -27
  107. package/dist/core/scaffold/auth-generator.js.map +1 -1
  108. package/dist/core/scaffold/model-parser.d.ts +23 -0
  109. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  110. package/dist/core/scaffold/model-parser.js +407 -137
  111. package/dist/core/scaffold/model-parser.js.map +1 -1
  112. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  113. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  114. package/dist/core/scaffold/native-schema-generator.js +294 -91
  115. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  116. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  117. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  118. package/dist/core/scaffold/openapi-generator.js +53 -3
  119. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  120. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  121. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  122. package/dist/core/scaffold/react-bff-generator.js +252 -0
  123. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  124. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  125. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  126. package/dist/core/scaffold/react-ui-generator.js +41 -0
  127. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  128. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  129. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  130. package/dist/core/scaffold/ui-generator.js +68 -7
  131. package/dist/core/scaffold/ui-generator.js.map +1 -1
  132. package/dist/core/verify/index.d.ts +4 -3
  133. package/dist/core/verify/index.d.ts.map +1 -1
  134. package/dist/core/verify/index.js +122 -5
  135. package/dist/core/verify/index.js.map +1 -1
  136. package/dist/index.d.ts +3 -3
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +3 -2
  139. package/dist/index.js.map +1 -1
  140. package/dist/machine/contracts.d.ts +1 -0
  141. package/dist/machine/contracts.d.ts.map +1 -1
  142. package/dist/machine/errors.d.ts +3 -2
  143. package/dist/machine/errors.d.ts.map +1 -1
  144. package/dist/machine/errors.js +2 -1
  145. package/dist/machine/errors.js.map +1 -1
  146. package/dist/machine/index.d.ts.map +1 -1
  147. package/dist/machine/index.js +61 -30
  148. package/dist/machine/index.js.map +1 -1
  149. package/dist/mcp/index.d.ts +6 -4
  150. package/dist/mcp/index.d.ts.map +1 -1
  151. package/dist/mcp/index.js +210 -25
  152. package/dist/mcp/index.js.map +1 -1
  153. package/dist/mcp/version-selection.d.ts +55 -0
  154. package/dist/mcp/version-selection.d.ts.map +1 -0
  155. package/dist/mcp/version-selection.js +152 -0
  156. package/dist/mcp/version-selection.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/utils/azure-cli.d.ts +56 -0
  160. package/dist/utils/azure-cli.d.ts.map +1 -0
  161. package/dist/utils/azure-cli.js +326 -0
  162. package/dist/utils/azure-cli.js.map +1 -0
  163. package/dist/utils/azure-diagnostics.d.ts +16 -0
  164. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  165. package/dist/utils/azure-diagnostics.js +68 -0
  166. package/dist/utils/azure-diagnostics.js.map +1 -0
  167. package/dist/utils/package-manager.d.ts +2 -2
  168. package/dist/utils/package-manager.d.ts.map +1 -1
  169. package/dist/utils/package-manager.js +6 -3
  170. package/dist/utils/package-manager.js.map +1 -1
  171. package/dist/utils/python-uv.d.ts +5 -7
  172. package/dist/utils/python-uv.d.ts.map +1 -1
  173. package/dist/utils/python-uv.js +50 -24
  174. package/dist/utils/python-uv.js.map +1 -1
  175. package/dist/utils/react-dev.d.ts +4 -0
  176. package/dist/utils/react-dev.d.ts.map +1 -0
  177. package/dist/utils/react-dev.js +35 -0
  178. package/dist/utils/react-dev.js.map +1 -0
  179. package/dist/utils/semver.d.ts +14 -0
  180. package/dist/utils/semver.d.ts.map +1 -0
  181. package/dist/utils/semver.js +61 -0
  182. package/dist/utils/semver.js.map +1 -0
  183. package/package.json +7 -4
  184. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  185. package/src/__tests__/agent-instructions.test.ts +6 -1
  186. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  187. package/src/__tests__/auth.test.ts +9 -0
  188. package/src/__tests__/azure-cli.test.ts +202 -0
  189. package/src/__tests__/boundaries.test.ts +42 -0
  190. package/src/__tests__/csharp-schema.test.ts +261 -0
  191. package/src/__tests__/dev.test.ts +13 -6
  192. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  193. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  194. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  195. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  196. package/src/__tests__/home-page.test.ts +50 -0
  197. package/src/__tests__/init.test.ts +145 -5
  198. package/src/__tests__/mcp-version.test.ts +262 -0
  199. package/src/__tests__/mcp.test.ts +120 -3
  200. package/src/__tests__/multi-auth.test.ts +115 -4
  201. package/src/__tests__/package-manager.test.ts +17 -2
  202. package/src/__tests__/plan-apply.test.ts +179 -0
  203. package/src/__tests__/provision-runtime.test.ts +722 -0
  204. package/src/__tests__/python-uv.test.ts +50 -14
  205. package/src/__tests__/python-venv.test.ts +48 -0
  206. package/src/__tests__/react-dev.test.ts +72 -0
  207. package/src/__tests__/react.test.ts +838 -0
  208. package/src/__tests__/release-metadata.test.ts +121 -0
  209. package/src/__tests__/schema-analysis.test.ts +329 -0
  210. package/src/cli/commands/add-auth.ts +103 -29
  211. package/src/cli/commands/configure-mcp.ts +54 -0
  212. package/src/cli/commands/dev.ts +145 -73
  213. package/src/cli/commands/init.ts +147 -172
  214. package/src/cli/commands/migrations.ts +22 -0
  215. package/src/cli/commands/provision.ts +294 -56
  216. package/src/cli/commands/scaffold.ts +145 -22
  217. package/src/cli/index.ts +46 -10
  218. package/src/core/config.ts +21 -1
  219. package/src/core/operations/create-model.ts +51 -13
  220. package/src/core/operations/migration-generator.ts +14 -1
  221. package/src/core/operations/model-migration.ts +52 -0
  222. package/src/core/operations/provision-deployment.ts +183 -0
  223. package/src/core/operations/provision-operations.ts +662 -154
  224. package/src/core/operations/scaffold-apply.ts +2 -0
  225. package/src/core/operations/scaffold-machine.ts +1 -0
  226. package/src/core/operations/scaffold-plan.ts +5 -1
  227. package/src/core/project/agent-instructions.ts +17 -1
  228. package/src/core/project/agent-templates/AGENTS.md +113 -15
  229. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  230. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  231. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  232. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  233. package/src/core/project/boundaries.ts +62 -20
  234. package/src/core/project/capabilities.ts +63 -6
  235. package/src/core/project/home-page.ts +114 -0
  236. package/src/core/project/manifest.ts +52 -27
  237. package/src/core/project/mcp-config.ts +84 -0
  238. package/src/core/project/react-project.ts +529 -0
  239. package/src/core/project/state.ts +70 -0
  240. package/src/core/project/validation.ts +32 -10
  241. package/src/core/project/workflows.ts +53 -7
  242. package/src/core/scaffold/auth-generator.ts +26 -27
  243. package/src/core/scaffold/model-parser.ts +413 -129
  244. package/src/core/scaffold/native-schema-generator.ts +319 -103
  245. package/src/core/scaffold/openapi-generator.ts +63 -5
  246. package/src/core/scaffold/react-bff-generator.ts +292 -0
  247. package/src/core/scaffold/react-ui-generator.ts +51 -0
  248. package/src/core/scaffold/ui-generator.ts +70 -8
  249. package/src/core/verify/index.ts +118 -6
  250. package/src/index.ts +3 -1
  251. package/src/machine/contracts.ts +1 -0
  252. package/src/machine/errors.ts +4 -2
  253. package/src/machine/index.ts +67 -36
  254. package/src/mcp/index.ts +221 -31
  255. package/src/mcp/version-selection.ts +163 -0
  256. package/src/types/index.ts +4 -0
  257. package/src/utils/azure-cli.ts +315 -0
  258. package/src/utils/azure-diagnostics.ts +75 -0
  259. package/src/utils/package-manager.ts +7 -4
  260. package/src/utils/python-uv.ts +53 -33
  261. package/src/utils/react-dev.ts +31 -0
  262. package/src/utils/semver.ts +59 -0
@@ -0,0 +1,529 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { BackendLanguage } from "../../types";
4
+ import { buildReactDevWatchIgnoredSource } from "../../utils/react-dev";
5
+ import { DEFAULT_AZURE_PYTHON_VERSION } from "../../utils/python-uv";
6
+ import { buildHomePage } from "./home-page";
7
+ import {
8
+ PackageManager,
9
+ getCommands,
10
+ getCiSetupStep,
11
+ getAzurePipelinesSetup,
12
+ } from "../../utils/package-manager";
13
+ import {
14
+ generateManagedFunctionConfig,
15
+ generateReactBffHelper,
16
+ generateReactGreetBff,
17
+ } from "../scaffold/react-bff-generator";
18
+
19
+ function write(
20
+ projectDir: string,
21
+ relativePath: string,
22
+ content: string
23
+ ): void {
24
+ const destination = path.join(projectDir, ...relativePath.split(/[\\/]/));
25
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
26
+ fs.writeFileSync(destination, content);
27
+ }
28
+
29
+ export function buildReactSwaConfig() {
30
+ return {
31
+ platform: { apiRuntime: "node:20" },
32
+ navigationFallback: {
33
+ rewrite: "/index.html",
34
+ exclude: [
35
+ "/api/*",
36
+ "/.auth/*",
37
+ "/assets/*",
38
+ "/*.{css,js,json,ico,png,svg,txt}",
39
+ ],
40
+ },
41
+ routes: [{ route: "/*", allowedRoles: ["anonymous"] }],
42
+ globalHeaders: {
43
+ "X-Content-Type-Options": "nosniff",
44
+ "X-Frame-Options": "DENY",
45
+ },
46
+ };
47
+ }
48
+
49
+ export function createReactProject(
50
+ projectDir: string,
51
+ projectName: string,
52
+ pm: PackageManager = "pnpm"
53
+ ): void {
54
+ write(
55
+ projectDir,
56
+ "package.json",
57
+ JSON.stringify(
58
+ {
59
+ name: projectName,
60
+ version: "0.1.0",
61
+ private: true,
62
+ scripts: { dev: "vite --port 3000", preview: "vite preview" },
63
+ dependencies: {
64
+ react: "^19.0.0",
65
+ "react-dom": "^19.0.0",
66
+ "react-router-dom": "^7.0.0",
67
+ },
68
+ devDependencies: {
69
+ "@types/node": "^22.0.0",
70
+ "@types/react": "^19.0.0",
71
+ "@types/react-dom": "^19.0.0",
72
+ "@vitejs/plugin-react": "^5.0.0",
73
+ vite: "^7.0.0",
74
+ typescript: "^5.9.0",
75
+ tailwindcss: "^4.0.0",
76
+ "@tailwindcss/vite": "^4.0.0",
77
+ "@azure/static-web-apps-cli": "^2.0.0",
78
+ },
79
+ },
80
+ null,
81
+ 2
82
+ )
83
+ );
84
+ write(
85
+ projectDir,
86
+ "tsconfig.json",
87
+ JSON.stringify(
88
+ {
89
+ compilerOptions: {
90
+ target: "ES2022",
91
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
92
+ module: "ESNext",
93
+ moduleResolution: "bundler",
94
+ jsx: "react-jsx",
95
+ strict: true,
96
+ noEmit: true,
97
+ esModuleInterop: true,
98
+ skipLibCheck: true,
99
+ resolveJsonModule: true,
100
+ paths: { "@/*": ["./*"] },
101
+ types: ["vite/client"],
102
+ },
103
+ include: ["src", "lib"],
104
+ exclude: ["node_modules", "functions", "bff", "shared", "dist"],
105
+ },
106
+ null,
107
+ 2
108
+ )
109
+ );
110
+ write(
111
+ projectDir,
112
+ "vite.config.mts",
113
+ `import { defineConfig } from 'vite';
114
+ import react from '@vitejs/plugin-react';
115
+ import tailwindcss from '@tailwindcss/vite';
116
+ import { fileURLToPath, URL } from 'node:url';
117
+
118
+ export default defineConfig({
119
+ plugins: [react(), tailwindcss()],
120
+ resolve: { alias: {
121
+ '@': fileURLToPath(new URL('.', import.meta.url)),
122
+ '@${projectName}/shared': fileURLToPath(new URL('./shared/index.ts', import.meta.url)),
123
+ } },
124
+ server: {
125
+ port: 3000,
126
+ strictPort: true,
127
+ watch: { ignored: ${buildReactDevWatchIgnoredSource("fileURLToPath(new URL('.', import.meta.url))")} },
128
+ },
129
+ build: { outDir: 'dist' },
130
+ });
131
+ `
132
+ );
133
+ write(
134
+ projectDir,
135
+ "index.html",
136
+ `<!doctype html>
137
+ <html lang="en" class="h-full antialiased">
138
+ <head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>SwallowKit</title></head>
139
+ <body class="min-h-full flex flex-col"><div id="root" class="flex flex-1 flex-col"></div><script type="module" src="/src/main.tsx"></script></body>
140
+ </html>
141
+ `
142
+ );
143
+ write(
144
+ projectDir,
145
+ "src\\main.tsx",
146
+ `import { StrictMode } from 'react';
147
+ import { createRoot } from 'react-dom/client';
148
+ import { BrowserRouter } from 'react-router-dom';
149
+ import App from './App';
150
+ import './style.css';
151
+
152
+ createRoot(document.getElementById('root')!).render(
153
+ <StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>,
154
+ );
155
+ `
156
+ );
157
+ write(
158
+ projectDir,
159
+ "src\\App.tsx",
160
+ `import { useRoutes, type RouteObject } from 'react-router-dom';
161
+ import Home from './Home';
162
+
163
+ const modules = import.meta.glob<RouteObject[]>('./pages/**/routes.tsx', { eager: true, import: 'default' });
164
+ const pages = import.meta.glob<{ default: React.ComponentType }>('./pages/login.tsx', { eager: true });
165
+ const Login = pages['./pages/login.tsx']?.default;
166
+
167
+ export default function App() {
168
+ return useRoutes([
169
+ { path: '/', element: <Home /> },
170
+ ...(Login ? [{ path: '/login', element: <Login /> }] : []),
171
+ ...Object.values(modules).flat(),
172
+ { path: '*', element: <main>Page not found</main> },
173
+ ]);
174
+ }
175
+ `
176
+ );
177
+ write(
178
+ projectDir,
179
+ "src\\Home.tsx",
180
+ buildHomePage("react", pm)
181
+ );
182
+ write(projectDir, "src\\style.css", `@import "tailwindcss";
183
+
184
+ :root {
185
+ --background: #ffffff;
186
+ --foreground: #171717;
187
+ }
188
+
189
+ @theme inline {
190
+ --color-background: var(--background);
191
+ --color-foreground: var(--foreground);
192
+ --font-sans: Arial, Helvetica, sans-serif;
193
+ }
194
+
195
+ @media (prefers-color-scheme: dark) {
196
+ :root {
197
+ --background: #0a0a0a;
198
+ --foreground: #ededed;
199
+ }
200
+ }
201
+
202
+ body {
203
+ background: var(--background);
204
+ color: var(--foreground);
205
+ font-family: Arial, Helvetica, sans-serif;
206
+ }
207
+ `);
208
+ write(
209
+ projectDir,
210
+ "lib\\scaffold-config.ts",
211
+ `export interface ScaffoldModel { name: string; path: string; label: string; }
212
+ export const scaffoldConfig = {
213
+ models: [] as ScaffoldModel[]
214
+ };
215
+ `
216
+ );
217
+ write(
218
+ projectDir,
219
+ ".gitignore",
220
+ "node_modules/\ndist/\n.env*.local\nlocal.settings.json\n"
221
+ );
222
+ }
223
+
224
+ export function createReactBffProject(
225
+ projectDir: string,
226
+ projectName: string,
227
+ pm: PackageManager
228
+ ): void {
229
+ write(
230
+ projectDir,
231
+ "bff\\package.json",
232
+ JSON.stringify(
233
+ {
234
+ name: "bff",
235
+ private: true,
236
+ version: "1.0.0",
237
+ description:
238
+ "SWA managed HTTP-only Functions BFF (not the standalone backend)",
239
+ engines: { node: ">=20" },
240
+ scripts: {
241
+ build: "tsc --noEmit && node build.cjs",
242
+ start: "func start --port 7073",
243
+ prestart: `${pm} run build`,
244
+ },
245
+ devDependencies: {
246
+ "@azure/functions": "^3.5.0",
247
+ "@types/node": "^22.0.0",
248
+ typescript: "^5.9.0",
249
+ esbuild: "^0.25.0",
250
+ [`@${projectName}/shared`]:
251
+ pm === "pnpm" ? "workspace:*" : "file:../shared",
252
+ zod: "^4.0.0",
253
+ },
254
+ },
255
+ null,
256
+ 2
257
+ )
258
+ );
259
+ write(projectDir, "bff\\build.cjs", buildReactBffBuildSource());
260
+ write(
261
+ projectDir,
262
+ "bff\\tsconfig.json",
263
+ JSON.stringify(
264
+ {
265
+ compilerOptions: {
266
+ target: "ES2022",
267
+ lib: ["ES2022", "DOM"],
268
+ module: "Node16",
269
+ moduleResolution: "Node16",
270
+ strict: true,
271
+ esModuleInterop: true,
272
+ skipLibCheck: true,
273
+ noEmit: true,
274
+ },
275
+ include: ["src/**/*.ts"],
276
+ },
277
+ null,
278
+ 2
279
+ )
280
+ );
281
+ // Classic function.json registrations work with SWA's managed Functions runtime.
282
+ write(
283
+ projectDir,
284
+ "bff\\host.json",
285
+ JSON.stringify(
286
+ { version: "2.0", extensions: { http: { routePrefix: "api" } } },
287
+ null,
288
+ 2
289
+ )
290
+ );
291
+ write(
292
+ projectDir,
293
+ "bff\\local.settings.json",
294
+ JSON.stringify(
295
+ {
296
+ IsEncrypted: false,
297
+ Values: {
298
+ FUNCTIONS_WORKER_RUNTIME: "node",
299
+ BACKEND_FUNCTIONS_BASE_URL: "http://localhost:7071",
300
+ BACKEND_FUNCTIONS_KEY: "",
301
+ },
302
+ Host: { LocalHttpPort: 7073, CORS: "http://localhost:4280" },
303
+ },
304
+ null,
305
+ 2
306
+ )
307
+ );
308
+ write(
309
+ projectDir,
310
+ "bff\\.gitignore",
311
+ "node_modules/\ndist/\nlocal.settings.json\n"
312
+ );
313
+ write(
314
+ projectDir,
315
+ "bff\\.funcignore",
316
+ "src/\nnode_modules/\nlocal.settings.json\ntsconfig.json\nbuild.cjs\n.gitignore\n"
317
+ );
318
+ write(
319
+ projectDir,
320
+ "bff\\src\\lib\\call-function.ts",
321
+ generateReactBffHelper(projectName)
322
+ );
323
+ write(projectDir, "bff\\src\\greet\\index.ts", generateReactGreetBff());
324
+ write(
325
+ projectDir,
326
+ "bff\\greet\\function.json",
327
+ generateManagedFunctionConfig("greet", ["GET", "POST"], "greet")
328
+ );
329
+ }
330
+
331
+ export function buildReactBffBuildSource(): string {
332
+ return `const fs = require('node:fs');
333
+ const path = require('node:path');
334
+ const { buildSync } = require('esbuild');
335
+
336
+ const entryPoints = fs.readdirSync(path.join(__dirname, 'src'), { withFileTypes: true })
337
+ .filter(entry => entry.isDirectory() && fs.existsSync(path.join(__dirname, 'src', entry.name, 'index.ts')))
338
+ .map(entry => path.join('src', entry.name, 'index.ts'));
339
+ if (!entryPoints.length) throw new Error('No managed BFF handlers found');
340
+ buildSync({
341
+ absWorkingDir: __dirname, entryPoints, bundle: true,
342
+ platform: 'node', target: 'node20', outbase: 'src', outdir: 'dist', logLevel: 'info',
343
+ });
344
+ for (const entry of entryPoints) {
345
+ const output = path.join(__dirname, 'dist', path.basename(path.dirname(entry)), 'index.js');
346
+ if (!fs.existsSync(output)) throw new Error('Missing managed BFF bundle: ' + output);
347
+ }
348
+ `;
349
+ }
350
+
351
+ export function buildReactReadme(
352
+ projectName: string,
353
+ pm: PackageManager,
354
+ backend: BackendLanguage
355
+ ): string {
356
+ const cmd = getCommands(pm);
357
+ return `# ${projectName}
358
+
359
+ Generated by SwallowKit: React + Vite + TypeScript on Azure Static Web Apps.
360
+
361
+ ## Architecture
362
+
363
+ Browser → React SPA → /api (SWA managed Functions BFF) → standalone ${backend} Functions → Cosmos DB.
364
+
365
+ - \`src/\`: React Router UI. No Next.js runtime.
366
+ - \`bff/\`: HTTP-only SWA managed Functions. Validates shared schemas and forwards requests; no database/business logic.
367
+ - \`functions/\`: independently deployed backend, NOT linked to SWA.
368
+ - \`shared/models/\`: Zod source of truth.
369
+ - \`infra/\`: Bicep infrastructure.
370
+
371
+ Only the BFF reads BACKEND_FUNCTIONS_BASE_URL and BACKEND_FUNCTIONS_KEY.
372
+ Never put those values in VITE_* variables or client code.
373
+
374
+ ## Develop
375
+
376
+ \`\`\`sh
377
+ ${cmd.exec} swallowkit create-model todo
378
+ ${cmd.exec} swallowkit scaffold todo
379
+ ${cmd.exec} swallowkit dev
380
+ \`\`\`
381
+
382
+ Use http://localhost:4280, not Vite's direct port, for same-origin UI/BFF requests.
383
+ Dev starts Vite (3000), the managed BFF (7073), the standalone Functions (7071),
384
+ and SWA CLI (4280). \`--bff-port\`, \`--port\`, \`--functions-port\`, and \`--swa-port\` override ports.
385
+ The BFF stays separate even with \`--no-functions\`; \`--no-swa\` starts only the
386
+ direct Vite/BFF servers (no same-origin API routing or SWA authentication).
387
+ Core Tools v4 must already be available. Start Cosmos DB Emulator manually if
388
+ this application needs Cosmos; SwallowKit never starts it.
389
+ ${backend === "python" ? `
390
+ Python environments use normal uv discovery and user settings, including installed
391
+ Python, \`.python-version\`, and \`UV_PYTHON\`. Local Python is not pinned to the Azure
392
+ deployment runtime (${DEFAULT_AZURE_PYTHON_VERSION}). Functions uses \`functions/.venv\`; scaffold uses
393
+ \`functions/.codegen-venv\`. Existing environments are never cleared automatically.
394
+ When switching interpreters, explicitly recreate them with uv. Do not disable
395
+ Windows application-control policy to run an unsigned interpreter.
396
+ ` : backend === "csharp" ? `
397
+ The .NET isolated backend starts with \`dotnet run\` so extensions are loaded from
398
+ the build output. Vite does not watch \`functions/\` or \`bff/\` build artifacts.
399
+ ` : ""}
400
+
401
+ Local BFF settings live in \`bff/local.settings.json\` (gitignored). Backend settings
402
+ live in \`functions/local.settings.json\`. Keep their secrets out of source control.
403
+ \`--mock-connectors\` still routes through the BFF and mocks only connector data.
404
+
405
+ ## Verify
406
+
407
+ \`\`\`sh
408
+ ${pm} run build
409
+ ${cmd.exec} swallowkit machine verify project
410
+ ${cmd.exec} swallowkit machine verify project --checks "structure,drift,typecheck,backend-build,build"
411
+ \`\`\`
412
+
413
+ The build compiles shared schemas, checks the SPA, builds static assets, and
414
+ typechecks/bundles the managed BFF. Exercise CRUD and authentication through
415
+ the SWA port as well; structural verification is not an end-to-end test.
416
+
417
+ ## Deploy
418
+
419
+ Use the generated GitHub Actions / Azure Pipelines workflows when selected.
420
+ Deploy \`dist/\` as the static app and \`bff/\` as its managed API, with
421
+ \`skip_app_build: true\` and \`skip_api_build: true\` after \`${pm} run build\`.
422
+ BFF bundles include shared schemas and Zod, so no workspace install is needed
423
+ in SWA's API build. Deploy \`functions/\` independently, never as SWA's api_location.
424
+ Do NOT link the standalone Functions app to SWA.
425
+
426
+ After approved provisioning and backend deployment, apply the generated SWA
427
+ appsettings module with the backend URL/key. The key is required for remote
428
+ Functions calls, and stays server-side. SPA deep links use navigationFallback;
429
+ /api and /.auth are excluded so missing APIs never become index.html.
430
+
431
+ See AGENTS.md for ownership, plan/apply, authentication, and verification guidance.
432
+ `;
433
+ }
434
+
435
+ const REACT_DEPLOY_PATHS = [
436
+ "src/**",
437
+ "bff/**",
438
+ "lib/**",
439
+ "shared/**",
440
+ "public/**",
441
+ "index.html",
442
+ "vite.config.mts",
443
+ "tsconfig.json",
444
+ "staticwebapp.config.json",
445
+ "package.json",
446
+ "pnpm-workspace.yaml",
447
+ ];
448
+
449
+ export function buildReactGitHubSwaWorkflow(pm: PackageManager): string {
450
+ const paths = [
451
+ ...REACT_DEPLOY_PATHS,
452
+ pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json",
453
+ ".github/workflows/deploy-swa.yml",
454
+ ];
455
+ return `name: Deploy React SPA and managed BFF
456
+ on:
457
+ push:
458
+ branches: [main]
459
+ paths:
460
+ ${paths.map((entry) => ` - '${entry}'`).join("\n")}
461
+ pull_request:
462
+ branches: [main]
463
+ paths:
464
+ ${paths.map((entry) => ` - '${entry}'`).join("\n")}
465
+ workflow_dispatch:
466
+ jobs:
467
+ build-and-deploy:
468
+ runs-on: ubuntu-latest
469
+ steps:
470
+ - uses: actions/checkout@v4
471
+ ${getCiSetupStep(pm)}
472
+ - uses: actions/setup-node@v4
473
+ with:
474
+ node-version: '22'
475
+ cache: ${pm}
476
+ - name: Build SPA and managed BFF
477
+ run: |
478
+ ${getCommands(pm).ci}
479
+ ${pm} run build
480
+ - name: Deploy SPA and managed BFF (standalone Functions are NOT linked)
481
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
482
+ uses: Azure/static-web-apps-deploy@v1
483
+ with:
484
+ azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
485
+ repo_token: \${{ secrets.GITHUB_TOKEN }}
486
+ action: upload
487
+ app_location: dist
488
+ api_location: bff
489
+ output_location: ''
490
+ skip_app_build: true
491
+ skip_api_build: true
492
+ `;
493
+ }
494
+
495
+ export function buildReactAzureSwaPipeline(pm: PackageManager): string {
496
+ return `trigger:
497
+ branches:
498
+ include: [main]
499
+ paths:
500
+ include:
501
+ ${[...REACT_DEPLOY_PATHS, pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json", "pipelines/swa.yml"].map((entry) => ` - '${entry}'`).join("\n")}
502
+ pr:
503
+ branches:
504
+ include: [main]
505
+ pool:
506
+ vmImage: ubuntu-latest
507
+ variables:
508
+ - group: azure-deployment
509
+ steps:
510
+ - task: NodeTool@0
511
+ inputs:
512
+ versionSpec: '22.x'
513
+ ${getAzurePipelinesSetup(pm)}
514
+ - script: |
515
+ ${getCommands(pm).ci}
516
+ ${pm} run build
517
+ displayName: Build React SPA and managed BFF
518
+ - task: AzureStaticWebApp@0
519
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
520
+ inputs:
521
+ app_location: dist
522
+ api_location: bff
523
+ output_location: ''
524
+ skip_app_build: true
525
+ skip_api_build: true
526
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
527
+ displayName: Deploy SPA and managed BFF (standalone Functions are NOT linked)
528
+ `;
529
+ }
@@ -57,6 +57,76 @@ export function deletePlanState(planId: string, projectRoot: string = process.cw
57
57
  }
58
58
  }
59
59
 
60
+ function provisionRunPath(runId: string, projectRoot: string): string {
61
+ if (!/^[a-f0-9]{12}$/.test(runId)) throw new Error("Invalid provision run ID.");
62
+ return path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision", `${runId}.json`);
63
+ }
64
+
65
+ export function saveProvisionRun<T extends { runId: string }>(run: T, projectRoot: string = process.cwd()): void {
66
+ ensureStateDir(projectRoot);
67
+ const file = provisionRunPath(run.runId, projectRoot);
68
+ fs.mkdirSync(path.dirname(file), { recursive: true });
69
+ const temporary = `${file}.tmp-${process.pid}`;
70
+ fs.writeFileSync(temporary, `${JSON.stringify(run, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
71
+ fs.renameSync(temporary, file);
72
+ }
73
+
74
+ export function loadProvisionRun<T>(runId: string, projectRoot: string = process.cwd()): T | null {
75
+ const file = provisionRunPath(runId, projectRoot);
76
+ if (!fs.existsSync(file)) return null;
77
+ // Unlike a preview, discarding a corrupt execution checkpoint could replay Azure writes.
78
+ return JSON.parse(fs.readFileSync(file, "utf-8")) as T;
79
+ }
80
+
81
+ export function listProvisionRuns<T>(projectRoot: string = process.cwd()): T[] {
82
+ const directory = path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision");
83
+ if (!fs.existsSync(directory)) return [];
84
+ return fs.readdirSync(directory).filter((name) => /^[a-f0-9]{12}\.json$/.test(name))
85
+ .map((name) => loadProvisionRun<T>(name.slice(0, -5), projectRoot)!);
86
+ }
87
+
88
+ /** A live local executor must not share an Azure target with another executor. */
89
+ export function lockProvisionTarget(key: string, projectRoot: string = process.cwd()): () => void {
90
+ if (!/^[a-f0-9]{64}$/.test(key)) throw new Error("Invalid provision lock key.");
91
+ const directory = path.join(ensureStateDir(projectRoot), "provision");
92
+ fs.mkdirSync(directory, { recursive: true });
93
+ const file = path.join(directory, `${key}.lock`);
94
+ const create = () => {
95
+ const descriptor = fs.openSync(file, "wx", 0o600);
96
+ try { fs.writeFileSync(descriptor, String(process.pid)); } finally { fs.closeSync(descriptor); }
97
+ };
98
+ try {
99
+ create();
100
+ } catch (error) {
101
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
102
+ const recoveryFile = `${file}.recovery`;
103
+ let descriptor: number;
104
+ try {
105
+ descriptor = fs.openSync(recoveryFile, "wx", 0o600);
106
+ } catch (recoveryError) {
107
+ if ((recoveryError as NodeJS.ErrnoException).code !== "EEXIST") throw recoveryError;
108
+ throw new Error("Provision lock recovery is already in progress. Inspect the checkpoint before retrying.");
109
+ }
110
+ fs.closeSync(descriptor);
111
+ try {
112
+ const pid = Number(fs.readFileSync(file, "utf-8"));
113
+ if (!Number.isInteger(pid) || pid < 1) throw new Error("Invalid provision lock; inspect the checkpoint before removing the lock.");
114
+ try {
115
+ process.kill(pid, 0);
116
+ } catch (probeError) {
117
+ if ((probeError as NodeJS.ErrnoException).code !== "ESRCH") throw probeError;
118
+ fs.unlinkSync(file);
119
+ create();
120
+ return () => fs.unlinkSync(file);
121
+ }
122
+ throw new Error("Another local provision executor is using this Azure target. Inspect its run; do not start a second deployment.");
123
+ } finally {
124
+ fs.unlinkSync(recoveryFile);
125
+ }
126
+ }
127
+ return () => fs.unlinkSync(file);
128
+ }
129
+
60
130
  const LAST_VERIFY_FILE = "last-verify.json";
61
131
 
62
132
  export function saveLastVerifyState(result: unknown, projectRoot: string = process.cwd()): void {
@@ -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") {