swallowkit 1.0.0-beta.5 → 1.0.0-beta.6
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 +245 -242
- package/README.md +246 -243
- package/dist/__tests__/fixtures.d.ts +14 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +85 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/create-model.js +14 -14
- package/dist/cli/commands/init.js +1992 -1587
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.js +100 -100
- package/dist/core/scaffold/functions-generator.js +218 -218
- package/dist/core/scaffold/model-parser.js +98 -98
- package/dist/core/scaffold/nextjs-generator.js +181 -181
- package/dist/core/scaffold/ui-generator.js +656 -656
- package/dist/utils/package-manager.js +4 -4
- package/package.json +80 -74
- package/dist/cli/commands/build.d.ts +0 -6
- package/dist/cli/commands/build.d.ts.map +0 -1
- package/dist/cli/commands/build.js +0 -177
- package/dist/cli/commands/build.js.map +0 -1
- package/dist/cli/commands/deploy.d.ts +0 -3
- package/dist/cli/commands/deploy.d.ts.map +0 -1
- package/dist/cli/commands/deploy.js +0 -147
- package/dist/cli/commands/deploy.js.map +0 -1
- package/dist/cli/commands/setup.d.ts +0 -6
- package/dist/cli/commands/setup.d.ts.map +0 -1
- package/dist/cli/commands/setup.js +0 -254
- package/dist/cli/commands/setup.js.map +0 -1
|
@@ -360,16 +360,16 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
|
|
|
360
360
|
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
361
361
|
}
|
|
362
362
|
// 3. Create SwallowKit config
|
|
363
|
-
const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
|
|
364
|
-
module.exports = {
|
|
365
|
-
functions: {
|
|
366
|
-
baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
367
|
-
},
|
|
368
|
-
deployment: {
|
|
369
|
-
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
370
|
-
swaName: process.env.AZURE_SWA_NAME || '',
|
|
371
|
-
},
|
|
372
|
-
}
|
|
363
|
+
const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
|
|
364
|
+
module.exports = {
|
|
365
|
+
functions: {
|
|
366
|
+
baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
367
|
+
},
|
|
368
|
+
deployment: {
|
|
369
|
+
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
370
|
+
swaName: process.env.AZURE_SWA_NAME || '',
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
373
|
`;
|
|
374
374
|
fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
|
|
375
375
|
// 4. Create shared workspace package for Zod models (Single Source of Truth)
|
|
@@ -380,191 +380,191 @@ module.exports = {
|
|
|
380
380
|
const apiLibDir = path.join(libDir, 'api');
|
|
381
381
|
fs.mkdirSync(apiLibDir, { recursive: true });
|
|
382
382
|
// Create backend utility for calling Azure Functions
|
|
383
|
-
const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
|
|
384
|
-
function getFunctionsBaseUrl(): string {
|
|
385
|
-
return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Simple HTTP client for calling backend APIs
|
|
390
|
-
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
391
|
-
*/
|
|
392
|
-
async function request<T>(
|
|
393
|
-
endpoint: string,
|
|
394
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
395
|
-
body?: any,
|
|
396
|
-
queryParams?: Record<string, string>
|
|
397
|
-
): Promise<T> {
|
|
398
|
-
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
399
|
-
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
400
|
-
if (queryParams) {
|
|
401
|
-
const params = new URLSearchParams(queryParams);
|
|
402
|
-
url += \`?\${params.toString()}\`;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
const response = await fetch(url, {
|
|
407
|
-
method,
|
|
408
|
-
headers: {
|
|
409
|
-
'Content-Type': 'application/json',
|
|
410
|
-
},
|
|
411
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
if (!response.ok) {
|
|
415
|
-
const text = await response.text();
|
|
416
|
-
let errorMessage = text || 'Failed to call backend function';
|
|
417
|
-
try {
|
|
418
|
-
const error = JSON.parse(text);
|
|
419
|
-
errorMessage = error.error || error.message || text;
|
|
420
|
-
} catch {
|
|
421
|
-
// If not JSON, use text as-is
|
|
422
|
-
}
|
|
423
|
-
throw new Error(errorMessage);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const contentType = response.headers.get('content-type');
|
|
427
|
-
if (!contentType?.includes('application/json')) {
|
|
428
|
-
const text = await response.text();
|
|
429
|
-
return text as T;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return await response.json();
|
|
433
|
-
} catch (error) {
|
|
434
|
-
console.error('Error calling backend:', error);
|
|
435
|
-
throw error;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Generic API client for making HTTP requests
|
|
441
|
-
* Simply calls endpoints - no DB dependencies, no schema validation
|
|
442
|
-
* Validation happens on the backend (BFF/Functions)
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
* // Call custom endpoint
|
|
446
|
-
* await api.get('/api/greet?name=World')
|
|
447
|
-
*
|
|
448
|
-
* // Call scaffolded CRUD endpoints
|
|
449
|
-
* await api.get('/api/todos')
|
|
450
|
-
* await api.post('/api/todos', { title: 'New task' })
|
|
451
|
-
* await api.put('/api/todos/123', { title: 'Updated' })
|
|
452
|
-
* await api.delete('/api/todos/123')
|
|
453
|
-
*/
|
|
454
|
-
export const api = {
|
|
455
|
-
/**
|
|
456
|
-
* Make a GET request
|
|
457
|
-
*/
|
|
458
|
-
get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
|
|
459
|
-
return request<T>(endpoint, 'GET', undefined, params);
|
|
460
|
-
},
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Make a POST request
|
|
464
|
-
*/
|
|
465
|
-
post: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
466
|
-
return request<T>(endpoint, 'POST', body);
|
|
467
|
-
},
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Make a PUT request
|
|
471
|
-
*/
|
|
472
|
-
put: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
473
|
-
return request<T>(endpoint, 'PUT', body);
|
|
474
|
-
},
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Make a DELETE request
|
|
478
|
-
*/
|
|
479
|
-
delete: <T>(endpoint: string): Promise<T> => {
|
|
480
|
-
return request<T>(endpoint, 'DELETE');
|
|
481
|
-
},
|
|
482
|
-
};
|
|
383
|
+
const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
|
|
384
|
+
function getFunctionsBaseUrl(): string {
|
|
385
|
+
return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Simple HTTP client for calling backend APIs
|
|
390
|
+
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
391
|
+
*/
|
|
392
|
+
async function request<T>(
|
|
393
|
+
endpoint: string,
|
|
394
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
395
|
+
body?: any,
|
|
396
|
+
queryParams?: Record<string, string>
|
|
397
|
+
): Promise<T> {
|
|
398
|
+
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
399
|
+
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
400
|
+
if (queryParams) {
|
|
401
|
+
const params = new URLSearchParams(queryParams);
|
|
402
|
+
url += \`?\${params.toString()}\`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const response = await fetch(url, {
|
|
407
|
+
method,
|
|
408
|
+
headers: {
|
|
409
|
+
'Content-Type': 'application/json',
|
|
410
|
+
},
|
|
411
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
const text = await response.text();
|
|
416
|
+
let errorMessage = text || 'Failed to call backend function';
|
|
417
|
+
try {
|
|
418
|
+
const error = JSON.parse(text);
|
|
419
|
+
errorMessage = error.error || error.message || text;
|
|
420
|
+
} catch {
|
|
421
|
+
// If not JSON, use text as-is
|
|
422
|
+
}
|
|
423
|
+
throw new Error(errorMessage);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const contentType = response.headers.get('content-type');
|
|
427
|
+
if (!contentType?.includes('application/json')) {
|
|
428
|
+
const text = await response.text();
|
|
429
|
+
return text as T;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return await response.json();
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error('Error calling backend:', error);
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Generic API client for making HTTP requests
|
|
441
|
+
* Simply calls endpoints - no DB dependencies, no schema validation
|
|
442
|
+
* Validation happens on the backend (BFF/Functions)
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* // Call custom endpoint
|
|
446
|
+
* await api.get('/api/greet?name=World')
|
|
447
|
+
*
|
|
448
|
+
* // Call scaffolded CRUD endpoints
|
|
449
|
+
* await api.get('/api/todos')
|
|
450
|
+
* await api.post('/api/todos', { title: 'New task' })
|
|
451
|
+
* await api.put('/api/todos/123', { title: 'Updated' })
|
|
452
|
+
* await api.delete('/api/todos/123')
|
|
453
|
+
*/
|
|
454
|
+
export const api = {
|
|
455
|
+
/**
|
|
456
|
+
* Make a GET request
|
|
457
|
+
*/
|
|
458
|
+
get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
|
|
459
|
+
return request<T>(endpoint, 'GET', undefined, params);
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Make a POST request
|
|
464
|
+
*/
|
|
465
|
+
post: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
466
|
+
return request<T>(endpoint, 'POST', body);
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Make a PUT request
|
|
471
|
+
*/
|
|
472
|
+
put: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
473
|
+
return request<T>(endpoint, 'PUT', body);
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Make a DELETE request
|
|
478
|
+
*/
|
|
479
|
+
delete: <T>(endpoint: string): Promise<T> => {
|
|
480
|
+
return request<T>(endpoint, 'DELETE');
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
483
|
`;
|
|
484
484
|
fs.writeFileSync(path.join(apiLibDir, 'backend.ts'), backendUtilContent);
|
|
485
485
|
// 5. Create components directory
|
|
486
486
|
const componentsDir = path.join(projectDir, 'components');
|
|
487
487
|
fs.mkdirSync(componentsDir, { recursive: true });
|
|
488
488
|
// 6. Create .env.example
|
|
489
|
-
const envExample = `# Azure Functions Backend URL
|
|
490
|
-
FUNCTIONS_BASE_URL=http://localhost:7071
|
|
491
|
-
|
|
492
|
-
# Azure Configuration
|
|
493
|
-
AZURE_RESOURCE_GROUP=your-resource-group
|
|
494
|
-
AZURE_SWA_NAME=your-static-web-app-name
|
|
489
|
+
const envExample = `# Azure Functions Backend URL
|
|
490
|
+
FUNCTIONS_BASE_URL=http://localhost:7071
|
|
491
|
+
|
|
492
|
+
# Azure Configuration
|
|
493
|
+
AZURE_RESOURCE_GROUP=your-resource-group
|
|
494
|
+
AZURE_SWA_NAME=your-static-web-app-name
|
|
495
495
|
`;
|
|
496
496
|
fs.writeFileSync(path.join(projectDir, '.env.example'), envExample);
|
|
497
497
|
// 7. Create instrumentation.ts for Application Insights (Next.js official way)
|
|
498
|
-
const instrumentationContent = `// Application Insights instrumentation for Next.js
|
|
499
|
-
// This file is automatically loaded by Next.js when instrumentationHook is enabled
|
|
500
|
-
export async function register() {
|
|
501
|
-
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
502
|
-
// Only run on server-side
|
|
503
|
-
const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
|
|
504
|
-
|
|
505
|
-
if (connectionString) {
|
|
506
|
-
const appInsights = await import('applicationinsights');
|
|
507
|
-
|
|
508
|
-
appInsights
|
|
509
|
-
.setup(connectionString)
|
|
510
|
-
.setAutoCollectConsole(true)
|
|
511
|
-
.setAutoCollectDependencies(true)
|
|
512
|
-
.setAutoCollectExceptions(true)
|
|
513
|
-
.setAutoCollectHeartbeat(true)
|
|
514
|
-
.setAutoCollectPerformance(true, true)
|
|
515
|
-
.setAutoCollectRequests(true)
|
|
516
|
-
.setAutoDependencyCorrelation(true)
|
|
517
|
-
.setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
|
|
518
|
-
.setSendLiveMetrics(true)
|
|
519
|
-
.setUseDiskRetryCaching(true);
|
|
520
|
-
|
|
521
|
-
appInsights.defaultClient.setAutoPopulateAzureProperties();
|
|
522
|
-
appInsights.start();
|
|
523
|
-
|
|
524
|
-
// Override console methods to send to Application Insights
|
|
525
|
-
const originalConsoleLog = console.log;
|
|
526
|
-
const originalConsoleError = console.error;
|
|
527
|
-
const originalConsoleWarn = console.warn;
|
|
528
|
-
|
|
529
|
-
console.log = function(...args: any[]) {
|
|
530
|
-
originalConsoleLog.apply(console, args);
|
|
531
|
-
const message = args.map(arg =>
|
|
532
|
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
533
|
-
).join(' ');
|
|
534
|
-
appInsights.defaultClient.trackTrace({
|
|
535
|
-
message: message,
|
|
536
|
-
severity: '1'
|
|
537
|
-
});
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
console.error = function(...args: any[]) {
|
|
541
|
-
originalConsoleError.apply(console, args);
|
|
542
|
-
const message = args.map(arg =>
|
|
543
|
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
544
|
-
).join(' ');
|
|
545
|
-
appInsights.defaultClient.trackTrace({
|
|
546
|
-
message: message,
|
|
547
|
-
severity: '3'
|
|
548
|
-
});
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
console.warn = function(...args: any[]) {
|
|
552
|
-
originalConsoleWarn.apply(console, args);
|
|
553
|
-
const message = args.map(arg =>
|
|
554
|
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
555
|
-
).join(' ');
|
|
556
|
-
appInsights.defaultClient.trackTrace({
|
|
557
|
-
message: message,
|
|
558
|
-
severity: '2'
|
|
559
|
-
});
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
|
|
563
|
-
} else {
|
|
564
|
-
console.log('[App Insights] Not configured (skipped in development mode)');
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
498
|
+
const instrumentationContent = `// Application Insights instrumentation for Next.js
|
|
499
|
+
// This file is automatically loaded by Next.js when instrumentationHook is enabled
|
|
500
|
+
export async function register() {
|
|
501
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
502
|
+
// Only run on server-side
|
|
503
|
+
const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
|
|
504
|
+
|
|
505
|
+
if (connectionString) {
|
|
506
|
+
const appInsights = await import('applicationinsights');
|
|
507
|
+
|
|
508
|
+
appInsights
|
|
509
|
+
.setup(connectionString)
|
|
510
|
+
.setAutoCollectConsole(true)
|
|
511
|
+
.setAutoCollectDependencies(true)
|
|
512
|
+
.setAutoCollectExceptions(true)
|
|
513
|
+
.setAutoCollectHeartbeat(true)
|
|
514
|
+
.setAutoCollectPerformance(true, true)
|
|
515
|
+
.setAutoCollectRequests(true)
|
|
516
|
+
.setAutoDependencyCorrelation(true)
|
|
517
|
+
.setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
|
|
518
|
+
.setSendLiveMetrics(true)
|
|
519
|
+
.setUseDiskRetryCaching(true);
|
|
520
|
+
|
|
521
|
+
appInsights.defaultClient.setAutoPopulateAzureProperties();
|
|
522
|
+
appInsights.start();
|
|
523
|
+
|
|
524
|
+
// Override console methods to send to Application Insights
|
|
525
|
+
const originalConsoleLog = console.log;
|
|
526
|
+
const originalConsoleError = console.error;
|
|
527
|
+
const originalConsoleWarn = console.warn;
|
|
528
|
+
|
|
529
|
+
console.log = function(...args: any[]) {
|
|
530
|
+
originalConsoleLog.apply(console, args);
|
|
531
|
+
const message = args.map(arg =>
|
|
532
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
533
|
+
).join(' ');
|
|
534
|
+
appInsights.defaultClient.trackTrace({
|
|
535
|
+
message: message,
|
|
536
|
+
severity: '1'
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
console.error = function(...args: any[]) {
|
|
541
|
+
originalConsoleError.apply(console, args);
|
|
542
|
+
const message = args.map(arg =>
|
|
543
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
544
|
+
).join(' ');
|
|
545
|
+
appInsights.defaultClient.trackTrace({
|
|
546
|
+
message: message,
|
|
547
|
+
severity: '3'
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
console.warn = function(...args: any[]) {
|
|
552
|
+
originalConsoleWarn.apply(console, args);
|
|
553
|
+
const message = args.map(arg =>
|
|
554
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
555
|
+
).join(' ');
|
|
556
|
+
appInsights.defaultClient.trackTrace({
|
|
557
|
+
message: message,
|
|
558
|
+
severity: '2'
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
|
|
563
|
+
} else {
|
|
564
|
+
console.log('[App Insights] Not configured (skipped in development mode)');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
568
|
`;
|
|
569
569
|
fs.writeFileSync(path.join(projectDir, 'instrumentation.ts'), instrumentationContent);
|
|
570
570
|
// 8. Create .env.local for local development
|
|
@@ -607,6 +607,8 @@ export async function register() {
|
|
|
607
607
|
console.log('✅ Project structure created\n');
|
|
608
608
|
// 18. Create README.md
|
|
609
609
|
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm);
|
|
610
|
+
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
611
|
+
createAiAgentFiles(projectDir, projectName);
|
|
610
612
|
}
|
|
611
613
|
async function createSharedPackage(projectDir, projectName) {
|
|
612
614
|
console.log('📦 Creating shared workspace package for Zod models...\n');
|
|
@@ -728,23 +730,23 @@ async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
|
|
|
728
730
|
};
|
|
729
731
|
fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
|
|
730
732
|
// Create .funcignore
|
|
731
|
-
const funcignore = `node_modules
|
|
732
|
-
.git
|
|
733
|
-
.vscode
|
|
734
|
-
local.settings.json
|
|
735
|
-
test
|
|
736
|
-
tsconfig.json
|
|
737
|
-
*.ts
|
|
738
|
-
!dist/**/*.js
|
|
733
|
+
const funcignore = `node_modules
|
|
734
|
+
.git
|
|
735
|
+
.vscode
|
|
736
|
+
local.settings.json
|
|
737
|
+
test
|
|
738
|
+
tsconfig.json
|
|
739
|
+
*.ts
|
|
740
|
+
!dist/**/*.js
|
|
739
741
|
`;
|
|
740
742
|
fs.writeFileSync(path.join(functionsDir, '.funcignore'), funcignore);
|
|
741
743
|
// Create .gitignore for functions directory
|
|
742
|
-
const functionsGitignore = `node_modules
|
|
743
|
-
dist
|
|
744
|
-
local.settings.json
|
|
745
|
-
*.log
|
|
746
|
-
.vscode
|
|
747
|
-
.DS_Store
|
|
744
|
+
const functionsGitignore = `node_modules
|
|
745
|
+
dist
|
|
746
|
+
local.settings.json
|
|
747
|
+
*.log
|
|
748
|
+
.vscode
|
|
749
|
+
.DS_Store
|
|
748
750
|
`;
|
|
749
751
|
fs.writeFileSync(path.join(functionsDir, '.gitignore'), functionsGitignore);
|
|
750
752
|
// Create local.settings.json
|
|
@@ -766,58 +768,58 @@ local.settings.json
|
|
|
766
768
|
const srcDir = path.join(functionsDir, 'src');
|
|
767
769
|
fs.mkdirSync(srcDir, { recursive: true });
|
|
768
770
|
// Create greet function directly in src
|
|
769
|
-
const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
770
|
-
import { z } from 'zod/v4';
|
|
771
|
-
|
|
772
|
-
// Zod schema for request validation
|
|
773
|
-
const greetRequestSchema = z.object({
|
|
774
|
-
name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
|
778
|
-
context.log('HTTP trigger function processed a request.');
|
|
779
|
-
|
|
780
|
-
try {
|
|
781
|
-
// Get name from query or body
|
|
782
|
-
const name = request.query.get('name') || (await request.text());
|
|
783
|
-
|
|
784
|
-
// Validate with Zod
|
|
785
|
-
const result = greetRequestSchema.safeParse({ name });
|
|
786
|
-
|
|
787
|
-
if (!result.success) {
|
|
788
|
-
return {
|
|
789
|
-
status: 400,
|
|
790
|
-
jsonBody: {
|
|
791
|
-
error: result.error.issues[0].message
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
|
|
797
|
-
|
|
798
|
-
return {
|
|
799
|
-
status: 200,
|
|
800
|
-
jsonBody: {
|
|
801
|
-
message: greeting,
|
|
802
|
-
timestamp: new Date().toISOString()
|
|
803
|
-
}
|
|
804
|
-
};
|
|
805
|
-
} catch (error) {
|
|
806
|
-
context.error('Error processing request:', error);
|
|
807
|
-
return {
|
|
808
|
-
status: 500,
|
|
809
|
-
jsonBody: {
|
|
810
|
-
error: 'Internal server error'
|
|
811
|
-
}
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
app.http('greet', {
|
|
817
|
-
methods: ['GET', 'POST'],
|
|
818
|
-
authLevel: 'anonymous',
|
|
819
|
-
handler: greet
|
|
820
|
-
});
|
|
771
|
+
const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
772
|
+
import { z } from 'zod/v4';
|
|
773
|
+
|
|
774
|
+
// Zod schema for request validation
|
|
775
|
+
const greetRequestSchema = z.object({
|
|
776
|
+
name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
|
780
|
+
context.log('HTTP trigger function processed a request.');
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
// Get name from query or body
|
|
784
|
+
const name = request.query.get('name') || (await request.text());
|
|
785
|
+
|
|
786
|
+
// Validate with Zod
|
|
787
|
+
const result = greetRequestSchema.safeParse({ name });
|
|
788
|
+
|
|
789
|
+
if (!result.success) {
|
|
790
|
+
return {
|
|
791
|
+
status: 400,
|
|
792
|
+
jsonBody: {
|
|
793
|
+
error: result.error.issues[0].message
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
status: 200,
|
|
802
|
+
jsonBody: {
|
|
803
|
+
message: greeting,
|
|
804
|
+
timestamp: new Date().toISOString()
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
} catch (error) {
|
|
808
|
+
context.error('Error processing request:', error);
|
|
809
|
+
return {
|
|
810
|
+
status: 500,
|
|
811
|
+
jsonBody: {
|
|
812
|
+
error: 'Internal server error'
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
app.http('greet', {
|
|
819
|
+
methods: ['GET', 'POST'],
|
|
820
|
+
authLevel: 'anonymous',
|
|
821
|
+
handler: greet
|
|
822
|
+
});
|
|
821
823
|
`;
|
|
822
824
|
fs.writeFileSync(path.join(srcDir, 'greet.ts'), greetFunction);
|
|
823
825
|
// Dependencies are installed via workspace root install
|
|
@@ -828,48 +830,48 @@ async function createBffApiRoute(projectDir) {
|
|
|
828
830
|
const apiDir = path.join(projectDir, 'app', 'api', 'greet');
|
|
829
831
|
fs.mkdirSync(apiDir, { recursive: true });
|
|
830
832
|
// Create API route that calls Azure Functions using shared utility
|
|
831
|
-
const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
|
|
832
|
-
import { api } from '@/lib/api/backend';
|
|
833
|
-
|
|
834
|
-
interface GreetResponse {
|
|
835
|
-
message: string;
|
|
836
|
-
timestamp: string;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
export async function GET(request: NextRequest) {
|
|
840
|
-
try {
|
|
841
|
-
const { searchParams } = new URL(request.url);
|
|
842
|
-
const name = searchParams.get('name') || 'World';
|
|
843
|
-
|
|
844
|
-
const data = await api.get<GreetResponse>('/api/greet', { name });
|
|
845
|
-
|
|
846
|
-
return NextResponse.json(data);
|
|
847
|
-
} catch (error) {
|
|
848
|
-
console.error('Error calling Azure Functions:', error);
|
|
849
|
-
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
850
|
-
return NextResponse.json(
|
|
851
|
-
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
852
|
-
{ status: 500 }
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
export async function POST(request: NextRequest) {
|
|
858
|
-
try {
|
|
859
|
-
const body = await request.json();
|
|
860
|
-
|
|
861
|
-
const data = await api.post<GreetResponse>('/api/greet', body);
|
|
862
|
-
|
|
863
|
-
return NextResponse.json(data);
|
|
864
|
-
} catch (error) {
|
|
865
|
-
console.error('Error calling Azure Functions:', error);
|
|
866
|
-
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
867
|
-
return NextResponse.json(
|
|
868
|
-
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
869
|
-
{ status: 500 }
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
833
|
+
const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
|
|
834
|
+
import { api } from '@/lib/api/backend';
|
|
835
|
+
|
|
836
|
+
interface GreetResponse {
|
|
837
|
+
message: string;
|
|
838
|
+
timestamp: string;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export async function GET(request: NextRequest) {
|
|
842
|
+
try {
|
|
843
|
+
const { searchParams } = new URL(request.url);
|
|
844
|
+
const name = searchParams.get('name') || 'World';
|
|
845
|
+
|
|
846
|
+
const data = await api.get<GreetResponse>('/api/greet', { name });
|
|
847
|
+
|
|
848
|
+
return NextResponse.json(data);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
console.error('Error calling Azure Functions:', error);
|
|
851
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
852
|
+
return NextResponse.json(
|
|
853
|
+
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
854
|
+
{ status: 500 }
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function POST(request: NextRequest) {
|
|
860
|
+
try {
|
|
861
|
+
const body = await request.json();
|
|
862
|
+
|
|
863
|
+
const data = await api.post<GreetResponse>('/api/greet', body);
|
|
864
|
+
|
|
865
|
+
return NextResponse.json(data);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
console.error('Error calling Azure Functions:', error);
|
|
868
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
869
|
+
return NextResponse.json(
|
|
870
|
+
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
871
|
+
{ status: 500 }
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
873
875
|
`;
|
|
874
876
|
fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
|
|
875
877
|
// Update .env.example to include FUNCTIONS_BASE_URL
|
|
@@ -891,111 +893,111 @@ export async function POST(request: NextRequest) {
|
|
|
891
893
|
async function createHomePage(projectDir, pm = 'pnpm') {
|
|
892
894
|
console.log('📦 Creating home page...\n');
|
|
893
895
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
894
|
-
const pageContent = `'use client'
|
|
895
|
-
|
|
896
|
-
export const dynamic = 'force-dynamic';
|
|
897
|
-
|
|
898
|
-
import { useState } from 'react';
|
|
899
|
-
import { scaffoldConfig } from '@/lib/scaffold-config';
|
|
900
|
-
|
|
901
|
-
export default function Home() {
|
|
902
|
-
const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
903
|
-
const [message, setMessage] = useState('');
|
|
904
|
-
|
|
905
|
-
const testConnection = async () => {
|
|
906
|
-
setGreetingStatus('loading');
|
|
907
|
-
try {
|
|
908
|
-
const response = await fetch('/api/greet?name=SwallowKit');
|
|
909
|
-
const data = await response.json();
|
|
910
|
-
if (!response.ok) {
|
|
911
|
-
throw new Error(data.error || \`Server error: \${response.status}\`);
|
|
912
|
-
}
|
|
913
|
-
setMessage(data.message);
|
|
914
|
-
setGreetingStatus('success');
|
|
915
|
-
} catch (error) {
|
|
916
|
-
setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
|
|
917
|
-
setGreetingStatus('error');
|
|
918
|
-
}
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
return (
|
|
922
|
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
|
|
923
|
-
<div className="container mx-auto px-4 py-12">
|
|
924
|
-
<header className="text-center mb-16">
|
|
925
|
-
<h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
|
|
926
|
-
Welcome to SwallowKit
|
|
927
|
-
</h1>
|
|
928
|
-
<p className="text-xl text-gray-600 dark:text-gray-400">
|
|
929
|
-
Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
|
|
930
|
-
</p>
|
|
931
|
-
</header>
|
|
932
|
-
|
|
933
|
-
{/* Connection Test */}
|
|
934
|
-
<section className="max-w-2xl mx-auto mb-12">
|
|
935
|
-
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
|
|
936
|
-
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
|
937
|
-
Test BFF → Functions Connection
|
|
938
|
-
</h2>
|
|
939
|
-
<button
|
|
940
|
-
onClick={testConnection}
|
|
941
|
-
disabled={greetingStatus === 'loading'}
|
|
942
|
-
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
|
|
943
|
-
>
|
|
944
|
-
{greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
|
|
945
|
-
</button>
|
|
946
|
-
{greetingStatus === 'success' && (
|
|
947
|
-
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
948
|
-
<p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
|
|
949
|
-
<p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
|
|
950
|
-
</div>
|
|
951
|
-
)}
|
|
952
|
-
{greetingStatus === 'error' && (
|
|
953
|
-
<div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
954
|
-
<p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
|
|
955
|
-
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
|
|
956
|
-
</div>
|
|
957
|
-
)}
|
|
958
|
-
</div>
|
|
959
|
-
</section>
|
|
960
|
-
|
|
961
|
-
{/* Scaffolded Models Menu */}
|
|
962
|
-
{scaffoldConfig.models.length > 0 ? (
|
|
963
|
-
<section className="max-w-6xl mx-auto">
|
|
964
|
-
<h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
|
|
965
|
-
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
966
|
-
{scaffoldConfig.models.map((model) => (
|
|
967
|
-
<a
|
|
968
|
-
key={model.name}
|
|
969
|
-
href={model.path}
|
|
970
|
-
className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
|
|
971
|
-
>
|
|
972
|
-
<h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
|
|
973
|
-
<p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
|
|
974
|
-
</a>
|
|
975
|
-
))}
|
|
976
|
-
</div>
|
|
977
|
-
</section>
|
|
978
|
-
) : (
|
|
979
|
-
<section className="max-w-2xl mx-auto text-center">
|
|
980
|
-
<div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
|
|
981
|
-
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
|
|
982
|
-
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
983
|
-
Create your first model with Zod and generate CRUD operations automatically.
|
|
984
|
-
</p>
|
|
985
|
-
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
986
|
-
${pmCmd.dlx} swallowkit scaffold lib/models/your-model.ts
|
|
987
|
-
</code>
|
|
988
|
-
</div>
|
|
989
|
-
</section>
|
|
990
|
-
)}
|
|
991
|
-
|
|
992
|
-
<footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
|
|
993
|
-
<p>Built with SwallowKit</p>
|
|
994
|
-
</footer>
|
|
995
|
-
</div>
|
|
996
|
-
</div>
|
|
997
|
-
);
|
|
998
|
-
}
|
|
896
|
+
const pageContent = `'use client'
|
|
897
|
+
|
|
898
|
+
export const dynamic = 'force-dynamic';
|
|
899
|
+
|
|
900
|
+
import { useState } from 'react';
|
|
901
|
+
import { scaffoldConfig } from '@/lib/scaffold-config';
|
|
902
|
+
|
|
903
|
+
export default function Home() {
|
|
904
|
+
const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
905
|
+
const [message, setMessage] = useState('');
|
|
906
|
+
|
|
907
|
+
const testConnection = async () => {
|
|
908
|
+
setGreetingStatus('loading');
|
|
909
|
+
try {
|
|
910
|
+
const response = await fetch('/api/greet?name=SwallowKit');
|
|
911
|
+
const data = await response.json();
|
|
912
|
+
if (!response.ok) {
|
|
913
|
+
throw new Error(data.error || \`Server error: \${response.status}\`);
|
|
914
|
+
}
|
|
915
|
+
setMessage(data.message);
|
|
916
|
+
setGreetingStatus('success');
|
|
917
|
+
} catch (error) {
|
|
918
|
+
setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
|
|
919
|
+
setGreetingStatus('error');
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
return (
|
|
924
|
+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
|
|
925
|
+
<div className="container mx-auto px-4 py-12">
|
|
926
|
+
<header className="text-center mb-16">
|
|
927
|
+
<h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
|
|
928
|
+
Welcome to SwallowKit
|
|
929
|
+
</h1>
|
|
930
|
+
<p className="text-xl text-gray-600 dark:text-gray-400">
|
|
931
|
+
Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
|
|
932
|
+
</p>
|
|
933
|
+
</header>
|
|
934
|
+
|
|
935
|
+
{/* Connection Test */}
|
|
936
|
+
<section className="max-w-2xl mx-auto mb-12">
|
|
937
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
|
|
938
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
|
939
|
+
Test BFF → Functions Connection
|
|
940
|
+
</h2>
|
|
941
|
+
<button
|
|
942
|
+
onClick={testConnection}
|
|
943
|
+
disabled={greetingStatus === 'loading'}
|
|
944
|
+
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
|
|
945
|
+
>
|
|
946
|
+
{greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
|
|
947
|
+
</button>
|
|
948
|
+
{greetingStatus === 'success' && (
|
|
949
|
+
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
950
|
+
<p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
|
|
951
|
+
<p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
|
|
952
|
+
</div>
|
|
953
|
+
)}
|
|
954
|
+
{greetingStatus === 'error' && (
|
|
955
|
+
<div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
956
|
+
<p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
|
|
957
|
+
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
|
|
958
|
+
</div>
|
|
959
|
+
)}
|
|
960
|
+
</div>
|
|
961
|
+
</section>
|
|
962
|
+
|
|
963
|
+
{/* Scaffolded Models Menu */}
|
|
964
|
+
{scaffoldConfig.models.length > 0 ? (
|
|
965
|
+
<section className="max-w-6xl mx-auto">
|
|
966
|
+
<h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
|
|
967
|
+
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
968
|
+
{scaffoldConfig.models.map((model) => (
|
|
969
|
+
<a
|
|
970
|
+
key={model.name}
|
|
971
|
+
href={model.path}
|
|
972
|
+
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"
|
|
973
|
+
>
|
|
974
|
+
<h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
|
|
975
|
+
<p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
|
|
976
|
+
</a>
|
|
977
|
+
))}
|
|
978
|
+
</div>
|
|
979
|
+
</section>
|
|
980
|
+
) : (
|
|
981
|
+
<section className="max-w-2xl mx-auto text-center">
|
|
982
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
|
|
983
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
|
|
984
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
985
|
+
Create your first model with Zod and generate CRUD operations automatically.
|
|
986
|
+
</p>
|
|
987
|
+
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
988
|
+
${pmCmd.dlx} swallowkit scaffold lib/models/your-model.ts
|
|
989
|
+
</code>
|
|
990
|
+
</div>
|
|
991
|
+
</section>
|
|
992
|
+
)}
|
|
993
|
+
|
|
994
|
+
<footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
|
|
995
|
+
<p>Built with SwallowKit</p>
|
|
996
|
+
</footer>
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
999
1001
|
`;
|
|
1000
1002
|
fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), pageContent);
|
|
1001
1003
|
console.log('✅ Home page created\n');
|
|
@@ -1004,17 +1006,17 @@ export default function Home() {
|
|
|
1004
1006
|
if (!fs.existsSync(scaffoldConfigDir)) {
|
|
1005
1007
|
fs.mkdirSync(scaffoldConfigDir, { recursive: true });
|
|
1006
1008
|
}
|
|
1007
|
-
const scaffoldConfigContent = `export interface ScaffoldModel {
|
|
1008
|
-
name: string;
|
|
1009
|
-
path: string;
|
|
1010
|
-
label: string;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
export const scaffoldConfig = {
|
|
1014
|
-
models: [
|
|
1015
|
-
// Scaffolded models will be added here by 'swallowkit scaffold' command
|
|
1016
|
-
] as ScaffoldModel[]
|
|
1017
|
-
};
|
|
1009
|
+
const scaffoldConfigContent = `export interface ScaffoldModel {
|
|
1010
|
+
name: string;
|
|
1011
|
+
path: string;
|
|
1012
|
+
label: string;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
export const scaffoldConfig = {
|
|
1016
|
+
models: [
|
|
1017
|
+
// Scaffolded models will be added here by 'swallowkit scaffold' command
|
|
1018
|
+
] as ScaffoldModel[]
|
|
1019
|
+
};
|
|
1018
1020
|
`;
|
|
1019
1021
|
fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
|
|
1020
1022
|
console.log('✅ Scaffold config created\n');
|
|
@@ -1026,215 +1028,618 @@ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm) {
|
|
|
1026
1028
|
const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
|
|
1027
1029
|
const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
|
|
1028
1030
|
'Outbound VNet (Cosmos DB Private Endpoint)';
|
|
1029
|
-
const readme = `# ${projectName}
|
|
1030
|
-
|
|
1031
|
-
A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
|
|
1032
|
-
|
|
1033
|
-
## 🚀 Tech Stack
|
|
1034
|
-
|
|
1035
|
-
- **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
|
|
1036
|
-
- **BFF (Backend for Frontend)**: Next.js API Routes
|
|
1037
|
-
- **Backend**: Azure Functions (TypeScript)
|
|
1038
|
-
- **Database**: Azure Cosmos DB
|
|
1039
|
-
- **Schema Validation**: Zod (shared between frontend and backend)
|
|
1040
|
-
- **Infrastructure**: Bicep (Infrastructure as Code)
|
|
1041
|
-
- **CI/CD**: ${cicdLabel}
|
|
1042
|
-
|
|
1043
|
-
## 📋 Project Configuration
|
|
1044
|
-
|
|
1045
|
-
This project was initialized with the following settings:
|
|
1046
|
-
|
|
1047
|
-
- **Azure Functions Plan**: Flex Consumption
|
|
1048
|
-
- **Cosmos DB Mode**: ${cosmosDbModeLabel}
|
|
1049
|
-
- **Network Security**: ${vnetLabel}
|
|
1050
|
-
- **CI/CD**: ${cicdLabel}
|
|
1051
|
-
|
|
1052
|
-
## ✅ Prerequisites
|
|
1053
|
-
|
|
1054
|
-
Before you begin, ensure you have the following installed:
|
|
1055
|
-
|
|
1056
|
-
1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1057
|
-
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1058
|
-
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1059
|
-
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
1060
|
-
${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local development
|
|
1061
|
-
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1062
|
-
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1063
|
-
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1064
|
-
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
|
|
1065
|
-
|
|
1066
|
-
## 📁 Project Structure
|
|
1067
|
-
|
|
1068
|
-
\`\`\`
|
|
1069
|
-
${projectName}/
|
|
1070
|
-
├── app/ # Next.js App Router (frontend)
|
|
1071
|
-
│ ├── api/ # BFF API routes (proxy to Functions)
|
|
1072
|
-
│ └── page.tsx # Home page
|
|
1073
|
-
├── functions/ # Azure Functions (backend)
|
|
1074
|
-
│ └── src/
|
|
1075
|
-
│ ├── models/ # Data models (copied from lib/models)
|
|
1076
|
-
│ └── hello.ts # Sample function
|
|
1077
|
-
├── lib/
|
|
1078
|
-
│ ├── models/ # Shared Zod schemas
|
|
1079
|
-
│ └── api/ # API client utilities
|
|
1080
|
-
├── infra/ # Bicep infrastructure files
|
|
1081
|
-
│ ├── main.bicep
|
|
1082
|
-
│ └── modules/ # Bicep modules for each resource
|
|
1083
|
-
└── .github/workflows/ # CI/CD workflows
|
|
1084
|
-
\`\`\`
|
|
1085
|
-
|
|
1086
|
-
## 🏗️ Getting Started
|
|
1087
|
-
|
|
1088
|
-
### 1. Create Your First Model
|
|
1089
|
-
|
|
1090
|
-
Define your data model with Zod schema:
|
|
1091
|
-
|
|
1092
|
-
\`\`\`bash
|
|
1093
|
-
${pmCmd.dlx} swallowkit create-model <model-name>
|
|
1094
|
-
\`\`\`
|
|
1095
|
-
|
|
1096
|
-
This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
|
|
1097
|
-
|
|
1098
|
-
### 2. Generate CRUD Code
|
|
1099
|
-
|
|
1100
|
-
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1101
|
-
|
|
1102
|
-
\`\`\`bash
|
|
1103
|
-
${pmCmd.dlx} swallowkit scaffold lib/models/<model-name>.ts
|
|
1104
|
-
\`\`\`
|
|
1105
|
-
|
|
1106
|
-
This generates:
|
|
1107
|
-
- Azure Functions CRUD endpoints
|
|
1108
|
-
- Next.js BFF API routes
|
|
1109
|
-
- React UI components (list, detail, create, edit)
|
|
1110
|
-
- Navigation menu integration
|
|
1111
|
-
|
|
1112
|
-
### 3. Start Development Servers
|
|
1113
|
-
|
|
1114
|
-
\`\`\`bash
|
|
1115
|
-
${pmCmd.dlx} swallowkit dev
|
|
1116
|
-
\`\`\`
|
|
1117
|
-
|
|
1118
|
-
This starts:
|
|
1119
|
-
- Next.js dev server (http://localhost:3000)
|
|
1120
|
-
- Azure Functions (http://localhost:7071)
|
|
1121
|
-
- Cosmos DB Emulator check (must be running separately)
|
|
1122
|
-
|
|
1123
|
-
**Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
|
|
1124
|
-
|
|
1125
|
-
## ☁️ Deploy to Azure
|
|
1126
|
-
|
|
1127
|
-
### Provision Azure Resources
|
|
1128
|
-
|
|
1129
|
-
Create all required Azure resources using Bicep:
|
|
1130
|
-
|
|
1131
|
-
\`\`\`bash
|
|
1132
|
-
${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
|
|
1133
|
-
\`\`\`
|
|
1134
|
-
|
|
1135
|
-
This creates:
|
|
1136
|
-
- Static Web App (\`swa-${projectName}\`)
|
|
1137
|
-
- Azure Functions (\`func-${projectName}\`)
|
|
1138
|
-
- Cosmos DB (\`cosmos-${projectName}\`)
|
|
1139
|
-
- Storage Account
|
|
1140
|
-
|
|
1141
|
-
You will be prompted to select Azure regions:
|
|
1142
|
-
1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
|
|
1143
|
-
2. **Static Web App location**: Limited availability (default: East Asia)
|
|
1144
|
-
|
|
1145
|
-
### CI/CD Setup
|
|
1146
|
-
|
|
1147
|
-
${cicdChoice === 'github' ? `#### GitHub Actions
|
|
1148
|
-
|
|
1149
|
-
1. Get Static Web App deployment token:
|
|
1150
|
-
\`\`\`bash
|
|
1151
|
-
az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
|
|
1152
|
-
\`\`\`
|
|
1153
|
-
|
|
1154
|
-
2. Get Function App publish profile:
|
|
1155
|
-
\`\`\`bash
|
|
1156
|
-
az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
|
|
1157
|
-
\`\`\`
|
|
1158
|
-
|
|
1159
|
-
3. Add secrets to GitHub repository:
|
|
1160
|
-
- \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
|
|
1161
|
-
- \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
|
|
1162
|
-
- \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
|
|
1163
|
-
|
|
1164
|
-
4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
|
|
1165
|
-
|
|
1166
|
-
1. Set up service connection in Azure DevOps
|
|
1167
|
-
2. Update \`azure-pipelines.yml\` with your resource names
|
|
1168
|
-
3. Configure pipeline variables:
|
|
1169
|
-
- \`azureSubscription\`: Service connection name
|
|
1170
|
-
- \`resourceGroupName\`: Resource group name
|
|
1171
|
-
4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
|
|
1172
|
-
|
|
1173
|
-
**Deploy Static Web App:**
|
|
1174
|
-
\`\`\`bash
|
|
1175
|
-
${pmCmd.run} build
|
|
1176
|
-
az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
|
|
1177
|
-
\`\`\`
|
|
1178
|
-
|
|
1179
|
-
**Deploy Functions:**
|
|
1180
|
-
\`\`\`bash
|
|
1181
|
-
cd functions
|
|
1182
|
-
${pmCmd.run} build
|
|
1183
|
-
func azure functionapp publish func-${projectName}
|
|
1184
|
-
\`\`\``}
|
|
1185
|
-
|
|
1186
|
-
## 🔧 Available Commands
|
|
1187
|
-
|
|
1188
|
-
- \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
|
|
1189
|
-
- \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
|
|
1190
|
-
- \`${pmCmd.dlx} swallowkit dev\` - Start development servers
|
|
1191
|
-
- \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
|
|
1192
|
-
${azureConfig.vnetOption !== 'none' ? `
|
|
1193
|
-
## 🔒 Network Security (VNet Configuration)
|
|
1194
|
-
|
|
1195
|
-
This project is configured with **${vnetLabel}**.
|
|
1196
|
-
|
|
1197
|
-
### Architecture
|
|
1198
|
-
|
|
1199
|
-
\`\`\`
|
|
1200
|
-
Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
|
|
1201
|
-
│
|
|
1202
|
-
VNet Integration
|
|
1203
|
-
(outbound only)
|
|
1204
|
-
\`\`\`
|
|
1205
|
-
|
|
1206
|
-
- **Functions → Cosmos DB**: Connected via Private Endpoint (private connection)
|
|
1207
|
-
- **SWA → Functions**: Connected via public endpoint (secured with CORS + IP restrictions)
|
|
1208
|
-
|
|
1209
|
-
### VNet Resources
|
|
1210
|
-
|
|
1211
|
-
| Resource | Purpose |
|
|
1212
|
-
|----------|---------|
|
|
1213
|
-
| \`vnet-${projectName}\` | Virtual Network (10.0.0.0/16) |
|
|
1214
|
-
| \`snet-functions\` | Functions subnet (10.0.1.0/24) |
|
|
1215
|
-
| \`snet-private-endpoints\` | Private Endpoints subnet (10.0.2.0/24) |
|
|
1216
|
-
| \`pe-cosmos-${projectName}\` | Cosmos DB Private Endpoint |
|
|
1217
|
-
|
|
1218
|
-
### Private DNS Zones
|
|
1219
|
-
|
|
1220
|
-
- \`privatelink.documents.azure.com\` (Cosmos DB)
|
|
1221
|
-
` : ''}
|
|
1222
|
-
## 📚 Learn More
|
|
1223
|
-
|
|
1224
|
-
- [SwallowKit Documentation](https://github.com/himanago/swallowkit)
|
|
1225
|
-
- [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
|
|
1226
|
-
- [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
|
|
1227
|
-
- [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
|
|
1228
|
-
- [Next.js](https://nextjs.org/)
|
|
1229
|
-
- [Zod](https://zod.dev/)
|
|
1230
|
-
|
|
1231
|
-
## 💭 Feedback
|
|
1232
|
-
|
|
1233
|
-
This project was generated by SwallowKit. If you encounter any issues or have suggestions for improvements, please open an issue on the [SwallowKit repository](https://github.com/himanago/swallowkit).
|
|
1031
|
+
const readme = `# ${projectName}
|
|
1032
|
+
|
|
1033
|
+
A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
|
|
1034
|
+
|
|
1035
|
+
## 🚀 Tech Stack
|
|
1036
|
+
|
|
1037
|
+
- **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
|
|
1038
|
+
- **BFF (Backend for Frontend)**: Next.js API Routes
|
|
1039
|
+
- **Backend**: Azure Functions (TypeScript)
|
|
1040
|
+
- **Database**: Azure Cosmos DB
|
|
1041
|
+
- **Schema Validation**: Zod (shared between frontend and backend)
|
|
1042
|
+
- **Infrastructure**: Bicep (Infrastructure as Code)
|
|
1043
|
+
- **CI/CD**: ${cicdLabel}
|
|
1044
|
+
|
|
1045
|
+
## 📋 Project Configuration
|
|
1046
|
+
|
|
1047
|
+
This project was initialized with the following settings:
|
|
1048
|
+
|
|
1049
|
+
- **Azure Functions Plan**: Flex Consumption
|
|
1050
|
+
- **Cosmos DB Mode**: ${cosmosDbModeLabel}
|
|
1051
|
+
- **Network Security**: ${vnetLabel}
|
|
1052
|
+
- **CI/CD**: ${cicdLabel}
|
|
1053
|
+
|
|
1054
|
+
## ✅ Prerequisites
|
|
1055
|
+
|
|
1056
|
+
Before you begin, ensure you have the following installed:
|
|
1057
|
+
|
|
1058
|
+
1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1059
|
+
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1060
|
+
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1061
|
+
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
1062
|
+
${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local development
|
|
1063
|
+
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1064
|
+
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1065
|
+
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1066
|
+
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
|
|
1067
|
+
|
|
1068
|
+
## 📁 Project Structure
|
|
1069
|
+
|
|
1070
|
+
\`\`\`
|
|
1071
|
+
${projectName}/
|
|
1072
|
+
├── app/ # Next.js App Router (frontend)
|
|
1073
|
+
│ ├── api/ # BFF API routes (proxy to Functions)
|
|
1074
|
+
│ └── page.tsx # Home page
|
|
1075
|
+
├── functions/ # Azure Functions (backend)
|
|
1076
|
+
│ └── src/
|
|
1077
|
+
│ ├── models/ # Data models (copied from lib/models)
|
|
1078
|
+
│ └── hello.ts # Sample function
|
|
1079
|
+
├── lib/
|
|
1080
|
+
│ ├── models/ # Shared Zod schemas
|
|
1081
|
+
│ └── api/ # API client utilities
|
|
1082
|
+
├── infra/ # Bicep infrastructure files
|
|
1083
|
+
│ ├── main.bicep
|
|
1084
|
+
│ └── modules/ # Bicep modules for each resource
|
|
1085
|
+
└── .github/workflows/ # CI/CD workflows
|
|
1086
|
+
\`\`\`
|
|
1087
|
+
|
|
1088
|
+
## 🏗️ Getting Started
|
|
1089
|
+
|
|
1090
|
+
### 1. Create Your First Model
|
|
1091
|
+
|
|
1092
|
+
Define your data model with Zod schema:
|
|
1093
|
+
|
|
1094
|
+
\`\`\`bash
|
|
1095
|
+
${pmCmd.dlx} swallowkit create-model <model-name>
|
|
1096
|
+
\`\`\`
|
|
1097
|
+
|
|
1098
|
+
This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
|
|
1099
|
+
|
|
1100
|
+
### 2. Generate CRUD Code
|
|
1101
|
+
|
|
1102
|
+
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1103
|
+
|
|
1104
|
+
\`\`\`bash
|
|
1105
|
+
${pmCmd.dlx} swallowkit scaffold lib/models/<model-name>.ts
|
|
1106
|
+
\`\`\`
|
|
1107
|
+
|
|
1108
|
+
This generates:
|
|
1109
|
+
- Azure Functions CRUD endpoints
|
|
1110
|
+
- Next.js BFF API routes
|
|
1111
|
+
- React UI components (list, detail, create, edit)
|
|
1112
|
+
- Navigation menu integration
|
|
1113
|
+
|
|
1114
|
+
### 3. Start Development Servers
|
|
1115
|
+
|
|
1116
|
+
\`\`\`bash
|
|
1117
|
+
${pmCmd.dlx} swallowkit dev
|
|
1118
|
+
\`\`\`
|
|
1119
|
+
|
|
1120
|
+
This starts:
|
|
1121
|
+
- Next.js dev server (http://localhost:3000)
|
|
1122
|
+
- Azure Functions (http://localhost:7071)
|
|
1123
|
+
- Cosmos DB Emulator check (must be running separately)
|
|
1124
|
+
|
|
1125
|
+
**Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
|
|
1126
|
+
|
|
1127
|
+
## ☁️ Deploy to Azure
|
|
1128
|
+
|
|
1129
|
+
### Provision Azure Resources
|
|
1130
|
+
|
|
1131
|
+
Create all required Azure resources using Bicep:
|
|
1132
|
+
|
|
1133
|
+
\`\`\`bash
|
|
1134
|
+
${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
|
|
1135
|
+
\`\`\`
|
|
1136
|
+
|
|
1137
|
+
This creates:
|
|
1138
|
+
- Static Web App (\`swa-${projectName}\`)
|
|
1139
|
+
- Azure Functions (\`func-${projectName}\`)
|
|
1140
|
+
- Cosmos DB (\`cosmos-${projectName}\`)
|
|
1141
|
+
- Storage Account
|
|
1142
|
+
|
|
1143
|
+
You will be prompted to select Azure regions:
|
|
1144
|
+
1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
|
|
1145
|
+
2. **Static Web App location**: Limited availability (default: East Asia)
|
|
1146
|
+
|
|
1147
|
+
### CI/CD Setup
|
|
1148
|
+
|
|
1149
|
+
${cicdChoice === 'github' ? `#### GitHub Actions
|
|
1150
|
+
|
|
1151
|
+
1. Get Static Web App deployment token:
|
|
1152
|
+
\`\`\`bash
|
|
1153
|
+
az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
|
|
1154
|
+
\`\`\`
|
|
1155
|
+
|
|
1156
|
+
2. Get Function App publish profile:
|
|
1157
|
+
\`\`\`bash
|
|
1158
|
+
az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
|
|
1159
|
+
\`\`\`
|
|
1160
|
+
|
|
1161
|
+
3. Add secrets to GitHub repository:
|
|
1162
|
+
- \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
|
|
1163
|
+
- \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
|
|
1164
|
+
- \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
|
|
1165
|
+
|
|
1166
|
+
4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
|
|
1167
|
+
|
|
1168
|
+
1. Set up service connection in Azure DevOps
|
|
1169
|
+
2. Update \`azure-pipelines.yml\` with your resource names
|
|
1170
|
+
3. Configure pipeline variables:
|
|
1171
|
+
- \`azureSubscription\`: Service connection name
|
|
1172
|
+
- \`resourceGroupName\`: Resource group name
|
|
1173
|
+
4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
|
|
1174
|
+
|
|
1175
|
+
**Deploy Static Web App:**
|
|
1176
|
+
\`\`\`bash
|
|
1177
|
+
${pmCmd.run} build
|
|
1178
|
+
az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
|
|
1179
|
+
\`\`\`
|
|
1180
|
+
|
|
1181
|
+
**Deploy Functions:**
|
|
1182
|
+
\`\`\`bash
|
|
1183
|
+
cd functions
|
|
1184
|
+
${pmCmd.run} build
|
|
1185
|
+
func azure functionapp publish func-${projectName}
|
|
1186
|
+
\`\`\``}
|
|
1187
|
+
|
|
1188
|
+
## 🔧 Available Commands
|
|
1189
|
+
|
|
1190
|
+
- \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
|
|
1191
|
+
- \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
|
|
1192
|
+
- \`${pmCmd.dlx} swallowkit dev\` - Start development servers
|
|
1193
|
+
- \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
|
|
1194
|
+
${azureConfig.vnetOption !== 'none' ? `
|
|
1195
|
+
## 🔒 Network Security (VNet Configuration)
|
|
1196
|
+
|
|
1197
|
+
This project is configured with **${vnetLabel}**.
|
|
1198
|
+
|
|
1199
|
+
### Architecture
|
|
1200
|
+
|
|
1201
|
+
\`\`\`
|
|
1202
|
+
Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
|
|
1203
|
+
│
|
|
1204
|
+
VNet Integration
|
|
1205
|
+
(outbound only)
|
|
1206
|
+
\`\`\`
|
|
1207
|
+
|
|
1208
|
+
- **Functions → Cosmos DB**: Connected via Private Endpoint (private connection)
|
|
1209
|
+
- **SWA → Functions**: Connected via public endpoint (secured with CORS + IP restrictions)
|
|
1210
|
+
|
|
1211
|
+
### VNet Resources
|
|
1212
|
+
|
|
1213
|
+
| Resource | Purpose |
|
|
1214
|
+
|----------|---------|
|
|
1215
|
+
| \`vnet-${projectName}\` | Virtual Network (10.0.0.0/16) |
|
|
1216
|
+
| \`snet-functions\` | Functions subnet (10.0.1.0/24) |
|
|
1217
|
+
| \`snet-private-endpoints\` | Private Endpoints subnet (10.0.2.0/24) |
|
|
1218
|
+
| \`pe-cosmos-${projectName}\` | Cosmos DB Private Endpoint |
|
|
1219
|
+
|
|
1220
|
+
### Private DNS Zones
|
|
1221
|
+
|
|
1222
|
+
- \`privatelink.documents.azure.com\` (Cosmos DB)
|
|
1223
|
+
` : ''}
|
|
1224
|
+
## 📚 Learn More
|
|
1225
|
+
|
|
1226
|
+
- [SwallowKit Documentation](https://github.com/himanago/swallowkit)
|
|
1227
|
+
- [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
|
|
1228
|
+
- [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
|
|
1229
|
+
- [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
|
|
1230
|
+
- [Next.js](https://nextjs.org/)
|
|
1231
|
+
- [Zod](https://zod.dev/)
|
|
1232
|
+
|
|
1233
|
+
## 💭 Feedback
|
|
1234
|
+
|
|
1235
|
+
This project was generated by SwallowKit. If you encounter any issues or have suggestions for improvements, please open an issue on the [SwallowKit repository](https://github.com/himanago/swallowkit).
|
|
1234
1236
|
`;
|
|
1235
1237
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1236
1238
|
console.log('✅ README.md created\n');
|
|
1237
1239
|
}
|
|
1240
|
+
function createAiAgentFiles(projectDir, projectName) {
|
|
1241
|
+
console.log('🤖 Creating AI agent instruction files...\n');
|
|
1242
|
+
// ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
|
|
1243
|
+
const agentsMd = `# AGENTS.md
|
|
1244
|
+
|
|
1245
|
+
This project was generated by **SwallowKit**.
|
|
1246
|
+
All coding agents **must** follow the architecture and conventions described below.
|
|
1247
|
+
|
|
1248
|
+
## Architecture Overview
|
|
1249
|
+
|
|
1250
|
+
This is a full-stack TypeScript application deployed on Azure with the following layers:
|
|
1251
|
+
|
|
1252
|
+
\`\`\`
|
|
1253
|
+
Frontend (React / Next.js App Router)
|
|
1254
|
+
↓ fetch('/api/{model}', ...)
|
|
1255
|
+
BFF Layer (Next.js API Routes)
|
|
1256
|
+
↓ HTTP → Azure Functions
|
|
1257
|
+
Backend (Azure Functions with Cosmos DB bindings)
|
|
1258
|
+
↓
|
|
1259
|
+
Azure Cosmos DB (Document Database)
|
|
1260
|
+
\`\`\`
|
|
1261
|
+
|
|
1262
|
+
### Project Structure
|
|
1263
|
+
|
|
1264
|
+
\`\`\`
|
|
1265
|
+
${projectName}/
|
|
1266
|
+
├── app/ # Next.js App Router
|
|
1267
|
+
│ ├── api/ # BFF API routes (proxy to Azure Functions)
|
|
1268
|
+
│ └── {model}/ # UI pages per model (list, detail, create, edit)
|
|
1269
|
+
├── functions/ # Azure Functions (backend)
|
|
1270
|
+
│ └── src/ # HTTP trigger handlers with Cosmos DB bindings
|
|
1271
|
+
├── shared/ # Shared workspace package
|
|
1272
|
+
│ ├── models/ # Zod schema definitions (single source of truth)
|
|
1273
|
+
│ └── index.ts # Re-exports all models
|
|
1274
|
+
├── lib/
|
|
1275
|
+
│ └── api/ # API client utilities (backend.ts, call-function.ts)
|
|
1276
|
+
├── components/ # Shared React components
|
|
1277
|
+
├── infra/ # Bicep infrastructure-as-code files
|
|
1278
|
+
│ ├── main.bicep
|
|
1279
|
+
│ └── modules/
|
|
1280
|
+
└── .github/workflows/ # CI/CD workflows (if configured)
|
|
1281
|
+
\`\`\`
|
|
1282
|
+
|
|
1283
|
+
## Critical Design Principles
|
|
1284
|
+
|
|
1285
|
+
### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
|
|
1286
|
+
|
|
1287
|
+
- \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
|
|
1288
|
+
- **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
|
|
1289
|
+
- The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
|
|
1290
|
+
- Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
|
|
1291
|
+
|
|
1292
|
+
Example BFF route pattern:
|
|
1293
|
+
|
|
1294
|
+
\`\`\`typescript
|
|
1295
|
+
// app/api/{model}/route.ts
|
|
1296
|
+
import { callFunction } from '@/lib/api/call-function';
|
|
1297
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
1298
|
+
import { z } from 'zod/v4';
|
|
1299
|
+
|
|
1300
|
+
export async function GET() {
|
|
1301
|
+
return callFunction({
|
|
1302
|
+
method: 'GET',
|
|
1303
|
+
path: '/api/{model}',
|
|
1304
|
+
responseSchema: z.array(ModelSchema),
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
export async function POST(request: NextRequest) {
|
|
1309
|
+
const body = await request.json();
|
|
1310
|
+
return callFunction({
|
|
1311
|
+
method: 'POST',
|
|
1312
|
+
path: '/api/{model}',
|
|
1313
|
+
body,
|
|
1314
|
+
inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
|
|
1315
|
+
responseSchema: ModelSchema,
|
|
1316
|
+
successStatus: 201,
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
\`\`\`
|
|
1320
|
+
|
|
1321
|
+
### 2. Zod Schemas Are the Single Source of Truth
|
|
1322
|
+
|
|
1323
|
+
- All data models are defined **once** as Zod schemas in \`shared/models/\`.
|
|
1324
|
+
- TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
|
|
1325
|
+
- The same schema is used in **all three layers**: frontend (validation), BFF (input/output validation), and Azure Functions (request/response validation + Cosmos DB documents).
|
|
1326
|
+
- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.
|
|
1327
|
+
|
|
1328
|
+
Model definition pattern:
|
|
1329
|
+
|
|
1330
|
+
\`\`\`typescript
|
|
1331
|
+
// shared/models/{model}.ts
|
|
1332
|
+
import { z } from 'zod/v4';
|
|
1333
|
+
|
|
1334
|
+
export const Todo = z.object({
|
|
1335
|
+
id: z.string(),
|
|
1336
|
+
name: z.string().min(1),
|
|
1337
|
+
// ... your fields
|
|
1338
|
+
createdAt: z.string().optional(),
|
|
1339
|
+
updatedAt: z.string().optional(),
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
export type Todo = z.infer<typeof Todo>;
|
|
1343
|
+
export const displayName = 'Todo';
|
|
1344
|
+
\`\`\`
|
|
1345
|
+
|
|
1346
|
+
Key rules:
|
|
1347
|
+
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1348
|
+
- \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
|
|
1349
|
+
- Always re-export models from \`shared/index.ts\`.
|
|
1350
|
+
|
|
1351
|
+
### 3. Azure Functions Own All Business Logic and Data Access
|
|
1352
|
+
|
|
1353
|
+
- All CRUD operations and business logic live in \`functions/src/\`.
|
|
1354
|
+
- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
|
|
1355
|
+
- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).
|
|
1356
|
+
- Validate all data against Zod schemas before writing to Cosmos DB.
|
|
1357
|
+
- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.
|
|
1358
|
+
|
|
1359
|
+
Azure Functions handler pattern:
|
|
1360
|
+
|
|
1361
|
+
\`\`\`typescript
|
|
1362
|
+
// functions/src/{model}.ts
|
|
1363
|
+
import { app } from '@azure/functions';
|
|
1364
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
1365
|
+
|
|
1366
|
+
const containerName = 'Models'; // PascalCase + 's'
|
|
1367
|
+
|
|
1368
|
+
app.http('{model}-get-all', {
|
|
1369
|
+
methods: ['GET'],
|
|
1370
|
+
route: '{model}',
|
|
1371
|
+
authLevel: 'anonymous',
|
|
1372
|
+
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
1373
|
+
handler: async (request, context) => {
|
|
1374
|
+
const documents = context.extraInputs.get('cosmosInput');
|
|
1375
|
+
const validated = z.array(ModelSchema).parse(documents);
|
|
1376
|
+
return { status: 200, jsonBody: validated };
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1379
|
+
\`\`\`
|
|
1380
|
+
|
|
1381
|
+
## Naming Conventions
|
|
1382
|
+
|
|
1383
|
+
| Item | Convention | Example |
|
|
1384
|
+
|------|-----------|---------|
|
|
1385
|
+
| Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
|
|
1386
|
+
| Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
|
|
1387
|
+
| Functions handler file | \`functions/src/{kebab-case}.ts\` | \`functions/src/todo.ts\` |
|
|
1388
|
+
| Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
|
|
1389
|
+
| API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
|
|
1390
|
+
| BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
|
|
1391
|
+
| BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
|
|
1392
|
+
| UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
|
|
1393
|
+
| React component | PascalCase | \`TodoForm.tsx\` |
|
|
1394
|
+
| Cosmos DB container | PascalCase + 's' | \`Todos\` |
|
|
1395
|
+
| Cosmos DB partition key | \`/id\` | Always \`/id\` |
|
|
1396
|
+
| Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
|
|
1397
|
+
|
|
1398
|
+
## Adding New Models (SwallowKit CLI Skills)
|
|
1399
|
+
|
|
1400
|
+
Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
|
|
1401
|
+
|
|
1402
|
+
### Skill: Create a new data model
|
|
1403
|
+
|
|
1404
|
+
\`\`\`bash
|
|
1405
|
+
npx swallowkit create-model <name>
|
|
1406
|
+
# Multiple models at once:
|
|
1407
|
+
npx swallowkit create-model user post comment
|
|
1408
|
+
\`\`\`
|
|
1409
|
+
|
|
1410
|
+
Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
|
|
1411
|
+
Edit the generated file to add your domain-specific fields, then run scaffold.
|
|
1412
|
+
|
|
1413
|
+
### Skill: Generate full CRUD from a model
|
|
1414
|
+
|
|
1415
|
+
\`\`\`bash
|
|
1416
|
+
npx swallowkit scaffold shared/models/<name>.ts
|
|
1417
|
+
\`\`\`
|
|
1418
|
+
|
|
1419
|
+
Generates:
|
|
1420
|
+
- Azure Functions handlers (\`functions/src/<name>.ts\`)
|
|
1421
|
+
- BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
|
|
1422
|
+
- UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
|
|
1423
|
+
- Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
|
|
1424
|
+
|
|
1425
|
+
### Skill: Start development servers
|
|
1426
|
+
|
|
1427
|
+
\`\`\`bash
|
|
1428
|
+
npx swallowkit dev
|
|
1429
|
+
\`\`\`
|
|
1430
|
+
|
|
1431
|
+
Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
|
|
1432
|
+
Checks for Cosmos DB Emulator availability.
|
|
1433
|
+
|
|
1434
|
+
### Skill: Provision Azure resources
|
|
1435
|
+
|
|
1436
|
+
\`\`\`bash
|
|
1437
|
+
npx swallowkit provision --resource-group <name> --location <region>
|
|
1438
|
+
\`\`\`
|
|
1439
|
+
|
|
1440
|
+
Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
|
|
1441
|
+
|
|
1442
|
+
### Typical workflow for "add a new feature/model"
|
|
1443
|
+
|
|
1444
|
+
1. \`npx swallowkit create-model <name>\`
|
|
1445
|
+
2. Edit \`shared/models/<name>.ts\` — add fields
|
|
1446
|
+
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1447
|
+
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1448
|
+
|
|
1449
|
+
## Do NOT
|
|
1450
|
+
|
|
1451
|
+
- **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
|
|
1452
|
+
- **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
|
|
1453
|
+
- **Do not** manually duplicate model definitions across layers. Use the shared package.
|
|
1454
|
+
- **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
|
|
1455
|
+
- **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
|
|
1456
|
+
- **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
|
|
1457
|
+
|
|
1458
|
+
## Technology Stack
|
|
1459
|
+
|
|
1460
|
+
- **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
|
|
1461
|
+
- **BFF**: Next.js API Routes (proxy only)
|
|
1462
|
+
- **Backend**: Azure Functions (TypeScript, Node.js)
|
|
1463
|
+
- **Database**: Azure Cosmos DB (NoSQL)
|
|
1464
|
+
- **Schema**: Zod (shared across all layers via workspace package)
|
|
1465
|
+
- **Infrastructure**: Bicep (IaC)
|
|
1466
|
+
- **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
|
|
1467
|
+
- **Auth**: Azure Managed Identity (no connection strings in production)
|
|
1468
|
+
- **Monitoring**: Application Insights
|
|
1469
|
+
`;
|
|
1470
|
+
fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
|
|
1471
|
+
console.log(' ✅ AGENTS.md (Codex / generic agents)');
|
|
1472
|
+
// ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
|
|
1473
|
+
const claudeMd = `# CLAUDE.md
|
|
1474
|
+
|
|
1475
|
+
This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
|
|
1476
|
+
|
|
1477
|
+
## Quick Reference
|
|
1478
|
+
|
|
1479
|
+
- **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
|
|
1480
|
+
- **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
|
|
1481
|
+
- **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
|
|
1482
|
+
- **Backend rule**: All business logic and Cosmos DB access lives in \`functions/src/\`.
|
|
1483
|
+
|
|
1484
|
+
## SwallowKit CLI Commands
|
|
1485
|
+
|
|
1486
|
+
| Task | Command |
|
|
1487
|
+
|------|---------|
|
|
1488
|
+
| Create model | \`npx swallowkit create-model <name>\` |
|
|
1489
|
+
| Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
|
|
1490
|
+
| Dev servers | \`npx swallowkit dev\` |
|
|
1491
|
+
| Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
|
|
1492
|
+
|
|
1493
|
+
## Workflow: Add a new model
|
|
1494
|
+
|
|
1495
|
+
1. \`npx swallowkit create-model <name>\`
|
|
1496
|
+
2. Edit \`shared/models/<name>.ts\` — add your fields
|
|
1497
|
+
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1498
|
+
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1499
|
+
`;
|
|
1500
|
+
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
|
|
1501
|
+
console.log(' ✅ CLAUDE.md (Claude Code)');
|
|
1502
|
+
// ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
|
|
1503
|
+
const ghDir = path.join(projectDir, '.github');
|
|
1504
|
+
fs.mkdirSync(ghDir, { recursive: true });
|
|
1505
|
+
const copilotInstructions = `# Copilot Instructions
|
|
1506
|
+
|
|
1507
|
+
This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
|
|
1508
|
+
|
|
1509
|
+
## Architecture (3-layer)
|
|
1510
|
+
|
|
1511
|
+
\`\`\`
|
|
1512
|
+
Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
|
|
1513
|
+
\`\`\`
|
|
1514
|
+
|
|
1515
|
+
## Key Rules
|
|
1516
|
+
|
|
1517
|
+
1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
|
|
1518
|
+
2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
|
|
1519
|
+
3. **Backend owns data** — All CRUD, business logic, and Cosmos DB access is in \`functions/src/\`.
|
|
1520
|
+
4. **Use the CLI** — Run \`npx swallowkit create-model <name>\` then \`npx swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
|
|
1521
|
+
|
|
1522
|
+
## Naming
|
|
1523
|
+
|
|
1524
|
+
- Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
|
|
1525
|
+
- Files: kebab-case (\`shared/models/todo.ts\`, \`functions/src/todo.ts\`)
|
|
1526
|
+
- Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
|
|
1527
|
+
|
|
1528
|
+
## Managed Fields
|
|
1529
|
+
|
|
1530
|
+
\`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
|
|
1531
|
+
`;
|
|
1532
|
+
fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
|
|
1533
|
+
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
|
|
1534
|
+
// ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
|
|
1535
|
+
const instructionsDir = path.join(ghDir, 'instructions');
|
|
1536
|
+
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
1537
|
+
// 4a. shared/models — Zod schema layer
|
|
1538
|
+
const sharedModelsInstructions = `---
|
|
1539
|
+
applyTo: "shared/models/**"
|
|
1540
|
+
---
|
|
1541
|
+
|
|
1542
|
+
# Shared Models — Zod Schema Rules
|
|
1543
|
+
|
|
1544
|
+
Files in this directory are the **single source of truth** for data models across the entire application.
|
|
1545
|
+
|
|
1546
|
+
## Rules
|
|
1547
|
+
|
|
1548
|
+
- Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
|
|
1549
|
+
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1550
|
+
\`\`\`typescript
|
|
1551
|
+
export const Todo = z.object({ ... });
|
|
1552
|
+
export type Todo = z.infer<typeof Todo>;
|
|
1553
|
+
\`\`\`
|
|
1554
|
+
- Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
|
|
1555
|
+
- Export a \`displayName\` string constant for UI display.
|
|
1556
|
+
- Re-export every model from \`shared/index.ts\`.
|
|
1557
|
+
- For relationships, use **nested schemas** (import and embed the related schema), not ID references.
|
|
1558
|
+
- After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
|
|
1559
|
+
`;
|
|
1560
|
+
fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
|
|
1561
|
+
// 4b. app/api — BFF layer
|
|
1562
|
+
const bffInstructions = `---
|
|
1563
|
+
applyTo: "app/api/**"
|
|
1564
|
+
---
|
|
1565
|
+
|
|
1566
|
+
# BFF API Routes — Rules
|
|
1567
|
+
|
|
1568
|
+
Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
|
|
1569
|
+
|
|
1570
|
+
## Rules
|
|
1571
|
+
|
|
1572
|
+
- **Never** put business logic, database access, or direct Cosmos DB calls here.
|
|
1573
|
+
- Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
|
|
1574
|
+
- You may validate input/output with Zod schemas before forwarding.
|
|
1575
|
+
- Import schemas from \`@${projectName}/shared\`.
|
|
1576
|
+
|
|
1577
|
+
## Pattern
|
|
1578
|
+
|
|
1579
|
+
\`\`\`typescript
|
|
1580
|
+
import { callFunction } from '@/lib/api/call-function';
|
|
1581
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
1582
|
+
import { z } from 'zod/v4';
|
|
1583
|
+
|
|
1584
|
+
export async function GET() {
|
|
1585
|
+
return callFunction({
|
|
1586
|
+
method: 'GET',
|
|
1587
|
+
path: '/api/{model}',
|
|
1588
|
+
responseSchema: z.array(ModelSchema),
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
\`\`\`
|
|
1592
|
+
`;
|
|
1593
|
+
fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
|
|
1594
|
+
// 4c. functions — Azure Functions backend layer
|
|
1595
|
+
const functionsInstructions = `---
|
|
1596
|
+
applyTo: "functions/**"
|
|
1597
|
+
---
|
|
1598
|
+
|
|
1599
|
+
# Azure Functions — Backend Rules
|
|
1600
|
+
|
|
1601
|
+
Files in \`functions/src/\` contain all business logic and data access for this application.
|
|
1602
|
+
|
|
1603
|
+
## Rules
|
|
1604
|
+
|
|
1605
|
+
- Use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
|
|
1606
|
+
- Use the Cosmos DB SDK client directly **only** for delete operations.
|
|
1607
|
+
- Validate all request data against Zod schemas from \`@${projectName}/shared\` before writing.
|
|
1608
|
+
- Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
|
|
1609
|
+
- Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
|
|
1610
|
+
|
|
1611
|
+
## Handler Pattern
|
|
1612
|
+
|
|
1613
|
+
\`\`\`typescript
|
|
1614
|
+
import { app } from '@azure/functions';
|
|
1615
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
1616
|
+
|
|
1617
|
+
app.http('{model}-get-all', {
|
|
1618
|
+
methods: ['GET'],
|
|
1619
|
+
route: '{model}',
|
|
1620
|
+
authLevel: 'anonymous',
|
|
1621
|
+
extraInputs: [cosmosInput],
|
|
1622
|
+
handler: async (request, context) => {
|
|
1623
|
+
const documents = context.extraInputs.get(cosmosInput);
|
|
1624
|
+
const validated = z.array(ModelSchema).parse(documents);
|
|
1625
|
+
return { status: 200, jsonBody: validated };
|
|
1626
|
+
},
|
|
1627
|
+
});
|
|
1628
|
+
\`\`\`
|
|
1629
|
+
`;
|
|
1630
|
+
fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
|
|
1631
|
+
console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
|
|
1632
|
+
console.log(' - shared-models.instructions.md');
|
|
1633
|
+
console.log(' - bff-routes.instructions.md');
|
|
1634
|
+
console.log(' - azure-functions.instructions.md');
|
|
1635
|
+
console.log('\n✅ AI agent files created\n');
|
|
1636
|
+
console.log(' Supported agents:');
|
|
1637
|
+
console.log(' - OpenAI Codex → AGENTS.md');
|
|
1638
|
+
console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
|
|
1639
|
+
console.log(' - GitHub Copilot → .github/copilot-instructions.md');
|
|
1640
|
+
console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
|
|
1641
|
+
console.log('');
|
|
1642
|
+
}
|
|
1238
1643
|
async function createInfrastructure(projectDir, projectName, azureConfig) {
|
|
1239
1644
|
console.log('📦 Creating infrastructure files (Bicep)...\n');
|
|
1240
1645
|
const infraDir = path.join(projectDir, 'infra');
|
|
@@ -1242,762 +1647,762 @@ async function createInfrastructure(projectDir, projectName, azureConfig) {
|
|
|
1242
1647
|
fs.mkdirSync(modulesDir, { recursive: true });
|
|
1243
1648
|
const enableVNet = azureConfig.vnetOption !== 'none';
|
|
1244
1649
|
// main.bicep
|
|
1245
|
-
const mainBicep = `targetScope = 'resourceGroup'
|
|
1246
|
-
|
|
1247
|
-
@description('Project name')
|
|
1248
|
-
param projectName string
|
|
1249
|
-
|
|
1250
|
-
@description('Location for Functions and Cosmos DB')
|
|
1251
|
-
param location string = resourceGroup().location
|
|
1252
|
-
|
|
1253
|
-
@description('Location for Static Web App (must be explicitly provided)')
|
|
1254
|
-
param swaLocation string
|
|
1255
|
-
|
|
1256
|
-
@description('Cosmos DB mode')
|
|
1257
|
-
@allowed(['freetier', 'serverless'])
|
|
1258
|
-
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
1259
|
-
|
|
1260
|
-
@description('Enable VNet integration')
|
|
1261
|
-
param enableVNet bool = ${enableVNet}
|
|
1262
|
-
|
|
1263
|
-
// Shared Log Analytics Workspace (in Functions region for data residency)
|
|
1264
|
-
module logAnalytics 'modules/loganalytics.bicep' = {
|
|
1265
|
-
name: 'logAnalytics'
|
|
1266
|
-
params: {
|
|
1267
|
-
name: 'log-\${projectName}'
|
|
1268
|
-
location: location
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
// Application Insights for Static Web App (must be in same region as SWA)
|
|
1273
|
-
module appInsightsSwa 'modules/appinsights.bicep' = {
|
|
1274
|
-
name: 'appInsightsSwa'
|
|
1275
|
-
params: {
|
|
1276
|
-
name: 'appi-\${projectName}-swa'
|
|
1277
|
-
location: swaLocation
|
|
1278
|
-
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// Application Insights for Functions (in same region as Functions)
|
|
1283
|
-
module appInsightsFunctions 'modules/appinsights.bicep' = {
|
|
1284
|
-
name: 'appInsightsFunctions'
|
|
1285
|
-
params: {
|
|
1286
|
-
name: 'appi-\${projectName}-func'
|
|
1287
|
-
location: location
|
|
1288
|
-
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
// Static Web App
|
|
1293
|
-
module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
1294
|
-
name: 'staticWebApp'
|
|
1295
|
-
params: {
|
|
1296
|
-
name: 'swa-\${projectName}'
|
|
1297
|
-
location: swaLocation
|
|
1298
|
-
sku: 'Standard'
|
|
1299
|
-
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// VNet (conditional)
|
|
1304
|
-
module vnet 'modules/vnet.bicep' = if (enableVNet) {
|
|
1305
|
-
name: 'vnet'
|
|
1306
|
-
params: {
|
|
1307
|
-
name: 'vnet-\${projectName}'
|
|
1308
|
-
location: location
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
|
|
1313
|
-
module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
|
|
1314
|
-
name: 'cosmosDb'
|
|
1315
|
-
params: {
|
|
1316
|
-
accountName: 'cosmos-\${projectName}'
|
|
1317
|
-
databaseName: '\${projectName}Database'
|
|
1318
|
-
location: location
|
|
1319
|
-
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
|
|
1324
|
-
name: 'cosmosDb'
|
|
1325
|
-
params: {
|
|
1326
|
-
accountName: 'cosmos-\${projectName}'
|
|
1327
|
-
databaseName: '\${projectName}Database'
|
|
1328
|
-
location: location
|
|
1329
|
-
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
// Cosmos DB Private Endpoint (conditional)
|
|
1334
|
-
module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
|
|
1335
|
-
name: 'cosmosPrivateEndpoint'
|
|
1336
|
-
params: {
|
|
1337
|
-
name: 'pe-cosmos-\${projectName}'
|
|
1338
|
-
location: location
|
|
1339
|
-
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
|
|
1340
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
1341
|
-
subnetId: vnet.outputs.privateEndpointSubnetId
|
|
1342
|
-
vnetId: vnet.outputs.id
|
|
1343
|
-
}
|
|
1344
|
-
dependsOn: [
|
|
1345
|
-
cosmosDbFreeTier
|
|
1346
|
-
cosmosDbServerless
|
|
1347
|
-
vnet
|
|
1348
|
-
]
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
1352
|
-
module functionsFlex 'modules/functions-flex.bicep' = {
|
|
1353
|
-
name: 'functionsApp'
|
|
1354
|
-
params: {
|
|
1355
|
-
name: 'func-\${projectName}'
|
|
1356
|
-
location: location
|
|
1357
|
-
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
1358
|
-
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
1359
|
-
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
1360
|
-
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
|
|
1361
|
-
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
|
|
1362
|
-
enableVNet: enableVNet
|
|
1363
|
-
vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
|
|
1364
|
-
}
|
|
1365
|
-
dependsOn: [
|
|
1366
|
-
cosmosDbFreeTier
|
|
1367
|
-
cosmosDbServerless
|
|
1368
|
-
cosmosPrivateEndpoint
|
|
1369
|
-
]
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// Cosmos DB role assignment for Functions (after Functions is created)
|
|
1373
|
-
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
1374
|
-
name: 'cosmosDbRoleAssignment'
|
|
1375
|
-
params: {
|
|
1376
|
-
cosmosAccountName: cosmosDbFreeTier.outputs.accountName
|
|
1377
|
-
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1378
|
-
}
|
|
1379
|
-
dependsOn: [
|
|
1380
|
-
functionsFlex
|
|
1381
|
-
]
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
1385
|
-
name: 'cosmosDbRoleAssignment'
|
|
1386
|
-
params: {
|
|
1387
|
-
cosmosAccountName: cosmosDbServerless.outputs.accountName
|
|
1388
|
-
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1389
|
-
}
|
|
1390
|
-
dependsOn: [
|
|
1391
|
-
functionsFlex
|
|
1392
|
-
]
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Update SWA config with Functions hostname (after Functions deployment)
|
|
1396
|
-
module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
1397
|
-
name: 'staticWebAppConfig'
|
|
1398
|
-
params: {
|
|
1399
|
-
staticWebAppName: staticWebApp.outputs.name
|
|
1400
|
-
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
1401
|
-
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1402
|
-
}
|
|
1403
|
-
dependsOn: [
|
|
1404
|
-
functionsFlex
|
|
1405
|
-
]
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
output staticWebAppName string = staticWebApp.outputs.name
|
|
1409
|
-
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
1410
|
-
output functionsAppName string = functionsFlex.outputs.name
|
|
1411
|
-
output functionsAppUrl string = functionsFlex.outputs.defaultHostname
|
|
1412
|
-
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
1413
|
-
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
|
|
1414
|
-
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
|
|
1415
|
-
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
1416
|
-
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
1417
|
-
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
1418
|
-
output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
|
|
1419
|
-
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
1420
|
-
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
1421
|
-
output vnetEnabled bool = enableVNet
|
|
1422
|
-
output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
1650
|
+
const mainBicep = `targetScope = 'resourceGroup'
|
|
1651
|
+
|
|
1652
|
+
@description('Project name')
|
|
1653
|
+
param projectName string
|
|
1654
|
+
|
|
1655
|
+
@description('Location for Functions and Cosmos DB')
|
|
1656
|
+
param location string = resourceGroup().location
|
|
1657
|
+
|
|
1658
|
+
@description('Location for Static Web App (must be explicitly provided)')
|
|
1659
|
+
param swaLocation string
|
|
1660
|
+
|
|
1661
|
+
@description('Cosmos DB mode')
|
|
1662
|
+
@allowed(['freetier', 'serverless'])
|
|
1663
|
+
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
1664
|
+
|
|
1665
|
+
@description('Enable VNet integration')
|
|
1666
|
+
param enableVNet bool = ${enableVNet}
|
|
1667
|
+
|
|
1668
|
+
// Shared Log Analytics Workspace (in Functions region for data residency)
|
|
1669
|
+
module logAnalytics 'modules/loganalytics.bicep' = {
|
|
1670
|
+
name: 'logAnalytics'
|
|
1671
|
+
params: {
|
|
1672
|
+
name: 'log-\${projectName}'
|
|
1673
|
+
location: location
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Application Insights for Static Web App (must be in same region as SWA)
|
|
1678
|
+
module appInsightsSwa 'modules/appinsights.bicep' = {
|
|
1679
|
+
name: 'appInsightsSwa'
|
|
1680
|
+
params: {
|
|
1681
|
+
name: 'appi-\${projectName}-swa'
|
|
1682
|
+
location: swaLocation
|
|
1683
|
+
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Application Insights for Functions (in same region as Functions)
|
|
1688
|
+
module appInsightsFunctions 'modules/appinsights.bicep' = {
|
|
1689
|
+
name: 'appInsightsFunctions'
|
|
1690
|
+
params: {
|
|
1691
|
+
name: 'appi-\${projectName}-func'
|
|
1692
|
+
location: location
|
|
1693
|
+
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// Static Web App
|
|
1698
|
+
module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
1699
|
+
name: 'staticWebApp'
|
|
1700
|
+
params: {
|
|
1701
|
+
name: 'swa-\${projectName}'
|
|
1702
|
+
location: swaLocation
|
|
1703
|
+
sku: 'Standard'
|
|
1704
|
+
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// VNet (conditional)
|
|
1709
|
+
module vnet 'modules/vnet.bicep' = if (enableVNet) {
|
|
1710
|
+
name: 'vnet'
|
|
1711
|
+
params: {
|
|
1712
|
+
name: 'vnet-\${projectName}'
|
|
1713
|
+
location: location
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
|
|
1718
|
+
module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
|
|
1719
|
+
name: 'cosmosDb'
|
|
1720
|
+
params: {
|
|
1721
|
+
accountName: 'cosmos-\${projectName}'
|
|
1722
|
+
databaseName: '\${projectName}Database'
|
|
1723
|
+
location: location
|
|
1724
|
+
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
|
|
1729
|
+
name: 'cosmosDb'
|
|
1730
|
+
params: {
|
|
1731
|
+
accountName: 'cosmos-\${projectName}'
|
|
1732
|
+
databaseName: '\${projectName}Database'
|
|
1733
|
+
location: location
|
|
1734
|
+
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Cosmos DB Private Endpoint (conditional)
|
|
1739
|
+
module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
|
|
1740
|
+
name: 'cosmosPrivateEndpoint'
|
|
1741
|
+
params: {
|
|
1742
|
+
name: 'pe-cosmos-\${projectName}'
|
|
1743
|
+
location: location
|
|
1744
|
+
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
|
|
1745
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
1746
|
+
subnetId: vnet.outputs.privateEndpointSubnetId
|
|
1747
|
+
vnetId: vnet.outputs.id
|
|
1748
|
+
}
|
|
1749
|
+
dependsOn: [
|
|
1750
|
+
cosmosDbFreeTier
|
|
1751
|
+
cosmosDbServerless
|
|
1752
|
+
vnet
|
|
1753
|
+
]
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
1757
|
+
module functionsFlex 'modules/functions-flex.bicep' = {
|
|
1758
|
+
name: 'functionsApp'
|
|
1759
|
+
params: {
|
|
1760
|
+
name: 'func-\${projectName}'
|
|
1761
|
+
location: location
|
|
1762
|
+
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
1763
|
+
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
1764
|
+
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
1765
|
+
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
|
|
1766
|
+
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
|
|
1767
|
+
enableVNet: enableVNet
|
|
1768
|
+
vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
|
|
1769
|
+
}
|
|
1770
|
+
dependsOn: [
|
|
1771
|
+
cosmosDbFreeTier
|
|
1772
|
+
cosmosDbServerless
|
|
1773
|
+
cosmosPrivateEndpoint
|
|
1774
|
+
]
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Cosmos DB role assignment for Functions (after Functions is created)
|
|
1778
|
+
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
1779
|
+
name: 'cosmosDbRoleAssignment'
|
|
1780
|
+
params: {
|
|
1781
|
+
cosmosAccountName: cosmosDbFreeTier.outputs.accountName
|
|
1782
|
+
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1783
|
+
}
|
|
1784
|
+
dependsOn: [
|
|
1785
|
+
functionsFlex
|
|
1786
|
+
]
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
1790
|
+
name: 'cosmosDbRoleAssignment'
|
|
1791
|
+
params: {
|
|
1792
|
+
cosmosAccountName: cosmosDbServerless.outputs.accountName
|
|
1793
|
+
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1794
|
+
}
|
|
1795
|
+
dependsOn: [
|
|
1796
|
+
functionsFlex
|
|
1797
|
+
]
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// Update SWA config with Functions hostname (after Functions deployment)
|
|
1801
|
+
module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
1802
|
+
name: 'staticWebAppConfig'
|
|
1803
|
+
params: {
|
|
1804
|
+
staticWebAppName: staticWebApp.outputs.name
|
|
1805
|
+
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
1806
|
+
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1807
|
+
}
|
|
1808
|
+
dependsOn: [
|
|
1809
|
+
functionsFlex
|
|
1810
|
+
]
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
output staticWebAppName string = staticWebApp.outputs.name
|
|
1814
|
+
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
1815
|
+
output functionsAppName string = functionsFlex.outputs.name
|
|
1816
|
+
output functionsAppUrl string = functionsFlex.outputs.defaultHostname
|
|
1817
|
+
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
1818
|
+
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
|
|
1819
|
+
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
|
|
1820
|
+
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
1821
|
+
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
1822
|
+
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
1823
|
+
output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
|
|
1824
|
+
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
1825
|
+
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
1826
|
+
output vnetEnabled bool = enableVNet
|
|
1827
|
+
output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
1423
1828
|
`;
|
|
1424
1829
|
fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
|
|
1425
1830
|
// main.parameters.json
|
|
1426
|
-
const params = `{
|
|
1427
|
-
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
|
1428
|
-
"contentVersion": "1.0.0.0",
|
|
1429
|
-
"parameters": {
|
|
1430
|
-
"projectName": {
|
|
1431
|
-
"value": "${projectName}"
|
|
1432
|
-
},
|
|
1433
|
-
"cosmosDbMode": {
|
|
1434
|
-
"value": "${azureConfig.cosmosDbMode}"
|
|
1435
|
-
},
|
|
1436
|
-
"enableVNet": {
|
|
1437
|
-
"value": ${enableVNet}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1831
|
+
const params = `{
|
|
1832
|
+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
|
1833
|
+
"contentVersion": "1.0.0.0",
|
|
1834
|
+
"parameters": {
|
|
1835
|
+
"projectName": {
|
|
1836
|
+
"value": "${projectName}"
|
|
1837
|
+
},
|
|
1838
|
+
"cosmosDbMode": {
|
|
1839
|
+
"value": "${azureConfig.cosmosDbMode}"
|
|
1840
|
+
},
|
|
1841
|
+
"enableVNet": {
|
|
1842
|
+
"value": ${enableVNet}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1441
1846
|
`;
|
|
1442
1847
|
fs.writeFileSync(path.join(infraDir, 'main.parameters.json'), params);
|
|
1443
1848
|
// modules/staticwebapp.bicep
|
|
1444
|
-
const staticWebAppBicep = `@description('Static Web App name')
|
|
1445
|
-
param name string
|
|
1446
|
-
|
|
1447
|
-
@description('Location for the Static Web App')
|
|
1448
|
-
param location string
|
|
1449
|
-
|
|
1450
|
-
@description('SKU name (Free or Standard)')
|
|
1451
|
-
@allowed([
|
|
1452
|
-
'Free'
|
|
1453
|
-
'Standard'
|
|
1454
|
-
])
|
|
1455
|
-
param sku string = 'Standard'
|
|
1456
|
-
|
|
1457
|
-
@description('Application Insights connection string')
|
|
1458
|
-
param appInsightsConnectionString string
|
|
1459
|
-
|
|
1460
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
|
|
1461
|
-
name: name
|
|
1462
|
-
location: location
|
|
1463
|
-
sku: {
|
|
1464
|
-
name: sku
|
|
1465
|
-
tier: sku
|
|
1466
|
-
}
|
|
1467
|
-
properties: {
|
|
1468
|
-
buildProperties: {
|
|
1469
|
-
skipGithubActionWorkflowGeneration: true
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
// Link Application Insights to Static Web App (for both client and server-side telemetry)
|
|
1475
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1476
|
-
parent: staticWebApp
|
|
1477
|
-
name: 'appsettings'
|
|
1478
|
-
properties: {
|
|
1479
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1480
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
output id string = staticWebApp.id
|
|
1485
|
-
output name string = staticWebApp.name
|
|
1486
|
-
output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
1849
|
+
const staticWebAppBicep = `@description('Static Web App name')
|
|
1850
|
+
param name string
|
|
1851
|
+
|
|
1852
|
+
@description('Location for the Static Web App')
|
|
1853
|
+
param location string
|
|
1854
|
+
|
|
1855
|
+
@description('SKU name (Free or Standard)')
|
|
1856
|
+
@allowed([
|
|
1857
|
+
'Free'
|
|
1858
|
+
'Standard'
|
|
1859
|
+
])
|
|
1860
|
+
param sku string = 'Standard'
|
|
1861
|
+
|
|
1862
|
+
@description('Application Insights connection string')
|
|
1863
|
+
param appInsightsConnectionString string
|
|
1864
|
+
|
|
1865
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
|
|
1866
|
+
name: name
|
|
1867
|
+
location: location
|
|
1868
|
+
sku: {
|
|
1869
|
+
name: sku
|
|
1870
|
+
tier: sku
|
|
1871
|
+
}
|
|
1872
|
+
properties: {
|
|
1873
|
+
buildProperties: {
|
|
1874
|
+
skipGithubActionWorkflowGeneration: true
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// Link Application Insights to Static Web App (for both client and server-side telemetry)
|
|
1880
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1881
|
+
parent: staticWebApp
|
|
1882
|
+
name: 'appsettings'
|
|
1883
|
+
properties: {
|
|
1884
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1885
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
output id string = staticWebApp.id
|
|
1890
|
+
output name string = staticWebApp.name
|
|
1891
|
+
output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
1487
1892
|
`;
|
|
1488
1893
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
1489
1894
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
1490
|
-
const staticWebAppConfigBicep = `@description('Static Web App name')
|
|
1491
|
-
param staticWebAppName string
|
|
1492
|
-
|
|
1493
|
-
@description('Functions App default hostname for backend API calls')
|
|
1494
|
-
param functionsDefaultHostname string
|
|
1495
|
-
|
|
1496
|
-
@description('Application Insights connection string for SWA')
|
|
1497
|
-
param appInsightsConnectionString string
|
|
1498
|
-
|
|
1499
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
1500
|
-
name: staticWebAppName
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1504
|
-
parent: staticWebApp
|
|
1505
|
-
name: 'appsettings'
|
|
1506
|
-
properties: {
|
|
1507
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1508
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1509
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
output configName string = staticWebAppConfig.name
|
|
1895
|
+
const staticWebAppConfigBicep = `@description('Static Web App name')
|
|
1896
|
+
param staticWebAppName string
|
|
1897
|
+
|
|
1898
|
+
@description('Functions App default hostname for backend API calls')
|
|
1899
|
+
param functionsDefaultHostname string
|
|
1900
|
+
|
|
1901
|
+
@description('Application Insights connection string for SWA')
|
|
1902
|
+
param appInsightsConnectionString string
|
|
1903
|
+
|
|
1904
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
1905
|
+
name: staticWebAppName
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1909
|
+
parent: staticWebApp
|
|
1910
|
+
name: 'appsettings'
|
|
1911
|
+
properties: {
|
|
1912
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1913
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1914
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
output configName string = staticWebAppConfig.name
|
|
1514
1919
|
`;
|
|
1515
1920
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
1516
1921
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
1517
|
-
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
1518
|
-
param name string
|
|
1519
|
-
|
|
1520
|
-
@description('Location for Log Analytics workspace')
|
|
1521
|
-
param location string
|
|
1522
|
-
|
|
1523
|
-
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
1524
|
-
name: name
|
|
1525
|
-
location: location
|
|
1526
|
-
properties: {
|
|
1527
|
-
sku: {
|
|
1528
|
-
name: 'PerGB2018'
|
|
1529
|
-
}
|
|
1530
|
-
retentionInDays: 30
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
output id string = logAnalytics.id
|
|
1535
|
-
output name string = logAnalytics.name
|
|
1922
|
+
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
1923
|
+
param name string
|
|
1924
|
+
|
|
1925
|
+
@description('Location for Log Analytics workspace')
|
|
1926
|
+
param location string
|
|
1927
|
+
|
|
1928
|
+
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
1929
|
+
name: name
|
|
1930
|
+
location: location
|
|
1931
|
+
properties: {
|
|
1932
|
+
sku: {
|
|
1933
|
+
name: 'PerGB2018'
|
|
1934
|
+
}
|
|
1935
|
+
retentionInDays: 30
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
output id string = logAnalytics.id
|
|
1940
|
+
output name string = logAnalytics.name
|
|
1536
1941
|
`;
|
|
1537
1942
|
fs.writeFileSync(path.join(modulesDir, 'loganalytics.bicep'), logAnalyticsBicep);
|
|
1538
1943
|
// modules/appinsights.bicep (Application Insights only, connects to shared Log Analytics)
|
|
1539
|
-
const appInsightsBicep = `@description('Application Insights name')
|
|
1540
|
-
param name string
|
|
1541
|
-
|
|
1542
|
-
@description('Location for Application Insights')
|
|
1543
|
-
param location string
|
|
1544
|
-
|
|
1545
|
-
@description('Log Analytics workspace resource ID')
|
|
1546
|
-
param logAnalyticsWorkspaceId string
|
|
1547
|
-
|
|
1548
|
-
// Application Insights
|
|
1549
|
-
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
|
1550
|
-
name: name
|
|
1551
|
-
location: location
|
|
1552
|
-
kind: 'web'
|
|
1553
|
-
properties: {
|
|
1554
|
-
Application_Type: 'web'
|
|
1555
|
-
WorkspaceResourceId: logAnalyticsWorkspaceId
|
|
1556
|
-
RetentionInDays: 30
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
output id string = appInsights.id
|
|
1561
|
-
output name string = appInsights.name
|
|
1562
|
-
output connectionString string = appInsights.properties.ConnectionString
|
|
1563
|
-
output instrumentationKey string = appInsights.properties.InstrumentationKey
|
|
1944
|
+
const appInsightsBicep = `@description('Application Insights name')
|
|
1945
|
+
param name string
|
|
1946
|
+
|
|
1947
|
+
@description('Location for Application Insights')
|
|
1948
|
+
param location string
|
|
1949
|
+
|
|
1950
|
+
@description('Log Analytics workspace resource ID')
|
|
1951
|
+
param logAnalyticsWorkspaceId string
|
|
1952
|
+
|
|
1953
|
+
// Application Insights
|
|
1954
|
+
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
|
1955
|
+
name: name
|
|
1956
|
+
location: location
|
|
1957
|
+
kind: 'web'
|
|
1958
|
+
properties: {
|
|
1959
|
+
Application_Type: 'web'
|
|
1960
|
+
WorkspaceResourceId: logAnalyticsWorkspaceId
|
|
1961
|
+
RetentionInDays: 30
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
output id string = appInsights.id
|
|
1966
|
+
output name string = appInsights.name
|
|
1967
|
+
output connectionString string = appInsights.properties.ConnectionString
|
|
1968
|
+
output instrumentationKey string = appInsights.properties.InstrumentationKey
|
|
1564
1969
|
`;
|
|
1565
1970
|
fs.writeFileSync(path.join(modulesDir, 'appinsights.bicep'), appInsightsBicep);
|
|
1566
1971
|
// modules/functions-flex.bicep (Flex Consumption)
|
|
1567
|
-
const functionsFlexBicep = `@description('Functions App name')
|
|
1568
|
-
param name string
|
|
1569
|
-
|
|
1570
|
-
@description('Location for the Functions App')
|
|
1571
|
-
param location string
|
|
1572
|
-
|
|
1573
|
-
@description('Storage account name for Functions')
|
|
1574
|
-
param storageAccountName string
|
|
1575
|
-
|
|
1576
|
-
@description('Application Insights connection string')
|
|
1577
|
-
param appInsightsConnectionString string
|
|
1578
|
-
|
|
1579
|
-
@description('Static Web App default hostname for CORS')
|
|
1580
|
-
param swaDefaultHostname string
|
|
1581
|
-
|
|
1582
|
-
@description('Cosmos DB endpoint')
|
|
1583
|
-
param cosmosDbEndpoint string
|
|
1584
|
-
|
|
1585
|
-
@description('Cosmos DB database name')
|
|
1586
|
-
param cosmosDbDatabaseName string
|
|
1587
|
-
|
|
1588
|
-
@description('Enable VNet integration')
|
|
1589
|
-
param enableVNet bool = false
|
|
1590
|
-
|
|
1591
|
-
@description('VNet subnet ID for Functions (required if enableVNet is true)')
|
|
1592
|
-
param vnetSubnetId string = ''
|
|
1593
|
-
|
|
1594
|
-
// Storage Account for Functions
|
|
1595
|
-
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
|
1596
|
-
name: storageAccountName
|
|
1597
|
-
location: location
|
|
1598
|
-
sku: {
|
|
1599
|
-
name: 'Standard_LRS'
|
|
1600
|
-
}
|
|
1601
|
-
kind: 'StorageV2'
|
|
1602
|
-
properties: {
|
|
1603
|
-
supportsHttpsTrafficOnly: true
|
|
1604
|
-
minimumTlsVersion: 'TLS1_2'
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
// Blob Service for deployment package container
|
|
1609
|
-
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
|
|
1610
|
-
parent: storageAccount
|
|
1611
|
-
name: 'default'
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
// Deployment package container
|
|
1615
|
-
resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
|
|
1616
|
-
parent: blobService
|
|
1617
|
-
name: 'deploymentpackage'
|
|
1618
|
-
properties: {
|
|
1619
|
-
publicAccess: 'None'
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// App Service Plan (Flex Consumption)
|
|
1624
|
-
resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
|
|
1625
|
-
name: '\${name}-plan'
|
|
1626
|
-
location: location
|
|
1627
|
-
sku: {
|
|
1628
|
-
name: 'FC1'
|
|
1629
|
-
tier: 'FlexConsumption'
|
|
1630
|
-
}
|
|
1631
|
-
properties: {
|
|
1632
|
-
reserved: true // Required for Linux
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// Azure Functions App
|
|
1637
|
-
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
|
|
1638
|
-
name: name
|
|
1639
|
-
location: location
|
|
1640
|
-
kind: 'functionapp,linux'
|
|
1641
|
-
identity: {
|
|
1642
|
-
type: 'SystemAssigned'
|
|
1643
|
-
}
|
|
1644
|
-
properties: {
|
|
1645
|
-
serverFarmId: hostingPlan.id
|
|
1646
|
-
reserved: true
|
|
1647
|
-
virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
|
|
1648
|
-
vnetContentShareEnabled: enableVNet
|
|
1649
|
-
functionAppConfig: {
|
|
1650
|
-
deployment: {
|
|
1651
|
-
storage: {
|
|
1652
|
-
type: 'blobContainer'
|
|
1653
|
-
value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
|
|
1654
|
-
authentication: {
|
|
1655
|
-
type: 'SystemAssignedIdentity'
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
scaleAndConcurrency: {
|
|
1660
|
-
maximumInstanceCount: 100
|
|
1661
|
-
instanceMemoryMB: 2048
|
|
1662
|
-
}
|
|
1663
|
-
runtime: {
|
|
1664
|
-
name: 'node'
|
|
1665
|
-
version: '22'
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
siteConfig: {
|
|
1669
|
-
appSettings: [
|
|
1670
|
-
{
|
|
1671
|
-
name: 'AzureWebJobsStorage__accountName'
|
|
1672
|
-
value: storageAccount.name
|
|
1673
|
-
}
|
|
1674
|
-
{
|
|
1675
|
-
name: 'FUNCTIONS_EXTENSION_VERSION'
|
|
1676
|
-
value: '~4'
|
|
1677
|
-
}
|
|
1678
|
-
{
|
|
1679
|
-
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
|
1680
|
-
value: appInsightsConnectionString
|
|
1681
|
-
}
|
|
1682
|
-
{
|
|
1683
|
-
name: 'CosmosDBConnection__accountEndpoint'
|
|
1684
|
-
value: cosmosDbEndpoint
|
|
1685
|
-
}
|
|
1686
|
-
{
|
|
1687
|
-
name: 'COSMOS_DB_DATABASE_NAME'
|
|
1688
|
-
value: cosmosDbDatabaseName
|
|
1689
|
-
}
|
|
1690
|
-
]
|
|
1691
|
-
cors: {
|
|
1692
|
-
allowedOrigins: [
|
|
1693
|
-
'https://\${swaDefaultHostname}'
|
|
1694
|
-
]
|
|
1695
|
-
}
|
|
1696
|
-
ipSecurityRestrictions: [
|
|
1697
|
-
{
|
|
1698
|
-
action: 'Allow'
|
|
1699
|
-
ipAddress: 'AzureCloud'
|
|
1700
|
-
tag: 'ServiceTag'
|
|
1701
|
-
priority: 100
|
|
1702
|
-
}
|
|
1703
|
-
]
|
|
1704
|
-
}
|
|
1705
|
-
httpsOnly: true
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
// Role Assignment: Storage Blob Data Contributor
|
|
1710
|
-
resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
1711
|
-
name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
1712
|
-
scope: storageAccount
|
|
1713
|
-
properties: {
|
|
1714
|
-
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
1715
|
-
principalId: functionApp.identity.principalId
|
|
1716
|
-
principalType: 'ServicePrincipal'
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
output id string = functionApp.id
|
|
1721
|
-
output name string = functionApp.name
|
|
1722
|
-
output defaultHostname string = functionApp.properties.defaultHostName
|
|
1723
|
-
output principalId string = functionApp.identity.principalId
|
|
1972
|
+
const functionsFlexBicep = `@description('Functions App name')
|
|
1973
|
+
param name string
|
|
1974
|
+
|
|
1975
|
+
@description('Location for the Functions App')
|
|
1976
|
+
param location string
|
|
1977
|
+
|
|
1978
|
+
@description('Storage account name for Functions')
|
|
1979
|
+
param storageAccountName string
|
|
1980
|
+
|
|
1981
|
+
@description('Application Insights connection string')
|
|
1982
|
+
param appInsightsConnectionString string
|
|
1983
|
+
|
|
1984
|
+
@description('Static Web App default hostname for CORS')
|
|
1985
|
+
param swaDefaultHostname string
|
|
1986
|
+
|
|
1987
|
+
@description('Cosmos DB endpoint')
|
|
1988
|
+
param cosmosDbEndpoint string
|
|
1989
|
+
|
|
1990
|
+
@description('Cosmos DB database name')
|
|
1991
|
+
param cosmosDbDatabaseName string
|
|
1992
|
+
|
|
1993
|
+
@description('Enable VNet integration')
|
|
1994
|
+
param enableVNet bool = false
|
|
1995
|
+
|
|
1996
|
+
@description('VNet subnet ID for Functions (required if enableVNet is true)')
|
|
1997
|
+
param vnetSubnetId string = ''
|
|
1998
|
+
|
|
1999
|
+
// Storage Account for Functions
|
|
2000
|
+
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
|
2001
|
+
name: storageAccountName
|
|
2002
|
+
location: location
|
|
2003
|
+
sku: {
|
|
2004
|
+
name: 'Standard_LRS'
|
|
2005
|
+
}
|
|
2006
|
+
kind: 'StorageV2'
|
|
2007
|
+
properties: {
|
|
2008
|
+
supportsHttpsTrafficOnly: true
|
|
2009
|
+
minimumTlsVersion: 'TLS1_2'
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Blob Service for deployment package container
|
|
2014
|
+
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
|
|
2015
|
+
parent: storageAccount
|
|
2016
|
+
name: 'default'
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Deployment package container
|
|
2020
|
+
resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
|
|
2021
|
+
parent: blobService
|
|
2022
|
+
name: 'deploymentpackage'
|
|
2023
|
+
properties: {
|
|
2024
|
+
publicAccess: 'None'
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// App Service Plan (Flex Consumption)
|
|
2029
|
+
resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
|
|
2030
|
+
name: '\${name}-plan'
|
|
2031
|
+
location: location
|
|
2032
|
+
sku: {
|
|
2033
|
+
name: 'FC1'
|
|
2034
|
+
tier: 'FlexConsumption'
|
|
2035
|
+
}
|
|
2036
|
+
properties: {
|
|
2037
|
+
reserved: true // Required for Linux
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Azure Functions App
|
|
2042
|
+
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
|
|
2043
|
+
name: name
|
|
2044
|
+
location: location
|
|
2045
|
+
kind: 'functionapp,linux'
|
|
2046
|
+
identity: {
|
|
2047
|
+
type: 'SystemAssigned'
|
|
2048
|
+
}
|
|
2049
|
+
properties: {
|
|
2050
|
+
serverFarmId: hostingPlan.id
|
|
2051
|
+
reserved: true
|
|
2052
|
+
virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
|
|
2053
|
+
vnetContentShareEnabled: enableVNet
|
|
2054
|
+
functionAppConfig: {
|
|
2055
|
+
deployment: {
|
|
2056
|
+
storage: {
|
|
2057
|
+
type: 'blobContainer'
|
|
2058
|
+
value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
|
|
2059
|
+
authentication: {
|
|
2060
|
+
type: 'SystemAssignedIdentity'
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
scaleAndConcurrency: {
|
|
2065
|
+
maximumInstanceCount: 100
|
|
2066
|
+
instanceMemoryMB: 2048
|
|
2067
|
+
}
|
|
2068
|
+
runtime: {
|
|
2069
|
+
name: 'node'
|
|
2070
|
+
version: '22'
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
siteConfig: {
|
|
2074
|
+
appSettings: [
|
|
2075
|
+
{
|
|
2076
|
+
name: 'AzureWebJobsStorage__accountName'
|
|
2077
|
+
value: storageAccount.name
|
|
2078
|
+
}
|
|
2079
|
+
{
|
|
2080
|
+
name: 'FUNCTIONS_EXTENSION_VERSION'
|
|
2081
|
+
value: '~4'
|
|
2082
|
+
}
|
|
2083
|
+
{
|
|
2084
|
+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
|
2085
|
+
value: appInsightsConnectionString
|
|
2086
|
+
}
|
|
2087
|
+
{
|
|
2088
|
+
name: 'CosmosDBConnection__accountEndpoint'
|
|
2089
|
+
value: cosmosDbEndpoint
|
|
2090
|
+
}
|
|
2091
|
+
{
|
|
2092
|
+
name: 'COSMOS_DB_DATABASE_NAME'
|
|
2093
|
+
value: cosmosDbDatabaseName
|
|
2094
|
+
}
|
|
2095
|
+
]
|
|
2096
|
+
cors: {
|
|
2097
|
+
allowedOrigins: [
|
|
2098
|
+
'https://\${swaDefaultHostname}'
|
|
2099
|
+
]
|
|
2100
|
+
}
|
|
2101
|
+
ipSecurityRestrictions: [
|
|
2102
|
+
{
|
|
2103
|
+
action: 'Allow'
|
|
2104
|
+
ipAddress: 'AzureCloud'
|
|
2105
|
+
tag: 'ServiceTag'
|
|
2106
|
+
priority: 100
|
|
2107
|
+
}
|
|
2108
|
+
]
|
|
2109
|
+
}
|
|
2110
|
+
httpsOnly: true
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Role Assignment: Storage Blob Data Contributor
|
|
2115
|
+
resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
2116
|
+
name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
2117
|
+
scope: storageAccount
|
|
2118
|
+
properties: {
|
|
2119
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
2120
|
+
principalId: functionApp.identity.principalId
|
|
2121
|
+
principalType: 'ServicePrincipal'
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
output id string = functionApp.id
|
|
2126
|
+
output name string = functionApp.name
|
|
2127
|
+
output defaultHostname string = functionApp.properties.defaultHostName
|
|
2128
|
+
output principalId string = functionApp.identity.principalId
|
|
1724
2129
|
`;
|
|
1725
2130
|
fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
|
|
1726
2131
|
// modules/cosmosdb-freetier.bicep (Free Tier)
|
|
1727
|
-
const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
|
|
1728
|
-
param accountName string
|
|
1729
|
-
|
|
1730
|
-
@description('Database name')
|
|
1731
|
-
param databaseName string
|
|
1732
|
-
|
|
1733
|
-
@description('Location for Cosmos DB')
|
|
1734
|
-
param location string
|
|
1735
|
-
|
|
1736
|
-
@description('Public network access')
|
|
1737
|
-
@allowed(['Enabled', 'Disabled'])
|
|
1738
|
-
param publicNetworkAccess string = 'Enabled'
|
|
1739
|
-
|
|
1740
|
-
// Cosmos DB Account (Free Tier)
|
|
1741
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
1742
|
-
name: accountName
|
|
1743
|
-
location: location
|
|
1744
|
-
kind: 'GlobalDocumentDB'
|
|
1745
|
-
properties: {
|
|
1746
|
-
databaseAccountOfferType: 'Standard'
|
|
1747
|
-
enableAutomaticFailover: false
|
|
1748
|
-
enableFreeTier: true
|
|
1749
|
-
publicNetworkAccess: publicNetworkAccess
|
|
1750
|
-
disableLocalAuth: true
|
|
1751
|
-
consistencyPolicy: {
|
|
1752
|
-
defaultConsistencyLevel: 'Session'
|
|
1753
|
-
}
|
|
1754
|
-
locations: [
|
|
1755
|
-
{
|
|
1756
|
-
locationName: location
|
|
1757
|
-
failoverPriority: 0
|
|
1758
|
-
isZoneRedundant: false
|
|
1759
|
-
}
|
|
1760
|
-
]
|
|
1761
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
// Cosmos DB Database
|
|
1766
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
1767
|
-
parent: cosmosAccount
|
|
1768
|
-
name: databaseName
|
|
1769
|
-
properties: {
|
|
1770
|
-
resource: {
|
|
1771
|
-
id: databaseName
|
|
1772
|
-
}
|
|
1773
|
-
options: {
|
|
1774
|
-
throughput: 1000
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
output id string = cosmosAccount.id
|
|
1780
|
-
output accountName string = cosmosAccount.name
|
|
1781
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
1782
|
-
output databaseName string = database.name
|
|
2132
|
+
const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
|
|
2133
|
+
param accountName string
|
|
2134
|
+
|
|
2135
|
+
@description('Database name')
|
|
2136
|
+
param databaseName string
|
|
2137
|
+
|
|
2138
|
+
@description('Location for Cosmos DB')
|
|
2139
|
+
param location string
|
|
2140
|
+
|
|
2141
|
+
@description('Public network access')
|
|
2142
|
+
@allowed(['Enabled', 'Disabled'])
|
|
2143
|
+
param publicNetworkAccess string = 'Enabled'
|
|
2144
|
+
|
|
2145
|
+
// Cosmos DB Account (Free Tier)
|
|
2146
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
2147
|
+
name: accountName
|
|
2148
|
+
location: location
|
|
2149
|
+
kind: 'GlobalDocumentDB'
|
|
2150
|
+
properties: {
|
|
2151
|
+
databaseAccountOfferType: 'Standard'
|
|
2152
|
+
enableAutomaticFailover: false
|
|
2153
|
+
enableFreeTier: true
|
|
2154
|
+
publicNetworkAccess: publicNetworkAccess
|
|
2155
|
+
disableLocalAuth: true
|
|
2156
|
+
consistencyPolicy: {
|
|
2157
|
+
defaultConsistencyLevel: 'Session'
|
|
2158
|
+
}
|
|
2159
|
+
locations: [
|
|
2160
|
+
{
|
|
2161
|
+
locationName: location
|
|
2162
|
+
failoverPriority: 0
|
|
2163
|
+
isZoneRedundant: false
|
|
2164
|
+
}
|
|
2165
|
+
]
|
|
2166
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// Cosmos DB Database
|
|
2171
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
2172
|
+
parent: cosmosAccount
|
|
2173
|
+
name: databaseName
|
|
2174
|
+
properties: {
|
|
2175
|
+
resource: {
|
|
2176
|
+
id: databaseName
|
|
2177
|
+
}
|
|
2178
|
+
options: {
|
|
2179
|
+
throughput: 1000
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
output id string = cosmosAccount.id
|
|
2185
|
+
output accountName string = cosmosAccount.name
|
|
2186
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
2187
|
+
output databaseName string = database.name
|
|
1783
2188
|
`;
|
|
1784
2189
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
|
|
1785
2190
|
// modules/cosmosdb-serverless.bicep (Serverless)
|
|
1786
|
-
const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
|
|
1787
|
-
param accountName string
|
|
1788
|
-
|
|
1789
|
-
@description('Database name')
|
|
1790
|
-
param databaseName string
|
|
1791
|
-
|
|
1792
|
-
@description('Location for Cosmos DB')
|
|
1793
|
-
param location string
|
|
1794
|
-
|
|
1795
|
-
@description('Public network access')
|
|
1796
|
-
@allowed(['Enabled', 'Disabled'])
|
|
1797
|
-
param publicNetworkAccess string = 'Enabled'
|
|
1798
|
-
|
|
1799
|
-
// Cosmos DB Account (Serverless)
|
|
1800
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
1801
|
-
name: accountName
|
|
1802
|
-
location: location
|
|
1803
|
-
kind: 'GlobalDocumentDB'
|
|
1804
|
-
properties: {
|
|
1805
|
-
databaseAccountOfferType: 'Standard'
|
|
1806
|
-
enableAutomaticFailover: false
|
|
1807
|
-
publicNetworkAccess: publicNetworkAccess
|
|
1808
|
-
disableLocalAuth: true
|
|
1809
|
-
consistencyPolicy: {
|
|
1810
|
-
defaultConsistencyLevel: 'Session'
|
|
1811
|
-
}
|
|
1812
|
-
locations: [
|
|
1813
|
-
{
|
|
1814
|
-
locationName: location
|
|
1815
|
-
failoverPriority: 0
|
|
1816
|
-
isZoneRedundant: false
|
|
1817
|
-
}
|
|
1818
|
-
]
|
|
1819
|
-
capabilities: [
|
|
1820
|
-
{
|
|
1821
|
-
name: 'EnableServerless'
|
|
1822
|
-
}
|
|
1823
|
-
]
|
|
1824
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
// Cosmos DB Database
|
|
1829
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
1830
|
-
parent: cosmosAccount
|
|
1831
|
-
name: databaseName
|
|
1832
|
-
properties: {
|
|
1833
|
-
resource: {
|
|
1834
|
-
id: databaseName
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
output id string = cosmosAccount.id
|
|
1840
|
-
output accountName string = cosmosAccount.name
|
|
1841
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
1842
|
-
output databaseName string = database.name
|
|
2191
|
+
const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
|
|
2192
|
+
param accountName string
|
|
2193
|
+
|
|
2194
|
+
@description('Database name')
|
|
2195
|
+
param databaseName string
|
|
2196
|
+
|
|
2197
|
+
@description('Location for Cosmos DB')
|
|
2198
|
+
param location string
|
|
2199
|
+
|
|
2200
|
+
@description('Public network access')
|
|
2201
|
+
@allowed(['Enabled', 'Disabled'])
|
|
2202
|
+
param publicNetworkAccess string = 'Enabled'
|
|
2203
|
+
|
|
2204
|
+
// Cosmos DB Account (Serverless)
|
|
2205
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
2206
|
+
name: accountName
|
|
2207
|
+
location: location
|
|
2208
|
+
kind: 'GlobalDocumentDB'
|
|
2209
|
+
properties: {
|
|
2210
|
+
databaseAccountOfferType: 'Standard'
|
|
2211
|
+
enableAutomaticFailover: false
|
|
2212
|
+
publicNetworkAccess: publicNetworkAccess
|
|
2213
|
+
disableLocalAuth: true
|
|
2214
|
+
consistencyPolicy: {
|
|
2215
|
+
defaultConsistencyLevel: 'Session'
|
|
2216
|
+
}
|
|
2217
|
+
locations: [
|
|
2218
|
+
{
|
|
2219
|
+
locationName: location
|
|
2220
|
+
failoverPriority: 0
|
|
2221
|
+
isZoneRedundant: false
|
|
2222
|
+
}
|
|
2223
|
+
]
|
|
2224
|
+
capabilities: [
|
|
2225
|
+
{
|
|
2226
|
+
name: 'EnableServerless'
|
|
2227
|
+
}
|
|
2228
|
+
]
|
|
2229
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Cosmos DB Database
|
|
2234
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
2235
|
+
parent: cosmosAccount
|
|
2236
|
+
name: databaseName
|
|
2237
|
+
properties: {
|
|
2238
|
+
resource: {
|
|
2239
|
+
id: databaseName
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
output id string = cosmosAccount.id
|
|
2245
|
+
output accountName string = cosmosAccount.name
|
|
2246
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
2247
|
+
output databaseName string = database.name
|
|
1843
2248
|
`;
|
|
1844
2249
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-serverless.bicep'), cosmosDbServerlessBicep);
|
|
1845
2250
|
// modules/cosmosdb-role-assignment.bicep (Role Assignment Module)
|
|
1846
|
-
const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
|
|
1847
|
-
param cosmosAccountName string
|
|
1848
|
-
|
|
1849
|
-
@description('Functions App Managed Identity Principal ID')
|
|
1850
|
-
param functionsPrincipalId string
|
|
1851
|
-
|
|
1852
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
1853
|
-
name: cosmosAccountName
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
// Built-in Cosmos DB Data Contributor role definition
|
|
1857
|
-
var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
|
|
1858
|
-
|
|
1859
|
-
// Role assignment for Functions to access Cosmos DB
|
|
1860
|
-
resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
|
|
1861
|
-
parent: cosmosAccount
|
|
1862
|
-
name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
|
|
1863
|
-
properties: {
|
|
1864
|
-
roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
|
|
1865
|
-
principalId: functionsPrincipalId
|
|
1866
|
-
scope: cosmosAccount.id
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
output roleAssignmentId string = roleAssignment.id
|
|
2251
|
+
const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
|
|
2252
|
+
param cosmosAccountName string
|
|
2253
|
+
|
|
2254
|
+
@description('Functions App Managed Identity Principal ID')
|
|
2255
|
+
param functionsPrincipalId string
|
|
2256
|
+
|
|
2257
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
2258
|
+
name: cosmosAccountName
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Built-in Cosmos DB Data Contributor role definition
|
|
2262
|
+
var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
|
|
2263
|
+
|
|
2264
|
+
// Role assignment for Functions to access Cosmos DB
|
|
2265
|
+
resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
|
|
2266
|
+
parent: cosmosAccount
|
|
2267
|
+
name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
|
|
2268
|
+
properties: {
|
|
2269
|
+
roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
|
|
2270
|
+
principalId: functionsPrincipalId
|
|
2271
|
+
scope: cosmosAccount.id
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
output roleAssignmentId string = roleAssignment.id
|
|
1871
2276
|
`;
|
|
1872
2277
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
|
|
1873
2278
|
// VNet modules (only generate if VNet is enabled)
|
|
1874
2279
|
if (enableVNet) {
|
|
1875
2280
|
// modules/vnet.bicep
|
|
1876
|
-
const vnetBicep = `@description('VNet name')
|
|
1877
|
-
param name string
|
|
1878
|
-
|
|
1879
|
-
@description('Location for VNet')
|
|
1880
|
-
param location string
|
|
1881
|
-
|
|
1882
|
-
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
|
|
1883
|
-
name: name
|
|
1884
|
-
location: location
|
|
1885
|
-
properties: {
|
|
1886
|
-
addressSpace: {
|
|
1887
|
-
addressPrefixes: [
|
|
1888
|
-
'10.0.0.0/16'
|
|
1889
|
-
]
|
|
1890
|
-
}
|
|
1891
|
-
subnets: [
|
|
1892
|
-
{
|
|
1893
|
-
name: 'snet-functions'
|
|
1894
|
-
properties: {
|
|
1895
|
-
addressPrefix: '10.0.1.0/24'
|
|
1896
|
-
delegations: [
|
|
1897
|
-
{
|
|
1898
|
-
name: 'delegation'
|
|
1899
|
-
properties: {
|
|
1900
|
-
serviceName: 'Microsoft.App/environments'
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
]
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
{
|
|
1907
|
-
name: 'snet-private-endpoints'
|
|
1908
|
-
properties: {
|
|
1909
|
-
addressPrefix: '10.0.2.0/24'
|
|
1910
|
-
privateEndpointNetworkPolicies: 'Disabled'
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
]
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
output id string = vnet.id
|
|
1918
|
-
output name string = vnet.name
|
|
1919
|
-
output functionsSubnetId string = vnet.properties.subnets[0].id
|
|
1920
|
-
output privateEndpointSubnetId string = vnet.properties.subnets[1].id
|
|
2281
|
+
const vnetBicep = `@description('VNet name')
|
|
2282
|
+
param name string
|
|
2283
|
+
|
|
2284
|
+
@description('Location for VNet')
|
|
2285
|
+
param location string
|
|
2286
|
+
|
|
2287
|
+
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
|
|
2288
|
+
name: name
|
|
2289
|
+
location: location
|
|
2290
|
+
properties: {
|
|
2291
|
+
addressSpace: {
|
|
2292
|
+
addressPrefixes: [
|
|
2293
|
+
'10.0.0.0/16'
|
|
2294
|
+
]
|
|
2295
|
+
}
|
|
2296
|
+
subnets: [
|
|
2297
|
+
{
|
|
2298
|
+
name: 'snet-functions'
|
|
2299
|
+
properties: {
|
|
2300
|
+
addressPrefix: '10.0.1.0/24'
|
|
2301
|
+
delegations: [
|
|
2302
|
+
{
|
|
2303
|
+
name: 'delegation'
|
|
2304
|
+
properties: {
|
|
2305
|
+
serviceName: 'Microsoft.App/environments'
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
]
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
{
|
|
2312
|
+
name: 'snet-private-endpoints'
|
|
2313
|
+
properties: {
|
|
2314
|
+
addressPrefix: '10.0.2.0/24'
|
|
2315
|
+
privateEndpointNetworkPolicies: 'Disabled'
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
]
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
output id string = vnet.id
|
|
2323
|
+
output name string = vnet.name
|
|
2324
|
+
output functionsSubnetId string = vnet.properties.subnets[0].id
|
|
2325
|
+
output privateEndpointSubnetId string = vnet.properties.subnets[1].id
|
|
1921
2326
|
`;
|
|
1922
2327
|
fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
|
|
1923
2328
|
// modules/private-endpoint-cosmos.bicep
|
|
1924
|
-
const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
|
|
1925
|
-
param name string
|
|
1926
|
-
|
|
1927
|
-
@description('Location')
|
|
1928
|
-
param location string
|
|
1929
|
-
|
|
1930
|
-
@description('Cosmos DB account resource ID')
|
|
1931
|
-
param cosmosAccountId string
|
|
1932
|
-
|
|
1933
|
-
@description('Cosmos DB account name')
|
|
1934
|
-
param cosmosAccountName string
|
|
1935
|
-
|
|
1936
|
-
@description('Subnet ID for private endpoint')
|
|
1937
|
-
param subnetId string
|
|
1938
|
-
|
|
1939
|
-
@description('VNet ID for DNS zone link')
|
|
1940
|
-
param vnetId string
|
|
1941
|
-
|
|
1942
|
-
// Private DNS Zone for Cosmos DB
|
|
1943
|
-
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
|
|
1944
|
-
name: 'privatelink.documents.azure.com'
|
|
1945
|
-
location: 'global'
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
// Link DNS Zone to VNet
|
|
1949
|
-
resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
|
|
1950
|
-
parent: privateDnsZone
|
|
1951
|
-
name: '\${cosmosAccountName}-vnet-link'
|
|
1952
|
-
location: 'global'
|
|
1953
|
-
properties: {
|
|
1954
|
-
virtualNetwork: {
|
|
1955
|
-
id: vnetId
|
|
1956
|
-
}
|
|
1957
|
-
registrationEnabled: false
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
// Private Endpoint for Cosmos DB
|
|
1962
|
-
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
|
|
1963
|
-
name: name
|
|
1964
|
-
location: location
|
|
1965
|
-
properties: {
|
|
1966
|
-
subnet: {
|
|
1967
|
-
id: subnetId
|
|
1968
|
-
}
|
|
1969
|
-
privateLinkServiceConnections: [
|
|
1970
|
-
{
|
|
1971
|
-
name: '\${cosmosAccountName}-connection'
|
|
1972
|
-
properties: {
|
|
1973
|
-
privateLinkServiceId: cosmosAccountId
|
|
1974
|
-
groupIds: [
|
|
1975
|
-
'Sql'
|
|
1976
|
-
]
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
]
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
// DNS Zone Group
|
|
1984
|
-
resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
|
|
1985
|
-
parent: privateEndpoint
|
|
1986
|
-
name: 'default'
|
|
1987
|
-
properties: {
|
|
1988
|
-
privateDnsZoneConfigs: [
|
|
1989
|
-
{
|
|
1990
|
-
name: 'cosmos-dns-config'
|
|
1991
|
-
properties: {
|
|
1992
|
-
privateDnsZoneId: privateDnsZone.id
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
]
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
output privateEndpointId string = privateEndpoint.id
|
|
2000
|
-
output privateDnsZoneId string = privateDnsZone.id
|
|
2329
|
+
const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
|
|
2330
|
+
param name string
|
|
2331
|
+
|
|
2332
|
+
@description('Location')
|
|
2333
|
+
param location string
|
|
2334
|
+
|
|
2335
|
+
@description('Cosmos DB account resource ID')
|
|
2336
|
+
param cosmosAccountId string
|
|
2337
|
+
|
|
2338
|
+
@description('Cosmos DB account name')
|
|
2339
|
+
param cosmosAccountName string
|
|
2340
|
+
|
|
2341
|
+
@description('Subnet ID for private endpoint')
|
|
2342
|
+
param subnetId string
|
|
2343
|
+
|
|
2344
|
+
@description('VNet ID for DNS zone link')
|
|
2345
|
+
param vnetId string
|
|
2346
|
+
|
|
2347
|
+
// Private DNS Zone for Cosmos DB
|
|
2348
|
+
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
|
|
2349
|
+
name: 'privatelink.documents.azure.com'
|
|
2350
|
+
location: 'global'
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// Link DNS Zone to VNet
|
|
2354
|
+
resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
|
|
2355
|
+
parent: privateDnsZone
|
|
2356
|
+
name: '\${cosmosAccountName}-vnet-link'
|
|
2357
|
+
location: 'global'
|
|
2358
|
+
properties: {
|
|
2359
|
+
virtualNetwork: {
|
|
2360
|
+
id: vnetId
|
|
2361
|
+
}
|
|
2362
|
+
registrationEnabled: false
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// Private Endpoint for Cosmos DB
|
|
2367
|
+
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
|
|
2368
|
+
name: name
|
|
2369
|
+
location: location
|
|
2370
|
+
properties: {
|
|
2371
|
+
subnet: {
|
|
2372
|
+
id: subnetId
|
|
2373
|
+
}
|
|
2374
|
+
privateLinkServiceConnections: [
|
|
2375
|
+
{
|
|
2376
|
+
name: '\${cosmosAccountName}-connection'
|
|
2377
|
+
properties: {
|
|
2378
|
+
privateLinkServiceId: cosmosAccountId
|
|
2379
|
+
groupIds: [
|
|
2380
|
+
'Sql'
|
|
2381
|
+
]
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
]
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// DNS Zone Group
|
|
2389
|
+
resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
|
|
2390
|
+
parent: privateEndpoint
|
|
2391
|
+
name: 'default'
|
|
2392
|
+
properties: {
|
|
2393
|
+
privateDnsZoneConfigs: [
|
|
2394
|
+
{
|
|
2395
|
+
name: 'cosmos-dns-config'
|
|
2396
|
+
properties: {
|
|
2397
|
+
privateDnsZoneId: privateDnsZone.id
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
]
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
output privateEndpointId string = privateEndpoint.id
|
|
2405
|
+
output privateDnsZoneId string = privateDnsZone.id
|
|
2001
2406
|
`;
|
|
2002
2407
|
fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
|
|
2003
2408
|
console.log('✅ VNet modules created\n');
|
|
@@ -2011,123 +2416,123 @@ async function createGitHubActionsWorkflows(projectDir, azureConfig, pm) {
|
|
|
2011
2416
|
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
2012
2417
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
2013
2418
|
// deploy-swa.yml
|
|
2014
|
-
const swaWorkflow = `name: Deploy Static Web App
|
|
2015
|
-
|
|
2016
|
-
on:
|
|
2017
|
-
push:
|
|
2018
|
-
branches:
|
|
2019
|
-
- main
|
|
2020
|
-
paths:
|
|
2021
|
-
- 'app/**'
|
|
2022
|
-
- 'components/**'
|
|
2023
|
-
- 'lib/**'
|
|
2024
|
-
- 'shared/**'
|
|
2025
|
-
- 'public/**'
|
|
2026
|
-
- 'package.json'
|
|
2027
|
-
- 'next.config.js'
|
|
2028
|
-
- 'next.config.ts'
|
|
2029
|
-
workflow_dispatch:
|
|
2030
|
-
pull_request:
|
|
2031
|
-
branches:
|
|
2032
|
-
- main
|
|
2033
|
-
paths:
|
|
2034
|
-
- 'app/**'
|
|
2035
|
-
- 'components/**'
|
|
2036
|
-
- 'lib/**'
|
|
2037
|
-
- 'shared/**'
|
|
2038
|
-
- 'public/**'
|
|
2039
|
-
- 'package.json'
|
|
2040
|
-
- 'next.config.js'
|
|
2041
|
-
- 'next.config.ts'
|
|
2042
|
-
|
|
2043
|
-
jobs:
|
|
2044
|
-
build-and-deploy:
|
|
2045
|
-
runs-on: ubuntu-latest
|
|
2046
|
-
name: Build and Deploy Static Web App
|
|
2047
|
-
|
|
2048
|
-
steps:
|
|
2049
|
-
- uses: actions/checkout@v4
|
|
2050
|
-
with:
|
|
2051
|
-
submodules: true
|
|
2052
|
-
|
|
2053
|
-
- name: Deploy to Azure Static Web Apps
|
|
2054
|
-
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2055
|
-
uses: Azure/static-web-apps-deploy@v1
|
|
2056
|
-
with:
|
|
2057
|
-
azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
|
2058
|
-
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
2059
|
-
action: 'upload'
|
|
2060
|
-
app_location: '/'
|
|
2061
|
-
api_location: ''
|
|
2062
|
-
output_location: ''
|
|
2063
|
-
env:
|
|
2064
|
-
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
2419
|
+
const swaWorkflow = `name: Deploy Static Web App
|
|
2420
|
+
|
|
2421
|
+
on:
|
|
2422
|
+
push:
|
|
2423
|
+
branches:
|
|
2424
|
+
- main
|
|
2425
|
+
paths:
|
|
2426
|
+
- 'app/**'
|
|
2427
|
+
- 'components/**'
|
|
2428
|
+
- 'lib/**'
|
|
2429
|
+
- 'shared/**'
|
|
2430
|
+
- 'public/**'
|
|
2431
|
+
- 'package.json'
|
|
2432
|
+
- 'next.config.js'
|
|
2433
|
+
- 'next.config.ts'
|
|
2434
|
+
workflow_dispatch:
|
|
2435
|
+
pull_request:
|
|
2436
|
+
branches:
|
|
2437
|
+
- main
|
|
2438
|
+
paths:
|
|
2439
|
+
- 'app/**'
|
|
2440
|
+
- 'components/**'
|
|
2441
|
+
- 'lib/**'
|
|
2442
|
+
- 'shared/**'
|
|
2443
|
+
- 'public/**'
|
|
2444
|
+
- 'package.json'
|
|
2445
|
+
- 'next.config.js'
|
|
2446
|
+
- 'next.config.ts'
|
|
2447
|
+
|
|
2448
|
+
jobs:
|
|
2449
|
+
build-and-deploy:
|
|
2450
|
+
runs-on: ubuntu-latest
|
|
2451
|
+
name: Build and Deploy Static Web App
|
|
2452
|
+
|
|
2453
|
+
steps:
|
|
2454
|
+
- uses: actions/checkout@v4
|
|
2455
|
+
with:
|
|
2456
|
+
submodules: true
|
|
2457
|
+
|
|
2458
|
+
- name: Deploy to Azure Static Web Apps
|
|
2459
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2460
|
+
uses: Azure/static-web-apps-deploy@v1
|
|
2461
|
+
with:
|
|
2462
|
+
azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
|
2463
|
+
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
2464
|
+
action: 'upload'
|
|
2465
|
+
app_location: '/'
|
|
2466
|
+
api_location: ''
|
|
2467
|
+
output_location: ''
|
|
2468
|
+
env:
|
|
2469
|
+
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
2065
2470
|
`;
|
|
2066
2471
|
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
2067
2472
|
// deploy-functions.yml
|
|
2068
|
-
const functionsWorkflow = `name: Deploy Azure Functions
|
|
2069
|
-
|
|
2070
|
-
on:
|
|
2071
|
-
push:
|
|
2072
|
-
branches:
|
|
2073
|
-
- main
|
|
2074
|
-
paths:
|
|
2075
|
-
- 'functions/**'
|
|
2076
|
-
- 'shared/**'
|
|
2077
|
-
pull_request:
|
|
2078
|
-
branches:
|
|
2079
|
-
- main
|
|
2080
|
-
paths:
|
|
2081
|
-
- 'functions/**'
|
|
2082
|
-
- 'shared/**'
|
|
2083
|
-
workflow_dispatch:
|
|
2084
|
-
|
|
2085
|
-
jobs:
|
|
2086
|
-
build-and-deploy:
|
|
2087
|
-
runs-on: ubuntu-latest
|
|
2088
|
-
name: Build and Deploy Functions
|
|
2089
|
-
|
|
2090
|
-
steps:
|
|
2091
|
-
- uses: actions/checkout@v4
|
|
2092
|
-
|
|
2093
|
-
- name: Setup Node.js
|
|
2094
|
-
uses: actions/setup-node@v4
|
|
2095
|
-
with:
|
|
2096
|
-
node-version: '22'
|
|
2097
|
-
${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
|
|
2098
|
-
- name: Install dependencies
|
|
2099
|
-
run: |
|
|
2100
|
-
${pmCmd.ci}
|
|
2101
|
-
|
|
2102
|
-
- name: Build shared package
|
|
2103
|
-
run: |
|
|
2104
|
-
${pmCmd.runFilter('shared')} build
|
|
2105
|
-
|
|
2106
|
-
- name: Build Functions
|
|
2107
|
-
run: |
|
|
2108
|
-
${pmCmd.runFilter('functions')} build
|
|
2109
|
-
|
|
2110
|
-
- name: Prepare functions for deployment
|
|
2111
|
-
run: |
|
|
2112
|
-
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2113
|
-
mkdir -p /tmp/fn-deps
|
|
2114
|
-
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2115
|
-
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2116
|
-
rm -rf ./functions/node_modules
|
|
2117
|
-
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2118
|
-
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2119
|
-
mkdir -p "$SHARED_DEST"
|
|
2120
|
-
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2121
|
-
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2122
|
-
|
|
2123
|
-
- name: Deploy to Azure Functions
|
|
2124
|
-
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2125
|
-
uses: Azure/functions-action@v1
|
|
2126
|
-
with:
|
|
2127
|
-
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2128
|
-
package: './functions'
|
|
2129
|
-
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2130
|
-
sku: flexconsumption
|
|
2473
|
+
const functionsWorkflow = `name: Deploy Azure Functions
|
|
2474
|
+
|
|
2475
|
+
on:
|
|
2476
|
+
push:
|
|
2477
|
+
branches:
|
|
2478
|
+
- main
|
|
2479
|
+
paths:
|
|
2480
|
+
- 'functions/**'
|
|
2481
|
+
- 'shared/**'
|
|
2482
|
+
pull_request:
|
|
2483
|
+
branches:
|
|
2484
|
+
- main
|
|
2485
|
+
paths:
|
|
2486
|
+
- 'functions/**'
|
|
2487
|
+
- 'shared/**'
|
|
2488
|
+
workflow_dispatch:
|
|
2489
|
+
|
|
2490
|
+
jobs:
|
|
2491
|
+
build-and-deploy:
|
|
2492
|
+
runs-on: ubuntu-latest
|
|
2493
|
+
name: Build and Deploy Functions
|
|
2494
|
+
|
|
2495
|
+
steps:
|
|
2496
|
+
- uses: actions/checkout@v4
|
|
2497
|
+
|
|
2498
|
+
- name: Setup Node.js
|
|
2499
|
+
uses: actions/setup-node@v4
|
|
2500
|
+
with:
|
|
2501
|
+
node-version: '22'
|
|
2502
|
+
${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
|
|
2503
|
+
- name: Install dependencies
|
|
2504
|
+
run: |
|
|
2505
|
+
${pmCmd.ci}
|
|
2506
|
+
|
|
2507
|
+
- name: Build shared package
|
|
2508
|
+
run: |
|
|
2509
|
+
${pmCmd.runFilter('shared')} build
|
|
2510
|
+
|
|
2511
|
+
- name: Build Functions
|
|
2512
|
+
run: |
|
|
2513
|
+
${pmCmd.runFilter('functions')} build
|
|
2514
|
+
|
|
2515
|
+
- name: Prepare functions for deployment
|
|
2516
|
+
run: |
|
|
2517
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2518
|
+
mkdir -p /tmp/fn-deps
|
|
2519
|
+
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2520
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2521
|
+
rm -rf ./functions/node_modules
|
|
2522
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2523
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2524
|
+
mkdir -p "$SHARED_DEST"
|
|
2525
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2526
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2527
|
+
|
|
2528
|
+
- name: Deploy to Azure Functions
|
|
2529
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2530
|
+
uses: Azure/functions-action@v1
|
|
2531
|
+
with:
|
|
2532
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2533
|
+
package: './functions'
|
|
2534
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2535
|
+
sku: flexconsumption
|
|
2131
2536
|
`;
|
|
2132
2537
|
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
2133
2538
|
console.log('✅ GitHub Actions workflows created\n');
|
|
@@ -2139,146 +2544,146 @@ async function createAzurePipelines(projectDir, pm) {
|
|
|
2139
2544
|
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
2140
2545
|
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
2141
2546
|
// swa.yml
|
|
2142
|
-
const swaPipeline = `trigger:
|
|
2143
|
-
branches:
|
|
2144
|
-
include:
|
|
2145
|
-
- main
|
|
2146
|
-
paths:
|
|
2147
|
-
include:
|
|
2148
|
-
- app/**
|
|
2149
|
-
- components/**
|
|
2150
|
-
- lib/**
|
|
2151
|
-
- shared/**
|
|
2152
|
-
- public/**
|
|
2153
|
-
- package.json
|
|
2154
|
-
- next.config.js
|
|
2155
|
-
|
|
2156
|
-
pr:
|
|
2157
|
-
branches:
|
|
2158
|
-
include:
|
|
2159
|
-
- main
|
|
2160
|
-
paths:
|
|
2161
|
-
include:
|
|
2162
|
-
- app/**
|
|
2163
|
-
- components/**
|
|
2164
|
-
- lib/**
|
|
2165
|
-
- shared/**
|
|
2166
|
-
- public/**
|
|
2167
|
-
- package.json
|
|
2168
|
-
- next.config.js
|
|
2169
|
-
|
|
2170
|
-
pool:
|
|
2171
|
-
vmImage: 'ubuntu-latest'
|
|
2172
|
-
|
|
2173
|
-
variables:
|
|
2174
|
-
- group: azure-deployment
|
|
2175
|
-
|
|
2176
|
-
steps:
|
|
2177
|
-
- task: NodeTool@0
|
|
2178
|
-
inputs:
|
|
2179
|
-
versionSpec: '22.x'
|
|
2180
|
-
displayName: 'Install Node.js'
|
|
2181
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2182
|
-
- script: |
|
|
2183
|
-
${pmCmd.ci}
|
|
2184
|
-
displayName: 'Install dependencies'
|
|
2185
|
-
|
|
2186
|
-
- script: |
|
|
2187
|
-
${pmCmd.run} build
|
|
2188
|
-
env:
|
|
2189
|
-
NODE_ENV: production
|
|
2190
|
-
displayName: 'Build Next.js app'
|
|
2191
|
-
|
|
2192
|
-
- task: AzureStaticWebApp@0
|
|
2193
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2194
|
-
inputs:
|
|
2195
|
-
app_location: '.'
|
|
2196
|
-
output_location: '.next/standalone'
|
|
2197
|
-
skip_app_build: true
|
|
2198
|
-
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
2199
|
-
displayName: 'Deploy to Azure Static Web Apps'
|
|
2547
|
+
const swaPipeline = `trigger:
|
|
2548
|
+
branches:
|
|
2549
|
+
include:
|
|
2550
|
+
- main
|
|
2551
|
+
paths:
|
|
2552
|
+
include:
|
|
2553
|
+
- app/**
|
|
2554
|
+
- components/**
|
|
2555
|
+
- lib/**
|
|
2556
|
+
- shared/**
|
|
2557
|
+
- public/**
|
|
2558
|
+
- package.json
|
|
2559
|
+
- next.config.js
|
|
2560
|
+
|
|
2561
|
+
pr:
|
|
2562
|
+
branches:
|
|
2563
|
+
include:
|
|
2564
|
+
- main
|
|
2565
|
+
paths:
|
|
2566
|
+
include:
|
|
2567
|
+
- app/**
|
|
2568
|
+
- components/**
|
|
2569
|
+
- lib/**
|
|
2570
|
+
- shared/**
|
|
2571
|
+
- public/**
|
|
2572
|
+
- package.json
|
|
2573
|
+
- next.config.js
|
|
2574
|
+
|
|
2575
|
+
pool:
|
|
2576
|
+
vmImage: 'ubuntu-latest'
|
|
2577
|
+
|
|
2578
|
+
variables:
|
|
2579
|
+
- group: azure-deployment
|
|
2580
|
+
|
|
2581
|
+
steps:
|
|
2582
|
+
- task: NodeTool@0
|
|
2583
|
+
inputs:
|
|
2584
|
+
versionSpec: '22.x'
|
|
2585
|
+
displayName: 'Install Node.js'
|
|
2586
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2587
|
+
- script: |
|
|
2588
|
+
${pmCmd.ci}
|
|
2589
|
+
displayName: 'Install dependencies'
|
|
2590
|
+
|
|
2591
|
+
- script: |
|
|
2592
|
+
${pmCmd.run} build
|
|
2593
|
+
env:
|
|
2594
|
+
NODE_ENV: production
|
|
2595
|
+
displayName: 'Build Next.js app'
|
|
2596
|
+
|
|
2597
|
+
- task: AzureStaticWebApp@0
|
|
2598
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2599
|
+
inputs:
|
|
2600
|
+
app_location: '.'
|
|
2601
|
+
output_location: '.next/standalone'
|
|
2602
|
+
skip_app_build: true
|
|
2603
|
+
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
2604
|
+
displayName: 'Deploy to Azure Static Web Apps'
|
|
2200
2605
|
`;
|
|
2201
2606
|
fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
|
|
2202
2607
|
// functions.yml
|
|
2203
|
-
const functionsPipeline = `trigger:
|
|
2204
|
-
branches:
|
|
2205
|
-
include:
|
|
2206
|
-
- main
|
|
2207
|
-
paths:
|
|
2208
|
-
include:
|
|
2209
|
-
- functions/**
|
|
2210
|
-
- shared/**
|
|
2211
|
-
|
|
2212
|
-
pr:
|
|
2213
|
-
branches:
|
|
2214
|
-
include:
|
|
2215
|
-
- main
|
|
2216
|
-
paths:
|
|
2217
|
-
include:
|
|
2218
|
-
- functions/**
|
|
2219
|
-
- shared/**
|
|
2220
|
-
|
|
2221
|
-
pool:
|
|
2222
|
-
vmImage: 'ubuntu-latest'
|
|
2223
|
-
|
|
2224
|
-
variables:
|
|
2225
|
-
- group: azure-deployment
|
|
2226
|
-
|
|
2227
|
-
steps:
|
|
2228
|
-
- task: NodeTool@0
|
|
2229
|
-
inputs:
|
|
2230
|
-
versionSpec: '22.x'
|
|
2231
|
-
displayName: 'Install Node.js'
|
|
2232
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2233
|
-
- script: |
|
|
2234
|
-
${pmCmd.ci}
|
|
2235
|
-
displayName: 'Install workspace dependencies'
|
|
2236
|
-
|
|
2237
|
-
- script: |
|
|
2238
|
-
${pmCmd.runFilter('shared')} build
|
|
2239
|
-
displayName: 'Build shared package'
|
|
2240
|
-
|
|
2241
|
-
- script: |
|
|
2242
|
-
${pmCmd.runFilter('functions')} build
|
|
2243
|
-
displayName: 'Build Functions'
|
|
2244
|
-
|
|
2245
|
-
- script: |
|
|
2246
|
-
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2247
|
-
mkdir -p /tmp/fn-deps
|
|
2248
|
-
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2249
|
-
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2250
|
-
rm -rf ./functions/node_modules
|
|
2251
|
-
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2252
|
-
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2253
|
-
mkdir -p "$SHARED_DEST"
|
|
2254
|
-
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2255
|
-
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2256
|
-
displayName: 'Prepare functions for deployment'
|
|
2257
|
-
|
|
2258
|
-
- task: ArchiveFiles@2
|
|
2259
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2260
|
-
inputs:
|
|
2261
|
-
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
|
|
2262
|
-
includeRootFolder: false
|
|
2263
|
-
archiveType: 'zip'
|
|
2264
|
-
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2265
|
-
displayName: 'Archive Functions'
|
|
2266
|
-
|
|
2267
|
-
- task: PublishBuildArtifacts@1
|
|
2268
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2269
|
-
inputs:
|
|
2270
|
-
PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2271
|
-
ArtifactName: 'functions'
|
|
2272
|
-
displayName: 'Publish Functions artifact'
|
|
2273
|
-
|
|
2274
|
-
- task: AzureFunctionApp@2
|
|
2275
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2276
|
-
inputs:
|
|
2277
|
-
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
2278
|
-
appType: 'functionAppLinux'
|
|
2279
|
-
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
2280
|
-
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2281
|
-
displayName: 'Deploy to Azure Functions'
|
|
2608
|
+
const functionsPipeline = `trigger:
|
|
2609
|
+
branches:
|
|
2610
|
+
include:
|
|
2611
|
+
- main
|
|
2612
|
+
paths:
|
|
2613
|
+
include:
|
|
2614
|
+
- functions/**
|
|
2615
|
+
- shared/**
|
|
2616
|
+
|
|
2617
|
+
pr:
|
|
2618
|
+
branches:
|
|
2619
|
+
include:
|
|
2620
|
+
- main
|
|
2621
|
+
paths:
|
|
2622
|
+
include:
|
|
2623
|
+
- functions/**
|
|
2624
|
+
- shared/**
|
|
2625
|
+
|
|
2626
|
+
pool:
|
|
2627
|
+
vmImage: 'ubuntu-latest'
|
|
2628
|
+
|
|
2629
|
+
variables:
|
|
2630
|
+
- group: azure-deployment
|
|
2631
|
+
|
|
2632
|
+
steps:
|
|
2633
|
+
- task: NodeTool@0
|
|
2634
|
+
inputs:
|
|
2635
|
+
versionSpec: '22.x'
|
|
2636
|
+
displayName: 'Install Node.js'
|
|
2637
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2638
|
+
- script: |
|
|
2639
|
+
${pmCmd.ci}
|
|
2640
|
+
displayName: 'Install workspace dependencies'
|
|
2641
|
+
|
|
2642
|
+
- script: |
|
|
2643
|
+
${pmCmd.runFilter('shared')} build
|
|
2644
|
+
displayName: 'Build shared package'
|
|
2645
|
+
|
|
2646
|
+
- script: |
|
|
2647
|
+
${pmCmd.runFilter('functions')} build
|
|
2648
|
+
displayName: 'Build Functions'
|
|
2649
|
+
|
|
2650
|
+
- script: |
|
|
2651
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2652
|
+
mkdir -p /tmp/fn-deps
|
|
2653
|
+
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2654
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2655
|
+
rm -rf ./functions/node_modules
|
|
2656
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2657
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2658
|
+
mkdir -p "$SHARED_DEST"
|
|
2659
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2660
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2661
|
+
displayName: 'Prepare functions for deployment'
|
|
2662
|
+
|
|
2663
|
+
- task: ArchiveFiles@2
|
|
2664
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2665
|
+
inputs:
|
|
2666
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
|
|
2667
|
+
includeRootFolder: false
|
|
2668
|
+
archiveType: 'zip'
|
|
2669
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2670
|
+
displayName: 'Archive Functions'
|
|
2671
|
+
|
|
2672
|
+
- task: PublishBuildArtifacts@1
|
|
2673
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2674
|
+
inputs:
|
|
2675
|
+
PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2676
|
+
ArtifactName: 'functions'
|
|
2677
|
+
displayName: 'Publish Functions artifact'
|
|
2678
|
+
|
|
2679
|
+
- task: AzureFunctionApp@2
|
|
2680
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2681
|
+
inputs:
|
|
2682
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
2683
|
+
appType: 'functionAppLinux'
|
|
2684
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
2685
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2686
|
+
displayName: 'Deploy to Azure Functions'
|
|
2282
2687
|
`;
|
|
2283
2688
|
fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
|
|
2284
2689
|
console.log('✅ Azure Pipelines created\n');
|