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.
- package/README.ja.md +61 -7
- package/README.md +61 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +103 -27
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +5 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +143 -67
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +14 -7
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +149 -170
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +5 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +123 -18
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +49 -8
- 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/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.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 +15 -0
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +17 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +45 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- 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/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- 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 +443 -0
- package/dist/core/project/react-project.js.map +1 -0
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- 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/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.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/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- 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/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +61 -30
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- 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 +6 -3
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +7 -4
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +6 -1
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +145 -5
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +838 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +103 -29
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +145 -73
- package/src/cli/commands/init.ts +147 -172
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +145 -22
- package/src/cli/index.ts +46 -10
- package/src/core/config.ts +21 -1
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +17 -1
- package/src/core/project/agent-templates/AGENTS.md +113 -15
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +63 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +529 -0
- package/src/core/project/state.ts +70 -0
- package/src/core/project/validation.ts +32 -10
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/index.ts +3 -1
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +67 -36
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/types/index.ts +4 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +7 -4
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- 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:
|
|
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") {
|