swallowkit 1.0.0-beta.4 → 1.0.0-beta.6

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