swallowkit 1.0.0-beta.5 → 1.0.0-beta.7

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