swallowkit 0.2.0-beta.1 → 0.3.0-beta.2

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 (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +183 -183
  3. package/README.md +184 -184
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.js +17 -14
  6. package/dist/cli/commands/build.js.map +1 -1
  7. package/dist/cli/commands/create-model.d.ts.map +1 -1
  8. package/dist/cli/commands/create-model.js +14 -11
  9. package/dist/cli/commands/create-model.js.map +1 -1
  10. package/dist/cli/commands/deploy.d.ts.map +1 -1
  11. package/dist/cli/commands/deploy.js +3 -0
  12. package/dist/cli/commands/deploy.js.map +1 -1
  13. package/dist/cli/commands/dev.d.ts.map +1 -1
  14. package/dist/cli/commands/dev.js +3 -0
  15. package/dist/cli/commands/dev.js.map +1 -1
  16. package/dist/cli/commands/index.d.ts +0 -2
  17. package/dist/cli/commands/index.d.ts.map +1 -1
  18. package/dist/cli/commands/index.js +1 -5
  19. package/dist/cli/commands/index.js.map +1 -1
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/init.js +1885 -1490
  22. package/dist/cli/commands/init.js.map +1 -1
  23. package/dist/cli/commands/provision.d.ts.map +1 -1
  24. package/dist/cli/commands/provision.js +3 -0
  25. package/dist/cli/commands/provision.js.map +1 -1
  26. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  27. package/dist/cli/commands/scaffold.js +131 -128
  28. package/dist/cli/commands/scaffold.js.map +1 -1
  29. package/dist/cli/commands/setup.d.ts +6 -0
  30. package/dist/cli/commands/setup.d.ts.map +1 -0
  31. package/dist/cli/commands/setup.js +224 -0
  32. package/dist/cli/commands/setup.js.map +1 -0
  33. package/dist/cli/index.js +0 -6
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/core/config.d.ts +13 -0
  36. package/dist/core/config.d.ts.map +1 -1
  37. package/dist/core/config.js +45 -0
  38. package/dist/core/config.js.map +1 -1
  39. package/dist/core/scaffold/functions-generator.js +351 -351
  40. package/dist/core/scaffold/model-parser.js +96 -96
  41. package/dist/core/scaffold/nextjs-generator.js +373 -373
  42. package/dist/core/scaffold/ui-generator.js +554 -554
  43. package/package.json +73 -73
@@ -76,9 +76,41 @@ async function promptAzureConfig() {
76
76
  ],
77
77
  initial: 0
78
78
  });
79
+ const functionsPlan = functionsResponse.plan || 'flex';
80
+ let vnetOption;
81
+ if (functionsPlan === 'premium') {
82
+ // Premium: 3 options including Full Private
83
+ const vnetResponse = await (0, prompts_1.default)({
84
+ type: 'select',
85
+ name: 'vnet',
86
+ message: 'Network security:',
87
+ choices: [
88
+ { title: 'Full Private (recommended) - Functions + Cosmos DB via Private Endpoints', value: 'full' },
89
+ { title: 'VNet Integration - Cosmos DB via Private Endpoint only', value: 'outbound' },
90
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
91
+ ],
92
+ initial: 0
93
+ });
94
+ vnetOption = vnetResponse.vnet || 'full';
95
+ }
96
+ else {
97
+ // Flex Consumption: 2 options (Full Private not supported)
98
+ const vnetResponse = await (0, prompts_1.default)({
99
+ type: 'select',
100
+ name: 'vnet',
101
+ message: 'Network security:',
102
+ choices: [
103
+ { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
104
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
105
+ ],
106
+ initial: 0
107
+ });
108
+ vnetOption = vnetResponse.vnet || 'outbound';
109
+ }
79
110
  return {
80
- functionsPlan: functionsResponse.plan || 'flex',
81
- cosmosDbMode: cosmosResponse.mode || 'freetier'
111
+ functionsPlan,
112
+ cosmosDbMode: cosmosResponse.mode || 'freetier',
113
+ vnetOption
82
114
  };
83
115
  }
84
116
  async function initCommand(options) {
@@ -243,8 +275,6 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig)
243
275
  ...packageJson.scripts,
244
276
  'build': 'next build --webpack && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/',
245
277
  'start': 'next start',
246
- 'build:azure': 'swallowkit build',
247
- 'deploy': 'swallowkit deploy',
248
278
  'functions:start': 'cd functions && npm start',
249
279
  };
250
280
  packageJson.engines = {
@@ -282,16 +312,16 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig)
282
312
  fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
283
313
  }
284
314
  // 3. Create SwallowKit config
285
- const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
286
- module.exports = {
287
- functions: {
288
- baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
289
- },
290
- deployment: {
291
- resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
292
- swaName: process.env.AZURE_SWA_NAME || '',
293
- },
294
- }
315
+ const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
316
+ module.exports = {
317
+ functions: {
318
+ baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
319
+ },
320
+ deployment: {
321
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
322
+ swaName: process.env.AZURE_SWA_NAME || '',
323
+ },
324
+ }
295
325
  `;
296
326
  fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
297
327
  // 4. Create lib directory for shared models
@@ -302,191 +332,191 @@ module.exports = {
302
332
  const apiLibDir = path.join(libDir, 'api');
303
333
  fs.mkdirSync(apiLibDir, { recursive: true });
304
334
  // Create backend utility for calling Azure Functions
305
- const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
306
- function getFunctionsBaseUrl(): string {
307
- return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
308
- }
309
-
310
- /**
311
- * Simple HTTP client for calling backend APIs
312
- * Use this to make requests to BFF API routes (which forward to Azure Functions)
313
- */
314
- async function request<T>(
315
- endpoint: string,
316
- method: 'GET' | 'POST' | 'PUT' | 'DELETE',
317
- body?: any,
318
- queryParams?: Record<string, string>
319
- ): Promise<T> {
320
- const functionsBaseUrl = getFunctionsBaseUrl();
321
- let url = \`\${functionsBaseUrl}\${endpoint}\`;
322
- if (queryParams) {
323
- const params = new URLSearchParams(queryParams);
324
- url += \`?\${params.toString()}\`;
325
- }
326
-
327
- try {
328
- const response = await fetch(url, {
329
- method,
330
- headers: {
331
- 'Content-Type': 'application/json',
332
- },
333
- body: body ? JSON.stringify(body) : undefined,
334
- });
335
-
336
- if (!response.ok) {
337
- const text = await response.text();
338
- let errorMessage = text || 'Failed to call backend function';
339
- try {
340
- const error = JSON.parse(text);
341
- errorMessage = error.error || error.message || text;
342
- } catch {
343
- // If not JSON, use text as-is
344
- }
345
- throw new Error(errorMessage);
346
- }
347
-
348
- const contentType = response.headers.get('content-type');
349
- if (!contentType?.includes('application/json')) {
350
- const text = await response.text();
351
- return text as T;
352
- }
353
-
354
- return await response.json();
355
- } catch (error) {
356
- console.error('Error calling backend:', error);
357
- throw error;
358
- }
359
- }
360
-
361
- /**
362
- * Generic API client for making HTTP requests
363
- * Simply calls endpoints - no DB dependencies, no schema validation
364
- * Validation happens on the backend (BFF/Functions)
365
- *
366
- * @example
367
- * // Call custom endpoint
368
- * await api.get('/api/greet?name=World')
369
- *
370
- * // Call scaffolded CRUD endpoints
371
- * await api.get('/api/todos')
372
- * await api.post('/api/todos', { title: 'New task' })
373
- * await api.put('/api/todos/123', { title: 'Updated' })
374
- * await api.delete('/api/todos/123')
375
- */
376
- export const api = {
377
- /**
378
- * Make a GET request
379
- */
380
- get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
381
- return request<T>(endpoint, 'GET', undefined, params);
382
- },
383
-
384
- /**
385
- * Make a POST request
386
- */
387
- post: <T>(endpoint: string, body?: any): Promise<T> => {
388
- return request<T>(endpoint, 'POST', body);
389
- },
390
-
391
- /**
392
- * Make a PUT request
393
- */
394
- put: <T>(endpoint: string, body?: any): Promise<T> => {
395
- return request<T>(endpoint, 'PUT', body);
396
- },
397
-
398
- /**
399
- * Make a DELETE request
400
- */
401
- delete: <T>(endpoint: string): Promise<T> => {
402
- return request<T>(endpoint, 'DELETE');
403
- },
404
- };
335
+ const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
336
+ function getFunctionsBaseUrl(): string {
337
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
338
+ }
339
+
340
+ /**
341
+ * Simple HTTP client for calling backend APIs
342
+ * Use this to make requests to BFF API routes (which forward to Azure Functions)
343
+ */
344
+ async function request<T>(
345
+ endpoint: string,
346
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
347
+ body?: any,
348
+ queryParams?: Record<string, string>
349
+ ): Promise<T> {
350
+ const functionsBaseUrl = getFunctionsBaseUrl();
351
+ let url = \`\${functionsBaseUrl}\${endpoint}\`;
352
+ if (queryParams) {
353
+ const params = new URLSearchParams(queryParams);
354
+ url += \`?\${params.toString()}\`;
355
+ }
356
+
357
+ try {
358
+ const response = await fetch(url, {
359
+ method,
360
+ headers: {
361
+ 'Content-Type': 'application/json',
362
+ },
363
+ body: body ? JSON.stringify(body) : undefined,
364
+ });
365
+
366
+ if (!response.ok) {
367
+ const text = await response.text();
368
+ let errorMessage = text || 'Failed to call backend function';
369
+ try {
370
+ const error = JSON.parse(text);
371
+ errorMessage = error.error || error.message || text;
372
+ } catch {
373
+ // If not JSON, use text as-is
374
+ }
375
+ throw new Error(errorMessage);
376
+ }
377
+
378
+ const contentType = response.headers.get('content-type');
379
+ if (!contentType?.includes('application/json')) {
380
+ const text = await response.text();
381
+ return text as T;
382
+ }
383
+
384
+ return await response.json();
385
+ } catch (error) {
386
+ console.error('Error calling backend:', error);
387
+ throw error;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Generic API client for making HTTP requests
393
+ * Simply calls endpoints - no DB dependencies, no schema validation
394
+ * Validation happens on the backend (BFF/Functions)
395
+ *
396
+ * @example
397
+ * // Call custom endpoint
398
+ * await api.get('/api/greet?name=World')
399
+ *
400
+ * // Call scaffolded CRUD endpoints
401
+ * await api.get('/api/todos')
402
+ * await api.post('/api/todos', { title: 'New task' })
403
+ * await api.put('/api/todos/123', { title: 'Updated' })
404
+ * await api.delete('/api/todos/123')
405
+ */
406
+ export const api = {
407
+ /**
408
+ * Make a GET request
409
+ */
410
+ get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
411
+ return request<T>(endpoint, 'GET', undefined, params);
412
+ },
413
+
414
+ /**
415
+ * Make a POST request
416
+ */
417
+ post: <T>(endpoint: string, body?: any): Promise<T> => {
418
+ return request<T>(endpoint, 'POST', body);
419
+ },
420
+
421
+ /**
422
+ * Make a PUT request
423
+ */
424
+ put: <T>(endpoint: string, body?: any): Promise<T> => {
425
+ return request<T>(endpoint, 'PUT', body);
426
+ },
427
+
428
+ /**
429
+ * Make a DELETE request
430
+ */
431
+ delete: <T>(endpoint: string): Promise<T> => {
432
+ return request<T>(endpoint, 'DELETE');
433
+ },
434
+ };
405
435
  `;
406
436
  fs.writeFileSync(path.join(apiLibDir, 'backend.ts'), backendUtilContent);
407
437
  // 5. Create components directory
408
438
  const componentsDir = path.join(projectDir, 'components');
409
439
  fs.mkdirSync(componentsDir, { recursive: true });
410
440
  // 6. Create .env.example
411
- const envExample = `# Azure Functions Backend URL
412
- FUNCTIONS_BASE_URL=http://localhost:7071
413
-
414
- # Azure Configuration
415
- AZURE_RESOURCE_GROUP=your-resource-group
416
- AZURE_SWA_NAME=your-static-web-app-name
441
+ const envExample = `# Azure Functions Backend URL
442
+ FUNCTIONS_BASE_URL=http://localhost:7071
443
+
444
+ # Azure Configuration
445
+ AZURE_RESOURCE_GROUP=your-resource-group
446
+ AZURE_SWA_NAME=your-static-web-app-name
417
447
  `;
418
448
  fs.writeFileSync(path.join(projectDir, '.env.example'), envExample);
419
449
  // 7. Create instrumentation.ts for Application Insights (Next.js official way)
420
- const instrumentationContent = `// Application Insights instrumentation for Next.js
421
- // This file is automatically loaded by Next.js when instrumentationHook is enabled
422
- export async function register() {
423
- if (process.env.NEXT_RUNTIME === 'nodejs') {
424
- // Only run on server-side
425
- const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
426
-
427
- if (connectionString) {
428
- const appInsights = await import('applicationinsights');
429
-
430
- appInsights
431
- .setup(connectionString)
432
- .setAutoCollectConsole(true)
433
- .setAutoCollectDependencies(true)
434
- .setAutoCollectExceptions(true)
435
- .setAutoCollectHeartbeat(true)
436
- .setAutoCollectPerformance(true, true)
437
- .setAutoCollectRequests(true)
438
- .setAutoDependencyCorrelation(true)
439
- .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
440
- .setSendLiveMetrics(true)
441
- .setUseDiskRetryCaching(true);
442
-
443
- appInsights.defaultClient.setAutoPopulateAzureProperties();
444
- appInsights.start();
445
-
446
- // Override console methods to send to Application Insights
447
- const originalConsoleLog = console.log;
448
- const originalConsoleError = console.error;
449
- const originalConsoleWarn = console.warn;
450
-
451
- console.log = function(...args: any[]) {
452
- originalConsoleLog.apply(console, args);
453
- const message = args.map(arg =>
454
- typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
455
- ).join(' ');
456
- appInsights.defaultClient.trackTrace({
457
- message: message,
458
- severity: '1'
459
- });
460
- };
461
-
462
- console.error = function(...args: any[]) {
463
- originalConsoleError.apply(console, args);
464
- const message = args.map(arg =>
465
- typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
466
- ).join(' ');
467
- appInsights.defaultClient.trackTrace({
468
- message: message,
469
- severity: '3'
470
- });
471
- };
472
-
473
- console.warn = function(...args: any[]) {
474
- originalConsoleWarn.apply(console, args);
475
- const message = args.map(arg =>
476
- typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
477
- ).join(' ');
478
- appInsights.defaultClient.trackTrace({
479
- message: message,
480
- severity: '2'
481
- });
482
- };
483
-
484
- console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
485
- } else {
486
- console.log('[App Insights] Not configured (skipped in development mode)');
487
- }
488
- }
489
- }
450
+ const instrumentationContent = `// Application Insights instrumentation for Next.js
451
+ // This file is automatically loaded by Next.js when instrumentationHook is enabled
452
+ export async function register() {
453
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
454
+ // Only run on server-side
455
+ const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
456
+
457
+ if (connectionString) {
458
+ const appInsights = await import('applicationinsights');
459
+
460
+ appInsights
461
+ .setup(connectionString)
462
+ .setAutoCollectConsole(true)
463
+ .setAutoCollectDependencies(true)
464
+ .setAutoCollectExceptions(true)
465
+ .setAutoCollectHeartbeat(true)
466
+ .setAutoCollectPerformance(true, true)
467
+ .setAutoCollectRequests(true)
468
+ .setAutoDependencyCorrelation(true)
469
+ .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
470
+ .setSendLiveMetrics(true)
471
+ .setUseDiskRetryCaching(true);
472
+
473
+ appInsights.defaultClient.setAutoPopulateAzureProperties();
474
+ appInsights.start();
475
+
476
+ // Override console methods to send to Application Insights
477
+ const originalConsoleLog = console.log;
478
+ const originalConsoleError = console.error;
479
+ const originalConsoleWarn = console.warn;
480
+
481
+ console.log = function(...args: any[]) {
482
+ originalConsoleLog.apply(console, args);
483
+ const message = args.map(arg =>
484
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
485
+ ).join(' ');
486
+ appInsights.defaultClient.trackTrace({
487
+ message: message,
488
+ severity: '1'
489
+ });
490
+ };
491
+
492
+ console.error = function(...args: any[]) {
493
+ originalConsoleError.apply(console, args);
494
+ const message = args.map(arg =>
495
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
496
+ ).join(' ');
497
+ appInsights.defaultClient.trackTrace({
498
+ message: message,
499
+ severity: '3'
500
+ });
501
+ };
502
+
503
+ console.warn = function(...args: any[]) {
504
+ originalConsoleWarn.apply(console, args);
505
+ const message = args.map(arg =>
506
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
507
+ ).join(' ');
508
+ appInsights.defaultClient.trackTrace({
509
+ message: message,
510
+ severity: '2'
511
+ });
512
+ };
513
+
514
+ console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
515
+ } else {
516
+ console.log('[App Insights] Not configured (skipped in development mode)');
517
+ }
518
+ }
519
+ }
490
520
  `;
