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