swallowkit 1.0.0-beta.6 → 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 (78) hide show
  1. package/README.ja.md +12 -6
  2. package/README.md +12 -6
  3. package/dist/cli/commands/dev.d.ts +8 -0
  4. package/dist/cli/commands/dev.d.ts.map +1 -1
  5. package/dist/cli/commands/dev.js +238 -30
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts +5 -0
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +723 -285
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts +3 -0
  12. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  13. package/dist/cli/commands/scaffold.js +181 -17
  14. package/dist/cli/commands/scaffold.js.map +1 -1
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/config.d.ts +2 -1
  18. package/dist/core/config.d.ts.map +1 -1
  19. package/dist/core/config.js +28 -0
  20. package/dist/core/config.js.map +1 -1
  21. package/dist/core/scaffold/functions-generator.d.ts +5 -0
  22. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  23. package/dist/core/scaffold/functions-generator.js +431 -0
  24. package/dist/core/scaffold/functions-generator.js.map +1 -1
  25. package/dist/core/scaffold/model-parser.d.ts +1 -1
  26. package/dist/core/scaffold/model-parser.js +1 -1
  27. package/dist/core/scaffold/nextjs-generator.js +1 -1
  28. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  29. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  30. package/dist/core/scaffold/openapi-generator.js +190 -0
  31. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  32. package/dist/database/base-model.d.ts +3 -3
  33. package/dist/database/base-model.js +3 -3
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/types/index.d.ts +4 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/utils/package-manager.d.ts +2 -1
  41. package/dist/utils/package-manager.d.ts.map +1 -1
  42. package/dist/utils/package-manager.js +10 -6
  43. package/dist/utils/package-manager.js.map +1 -1
  44. package/package.json +2 -1
  45. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
  46. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  47. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
  48. package/src/__tests__/config.test.ts +122 -0
  49. package/src/__tests__/dev.test.ts +42 -0
  50. package/src/__tests__/fixtures.ts +83 -0
  51. package/src/__tests__/functions-generator.test.ts +101 -0
  52. package/src/__tests__/init.test.ts +59 -0
  53. package/src/__tests__/nextjs-generator.test.ts +97 -0
  54. package/src/__tests__/openapi-generator.test.ts +43 -0
  55. package/src/__tests__/package-manager.test.ts +189 -0
  56. package/src/__tests__/scaffold.test.ts +39 -0
  57. package/src/__tests__/string-utils.test.ts +75 -0
  58. package/src/__tests__/ui-generator.test.ts +144 -0
  59. package/src/cli/commands/create-model.ts +141 -0
  60. package/src/cli/commands/dev.ts +794 -0
  61. package/src/cli/commands/index.ts +8 -0
  62. package/src/cli/commands/init.ts +3363 -0
  63. package/src/cli/commands/provision.ts +193 -0
  64. package/src/cli/commands/scaffold.ts +786 -0
  65. package/src/cli/index.ts +73 -0
  66. package/src/core/config.ts +244 -0
  67. package/src/core/scaffold/functions-generator.ts +674 -0
  68. package/src/core/scaffold/model-parser.ts +627 -0
  69. package/src/core/scaffold/nextjs-generator.ts +217 -0
  70. package/src/core/scaffold/openapi-generator.ts +212 -0
  71. package/src/core/scaffold/ui-generator.ts +945 -0
  72. package/src/database/base-model.ts +184 -0
  73. package/src/database/client.ts +140 -0
  74. package/src/database/repository.ts +104 -0
  75. package/src/database/runtime-check.ts +25 -0
  76. package/src/index.ts +27 -0
  77. package/src/types/index.ts +45 -0
  78. package/src/utils/package-manager.ts +229 -0
@@ -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
  }
@@ -362,8 +460,11 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
362
460
  // 3. Create SwallowKit config
363
461
  const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