491
521
  fs.writeFileSync(path.join(projectDir, 'instrumentation.ts'), instrumentationContent);
492
522
  // 8. Create .env.local for local development
@@ -589,23 +619,23 @@ async function createAzureFunctionsProject(projectDir) {
589
619
  };
590
620
  fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
591
621
  // Create .funcignore
592
- const funcignore = `node_modules
593
- .git
594
- .vscode
595
- local.settings.json
596
- test
597
- tsconfig.json
598
- *.ts
599
- !dist/**/*.js
622
+ const funcignore = `node_modules
623
+ .git
624
+ .vscode
625
+ local.settings.json
626
+ test
627
+ tsconfig.json
628
+ *.ts
629
+ !dist/**/*.js
600
630
  `;
601
631
  fs.writeFileSync(path.join(functionsDir, '.funcignore'), funcignore);
602
632
  // Create .gitignore for functions directory
603
- const functionsGitignore = `node_modules
604
- dist
605
- local.settings.json
606
- *.log
607
- .vscode
608
- .DS_Store
633
+ const functionsGitignore = `node_modules
634
+ dist
635
+ local.settings.json
636
+ *.log
637
+ .vscode
638
+ .DS_Store
609
639
  `;
610
640
  fs.writeFileSync(path.join(functionsDir, '.gitignore'), functionsGitignore);
611
641
  // Create local.settings.json
@@ -627,58 +657,58 @@ local.settings.json
627
657
  const srcDir = path.join(functionsDir, 'src');
628
658
  fs.mkdirSync(srcDir, { recursive: true });
629
659
  // Create greet function directly in src
630
- const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
631
- import { z } from 'zod';
632
-
633
- // Zod schema for request validation
634
- const greetRequestSchema = z.object({
635
- name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
636
- });
637
-
638
- export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
639
- context.log('HTTP trigger function processed a request.');
640
-
641
- try {
642
- // Get name from query or body
643
- const name = request.query.get('name') || (await request.text());
644
-
645
- // Validate with Zod
646
- const result = greetRequestSchema.safeParse({ name });
647
-
648
- if (!result.success) {
649
- return {
650
- status: 400,
651
- jsonBody: {
652
- error: result.error.errors[0].message
653
- }
654
- };
655
- }
656
-
657
- const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
658
-
659
- return {
660
- status: 200,
661
- jsonBody: {
662
- message: greeting,
663
- timestamp: new Date().toISOString()
664
- }
665
- };
666
- } catch (error) {
667
- context.error('Error processing request:', error);
668
- return {
669
- status: 500,
670
- jsonBody: {
671
- error: 'Internal server error'
672
- }
673
- };
674
- }
675
- }
676
-
677
- app.http('greet', {
678
- methods: ['GET', 'POST'],
679
- authLevel: 'anonymous',
680
- handler: greet
681
- });
660
+ const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
661
+ import { z } from 'zod';
662
+
663
+ // Zod schema for request validation
664
+ const greetRequestSchema = z.object({
665
+ name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
666
+ });
667
+
668
+ export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
669
+ context.log('HTTP trigger function processed a request.');
670
+
671
+ try {
672
+ // Get name from query or body
673
+ const name = request.query.get('name') || (await request.text());
674
+
675
+ // Validate with Zod
676
+ const result = greetRequestSchema.safeParse({ name });
677
+
678
+ if (!result.success) {
679
+ return {
680
+ status: 400,
681
+ jsonBody: {
682
+ error: result.error.errors[0].message
683
+ }
684
+ };
685
+ }
686
+
687
+ const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
688
+
689
+ return {
690
+ status: 200,
691
+ jsonBody: {
692
+ message: greeting,
693
+ timestamp: new Date().toISOString()
694
+ }
695
+ };
696
+ } catch (error) {
697
+ context.error('Error processing request:', error);
698
+ return {
699
+ status: 500,
700
+ jsonBody: {
701
+ error: 'Internal server error'
702
+ }
703
+ };
704
+ }
705
+ }
706
+
707
+ app.http('greet', {
708
+ methods: ['GET', 'POST'],
709
+ authLevel: 'anonymous',
710
+ handler: greet
711
+ });
682
712
  `;
683
713
  fs.writeFileSync(path.join(srcDir, 'greet.ts'), greetFunction);
684
714
  console.log('✅ Azure Functions project created\n');
@@ -688,48 +718,48 @@ async function createBffApiRoute(projectDir) {
688
718
  const apiDir = path.join(projectDir, 'app', 'api', 'greet');
689
719
  fs.mkdirSync(apiDir, { recursive: true });
690
720
  // Create API route that calls Azure Functions using shared utility
691
- const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
692
- import { api } from '@/lib/api/backend';
693
-
694
- interface GreetResponse {
695
- message: string;
696
- timestamp: string;
697
- }
698
-
699
- export async function GET(request: NextRequest) {
700
- try {
701
- const { searchParams } = new URL(request.url);
702
- const name = searchParams.get('name') || 'World';
703
-
704
- const data = await api.get<GreetResponse>('/api/greet', { name });
705
-
706
- return NextResponse.json(data);
707
- } catch (error) {
708
- console.error('Error calling Azure Functions:', error);
709
- const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
710
- return NextResponse.json(
711
- { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
712
- { status: 500 }
713
- );
714
- }
715
- }
716
-
717
- export async function POST(request: NextRequest) {
718
- try {
719
- const body = await request.json();
720
-
721
- const data = await api.post<GreetResponse>('/api/greet', body);
722
-
723
- return NextResponse.json(data);
724
- } catch (error) {
725
- console.error('Error calling Azure Functions:', error);
726
- const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
727
- return NextResponse.json(
728
- { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
729
- { status: 500 }
730
- );
731
- }
732
- }
721
+ const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
722
+ import { api } from '@/lib/api/backend';
723
+
724
+ interface GreetResponse {
725
+ message: string;
726
+ timestamp: string;
727
+ }
728
+
729
+ export async function GET(request: NextRequest) {
730
+ try {
731
+ const { searchParams } = new URL(request.url);
732
+ const name = searchParams.get('name') || 'World';
733
+
734
+ const data = await api.get<GreetResponse>('/api/greet', { name });
735
+
736
+ return NextResponse.json(data);
737
+ } catch (error) {
738
+ console.error('Error calling Azure Functions:', error);
739
+ const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
740
+ return NextResponse.json(
741
+ { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
742
+ { status: 500 }
743
+ );
744
+ }
745
+ }
746
+
747
+ export async function POST(request: NextRequest) {
748
+ try {
749
+ const body = await request.json();
750
+
751
+ const data = await api.post<GreetResponse>('/api/greet', body);
752
+
753
+ return NextResponse.json(data);
754
+ } catch (error) {
755
+ console.error('Error calling Azure Functions:', error);
756
+ const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
757
+ return NextResponse.json(
758
+ { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
759
+ { status: 500 }
760
+ );
761
+ }
762
+ }
733
763
  `;
734
764
  fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
735
765
  // Update .env.example to include FUNCTIONS_BASE_URL
@@ -750,108 +780,108 @@ export async function POST(request: NextRequest) {
750
780
  }
