swallowkit 0.4.0-beta.3 → 1.0.0-beta.10

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