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