751
781
  async function createHomePage(projectDir) {
752
782
  console.log('📦 Creating home page...\n');
753
- const pageContent = `'use client'
754
-
755
- export const dynamic = 'force-dynamic';
756
-
757
- import { useState } from 'react';
758
- import { scaffoldConfig } from '@/lib/scaffold-config';
759
-
760
- export default function Home() {
761
- const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
762
- const [message, setMessage] = useState('');
763
-
764
- const testConnection = async () => {
765
- setGreetingStatus('loading');
766
- try {
767
- const response = await fetch('/api/greet?name=SwallowKit');
768
- const data = await response.json();
769
- setMessage(data.message);
770
- setGreetingStatus('success');
771
- } catch (error) {
772
- setMessage('Failed to connect to Azure Functions');
773
- setGreetingStatus('error');
774
- }
775
- };
776
-
777
- return (
778
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
779
- <div className="container mx-auto px-4 py-12">
780
- <header className="text-center mb-16">
781
- <h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
782
- Welcome to SwallowKit
783
- </h1>
784
- <p className="text-xl text-gray-600 dark:text-gray-400">
785
- Full-stack TypeScript with Next.js + Azure Functions + Zod
786
- </p>
787
- </header>
788
-
789
- {/* Connection Test */}
790
- <section className="max-w-2xl mx-auto mb-12">
791
- <div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
792
- <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
793
- Test BFF → Functions Connection
794
- </h2>
795
- <button
796
- onClick={testConnection}
797
- disabled={greetingStatus === 'loading'}
798
- className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
799
- >
800
- {greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
801
- </button>
802
- {greetingStatus === 'success' && (
803
- <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
804
- <p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
805
- <p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
806
- </div>
807
- )}
808
- {greetingStatus === 'error' && (
809
- <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
810
- <p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
811
- <p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
812
- </div>
813
- )}
814
- </div>
815
- </section>
816
-
817
- {/* Scaffolded Models Menu */}
818
- {scaffoldConfig.models.length > 0 ? (
819
- <section className="max-w-6xl mx-auto">
820
- <h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
821
- <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
822
- {scaffoldConfig.models.map((model) => (
823
- <a
824
- key={model.name}
825
- href={model.path}
826
- 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"
827
- >
828
- <h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
829
- <p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
830
- </a>
831
- ))}
832
- </div>
833
- </section>
834
- ) : (
835
- <section className="max-w-2xl mx-auto text-center">
836
- <div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
837
- <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
838
- <p className="text-gray-600 dark:text-gray-400 mb-6">
839
- Create your first model with Zod and generate CRUD operations automatically.
840
- </p>
841
- <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
842
- npx swallowkit scaffold lib/models/your-model.ts
843
- </code>
844
- </div>
845
- </section>
846
- )}
847
-
848
- <footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
849
- <p>Built with SwallowKit</p>
850
- </footer>
851
- </div>
852
- </div>
853
- );
854
- }
783
+ const pageContent = `'use client'
784
+
785
+ export const dynamic = 'force-dynamic';
786
+
787
+ import { useState } from 'react';
788
+ import { scaffoldConfig } from '@/lib/scaffold-config';
789
+
790
+ export default function Home() {
791
+ const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
792
+ const [message, setMessage] = useState('');
793
+
794
+ const testConnection = async () => {
795
+ setGreetingStatus('loading');
796
+ try {
797
+ const response = await fetch('/api/greet?name=SwallowKit');
798
+ const data = await response.json();
799
+ setMessage(data.message);
800
+ setGreetingStatus('success');
801
+ } catch (error) {
802
+ setMessage('Failed to connect to Azure Functions');
803
+ setGreetingStatus('error');
804
+ }
805
+ };
806
+
807
+ return (
808
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
809
+ <div className="container mx-auto px-4 py-12">
810
+ <header className="text-center mb-16">
811
+ <h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
812
+ Welcome to SwallowKit
813
+ </h1>
814
+ <p className="text-xl text-gray-600 dark:text-gray-400">
815
+ Full-stack TypeScript with Next.js + Azure Functions + Zod
816
+ </p>
817
+ </header>
818
+
819
+ {/* Connection Test */}
820
+ <section className="max-w-2xl mx-auto mb-12">
821
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
822
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
823
+ Test BFF → Functions Connection
824
+ </h2>
825
+ <button
826
+ onClick={testConnection}
827
+ disabled={greetingStatus === 'loading'}
828
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
829
+ >
830
+ {greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
831
+ </button>
832
+ {greetingStatus === 'success' && (
833
+ <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
834
+ <p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
835
+ <p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
836
+ </div>
837
+ )}
838
+ {greetingStatus === 'error' && (
839
+ <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
840
+ <p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
841
+ <p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
842
+ </div>
843
+ )}
844
+ </div>
845
+ </section>
846
+
847
+ {/* Scaffolded Models Menu */}
848
+ {scaffoldConfig.models.length > 0 ? (
849
+ <section className="max-w-6xl mx-auto">
850
+ <h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
851
+ <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
852
+ {scaffoldConfig.models.map((model) => (
853
+ <a
854
+ key={model.name}
855
+ href={model.path}
856
+ 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"
857
+ >
858
+ <h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
859
+ <p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
860
+ </a>
861
+ ))}
862
+ </div>
863
+ </section>
864
+ ) : (
865
+ <section className="max-w-2xl mx-auto text-center">
866
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
867
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
868
+ <p className="text-gray-600 dark:text-gray-400 mb-6">
869
+ Create your first model with Zod and generate CRUD operations automatically.
870
+ </p>
871
+ <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
872
+ npx swallowkit scaffold lib/models/your-model.ts
873
+ </code>
874
+ </div>
875
+ </section>
876
+ )}
877
+
878
+ <footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
879
+ <p>Built with SwallowKit</p>
880
+ </footer>
881
+ </div>
882
+ </div>
883
+ );
884
+ }
855
885
  `;
856
886
  fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), pageContent);
857
887
  console.log('✅ Home page created\n');
@@ -860,17 +890,17 @@ export default function Home() {
860
890
  if (!fs.existsSync(scaffoldConfigDir)) {
861
891
  fs.mkdirSync(scaffoldConfigDir, { recursive: true });
862
892
  }
863
- const scaffoldConfigContent = `export interface ScaffoldModel {
864
- name: string;
865
- path: string;
866
- label: string;
867
- }
868
-
869
- export const scaffoldConfig = {
870
- models: [
871
- // Scaffolded models will be added here by 'npx swallowkit scaffold' command
872
- ] as ScaffoldModel[]
873
- };
893
+ const scaffoldConfigContent = `export interface ScaffoldModel {
894
+ name: string;
895
+ path: string;
896
+ label: string;
897
+ }
898
+
899
+ export const scaffoldConfig = {
900
+ models: [
901
+ // Scaffolded models will be added here by 'npx swallowkit scaffold' command
902
+ ] as ScaffoldModel[]
903
+ };
874
904
  `;
875
905
  fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
876
906
  console.log('✅ Scaffold config created\n');
@@ -880,181 +910,229 @@ function createReadme(projectDir, projectName, cicdChoice, azureConfig) {
880
910
  const functionsPlanLabel = azureConfig.functionsPlan === 'flex' ? 'Flex Consumption' : 'Premium';
881
911
  const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
882
912
  const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
883
- const readme = `# ${projectName}
884
-
885
- A full-stack application built with **SwallowKit** - a modern TypeScript framework for building Next.js + Azure Functions applications.
886
-
887
- ## 🚀 Tech Stack
888
-
889
- - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
890
- - **BFF (Backend for Frontend)**: Next.js API Routes
891
- - **Backend**: Azure Functions (TypeScript)
892
- - **Database**: Azure Cosmos DB
893
- - **Schema Validation**: Zod (shared between frontend and backend)
894
- - **Infrastructure**: Bicep (Infrastructure as Code)
895
- - **CI/CD**: ${cicdLabel}
896
-
897
- ## 📋 Project Configuration
898
-
899
- This project was initialized with the following settings:
900
-
901
- - **Azure Functions Plan**: ${functionsPlanLabel}
902
- - **Cosmos DB Mode**: ${cosmosDbModeLabel}
903
- - **CI/CD**: ${cicdLabel}
904
-
905
- ## Prerequisites
906
-
907
- Before you begin, ensure you have the following installed:
908
-
909
- 1. **Node.js 18+**: [Download](https://nodejs.org/)
910
- 2. **Azure CLI**: Required for provisioning Azure resources
911
- - Install: \`winget install Microsoft.AzureCLI\` (Windows)
912
- - Or: [Download](https://aka.ms/installazurecliwindows)
913
- 3. **Azure Cosmos DB Emulator**: Required for local development
914
- - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
915
- - Or: [Download](https://aka.ms/cosmosdb-emulator)
916
- - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
917
- 4. **Azure Functions Core Tools**: Automatically installed with project dependencies
918
-
919
- ## 📁 Project Structure
920
-
921
- \`\`\`
922
- ${projectName}/
923
- ├── app/ # Next.js App Router (frontend)
924
- │ ├── api/ # BFF API routes (proxy to Functions)
925
- │ └── page.tsx # Home page
926
- ├── functions/ # Azure Functions (backend)
927
- │ └── src/
928
- ├── models/ # Data models (copied from lib/models)
929
- └── hello.ts # Sample function
930
- ├── lib/
931
- ├── models/ # Shared Zod schemas
932
- └── api/ # API client utilities
933
- ├── infra/ # Bicep infrastructure files
934
- ├── main.bicep
935
- └── modules/ # Bicep modules for each resource
936
- └── .github/workflows/ # CI/CD workflows
937
- \`\`\`
938
-
939
- ## 🏗️ Getting Started
940
-
941
- ### 1. Create Your First Model
942
-
943
- Define your data model with Zod schema:
944
-
945
- \`\`\`bash
946
- npx swallowkit create-model <model-name>
947
- \`\`\`
948
-
949
- This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
950
-
951
- ### 2. Generate CRUD Code
952
-
953
- Generate complete CRUD operations (Functions, API routes, UI):
954
-
955
- \`\`\`bash
956
- npx swallowkit scaffold lib/models/<model-name>.ts
957
- \`\`\`
958
-
959
- This generates:
960
- - Azure Functions CRUD endpoints
961
- - Next.js BFF API routes
962
- - React UI components (list, detail, create, edit)
963
- - Navigation menu integration
964
-
965
- ### 3. Start Development Servers
966
-
967
- \`\`\`bash
968
- npx swallowkit dev
969
- \`\`\`
970
-
971
- This starts:
972
- - Next.js dev server (http://localhost:3000)
973
- - Azure Functions (http://localhost:7071)
974
- - Cosmos DB Emulator check (must be running separately)
975
-
976
- **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
977
-
978
- ## ☁️ Deploy to Azure
979
-
980
- ### Provision Azure Resources
981
-
982
- Create all required Azure resources using Bicep:
983
-
984
- \`\`\`bash
985
- npx swallowkit provision --resource-group <rg-name>
986
- \`\`\`
987
-
988
- This creates:
989
- - Static Web App (\`swa-${projectName}\`)
990
- - Azure Functions (\`func-${projectName}\`)
991
- - Cosmos DB (\`cosmos-${projectName}\`)
992
- - Storage Account
993
-
994
- You will be prompted to select Azure regions:
995
- 1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
996
- 2. **Static Web App location**: Limited availability (default: East Asia)
997
-
998
- ### CI/CD Setup
999
-
1000
- ${cicdChoice === 'github' ? `#### GitHub Actions
1001
-
1002
- 1. Get Static Web App deployment token:
1003
- \`\`\`bash
1004
- az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
1005
- \`\`\`
1006
-
1007
- 2. Get Function App publish profile:
1008
- \`\`\`bash
1009
- az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
1010
- \`\`\`
1011
-
1012
- 3. Add secrets to GitHub repository:
1013
- - \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
1014
- - \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
1015
- - \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
1016
-
1017
- 4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
1018
-
1019
- 1. Set up service connection in Azure DevOps
1020
- 2. Update \`azure-pipelines.yml\` with your resource names
1021
- 3. Configure pipeline variables:
1022
- - \`azureSubscription\`: Service connection name
1023
- - \`resourceGroupName\`: Resource group name
1024
- 4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
1025
-
1026
- **Deploy Static Web App:**
1027
- \`\`\`bash
1028
- npm run build
1029
- az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
1030
- \`\`\`
1031
-
1032
- **Deploy Functions:**
1033
- \`\`\`bash
1034
- cd functions
1035
- npm run build
1036
- func azure functionapp publish func-${projectName}
1037
- \`\`\``}
1038
-
1039
- ## 🔧 Available Commands
1040
-
1041
- - \`npx swallowkit create-model <name>\` - Create a new data model
1042
- - \`npx swallowkit scaffold <model-file>\` - Generate CRUD code
1043
- - \`npx swallowkit dev\` - Start development servers
1044
- - \`npx swallowkit provision -g <rg-name>\` - Provision Azure resources
1045
-
1046
- ## 📚 Learn More
1047
-
1048
- - [SwallowKit Documentation](https://github.com/himanago/swallowkit)
1049
- - [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
1050
- - [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
1051
- - [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
1052
- - [Next.js](https://nextjs.org/)
1053
- - [Zod](https://zod.dev/)
1054
-
1055
- ## 💭 Feedback
1056
-
1057
- 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).
913
+ const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
914
+ azureConfig.vnetOption === 'outbound' ? 'Outbound VNet (Cosmos DB Private Endpoint)' :
915
+ 'Full Private (Functions + Cosmos DB Private Endpoints)';
916
+ const readme = `# ${projectName}
917
+
918
+ A full-stack application built with **SwallowKit** - a modern TypeScript framework for building Next.js + Azure Functions applications.
919
+
920
+ ## 🚀 Tech Stack
921
+
922
+ - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
923
+ - **BFF (Backend for Frontend)**: Next.js API Routes
924
+ - **Backend**: Azure Functions (TypeScript)
925
+ - **Database**: Azure Cosmos DB
926
+ - **Schema Validation**: Zod (shared between frontend and backend)
927
+ - **Infrastructure**: Bicep (Infrastructure as Code)
928
+ - **CI/CD**: ${cicdLabel}
929
+
930
+ ## 📋 Project Configuration
931
+
932
+ This project was initialized with the following settings:
933
+
934
+ - **Azure Functions Plan**: ${functionsPlanLabel}
935
+ - **Cosmos DB Mode**: ${cosmosDbModeLabel}
936
+ - **Network Security**: ${vnetLabel}
937
+ - **CI/CD**: ${cicdLabel}
938
+
939
+ ## Prerequisites
940
+
941
+ Before you begin, ensure you have the following installed:
942
+
943
+ 1. **Node.js 18+**: [Download](https://nodejs.org/)
944
+ 2. **Azure CLI**: Required for provisioning Azure resources
945
+ - Install: \`winget install Microsoft.AzureCLI\` (Windows)
946
+ - Or: [Download](https://aka.ms/installazurecliwindows)
947
+ 3. **Azure Cosmos DB Emulator**: Required for local development
948
+ - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
949
+ - Or: [Download](https://aka.ms/cosmosdb-emulator)
950
+ - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
951
+ 4. **Azure Functions Core Tools**: Automatically installed with project dependencies
952
+
953
+ ## 📁 Project Structure
954
+
955
+ \`\`\`
956
+ ${projectName}/
957
+ ├── app/ # Next.js App Router (frontend)
958
+ ├── api/ # BFF API routes (proxy to Functions)
959
+ └── page.tsx # Home page
960
+ ├── functions/ # Azure Functions (backend)
961
+ └── src/
962
+ ├── models/ # Data models (copied from lib/models)
963
+ │ └── hello.ts # Sample function
964
+ ├── lib/
965
+ ├── models/ # Shared Zod schemas
966
+ └── api/ # API client utilities
967
+ ├── infra/ # Bicep infrastructure files
968
+ │ ├── main.bicep
969
+ │ └── modules/ # Bicep modules for each resource
970
+ └── .github/workflows/ # CI/CD workflows
971
+ \`\`\`
972
+
973
+ ## 🏗️ Getting Started
974
+
975
+ ### 1. Create Your First Model
976
+
977
+ Define your data model with Zod schema:
978
+
979
+ \`\`\`bash
980
+ npx swallowkit create-model <model-name>
981
+ \`\`\`
982
+
983
+ This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
984
+
985
+ ### 2. Generate CRUD Code
986
+
987
+ Generate complete CRUD operations (Functions, API routes, UI):
988
+
989
+ \`\`\`bash
990
+ npx swallowkit scaffold lib/models/<model-name>.ts
991
+ \`\`\`
992
+
993
+ This generates:
994
+ - Azure Functions CRUD endpoints
995
+ - Next.js BFF API routes
996
+ - React UI components (list, detail, create, edit)
997
+ - Navigation menu integration
998
+
999
+ ### 3. Start Development Servers
1000
+
1001
+ \`\`\`bash
1002
+ npx swallowkit dev
1003
+ \`\`\`
1004
+
1005
+ This starts:
1006
+ - Next.js dev server (http://localhost:3000)
1007
+ - Azure Functions (http://localhost:7071)
1008
+ - Cosmos DB Emulator check (must be running separately)
1009
+
1010
+ **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
1011
+
1012
+ ## ☁️ Deploy to Azure
1013
+
1014
+ ### Provision Azure Resources
1015
+
1016
+ Create all required Azure resources using Bicep:
1017
+
1018
+ \`\`\`bash
1019
+ npx swallowkit provision --resource-group <rg-name>
1020
+ \`\`\`
1021
+
1022
+ This creates:
1023
+ - Static Web App (\`swa-${projectName}\`)
1024
+ - Azure Functions (\`func-${projectName}\`)
1025
+ - Cosmos DB (\`cosmos-${projectName}\`)
1026
+ - Storage Account
1027
+
1028
+ You will be prompted to select Azure regions:
1029
+ 1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
1030
+ 2. **Static Web App location**: Limited availability (default: East Asia)
1031
+
1032
+ ### CI/CD Setup
1033
+
1034
+ ${cicdChoice === 'github' ? `#### GitHub Actions
1035
+
1036
+ 1. Get Static Web App deployment token:
1037
+ \`\`\`bash
1038
+ az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
1039
+ \`\`\`
1040
+
1041
+ 2. Get Function App publish profile:
1042
+ \`\`\`bash
1043
+ az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
1044
+ \`\`\`
1045
+
1046
+ 3. Add secrets to GitHub repository:
1047
+ - \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
1048
+ - \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
1049
+ - \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
1050
+
1051
+ 4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
1052
+
1053
+ 1. Set up service connection in Azure DevOps
1054
+ 2. Update \`azure-pipelines.yml\` with your resource names
1055
+ 3. Configure pipeline variables:
1056
+ - \`azureSubscription\`: Service connection name
1057
+ - \`resourceGroupName\`: Resource group name
1058
+ 4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
1059
+
1060
+ **Deploy Static Web App:**
1061
+ \`\`\`bash
1062
+ npm run build
1063
+ az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
1064
+ \`\`\`
1065
+
1066
+ **Deploy Functions:**
1067
+ \`\`\`bash
1068
+ cd functions
1069
+ npm run build
1070
+ func azure functionapp publish func-${projectName}
1071
+ \`\`\``}
1072
+
1073
+ ## 🔧 Available Commands
1074
+
1075
+ - \`npx swallowkit create-model <name>\` - Create a new data model
1076
+ - \`npx swallowkit scaffold <model-file>\` - Generate CRUD code
1077
+ - \`npx swallowkit dev\` - Start development servers
1078
+ - \`npx swallowkit provision -g <rg-name>\` - Provision Azure resources
1079
+ ${azureConfig.vnetOption !== 'none' ? `
1080
+ ## 🔒 Network Security (VNet Configuration)
1081
+
1082
+ This project is configured with **${vnetLabel}**.
1083
+
1084
+ ### Architecture
1085
+ ${azureConfig.vnetOption === 'outbound' ? `
1086
+ \`\`\`
1087
+ Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
1088
+
1089
+ VNet Integration
1090
+ (outbound only)
1091
+ \`\`\`
1092
+
1093
+ - **Functions → Cosmos DB**: Private Endpoint経由(プライベート接続)
1094
+ - **SWA → Functions**: パブリックエンドポイント経由(CORS + IP制限で保護)
1095
+ ` : `
1096
+ \`\`\`
1097
+ Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
1098
+
1099
+ Private Endpoint
1100
+ (inbound + outbound)
1101
+ \`\`\`
1102
+
1103
+ - **Functions**: 完全プライベート(プライベートエンドポイント経由のみアクセス可能)
1104
+ - **Cosmos DB**: プライベートエンドポイント経由のみアクセス可能
1105
+
1106
+ ⚠️ **注意**: Full Private構成では、SWAからFunctionsへの直接アクセスが制限されます。
1107
+ Azure Front DoorまたはAPI Management経由でのアクセスを検討してください。
1108
+ `}
1109
+ ### VNet Resources
1110
+
1111
+ | Resource | Purpose |
1112
+ |----------|---------|
1113
+ | \`vnet-${projectName}\` | 仮想ネットワーク (10.0.0.0/16) |
1114
+ | \`snet-functions\` | Functions用サブネット (10.0.1.0/24) |
1115
+ | \`snet-private-endpoints\` | プライベートエンドポイント用 (10.0.2.0/24) |
1116
+ | \`pe-cosmos-${projectName}\` | Cosmos DBプライベートエンドポイント |
1117
+ ${azureConfig.vnetOption === 'full' ? `| \`pe-func-${projectName}\` | Functionsプライベートエンドポイント |` : ''}
1118
+
1119
+ ### Private DNS Zones
1120
+
1121
+ - \`privatelink.documents.azure.com\` (Cosmos DB)
1122
+ ${azureConfig.vnetOption === 'full' ? `- \`privatelink.azurewebsites.net\` (Functions)` : ''}
1123
+ ` : ''}
1124
+ ## 📚 Learn More
1125
+
1126
+ - [SwallowKit Documentation](https://github.com/himanago/swallowkit)
1127
+ - [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
1128
+ - [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
1129
+ - [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
1130
+ - [Next.js](https://nextjs.org/)
1131
+ - [Zod](https://zod.dev/)
1132
+
1133
+ ## 💭 Feedback
1134
+
1135
+ 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).
1058
1136
  `;