364
462
  module.exports = {
463
+ backend: {
464
+ language: '${backendLanguage}',
465
+ },
365
466
  functions: {
366
- baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
467
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
367
468
  },
368
469
  deployment: {
369
470
  resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
@@ -382,7 +483,7 @@ module.exports = {
382
483
  // Create backend utility for calling Azure Functions
383
484
  const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
384
485
  function getFunctionsBaseUrl(): string {
385
- return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
486
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
386
487
  }
387
488
 
388
489
  /**
@@ -487,7 +588,7 @@ export const api = {
487
588
  fs.mkdirSync(componentsDir, { recursive: true });
488
589
  // 6. Create .env.example
489
590
  const envExample = `# Azure Functions Backend URL
490
- FUNCTIONS_BASE_URL=http://localhost:7071
591
+ BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
491
592
 
492
593
  # Azure Configuration
493
594
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -570,7 +671,7 @@ export async function register() {
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,9 +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);
610
711
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
611
- createAiAgentFiles(projectDir, projectName);
712
+ createAiAgentFiles(projectDir, projectName, backendLanguage);
612
713
  }
613
714
  async function createSharedPackage(projectDir, projectName) {
614
715
  console.log('📦 Creating shared workspace package for Zod models...\n');
@@ -661,11 +762,93 @@ async function createSharedPackage(projectDir, projectName) {
661
762
  fs.writeFileSync(path.join(sharedDir, '.gitignore'), `node_modules\ndist\n`);
662
763
  console.log('✅ Shared package created\n');
663
764
  }
664
- async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
665
- 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`);
666
767
  const functionsDir = path.join(projectDir, 'functions');
667
768
  fs.mkdirSync(functionsDir, { recursive: true });
668
- // 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) {
669
852
  const functionsPackageJson = {
670
853
  name: 'functions',
671
854
  version: '1.0.0',
@@ -674,22 +857,21 @@ async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
674
857
  scripts: {
675
858
  start: 'func start',
676
859
  build: 'tsc',
677
- prestart: (0, package_manager_1.getFunctionsPrestart)(pm)
860
+ prestart: (0, package_manager_1.getFunctionsPrestart)(pm),
678
861
  },
679
862
  dependencies: {
680
863
  '@azure/functions': '~4.5.0',
681
864
  '@azure/cosmos': '^4.0.0',
682
865
  '@azure/identity': '^4.0.0',
683
- 'zod': '>=3.25.0',
866
+ zod: '>=3.25.0',
684
867
  [`@${path.basename(projectDir)}/shared`]: '*',
685
868
  },
686
869
  devDependencies: {
687
870
  '@types/node': '^20.0.0',
688
- 'typescript': '^5.0.0'
689
- }
871
+ typescript: '^5.0.0',
872
+ },
690
873
  };
691
874
  fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
692
- // Create functions tsconfig.json
693
875
  const sharedPkgName = `@${path.basename(projectDir)}/shared`;
694
876
  const functionsTsConfig = {
695
877
  compilerOptions: {
@@ -709,69 +891,14 @@ async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
709
891
  },
710
892
  },
711
893
  include: ['src/**/*'],
712
- exclude: ['node_modules', 'dist']
894
+ exclude: ['node_modules', 'dist'],
713
895
  };
714
896
  fs.writeFileSync(path.join(functionsDir, 'tsconfig.json'), JSON.stringify(functionsTsConfig, null, 2));
715
- // Create host.json
716
- const hostJson = {
717
- version: '2.0',
718
- logging: {
719
- applicationInsights: {
720
- samplingSettings: {
721
- isEnabled: true,
722
- maxTelemetryItemsPerSecond: 20
723
- }
724
- }
725
- },
726
- extensionBundle: {
727
- id: 'Microsoft.Azure.Functions.ExtensionBundle',
728
- version: '[4.0.0, 4.10.0)'
729
- }
730
- };
731
- fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
732
- // Create .funcignore
733
- const funcignore = `node_modules
734
- .git
735
- .vscode
736
- local.settings.json
737
- test
738
- tsconfig.json
739
- *.ts
740
- !dist/**/*.js
741
- `;
742
- fs.writeFileSync(path.join(functionsDir, '.funcignore'), funcignore);
743
- // Create .gitignore for functions directory
744
- const functionsGitignore = `node_modules
745
- dist
746
- local.settings.json
747
- *.log
748
- .vscode
749
- .DS_Store
750
- `;
751
- fs.writeFileSync(path.join(functionsDir, '.gitignore'), functionsGitignore);
752
- // Create local.settings.json
753
- const projectName = path.basename(projectDir);
754
- const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
755
- const localSettings = {
756
- IsEncrypted: false,
757
- Values: {
758
- AzureWebJobsStorage: '',
759
- FUNCTIONS_WORKER_RUNTIME: 'node',
760
- AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
761
- CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
762
- COSMOS_DB_DATABASE_NAME: databaseName,
763
- NODE_TLS_REJECT_UNAUTHORIZED: '0'
764
- }
765
- };
766
- fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
767
- // Create src directory
768
897
  const srcDir = path.join(functionsDir, 'src');
769
898
  fs.mkdirSync(srcDir, { recursive: true });
770
- // Create greet function directly in src
771
- const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
899
+ fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
772
900
  import { z } from 'zod/v4';
773
901
 
774
- // Zod schema for request validation
775
902
  const greetRequestSchema = z.object({
776
903
  name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
777
904
  });
@@ -780,12 +907,9 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
780
907
  context.log('HTTP trigger function processed a request.');
781
908
 
782
909
  try {
783
- // Get name from query or body
784
910
  const name = request.query.get('name') || (await request.text());
785
-
786
- // Validate with Zod
787
911
  const result = greetRequestSchema.safeParse({ name });
788
-
912
+
789
913
  if (!result.success) {
790
914
  return {
791
915
  status: 400,
@@ -796,7 +920,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
796
920
  }
797
921
 
798
922
  const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
799
-
923
+
800
924
  return {
801
925
  status: 200,
802
926
  jsonBody: {
@@ -820,10 +944,89 @@ app.http('greet', {
820
944
  authLevel: 'anonymous',
821
945
  handler: greet
822
946
  });
823
- `;
824
- fs.writeFileSync(path.join(srcDir, 'greet.ts'), greetFunction);
825
- // Dependencies are installed via workspace root install
826
- console.log('✅ Azure Functions project created\n');
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
+ `);
827
1030
  }
828
1031
  async function createBffApiRoute(projectDir) {
829
1032
  console.log('📦 Creating BFF API route...\n');
@@ -874,17 +1077,17 @@ export async function POST(request: NextRequest) {
874
1077
  }
875
1078
  `;
876
1079
  fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
877
- // Update .env.example to include FUNCTIONS_BASE_URL
1080
+ // Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
878
1081
  const envExamplePath = path.join(projectDir, '.env.example');
879
1082
  let envExample = fs.readFileSync(envExamplePath, 'utf-8');
880
- if (!envExample.includes('FUNCTIONS_BASE_URL')) {
1083
+ if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
881
1084
  envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
882
1085
  fs.writeFileSync(envExamplePath, envExample);
883
1086
  }
884
1087
  // Update .env.local
885
1088
  const envLocalPath = path.join(projectDir, '.env.local');
886
1089
  let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
887
- if (!envLocal.includes('FUNCTIONS_BASE_URL')) {
1090
+ if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
888
1091
  envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
889
1092
  fs.writeFileSync(envLocalPath, envLocal);
890
1093
  }
@@ -985,7 +1188,7 @@ export default function Home() {
985
1188
  Create your first model with Zod and generate CRUD operations automatically.
986
1189
  </p>
987
1190
  <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
988
- ${pmCmd.dlx} swallowkit scaffold lib/models/your-model.ts
1191
+ ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
989
1192
  </code>
990
1193
  </div>
991
1194
  </section>
@@ -1021,13 +1224,28 @@ export const scaffoldConfig = {
1021
1224
  fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
1022
1225
  console.log('✅ Scaffold config created\n');
1023
1226
  }
1024
- function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm) {
1227
+ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage) {
1025
1228
  console.log('📝 Creating README.md...\n');
1026
1229
  const pmCmd = (0, package_manager_1.getCommands)(pm);
1027
1230
  const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
1028
1231
  const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
1029
1232
  const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
1030
1233
  'Outbound VNet (Cosmos DB Private Endpoint)';
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
+ : '';
1031
1249
  const readme = `# ${projectName}
1032
1250
 
1033
1251
  A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
@@ -1036,9 +1254,9 @@ A full-stack application built with **SwallowKit** - Next.js on Azure Static Web
1036
1254
 
1037
1255
  - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
1038
1256
  - **BFF (Backend for Frontend)**: Next.js API Routes
1039
- - **Backend**: Azure Functions (TypeScript)
1257
+ - **Backend**: Azure Functions (${backendLanguageLabel})
1040
1258
  - **Database**: Azure Cosmos DB
1041
- - **Schema Validation**: Zod (shared between frontend and backend)
1259
+ - **Schema Validation**: ${schemaBridgeDescription}
1042
1260
  - **Infrastructure**: Bicep (Infrastructure as Code)
1043
1261
  - **CI/CD**: ${cicdLabel}
1044
1262
 
@@ -1073,11 +1291,8 @@ ${projectName}/
1073
1291
  │ ├── api/ # BFF API routes (proxy to Functions)
1074
1292
  │ └── page.tsx # Home page
1075
1293
  ├── functions/ # Azure Functions (backend)
1076
- │ └── src/
1077
- │ ├── models/ # Data models (copied from lib/models)
1078
- │ └── hello.ts # Sample function
1294
+ ${functionsTree}
1079
1295
  ├── lib/
1080
- │ ├── models/ # Shared Zod schemas
1081
1296
  │ └── api/ # API client utilities
1082
1297
  ├── infra/ # Bicep infrastructure files
1083
1298
  │ ├── main.bicep
@@ -1095,18 +1310,18 @@ Define your data model with Zod schema:
1095
1310
  ${pmCmd.dlx} swallowkit create-model <model-name>
1096
1311
  \`\`\`
1097
1312
 
1098
- This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
1313
+ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
1099
1314
 
1100
1315
  ### 2. Generate CRUD Code
1101
1316
 
1102
1317
  Generate complete CRUD operations (Functions, API routes, UI):
1103
1318
 
1104
1319
  \`\`\`bash
1105
- ${pmCmd.dlx} swallowkit scaffold lib/models/<model-name>.ts
1320
+ ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1106
1321
  \`\`\`
1107
1322
 
1108
1323
  This generates:
1109
- - Azure Functions CRUD endpoints
1324
+ ${backendScaffoldNote}
1110
1325
  - Next.js BFF API routes
1111
1326
  - React UI components (list, detail, create, edit)
1112
1327
  - Navigation menu integration
@@ -1123,6 +1338,7 @@ This starts:
1123
1338
  - Cosmos DB Emulator check (must be running separately)
1124
1339
 
1125
1340
  **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
1341
+ ${pythonLocalDevNote}
1126
1342
 
1127
1343
  ## ☁️ Deploy to Azure
1128
1344
 
@@ -1237,8 +1453,20 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1237
1453
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1238
1454
  console.log('✅ README.md created\n');
1239
1455
  }
1240
- function createAiAgentFiles(projectDir, projectName) {
1456
+ function createAiAgentFiles(projectDir, projectName, backendLanguage) {
1241
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\`.`;
1242
1470
  // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1243
1471
  const agentsMd = `# AGENTS.md
1244
1472
 
@@ -1247,14 +1475,14 @@ All coding agents **must** follow the architecture and conventions described bel
1247
1475
 
1248
1476
  ## Architecture Overview
1249
1477
 
1250
- This is a full-stack TypeScript application deployed on Azure with the following layers:
1478
+ This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
1251
1479
 
1252
1480
  \`\`\`
1253
1481
  Frontend (React / Next.js App Router)
1254
1482
  ↓ fetch('/api/{model}', ...)
1255
1483
  BFF Layer (Next.js API Routes)
1256
1484
  ↓ HTTP → Azure Functions
1257
- Backend (Azure Functions with Cosmos DB bindings)
1485
+ Backend (Azure Functions)
1258
1486
 
1259
1487
  Azure Cosmos DB (Document Database)
1260
1488
  \`\`\`
@@ -1267,7 +1495,7 @@ ${projectName}/
1267
1495
  │ ├── api/ # BFF API routes (proxy to Azure Functions)
1268
1496
  │ └── {model}/ # UI pages per model (list, detail, create, edit)
1269
1497
  ├── functions/ # Azure Functions (backend)
1270
- │ └── src/ # HTTP trigger handlers with Cosmos DB bindings
1498
+ ${functionsStructureLine}
1271
1499
  ├── shared/ # Shared workspace package
1272
1500
  │ ├── models/ # Zod schema definitions (single source of truth)
1273
1501
  │ └── index.ts # Re-exports all models
@@ -1322,8 +1550,7 @@ export async function POST(request: NextRequest) {
1322
1550
 
1323
1551
  - All data models are defined **once** as Zod schemas in \`shared/models/\`.
1324
1552
  - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
1325
- - The same schema is used in **all three layers**: frontend (validation), BFF (input/output validation), and Azure Functions (request/response validation + Cosmos DB documents).
1326
- - The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.
1553
+ - ${backendSchemaNote}
1327
1554
 
1328
1555
  Model definition pattern:
1329
1556
 
@@ -1350,15 +1577,11 @@ Key rules:
1350
1577
 
1351
1578
  ### 3. Azure Functions Own All Business Logic and Data Access
1352
1579
 
1353
- - All CRUD operations and business logic live in \`functions/src/\`.
1354
- - Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
1355
- - Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).
1356
- - Validate all data against Zod schemas before writing to Cosmos DB.
1357
- - The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.
1580
+ - ${backendRulesNote}
1358
1581
 
1359
- Azure Functions handler pattern:
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.`}
1360
1583
 
1361
- \`\`\`typescript
1584
+ ${backendLanguage === 'typescript' ? `\`\`\`typescript
1362
1585
  // functions/src/{model}.ts
1363
1586
  import { app } from '@azure/functions';
1364
1587
  import { ModelSchema } from '@${projectName}/shared';
@@ -1376,7 +1599,7 @@ app.http('{model}-get-all', {
1376
1599
  return { status: 200, jsonBody: validated };
1377
1600
  },
1378
1601
  });
1379
- \`\`\`
1602
+ \`\`\`` : ''}
1380
1603
 
1381
1604
  ## Naming Conventions
1382
1605
 
@@ -1384,7 +1607,7 @@ app.http('{model}-get-all', {
1384
1607
  |------|-----------|---------|
1385
1608
  | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1386
1609
  | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1387
- | Functions handler file | \`functions/src/{kebab-case}.ts\` | \`functions/src/todo.ts\` |
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'}\` |
1388
1611
  | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1389
1612
  | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1390
1613
  | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
@@ -1417,7 +1640,7 @@ npx swallowkit scaffold shared/models/<name>.ts
1417
1640
  \`\`\`
1418
1641
 
1419
1642
  Generates:
1420
- - Azure Functions handlers (\`functions/src/<name>.ts\`)
1643
+ - Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
1421
1644
  - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
1422
1645
  - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
1423
1646
  - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
@@ -1459,7 +1682,7 @@ Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Ma
1459
1682
 
1460
1683
  - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
1461
1684
  - **BFF**: Next.js API Routes (proxy only)
1462
- - **Backend**: Azure Functions (TypeScript, Node.js)
1685
+ - **Backend**: Azure Functions (${backendLanguageLabel})
1463
1686
  - **Database**: Azure Cosmos DB (NoSQL)
1464
1687
  - **Schema**: Zod (shared across all layers via workspace package)
1465
1688
  - **Infrastructure**: Bicep (IaC)
@@ -1479,7 +1702,8 @@ This file is for Claude Code. Read AGENTS.md in the project root for the full ar
1479
1702
  - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
1480
1703
  - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
1481
1704
  - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
1482
- - **Backend rule**: All business logic and Cosmos DB access lives in \`functions/src/\`.
1705
+ - **Backend language**: ${backendLanguageLabel}
1706
+ - **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
1483
1707
 
1484
1708
  ## SwallowKit CLI Commands
1485
1709
 
@@ -1516,13 +1740,13 @@ Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Fu
1516
1740
 
1517
1741
  1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
1518
1742
  2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
1519
- 3. **Backend owns data** — All CRUD, business logic, and Cosmos DB access is in \`functions/src/\`.
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/\`.
1520
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.
1521
1745
 
1522
1746
  ## Naming
1523
1747
 
1524
1748
  - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
1525
- - Files: kebab-case (\`shared/models/todo.ts\`, \`functions/src/todo.ts\`)
1749
+ - Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
1526
1750
  - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
1527
1751
 
1528
1752
  ## Managed Fields
@@ -1598,13 +1822,13 @@ applyTo: "functions/**"
1598
1822
 
1599
1823
  # Azure Functions — Backend Rules
1600
1824
 
1601
- Files in \`functions/src/\` contain all business logic and data access for this application.
1825
+ Files in \`functions/\` contain all business logic and data access for this application.
1602
1826
 
1603
1827
  ## Rules
1604
1828
 
1605
- - Use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
1606
- - Use the Cosmos DB SDK client directly **only** for delete operations.
1607
- - Validate all request data against Zod schemas from \`@${projectName}/shared\` before writing.
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/\`.
1608
1832
  - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
1609
1833
  - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
1610
1834
 
@@ -1640,12 +1864,13 @@ app.http('{model}-get-all', {
1640
1864
  console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
1641
1865
  console.log('');
1642
1866
  }
1643
- async function createInfrastructure(projectDir, projectName, azureConfig) {
1867
+ async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
1644
1868
  console.log('📦 Creating infrastructure files (Bicep)...\n');
1645
1869
  const infraDir = path.join(projectDir, 'infra');
1646
1870
  const modulesDir = path.join(infraDir, 'modules');
1647
1871
  fs.mkdirSync(modulesDir, { recursive: true });
1648
1872
  const enableVNet = azureConfig.vnetOption !== 'none';
1873
+ const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
1649
1874
  // main.bicep
1650
1875
  const mainBicep = `targetScope = 'resourceGroup'
1651
1876
 
@@ -1756,16 +1981,18 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
1756
1981
  // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
1757
1982
  module functionsFlex 'modules/functions-flex.bicep' = {
1758
1983
  name: 'functionsApp'
1759
- params: {
1760
- name: 'func-\${projectName}'
1761
- location: location
1762
- storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1763
- appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1764
- swaDefaultHostname: staticWebApp.outputs.defaultHostname
1765
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1766
- cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1767
- enableVNet: enableVNet
1768
- vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
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 : ''
1769
1996
  }
1770
1997
  dependsOn: [
1771
1998
  cosmosDbFreeTier
@@ -1996,6 +2223,12 @@ param enableVNet bool = false
1996
2223
  @description('VNet subnet ID for Functions (required if enableVNet is true)')
1997
2224
  param vnetSubnetId string = ''
1998
2225
 
2226
+ @description('Functions runtime name')
2227
+ param functionsRuntimeName string
2228
+
2229
+ @description('Functions runtime version')
2230
+ param functionsRuntimeVersion string
2231
+
1999
2232
  // Storage Account for Functions
2000
2233
  resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
2001
2234
  name: storageAccountName
@@ -2066,8 +2299,8 @@ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
2066
2299
  instanceMemoryMB: 2048
2067
2300
  }
2068
2301
  runtime: {
2069
- name: 'node'
2070
- version: '22'
2302
+ name: functionsRuntimeName
2303
+ version: functionsRuntimeVersion
2071
2304
  }
2072
2305
  }
2073
2306
  siteConfig: {
@@ -2409,68 +2642,77 @@ output privateDnsZoneId string = privateDnsZone.id
2409
2642
  }
2410
2643
  console.log('✅ Infrastructure files created\n');
2411
2644
  }
2412
- async function createGitHubActionsWorkflows(projectDir, azureConfig, pm) {
2413
- console.log('📦 Creating GitHub Actions workflows...\n');
2645
+ function getGitHubFunctionsWorkflow(pm, backendLanguage) {
2414
2646
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2415
2647
  const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
2416
- const workflowsDir = path.join(projectDir, '.github', 'workflows');
2417
- fs.mkdirSync(workflowsDir, { recursive: true });
2418
- // deploy-swa.yml
2419
- const swaWorkflow = `name: Deploy Static Web App
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
2420
2665
 
2421
2666
  on:
2422
2667
  push:
2423
2668
  branches:
2424
2669
  - main
2425
2670
  paths:
2426
- - 'app/**'
2427
- - 'components/**'
2428
- - 'lib/**'
2671
+ - 'functions/**'
2429
2672
  - 'shared/**'
2430
- - 'public/**'
2431
- - 'package.json'
2432
- - 'next.config.js'
2433
- - 'next.config.ts'
2434
- workflow_dispatch:
2435
2673
  pull_request:
2436
2674
  branches:
2437
2675
  - main
2438
2676
  paths:
2439
- - 'app/**'
2440
- - 'components/**'
2441
- - 'lib/**'
2677
+ - 'functions/**'
2442
2678
  - 'shared/**'
2443
- - 'public/**'
2444
- - 'package.json'
2445
- - 'next.config.js'
2446
- - 'next.config.ts'
2679
+ workflow_dispatch:
2447
2680
 
2448
2681
  jobs:
2449
2682
  build-and-deploy:
2450
2683
  runs-on: ubuntu-latest
2451
- name: Build and Deploy Static Web App
2684
+ name: Build and Deploy Functions
2452
2685
 
2453
2686
  steps:
2454
- - uses: actions/checkout@v4
2455
- with:
2456
- submodules: true
2687
+ ${commonSetup} - name: Build Functions
2688
+ run: |
2689
+ ${pmCmd.runFilter('functions')} build
2457
2690
 
2458
- - name: Deploy to Azure Static Web Apps
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
2459
2705
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2460
- uses: Azure/static-web-apps-deploy@v1
2706
+ uses: Azure/functions-action@v1
2461
2707
  with:
2462
- azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
2463
- repo_token: \${{ secrets.GITHUB_TOKEN }}
2464
- action: 'upload'
2465
- app_location: '/'
2466
- api_location: ''
2467
- output_location: ''
2468
- env:
2469
- NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
2708
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2709
+ package: './functions'
2710
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2711
+ sku: flexconsumption
2470
2712
  `;
2471
- fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
2472
- // deploy-functions.yml
2473
- const functionsWorkflow = `name: Deploy Azure Functions
2713
+ }
2714
+ if (backendLanguage === 'csharp') {
2715
+ return `name: Deploy Azure Functions
2474
2716
 
2475
2717
  on:
2476
2718
  push:
@@ -2493,119 +2735,86 @@ jobs:
2493
2735
  name: Build and Deploy Functions
2494
2736
 
2495
2737
  steps:
2496
- - uses: actions/checkout@v4
2497
-
2498
- - name: Setup Node.js
2499
- uses: actions/setup-node@v4
2738
+ ${commonSetup} - name: Setup .NET
2739
+ uses: actions/setup-dotnet@v4
2500
2740
  with:
2501
- node-version: '22'
2502
- ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2503
- - name: Install dependencies
2504
- run: |
2505
- ${pmCmd.ci}
2506
-
2507
- - name: Build shared package
2508
- run: |
2509
- ${pmCmd.runFilter('shared')} build
2510
-
2511
- - name: Build Functions
2512
- run: |
2513
- ${pmCmd.runFilter('functions')} build
2514
-
2515
- - name: Prepare functions for deployment
2741
+ dotnet-version: '8.0.x'
2742
+
2743
+ - name: Publish Functions
2516
2744
  run: |
2517
- SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2518
- mkdir -p /tmp/fn-deps
2519
- node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2520
- cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2521
- rm -rf ./functions/node_modules
2522
- mv /tmp/fn-deps/node_modules ./functions/node_modules
2523
- SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2524
- mkdir -p "$SHARED_DEST"
2525
- cp -r ./shared/dist "$SHARED_DEST/dist"
2526
- cp ./shared/package.json "$SHARED_DEST/package.json"
2745
+ dotnet publish ./functions -c Release -o ./functions/publish
2527
2746
 
2528
2747
  - name: Deploy to Azure Functions
2529
2748
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2530
2749
  uses: Azure/functions-action@v1
2531
2750
  with:
2532
2751
  app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2533
- package: './functions'
2752
+ package: './functions/publish'
2534
2753
  publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2535
2754
  sku: flexconsumption
2536
2755
  `;
2537
- fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
2538
- console.log('✅ GitHub Actions workflows created\n');
2539
- }
2540
- async function createAzurePipelines(projectDir, pm) {
2541
- console.log('📦 Creating Azure Pipelines...\n');
2542
- const pmCmd = (0, package_manager_1.getCommands)(pm);
2543
- const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2544
- const pipelinesDir = path.join(projectDir, 'pipelines');
2545
- fs.mkdirSync(pipelinesDir, { recursive: true });
2546
- // swa.yml
2547
- const swaPipeline = `trigger:
2548
- branches:
2549
- include:
2550
- - main
2551
- paths:
2552
- include:
2553
- - app/**
2554
- - components/**
2555
- - lib/**
2556
- - shared/**
2557
- - public/**
2558
- - package.json
2559
- - next.config.js
2756
+ }
2757
+ return `name: Deploy Azure Functions
2560
2758
 
2561
- pr:
2562
- branches:
2563
- include:
2759
+ on:
2760
+ push:
2761
+ branches:
2564
2762
  - main
2565
- paths:
2566
- include:
2567
- - app/**
2568
- - components/**
2569
- - lib/**
2570
- - shared/**
2571
- - public/**
2572
- - package.json
2573
- - next.config.js
2574
-
2575
- pool:
2576
- vmImage: 'ubuntu-latest'
2763
+ paths:
2764
+ - 'functions/**'
2765
+ - 'shared/**'
2766
+ pull_request:
2767
+ branches:
2768
+ - main
2769
+ paths:
2770
+ - 'functions/**'
2771
+ - 'shared/**'
2772
+ workflow_dispatch:
2577
2773
 
2578
- variables:
2579
- - group: azure-deployment
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'
2580
2784
 
2581
- steps:
2582
- - task: NodeTool@0
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
2583
2804
  inputs:
2584
2805
  versionSpec: '22.x'
2585
2806
  displayName: 'Install Node.js'
2586
2807
  ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2587
2808
  - script: |
2588
2809
  ${pmCmd.ci}
2589
- displayName: 'Install dependencies'
2810
+ displayName: 'Install workspace dependencies'
2590
2811
 
2591
2812
  - script: |
2592
- ${pmCmd.run} build
2593
- env:
2594
- NODE_ENV: production
2595
- displayName: 'Build Next.js app'
2596
-
2597
- - task: AzureStaticWebApp@0
2598
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2599
- inputs:
2600
- app_location: '.'
2601
- output_location: '.next/standalone'
2602
- skip_app_build: true
2603
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
2604
- displayName: 'Deploy to Azure Static Web Apps'
2813
+ ${pmCmd.runFilter('shared')} build
2814
+ displayName: 'Build shared package'
2605
2815
  `;
2606
- fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
2607
- // functions.yml
2608
- const functionsPipeline = `trigger:
2816
+ if (backendLanguage === 'typescript') {
2817
+ return `trigger:
2609
2818
  branches:
2610
2819
  include:
2611
2820
  - main
@@ -2630,20 +2839,7 @@ variables:
2630
2839
  - group: azure-deployment
2631
2840
 
2632
2841
  steps:
2633
- - task: NodeTool@0
2634
- inputs:
2635
- versionSpec: '22.x'
2636
- displayName: 'Install Node.js'
2637
- ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2638
- - script: |
2639
- ${pmCmd.ci}
2640
- displayName: 'Install workspace dependencies'
2641
-
2642
- - script: |
2643
- ${pmCmd.runFilter('shared')} build
2644
- displayName: 'Build shared package'
2645
-
2646
- - script: |
2842
+ ${commonSetup} - script: |
2647
2843
  ${pmCmd.runFilter('functions')} build
2648
2844
  displayName: 'Build Functions'
2649
2845
 
@@ -2685,6 +2881,248 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2685
2881
  package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2686
2882
  displayName: 'Deploy to Azure Functions'
2687
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);
2996
+ const workflowsDir = path.join(projectDir, '.github', 'workflows');
2997
+ fs.mkdirSync(workflowsDir, { recursive: true });
2998
+ // deploy-swa.yml
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'
3050
+ `;
3051
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3052
+ // deploy-functions.yml
3053
+ const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
3054
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3055
+ console.log('✅ GitHub Actions workflows created\n');
3056
+ }
3057
+ async function createAzurePipelines(projectDir, pm, backendLanguage) {
3058
+ console.log('📦 Creating Azure Pipelines...\n');
3059
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
3060
+ const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
3061
+ const pipelinesDir = path.join(projectDir, 'pipelines');
3062
+ fs.mkdirSync(pipelinesDir, { recursive: true });
3063
+ // swa.yml
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'
3122
+ `;
3123
+ fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3124
+ // functions.yml
3125
+ const functionsPipeline = getAzureFunctionsPipeline(pm, backendLanguage);
2688
3126
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
2689
3127
  console.log('✅ Azure Pipelines created\n');
2690
3128
  }