swallowkit 1.0.0-beta.9 → 1.0.1
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 +69 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +788 -766
- 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 +190 -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 +619 -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 +366 -2
- 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 +851 -786
- 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 +203 -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,12 +692,12 @@ 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
|
`;
|
|
@@ -440,14 +719,70 @@ export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): s
|
|
|
440
719
|
`;
|
|
441
720
|
}
|
|
442
721
|
|
|
443
|
-
export function buildGeneratedProjectDependencies(
|
|
722
|
+
export function buildGeneratedProjectDependencies(
|
|
723
|
+
projectName: string,
|
|
724
|
+
pm: PackageManager = 'pnpm',
|
|
725
|
+
): Record<string, string> {
|
|
444
726
|
return {
|
|
445
727
|
'@azure/cosmos': '^4.0.0',
|
|
446
728
|
'applicationinsights': '^3.3.0',
|
|
447
|
-
|
|
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",
|
|
448
754
|
};
|
|
449
755
|
}
|
|
450
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
|
+
|
|
451
786
|
async function addSwallowKitFiles(
|
|
452
787
|
projectDir: string,
|
|
453
788
|
options: InitOptions,
|
|
@@ -464,19 +799,16 @@ async function addSwallowKitFiles(
|
|
|
464
799
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
465
800
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
466
801
|
|
|
467
|
-
//
|
|
802
|
+
// Root BFF/auth code imports Zod directly; declare it as well as the shared package.
|
|
468
803
|
packageJson.dependencies = {
|
|
469
804
|
...packageJson.dependencies,
|
|
470
|
-
...buildGeneratedProjectDependencies(projectName),
|
|
805
|
+
...buildGeneratedProjectDependencies(projectName, pm),
|
|
806
|
+
};
|
|
807
|
+
packageJson.devDependencies = {
|
|
808
|
+
...packageJson.devDependencies,
|
|
809
|
+
...buildGeneratedProjectDevDependencies(),
|
|
471
810
|
};
|
|
472
811
|
|
|
473
|
-
if (backendLanguage !== "typescript") {
|
|
474
|
-
packageJson.devDependencies = {
|
|
475
|
-
...packageJson.devDependencies,
|
|
476
|
-
'@openapitools/openapi-generator-cli': '^2.21.0',
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
812
|
packageJson.scripts = {
|
|
481
813
|
...packageJson.scripts,
|
|
482
814
|
'build': getBuildScript(pm),
|
|
@@ -484,28 +816,27 @@ async function addSwallowKitFiles(
|
|
|
484
816
|
'functions:start': getFunctionsStartScript(pm, backendLanguage),
|
|
485
817
|
};
|
|
486
818
|
|
|
487
|
-
if (pm === 'pnpm') {
|
|
488
|
-
packageJson.packageManager = 'pnpm@latest';
|
|
489
|
-
}
|
|
490
|
-
|
|
491
819
|
packageJson.engines = {
|
|
492
|
-
node: '
|
|
820
|
+
node: '>=22.14.0',
|
|
821
|
+
...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
|
|
493
822
|
};
|
|
823
|
+
packageJson.devEngines = {
|
|
824
|
+
packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
|
|
825
|
+
};
|
|
826
|
+
delete packageJson.packageManager;
|
|
494
827
|
|
|
495
|
-
//
|
|
828
|
+
// Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
|
|
496
829
|
const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
|
|
497
830
|
const wsConfig = getWorkspaceConfig(pm, workspacePackages);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
} else {
|
|
502
|
-
// npm: workspaces are defined in package.json
|
|
831
|
+
packageJson.workspaces = workspacePackages;
|
|
832
|
+
|
|
833
|
+
if (wsConfig.type !== 'file') {
|
|
503
834
|
packageJson.workspaces = wsConfig.value;
|
|
504
835
|
}
|
|
505
836
|
|
|
506
837
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
507
838
|
|
|
508
|
-
// Create workspace
|
|
839
|
+
// Create pnpm-workspace.yaml for local pnpm development when needed.
|
|
509
840
|
if (wsConfig.type === 'file') {
|
|
510
841
|
fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
|
|
511
842
|
}
|
|
@@ -565,6 +896,13 @@ function getFunctionsBaseUrl(): string {
|
|
|
565
896
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
566
897
|
}
|
|
567
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
|
+
|
|
568
906
|
/**
|
|
569
907
|
* Simple HTTP client for calling backend APIs
|
|
570
908
|
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
@@ -572,10 +910,11 @@ function getFunctionsBaseUrl(): string {
|
|
|
572
910
|
async function request<T>(
|
|
573
911
|
endpoint: string,
|
|
574
912
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
575
|
-
body?:
|
|
913
|
+
body?: unknown,
|
|
576
914
|
queryParams?: Record<string, string>
|
|
577
915
|
): Promise<T> {
|
|
578
916
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
917
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
579
918
|
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
580
919
|
if (queryParams) {
|
|
581
920
|
const params = new URLSearchParams(queryParams);
|
|
@@ -587,6 +926,7 @@ async function request<T>(
|
|
|
587
926
|
method,
|
|
588
927
|
headers: {
|
|
589
928
|
'Content-Type': 'application/json',
|
|
929
|
+
...keyHeaders,
|
|
590
930
|
},
|
|
591
931
|
body: body ? JSON.stringify(body) : undefined,
|
|
592
932
|
});
|
|
@@ -642,14 +982,14 @@ export const api = {
|
|
|
642
982
|
/**
|
|
643
983
|
* Make a POST request
|
|
644
984
|
*/
|
|
645
|
-
post: <T>(endpoint: string, body?:
|
|
985
|
+
post: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
646
986
|
return request<T>(endpoint, 'POST', body);
|
|
647
987
|
},
|
|
648
988
|
|
|
649
989
|
/**
|
|
650
990
|
* Make a PUT request
|
|
651
991
|
*/
|
|
652
|
-
put: <T>(endpoint: string, body?:
|
|
992
|
+
put: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
653
993
|
return request<T>(endpoint, 'PUT', body);
|
|
654
994
|
},
|
|
655
995
|
|
|
@@ -670,6 +1010,7 @@ export const api = {
|
|
|
670
1010
|
// 6. Create .env.example
|
|
671
1011
|
const envExample = `# Azure Functions Backend URL
|
|
672
1012
|
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
1013
|
+
BACKEND_FUNCTIONS_KEY=
|
|
673
1014
|
|
|
674
1015
|
# Azure Configuration
|
|
675
1016
|
AZURE_RESOURCE_GROUP=your-resource-group
|
|
@@ -709,7 +1050,7 @@ export async function register() {
|
|
|
709
1050
|
const originalConsoleError = console.error;
|
|
710
1051
|
const originalConsoleWarn = console.warn;
|
|
711
1052
|
|
|
712
|
-
console.log = function(...args:
|
|
1053
|
+
console.log = function(...args: unknown[]) {
|
|
713
1054
|
originalConsoleLog.apply(console, args);
|
|
714
1055
|
const message = args.map(arg =>
|
|
715
1056
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -720,7 +1061,7 @@ export async function register() {
|
|
|
720
1061
|
});
|
|
721
1062
|
};
|
|
722
1063
|
|
|
723
|
-
console.error = function(...args:
|
|
1064
|
+
console.error = function(...args: unknown[]) {
|
|
724
1065
|
originalConsoleError.apply(console, args);
|
|
725
1066
|
const message = args.map(arg =>
|
|
726
1067
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -731,7 +1072,7 @@ export async function register() {
|
|
|
731
1072
|
});
|
|
732
1073
|
};
|
|
733
1074
|
|
|
734
|
-
console.warn = function(...args:
|
|
1075
|
+
console.warn = function(...args: unknown[]) {
|
|
735
1076
|
originalConsoleWarn.apply(console, args);
|
|
736
1077
|
const message = args.map(arg =>
|
|
737
1078
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -755,6 +1096,7 @@ export async function register() {
|
|
|
755
1096
|
const envLocalContent = [
|
|
756
1097
|
'# Azure Functions Backend URL (Local)',
|
|
757
1098
|
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
1099
|
+
'BACKEND_FUNCTIONS_KEY=',
|
|
758
1100
|
''
|
|
759
1101
|
].join('\n');
|
|
760
1102
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -803,7 +1145,7 @@ export async function register() {
|
|
|
803
1145
|
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
|
|
804
1146
|
|
|
805
1147
|
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
806
|
-
createAiAgentFiles(projectDir, projectName, backendLanguage);
|
|
1148
|
+
createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
|
|
807
1149
|
}
|
|
808
1150
|
|
|
809
1151
|
async function createSharedPackage(projectDir: string, projectName: string) {
|
|
@@ -815,6 +1157,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
|
|
|
815
1157
|
|
|
816
1158
|
// shared/package.json
|
|
817
1159
|
const sharedPackageJson = {
|
|
1160
|
+
private: true,
|
|
818
1161
|
name: `@${projectName}/shared`,
|
|
819
1162
|
version: '1.0.0',
|
|
820
1163
|
description: 'Shared Zod models — Single Source of Truth for validation schemas',
|
|
@@ -837,28 +1180,9 @@ async function createSharedPackage(projectDir: string, projectName: string) {
|
|
|
837
1180
|
);
|
|
838
1181
|
|
|
839
1182
|
// shared/tsconfig.json
|
|
840
|
-
const sharedTsConfig = {
|
|
841
|
-
compilerOptions: {
|
|
842
|
-
target: 'ES2020',
|
|
843
|
-
module: 'commonjs',
|
|
844
|
-
moduleResolution: 'node',
|
|
845
|
-
lib: ['ES2020'],
|
|
846
|
-
outDir: 'dist',
|
|
847
|
-
rootDir: '.',
|
|
848
|
-
declaration: true,
|
|
849
|
-
declarationMap: true,
|
|
850
|
-
sourceMap: true,
|
|
851
|
-
strict: true,
|
|
852
|
-
esModuleInterop: true,
|
|
853
|
-
skipLibCheck: true,
|
|
854
|
-
forceConsistentCasingInFileNames: true,
|
|
855
|
-
},
|
|
856
|
-
include: ['index.ts', 'models/**/*'],
|
|
857
|
-
exclude: ['node_modules', 'dist'],
|
|
858
|
-
};
|
|
859
1183
|
fs.writeFileSync(
|
|
860
1184
|
path.join(sharedDir, 'tsconfig.json'),
|
|
861
|
-
JSON.stringify(
|
|
1185
|
+
JSON.stringify(buildSharedTsConfig(), null, 2)
|
|
862
1186
|
);
|
|
863
1187
|
|
|
864
1188
|
// shared/index.ts (empty re-export file, scaffold will add entries)
|
|
@@ -952,8 +1276,9 @@ tsconfig.json
|
|
|
952
1276
|
!dist/**/*.js
|
|
953
1277
|
`);
|
|
954
1278
|
} else if (backendLanguage === 'python') {
|
|
955
|
-
gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
|
|
1279
|
+
gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
|
|
956
1280
|
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
|
|
1281
|
+
.codegen-venv
|
|
957
1282
|
__pycache__
|
|
958
1283
|
.pytest_cache
|
|
959
1284
|
.mypy_cache
|
|
@@ -973,8 +1298,8 @@ tests
|
|
|
973
1298
|
fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
|
|
974
1299
|
}
|
|
975
1300
|
|
|
976
|
-
function
|
|
977
|
-
|
|
1301
|
+
export function buildTypeScriptFunctionsPackageJson(projectName: string, pm: PackageManager = 'pnpm') {
|
|
1302
|
+
return {
|
|
978
1303
|
name: 'functions',
|
|
979
1304
|
version: '1.0.0',
|
|
980
1305
|
description: 'Azure Functions backend',
|
|
@@ -989,13 +1314,17 @@ function createTypeScriptFunctionsProject(projectDir: string, functionsDir: stri
|
|
|
989
1314
|
'@azure/cosmos': '^4.0.0',
|
|
990
1315
|
'@azure/identity': '^4.0.0',
|
|
991
1316
|
zod: '>=3.25.0',
|
|
992
|
-
[`@${
|
|
1317
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
|
|
993
1318
|
},
|
|
994
1319
|
devDependencies: {
|
|
995
1320
|
'@types/node': '^20.0.0',
|
|
996
1321
|
typescript: '^5.0.0',
|
|
997
1322
|
},
|
|
998
1323
|
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function createTypeScriptFunctionsProject(projectDir: string, functionsDir: string, pm: PackageManager): void {
|
|
1327
|
+
const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
|
|
999
1328
|
fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
|
|
1000
1329
|
|
|
1001
1330
|
const sharedPkgName = `@${path.basename(projectDir)}/shared`;
|
|
@@ -1068,7 +1397,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
1068
1397
|
|
|
1069
1398
|
app.http('greet', {
|
|
1070
1399
|
methods: ['GET', 'POST'],
|
|
1071
|
-
authLevel: '
|
|
1400
|
+
authLevel: 'function',
|
|
1072
1401
|
handler: greet
|
|
1073
1402
|
});
|
|
1074
1403
|
`);
|
|
@@ -1080,6 +1409,11 @@ function createCSharpFunctionsProject(projectDir: string, functionsDir: string):
|
|
|
1080
1409
|
const csprojName = `${projectPascal}.Functions.csproj`;
|
|
1081
1410
|
|
|
1082
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
|
+
);
|
|
1083
1417
|
|
|
1084
1418
|
fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
|
|
1085
1419
|
|
|
@@ -1095,7 +1429,7 @@ public sealed class GreetFunction
|
|
|
1095
1429
|
{
|
|
1096
1430
|
[Function("greet")]
|
|
1097
1431
|
public async Task<HttpResponseData> Run(
|
|
1098
|
-
[HttpTrigger(AuthorizationLevel.
|
|
1432
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
|
|
1099
1433
|
{
|
|
1100
1434
|
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
1101
1435
|
var name = "SwallowKit";
|
|
@@ -1122,11 +1456,16 @@ public sealed class GreetFunction
|
|
|
1122
1456
|
|
|
1123
1457
|
function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
|
|
1124
1458
|
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1459
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1125
1460
|
|
|
1126
1461
|
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
1127
1462
|
azure-cosmos>=4.9.0
|
|
1128
1463
|
azure-identity>=1.19.0
|
|
1129
1464
|
`);
|
|
1465
|
+
fs.writeFileSync(
|
|
1466
|
+
path.join(functionsDir, 'requirements.codegen.txt'),
|
|
1467
|
+
buildPythonCodegenRequirementsSource()
|
|
1468
|
+
);
|
|
1130
1469
|
|
|
1131
1470
|
const blueprintsDir = path.join(functionsDir, 'blueprints');
|
|
1132
1471
|
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
@@ -1158,13 +1497,29 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
1158
1497
|
|
|
1159
1498
|
from blueprints.greet import bp as greet_bp
|
|
1160
1499
|
|
|
1161
|
-
app = func.FunctionApp(http_auth_level=func.AuthLevel.
|
|
1500
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
|
1162
1501
|
|
|
1163
1502
|
app.register_blueprint(greet_bp)
|
|
1164
1503
|
# SwallowKit scaffold registrations
|
|
1165
1504
|
`);
|
|
1166
1505
|
}
|
|
1167
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
|
+
|
|
1168
1523
|
async function createBffApiRoute(projectDir: string) {
|
|
1169
1524
|
console.log('📦 Creating BFF API route...\n');
|
|
1170
1525
|
|
|
@@ -1335,7 +1690,7 @@ export default function Home() {
|
|
|
1335
1690
|
Create your first model with Zod and generate CRUD operations automatically.
|
|
1336
1691
|
</p>
|
|
1337
1692
|
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
1338
|
-
${pmCmd.
|
|
1693
|
+
${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
|
|
1339
1694
|
</code>
|
|
1340
1695
|
</div>
|
|
1341
1696
|
</section>
|
|
@@ -1395,17 +1750,18 @@ function createReadme(
|
|
|
1395
1750
|
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1396
1751
|
const schemaBridgeDescription = backendLanguage === 'typescript'
|
|
1397
1752
|
? 'Zod (shared between frontend and backend)'
|
|
1398
|
-
: `Zod + OpenAPI
|
|
1753
|
+
: `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
|
|
1399
1754
|
const functionsTree = backendLanguage === 'typescript'
|
|
1400
1755
|
? `│ └── src/\n│ └── greet.ts # Sample function`
|
|
1401
1756
|
: backendLanguage === 'csharp'
|
|
1402
|
-
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ #
|
|
1403
|
-
: `│ ├── 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`;
|
|
1404
1759
|
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1405
1760
|
? '- Azure Functions CRUD endpoints'
|
|
1406
|
-
: `- 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);
|
|
1407
1763
|
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1408
|
-
? `\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`
|
|
1409
1765
|
: '';
|
|
1410
1766
|
|
|
1411
1767
|
const readme = `# ${projectName}
|
|
@@ -1435,7 +1791,7 @@ This project was initialized with the following settings:
|
|
|
1435
1791
|
|
|
1436
1792
|
Before you begin, ensure you have the following installed:
|
|
1437
1793
|
|
|
1438
|
-
1. **Node.js
|
|
1794
|
+
1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1439
1795
|
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1440
1796
|
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1441
1797
|
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
@@ -1443,7 +1799,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
|
|
|
1443
1799
|
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1444
1800
|
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1445
1801
|
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1446
|
-
${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
|
|
1447
1803
|
|
|
1448
1804
|
## 📁 Project Structure
|
|
1449
1805
|
|
|
@@ -1469,7 +1825,7 @@ ${functionsTree}
|
|
|
1469
1825
|
Define your data model with Zod schema:
|
|
1470
1826
|
|
|
1471
1827
|
\`\`\`bash
|
|
1472
|
-
${pmCmd.
|
|
1828
|
+
${pmCmd.exec} swallowkit create-model <model-name>
|
|
1473
1829
|
\`\`\`
|
|
1474
1830
|
|
|
1475
1831
|
This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
|
|
@@ -1479,7 +1835,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
|
|
|
1479
1835
|
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1480
1836
|
|
|
1481
1837
|
\`\`\`bash
|
|
1482
|
-
${pmCmd.
|
|
1838
|
+
${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
|
|
1483
1839
|
\`\`\`
|
|
1484
1840
|
|
|
1485
1841
|
This generates:
|
|
@@ -1491,7 +1847,7 @@ ${backendScaffoldNote}
|
|
|
1491
1847
|
### 3. Start Development Servers
|
|
1492
1848
|
|
|
1493
1849
|
\`\`\`bash
|
|
1494
|
-
${pmCmd.
|
|
1850
|
+
${pmCmd.exec} swallowkit dev
|
|
1495
1851
|
\`\`\`
|
|
1496
1852
|
|
|
1497
1853
|
This starts:
|
|
@@ -1509,7 +1865,7 @@ ${pythonLocalDevNote}
|
|
|
1509
1865
|
Create all required Azure resources using Bicep:
|
|
1510
1866
|
|
|
1511
1867
|
\`\`\`bash
|
|
1512
|
-
${pmCmd.
|
|
1868
|
+
${pmCmd.exec} swallowkit provision --resource-group <rg-name>
|
|
1513
1869
|
\`\`\`
|
|
1514
1870
|
|
|
1515
1871
|
This creates:
|
|
@@ -1565,10 +1921,10 @@ func azure functionapp publish func-${projectName}
|
|
|
1565
1921
|
|
|
1566
1922
|
## 🔧 Available Commands
|
|
1567
1923
|
|
|
1568
|
-
- \`${pmCmd.
|
|
1569
|
-
- \`${pmCmd.
|
|
1570
|
-
- \`${pmCmd.
|
|
1571
|
-
- \`${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
|
|
1572
1928
|
${azureConfig.vnetOption !== 'none' ? `
|
|
1573
1929
|
## 🔒 Network Security (VNet Configuration)
|
|
1574
1930
|
|
|
@@ -1617,448 +1973,79 @@ This project was generated by SwallowKit. If you encounter any issues or have su
|
|
|
1617
1973
|
console.log('✅ README.md created\n');
|
|
1618
1974
|
}
|
|
1619
1975
|
|
|
1620
|
-
function createAiAgentFiles(
|
|
1976
|
+
function createAiAgentFiles(
|
|
1977
|
+
projectDir: string,
|
|
1978
|
+
projectName: string,
|
|
1979
|
+
backendLanguage: BackendLanguage,
|
|
1980
|
+
pm: PackageManager
|
|
1981
|
+
) {
|
|
1621
1982
|
console.log('🤖 Creating AI agent instruction files...\n');
|
|
1622
|
-
const
|
|
1623
|
-
const functionsStructureLine = backendLanguage === 'typescript'
|
|
1624
|
-
? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
|
|
1625
|
-
: backendLanguage === 'csharp'
|
|
1626
|
-
? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived C# schema assets`
|
|
1627
|
-
: `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived Python schema assets`;
|
|
1628
|
-
const backendSchemaNote = backendLanguage === 'typescript'
|
|
1629
|
-
? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
|
|
1630
|
-
: `- 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.`;
|
|
1631
|
-
const backendRulesNote = backendLanguage === 'typescript'
|
|
1632
|
-
? `- 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.`
|
|
1633
|
-
: `- 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\`.`;
|
|
1983
|
+
const runCmd = getCommands(pm).exec;
|
|
1634
1984
|
|
|
1635
|
-
|
|
1985
|
+
writeAgentInstructionFiles(projectDir, {
|
|
1986
|
+
projectName,
|
|
1987
|
+
backendLanguage,
|
|
1988
|
+
packageManager: pm,
|
|
1989
|
+
});
|
|
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)');
|
|
1636
1994
|
|
|
1637
|
-
|
|
1995
|
+
fs.writeFileSync(
|
|
1996
|
+
path.join(projectDir, '.mcp.json'),
|
|
1997
|
+
buildSwallowKitMcpProjectConfigSource(pm)
|
|
1998
|
+
);
|
|
1999
|
+
console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
|
|
1638
2000
|
|
|
1639
|
-
|
|
1640
|
-
|
|
2001
|
+
writeWorkflowDocs(projectDir, runCmd);
|
|
2002
|
+
console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
|
|
1641
2003
|
|
|
1642
|
-
|
|
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
|
+
}
|
|
1643
2009
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
\`\`\`
|
|
1647
|
-
Frontend (React / Next.js App Router)
|
|
1648
|
-
↓ fetch('/api/{model}', ...)
|
|
1649
|
-
BFF Layer (Next.js API Routes)
|
|
1650
|
-
↓ HTTP → Azure Functions
|
|
1651
|
-
Backend (Azure Functions)
|
|
1652
|
-
↓
|
|
1653
|
-
Azure Cosmos DB (Document Database)
|
|
1654
|
-
\`\`\`
|
|
1655
|
-
|
|
1656
|
-
### Project Structure
|
|
1657
|
-
|
|
1658
|
-
\`\`\`
|
|
1659
|
-
${projectName}/
|
|
1660
|
-
├── app/ # Next.js App Router
|
|
1661
|
-
│ ├── api/ # BFF API routes (proxy to Azure Functions)
|
|
1662
|
-
│ └── {model}/ # UI pages per model (list, detail, create, edit)
|
|
1663
|
-
├── functions/ # Azure Functions (backend)
|
|
1664
|
-
${functionsStructureLine}
|
|
1665
|
-
├── shared/ # Shared workspace package
|
|
1666
|
-
│ ├── models/ # Zod schema definitions (single source of truth)
|
|
1667
|
-
│ └── index.ts # Re-exports all models
|
|
1668
|
-
├── lib/
|
|
1669
|
-
│ └── api/ # API client utilities (backend.ts, call-function.ts)
|
|
1670
|
-
├── components/ # Shared React components
|
|
1671
|
-
├── infra/ # Bicep infrastructure-as-code files
|
|
1672
|
-
│ ├── main.bicep
|
|
1673
|
-
│ └── modules/
|
|
1674
|
-
└── .github/workflows/ # CI/CD workflows (if configured)
|
|
1675
|
-
\`\`\`
|
|
1676
|
-
|
|
1677
|
-
## Critical Design Principles
|
|
1678
|
-
|
|
1679
|
-
### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
|
|
1680
|
-
|
|
1681
|
-
- \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
|
|
1682
|
-
- **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
|
|
1683
|
-
- The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
|
|
1684
|
-
- Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
|
|
1685
|
-
|
|
1686
|
-
Example BFF route pattern:
|
|
1687
|
-
|
|
1688
|
-
\`\`\`typescript
|
|
1689
|
-
// app/api/{model}/route.ts
|
|
1690
|
-
import { callFunction } from '@/lib/api/call-function';
|
|
1691
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1692
|
-
import { z } from 'zod/v4';
|
|
1693
|
-
|
|
1694
|
-
export async function GET() {
|
|
1695
|
-
return callFunction({
|
|
1696
|
-
method: 'GET',
|
|
1697
|
-
path: '/api/{model}',
|
|
1698
|
-
responseSchema: z.array(ModelSchema),
|
|
1699
|
-
});
|
|
2010
|
+
console.log('\n✅ AI agent files created\n');
|
|
1700
2011
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
const body = await request.json();
|
|
1704
|
-
return callFunction({
|
|
1705
|
-
method: 'POST',
|
|
1706
|
-
path: '/api/{model}',
|
|
1707
|
-
body,
|
|
1708
|
-
inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
|
|
1709
|
-
responseSchema: ModelSchema,
|
|
1710
|
-
successStatus: 201,
|
|
1711
|
-
});
|
|
2012
|
+
export function buildFunctionsHostKeyBicepExpression(): string {
|
|
2013
|
+
return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
|
|
1712
2014
|
}
|
|
1713
|
-
\`\`\`
|
|
1714
|
-
|
|
1715
|
-
### 2. Zod Schemas Are the Single Source of Truth
|
|
1716
|
-
|
|
1717
|
-
- All data models are defined **once** as Zod schemas in \`shared/models/\`.
|
|
1718
|
-
- TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
|
|
1719
|
-
- ${backendSchemaNote}
|
|
1720
|
-
|
|
1721
|
-
Model definition pattern:
|
|
1722
|
-
|
|
1723
|
-
\`\`\`typescript
|
|
1724
|
-
// shared/models/{model}.ts
|
|
1725
|
-
import { z } from 'zod/v4';
|
|
1726
|
-
|
|
1727
|
-
export const Todo = z.object({
|
|
1728
|
-
id: z.string(),
|
|
1729
|
-
name: z.string().min(1),
|
|
1730
|
-
// ... your fields
|
|
1731
|
-
createdAt: z.string().optional(),
|
|
1732
|
-
updatedAt: z.string().optional(),
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1736
|
-
export const displayName = 'Todo';
|
|
1737
|
-
\`\`\`
|
|
1738
|
-
|
|
1739
|
-
Key rules:
|
|
1740
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1741
|
-
- \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
|
|
1742
|
-
- Always re-export models from \`shared/index.ts\`.
|
|
1743
|
-
|
|
1744
|
-
### 3. Azure Functions Own All Business Logic and Data Access
|
|
1745
|
-
|
|
1746
|
-
- ${backendRulesNote}
|
|
1747
|
-
|
|
1748
|
-
${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.`}
|
|
1749
|
-
|
|
1750
|
-
${backendLanguage === 'typescript' ? `\`\`\`typescript
|
|
1751
|
-
// functions/src/{model}.ts
|
|
1752
|
-
import { app } from '@azure/functions';
|
|
1753
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1754
|
-
|
|
1755
|
-
const containerName = 'Models'; // PascalCase + 's'
|
|
1756
|
-
|
|
1757
|
-
app.http('{model}-get-all', {
|
|
1758
|
-
methods: ['GET'],
|
|
1759
|
-
route: '{model}',
|
|
1760
|
-
authLevel: 'anonymous',
|
|
1761
|
-
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
1762
|
-
handler: async (request, context) => {
|
|
1763
|
-
const documents = context.extraInputs.get('cosmosInput');
|
|
1764
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
1765
|
-
return { status: 200, jsonBody: validated };
|
|
1766
|
-
},
|
|
1767
|
-
});
|
|
1768
|
-
\`\`\`` : ''}
|
|
1769
|
-
|
|
1770
|
-
## Naming Conventions
|
|
1771
|
-
|
|
1772
|
-
| Item | Convention | Example |
|
|
1773
|
-
|------|-----------|---------|
|
|
1774
|
-
| Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
|
|
1775
|
-
| Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
|
|
1776
|
-
| Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
|
|
1777
|
-
| Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
|
|
1778
|
-
| API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
|
|
1779
|
-
| BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
|
|
1780
|
-
| BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
|
|
1781
|
-
| UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
|
|
1782
|
-
| React component | PascalCase | \`TodoForm.tsx\` |
|
|
1783
|
-
| Cosmos DB container | PascalCase + 's' | \`Todos\` |
|
|
1784
|
-
| Cosmos DB partition key | \`/id\` | Always \`/id\` |
|
|
1785
|
-
| Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
|
|
1786
|
-
|
|
1787
|
-
## Adding New Models (SwallowKit CLI Skills)
|
|
1788
|
-
|
|
1789
|
-
Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
|
|
1790
|
-
|
|
1791
|
-
### Skill: Create a new data model
|
|
1792
|
-
|
|
1793
|
-
\`\`\`bash
|
|
1794
|
-
npx swallowkit create-model <name>
|
|
1795
|
-
# Multiple models at once:
|
|
1796
|
-
npx swallowkit create-model user post comment
|
|
1797
|
-
\`\`\`
|
|
1798
|
-
|
|
1799
|
-
Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
|
|
1800
|
-
Edit the generated file to add your domain-specific fields, then run scaffold.
|
|
1801
|
-
|
|
1802
|
-
### Skill: Generate full CRUD from a model
|
|
1803
|
-
|
|
1804
|
-
\`\`\`bash
|
|
1805
|
-
npx swallowkit scaffold shared/models/<name>.ts
|
|
1806
|
-
\`\`\`
|
|
1807
|
-
|
|
1808
|
-
Generates:
|
|
1809
|
-
- Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
|
|
1810
|
-
- BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
|
|
1811
|
-
- UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
|
|
1812
|
-
- Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
|
|
1813
2015
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
npx swallowkit dev
|
|
1818
|
-
\`\`\`
|
|
1819
|
-
|
|
1820
|
-
Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
|
|
1821
|
-
Checks for Cosmos DB Emulator availability.
|
|
1822
|
-
|
|
1823
|
-
### Skill: Provision Azure resources
|
|
1824
|
-
|
|
1825
|
-
\`\`\`bash
|
|
1826
|
-
npx swallowkit provision --resource-group <name> --location <region>
|
|
1827
|
-
\`\`\`
|
|
1828
|
-
|
|
1829
|
-
Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
|
|
1830
|
-
|
|
1831
|
-
### Typical workflow for "add a new feature/model"
|
|
1832
|
-
|
|
1833
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1834
|
-
2. Edit \`shared/models/<name>.ts\` — add fields
|
|
1835
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1836
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1837
|
-
|
|
1838
|
-
## Do NOT
|
|
1839
|
-
|
|
1840
|
-
- **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
|
|
1841
|
-
- **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
|
|
1842
|
-
- **Do not** manually duplicate model definitions across layers. Use the shared package.
|
|
1843
|
-
- **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
|
|
1844
|
-
- **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
|
|
1845
|
-
- **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
|
|
1846
|
-
|
|
1847
|
-
## Technology Stack
|
|
1848
|
-
|
|
1849
|
-
- **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
|
|
1850
|
-
- **BFF**: Next.js API Routes (proxy only)
|
|
1851
|
-
- **Backend**: Azure Functions (${backendLanguageLabel})
|
|
1852
|
-
- **Database**: Azure Cosmos DB (NoSQL)
|
|
1853
|
-
- **Schema**: Zod (shared across all layers via workspace package)
|
|
1854
|
-
- **Infrastructure**: Bicep (IaC)
|
|
1855
|
-
- **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
|
|
1856
|
-
- **Auth**: Azure Managed Identity (no connection strings in production)
|
|
1857
|
-
- **Monitoring**: Application Insights
|
|
1858
|
-
`;
|
|
1859
|
-
|
|
1860
|
-
fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
|
|
1861
|
-
console.log(' ✅ AGENTS.md (Codex / generic agents)');
|
|
1862
|
-
|
|
1863
|
-
// ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
|
|
1864
|
-
|
|
1865
|
-
const claudeMd = `# CLAUDE.md
|
|
1866
|
-
|
|
1867
|
-
This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
|
|
1868
|
-
|
|
1869
|
-
## Quick Reference
|
|
1870
|
-
|
|
1871
|
-
- **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
|
|
1872
|
-
- **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
|
|
1873
|
-
- **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
|
|
1874
|
-
- **Backend language**: ${backendLanguageLabel}
|
|
1875
|
-
- **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
|
|
1876
|
-
|
|
1877
|
-
## SwallowKit CLI Commands
|
|
1878
|
-
|
|
1879
|
-
| Task | Command |
|
|
1880
|
-
|------|---------|
|
|
1881
|
-
| Create model | \`npx swallowkit create-model <name>\` |
|
|
1882
|
-
| Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
|
|
1883
|
-
| Dev servers | \`npx swallowkit dev\` |
|
|
1884
|
-
| Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
|
|
1885
|
-
|
|
1886
|
-
## Workflow: Add a new model
|
|
1887
|
-
|
|
1888
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1889
|
-
2. Edit \`shared/models/<name>.ts\` — add your fields
|
|
1890
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1891
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1892
|
-
`;
|
|
1893
|
-
|
|
1894
|
-
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
|
|
1895
|
-
console.log(' ✅ CLAUDE.md (Claude Code)');
|
|
1896
|
-
|
|
1897
|
-
// ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
|
|
1898
|
-
|
|
1899
|
-
const ghDir = path.join(projectDir, '.github');
|
|
1900
|
-
fs.mkdirSync(ghDir, { recursive: true });
|
|
1901
|
-
|
|
1902
|
-
const copilotInstructions = `# Copilot Instructions
|
|
1903
|
-
|
|
1904
|
-
This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
|
|
1905
|
-
|
|
1906
|
-
## Architecture (3-layer)
|
|
1907
|
-
|
|
1908
|
-
\`\`\`
|
|
1909
|
-
Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
|
|
1910
|
-
\`\`\`
|
|
1911
|
-
|
|
1912
|
-
## Key Rules
|
|
1913
|
-
|
|
1914
|
-
1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
|
|
1915
|
-
2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
|
|
1916
|
-
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/\`.
|
|
1917
|
-
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.
|
|
1918
|
-
|
|
1919
|
-
## Naming
|
|
1920
|
-
|
|
1921
|
-
- Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
|
|
1922
|
-
- Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
|
|
1923
|
-
- Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
|
|
1924
|
-
|
|
1925
|
-
## Managed Fields
|
|
1926
|
-
|
|
1927
|
-
\`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
|
|
1928
|
-
`;
|
|
1929
|
-
|
|
1930
|
-
fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
|
|
1931
|
-
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
|
|
1932
|
-
|
|
1933
|
-
// ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
|
|
1934
|
-
|
|
1935
|
-
const instructionsDir = path.join(ghDir, 'instructions');
|
|
1936
|
-
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
1937
|
-
|
|
1938
|
-
// 4a. shared/models — Zod schema layer
|
|
1939
|
-
const sharedModelsInstructions = `---
|
|
1940
|
-
applyTo: "shared/models/**"
|
|
1941
|
-
---
|
|
1942
|
-
|
|
1943
|
-
# Shared Models — Zod Schema Rules
|
|
1944
|
-
|
|
1945
|
-
Files in this directory are the **single source of truth** for data models across the entire application.
|
|
1946
|
-
|
|
1947
|
-
## Rules
|
|
1948
|
-
|
|
1949
|
-
- Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
|
|
1950
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1951
|
-
\`\`\`typescript
|
|
1952
|
-
export const Todo = z.object({ ... });
|
|
1953
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1954
|
-
\`\`\`
|
|
1955
|
-
- Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
|
|
1956
|
-
- Export a \`displayName\` string constant for UI display.
|
|
1957
|
-
- Re-export every model from \`shared/index.ts\`.
|
|
1958
|
-
- For relationships, use **nested schemas** (import and embed the related schema), not ID references.
|
|
1959
|
-
- After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
|
|
1960
|
-
`;
|
|
1961
|
-
|
|
1962
|
-
fs.writeFileSync(
|
|
1963
|
-
path.join(instructionsDir, 'shared-models.instructions.md'),
|
|
1964
|
-
sharedModelsInstructions
|
|
1965
|
-
);
|
|
1966
|
-
|
|
1967
|
-
// 4b. app/api — BFF layer
|
|
1968
|
-
const bffInstructions = `---
|
|
1969
|
-
applyTo: "app/api/**"
|
|
1970
|
-
---
|
|
1971
|
-
|
|
1972
|
-
# BFF API Routes — Rules
|
|
1973
|
-
|
|
1974
|
-
Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
|
|
1975
|
-
|
|
1976
|
-
## Rules
|
|
2016
|
+
export function buildStaticWebAppConfigBicepSource(): string {
|
|
2017
|
+
return `@description('Static Web App name')
|
|
2018
|
+
param staticWebAppName string
|
|
1977
2019
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
- You may validate input/output with Zod schemas before forwarding.
|
|
1981
|
-
- Import schemas from \`@${projectName}/shared\`.
|
|
2020
|
+
@description('Functions App default hostname for backend API calls')
|
|
2021
|
+
param functionsDefaultHostname string
|
|
1982
2022
|
|
|
1983
|
-
|
|
2023
|
+
@description('Functions App name used to resolve its host key after deployment')
|
|
2024
|
+
param functionsAppName string
|
|
1984
2025
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1988
|
-
import { z } from 'zod/v4';
|
|
2026
|
+
@description('Application Insights connection string for SWA')
|
|
2027
|
+
param appInsightsConnectionString string
|
|
1989
2028
|
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
method: 'GET',
|
|
1993
|
-
path: '/api/{model}',
|
|
1994
|
-
responseSchema: z.array(ModelSchema),
|
|
1995
|
-
});
|
|
2029
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2030
|
+
name: staticWebAppName
|
|
1996
2031
|
}
|
|
1997
|
-
\`\`\`
|
|
1998
|
-
`;
|
|
1999
|
-
|
|
2000
|
-
fs.writeFileSync(
|
|
2001
|
-
path.join(instructionsDir, 'bff-routes.instructions.md'),
|
|
2002
|
-
bffInstructions
|
|
2003
|
-
);
|
|
2004
|
-
|
|
2005
|
-
// 4c. functions — Azure Functions backend layer
|
|
2006
|
-
const functionsInstructions = `---
|
|
2007
|
-
applyTo: "functions/**"
|
|
2008
|
-
---
|
|
2009
|
-
|
|
2010
|
-
# Azure Functions — Backend Rules
|
|
2011
2032
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
## Handler Pattern
|
|
2023
|
-
|
|
2024
|
-
\`\`\`typescript
|
|
2025
|
-
import { app } from '@azure/functions';
|
|
2026
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
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
|
+
}
|
|
2027
2043
|
|
|
2028
|
-
|
|
2029
|
-
methods: ['GET'],
|
|
2030
|
-
route: '{model}',
|
|
2031
|
-
authLevel: 'anonymous',
|
|
2032
|
-
extraInputs: [cosmosInput],
|
|
2033
|
-
handler: async (request, context) => {
|
|
2034
|
-
const documents = context.extraInputs.get(cosmosInput);
|
|
2035
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
2036
|
-
return { status: 200, jsonBody: validated };
|
|
2037
|
-
},
|
|
2038
|
-
});
|
|
2039
|
-
\`\`\`
|
|
2044
|
+
output configName string = staticWebAppConfig.name
|
|
2040
2045
|
`;
|
|
2041
|
-
|
|
2042
|
-
fs.writeFileSync(
|
|
2043
|
-
path.join(instructionsDir, 'azure-functions.instructions.md'),
|
|
2044
|
-
functionsInstructions
|
|
2045
|
-
);
|
|
2046
|
-
|
|
2047
|
-
console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
|
|
2048
|
-
console.log(' - shared-models.instructions.md');
|
|
2049
|
-
console.log(' - bff-routes.instructions.md');
|
|
2050
|
-
console.log(' - azure-functions.instructions.md');
|
|
2051
|
-
|
|
2052
|
-
console.log('\n✅ AI agent files created\n');
|
|
2053
|
-
console.log(' Supported agents:');
|
|
2054
|
-
console.log(' - OpenAI Codex → AGENTS.md');
|
|
2055
|
-
console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
|
|
2056
|
-
console.log(' - GitHub Copilot → .github/copilot-instructions.md');
|
|
2057
|
-
console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
|
|
2058
|
-
console.log('');
|
|
2059
2046
|
}
|
|
2060
2047
|
|
|
2061
|
-
async function createInfrastructure(
|
|
2048
|
+
export async function createInfrastructure(
|
|
2062
2049
|
projectDir: string,
|
|
2063
2050
|
projectName: string,
|
|
2064
2051
|
azureConfig: AzureConfig,
|
|
@@ -2072,6 +2059,7 @@ async function createInfrastructure(
|
|
|
2072
2059
|
|
|
2073
2060
|
const enableVNet = azureConfig.vnetOption !== 'none';
|
|
2074
2061
|
const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
|
|
2062
|
+
const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
|
|
2075
2063
|
|
|
2076
2064
|
// main.bicep
|
|
2077
2065
|
const mainBicep = `targetScope = 'resourceGroup'
|
|
@@ -2085,6 +2073,13 @@ param location string = resourceGroup().location
|
|
|
2085
2073
|
@description('Location for Static Web App (must be explicitly provided)')
|
|
2086
2074
|
param swaLocation string
|
|
2087
2075
|
|
|
2076
|
+
@description('Static Web Apps hosting plan')
|
|
2077
|
+
@allowed([
|
|
2078
|
+
'Free'
|
|
2079
|
+
'Standard'
|
|
2080
|
+
])
|
|
2081
|
+
param staticWebAppSku string = '${staticWebAppSku}'
|
|
2082
|
+
|
|
2088
2083
|
@description('Cosmos DB mode')
|
|
2089
2084
|
@allowed(['freetier', 'serverless'])
|
|
2090
2085
|
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
@@ -2127,7 +2122,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
|
2127
2122
|
params: {
|
|
2128
2123
|
name: 'swa-\${projectName}'
|
|
2129
2124
|
location: swaLocation
|
|
2130
|
-
sku:
|
|
2125
|
+
sku: staticWebAppSku
|
|
2131
2126
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2132
2127
|
}
|
|
2133
2128
|
}
|
|
@@ -2168,16 +2163,11 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
|
|
|
2168
2163
|
params: {
|
|
2169
2164
|
name: 'pe-cosmos-\${projectName}'
|
|
2170
2165
|
location: location
|
|
2171
|
-
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2172
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2173
|
-
subnetId: vnet
|
|
2174
|
-
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
|
|
2175
2170
|
}
|
|
2176
|
-
dependsOn: [
|
|
2177
|
-
cosmosDbFreeTier
|
|
2178
|
-
cosmosDbServerless
|
|
2179
|
-
vnet
|
|
2180
|
-
]
|
|
2181
2171
|
}
|
|
2182
2172
|
|
|
2183
2173
|
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
@@ -2189,16 +2179,14 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
2189
2179
|
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
2190
2180
|
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
2191
2181
|
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
2192
|
-
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2193
|
-
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2182
|
+
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
2183
|
+
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
2194
2184
|
functionsRuntimeName: '${functionsRuntime.name}'
|
|
2195
2185
|
functionsRuntimeVersion: '${functionsRuntime.version}'
|
|
2196
2186
|
enableVNet: enableVNet
|
|
2197
|
-
vnetSubnetId: enableVNet ? vnet
|
|
2187
|
+
vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
|
|
2198
2188
|
}
|
|
2199
2189
|
dependsOn: [
|
|
2200
|
-
cosmosDbFreeTier
|
|
2201
|
-
cosmosDbServerless
|
|
2202
2190
|
cosmosPrivateEndpoint
|
|
2203
2191
|
]
|
|
2204
2192
|
}
|
|
@@ -2207,23 +2195,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
2207
2195
|
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
2208
2196
|
name: 'cosmosDbRoleAssignment'
|
|
2209
2197
|
params: {
|
|
2210
|
-
cosmosAccountName: cosmosDbFreeTier
|
|
2198
|
+
cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
|
|
2211
2199
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2212
2200
|
}
|
|
2213
|
-
dependsOn: [
|
|
2214
|
-
functionsFlex
|
|
2215
|
-
]
|
|
2216
2201
|
}
|
|
2217
2202
|
|
|
2218
2203
|
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
2219
2204
|
name: 'cosmosDbRoleAssignment'
|
|
2220
2205
|
params: {
|
|
2221
|
-
cosmosAccountName: cosmosDbServerless
|
|
2206
|
+
cosmosAccountName: cosmosDbServerless!.outputs.accountName
|
|
2222
2207
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2223
2208
|
}
|
|
2224
|
-
dependsOn: [
|
|
2225
|
-
functionsFlex
|
|
2226
|
-
]
|
|
2227
2209
|
}
|
|
2228
2210
|
|
|
2229
2211
|
// Update SWA config with Functions hostname (after Functions deployment)
|
|
@@ -2232,20 +2214,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
|
2232
2214
|
params: {
|
|
2233
2215
|
staticWebAppName: staticWebApp.outputs.name
|
|
2234
2216
|
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
2217
|
+
functionsAppName: functionsFlex.outputs.name
|
|
2235
2218
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2236
2219
|
}
|
|
2237
|
-
dependsOn: [
|
|
2238
|
-
functionsFlex
|
|
2239
|
-
]
|
|
2240
2220
|
}
|
|
2241
2221
|
|
|
2242
2222
|
output staticWebAppName string = staticWebApp.outputs.name
|
|
2243
2223
|
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
2244
2224
|
output functionsAppName string = functionsFlex.outputs.name
|
|
2245
2225
|
output functionsAppUrl string = functionsFlex.outputs.defaultHostname
|
|
2246
|
-
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2247
|
-
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2248
|
-
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
|
|
2249
2229
|
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
2250
2230
|
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
2251
2231
|
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
@@ -2253,7 +2233,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
|
|
|
2253
2233
|
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
2254
2234
|
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
2255
2235
|
output vnetEnabled bool = enableVNet
|
|
2256
|
-
output vnetName string = enableVNet ? vnet
|
|
2236
|
+
output vnetName string = enableVNet ? vnet!.outputs.name : ''
|
|
2257
2237
|
`;
|
|
2258
2238
|
|
|
2259
2239
|
fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
|
|
@@ -2269,6 +2249,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
|
2269
2249
|
"cosmosDbMode": {
|
|
2270
2250
|
"value": "${azureConfig.cosmosDbMode}"
|
|
2271
2251
|
},
|
|
2252
|
+
"staticWebAppSku": {
|
|
2253
|
+
"value": "${staticWebAppSku}"
|
|
2254
|
+
},
|
|
2272
2255
|
"enableVNet": {
|
|
2273
2256
|
"value": ${enableVNet}
|
|
2274
2257
|
}
|
|
@@ -2325,31 +2308,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
|
2325
2308
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
2326
2309
|
|
|
2327
2310
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
2328
|
-
const staticWebAppConfigBicep =
|
|
2329
|
-
param staticWebAppName string
|
|
2330
|
-
|
|
2331
|
-
@description('Functions App default hostname for backend API calls')
|
|
2332
|
-
param functionsDefaultHostname string
|
|
2333
|
-
|
|
2334
|
-
@description('Application Insights connection string for SWA')
|
|
2335
|
-
param appInsightsConnectionString string
|
|
2336
|
-
|
|
2337
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2338
|
-
name: staticWebAppName
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2342
|
-
parent: staticWebApp
|
|
2343
|
-
name: 'appsettings'
|
|
2344
|
-
properties: {
|
|
2345
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2346
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2347
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
output configName string = staticWebAppConfig.name
|
|
2352
|
-
`;
|
|
2311
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
2353
2312
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
2354
2313
|
|
|
2355
2314
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
@@ -2572,63 +2531,7 @@ output principalId string = functionApp.identity.principalId
|
|
|
2572
2531
|
fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
|
|
2573
2532
|
|
|
2574
2533
|
// modules/cosmosdb-freetier.bicep (Free Tier)
|
|
2575
|
-
const cosmosDbFreeTierBicep =
|
|
2576
|
-
param accountName string
|
|
2577
|
-
|
|
2578
|
-
@description('Database name')
|
|
2579
|
-
param databaseName string
|
|
2580
|
-
|
|
2581
|
-
@description('Location for Cosmos DB')
|
|
2582
|
-
param location string
|
|
2583
|
-
|
|
2584
|
-
@description('Public network access')
|
|
2585
|
-
@allowed(['Enabled', 'Disabled'])
|
|
2586
|
-
param publicNetworkAccess string = 'Enabled'
|
|
2587
|
-
|
|
2588
|
-
// Cosmos DB Account (Free Tier)
|
|
2589
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
2590
|
-
name: accountName
|
|
2591
|
-
location: location
|
|
2592
|
-
kind: 'GlobalDocumentDB'
|
|
2593
|
-
properties: {
|
|
2594
|
-
databaseAccountOfferType: 'Standard'
|
|
2595
|
-
enableAutomaticFailover: false
|
|
2596
|
-
enableFreeTier: true
|
|
2597
|
-
publicNetworkAccess: publicNetworkAccess
|
|
2598
|
-
disableLocalAuth: true
|
|
2599
|
-
consistencyPolicy: {
|
|
2600
|
-
defaultConsistencyLevel: 'Session'
|
|
2601
|
-
}
|
|
2602
|
-
locations: [
|
|
2603
|
-
{
|
|
2604
|
-
locationName: location
|
|
2605
|
-
failoverPriority: 0
|
|
2606
|
-
isZoneRedundant: false
|
|
2607
|
-
}
|
|
2608
|
-
]
|
|
2609
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
// Cosmos DB Database
|
|
2614
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
2615
|
-
parent: cosmosAccount
|
|
2616
|
-
name: databaseName
|
|
2617
|
-
properties: {
|
|
2618
|
-
resource: {
|
|
2619
|
-
id: databaseName
|
|
2620
|
-
}
|
|
2621
|
-
options: {
|
|
2622
|
-
throughput: 1000
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
output id string = cosmosAccount.id
|
|
2628
|
-
output accountName string = cosmosAccount.name
|
|
2629
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
2630
|
-
output databaseName string = database.name
|
|
2631
|
-
`;
|
|
2534
|
+
const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
|
|
2632
2535
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
|
|
2633
2536
|
|
|
2634
2537
|
// modules/cosmosdb-serverless.bicep (Serverless)
|
|
@@ -2721,9 +2624,10 @@ output roleAssignmentId string = roleAssignment.id
|
|
|
2721
2624
|
`;
|
|
2722
2625
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
|
|
2723
2626
|
|
|
2724
|
-
//
|
|
2725
|
-
|
|
2726
|
-
|
|
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
|
|
2727
2631
|
const vnetBicep = `@description('VNet name')
|
|
2728
2632
|
param name string
|
|
2729
2633
|
|
|
@@ -2853,26 +2757,30 @@ output privateDnsZoneId string = privateDnsZone.id
|
|
|
2853
2757
|
`;
|
|
2854
2758
|
fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
|
|
2855
2759
|
|
|
2856
|
-
|
|
2857
|
-
}
|
|
2760
|
+
console.log('✅ VNet module definitions created\n');
|
|
2858
2761
|
|
|
2859
2762
|
console.log('✅ Infrastructure files created\n');
|
|
2860
2763
|
}
|
|
2861
2764
|
|
|
2862
|
-
function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage): string {
|
|
2765
|
+
export function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
|
|
2863
2766
|
const pmCmd = getCommands(pm);
|
|
2864
2767
|
const pnpmSetupStep = getCiSetupStep(pm);
|
|
2768
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2865
2769
|
|
|
2866
2770
|
const commonSetup = ` - uses: actions/checkout@v4
|
|
2867
2771
|
|
|
2772
|
+
${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
|
|
2868
2773
|
- name: Setup Node.js
|
|
2869
2774
|
uses: actions/setup-node@v4
|
|
2870
2775
|
with:
|
|
2871
2776
|
node-version: '22'
|
|
2872
|
-
${
|
|
2777
|
+
${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
|
|
2778
|
+
cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
2779
|
+
`}
|
|
2780
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
2873
2781
|
- name: Install dependencies
|
|
2874
2782
|
run: |
|
|
2875
|
-
${
|
|
2783
|
+
${getCiInstallCommand(pm, projectPm)}
|
|
2876
2784
|
|
|
2877
2785
|
- name: Build shared package
|
|
2878
2786
|
run: |
|
|
@@ -2958,7 +2866,7 @@ jobs:
|
|
|
2958
2866
|
${commonSetup} - name: Setup .NET
|
|
2959
2867
|
uses: actions/setup-dotnet@v4
|
|
2960
2868
|
with:
|
|
2961
|
-
dotnet-version: '
|
|
2869
|
+
dotnet-version: '10.0.x'
|
|
2962
2870
|
|
|
2963
2871
|
- name: Publish Functions
|
|
2964
2872
|
run: |
|
|
@@ -3019,16 +2927,18 @@ ${commonSetup} - name: Setup Python
|
|
|
3019
2927
|
`;
|
|
3020
2928
|
}
|
|
3021
2929
|
|
|
3022
|
-
function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage): string {
|
|
2930
|
+
export function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
|
|
3023
2931
|
const pmCmd = getCommands(pm);
|
|
3024
2932
|
const azPipelinesSetup = getAzurePipelinesSetup(pm);
|
|
2933
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
3025
2934
|
const commonSetup = ` - task: NodeTool@0
|
|
3026
2935
|
inputs:
|
|
3027
2936
|
versionSpec: '22.x'
|
|
3028
2937
|
displayName: 'Install Node.js'
|
|
3029
2938
|
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2939
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
3030
2940
|
- script: |
|
|
3031
|
-
${
|
|
2941
|
+
${getCiInstallCommand(pm, projectPm)}
|
|
3032
2942
|
displayName: 'Install workspace dependencies'
|
|
3033
2943
|
|
|
3034
2944
|
- script: |
|
|
@@ -3134,7 +3044,7 @@ variables:
|
|
|
3134
3044
|
steps:
|
|
3135
3045
|
${commonSetup} - task: UseDotNet@2
|
|
3136
3046
|
inputs:
|
|
3137
|
-
version: '
|
|
3047
|
+
version: '10.0.x'
|
|
3138
3048
|
displayName: 'Install .NET SDK'
|
|
3139
3049
|
|
|
3140
3050
|
- script: |
|
|
@@ -3216,6 +3126,195 @@ ${commonSetup} - task: UsePythonVersion@0
|
|
|
3216
3126
|
`;
|
|
3217
3127
|
}
|
|
3218
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
|
+
|
|
3219
3318
|
async function createGitHubActionsWorkflows(
|
|
3220
3319
|
projectDir: string,
|
|
3221
3320
|
azureConfig: AzureConfig,
|
|
@@ -3223,13 +3322,27 @@ async function createGitHubActionsWorkflows(
|
|
|
3223
3322
|
backendLanguage: BackendLanguage
|
|
3224
3323
|
) {
|
|
3225
3324
|
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
3226
|
-
|
|
3227
|
-
const pmCmd = getCommands(pm);
|
|
3228
3325
|
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
3229
3326
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
3230
3327
|
|
|
3231
|
-
|
|
3232
|
-
|
|
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
|
|
3233
3346
|
|
|
3234
3347
|
on:
|
|
3235
3348
|
push:
|
|
@@ -3242,6 +3355,7 @@ on:
|
|
|
3242
3355
|
- 'shared/**'
|
|
3243
3356
|
- 'public/**'
|
|
3244
3357
|
- 'package.json'
|
|
3358
|
+
- '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3245
3359
|
- 'next.config.js'
|
|
3246
3360
|
- 'next.config.ts'
|
|
3247
3361
|
workflow_dispatch:
|
|
@@ -3255,6 +3369,7 @@ on:
|
|
|
3255
3369
|
- 'shared/**'
|
|
3256
3370
|
- 'public/**'
|
|
3257
3371
|
- 'package.json'
|
|
3372
|
+
- '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3258
3373
|
- 'next.config.js'
|
|
3259
3374
|
- 'next.config.ts'
|
|
3260
3375
|
|
|
@@ -3267,6 +3382,22 @@ jobs:
|
|
|
3267
3382
|
- uses: actions/checkout@v4
|
|
3268
3383
|
with:
|
|
3269
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)}
|
|
3270
3401
|
|
|
3271
3402
|
- name: Deploy to Azure Static Web Apps
|
|
3272
3403
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
@@ -3278,93 +3409,27 @@ jobs:
|
|
|
3278
3409
|
app_location: '/'
|
|
3279
3410
|
api_location: ''
|
|
3280
3411
|
output_location: ''
|
|
3412
|
+
app_build_command: 'npm run build'
|
|
3281
3413
|
env:
|
|
3282
3414
|
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
3283
3415
|
`;
|
|
3284
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
3285
|
-
|
|
3286
|
-
// deploy-functions.yml
|
|
3287
|
-
const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
|
|
3288
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
3289
|
-
|
|
3290
|
-
console.log('✅ GitHub Actions workflows created\n');
|
|
3291
3416
|
}
|
|
3292
3417
|
|
|
3293
3418
|
async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
|
|
3294
3419
|
console.log('📦 Creating Azure Pipelines...\n');
|
|
3295
|
-
|
|
3296
|
-
const
|
|
3297
|
-
const azPipelinesSetup = getAzurePipelinesSetup(pm);
|
|
3420
|
+
|
|
3421
|
+
const pipelinesPm: PackageManager = pm;
|
|
3298
3422
|
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
3299
3423
|
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
3300
3424
|
|
|
3301
3425
|
// swa.yml
|
|
3302
|
-
const swaPipeline =
|
|
3303
|
-
branches:
|
|
3304
|
-
include:
|
|
3305
|
-
- main
|
|
3306
|
-
paths:
|
|
3307
|
-
include:
|
|
3308
|
-
- app/**
|
|
3309
|
-
- components/**
|
|
3310
|
-
- lib/**
|
|
3311
|
-
- shared/**
|
|
3312
|
-
- public/**
|
|
3313
|
-
- package.json
|
|
3314
|
-
- next.config.js
|
|
3315
|
-
|
|
3316
|
-
pr:
|
|
3317
|
-
branches:
|
|
3318
|
-
include:
|
|
3319
|
-
- main
|
|
3320
|
-
paths:
|
|
3321
|
-
include:
|
|
3322
|
-
- app/**
|
|
3323
|
-
- components/**
|
|
3324
|
-
- lib/**
|
|
3325
|
-
- shared/**
|
|
3326
|
-
- public/**
|
|
3327
|
-
- package.json
|
|
3328
|
-
- next.config.js
|
|
3329
|
-
|
|
3330
|
-
pool:
|
|
3331
|
-
vmImage: 'ubuntu-latest'
|
|
3332
|
-
|
|
3333
|
-
variables:
|
|
3334
|
-
- group: azure-deployment
|
|
3335
|
-
|
|
3336
|
-
steps:
|
|
3337
|
-
- task: NodeTool@0
|
|
3338
|
-
inputs:
|
|
3339
|
-
versionSpec: '22.x'
|
|
3340
|
-
displayName: 'Install Node.js'
|
|
3341
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
3342
|
-
- script: |
|
|
3343
|
-
${pmCmd.ci}
|
|
3344
|
-
displayName: 'Install dependencies'
|
|
3345
|
-
|
|
3346
|
-
- script: |
|
|
3347
|
-
${pmCmd.run} build
|
|
3348
|
-
env:
|
|
3349
|
-
NODE_ENV: production
|
|
3350
|
-
displayName: 'Build Next.js app'
|
|
3351
|
-
|
|
3352
|
-
- task: AzureStaticWebApp@0
|
|
3353
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3354
|
-
inputs:
|
|
3355
|
-
app_location: '.'
|
|
3356
|
-
output_location: '.next/standalone'
|
|
3357
|
-
skip_app_build: true
|
|
3358
|
-
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
3359
|
-
displayName: 'Deploy to Azure Static Web Apps'
|
|
3360
|
-
`;
|
|
3426
|
+
const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
|
|
3361
3427
|
fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
|
|
3362
3428
|
|
|
3363
3429
|
// functions.yml
|
|
3364
|
-
const functionsPipeline = getAzureFunctionsPipeline(
|
|
3430
|
+
const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
|
|
3365
3431
|
fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
|
|
3366
3432
|
|
|
3367
3433
|
console.log('✅ Azure Pipelines created\n');
|
|
3368
3434
|
}
|
|
3369
3435
|
|
|
3370
|
-
|