1059
1137
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1060
1138
  console.log('✅ README.md created\n');
@@ -1064,730 +1142,1047 @@ async function createInfrastructure(projectDir, projectName, azureConfig) {
1064
1142
  const infraDir = path.join(projectDir, 'infra');
1065
1143
  const modulesDir = path.join(infraDir, 'modules');
1066
1144
  fs.mkdirSync(modulesDir, { recursive: true });
1145
+ const enableVNet = azureConfig.vnetOption !== 'none';
1146
+ const enableFullPrivate = azureConfig.vnetOption === 'full' && azureConfig.functionsPlan === 'premium';
1067
1147
  // main.bicep
1068
- const mainBicep = `targetScope = 'resourceGroup'
1069
-
1070
- @description('Project name')
1071
- param projectName string
1072
-
1073
- @description('Location for Functions and Cosmos DB')
1074
- param location string = resourceGroup().location
1075
-
1076
- @description('Location for Static Web App (must be explicitly provided)')
1077
- param swaLocation string
1078
-
1079
- @description('Functions plan type')
1080
- @allowed(['flex', 'premium'])
1081
- param functionsPlan string = '${azureConfig.functionsPlan}'
1082
-
1083
- @description('Cosmos DB mode')
1084
- @allowed(['freetier', 'serverless'])
1085
- param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
1086
-
1087
- // Shared Log Analytics Workspace (in Functions region for data residency)
1088
- module logAnalytics 'modules/loganalytics.bicep' = {
1089
- name: 'logAnalytics'
1090
- params: {
1091
- name: 'log-\${projectName}'
1092
- location: location
1093
- }
1094
- }
1095
-
1096
- // Application Insights for Static Web App (must be in same region as SWA)
1097
- module appInsightsSwa 'modules/appinsights.bicep' = {
1098
- name: 'appInsightsSwa'
1099
- params: {
1100
- name: 'appi-\${projectName}-swa'
1101
- location: swaLocation
1102
- logAnalyticsWorkspaceId: logAnalytics.outputs.id
1103
- }
1104
- }
1105
-
1106
- // Application Insights for Functions (in same region as Functions)
1107
- module appInsightsFunctions 'modules/appinsights.bicep' = {
1108
- name: 'appInsightsFunctions'
1109
- params: {
1110
- name: 'appi-\${projectName}-func'
1111
- location: location
1112
- logAnalyticsWorkspaceId: logAnalytics.outputs.id
1113
- }
1114
- }
1115
-
1116
- // Static Web App
1117
- module staticWebApp 'modules/staticwebapp.bicep' = {
1118
- name: 'staticWebApp'
1119
- params: {
1120
- name: 'swa-\${projectName}'
1121
- location: swaLocation
1122
- sku: 'Standard'
1123
- appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1124
- }
1125
- }
1126
-
1127
- // Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
1128
- module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
1129
- name: 'cosmosDb'
1130
- params: {
1131
- accountName: 'cosmos-\${projectName}'
1132
- databaseName: '\${projectName}Database'
1133
- location: location
1134
- }
1135
- }
1136
-
1137
- module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
1138
- name: 'cosmosDb'
1139
- params: {
1140
- accountName: 'cosmos-\${projectName}'
1141
- databaseName: '\${projectName}Database'
1142
- location: location
1143
- }
1144
- }
1145
-
1146
- // Azure Functions (conditional based on plan) - Deploy AFTER Cosmos DB
1147
- module functionsFlex 'modules/functions-flex.bicep' = if (functionsPlan == 'flex') {
1148
- name: 'functionsApp'
1149
- params: {
1150
- name: 'func-\${projectName}'
1151
- location: location
1152
- storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1153
- appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1154
- swaDefaultHostname: staticWebApp.outputs.defaultHostname
1155
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1156
- cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1157
- }
1158
- dependsOn: [
1159
- cosmosDbFreeTier
1160
- cosmosDbServerless
1161
- ]
1162
- }
1163
-
1164
- module functionsPremium 'modules/functions-premium.bicep' = if (functionsPlan == 'premium') {
1165
- name: 'functionsApp'
1166
- params: {
1167
- name: 'func-\${projectName}'
1168
- location: location
1169
- storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1170
- appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1171
- swaDefaultHostname: staticWebApp.outputs.defaultHostname
1172
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1173
- cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1174
- }
1175
- dependsOn: [
1176
- cosmosDbFreeTier
1177
- cosmosDbServerless
1178
- ]
1179
- }
1180
-
1181
- // Cosmos DB role assignment for Functions (after Functions is created)
1182
- module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
1183
- name: 'cosmosDbRoleAssignment'
1184
- params: {
1185
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
1186
- functionsPrincipalId: functionsPlan == 'flex' ? functionsFlex.outputs.principalId : functionsPremium.outputs.principalId
1187
- }
1188
- dependsOn: [
1189
- functionsFlex
1190
- functionsPremium
1191
- ]
1192
- }
1193
-
1194
- module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
1195
- name: 'cosmosDbRoleAssignment'
1196
- params: {
1197
- cosmosAccountName: cosmosDbServerless.outputs.accountName
1198
- functionsPrincipalId: functionsPlan == 'flex' ? functionsFlex.outputs.principalId : functionsPremium.outputs.principalId
1199
- }
1200
- dependsOn: [
1201
- functionsFlex
1202
- functionsPremium
1203
- ]
1204
- }
1205
-
1206
- // Update SWA config with Functions hostname (after Functions deployment)
1207
- module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
1208
- name: 'staticWebAppConfig'
1209
- params: {
1210
- staticWebAppName: staticWebApp.outputs.name
1211
- functionsDefaultHostname: functionsPlan == 'flex' ? functionsFlex.outputs.defaultHostname : functionsPremium.outputs.defaultHostname
1212
- appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1213
- }
1214
- dependsOn: [
1215
- functionsFlex
1216
- functionsPremium
1217
- ]
1218
- }
1219
-
1220
- output staticWebAppName string = staticWebApp.outputs.name
1221
- output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
1222
- output functionsAppName string = functionsPlan == 'flex' ? functionsFlex.outputs.name : functionsPremium.outputs.name
1223
- output functionsAppUrl string = functionsPlan == 'flex' ? functionsFlex.outputs.defaultHostname : functionsPremium.outputs.defaultHostname
1224
- output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1225
- output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1226
- output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1227
- output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
1228
- output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
1229
- output appInsightsSwaName string = appInsightsSwa.outputs.name
1230
- output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
1231
- output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
1232
- output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
1148
+ const mainBicep = `targetScope = 'resourceGroup'
1149
+
1150
+ @description('Project name')
1151
+ param projectName string
1152
+
1153
+ @description('Location for Functions and Cosmos DB')
1154
+ param location string = resourceGroup().location
1155
+
1156
+ @description('Location for Static Web App (must be explicitly provided)')
1157
+ param swaLocation string
1158
+
1159
+ @description('Functions plan type')
1160
+ @allowed(['flex', 'premium'])
1161
+ param functionsPlan string = '${azureConfig.functionsPlan}'
1162
+
1163
+ @description('Cosmos DB mode')
1164
+ @allowed(['freetier', 'serverless'])
1165
+ param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
1166
+
1167
+ @description('Enable VNet integration')
1168
+ param enableVNet bool = ${enableVNet}
1169
+
1170
+ @description('Enable full private network (Functions private endpoint)')
1171
+ param enableFullPrivate bool = ${enableFullPrivate}
1172
+
1173
+ // Shared Log Analytics Workspace (in Functions region for data residency)
1174
+ module logAnalytics 'modules/loganalytics.bicep' = {
1175
+ name: 'logAnalytics'
1176
+ params: {
1177
+ name: 'log-\${projectName}'
1178
+ location: location
1179
+ }
1180
+ }
1181
+
1182
+ // Application Insights for Static Web App (must be in same region as SWA)
1183
+ module appInsightsSwa 'modules/appinsights.bicep' = {
1184
+ name: 'appInsightsSwa'
1185
+ params: {
1186
+ name: 'appi-\${projectName}-swa'
1187
+ location: swaLocation
1188
+ logAnalyticsWorkspaceId: logAnalytics.outputs.id
1189
+ }
1190
+ }
1191
+
1192
+ // Application Insights for Functions (in same region as Functions)
1193
+ module appInsightsFunctions 'modules/appinsights.bicep' = {
1194
+ name: 'appInsightsFunctions'
1195
+ params: {
1196
+ name: 'appi-\${projectName}-func'
1197
+ location: location
1198
+ logAnalyticsWorkspaceId: logAnalytics.outputs.id
1199
+ }
1200
+ }
1201
+
1202
+ // Static Web App
1203
+ module staticWebApp 'modules/staticwebapp.bicep' = {
1204
+ name: 'staticWebApp'
1205
+ params: {
1206
+ name: 'swa-\${projectName}'
1207
+ location: swaLocation
1208
+ sku: 'Standard'
1209
+ appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1210
+ }
1211
+ }
1212
+
1213
+ // VNet (conditional)
1214
+ module vnet 'modules/vnet.bicep' = if (enableVNet) {
1215
+ name: 'vnet'
1216
+ params: {
1217
+ name: 'vnet-\${projectName}'
1218
+ location: location
1219
+ functionsPlan: functionsPlan
1220
+ }
1221
+ }
1222
+
1223
+ // Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
1224
+ module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
1225
+ name: 'cosmosDb'
1226
+ params: {
1227
+ accountName: 'cosmos-\${projectName}'
1228
+ databaseName: '\${projectName}Database'
1229
+ location: location
1230
+ publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
1231
+ }
1232
+ }
1233
+
1234
+ module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
1235
+ name: 'cosmosDb'
1236
+ params: {
1237
+ accountName: 'cosmos-\${projectName}'
1238
+ databaseName: '\${projectName}Database'
1239
+ location: location
1240
+ publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
1241
+ }
1242
+ }
1243
+
1244
+ // Cosmos DB Private Endpoint (conditional)
1245
+ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
1246
+ name: 'cosmosPrivateEndpoint'
1247
+ params: {
1248
+ name: 'pe-cosmos-\${projectName}'
1249
+ location: location
1250
+ cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
1251
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1252
+ subnetId: vnet.outputs.privateEndpointSubnetId
1253
+ vnetId: vnet.outputs.id
1254
+ }
1255
+ dependsOn: [
1256
+ cosmosDbFreeTier
1257
+ cosmosDbServerless
1258
+ vnet
1259
+ ]
1260
+ }
1261
+
1262
+ // Azure Functions (conditional based on plan) - Deploy AFTER Cosmos DB
1263
+ module functionsFlex 'modules/functions-flex.bicep' = if (functionsPlan == 'flex') {
1264
+ name: 'functionsApp'
1265
+ params: {
1266
+ name: 'func-\${projectName}'
1267
+ location: location
1268
+ storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1269
+ appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1270
+ swaDefaultHostname: staticWebApp.outputs.defaultHostname
1271
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1272
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1273
+ enableVNet: enableVNet
1274
+ vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
1275
+ }
1276
+ dependsOn: [
1277
+ cosmosDbFreeTier
1278
+ cosmosDbServerless
1279
+ cosmosPrivateEndpoint
1280
+ ]
1281
+ }
1282
+
1283
+ module functionsPremium 'modules/functions-premium.bicep' = if (functionsPlan == 'premium') {
1284
+ name: 'functionsApp'
1285
+ params: {
1286
+ name: 'func-\${projectName}'
1287
+ location: location
1288
+ storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1289
+ appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1290
+ swaDefaultHostname: staticWebApp.outputs.defaultHostname
1291
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1292
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1293
+ enableVNet: enableVNet
1294
+ vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
1295
+ enablePrivateEndpoint: enableFullPrivate
1296
+ }
1297
+ dependsOn: [
1298
+ cosmosDbFreeTier
1299
+ cosmosDbServerless
1300
+ cosmosPrivateEndpoint
1301
+ ]
1302
+ }
1303
+
1304
+ // Functions Private Endpoint (Premium only, full private mode)
1305
+ module functionsPrivateEndpoint 'modules/private-endpoint-functions.bicep' = if (enableFullPrivate) {
1306
+ name: 'functionsPrivateEndpoint'
1307
+ params: {
1308
+ name: 'pe-func-\${projectName}'
1309
+ location: location
1310
+ functionAppId: functionsPremium.outputs.id
1311
+ functionAppName: functionsPremium.outputs.name
1312
+ subnetId: vnet.outputs.privateEndpointSubnetId
1313
+ vnetId: vnet.outputs.id
1314
+ }
1315
+ dependsOn: [
1316
+ functionsPremium
1317
+ vnet
1318
+ ]
1319
+ }
1320
+
1321
+ // Cosmos DB role assignment for Functions (after Functions is created)
1322
+ module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
1323
+ name: 'cosmosDbRoleAssignment'
1324
+ params: {
1325
+ cosmosAccountName: cosmosDbFreeTier.outputs.accountName
1326
+ functionsPrincipalId: functionsPlan == 'flex' ? functionsFlex.outputs.principalId : functionsPremium.outputs.principalId
1327
+ }
1328
+ dependsOn: [
1329
+ functionsFlex
1330
+ functionsPremium
1331
+ ]
1332
+ }
1333
+
1334
+ module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
1335
+ name: 'cosmosDbRoleAssignment'
1336
+ params: {
1337
+ cosmosAccountName: cosmosDbServerless.outputs.accountName
1338
+ functionsPrincipalId: functionsPlan == 'flex' ? functionsFlex.outputs.principalId : functionsPremium.outputs.principalId
1339
+ }
1340
+ dependsOn: [
1341
+ functionsFlex
1342
+ functionsPremium
1343
+ ]
1344
+ }
1345
+
1346
+ // Update SWA config with Functions hostname (after Functions deployment)
1347
+ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
1348
+ name: 'staticWebAppConfig'
1349
+ params: {
1350
+ staticWebAppName: staticWebApp.outputs.name
1351
+ functionsDefaultHostname: functionsPlan == 'flex' ? functionsFlex.outputs.defaultHostname : functionsPremium.outputs.defaultHostname
1352
+ appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1353
+ }
1354
+ dependsOn: [
1355
+ functionsFlex
1356
+ functionsPremium
1357
+ ]
1358
+ }
1359
+
1360
+ output staticWebAppName string = staticWebApp.outputs.name
1361
+ output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
1362
+ output functionsAppName string = functionsPlan == 'flex' ? functionsFlex.outputs.name : functionsPremium.outputs.name
1363
+ output functionsAppUrl string = functionsPlan == 'flex' ? functionsFlex.outputs.defaultHostname : functionsPremium.outputs.defaultHostname
1364
+ output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1365
+ output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1366
+ output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1367
+ output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
1368
+ output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
1369
+ output appInsightsSwaName string = appInsightsSwa.outputs.name
1370
+ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
1371
+ output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
1372
+ output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
1373
+ output vnetEnabled bool = enableVNet
1374
+ output vnetName string = enableVNet ? vnet.outputs.name : ''
1233
1375
  `;
1234
1376
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
1235
1377
  // main.parameters.json
1236
- const params = `{
1237
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
1238
- "contentVersion": "1.0.0.0",
1239
- "parameters": {
1240
- "projectName": {
1241
- "value": "${projectName}"
1242
- },
1243
- "functionsPlan": {
1244
- "value": "${azureConfig.functionsPlan}"
1245
- },
1246
- "cosmosDbMode": {
1247
- "value": "${azureConfig.cosmosDbMode}"
1248
- }
1249
- }
1250
- }
1378
+ const params = `{
1379
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
1380
+ "contentVersion": "1.0.0.0",
1381
+ "parameters": {
1382
+ "projectName": {
1383
+ "value": "${projectName}"
1384
+ },
1385
+ "functionsPlan": {
1386
+ "value": "${azureConfig.functionsPlan}"
1387
+ },
1388
+ "cosmosDbMode": {
1389
+ "value": "${azureConfig.cosmosDbMode}"
1390
+ },
1391
+ "enableVNet": {
1392
+ "value": ${enableVNet}
1393
+ },
1394
+ "enableFullPrivate": {
1395
+ "value": ${enableFullPrivate}
1396
+ }
1397
+ }
1398
+ }
1251
1399
  `;
