swallowkit 1.0.0-beta.3 → 1.0.0-beta.31

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 (201) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +353 -215
  3. package/README.md +406 -216
  4. package/dist/__tests__/fixtures.d.ts +22 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +146 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/add-auth.d.ts +10 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +444 -0
  11. package/dist/cli/commands/add-auth.js.map +1 -0
  12. package/dist/cli/commands/add-connector.d.ts +20 -0
  13. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  14. package/dist/cli/commands/add-connector.js +163 -0
  15. package/dist/cli/commands/add-connector.js.map +1 -0
  16. package/dist/cli/commands/create-model.d.ts +1 -4
  17. package/dist/cli/commands/create-model.d.ts.map +1 -1
  18. package/dist/cli/commands/create-model.js +21 -82
  19. package/dist/cli/commands/create-model.js.map +1 -1
  20. package/dist/cli/commands/dev-seeds.d.ts +57 -0
  21. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  22. package/dist/cli/commands/dev-seeds.js +470 -0
  23. package/dist/cli/commands/dev-seeds.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts +33 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +628 -146
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/index.d.ts +1 -0
  29. package/dist/cli/commands/index.d.ts.map +1 -1
  30. package/dist/cli/commands/index.js +3 -1
  31. package/dist/cli/commands/index.js.map +1 -1
  32. package/dist/cli/commands/init.d.ts +15 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +2696 -1706
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  37. package/dist/cli/commands/scaffold.js +448 -129
  38. package/dist/cli/commands/scaffold.js.map +1 -1
  39. package/dist/cli/index.d.ts +5 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +200 -42
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/core/config.d.ts +8 -2
  44. package/dist/core/config.d.ts.map +1 -1
  45. package/dist/core/config.js +94 -5
  46. package/dist/core/config.js.map +1 -1
  47. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  48. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  49. package/dist/core/mock/connector-mock-server.js +480 -0
  50. package/dist/core/mock/connector-mock-server.js.map +1 -0
  51. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  53. package/dist/core/mock/zod-mock-generator.js +163 -0
  54. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  55. package/dist/core/operations/create-model.d.ts +15 -0
  56. package/dist/core/operations/create-model.d.ts.map +1 -0
  57. package/dist/core/operations/create-model.js +171 -0
  58. package/dist/core/operations/create-model.js.map +1 -0
  59. package/dist/core/operations/runtime.d.ts +32 -0
  60. package/dist/core/operations/runtime.d.ts.map +1 -0
  61. package/dist/core/operations/runtime.js +225 -0
  62. package/dist/core/operations/runtime.js.map +1 -0
  63. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  64. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  65. package/dist/core/operations/scaffold-machine.js +63 -0
  66. package/dist/core/operations/scaffold-machine.js.map +1 -0
  67. package/dist/core/project/manifest.d.ts +92 -0
  68. package/dist/core/project/manifest.d.ts.map +1 -0
  69. package/dist/core/project/manifest.js +321 -0
  70. package/dist/core/project/manifest.js.map +1 -0
  71. package/dist/core/project/validation.d.ts +20 -0
  72. package/dist/core/project/validation.d.ts.map +1 -0
  73. package/dist/core/project/validation.js +209 -0
  74. package/dist/core/project/validation.js.map +1 -0
  75. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  76. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  77. package/dist/core/scaffold/auth-generator.js +1244 -0
  78. package/dist/core/scaffold/auth-generator.js.map +1 -0
  79. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  80. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  81. package/dist/core/scaffold/connector-functions-generator.js +1027 -0
  82. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  83. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  84. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  85. package/dist/core/scaffold/functions-generator.js +920 -213
  86. package/dist/core/scaffold/functions-generator.js.map +1 -1
  87. package/dist/core/scaffold/model-parser.d.ts +20 -1
  88. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  89. package/dist/core/scaffold/model-parser.js +328 -135
  90. package/dist/core/scaffold/model-parser.js.map +1 -1
  91. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  92. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  93. package/dist/core/scaffold/native-schema-generator.js +677 -0
  94. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  95. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  96. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  97. package/dist/core/scaffold/nextjs-generator.js +314 -182
  98. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  99. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  100. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  101. package/dist/core/scaffold/openapi-generator.js +190 -0
  102. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  103. package/dist/core/scaffold/ui-generator.d.ts +10 -4
  104. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  105. package/dist/core/scaffold/ui-generator.js +768 -663
  106. package/dist/core/scaffold/ui-generator.js.map +1 -1
  107. package/dist/database/base-model.d.ts +3 -3
  108. package/dist/database/base-model.js +3 -3
  109. package/dist/index.d.ts +2 -2
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +2 -1
  112. package/dist/index.js.map +1 -1
  113. package/dist/machine/contracts.d.ts +16 -0
  114. package/dist/machine/contracts.d.ts.map +1 -0
  115. package/dist/machine/contracts.js +3 -0
  116. package/dist/machine/contracts.js.map +1 -0
  117. package/dist/machine/errors.d.ts +11 -0
  118. package/dist/machine/errors.d.ts.map +1 -0
  119. package/dist/machine/errors.js +34 -0
  120. package/dist/machine/errors.js.map +1 -0
  121. package/dist/machine/index.d.ts +3 -0
  122. package/dist/machine/index.d.ts.map +1 -0
  123. package/dist/machine/index.js +156 -0
  124. package/dist/machine/index.js.map +1 -0
  125. package/dist/mcp/index.d.ts +25 -0
  126. package/dist/mcp/index.d.ts.map +1 -0
  127. package/dist/mcp/index.js +184 -0
  128. package/dist/mcp/index.js.map +1 -0
  129. package/dist/types/index.d.ts +65 -0
  130. package/dist/types/index.d.ts.map +1 -1
  131. package/dist/utils/package-manager.d.ts +109 -0
  132. package/dist/utils/package-manager.d.ts.map +1 -0
  133. package/dist/utils/package-manager.js +215 -0
  134. package/dist/utils/package-manager.js.map +1 -0
  135. package/dist/utils/python-uv.d.ts +21 -0
  136. package/dist/utils/python-uv.d.ts.map +1 -0
  137. package/dist/utils/python-uv.js +111 -0
  138. package/dist/utils/python-uv.js.map +1 -0
  139. package/package.json +85 -73
  140. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  141. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  142. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  143. package/src/__tests__/auth.test.ts +654 -0
  144. package/src/__tests__/config.test.ts +274 -0
  145. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  146. package/src/__tests__/connector-mock-server.test.ts +439 -0
  147. package/src/__tests__/connector-model-bff.test.ts +162 -0
  148. package/src/__tests__/dev-seeds.test.ts +173 -0
  149. package/src/__tests__/dev.test.ts +252 -0
  150. package/src/__tests__/fixtures.ts +144 -0
  151. package/src/__tests__/functions-generator.test.ts +237 -0
  152. package/src/__tests__/init.test.ts +115 -0
  153. package/src/__tests__/machine.test.ts +251 -0
  154. package/src/__tests__/mcp.test.ts +117 -0
  155. package/src/__tests__/model-parser.test.ts +52 -0
  156. package/src/__tests__/nextjs-generator.test.ts +97 -0
  157. package/src/__tests__/openapi-generator.test.ts +43 -0
  158. package/src/__tests__/package-manager.test.ts +189 -0
  159. package/src/__tests__/python-uv.test.ts +48 -0
  160. package/src/__tests__/scaffold.test.ts +67 -0
  161. package/src/__tests__/string-utils.test.ts +75 -0
  162. package/src/__tests__/ui-generator.test.ts +144 -0
  163. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  164. package/src/cli/commands/add-auth.ts +500 -0
  165. package/src/cli/commands/add-connector.ts +158 -0
  166. package/src/cli/commands/create-model.ts +62 -0
  167. package/src/cli/commands/dev-seeds.ts +614 -0
  168. package/src/cli/commands/dev.ts +1134 -0
  169. package/src/cli/commands/index.ts +9 -0
  170. package/src/cli/commands/init.ts +3480 -0
  171. package/src/cli/commands/provision.ts +193 -0
  172. package/src/cli/commands/scaffold.ts +1001 -0
  173. package/src/cli/index.ts +196 -0
  174. package/src/core/config.ts +312 -0
  175. package/src/core/mock/connector-mock-server.ts +555 -0
  176. package/src/core/mock/zod-mock-generator.ts +205 -0
  177. package/src/core/operations/create-model.ts +174 -0
  178. package/src/core/operations/runtime.ts +235 -0
  179. package/src/core/operations/scaffold-machine.ts +91 -0
  180. package/src/core/project/manifest.ts +402 -0
  181. package/src/core/project/validation.ts +229 -0
  182. package/src/core/scaffold/auth-generator.ts +1284 -0
  183. package/src/core/scaffold/connector-functions-generator.ts +1128 -0
  184. package/src/core/scaffold/functions-generator.ts +970 -0
  185. package/src/core/scaffold/model-parser.ts +841 -0
  186. package/src/core/scaffold/native-schema-generator.ts +798 -0
  187. package/src/core/scaffold/nextjs-generator.ts +370 -0
  188. package/src/core/scaffold/openapi-generator.ts +212 -0
  189. package/src/core/scaffold/ui-generator.ts +1061 -0
  190. package/src/database/base-model.ts +184 -0
  191. package/src/database/client.ts +140 -0
  192. package/src/database/repository.ts +104 -0
  193. package/src/database/runtime-check.ts +25 -0
  194. package/src/index.ts +27 -0
  195. package/src/machine/contracts.ts +17 -0
  196. package/src/machine/errors.ts +34 -0
  197. package/src/machine/index.ts +173 -0
  198. package/src/mcp/index.ts +185 -0
  199. package/src/types/index.ts +134 -0
  200. package/src/utils/package-manager.ts +229 -0
  201. package/src/utils/python-uv.ts +96 -0
@@ -37,10 +37,60 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.initCommand = initCommand;
40
+ exports.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
41
+ exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
42
+ exports.buildCSharpFunctionsProjectSource = buildCSharpFunctionsProjectSource;
43
+ exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
44
+ exports.buildGeneratedProjectDependencies = buildGeneratedProjectDependencies;
45
+ exports.buildGeneratedProjectDevDependencies = buildGeneratedProjectDevDependencies;
46
+ exports.buildSwallowKitMcpProjectConfigSource = buildSwallowKitMcpProjectConfigSource;
40
47
  const fs = __importStar(require("fs"));
41
48
  const path = __importStar(require("path"));
42
49
  const child_process_1 = require("child_process");
43
50
  const prompts_1 = __importDefault(require("prompts"));
