swallowkit 1.0.0-beta.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +87 -181
- package/README.md +87 -238
- package/dist/__tests__/fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +101 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-auth.d.ts +15 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +790 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +164 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +20 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +27 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
- package/dist/cli/commands/dev-seeds.js +188 -6
- package/dist/cli/commands/dev-seeds.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +30 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +495 -129
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +70 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +797 -768
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +408 -189
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +335 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +13 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +151 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +160 -0
- package/dist/core/project/agent-instructions.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +274 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +299 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1774 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +4 -3
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +405 -134
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +229 -78
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +152 -8
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +105 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +7 -11
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +42 -17
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +15 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +513 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +176 -3
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +459 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
- package/src/__tests__/dev-seeds.test.ts +124 -0
- package/src/__tests__/dev.test.ts +254 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +101 -0
- package/src/__tests__/functions-generator.test.ts +137 -1
- package/src/__tests__/init.test.ts +386 -1
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/__tests__/package-manager.test.ts +59 -4
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +287 -24
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +104 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +21 -100
- package/src/cli/commands/dev-seeds.ts +278 -7
- package/src/cli/commands/dev.ts +607 -160
- package/src/cli/commands/init.ts +880 -808
- package/src/cli/commands/scaffold.ts +531 -236
- package/src/cli/index.ts +338 -60
- package/src/core/config.ts +145 -9
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +504 -0
- package/src/core/project/agent-templates/CLAUDE.md +31 -0
- package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
- package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
- package/src/core/project/agent-templates/copilot-instructions.md +27 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/validation.ts +295 -0
- package/src/core/project/workflows.ts +295 -0
- package/src/core/scaffold/auth-generator.ts +1824 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +432 -134
- package/src/core/scaffold/model-parser.ts +244 -90
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +174 -9
- package/src/core/scaffold/ui-generator.ts +191 -36
- package/src/core/verify/index.ts +468 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +137 -0
- package/src/utils/package-manager.ts +41 -19
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -36,15 +36,41 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.getStaticWebAppSku = getStaticWebAppSku;
|
|
40
|
+
exports.buildCosmosDbFreeTierBicepSource = buildCosmosDbFreeTierBicepSource;
|
|
41
|
+
exports.buildSharedTsConfig = buildSharedTsConfig;
|
|
39
42
|
exports.initCommand = initCommand;
|
|
43
|
+
exports.withNpmLockfileSafeManifests = withNpmLockfileSafeManifests;
|
|
44
|
+
exports.parseIgnoredBuilds = parseIgnoredBuilds;
|
|
40
45
|
exports.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
|
|
41
46
|
exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
|
|
42
47
|
exports.buildCSharpFunctionsProjectSource = buildCSharpFunctionsProjectSource;
|
|
48
|
+
exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
|
|
49
|
+
exports.buildGeneratedProjectDependencies = buildGeneratedProjectDependencies;
|
|
50
|
+
exports.buildGeneratedProjectDevDependencies = buildGeneratedProjectDevDependencies;
|
|
51
|
+
exports.buildSwallowKitMcpProjectConfigSource = buildSwallowKitMcpProjectConfigSource;
|
|
52
|
+
exports.buildTypeScriptFunctionsPackageJson = buildTypeScriptFunctionsPackageJson;
|
|
53
|
+
exports.buildFunctionsHostKeyBicepExpression = buildFunctionsHostKeyBicepExpression;
|
|
54
|
+
exports.buildStaticWebAppConfigBicepSource = buildStaticWebAppConfigBicepSource;
|
|
55
|
+
exports.createInfrastructure = createInfrastructure;
|
|
56
|
+
exports.getGitHubFunctionsWorkflow = getGitHubFunctionsWorkflow;
|
|
57
|
+
exports.getAzureFunctionsPipeline = getAzureFunctionsPipeline;
|
|
58
|
+
exports.getSwaNodeVersionCommand = getSwaNodeVersionCommand;
|
|
59
|
+
exports.buildAzureSwaPipeline = buildAzureSwaPipeline;
|
|
60
|
+
exports.buildGitHubSwaWorkflow = buildGitHubSwaWorkflow;
|
|
43
61
|
const fs = __importStar(require("fs"));
|
|
44
62
|
const path = __importStar(require("path"));
|
|
45
63
|
const child_process_1 = require("child_process");
|
|
46
64
|
const prompts_1 = __importDefault(require("prompts"));
|
|
47
65
|
const package_manager_1 = require("../../utils/package-manager");
|
|
66
|
+
const manifest_1 = require("../../core/project/manifest");
|
|
67
|
+
const workflows_1 = require("../../core/project/workflows");
|
|
68
|
+
const agent_instructions_1 = require("../../core/project/agent-instructions");
|
|
69
|
+
const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
|
|
70
|
+
const python_uv_1 = require("../../utils/python-uv");
|
|
71
|
+
function getStaticWebAppSku(plan) {
|
|
72
|
+
return plan === 'free' ? 'Free' : 'Standard';
|
|
73
|
+
}
|
|
48
74
|
const BACKEND_LANGUAGE_CHOICES = [
|
|
49
75
|
{ title: "TypeScript", value: "typescript" },
|
|
50
76
|
{ title: "C#", value: "csharp" },
|
|
@@ -56,6 +82,65 @@ function usesNodeFunctionsProject(backendLanguage) {
|
|
|
56
82
|
function getBackendLanguageLabel(backendLanguage) {
|
|
57
83
|
return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
|
|
58
84
|
}
|
|
85
|
+
function buildCosmosDbFreeTierBicepSource() {
|
|
86
|
+
return `@description('Cosmos DB account name')
|
|
87
|
+
param accountName string
|
|
88
|
+
|
|
89
|
+
@description('Database name')
|
|
90
|
+
param databaseName string
|
|
91
|
+
|
|
92
|
+
@description('Location for Cosmos DB')
|
|
93
|
+
param location string
|
|
94
|
+
|
|
95
|
+
@description('Public network access')
|
|
96
|
+
@allowed(['Enabled', 'Disabled'])
|
|
97
|
+
param publicNetworkAccess string = 'Enabled'
|
|
98
|
+
|
|
99
|
+
// Cosmos DB Account (Free Tier)
|
|
100
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
101
|
+
name: accountName
|
|
102
|
+
location: location
|
|
103
|
+
kind: 'GlobalDocumentDB'
|
|
104
|
+
properties: {
|
|
105
|
+
databaseAccountOfferType: 'Standard'
|
|
106
|
+
enableAutomaticFailover: false
|
|
107
|
+
enableFreeTier: true
|
|
108
|
+
publicNetworkAccess: publicNetworkAccess
|
|
109
|
+
disableLocalAuth: true
|
|
110
|
+
consistencyPolicy: {
|
|
111
|
+
defaultConsistencyLevel: 'Session'
|
|
112
|
+
}
|
|
113
|
+
locations: [
|
|
114
|
+
{
|
|
115
|
+
locationName: location
|
|
116
|
+
failoverPriority: 0
|
|
117
|
+
isZoneRedundant: false
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cosmos DB Database
|
|
125
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
126
|
+
parent: cosmosAccount
|
|
127
|
+
name: databaseName
|
|
128
|
+
properties: {
|
|
129
|
+
resource: {
|
|
130
|
+
id: databaseName
|
|
131
|
+
}
|
|
132
|
+
options: {
|
|
133
|
+
throughput: 1000
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
output id string = cosmosAccount.id
|
|
139
|
+
output accountName string = cosmosAccount.name
|
|
140
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
141
|
+
output databaseName string = database.name
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
59
144
|
function getFunctionsWorkerRuntime(backendLanguage) {
|
|
60
145
|
if (backendLanguage === "csharp") {
|
|
61
146
|
return "dotnet-isolated";
|
|
@@ -67,13 +152,34 @@ function getFunctionsWorkerRuntime(backendLanguage) {
|
|
|
67
152
|
}
|
|
68
153
|
function getFunctionsRuntimeConfig(backendLanguage) {
|
|
69
154
|
if (backendLanguage === "csharp") {
|
|
70
|
-
return { name: "dotnet-isolated", version: "
|
|
155
|
+
return { name: "dotnet-isolated", version: "10.0" };
|
|
71
156
|
}
|
|
72
157
|
if (backendLanguage === "python") {
|
|
73
158
|
return { name: "python", version: "3.11" };
|
|
74
159
|
}
|
|
75
160
|
return { name: "node", version: "22" };
|
|
76
161
|
}
|
|
162
|
+
function buildSharedTsConfig() {
|
|
163
|
+
return {
|
|
164
|
+
compilerOptions: {
|
|
165
|
+
target: "ES2020",
|
|
166
|
+
module: "Node16",
|
|
167
|
+
moduleResolution: "node16",
|
|
168
|
+
lib: ["ES2020"],
|
|
169
|
+
outDir: "dist",
|
|
170
|
+
rootDir: ".",
|
|
171
|
+
declaration: true,
|
|
172
|
+
declarationMap: true,
|
|
173
|
+
sourceMap: true,
|
|
174
|
+
strict: true,
|
|
175
|
+
esModuleInterop: true,
|
|
176
|
+
skipLibCheck: true,
|
|
177
|
+
forceConsistentCasingInFileNames: true,
|
|
178
|
+
},
|
|
179
|
+
include: ["index.ts", "models/**/*"],
|
|
180
|
+
exclude: ["node_modules", "dist"],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
77
183
|
async function promptBackendLanguage() {
|
|
78
184
|
const response = await (0, prompts_1.default)({
|
|
79
185
|
type: "select",
|
|
@@ -98,37 +204,64 @@ async function promptCiCd() {
|
|
|
98
204
|
});
|
|
99
205
|
return response.cicd || 'skip';
|
|
100
206
|
}
|
|
101
|
-
async function promptAzureConfig() {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
207
|
+
async function promptAzureConfig(options) {
|
|
208
|
+
let swaPlan = options.swaPlan;
|
|
209
|
+
if (!swaPlan) {
|
|
210
|
+
const response = await (0, prompts_1.default)({
|
|
211
|
+
type: 'select',
|
|
212
|
+
name: 'plan',
|
|
213
|
+
message: 'Azure Static Web Apps plan:',
|
|
214
|
+
choices: [
|
|
215
|
+
{ title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
|
|
216
|
+
{ title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
|
|
217
|
+
],
|
|
218
|
+
initial: 0
|
|
219
|
+
});
|
|
220
|
+
swaPlan = response.plan || 'standard';
|
|
221
|
+
}
|
|
222
|
+
let cosmosDbMode = options.cosmosDbMode;
|
|
223
|
+
if (!cosmosDbMode) {
|
|
224
|
+
const response = await (0, prompts_1.default)({
|
|
225
|
+
type: 'select',
|
|
226
|
+
name: 'mode',
|
|
227
|
+
message: 'Cosmos DB mode (affects cost):',
|
|
228
|
+
choices: [
|
|
229
|
+
{ title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
|
|
230
|
+
{ title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
|
|
231
|
+
],
|
|
232
|
+
initial: 0
|
|
233
|
+
});
|
|
234
|
+
cosmosDbMode = response.mode || 'freetier';
|
|
235
|
+
}
|
|
236
|
+
let vnetOption = options.vnet;
|
|
237
|
+
if (!vnetOption) {
|
|
238
|
+
const response = await (0, prompts_1.default)({
|
|
239
|
+
type: 'select',
|
|
240
|
+
name: 'vnet',
|
|
241
|
+
message: 'Network security:',
|
|
242
|
+
choices: [
|
|
243
|
+
{ title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
|
|
244
|
+
{ title: 'None - Public endpoints, simplest but least secure', value: 'none' }
|
|
245
|
+
],
|
|
246
|
+
initial: 0
|
|
247
|
+
});
|
|
248
|
+
vnetOption = response.vnet || 'outbound';
|
|
249
|
+
}
|
|
122
250
|
return {
|
|
123
|
-
cosmosDbMode:
|
|
124
|
-
vnetOption:
|
|
251
|
+
cosmosDbMode: cosmosDbMode || 'freetier',
|
|
252
|
+
vnetOption: vnetOption || 'outbound',
|
|
253
|
+
swaPlan: swaPlan || 'standard'
|
|
125
254
|
};
|
|
126
255
|
}
|
|
127
256
|
const VALID_CICD = ['github', 'azure', 'skip'];
|
|
128
257
|
const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
|
|
129
258
|
const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
|
|
130
259
|
const VALID_VNET = ['none', 'outbound'];
|
|
260
|
+
const VALID_SWA_PLAN = ['free', 'standard'];
|
|
131
261
|
function validateInitFlags(options) {
|
|
262
|
+
if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
|
|
263
|
+
throw new Error('Invalid --package-manager: expected npm or pnpm');
|
|
264
|
+
}
|
|
132
265
|
if (options.cicd && !VALID_CICD.includes(options.cicd)) {
|
|
133
266
|
console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
|
|
134
267
|
process.exit(1);
|
|
@@ -145,6 +278,10 @@ function validateInitFlags(options) {
|
|
|
145
278
|
console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
|
|
146
279
|
process.exit(1);
|
|
147
280
|
}
|
|
281
|
+
if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
|
|
282
|
+
console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
148
285
|
}
|
|
149
286
|
async function initCommand(options) {
|
|
150
287
|
// Validate flag values before doing anything
|
|
@@ -152,7 +289,7 @@ async function initCommand(options) {
|
|
|
152
289
|
console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
|
|
153
290
|
console.log(`📋 Template: ${options.template}`);
|
|
154
291
|
// Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
|
|
155
|
-
const pm = (0, package_manager_1.detectFromUserAgent)();
|
|
292
|
+
const pm = options.packageManager || (0, package_manager_1.detectFromUserAgent)();
|
|
156
293
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
157
294
|
console.log(`📦 Package manager: ${pm}`);
|
|
158
295
|
const projectDir = path.join(process.cwd(), options.name);
|
|
@@ -162,12 +299,30 @@ async function initCommand(options) {
|
|
|
162
299
|
console.error(`❌ Directory "${options.name}" already exists.`);
|
|
163
300
|
process.exit(1);
|
|
164
301
|
}
|
|
302
|
+
// 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
|
|
303
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
|
|
304
|
+
if (!interactive) {
|
|
305
|
+
const missing = [];
|
|
306
|
+
if (!options.cicd)
|
|
307
|
+
missing.push('--cicd <github|azure|skip>');
|
|
308
|
+
if (!options.backendLanguage)
|
|
309
|
+
missing.push('--backend-language <typescript|csharp|python>');
|
|
310
|
+
if (!options.cosmosDbMode)
|
|
311
|
+
missing.push('--cosmos-db-mode <freetier|serverless>');
|
|
312
|
+
if (!options.vnet)
|
|
313
|
+
missing.push('--vnet <outbound|none>');
|
|
314
|
+
if (!options.swaPlan)
|
|
315
|
+
missing.push('--swa-plan <free|standard>');
|
|
316
|
+
if (missing.length > 0) {
|
|
317
|
+
console.error(`❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
|
|
318
|
+
missing.map((flag) => ` ${flag}`).join('\n'));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
165
322
|
// Use flag values if provided, otherwise prompt interactively
|
|
166
323
|
const cicdProvider = options.cicd || await promptCiCd();
|
|
167
324
|
const backendLanguage = options.backendLanguage || await promptBackendLanguage();
|
|
168
|
-
const azureConfig = (options
|
|
169
|
-
? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
|
|
170
|
-
: await promptAzureConfig();
|
|
325
|
+
const azureConfig = await promptAzureConfig(options);
|
|
171
326
|
// Create Next.js project with create-next-app
|
|
172
327
|
await createNextJsProject(options.name, pm);
|
|
173
328
|
// Upgrade Next.js to specified version (or latest) to avoid cached old versions
|
|
@@ -183,6 +338,7 @@ async function initCommand(options) {
|
|
|
183
338
|
else if (cicdProvider === 'azure') {
|
|
184
339
|
await createAzurePipelines(projectDir, pm, backendLanguage);
|
|
185
340
|
}
|
|
341
|
+
await (0, manifest_1.syncProjectManifest)(projectDir);
|
|
186
342
|
// Initialize Git repository and create initial commit
|
|
187
343
|
try {
|
|
188
344
|
// Try git init with -b main (Git 2.28+), fallback to git init
|
|
@@ -226,11 +382,11 @@ async function initCommand(options) {
|
|
|
226
382
|
console.log(`\n✅ Project "${options.name}" created successfully!`);
|
|
227
383
|
console.log("\n📝 Next steps:");
|
|
228
384
|
console.log(` cd ${options.name}`);
|
|
229
|
-
console.log(` ${pmCmd.
|
|
230
|
-
console.log(` ${pmCmd.
|
|
231
|
-
console.log(` ${pmCmd.
|
|
385
|
+
console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
|
|
386
|
+
console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
|
|
387
|
+
console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
|
|
232
388
|
console.log("\n🚀 Deploy to Azure:");
|
|
233
|
-
console.log(` ${pmCmd.
|
|
389
|
+
console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
|
|
234
390
|
if (cicdProvider !== 'skip') {
|
|
235
391
|
console.log(" Configure CI/CD secrets and push to repository");
|
|
236
392
|
}
|
|
@@ -252,7 +408,7 @@ async function createNextJsProject(projectName, pm) {
|
|
|
252
408
|
// for npm use "npx create-next-app@latest ..."
|
|
253
409
|
const baseArgs = pm === 'pnpm'
|
|
254
410
|
? ['dlx', 'create-next-app@latest']
|
|
255
|
-
: ['create-next-app@latest'];
|
|
411
|
+
: ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
|
|
256
412
|
const args = [
|
|
257
413
|
...baseArgs,
|
|
258
414
|
projectName,
|
|
@@ -261,6 +417,7 @@ async function createNextJsProject(projectName, pm) {
|
|
|
261
417
|
'--app',
|
|
262
418
|
'--no-src',
|
|
263
419
|
'--disable-git',
|
|
420
|
+
'--skip-install',
|
|
264
421
|
'--import-alias',
|
|
265
422
|
'@/*',
|
|
266
423
|
...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
|
|
@@ -286,65 +443,164 @@ async function createNextJsProject(projectName, pm) {
|
|
|
286
443
|
});
|
|
287
444
|
}
|
|
288
445
|
async function upgradeNextJs(projectDir, version, pm) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
446
|
+
console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
|
|
447
|
+
// pnpm: pnpm add next@... ; npm: npm install next@...
|
|
448
|
+
const args = pm === 'pnpm'
|
|
449
|
+
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
|
|
450
|
+
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
|
|
451
|
+
await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
|
|
452
|
+
console.log(`\n✅ Next.js ${version} installed\n`);
|
|
453
|
+
}
|
|
454
|
+
async function installDependencies(projectDir, pm = 'pnpm') {
|
|
455
|
+
console.log('\n📦 Installing dependencies...\n');
|
|
456
|
+
await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
|
|
457
|
+
console.log('\n✅ Dependencies installed\n');
|
|
458
|
+
}
|
|
459
|
+
async function withNpmLockfileSafeManifests(projectDir, action) {
|
|
460
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
461
|
+
const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
|
|
462
|
+
const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
|
|
463
|
+
const originalFiles = new Map();
|
|
464
|
+
const hiddenNodeModules = new Map();
|
|
465
|
+
function rememberOriginal(filePath) {
|
|
466
|
+
if (!originalFiles.has(filePath)) {
|
|
467
|
+
originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function readJson(filePath) {
|
|
471
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
472
|
+
}
|
|
473
|
+
function writeJson(filePath, value) {
|
|
474
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
475
|
+
}
|
|
476
|
+
const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
|
|
477
|
+
? readJson(sharedPackageJsonPath).name
|
|
478
|
+
: undefined;
|
|
479
|
+
function replaceSharedWorkspaceDependency(packageJson, replacement) {
|
|
480
|
+
let changed = false;
|
|
481
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
482
|
+
const dependencies = packageJson[section];
|
|
483
|
+
if (!dependencies)
|
|
484
|
+
continue;
|
|
485
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
486
|
+
if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
487
|
+
dependencies[name] = replacement;
|
|
488
|
+
changed = true;
|
|
489
|
+
}
|
|
303
490
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
491
|
+
}
|
|
492
|
+
return changed;
|
|
493
|
+
}
|
|
494
|
+
function normalizePackageJson(filePath, replacement) {
|
|
495
|
+
if (!fs.existsSync(filePath))
|
|
496
|
+
return;
|
|
497
|
+
const packageJson = readJson(filePath);
|
|
498
|
+
if (!replaceSharedWorkspaceDependency(packageJson, replacement))
|
|
499
|
+
return;
|
|
500
|
+
rememberOriginal(filePath);
|
|
501
|
+
writeJson(filePath, packageJson);
|
|
502
|
+
}
|
|
503
|
+
function hideNodeModules(directory) {
|
|
504
|
+
const nodeModulesPath = path.join(directory, 'node_modules');
|
|
505
|
+
if (!fs.existsSync(nodeModulesPath))
|
|
506
|
+
return;
|
|
507
|
+
const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
|
|
508
|
+
fs.renameSync(nodeModulesPath, hiddenPath);
|
|
509
|
+
hiddenNodeModules.set(nodeModulesPath, hiddenPath);
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
normalizePackageJson(packageJsonPath, 'file:shared');
|
|
513
|
+
normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
|
|
514
|
+
hideNodeModules(projectDir);
|
|
515
|
+
hideNodeModules(path.join(projectDir, 'shared'));
|
|
516
|
+
hideNodeModules(path.join(projectDir, 'functions'));
|
|
517
|
+
await action();
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
|
|
521
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
522
|
+
fs.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
307
523
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
524
|
+
fs.renameSync(hiddenPath, nodeModulesPath);
|
|
525
|
+
}
|
|
526
|
+
for (const [filePath, content] of originalFiles) {
|
|
527
|
+
fs.writeFileSync(filePath, content);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
313
530
|
}
|
|
314
|
-
|
|
531
|
+
/** Run installs without changing the project security policy. */
|
|
532
|
+
async function runPackageManagerCommand(pm, args, projectDir, label) {
|
|
533
|
+
const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
|
|
534
|
+
if (code === 0)
|
|
535
|
+
return;
|
|
536
|
+
if (pm === 'pnpm') {
|
|
537
|
+
if (parseIgnoredBuilds(output).length > 0) {
|
|
538
|
+
throw new Error(`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
|
|
539
|
+
'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
throw new Error(`${label} exited with code ${code}`);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Spawn a child process inheriting stdin (so interactive prompts still work)
|
|
546
|
+
* while either inheriting or capturing+teeing stdout/stderr.
|
|
547
|
+
*/
|
|
548
|
+
function spawnAndCapture(pm, args, projectDir, capture) {
|
|
315
549
|
return new Promise((resolve, reject) => {
|
|
316
|
-
|
|
317
|
-
|
|
550
|
+
const stdio = capture
|
|
551
|
+
? ['inherit', 'pipe', 'pipe']
|
|
552
|
+
: ['inherit', 'inherit', 'inherit'];
|
|
553
|
+
const child = (0, child_process_1.spawn)(pm, args, {
|
|
318
554
|
cwd: projectDir,
|
|
319
|
-
stdio
|
|
555
|
+
stdio,
|
|
320
556
|
shell: true,
|
|
321
557
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
});
|
|
558
|
+
let combined = '';
|
|
559
|
+
if (capture) {
|
|
560
|
+
child.stdout?.on('data', (chunk) => {
|
|
561
|
+
process.stdout.write(chunk);
|
|
562
|
+
combined += chunk.toString('utf8');
|
|
563
|
+
});
|
|
564
|
+
child.stderr?.on('data', (chunk) => {
|
|
565
|
+
process.stderr.write(chunk);
|
|
566
|
+
combined += chunk.toString('utf8');
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
|
|
570
|
+
child.on('error', reject);
|
|
334
571
|
});
|
|
335
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
|
|
575
|
+
* return the bare package names (without versions), de-duplicated.
|
|
576
|
+
*/
|
|
577
|
+
function parseIgnoredBuilds(output) {
|
|
578
|
+
const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
|
|
579
|
+
if (!match)
|
|
580
|
+
return [];
|
|
581
|
+
const list = match[1]
|
|
582
|
+
.split(',')
|
|
583
|
+
.map((entry) => entry.trim())
|
|
584
|
+
// strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
|
|
585
|
+
.map((entry) => entry.replace(/@[^@]+$/, ''))
|
|
586
|
+
.filter((name) => name.length > 0);
|
|
587
|
+
return Array.from(new Set(list));
|
|
588
|
+
}
|
|
336
589
|
function injectSwallowKitNextConfig(nextConfigContent, projectName) {
|
|
337
|
-
return nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
|
|
590
|
+
return nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$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`);
|
|
338
591
|
}
|
|
339
592
|
function buildCSharpFunctionsProgramSource() {
|
|
340
|
-
return `using Microsoft.
|
|
593
|
+
return `using Microsoft.Azure.Functions.Worker;
|
|
594
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
341
595
|
using Microsoft.Extensions.Hosting;
|
|
596
|
+
using Microsoft.Azure.Functions.Worker.ApplicationInsights;
|
|
342
597
|
|
|
343
598
|
var host = new HostBuilder()
|
|
344
599
|
.ConfigureFunctionsWorkerDefaults()
|
|
345
600
|
.ConfigureServices(services =>
|
|
346
601
|
{
|
|
347
602
|
services.AddApplicationInsightsTelemetryWorkerService();
|
|
603
|
+
services.ConfigureFunctionsApplicationInsights();
|
|
348
604
|
})
|
|
349
605
|
.Build();
|
|
350
606
|
|
|
@@ -354,7 +610,7 @@ host.Run();
|
|
|
354
610
|
function buildCSharpFunctionsProjectSource() {
|
|
355
611
|
return `<Project Sdk="Microsoft.NET.Sdk">
|
|
356
612
|
<PropertyGroup>
|
|
357
|
-
<TargetFramework>
|
|
613
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
358
614
|
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
|
359
615
|
<OutputType>Exe</OutputType>
|
|
360
616
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
@@ -368,62 +624,117 @@ function buildCSharpFunctionsProjectSource() {
|
|
|
368
624
|
<ItemGroup>
|
|
369
625
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
|
|
370
626
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
371
|
-
<PackageReference Include="Azure.Identity" Version="1.
|
|
372
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="
|
|
373
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.
|
|
374
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="
|
|
627
|
+
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
|
628
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
|
629
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
|
630
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
|
|
375
631
|
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
|
|
376
|
-
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="
|
|
632
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
|
377
633
|
</ItemGroup>
|
|
378
634
|
</Project>
|
|
379
635
|
`;
|
|
380
636
|
}
|
|
637
|
+
function buildSwallowKitConfigSource(backendLanguage) {
|
|
638
|
+
return `module.exports = {
|
|
639
|
+
backend: {
|
|
640
|
+
language: '${backendLanguage}',
|
|
641
|
+
},
|
|
642
|
+
functions: {
|
|
643
|
+
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
644
|
+
},
|
|
645
|
+
deployment: {
|
|
646
|
+
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
647
|
+
swaName: process.env.AZURE_SWA_NAME || '',
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
`;
|
|
651
|
+
}
|
|
652
|
+
function buildGeneratedProjectDependencies(projectName, pm = 'pnpm') {
|
|
653
|
+
return {
|
|
654
|
+
'@azure/cosmos': '^4.0.0',
|
|
655
|
+
'applicationinsights': '^3.3.0',
|
|
656
|
+
'zod': '^4.0.0',
|
|
657
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function getSwallowKitPackageMetadata() {
|
|
661
|
+
const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
|
|
662
|
+
try {
|
|
663
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
664
|
+
return {
|
|
665
|
+
name: packageJson.name || "swallowkit",
|
|
666
|
+
version: packageJson.version || "latest",
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
// Fall back to the published package name when package metadata is unavailable.
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
name: "swallowkit",
|
|
674
|
+
version: "latest",
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function buildGeneratedProjectDevDependencies() {
|
|
678
|
+
const { name, version } = getSwallowKitPackageMetadata();
|
|
679
|
+
return {
|
|
680
|
+
[name]: version,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function buildSwallowKitMcpProjectConfigSource(pm = 'pnpm') {
|
|
684
|
+
const { name } = getSwallowKitPackageMetadata();
|
|
685
|
+
return JSON.stringify({
|
|
686
|
+
mcpServers: {
|
|
687
|
+
swallowkit: {
|
|
688
|
+
command: pm === 'pnpm' ? 'pnpm' : 'npx',
|
|
689
|
+
args: pm === 'pnpm'
|
|
690
|
+
? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
|
|
691
|
+
: ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
|
|
692
|
+
cwd: ".",
|
|
693
|
+
env: {
|
|
694
|
+
SWALLOWKIT_MCP_VERSION: "latest",
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
}, null, 2);
|
|
699
|
+
}
|
|
381
700
|
async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
|
|
382
701
|
console.log('📦 Adding SwallowKit files...\n');
|
|
383
702
|
const projectName = options.name;
|
|
384
|
-
// 1. Update package.json to add
|
|
703
|
+
// 1. Update package.json to add runtime dependencies for generated projects
|
|
385
704
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
386
705
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
387
|
-
//
|
|
388
|
-
// zod is in the shared workspace package, not here
|
|
706
|
+
// Root BFF/auth code imports Zod directly; declare it as well as the shared package.
|
|
389
707
|
packageJson.dependencies = {
|
|
390
708
|
...packageJson.dependencies,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
709
|
+
...buildGeneratedProjectDependencies(projectName, pm),
|
|
710
|
+
};
|
|
711
|
+
packageJson.devDependencies = {
|
|
712
|
+
...packageJson.devDependencies,
|
|
713
|
+
...buildGeneratedProjectDevDependencies(),
|
|
395
714
|
};
|
|
396
|
-
if (backendLanguage !== "typescript") {
|
|
397
|
-
packageJson.devDependencies = {
|
|
398
|
-
...packageJson.devDependencies,
|
|
399
|
-
'@openapitools/openapi-generator-cli': '^2.21.0',
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
715
|
packageJson.scripts = {
|
|
403
716
|
...packageJson.scripts,
|
|
404
717
|
'build': (0, package_manager_1.getBuildScript)(pm),
|
|
405
718
|
'start': 'next start',
|
|
406
719
|
'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
|
|
407
720
|
};
|
|
408
|
-
if (pm === 'pnpm') {
|
|
409
|
-
packageJson.packageManager = 'pnpm@latest';
|
|
410
|
-
}
|
|
411
721
|
packageJson.engines = {
|
|
412
|
-
node: '
|
|
722
|
+
node: '>=22.14.0',
|
|
723
|
+
...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
|
|
413
724
|
};
|
|
414
|
-
|
|
725
|
+
packageJson.devEngines = {
|
|
726
|
+
packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
|
|
727
|
+
};
|
|
728
|
+
delete packageJson.packageManager;
|
|
729
|
+
// Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
|
|
415
730
|
const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
|
|
416
731
|
const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
delete packageJson.workspaces;
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
// npm: workspaces are defined in package.json
|
|
732
|
+
packageJson.workspaces = workspacePackages;
|
|
733
|
+
if (wsConfig.type !== 'file') {
|
|
423
734
|
packageJson.workspaces = wsConfig.value;
|
|
424
735
|
}
|
|
425
736
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
426
|
-
// Create workspace
|
|
737
|
+
// Create pnpm-workspace.yaml for local pnpm development when needed.
|
|
427
738
|
if (wsConfig.type === 'file') {
|
|
428
739
|
fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
|
|
429
740
|
}
|
|
@@ -458,20 +769,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
|
|
|
458
769
|
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
459
770
|
}
|
|
460
771
|
// 3. Create SwallowKit config
|
|
461
|
-
const swallowkitConfig =
|
|
462
|
-
module.exports = {
|
|
463
|
-
backend: {
|
|
464
|
-
language: '${backendLanguage}',
|
|
465
|
-
},
|
|
466
|
-
functions: {
|
|
467
|
-
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
468
|
-
},
|
|
469
|
-
deployment: {
|
|
470
|
-
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
471
|
-
swaName: process.env.AZURE_SWA_NAME || '',
|
|
472
|
-
},
|
|
473
|
-
}
|
|
474
|
-
`;
|
|
772
|
+
const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
|
|
475
773
|
fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
|
|
476
774
|
// 4. Create shared workspace package for Zod models (Single Source of Truth)
|
|
477
775
|
await createSharedPackage(projectDir, projectName);
|
|
@@ -486,6 +784,13 @@ function getFunctionsBaseUrl(): string {
|
|
|
486
784
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
487
785
|
}
|
|
488
786
|
|
|
787
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
788
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
789
|
+
if (key) return { 'x-functions-key': key };
|
|
790
|
+
if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
|
|
791
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
792
|
+
}
|
|
793
|
+
|
|
489
794
|
/**
|
|
490
795
|
* Simple HTTP client for calling backend APIs
|
|
491
796
|
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
@@ -493,10 +798,11 @@ function getFunctionsBaseUrl(): string {
|
|
|
493
798
|
async function request<T>(
|
|
494
799
|
endpoint: string,
|
|
495
800
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
496
|
-
body?:
|
|
801
|
+
body?: unknown,
|
|
497
802
|
queryParams?: Record<string, string>
|
|
498
803
|
): Promise<T> {
|
|
499
804
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
805
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
500
806
|
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
501
807
|
if (queryParams) {
|
|
502
808
|
const params = new URLSearchParams(queryParams);
|
|
@@ -508,6 +814,7 @@ async function request<T>(
|
|
|
508
814
|
method,
|
|
509
815
|
headers: {
|
|
510
816
|
'Content-Type': 'application/json',
|
|
817
|
+
...keyHeaders,
|
|
511
818
|
},
|
|
512
819
|
body: body ? JSON.stringify(body) : undefined,
|
|
513
820
|
});
|
|
@@ -563,14 +870,14 @@ export const api = {
|
|
|
563
870
|
/**
|
|
564
871
|
* Make a POST request
|
|
565
872
|
*/
|
|
566
|
-
post: <T>(endpoint: string, body?:
|
|
873
|
+
post: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
567
874
|
return request<T>(endpoint, 'POST', body);
|
|
568
875
|
},
|
|
569
876
|
|
|
570
877
|
/**
|
|
571
878
|
* Make a PUT request
|
|
572
879
|
*/
|
|
573
|
-
put: <T>(endpoint: string, body?:
|
|
880
|
+
put: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
574
881
|
return request<T>(endpoint, 'PUT', body);
|
|
575
882
|
},
|
|
576
883
|
|
|
@@ -589,6 +896,7 @@ export const api = {
|
|
|
589
896
|
// 6. Create .env.example
|
|
590
897
|
const envExample = `# Azure Functions Backend URL
|
|
591
898
|
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
899
|
+
BACKEND_FUNCTIONS_KEY=
|
|
592
900
|
|
|
593
901
|
# Azure Configuration
|
|
594
902
|
AZURE_RESOURCE_GROUP=your-resource-group
|
|
@@ -627,7 +935,7 @@ export async function register() {
|
|
|
627
935
|
const originalConsoleError = console.error;
|
|
628
936
|
const originalConsoleWarn = console.warn;
|
|
629
937
|
|
|
630
|
-
console.log = function(...args:
|
|
938
|
+
console.log = function(...args: unknown[]) {
|
|
631
939
|
originalConsoleLog.apply(console, args);
|
|
632
940
|
const message = args.map(arg =>
|
|
633
941
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -638,7 +946,7 @@ export async function register() {
|
|
|
638
946
|
});
|
|
639
947
|
};
|
|
640
948
|
|
|
641
|
-
console.error = function(...args:
|
|
949
|
+
console.error = function(...args: unknown[]) {
|
|
642
950
|
originalConsoleError.apply(console, args);
|
|
643
951
|
const message = args.map(arg =>
|
|
644
952
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -649,7 +957,7 @@ export async function register() {
|
|
|
649
957
|
});
|
|
650
958
|
};
|
|
651
959
|
|
|
652
|
-
console.warn = function(...args:
|
|
960
|
+
console.warn = function(...args: unknown[]) {
|
|
653
961
|
originalConsoleWarn.apply(console, args);
|
|
654
962
|
const message = args.map(arg =>
|
|
655
963
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -672,6 +980,7 @@ export async function register() {
|
|
|
672
980
|
const envLocalContent = [
|
|
673
981
|
'# Azure Functions Backend URL (Local)',
|
|
674
982
|
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
983
|
+
'BACKEND_FUNCTIONS_KEY=',
|
|
675
984
|
''
|
|
676
985
|
].join('\n');
|
|
677
986
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -709,7 +1018,7 @@ export async function register() {
|
|
|
709
1018
|
// 18. Create README.md
|
|
710
1019
|
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
|
|
711
1020
|
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
712
|
-
createAiAgentFiles(projectDir, projectName, backendLanguage);
|
|
1021
|
+
createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
|
|
713
1022
|
}
|
|
714
1023
|
async function createSharedPackage(projectDir, projectName) {
|
|
715
1024
|
console.log('📦 Creating shared workspace package for Zod models...\n');
|
|
@@ -718,6 +1027,7 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
718
1027
|
fs.mkdirSync(modelsDir, { recursive: true });
|
|
719
1028
|
// shared/package.json
|
|
720
1029
|
const sharedPackageJson = {
|
|
1030
|
+
private: true,
|
|
721
1031
|
name: `@${projectName}/shared`,
|
|
722
1032
|
version: '1.0.0',
|
|
723
1033
|
description: 'Shared Zod models — Single Source of Truth for validation schemas',
|
|
@@ -736,26 +1046,7 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
736
1046
|
};
|
|
737
1047
|
fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
|
|
738
1048
|
// shared/tsconfig.json
|
|
739
|
-
|
|
740
|
-
compilerOptions: {
|
|
741
|
-
target: 'ES2020',
|
|
742
|
-
module: 'commonjs',
|
|
743
|
-
moduleResolution: 'node',
|
|
744
|
-
lib: ['ES2020'],
|
|
745
|
-
outDir: 'dist',
|
|
746
|
-
rootDir: '.',
|
|
747
|
-
declaration: true,
|
|
748
|
-
declarationMap: true,
|
|
749
|
-
sourceMap: true,
|
|
750
|
-
strict: true,
|
|
751
|
-
esModuleInterop: true,
|
|
752
|
-
skipLibCheck: true,
|
|
753
|
-
forceConsistentCasingInFileNames: true,
|
|
754
|
-
},
|
|
755
|
-
include: ['index.ts', 'models/**/*'],
|
|
756
|
-
exclude: ['node_modules', 'dist'],
|
|
757
|
-
};
|
|
758
|
-
fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(sharedTsConfig, null, 2));
|
|
1049
|
+
fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(buildSharedTsConfig(), null, 2));
|
|
759
1050
|
// shared/index.ts (empty re-export file, scaffold will add entries)
|
|
760
1051
|
fs.writeFileSync(path.join(sharedDir, 'index.ts'), `// Shared Zod models — auto-managed by SwallowKit scaffold command\n// Do not edit the export list below manually\n`);
|
|
761
1052
|
// shared/.gitignore
|
|
@@ -828,8 +1119,9 @@ tsconfig.json
|
|
|
828
1119
|
`);
|
|
829
1120
|
}
|
|
830
1121
|
else if (backendLanguage === 'python') {
|
|
831
|
-
gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
|
|
1122
|
+
gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
|
|
832
1123
|
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
|
|
1124
|
+
.codegen-venv
|
|
833
1125
|
__pycache__
|
|
834
1126
|
.pytest_cache
|
|
835
1127
|
.mypy_cache
|
|
@@ -848,8 +1140,8 @@ tests
|
|
|
848
1140
|
}
|
|
849
1141
|
fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
|
|
850
1142
|
}
|
|
851
|
-
function
|
|
852
|
-
|
|
1143
|
+
function buildTypeScriptFunctionsPackageJson(projectName, pm = 'pnpm') {
|
|
1144
|
+
return {
|
|
853
1145
|
name: 'functions',
|
|
854
1146
|
version: '1.0.0',
|
|
855
1147
|
description: 'Azure Functions backend',
|
|
@@ -864,13 +1156,16 @@ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
|
|
|
864
1156
|
'@azure/cosmos': '^4.0.0',
|
|
865
1157
|
'@azure/identity': '^4.0.0',
|
|
866
1158
|
zod: '>=3.25.0',
|
|
867
|
-
[`@${
|
|
1159
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
|
|
868
1160
|
},
|
|
869
1161
|
devDependencies: {
|
|
870
1162
|
'@types/node': '^20.0.0',
|
|
871
1163
|
typescript: '^5.0.0',
|
|
872
1164
|
},
|
|
873
1165
|
};
|
|
1166
|
+
}
|
|
1167
|
+
function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
|
|
1168
|
+
const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
|
|
874
1169
|
fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
|
|
875
1170
|
const sharedPkgName = `@${path.basename(projectDir)}/shared`;
|
|
876
1171
|
const functionsTsConfig = {
|
|
@@ -941,7 +1236,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
941
1236
|
|
|
942
1237
|
app.http('greet', {
|
|
943
1238
|
methods: ['GET', 'POST'],
|
|
944
|
-
authLevel: '
|
|
1239
|
+
authLevel: 'function',
|
|
945
1240
|
handler: greet
|
|
946
1241
|
});
|
|
947
1242
|
`);
|
|
@@ -951,6 +1246,8 @@ function createCSharpFunctionsProject(projectDir, functionsDir) {
|
|
|
951
1246
|
const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
|
|
952
1247
|
const csprojName = `${projectPascal}.Functions.csproj`;
|
|
953
1248
|
fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
|
|
1249
|
+
fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
|
|
1250
|
+
fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
|
|
954
1251
|
fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
|
|
955
1252
|
const crudDir = path.join(functionsDir, 'Crud');
|
|
956
1253
|
fs.mkdirSync(crudDir, { recursive: true });
|
|
@@ -964,7 +1261,7 @@ public sealed class GreetFunction
|
|
|
964
1261
|
{
|
|
965
1262
|
[Function("greet")]
|
|
966
1263
|
public async Task<HttpResponseData> Run(
|
|
967
|
-
[HttpTrigger(AuthorizationLevel.
|
|
1264
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
|
|
968
1265
|
{
|
|
969
1266
|
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
970
1267
|
var name = "SwallowKit";
|
|
@@ -990,10 +1287,12 @@ public sealed class GreetFunction
|
|
|
990
1287
|
}
|
|
991
1288
|
function createPythonFunctionsProject(projectDir, functionsDir) {
|
|
992
1289
|
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1290
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
993
1291
|
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
994
1292
|
azure-cosmos>=4.9.0
|
|
995
1293
|
azure-identity>=1.19.0
|
|
996
1294
|
`);
|
|
1295
|
+
fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
|
|
997
1296
|
const blueprintsDir = path.join(functionsDir, 'blueprints');
|
|
998
1297
|
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
999
1298
|
fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
|
|
@@ -1022,12 +1321,25 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
1022
1321
|
|
|
1023
1322
|
from blueprints.greet import bp as greet_bp
|
|
1024
1323
|
|
|
1025
|
-
app = func.FunctionApp(http_auth_level=func.AuthLevel.
|
|
1324
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
|
1026
1325
|
|
|
1027
1326
|
app.register_blueprint(greet_bp)
|
|
1028
1327
|
# SwallowKit scaffold registrations
|
|
1029
1328
|
`);
|
|
1030
1329
|
}
|
|
1330
|
+
function ensureProjectGitignoreEntry(projectDir, entry) {
|
|
1331
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
1332
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const current = fs.readFileSync(gitignorePath, 'utf8');
|
|
1336
|
+
const lines = current.split(/\r?\n/);
|
|
1337
|
+
if (lines.includes(entry)) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const normalized = current.endsWith('\n') ? current : `${current}\n`;
|
|
1341
|
+
fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
|
|
1342
|
+
}
|
|
1031
1343
|
async function createBffApiRoute(projectDir) {
|
|
1032
1344
|
console.log('📦 Creating BFF API route...\n');
|
|
1033
1345
|
const apiDir = path.join(projectDir, 'app', 'api', 'greet');
|
|
@@ -1188,7 +1500,7 @@ export default function Home() {
|
|
|
1188
1500
|
Create your first model with Zod and generate CRUD operations automatically.
|
|
1189
1501
|
</p>
|
|
1190
1502
|
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
1191
|
-
${pmCmd.
|
|
1503
|
+
${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
|
|
1192
1504
|
</code>
|
|
1193
1505
|
</div>
|
|
1194
1506
|
</section>
|
|
@@ -1234,17 +1546,18 @@ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, back
|
|
|
1234
1546
|
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1235
1547
|
const schemaBridgeDescription = backendLanguage === 'typescript'
|
|
1236
1548
|
? 'Zod (shared between frontend and backend)'
|
|
1237
|
-
: `Zod + OpenAPI
|
|
1549
|
+
: `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
|
|
1238
1550
|
const functionsTree = backendLanguage === 'typescript'
|
|
1239
1551
|
? `│ └── src/\n│ └── greet.ts # Sample function`
|
|
1240
1552
|
: backendLanguage === 'csharp'
|
|
1241
|
-
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ #
|
|
1242
|
-
: `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ #
|
|
1553
|
+
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
|
|
1554
|
+
: `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
|
|
1243
1555
|
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1244
1556
|
? '- Azure Functions CRUD endpoints'
|
|
1245
|
-
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI
|
|
1557
|
+
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1558
|
+
const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
|
|
1246
1559
|
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1247
|
-
? `\n**Python local dev note**: SwallowKit uses \`
|
|
1560
|
+
? `\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`
|
|
1248
1561
|
: '';
|
|
1249
1562
|
const readme = `# ${projectName}
|
|
1250
1563
|
|
|
@@ -1273,7 +1586,7 @@ This project was initialized with the following settings:
|
|
|
1273
1586
|
|
|
1274
1587
|
Before you begin, ensure you have the following installed:
|
|
1275
1588
|
|
|
1276
|
-
1. **Node.js
|
|
1589
|
+
1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1277
1590
|
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1278
1591
|
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1279
1592
|
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
@@ -1281,7 +1594,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
|
|
|
1281
1594
|
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1282
1595
|
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1283
1596
|
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1284
|
-
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**:
|
|
1597
|
+
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
|
|
1285
1598
|
|
|
1286
1599
|
## 📁 Project Structure
|
|
1287
1600
|
|
|
@@ -1307,7 +1620,7 @@ ${functionsTree}
|
|
|
1307
1620
|
Define your data model with Zod schema:
|
|
1308
1621
|
|
|
1309
1622
|
\`\`\`bash
|
|
1310
|
-
${pmCmd.
|
|
1623
|
+
${pmCmd.exec} swallowkit create-model <model-name>
|
|
1311
1624
|
\`\`\`
|
|
1312
1625
|
|
|
1313
1626
|
This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
|
|
@@ -1317,7 +1630,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
|
|
|
1317
1630
|
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1318
1631
|
|
|
1319
1632
|
\`\`\`bash
|
|
1320
|
-
${pmCmd.
|
|
1633
|
+
${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
|
|
1321
1634
|
\`\`\`
|
|
1322
1635
|
|
|
1323
1636
|
This generates:
|
|
@@ -1329,7 +1642,7 @@ ${backendScaffoldNote}
|
|
|
1329
1642
|
### 3. Start Development Servers
|
|
1330
1643
|
|
|
1331
1644
|
\`\`\`bash
|
|
1332
|
-
${pmCmd.
|
|
1645
|
+
${pmCmd.exec} swallowkit dev
|
|
1333
1646
|
\`\`\`
|
|
1334
1647
|
|
|
1335
1648
|
This starts:
|
|
@@ -1347,7 +1660,7 @@ ${pythonLocalDevNote}
|
|
|
1347
1660
|
Create all required Azure resources using Bicep:
|
|
1348
1661
|
|
|
1349
1662
|
\`\`\`bash
|
|
1350
|
-
${pmCmd.
|
|
1663
|
+
${pmCmd.exec} swallowkit provision --resource-group <rg-name>
|
|
1351
1664
|
\`\`\`
|
|
1352
1665
|
|
|
1353
1666
|
This creates:
|
|
@@ -1403,10 +1716,10 @@ func azure functionapp publish func-${projectName}
|
|
|
1403
1716
|
|
|
1404
1717
|
## 🔧 Available Commands
|
|
1405
1718
|
|
|
1406
|
-
- \`${pmCmd.
|
|
1407
|
-
- \`${pmCmd.
|
|
1408
|
-
- \`${pmCmd.
|
|
1409
|
-
- \`${pmCmd.
|
|
1719
|
+
- \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
|
|
1720
|
+
- \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
|
|
1721
|
+
- \`${pmCmd.exec} swallowkit dev\` - Start development servers
|
|
1722
|
+
- \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
|
|
1410
1723
|
${azureConfig.vnetOption !== 'none' ? `
|
|
1411
1724
|
## 🔒 Network Security (VNet Configuration)
|
|
1412
1725
|
|
|
@@ -1453,416 +1766,62 @@ This project was generated by SwallowKit. If you encounter any issues or have su
|
|
|
1453
1766
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1454
1767
|
console.log('✅ README.md created\n');
|
|
1455
1768
|
}
|
|
1456
|
-
function createAiAgentFiles(projectDir, projectName, backendLanguage) {
|
|
1769
|
+
function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
|
|
1457
1770
|
console.log('🤖 Creating AI agent instruction files...\n');
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
Backend (Azure Functions)
|
|
1486
|
-
↓
|
|
1487
|
-
Azure Cosmos DB (Document Database)
|
|
1488
|
-
\`\`\`
|
|
1489
|
-
|
|
1490
|
-
### Project Structure
|
|
1491
|
-
|
|
1492
|
-
\`\`\`
|
|
1493
|
-
${projectName}/
|
|
1494
|
-
├── app/ # Next.js App Router
|
|
1495
|
-
│ ├── api/ # BFF API routes (proxy to Azure Functions)
|
|
1496
|
-
│ └── {model}/ # UI pages per model (list, detail, create, edit)
|
|
1497
|
-
├── functions/ # Azure Functions (backend)
|
|
1498
|
-
${functionsStructureLine}
|
|
1499
|
-
├── shared/ # Shared workspace package
|
|
1500
|
-
│ ├── models/ # Zod schema definitions (single source of truth)
|
|
1501
|
-
│ └── index.ts # Re-exports all models
|
|
1502
|
-
├── lib/
|
|
1503
|
-
│ └── api/ # API client utilities (backend.ts, call-function.ts)
|
|
1504
|
-
├── components/ # Shared React components
|
|
1505
|
-
├── infra/ # Bicep infrastructure-as-code files
|
|
1506
|
-
│ ├── main.bicep
|
|
1507
|
-
│ └── modules/
|
|
1508
|
-
└── .github/workflows/ # CI/CD workflows (if configured)
|
|
1509
|
-
\`\`\`
|
|
1510
|
-
|
|
1511
|
-
## Critical Design Principles
|
|
1512
|
-
|
|
1513
|
-
### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
|
|
1771
|
+
const runCmd = (0, package_manager_1.getCommands)(pm).exec;
|
|
1772
|
+
(0, agent_instructions_1.writeAgentInstructionFiles)(projectDir, {
|
|
1773
|
+
projectName,
|
|
1774
|
+
backendLanguage,
|
|
1775
|
+
packageManager: pm,
|
|
1776
|
+
});
|
|
1777
|
+
console.log(' ✅ AGENTS.md (canonical project contract)');
|
|
1778
|
+
console.log(' ✅ CLAUDE.md (Claude Code adapter)');
|
|
1779
|
+
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
|
|
1780
|
+
console.log(' ✅ .github/instructions/ (path-specific instructions)');
|
|
1781
|
+
fs.writeFileSync(path.join(projectDir, '.mcp.json'), buildSwallowKitMcpProjectConfigSource(pm));
|
|
1782
|
+
console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
|
|
1783
|
+
(0, workflows_1.writeWorkflowDocs)(projectDir, runCmd);
|
|
1784
|
+
console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
|
|
1785
|
+
const skillPaths = (0, workflows_1.writeAgentSkills)(projectDir, runCmd);
|
|
1786
|
+
console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
|
|
1787
|
+
for (const skillPath of skillPaths) {
|
|
1788
|
+
console.log(` - ${skillPath}`);
|
|
1789
|
+
}
|
|
1790
|
+
console.log('\n✅ AI agent files created\n');
|
|
1791
|
+
}
|
|
1792
|
+
function buildFunctionsHostKeyBicepExpression() {
|
|
1793
|
+
return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
|
|
1794
|
+
}
|
|
1795
|
+
function buildStaticWebAppConfigBicepSource() {
|
|
1796
|
+
return `@description('Static Web App name')
|
|
1797
|
+
param staticWebAppName string
|
|
1514
1798
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
- The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
|
|
1518
|
-
- Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
|
|
1799
|
+
@description('Functions App default hostname for backend API calls')
|
|
1800
|
+
param functionsDefaultHostname string
|
|
1519
1801
|
|
|
1520
|
-
|
|
1802
|
+
@description('Functions App name used to resolve its host key after deployment')
|
|
1803
|
+
param functionsAppName string
|
|
1521
1804
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
import { callFunction } from '@/lib/api/call-function';
|
|
1525
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1526
|
-
import { z } from 'zod/v4';
|
|
1805
|
+
@description('Application Insights connection string for SWA')
|
|
1806
|
+
param appInsightsConnectionString string
|
|
1527
1807
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
method: 'GET',
|
|
1531
|
-
path: '/api/{model}',
|
|
1532
|
-
responseSchema: z.array(ModelSchema),
|
|
1533
|
-
});
|
|
1808
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
1809
|
+
name: staticWebAppName
|
|
1534
1810
|
}
|
|
1535
1811
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
});
|
|
1546
|
-
}
|
|
1547
|
-
\`\`\`
|
|
1548
|
-
|
|
1549
|
-
### 2. Zod Schemas Are the Single Source of Truth
|
|
1550
|
-
|
|
1551
|
-
- All data models are defined **once** as Zod schemas in \`shared/models/\`.
|
|
1552
|
-
- TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
|
|
1553
|
-
- ${backendSchemaNote}
|
|
1554
|
-
|
|
1555
|
-
Model definition pattern:
|
|
1556
|
-
|
|
1557
|
-
\`\`\`typescript
|
|
1558
|
-
// shared/models/{model}.ts
|
|
1559
|
-
import { z } from 'zod/v4';
|
|
1560
|
-
|
|
1561
|
-
export const Todo = z.object({
|
|
1562
|
-
id: z.string(),
|
|
1563
|
-
name: z.string().min(1),
|
|
1564
|
-
// ... your fields
|
|
1565
|
-
createdAt: z.string().optional(),
|
|
1566
|
-
updatedAt: z.string().optional(),
|
|
1567
|
-
});
|
|
1568
|
-
|
|
1569
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1570
|
-
export const displayName = 'Todo';
|
|
1571
|
-
\`\`\`
|
|
1572
|
-
|
|
1573
|
-
Key rules:
|
|
1574
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1575
|
-
- \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
|
|
1576
|
-
- Always re-export models from \`shared/index.ts\`.
|
|
1577
|
-
|
|
1578
|
-
### 3. Azure Functions Own All Business Logic and Data Access
|
|
1579
|
-
|
|
1580
|
-
- ${backendRulesNote}
|
|
1581
|
-
|
|
1582
|
-
${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.`}
|
|
1583
|
-
|
|
1584
|
-
${backendLanguage === 'typescript' ? `\`\`\`typescript
|
|
1585
|
-
// functions/src/{model}.ts
|
|
1586
|
-
import { app } from '@azure/functions';
|
|
1587
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1588
|
-
|
|
1589
|
-
const containerName = 'Models'; // PascalCase + 's'
|
|
1590
|
-
|
|
1591
|
-
app.http('{model}-get-all', {
|
|
1592
|
-
methods: ['GET'],
|
|
1593
|
-
route: '{model}',
|
|
1594
|
-
authLevel: 'anonymous',
|
|
1595
|
-
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
1596
|
-
handler: async (request, context) => {
|
|
1597
|
-
const documents = context.extraInputs.get('cosmosInput');
|
|
1598
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
1599
|
-
return { status: 200, jsonBody: validated };
|
|
1600
|
-
},
|
|
1601
|
-
});
|
|
1602
|
-
\`\`\`` : ''}
|
|
1603
|
-
|
|
1604
|
-
## Naming Conventions
|
|
1605
|
-
|
|
1606
|
-
| Item | Convention | Example |
|
|
1607
|
-
|------|-----------|---------|
|
|
1608
|
-
| Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
|
|
1609
|
-
| Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
|
|
1610
|
-
| Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
|
|
1611
|
-
| Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
|
|
1612
|
-
| API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
|
|
1613
|
-
| BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
|
|
1614
|
-
| BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
|
|
1615
|
-
| UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
|
|
1616
|
-
| React component | PascalCase | \`TodoForm.tsx\` |
|
|
1617
|
-
| Cosmos DB container | PascalCase + 's' | \`Todos\` |
|
|
1618
|
-
| Cosmos DB partition key | \`/id\` | Always \`/id\` |
|
|
1619
|
-
| Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
|
|
1620
|
-
|
|
1621
|
-
## Adding New Models (SwallowKit CLI Skills)
|
|
1622
|
-
|
|
1623
|
-
Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
|
|
1624
|
-
|
|
1625
|
-
### Skill: Create a new data model
|
|
1626
|
-
|
|
1627
|
-
\`\`\`bash
|
|
1628
|
-
npx swallowkit create-model <name>
|
|
1629
|
-
# Multiple models at once:
|
|
1630
|
-
npx swallowkit create-model user post comment
|
|
1631
|
-
\`\`\`
|
|
1632
|
-
|
|
1633
|
-
Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
|
|
1634
|
-
Edit the generated file to add your domain-specific fields, then run scaffold.
|
|
1635
|
-
|
|
1636
|
-
### Skill: Generate full CRUD from a model
|
|
1637
|
-
|
|
1638
|
-
\`\`\`bash
|
|
1639
|
-
npx swallowkit scaffold shared/models/<name>.ts
|
|
1640
|
-
\`\`\`
|
|
1641
|
-
|
|
1642
|
-
Generates:
|
|
1643
|
-
- Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
|
|
1644
|
-
- BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
|
|
1645
|
-
- UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
|
|
1646
|
-
- Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
|
|
1647
|
-
|
|
1648
|
-
### Skill: Start development servers
|
|
1649
|
-
|
|
1650
|
-
\`\`\`bash
|
|
1651
|
-
npx swallowkit dev
|
|
1652
|
-
\`\`\`
|
|
1653
|
-
|
|
1654
|
-
Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
|
|
1655
|
-
Checks for Cosmos DB Emulator availability.
|
|
1656
|
-
|
|
1657
|
-
### Skill: Provision Azure resources
|
|
1658
|
-
|
|
1659
|
-
\`\`\`bash
|
|
1660
|
-
npx swallowkit provision --resource-group <name> --location <region>
|
|
1661
|
-
\`\`\`
|
|
1662
|
-
|
|
1663
|
-
Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
|
|
1664
|
-
|
|
1665
|
-
### Typical workflow for "add a new feature/model"
|
|
1666
|
-
|
|
1667
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1668
|
-
2. Edit \`shared/models/<name>.ts\` — add fields
|
|
1669
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1670
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1671
|
-
|
|
1672
|
-
## Do NOT
|
|
1673
|
-
|
|
1674
|
-
- **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
|
|
1675
|
-
- **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
|
|
1676
|
-
- **Do not** manually duplicate model definitions across layers. Use the shared package.
|
|
1677
|
-
- **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
|
|
1678
|
-
- **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
|
|
1679
|
-
- **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
|
|
1680
|
-
|
|
1681
|
-
## Technology Stack
|
|
1682
|
-
|
|
1683
|
-
- **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
|
|
1684
|
-
- **BFF**: Next.js API Routes (proxy only)
|
|
1685
|
-
- **Backend**: Azure Functions (${backendLanguageLabel})
|
|
1686
|
-
- **Database**: Azure Cosmos DB (NoSQL)
|
|
1687
|
-
- **Schema**: Zod (shared across all layers via workspace package)
|
|
1688
|
-
- **Infrastructure**: Bicep (IaC)
|
|
1689
|
-
- **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
|
|
1690
|
-
- **Auth**: Azure Managed Identity (no connection strings in production)
|
|
1691
|
-
- **Monitoring**: Application Insights
|
|
1692
|
-
`;
|
|
1693
|
-
fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
|
|
1694
|
-
console.log(' ✅ AGENTS.md (Codex / generic agents)');
|
|
1695
|
-
// ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
|
|
1696
|
-
const claudeMd = `# CLAUDE.md
|
|
1697
|
-
|
|
1698
|
-
This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
|
|
1699
|
-
|
|
1700
|
-
## Quick Reference
|
|
1701
|
-
|
|
1702
|
-
- **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
|
|
1703
|
-
- **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
|
|
1704
|
-
- **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
|
|
1705
|
-
- **Backend language**: ${backendLanguageLabel}
|
|
1706
|
-
- **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
|
|
1707
|
-
|
|
1708
|
-
## SwallowKit CLI Commands
|
|
1709
|
-
|
|
1710
|
-
| Task | Command |
|
|
1711
|
-
|------|---------|
|
|
1712
|
-
| Create model | \`npx swallowkit create-model <name>\` |
|
|
1713
|
-
| Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
|
|
1714
|
-
| Dev servers | \`npx swallowkit dev\` |
|
|
1715
|
-
| Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
|
|
1716
|
-
|
|
1717
|
-
## Workflow: Add a new model
|
|
1718
|
-
|
|
1719
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1720
|
-
2. Edit \`shared/models/<name>.ts\` — add your fields
|
|
1721
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1722
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1723
|
-
`;
|
|
1724
|
-
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
|
|
1725
|
-
console.log(' ✅ CLAUDE.md (Claude Code)');
|
|
1726
|
-
// ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
|
|
1727
|
-
const ghDir = path.join(projectDir, '.github');
|
|
1728
|
-
fs.mkdirSync(ghDir, { recursive: true });
|
|
1729
|
-
const copilotInstructions = `# Copilot Instructions
|
|
1730
|
-
|
|
1731
|
-
This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
|
|
1732
|
-
|
|
1733
|
-
## Architecture (3-layer)
|
|
1734
|
-
|
|
1735
|
-
\`\`\`
|
|
1736
|
-
Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
|
|
1737
|
-
\`\`\`
|
|
1738
|
-
|
|
1739
|
-
## Key Rules
|
|
1740
|
-
|
|
1741
|
-
1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
|
|
1742
|
-
2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
|
|
1743
|
-
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/\`.
|
|
1744
|
-
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.
|
|
1745
|
-
|
|
1746
|
-
## Naming
|
|
1747
|
-
|
|
1748
|
-
- Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
|
|
1749
|
-
- Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
|
|
1750
|
-
- Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
|
|
1751
|
-
|
|
1752
|
-
## Managed Fields
|
|
1753
|
-
|
|
1754
|
-
\`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
|
|
1755
|
-
`;
|
|
1756
|
-
fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
|
|
1757
|
-
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
|
|
1758
|
-
// ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
|
|
1759
|
-
const instructionsDir = path.join(ghDir, 'instructions');
|
|
1760
|
-
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
1761
|
-
// 4a. shared/models — Zod schema layer
|
|
1762
|
-
const sharedModelsInstructions = `---
|
|
1763
|
-
applyTo: "shared/models/**"
|
|
1764
|
-
---
|
|
1765
|
-
|
|
1766
|
-
# Shared Models — Zod Schema Rules
|
|
1767
|
-
|
|
1768
|
-
Files in this directory are the **single source of truth** for data models across the entire application.
|
|
1769
|
-
|
|
1770
|
-
## Rules
|
|
1771
|
-
|
|
1772
|
-
- Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
|
|
1773
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1774
|
-
\`\`\`typescript
|
|
1775
|
-
export const Todo = z.object({ ... });
|
|
1776
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1777
|
-
\`\`\`
|
|
1778
|
-
- Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
|
|
1779
|
-
- Export a \`displayName\` string constant for UI display.
|
|
1780
|
-
- Re-export every model from \`shared/index.ts\`.
|
|
1781
|
-
- For relationships, use **nested schemas** (import and embed the related schema), not ID references.
|
|
1782
|
-
- After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
|
|
1783
|
-
`;
|
|
1784
|
-
fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
|
|
1785
|
-
// 4b. app/api — BFF layer
|
|
1786
|
-
const bffInstructions = `---
|
|
1787
|
-
applyTo: "app/api/**"
|
|
1788
|
-
---
|
|
1789
|
-
|
|
1790
|
-
# BFF API Routes — Rules
|
|
1791
|
-
|
|
1792
|
-
Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
|
|
1793
|
-
|
|
1794
|
-
## Rules
|
|
1795
|
-
|
|
1796
|
-
- **Never** put business logic, database access, or direct Cosmos DB calls here.
|
|
1797
|
-
- Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
|
|
1798
|
-
- You may validate input/output with Zod schemas before forwarding.
|
|
1799
|
-
- Import schemas from \`@${projectName}/shared\`.
|
|
1800
|
-
|
|
1801
|
-
## Pattern
|
|
1802
|
-
|
|
1803
|
-
\`\`\`typescript
|
|
1804
|
-
import { callFunction } from '@/lib/api/call-function';
|
|
1805
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1806
|
-
import { z } from 'zod/v4';
|
|
1807
|
-
|
|
1808
|
-
export async function GET() {
|
|
1809
|
-
return callFunction({
|
|
1810
|
-
method: 'GET',
|
|
1811
|
-
path: '/api/{model}',
|
|
1812
|
-
responseSchema: z.array(ModelSchema),
|
|
1813
|
-
});
|
|
1812
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1813
|
+
parent: staticWebApp
|
|
1814
|
+
name: 'appsettings'
|
|
1815
|
+
properties: {
|
|
1816
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1817
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1818
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
1819
|
+
BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
|
|
1820
|
+
}
|
|
1814
1821
|
}
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
|
|
1818
|
-
// 4c. functions — Azure Functions backend layer
|
|
1819
|
-
const functionsInstructions = `---
|
|
1820
|
-
applyTo: "functions/**"
|
|
1821
|
-
---
|
|
1822
|
-
|
|
1823
|
-
# Azure Functions — Backend Rules
|
|
1824
|
-
|
|
1825
|
-
Files in \`functions/\` contain all business logic and data access for this application.
|
|
1826
|
-
|
|
1827
|
-
## Rules
|
|
1828
|
-
|
|
1829
|
-
- Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
|
|
1830
|
-
- For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
|
|
1831
|
-
- For C#/Python backends, consume the generated OpenAPI-derived assets in \`functions/generated/\`.
|
|
1832
|
-
- Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
|
|
1833
|
-
- Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
|
|
1834
|
-
|
|
1835
|
-
## Handler Pattern
|
|
1836
|
-
|
|
1837
|
-
\`\`\`typescript
|
|
1838
|
-
import { app } from '@azure/functions';
|
|
1839
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1840
|
-
|
|
1841
|
-
app.http('{model}-get-all', {
|
|
1842
|
-
methods: ['GET'],
|
|
1843
|
-
route: '{model}',
|
|
1844
|
-
authLevel: 'anonymous',
|
|
1845
|
-
extraInputs: [cosmosInput],
|
|
1846
|
-
handler: async (request, context) => {
|
|
1847
|
-
const documents = context.extraInputs.get(cosmosInput);
|
|
1848
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
1849
|
-
return { status: 200, jsonBody: validated };
|
|
1850
|
-
},
|
|
1851
|
-
});
|
|
1852
|
-
\`\`\`
|
|
1822
|
+
|
|
1823
|
+
output configName string = staticWebAppConfig.name
|
|
1853
1824
|
`;
|
|
1854
|
-
fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
|
|
1855
|
-
console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
|
|
1856
|
-
console.log(' - shared-models.instructions.md');
|
|
1857
|
-
console.log(' - bff-routes.instructions.md');
|
|
1858
|
-
console.log(' - azure-functions.instructions.md');
|
|
1859
|
-
console.log('\n✅ AI agent files created\n');
|
|
1860
|
-
console.log(' Supported agents:');
|
|
1861
|
-
console.log(' - OpenAI Codex → AGENTS.md');
|
|
1862
|
-
console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
|
|
1863
|
-
console.log(' - GitHub Copilot → .github/copilot-instructions.md');
|
|
1864
|
-
console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
|
|
1865
|
-
console.log('');
|
|
1866
1825
|
}
|
|
1867
1826
|
async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
|
|
1868
1827
|
console.log('📦 Creating infrastructure files (Bicep)...\n');
|
|
@@ -1871,6 +1830,7 @@ async function createInfrastructure(projectDir, projectName, azureConfig, backen
|
|
|
1871
1830
|
fs.mkdirSync(modulesDir, { recursive: true });
|
|
1872
1831
|
const enableVNet = azureConfig.vnetOption !== 'none';
|
|
1873
1832
|
const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
|
|
1833
|
+
const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
|
|
1874
1834
|
// main.bicep
|
|
1875
1835
|
const mainBicep = `targetScope = 'resourceGroup'
|
|
1876
1836
|
|
|
@@ -1883,6 +1843,13 @@ param location string = resourceGroup().location
|
|
|
1883
1843
|
@description('Location for Static Web App (must be explicitly provided)')
|
|
1884
1844
|
param swaLocation string
|
|
1885
1845
|
|
|
1846
|
+
@description('Static Web Apps hosting plan')
|
|
1847
|
+
@allowed([
|
|
1848
|
+
'Free'
|
|
1849
|
+
'Standard'
|
|
1850
|
+
])
|
|
1851
|
+
param staticWebAppSku string = '${staticWebAppSku}'
|
|
1852
|
+
|
|
1886
1853
|
@description('Cosmos DB mode')
|
|
1887
1854
|
@allowed(['freetier', 'serverless'])
|
|
1888
1855
|
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
@@ -1925,7 +1892,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
|
1925
1892
|
params: {
|
|
1926
1893
|
name: 'swa-\${projectName}'
|
|
1927
1894
|
location: swaLocation
|
|
1928
|
-
sku:
|
|
1895
|
+
sku: staticWebAppSku
|
|
1929
1896
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1930
1897
|
}
|
|
1931
1898
|
}
|
|
@@ -1966,16 +1933,11 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
|
|
|
1966
1933
|
params: {
|
|
1967
1934
|
name: 'pe-cosmos-\${projectName}'
|
|
1968
1935
|
location: location
|
|
1969
|
-
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1970
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1971
|
-
subnetId: vnet
|
|
1972
|
-
vnetId: vnet
|
|
1936
|
+
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
|
|
1937
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
1938
|
+
subnetId: vnet!.outputs.privateEndpointSubnetId
|
|
1939
|
+
vnetId: vnet!.outputs.id
|
|
1973
1940
|
}
|
|
1974
|
-
dependsOn: [
|
|
1975
|
-
cosmosDbFreeTier
|
|
1976
|
-
cosmosDbServerless
|
|
1977
|
-
vnet
|
|
1978
|
-
]
|
|
1979
1941
|
}
|
|
1980
1942
|
|
|
1981
1943
|
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
@@ -1987,16 +1949,14 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
1987
1949
|
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
1988
1950
|
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
1989
1951
|
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
1990
|
-
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1991
|
-
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1952
|
+
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
1953
|
+
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
1992
1954
|
functionsRuntimeName: '${functionsRuntime.name}'
|
|
1993
1955
|
functionsRuntimeVersion: '${functionsRuntime.version}'
|
|
1994
1956
|
enableVNet: enableVNet
|
|
1995
|
-
vnetSubnetId: enableVNet ? vnet
|
|
1957
|
+
vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
|
|
1996
1958
|
}
|
|
1997
1959
|
dependsOn: [
|
|
1998
|
-
cosmosDbFreeTier
|
|
1999
|
-
cosmosDbServerless
|
|
2000
1960
|
cosmosPrivateEndpoint
|
|
2001
1961
|
]
|
|
2002
1962
|
}
|
|
@@ -2005,23 +1965,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
2005
1965
|
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
2006
1966
|
name: 'cosmosDbRoleAssignment'
|
|
2007
1967
|
params: {
|
|
2008
|
-
cosmosAccountName: cosmosDbFreeTier
|
|
1968
|
+
cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
|
|
2009
1969
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2010
1970
|
}
|
|
2011
|
-
dependsOn: [
|
|
2012
|
-
functionsFlex
|
|
2013
|
-
]
|
|
2014
1971
|
}
|
|
2015
1972
|
|
|
2016
1973
|
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
2017
1974
|
name: 'cosmosDbRoleAssignment'
|
|
2018
1975
|
params: {
|
|
2019
|
-
cosmosAccountName: cosmosDbServerless
|
|
1976
|
+
cosmosAccountName: cosmosDbServerless!.outputs.accountName
|
|
2020
1977
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2021
1978
|
}
|
|
2022
|
-
dependsOn: [
|
|
2023
|
-
functionsFlex
|
|
2024
|
-
]
|
|
2025
1979
|
}
|
|
2026
1980
|
|
|
2027
1981
|
// Update SWA config with Functions hostname (after Functions deployment)
|
|
@@ -2030,20 +1984,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
|
2030
1984
|
params: {
|
|
2031
1985
|
staticWebAppName: staticWebApp.outputs.name
|
|
2032
1986
|
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
1987
|
+
functionsAppName: functionsFlex.outputs.name
|
|
2033
1988
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2034
1989
|
}
|
|
2035
|
-
dependsOn: [
|
|
2036
|
-
functionsFlex
|
|
2037
|
-
]
|
|
2038
1990
|
}
|
|
2039
1991
|
|
|
2040
1992
|
output staticWebAppName string = staticWebApp.outputs.name
|
|
2041
1993
|
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
2042
1994
|
output functionsAppName string = functionsFlex.outputs.name
|
|
2043
1995
|
output functionsAppUrl string = functionsFlex.outputs.defaultHostname
|
|
2044
|
-
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2045
|
-
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
2046
|
-
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1996
|
+
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
1997
|
+
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
1998
|
+
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
2047
1999
|
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
2048
2000
|
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
2049
2001
|
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
@@ -2051,7 +2003,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
|
|
|
2051
2003
|
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
2052
2004
|
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
2053
2005
|
output vnetEnabled bool = enableVNet
|
|
2054
|
-
output vnetName string = enableVNet ? vnet
|
|
2006
|
+
output vnetName string = enableVNet ? vnet!.outputs.name : ''
|
|
2055
2007
|
`;
|
|
2056
2008
|
fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
|
|
2057
2009
|
// main.parameters.json
|
|
@@ -2065,6 +2017,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
|
2065
2017
|
"cosmosDbMode": {
|
|
2066
2018
|
"value": "${azureConfig.cosmosDbMode}"
|
|
2067
2019
|
},
|
|
2020
|
+
"staticWebAppSku": {
|
|
2021
|
+
"value": "${staticWebAppSku}"
|
|
2022
|
+
},
|
|
2068
2023
|
"enableVNet": {
|
|
2069
2024
|
"value": ${enableVNet}
|
|
2070
2025
|
}
|
|
@@ -2119,31 +2074,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
|
2119
2074
|
`;
|
|
2120
2075
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
2121
2076
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
2122
|
-
const staticWebAppConfigBicep =
|
|
2123
|
-
param staticWebAppName string
|
|
2124
|
-
|
|
2125
|
-
@description('Functions App default hostname for backend API calls')
|
|
2126
|
-
param functionsDefaultHostname string
|
|
2127
|
-
|
|
2128
|
-
@description('Application Insights connection string for SWA')
|
|
2129
|
-
param appInsightsConnectionString string
|
|
2130
|
-
|
|
2131
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2132
|
-
name: staticWebAppName
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2136
|
-
parent: staticWebApp
|
|
2137
|
-
name: 'appsettings'
|
|
2138
|
-
properties: {
|
|
2139
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2140
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2141
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
output configName string = staticWebAppConfig.name
|
|
2146
|
-
`;
|
|
2077
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
2147
2078
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
2148
2079
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
2149
2080
|
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
@@ -2362,63 +2293,7 @@ output principalId string = functionApp.identity.principalId
|
|
|
2362
2293
|
`;
|
|
2363
2294
|
fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
|
|
2364
2295
|
// modules/cosmosdb-freetier.bicep (Free Tier)
|
|
2365
|
-
const cosmosDbFreeTierBicep =
|
|
2366
|
-
param accountName string
|
|
2367
|
-
|
|
2368
|
-
@description('Database name')
|
|
2369
|
-
param databaseName string
|
|
2370
|
-
|
|
2371
|
-
@description('Location for Cosmos DB')
|
|
2372
|
-
param location string
|
|
2373
|
-
|
|
2374
|
-
@description('Public network access')
|
|
2375
|
-
@allowed(['Enabled', 'Disabled'])
|
|
2376
|
-
param publicNetworkAccess string = 'Enabled'
|
|
2377
|
-
|
|
2378
|
-
// Cosmos DB Account (Free Tier)
|
|
2379
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
2380
|
-
name: accountName
|
|
2381
|
-
location: location
|
|
2382
|
-
kind: 'GlobalDocumentDB'
|
|
2383
|
-
properties: {
|
|
2384
|
-
databaseAccountOfferType: 'Standard'
|
|
2385
|
-
enableAutomaticFailover: false
|
|
2386
|
-
enableFreeTier: true
|
|
2387
|
-
publicNetworkAccess: publicNetworkAccess
|
|
2388
|
-
disableLocalAuth: true
|
|
2389
|
-
consistencyPolicy: {
|
|
2390
|
-
defaultConsistencyLevel: 'Session'
|
|
2391
|
-
}
|
|
2392
|
-
locations: [
|
|
2393
|
-
{
|
|
2394
|
-
locationName: location
|
|
2395
|
-
failoverPriority: 0
|
|
2396
|
-
isZoneRedundant: false
|
|
2397
|
-
}
|
|
2398
|
-
]
|
|
2399
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
// Cosmos DB Database
|
|
2404
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
2405
|
-
parent: cosmosAccount
|
|
2406
|
-
name: databaseName
|
|
2407
|
-
properties: {
|
|
2408
|
-
resource: {
|
|
2409
|
-
id: databaseName
|
|
2410
|
-
}
|
|
2411
|
-
options: {
|
|
2412
|
-
throughput: 1000
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
output id string = cosmosAccount.id
|
|
2418
|
-
output accountName string = cosmosAccount.name
|
|
2419
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
2420
|
-
output databaseName string = database.name
|
|
2421
|
-
`;
|
|
2296
|
+
const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
|
|
2422
2297
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
|
|
2423
2298
|
// modules/cosmosdb-serverless.bicep (Serverless)
|
|
2424
2299
|
const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
|
|
@@ -2508,10 +2383,11 @@ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignment
|
|
|
2508
2383
|
output roleAssignmentId string = roleAssignment.id
|
|
2509
2384
|
`;
|
|
2510
2385
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
|
|
2511
|
-
//
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2386
|
+
// Bicep resolves module files at compile time even when their deployment condition is false,
|
|
2387
|
+
// so these definitions must always be generated. main.bicep still controls deployment with
|
|
2388
|
+
// `if (enableVNet)`.
|
|
2389
|
+
// modules/vnet.bicep
|
|
2390
|
+
const vnetBicep = `@description('VNet name')
|
|
2515
2391
|
param name string
|
|
2516
2392
|
|
|
2517
2393
|
@description('Location for VNet')
|
|
@@ -2557,9 +2433,9 @@ output name string = vnet.name
|
|
|
2557
2433
|
output functionsSubnetId string = vnet.properties.subnets[0].id
|
|
2558
2434
|
output privateEndpointSubnetId string = vnet.properties.subnets[1].id
|
|
2559
2435
|
`;
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2436
|
+
fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
|
|
2437
|
+
// modules/private-endpoint-cosmos.bicep
|
|
2438
|
+
const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
|
|
2563
2439
|
param name string
|
|
2564
2440
|
|
|
2565
2441
|
@description('Location')
|
|
@@ -2637,24 +2513,28 @@ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneG
|
|
|
2637
2513
|
output privateEndpointId string = privateEndpoint.id
|
|
2638
2514
|
output privateDnsZoneId string = privateDnsZone.id
|
|
2639
2515
|
`;
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
}
|
|
2516
|
+
fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
|
|
2517
|
+
console.log('✅ VNet module definitions created\n');
|
|
2643
2518
|
console.log('✅ Infrastructure files created\n');
|
|
2644
2519
|
}
|
|
2645
|
-
function getGitHubFunctionsWorkflow(pm, backendLanguage) {
|
|
2520
|
+
function getGitHubFunctionsWorkflow(pm, backendLanguage, projectPm = pm) {
|
|
2646
2521
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2647
2522
|
const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
|
|
2523
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2648
2524
|
const commonSetup = ` - uses: actions/checkout@v4
|
|
2649
2525
|
|
|
2526
|
+
${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
|
|
2650
2527
|
- name: Setup Node.js
|
|
2651
2528
|
uses: actions/setup-node@v4
|
|
2652
2529
|
with:
|
|
2653
2530
|
node-version: '22'
|
|
2654
|
-
${
|
|
2531
|
+
${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
|
|
2532
|
+
cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
2533
|
+
`}
|
|
2534
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
2655
2535
|
- name: Install dependencies
|
|
2656
2536
|
run: |
|
|
2657
|
-
${
|
|
2537
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2658
2538
|
|
|
2659
2539
|
- name: Build shared package
|
|
2660
2540
|
run: |
|
|
@@ -2738,7 +2618,7 @@ jobs:
|
|
|
2738
2618
|
${commonSetup} - name: Setup .NET
|
|
2739
2619
|
uses: actions/setup-dotnet@v4
|
|
2740
2620
|
with:
|
|
2741
|
-
dotnet-version: '
|
|
2621
|
+
dotnet-version: '10.0.x'
|
|
2742
2622
|
|
|
2743
2623
|
- name: Publish Functions
|
|
2744
2624
|
run: |
|
|
@@ -2797,16 +2677,18 @@ ${commonSetup} - name: Setup Python
|
|
|
2797
2677
|
sku: flexconsumption
|
|
2798
2678
|
`;
|
|
2799
2679
|
}
|
|
2800
|
-
function getAzureFunctionsPipeline(pm, backendLanguage) {
|
|
2680
|
+
function getAzureFunctionsPipeline(pm, backendLanguage, projectPm = pm) {
|
|
2801
2681
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2802
2682
|
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
2683
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2803
2684
|
const commonSetup = ` - task: NodeTool@0
|
|
2804
2685
|
inputs:
|
|
2805
2686
|
versionSpec: '22.x'
|
|
2806
2687
|
displayName: 'Install Node.js'
|
|
2807
2688
|
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2689
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
2808
2690
|
- script: |
|
|
2809
|
-
${
|
|
2691
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2810
2692
|
displayName: 'Install workspace dependencies'
|
|
2811
2693
|
|
|
2812
2694
|
- script: |
|
|
@@ -2910,7 +2792,7 @@ variables:
|
|
|
2910
2792
|
steps:
|
|
2911
2793
|
${commonSetup} - task: UseDotNet@2
|
|
2912
2794
|
inputs:
|
|
2913
|
-
version: '
|
|
2795
|
+
version: '10.0.x'
|
|
2914
2796
|
displayName: 'Install .NET SDK'
|
|
2915
2797
|
|
|
2916
2798
|
- script: |
|
|
@@ -2990,13 +2872,205 @@ ${commonSetup} - task: UsePythonVersion@0
|
|
|
2990
2872
|
displayName: 'Deploy to Azure Functions'
|
|
2991
2873
|
`;
|
|
2992
2874
|
}
|
|
2875
|
+
// setup-node selects the runner runtime only; Oryx reads the app manifest separately.
|
|
2876
|
+
function getSwaNodeVersionCommand() {
|
|
2877
|
+
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));"`;
|
|
2878
|
+
}
|
|
2879
|
+
function buildAzureSwaPipeline(pm, projectPm = pm) {
|
|
2880
|
+
// SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
|
|
2881
|
+
pm = 'npm';
|
|
2882
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2883
|
+
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
2884
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2885
|
+
return `trigger:
|
|
2886
|
+
branches:
|
|
2887
|
+
include:
|
|
2888
|
+
- main
|
|
2889
|
+
paths:
|
|
2890
|
+
include:
|
|
2891
|
+
- app/**
|
|
2892
|
+
- components/**
|
|
2893
|
+
- lib/**
|
|
2894
|
+
- shared/**
|
|
2895
|
+
- public/**
|
|
2896
|
+
- package.json
|
|
2897
|
+
- next.config.js
|
|
2898
|
+
|
|
2899
|
+
pr:
|
|
2900
|
+
branches:
|
|
2901
|
+
include:
|
|
2902
|
+
- main
|
|
2903
|
+
paths:
|
|
2904
|
+
include:
|
|
2905
|
+
- app/**
|
|
2906
|
+
- components/**
|
|
2907
|
+
- lib/**
|
|
2908
|
+
- shared/**
|
|
2909
|
+
- public/**
|
|
2910
|
+
- package.json
|
|
2911
|
+
- next.config.js
|
|
2912
|
+
|
|
2913
|
+
pool:
|
|
2914
|
+
vmImage: 'ubuntu-latest'
|
|
2915
|
+
|
|
2916
|
+
variables:
|
|
2917
|
+
- group: azure-deployment
|
|
2918
|
+
|
|
2919
|
+
steps:
|
|
2920
|
+
- task: NodeTool@0
|
|
2921
|
+
inputs:
|
|
2922
|
+
versionSpec: '22.x'
|
|
2923
|
+
displayName: 'Install Node.js'
|
|
2924
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2925
|
+
- script: |
|
|
2926
|
+
${getSwaNodeVersionCommand()}
|
|
2927
|
+
displayName: 'Select Node.js 22 for SWA'
|
|
2928
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
2929
|
+
- script: |
|
|
2930
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2931
|
+
displayName: 'Install dependencies'
|
|
2932
|
+
|
|
2933
|
+
- script: |
|
|
2934
|
+
${pmCmd.run} build
|
|
2935
|
+
env:
|
|
2936
|
+
NODE_ENV: production
|
|
2937
|
+
displayName: 'Build Next.js app'
|
|
2938
|
+
|
|
2939
|
+
- task: AzureStaticWebApp@0
|
|
2940
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2941
|
+
inputs:
|
|
2942
|
+
app_location: '.'
|
|
2943
|
+
output_location: '.next/standalone'
|
|
2944
|
+
skip_app_build: true
|
|
2945
|
+
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
2946
|
+
displayName: 'Deploy to Azure Static Web Apps'
|
|
2947
|
+
`;
|
|
2948
|
+
}
|
|
2949
|
+
function getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm) {
|
|
2950
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
2951
|
+
return '';
|
|
2952
|
+
}
|
|
2953
|
+
return ` - script: |
|
|
2954
|
+
node <<'NODE'
|
|
2955
|
+
const fs = require('fs');
|
|
2956
|
+
|
|
2957
|
+
function readJson(path) {
|
|
2958
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
function writeJson(path, value) {
|
|
2962
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
const sharedPackagePath = 'shared/package.json';
|
|
2966
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
2967
|
+
|
|
2968
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
2969
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
2970
|
+
if (!pkg[section]) continue;
|
|
2971
|
+
|
|
2972
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
2973
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
2974
|
+
pkg[section][name] = replacement;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
const root = readJson('package.json');
|
|
2981
|
+
root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
|
|
2982
|
+
delete root.packageManager;
|
|
2983
|
+
root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
|
|
2984
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
2985
|
+
root.scripts = root.scripts || {};
|
|
2986
|
+
root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
|
|
2987
|
+
writeJson('package.json', root);
|
|
2988
|
+
|
|
2989
|
+
if (fs.existsSync('functions/package.json')) {
|
|
2990
|
+
const functionsPkg = readJson('functions/package.json');
|
|
2991
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
2992
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
2993
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
2994
|
+
}
|
|
2995
|
+
writeJson('functions/package.json', functionsPkg);
|
|
2996
|
+
}
|
|
2997
|
+
NODE
|
|
2998
|
+
|
|
2999
|
+
npm install --package-lock-only --ignore-scripts
|
|
3000
|
+
displayName: 'Normalize pnpm workspace for npm CI'`;
|
|
3001
|
+
}
|
|
3002
|
+
function getGitHubNpmWorkspaceNormalizationStep(pm, projectPm) {
|
|
3003
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
3004
|
+
return '';
|
|
3005
|
+
}
|
|
3006
|
+
return ` - name: Normalize pnpm workspace for npm CI
|
|
3007
|
+
shell: bash
|
|
3008
|
+
run: |
|
|
3009
|
+
node <<'NODE'
|
|
3010
|
+
const fs = require('fs');
|
|
3011
|
+
|
|
3012
|
+
function readJson(path) {
|
|
3013
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
function writeJson(path, value) {
|
|
3017
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
const sharedPackagePath = 'shared/package.json';
|
|
3021
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
3022
|
+
|
|
3023
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
3024
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
3025
|
+
if (!pkg[section]) continue;
|
|
3026
|
+
|
|
3027
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
3028
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
3029
|
+
pkg[section][name] = replacement;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
const root = readJson('package.json');
|
|
3036
|
+
root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
|
|
3037
|
+
delete root.packageManager;
|
|
3038
|
+
root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
|
|
3039
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
3040
|
+
root.scripts = root.scripts || {};
|
|
3041
|
+
root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
|
|
3042
|
+
writeJson('package.json', root);
|
|
3043
|
+
|
|
3044
|
+
if (fs.existsSync('functions/package.json')) {
|
|
3045
|
+
const functionsPkg = readJson('functions/package.json');
|
|
3046
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
3047
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
3048
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
3049
|
+
}
|
|
3050
|
+
writeJson('functions/package.json', functionsPkg);
|
|
3051
|
+
}
|
|
3052
|
+
NODE
|
|
3053
|
+
|
|
3054
|
+
npm install --package-lock-only --ignore-scripts
|
|
3055
|
+
`;
|
|
3056
|
+
}
|
|
2993
3057
|
async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
|
|
2994
3058
|
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
2995
|
-
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2996
3059
|
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
2997
3060
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
2998
|
-
|
|
2999
|
-
const swaWorkflow =
|
|
3061
|
+
const actionsPm = pm;
|
|
3062
|
+
const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
|
|
3063
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
3064
|
+
// deploy-functions.yml
|
|
3065
|
+
const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
|
|
3066
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
3067
|
+
console.log('✅ GitHub Actions workflows created\n');
|
|
3068
|
+
}
|
|
3069
|
+
function buildGitHubSwaWorkflow(pm, projectPm = pm) {
|
|
3070
|
+
// SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
|
|
3071
|
+
pm = 'npm';
|
|
3072
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
3073
|
+
return `name: Deploy Static Web App
|
|
3000
3074
|
|
|
3001
3075
|
on:
|
|
3002
3076
|
push:
|
|
@@ -3009,6 +3083,7 @@ on:
|
|
|
3009
3083
|
- 'shared/**'
|
|
3010
3084
|
- 'public/**'
|
|
3011
3085
|
- 'package.json'
|
|
3086
|
+
- '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3012
3087
|
- 'next.config.js'
|
|
3013
3088
|
- 'next.config.ts'
|
|
3014
3089
|
workflow_dispatch:
|
|
@@ -3022,6 +3097,7 @@ on:
|
|
|
3022
3097
|
- 'shared/**'
|
|
3023
3098
|
- 'public/**'
|
|
3024
3099
|
- 'package.json'
|
|
3100
|
+
- '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3025
3101
|
- 'next.config.js'
|
|
3026
3102
|
- 'next.config.ts'
|
|
3027
3103
|
|
|
@@ -3034,6 +3110,22 @@ jobs:
|
|
|
3034
3110
|
- uses: actions/checkout@v4
|
|
3035
3111
|
with:
|
|
3036
3112
|
submodules: true
|
|
3113
|
+
|
|
3114
|
+
${(0, package_manager_1.getCiSetupStep)(pm) ? `${(0, package_manager_1.getCiSetupStep)(pm)}\n` : ''}
|
|
3115
|
+
- name: Setup Node.js
|
|
3116
|
+
uses: actions/setup-node@v4
|
|
3117
|
+
with:
|
|
3118
|
+
node-version: '22'
|
|
3119
|
+
${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
|
|
3120
|
+
cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
3121
|
+
`}
|
|
3122
|
+
- name: Select Node.js 22 for SWA
|
|
3123
|
+
run: |
|
|
3124
|
+
${getSwaNodeVersionCommand()}
|
|
3125
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
3126
|
+
- name: Install and build app
|
|
3127
|
+
run: |
|
|
3128
|
+
${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}
|
|
3037
3129
|
|
|
3038
3130
|
- name: Deploy to Azure Static Web Apps
|
|
3039
3131
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
@@ -3045,84 +3137,21 @@ jobs:
|
|
|
3045
3137
|
app_location: '/'
|
|
3046
3138
|
api_location: ''
|
|
3047
3139
|
output_location: ''
|
|
3140
|
+
app_build_command: 'npm run build'
|
|
3048
3141
|
env:
|
|
3049
3142
|
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
3050
3143
|
`;
|
|
3051
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
3052
|
-
// deploy-functions.yml
|
|
3053
|
-
const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
|
|
3054
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
3055
|
-
console.log('✅ GitHub Actions workflows created\n');
|
|
3056
3144
|
}
|
|
3057
3145
|
async function createAzurePipelines(projectDir, pm, backendLanguage) {
|
|
3058
3146
|
console.log('📦 Creating Azure Pipelines...\n');
|
|
3059
|
-
const
|
|
3060
|
-
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
3147
|
+
const pipelinesPm = pm;
|
|
3061
3148
|
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
3062
3149
|
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
3063
3150
|
// swa.yml
|
|
3064
|
-
const swaPipeline =
|
|
3065
|
-
branches:
|
|
3066
|
-
include:
|
|
3067
|
-
- main
|
|
3068
|
-
paths:
|
|
3069
|
-
include:
|
|
3070
|
-
- app/**
|
|
3071
|
-
- components/**
|
|
3072
|
-
- lib/**
|
|
3073
|
-
- shared/**
|
|
3074
|
-
- public/**
|
|
3075
|
-
- package.json
|
|
3076
|
-
- next.config.js
|
|
3077
|
-
|
|
3078
|
-
pr:
|
|
3079
|
-
branches:
|
|
3080
|
-
include:
|
|
3081
|
-
- main
|
|
3082
|
-
paths:
|
|
3083
|
-
include:
|
|
3084
|
-
- app/**
|
|
3085
|
-
- components/**
|
|
3086
|
-
- lib/**
|
|
3087
|
-
- shared/**
|
|
3088
|
-
- public/**
|
|
3089
|
-
- package.json
|
|
3090
|
-
- next.config.js
|
|
3091
|
-
|
|
3092
|
-
pool:
|
|
3093
|
-
vmImage: 'ubuntu-latest'
|
|
3094
|
-
|
|
3095
|
-
variables:
|
|
3096
|
-
- group: azure-deployment
|
|
3097
|
-
|
|
3098
|
-
steps:
|
|
3099
|
-
- task: NodeTool@0
|
|
3100
|
-
inputs:
|
|
3101
|
-
versionSpec: '22.x'
|
|
3102
|
-
displayName: 'Install Node.js'
|
|
3103
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
3104
|
-
- script: |
|
|
3105
|
-
${pmCmd.ci}
|
|
3106
|
-
displayName: 'Install dependencies'
|
|
3107
|
-
|
|
3108
|
-
- script: |
|
|
3109
|
-
${pmCmd.run} build
|
|
3110
|
-
env:
|
|
3111
|
-
NODE_ENV: production
|
|
3112
|
-
displayName: 'Build Next.js app'
|
|
3113
|
-
|
|
3114
|
-
- task: AzureStaticWebApp@0
|
|
3115
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3116
|
-
inputs:
|
|
3117
|
-
app_location: '.'
|
|
3118
|
-
output_location: '.next/standalone'
|
|
3119
|
-
skip_app_build: true
|
|
3120
|
-
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
3121
|
-
displayName: 'Deploy to Azure Static Web Apps'
|
|
3122
|
-
`;
|
|
3151
|
+
const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
|
|
3123
3152
|
fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
|
|
3124
3153
|
// functions.yml
|
|
3125
|
-
const functionsPipeline = getAzureFunctionsPipeline(
|
|
3154
|
+
const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
|
|
3126
3155
|
fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
|
|
3127
3156
|
console.log('✅ Azure Pipelines created\n');
|
|
3128
3157
|
}
|