1252
1400
  fs.writeFileSync(path.join(infraDir, 'main.parameters.json'), params);
1253
1401
  // modules/staticwebapp.bicep
1254
- const staticWebAppBicep = `@description('Static Web App name')
1255
- param name string
1256
-
1257
- @description('Location for the Static Web App')
1258
- param location string
1259
-
1260
- @description('SKU name (Free or Standard)')
1261
- @allowed([
1262
- 'Free'
1263
- 'Standard'
1264
- ])
1265
- param sku string = 'Standard'
1266
-
1267
- @description('Application Insights connection string')
1268
- param appInsightsConnectionString string
1269
-
1270
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
1271
- name: name
1272
- location: location
1273
- sku: {
1274
- name: sku
1275
- tier: sku
1276
- }
1277
- properties: {
1278
- buildProperties: {
1279
- skipGithubActionWorkflowGeneration: true
1280
- }
1281
- }
1282
- }
1283
-
1284
- // Link Application Insights to Static Web App (for both client and server-side telemetry)
1285
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1286
- parent: staticWebApp
1287
- name: 'appsettings'
1288
- properties: {
1289
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1290
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1291
- }
1292
- }
1293
-
1294
- output id string = staticWebApp.id
1295
- output name string = staticWebApp.name
1296
- output defaultHostname string = staticWebApp.properties.defaultHostname
1402
+ const staticWebAppBicep = `@description('Static Web App name')
1403
+ param name string
1404
+
1405
+ @description('Location for the Static Web App')
1406
+ param location string
1407
+
1408
+ @description('SKU name (Free or Standard)')
1409
+ @allowed([
1410
+ 'Free'
1411
+ 'Standard'
1412
+ ])
1413
+ param sku string = 'Standard'
1414
+
1415
+ @description('Application Insights connection string')
1416
+ param appInsightsConnectionString string
1417
+
1418
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
1419
+ name: name
1420
+ location: location
1421
+ sku: {
1422
+ name: sku
1423
+ tier: sku
1424
+ }
1425
+ properties: {
1426
+ buildProperties: {
1427
+ skipGithubActionWorkflowGeneration: true
1428
+ }
1429
+ }
1430
+ }
1431
+
1432
+ // Link Application Insights to Static Web App (for both client and server-side telemetry)
1433
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1434
+ parent: staticWebApp
1435
+ name: 'appsettings'
1436
+ properties: {
1437
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1438
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1439
+ }
1440
+ }
1441
+
1442
+ output id string = staticWebApp.id
1443
+ output name string = staticWebApp.name
1444
+ output defaultHostname string = staticWebApp.properties.defaultHostname
1297
1445
  `;
1298
1446
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
1299
1447
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
1300
- const staticWebAppConfigBicep = `@description('Static Web App name')
1301
- param staticWebAppName string
1302
-
1303
- @description('Functions App default hostname for backend API calls')
1304
- param functionsDefaultHostname string
1305
-
1306
- @description('Application Insights connection string for SWA')
1307
- param appInsightsConnectionString string
1308
-
1309
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1310
- name: staticWebAppName
1311
- }
1312
-
1313
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1314
- parent: staticWebApp
1315
- name: 'appsettings'
1316
- properties: {
1317
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1318
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1319
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
1320
- }
1321
- }
1322
-
1323
- output configName string = staticWebAppConfig.name
1448
+ const staticWebAppConfigBicep = `@description('Static Web App name')
1449
+ param staticWebAppName string
1450
+
1451
+ @description('Functions App default hostname for backend API calls')
1452
+ param functionsDefaultHostname string
1453
+
1454
+ @description('Application Insights connection string for SWA')
1455
+ param appInsightsConnectionString string
1456
+
1457
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1458
+ name: staticWebAppName
1459
+ }
1460
+
1461
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1462
+ parent: staticWebApp
1463
+ name: 'appsettings'
1464
+ properties: {
1465
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1466
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1467
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
1468
+ }
1469
+ }
1470
+
1471
+ output configName string = staticWebAppConfig.name
1324
1472
  `;
1325
1473
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
1326
1474
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
1327
- const logAnalyticsBicep = `@description('Log Analytics workspace name')
1328
- param name string
1329
-
1330
- @description('Location for Log Analytics workspace')
1331
- param location string
1332
-
1333
- resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
1334
- name: name
1335
- location: location
1336
- properties: {
1337
- sku: {
1338
- name: 'PerGB2018'
1339
- }
1340
- retentionInDays: 30
1341
- }
1342
- }
1343
-
1344
- output id string = logAnalytics.id
1345
- output name string = logAnalytics.name
1475
+ const logAnalyticsBicep = `@description('Log Analytics workspace name')
1476
+ param name string
1477
+
1478
+ @description('Location for Log Analytics workspace')
1479
+ param location string
1480
+
1481
+ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
1482
+ name: name
1483
+ location: location
1484
+ properties: {
1485
+ sku: {
1486
+ name: 'PerGB2018'
1487
+ }
1488
+ retentionInDays: 30
1489
+ }
1490
+ }
1491
+
1492
+ output id string = logAnalytics.id
1493
+ output name string = logAnalytics.name
1346
1494
  `;
1347
1495
  fs.writeFileSync(path.join(modulesDir, 'loganalytics.bicep'), logAnalyticsBicep);
1348
1496
  // modules/appinsights.bicep (Application Insights only, connects to shared Log Analytics)
1349
- const appInsightsBicep = `@description('Application Insights name')
1350
- param name string
1351
-
1352
- @description('Location for Application Insights')
1353
- param location string
1354
-
1355
- @description('Log Analytics workspace resource ID')
1356
- param logAnalyticsWorkspaceId string
1357
-
1358
- // Application Insights
1359
- resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
1360
- name: name
1361
- location: location
1362
- kind: 'web'
1363
- properties: {
1364
- Application_Type: 'web'
1365
- WorkspaceResourceId: logAnalyticsWorkspaceId
1366
- RetentionInDays: 30
1367
- }
1368
- }
1369
-
1370
- output id string = appInsights.id
1371
- output name string = appInsights.name
1372
- output connectionString string = appInsights.properties.ConnectionString
1373
- output instrumentationKey string = appInsights.properties.InstrumentationKey
1497
+ const appInsightsBicep = `@description('Application Insights name')
1498
+ param name string
1499
+
1500
+ @description('Location for Application Insights')
1501
+ param location string
1502
+
1503
+ @description('Log Analytics workspace resource ID')
1504
+ param logAnalyticsWorkspaceId string
1505
+
1506
+ // Application Insights
1507
+ resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
1508
+ name: name
1509
+ location: location
1510
+ kind: 'web'
1511
+ properties: {
1512
+ Application_Type: 'web'
1513
+ WorkspaceResourceId: logAnalyticsWorkspaceId
1514
+ RetentionInDays: 30
1515
+ }
1516
+ }
1517
+
1518
+ output id string = appInsights.id
1519
+ output name string = appInsights.name
1520
+ output connectionString string = appInsights.properties.ConnectionString
1521
+ output instrumentationKey string = appInsights.properties.InstrumentationKey
1374
1522
  `;
1375
1523
  fs.writeFileSync(path.join(modulesDir, 'appinsights.bicep'), appInsightsBicep);
1376
1524
  // modules/functions-flex.bicep (Flex Consumption)
