swallowkit 1.0.0-beta.2 → 1.0.0-beta.20

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