swallowkit 1.0.0-beta.3 → 1.0.0-beta.30

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