1377
- const functionsFlexBicep = `@description('Functions App name')
1378
- param name string
1379
-
1380
- @description('Location for the Functions App')
1381
- param location string
1382
-
1383
- @description('Storage account name for Functions')
1384
- param storageAccountName string
1385
-
1386
- @description('Application Insights connection string')
1387
- param appInsightsConnectionString string
1388
-
1389
- @description('Static Web App default hostname for CORS')
1390
- param swaDefaultHostname string
1391
-
1392
- @description('Cosmos DB endpoint')
1393
- param cosmosDbEndpoint string
1394
-
1395
- @description('Cosmos DB database name')
1396
- param cosmosDbDatabaseName string
1397
-
1398
- // Storage Account for Functions
1399
- resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
1400
- name: storageAccountName
1401
- location: location
1402
- sku: {
1403
- name: 'Standard_LRS'
1404
- }
1405
- kind: 'StorageV2'
1406
- properties: {
1407
- supportsHttpsTrafficOnly: true
1408
- minimumTlsVersion: 'TLS1_2'
1409
- }
1410
- }
1411
-
1412
- // Blob Service for deployment package container
1413
- resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
1414
- parent: storageAccount
1415
- name: 'default'
1416
- }
1417
-
1418
- // Deployment package container
1419
- resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
1420
- parent: blobService
1421
- name: 'deploymentpackage'
1422
- properties: {
1423
- publicAccess: 'None'
1424
- }
1425
- }
1426
-
1427
- // App Service Plan (Flex Consumption)
1428
- resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
1429
- name: '\${name}-plan'
1430
- location: location
1431
- sku: {
1432
- name: 'FC1'
1433
- tier: 'FlexConsumption'
1434
- }
1435
- properties: {
1436
- reserved: true // Required for Linux
1437
- }
1438
- }
1439
-
1440
- // Azure Functions App
1441
- resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
1442
- name: name
1443
- location: location
1444
- kind: 'functionapp,linux'
1445
- identity: {
1446
- type: 'SystemAssigned'
1447
- }
1448
- properties: {
1449
- serverFarmId: hostingPlan.id
1450
- reserved: true
1451
- functionAppConfig: {
1452
- deployment: {
1453
- storage: {
1454
- type: 'blobContainer'
1455
- value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
1456
- authentication: {
1457
- type: 'SystemAssignedIdentity'
1458
- }
1459
- }
1460
- }
1461
- scaleAndConcurrency: {
1462
- maximumInstanceCount: 100
1463
- instanceMemoryMB: 2048
1464
- }
1465
- runtime: {
1466
- name: 'node'
1467
- version: '22'
1468
- }
1469
- }
1470
- siteConfig: {
1471
- appSettings: [
1472
- {
1473
- name: 'AzureWebJobsStorage__accountName'
1474
- value: storageAccount.name
1475
- }
1476
- {
1477
- name: 'FUNCTIONS_EXTENSION_VERSION'
1478
- value: '~4'
1479
- }
1480
- {
1481
- name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1482
- value: appInsightsConnectionString
1483
- }
1484
- {
1485
- name: 'CosmosDBConnection__accountEndpoint'
1486
- value: cosmosDbEndpoint
1487
- }
1488
- {
1489
- name: 'COSMOS_DB_DATABASE_NAME'
1490
- value: cosmosDbDatabaseName
1491
- }
1492
- ]
1493
- cors: {
1494
- allowedOrigins: [
1495
- 'https://\${swaDefaultHostname}'
1496
- ]
1497
- }
1498
- ipSecurityRestrictions: [
1499
- {
1500
- action: 'Allow'
1501
- ipAddress: 'AzureCloud'
1502
- tag: 'ServiceTag'
1503
- priority: 100
1504
- }
1505
- ]
1506
- }
1507
- httpsOnly: true
1508
- }
1509
- }
1510
-
1511
- // Role Assignment: Storage Blob Data Contributor
1512
- resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
1513
- name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
1514
- scope: storageAccount
1515
- properties: {
1516
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
1517
- principalId: functionApp.identity.principalId
1518
- principalType: 'ServicePrincipal'
1519
- }
1520
- }
1521
-
1522
- output id string = functionApp.id
1523
- output name string = functionApp.name
1524
- output defaultHostname string = functionApp.properties.defaultHostName
1525
- output principalId string = functionApp.identity.principalId
1525
+ const functionsFlexBicep = `@description('Functions App name')
1526
+ param name string
1527
+
1528
+ @description('Location for the Functions App')
1529
+ param location string
1530
+
1531
+ @description('Storage account name for Functions')
1532
+ param storageAccountName string
1533
+
1534
+ @description('Application Insights connection string')
1535
+ param appInsightsConnectionString string
1536
+
1537
+ @description('Static Web App default hostname for CORS')
1538
+ param swaDefaultHostname string
1539
+
1540
+ @description('Cosmos DB endpoint')
1541
+ param cosmosDbEndpoint string
1542
+
1543
+ @description('Cosmos DB database name')
1544
+ param cosmosDbDatabaseName string
1545
+
1546
+ @description('Enable VNet integration')
1547
+ param enableVNet bool = false
1548
+
1549
+ @description('VNet subnet ID for Functions (required if enableVNet is true)')
1550
+ param vnetSubnetId string = ''
1551
+
1552
+ // Storage Account for Functions
1553
+ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
1554
+ name: storageAccountName
1555
+ location: location
1556
+ sku: {
1557
+ name: 'Standard_LRS'
1558
+ }
1559
+ kind: 'StorageV2'
1560
+ properties: {
1561
+ supportsHttpsTrafficOnly: true
1562
+ minimumTlsVersion: 'TLS1_2'
1563
+ }
1564
+ }
1565
+
1566
+ // Blob Service for deployment package container
1567
+ resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
1568
+ parent: storageAccount
1569
+ name: 'default'
1570
+ }
1571
+
1572
+ // Deployment package container
1573
+ resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
1574
+ parent: blobService
1575
+ name: 'deploymentpackage'
1576
+ properties: {
1577
+ publicAccess: 'None'
1578
+ }
1579
+ }
1580
+
1581
+ // App Service Plan (Flex Consumption)
1582
+ resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
1583
+ name: '\${name}-plan'
1584
+ location: location
1585
+ sku: {
1586
+ name: 'FC1'
1587
+ tier: 'FlexConsumption'
1588
+ }
1589
+ properties: {
1590
+ reserved: true // Required for Linux
1591
+ }
1592
+ }
1593
+
1594
+ // Azure Functions App
1595
+ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
1596
+ name: name
1597
+ location: location
1598
+ kind: 'functionapp,linux'
1599
+ identity: {
1600
+ type: 'SystemAssigned'
1601
+ }
1602
+ properties: {
1603
+ serverFarmId: hostingPlan.id
1604
+ reserved: true
1605
+ virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
1606
+ vnetContentShareEnabled: enableVNet
1607
+ functionAppConfig: {
1608
+ deployment: {
1609
+ storage: {
1610
+ type: 'blobContainer'
1611
+ value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
1612
+ authentication: {
1613
+ type: 'SystemAssignedIdentity'
1614
+ }
1615
+ }
1616
+ }
1617
+ scaleAndConcurrency: {
1618
+ maximumInstanceCount: 100
1619
+ instanceMemoryMB: 2048
1620
+ }
1621
+ runtime: {
1622
+ name: 'node'
1623
+ version: '22'
1624
+ }
1625
+ }
1626
+ siteConfig: {
1627
+ appSettings: [
1628
+ {
1629
+ name: 'AzureWebJobsStorage__accountName'
1630
+ value: storageAccount.name
1631
+ }
1632
+ {
1633
+ name: 'FUNCTIONS_EXTENSION_VERSION'
1634
+ value: '~4'
1635
+ }
1636
+ {
1637
+ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1638
+ value: appInsightsConnectionString
1639
+ }
1640
+ {
1641
+ name: 'CosmosDBConnection__accountEndpoint'
1642
+ value: cosmosDbEndpoint
1643
+ }
1644
+ {
1645
+ name: 'COSMOS_DB_DATABASE_NAME'
1646
+ value: cosmosDbDatabaseName
1647
+ }
1648
+ ]
1649
+ cors: {
1650
+ allowedOrigins: [
1651
+ 'https://\${swaDefaultHostname}'
1652
+ ]
1653
+ }
1654
+ ipSecurityRestrictions: [
1655
+ {
1656
+ action: 'Allow'
1657
+ ipAddress: 'AzureCloud'
1658
+ tag: 'ServiceTag'
1659
+ priority: 100
1660
+ }
1661
+ ]
1662
+ }
1663
+ httpsOnly: true
1664
+ }
1665
+ }
1666
+
1667
+ // Role Assignment: Storage Blob Data Contributor
1668
+ resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
1669
+ name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
1670
+ scope: storageAccount
1671
+ properties: {
1672
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
1673
+ principalId: functionApp.identity.principalId
1674
+ principalType: 'ServicePrincipal'
1675
+ }
1676
+ }
1677
+
1678
+ output id string = functionApp.id
1679
+ output name string = functionApp.name
1680
+ output defaultHostname string = functionApp.properties.defaultHostName
1681
+ output principalId string = functionApp.identity.principalId
1526
1682
  `;
1527
1683
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
1528
1684
  // modules/functions-premium.bicep (Premium Plan)
1529
- const functionsPremiumBicep = `@description('Functions App name')
1530
- param name string
1531
-
1532
- @description('Location for the Functions App')
1533
- param location string
1534
-
1535
- @description('Storage account name for Functions')
1536
- param storageAccountName string
1537
-
1538
- @description('Application Insights connection string')
1539
- param appInsightsConnectionString string
1540
-
1541
- @description('Static Web App default hostname for CORS')
1542
- param swaDefaultHostname string
1543
-
1544
- @description('Cosmos DB endpoint')
1545
- param cosmosDbEndpoint string
1546
-
1547
- @description('Cosmos DB database name')
1548
- param cosmosDbDatabaseName string
1549
-
1550
- // Storage Account for Functions
1551
- resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
1552
- name: storageAccountName
1553
- location: location
1554
- sku: {
1555
- name: 'Standard_LRS'
1556
- }
1557
- kind: 'StorageV2'
1558
- properties: {
1559
- supportsHttpsTrafficOnly: true
1560
- minimumTlsVersion: 'TLS1_2'
1561
- }
1562
- }
1563
-
1564
- // App Service Plan (Premium)
1565
- resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
1566
- name: '\${name}-plan'
1567
- location: location
1568
- sku: {
1569
- name: 'EP1'
1570
- tier: 'ElasticPremium'
1571
- }
1572
- properties: {
1573
- reserved: true // Required for Linux
1574
- maximumElasticWorkerCount: 20
1575
- }
1576
- }
1577
-
1578
- // Azure Functions App
1579
- resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
1580
- name: name
1581
- location: location
1582
- kind: 'functionapp,linux'
1583
- identity: {
1584
- type: 'SystemAssigned'
1585
- }
1586
- properties: {
1587
- serverFarmId: hostingPlan.id
1588
- reserved: true
1589
- siteConfig: {
1590
- linuxFxVersion: 'NODE|22'
1591
- appSettings: [
1592
- {
1593
- name: 'AzureWebJobsStorage'
1594
- value: 'DefaultEndpointsProtocol=https;AccountName=\${storageAccount.name};EndpointSuffix=\${environment().suffixes.storage};AccountKey=\${storageAccount.listKeys().keys[0].value}'
1595
- }
1596
- {
1597
- name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
1598
- value: 'DefaultEndpointsProtocol=https;AccountName=\${storageAccount.name};EndpointSuffix=\${environment().suffixes.storage};AccountKey=\${storageAccount.listKeys().keys[0].value}'
1599
- }
1600
- {
1601
- name: 'WEBSITE_CONTENTSHARE'
1602
- value: toLower(name)
1603
- }
1604
- {
1605
- name: 'FUNCTIONS_EXTENSION_VERSION'
1606
- value: '~4'
1607
- }
1608
- {
1609
- name: 'FUNCTIONS_WORKER_RUNTIME'
1610
- value: 'node'
1611
- }
1612
- {
1613
- name: 'WEBSITE_NODE_DEFAULT_VERSION'
1614
- value: '~22'
1615
- }
1616
- {
1617
- name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1618
- value: appInsightsConnectionString
1619
- }
1620
- {
1621
- name: 'CosmosDBConnection__accountEndpoint'
1622
- value: cosmosDbEndpoint
1623
- }
1624
- {
1625
- name: 'COSMOS_DB_DATABASE_NAME'
1626
- value: cosmosDbDatabaseName
1627
- }
1628
- ]
1629
- cors: {
1630
- allowedOrigins: [
1631
- 'https://\${swaDefaultHostname}'
1632
- ]
1633
- }
1634
- ipSecurityRestrictions: [
1635
- {
1636
- action: 'Allow'
1637
- ipAddress: 'AzureCloud'
1638
- tag: 'ServiceTag'
1639
- priority: 100
1640
- }
1641
- ]
1642
- alwaysOn: true
1643
- }
1644
- httpsOnly: true
1645
- }
1646
- }
1647
-
1648
- output id string = functionApp.id
1649
- output name string = functionApp.name
1650
- output defaultHostname string = functionApp.properties.defaultHostName
1651
- output principalId string = functionApp.identity.principalId
1685
+ const functionsPremiumBicep = `@description('Functions App name')
1686
+ param name string
1687
+
1688
+ @description('Location for the Functions App')
1689
+ param location string
1690
+
1691
+ @description('Storage account name for Functions')
1692
+ param storageAccountName string
1693
+
1694
+ @description('Application Insights connection string')
1695
+ param appInsightsConnectionString string
1696
+
1697
+ @description('Static Web App default hostname for CORS')
1698
+ param swaDefaultHostname string
1699
+
1700
+ @description('Cosmos DB endpoint')
1701
+ param cosmosDbEndpoint string
1702
+
1703
+ @description('Cosmos DB database name')
1704
+ param cosmosDbDatabaseName string
1705
+
1706
+ @description('Enable VNet integration')
1707
+ param enableVNet bool = false
1708
+
1709
+ @description('VNet subnet ID for Functions (required if enableVNet is true)')
1710
+ param vnetSubnetId string = ''
1711
+
1712
+ @description('Enable private endpoint for inbound traffic')
1713
+ param enablePrivateEndpoint bool = false
1714
+
1715
+ // Storage Account for Functions
1716
+ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
1717
+ name: storageAccountName
1718
+ location: location
1719
+ sku: {
1720
+ name: 'Standard_LRS'
1721
+ }
1722
+ kind: 'StorageV2'
1723
+ properties: {
1724
+ supportsHttpsTrafficOnly: true
1725
+ minimumTlsVersion: 'TLS1_2'
1726
+ }
1727
+ }
1728
+
1729
+ // App Service Plan (Premium)
1730
+ resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
1731
+ name: '\${name}-plan'
1732
+ location: location
1733
+ sku: {
1734
+ name: 'EP1'
1735
+ tier: 'ElasticPremium'
1736
+ }
1737
+ properties: {
1738
+ reserved: true // Required for Linux
1739
+ maximumElasticWorkerCount: 20
1740
+ }
1741
+ }
1742
+
1743
+ // Azure Functions App
1744
+ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
1745
+ name: name
1746
+ location: location
1747
+ kind: 'functionapp,linux'
1748
+ identity: {
1749
+ type: 'SystemAssigned'
1750
+ }
1751
+ properties: {
1752
+ serverFarmId: hostingPlan.id
1753
+ reserved: true
1754
+ virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
1755
+ publicNetworkAccess: enablePrivateEndpoint ? 'Disabled' : 'Enabled'
1756
+ siteConfig: {
1757
+ linuxFxVersion: 'NODE|22'
1758
+ appSettings: [
1759
+ {
1760
+ name: 'AzureWebJobsStorage'
1761
+ value: 'DefaultEndpointsProtocol=https;AccountName=\${storageAccount.name};EndpointSuffix=\${environment().suffixes.storage};AccountKey=\${storageAccount.listKeys().keys[0].value}'
1762
+ }
1763
+ {
1764
+ name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
1765
+ value: 'DefaultEndpointsProtocol=https;AccountName=\${storageAccount.name};EndpointSuffix=\${environment().suffixes.storage};AccountKey=\${storageAccount.listKeys().keys[0].value}'
1766
+ }
1767
+ {
1768
+ name: 'WEBSITE_CONTENTSHARE'
1769
+ value: toLower(name)
1770
+ }
1771
+ {
1772
+ name: 'FUNCTIONS_EXTENSION_VERSION'
1773
+ value: '~4'
1774
+ }
1775
+ {
1776
+ name: 'FUNCTIONS_WORKER_RUNTIME'
1777
+ value: 'node'
1778
+ }
1779
+ {
1780
+ name: 'WEBSITE_NODE_DEFAULT_VERSION'
1781
+ value: '~22'
1782
+ }
1783
+ {
1784
+ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
1785
+ value: appInsightsConnectionString
1786
+ }
1787
+ {
1788
+ name: 'CosmosDBConnection__accountEndpoint'
1789
+ value: cosmosDbEndpoint
1790
+ }
1791
+ {
1792
+ name: 'COSMOS_DB_DATABASE_NAME'
1793
+ value: cosmosDbDatabaseName
1794
+ }
1795
+ ]
1796
+ cors: {
1797
+ allowedOrigins: [
1798
+ 'https://\${swaDefaultHostname}'
1799
+ ]
1800
+ }
1801
+ ipSecurityRestrictions: enablePrivateEndpoint ? [] : [
1802
+ {
1803
+ action: 'Allow'
1804
+ ipAddress: 'AzureCloud'
1805
+ tag: 'ServiceTag'
1806
+ priority: 100
1807
+ }
1808
+ ]
1809
+ alwaysOn: true
1810
+ }
1811
+ httpsOnly: true
1812
+ }
1813
+ }
1814
+
1815
+ output id string = functionApp.id
1816
+ output name string = functionApp.name
1817
+ output defaultHostname string = functionApp.properties.defaultHostName
1818
+ output principalId string = functionApp.identity.principalId
1652
1819
  `;