51
+ const package_manager_1 = require("../../utils/package-manager");
52
+ const manifest_1 = require("../../core/project/manifest");
53
+ const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
54
+ const python_uv_1 = require("../../utils/python-uv");
55
+ const BACKEND_LANGUAGE_CHOICES = [
56
+ { title: "TypeScript", value: "typescript" },
57
+ { title: "C#", value: "csharp" },
58
+ { title: "Python", value: "python" },
59
+ ];
60
+ function usesNodeFunctionsProject(backendLanguage) {
61
+ return backendLanguage === "typescript";
62
+ }
63
+ function getBackendLanguageLabel(backendLanguage) {
64
+ return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
65
+ }
66
+ function getFunctionsWorkerRuntime(backendLanguage) {
67
+ if (backendLanguage === "csharp") {
68
+ return "dotnet-isolated";
69
+ }
70
+ if (backendLanguage === "python") {
71
+ return "python";
72
+ }
73
+ return "node";
74
+ }
75
+ function getFunctionsRuntimeConfig(backendLanguage) {
76
+ if (backendLanguage === "csharp") {
77
+ return { name: "dotnet-isolated", version: "10.0" };
78
+ }
79
+ if (backendLanguage === "python") {
80
+ return { name: "python", version: "3.11" };
81
+ }
82
+ return { name: "node", version: "22" };
83
+ }
84
+ async function promptBackendLanguage() {
85
+ const response = await (0, prompts_1.default)({
86
+ type: "select",
87
+ name: "backendLanguage",
88
+ message: "Azure Functions backend language:",
89
+ choices: BACKEND_LANGUAGE_CHOICES,
90
+ initial: 0,
91
+ });
92
+ return response.backendLanguage || "typescript";
93
+ }
44
94
  async function promptCiCd() {
45
95
  const response = await (0, prompts_1.default)({
46
96
  type: 'select',
@@ -81,9 +131,37 @@ async function promptAzureConfig() {
81
131
  vnetOption: vnetResponse.vnet || 'outbound'
82
132
  };
83
133
  }
134
+ const VALID_CICD = ['github', 'azure', 'skip'];
135
+ const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
136
+ const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
137
+ const VALID_VNET = ['none', 'outbound'];
138
+ function validateInitFlags(options) {
139
+ if (options.cicd && !VALID_CICD.includes(options.cicd)) {
140
+ console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
141
+ process.exit(1);
142
+ }
143
+ if (options.backendLanguage && !VALID_BACKEND_LANGUAGE.includes(options.backendLanguage)) {
144
+ console.error(`❌ Invalid --backend-language value: "${options.backendLanguage}". Must be: ${VALID_BACKEND_LANGUAGE.join(', ')}`);
145
+ process.exit(1);
146
+ }
147
+ if (options.cosmosDbMode && !VALID_COSMOS_DB_MODE.includes(options.cosmosDbMode)) {
148
+ console.error(`❌ Invalid --cosmos-db-mode value: "${options.cosmosDbMode}". Must be: ${VALID_COSMOS_DB_MODE.join(', ')}`);
149
+ process.exit(1);
150
+ }
151
+ if (options.vnet && !VALID_VNET.includes(options.vnet)) {
152
+ console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
153
+ process.exit(1);
154
+ }
155
+ }
84
156
  async function initCommand(options) {
157
+ // Validate flag values before doing anything
158
+ validateInitFlags(options);
85
159
  console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
86
160
  console.log(`📋 Template: ${options.template}`);
161
+ // Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
162
+ const pm = (0, package_manager_1.detectFromUserAgent)();
163
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
164
+ console.log(`📦 Package manager: ${pm}`);
87
165
  const projectDir = path.join(process.cwd(), options.name);
88
166
  try {
89
167
  // Check if directory already exists
@@ -91,25 +169,28 @@ async function initCommand(options) {
91
169
  console.error(`❌ Directory "${options.name}" already exists.`);
92
170
  process.exit(1);
93
171
  }
94
- // Ask for CI/CD choice FIRST (before long operations)
95
- const cicdProvider = await promptCiCd();
96
- // Ask for Azure infrastructure configuration
97
- const azureConfig = await promptAzureConfig();
172
+ // Use flag values if provided, otherwise prompt interactively
173
+ const cicdProvider = options.cicd || await promptCiCd();
174
+ const backendLanguage = options.backendLanguage || await promptBackendLanguage();
175
+ const azureConfig = (options.cosmosDbMode && options.vnet)
176
+ ? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
177
+ : await promptAzureConfig();
98
178
  // Create Next.js project with create-next-app
99
- await createNextJsProject(options.name);
179
+ await createNextJsProject(options.name, pm);
100
180
  // Upgrade Next.js to specified version (or latest) to avoid cached old versions
101
- await upgradeNextJs(projectDir, options.nextVersion || 'latest');
181
+ await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
102
182
  // Add SwallowKit specific files
103
- await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig);
183
+ await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
104
184
  // Create infrastructure files (Bicep)
105
- await createInfrastructure(projectDir, options.name, azureConfig);
185
+ await createInfrastructure(projectDir, options.name, azureConfig, backendLanguage);
106
186
  // Create CI/CD files based on choice
107
187
  if (cicdProvider === 'github') {
108
- await createGitHubActionsWorkflows(projectDir, azureConfig);
188
+ await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
109
189
  }
110
190
  else if (cicdProvider === 'azure') {
111
- await createAzurePipelines(projectDir);
191
+ await createAzurePipelines(projectDir, pm, backendLanguage);
112
192
  }
193
+ await (0, manifest_1.syncProjectManifest)(projectDir);
113
194
  // Initialize Git repository and create initial commit
114
195
  try {
115
196
  // Try git init with -b main (Git 2.28+), fallback to git init
@@ -153,11 +234,11 @@ async function initCommand(options) {
153
234
  console.log(`\n✅ Project "${options.name}" created successfully!`);
154
235
  console.log("\n📝 Next steps:");
155
236
  console.log(` cd ${options.name}`);
156
- console.log(" npx swallowkit create-model <name> # Create your first model");
157
- console.log(" npx swallowkit scaffold lib/models/<name>.ts # Generate CRUD code");
158
- console.log(" npx swallowkit dev # Start development servers");
237
+ console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
238
+ console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
239
+ console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
159
240
  console.log("\n🚀 Deploy to Azure:");
160
- console.log(" npx swallowkit provision --resource-group <name>");
241
+ console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
161
242
  if (cicdProvider !== 'skip') {
162
243
  console.log(" Configure CI/CD secrets and push to repository");
163
244
  }
@@ -171,12 +252,17 @@ async function initCommand(options) {
171
252
  process.exit(1);
172
253
  }
173
254
  }
174
- async function createNextJsProject(projectName) {
255
+ async function createNextJsProject(projectName, pm) {
175
256
  return new Promise((resolve, reject) => {
176
257
  console.log('\n📦 Creating Next.js project with create-next-app...\n');
177
- // Run create-next-app with recommended options for Azure
178
- const createNextApp = (0, child_process_1.spawn)('npx', [
179
- 'create-next-app@latest',
258
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
259
+ // Build args: for pnpm use "pnpm dlx create-next-app@latest ... --use-pnpm"
260
+ // for npm use "npx create-next-app@latest ..."
261
+ const baseArgs = pm === 'pnpm'
262
+ ? ['dlx', 'create-next-app@latest']
263
+ : ['create-next-app@latest'];
264
+ const args = [
265
+ ...baseArgs,
180
266
  projectName,
181
267
  '--typescript',
182
268
  '--tailwind',
@@ -185,9 +271,11 @@ async function createNextJsProject(projectName) {
185
271
  '--disable-git',
186
272
  '--import-alias',
187
273
  '@/*',
188
- '--use-npm',
274
+ ...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
189
275
  '--yes'
190
- ], {
276
+ ];
277
+ // Run create-next-app with recommended options for Azure
278
+ const createNextApp = (0, child_process_1.spawn)(pm === 'pnpm' ? 'pnpm' : 'npx', args, {
191
279
  stdio: 'inherit',
192
280
  shell: true,
193
281
  });
@@ -205,83 +293,202 @@ async function createNextJsProject(projectName) {
205
293
  });
206
294
  });
207
295
  }
208
- async function upgradeNextJs(projectDir, version) {
296
+ async function upgradeNextJs(projectDir, version, pm) {
209
297
  return new Promise((resolve, reject) => {
210
298
  console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
211
- const npmInstall = (0, child_process_1.spawn)('npm', [
212
- 'install',
213
- `next@${version}`,
214
- `react@latest`,
215
- `react-dom@latest`,
216
- '--save-exact'
217
- ], {
299
+ // pnpm: pnpm add next@... ; npm: npm install next@...
300
+ const args = pm === 'pnpm'
301
+ ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
302
+ : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
303
+ const child = (0, child_process_1.spawn)(pm, args, {
218
304
  cwd: projectDir,
219
305
  stdio: 'inherit',
220
306
  shell: true,
221
307
  });
222
- npmInstall.on('close', (code) => {
308
+ child.on('close', (code) => {
223
309
  if (code !== 0) {
224
- reject(new Error(`npm install next@${version} exited with code ${code}`));
310
+ reject(new Error(`${pm} add next@${version} exited with code ${code}`));
225
311
  }
226
312
  else {
227
313
  console.log(`\n✅ Next.js ${version} installed\n`);
228
314
  resolve();
229
315
  }
230
316
  });
231
- npmInstall.on('error', (error) => {
317
+ child.on('error', (error) => {
232
318
  reject(error);
233
319
  });
234
320
  });
235
321
  }
236
- async function installDependencies(projectDir) {
322
+ async function installDependencies(projectDir, pm = 'pnpm') {
237
323
  return new Promise((resolve, reject) => {
238
324
  console.log('\n📦 Installing dependencies...\n');
239
- const npmInstall = (0, child_process_1.spawn)('npm', ['install'], {
325
+ const child = (0, child_process_1.spawn)(pm, ['install'], {
240
326
  cwd: projectDir,
241
327
  stdio: 'inherit',
242
328
  shell: true,
243
329
  });
244
- npmInstall.on('close', (code) => {
330
+ child.on('close', (code) => {
245
331
  if (code !== 0) {
246
- reject(new Error(`npm install exited with code ${code}`));
332
+ reject(new Error(`${pm} install exited with code ${code}`));
247
333
  }
248
334
  else {
249
335
  console.log('\n✅ Dependencies installed\n');
250
336
  resolve();
251
337
  }
252
338
  });
253
- npmInstall.on('error', (error) => {
339
+ child.on('error', (error) => {
254
340
  reject(error);
255
341
  });
256
342
  });
257
343
  }
258
- async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig) {
344
+ function injectSwallowKitNextConfig(nextConfigContent, projectName) {
345
+ return nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
346
+ }
347
+ function buildCSharpFunctionsProgramSource() {
348
+ return `using Microsoft.Azure.Functions.Worker;
349
+ using Microsoft.Extensions.DependencyInjection;
350
+ using Microsoft.Extensions.Hosting;
351
+ using Microsoft.Azure.Functions.Worker.ApplicationInsights;
352
+
353
+ var host = new HostBuilder()
354
+ .ConfigureFunctionsWorkerDefaults()
355
+ .ConfigureServices(services =>
356
+ {
357
+ services.AddApplicationInsightsTelemetryWorkerService();
358
+ services.ConfigureFunctionsApplicationInsights();
359
+ })
360
+ .Build();
361
+
362
+ host.Run();
363
+ `;
364
+ }
365
+ function buildCSharpFunctionsProjectSource() {
366
+ return `<Project Sdk="Microsoft.NET.Sdk">
367
+ <PropertyGroup>
368
+ <TargetFramework>net10.0</TargetFramework>
369
+ <AzureFunctionsVersion>v4</AzureFunctionsVersion>
370
+ <OutputType>Exe</OutputType>
371
+ <ImplicitUsings>enable</ImplicitUsings>
372
+ <Nullable>enable</Nullable>
373
+ </PropertyGroup>
374
+ <ItemGroup>
375
+ <Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />
376
+ <EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
377
+ <None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
378
+ </ItemGroup>
379
+ <ItemGroup>
380
+ <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
381
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
382
+ <PackageReference Include="Azure.Identity" Version="1.17.0" />
383
+ <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
384
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
385
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
386
+ <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
387
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
388
+ </ItemGroup>
389
+ </Project>
390
+ `;
391
+ }
392
+ function buildSwallowKitConfigSource(backendLanguage) {
393
+ return `module.exports = {
394
+ backend: {
395
+ language: '${backendLanguage}',
396
+ },
397
+ functions: {
398
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
399
+ },
400
+ deployment: {
401
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
402
+ swaName: process.env.AZURE_SWA_NAME || '',
403
+ },
404
+ }
405
+ `;
406
+ }
407
+ function buildGeneratedProjectDependencies(projectName) {
408
+ return {
409
+ '@azure/cosmos': '^4.0.0',
410
+ 'applicationinsights': '^3.3.0',
411
+ [`@${projectName}/shared`]: '*',
412
+ };
413
+ }
414
+ function getSwallowKitPackageMetadata() {
415
+ const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
416
+ try {
417
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
418
+ return {
419
+ name: packageJson.name || "swallowkit",
420
+ version: packageJson.version || "latest",
421
+ };
422
+ }
423
+ catch {
424
+ // Fall back to the published package name when package metadata is unavailable.
425
+ }
426
+ return {
427
+ name: "swallowkit",
428
+ version: "latest",
429
+ };
430
+ }
431
+ function buildGeneratedProjectDevDependencies() {
432
+ const { name, version } = getSwallowKitPackageMetadata();
433
+ return {
434
+ [name]: version,
435
+ };
436
+ }
437
+ function buildSwallowKitMcpProjectConfigSource() {
438
+ const { name } = getSwallowKitPackageMetadata();
439
+ return JSON.stringify({
440
+ mcpServers: {
441
+ swallowkit: {
442
+ command: "node",
443
+ args: [`./node_modules/${name}/dist/mcp/index.js`],
444
+ cwd: ".",
445
+ },
446
+ },
447
+ }, null, 2);
448
+ }
449
+ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
259
450
  console.log('📦 Adding SwallowKit files...\n');
260
451
  const projectName = options.name;
261
- // 1. Update package.json to add swallowkit and @azure/cosmos dependencies
452
+ // 1. Update package.json to add runtime dependencies for generated projects
262
453
  const packageJsonPath = path.join(projectDir, 'package.json');
263
454
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
264
- // Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
265
455
  // zod is in the shared workspace package, not here
266
456
  packageJson.dependencies = {
267
457
  ...packageJson.dependencies,
268
- 'swallowkit': 'latest',
269
- '@azure/cosmos': '^4.0.0',
270
- 'applicationinsights': '^3.3.0',
271
- [`@${projectName}/shared`]: '*',
458
+ ...buildGeneratedProjectDependencies(projectName),
459
+ };
460
+ packageJson.devDependencies = {
461
+ ...packageJson.devDependencies,
462
+ ...buildGeneratedProjectDevDependencies(),
272
463
  };
273
464
  packageJson.scripts = {
274
465
  ...packageJson.scripts,
275
- 'build': 'npm run build -w shared && next build --webpack && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/',
466
+ 'build': (0, package_manager_1.getBuildScript)(pm),
276
467
  'start': 'next start',
277
- 'functions:start': 'cd functions && npm start',
468
+ 'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
278
469
  };
470
+ if (pm === 'pnpm') {
471
+ packageJson.packageManager = 'pnpm@latest';
472
+ }
279
473
  packageJson.engines = {
280
474
  node: '20.x',
281
475
  };
282
- // npm workspaces: shared package for Zod models, functions for Azure Functions
283
- packageJson.workspaces = ['shared', 'functions'];
476
+ // Workspace configuration depends on package manager
477
+ const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
478
+ const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
479
+ if (wsConfig.type === 'file') {
480
+ // pnpm: workspaces are defined in pnpm-workspace.yaml
481
+ delete packageJson.workspaces;
482
+ }
483
+ else {
484
+ // npm: workspaces are defined in package.json
485
+ packageJson.workspaces = wsConfig.value;
486
+ }
284
487
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
488
+ // Create workspace config file if needed (pnpm-workspace.yaml)
489
+ if (wsConfig.type === 'file') {
490
+ fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
491
+ }
285
492
  // Don't install yet — wait until all workspace packages (shared, functions) are created
286
493
  // 2. Update next.config to add standalone output
287
494
  // Check for both .ts and .js variants
@@ -291,11 +498,9 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig)
291
498
  }
292
499
  if (fs.existsSync(nextConfigPath)) {
293
500
  let nextConfigContent = fs.readFileSync(nextConfigPath, 'utf-8');
294
- // Add output: 'standalone', experimental.turbopackUseSystemTlsCerts, and serverExternalPackages
501
+ // Add output, transpiled workspace package, and server externals for standalone deployment
295
502
  if (!nextConfigContent.includes("output:") && !nextConfigContent.includes('output =')) {
296
- // Handle TypeScript config format: const nextConfig: NextConfig = {
297
- // Handle JavaScript config format: const nextConfig = {
298
- nextConfigContent = nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n experimental: {\n turbopackUseSystemTlsCerts: true,\n },\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
503
+ nextConfigContent = injectSwallowKitNextConfig(nextConfigContent, projectName);
299
504
  fs.writeFileSync(nextConfigPath, nextConfigContent);
300
505
  }
301
506
  }
@@ -315,17 +520,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig)
315
520
  fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
316
521
  }
317
522
  // 3. Create SwallowKit config
318
- const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
319
- module.exports = {
320
- functions: {
321
- baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
322
- },
323
- deployment: {
324
- resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
325
- swaName: process.env.AZURE_SWA_NAME || '',
326
- },
327
- }
328
- `;
523
+ const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
329
524
  fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
330
525
  // 4. Create shared workspace package for Zod models (Single Source of Truth)
331
526
  await createSharedPackage(projectDir, projectName);
@@ -335,197 +530,197 @@ module.exports = {
335
530
  const apiLibDir = path.join(libDir, 'api');
336
531
  fs.mkdirSync(apiLibDir, { recursive: true });
337
532
  // Create backend utility for calling Azure Functions
338
- const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
339
- function getFunctionsBaseUrl(): string {
340
- return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
341
- }
342
-
343
- /**
344
- * Simple HTTP client for calling backend APIs
345
- * Use this to make requests to BFF API routes (which forward to Azure Functions)
346
- */
347
- async function request<T>(
348
- endpoint: string,
349
- method: 'GET' | 'POST' | 'PUT' | 'DELETE',
350
- body?: any,
351
- queryParams?: Record<string, string>
352
- ): Promise<T> {
353
- const functionsBaseUrl = getFunctionsBaseUrl();
354
- let url = \`\${functionsBaseUrl}\${endpoint}\`;
355
- if (queryParams) {
356
- const params = new URLSearchParams(queryParams);
357
- url += \`?\${params.toString()}\`;
358
- }
359
-
360
- try {
361
- const response = await fetch(url, {
362
- method,
363
- headers: {
364
- 'Content-Type': 'application/json',
365
- },
366
- body: body ? JSON.stringify(body) : undefined,
367
- });
368
-
369
- if (!response.ok) {
370
- const text = await response.text();
371
- let errorMessage = text || 'Failed to call backend function';
372
- try {
373
- const error = JSON.parse(text);
374
- errorMessage = error.error || error.message || text;
375
- } catch {
376
- // If not JSON, use text as-is
377
- }
378
- throw new Error(errorMessage);
379
- }
380
-
381
- const contentType = response.headers.get('content-type');
382
- if (!contentType?.includes('application/json')) {
383
- const text = await response.text();
384
- return text as T;
385
- }
386
-
387
- return await response.json();
388
- } catch (error) {
389
- console.error('Error calling backend:', error);
390
- throw error;
391
- }
392
- }
393
-
394
- /**
395
- * Generic API client for making HTTP requests
396
- * Simply calls endpoints - no DB dependencies, no schema validation
397
- * Validation happens on the backend (BFF/Functions)
398
- *
399
- * @example
400
- * // Call custom endpoint
401
- * await api.get('/api/greet?name=World')
402
- *
403
- * // Call scaffolded CRUD endpoints
404
- * await api.get('/api/todos')
405
- * await api.post('/api/todos', { title: 'New task' })
406
- * await api.put('/api/todos/123', { title: 'Updated' })
407
- * await api.delete('/api/todos/123')
408
- */
409
- export const api = {
410
- /**
411
- * Make a GET request
412
- */
413
- get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
414
- return request<T>(endpoint, 'GET', undefined, params);
415
- },
416
-
417
- /**
418
- * Make a POST request
419
- */
420
- post: <T>(endpoint: string, body?: any): Promise<T> => {
421
- return request<T>(endpoint, 'POST', body);
422
- },
423
-
424
- /**
425
- * Make a PUT request
426
- */
427
- put: <T>(endpoint: string, body?: any): Promise<T> => {
428
- return request<T>(endpoint, 'PUT', body);
429
- },
430
-
431
- /**
432
- * Make a DELETE request
433
- */
434
- delete: <T>(endpoint: string): Promise<T> => {
435
- return request<T>(endpoint, 'DELETE');
436
- },
437
- };
533
+ const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
534
+ function getFunctionsBaseUrl(): string {
535
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
536
+ }
537
+
538
+ /**
539
+ * Simple HTTP client for calling backend APIs
540
+ * Use this to make requests to BFF API routes (which forward to Azure Functions)
541
+ */
542
+ async function request<T>(
543
+ endpoint: string,
544
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
545
+ body?: any,
546
+ queryParams?: Record<string, string>
547
+ ): Promise<T> {
548
+ const functionsBaseUrl = getFunctionsBaseUrl();
549
+ let url = \`\${functionsBaseUrl}\${endpoint}\`;
550
+ if (queryParams) {
551
+ const params = new URLSearchParams(queryParams);
552
+ url += \`?\${params.toString()}\`;
553
+ }
554
+
555
+ try {
556
+ const response = await fetch(url, {
557
+ method,
558
+ headers: {
559
+ 'Content-Type': 'application/json',
560
+ },
561
+ body: body ? JSON.stringify(body) : undefined,
562
+ });
563
+
564
+ if (!response.ok) {
565
+ const text = await response.text();
566
+ let errorMessage = text || 'Failed to call backend function';
567
+ try {
568
+ const error = JSON.parse(text);
569
+ errorMessage = error.error || error.message || text;
570
+ } catch {
571
+ // If not JSON, use text as-is
572
+ }
573
+ throw new Error(errorMessage);
574
+ }
575
+
576
+ const contentType = response.headers.get('content-type');
577
+ if (!contentType?.includes('application/json')) {
578
+ const text = await response.text();
579
+ return text as T;
580
+ }
581
+
582
+ return await response.json();
583
+ } catch (error) {
584
+ console.error('Error calling backend:', error);
585
+ throw error;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Generic API client for making HTTP requests
591
+ * Simply calls endpoints - no DB dependencies, no schema validation
592
+ * Validation happens on the backend (BFF/Functions)
593
+ *
594
+ * @example
595
+ * // Call custom endpoint
596
+ * await api.get('/api/greet?name=World')
597
+ *
598
+ * // Call scaffolded CRUD endpoints
599
+ * await api.get('/api/todos')
600
+ * await api.post('/api/todos', { title: 'New task' })
601
+ * await api.put('/api/todos/123', { title: 'Updated' })
602
+ * await api.delete('/api/todos/123')
603
+ */
604
+ export const api = {
605
+ /**
606
+ * Make a GET request
607
+ */
608
+ get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
609
+ return request<T>(endpoint, 'GET', undefined, params);
610
+ },
611
+
612
+ /**
613
+ * Make a POST request
614
+ */
615
+ post: <T>(endpoint: string, body?: any): Promise<T> => {
616
+ return request<T>(endpoint, 'POST', body);
617
+ },
618
+
619
+ /**
620
+ * Make a PUT request
621
+ */
622
+ put: <T>(endpoint: string, body?: any): Promise<T> => {
623
+ return request<T>(endpoint, 'PUT', body);
624
+ },
625
+
626
+ /**
627
+ * Make a DELETE request
628
+ */
629
+ delete: <T>(endpoint: string): Promise<T> => {
630
+ return request<T>(endpoint, 'DELETE');
631
+ },
632
+ };
438
633
  `;
439
634
  fs.writeFileSync(path.join(apiLibDir, 'backend.ts'), backendUtilContent);
440
635
  // 5. Create components directory
441
636
  const componentsDir = path.join(projectDir, 'components');
442
637
  fs.mkdirSync(componentsDir, { recursive: true });
443
638
  // 6. Create .env.example
444
- const envExample = `# Azure Functions Backend URL
445
- FUNCTIONS_BASE_URL=http://localhost:7071
446
-
447
- # Azure Configuration
448
- AZURE_RESOURCE_GROUP=your-resource-group
449
- AZURE_SWA_NAME=your-static-web-app-name
639
+ const envExample = `# Azure Functions Backend URL
640
+ BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
641
+
642
+ # Azure Configuration
643
+ AZURE_RESOURCE_GROUP=your-resource-group
644
+ AZURE_SWA_NAME=your-static-web-app-name
450
645
  `;
451
646
  fs.writeFileSync(path.join(projectDir, '.env.example'), envExample);
452
647
  // 7. Create instrumentation.ts for Application Insights (Next.js official way)
453
- const instrumentationContent = `// Application Insights instrumentation for Next.js
454
- // This file is automatically loaded by Next.js when instrumentationHook is enabled
455
- export async function register() {
456
- if (process.env.NEXT_RUNTIME === 'nodejs') {
457
- // Only run on server-side
458
- const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
459
-
460
- if (connectionString) {
461
- const appInsights = await import('applicationinsights');
462
-
463
- appInsights
464
- .setup(connectionString)
465
- .setAutoCollectConsole(true)
466
- .setAutoCollectDependencies(true)
467
- .setAutoCollectExceptions(true)
468
- .setAutoCollectHeartbeat(true)
469
- .setAutoCollectPerformance(true, true)
470
- .setAutoCollectRequests(true)
471
- .setAutoDependencyCorrelation(true)
472
- .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
473
- .setSendLiveMetrics(true)
474
- .setUseDiskRetryCaching(true);
475
-
476
- appInsights.defaultClient.setAutoPopulateAzureProperties();
477
- appInsights.start();
478
-
479
- // Override console methods to send to Application Insights
480
- const originalConsoleLog = console.log;
481
- const originalConsoleError = console.error;
482
- const originalConsoleWarn = console.warn;
483
-
484
- console.log = function(...args: any[]) {
485
- originalConsoleLog.apply(console, args);
486
- const message = args.map(arg =>
487
- typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
488
- ).join(' ');
489
- appInsights.defaultClient.trackTrace({
490
- message: message,
491
- severity: '1'
492
- });
493
- };
494
-
495
- console.error = function(...args: any[]) {
496
- originalConsoleError.apply(console, args);
497
- const message = args.map(arg =>
498
- typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
499
- ).join(' ');
500
- appInsights.defaultClient.trackTrace({
501
- message: message,
502
- severity: '3'
503
- });
504
- };
505
-
506
- console.warn = function(...args: any[]) {
507
- originalConsoleWarn.apply(console, args);
508
- const message = args.map(arg =>
509
- typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
510
- ).join(' ');
511
- appInsights.defaultClient.trackTrace({
512
- message: message,
513
- severity: '2'
514
- });
515
- };
516
-
517
- console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
518
- } else {
519
- console.log('[App Insights] Not configured (skipped in development mode)');
520
- }
521
- }
522
- }
648
+ const instrumentationContent = `// Application Insights instrumentation for Next.js
649
+ // This file is automatically loaded by Next.js when instrumentationHook is enabled
650
+ export async function register() {
651
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
652
+ // Only run on server-side
653
+ const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
654
+
655
+ if (connectionString) {
656
+ const appInsights = await import('applicationinsights');
657
+
658
+ appInsights
659
+ .setup(connectionString)
660
+ .setAutoCollectConsole(true)
661
+ .setAutoCollectDependencies(true)
662
+ .setAutoCollectExceptions(true)
663
+ .setAutoCollectHeartbeat(true)
664
+ .setAutoCollectPerformance(true, true)
665
+ .setAutoCollectRequests(true)
666
+ .setAutoDependencyCorrelation(true)
667
+ .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
668
+ .setSendLiveMetrics(true)
669
+ .setUseDiskRetryCaching(true);
670
+
671
+ appInsights.defaultClient.setAutoPopulateAzureProperties();
672
+ appInsights.start();
673
+
674
+ // Override console methods to send to Application Insights
675
+ const originalConsoleLog = console.log;
676
+ const originalConsoleError = console.error;
677
+ const originalConsoleWarn = console.warn;
678
+
679
+ console.log = function(...args: any[]) {
680
+ originalConsoleLog.apply(console, args);
681
+ const message = args.map(arg =>
682
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
683
+ ).join(' ');
684
+ appInsights.defaultClient.trackTrace({
685
+ message: message,
686
+ severity: '1'
687
+ });
688
+ };
689
+
690
+ console.error = function(...args: any[]) {
691
+ originalConsoleError.apply(console, args);
692
+ const message = args.map(arg =>
693
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
694
+ ).join(' ');
695
+ appInsights.defaultClient.trackTrace({
696
+ message: message,
697
+ severity: '3'
698
+ });
699
+ };
700
+
701
+ console.warn = function(...args: any[]) {
702
+ originalConsoleWarn.apply(console, args);
703
+ const message = args.map(arg =>
704
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
705
+ ).join(' ');
706
+ appInsights.defaultClient.trackTrace({
707
+ message: message,
708
+ severity: '2'
709
+ });
710
+ };
711
+
712
+ console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
713
+ } else {
714
+ console.log('[App Insights] Not configured (skipped in development mode)');
715
+ }
716
+ }
717
+ }
523
718
  `;
524
719
  fs.writeFileSync(path.join(projectDir, 'instrumentation.ts'), instrumentationContent);
525
720
  // 8. Create .env.local for local development
526
721
  const envLocalContent = [
527
722
  '# Azure Functions Backend URL (Local)',
528
- 'FUNCTIONS_BASE_URL=http://localhost:7071',
723
+ 'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
529
724
  ''
530
725
  ].join('\n');
531
726
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -551,17 +746,19 @@ export async function register() {
551
746
  };
552
747
  fs.writeFileSync(path.join(projectDir, 'staticwebapp.config.json'), JSON.stringify(swaConfig, null, 2));
553
748
  // 14. Create Azure Functions project
554
- await createAzureFunctionsProject(projectDir);
749
+ await createAzureFunctionsProject(projectDir, pm, backendLanguage);
555
750
  // 15. Create BFF API route to call Azure Functions
556
751
  await createBffApiRoute(projectDir);
557
752
  // 16. Create home page
558
- await createHomePage(projectDir);
753
+ await createHomePage(projectDir, pm);
559
754
  // 17. Install all workspace dependencies (root + shared + functions)
560
755
  console.log('📦 Installing workspace dependencies...\n');
561
- await installDependencies(projectDir);
756
+ await installDependencies(projectDir, pm);
562
757
  console.log('✅ Project structure created\n');
563
758
  // 18. Create README.md
564
- createReadme(projectDir, projectName, cicdChoice, azureConfig);
759
+ createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
760
+ // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
761
+ createAiAgentFiles(projectDir, projectName, backendLanguage);
565
762
  }
566
763
  async function createSharedPackage(projectDir, projectName) {
567
764
  console.log('📦 Creating shared workspace package for Zod models...\n');
@@ -614,11 +811,94 @@ async function createSharedPackage(projectDir, projectName) {
614
811
  fs.writeFileSync(path.join(sharedDir, '.gitignore'), `node_modules\ndist\n`);
615
812
  console.log('✅ Shared package created\n');
616
813
  }
617
- async function createAzureFunctionsProject(projectDir) {
618
- console.log('📦 Creating Azure Functions project...\n');
814
+ async function createAzureFunctionsProject(projectDir, pm = 'pnpm', backendLanguage = 'typescript') {
815
+ console.log(`📦 Creating Azure Functions project (${getBackendLanguageLabel(backendLanguage)})...\n`);
619
816
  const functionsDir = path.join(projectDir, 'functions');
620
817
  fs.mkdirSync(functionsDir, { recursive: true });
621
- // Create functions package.json
818
+ const projectName = path.basename(projectDir);
819
+ const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
820
+ createFunctionsHostFiles(functionsDir, databaseName, backendLanguage);
821
+ if (backendLanguage === 'typescript') {
822
+ createTypeScriptFunctionsProject(projectDir, functionsDir, pm);
823
+ }
824
+ else if (backendLanguage === 'csharp') {
825
+ createCSharpFunctionsProject(projectDir, functionsDir);
826
+ }
827
+ else {
828
+ createPythonFunctionsProject(projectDir, functionsDir);
829
+ }
830
+ console.log('✅ Azure Functions project created\n');
831
+ }
832
+ function createFunctionsHostFiles(functionsDir, databaseName, backendLanguage) {
833
+ const hostJson = {
834
+ version: '2.0',
835
+ logging: {
836
+ applicationInsights: {
837
+ samplingSettings: {
838
+ isEnabled: true,
839
+ maxTelemetryItemsPerSecond: 20,
840
+ },
841
+ },
842
+ },
843
+ extensionBundle: {
844
+ id: 'Microsoft.Azure.Functions.ExtensionBundle',
845
+ version: '[4.0.0, 4.10.0)',
846
+ },
847
+ };
848
+ fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
849
+ const localSettings = {
850
+ IsEncrypted: false,
851
+ Values: {
852
+ AzureWebJobsStorage: '',
853
+ FUNCTIONS_WORKER_RUNTIME: getFunctionsWorkerRuntime(backendLanguage),
854
+ AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
855
+ CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
856
+ COSMOS_DB_DATABASE_NAME: databaseName,
857
+ NODE_TLS_REJECT_UNAUTHORIZED: '0',
858
+ },
859
+ };
860
+ fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
861
+ const gitignoreLines = [
862
+ 'local.settings.json',
863
+ '*.log',
864
+ '.vscode',
865
+ '.DS_Store',
866
+ ];
867
+ if (backendLanguage === 'typescript') {
868
+ gitignoreLines.unshift('node_modules', 'dist');
869
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `node_modules
870
+ .git
871
+ .vscode
872
+ local.settings.json
873
+ test
874
+ tsconfig.json
875
+ *.ts
876
+ !dist/**/*.js
877
+ `);
878
+ }
879
+ else if (backendLanguage === 'python') {
880
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
881
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
882
+ .codegen-venv
883
+ __pycache__
884
+ .pytest_cache
885
+ .mypy_cache
886
+ .ruff_cache
887
+ local.settings.json
888
+ tests
889
+ `);
890
+ }
891
+ else {
892
+ gitignoreLines.unshift('bin', 'obj');
893
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `bin
894
+ obj
895
+ local.settings.json
896
+ tests
897
+ `);
898
+ }
899
+ fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
900
+ }
901
+ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
622
902
  const functionsPackageJson = {
623
903
  name: 'functions',
624
904
  version: '1.0.0',
@@ -627,21 +907,21 @@ async function createAzureFunctionsProject(projectDir) {
627
907
  scripts: {
628
908
  start: 'func start',
629
909
  build: 'tsc',
630
- prestart: 'npm run build'
910
+ prestart: (0, package_manager_1.getFunctionsPrestart)(pm),
631
911
  },
632
912
  dependencies: {
633
913
  '@azure/functions': '~4.5.0',
634
914
  '@azure/cosmos': '^4.0.0',
635
- 'zod': '>=3.25.0',
915
+ '@azure/identity': '^4.0.0',
916
+ zod: '>=3.25.0',
636
917
  [`@${path.basename(projectDir)}/shared`]: '*',
637
918
  },
638
919
  devDependencies: {
639
920
  '@types/node': '^20.0.0',
640
- 'typescript': '^5.0.0'
641
- }
921
+ typescript: '^5.0.0',
922
+ },
642
923
  };
643
924
  fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
644
- // Create functions tsconfig.json
645
925
  const sharedPkgName = `@${path.basename(projectDir)}/shared`;
646
926
  const functionsTsConfig = {
647
927
  compilerOptions: {
@@ -661,294 +941,333 @@ async function createAzureFunctionsProject(projectDir) {
661
941
  },
662
942
  },
663
943
  include: ['src/**/*'],
664
- exclude: ['node_modules', 'dist']
944
+ exclude: ['node_modules', 'dist'],
665
945
  };
666
946
  fs.writeFileSync(path.join(functionsDir, 'tsconfig.json'), JSON.stringify(functionsTsConfig, null, 2));
667
- // Create host.json
668
- const hostJson = {
669
- version: '2.0',
670
- logging: {
671
- applicationInsights: {
672
- samplingSettings: {
673
- isEnabled: true,
674
- maxTelemetryItemsPerSecond: 20
675
- }
676
- }
677
- },
678
- extensionBundle: {
679
- id: 'Microsoft.Azure.Functions.ExtensionBundle',
680
- version: '[4.0.0, 4.10.0)'
947
+ const srcDir = path.join(functionsDir, 'src');
948
+ fs.mkdirSync(srcDir, { recursive: true });
949
+ fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
950
+ import { z } from 'zod/v4';
951
+
952
+ const greetRequestSchema = z.object({
953
+ name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
954
+ });
955
+
956
+ export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
957
+ context.log('HTTP trigger function processed a request.');
958
+
959
+ try {
960
+ const name = request.query.get('name') || (await request.text());
961
+ const result = greetRequestSchema.safeParse({ name });
962
+
963
+ if (!result.success) {
964
+ return {
965
+ status: 400,
966
+ jsonBody: {
967
+ error: result.error.issues[0].message
681
968
  }
969
+ };
970
+ }
971
+
972
+ const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
973
+
974
+ return {
975
+ status: 200,
976
+ jsonBody: {
977
+ message: greeting,
978
+ timestamp: new Date().toISOString()
979
+ }
682
980
  };
683
- fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
684
- // Create .funcignore
685
- const funcignore = `node_modules
686
- .git
687
- .vscode
688
- local.settings.json
689
- test
690
- tsconfig.json
691
- *.ts
692
- !dist/**/*.js
693
- `;
694
- fs.writeFileSync(path.join(functionsDir, '.funcignore'), funcignore);
695
- // Create .gitignore for functions directory
696
- const functionsGitignore = `node_modules
697
- dist
698
- local.settings.json
699
- *.log
700
- .vscode
701
- .DS_Store
702
- `;
703
- fs.writeFileSync(path.join(functionsDir, '.gitignore'), functionsGitignore);
704
- // Create local.settings.json
705
- const projectName = path.basename(projectDir);
706
- const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
707
- const localSettings = {
708
- IsEncrypted: false,
709
- Values: {
710
- AzureWebJobsStorage: '',
711
- FUNCTIONS_WORKER_RUNTIME: 'node',
712
- AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
713
- CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
714
- COSMOS_DB_DATABASE_NAME: databaseName,
715
- NODE_TLS_REJECT_UNAUTHORIZED: '0'
716
- }
981
+ } catch (error) {
982
+ context.error('Error processing request:', error);
983
+ return {
984
+ status: 500,
985
+ jsonBody: {
986
+ error: 'Internal server error'
987
+ }
717
988
  };
718
- fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
719
- // Create src directory
720
- const srcDir = path.join(functionsDir, 'src');
721
- fs.mkdirSync(srcDir, { recursive: true });
722
- // Create greet function directly in src
723
- const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
724
- import { z } from 'zod/v4';
725
-
726
- // Zod schema for request validation
727
- const greetRequestSchema = z.object({
728
- name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
729
- });
730
-
731
- export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
732
- context.log('HTTP trigger function processed a request.');
733
-
734
- try {
735
- // Get name from query or body
736
- const name = request.query.get('name') || (await request.text());
737
-
738
- // Validate with Zod
739
- const result = greetRequestSchema.safeParse({ name });
740
-
741
- if (!result.success) {
742
- return {
743
- status: 400,
744
- jsonBody: {
745
- error: result.error.issues[0].message
746
- }
747
- };
748
- }
749
-
750
- const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
751
-
752
- return {
753
- status: 200,
754
- jsonBody: {
755
- message: greeting,
756
- timestamp: new Date().toISOString()
757
- }
758
- };
759
- } catch (error) {
760
- context.error('Error processing request:', error);
761
- return {
762
- status: 500,
763
- jsonBody: {
764
- error: 'Internal server error'
765
- }
766
- };
767
- }
768
- }
769
-
770
- app.http('greet', {
771
- methods: ['GET', 'POST'],
772
- authLevel: 'anonymous',
773
- handler: greet
774
- });
775
- `;
776
- fs.writeFileSync(path.join(srcDir, 'greet.ts'), greetFunction);
777
- // Dependencies are installed via workspace root 'npm install'
778
- console.log('✅ Azure Functions project created\n');
989
+ }
990
+ }
991
+
992
+ app.http('greet', {
993
+ methods: ['GET', 'POST'],
994
+ authLevel: 'anonymous',
995
+ handler: greet
996
+ });
997
+ `);
998
+ }
999
+ function createCSharpFunctionsProject(projectDir, functionsDir) {
1000
+ const projectBaseName = path.basename(projectDir);
1001
+ const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
1002
+ const csprojName = `${projectPascal}.Functions.csproj`;
1003
+ fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
1004
+ fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
1005
+ fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
1006
+ fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
1007
+ const crudDir = path.join(functionsDir, 'Crud');
1008
+ fs.mkdirSync(crudDir, { recursive: true });
1009
+ fs.writeFileSync(path.join(crudDir, 'GreetFunction.cs'), `using System.Net;
1010
+ using Microsoft.Azure.Functions.Worker;
1011
+ using Microsoft.Azure.Functions.Worker.Http;
1012
+
1013
+ namespace SwallowKit.Functions;
1014
+
1015
+ public sealed class GreetFunction
1016
+ {
1017
+ [Function("greet")]
1018
+ public async Task<HttpResponseData> Run(
1019
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
1020
+ {
1021
+ var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1022
+ var name = "SwallowKit";
1023
+ foreach (var segment in query)
1024
+ {
1025
+ var parts = segment.Split('=', 2);
1026
+ if (parts.Length == 2 && parts[0] == "name")
1027
+ {
1028
+ name = Uri.UnescapeDataString(parts[1]);
1029
+ break;
1030
+ }
1031
+ }
1032
+ var response = request.CreateResponse(HttpStatusCode.OK);
1033
+ await response.WriteAsJsonAsync(new
1034
+ {
1035
+ message = $"Hello, {name}! This message is from Azure Functions.",
1036
+ timestamp = DateTimeOffset.UtcNow.ToString("O"),
1037
+ });
1038
+ return response;
1039
+ }
1040
+ }
1041
+ `);
1042
+ }
1043
+ function createPythonFunctionsProject(projectDir, functionsDir) {
1044
+ fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1045
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1046
+ fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1047
+ azure-cosmos>=4.9.0
1048
+ azure-identity>=1.19.0
1049
+ `);
1050
+ fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
1051
+ const blueprintsDir = path.join(functionsDir, 'blueprints');
1052
+ fs.mkdirSync(blueprintsDir, { recursive: true });
1053
+ fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
1054
+ fs.writeFileSync(path.join(blueprintsDir, 'greet.py'), `import json
1055
+ from datetime import datetime, timezone
1056
+
1057
+ import azure.functions as func
1058
+
1059
+ bp = func.Blueprint()
1060
+
1061
+
1062
+ @bp.route(route="greet", methods=["GET", "POST"])
1063
+ def greet(req: func.HttpRequest) -> func.HttpResponse:
1064
+ name = req.params.get("name") or "SwallowKit"
1065
+ payload = {
1066
+ "message": f"Hello, {name}! This message is from Azure Functions.",
1067
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1068
+ }
1069
+ return func.HttpResponse(
1070
+ body=json.dumps(payload, ensure_ascii=False),
1071
+ status_code=200,
1072
+ mimetype="application/json",
1073
+ )
1074
+ `);
1075
+ fs.writeFileSync(path.join(functionsDir, 'function_app.py'), `import azure.functions as func
1076
+
1077
+ from blueprints.greet import bp as greet_bp
1078
+
1079
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
1080
+
1081
+ app.register_blueprint(greet_bp)
1082
+ # SwallowKit scaffold registrations
1083
+ `);
1084
+ }
1085
+ function ensureProjectGitignoreEntry(projectDir, entry) {
1086
+ const gitignorePath = path.join(projectDir, '.gitignore');
1087
+ if (!fs.existsSync(gitignorePath)) {
1088
+ return;
1089
+ }
1090
+ const current = fs.readFileSync(gitignorePath, 'utf8');
1091
+ const lines = current.split(/\r?\n/);
1092
+ if (lines.includes(entry)) {
1093
+ return;
1094
+ }
1095
+ const normalized = current.endsWith('\n') ? current : `${current}\n`;
1096
+ fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
779
1097
  }
780
1098
  async function createBffApiRoute(projectDir) {
781
1099
  console.log('📦 Creating BFF API route...\n');
782
1100
  const apiDir = path.join(projectDir, 'app', 'api', 'greet');
783
1101
  fs.mkdirSync(apiDir, { recursive: true });
784
1102
  // Create API route that calls Azure Functions using shared utility
785
- const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
786
- import { api } from '@/lib/api/backend';
787
-
788
- interface GreetResponse {
789
- message: string;
790
- timestamp: string;
791
- }
792
-
793
- export async function GET(request: NextRequest) {
794
- try {
795
- const { searchParams } = new URL(request.url);
796
- const name = searchParams.get('name') || 'World';
797
-
798
- const data = await api.get<GreetResponse>('/api/greet', { name });
799
-
800
- return NextResponse.json(data);
801
- } catch (error) {
802
- console.error('Error calling Azure Functions:', error);
803
- const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
804
- return NextResponse.json(
805
- { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
806
- { status: 500 }
807
- );
808
- }
809
- }
810
-
811
- export async function POST(request: NextRequest) {
812
- try {
813
- const body = await request.json();
814
-
815
- const data = await api.post<GreetResponse>('/api/greet', body);
816
-
817
- return NextResponse.json(data);
818
- } catch (error) {
819
- console.error('Error calling Azure Functions:', error);
820
- const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
821
- return NextResponse.json(
822
- { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
823
- { status: 500 }
824
- );
825
- }
826
- }
1103
+ const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
1104
+ import { api } from '@/lib/api/backend';
1105
+
1106
+ interface GreetResponse {
1107
+ message: string;
1108
+ timestamp: string;
1109
+ }
1110
+
1111
+ export async function GET(request: NextRequest) {
1112
+ try {
1113
+ const { searchParams } = new URL(request.url);
1114
+ const name = searchParams.get('name') || 'World';
1115
+
1116
+ const data = await api.get<GreetResponse>('/api/greet', { name });
1117
+
1118
+ return NextResponse.json(data);
1119
+ } catch (error) {
1120
+ console.error('Error calling Azure Functions:', error);
1121
+ const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
1122
+ return NextResponse.json(
1123
+ { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
1124
+ { status: 500 }
1125
+ );
1126
+ }
1127
+ }
1128
+
1129
+ export async function POST(request: NextRequest) {
1130
+ try {
1131
+ const body = await request.json();
1132
+
1133
+ const data = await api.post<GreetResponse>('/api/greet', body);
1134
+
1135
+ return NextResponse.json(data);
1136
+ } catch (error) {
1137
+ console.error('Error calling Azure Functions:', error);
1138
+ const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
1139
+ return NextResponse.json(
1140
+ { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
1141
+ { status: 500 }
1142
+ );
1143
+ }
1144
+ }
827
1145
  `;
828
1146
  fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
829
- // Update .env.example to include FUNCTIONS_BASE_URL
1147
+ // Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
830
1148
  const envExamplePath = path.join(projectDir, '.env.example');
831
1149
  let envExample = fs.readFileSync(envExamplePath, 'utf-8');
832
- if (!envExample.includes('FUNCTIONS_BASE_URL')) {
1150
+ if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
833
1151
  envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
834
1152
  fs.writeFileSync(envExamplePath, envExample);
835
1153
  }
836
1154
  // Update .env.local
837
1155
  const envLocalPath = path.join(projectDir, '.env.local');
838
1156
  let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
839
- if (!envLocal.includes('FUNCTIONS_BASE_URL')) {
1157
+ if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
840
1158
  envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
841
1159
  fs.writeFileSync(envLocalPath, envLocal);
842
1160
  }
843
1161
  console.log('✅ BFF API route created\n');
844
1162
  }
845
- async function createHomePage(projectDir) {
1163
+ async function createHomePage(projectDir, pm = 'pnpm') {
846
1164
  console.log('📦 Creating home page...\n');
847
- const pageContent = `'use client'
848
-
849
- export const dynamic = 'force-dynamic';
850
-
851
- import { useState } from 'react';
852
- import { scaffoldConfig } from '@/lib/scaffold-config';
853
-
854
- export default function Home() {
855
- const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
856
- const [message, setMessage] = useState('');
857
-
858
- const testConnection = async () => {
859
- setGreetingStatus('loading');
860
- try {
861
- const response = await fetch('/api/greet?name=SwallowKit');
862
- const data = await response.json();
863
- if (!response.ok) {
864
- throw new Error(data.error || \`Server error: \${response.status}\`);
865
- }
866
- setMessage(data.message);
867
- setGreetingStatus('success');
868
- } catch (error) {
869
- setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
870
- setGreetingStatus('error');
871
- }
872
- };
873
-
874
- return (
875
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
876
- <div className="container mx-auto px-4 py-12">
877
- <header className="text-center mb-16">
878
- <h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
879
- Welcome to SwallowKit
880
- </h1>
881
- <p className="text-xl text-gray-600 dark:text-gray-400">
882
- Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
883
- </p>
884
- </header>
885
-
886
- {/* Connection Test */}
887
- <section className="max-w-2xl mx-auto mb-12">
888
- <div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
889
- <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
890
- Test BFF Functions Connection
891
- </h2>
892
- <button
893
- onClick={testConnection}
894
- disabled={greetingStatus === 'loading'}
895
- className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
896
- >
897
- {greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
898
- </button>
899
- {greetingStatus === 'success' && (
900
- <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
901
- <p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
902
- <p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
903
- </div>
904
- )}
905
- {greetingStatus === 'error' && (
906
- <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
907
- <p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
908
- <p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
909
- </div>
910
- )}
911
- </div>
912
- </section>
913
-
914
- {/* Scaffolded Models Menu */}
915
- {scaffoldConfig.models.length > 0 ? (
916
- <section className="max-w-6xl mx-auto">
917
- <h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
918
- <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
919
- {scaffoldConfig.models.map((model) => (
920
- <a
921
- key={model.name}
922
- href={model.path}
923
- className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
924
- >
925
- <h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
926
- <p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
927
- </a>
928
- ))}
929
- </div>
930
- </section>
931
- ) : (
932
- <section className="max-w-2xl mx-auto text-center">
933
- <div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
934
- <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
935
- <p className="text-gray-600 dark:text-gray-400 mb-6">
936
- Create your first model with Zod and generate CRUD operations automatically.
937
- </p>
938
- <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
939
- npx swallowkit scaffold lib/models/your-model.ts
940
- </code>
941
- </div>
942
- </section>
943
- )}
944
-
945
- <footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
946
- <p>Built with SwallowKit</p>
947
- </footer>
948
- </div>
949
- </div>
950
- );
951
- }
1165
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
1166
+ const pageContent = `'use client'
1167
+
1168
+ export const dynamic = 'force-dynamic';
1169
+
1170
+ import { useState } from 'react';
1171
+ import { scaffoldConfig } from '@/lib/scaffold-config';
1172
+
1173
+ export default function Home() {
1174
+ const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
1175
+ const [message, setMessage] = useState('');
1176
+
1177
+ const testConnection = async () => {
1178
+ setGreetingStatus('loading');
1179
+ try {
1180
+ const response = await fetch('/api/greet?name=SwallowKit');
1181
+ const data = await response.json();
1182
+ if (!response.ok) {
1183
+ throw new Error(data.error || \`Server error: \${response.status}\`);
1184
+ }
1185
+ setMessage(data.message);
1186
+ setGreetingStatus('success');
1187
+ } catch (error) {
1188
+ setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
1189
+ setGreetingStatus('error');
1190
+ }
1191
+ };
1192
+
1193
+ return (
1194
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
1195
+ <div className="container mx-auto px-4 py-12">
1196
+ <header className="text-center mb-16">
1197
+ <h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
1198
+ Welcome to SwallowKit
1199
+ </h1>
1200
+ <p className="text-xl text-gray-600 dark:text-gray-400">
1201
+ Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
1202
+ </p>
1203
+ </header>
1204
+
1205
+ {/* Connection Test */}
1206
+ <section className="max-w-2xl mx-auto mb-12">
1207
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
1208
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
1209
+ Test BFF → Functions Connection
1210
+ </h2>
1211
+ <button
1212
+ onClick={testConnection}
1213
+ disabled={greetingStatus === 'loading'}
1214
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
1215
+ >
1216
+ {greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
1217
+ </button>
1218
+ {greetingStatus === 'success' && (
1219
+ <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
1220
+ <p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
1221
+ <p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
1222
+ </div>
1223
+ )}
1224
+ {greetingStatus === 'error' && (
1225
+ <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
1226
+ <p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
1227
+ <p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
1228
+ </div>
1229
+ )}
1230
+ </div>
1231
+ </section>
1232
+
1233
+ {/* Scaffolded Models Menu */}
1234
+ {scaffoldConfig.models.length > 0 ? (
1235
+ <section className="max-w-6xl mx-auto">
1236
+ <h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
1237
+ <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
1238
+ {scaffoldConfig.models.map((model) => (
1239
+ <a
1240
+ key={model.name}
1241
+ href={model.path}
1242
+ className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
1243
+ >
1244
+ <h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
1245
+ <p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
1246
+ </a>
1247
+ ))}
1248
+ </div>
1249
+ </section>
1250
+ ) : (
1251
+ <section className="max-w-2xl mx-auto text-center">
1252
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
1253
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
1254
+ <p className="text-gray-600 dark:text-gray-400 mb-6">
1255
+ Create your first model with Zod and generate CRUD operations automatically.
1256
+ </p>
1257
+ <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1258
+ ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
1259
+ </code>
1260
+ </div>
1261
+ </section>
1262
+ )}
1263
+
1264
+ <footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
1265
+ <p>Built with SwallowKit</p>
1266
+ </footer>
1267
+ </div>
1268
+ </div>
1269
+ );
1270
+ }
952
1271
  `;
953
1272
  fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), pageContent);
954
1273
  console.log('✅ Home page created\n');
@@ -957,1277 +1276,1948 @@ export default function Home() {
957
1276
  if (!fs.existsSync(scaffoldConfigDir)) {
958
1277
  fs.mkdirSync(scaffoldConfigDir, { recursive: true });
959
1278
  }
960
- const scaffoldConfigContent = `export interface ScaffoldModel {
961
- name: string;
962
- path: string;
963
- label: string;
964
- }
965
-
966
- export const scaffoldConfig = {
967
- models: [
968
- // Scaffolded models will be added here by 'npx swallowkit scaffold' command
969
- ] as ScaffoldModel[]
970
- };
1279
+ const scaffoldConfigContent = `export interface ScaffoldModel {
1280
+ name: string;
1281
+ path: string;
1282
+ label: string;
1283
+ }
1284
+
1285
+ export const scaffoldConfig = {
1286
+ models: [
1287
+ // Scaffolded models will be added here by 'swallowkit scaffold' command
1288
+ ] as ScaffoldModel[]
1289
+ };
971
1290
  `;
972
1291
  fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
973
1292
  console.log('✅ Scaffold config created\n');
974
1293
  }
975
- function createReadme(projectDir, projectName, cicdChoice, azureConfig) {
1294
+ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage) {
976
1295
  console.log('📝 Creating README.md...\n');
1296
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
977
1297
  const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
978
1298
  const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
979
1299
  const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
980
1300
  'Outbound VNet (Cosmos DB Private Endpoint)';
981
- const readme = `# ${projectName}
982
-
983
- A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
984
-
985
- ## 🚀 Tech Stack
986
-
987
- - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
988
- - **BFF (Backend for Frontend)**: Next.js API Routes
989
- - **Backend**: Azure Functions (TypeScript)
990
- - **Database**: Azure Cosmos DB
991
- - **Schema Validation**: Zod (shared between frontend and backend)
992
- - **Infrastructure**: Bicep (Infrastructure as Code)
993
- - **CI/CD**: ${cicdLabel}
994
-
995
- ## 📋 Project Configuration
996
-
997
- This project was initialized with the following settings:
998
-
999
- - **Azure Functions Plan**: Flex Consumption
1000
- - **Cosmos DB Mode**: ${cosmosDbModeLabel}
1001
- - **Network Security**: ${vnetLabel}
1002
- - **CI/CD**: ${cicdLabel}
1003
-
1004
- ## Prerequisites
1005
-
1006
- Before you begin, ensure you have the following installed:
1007
-
1008
- 1. **Node.js 18+**: [Download](https://nodejs.org/)
1009
- 2. **Azure CLI**: Required for provisioning Azure resources
1010
- - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1011
- - Or: [Download](https://aka.ms/installazurecliwindows)
1012
- 3. **Azure Cosmos DB Emulator**: Required for local development
1013
- - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1014
- - Or: [Download](https://aka.ms/cosmosdb-emulator)
1015
- - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1016
- 4. **Azure Functions Core Tools**: Automatically installed with project dependencies
1017
-
1018
- ## 📁 Project Structure
1019
-
1020
- \`\`\`
1021
- ${projectName}/
1022
- ├── app/ # Next.js App Router (frontend)
1023
- │ ├── api/ # BFF API routes (proxy to Functions)
1024
- │ └── page.tsx # Home page
1025
- ├── functions/ # Azure Functions (backend)
1026
- └── src/
1027
- │ ├── models/ # Data models (copied from lib/models)
1028
- │ └── hello.ts # Sample function
1029
- ├── lib/
1030
- ├── models/ # Shared Zod schemas
1031
- └── api/ # API client utilities
1032
- ├── infra/ # Bicep infrastructure files
1033
- │ ├── main.bicep
1034
- │ └── modules/ # Bicep modules for each resource
1035
- └── .github/workflows/ # CI/CD workflows
1036
- \`\`\`
1037
-
1038
- ## 🏗️ Getting Started
1039
-
1040
- ### 1. Create Your First Model
1041
-
1042
- Define your data model with Zod schema:
1043
-
1044
- \`\`\`bash
1045
- npx swallowkit create-model <model-name>
1046
- \`\`\`
1047
-
1048
- This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
1049
-
1050
- ### 2. Generate CRUD Code
1051
-
1052
- Generate complete CRUD operations (Functions, API routes, UI):
1053
-
1054
- \`\`\`bash
1055
- npx swallowkit scaffold lib/models/<model-name>.ts
1056
- \`\`\`
1057
-
1058
- This generates:
1059
- - Azure Functions CRUD endpoints
1060
- - Next.js BFF API routes
1061
- - React UI components (list, detail, create, edit)
1062
- - Navigation menu integration
1063
-
1064
- ### 3. Start Development Servers
1065
-
1066
- \`\`\`bash
1067
- npx swallowkit dev
1068
- \`\`\`
1069
-
1070
- This starts:
1071
- - Next.js dev server (http://localhost:3000)
1072
- - Azure Functions (http://localhost:7071)
1073
- - Cosmos DB Emulator check (must be running separately)
1074
-
1075
- **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
1076
-
1077
- ## ☁️ Deploy to Azure
1078
-
1079
- ### Provision Azure Resources
1080
-
1081
- Create all required Azure resources using Bicep:
1082
-
1083
- \`\`\`bash
1084
- npx swallowkit provision --resource-group <rg-name>
1085
- \`\`\`
1086
-
1087
- This creates:
1088
- - Static Web App (\`swa-${projectName}\`)
1089
- - Azure Functions (\`func-${projectName}\`)
1090
- - Cosmos DB (\`cosmos-${projectName}\`)
1091
- - Storage Account
1092
-
1093
- You will be prompted to select Azure regions:
1094
- 1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
1095
- 2. **Static Web App location**: Limited availability (default: East Asia)
1096
-
1097
- ### CI/CD Setup
1098
-
1099
- ${cicdChoice === 'github' ? `#### GitHub Actions
1100
-
1101
- 1. Get Static Web App deployment token:
1102
- \`\`\`bash
1103
- az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
1104
- \`\`\`
1105
-
1106
- 2. Get Function App publish profile:
1107
- \`\`\`bash
1108
- az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
1109
- \`\`\`
1110
-
1111
- 3. Add secrets to GitHub repository:
1112
- - \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
1113
- - \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
1114
- - \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
1115
-
1116
- 4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
1117
-
1118
- 1. Set up service connection in Azure DevOps
1119
- 2. Update \`azure-pipelines.yml\` with your resource names
1120
- 3. Configure pipeline variables:
1121
- - \`azureSubscription\`: Service connection name
1122
- - \`resourceGroupName\`: Resource group name
1123
- 4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
1124
-
1125
- **Deploy Static Web App:**
1126
- \`\`\`bash
1127
- npm run build
1128
- az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
1129
- \`\`\`
1130
-
1131
- **Deploy Functions:**
1132
- \`\`\`bash
1133
- cd functions
1134
- npm run build
1135
- func azure functionapp publish func-${projectName}
1136
- \`\`\``}
1137
-
1138
- ## 🔧 Available Commands
1139
-
1140
- - \`npx swallowkit create-model <name>\` - Create a new data model
1141
- - \`npx swallowkit scaffold <model-file>\` - Generate CRUD code
1142
- - \`npx swallowkit dev\` - Start development servers
1143
- - \`npx swallowkit provision -g <rg-name>\` - Provision Azure resources
1144
- ${azureConfig.vnetOption !== 'none' ? `
1145
- ## 🔒 Network Security (VNet Configuration)
1146
-
1147
- This project is configured with **${vnetLabel}**.
1148
-
1149
- ### Architecture
1150
-
1151
- \`\`\`
1152
- Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
1153
-
1154
- VNet Integration
1155
- (outbound only)
1156
- \`\`\`
1157
-
1158
- - **Functions Cosmos DB**: Connected via Private Endpoint (private connection)
1159
- - **SWA Functions**: Connected via public endpoint (secured with CORS + IP restrictions)
1160
-
1161
- ### VNet Resources
1162
-
1163
- | Resource | Purpose |
1164
- |----------|---------|
1165
- | \`vnet-${projectName}\` | Virtual Network (10.0.0.0/16) |
1166
- | \`snet-functions\` | Functions subnet (10.0.1.0/24) |
1167
- | \`snet-private-endpoints\` | Private Endpoints subnet (10.0.2.0/24) |
1168
- | \`pe-cosmos-${projectName}\` | Cosmos DB Private Endpoint |
1169
-
1170
- ### Private DNS Zones
1171
-
1172
- - \`privatelink.documents.azure.com\` (Cosmos DB)
1173
- ` : ''}
1174
- ## 📚 Learn More
1175
-
1176
- - [SwallowKit Documentation](https://github.com/himanago/swallowkit)
1177
- - [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
1178
- - [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
1179
- - [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
1180
- - [Next.js](https://nextjs.org/)
1181
- - [Zod](https://zod.dev/)
1182
-
1183
- ## 💭 Feedback
1184
-
1185
- This project was generated by SwallowKit. If you encounter any issues or have suggestions for improvements, please open an issue on the [SwallowKit repository](https://github.com/himanago/swallowkit).
1301
+ const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1302
+ const schemaBridgeDescription = backendLanguage === 'typescript'
1303
+ ? 'Zod (shared between frontend and backend)'
1304
+ : `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
1305
+ const functionsTree = backendLanguage === 'typescript'
1306
+ ? `│ └── src/\n│ └── greet.ts # Sample function`
1307
+ : backendLanguage === 'csharp'
1308
+ ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
1309
+ : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
1310
+ const backendScaffoldNote = backendLanguage === 'typescript'
1311
+ ? '- Azure Functions CRUD endpoints'
1312
+ : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1313
+ const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
1314
+ const pythonLocalDevNote = backendLanguage === 'python'
1315
+ ? `\n**Python local dev note**: SwallowKit uses \`uv\` for Python backends and keeps the managed Python runtime under \`${path.relative(projectDir, pythonUvPaths.pythonInstallDir)}\`. Local Azure Functions runs from \`functions/.venv\`, schema generation uses \`functions/.codegen-venv\`, and \`swallowkit dev\` bootstraps a project-local \`uv\` binary automatically when needed. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
1316
+ : '';
1317
+ const readme = `# ${projectName}
1318
+
1319
+ A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
1320
+
1321
+ ## 🚀 Tech Stack
1322
+
1323
+ - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
1324
+ - **BFF (Backend for Frontend)**: Next.js API Routes
1325
+ - **Backend**: Azure Functions (${backendLanguageLabel})
1326
+ - **Database**: Azure Cosmos DB
1327
+ - **Schema Validation**: ${schemaBridgeDescription}
1328
+ - **Infrastructure**: Bicep (Infrastructure as Code)
1329
+ - **CI/CD**: ${cicdLabel}
1330
+
1331
+ ## 📋 Project Configuration
1332
+
1333
+ This project was initialized with the following settings:
1334
+
1335
+ - **Azure Functions Plan**: Flex Consumption
1336
+ - **Cosmos DB Mode**: ${cosmosDbModeLabel}
1337
+ - **Network Security**: ${vnetLabel}
1338
+ - **CI/CD**: ${cicdLabel}
1339
+
1340
+ ## ✅ Prerequisites
1341
+
1342
+ Before you begin, ensure you have the following installed:
1343
+
1344
+ 1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1345
+ ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1346
+ - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1347
+ - Or: [Download](https://aka.ms/installazurecliwindows)
1348
+ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local development
1349
+ - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1350
+ - Or: [Download](https://aka.ms/cosmosdb-emulator)
1351
+ - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1352
+ ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
1353
+
1354
+ ## 📁 Project Structure
1355
+
1356
+ \`\`\`
1357
+ ${projectName}/
1358
+ ├── app/ # Next.js App Router (frontend)
1359
+ │ ├── api/ # BFF API routes (proxy to Functions)
1360
+ │ └── page.tsx # Home page
1361
+ ├── functions/ # Azure Functions (backend)
1362
+ ${functionsTree}
1363
+ ├── lib/
1364
+ │ └── api/ # API client utilities
1365
+ ├── infra/ # Bicep infrastructure files
1366
+ │ ├── main.bicep
1367
+ │ └── modules/ # Bicep modules for each resource
1368
+ └── .github/workflows/ # CI/CD workflows
1369
+ \`\`\`
1370
+
1371
+ ## 🏗️ Getting Started
1372
+
1373
+ ### 1. Create Your First Model
1374
+
1375
+ Define your data model with Zod schema:
1376
+
1377
+ \`\`\`bash
1378
+ ${pmCmd.dlx} swallowkit create-model <model-name>
1379
+ \`\`\`
1380
+
1381
+ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
1382
+
1383
+ ### 2. Generate CRUD Code
1384
+
1385
+ Generate complete CRUD operations (Functions, API routes, UI):
1386
+
1387
+ \`\`\`bash
1388
+ ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1389
+ \`\`\`
1390
+
1391
+ This generates:
1392
+ ${backendScaffoldNote}
1393
+ - Next.js BFF API routes
1394
+ - React UI components (list, detail, create, edit)
1395
+ - Navigation menu integration
1396
+
1397
+ ### 3. Start Development Servers
1398
+
1399
+ \`\`\`bash
1400
+ ${pmCmd.dlx} swallowkit dev
1401
+ \`\`\`
1402
+
1403
+ This starts:
1404
+ - Next.js dev server (http://localhost:3000)
1405
+ - Azure Functions (http://localhost:7071)
1406
+ - Cosmos DB Emulator check (must be running separately)
1407
+
1408
+ **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
1409
+ ${pythonLocalDevNote}
1410
+
1411
+ ## ☁️ Deploy to Azure
1412
+
1413
+ ### Provision Azure Resources
1414
+
1415
+ Create all required Azure resources using Bicep:
1416
+
1417
+ \`\`\`bash
1418
+ ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1419
+ \`\`\`
1420
+
1421
+ This creates:
1422
+ - Static Web App (\`swa-${projectName}\`)
1423
+ - Azure Functions (\`func-${projectName}\`)
1424
+ - Cosmos DB (\`cosmos-${projectName}\`)
1425
+ - Storage Account
1426
+
1427
+ You will be prompted to select Azure regions:
1428
+ 1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
1429
+ 2. **Static Web App location**: Limited availability (default: East Asia)
1430
+
1431
+ ### CI/CD Setup
1432
+
1433
+ ${cicdChoice === 'github' ? `#### GitHub Actions
1434
+
1435
+ 1. Get Static Web App deployment token:
1436
+ \`\`\`bash
1437
+ az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
1438
+ \`\`\`
1439
+
1440
+ 2. Get Function App publish profile:
1441
+ \`\`\`bash
1442
+ az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
1443
+ \`\`\`
1444
+
1445
+ 3. Add secrets to GitHub repository:
1446
+ - \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
1447
+ - \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
1448
+ - \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
1449
+
1450
+ 4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
1451
+
1452
+ 1. Set up service connection in Azure DevOps
1453
+ 2. Update \`azure-pipelines.yml\` with your resource names
1454
+ 3. Configure pipeline variables:
1455
+ - \`azureSubscription\`: Service connection name
1456
+ - \`resourceGroupName\`: Resource group name
1457
+ 4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
1458
+
1459
+ **Deploy Static Web App:**
1460
+ \`\`\`bash
1461
+ ${pmCmd.run} build
1462
+ az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
1463
+ \`\`\`
1464
+
1465
+ **Deploy Functions:**
1466
+ \`\`\`bash
1467
+ cd functions
1468
+ ${pmCmd.run} build
1469
+ func azure functionapp publish func-${projectName}
1470
+ \`\`\``}
1471
+
1472
+ ## 🔧 Available Commands
1473
+
1474
+ - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
1475
+ - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
1476
+ - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
1477
+ - \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
1478
+ ${azureConfig.vnetOption !== 'none' ? `
1479
+ ## 🔒 Network Security (VNet Configuration)
1480
+
1481
+ This project is configured with **${vnetLabel}**.
1482
+
1483
+ ### Architecture
1484
+
1485
+ \`\`\`
1486
+ Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
1487
+
1488
+ VNet Integration
1489
+ (outbound only)
1490
+ \`\`\`
1491
+
1492
+ - **Functions Cosmos DB**: Connected via Private Endpoint (private connection)
1493
+ - **SWA → Functions**: Connected via public endpoint (secured with CORS + IP restrictions)
1494
+
1495
+ ### VNet Resources
1496
+
1497
+ | Resource | Purpose |
1498
+ |----------|---------|
1499
+ | \`vnet-${projectName}\` | Virtual Network (10.0.0.0/16) |
1500
+ | \`snet-functions\` | Functions subnet (10.0.1.0/24) |
1501
+ | \`snet-private-endpoints\` | Private Endpoints subnet (10.0.2.0/24) |
1502
+ | \`pe-cosmos-${projectName}\` | Cosmos DB Private Endpoint |
1503
+
1504
+ ### Private DNS Zones
1505
+
1506
+ - \`privatelink.documents.azure.com\` (Cosmos DB)
1507
+ ` : ''}
1508
+ ## 📚 Learn More
1509
+
1510
+ - [SwallowKit Documentation](https://github.com/himanago/swallowkit)
1511
+ - [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
1512
+ - [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
1513
+ - [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
1514
+ - [Next.js](https://nextjs.org/)
1515
+ - [Zod](https://zod.dev/)
1516
+
1517
+ ## 💭 Feedback
1518
+
1519
+ This project was generated by SwallowKit. If you encounter any issues or have suggestions for improvements, please open an issue on the [SwallowKit repository](https://github.com/himanago/swallowkit).
1186
1520
  `;
1187
1521
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1188
1522
  console.log('✅ README.md created\n');
1189
1523
  }
1190
- async function createInfrastructure(projectDir, projectName, azureConfig) {
1524
+ function createAiAgentFiles(projectDir, projectName, backendLanguage) {
1525
+ console.log('🤖 Creating AI agent instruction files...\n');
1526
+ const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1527
+ const projectMcpConfigSource = buildSwallowKitMcpProjectConfigSource();
1528
+ const functionsStructureLine = backendLanguage === 'typescript'
1529
+ ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
1530
+ : backendLanguage === 'csharp'
1531
+ ? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # Native-generated C# schema assets`
1532
+ : `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # Native-generated Python schema assets`;
1533
+ const backendSchemaNote = backendLanguage === 'typescript'
1534
+ ? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
1535
+ : `- The frontend/BFF source of truth stays in \`shared/models/\` as Zod schemas.\n- \`swallowkit scaffold\` exports OpenAPI into \`functions/openapi/\` and generates ${backendLanguageLabel} schema assets into \`functions/generated/\` with native ${backendLanguageLabel} tooling.`;
1536
+ const backendRulesNote = backendLanguage === 'typescript'
1537
+ ? `- All CRUD operations and business logic live in \`functions/src/\`.\n- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.\n- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).\n- Validate all data against Zod schemas before writing to Cosmos DB.\n- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.`
1538
+ : `- All business logic lives in \`functions/\` and the generated handlers perform real Cosmos DB CRUD.\n- Keep Zod schemas in \`shared/models/\` as the source of truth.\n- Regenerate backend contracts with \`swallowkit scaffold shared/models/<name>.ts\` whenever a schema changes.\n- Use the native-generated schema assets in \`functions/generated/\` to keep backend contracts aligned.\n- The backend should still own \`id\`, \`createdAt\`, and \`updatedAt\`.`;
1539
+ // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1540
+ const agentsMd = `# AGENTS.md
1541
+
1542
+ This project was generated by **SwallowKit**.
1543
+ All coding agents **must** follow the architecture and conventions described below.
1544
+
1545
+ ## Architecture Overview
1546
+
1547
+ This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
1548
+
1549
+ \`\`\`
1550
+ Frontend (React / Next.js App Router)
1551
+ ↓ fetch('/api/{model}', ...)
1552
+ BFF Layer (Next.js API Routes)
1553
+ ↓ HTTP → Azure Functions
1554
+ Backend (Azure Functions)
1555
+
1556
+ Azure Cosmos DB (Document Database)
1557
+ \`\`\`
1558
+
1559
+ ### Project Structure
1560
+
1561
+ \`\`\`
1562
+ ${projectName}/
1563
+ ├── app/ # Next.js App Router
1564
+ │ ├── api/ # BFF API routes (proxy to Azure Functions)
1565
+ │ └── {model}/ # UI pages per model (list, detail, create, edit)
1566
+ ├── functions/ # Azure Functions (backend)
1567
+ ${functionsStructureLine}
1568
+ ├── shared/ # Shared workspace package
1569
+ │ ├── models/ # Zod schema definitions (single source of truth)
1570
+ │ └── index.ts # Re-exports all models
1571
+ ├── lib/
1572
+ │ └── api/ # API client utilities (backend.ts, call-function.ts)
1573
+ ├── components/ # Shared React components
1574
+ ├── infra/ # Bicep infrastructure-as-code files
1575
+ │ ├── main.bicep
1576
+ │ └── modules/
1577
+ ├── .mcp.json # Project-scoped MCP bootstrap using local installed SwallowKit
1578
+ └── .github/workflows/ # CI/CD workflows (if configured)
1579
+ \`\`\`
1580
+
1581
+ ## SwallowKit MCP / Machine Workflow
1582
+
1583
+ - This repository includes a project-scoped \`.mcp.json\` file that starts the locally installed SwallowKit MCP server on runtimes that auto-load project MCP configurations.
1584
+ - Prefer the \`swallowkit_*\` MCP tools for framework-owned inspection, validation, and generation when they are available.
1585
+ - If MCP is unavailable in your runtime, fall back to the machine CLI:
1586
+ - \`npx swallowkit machine inspect project\`
1587
+ - \`npx swallowkit machine validate project\`
1588
+ - \`npx swallowkit machine generate scaffold <name> --api-only\`
1589
+ - Do not hand-edit framework-owned artifacts when the MCP or machine interface can generate or validate them for you.
1590
+ - The local MCP bootstrap depends on project dependencies already being installed.
1591
+
1592
+ ## Critical Design Principles
1593
+
1594
+ ### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
1595
+
1596
+ - \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
1597
+ - **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
1598
+ - The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
1599
+ - Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
1600
+
1601
+ Example BFF route pattern:
1602
+
1603
+ \`\`\`typescript
1604
+ // app/api/{model}/route.ts
1605
+ import { callFunction } from '@/lib/api/call-function';
1606
+ import { ModelSchema } from '@${projectName}/shared';
1607
+ import { z } from 'zod/v4';
1608
+
1609
+ export async function GET() {
1610
+ return callFunction({
1611
+ method: 'GET',
1612
+ path: '/api/{model}',
1613
+ responseSchema: z.array(ModelSchema),
1614
+ });
1615
+ }
1616
+
1617
+ export async function POST(request: NextRequest) {
1618
+ const body = await request.json();
1619
+ return callFunction({
1620
+ method: 'POST',
1621
+ path: '/api/{model}',
1622
+ body,
1623
+ inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
1624
+ responseSchema: ModelSchema,
1625
+ successStatus: 201,
1626
+ });
1627
+ }
1628
+ \`\`\`
1629
+
1630
+ ### 2. Zod Schemas Are the Single Source of Truth
1631
+
1632
+ - All data models are defined **once** as Zod schemas in \`shared/models/\`.
1633
+ - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
1634
+ - ${backendSchemaNote}
1635
+
1636
+ Model definition pattern:
1637
+
1638
+ \`\`\`typescript
1639
+ // shared/models/{model}.ts
1640
+ import { z } from 'zod/v4';
1641
+
1642
+ export const Todo = z.object({
1643
+ id: z.string(),
1644
+ name: z.string().min(1),
1645
+ // ... your fields
1646
+ createdAt: z.string().optional(),
1647
+ updatedAt: z.string().optional(),
1648
+ });
1649
+
1650
+ export type Todo = z.infer<typeof Todo>;
1651
+ export const displayName = 'Todo';
1652
+ \`\`\`
1653
+
1654
+ Key rules:
1655
+ - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1656
+ - \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
1657
+ - Always re-export models from \`shared/index.ts\`.
1658
+
1659
+ ### 3. Azure Functions Own All Business Logic and Data Access
1660
+
1661
+ - ${backendRulesNote}
1662
+
1663
+ ${backendLanguage === 'typescript' ? 'Azure Functions handler pattern:' : `Generated ${backendLanguageLabel} handlers live under \`functions/\`. Re-run \`swallowkit scaffold shared/models/<name>.ts\` after schema changes to keep generated CRUD handlers and the native schema assets under \`functions/generated/\` in sync.`}
1664
+
1665
+ ${backendLanguage === 'typescript' ? `\`\`\`typescript
1666
+ // functions/src/{model}.ts
1667
+ import { app } from '@azure/functions';
1668
+ import { ModelSchema } from '@${projectName}/shared';
1669
+
1670
+ const containerName = 'Models'; // PascalCase + 's'
1671
+
1672
+ app.http('{model}-get-all', {
1673
+ methods: ['GET'],
1674
+ route: '{model}',
1675
+ authLevel: 'anonymous',
1676
+ extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
1677
+ handler: async (request, context) => {
1678
+ const documents = context.extraInputs.get('cosmosInput');
1679
+ const validated = z.array(ModelSchema).parse(documents);
1680
+ return { status: 200, jsonBody: validated };
1681
+ },
1682
+ });
1683
+ \`\`\`` : ''}
1684
+
1685
+ ## Naming Conventions
1686
+
1687
+ | Item | Convention | Example |
1688
+ |------|-----------|---------|
1689
+ | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1690
+ | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1691
+ | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
1692
+ | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1693
+ | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1694
+ | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
1695
+ | BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
1696
+ | UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
1697
+ | React component | PascalCase | \`TodoForm.tsx\` |
1698
+ | Cosmos DB container | PascalCase + 's' | \`Todos\` |
1699
+ | Cosmos DB partition key | \`/id\` (default) | Custom: \`export const partitionKey = '/field'\` |
1700
+ | Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
1701
+
1702
+ ## Adding New Models (SwallowKit CLI Skills)
1703
+
1704
+ Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
1705
+
1706
+ ### Skill: Create a new data model
1707
+
1708
+ \`\`\`bash
1709
+ npx swallowkit create-model <name>
1710
+ # Multiple models at once:
1711
+ npx swallowkit create-model user post comment
1712
+ \`\`\`
1713
+
1714
+ Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
1715
+ Edit the generated file to add your domain-specific fields, then run scaffold.
1716
+
1717
+ ### Skill: Generate full CRUD from a model
1718
+
1719
+ \`\`\`bash
1720
+ npx swallowkit scaffold shared/models/<name>.ts
1721
+ \`\`\`
1722
+
1723
+ Generates:
1724
+ - Azure Functions handlers (${backendLanguage === 'typescript' ? '`functions/src/<name>.ts`' : '`functions/` language-specific CRUD files + `functions/generated/` schema assets'})
1725
+ - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
1726
+ - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
1727
+ - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
1728
+
1729
+ ### Skill: Start development servers
1730
+
1731
+ \`\`\`bash
1732
+ npx swallowkit dev
1733
+ \`\`\`
1734
+
1735
+ Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
1736
+ Checks for Cosmos DB Emulator availability.
1737
+
1738
+ ### Skill: Provision Azure resources
1739
+
1740
+ \`\`\`bash
1741
+ npx swallowkit provision --resource-group <name> --location <region>
1742
+ \`\`\`
1743
+
1744
+ Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
1745
+
1746
+ ### Typical workflow for "add a new feature/model"
1747
+
1748
+ 1. \`npx swallowkit create-model <name>\`
1749
+ 2. Edit \`shared/models/<name>.ts\` — add fields
1750
+ 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1751
+ 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1752
+
1753
+ ## Do NOT
1754
+
1755
+ - **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
1756
+ - **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
1757
+ - **Do not** manually duplicate model definitions across layers. Use the shared package.
1758
+ - **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
1759
+ - **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
1760
+ - By default, all containers use \`/id\` as the partition key. To use a custom partition key, add \`export const partitionKey = '/yourField'\` to the model file. The scaffold command will apply it across all layers.
1761
+
1762
+ ## Technology Stack
1763
+
1764
+ - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
1765
+ - **BFF**: Next.js API Routes (proxy only)
1766
+ - **Backend**: Azure Functions (${backendLanguageLabel})
1767
+ - **Database**: Azure Cosmos DB (NoSQL)
1768
+ - **Schema**: Zod (shared across all layers via workspace package)
1769
+ - **Infrastructure**: Bicep (IaC)
1770
+ - **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
1771
+ - **Auth**: Azure Managed Identity (no connection strings in production)
1772
+ - **Monitoring**: Application Insights
1773
+ `;
1774
+ fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
1775
+ console.log(' ✅ AGENTS.md (Codex / generic agents)');
1776
+ // ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
1777
+ const claudeMd = `# CLAUDE.md
1778
+
1779
+ This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
1780
+
1781
+ ## Quick Reference
1782
+
1783
+ - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
1784
+ - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
1785
+ - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
1786
+ - **Backend language**: ${backendLanguageLabel}
1787
+ - **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
1788
+
1789
+ ## SwallowKit MCP
1790
+
1791
+ - This repository includes a project-scoped \`.mcp.json\` that registers the locally installed SwallowKit MCP server for runtimes that support project MCP files.
1792
+ - When the \`swallowkit_*\` tools are available, prefer them for inspect / validate / generate tasks.
1793
+ - If MCP is unavailable, use \`npx swallowkit machine ...\` instead.
1794
+
1795
+ ## SwallowKit CLI Commands
1796
+
1797
+ | Task | Command |
1798
+ |------|---------|
1799
+ | Create model | \`npx swallowkit create-model <name>\` |
1800
+ | Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
1801
+ | Dev servers | \`npx swallowkit dev\` |
1802
+ | Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
1803
+
1804
+ ## Workflow: Add a new model
1805
+
1806
+ 1. \`npx swallowkit create-model <name>\`
1807
+ 2. Edit \`shared/models/<name>.ts\` — add your fields
1808
+ 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1809
+ 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1810
+ `;
1811
+ fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
1812
+ console.log(' ✅ CLAUDE.md (Claude Code)');
1813
+ fs.writeFileSync(path.join(projectDir, '.mcp.json'), projectMcpConfigSource);
1814
+ console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
1815
+ // ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
1816
+ const ghDir = path.join(projectDir, '.github');
1817
+ fs.mkdirSync(ghDir, { recursive: true });
1818
+ const copilotInstructions = `# Copilot Instructions
1819
+
1820
+ This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
1821
+
1822
+ ## Architecture (3-layer)
1823
+
1824
+ \`\`\`
1825
+ Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
1826
+ \`\`\`
1827
+
1828
+ ## Key Rules
1829
+
1830
+ 1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
1831
+ 2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
1832
+ 3. **Backend owns data** — All CRUD and business logic stay in \`functions/\`, and generated contract assets under \`functions/generated/\` must stay aligned with \`shared/models/\`.
1833
+ 4. **Use the CLI** — Run \`npx swallowkit create-model <name>\` then \`npx swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
1834
+
1835
+ ## SwallowKit Framework Operations
1836
+
1837
+ - Prefer the SwallowKit MCP or machine interface for framework-owned inspection, validation, and generation instead of hand-editing generated files.
1838
+ - If your runtime loads project-scoped MCP config from \`.mcp.json\`, use the \`swallowkit_*\` tools.
1839
+ - Otherwise use \`npx swallowkit machine inspect project\`, \`npx swallowkit machine validate project\`, and \`npx swallowkit machine generate scaffold <name> --api-only\`.
1840
+
1841
+ ## Naming
1842
+
1843
+ - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
1844
+ - Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
1845
+ - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key default \`/id\` (customizable via \`export const partitionKey\`)
1846
+
1847
+ ## Managed Fields
1848
+
1849
+ \`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
1850
+ `;
1851
+ fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
1852
+ console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
1853
+ // ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
1854
+ const instructionsDir = path.join(ghDir, 'instructions');
1855
+ fs.mkdirSync(instructionsDir, { recursive: true });
1856
+ // 4a. shared/models — Zod schema layer
1857
+ const sharedModelsInstructions = `---
1858
+ applyTo: "shared/models/**"
1859
+ ---
1860
+
1861
+ # Shared Models — Zod Schema Rules
1862
+
1863
+ Files in this directory are the **single source of truth** for data models across the entire application.
1864
+
1865
+ ## Rules
1866
+
1867
+ - Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
1868
+ - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1869
+ \`\`\`typescript
1870
+ export const Todo = z.object({ ... });
1871
+ export type Todo = z.infer<typeof Todo>;
1872
+ \`\`\`
1873
+ - Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
1874
+ - Export a \`displayName\` string constant for UI display.
1875
+ - Re-export every model from \`shared/index.ts\`.
1876
+ - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
1877
+ - After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
1878
+ `;
1879
+ fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
1880
+ // 4b. app/api — BFF layer
1881
+ const bffInstructions = `---
1882
+ applyTo: "app/api/**"
1883
+ ---
1884
+
1885
+ # BFF API Routes — Rules
1886
+
1887
+ Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
1888
+
1889
+ ## Rules
1890
+
1891
+ - **Never** put business logic, database access, or direct Cosmos DB calls here.
1892
+ - Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
1893
+ - You may validate input/output with Zod schemas before forwarding.
1894
+ - Import schemas from \`@${projectName}/shared\`.
1895
+
1896
+ ## Pattern
1897
+
1898
+ \`\`\`typescript
1899
+ import { callFunction } from '@/lib/api/call-function';
1900
+ import { ModelSchema } from '@${projectName}/shared';
1901
+ import { z } from 'zod/v4';
1902
+
1903
+ export async function GET() {
1904
+ return callFunction({
1905
+ method: 'GET',
1906
+ path: '/api/{model}',
1907
+ responseSchema: z.array(ModelSchema),
1908
+ });
1909
+ }
1910
+ \`\`\`
1911
+ `;
1912
+ fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
1913
+ // 4c. functions — Azure Functions backend layer
1914
+ const functionsInstructions = `---
1915
+ applyTo: "functions/**"
1916
+ ---
1917
+
1918
+ # Azure Functions — Backend Rules
1919
+
1920
+ Files in \`functions/\` contain all business logic and data access for this application.
1921
+
1922
+ ## Rules
1923
+
1924
+ - Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
1925
+ - For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
1926
+ - For C#/Python backends, consume the native-generated assets in \`functions/generated/\`.
1927
+ - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
1928
+ - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key defaults to \`/id\` but can be customized per model.
1929
+
1930
+ ## Handler Pattern
1931
+
1932
+ \`\`\`typescript
1933
+ import { app } from '@azure/functions';
1934
+ import { ModelSchema } from '@${projectName}/shared';
1935
+
1936
+ app.http('{model}-get-all', {
1937
+ methods: ['GET'],
1938
+ route: '{model}',
1939
+ authLevel: 'anonymous',
1940
+ extraInputs: [cosmosInput],
1941
+ handler: async (request, context) => {
1942
+ const documents = context.extraInputs.get(cosmosInput);
1943
+ const validated = z.array(ModelSchema).parse(documents);
1944
+ return { status: 200, jsonBody: validated };
1945
+ },
1946
+ });
1947
+ \`\`\`
1948
+ `;
1949
+ fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
1950
+ console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
1951
+ console.log(' - shared-models.instructions.md');
1952
+ console.log(' - bff-routes.instructions.md');
1953
+ console.log(' - azure-functions.instructions.md');
1954
+ console.log('\n✅ AI agent files created\n');
1955
+ console.log(' Supported agents:');
1956
+ console.log(' - OpenAI Codex → AGENTS.md');
1957
+ console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
1958
+ console.log(' - Project MCP runtimes → .mcp.json');
1959
+ console.log(' - GitHub Copilot → .github/copilot-instructions.md');
1960
+ console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
1961
+ console.log('');
1962
+ }
1963
+ async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
1191
1964
  console.log('📦 Creating infrastructure files (Bicep)...\n');
1192
1965
  const infraDir = path.join(projectDir, 'infra');
1193
1966
  const modulesDir = path.join(infraDir, 'modules');
1194
1967
  fs.mkdirSync(modulesDir, { recursive: true });
1195
1968
  const enableVNet = azureConfig.vnetOption !== 'none';
1969
+ const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
1196
1970
  // main.bicep
1197
- const mainBicep = `targetScope = 'resourceGroup'
1198
-
1199
- @description('Project name')
1200
- param projectName string
1201
-
1202
- @description('Location for Functions and Cosmos DB')
1203
- param location string = resourceGroup().location
1204
-
1205
- @description('Location for Static Web App (must be explicitly provided)')
1206
- param swaLocation string
1207
-
1208
- @description('Cosmos DB mode')
1209
- @allowed(['freetier', 'serverless'])
1210
- param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
1211
-
1212
- @description('Enable VNet integration')
1213
- param enableVNet bool = ${enableVNet}
1214
-
1215
- // Shared Log Analytics Workspace (in Functions region for data residency)
1216
- module logAnalytics 'modules/loganalytics.bicep' = {
1217
- name: 'logAnalytics'
1218
- params: {
1219
- name: 'log-\${projectName}'
1220
- location: location
1221
- }
1222
- }
1223
-
1224
- // Application Insights for Static Web App (must be in same region as SWA)
1225
- module appInsightsSwa 'modules/appinsights.bicep' = {
1226
- name: 'appInsightsSwa'
1227
- params: {
1228
- name: 'appi-\${projectName}-swa'
1229
- location: swaLocation
1230
- logAnalyticsWorkspaceId: logAnalytics.outputs.id
1231
- }
1232
- }
1233
-
1234
- // Application Insights for Functions (in same region as Functions)
1235
- module appInsightsFunctions 'modules/appinsights.bicep' = {
1236
- name: 'appInsightsFunctions'
1237
- params: {
1238
- name: 'appi-\${projectName}-func'
1239
- location: location
1240
- logAnalyticsWorkspaceId: logAnalytics.outputs.id
1241
- }
1242
- }
1243
-
1244
- // Static Web App
1245
- module staticWebApp 'modules/staticwebapp.bicep' = {
1246
- name: 'staticWebApp'
1247
- params: {
1248
- name: 'swa-\${projectName}'
1249
- location: swaLocation
1250
- sku: 'Standard'
1251
- appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1252
- }
1253
- }
1254
-
1255
- // VNet (conditional)
1256
- module vnet 'modules/vnet.bicep' = if (enableVNet) {
1257
- name: 'vnet'
1258
- params: {
1259
- name: 'vnet-\${projectName}'
1260
- location: location
1261
- }
1262
- }
1263
-
1264
- // Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
1265
- module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
1266
- name: 'cosmosDb'
1267
- params: {
1268
- accountName: 'cosmos-\${projectName}'
1269
- databaseName: '\${projectName}Database'
1270
- location: location
1271
- publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
1272
- }
1273
- }
1274
-
1275
- module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
1276
- name: 'cosmosDb'
1277
- params: {
1278
- accountName: 'cosmos-\${projectName}'
1279
- databaseName: '\${projectName}Database'
1280
- location: location
1281
- publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
1282
- }
1283
- }
1284
-
1285
- // Cosmos DB Private Endpoint (conditional)
1286
- module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
1287
- name: 'cosmosPrivateEndpoint'
1288
- params: {
1289
- name: 'pe-cosmos-\${projectName}'
1290
- location: location
1291
- cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
1292
- cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1293
- subnetId: vnet.outputs.privateEndpointSubnetId
1294
- vnetId: vnet.outputs.id
1295
- }
1296
- dependsOn: [
1297
- cosmosDbFreeTier
1298
- cosmosDbServerless
1299
- vnet
1300
- ]
1301
- }
1302
-
1303
- // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
1304
- module functionsFlex 'modules/functions-flex.bicep' = {
1305
- name: 'functionsApp'
1306
- params: {
1307
- name: 'func-\${projectName}'
1308
- location: location
1309
- storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1310
- appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1311
- swaDefaultHostname: staticWebApp.outputs.defaultHostname
1312
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1313
- cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1314
- enableVNet: enableVNet
1315
- vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
1316
- }
1317
- dependsOn: [
1318
- cosmosDbFreeTier
1319
- cosmosDbServerless
1320
- cosmosPrivateEndpoint
1321
- ]
1322
- }
1323
-
1324
- // Cosmos DB role assignment for Functions (after Functions is created)
1325
- module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
1326
- name: 'cosmosDbRoleAssignment'
1327
- params: {
1328
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
1329
- functionsPrincipalId: functionsFlex.outputs.principalId
1330
- }
1331
- dependsOn: [
1332
- functionsFlex
1333
- ]
1334
- }
1335
-
1336
- module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
1337
- name: 'cosmosDbRoleAssignment'
1338
- params: {
1339
- cosmosAccountName: cosmosDbServerless.outputs.accountName
1340
- functionsPrincipalId: functionsFlex.outputs.principalId
1341
- }
1342
- dependsOn: [
1343
- functionsFlex
1344
- ]
1345
- }
1346
-
1347
- // Update SWA config with Functions hostname (after Functions deployment)
1348
- module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
1349
- name: 'staticWebAppConfig'
1350
- params: {
1351
- staticWebAppName: staticWebApp.outputs.name
1352
- functionsDefaultHostname: functionsFlex.outputs.defaultHostname
1353
- appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1354
- }
1355
- dependsOn: [
1356
- functionsFlex
1357
- ]
1358
- }
1359
-
1360
- output staticWebAppName string = staticWebApp.outputs.name
1361
- output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
1362
- output functionsAppName string = functionsFlex.outputs.name
1363
- output functionsAppUrl string = functionsFlex.outputs.defaultHostname
1364
- output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1365
- output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1366
- output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1367
- output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
1368
- output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
1369
- output appInsightsSwaName string = appInsightsSwa.outputs.name
1370
- output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
1371
- output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
1372
- output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
1373
- output vnetEnabled bool = enableVNet
1374
- output vnetName string = enableVNet ? vnet.outputs.name : ''
1971
+ const mainBicep = `targetScope = 'resourceGroup'
1972
+
1973
+ @description('Project name')
1974
+ param projectName string
1975
+
1976
+ @description('Location for Functions and Cosmos DB')
1977
+ param location string = resourceGroup().location
1978
+
1979
+ @description('Location for Static Web App (must be explicitly provided)')
1980
+ param swaLocation string
1981
+
1982
+ @description('Cosmos DB mode')
1983
+ @allowed(['freetier', 'serverless'])
1984
+ param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
1985
+
1986
+ @description('Enable VNet integration')
1987
+ param enableVNet bool = ${enableVNet}
1988
+
1989
+ // Shared Log Analytics Workspace (in Functions region for data residency)
1990
+ module logAnalytics 'modules/loganalytics.bicep' = {
1991
+ name: 'logAnalytics'
1992
+ params: {
1993
+ name: 'log-\${projectName}'
1994
+ location: location
1995
+ }
1996
+ }
1997
+
1998
+ // Application Insights for Static Web App (must be in same region as SWA)
1999
+ module appInsightsSwa 'modules/appinsights.bicep' = {
2000
+ name: 'appInsightsSwa'
2001
+ params: {
2002
+ name: 'appi-\${projectName}-swa'
2003
+ location: swaLocation
2004
+ logAnalyticsWorkspaceId: logAnalytics.outputs.id
2005
+ }
2006
+ }
2007
+
2008
+ // Application Insights for Functions (in same region as Functions)
2009
+ module appInsightsFunctions 'modules/appinsights.bicep' = {
2010
+ name: 'appInsightsFunctions'
2011
+ params: {
2012
+ name: 'appi-\${projectName}-func'
2013
+ location: location
2014
+ logAnalyticsWorkspaceId: logAnalytics.outputs.id
2015
+ }
2016
+ }
2017
+
2018
+ // Static Web App
2019
+ module staticWebApp 'modules/staticwebapp.bicep' = {
2020
+ name: 'staticWebApp'
2021
+ params: {
2022
+ name: 'swa-\${projectName}'
2023
+ location: swaLocation
2024
+ sku: 'Standard'
2025
+ appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2026
+ }
2027
+ }
2028
+
2029
+ // VNet (conditional)
2030
+ module vnet 'modules/vnet.bicep' = if (enableVNet) {
2031
+ name: 'vnet'
2032
+ params: {
2033
+ name: 'vnet-\${projectName}'
2034
+ location: location
2035
+ }
2036
+ }
2037
+
2038
+ // Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
2039
+ module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
2040
+ name: 'cosmosDb'
2041
+ params: {
2042
+ accountName: 'cosmos-\${projectName}'
2043
+ databaseName: '\${projectName}Database'
2044
+ location: location
2045
+ publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
2046
+ }
2047
+ }
2048
+
2049
+ module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
2050
+ name: 'cosmosDb'
2051
+ params: {
2052
+ accountName: 'cosmos-\${projectName}'
2053
+ databaseName: '\${projectName}Database'
2054
+ location: location
2055
+ publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
2056
+ }
2057
+ }
2058
+
2059
+ // Cosmos DB Private Endpoint (conditional)
2060
+ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
2061
+ name: 'cosmosPrivateEndpoint'
2062
+ params: {
2063
+ name: 'pe-cosmos-\${projectName}'
2064
+ location: location
2065
+ cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
2066
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2067
+ subnetId: vnet.outputs.privateEndpointSubnetId
2068
+ vnetId: vnet.outputs.id
2069
+ }
2070
+ dependsOn: [
2071
+ cosmosDbFreeTier
2072
+ cosmosDbServerless
2073
+ vnet
2074
+ ]
2075
+ }
2076
+
2077
+ // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
2078
+ module functionsFlex 'modules/functions-flex.bicep' = {
2079
+ name: 'functionsApp'
2080
+ params: {
2081
+ name: 'func-\${projectName}'
2082
+ location: location
2083
+ storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
2084
+ appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
2085
+ swaDefaultHostname: staticWebApp.outputs.defaultHostname
2086
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2087
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
2088
+ functionsRuntimeName: '${functionsRuntime.name}'
2089
+ functionsRuntimeVersion: '${functionsRuntime.version}'
2090
+ enableVNet: enableVNet
2091
+ vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
2092
+ }
2093
+ dependsOn: [
2094
+ cosmosDbFreeTier
2095
+ cosmosDbServerless
2096
+ cosmosPrivateEndpoint
2097
+ ]
2098
+ }
2099
+
2100
+ // Cosmos DB role assignment for Functions (after Functions is created)
2101
+ module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
2102
+ name: 'cosmosDbRoleAssignment'
2103
+ params: {
2104
+ cosmosAccountName: cosmosDbFreeTier.outputs.accountName
2105
+ functionsPrincipalId: functionsFlex.outputs.principalId
2106
+ }
2107
+ dependsOn: [
2108
+ functionsFlex
2109
+ ]
2110
+ }
2111
+
2112
+ module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
2113
+ name: 'cosmosDbRoleAssignment'
2114
+ params: {
2115
+ cosmosAccountName: cosmosDbServerless.outputs.accountName
2116
+ functionsPrincipalId: functionsFlex.outputs.principalId
2117
+ }
2118
+ dependsOn: [
2119
+ functionsFlex
2120
+ ]
2121
+ }
2122
+
2123
+ // Update SWA config with Functions hostname (after Functions deployment)
2124
+ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2125
+ name: 'staticWebAppConfig'
2126
+ params: {
2127
+ staticWebAppName: staticWebApp.outputs.name
2128
+ functionsDefaultHostname: functionsFlex.outputs.defaultHostname
2129
+ appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2130
+ }
2131
+ dependsOn: [
2132
+ functionsFlex
2133
+ ]
2134
+ }
2135
+
2136
+ output staticWebAppName string = staticWebApp.outputs.name
2137
+ output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
2138
+ output functionsAppName string = functionsFlex.outputs.name
2139
+ output functionsAppUrl string = functionsFlex.outputs.defaultHostname
2140
+ output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2141
+ output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2142
+ output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
2143
+ output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
2144
+ output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
2145
+ output appInsightsSwaName string = appInsightsSwa.outputs.name
2146
+ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
2147
+ output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
2148
+ output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
2149
+ output vnetEnabled bool = enableVNet
2150
+ output vnetName string = enableVNet ? vnet.outputs.name : ''
1375
2151
  `;
1376
2152
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
1377
2153
  // main.parameters.json
1378
- const params = `{
1379
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
1380
- "contentVersion": "1.0.0.0",
1381
- "parameters": {
1382
- "projectName": {
1383
- "value": "${projectName}"
1384
- },
1385
- "cosmosDbMode": {
1386
- "value": "${azureConfig.cosmosDbMode}"
1387
- },
1388
- "enableVNet": {
1389
- "value": ${enableVNet}
1390
- }
1391
- }
1392
- }
2154
+ const params = `{
2155
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
2156
+ "contentVersion": "1.0.0.0",
2157
+ "parameters": {
2158
+ "projectName": {
2159
+ "value": "${projectName}"
2160
+ },
2161
+ "cosmosDbMode": {
2162
+ "value": "${azureConfig.cosmosDbMode}"
2163
+ },
2164
+ "enableVNet": {
2165
+ "value": ${enableVNet}
2166
+ }
2167
+ }
2168
+ }
1393
2169
  `;
1394
2170
  fs.writeFileSync(path.join(infraDir, 'main.parameters.json'), params);
1395
2171
  // modules/staticwebapp.bicep
1396
- const staticWebAppBicep = `@description('Static Web App name')
1397
- param name string
1398
-
1399
- @description('Location for the Static Web App')
1400
- param location string
1401
-
1402
- @description('SKU name (Free or Standard)')
1403
- @allowed([
1404
- 'Free'
1405
- 'Standard'
1406
- ])
1407
- param sku string = 'Standard'
1408
-
1409
- @description('Application Insights connection string')
1410
- param appInsightsConnectionString string
1411
-
1412
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
1413
- name: name
1414
- location: location
1415
- sku: {
1416
- name: sku
1417
- tier: sku
1418
- }
1419
- properties: {
1420
- buildProperties: {
1421
- skipGithubActionWorkflowGeneration: true
1422
- }
1423
- }
1424
- }
1425
-
1426
- // Link Application Insights to Static Web App (for both client and server-side telemetry)
1427
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1428
- parent: staticWebApp
1429
- name: 'appsettings'
1430
- properties: {
1431
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1432
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1433
- }
1434
- }
1435
-
1436
- output id string = staticWebApp.id
1437
- output name string = staticWebApp.name
1438
- output defaultHostname string = staticWebApp.properties.defaultHostname
2172
+ const staticWebAppBicep = `@description('Static Web App name')
2173
+ param name string
2174
+
2175
+ @description('Location for the Static Web App')
2176
+ param location string
2177
+
2178
+ @description('SKU name (Free or Standard)')
2179
+ @allowed([
2180
+ 'Free'
2181
+ 'Standard'
2182
+ ])
2183
+ param sku string = 'Standard'
2184
+
2185
+ @description('Application Insights connection string')
2186
+ param appInsightsConnectionString string
2187
+
2188
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
2189
+ name: name
2190
+ location: location
2191
+ sku: {
2192
+ name: sku
2193
+ tier: sku
2194
+ }
2195
+ properties: {
2196
+ buildProperties: {
2197
+ skipGithubActionWorkflowGeneration: true
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ // Link Application Insights to Static Web App (for both client and server-side telemetry)
2203
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2204
+ parent: staticWebApp
2205
+ name: 'appsettings'
2206
+ properties: {
2207
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2208
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2209
+ }
2210
+ }
2211
+
2212
+ output id string = staticWebApp.id
2213
+ output name string = staticWebApp.name
2214
+ output defaultHostname string = staticWebApp.properties.defaultHostname
1439
2215
  `;
1440
2216
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
1441
2217
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
1442
- const staticWebAppConfigBicep = `@description('Static Web App name')
1443
- param staticWebAppName string
1444
-
1445
- @description('Functions App default hostname for backend API calls')
1446
- param functionsDefaultHostname string
1447
-
1448
- @description('Application Insights connection string for SWA')
1449
- param appInsightsConnectionString string
1450
-
1451
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1452
- name: staticWebAppName
1453
- }
1454
-
1455
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1456
- parent: staticWebApp
1457
- name: 'appsettings'
1458
- properties: {
1459
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1460
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1461
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
1462
- }
1463
- }
1464
-
1465
- output configName string = staticWebAppConfig.name
2218
+ const staticWebAppConfigBicep = `@description('Static Web App name')
2219
+ param staticWebAppName string
2220
+
2221
+ @description('Functions App default hostname for backend API calls')
2222
+ param functionsDefaultHostname string
2223
+
2224
+ @description('Application Insights connection string for SWA')
2225
+ param appInsightsConnectionString string
2226
+
2227
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2228
+ name: staticWebAppName
2229
+ }
2230
+
2231
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2232
+ parent: staticWebApp
2233
+ name: 'appsettings'
2234
+ properties: {
2235
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2236
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2237
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2238
+ }
2239
+ }
2240
+
2241
+ output configName string = staticWebAppConfig.name
1466
2242
  `;
1467
2243
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
1468
2244
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
1469
- const logAnalyticsBicep = `@description('Log Analytics workspace name')
1470
- param name string
1471
-
1472
- @description('Location for Log Analytics workspace')
1473
- param location string
1474
-
1475
- resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
1476
- name: name
1477
- location: location
1478
- properties: {
1479
- sku: {
1480
- name: 'PerGB2018'
1481
- }
1482
- retentionInDays: 30
1483
- }
1484
- }
1485
-
1486
- output id string = logAnalytics.id
1487
- output name string = logAnalytics.name
2245
+ const logAnalyticsBicep = `@description('Log Analytics workspace name')
2246
+ param name string
2247
+
2248
+ @description('Location for Log Analytics workspace')
2249
+ param location string
2250
+
2251
+ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
2252
+ name: name
2253
+ location: location
2254
+ properties: {
2255
+ sku: {
2256
+ name: 'PerGB2018'
2257
+ }
2258
+ retentionInDays: 30
2259
+ }
2260
+ }
2261
+
2262
+ output id string = logAnalytics.id
2263
+ output name string = logAnalytics.name
1488
2264
  `;
1489
2265
  fs.writeFileSync(path.join(modulesDir, 'loganalytics.bicep'), logAnalyticsBicep);
1490
2266
  // modules/appinsights.bicep (Application Insights only, connects to shared Log Analytics)
1491
- const appInsightsBicep = `@description('Application Insights name')
1492
- param name string
1493
-
1494
- @description('Location for Application Insights')
1495
- param location string
1496
-
1497
- @description('Log Analytics workspace resource ID')
1498
- param logAnalyticsWorkspaceId string
1499
-
1500
- // Application Insights
1501
- resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
1502
- name: name
1503
- location: location
1504
- kind: 'web'
1505
- properties: {
1506
- Application_Type: 'web'
1507
- WorkspaceResourceId: logAnalyticsWorkspaceId
1508
- RetentionInDays: 30
1509
- }
1510
- }
1511
-
1512
- output id string = appInsights.id
1513
- output name string = appInsights.name
1514
- output connectionString string = appInsights.properties.ConnectionString
1515
- output instrumentationKey string = appInsights.properties.InstrumentationKey
2267
+ const appInsightsBicep = `@description('Application Insights name')
2268
+ param name string
2269
+
2270
+ @description('Location for Application Insights')
2271
+ param location string
2272
+
2273
+ @description('Log Analytics workspace resource ID')
2274
+ param logAnalyticsWorkspaceId string
2275
+
2276
+ // Application Insights
2277
+ resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
2278
+ name: name
2279
+ location: location
2280
+ kind: 'web'
2281
+ properties: {
2282
+ Application_Type: 'web'
2283
+ WorkspaceResourceId: logAnalyticsWorkspaceId
2284
+ RetentionInDays: 30
2285
+ }
2286
+ }
2287
+
2288
+ output id string = appInsights.id
2289
+ output name string = appInsights.name
2290
+ output connectionString string = appInsights.properties.ConnectionString
2291
+ output instrumentationKey string = appInsights.properties.InstrumentationKey
1516
2292
  `;
1517
2293
  fs.writeFileSync(path.join(modulesDir, 'appinsights.bicep'), appInsightsBicep);
1518
2294
  // modules/functions-flex.bicep (Flex Consumption)
1519
- const functionsFlexBicep = `@description('Functions App name')
1520
- param name string
1521
-
1522
- @description('Location for the Functions App')
1523
- param location string
1524
-
1525
- @description('Storage account name for Functions')
1526
- param storageAccountName string
1527
-
1528
- @description('Application Insights connection string')
1529
- param appInsightsConnectionString string
1530
-
1531
- @description('Static Web App default hostname for CORS')
1532
- param swaDefaultHostname string
1533
-
1534
- @description('Cosmos DB endpoint')
1535
- param cosmosDbEndpoint string
1536
-
1537
- @description('Cosmos DB database name')
1538
- param cosmosDbDatabaseName string
1539
-
1540
- @description('Enable VNet integration')
1541
- param enableVNet bool = false
1542
-
1543
- @description('VNet subnet ID for Functions (required if enableVNet is true)')
1544
- param vnetSubnetId string = ''
1545
-
1546
- // Storage Account for Functions
1547
- resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
1548
- name: storageAccountName
1549
- location: location
1550
- sku: {
1551
- name: 'Standard_LRS'
1552
- }
1553
- kind: 'StorageV2'
1554
- properties: {
1555
- supportsHttpsTrafficOnly: true
1556
- minimumTlsVersion: 'TLS1_2'
1557
- }
1558
- }
1559
-
1560
- // Blob Service for deployment package container
1561
- resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
1562
- parent: storageAccount
1563
- name: 'default'
1564
- }
1565
-
1566
- // Deployment package container
1567
- resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
1568
- parent: blobService
1569
- name: 'deploymentpackage'
1570
- properties: {
1571
- publicAccess: 'None'
1572
- }
1573
- }
1574
-
1575
- // App Service Plan (Flex Consumption)
1576
- resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
1577
- name: '\${name}-plan'
1578
- location: location
1579
- sku: {
1580
- name: 'FC1'
1581
- tier: 'FlexConsumption'
1582
- }
1583
- properties: {
1584
- reserved: true // Required for Linux
1585
- }
1586
- }
1587
-
1588
- // Azure Functions App
1589
- resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
1590
- name: name
1591
- location: location
1592
- kind: 'functionapp,linux'
1593
- identity: {
1594
- type: 'SystemAssigned'
1595
- }
1596
- properties: {
1597
- serverFarmId: hostingPlan.id
1598
- reserved: true
1599
- virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
1600
- vnetContentShareEnabled: enableVNet
1601
- functionAppConfig: {
1602
- deployment: {
1603
- storage: {
1604
- type: 'blobContainer'
1605
- value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
1606
- authentication: {
1607
- type: 'SystemAssignedIdentity'
1608
- }
1609
- }
1610
- }
1611
- scaleAndConcurrency: {
1612
- maximumInstanceCount: 100
1613
- instanceMemoryMB: 2048
1614
- }
1615
- runtime: {
1616
- name: 'node'
1617
- version: '22'
1618
- }
1619
- }
1620
- siteConfig: {
1621
- appSettings: [
1622
- {
1623
- name: 'AzureWebJobsStorage__accountName'
1624
- value: storageAccount.name
1625
- }
1626
- {
1627
- name: 'FUNCTIONS_EXTENSION_VERSION'
1628
- value: '~4'
1629
- }
1630
- {
1631
- name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1632
- value: appInsightsConnectionString
1633
- }
1634
- {
1635
- name: 'CosmosDBConnection__accountEndpoint'
1636
- value: cosmosDbEndpoint
1637
- }
1638
- {
1639
- name: 'COSMOS_DB_DATABASE_NAME'
1640
- value: cosmosDbDatabaseName
1641
- }
1642
- ]
1643
- cors: {
1644
- allowedOrigins: [
1645
- 'https://\${swaDefaultHostname}'
1646
- ]
1647
- }
1648
- ipSecurityRestrictions: [
1649
- {
1650
- action: 'Allow'
1651
- ipAddress: 'AzureCloud'
1652
- tag: 'ServiceTag'
1653
- priority: 100
1654
- }
1655
- ]
1656
- }
1657
- httpsOnly: true
1658
- }
1659
- }
1660
-
1661
- // Role Assignment: Storage Blob Data Contributor
1662
- resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
1663
- name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
1664
- scope: storageAccount
1665
- properties: {
1666
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
1667
- principalId: functionApp.identity.principalId
1668
- principalType: 'ServicePrincipal'
1669
- }
1670
- }
1671
-
1672
- output id string = functionApp.id
1673
- output name string = functionApp.name
1674
- output defaultHostname string = functionApp.properties.defaultHostName
1675
- output principalId string = functionApp.identity.principalId
2295
+ const functionsFlexBicep = `@description('Functions App name')
2296
+ param name string
2297
+
2298
+ @description('Location for the Functions App')
2299
+ param location string
2300
+
2301
+ @description('Storage account name for Functions')
2302
+ param storageAccountName string
2303
+
2304
+ @description('Application Insights connection string')
2305
+ param appInsightsConnectionString string
2306
+
2307
+ @description('Static Web App default hostname for CORS')
2308
+ param swaDefaultHostname string
2309
+
2310
+ @description('Cosmos DB endpoint')
2311
+ param cosmosDbEndpoint string
2312
+
2313
+ @description('Cosmos DB database name')
2314
+ param cosmosDbDatabaseName string
2315
+
2316
+ @description('Enable VNet integration')
2317
+ param enableVNet bool = false
2318
+
2319
+ @description('VNet subnet ID for Functions (required if enableVNet is true)')
2320
+ param vnetSubnetId string = ''
2321
+
2322
+ @description('Functions runtime name')
2323
+ param functionsRuntimeName string
2324
+
2325
+ @description('Functions runtime version')
2326
+ param functionsRuntimeVersion string
2327
+
2328
+ // Storage Account for Functions
2329
+ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
2330
+ name: storageAccountName
2331
+ location: location
2332
+ sku: {
2333
+ name: 'Standard_LRS'
2334
+ }
2335
+ kind: 'StorageV2'
2336
+ properties: {
2337
+ supportsHttpsTrafficOnly: true
2338
+ minimumTlsVersion: 'TLS1_2'
2339
+ }
2340
+ }
2341
+
2342
+ // Blob Service for deployment package container
2343
+ resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
2344
+ parent: storageAccount
2345
+ name: 'default'
2346
+ }
2347
+
2348
+ // Deployment package container
2349
+ resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
2350
+ parent: blobService
2351
+ name: 'deploymentpackage'
2352
+ properties: {
2353
+ publicAccess: 'None'
2354
+ }
2355
+ }
2356
+
2357
+ // App Service Plan (Flex Consumption)
2358
+ resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
2359
+ name: '\${name}-plan'
2360
+ location: location
2361
+ sku: {
2362
+ name: 'FC1'
2363
+ tier: 'FlexConsumption'
2364
+ }
2365
+ properties: {
2366
+ reserved: true // Required for Linux
2367
+ }
2368
+ }
2369
+
2370
+ // Azure Functions App
2371
+ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
2372
+ name: name
2373
+ location: location
2374
+ kind: 'functionapp,linux'
2375
+ identity: {
2376
+ type: 'SystemAssigned'
2377
+ }
2378
+ properties: {
2379
+ serverFarmId: hostingPlan.id
2380
+ reserved: true
2381
+ virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
2382
+ vnetContentShareEnabled: enableVNet
2383
+ functionAppConfig: {
2384
+ deployment: {
2385
+ storage: {
2386
+ type: 'blobContainer'
2387
+ value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
2388
+ authentication: {
2389
+ type: 'SystemAssignedIdentity'
2390
+ }
2391
+ }
2392
+ }
2393
+ scaleAndConcurrency: {
2394
+ maximumInstanceCount: 100
2395
+ instanceMemoryMB: 2048
2396
+ }
2397
+ runtime: {
2398
+ name: functionsRuntimeName
2399
+ version: functionsRuntimeVersion
2400
+ }
2401
+ }
2402
+ siteConfig: {
2403
+ appSettings: [
2404
+ {
2405
+ name: 'AzureWebJobsStorage__accountName'
2406
+ value: storageAccount.name
2407
+ }
2408
+ {
2409
+ name: 'FUNCTIONS_EXTENSION_VERSION'
2410
+ value: '~4'
2411
+ }
2412
+ {
2413
+ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
2414
+ value: appInsightsConnectionString
2415
+ }
2416
+ {
2417
+ name: 'CosmosDBConnection__accountEndpoint'
2418
+ value: cosmosDbEndpoint
2419
+ }
2420
+ {
2421
+ name: 'COSMOS_DB_DATABASE_NAME'
2422
+ value: cosmosDbDatabaseName
2423
+ }
2424
+ ]
2425
+ cors: {
2426
+ allowedOrigins: [
2427
+ 'https://\${swaDefaultHostname}'
2428
+ ]
2429
+ }
2430
+ ipSecurityRestrictions: [
2431
+ {
2432
+ action: 'Allow'
2433
+ ipAddress: 'AzureCloud'
2434
+ tag: 'ServiceTag'
2435
+ priority: 100
2436
+ }
2437
+ ]
2438
+ }
2439
+ httpsOnly: true
2440
+ }
2441
+ }
2442
+
2443
+ // Role Assignment: Storage Blob Data Contributor
2444
+ resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
2445
+ name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
2446
+ scope: storageAccount
2447
+ properties: {
2448
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
2449
+ principalId: functionApp.identity.principalId
2450
+ principalType: 'ServicePrincipal'
2451
+ }
2452
+ }
2453
+
2454
+ output id string = functionApp.id
2455
+ output name string = functionApp.name
2456
+ output defaultHostname string = functionApp.properties.defaultHostName
2457
+ output principalId string = functionApp.identity.principalId
1676
2458
  `;
1677
2459
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
1678
2460
  // modules/cosmosdb-freetier.bicep (Free Tier)
1679
- const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
1680
- param accountName string
1681
-
1682
- @description('Database name')
1683
- param databaseName string
1684
-
1685
- @description('Location for Cosmos DB')
1686
- param location string
1687
-
1688
- @description('Public network access')
1689
- @allowed(['Enabled', 'Disabled'])
1690
- param publicNetworkAccess string = 'Enabled'
1691
-
1692
- // Cosmos DB Account (Free Tier)
1693
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
1694
- name: accountName
1695
- location: location
1696
- kind: 'GlobalDocumentDB'
1697
- properties: {
1698
- databaseAccountOfferType: 'Standard'
1699
- enableAutomaticFailover: false
1700
- enableFreeTier: true
1701
- publicNetworkAccess: publicNetworkAccess
1702
- disableLocalAuth: true
1703
- consistencyPolicy: {
1704
- defaultConsistencyLevel: 'Session'
1705
- }
1706
- locations: [
1707
- {
1708
- locationName: location
1709
- failoverPriority: 0
1710
- isZoneRedundant: false
1711
- }
1712
- ]
1713
- disableKeyBasedMetadataWriteAccess: true
1714
- }
1715
- }
1716
-
1717
- // Cosmos DB Database
1718
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
1719
- parent: cosmosAccount
1720
- name: databaseName
1721
- properties: {
1722
- resource: {
1723
- id: databaseName
1724
- }
1725
- options: {
1726
- throughput: 1000
1727
- }
1728
- }
1729
- }
1730
-
1731
- output id string = cosmosAccount.id
1732
- output accountName string = cosmosAccount.name
1733
- output endpoint string = cosmosAccount.properties.documentEndpoint
1734
- output databaseName string = database.name
2461
+ const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
2462
+ param accountName string
2463
+
2464
+ @description('Database name')
2465
+ param databaseName string
2466
+
2467
+ @description('Location for Cosmos DB')
2468
+ param location string
2469
+
2470
+ @description('Public network access')
2471
+ @allowed(['Enabled', 'Disabled'])
2472
+ param publicNetworkAccess string = 'Enabled'
2473
+
2474
+ // Cosmos DB Account (Free Tier)
2475
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
2476
+ name: accountName
2477
+ location: location
2478
+ kind: 'GlobalDocumentDB'
2479
+ properties: {
2480
+ databaseAccountOfferType: 'Standard'
2481
+ enableAutomaticFailover: false
2482
+ enableFreeTier: true
2483
+ publicNetworkAccess: publicNetworkAccess
2484
+ disableLocalAuth: true
2485
+ consistencyPolicy: {
2486
+ defaultConsistencyLevel: 'Session'
2487
+ }
2488
+ locations: [
2489
+ {
2490
+ locationName: location
2491
+ failoverPriority: 0
2492
+ isZoneRedundant: false
2493
+ }
2494
+ ]
2495
+ disableKeyBasedMetadataWriteAccess: true
2496
+ }
2497
+ }
2498
+
2499
+ // Cosmos DB Database
2500
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
2501
+ parent: cosmosAccount
2502
+ name: databaseName
2503
+ properties: {
2504
+ resource: {
2505
+ id: databaseName
2506
+ }
2507
+ options: {
2508
+ throughput: 1000
2509
+ }
2510
+ }
2511
+ }
2512
+
2513
+ output id string = cosmosAccount.id
2514
+ output accountName string = cosmosAccount.name
2515
+ output endpoint string = cosmosAccount.properties.documentEndpoint
2516
+ output databaseName string = database.name
1735
2517
  `;
1736
2518
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
1737
2519
  // modules/cosmosdb-serverless.bicep (Serverless)
1738
- const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
1739
- param accountName string
1740
-
1741
- @description('Database name')
1742
- param databaseName string
1743
-
1744
- @description('Location for Cosmos DB')
1745
- param location string
1746
-
1747
- @description('Public network access')
1748
- @allowed(['Enabled', 'Disabled'])
1749
- param publicNetworkAccess string = 'Enabled'
1750
-
1751
- // Cosmos DB Account (Serverless)
1752
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
1753
- name: accountName
1754
- location: location
1755
- kind: 'GlobalDocumentDB'
1756
- properties: {
1757
- databaseAccountOfferType: 'Standard'
1758
- enableAutomaticFailover: false
1759
- publicNetworkAccess: publicNetworkAccess
1760
- disableLocalAuth: true
1761
- consistencyPolicy: {
1762
- defaultConsistencyLevel: 'Session'
1763
- }
1764
- locations: [
1765
- {
1766
- locationName: location
1767
- failoverPriority: 0
1768
- isZoneRedundant: false
1769
- }
1770
- ]
1771
- capabilities: [
1772
- {
1773
- name: 'EnableServerless'
1774
- }
1775
- ]
1776
- disableKeyBasedMetadataWriteAccess: true
1777
- }
1778
- }
1779
-
1780
- // Cosmos DB Database
1781
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
1782
- parent: cosmosAccount
1783
- name: databaseName
1784
- properties: {
1785
- resource: {
1786
- id: databaseName
1787
- }
1788
- }
1789
- }
1790
-
1791
- output id string = cosmosAccount.id
1792
- output accountName string = cosmosAccount.name
1793
- output endpoint string = cosmosAccount.properties.documentEndpoint
1794
- output databaseName string = database.name
2520
+ const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
2521
+ param accountName string
2522
+
2523
+ @description('Database name')
2524
+ param databaseName string
2525
+
2526
+ @description('Location for Cosmos DB')
2527
+ param location string
2528
+
2529
+ @description('Public network access')
2530
+ @allowed(['Enabled', 'Disabled'])
2531
+ param publicNetworkAccess string = 'Enabled'
2532
+
2533
+ // Cosmos DB Account (Serverless)
2534
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
2535
+ name: accountName
2536
+ location: location
2537
+ kind: 'GlobalDocumentDB'
2538
+ properties: {
2539
+ databaseAccountOfferType: 'Standard'
2540
+ enableAutomaticFailover: false
2541
+ publicNetworkAccess: publicNetworkAccess
2542
+ disableLocalAuth: true
2543
+ consistencyPolicy: {
2544
+ defaultConsistencyLevel: 'Session'
2545
+ }
2546
+ locations: [
2547
+ {
2548
+ locationName: location
2549
+ failoverPriority: 0
2550
+ isZoneRedundant: false
2551
+ }
2552
+ ]
2553
+ capabilities: [
2554
+ {
2555
+ name: 'EnableServerless'
2556
+ }
2557
+ ]
2558
+ disableKeyBasedMetadataWriteAccess: true
2559
+ }
2560
+ }
2561
+
2562
+ // Cosmos DB Database
2563
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
2564
+ parent: cosmosAccount
2565
+ name: databaseName
2566
+ properties: {
2567
+ resource: {
2568
+ id: databaseName
2569
+ }
2570
+ }
2571
+ }
2572
+
2573
+ output id string = cosmosAccount.id
2574
+ output accountName string = cosmosAccount.name
2575
+ output endpoint string = cosmosAccount.properties.documentEndpoint
2576
+ output databaseName string = database.name
1795
2577
  `;
1796
2578
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-serverless.bicep'), cosmosDbServerlessBicep);
1797
2579
  // modules/cosmosdb-role-assignment.bicep (Role Assignment Module)
1798
- const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
1799
- param cosmosAccountName string
1800
-
1801
- @description('Functions App Managed Identity Principal ID')
1802
- param functionsPrincipalId string
1803
-
1804
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
1805
- name: cosmosAccountName
1806
- }
1807
-
1808
- // Built-in Cosmos DB Data Contributor role definition
1809
- var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
1810
-
1811
- // Role assignment for Functions to access Cosmos DB
1812
- resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
1813
- parent: cosmosAccount
1814
- name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
1815
- properties: {
1816
- roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
1817
- principalId: functionsPrincipalId
1818
- scope: cosmosAccount.id
1819
- }
1820
- }
1821
-
1822
- output roleAssignmentId string = roleAssignment.id
2580
+ const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
2581
+ param cosmosAccountName string
2582
+
2583
+ @description('Functions App Managed Identity Principal ID')
2584
+ param functionsPrincipalId string
2585
+
2586
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
2587
+ name: cosmosAccountName
2588
+ }
2589
+
2590
+ // Built-in Cosmos DB Data Contributor role definition
2591
+ var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
2592
+
2593
+ // Role assignment for Functions to access Cosmos DB
2594
+ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
2595
+ parent: cosmosAccount
2596
+ name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
2597
+ properties: {
2598
+ roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
2599
+ principalId: functionsPrincipalId
2600
+ scope: cosmosAccount.id
2601
+ }
2602
+ }
2603
+
2604
+ output roleAssignmentId string = roleAssignment.id
1823
2605
  `;
1824
2606
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
1825
2607
  // VNet modules (only generate if VNet is enabled)
1826
2608
  if (enableVNet) {
1827
2609
  // modules/vnet.bicep
1828
- const vnetBicep = `@description('VNet name')
1829
- param name string
1830
-
1831
- @description('Location for VNet')
1832
- param location string
1833
-
1834
- resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
1835
- name: name
1836
- location: location
1837
- properties: {
1838
- addressSpace: {
1839
- addressPrefixes: [
1840
- '10.0.0.0/16'
1841
- ]
1842
- }
1843
- subnets: [
1844
- {
1845
- name: 'snet-functions'
1846
- properties: {
1847
- addressPrefix: '10.0.1.0/24'
1848
- delegations: [
1849
- {
1850
- name: 'delegation'
1851
- properties: {
1852
- serviceName: 'Microsoft.App/environments'
1853
- }
1854
- }
1855
- ]
1856
- }
1857
- }
1858
- {
1859
- name: 'snet-private-endpoints'
1860
- properties: {
1861
- addressPrefix: '10.0.2.0/24'
1862
- privateEndpointNetworkPolicies: 'Disabled'
1863
- }
1864
- }
1865
- ]
1866
- }
1867
- }
1868
-
1869
- output id string = vnet.id
1870
- output name string = vnet.name
1871
- output functionsSubnetId string = vnet.properties.subnets[0].id
1872
- output privateEndpointSubnetId string = vnet.properties.subnets[1].id
2610
+ const vnetBicep = `@description('VNet name')
2611
+ param name string
2612
+
2613
+ @description('Location for VNet')
2614
+ param location string
2615
+
2616
+ resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
2617
+ name: name
2618
+ location: location
2619
+ properties: {
2620
+ addressSpace: {
2621
+ addressPrefixes: [
2622
+ '10.0.0.0/16'
2623
+ ]
2624
+ }
2625
+ subnets: [
2626
+ {
2627
+ name: 'snet-functions'
2628
+ properties: {
2629
+ addressPrefix: '10.0.1.0/24'
2630
+ delegations: [
2631
+ {
2632
+ name: 'delegation'
2633
+ properties: {
2634
+ serviceName: 'Microsoft.App/environments'
2635
+ }
2636
+ }
2637
+ ]
2638
+ }
2639
+ }
2640
+ {
2641
+ name: 'snet-private-endpoints'
2642
+ properties: {
2643
+ addressPrefix: '10.0.2.0/24'
2644
+ privateEndpointNetworkPolicies: 'Disabled'
2645
+ }
2646
+ }
2647
+ ]
2648
+ }
2649
+ }
2650
+
2651
+ output id string = vnet.id
2652
+ output name string = vnet.name
2653
+ output functionsSubnetId string = vnet.properties.subnets[0].id
2654
+ output privateEndpointSubnetId string = vnet.properties.subnets[1].id
1873
2655
  `;
1874
2656
  fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
1875
2657
  // modules/private-endpoint-cosmos.bicep
1876
- const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
1877
- param name string
1878
-
1879
- @description('Location')
1880
- param location string
1881
-
1882
- @description('Cosmos DB account resource ID')
1883
- param cosmosAccountId string
1884
-
1885
- @description('Cosmos DB account name')
1886
- param cosmosAccountName string
1887
-
1888
- @description('Subnet ID for private endpoint')
1889
- param subnetId string
1890
-
1891
- @description('VNet ID for DNS zone link')
1892
- param vnetId string
1893
-
1894
- // Private DNS Zone for Cosmos DB
1895
- resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
1896
- name: 'privatelink.documents.azure.com'
1897
- location: 'global'
1898
- }
1899
-
1900
- // Link DNS Zone to VNet
1901
- resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
1902
- parent: privateDnsZone
1903
- name: '\${cosmosAccountName}-vnet-link'
1904
- location: 'global'
1905
- properties: {
1906
- virtualNetwork: {
1907
- id: vnetId
1908
- }
1909
- registrationEnabled: false
1910
- }
1911
- }
1912
-
1913
- // Private Endpoint for Cosmos DB
1914
- resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
1915
- name: name
1916
- location: location
1917
- properties: {
1918
- subnet: {
1919
- id: subnetId
1920
- }
1921
- privateLinkServiceConnections: [
1922
- {
1923
- name: '\${cosmosAccountName}-connection'
1924
- properties: {
1925
- privateLinkServiceId: cosmosAccountId
1926
- groupIds: [
1927
- 'Sql'
1928
- ]
1929
- }
1930
- }
1931
- ]
1932
- }
1933
- }
1934
-
1935
- // DNS Zone Group
1936
- resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
1937
- parent: privateEndpoint
1938
- name: 'default'
1939
- properties: {
1940
- privateDnsZoneConfigs: [
1941
- {
1942
- name: 'cosmos-dns-config'
1943
- properties: {
1944
- privateDnsZoneId: privateDnsZone.id
1945
- }
1946
- }
1947
- ]
1948
- }
1949
- }
1950
-
1951
- output privateEndpointId string = privateEndpoint.id
1952
- output privateDnsZoneId string = privateDnsZone.id
2658
+ const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2659
+ param name string
2660
+
2661
+ @description('Location')
2662
+ param location string
2663
+
2664
+ @description('Cosmos DB account resource ID')
2665
+ param cosmosAccountId string
2666
+
2667
+ @description('Cosmos DB account name')
2668
+ param cosmosAccountName string
2669
+
2670
+ @description('Subnet ID for private endpoint')
2671
+ param subnetId string
2672
+
2673
+ @description('VNet ID for DNS zone link')
2674
+ param vnetId string
2675
+
2676
+ // Private DNS Zone for Cosmos DB
2677
+ resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
2678
+ name: 'privatelink.documents.azure.com'
2679
+ location: 'global'
2680
+ }
2681
+
2682
+ // Link DNS Zone to VNet
2683
+ resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
2684
+ parent: privateDnsZone
2685
+ name: '\${cosmosAccountName}-vnet-link'
2686
+ location: 'global'
2687
+ properties: {
2688
+ virtualNetwork: {
2689
+ id: vnetId
2690
+ }
2691
+ registrationEnabled: false
2692
+ }
2693
+ }
2694
+
2695
+ // Private Endpoint for Cosmos DB
2696
+ resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
2697
+ name: name
2698
+ location: location
2699
+ properties: {
2700
+ subnet: {
2701
+ id: subnetId
2702
+ }
2703
+ privateLinkServiceConnections: [
2704
+ {
2705
+ name: '\${cosmosAccountName}-connection'
2706
+ properties: {
2707
+ privateLinkServiceId: cosmosAccountId
2708
+ groupIds: [
2709
+ 'Sql'
2710
+ ]
2711
+ }
2712
+ }
2713
+ ]
2714
+ }
2715
+ }
2716
+
2717
+ // DNS Zone Group
2718
+ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
2719
+ parent: privateEndpoint
2720
+ name: 'default'
2721
+ properties: {
2722
+ privateDnsZoneConfigs: [
2723
+ {
2724
+ name: 'cosmos-dns-config'
2725
+ properties: {
2726
+ privateDnsZoneId: privateDnsZone.id
2727
+ }
2728
+ }
2729
+ ]
2730
+ }
2731
+ }
2732
+
2733
+ output privateEndpointId string = privateEndpoint.id
2734
+ output privateDnsZoneId string = privateDnsZone.id
1953
2735
  `;
1954
2736
  fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
1955
2737
  console.log('✅ VNet modules created\n');
1956
2738
  }
1957
2739
  console.log('✅ Infrastructure files created\n');
1958
2740
  }
1959
- async function createGitHubActionsWorkflows(projectDir, azureConfig) {
2741
+ function getGitHubFunctionsWorkflow(pm, backendLanguage) {
2742
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
2743
+ const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
2744
+ const commonSetup = ` - uses: actions/checkout@v4
2745
+
2746
+ - name: Setup Node.js
2747
+ uses: actions/setup-node@v4
2748
+ with:
2749
+ node-version: '22'
2750
+ ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2751
+ - name: Install dependencies
2752
+ run: |
2753
+ ${pmCmd.ci}
2754
+
2755
+ - name: Build shared package
2756
+ run: |
2757
+ ${pmCmd.runFilter('shared')} build
2758
+ `;
2759
+ if (backendLanguage === 'typescript') {
2760
+ return `name: Deploy Azure Functions
2761
+
2762
+ on:
2763
+ push:
2764
+ branches:
2765
+ - main
2766
+ paths:
2767
+ - 'functions/**'
2768
+ - 'shared/**'
2769
+ pull_request:
2770
+ branches:
2771
+ - main
2772
+ paths:
2773
+ - 'functions/**'
2774
+ - 'shared/**'
2775
+ workflow_dispatch:
2776
+
2777
+ jobs:
2778
+ build-and-deploy:
2779
+ runs-on: ubuntu-latest
2780
+ name: Build and Deploy Functions
2781
+
2782
+ steps:
2783
+ ${commonSetup} - name: Build Functions
2784
+ run: |
2785
+ ${pmCmd.runFilter('functions')} build
2786
+
2787
+ - name: Prepare functions for deployment
2788
+ run: |
2789
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2790
+ mkdir -p /tmp/fn-deps
2791
+ node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2792
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2793
+ rm -rf ./functions/node_modules
2794
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
2795
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2796
+ mkdir -p "$SHARED_DEST"
2797
+ cp -r ./shared/dist "$SHARED_DEST/dist"
2798
+ cp ./shared/package.json "$SHARED_DEST/package.json"
2799
+
2800
+ - name: Deploy to Azure Functions
2801
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2802
+ uses: Azure/functions-action@v1
2803
+ with:
2804
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2805
+ package: './functions'
2806
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2807
+ sku: flexconsumption
2808
+ `;
2809
+ }
2810
+ if (backendLanguage === 'csharp') {
2811
+ return `name: Deploy Azure Functions
2812
+
2813
+ on:
2814
+ push:
2815
+ branches:
2816
+ - main
2817
+ paths:
2818
+ - 'functions/**'
2819
+ - 'shared/**'
2820
+ pull_request:
2821
+ branches:
2822
+ - main
2823
+ paths:
2824
+ - 'functions/**'
2825
+ - 'shared/**'
2826
+ workflow_dispatch:
2827
+
2828
+ jobs:
2829
+ build-and-deploy:
2830
+ runs-on: ubuntu-latest
2831
+ name: Build and Deploy Functions
2832
+
2833
+ steps:
2834
+ ${commonSetup} - name: Setup .NET
2835
+ uses: actions/setup-dotnet@v4
2836
+ with:
2837
+ dotnet-version: '10.0.x'
2838
+
2839
+ - name: Publish Functions
2840
+ run: |
2841
+ dotnet publish ./functions -c Release -o ./functions/publish
2842
+
2843
+ - name: Deploy to Azure Functions
2844
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2845
+ uses: Azure/functions-action@v1
2846
+ with:
2847
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2848
+ package: './functions/publish'
2849
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2850
+ sku: flexconsumption
2851
+ `;
2852
+ }
2853
+ return `name: Deploy Azure Functions
2854
+
2855
+ on:
2856
+ push:
2857
+ branches:
2858
+ - main
2859
+ paths:
2860
+ - 'functions/**'
2861
+ - 'shared/**'
2862
+ pull_request:
2863
+ branches:
2864
+ - main
2865
+ paths:
2866
+ - 'functions/**'
2867
+ - 'shared/**'
2868
+ workflow_dispatch:
2869
+
2870
+ jobs:
2871
+ build-and-deploy:
2872
+ runs-on: ubuntu-latest
2873
+ name: Build and Deploy Functions
2874
+
2875
+ steps:
2876
+ ${commonSetup} - name: Setup Python
2877
+ uses: actions/setup-python@v5
2878
+ with:
2879
+ python-version: '3.11'
2880
+
2881
+ - name: Install Functions dependencies
2882
+ run: |
2883
+ python -m pip install --upgrade pip
2884
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
2885
+
2886
+ - name: Deploy to Azure Functions
2887
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2888
+ uses: Azure/functions-action@v1
2889
+ with:
2890
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2891
+ package: './functions'
2892
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2893
+ sku: flexconsumption
2894
+ `;
2895
+ }
2896
+ function getAzureFunctionsPipeline(pm, backendLanguage) {
2897
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
2898
+ const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2899
+ const commonSetup = ` - task: NodeTool@0
2900
+ inputs:
2901
+ versionSpec: '22.x'
2902
+ displayName: 'Install Node.js'
2903
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2904
+ - script: |
2905
+ ${pmCmd.ci}
2906
+ displayName: 'Install workspace dependencies'
2907
+
2908
+ - script: |
2909
+ ${pmCmd.runFilter('shared')} build
2910
+ displayName: 'Build shared package'
2911
+ `;
2912
+ if (backendLanguage === 'typescript') {
2913
+ return `trigger:
2914
+ branches:
2915
+ include:
2916
+ - main
2917
+ paths:
2918
+ include:
2919
+ - functions/**
2920
+ - shared/**
2921
+
2922
+ pr:
2923
+ branches:
2924
+ include:
2925
+ - main
2926
+ paths:
2927
+ include:
2928
+ - functions/**
2929
+ - shared/**
2930
+
2931
+ pool:
2932
+ vmImage: 'ubuntu-latest'
2933
+
2934
+ variables:
2935
+ - group: azure-deployment
2936
+
2937
+ steps:
2938
+ ${commonSetup} - script: |
2939
+ ${pmCmd.runFilter('functions')} build
2940
+ displayName: 'Build Functions'
2941
+
2942
+ - script: |
2943
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2944
+ mkdir -p /tmp/fn-deps
2945
+ node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2946
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2947
+ rm -rf ./functions/node_modules
2948
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
2949
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2950
+ mkdir -p "$SHARED_DEST"
2951
+ cp -r ./shared/dist "$SHARED_DEST/dist"
2952
+ cp ./shared/package.json "$SHARED_DEST/package.json"
2953
+ displayName: 'Prepare functions for deployment'
2954
+
2955
+ - task: ArchiveFiles@2
2956
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2957
+ inputs:
2958
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
2959
+ includeRootFolder: false
2960
+ archiveType: 'zip'
2961
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2962
+ displayName: 'Archive Functions'
2963
+
2964
+ - task: PublishBuildArtifacts@1
2965
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2966
+ inputs:
2967
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
2968
+ ArtifactName: 'functions'
2969
+ displayName: 'Publish Functions artifact'
2970
+
2971
+ - task: AzureFunctionApp@2
2972
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2973
+ inputs:
2974
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
2975
+ appType: 'functionAppLinux'
2976
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
2977
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2978
+ displayName: 'Deploy to Azure Functions'
2979
+ `;
2980
+ }
2981
+ if (backendLanguage === 'csharp') {
2982
+ return `trigger:
2983
+ branches:
2984
+ include:
2985
+ - main
2986
+ paths:
2987
+ include:
2988
+ - functions/**
2989
+ - shared/**
2990
+
2991
+ pr:
2992
+ branches:
2993
+ include:
2994
+ - main
2995
+ paths:
2996
+ include:
2997
+ - functions/**
2998
+ - shared/**
2999
+
3000
+ pool:
3001
+ vmImage: 'ubuntu-latest'
3002
+
3003
+ variables:
3004
+ - group: azure-deployment
3005
+
3006
+ steps:
3007
+ ${commonSetup} - task: UseDotNet@2
3008
+ inputs:
3009
+ version: '10.0.x'
3010
+ displayName: 'Install .NET SDK'
3011
+
3012
+ - script: |
3013
+ dotnet publish ./functions -c Release -o ./functions/publish
3014
+ displayName: 'Publish Functions'
3015
+
3016
+ - task: ArchiveFiles@2
3017
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3018
+ inputs:
3019
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions/publish'
3020
+ includeRootFolder: false
3021
+ archiveType: 'zip'
3022
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
3023
+ displayName: 'Archive Functions'
3024
+
3025
+ - task: AzureFunctionApp@2
3026
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3027
+ inputs:
3028
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
3029
+ appType: 'functionAppLinux'
3030
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
3031
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
3032
+ displayName: 'Deploy to Azure Functions'
3033
+ `;
3034
+ }
3035
+ return `trigger:
3036
+ branches:
3037
+ include:
3038
+ - main
3039
+ paths:
3040
+ include:
3041
+ - functions/**
3042
+ - shared/**
3043
+
3044
+ pr:
3045
+ branches:
3046
+ include:
3047
+ - main
3048
+ paths:
3049
+ include:
3050
+ - functions/**
3051
+ - shared/**
3052
+
3053
+ pool:
3054
+ vmImage: 'ubuntu-latest'
3055
+
3056
+ variables:
3057
+ - group: azure-deployment
3058
+
3059
+ steps:
3060
+ ${commonSetup} - task: UsePythonVersion@0
3061
+ inputs:
3062
+ versionSpec: '3.11'
3063
+ displayName: 'Install Python'
3064
+
3065
+ - script: |
3066
+ python -m pip install --upgrade pip
3067
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
3068
+ displayName: 'Install Functions dependencies'
3069
+
3070
+ - task: ArchiveFiles@2
3071
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3072
+ inputs:
3073
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
3074
+ includeRootFolder: false
3075
+ archiveType: 'zip'
3076
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
3077
+ displayName: 'Archive Functions'
3078
+
3079
+ - task: AzureFunctionApp@2
3080
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3081
+ inputs:
3082
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
3083
+ appType: 'functionAppLinux'
3084
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
3085
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
3086
+ displayName: 'Deploy to Azure Functions'
3087
+ `;
3088
+ }
3089
+ async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
1960
3090
  console.log('📦 Creating GitHub Actions workflows...\n');
1961
3091
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
1962
3092
  fs.mkdirSync(workflowsDir, { recursive: true });
1963
3093
  // deploy-swa.yml
1964
- const swaWorkflow = `name: Deploy Static Web App
1965
-
1966
- on:
1967
- push:
1968
- branches:
1969
- - main
1970
- paths:
1971
- - 'app/**'
1972
- - 'components/**'
1973
- - 'lib/**'
1974
- - 'shared/**'
1975
- - 'public/**'
1976
- - 'package.json'
1977
- - 'next.config.js'
1978
- - 'next.config.ts'
1979
- workflow_dispatch:
1980
- pull_request:
1981
- branches:
1982
- - main
1983
- paths:
1984
- - 'app/**'
1985
- - 'components/**'
1986
- - 'lib/**'
1987
- - 'shared/**'
1988
- - 'public/**'
1989
- - 'package.json'
1990
- - 'next.config.js'
1991
- - 'next.config.ts'
1992
-
1993
- jobs:
1994
- build-and-deploy:
1995
- runs-on: ubuntu-latest
1996
- name: Build and Deploy Static Web App
1997
-
1998
- steps:
1999
- - uses: actions/checkout@v4
2000
- with:
2001
- submodules: true
2002
-
2003
- - name: Deploy to Azure Static Web Apps
2004
- if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2005
- uses: Azure/static-web-apps-deploy@v1
2006
- with:
2007
- azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
2008
- repo_token: \${{ secrets.GITHUB_TOKEN }}
2009
- action: 'upload'
2010
- app_location: '/'
2011
- api_location: ''
2012
- output_location: ''
2013
- env:
2014
- NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3094
+ const swaWorkflow = `name: Deploy Static Web App
3095
+
3096
+ on:
3097
+ push:
3098
+ branches:
3099
+ - main
3100
+ paths:
3101
+ - 'app/**'
3102
+ - 'components/**'
3103
+ - 'lib/**'
3104
+ - 'shared/**'
3105
+ - 'public/**'
3106
+ - 'package.json'
3107
+ - 'next.config.js'
3108
+ - 'next.config.ts'
3109
+ workflow_dispatch:
3110
+ pull_request:
3111
+ branches:
3112
+ - main
3113
+ paths:
3114
+ - 'app/**'
3115
+ - 'components/**'
3116
+ - 'lib/**'
3117
+ - 'shared/**'
3118
+ - 'public/**'
3119
+ - 'package.json'
3120
+ - 'next.config.js'
3121
+ - 'next.config.ts'
3122
+
3123
+ jobs:
3124
+ build-and-deploy:
3125
+ runs-on: ubuntu-latest
3126
+ name: Build and Deploy Static Web App
3127
+
3128
+ steps:
3129
+ - uses: actions/checkout@v4
3130
+ with:
3131
+ submodules: true
3132
+
3133
+ - name: Deploy to Azure Static Web Apps
3134
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
3135
+ uses: Azure/static-web-apps-deploy@v1
3136
+ with:
3137
+ azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
3138
+ repo_token: \${{ secrets.GITHUB_TOKEN }}
3139
+ action: 'upload'
3140
+ app_location: '/'
3141
+ api_location: ''
3142
+ output_location: ''
3143
+ env:
3144
+ NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
2015
3145
  `;
2016
3146
  fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
2017
3147
  // deploy-functions.yml
2018
- const functionsWorkflow = `name: Deploy Azure Functions
2019
-
2020
- on:
2021
- push:
2022
- branches:
2023
- - main
2024
- paths:
2025
- - 'functions/**'
2026
- - 'shared/**'
2027
- pull_request:
2028
- branches:
2029
- - main
2030
- paths:
2031
- - 'functions/**'
2032
- - 'shared/**'
2033
- workflow_dispatch:
2034
-
2035
- jobs:
2036
- build-and-deploy:
2037
- runs-on: ubuntu-latest
2038
- name: Build and Deploy Functions
2039
-
2040
- steps:
2041
- - uses: actions/checkout@v4
2042
-
2043
- - name: Setup Node.js
2044
- uses: actions/setup-node@v4
2045
- with:
2046
- node-version: '22'
2047
-
2048
- - name: Install dependencies
2049
- run: |
2050
- npm ci
2051
-
2052
- - name: Build shared package
2053
- run: |
2054
- npm run build -w shared
2055
-
2056
- - name: Build Functions
2057
- run: |
2058
- npm run build -w functions
2059
-
2060
- - name: Prepare functions for deployment
2061
- run: |
2062
- SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2063
- mkdir -p /tmp/fn-deps
2064
- node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2065
- cd /tmp/fn-deps && npm install --omit=dev && cd -
2066
- rm -rf ./functions/node_modules
2067
- mv /tmp/fn-deps/node_modules ./functions/node_modules
2068
- SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2069
- mkdir -p "$SHARED_DEST"
2070
- cp -r ./shared/dist "$SHARED_DEST/dist"
2071
- cp ./shared/package.json "$SHARED_DEST/package.json"
2072
-
2073
- - name: Deploy to Azure Functions
2074
- if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2075
- uses: Azure/functions-action@v1
2076
- with:
2077
- app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2078
- package: './functions'
2079
- publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2080
- sku: flexconsumption
2081
- `;
3148
+ const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
2082
3149
  fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
2083
3150
  console.log('✅ GitHub Actions workflows created\n');
2084
3151
  }
2085
- async function createAzurePipelines(projectDir) {
3152
+ async function createAzurePipelines(projectDir, pm, backendLanguage) {
2086
3153
  console.log('📦 Creating Azure Pipelines...\n');
3154
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
3155
+ const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2087
3156
  const pipelinesDir = path.join(projectDir, 'pipelines');
2088
3157
  fs.mkdirSync(pipelinesDir, { recursive: true });
2089
3158
  // swa.yml
2090
- const swaPipeline = `trigger:
2091
- branches:
2092
- include:
2093
- - main
2094
- paths:
2095
- include:
2096
- - app/**
2097
- - components/**
2098
- - lib/**
2099
- - shared/**
2100
- - public/**
2101
- - package.json
2102
- - next.config.js
2103
-
2104
- pr:
2105
- branches:
2106
- include:
2107
- - main
2108
- paths:
2109
- include:
2110
- - app/**
2111
- - components/**
2112
- - lib/**
2113
- - shared/**
2114
- - public/**
2115
- - package.json
2116
- - next.config.js
2117
-
2118
- pool:
2119
- vmImage: 'ubuntu-latest'
2120
-
2121
- variables:
2122
- - group: azure-deployment
2123
-
2124
- steps:
2125
- - task: NodeTool@0
2126
- inputs:
2127
- versionSpec: '22.x'
2128
- displayName: 'Install Node.js'
2129
-
2130
- - script: |
2131
- npm ci
2132
- displayName: 'Install dependencies'
2133
-
2134
- - script: |
2135
- npm run build
2136
- env:
2137
- NODE_ENV: production
2138
- displayName: 'Build Next.js app'
2139
-
2140
- - task: AzureStaticWebApp@0
2141
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2142
- inputs:
2143
- app_location: '.'
2144
- output_location: '.next/standalone'
2145
- skip_app_build: true
2146
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
2147
- displayName: 'Deploy to Azure Static Web Apps'
3159
+ const swaPipeline = `trigger:
3160
+ branches:
3161
+ include:
3162
+ - main
3163
+ paths:
3164
+ include:
3165
+ - app/**
3166
+ - components/**
3167
+ - lib/**
3168
+ - shared/**
3169
+ - public/**
3170
+ - package.json
3171
+ - next.config.js
3172
+
3173
+ pr:
3174
+ branches:
3175
+ include:
3176
+ - main
3177
+ paths:
3178
+ include:
3179
+ - app/**
3180
+ - components/**
3181
+ - lib/**
3182
+ - shared/**
3183
+ - public/**
3184
+ - package.json
3185
+ - next.config.js
3186
+
3187
+ pool:
3188
+ vmImage: 'ubuntu-latest'
3189
+
3190
+ variables:
3191
+ - group: azure-deployment
3192
+
3193
+ steps:
3194
+ - task: NodeTool@0
3195
+ inputs:
3196
+ versionSpec: '22.x'
3197
+ displayName: 'Install Node.js'
3198
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3199
+ - script: |
3200
+ ${pmCmd.ci}
3201
+ displayName: 'Install dependencies'
3202
+
3203
+ - script: |
3204
+ ${pmCmd.run} build
3205
+ env:
3206
+ NODE_ENV: production
3207
+ displayName: 'Build Next.js app'
3208
+
3209
+ - task: AzureStaticWebApp@0
3210
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3211
+ inputs:
3212
+ app_location: '.'
3213
+ output_location: '.next/standalone'
3214
+ skip_app_build: true
3215
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3216
+ displayName: 'Deploy to Azure Static Web Apps'
2148
3217
  `;
2149
3218
  fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
2150
3219
  // functions.yml
2151
- const functionsPipeline = `trigger:
2152
- branches:
2153
- include:
2154
- - main
2155
- paths:
2156
- include:
2157
- - functions/**
2158
- - shared/**
2159
-
2160
- pr:
2161
- branches:
2162
- include:
2163
- - main
2164
- paths:
2165
- include:
2166
- - functions/**
2167
- - shared/**
2168
-
2169
- pool:
2170
- vmImage: 'ubuntu-latest'
2171
-
2172
- variables:
2173
- - group: azure-deployment
2174
-
2175
- steps:
2176
- - task: NodeTool@0
2177
- inputs:
2178
- versionSpec: '22.x'
2179
- displayName: 'Install Node.js'
2180
-
2181
- - script: |
2182
- npm ci
2183
- displayName: 'Install workspace dependencies'
2184
-
2185
- - script: |
2186
- npm run build -w shared
2187
- displayName: 'Build shared package'
2188
-
2189
- - script: |
2190
- npm run build -w functions
2191
- displayName: 'Build Functions'
2192
-
2193
- - script: |
2194
- SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2195
- mkdir -p /tmp/fn-deps
2196
- node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2197
- cd /tmp/fn-deps && npm install --omit=dev && cd -
2198
- rm -rf ./functions/node_modules
2199
- mv /tmp/fn-deps/node_modules ./functions/node_modules
2200
- SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2201
- mkdir -p "$SHARED_DEST"
2202
- cp -r ./shared/dist "$SHARED_DEST/dist"
2203
- cp ./shared/package.json "$SHARED_DEST/package.json"
2204
- displayName: 'Prepare functions for deployment'
2205
-
2206
- - task: ArchiveFiles@2
2207
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2208
- inputs:
2209
- rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
2210
- includeRootFolder: false
2211
- archiveType: 'zip'
2212
- archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2213
- displayName: 'Archive Functions'
2214
-
2215
- - task: PublishBuildArtifacts@1
2216
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2217
- inputs:
2218
- PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
2219
- ArtifactName: 'functions'
2220
- displayName: 'Publish Functions artifact'
2221
-
2222
- - task: AzureFunctionApp@2
2223
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2224
- inputs:
2225
- azureSubscription: '$(AZURE_SUBSCRIPTION)'
2226
- appType: 'functionAppLinux'
2227
- appName: '$(AZURE_FUNCTIONAPP_NAME)'
2228
- package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2229
- displayName: 'Deploy to Azure Functions'
2230
- `;
3220
+ const functionsPipeline = getAzureFunctionsPipeline(pm, backendLanguage);
2231
3221
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
2232
3222
  console.log('✅ Azure Pipelines created\n');
2233
3223
  }