swallowkit 1.0.0-beta.4 → 1.0.0-beta.41

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