1653
1820
  fs.writeFileSync(path.join(modulesDir, 'functions-premium.bicep'), functionsPremiumBicep);
1654
1821
  // modules/cosmosdb-freetier.bicep (Free Tier)
1655
- const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
1656
- param accountName string
1657
-
1658
- @description('Database name')
1659
- param databaseName string
1660
-
1661
- @description('Location for Cosmos DB')
1662
- param location string
1663
-
1664
- // Cosmos DB Account (Free Tier)
1665
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
1666
- name: accountName
1667
- location: location
1668
- kind: 'GlobalDocumentDB'
1669
- properties: {
1670
- databaseAccountOfferType: 'Standard'
1671
- enableAutomaticFailover: false
1672
- enableFreeTier: true
1673
- publicNetworkAccess: 'Enabled'
1674
- disableLocalAuth: true
1675
- consistencyPolicy: {
1676
- defaultConsistencyLevel: 'Session'
1677
- }
1678
- locations: [
1679
- {
1680
- locationName: location
1681
- failoverPriority: 0
1682
- isZoneRedundant: false
1683
- }
1684
- ]
1685
- disableKeyBasedMetadataWriteAccess: true
1686
- }
1687
- }
1688
-
1689
- // Cosmos DB Database
1690
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
1691
- parent: cosmosAccount
1692
- name: databaseName
1693
- properties: {
1694
- resource: {
1695
- id: databaseName
1696
- }
1697
- options: {
1698
- throughput: 1000
1699
- }
1700
- }
1701
- }
1702
-
1703
- output accountName string = cosmosAccount.name
1704
- output endpoint string = cosmosAccount.properties.documentEndpoint
1705
- output databaseName string = database.name
1822
+ const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
1823
+ param accountName string
1824
+
1825
+ @description('Database name')
1826
+ param databaseName string
1827
+
1828
+ @description('Location for Cosmos DB')
1829
+ param location string
1830
+
1831
+ @description('Public network access')
1832
+ @allowed(['Enabled', 'Disabled'])
1833
+ param publicNetworkAccess string = 'Enabled'
1834
+
1835
+ // Cosmos DB Account (Free Tier)
1836
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
1837
+ name: accountName
1838
+ location: location
1839
+ kind: 'GlobalDocumentDB'
1840
+ properties: {
1841
+ databaseAccountOfferType: 'Standard'
1842
+ enableAutomaticFailover: false
1843
+ enableFreeTier: true
1844
+ publicNetworkAccess: publicNetworkAccess
1845
+ disableLocalAuth: true
1846
+ consistencyPolicy: {
1847
+ defaultConsistencyLevel: 'Session'
1848
+ }
1849
+ locations: [
1850
+ {
1851
+ locationName: location
1852
+ failoverPriority: 0
1853
+ isZoneRedundant: false
1854
+ }
1855
+ ]
1856
+ disableKeyBasedMetadataWriteAccess: true
1857
+ }
1858
+ }
1859
+
1860
+ // Cosmos DB Database
1861
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
1862
+ parent: cosmosAccount
1863
+ name: databaseName
1864
+ properties: {
1865
+ resource: {
1866
+ id: databaseName
1867
+ }
1868
+ options: {
1869
+ throughput: 1000
1870
+ }
1871
+ }
1872
+ }
1873
+
1874
+ output id string = cosmosAccount.id
1875
+ output accountName string = cosmosAccount.name
1876
+ output endpoint string = cosmosAccount.properties.documentEndpoint
1877
+ output databaseName string = database.name
1706
1878
  `;
1707
1879
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
1708
1880
  // modules/cosmosdb-serverless.bicep (Serverless)
1709
- const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
1710
- param accountName string
1711
-
1712
- @description('Database name')
1713
- param databaseName string
1714
-
1715
- @description('Location for Cosmos DB')
1716
- param location string
1717
-
1718
- // Cosmos DB Account (Serverless)
1719
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
1720
- name: accountName
1721
- location: location
1722
- kind: 'GlobalDocumentDB'
1723
- properties: {
1724
- databaseAccountOfferType: 'Standard'
1725
- enableAutomaticFailover: false
1726
- publicNetworkAccess: 'Enabled'
1727
- disableLocalAuth: true
1728
- consistencyPolicy: {
1729
- defaultConsistencyLevel: 'Session'
1730
- }
1731
- locations: [
1732
- {
1733
- locationName: location
1734
- failoverPriority: 0
1735
- isZoneRedundant: false
1736
- }
1737
- ]
1738
- capabilities: [
1739
- {
1740
- name: 'EnableServerless'
1741
- }
1742
- ]
1743
- disableKeyBasedMetadataWriteAccess: true
1744
- }
1745
- }
1746
-
1747
- // Cosmos DB Database
1748
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
1749
- parent: cosmosAccount
1750
- name: databaseName
1751
- properties: {
1752
- resource: {
1753
- id: databaseName
1754
- }
1755
- }
1756
- }
1757
-
1758
- output accountName string = cosmosAccount.name
1759
- output endpoint string = cosmosAccount.properties.documentEndpoint
1760
- output databaseName string = database.name
1881
+ const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
1882
+ param accountName string
1883
+
1884
+ @description('Database name')
1885
+ param databaseName string
1886
+
1887
+ @description('Location for Cosmos DB')
1888
+ param location string
1889
+
1890
+ @description('Public network access')
1891
+ @allowed(['Enabled', 'Disabled'])
1892
+ param publicNetworkAccess string = 'Enabled'
1893
+
1894
+ // Cosmos DB Account (Serverless)
1895
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
1896
+ name: accountName
1897
+ location: location
1898
+ kind: 'GlobalDocumentDB'
1899
+ properties: {
1900
+ databaseAccountOfferType: 'Standard'
1901
+ enableAutomaticFailover: false
1902
+ publicNetworkAccess: publicNetworkAccess
1903
+ disableLocalAuth: true
1904
+ consistencyPolicy: {
1905
+ defaultConsistencyLevel: 'Session'
1906
+ }
1907
+ locations: [
1908
+ {
1909
+ locationName: location
1910
+ failoverPriority: 0
1911
+ isZoneRedundant: false
1912
+ }
1913
+ ]
1914
+ capabilities: [
1915
+ {
1916
+ name: 'EnableServerless'
1917
+ }
1918
+ ]
1919
+ disableKeyBasedMetadataWriteAccess: true
1920
+ }
1921
+ }
1922
+
1923
+ // Cosmos DB Database
1924
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
1925
+ parent: cosmosAccount
1926
+ name: databaseName
1927
+ properties: {
1928
+ resource: {
1929
+ id: databaseName
1930
+ }
1931
+ }
1932
+ }
1933
+
1934
+ output id string = cosmosAccount.id
1935
+ output accountName string = cosmosAccount.name
1936
+ output endpoint string = cosmosAccount.properties.documentEndpoint
1937
+ output databaseName string = database.name
1761
1938
  `;
1762
1939
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-serverless.bicep'), cosmosDbServerlessBicep);
1763
1940
  // modules/cosmosdb-role-assignment.bicep (Role Assignment Module)
1764
- const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
1765
- param cosmosAccountName string
1766
-
1767
- @description('Functions App Managed Identity Principal ID')
1768
- param functionsPrincipalId string
1769
-
1770
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
1771
- name: cosmosAccountName
1772
- }
1773
-
1774
- // Built-in Cosmos DB Data Contributor role definition
1775
- var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
1776
-
1777
- // Role assignment for Functions to access Cosmos DB
1778
- resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
1779
- parent: cosmosAccount
1780
- name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
1781
- properties: {
1782
- roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
1783
- principalId: functionsPrincipalId
1784
- scope: cosmosAccount.id
1785
- }
1786
- }
1787
-
1788
- output roleAssignmentId string = roleAssignment.id
1941
+ const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
1942
+ param cosmosAccountName string
1943
+
1944
+ @description('Functions App Managed Identity Principal ID')
1945
+ param functionsPrincipalId string
1946
+
1947
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
1948
+ name: cosmosAccountName
1949
+ }
1950
+
1951
+ // Built-in Cosmos DB Data Contributor role definition
1952
+ var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
1953
+
1954
+ // Role assignment for Functions to access Cosmos DB
1955
+ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
1956
+ parent: cosmosAccount
1957
+ name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
1958
+ properties: {
1959
+ roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
1960
+ principalId: functionsPrincipalId
1961
+ scope: cosmosAccount.id
1962
+ }
1963
+ }
1964
+
1965
+ output roleAssignmentId string = roleAssignment.id
1789
1966
  `;
1790
1967
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
1968
+ // VNet modules (only generate if VNet is enabled)
1969
+ if (enableVNet) {
1970
+ // modules/vnet.bicep
1971
+ const vnetBicep = `@description('VNet name')
1972
+ param name string
1973
+
1974
+ @description('Location for VNet')
1975
+ param location string
1976
+
1977
+ @description('Functions plan type (affects subnet delegation)')
1978
+ @allowed(['flex', 'premium'])
1979
+ param functionsPlan string
1980
+
1981
+ var functionsSubnetDelegation = functionsPlan == 'flex' ? 'Microsoft.App/environments' : 'Microsoft.Web/serverFarms'
1982
+
1983
+ resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
1984
+ name: name
1985
+ location: location
1986
+ properties: {
1987
+ addressSpace: {
1988
+ addressPrefixes: [
1989
+ '10.0.0.0/16'
1990
+ ]
1991
+ }
1992
+ subnets: [
1993
+ {
1994
+ name: 'snet-functions'
1995
+ properties: {
1996
+ addressPrefix: '10.0.1.0/24'
1997
+ delegations: [
1998
+ {
1999
+ name: 'delegation'
2000
+ properties: {
2001
+ serviceName: functionsSubnetDelegation
2002
+ }
2003
+ }
2004
+ ]
2005
+ }
2006
+ }
2007
+ {
2008
+ name: 'snet-private-endpoints'
2009
+ properties: {
2010
+ addressPrefix: '10.0.2.0/24'
2011
+ privateEndpointNetworkPolicies: 'Disabled'
2012
+ }
2013
+ }
2014
+ ]
2015
+ }
2016
+ }
2017
+
2018
+ output id string = vnet.id
2019
+ output name string = vnet.name
2020
+ output functionsSubnetId string = vnet.properties.subnets[0].id
2021
+ output privateEndpointSubnetId string = vnet.properties.subnets[1].id
2022
+ `;
2023
+ fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2024
+ // modules/private-endpoint-cosmos.bicep
2025
+ const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2026
+ param name string
2027
+
2028
+ @description('Location')
2029
+ param location string
2030
+
2031
+ @description('Cosmos DB account resource ID')
2032
+ param cosmosAccountId string
2033
+
2034
+ @description('Cosmos DB account name')
2035
+ param cosmosAccountName string
2036
+
2037
+ @description('Subnet ID for private endpoint')
2038
+ param subnetId string
2039
+
2040
+ @description('VNet ID for DNS zone link')
2041
+ param vnetId string
2042
+
2043
+ // Private DNS Zone for Cosmos DB
2044
+ resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
2045
+ name: 'privatelink.documents.azure.com'
2046
+ location: 'global'
2047
+ }
2048
+
2049
+ // Link DNS Zone to VNet
2050
+ resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
2051
+ parent: privateDnsZone
2052
+ name: '\${cosmosAccountName}-vnet-link'
2053
+ location: 'global'
2054
+ properties: {
2055
+ virtualNetwork: {
2056
+ id: vnetId
2057
+ }
2058
+ registrationEnabled: false
2059
+ }
2060
+ }
2061
+
2062
+ // Private Endpoint for Cosmos DB
2063
+ resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
2064
+ name: name
2065
+ location: location
2066
+ properties: {
2067
+ subnet: {
2068
+ id: subnetId
2069
+ }
2070
+ privateLinkServiceConnections: [
2071
+ {
2072
+ name: '\${cosmosAccountName}-connection'
2073
+ properties: {
2074
+ privateLinkServiceId: cosmosAccountId
2075
+ groupIds: [
2076
+ 'Sql'
2077
+ ]
2078
+ }
2079
+ }
2080
+ ]
2081
+ }
2082
+ }
2083
+
2084
+ // DNS Zone Group
2085
+ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
2086
+ parent: privateEndpoint
2087
+ name: 'default'
2088
+ properties: {
2089
+ privateDnsZoneConfigs: [
2090
+ {
2091
+ name: 'cosmos-dns-config'
2092
+ properties: {
2093
+ privateDnsZoneId: privateDnsZone.id
2094
+ }
2095
+ }
2096
+ ]
2097
+ }
2098
+ }
2099
+
2100
+ output privateEndpointId string = privateEndpoint.id
2101
+ output privateDnsZoneId string = privateDnsZone.id
2102
+ `;
2103
+ fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2104
+ // modules/private-endpoint-functions.bicep (for Premium plan full private mode)
2105
+ const functionsPrivateEndpointBicep = `@description('Private endpoint name')
2106
+ param name string
2107
+
2108
+ @description('Location')
2109
+ param location string
2110
+
2111
+ @description('Functions App resource ID')
2112
+ param functionAppId string
2113
+
2114
+ @description('Functions App name')
2115
+ param functionAppName string
2116
+
2117
+ @description('Subnet ID for private endpoint')
2118
+ param subnetId string
2119
+
2120
+ @description('VNet ID for DNS zone link')
2121
+ param vnetId string
2122
+
2123
+ // Private DNS Zone for Azure Websites
2124
+ resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
2125
+ name: 'privatelink.azurewebsites.net'
2126
+ location: 'global'
2127
+ }
2128
+
2129
+ // Link DNS Zone to VNet
2130
+ resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
2131
+ parent: privateDnsZone
2132
+ name: '\${functionAppName}-vnet-link'
2133
+ location: 'global'
2134
+ properties: {
2135
+ virtualNetwork: {
2136
+ id: vnetId
2137
+ }
2138
+ registrationEnabled: false
2139
+ }
2140
+ }
2141
+
2142
+ // Private Endpoint for Functions
2143
+ resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
2144
+ name: name
2145
+ location: location
2146
+ properties: {
2147
+ subnet: {
2148
+ id: subnetId
2149
+ }
2150
+ privateLinkServiceConnections: [
2151
+ {
2152
+ name: '\${functionAppName}-connection'
2153
+ properties: {
2154
+ privateLinkServiceId: functionAppId
2155
+ groupIds: [
2156
+ 'sites'
2157
+ ]
2158
+ }
2159
+ }
2160
+ ]
2161
+ }
2162
+ }
2163
+
2164
+ // DNS Zone Group
2165
+ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
2166
+ parent: privateEndpoint
2167
+ name: 'default'
2168
+ properties: {
2169
+ privateDnsZoneConfigs: [
2170
+ {
2171
+ name: 'functions-dns-config'
2172
+ properties: {
2173
+ privateDnsZoneId: privateDnsZone.id
2174
+ }
2175
+ }
2176
+ ]
2177
+ }
2178
+ }
2179
+
2180
+ output privateEndpointId string = privateEndpoint.id
2181
+ output privateDnsZoneId string = privateDnsZone.id
2182
+ `;
2183
+ fs.writeFileSync(path.join(modulesDir, 'private-endpoint-functions.bicep'), functionsPrivateEndpointBicep);
2184
+ console.log('✅ VNet modules created\n');
2185
+ }
1791
2186
  console.log('✅ Infrastructure files created\n');
