swallowkit 1.0.0-beta.8 → 1.0.0
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 +87 -181
- package/README.md +87 -238
- 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 +164 -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 +20 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +27 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
- package/dist/cli/commands/dev-seeds.js +188 -6
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +30 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +495 -129
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +70 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +797 -768
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +408 -189
- 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 +335 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +13 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +151 -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 +160 -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 +4 -3
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +405 -134
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +229 -78
- 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 +152 -8
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- 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/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 +105 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +7 -11
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +42 -17
- 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 +15 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
- 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 +176 -3
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +459 -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 +124 -0
- package/src/__tests__/dev.test.ts +254 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +101 -0
- package/src/__tests__/functions-generator.test.ts +137 -1
- package/src/__tests__/init.test.ts +386 -1
- 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 +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/__tests__/package-manager.test.ts +59 -4
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +287 -24
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +104 -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 +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +21 -100
- package/src/cli/commands/dev-seeds.ts +278 -7
- package/src/cli/commands/dev.ts +607 -160
- package/src/cli/commands/init.ts +880 -808
- package/src/cli/commands/scaffold.ts +531 -236
- package/src/cli/index.ts +338 -60
- package/src/core/config.ts +145 -9
- 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 +504 -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 +432 -134
- package/src/core/scaffold/model-parser.ts +244 -90
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +174 -9
- package/src/core/scaffold/ui-generator.ts +191 -36
- package/src/core/verify/index.ts +468 -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 +137 -0
- package/src/utils/package-manager.ts +41 -19
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
package/src/cli/commands/init.ts
CHANGED
|
@@ -9,11 +9,21 @@ import {
|
|
|
9
9
|
getCommands,
|
|
10
10
|
getWorkspaceConfig,
|
|
11
11
|
getCiSetupStep,
|
|
12
|
+
getCiInstallCommand,
|
|
12
13
|
getAzurePipelinesSetup,
|
|
13
14
|
getBuildScript,
|
|
14
15
|
getFunctionsPrestart,
|
|
15
16
|
getFunctionsStartScript,
|
|
17
|
+
getSwaAppBuildCommand,
|
|
16
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";
|
|
17
27
|
|
|
18
28
|
interface InitOptions {
|
|
19
29
|
name: string;
|
|
@@ -23,15 +33,23 @@ interface InitOptions {
|
|
|
23
33
|
backendLanguage?: BackendLanguage;
|
|
24
34
|
cosmosDbMode?: CosmosDbMode;
|
|
25
35
|
vnet?: VNetOption;
|
|
36
|
+
swaPlan?: StaticWebAppPlan;
|
|
37
|
+
packageManager?: PackageManager;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
type CiCdProvider = 'github' | 'azure' | 'skip';
|
|
29
41
|
type CosmosDbMode = 'freetier' | 'serverless';
|
|
30
42
|
type VNetOption = 'none' | 'outbound';
|
|
43
|
+
type StaticWebAppPlan = 'free' | 'standard';
|
|
44
|
+
|
|
45
|
+
export function getStaticWebAppSku(plan: StaticWebAppPlan): 'Free' | 'Standard' {
|
|
46
|
+
return plan === 'free' ? 'Free' : 'Standard';
|
|
47
|
+
}
|
|
31
48
|
|
|
32
|
-
interface AzureConfig {
|
|
49
|
+
export interface AzureConfig {
|
|
33
50
|
cosmosDbMode: CosmosDbMode;
|
|
34
51
|
vnetOption: VNetOption;
|
|
52
|
+
swaPlan: StaticWebAppPlan;
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
const BACKEND_LANGUAGE_CHOICES: Array<{ title: string; value: BackendLanguage }> = [
|
|
@@ -48,6 +66,66 @@ function getBackendLanguageLabel(backendLanguage: BackendLanguage): string {
|
|
|
48
66
|
return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
|
|
49
67
|
}
|
|
50
68
|
|
|
69
|
+
export function buildCosmosDbFreeTierBicepSource(): string {
|
|
70
|
+
return `@description('Cosmos DB account name')
|
|
71
|
+
param accountName string
|
|
72
|
+
|
|
73
|
+
@description('Database name')
|
|
74
|
+
param databaseName string
|
|
75
|
+
|
|
76
|
+
@description('Location for Cosmos DB')
|
|
77
|
+
param location string
|
|
78
|
+
|
|
79
|
+
@description('Public network access')
|
|
80
|
+
@allowed(['Enabled', 'Disabled'])
|
|
81
|
+
param publicNetworkAccess string = 'Enabled'
|
|
82
|
+
|
|
83
|
+
// Cosmos DB Account (Free Tier)
|
|
84
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
85
|
+
name: accountName
|
|
86
|
+
location: location
|
|
87
|
+
kind: 'GlobalDocumentDB'
|
|
88
|
+
properties: {
|
|
89
|
+
databaseAccountOfferType: 'Standard'
|
|
90
|
+
enableAutomaticFailover: false
|
|
91
|
+
enableFreeTier: true
|
|
92
|
+
publicNetworkAccess: publicNetworkAccess
|
|
93
|
+
disableLocalAuth: true
|
|
94
|
+
consistencyPolicy: {
|
|
95
|
+
defaultConsistencyLevel: 'Session'
|
|
96
|
+
}
|
|
97
|
+
locations: [
|
|
98
|
+
{
|
|
99
|
+
locationName: location
|
|
100
|
+
failoverPriority: 0
|
|
101
|
+
isZoneRedundant: false
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Cosmos DB Database
|
|
109
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
110
|
+
parent: cosmosAccount
|
|
111
|
+
name: databaseName
|
|
112
|
+
properties: {
|
|
113
|
+
resource: {
|
|
114
|
+
id: databaseName
|
|
115
|
+
}
|
|
116
|
+
options: {
|
|
117
|
+
throughput: 1000
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
output id string = cosmosAccount.id
|
|
123
|
+
output accountName string = cosmosAccount.name
|
|
124
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
125
|
+
output databaseName string = database.name
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
51
129
|
function getFunctionsWorkerRuntime(backendLanguage: BackendLanguage): string {
|
|
52
130
|
if (backendLanguage === "csharp") {
|
|
53
131
|
return "dotnet-isolated";
|
|
@@ -60,7 +138,7 @@ function getFunctionsWorkerRuntime(backendLanguage: BackendLanguage): string {
|
|
|
60
138
|
|
|
61
139
|
function getFunctionsRuntimeConfig(backendLanguage: BackendLanguage): { name: string; version: string } {
|
|
62
140
|
if (backendLanguage === "csharp") {
|
|
63
|
-
return { name: "dotnet-isolated", version: "
|
|
141
|
+
return { name: "dotnet-isolated", version: "10.0" };
|
|
64
142
|
}
|
|
65
143
|
if (backendLanguage === "python") {
|
|
66
144
|
return { name: "python", version: "3.11" };
|
|
@@ -68,6 +146,28 @@ function getFunctionsRuntimeConfig(backendLanguage: BackendLanguage): { name: st
|
|
|
68
146
|
return { name: "node", version: "22" };
|
|
69
147
|
}
|
|
70
148
|
|
|
149
|
+
export function buildSharedTsConfig() {
|
|
150
|
+
return {
|
|
151
|
+
compilerOptions: {
|
|
152
|
+
target: "ES2020",
|
|
153
|
+
module: "Node16",
|
|
154
|
+
moduleResolution: "node16",
|
|
155
|
+
lib: ["ES2020"],
|
|
156
|
+
outDir: "dist",
|
|
157
|
+
rootDir: ".",
|
|
158
|
+
declaration: true,
|
|
159
|
+
declarationMap: true,
|
|
160
|
+
sourceMap: true,
|
|
161
|
+
strict: true,
|
|
162
|
+
esModuleInterop: true,
|
|
163
|
+
skipLibCheck: true,
|
|
164
|
+
forceConsistentCasingInFileNames: true,
|
|
165
|
+
},
|
|
166
|
+
include: ["index.ts", "models/**/*"],
|
|
167
|
+
exclude: ["node_modules", "dist"],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
71
171
|
async function promptBackendLanguage(): Promise<BackendLanguage> {
|
|
72
172
|
const response = await prompts({
|
|
73
173
|
type: "select",
|
|
@@ -96,32 +196,56 @@ async function promptCiCd(): Promise<CiCdProvider> {
|
|
|
96
196
|
return response.cicd || 'skip';
|
|
97
197
|
}
|
|
98
198
|
|
|
99
|
-
async function promptAzureConfig(): Promise<AzureConfig> {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
199
|
+
async function promptAzureConfig(options: InitOptions): Promise<AzureConfig> {
|
|
200
|
+
let swaPlan = options.swaPlan;
|
|
201
|
+
if (!swaPlan) {
|
|
202
|
+
const response = await prompts({
|
|
203
|
+
type: 'select',
|
|
204
|
+
name: 'plan',
|
|
205
|
+
message: 'Azure Static Web Apps plan:',
|
|
206
|
+
choices: [
|
|
207
|
+
{ title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
|
|
208
|
+
{ title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
|
|
209
|
+
],
|
|
210
|
+
initial: 0
|
|
211
|
+
});
|
|
212
|
+
swaPlan = response.plan || 'standard';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let cosmosDbMode = options.cosmosDbMode;
|
|
216
|
+
if (!cosmosDbMode) {
|
|
217
|
+
const response = await prompts({
|
|
218
|
+
type: 'select',
|
|
219
|
+
name: 'mode',
|
|
220
|
+
message: 'Cosmos DB mode (affects cost):',
|
|
221
|
+
choices: [
|
|
222
|
+
{ title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
|
|
223
|
+
{ title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
|
|
224
|
+
],
|
|
225
|
+
initial: 0
|
|
226
|
+
});
|
|
227
|
+
cosmosDbMode = response.mode || 'freetier';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let vnetOption = options.vnet;
|
|
231
|
+
if (!vnetOption) {
|
|
232
|
+
const response = await prompts({
|
|
233
|
+
type: 'select',
|
|
234
|
+
name: 'vnet',
|
|
235
|
+
message: 'Network security:',
|
|
236
|
+
choices: [
|
|
237
|
+
{ title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
|
|
238
|
+
{ title: 'None - Public endpoints, simplest but least secure', value: 'none' }
|
|
239
|
+
],
|
|
240
|
+
initial: 0
|
|
241
|
+
});
|
|
242
|
+
vnetOption = response.vnet || 'outbound';
|
|
243
|
+
}
|
|
121
244
|
|
|
122
245
|
return {
|
|
123
|
-
cosmosDbMode:
|
|
124
|
-
vnetOption:
|
|
246
|
+
cosmosDbMode: cosmosDbMode || 'freetier',
|
|
247
|
+
vnetOption: vnetOption || 'outbound',
|
|
248
|
+
swaPlan: swaPlan || 'standard'
|
|
125
249
|
};
|
|
126
250
|
}
|
|
127
251
|
|
|
@@ -129,8 +253,12 @@ const VALID_CICD: CiCdProvider[] = ['github', 'azure', 'skip'];
|
|
|
129
253
|
const VALID_BACKEND_LANGUAGE: BackendLanguage[] = ['typescript', 'csharp', 'python'];
|
|
130
254
|
const VALID_COSMOS_DB_MODE: CosmosDbMode[] = ['freetier', 'serverless'];
|
|
131
255
|
const VALID_VNET: VNetOption[] = ['none', 'outbound'];
|
|
256
|
+
const VALID_SWA_PLAN: StaticWebAppPlan[] = ['free', 'standard'];
|
|
132
257
|
|
|
133
258
|
function validateInitFlags(options: InitOptions): void {
|
|
259
|
+
if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
|
|
260
|
+
throw new Error('Invalid --package-manager: expected npm or pnpm');
|
|
261
|
+
}
|
|
134
262
|
if (options.cicd && !VALID_CICD.includes(options.cicd)) {
|
|
135
263
|
console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
|
|
136
264
|
process.exit(1);
|
|
@@ -147,6 +275,10 @@ function validateInitFlags(options: InitOptions): void {
|
|
|
147
275
|
console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
|
|
148
276
|
process.exit(1);
|
|
149
277
|
}
|
|
278
|
+
if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
|
|
279
|
+
console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
150
282
|
}
|
|
151
283
|
|
|
152
284
|
export async function initCommand(options: InitOptions) {
|
|
@@ -157,7 +289,7 @@ export async function initCommand(options: InitOptions) {
|
|
|
157
289
|
console.log(`📋 Template: ${options.template}`);
|
|
158
290
|
|
|
159
291
|
// Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
|
|
160
|
-
const pm: PackageManager = detectFromUserAgent();
|
|
292
|
+
const pm: PackageManager = options.packageManager || detectFromUserAgent();
|
|
161
293
|
const pmCmd = getCommands(pm);
|
|
162
294
|
console.log(`📦 Package manager: ${pm}`);
|
|
163
295
|
|
|
@@ -170,13 +302,30 @@ export async function initCommand(options: InitOptions) {
|
|
|
170
302
|
process.exit(1);
|
|
171
303
|
}
|
|
172
304
|
|
|
305
|
+
// 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
|
|
306
|
+
const interactive =
|
|
307
|
+
process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
|
|
308
|
+
if (!interactive) {
|
|
309
|
+
const missing: string[] = [];
|
|
310
|
+
if (!options.cicd) missing.push('--cicd <github|azure|skip>');
|
|
311
|
+
if (!options.backendLanguage) missing.push('--backend-language <typescript|csharp|python>');
|
|
312
|
+
if (!options.cosmosDbMode) missing.push('--cosmos-db-mode <freetier|serverless>');
|
|
313
|
+
if (!options.vnet) missing.push('--vnet <outbound|none>');
|
|
314
|
+
if (!options.swaPlan) missing.push('--swa-plan <free|standard>');
|
|
315
|
+
if (missing.length > 0) {
|
|
316
|
+
console.error(
|
|
317
|
+
`❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
|
|
318
|
+
missing.map((flag) => ` ${flag}`).join('\n')
|
|
319
|
+
);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
173
324
|
// Use flag values if provided, otherwise prompt interactively
|
|
174
325
|
const cicdProvider: CiCdProvider = options.cicd || await promptCiCd();
|
|
175
326
|
const backendLanguage: BackendLanguage = options.backendLanguage || await promptBackendLanguage();
|
|
176
327
|
|
|
177
|
-
const azureConfig
|
|
178
|
-
? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
|
|
179
|
-
: await promptAzureConfig();
|
|
328
|
+
const azureConfig = await promptAzureConfig(options);
|
|
180
329
|
|
|
181
330
|
// Create Next.js project with create-next-app
|
|
182
331
|
await createNextJsProject(options.name, pm);
|
|
@@ -197,6 +346,8 @@ export async function initCommand(options: InitOptions) {
|
|
|
197
346
|
await createAzurePipelines(projectDir, pm, backendLanguage);
|
|
198
347
|
}
|
|
199
348
|
|
|
349
|
+
await syncProjectManifest(projectDir);
|
|
350
|
+
|
|
200
351
|
// Initialize Git repository and create initial commit
|
|
201
352
|
try {
|
|
202
353
|
// Try git init with -b main (Git 2.28+), fallback to git init
|
|
@@ -241,11 +392,11 @@ export async function initCommand(options: InitOptions) {
|
|
|
241
392
|
console.log(`\n✅ Project "${options.name}" created successfully!`);
|
|
242
393
|
console.log("\n📝 Next steps:");
|
|
243
394
|
console.log(` cd ${options.name}`);
|
|
244
|
-
console.log(` ${pmCmd.
|
|
245
|
-
console.log(` ${pmCmd.
|
|
246
|
-
console.log(` ${pmCmd.
|
|
395
|
+
console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
|
|
396
|
+
console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
|
|
397
|
+
console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
|
|
247
398
|
console.log("\n🚀 Deploy to Azure:");
|
|
248
|
-
console.log(` ${pmCmd.
|
|
399
|
+
console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
|
|
249
400
|
if (cicdProvider !== 'skip') {
|
|
250
401
|
console.log(" Configure CI/CD secrets and push to repository");
|
|
251
402
|
}
|
|
@@ -269,7 +420,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
|
|
|
269
420
|
// for npm use "npx create-next-app@latest ..."
|
|
270
421
|
const baseArgs = pm === 'pnpm'
|
|
271
422
|
? ['dlx', 'create-next-app@latest']
|
|
272
|
-
: ['create-next-app@latest'];
|
|
423
|
+
: ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
|
|
273
424
|
|
|
274
425
|
const args = [
|
|
275
426
|
...baseArgs,
|
|
@@ -279,6 +430,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
|
|
|
279
430
|
'--app',
|
|
280
431
|
'--no-src',
|
|
281
432
|
'--disable-git',
|
|
433
|
+
'--skip-install',
|
|
282
434
|
'--import-alias',
|
|
283
435
|
'@/*',
|
|
284
436
|
...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
|
|
@@ -311,84 +463,211 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
|
|
|
311
463
|
}
|
|
312
464
|
|
|
313
465
|
async function upgradeNextJs(projectDir: string, version: string, pm: PackageManager): Promise<void> {
|
|
314
|
-
|
|
315
|
-
console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
|
|
316
|
-
|
|
317
|
-
// pnpm: pnpm add next@... ; npm: npm install next@...
|
|
318
|
-
const args = pm === 'pnpm'
|
|
319
|
-
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
|
|
320
|
-
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
|
|
466
|
+
console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
|
|
321
467
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
cwd: projectDir,
|
|
327
|
-
stdio: 'inherit',
|
|
328
|
-
shell: true,
|
|
329
|
-
}
|
|
330
|
-
);
|
|
468
|
+
// pnpm: pnpm add next@... ; npm: npm install next@...
|
|
469
|
+
const args = pm === 'pnpm'
|
|
470
|
+
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
|
|
471
|
+
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
|
|
331
472
|
|
|
332
|
-
|
|
333
|
-
if (code !== 0) {
|
|
334
|
-
reject(new Error(`${pm} add next@${version} exited with code ${code}`));
|
|
335
|
-
} else {
|
|
336
|
-
console.log(`\n✅ Next.js ${version} installed\n`);
|
|
337
|
-
resolve();
|
|
338
|
-
}
|
|
339
|
-
});
|
|
473
|
+
await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
|
|
340
474
|
|
|
341
|
-
|
|
342
|
-
reject(error);
|
|
343
|
-
});
|
|
344
|
-
});
|
|
475
|
+
console.log(`\n✅ Next.js ${version} installed\n`);
|
|
345
476
|
}
|
|
346
477
|
|
|
347
478
|
async function installDependencies(projectDir: string, pm: PackageManager = 'pnpm'): Promise<void> {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
479
|
+
console.log('\n📦 Installing dependencies...\n');
|
|
480
|
+
await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
|
|
481
|
+
|
|
482
|
+
console.log('\n✅ Dependencies installed\n');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
type PackageJson = {
|
|
486
|
+
dependencies?: Record<string, string>;
|
|
487
|
+
devDependencies?: Record<string, string>;
|
|
488
|
+
optionalDependencies?: Record<string, string>;
|
|
489
|
+
[key: string]: unknown;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
export async function withNpmLockfileSafeManifests(projectDir: string, action: () => Promise<void>): Promise<void> {
|
|
493
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
494
|
+
const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
|
|
495
|
+
const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
|
|
496
|
+
const originalFiles = new Map<string, string>();
|
|
497
|
+
const hiddenNodeModules = new Map<string, string>();
|
|
498
|
+
|
|
499
|
+
function rememberOriginal(filePath: string): void {
|
|
500
|
+
if (!originalFiles.has(filePath)) {
|
|
501
|
+
originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function readJson(filePath: string): PackageJson {
|
|
506
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function writeJson(filePath: string, value: PackageJson): void {
|
|
510
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
|
|
514
|
+
? (readJson(sharedPackageJsonPath).name as string | undefined)
|
|
515
|
+
: undefined;
|
|
516
|
+
|
|
517
|
+
function replaceSharedWorkspaceDependency(packageJson: PackageJson, replacement: string): boolean {
|
|
518
|
+
let changed = false;
|
|
519
|
+
|
|
520
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies'] as const) {
|
|
521
|
+
const dependencies = packageJson[section];
|
|
522
|
+
if (!dependencies) continue;
|
|
523
|
+
|
|
524
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
525
|
+
if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
526
|
+
dependencies[name] = replacement;
|
|
527
|
+
changed = true;
|
|
528
|
+
}
|
|
358
529
|
}
|
|
359
|
-
|
|
530
|
+
}
|
|
360
531
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
532
|
+
return changed;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function normalizePackageJson(filePath: string, replacement: string): void {
|
|
536
|
+
if (!fs.existsSync(filePath)) return;
|
|
537
|
+
|
|
538
|
+
const packageJson = readJson(filePath);
|
|
539
|
+
if (!replaceSharedWorkspaceDependency(packageJson, replacement)) return;
|
|
540
|
+
|
|
541
|
+
rememberOriginal(filePath);
|
|
542
|
+
writeJson(filePath, packageJson);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function hideNodeModules(directory: string): void {
|
|
546
|
+
const nodeModulesPath = path.join(directory, 'node_modules');
|
|
547
|
+
if (!fs.existsSync(nodeModulesPath)) return;
|
|
548
|
+
|
|
549
|
+
const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
|
|
550
|
+
fs.renameSync(nodeModulesPath, hiddenPath);
|
|
551
|
+
hiddenNodeModules.set(nodeModulesPath, hiddenPath);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
normalizePackageJson(packageJsonPath, 'file:shared');
|
|
556
|
+
normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
|
|
557
|
+
hideNodeModules(projectDir);
|
|
558
|
+
hideNodeModules(path.join(projectDir, 'shared'));
|
|
559
|
+
hideNodeModules(path.join(projectDir, 'functions'));
|
|
560
|
+
await action();
|
|
561
|
+
} finally {
|
|
562
|
+
for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
|
|
563
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
564
|
+
fs.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
367
565
|
}
|
|
368
|
-
|
|
566
|
+
fs.renameSync(hiddenPath, nodeModulesPath);
|
|
567
|
+
}
|
|
369
568
|
|
|
370
|
-
|
|
371
|
-
|
|
569
|
+
for (const [filePath, content] of originalFiles) {
|
|
570
|
+
fs.writeFileSync(filePath, content);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Run installs without changing the project security policy. */
|
|
576
|
+
async function runPackageManagerCommand(
|
|
577
|
+
pm: PackageManager,
|
|
578
|
+
args: string[],
|
|
579
|
+
projectDir: string,
|
|
580
|
+
label: string,
|
|
581
|
+
): Promise<void> {
|
|
582
|
+
const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
|
|
583
|
+
|
|
584
|
+
if (code === 0) return;
|
|
585
|
+
|
|
586
|
+
if (pm === 'pnpm') {
|
|
587
|
+
if (parseIgnoredBuilds(output).length > 0) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
|
|
590
|
+
'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.'
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
throw new Error(`${label} exited with code ${code}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Spawn a child process inheriting stdin (so interactive prompts still work)
|
|
600
|
+
* while either inheriting or capturing+teeing stdout/stderr.
|
|
601
|
+
*/
|
|
602
|
+
function spawnAndCapture(
|
|
603
|
+
pm: PackageManager,
|
|
604
|
+
args: string[],
|
|
605
|
+
projectDir: string,
|
|
606
|
+
capture: boolean,
|
|
607
|
+
): Promise<{ code: number; output: string }> {
|
|
608
|
+
return new Promise((resolve, reject) => {
|
|
609
|
+
const stdio: ('inherit' | 'pipe')[] = capture
|
|
610
|
+
? ['inherit', 'pipe', 'pipe']
|
|
611
|
+
: ['inherit', 'inherit', 'inherit'];
|
|
612
|
+
|
|
613
|
+
const child = spawn(pm, args, {
|
|
614
|
+
cwd: projectDir,
|
|
615
|
+
stdio,
|
|
616
|
+
shell: true,
|
|
372
617
|
});
|
|
618
|
+
|
|
619
|
+
let combined = '';
|
|
620
|
+
if (capture) {
|
|
621
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
622
|
+
process.stdout.write(chunk);
|
|
623
|
+
combined += chunk.toString('utf8');
|
|
624
|
+
});
|
|
625
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
626
|
+
process.stderr.write(chunk);
|
|
627
|
+
combined += chunk.toString('utf8');
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
|
|
632
|
+
child.on('error', reject);
|
|
373
633
|
});
|
|
374
634
|
}
|
|
375
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
|
|
638
|
+
* return the bare package names (without versions), de-duplicated.
|
|
639
|
+
*/
|
|
640
|
+
export function parseIgnoredBuilds(output: string): string[] {
|
|
641
|
+
const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
|
|
642
|
+
if (!match) return [];
|
|
643
|
+
const list = match[1]
|
|
644
|
+
.split(',')
|
|
645
|
+
.map((entry) => entry.trim())
|
|
646
|
+
// strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
|
|
647
|
+
.map((entry) => entry.replace(/@[^@]+$/, ''))
|
|
648
|
+
.filter((name) => name.length > 0);
|
|
649
|
+
return Array.from(new Set(list));
|
|
650
|
+
}
|
|
651
|
+
|
|
376
652
|
export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
|
|
377
653
|
return nextConfigContent.replace(
|
|
378
654
|
/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/,
|
|
379
|
-
`$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`
|
|
655
|
+
`$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`
|
|
380
656
|
);
|
|
381
657
|
}
|
|
382
658
|
|
|
383
659
|
export function buildCSharpFunctionsProgramSource(): string {
|
|
384
|
-
return `using Microsoft.
|
|
660
|
+
return `using Microsoft.Azure.Functions.Worker;
|
|
661
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
385
662
|
using Microsoft.Extensions.Hosting;
|
|
663
|
+
using Microsoft.Azure.Functions.Worker.ApplicationInsights;
|
|
386
664
|
|
|
387
665
|
var host = new HostBuilder()
|
|
388
666
|
.ConfigureFunctionsWorkerDefaults()
|
|
389
667
|
.ConfigureServices(services =>
|
|
390
668
|
{
|
|
391
669
|
services.AddApplicationInsightsTelemetryWorkerService();
|
|
670
|
+
services.ConfigureFunctionsApplicationInsights();
|
|
392
671
|
})
|
|
393
672
|
.Build();
|
|
394
673
|
|
|
@@ -399,7 +678,7 @@ host.Run();
|
|
|
399
678
|
export function buildCSharpFunctionsProjectSource(): string {
|
|
400
679
|
return `<Project Sdk="Microsoft.NET.Sdk">
|
|
401
680
|
<PropertyGroup>
|
|
402
|
-
<TargetFramework>
|
|
681
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
403
682
|
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
|
404
683
|
<OutputType>Exe</OutputType>
|
|
405
684
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
@@ -413,17 +692,97 @@ export function buildCSharpFunctionsProjectSource(): string {
|
|
|
413
692
|
<ItemGroup>
|
|
414
693
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
|
|
415
694
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
416
|
-
<PackageReference Include="Azure.Identity" Version="1.
|
|
417
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="
|
|
418
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.
|
|
419
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="
|
|
695
|
+
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
|
696
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
|
697
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
|
698
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
|
|
420
699
|
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
|
|
421
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="
|
|
700
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
|
422
701
|
</ItemGroup>
|
|
423
702
|
</Project>
|
|
424
703
|
`;
|
|
425
704
|
}
|
|
426
705
|
|
|
706
|
+
export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): string {
|
|
707
|
+
return `module.exports = {
|
|
708
|
+
backend: {
|
|
709
|
+
language: '${backendLanguage}',
|
|
710
|
+
},
|
|
711
|
+
functions: {
|
|
712
|
+
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
713
|
+
},
|
|
714
|
+
deployment: {
|
|
715
|
+
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
716
|
+
swaName: process.env.AZURE_SWA_NAME || '',
|
|
717
|
+
},
|
|
718
|
+
}
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function buildGeneratedProjectDependencies(
|
|
723
|
+
projectName: string,
|
|
724
|
+
pm: PackageManager = 'pnpm',
|
|
725
|
+
): Record<string, string> {
|
|
726
|
+
return {
|
|
727
|
+
'@azure/cosmos': '^4.0.0',
|
|
728
|
+
'applicationinsights': '^3.3.0',
|
|
729
|
+
'zod': '^4.0.0',
|
|
730
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function getSwallowKitPackageMetadata(): { name: string; version: string } {
|
|
735
|
+
const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
|
|
739
|
+
name?: string;
|
|
740
|
+
version?: string;
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
name: packageJson.name || "swallowkit",
|
|
745
|
+
version: packageJson.version || "latest",
|
|
746
|
+
};
|
|
747
|
+
} catch {
|
|
748
|
+
// Fall back to the published package name when package metadata is unavailable.
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
name: "swallowkit",
|
|
753
|
+
version: "latest",
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function buildGeneratedProjectDevDependencies(): Record<string, string> {
|
|
758
|
+
const { name, version } = getSwallowKitPackageMetadata();
|
|
759
|
+
return {
|
|
760
|
+
[name]: version,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
|
|
765
|
+
const { name } = getSwallowKitPackageMetadata();
|
|
766
|
+
return JSON.stringify(
|
|
767
|
+
{
|
|
768
|
+
mcpServers: {
|
|
769
|
+
swallowkit: {
|
|
770
|
+
command: pm === 'pnpm' ? 'pnpm' : 'npx',
|
|
771
|
+
args: pm === 'pnpm'
|
|
772
|
+
? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
|
|
773
|
+
: ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
|
|
774
|
+
cwd: ".",
|
|
775
|
+
env: {
|
|
776
|
+
SWALLOWKIT_MCP_VERSION: "latest",
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
null,
|
|
782
|
+
2
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
427
786
|
async function addSwallowKitFiles(
|
|
428
787
|
projectDir: string,
|
|
429
788
|
options: InitOptions,
|
|
@@ -436,27 +795,20 @@ async function addSwallowKitFiles(
|
|
|
436
795
|
|
|
437
796
|
const projectName = options.name;
|
|
438
797
|
|
|
439
|
-
// 1. Update package.json to add
|
|
798
|
+
// 1. Update package.json to add runtime dependencies for generated projects
|
|
440
799
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
441
800
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
442
801
|
|
|
443
|
-
//
|
|
444
|
-
// zod is in the shared workspace package, not here
|
|
802
|
+
// Root BFF/auth code imports Zod directly; declare it as well as the shared package.
|
|
445
803
|
packageJson.dependencies = {
|
|
446
804
|
...packageJson.dependencies,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
805
|
+
...buildGeneratedProjectDependencies(projectName, pm),
|
|
806
|
+
};
|
|
807
|
+
packageJson.devDependencies = {
|
|
808
|
+
...packageJson.devDependencies,
|
|
809
|
+
...buildGeneratedProjectDevDependencies(),
|
|
451
810
|
};
|
|
452
811
|
|
|
453
|
-
if (backendLanguage !== "typescript") {
|
|
454
|
-
packageJson.devDependencies = {
|
|
455
|
-
...packageJson.devDependencies,
|
|
456
|
-
'@openapitools/openapi-generator-cli': '^2.21.0',
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
812
|
packageJson.scripts = {
|
|
461
813
|
...packageJson.scripts,
|
|
462
814
|
'build': getBuildScript(pm),
|
|
@@ -464,28 +816,27 @@ async function addSwallowKitFiles(
|
|
|
464
816
|
'functions:start': getFunctionsStartScript(pm, backendLanguage),
|
|
465
817
|
};
|
|
466
818
|
|
|
467
|
-
if (pm === 'pnpm') {
|
|
468
|
-
packageJson.packageManager = 'pnpm@latest';
|
|
469
|
-
}
|
|
470
|
-
|
|
471
819
|
packageJson.engines = {
|
|
472
|
-
node: '
|
|
820
|
+
node: '>=22.14.0',
|
|
821
|
+
...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
|
|
473
822
|
};
|
|
823
|
+
packageJson.devEngines = {
|
|
824
|
+
packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
|
|
825
|
+
};
|
|
826
|
+
delete packageJson.packageManager;
|
|
474
827
|
|
|
475
|
-
//
|
|
828
|
+
// Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
|
|
476
829
|
const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
|
|
477
830
|
const wsConfig = getWorkspaceConfig(pm, workspacePackages);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
} else {
|
|
482
|
-
// npm: workspaces are defined in package.json
|
|
831
|
+
packageJson.workspaces = workspacePackages;
|
|
832
|
+
|
|
833
|
+
if (wsConfig.type !== 'file') {
|
|
483
834
|
packageJson.workspaces = wsConfig.value;
|
|
484
835
|
}
|
|
485
836
|
|
|
486
837
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
487
838
|
|
|
488
|
-
// Create workspace
|
|
839
|
+
// Create pnpm-workspace.yaml for local pnpm development when needed.
|
|
489
840
|
if (wsConfig.type === 'file') {
|
|
490
841
|
fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
|
|
491
842
|
}
|
|
@@ -526,20 +877,7 @@ async function addSwallowKitFiles(
|
|
|
526
877
|
}
|
|
527
878
|
|
|
528
879
|
// 3. Create SwallowKit config
|
|
529
|
-
const swallowkitConfig =
|
|
530
|
-
module.exports = {
|
|
531
|
-
backend: {
|
|
532
|
-
language: '${backendLanguage}',
|
|
533
|
-
},
|
|
534
|
-
functions: {
|
|
535
|
-
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
536
|
-
},
|
|
537
|
-
deployment: {
|
|
538
|
-
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
539
|
-
swaName: process.env.AZURE_SWA_NAME || '',
|
|
540
|
-
},
|
|
541
|
-
}
|
|
542
|
-
`;
|
|
880
|
+
const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
|
|
543
881
|
fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
|
|
544
882
|
|
|
545
883
|
// 4. Create shared workspace package for Zod models (Single Source of Truth)
|
|
@@ -558,6 +896,13 @@ function getFunctionsBaseUrl(): string {
|
|
|
558
896
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
559
897
|
}
|
|
560
898
|
|
|
899
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
900
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
901
|
+
if (key) return { 'x-functions-key': key };
|
|
902
|
+
if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
|
|
903
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
904
|
+
}
|
|
905
|
+
|
|
561
906
|
/**
|
|
562
907
|
* Simple HTTP client for calling backend APIs
|
|
563
908
|
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
@@ -565,10 +910,11 @@ function getFunctionsBaseUrl(): string {
|
|
|
565
910
|
async function request<T>(
|
|
566
911
|
endpoint: string,
|
|
567
912
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
568
|
-
body?:
|
|
913
|
+
body?: unknown,
|
|
569
914
|
queryParams?: Record<string, string>
|
|
570
915
|
): Promise<T> {
|
|
571
916
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
917
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
572
918
|
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
573
919
|
if (queryParams) {
|
|
574
920
|
const params = new URLSearchParams(queryParams);
|
|
@@ -580,6 +926,7 @@ async function request<T>(
|
|
|
580
926
|
method,
|
|
581
927
|
headers: {
|
|
582
928
|
'Content-Type': 'application/json',
|
|
929
|
+
...keyHeaders,
|
|
583
930
|
},
|
|
584
931
|
body: body ? JSON.stringify(body) : undefined,
|
|
585
932
|
});
|
|
@@ -635,14 +982,14 @@ export const api = {
|
|
|
635
982
|
/**
|
|
636
983
|
* Make a POST request
|
|
637
984
|
*/
|
|
638
|
-
post: <T>(endpoint: string, body?:
|
|
985
|
+
post: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
639
986
|
return request<T>(endpoint, 'POST', body);
|
|
640
987
|
},
|
|
641
988
|
|
|
642
989
|
/**
|
|
643
990
|
* Make a PUT request
|
|
644
991
|
*/
|
|
645
|
-
put: <T>(endpoint: string, body?:
|
|
992
|
+
put: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
646
993
|
return request<T>(endpoint, 'PUT', body);
|
|
647
994
|
},
|
|
648
995
|
|
|
@@ -663,6 +1010,7 @@ export const api = {
|
|
|
663
1010
|
// 6. Create .env.example
|
|
664
1011
|
const envExample = `# Azure Functions Backend URL
|
|
665
1012
|
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
1013
|
+
BACKEND_FUNCTIONS_KEY=
|
|
666
1014
|
|
|
667
1015
|
# Azure Configuration
|
|
668
1016
|
AZURE_RESOURCE_GROUP=your-resource-group
|
|
@@ -702,7 +1050,7 @@ export async function register() {
|
|
|
702
1050
|
const originalConsoleError = console.error;
|
|
703
1051
|
const originalConsoleWarn = console.warn;
|
|
704
1052
|
|
|
705
|
-
console.log = function(...args:
|
|
1053
|
+
console.log = function(...args: unknown[]) {
|
|
706
1054
|
originalConsoleLog.apply(console, args);
|
|
707
1055
|
const message = args.map(arg =>
|
|
708
1056
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -713,7 +1061,7 @@ export async function register() {
|
|
|
713
1061
|
});
|
|
714
1062
|
};
|
|
715
1063
|
|
|
716
|
-
console.error = function(...args:
|
|
1064
|
+
console.error = function(...args: unknown[]) {
|
|
717
1065
|
originalConsoleError.apply(console, args);
|
|
718
1066
|
const message = args.map(arg =>
|
|
719
1067
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -724,7 +1072,7 @@ export async function register() {
|
|
|
724
1072
|
});
|
|
725
1073
|
};
|
|
726
1074
|
|
|
727
|
-
console.warn = function(...args:
|
|
1075
|
+
console.warn = function(...args: unknown[]) {
|
|
728
1076
|
originalConsoleWarn.apply(console, args);
|
|
729
1077
|
const message = args.map(arg =>
|
|
730
1078
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -748,6 +1096,7 @@ export async function register() {
|
|
|
748
1096
|
const envLocalContent = [
|
|
749
1097
|
'# Azure Functions Backend URL (Local)',
|
|
750
1098
|
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
1099
|
+
'BACKEND_FUNCTIONS_KEY=',
|
|
751
1100
|
''
|
|
752
1101
|
].join('\n');
|
|
753
1102
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -796,7 +1145,7 @@ export async function register() {
|
|
|
796
1145
|
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
|
|
797
1146
|
|
|
798
1147
|
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
799
|
-
createAiAgentFiles(projectDir, projectName, backendLanguage);
|
|
1148
|
+
createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
|
|
800
1149
|
}
|
|
801
1150
|
|
|
802
1151
|
async function createSharedPackage(projectDir: string, projectName: string) {
|
|
@@ -808,6 +1157,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
|
|
|
808
1157
|
|
|
809
1158
|
// shared/package.json
|
|
810
1159
|
const sharedPackageJson = {
|
|
1160
|
+
private: true,
|
|
811
1161
|
name: `@${projectName}/shared`,
|
|
812
1162
|
version: '1.0.0',
|
|
813
1163
|
description: 'Shared Zod models — Single Source of Truth for validation schemas',
|
|
@@ -830,28 +1180,9 @@ async function createSharedPackage(projectDir: string, projectName: string) {
|
|
|
830
1180
|
);
|
|
831
1181
|
|
|
832
1182
|
// shared/tsconfig.json
|
|
833
|
-
const sharedTsConfig = {
|
|
834
|
-
compilerOptions: {
|
|
835
|
-
target: 'ES2020',
|
|
836
|
-
module: 'commonjs',
|
|
837
|
-
moduleResolution: 'node',
|
|
838
|
-
lib: ['ES2020'],
|
|
839
|
-
outDir: 'dist',
|
|
840
|
-
rootDir: '.',
|
|
841
|
-
declaration: true,
|
|
842
|
-
declarationMap: true,
|
|
843
|
-
sourceMap: true,
|
|
844
|
-
strict: true,
|
|
845
|
-
esModuleInterop: true,
|
|
846
|
-
skipLibCheck: true,
|
|
847
|
-
forceConsistentCasingInFileNames: true,
|
|
848
|
-
},
|
|
849
|
-
include: ['index.ts', 'models/**/*'],
|
|
850
|
-
exclude: ['node_modules', 'dist'],
|
|
851
|
-
};
|
|
852
1183
|
fs.writeFileSync(
|
|
853
1184
|
path.join(sharedDir, 'tsconfig.json'),
|
|
854
|
-
JSON.stringify(
|
|
1185
|
+
JSON.stringify(buildSharedTsConfig(), null, 2)
|
|
855
1186
|
);
|
|
856
1187
|
|
|
857
1188
|
// shared/index.ts (empty re-export file, scaffold will add entries)
|
|
@@ -945,8 +1276,9 @@ tsconfig.json
|
|
|
945
1276
|
!dist/**/*.js
|
|
946
1277
|
`);
|
|
947
1278
|
} else if (backendLanguage === 'python') {
|
|
948
|
-
gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
|
|
1279
|
+
gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
|
|
949
1280
|
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
|
|
1281
|
+
.codegen-venv
|
|
950
1282
|
__pycache__
|
|
951
1283
|
.pytest_cache
|
|
952
1284
|
.mypy_cache
|
|
@@ -966,8 +1298,8 @@ tests
|
|
|
966
1298
|
fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
|
|
967
1299
|
}
|
|
968
1300
|
|
|
969
|
-
function
|
|
970
|
-
|
|
1301
|
+
export function buildTypeScriptFunctionsPackageJson(projectName: string, pm: PackageManager = 'pnpm') {
|
|
1302
|
+
return {
|
|
971
1303
|
name: 'functions',
|
|
972
1304
|
version: '1.0.0',
|
|
973
1305
|
description: 'Azure Functions backend',
|
|
@@ -982,13 +1314,17 @@ function createTypeScriptFunctionsProject(projectDir: string, functionsDir: stri
|
|
|
982
1314
|
'@azure/cosmos': '^4.0.0',
|
|
983
1315
|
'@azure/identity': '^4.0.0',
|
|
984
1316
|
zod: '>=3.25.0',
|
|
985
|
-
[`@${
|
|
1317
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
|
|
986
1318
|
},
|
|
987
1319
|
devDependencies: {
|
|
988
1320
|
'@types/node': '^20.0.0',
|
|
989
1321
|
typescript: '^5.0.0',
|
|
990
1322
|
},
|
|
991
1323
|
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function createTypeScriptFunctionsProject(projectDir: string, functionsDir: string, pm: PackageManager): void {
|
|
1327
|
+
const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
|
|
992
1328
|
fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
|
|
993
1329
|
|
|
994
1330
|
const sharedPkgName = `@${path.basename(projectDir)}/shared`;
|
|
@@ -1061,7 +1397,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
1061
1397
|
|
|
1062
1398
|
app.http('greet', {
|
|
1063
1399
|
methods: ['GET', 'POST'],
|
|
1064
|
-
authLevel: '
|
|
1400
|
+
authLevel: 'function',
|
|
1065
1401
|
handler: greet
|
|
1066
1402
|
});
|
|
1067
1403
|
`);
|
|
@@ -1073,6 +1409,11 @@ function createCSharpFunctionsProject(projectDir: string, functionsDir: string):
|
|
|
1073
1409
|
const csprojName = `${projectPascal}.Functions.csproj`;
|
|
1074
1410
|
|
|
1075
1411
|
fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
|
|
1412
|
+
fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
|
|
1413
|
+
fs.writeFileSync(
|
|
1414
|
+
path.join(functionsDir, '.config', 'dotnet-tools.json'),
|
|
1415
|
+
buildCSharpCodegenToolManifestSource()
|
|
1416
|
+
);
|
|
1076
1417
|
|
|
1077
1418
|
fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
|
|
1078
1419
|
|
|
@@ -1088,7 +1429,7 @@ public sealed class GreetFunction
|
|
|
1088
1429
|
{
|
|
1089
1430
|
[Function("greet")]
|
|
1090
1431
|
public async Task<HttpResponseData> Run(
|
|
1091
|
-
[HttpTrigger(AuthorizationLevel.
|
|
1432
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
|
|
1092
1433
|
{
|
|
1093
1434
|
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
1094
1435
|
var name = "SwallowKit";
|
|
@@ -1115,11 +1456,16 @@ public sealed class GreetFunction
|
|
|
1115
1456
|
|
|
1116
1457
|
function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
|
|
1117
1458
|
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1459
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1118
1460
|
|
|
1119
1461
|
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
1120
1462
|
azure-cosmos>=4.9.0
|
|
1121
1463
|
azure-identity>=1.19.0
|
|
1122
1464
|
`);
|
|
1465
|
+
fs.writeFileSync(
|
|
1466
|
+
path.join(functionsDir, 'requirements.codegen.txt'),
|
|
1467
|
+
buildPythonCodegenRequirementsSource()
|
|
1468
|
+
);
|
|
1123
1469
|
|
|
1124
1470
|
const blueprintsDir = path.join(functionsDir, 'blueprints');
|
|
1125
1471
|
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
@@ -1151,13 +1497,29 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
1151
1497
|
|
|
1152
1498
|
from blueprints.greet import bp as greet_bp
|
|
1153
1499
|
|
|
1154
|
-
app = func.FunctionApp(http_auth_level=func.AuthLevel.
|
|
1500
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
|
1155
1501
|
|
|
1156
1502
|
app.register_blueprint(greet_bp)
|
|
1157
1503
|
# SwallowKit scaffold registrations
|
|
1158
1504
|
`);
|
|
1159
1505
|
}
|
|
1160
1506
|
|
|
1507
|
+
function ensureProjectGitignoreEntry(projectDir: string, entry: string): void {
|
|
1508
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
1509
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const current = fs.readFileSync(gitignorePath, 'utf8');
|
|
1514
|
+
const lines = current.split(/\r?\n/);
|
|
1515
|
+
if (lines.includes(entry)) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const normalized = current.endsWith('\n') ? current : `${current}\n`;
|
|
1520
|
+
fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1161
1523
|
async function createBffApiRoute(projectDir: string) {
|
|
1162
1524
|
console.log('📦 Creating BFF API route...\n');
|
|
1163
1525
|
|
|
@@ -1328,7 +1690,7 @@ export default function Home() {
|
|
|
1328
1690
|
Create your first model with Zod and generate CRUD operations automatically.
|
|
1329
1691
|
</p>
|
|
1330
1692
|
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
1331
|
-
${pmCmd.
|
|
1693
|
+
${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
|
|
1332
1694
|
</code>
|
|
1333
1695
|
</div>
|
|
1334
1696
|
</section>
|
|
@@ -1388,17 +1750,18 @@ function createReadme(
|
|
|
1388
1750
|
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1389
1751
|
const schemaBridgeDescription = backendLanguage === 'typescript'
|
|
1390
1752
|
? 'Zod (shared between frontend and backend)'
|
|
1391
|
-
: `Zod + OpenAPI
|
|
1753
|
+
: `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
|
|
1392
1754
|
const functionsTree = backendLanguage === 'typescript'
|
|
1393
1755
|
? `│ └── src/\n│ └── greet.ts # Sample function`
|
|
1394
1756
|
: backendLanguage === 'csharp'
|
|
1395
|
-
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ #
|
|
1396
|
-
: `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ #
|
|
1757
|
+
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
|
|
1758
|
+
: `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
|
|
1397
1759
|
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1398
1760
|
? '- Azure Functions CRUD endpoints'
|
|
1399
|
-
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI
|
|
1761
|
+
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1762
|
+
const pythonUvPaths = getProjectLocalUvPaths(projectDir);
|
|
1400
1763
|
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1401
|
-
? `\n**Python local dev note**: SwallowKit uses \`
|
|
1764
|
+
? `\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`
|
|
1402
1765
|
: '';
|
|
1403
1766
|
|
|
1404
1767
|
const readme = `# ${projectName}
|
|
@@ -1428,7 +1791,7 @@ This project was initialized with the following settings:
|
|
|
1428
1791
|
|
|
1429
1792
|
Before you begin, ensure you have the following installed:
|
|
1430
1793
|
|
|
1431
|
-
1. **Node.js
|
|
1794
|
+
1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1432
1795
|
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1433
1796
|
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1434
1797
|
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
@@ -1436,7 +1799,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
|
|
|
1436
1799
|
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1437
1800
|
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1438
1801
|
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1439
|
-
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**:
|
|
1802
|
+
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
|
|
1440
1803
|
|
|
1441
1804
|
## 📁 Project Structure
|
|
1442
1805
|
|
|
@@ -1462,7 +1825,7 @@ ${functionsTree}
|
|
|
1462
1825
|
Define your data model with Zod schema:
|
|
1463
1826
|
|
|
1464
1827
|
\`\`\`bash
|
|
1465
|
-
${pmCmd.
|
|
1828
|
+
${pmCmd.exec} swallowkit create-model <model-name>
|
|
1466
1829
|
\`\`\`
|
|
1467
1830
|
|
|
1468
1831
|
This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
|
|
@@ -1472,7 +1835,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
|
|
|
1472
1835
|
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1473
1836
|
|
|
1474
1837
|
\`\`\`bash
|
|
1475
|
-
${pmCmd.
|
|
1838
|
+
${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
|
|
1476
1839
|
\`\`\`
|
|
1477
1840
|
|
|
1478
1841
|
This generates:
|
|
@@ -1484,7 +1847,7 @@ ${backendScaffoldNote}
|
|
|
1484
1847
|
### 3. Start Development Servers
|
|
1485
1848
|
|
|
1486
1849
|
\`\`\`bash
|
|
1487
|
-
${pmCmd.
|
|
1850
|
+
${pmCmd.exec} swallowkit dev
|
|
1488
1851
|
\`\`\`
|
|
1489
1852
|
|
|
1490
1853
|
This starts:
|
|
@@ -1502,7 +1865,7 @@ ${pythonLocalDevNote}
|
|
|
1502
1865
|
Create all required Azure resources using Bicep:
|
|
1503
1866
|
|
|
1504
1867
|
\`\`\`bash
|
|
1505
|
-
${pmCmd.
|
|
1868
|
+
${pmCmd.exec} swallowkit provision --resource-group <rg-name>
|
|
1506
1869
|
\`\`\`
|
|
1507
1870
|
|
|
1508
1871
|
This creates:
|
|
@@ -1558,10 +1921,10 @@ func azure functionapp publish func-${projectName}
|
|
|
1558
1921
|
|
|
1559
1922
|
## 🔧 Available Commands
|
|
1560
1923
|
|
|
1561
|
-
- \`${pmCmd.
|
|
1562
|
-
- \`${pmCmd.
|
|
1563
|
-
- \`${pmCmd.
|
|
1564
|
-
- \`${pmCmd.
|
|
1924
|
+
- \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
|
|
1925
|
+
- \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
|
|
1926
|
+
- \`${pmCmd.exec} swallowkit dev\` - Start development servers
|
|
1927
|
+
- \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
|
|
1565
1928
|
${azureConfig.vnetOption !== 'none' ? `
|
|
1566
1929
|
## 🔒 Network Security (VNet Configuration)
|
|
1567
1930
|
|
|
@@ -1610,448 +1973,79 @@ This project was generated by SwallowKit. If you encounter any issues or have su
|
|
|
1610
1973
|
console.log('✅ README.md created\n');
|
|
1611
1974
|
}
|
|
1612
1975
|
|
|
1613
|
-
function createAiAgentFiles(
|
|
1976
|
+
function createAiAgentFiles(
|
|
1977
|
+
projectDir: string,
|
|
1978
|
+
projectName: string,
|
|
1979
|
+
backendLanguage: BackendLanguage,
|
|
1980
|
+
pm: PackageManager
|
|
1981
|
+
) {
|
|
1614
1982
|
console.log('🤖 Creating AI agent instruction files...\n');
|
|
1615
|
-
const
|
|
1616
|
-
const functionsStructureLine = backendLanguage === 'typescript'
|
|
1617
|
-
? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
|
|
1618
|
-
: backendLanguage === 'csharp'
|
|
1619
|
-
? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived C# schema assets`
|
|
1620
|
-
: `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived Python schema assets`;
|
|
1621
|
-
const backendSchemaNote = backendLanguage === 'typescript'
|
|
1622
|
-
? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
|
|
1623
|
-
: `- The frontend/BFF source of truth stays in \`shared/models/\` as Zod schemas.\n- \`swallowkit scaffold\` exports OpenAPI into \`functions/openapi/\` and generates ${backendLanguageLabel} schema assets into \`functions/generated/\` for backend use.`;
|
|
1624
|
-
const backendRulesNote = backendLanguage === 'typescript'
|
|
1625
|
-
? `- All CRUD operations and business logic live in \`functions/src/\`.\n- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.\n- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).\n- Validate all data against Zod schemas before writing to Cosmos DB.\n- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.`
|
|
1626
|
-
: `- All business logic lives in \`functions/\` and the generated handlers perform real Cosmos DB CRUD.\n- Keep Zod schemas in \`shared/models/\` as the source of truth.\n- Regenerate backend contracts with \`swallowkit scaffold shared/models/<name>.ts\` whenever a schema changes.\n- Use the generated OpenAPI-derived models in \`functions/generated/\` to keep backend contracts aligned.\n- The backend should still own \`id\`, \`createdAt\`, and \`updatedAt\`.`;
|
|
1627
|
-
|
|
1628
|
-
// ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
|
|
1629
|
-
|
|
1630
|
-
const agentsMd = `# AGENTS.md
|
|
1631
|
-
|
|
1632
|
-
This project was generated by **SwallowKit**.
|
|
1633
|
-
All coding agents **must** follow the architecture and conventions described below.
|
|
1634
|
-
|
|
1635
|
-
## Architecture Overview
|
|
1636
|
-
|
|
1637
|
-
This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
|
|
1638
|
-
|
|
1639
|
-
\`\`\`
|
|
1640
|
-
Frontend (React / Next.js App Router)
|
|
1641
|
-
↓ fetch('/api/{model}', ...)
|
|
1642
|
-
BFF Layer (Next.js API Routes)
|
|
1643
|
-
↓ HTTP → Azure Functions
|
|
1644
|
-
Backend (Azure Functions)
|
|
1645
|
-
↓
|
|
1646
|
-
Azure Cosmos DB (Document Database)
|
|
1647
|
-
\`\`\`
|
|
1648
|
-
|
|
1649
|
-
### Project Structure
|
|
1650
|
-
|
|
1651
|
-
\`\`\`
|
|
1652
|
-
${projectName}/
|
|
1653
|
-
├── app/ # Next.js App Router
|
|
1654
|
-
│ ├── api/ # BFF API routes (proxy to Azure Functions)
|
|
1655
|
-
│ └── {model}/ # UI pages per model (list, detail, create, edit)
|
|
1656
|
-
├── functions/ # Azure Functions (backend)
|
|
1657
|
-
${functionsStructureLine}
|
|
1658
|
-
├── shared/ # Shared workspace package
|
|
1659
|
-
│ ├── models/ # Zod schema definitions (single source of truth)
|
|
1660
|
-
│ └── index.ts # Re-exports all models
|
|
1661
|
-
├── lib/
|
|
1662
|
-
│ └── api/ # API client utilities (backend.ts, call-function.ts)
|
|
1663
|
-
├── components/ # Shared React components
|
|
1664
|
-
├── infra/ # Bicep infrastructure-as-code files
|
|
1665
|
-
│ ├── main.bicep
|
|
1666
|
-
│ └── modules/
|
|
1667
|
-
└── .github/workflows/ # CI/CD workflows (if configured)
|
|
1668
|
-
\`\`\`
|
|
1669
|
-
|
|
1670
|
-
## Critical Design Principles
|
|
1671
|
-
|
|
1672
|
-
### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
|
|
1983
|
+
const runCmd = getCommands(pm).exec;
|
|
1673
1984
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
Example BFF route pattern:
|
|
1680
|
-
|
|
1681
|
-
\`\`\`typescript
|
|
1682
|
-
// app/api/{model}/route.ts
|
|
1683
|
-
import { callFunction } from '@/lib/api/call-function';
|
|
1684
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1685
|
-
import { z } from 'zod/v4';
|
|
1686
|
-
|
|
1687
|
-
export async function GET() {
|
|
1688
|
-
return callFunction({
|
|
1689
|
-
method: 'GET',
|
|
1690
|
-
path: '/api/{model}',
|
|
1691
|
-
responseSchema: z.array(ModelSchema),
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
export async function POST(request: NextRequest) {
|
|
1696
|
-
const body = await request.json();
|
|
1697
|
-
return callFunction({
|
|
1698
|
-
method: 'POST',
|
|
1699
|
-
path: '/api/{model}',
|
|
1700
|
-
body,
|
|
1701
|
-
inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
|
|
1702
|
-
responseSchema: ModelSchema,
|
|
1703
|
-
successStatus: 201,
|
|
1985
|
+
writeAgentInstructionFiles(projectDir, {
|
|
1986
|
+
projectName,
|
|
1987
|
+
backendLanguage,
|
|
1988
|
+
packageManager: pm,
|
|
1704
1989
|
});
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
- All data models are defined **once** as Zod schemas in \`shared/models/\`.
|
|
1711
|
-
- TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
|
|
1712
|
-
- ${backendSchemaNote}
|
|
1713
|
-
|
|
1714
|
-
Model definition pattern:
|
|
1715
|
-
|
|
1716
|
-
\`\`\`typescript
|
|
1717
|
-
// shared/models/{model}.ts
|
|
1718
|
-
import { z } from 'zod/v4';
|
|
1719
|
-
|
|
1720
|
-
export const Todo = z.object({
|
|
1721
|
-
id: z.string(),
|
|
1722
|
-
name: z.string().min(1),
|
|
1723
|
-
// ... your fields
|
|
1724
|
-
createdAt: z.string().optional(),
|
|
1725
|
-
updatedAt: z.string().optional(),
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1729
|
-
export const displayName = 'Todo';
|
|
1730
|
-
\`\`\`
|
|
1731
|
-
|
|
1732
|
-
Key rules:
|
|
1733
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1734
|
-
- \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
|
|
1735
|
-
- Always re-export models from \`shared/index.ts\`.
|
|
1736
|
-
|
|
1737
|
-
### 3. Azure Functions Own All Business Logic and Data Access
|
|
1738
|
-
|
|
1739
|
-
- ${backendRulesNote}
|
|
1740
|
-
|
|
1741
|
-
${backendLanguage === 'typescript' ? 'Azure Functions handler pattern:' : `Generated ${backendLanguageLabel} handlers live under \`functions/\`. Re-run \`swallowkit scaffold shared/models/<name>.ts\` after schema changes to keep generated CRUD handlers and \`functions/generated/\` in sync.`}
|
|
1742
|
-
|
|
1743
|
-
${backendLanguage === 'typescript' ? `\`\`\`typescript
|
|
1744
|
-
// functions/src/{model}.ts
|
|
1745
|
-
import { app } from '@azure/functions';
|
|
1746
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1747
|
-
|
|
1748
|
-
const containerName = 'Models'; // PascalCase + 's'
|
|
1749
|
-
|
|
1750
|
-
app.http('{model}-get-all', {
|
|
1751
|
-
methods: ['GET'],
|
|
1752
|
-
route: '{model}',
|
|
1753
|
-
authLevel: 'anonymous',
|
|
1754
|
-
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
1755
|
-
handler: async (request, context) => {
|
|
1756
|
-
const documents = context.extraInputs.get('cosmosInput');
|
|
1757
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
1758
|
-
return { status: 200, jsonBody: validated };
|
|
1759
|
-
},
|
|
1760
|
-
});
|
|
1761
|
-
\`\`\`` : ''}
|
|
1762
|
-
|
|
1763
|
-
## Naming Conventions
|
|
1764
|
-
|
|
1765
|
-
| Item | Convention | Example |
|
|
1766
|
-
|------|-----------|---------|
|
|
1767
|
-
| Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
|
|
1768
|
-
| Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
|
|
1769
|
-
| Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
|
|
1770
|
-
| Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
|
|
1771
|
-
| API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
|
|
1772
|
-
| BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
|
|
1773
|
-
| BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
|
|
1774
|
-
| UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
|
|
1775
|
-
| React component | PascalCase | \`TodoForm.tsx\` |
|
|
1776
|
-
| Cosmos DB container | PascalCase + 's' | \`Todos\` |
|
|
1777
|
-
| Cosmos DB partition key | \`/id\` | Always \`/id\` |
|
|
1778
|
-
| Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
|
|
1779
|
-
|
|
1780
|
-
## Adding New Models (SwallowKit CLI Skills)
|
|
1781
|
-
|
|
1782
|
-
Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
|
|
1783
|
-
|
|
1784
|
-
### Skill: Create a new data model
|
|
1785
|
-
|
|
1786
|
-
\`\`\`bash
|
|
1787
|
-
npx swallowkit create-model <name>
|
|
1788
|
-
# Multiple models at once:
|
|
1789
|
-
npx swallowkit create-model user post comment
|
|
1790
|
-
\`\`\`
|
|
1791
|
-
|
|
1792
|
-
Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
|
|
1793
|
-
Edit the generated file to add your domain-specific fields, then run scaffold.
|
|
1794
|
-
|
|
1795
|
-
### Skill: Generate full CRUD from a model
|
|
1796
|
-
|
|
1797
|
-
\`\`\`bash
|
|
1798
|
-
npx swallowkit scaffold shared/models/<name>.ts
|
|
1799
|
-
\`\`\`
|
|
1800
|
-
|
|
1801
|
-
Generates:
|
|
1802
|
-
- Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
|
|
1803
|
-
- BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
|
|
1804
|
-
- UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
|
|
1805
|
-
- Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
|
|
1806
|
-
|
|
1807
|
-
### Skill: Start development servers
|
|
1808
|
-
|
|
1809
|
-
\`\`\`bash
|
|
1810
|
-
npx swallowkit dev
|
|
1811
|
-
\`\`\`
|
|
1812
|
-
|
|
1813
|
-
Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
|
|
1814
|
-
Checks for Cosmos DB Emulator availability.
|
|
1815
|
-
|
|
1816
|
-
### Skill: Provision Azure resources
|
|
1817
|
-
|
|
1818
|
-
\`\`\`bash
|
|
1819
|
-
npx swallowkit provision --resource-group <name> --location <region>
|
|
1820
|
-
\`\`\`
|
|
1821
|
-
|
|
1822
|
-
Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
|
|
1823
|
-
|
|
1824
|
-
### Typical workflow for "add a new feature/model"
|
|
1825
|
-
|
|
1826
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1827
|
-
2. Edit \`shared/models/<name>.ts\` — add fields
|
|
1828
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1829
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1830
|
-
|
|
1831
|
-
## Do NOT
|
|
1832
|
-
|
|
1833
|
-
- **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
|
|
1834
|
-
- **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
|
|
1835
|
-
- **Do not** manually duplicate model definitions across layers. Use the shared package.
|
|
1836
|
-
- **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
|
|
1837
|
-
- **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
|
|
1838
|
-
- **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
|
|
1839
|
-
|
|
1840
|
-
## Technology Stack
|
|
1841
|
-
|
|
1842
|
-
- **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
|
|
1843
|
-
- **BFF**: Next.js API Routes (proxy only)
|
|
1844
|
-
- **Backend**: Azure Functions (${backendLanguageLabel})
|
|
1845
|
-
- **Database**: Azure Cosmos DB (NoSQL)
|
|
1846
|
-
- **Schema**: Zod (shared across all layers via workspace package)
|
|
1847
|
-
- **Infrastructure**: Bicep (IaC)
|
|
1848
|
-
- **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
|
|
1849
|
-
- **Auth**: Azure Managed Identity (no connection strings in production)
|
|
1850
|
-
- **Monitoring**: Application Insights
|
|
1851
|
-
`;
|
|
1852
|
-
|
|
1853
|
-
fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
|
|
1854
|
-
console.log(' ✅ AGENTS.md (Codex / generic agents)');
|
|
1855
|
-
|
|
1856
|
-
// ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
|
|
1857
|
-
|
|
1858
|
-
const claudeMd = `# CLAUDE.md
|
|
1859
|
-
|
|
1860
|
-
This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
|
|
1861
|
-
|
|
1862
|
-
## Quick Reference
|
|
1863
|
-
|
|
1864
|
-
- **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
|
|
1865
|
-
- **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
|
|
1866
|
-
- **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
|
|
1867
|
-
- **Backend language**: ${backendLanguageLabel}
|
|
1868
|
-
- **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
|
|
1869
|
-
|
|
1870
|
-
## SwallowKit CLI Commands
|
|
1871
|
-
|
|
1872
|
-
| Task | Command |
|
|
1873
|
-
|------|---------|
|
|
1874
|
-
| Create model | \`npx swallowkit create-model <name>\` |
|
|
1875
|
-
| Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
|
|
1876
|
-
| Dev servers | \`npx swallowkit dev\` |
|
|
1877
|
-
| Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
|
|
1878
|
-
|
|
1879
|
-
## Workflow: Add a new model
|
|
1880
|
-
|
|
1881
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1882
|
-
2. Edit \`shared/models/<name>.ts\` — add your fields
|
|
1883
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1884
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1885
|
-
`;
|
|
1886
|
-
|
|
1887
|
-
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
|
|
1888
|
-
console.log(' ✅ CLAUDE.md (Claude Code)');
|
|
1889
|
-
|
|
1890
|
-
// ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
|
|
1891
|
-
|
|
1892
|
-
const ghDir = path.join(projectDir, '.github');
|
|
1893
|
-
fs.mkdirSync(ghDir, { recursive: true });
|
|
1894
|
-
|
|
1895
|
-
const copilotInstructions = `# Copilot Instructions
|
|
1896
|
-
|
|
1897
|
-
This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
|
|
1898
|
-
|
|
1899
|
-
## Architecture (3-layer)
|
|
1900
|
-
|
|
1901
|
-
\`\`\`
|
|
1902
|
-
Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
|
|
1903
|
-
\`\`\`
|
|
1904
|
-
|
|
1905
|
-
## Key Rules
|
|
1906
|
-
|
|
1907
|
-
1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
|
|
1908
|
-
2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
|
|
1909
|
-
3. **Backend owns data** — All CRUD and business logic stay in \`functions/\`, and generated contract assets under \`functions/generated/\` must stay aligned with \`shared/models/\`.
|
|
1910
|
-
4. **Use the CLI** — Run \`npx swallowkit create-model <name>\` then \`npx swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
|
|
1911
|
-
|
|
1912
|
-
## Naming
|
|
1913
|
-
|
|
1914
|
-
- Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
|
|
1915
|
-
- Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
|
|
1916
|
-
- Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
|
|
1917
|
-
|
|
1918
|
-
## Managed Fields
|
|
1919
|
-
|
|
1920
|
-
\`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
|
|
1921
|
-
`;
|
|
1922
|
-
|
|
1923
|
-
fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
|
|
1924
|
-
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
|
|
1925
|
-
|
|
1926
|
-
// ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
|
|
1927
|
-
|
|
1928
|
-
const instructionsDir = path.join(ghDir, 'instructions');
|
|
1929
|
-
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
1930
|
-
|
|
1931
|
-
// 4a. shared/models — Zod schema layer
|
|
1932
|
-
const sharedModelsInstructions = `---
|
|
1933
|
-
applyTo: "shared/models/**"
|
|
1934
|
-
---
|
|
1935
|
-
|
|
1936
|
-
# Shared Models — Zod Schema Rules
|
|
1937
|
-
|
|
1938
|
-
Files in this directory are the **single source of truth** for data models across the entire application.
|
|
1939
|
-
|
|
1940
|
-
## Rules
|
|
1941
|
-
|
|
1942
|
-
- Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
|
|
1943
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1944
|
-
\`\`\`typescript
|
|
1945
|
-
export const Todo = z.object({ ... });
|
|
1946
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1947
|
-
\`\`\`
|
|
1948
|
-
- Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
|
|
1949
|
-
- Export a \`displayName\` string constant for UI display.
|
|
1950
|
-
- Re-export every model from \`shared/index.ts\`.
|
|
1951
|
-
- For relationships, use **nested schemas** (import and embed the related schema), not ID references.
|
|
1952
|
-
- After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
|
|
1953
|
-
`;
|
|
1990
|
+
console.log(' ✅ AGENTS.md (canonical project contract)');
|
|
1991
|
+
console.log(' ✅ CLAUDE.md (Claude Code adapter)');
|
|
1992
|
+
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
|
|
1993
|
+
console.log(' ✅ .github/instructions/ (path-specific instructions)');
|
|
1954
1994
|
|
|
1955
1995
|
fs.writeFileSync(
|
|
1956
|
-
path.join(
|
|
1957
|
-
|
|
1996
|
+
path.join(projectDir, '.mcp.json'),
|
|
1997
|
+
buildSwallowKitMcpProjectConfigSource(pm)
|
|
1958
1998
|
);
|
|
1999
|
+
console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
|
|
1959
2000
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
applyTo: "app/api/**"
|
|
1963
|
-
---
|
|
1964
|
-
|
|
1965
|
-
# BFF API Routes — Rules
|
|
1966
|
-
|
|
1967
|
-
Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
|
|
1968
|
-
|
|
1969
|
-
## Rules
|
|
1970
|
-
|
|
1971
|
-
- **Never** put business logic, database access, or direct Cosmos DB calls here.
|
|
1972
|
-
- Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
|
|
1973
|
-
- You may validate input/output with Zod schemas before forwarding.
|
|
1974
|
-
- Import schemas from \`@${projectName}/shared\`.
|
|
2001
|
+
writeWorkflowDocs(projectDir, runCmd);
|
|
2002
|
+
console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
|
|
1975
2003
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
import { z } from 'zod/v4';
|
|
2004
|
+
const skillPaths = writeAgentSkills(projectDir, runCmd);
|
|
2005
|
+
console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
|
|
2006
|
+
for (const skillPath of skillPaths) {
|
|
2007
|
+
console.log(` - ${skillPath}`);
|
|
2008
|
+
}
|
|
1982
2009
|
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
responseSchema: z.array(ModelSchema),
|
|
1988
|
-
});
|
|
2010
|
+
console.log('\n✅ AI agent files created\n');
|
|
2011
|
+
}
|
|
2012
|
+
export function buildFunctionsHostKeyBicepExpression(): string {
|
|
2013
|
+
return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
|
|
1989
2014
|
}
|
|
1990
|
-
\`\`\`
|
|
1991
|
-
`;
|
|
1992
|
-
|
|
1993
|
-
fs.writeFileSync(
|
|
1994
|
-
path.join(instructionsDir, 'bff-routes.instructions.md'),
|
|
1995
|
-
bffInstructions
|
|
1996
|
-
);
|
|
1997
|
-
|
|
1998
|
-
// 4c. functions — Azure Functions backend layer
|
|
1999
|
-
const functionsInstructions = `---
|
|
2000
|
-
applyTo: "functions/**"
|
|
2001
|
-
---
|
|
2002
2015
|
|
|
2003
|
-
|
|
2016
|
+
export function buildStaticWebAppConfigBicepSource(): string {
|
|
2017
|
+
return `@description('Static Web App name')
|
|
2018
|
+
param staticWebAppName string
|
|
2004
2019
|
|
|
2005
|
-
|
|
2020
|
+
@description('Functions App default hostname for backend API calls')
|
|
2021
|
+
param functionsDefaultHostname string
|
|
2006
2022
|
|
|
2007
|
-
|
|
2023
|
+
@description('Functions App name used to resolve its host key after deployment')
|
|
2024
|
+
param functionsAppName string
|
|
2008
2025
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
- For C#/Python backends, consume the generated OpenAPI-derived assets in \`functions/generated/\`.
|
|
2012
|
-
- Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
|
|
2013
|
-
- Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
|
|
2026
|
+
@description('Application Insights connection string for SWA')
|
|
2027
|
+
param appInsightsConnectionString string
|
|
2014
2028
|
|
|
2015
|
-
|
|
2029
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2030
|
+
name: staticWebAppName
|
|
2031
|
+
}
|
|
2016
2032
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2033
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2034
|
+
parent: staticWebApp
|
|
2035
|
+
name: 'appsettings'
|
|
2036
|
+
properties: {
|
|
2037
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2038
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2039
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2040
|
+
BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2020
2043
|
|
|
2021
|
-
|
|
2022
|
-
methods: ['GET'],
|
|
2023
|
-
route: '{model}',
|
|
2024
|
-
authLevel: 'anonymous',
|
|
2025
|
-
extraInputs: [cosmosInput],
|
|
2026
|
-
handler: async (request, context) => {
|
|
2027
|
-
const documents = context.extraInputs.get(cosmosInput);
|
|
2028
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
2029
|
-
return { status: 200, jsonBody: validated };
|
|
2030
|
-
},
|
|
2031
|
-
});
|
|
2032
|
-
\`\`\`
|
|
2044
|
+
output configName string = staticWebAppConfig.name
|
|
2033
2045
|
`;
|
|
2034
|
-
|
|
2035
|
-
fs.writeFileSync(
|
|
2036
|
-
path.join(instructionsDir, 'azure-functions.instructions.md'),
|
|
2037
|
-
functionsInstructions
|
|
2038
|
-
);
|
|
2039
|
-
|
|
2040
|
-
console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
|
|
2041
|
-
console.log(' - shared-models.instructions.md');
|
|
2042
|
-
console.log(' - bff-routes.instructions.md');
|
|
2043
|
-
console.log(' - azure-functions.instructions.md');
|
|
2044
|
-
|
|
2045
|
-
console.log('\n✅ AI agent files created\n');
|
|
2046
|
-
console.log(' Supported agents:');
|
|
2047
|
-
console.log(' - OpenAI Codex → AGENTS.md');
|
|
2048
|
-
console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
|
|
2049
|
-
console.log(' - GitHub Copilot → .github/copilot-instructions.md');
|
|
2050
|
-
console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
|
|
2051
|
-
console.log('');
|
|
2052
2046
|
}
|
|
2053
2047
|
|
|
2054
|
-
async function createInfrastructure(
|
|
2048
|
+
export async function createInfrastructure(
|
|
2055
2049
|
projectDir: string,
|
|
2056
2050
|
projectName: string,
|
|
2057
2051
|
azureConfig: AzureConfig,
|
|
@@ -2065,6 +2059,7 @@ async function createInfrastructure(
|
|
|
2065
2059
|
|
|
2066
2060
|
const enableVNet = azureConfig.vnetOption !== 'none';
|
|
2067
2061
|
const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
|
|
2062
|
+
const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
|
|
2068
2063
|
|
|
2069
2064
|
// main.bicep
|
|
2070
2065
|
const mainBicep = `targetScope = 'resourceGroup'
|
|
@@ -2078,6 +2073,13 @@ param location string = resourceGroup().location
|
|
|
2078
2073
|
@description('Location for Static Web App (must be explicitly provided)')
|
|
2079
2074
|
param swaLocation string
|
|
2080
2075
|
|
|
2076
|
+
@description('Static Web Apps hosting plan')
|
|
2077
|
+
@allowed([
|
|
2078
|
+
'Free'
|
|
2079
|
+
'Standard'
|
|
2080
|
+
])
|
|
2081
|
+
param staticWebAppSku string = '${staticWebAppSku}'
|
|
2082
|
+
|
|
2081
2083
|
@description('Cosmos DB mode')
|
|
2082
2084
|
@allowed(['freetier', 'serverless'])
|
|
2083
2085
|
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
@@ -2120,7 +2122,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
|
2120
2122
|
params: {
|
|
2121
2123
|
name: 'swa-\${projectName}'
|
|
2122
2124
|
location: swaLocation
|
|
2123
|
-
sku:
|
|
2125
|
+
sku: staticWebAppSku
|
|
2124
2126
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2125
2127
|
}
|
|
2126
2128
|
}
|
|
@@ -2161,16 +2163,11 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
|
|
|
2161
2163
|
params: {
|
|
2162
2164
|
name: 'pe-cosmos-\${projectName}'
|
|
2163
2165
|
location: location
|
|
2164
|
-
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2165
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2166
|
-
subnetId: vnet
|
|
2167
|
-
vnetId: vnet
|
|
2166
|
+
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
|
|
2167
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
2168
|
+
subnetId: vnet!.outputs.privateEndpointSubnetId
|
|
2169
|
+
vnetId: vnet!.outputs.id
|
|
2168
2170
|
}
|
|
2169
|
-
dependsOn: [
|
|
2170
|
-
cosmosDbFreeTier
|
|
2171
|
-
cosmosDbServerless
|
|
2172
|
-
vnet
|
|
2173
|
-
]
|
|
2174
2171
|
}
|
|
2175
2172
|
|
|
2176
2173
|
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
@@ -2182,16 +2179,14 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
2182
2179
|
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
2183
2180
|
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
2184
2181
|
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
2185
|
-
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2186
|
-
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2182
|
+
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
2183
|
+
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
2187
2184
|
functionsRuntimeName: '${functionsRuntime.name}'
|
|
2188
2185
|
functionsRuntimeVersion: '${functionsRuntime.version}'
|
|
2189
2186
|
enableVNet: enableVNet
|
|
2190
|
-
vnetSubnetId: enableVNet ? vnet
|
|
2187
|
+
vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
|
|
2191
2188
|
}
|
|
2192
2189
|
dependsOn: [
|
|
2193
|
-
cosmosDbFreeTier
|
|
2194
|
-
cosmosDbServerless
|
|
2195
2190
|
cosmosPrivateEndpoint
|
|
2196
2191
|
]
|
|
2197
2192
|
}
|
|
@@ -2200,23 +2195,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
2200
2195
|
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
2201
2196
|
name: 'cosmosDbRoleAssignment'
|
|
2202
2197
|
params: {
|
|
2203
|
-
cosmosAccountName: cosmosDbFreeTier
|
|
2198
|
+
cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
|
|
2204
2199
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2205
2200
|
}
|
|
2206
|
-
dependsOn: [
|
|
2207
|
-
functionsFlex
|
|
2208
|
-
]
|
|
2209
2201
|
}
|
|
2210
2202
|
|
|
2211
2203
|
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
2212
2204
|
name: 'cosmosDbRoleAssignment'
|
|
2213
2205
|
params: {
|
|
2214
|
-
cosmosAccountName: cosmosDbServerless
|
|
2206
|
+
cosmosAccountName: cosmosDbServerless!.outputs.accountName
|
|
2215
2207
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2216
2208
|
}
|
|
2217
|
-
dependsOn: [
|
|
2218
|
-
functionsFlex
|
|
2219
|
-
]
|
|
2220
2209
|
}
|
|
2221
2210
|
|
|
2222
2211
|
// Update SWA config with Functions hostname (after Functions deployment)
|
|
@@ -2225,20 +2214,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
|
2225
2214
|
params: {
|
|
2226
2215
|
staticWebAppName: staticWebApp.outputs.name
|
|
2227
2216
|
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
2217
|
+
functionsAppName: functionsFlex.outputs.name
|
|
2228
2218
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2229
2219
|
}
|
|
2230
|
-
dependsOn: [
|
|
2231
|
-
functionsFlex
|
|
2232
|
-
]
|
|
2233
2220
|
}
|
|
2234
2221
|
|
|
2235
2222
|
output staticWebAppName string = staticWebApp.outputs.name
|
|
2236
2223
|
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
2237
2224
|
output functionsAppName string = functionsFlex.outputs.name
|
|
2238
2225
|
output functionsAppUrl string = functionsFlex.outputs.defaultHostname
|
|
2239
|
-
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2240
|
-
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2241
|
-
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2226
|
+
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
2227
|
+
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
2228
|
+
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
2242
2229
|
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
2243
2230
|
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
2244
2231
|
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
@@ -2246,7 +2233,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
|
|
|
2246
2233
|
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
2247
2234
|
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
2248
2235
|
output vnetEnabled bool = enableVNet
|
|
2249
|
-
output vnetName string = enableVNet ? vnet
|
|
2236
|
+
output vnetName string = enableVNet ? vnet!.outputs.name : ''
|
|
2250
2237
|
`;
|
|
2251
2238
|
|
|
2252
2239
|
fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
|
|
@@ -2262,6 +2249,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
|
2262
2249
|
"cosmosDbMode": {
|
|
2263
2250
|
"value": "${azureConfig.cosmosDbMode}"
|
|
2264
2251
|
},
|
|
2252
|
+
"staticWebAppSku": {
|
|
2253
|
+
"value": "${staticWebAppSku}"
|
|
2254
|
+
},
|
|
2265
2255
|
"enableVNet": {
|
|
2266
2256
|
"value": ${enableVNet}
|
|
2267
2257
|
}
|
|
@@ -2311,38 +2301,14 @@ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
|
2311
2301
|
}
|
|
2312
2302
|
}
|
|
2313
2303
|
|
|
2314
|
-
output id string = staticWebApp.id
|
|
2315
|
-
output name string = staticWebApp.name
|
|
2316
|
-
output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
2317
|
-
`;
|
|
2318
|
-
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
2319
|
-
|
|
2320
|
-
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
2321
|
-
const staticWebAppConfigBicep = `@description('Static Web App name')
|
|
2322
|
-
param staticWebAppName string
|
|
2323
|
-
|
|
2324
|
-
@description('Functions App default hostname for backend API calls')
|
|
2325
|
-
param functionsDefaultHostname string
|
|
2326
|
-
|
|
2327
|
-
@description('Application Insights connection string for SWA')
|
|
2328
|
-
param appInsightsConnectionString string
|
|
2329
|
-
|
|
2330
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2331
|
-
name: staticWebAppName
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2335
|
-
parent: staticWebApp
|
|
2336
|
-
name: 'appsettings'
|
|
2337
|
-
properties: {
|
|
2338
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2339
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2340
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
output configName string = staticWebAppConfig.name
|
|
2304
|
+
output id string = staticWebApp.id
|
|
2305
|
+
output name string = staticWebApp.name
|
|
2306
|
+
output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
2345
2307
|
`;
|
|
2308
|
+
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
2309
|
+
|
|
2310
|
+
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
2311
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
2346
2312
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
2347
2313
|
|
|
2348
2314
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
@@ -2565,63 +2531,7 @@ output principalId string = functionApp.identity.principalId
|
|
|
2565
2531
|
fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
|
|
2566
2532
|
|
|
2567
2533
|
// modules/cosmosdb-freetier.bicep (Free Tier)
|
|
2568
|
-
const cosmosDbFreeTierBicep =
|
|
2569
|
-
param accountName string
|
|
2570
|
-
|
|
2571
|
-
@description('Database name')
|
|
2572
|
-
param databaseName string
|
|
2573
|
-
|
|
2574
|
-
@description('Location for Cosmos DB')
|
|
2575
|
-
param location string
|
|
2576
|
-
|
|
2577
|
-
@description('Public network access')
|
|
2578
|
-
@allowed(['Enabled', 'Disabled'])
|
|
2579
|
-
param publicNetworkAccess string = 'Enabled'
|
|
2580
|
-
|
|
2581
|
-
// Cosmos DB Account (Free Tier)
|
|
2582
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
2583
|
-
name: accountName
|
|
2584
|
-
location: location
|
|
2585
|
-
kind: 'GlobalDocumentDB'
|
|
2586
|
-
properties: {
|
|
2587
|
-
databaseAccountOfferType: 'Standard'
|
|
2588
|
-
enableAutomaticFailover: false
|
|
2589
|
-
enableFreeTier: true
|
|
2590
|
-
publicNetworkAccess: publicNetworkAccess
|
|
2591
|
-
disableLocalAuth: true
|
|
2592
|
-
consistencyPolicy: {
|
|
2593
|
-
defaultConsistencyLevel: 'Session'
|
|
2594
|
-
}
|
|
2595
|
-
locations: [
|
|
2596
|
-
{
|
|
2597
|
-
locationName: location
|
|
2598
|
-
failoverPriority: 0
|
|
2599
|
-
isZoneRedundant: false
|
|
2600
|
-
}
|
|
2601
|
-
]
|
|
2602
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
// Cosmos DB Database
|
|
2607
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
2608
|
-
parent: cosmosAccount
|
|
2609
|
-
name: databaseName
|
|
2610
|
-
properties: {
|
|
2611
|
-
resource: {
|
|
2612
|
-
id: databaseName
|
|
2613
|
-
}
|
|
2614
|
-
options: {
|
|
2615
|
-
throughput: 1000
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
output id string = cosmosAccount.id
|
|
2621
|
-
output accountName string = cosmosAccount.name
|
|
2622
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
2623
|
-
output databaseName string = database.name
|
|
2624
|
-
`;
|
|
2534
|
+
const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
|
|
2625
2535
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
|
|
2626
2536
|
|
|
2627
2537
|
// modules/cosmosdb-serverless.bicep (Serverless)
|
|
@@ -2714,9 +2624,10 @@ output roleAssignmentId string = roleAssignment.id
|
|
|
2714
2624
|
`;
|
|
2715
2625
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
|
|
2716
2626
|
|
|
2717
|
-
//
|
|
2718
|
-
|
|
2719
|
-
|
|
2627
|
+
// Bicep resolves module files at compile time even when their deployment condition is false,
|
|
2628
|
+
// so these definitions must always be generated. main.bicep still controls deployment with
|
|
2629
|
+
// `if (enableVNet)`.
|
|
2630
|
+
// modules/vnet.bicep
|
|
2720
2631
|
const vnetBicep = `@description('VNet name')
|
|
2721
2632
|
param name string
|
|
2722
2633
|
|
|
@@ -2846,26 +2757,30 @@ output privateDnsZoneId string = privateDnsZone.id
|
|
|
2846
2757
|
`;
|
|
2847
2758
|
fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
|
|
2848
2759
|
|
|
2849
|
-
|
|
2850
|
-
}
|
|
2760
|
+
console.log('✅ VNet module definitions created\n');
|
|
2851
2761
|
|
|
2852
2762
|
console.log('✅ Infrastructure files created\n');
|
|
2853
2763
|
}
|
|
2854
2764
|
|
|
2855
|
-
function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage): string {
|
|
2765
|
+
export function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
|
|
2856
2766
|
const pmCmd = getCommands(pm);
|
|
2857
2767
|
const pnpmSetupStep = getCiSetupStep(pm);
|
|
2768
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2858
2769
|
|
|
2859
2770
|
const commonSetup = ` - uses: actions/checkout@v4
|
|
2860
2771
|
|
|
2772
|
+
${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
|
|
2861
2773
|
- name: Setup Node.js
|
|
2862
2774
|
uses: actions/setup-node@v4
|
|
2863
2775
|
with:
|
|
2864
2776
|
node-version: '22'
|
|
2865
|
-
${
|
|
2777
|
+
${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
|
|
2778
|
+
cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
2779
|
+
`}
|
|
2780
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
2866
2781
|
- name: Install dependencies
|
|
2867
2782
|
run: |
|
|
2868
|
-
${
|
|
2783
|
+
${getCiInstallCommand(pm, projectPm)}
|
|
2869
2784
|
|
|
2870
2785
|
- name: Build shared package
|
|
2871
2786
|
run: |
|
|
@@ -2951,7 +2866,7 @@ jobs:
|
|
|
2951
2866
|
${commonSetup} - name: Setup .NET
|
|
2952
2867
|
uses: actions/setup-dotnet@v4
|
|
2953
2868
|
with:
|
|
2954
|
-
dotnet-version: '
|
|
2869
|
+
dotnet-version: '10.0.x'
|
|
2955
2870
|
|
|
2956
2871
|
- name: Publish Functions
|
|
2957
2872
|
run: |
|
|
@@ -3012,16 +2927,18 @@ ${commonSetup} - name: Setup Python
|
|
|
3012
2927
|
`;
|
|
3013
2928
|
}
|
|
3014
2929
|
|
|
3015
|
-
function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage): string {
|
|
2930
|
+
export function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
|
|
3016
2931
|
const pmCmd = getCommands(pm);
|
|
3017
2932
|
const azPipelinesSetup = getAzurePipelinesSetup(pm);
|
|
2933
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
3018
2934
|
const commonSetup = ` - task: NodeTool@0
|
|
3019
2935
|
inputs:
|
|
3020
2936
|
versionSpec: '22.x'
|
|
3021
2937
|
displayName: 'Install Node.js'
|
|
3022
2938
|
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2939
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
3023
2940
|
- script: |
|
|
3024
|
-
${
|
|
2941
|
+
${getCiInstallCommand(pm, projectPm)}
|
|
3025
2942
|
displayName: 'Install workspace dependencies'
|
|
3026
2943
|
|
|
3027
2944
|
- script: |
|
|
@@ -3127,7 +3044,7 @@ variables:
|
|
|
3127
3044
|
steps:
|
|
3128
3045
|
${commonSetup} - task: UseDotNet@2
|
|
3129
3046
|
inputs:
|
|
3130
|
-
version: '
|
|
3047
|
+
version: '10.0.x'
|
|
3131
3048
|
displayName: 'Install .NET SDK'
|
|
3132
3049
|
|
|
3133
3050
|
- script: |
|
|
@@ -3209,6 +3126,195 @@ ${commonSetup} - task: UsePythonVersion@0
|
|
|
3209
3126
|
`;
|
|
3210
3127
|
}
|
|
3211
3128
|
|
|
3129
|
+
// setup-node selects the runner runtime only; Oryx reads the app manifest separately.
|
|
3130
|
+
export function getSwaNodeVersionCommand(): string {
|
|
3131
|
+
return `node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.engines={...p.engines,node:'>=22.14.0 <23'};fs.writeFileSync('package.json',JSON.stringify(p,null,2)+String.fromCharCode(10));"`;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
export function buildAzureSwaPipeline(pm: PackageManager, projectPm: PackageManager = pm): string {
|
|
3135
|
+
// SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
|
|
3136
|
+
pm = 'npm';
|
|
3137
|
+
const pmCmd = getCommands(pm);
|
|
3138
|
+
const azPipelinesSetup = getAzurePipelinesSetup(pm);
|
|
3139
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
3140
|
+
|
|
3141
|
+
return `trigger:
|
|
3142
|
+
branches:
|
|
3143
|
+
include:
|
|
3144
|
+
- main
|
|
3145
|
+
paths:
|
|
3146
|
+
include:
|
|
3147
|
+
- app/**
|
|
3148
|
+
- components/**
|
|
3149
|
+
- lib/**
|
|
3150
|
+
- shared/**
|
|
3151
|
+
- public/**
|
|
3152
|
+
- package.json
|
|
3153
|
+
- next.config.js
|
|
3154
|
+
|
|
3155
|
+
pr:
|
|
3156
|
+
branches:
|
|
3157
|
+
include:
|
|
3158
|
+
- main
|
|
3159
|
+
paths:
|
|
3160
|
+
include:
|
|
3161
|
+
- app/**
|
|
3162
|
+
- components/**
|
|
3163
|
+
- lib/**
|
|
3164
|
+
- shared/**
|
|
3165
|
+
- public/**
|
|
3166
|
+
- package.json
|
|
3167
|
+
- next.config.js
|
|
3168
|
+
|
|
3169
|
+
pool:
|
|
3170
|
+
vmImage: 'ubuntu-latest'
|
|
3171
|
+
|
|
3172
|
+
variables:
|
|
3173
|
+
- group: azure-deployment
|
|
3174
|
+
|
|
3175
|
+
steps:
|
|
3176
|
+
- task: NodeTool@0
|
|
3177
|
+
inputs:
|
|
3178
|
+
versionSpec: '22.x'
|
|
3179
|
+
displayName: 'Install Node.js'
|
|
3180
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
3181
|
+
- script: |
|
|
3182
|
+
${getSwaNodeVersionCommand()}
|
|
3183
|
+
displayName: 'Select Node.js 22 for SWA'
|
|
3184
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
3185
|
+
- script: |
|
|
3186
|
+
${getCiInstallCommand(pm, projectPm)}
|
|
3187
|
+
displayName: 'Install dependencies'
|
|
3188
|
+
|
|
3189
|
+
- script: |
|
|
3190
|
+
${pmCmd.run} build
|
|
3191
|
+
env:
|
|
3192
|
+
NODE_ENV: production
|
|
3193
|
+
displayName: 'Build Next.js app'
|
|
3194
|
+
|
|
3195
|
+
- task: AzureStaticWebApp@0
|
|
3196
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3197
|
+
inputs:
|
|
3198
|
+
app_location: '.'
|
|
3199
|
+
output_location: '.next/standalone'
|
|
3200
|
+
skip_app_build: true
|
|
3201
|
+
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
3202
|
+
displayName: 'Deploy to Azure Static Web Apps'
|
|
3203
|
+
`;
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
function getAzurePipelinesNpmWorkspaceNormalizationStep(pm: PackageManager, projectPm: PackageManager): string {
|
|
3207
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
3208
|
+
return '';
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
return ` - script: |
|
|
3212
|
+
node <<'NODE'
|
|
3213
|
+
const fs = require('fs');
|
|
3214
|
+
|
|
3215
|
+
function readJson(path) {
|
|
3216
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function writeJson(path, value) {
|
|
3220
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
const sharedPackagePath = 'shared/package.json';
|
|
3224
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
3225
|
+
|
|
3226
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
3227
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
3228
|
+
if (!pkg[section]) continue;
|
|
3229
|
+
|
|
3230
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
3231
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
3232
|
+
pkg[section][name] = replacement;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
const root = readJson('package.json');
|
|
3239
|
+
root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
|
|
3240
|
+
delete root.packageManager;
|
|
3241
|
+
root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
|
|
3242
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
3243
|
+
root.scripts = root.scripts || {};
|
|
3244
|
+
root.scripts.build = ${JSON.stringify(getBuildScript('npm'))};
|
|
3245
|
+
writeJson('package.json', root);
|
|
3246
|
+
|
|
3247
|
+
if (fs.existsSync('functions/package.json')) {
|
|
3248
|
+
const functionsPkg = readJson('functions/package.json');
|
|
3249
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
3250
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
3251
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
3252
|
+
}
|
|
3253
|
+
writeJson('functions/package.json', functionsPkg);
|
|
3254
|
+
}
|
|
3255
|
+
NODE
|
|
3256
|
+
|
|
3257
|
+
npm install --package-lock-only --ignore-scripts
|
|
3258
|
+
displayName: 'Normalize pnpm workspace for npm CI'`;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
function getGitHubNpmWorkspaceNormalizationStep(pm: PackageManager, projectPm: PackageManager): string {
|
|
3262
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
3263
|
+
return '';
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
return ` - name: Normalize pnpm workspace for npm CI
|
|
3267
|
+
shell: bash
|
|
3268
|
+
run: |
|
|
3269
|
+
node <<'NODE'
|
|
3270
|
+
const fs = require('fs');
|
|
3271
|
+
|
|
3272
|
+
function readJson(path) {
|
|
3273
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
function writeJson(path, value) {
|
|
3277
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
const sharedPackagePath = 'shared/package.json';
|
|
3281
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
3282
|
+
|
|
3283
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
3284
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
3285
|
+
if (!pkg[section]) continue;
|
|
3286
|
+
|
|
3287
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
3288
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
3289
|
+
pkg[section][name] = replacement;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
const root = readJson('package.json');
|
|
3296
|
+
root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
|
|
3297
|
+
delete root.packageManager;
|
|
3298
|
+
root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
|
|
3299
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
3300
|
+
root.scripts = root.scripts || {};
|
|
3301
|
+
root.scripts.build = ${JSON.stringify(getBuildScript('npm'))};
|
|
3302
|
+
writeJson('package.json', root);
|
|
3303
|
+
|
|
3304
|
+
if (fs.existsSync('functions/package.json')) {
|
|
3305
|
+
const functionsPkg = readJson('functions/package.json');
|
|
3306
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
3307
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
3308
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
3309
|
+
}
|
|
3310
|
+
writeJson('functions/package.json', functionsPkg);
|
|
3311
|
+
}
|
|
3312
|
+
NODE
|
|
3313
|
+
|
|
3314
|
+
npm install --package-lock-only --ignore-scripts
|
|
3315
|
+
`;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3212
3318
|
async function createGitHubActionsWorkflows(
|
|
3213
3319
|
projectDir: string,
|
|
3214
3320
|
azureConfig: AzureConfig,
|
|
@@ -3216,13 +3322,27 @@ async function createGitHubActionsWorkflows(
|
|
|
3216
3322
|
backendLanguage: BackendLanguage
|
|
3217
3323
|
) {
|
|
3218
3324
|
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
3219
|
-
|
|
3220
|
-
const pmCmd = getCommands(pm);
|
|
3221
3325
|
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
3222
3326
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
3223
3327
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3328
|
+
const actionsPm: PackageManager = pm;
|
|
3329
|
+
|
|
3330
|
+
const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
|
|
3331
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
3332
|
+
|
|
3333
|
+
// deploy-functions.yml
|
|
3334
|
+
const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
|
|
3335
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
3336
|
+
|
|
3337
|
+
console.log('✅ GitHub Actions workflows created\n');
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
export function buildGitHubSwaWorkflow(pm: PackageManager, projectPm: PackageManager = pm): string {
|
|
3341
|
+
// SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
|
|
3342
|
+
pm = 'npm';
|
|
3343
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
3344
|
+
|
|
3345
|
+
return `name: Deploy Static Web App
|
|
3226
3346
|
|
|
3227
3347
|
on:
|
|
3228
3348
|
push:
|
|
@@ -3235,6 +3355,7 @@ on:
|
|
|
3235
3355
|
- 'shared/**'
|
|
3236
3356
|
- 'public/**'
|
|
3237
3357
|
- 'package.json'
|
|
3358
|
+
- '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3238
3359
|
- 'next.config.js'
|
|
3239
3360
|
- 'next.config.ts'
|
|
3240
3361
|
workflow_dispatch:
|
|
@@ -3248,6 +3369,7 @@ on:
|
|
|
3248
3369
|
- 'shared/**'
|
|
3249
3370
|
- 'public/**'
|
|
3250
3371
|
- 'package.json'
|
|
3372
|
+
- '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3251
3373
|
- 'next.config.js'
|
|
3252
3374
|
- 'next.config.ts'
|
|
3253
3375
|
|
|
@@ -3260,6 +3382,22 @@ jobs:
|
|
|
3260
3382
|
- uses: actions/checkout@v4
|
|
3261
3383
|
with:
|
|
3262
3384
|
submodules: true
|
|
3385
|
+
|
|
3386
|
+
${getCiSetupStep(pm) ? `${getCiSetupStep(pm)}\n` : ''}
|
|
3387
|
+
- name: Setup Node.js
|
|
3388
|
+
uses: actions/setup-node@v4
|
|
3389
|
+
with:
|
|
3390
|
+
node-version: '22'
|
|
3391
|
+
${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
|
|
3392
|
+
cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
3393
|
+
`}
|
|
3394
|
+
- name: Select Node.js 22 for SWA
|
|
3395
|
+
run: |
|
|
3396
|
+
${getSwaNodeVersionCommand()}
|
|
3397
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
3398
|
+
- name: Install and build app
|
|
3399
|
+
run: |
|
|
3400
|
+
${getSwaAppBuildCommand(pm, projectPm)}
|
|
3263
3401
|
|
|
3264
3402
|
- name: Deploy to Azure Static Web Apps
|
|
3265
3403
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
@@ -3271,93 +3409,27 @@ jobs:
|
|
|
3271
3409
|
app_location: '/'
|
|
3272
3410
|
api_location: ''
|
|
3273
3411
|
output_location: ''
|
|
3412
|
+
app_build_command: 'npm run build'
|
|
3274
3413
|
env:
|
|
3275
3414
|
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
3276
3415
|
`;
|
|
3277
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
3278
|
-
|
|
3279
|
-
// deploy-functions.yml
|
|
3280
|
-
const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
|
|
3281
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
3282
|
-
|
|
3283
|
-
console.log('✅ GitHub Actions workflows created\n');
|
|
3284
3416
|
}
|
|
3285
3417
|
|
|
3286
3418
|
async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
|
|
3287
3419
|
console.log('📦 Creating Azure Pipelines...\n');
|
|
3288
|
-
|
|
3289
|
-
const
|
|
3290
|
-
const azPipelinesSetup = getAzurePipelinesSetup(pm);
|
|
3420
|
+
|
|
3421
|
+
const pipelinesPm: PackageManager = pm;
|
|
3291
3422
|
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
3292
3423
|
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
3293
3424
|
|
|
3294
3425
|
// swa.yml
|
|
3295
|
-
const swaPipeline =
|
|
3296
|
-
branches:
|
|
3297
|
-
include:
|
|
3298
|
-
- main
|
|
3299
|
-
paths:
|
|
3300
|
-
include:
|
|
3301
|
-
- app/**
|
|
3302
|
-
- components/**
|
|
3303
|
-
- lib/**
|
|
3304
|
-
- shared/**
|
|
3305
|
-
- public/**
|
|
3306
|
-
- package.json
|
|
3307
|
-
- next.config.js
|
|
3308
|
-
|
|
3309
|
-
pr:
|
|
3310
|
-
branches:
|
|
3311
|
-
include:
|
|
3312
|
-
- main
|
|
3313
|
-
paths:
|
|
3314
|
-
include:
|
|
3315
|
-
- app/**
|
|
3316
|
-
- components/**
|
|
3317
|
-
- lib/**
|
|
3318
|
-
- shared/**
|
|
3319
|
-
- public/**
|
|
3320
|
-
- package.json
|
|
3321
|
-
- next.config.js
|
|
3322
|
-
|
|
3323
|
-
pool:
|
|
3324
|
-
vmImage: 'ubuntu-latest'
|
|
3325
|
-
|
|
3326
|
-
variables:
|
|
3327
|
-
- group: azure-deployment
|
|
3328
|
-
|
|
3329
|
-
steps:
|
|
3330
|
-
- task: NodeTool@0
|
|
3331
|
-
inputs:
|
|
3332
|
-
versionSpec: '22.x'
|
|
3333
|
-
displayName: 'Install Node.js'
|
|
3334
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
3335
|
-
- script: |
|
|
3336
|
-
${pmCmd.ci}
|
|
3337
|
-
displayName: 'Install dependencies'
|
|
3338
|
-
|
|
3339
|
-
- script: |
|
|
3340
|
-
${pmCmd.run} build
|
|
3341
|
-
env:
|
|
3342
|
-
NODE_ENV: production
|
|
3343
|
-
displayName: 'Build Next.js app'
|
|
3344
|
-
|
|
3345
|
-
- task: AzureStaticWebApp@0
|
|
3346
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3347
|
-
inputs:
|
|
3348
|
-
app_location: '.'
|
|
3349
|
-
output_location: '.next/standalone'
|
|
3350
|
-
skip_app_build: true
|
|
3351
|
-
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
3352
|
-
displayName: 'Deploy to Azure Static Web Apps'
|
|
3353
|
-
`;
|
|
3426
|
+
const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
|
|
3354
3427
|
fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
|
|
3355
3428
|
|
|
3356
3429
|
// functions.yml
|
|
3357
|
-
const functionsPipeline = getAzureFunctionsPipeline(
|
|
3430
|
+
const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
|
|
3358
3431
|
fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
|
|
3359
3432
|
|
|
3360
3433
|
console.log('✅ Azure Pipelines created\n');
|
|
3361
3434
|
}
|
|
3362
3435
|
|
|
3363
|
-
|