1792
2187
  }
1793
2188
  async function createGitHubActionsWorkflows(projectDir, azureConfig) {
@@ -1795,104 +2190,104 @@ async function createGitHubActionsWorkflows(projectDir, azureConfig) {
1795
2190
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
1796
2191
  fs.mkdirSync(workflowsDir, { recursive: true });
1797
2192
  // deploy-swa.yml
1798
- const swaWorkflow = `name: Deploy Static Web App
1799
-
1800
- on:
1801
- push:
1802
- branches:
1803
- - main
1804
- paths:
1805
- - 'app/**'
1806
- - 'components/**'
1807
- - 'lib/**'
1808
- - 'public/**'
1809
- - 'package.json'
1810
- - 'next.config.js'
1811
- - 'next.config.ts'
1812
- workflow_dispatch:
1813
- pull_request:
1814
- branches:
1815
- - main
1816
- paths:
1817
- - 'app/**'
1818
- - 'components/**'
1819
- - 'lib/**'
1820
- - 'public/**'
1821
- - 'package.json'
1822
- - 'next.config.js'
1823
- - 'next.config.ts'
1824
-
1825
- jobs:
1826
- build-and-deploy:
1827
- runs-on: ubuntu-latest
1828
- name: Build and Deploy Static Web App
1829
-
1830
- steps:
1831
- - uses: actions/checkout@v4
1832
- with:
1833
- submodules: true
1834
-
1835
- - name: Deploy to Azure Static Web Apps
1836
- if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
1837
- uses: Azure/static-web-apps-deploy@v1
1838
- with:
1839
- azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
1840
- repo_token: \${{ secrets.GITHUB_TOKEN }}
1841
- action: 'upload'
1842
- app_location: '/'
1843
- api_location: ''
1844
- output_location: ''
1845
- env:
1846
- NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
2193
+ const swaWorkflow = `name: Deploy Static Web App
2194
+
2195
+ on:
2196
+ push:
2197
+ branches:
2198
+ - main
2199
+ paths:
2200
+ - 'app/**'
2201
+ - 'components/**'
2202
+ - 'lib/**'
2203
+ - 'public/**'
2204
+ - 'package.json'
2205
+ - 'next.config.js'
2206
+ - 'next.config.ts'
2207
+ workflow_dispatch:
2208
+ pull_request:
2209
+ branches:
2210
+ - main
2211
+ paths:
2212
+ - 'app/**'
2213
+ - 'components/**'
2214
+ - 'lib/**'
2215
+ - 'public/**'
2216
+ - 'package.json'
2217
+ - 'next.config.js'
2218
+ - 'next.config.ts'
2219
+
2220
+ jobs:
2221
+ build-and-deploy:
2222
+ runs-on: ubuntu-latest
2223
+ name: Build and Deploy Static Web App
2224
+
2225
+ steps:
2226
+ - uses: actions/checkout@v4
2227
+ with:
2228
+ submodules: true
2229
+
2230
+ - name: Deploy to Azure Static Web Apps
2231
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2232
+ uses: Azure/static-web-apps-deploy@v1
2233
+ with:
2234
+ azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
2235
+ repo_token: \${{ secrets.GITHUB_TOKEN }}
2236
+ action: 'upload'
2237
+ app_location: '/'
2238
+ api_location: ''
2239
+ output_location: ''
2240
+ env:
2241
+ NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
1847
2242
  `;
1848
2243
  fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
1849
2244
  // deploy-functions.yml
1850
- const functionsWorkflow = `name: Deploy Azure Functions
1851
-
1852
- on:
1853
- push:
1854
- branches:
1855
- - main
1856
- paths:
1857
- - 'functions/**'
1858
- pull_request:
1859
- branches:
1860
- - main
1861
- paths:
1862
- - 'functions/**'
1863
- workflow_dispatch:
1864
-
1865
- jobs:
1866
- build-and-deploy:
1867
- runs-on: ubuntu-latest
1868
- name: Build and Deploy Functions
1869
-
1870
- steps:
1871
- - uses: actions/checkout@v4
1872
-
1873
- - name: Setup Node.js
1874
- uses: actions/setup-node@v4
1875
- with:
1876
- node-version: '22'
1877
-
1878
- - name: Install dependencies
1879
- run: |
1880
- cd functions
1881
- npm install
1882
-
1883
- - name: Build Functions
1884
- run: |
1885
- cd functions
1886
- npm run build
1887
-
1888
- - name: Deploy to Azure Functions
1889
- if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
1890
- uses: Azure/functions-action@v1
1891
- with:
1892
- app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
1893
- package: './functions'
1894
- publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}${azureConfig.functionsPlan === 'flex' ? `
1895
- sku: flexconsumption` : ''}
2245
+ const functionsWorkflow = `name: Deploy Azure Functions
2246
+
2247
+ on:
2248
+ push:
2249
+ branches:
2250
+ - main
2251
+ paths:
2252
+ - 'functions/**'
2253
+ pull_request:
2254
+ branches:
2255
+ - main
2256
+ paths:
2257
+ - 'functions/**'
2258
+ workflow_dispatch:
2259
+
2260
+ jobs:
2261
+ build-and-deploy:
2262
+ runs-on: ubuntu-latest
2263
+ name: Build and Deploy Functions
2264
+
2265
+ steps:
2266
+ - uses: actions/checkout@v4
2267
+
2268
+ - name: Setup Node.js
2269
+ uses: actions/setup-node@v4
2270
+ with:
2271
+ node-version: '22'
2272
+
2273
+ - name: Install dependencies
2274
+ run: |
2275
+ cd functions
2276
+ npm install
2277
+
2278
+ - name: Build Functions
2279
+ run: |
2280
+ cd functions
2281
+ npm run build
2282
+
2283
+ - name: Deploy to Azure Functions
2284
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2285
+ uses: Azure/functions-action@v1
2286
+ with:
2287
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2288
+ package: './functions'
2289
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}${azureConfig.functionsPlan === 'flex' ? `
2290
+ sku: flexconsumption` : ''}
1896
2291
  `;
1897
2292
  fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
1898
2293
  console.log('✅ GitHub Actions workflows created\n');
@@ -1902,127 +2297,127 @@ async function createAzurePipelines(projectDir) {
1902
2297
  const pipelinesDir = path.join(projectDir, 'pipelines');
1903
2298
  fs.mkdirSync(pipelinesDir, { recursive: true });
1904
2299
  // swa.yml
1905
- const swaPipeline = `trigger:
1906
- branches:
1907
- include:
1908
- - main
1909
- paths:
1910
- include:
1911
- - app/**
1912
- - components/**
1913
- - lib/**
1914
- - public/**
1915
- - package.json
1916
- - next.config.js
1917
-
1918
- pr:
1919
- branches:
1920
- include:
1921
- - main
1922
- paths:
1923
- include:
1924
- - app/**
1925
- - components/**
1926
- - lib/**
1927
- - public/**
1928
- - package.json
1929
- - next.config.js
1930
-
1931
- pool:
1932
- vmImage: 'ubuntu-latest'
1933
-
1934
- variables:
1935
- - group: azure-deployment
1936
-
1937
- steps:
1938
- - task: NodeTool@0
1939
- inputs:
1940
- versionSpec: '22.x'
1941
- displayName: 'Install Node.js'
1942
-
1943
- - script: |
1944
- npm ci
1945
- displayName: 'Install dependencies'
1946
-
1947
- - script: |
1948
- npm run build
1949
- env:
1950
- NODE_ENV: production
1951
- displayName: 'Build Next.js app'
1952
-
1953
- - task: AzureStaticWebApp@0
1954
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
1955
- inputs:
1956
- app_location: '.'
1957
- output_location: '.next/standalone'
1958
- skip_app_build: true
1959
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
1960
- displayName: 'Deploy to Azure Static Web Apps'
2300
+ const swaPipeline = `trigger:
2301
+ branches:
2302
+ include:
2303
+ - main
2304
+ paths:
2305
+ include:
2306
+ - app/**
2307
+ - components/**
2308
+ - lib/**
2309
+ - public/**
2310
+ - package.json
2311
+ - next.config.js
2312
+
2313
+ pr:
2314
+ branches:
2315
+ include:
2316
+ - main
2317
+ paths:
2318
+ include:
2319
+ - app/**
2320
+ - components/**
2321
+ - lib/**
2322
+ - public/**
2323
+ - package.json
2324
+ - next.config.js
2325
+
2326
+ pool:
2327
+ vmImage: 'ubuntu-latest'
2328
+
2329
+ variables:
2330
+ - group: azure-deployment
2331
+
2332
+ steps:
2333
+ - task: NodeTool@0
2334
+ inputs:
2335
+ versionSpec: '22.x'
2336
+ displayName: 'Install Node.js'
2337
+
2338
+ - script: |
2339
+ npm ci
2340
+ displayName: 'Install dependencies'
2341
+
2342
+ - script: |
2343
+ npm run build
2344
+ env:
2345
+ NODE_ENV: production
2346
+ displayName: 'Build Next.js app'
2347
+
2348
+ - task: AzureStaticWebApp@0
2349
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2350
+ inputs:
2351
+ app_location: '.'
2352
+ output_location: '.next/standalone'
2353
+ skip_app_build: true
2354
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
2355
+ displayName: 'Deploy to Azure Static Web Apps'
1961
2356
  `;
1962
2357
  fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
1963
2358
  // functions.yml
1964
- const functionsPipeline = `trigger:
1965
- branches:
1966
- include:
1967
- - main
1968
- paths:
1969
- include:
1970
- - functions/**
1971
-
1972
- pr:
1973
- branches:
1974
- include:
1975
- - main
1976
- paths:
1977
- include:
1978
- - functions/**
1979
-
1980
- pool:
1981
- vmImage: 'ubuntu-latest'
1982
-
1983
- variables:
1984
- - group: azure-deployment
1985
-
1986
- steps:
1987
- - task: NodeTool@0
1988
- inputs:
1989
- versionSpec: '22.x'
1990
- displayName: 'Install Node.js'
1991
-
1992
- - script: |
1993
- cd functions
1994
- npm ci
1995
- displayName: 'Install Functions dependencies'
1996
-
1997
- - script: |
1998
- cd functions
1999
- npm run build
2000
- displayName: 'Build Functions'
2001
-
2002
- - task: ArchiveFiles@2
2003
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2004
- inputs:
2005
- rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
2006
- includeRootFolder: false
2007
- archiveType: 'zip'
2008
- archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2009
- displayName: 'Archive Functions'
2010
-
2011
- - task: PublishBuildArtifacts@1
2012
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2013
- inputs:
2014
- PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
2015
- ArtifactName: 'functions'
2016
- displayName: 'Publish Functions artifact'
2017
-
2018
- - task: AzureFunctionApp@2
2019
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2020
- inputs:
2021
- azureSubscription: '$(AZURE_SUBSCRIPTION)'
2022
- appType: 'functionAppLinux'
2023
- appName: '$(AZURE_FUNCTIONAPP_NAME)'
2024
- package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2025
- displayName: 'Deploy to Azure Functions'
2359
+ const functionsPipeline = `trigger:
2360
+ branches:
2361
+ include:
2362
+ - main
2363
+ paths:
2364
+ include:
2365
+ - functions/**
2366
+
2367
+ pr:
2368
+ branches:
2369
+ include:
2370
+ - main
2371
+ paths:
2372
+ include:
2373
+ - functions/**
2374
+
2375
+ pool:
2376
+ vmImage: 'ubuntu-latest'
2377
+
2378
+ variables:
2379
+ - group: azure-deployment
2380
+
2381
+ steps:
2382
+ - task: NodeTool@0
2383
+ inputs:
2384
+ versionSpec: '22.x'
2385
+ displayName: 'Install Node.js'
2386
+
2387
+ - script: |
2388
+ cd functions
2389
+ npm ci
2390
+ displayName: 'Install Functions dependencies'
2391
+
2392
+ - script: |
2393
+ cd functions
2394
+ npm run build
2395
+ displayName: 'Build Functions'
2396
+
2397
+ - task: ArchiveFiles@2
2398
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2399
+ inputs:
2400
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
2401
+ includeRootFolder: false
2402
+ archiveType: 'zip'
2403
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2404
+ displayName: 'Archive Functions'
2405
+
2406
+ - task: PublishBuildArtifacts@1
2407
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2408
+ inputs:
2409
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
2410
+ ArtifactName: 'functions'
2411
+ displayName: 'Publish Functions artifact'
2412
+
2413
+ - task: AzureFunctionApp@2
2414
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2415
+ inputs:
2416
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
2417
+ appType: 'functionAppLinux'
2418
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
2419
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2420
+ displayName: 'Deploy to Azure Functions'
2026
2421
  `;
2027
2422
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
2028
2423
  console.log('✅ Azure Pipelines created\n');