swallowkit 1.0.0-beta.6 → 1.0.0-beta.61
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 -175
- package/README.md +87 -176
- 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 +62 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +474 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +38 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +733 -155
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +74 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +1376 -930
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +473 -90
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +335 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +14 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +179 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +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 +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +771 -69
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +21 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +230 -79
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +153 -9
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +109 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +9 -12
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +51 -22
- 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 +16 -17
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +206 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +513 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +295 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +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 +236 -0
- package/src/__tests__/dev.test.ts +296 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +184 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +363 -0
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +142 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +105 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +244 -0
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +302 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +248 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +159 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +629 -0
- package/src/cli/commands/dev.ts +1252 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3414 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1081 -0
- package/src/cli/index.ts +352 -0
- package/src/core/config.ts +380 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +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 +972 -0
- package/src/core/scaffold/model-parser.ts +781 -0
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +382 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1100 -0
- package/src/core/verify/index.ts +468 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +182 -0
- package/src/utils/package-manager.ts +251 -0
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -36,12 +36,159 @@ 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;
|
|
45
|
+
exports.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
|
|
46
|
+
exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
|
|
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.buildAzureSwaPipeline = buildAzureSwaPipeline;
|
|
59
|
+
exports.buildGitHubSwaWorkflow = buildGitHubSwaWorkflow;
|
|
40
60
|
const fs = __importStar(require("fs"));
|
|
41
61
|
const path = __importStar(require("path"));
|
|
42
62
|
const child_process_1 = require("child_process");
|
|
43
63
|
const prompts_1 = __importDefault(require("prompts"));
|
|
44
64
|
const package_manager_1 = require("../../utils/package-manager");
|
|
65
|
+
const manifest_1 = require("../../core/project/manifest");
|
|
66
|
+
const workflows_1 = require("../../core/project/workflows");
|
|
67
|
+
const agent_instructions_1 = require("../../core/project/agent-instructions");
|
|
68
|
+
const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
|
|
69
|
+
const python_uv_1 = require("../../utils/python-uv");
|
|
70
|
+
function getStaticWebAppSku(plan) {
|
|
71
|
+
return plan === 'free' ? 'Free' : 'Standard';
|
|
72
|
+
}
|
|
73
|
+
const BACKEND_LANGUAGE_CHOICES = [
|
|
74
|
+
{ title: "TypeScript", value: "typescript" },
|
|
75
|
+
{ title: "C#", value: "csharp" },
|
|
76
|
+
{ title: "Python", value: "python" },
|
|
77
|
+
];
|
|
78
|
+
function usesNodeFunctionsProject(backendLanguage) {
|
|
79
|
+
return backendLanguage === "typescript";
|
|
80
|
+
}
|
|
81
|
+
function getBackendLanguageLabel(backendLanguage) {
|
|
82
|
+
return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
|
|
83
|
+
}
|
|
84
|
+
function buildCosmosDbFreeTierBicepSource() {
|
|
85
|
+
return `@description('Cosmos DB account name')
|
|
86
|
+
param accountName string
|
|
87
|
+
|
|
88
|
+
@description('Database name')
|
|
89
|
+
param databaseName string
|
|
90
|
+
|
|
91
|
+
@description('Location for Cosmos DB')
|
|
92
|
+
param location string
|
|
93
|
+
|
|
94
|
+
@description('Public network access')
|
|
95
|
+
@allowed(['Enabled', 'Disabled'])
|
|
96
|
+
param publicNetworkAccess string = 'Enabled'
|
|
97
|
+
|
|
98
|
+
// Cosmos DB Account (Free Tier)
|
|
99
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
100
|
+
name: accountName
|
|
101
|
+
location: location
|
|
102
|
+
kind: 'GlobalDocumentDB'
|
|
103
|
+
properties: {
|
|
104
|
+
databaseAccountOfferType: 'Standard'
|
|
105
|
+
enableAutomaticFailover: false
|
|
106
|
+
enableFreeTier: true
|
|
107
|
+
publicNetworkAccess: publicNetworkAccess
|
|
108
|
+
disableLocalAuth: true
|
|
109
|
+
consistencyPolicy: {
|
|
110
|
+
defaultConsistencyLevel: 'Session'
|
|
111
|
+
}
|
|
112
|
+
locations: [
|
|
113
|
+
{
|
|
114
|
+
locationName: location
|
|
115
|
+
failoverPriority: 0
|
|
116
|
+
isZoneRedundant: false
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Cosmos DB Database
|
|
124
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
125
|
+
parent: cosmosAccount
|
|
126
|
+
name: databaseName
|
|
127
|
+
properties: {
|
|
128
|
+
resource: {
|
|
129
|
+
id: databaseName
|
|
130
|
+
}
|
|
131
|
+
options: {
|
|
132
|
+
throughput: 1000
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
output id string = cosmosAccount.id
|
|
138
|
+
output accountName string = cosmosAccount.name
|
|
139
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
140
|
+
output databaseName string = database.name
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
function getFunctionsWorkerRuntime(backendLanguage) {
|
|
144
|
+
if (backendLanguage === "csharp") {
|
|
145
|
+
return "dotnet-isolated";
|
|
146
|
+
}
|
|
147
|
+
if (backendLanguage === "python") {
|
|
148
|
+
return "python";
|
|
149
|
+
}
|
|
150
|
+
return "node";
|
|
151
|
+
}
|
|
152
|
+
function getFunctionsRuntimeConfig(backendLanguage) {
|
|
153
|
+
if (backendLanguage === "csharp") {
|
|
154
|
+
return { name: "dotnet-isolated", version: "10.0" };
|
|
155
|
+
}
|
|
156
|
+
if (backendLanguage === "python") {
|
|
157
|
+
return { name: "python", version: "3.11" };
|
|
158
|
+
}
|
|
159
|
+
return { name: "node", version: "22" };
|
|
160
|
+
}
|
|
161
|
+
function buildSharedTsConfig() {
|
|
162
|
+
return {
|
|
163
|
+
compilerOptions: {
|
|
164
|
+
target: "ES2020",
|
|
165
|
+
module: "Node16",
|
|
166
|
+
moduleResolution: "node16",
|
|
167
|
+
lib: ["ES2020"],
|
|
168
|
+
outDir: "dist",
|
|
169
|
+
rootDir: ".",
|
|
170
|
+
declaration: true,
|
|
171
|
+
declarationMap: true,
|
|
172
|
+
sourceMap: true,
|
|
173
|
+
strict: true,
|
|
174
|
+
esModuleInterop: true,
|
|
175
|
+
skipLibCheck: true,
|
|
176
|
+
forceConsistentCasingInFileNames: true,
|
|
177
|
+
},
|
|
178
|
+
include: ["index.ts", "models/**/*"],
|
|
179
|
+
exclude: ["node_modules", "dist"],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function promptBackendLanguage() {
|
|
183
|
+
const response = await (0, prompts_1.default)({
|
|
184
|
+
type: "select",
|
|
185
|
+
name: "backendLanguage",
|
|
186
|
+
message: "Azure Functions backend language:",
|
|
187
|
+
choices: BACKEND_LANGUAGE_CHOICES,
|
|
188
|
+
initial: 0,
|
|
189
|
+
});
|
|
190
|
+
return response.backendLanguage || "typescript";
|
|
191
|
+
}
|
|
45
192
|
async function promptCiCd() {
|
|
46
193
|
const response = await (0, prompts_1.default)({
|
|
47
194
|
type: 'select',
|
|
@@ -56,40 +203,72 @@ async function promptCiCd() {
|
|
|
56
203
|
});
|
|
57
204
|
return response.cicd || 'skip';
|
|
58
205
|
}
|
|
59
|
-
async function promptAzureConfig() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
206
|
+
async function promptAzureConfig(options) {
|
|
207
|
+
let swaPlan = options.swaPlan;
|
|
208
|
+
if (!swaPlan) {
|
|
209
|
+
const response = await (0, prompts_1.default)({
|
|
210
|
+
type: 'select',
|
|
211
|
+
name: 'plan',
|
|
212
|
+
message: 'Azure Static Web Apps plan:',
|
|
213
|
+
choices: [
|
|
214
|
+
{ title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
|
|
215
|
+
{ title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
|
|
216
|
+
],
|
|
217
|
+
initial: 0
|
|
218
|
+
});
|
|
219
|
+
swaPlan = response.plan || 'standard';
|
|
220
|
+
}
|
|
221
|
+
let cosmosDbMode = options.cosmosDbMode;
|
|
222
|
+
if (!cosmosDbMode) {
|
|
223
|
+
const response = await (0, prompts_1.default)({
|
|
224
|
+
type: 'select',
|
|
225
|
+
name: 'mode',
|
|
226
|
+
message: 'Cosmos DB mode (affects cost):',
|
|
227
|
+
choices: [
|
|
228
|
+
{ title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
|
|
229
|
+
{ title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
|
|
230
|
+
],
|
|
231
|
+
initial: 0
|
|
232
|
+
});
|
|
233
|
+
cosmosDbMode = response.mode || 'freetier';
|
|
234
|
+
}
|
|
235
|
+
let vnetOption = options.vnet;
|
|
236
|
+
if (!vnetOption) {
|
|
237
|
+
const response = await (0, prompts_1.default)({
|
|
238
|
+
type: 'select',
|
|
239
|
+
name: 'vnet',
|
|
240
|
+
message: 'Network security:',
|
|
241
|
+
choices: [
|
|
242
|
+
{ title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
|
|
243
|
+
{ title: 'None - Public endpoints, simplest but least secure', value: 'none' }
|
|
244
|
+
],
|
|
245
|
+
initial: 0
|
|
246
|
+
});
|
|
247
|
+
vnetOption = response.vnet || 'outbound';
|
|
248
|
+
}
|
|
80
249
|
return {
|
|
81
|
-
cosmosDbMode:
|
|
82
|
-
vnetOption:
|
|
250
|
+
cosmosDbMode: cosmosDbMode || 'freetier',
|
|
251
|
+
vnetOption: vnetOption || 'outbound',
|
|
252
|
+
swaPlan: swaPlan || 'standard'
|
|
83
253
|
};
|
|
84
254
|
}
|
|
85
255
|
const VALID_CICD = ['github', 'azure', 'skip'];
|
|
256
|
+
const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
|
|
86
257
|
const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
|
|
87
258
|
const VALID_VNET = ['none', 'outbound'];
|
|
259
|
+
const VALID_SWA_PLAN = ['free', 'standard'];
|
|
88
260
|
function validateInitFlags(options) {
|
|
261
|
+
if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
|
|
262
|
+
throw new Error('Invalid --package-manager: expected npm or pnpm');
|
|
263
|
+
}
|
|
89
264
|
if (options.cicd && !VALID_CICD.includes(options.cicd)) {
|
|
90
265
|
console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
|
|
91
266
|
process.exit(1);
|
|
92
267
|
}
|
|
268
|
+
if (options.backendLanguage && !VALID_BACKEND_LANGUAGE.includes(options.backendLanguage)) {
|
|
269
|
+
console.error(`❌ Invalid --backend-language value: "${options.backendLanguage}". Must be: ${VALID_BACKEND_LANGUAGE.join(', ')}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
93
272
|
if (options.cosmosDbMode && !VALID_COSMOS_DB_MODE.includes(options.cosmosDbMode)) {
|
|
94
273
|
console.error(`❌ Invalid --cosmos-db-mode value: "${options.cosmosDbMode}". Must be: ${VALID_COSMOS_DB_MODE.join(', ')}`);
|
|
95
274
|
process.exit(1);
|
|
@@ -98,6 +277,10 @@ function validateInitFlags(options) {
|
|
|
98
277
|
console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
|
|
99
278
|
process.exit(1);
|
|
100
279
|
}
|
|
280
|
+
if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
|
|
281
|
+
console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
101
284
|
}
|
|
102
285
|
async function initCommand(options) {
|
|
103
286
|
// Validate flag values before doing anything
|
|
@@ -105,7 +288,7 @@ async function initCommand(options) {
|
|
|
105
288
|
console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
|
|
106
289
|
console.log(`📋 Template: ${options.template}`);
|
|
107
290
|
// Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
|
|
108
|
-
const pm = (0, package_manager_1.detectFromUserAgent)();
|
|
291
|
+
const pm = options.packageManager || (0, package_manager_1.detectFromUserAgent)();
|
|
109
292
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
110
293
|
console.log(`📦 Package manager: ${pm}`);
|
|
111
294
|
const projectDir = path.join(process.cwd(), options.name);
|
|
@@ -115,26 +298,46 @@ async function initCommand(options) {
|
|
|
115
298
|
console.error(`❌ Directory "${options.name}" already exists.`);
|
|
116
299
|
process.exit(1);
|
|
117
300
|
}
|
|
301
|
+
// 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
|
|
302
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
|
|
303
|
+
if (!interactive) {
|
|
304
|
+
const missing = [];
|
|
305
|
+
if (!options.cicd)
|
|
306
|
+
missing.push('--cicd <github|azure|skip>');
|
|
307
|
+
if (!options.backendLanguage)
|
|
308
|
+
missing.push('--backend-language <typescript|csharp|python>');
|
|
309
|
+
if (!options.cosmosDbMode)
|
|
310
|
+
missing.push('--cosmos-db-mode <freetier|serverless>');
|
|
311
|
+
if (!options.vnet)
|
|
312
|
+
missing.push('--vnet <outbound|none>');
|
|
313
|
+
if (!options.swaPlan)
|
|
314
|
+
missing.push('--swa-plan <free|standard>');
|
|
315
|
+
if (missing.length > 0) {
|
|
316
|
+
console.error(`❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
|
|
317
|
+
missing.map((flag) => ` ${flag}`).join('\n'));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
118
321
|
// Use flag values if provided, otherwise prompt interactively
|
|
119
322
|
const cicdProvider = options.cicd || await promptCiCd();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
: await promptAzureConfig();
|
|
323
|
+
const backendLanguage = options.backendLanguage || await promptBackendLanguage();
|
|
324
|
+
const azureConfig = await promptAzureConfig(options);
|
|
123
325
|
// Create Next.js project with create-next-app
|
|
124
326
|
await createNextJsProject(options.name, pm);
|
|
125
327
|
// Upgrade Next.js to specified version (or latest) to avoid cached old versions
|
|
126
328
|
await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
|
|
127
329
|
// Add SwallowKit specific files
|
|
128
|
-
await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm);
|
|
330
|
+
await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
|
|
129
331
|
// Create infrastructure files (Bicep)
|
|
130
|
-
await createInfrastructure(projectDir, options.name, azureConfig);
|
|
332
|
+
await createInfrastructure(projectDir, options.name, azureConfig, backendLanguage);
|
|
131
333
|
// Create CI/CD files based on choice
|
|
132
334
|
if (cicdProvider === 'github') {
|
|
133
|
-
await createGitHubActionsWorkflows(projectDir, azureConfig, pm);
|
|
335
|
+
await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
|
|
134
336
|
}
|
|
135
337
|
else if (cicdProvider === 'azure') {
|
|
136
|
-
await createAzurePipelines(projectDir, pm);
|
|
338
|
+
await createAzurePipelines(projectDir, pm, backendLanguage);
|
|
137
339
|
}
|
|
340
|
+
await (0, manifest_1.syncProjectManifest)(projectDir);
|
|
138
341
|
// Initialize Git repository and create initial commit
|
|
139
342
|
try {
|
|
140
343
|
// Try git init with -b main (Git 2.28+), fallback to git init
|
|
@@ -178,11 +381,11 @@ async function initCommand(options) {
|
|
|
178
381
|
console.log(`\n✅ Project "${options.name}" created successfully!`);
|
|
179
382
|
console.log("\n📝 Next steps:");
|
|
180
383
|
console.log(` cd ${options.name}`);
|
|
181
|
-
console.log(` ${pmCmd.
|
|
182
|
-
console.log(` ${pmCmd.
|
|
183
|
-
console.log(` ${pmCmd.
|
|
384
|
+
console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
|
|
385
|
+
console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
|
|
386
|
+
console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
|
|
184
387
|
console.log("\n🚀 Deploy to Azure:");
|
|
185
|
-
console.log(` ${pmCmd.
|
|
388
|
+
console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
|
|
186
389
|
if (cicdProvider !== 'skip') {
|
|
187
390
|
console.log(" Configure CI/CD secrets and push to repository");
|
|
188
391
|
}
|
|
@@ -204,7 +407,7 @@ async function createNextJsProject(projectName, pm) {
|
|
|
204
407
|
// for npm use "npx create-next-app@latest ..."
|
|
205
408
|
const baseArgs = pm === 'pnpm'
|
|
206
409
|
? ['dlx', 'create-next-app@latest']
|
|
207
|
-
: ['create-next-app@latest'];
|
|
410
|
+
: ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
|
|
208
411
|
const args = [
|
|
209
412
|
...baseArgs,
|
|
210
413
|
projectName,
|
|
@@ -213,6 +416,7 @@ async function createNextJsProject(projectName, pm) {
|
|
|
213
416
|
'--app',
|
|
214
417
|
'--no-src',
|
|
215
418
|
'--disable-git',
|
|
419
|
+
'--skip-install',
|
|
216
420
|
'--import-alias',
|
|
217
421
|
'@/*',
|
|
218
422
|
...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
|
|
@@ -238,92 +442,298 @@ async function createNextJsProject(projectName, pm) {
|
|
|
238
442
|
});
|
|
239
443
|
}
|
|
240
444
|
async function upgradeNextJs(projectDir, version, pm) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
445
|
+
console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
|
|
446
|
+
// pnpm: pnpm add next@... ; npm: npm install next@...
|
|
447
|
+
const args = pm === 'pnpm'
|
|
448
|
+
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
|
|
449
|
+
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
|
|
450
|
+
await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
|
|
451
|
+
console.log(`\n✅ Next.js ${version} installed\n`);
|
|
452
|
+
}
|
|
453
|
+
async function installDependencies(projectDir, pm = 'pnpm') {
|
|
454
|
+
console.log('\n📦 Installing dependencies...\n');
|
|
455
|
+
await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
|
|
456
|
+
console.log('\n✅ Dependencies installed\n');
|
|
457
|
+
}
|
|
458
|
+
async function withNpmLockfileSafeManifests(projectDir, action) {
|
|
459
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
460
|
+
const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
|
|
461
|
+
const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
|
|
462
|
+
const originalFiles = new Map();
|
|
463
|
+
const hiddenNodeModules = new Map();
|
|
464
|
+
function rememberOriginal(filePath) {
|
|
465
|
+
if (!originalFiles.has(filePath)) {
|
|
466
|
+
originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function readJson(filePath) {
|
|
470
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
471
|
+
}
|
|
472
|
+
function writeJson(filePath, value) {
|
|
473
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
474
|
+
}
|
|
475
|
+
const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
|
|
476
|
+
? readJson(sharedPackageJsonPath).name
|
|
477
|
+
: undefined;
|
|
478
|
+
function replaceSharedWorkspaceDependency(packageJson, replacement) {
|
|
479
|
+
let changed = false;
|
|
480
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
481
|
+
const dependencies = packageJson[section];
|
|
482
|
+
if (!dependencies)
|
|
483
|
+
continue;
|
|
484
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
485
|
+
if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
486
|
+
dependencies[name] = replacement;
|
|
487
|
+
changed = true;
|
|
488
|
+
}
|
|
255
489
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
490
|
+
}
|
|
491
|
+
return changed;
|
|
492
|
+
}
|
|
493
|
+
function normalizePackageJson(filePath, replacement) {
|
|
494
|
+
if (!fs.existsSync(filePath))
|
|
495
|
+
return;
|
|
496
|
+
const packageJson = readJson(filePath);
|
|
497
|
+
if (!replaceSharedWorkspaceDependency(packageJson, replacement))
|
|
498
|
+
return;
|
|
499
|
+
rememberOriginal(filePath);
|
|
500
|
+
writeJson(filePath, packageJson);
|
|
501
|
+
}
|
|
502
|
+
function hideNodeModules(directory) {
|
|
503
|
+
const nodeModulesPath = path.join(directory, 'node_modules');
|
|
504
|
+
if (!fs.existsSync(nodeModulesPath))
|
|
505
|
+
return;
|
|
506
|
+
const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
|
|
507
|
+
fs.renameSync(nodeModulesPath, hiddenPath);
|
|
508
|
+
hiddenNodeModules.set(nodeModulesPath, hiddenPath);
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
normalizePackageJson(packageJsonPath, 'file:shared');
|
|
512
|
+
normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
|
|
513
|
+
hideNodeModules(projectDir);
|
|
514
|
+
hideNodeModules(path.join(projectDir, 'shared'));
|
|
515
|
+
hideNodeModules(path.join(projectDir, 'functions'));
|
|
516
|
+
await action();
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
|
|
520
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
521
|
+
fs.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
259
522
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
523
|
+
fs.renameSync(hiddenPath, nodeModulesPath);
|
|
524
|
+
}
|
|
525
|
+
for (const [filePath, content] of originalFiles) {
|
|
526
|
+
fs.writeFileSync(filePath, content);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
265
529
|
}
|
|
266
|
-
|
|
530
|
+
/** Run installs without changing the project security policy. */
|
|
531
|
+
async function runPackageManagerCommand(pm, args, projectDir, label) {
|
|
532
|
+
const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
|
|
533
|
+
if (code === 0)
|
|
534
|
+
return;
|
|
535
|
+
if (pm === 'pnpm') {
|
|
536
|
+
if (parseIgnoredBuilds(output).length > 0) {
|
|
537
|
+
throw new Error(`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
|
|
538
|
+
'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
throw new Error(`${label} exited with code ${code}`);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Spawn a child process inheriting stdin (so interactive prompts still work)
|
|
545
|
+
* while either inheriting or capturing+teeing stdout/stderr.
|
|
546
|
+
*/
|
|
547
|
+
function spawnAndCapture(pm, args, projectDir, capture) {
|
|
267
548
|
return new Promise((resolve, reject) => {
|
|
268
|
-
|
|
269
|
-
|
|
549
|
+
const stdio = capture
|
|
550
|
+
? ['inherit', 'pipe', 'pipe']
|
|
551
|
+
: ['inherit', 'inherit', 'inherit'];
|
|
552
|
+
const child = (0, child_process_1.spawn)(pm, args, {
|
|
270
553
|
cwd: projectDir,
|
|
271
|
-
stdio
|
|
554
|
+
stdio,
|
|
272
555
|
shell: true,
|
|
273
556
|
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
557
|
+
let combined = '';
|
|
558
|
+
if (capture) {
|
|
559
|
+
child.stdout?.on('data', (chunk) => {
|
|
560
|
+
process.stdout.write(chunk);
|
|
561
|
+
combined += chunk.toString('utf8');
|
|
562
|
+
});
|
|
563
|
+
child.stderr?.on('data', (chunk) => {
|
|
564
|
+
process.stderr.write(chunk);
|
|
565
|
+
combined += chunk.toString('utf8');
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
|
|
569
|
+
child.on('error', reject);
|
|
286
570
|
});
|
|
287
571
|
}
|
|
288
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
|
|
574
|
+
* return the bare package names (without versions), de-duplicated.
|
|
575
|
+
*/
|
|
576
|
+
function parseIgnoredBuilds(output) {
|
|
577
|
+
const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
|
|
578
|
+
if (!match)
|
|
579
|
+
return [];
|
|
580
|
+
const list = match[1]
|
|
581
|
+
.split(',')
|
|
582
|
+
.map((entry) => entry.trim())
|
|
583
|
+
// strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
|
|
584
|
+
.map((entry) => entry.replace(/@[^@]+$/, ''))
|
|
585
|
+
.filter((name) => name.length > 0);
|
|
586
|
+
return Array.from(new Set(list));
|
|
587
|
+
}
|
|
588
|
+
function injectSwallowKitNextConfig(nextConfigContent, projectName) {
|
|
589
|
+
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`);
|
|
590
|
+
}
|
|
591
|
+
function buildCSharpFunctionsProgramSource() {
|
|
592
|
+
return `using Microsoft.Azure.Functions.Worker;
|
|
593
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
594
|
+
using Microsoft.Extensions.Hosting;
|
|
595
|
+
using Microsoft.Azure.Functions.Worker.ApplicationInsights;
|
|
596
|
+
|
|
597
|
+
var host = new HostBuilder()
|
|
598
|
+
.ConfigureFunctionsWorkerDefaults()
|
|
599
|
+
.ConfigureServices(services =>
|
|
600
|
+
{
|
|
601
|
+
services.AddApplicationInsightsTelemetryWorkerService();
|
|
602
|
+
services.ConfigureFunctionsApplicationInsights();
|
|
603
|
+
})
|
|
604
|
+
.Build();
|
|
605
|
+
|
|
606
|
+
host.Run();
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
function buildCSharpFunctionsProjectSource() {
|
|
610
|
+
return `<Project Sdk="Microsoft.NET.Sdk">
|
|
611
|
+
<PropertyGroup>
|
|
612
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
613
|
+
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
|
614
|
+
<OutputType>Exe</OutputType>
|
|
615
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
616
|
+
<Nullable>enable</Nullable>
|
|
617
|
+
</PropertyGroup>
|
|
618
|
+
<ItemGroup>
|
|
619
|
+
<Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />
|
|
620
|
+
<EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
|
|
621
|
+
<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
|
|
622
|
+
</ItemGroup>
|
|
623
|
+
<ItemGroup>
|
|
624
|
+
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
|
|
625
|
+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
626
|
+
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
|
627
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
|
628
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
|
629
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
|
|
630
|
+
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
|
|
631
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
|
632
|
+
</ItemGroup>
|
|
633
|
+
</Project>
|
|
634
|
+
`;
|
|
635
|
+
}
|
|
636
|
+
function buildSwallowKitConfigSource(backendLanguage) {
|
|
637
|
+
return `module.exports = {
|
|
638
|
+
backend: {
|
|
639
|
+
language: '${backendLanguage}',
|
|
640
|
+
},
|
|
641
|
+
functions: {
|
|
642
|
+
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
643
|
+
},
|
|
644
|
+
deployment: {
|
|
645
|
+
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
646
|
+
swaName: process.env.AZURE_SWA_NAME || '',
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
`;
|
|
650
|
+
}
|
|
651
|
+
function buildGeneratedProjectDependencies(projectName, pm = 'pnpm') {
|
|
652
|
+
return {
|
|
653
|
+
'@azure/cosmos': '^4.0.0',
|
|
654
|
+
'applicationinsights': '^3.3.0',
|
|
655
|
+
'zod': '^4.0.0',
|
|
656
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function getSwallowKitPackageMetadata() {
|
|
660
|
+
const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
|
|
661
|
+
try {
|
|
662
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
663
|
+
return {
|
|
664
|
+
name: packageJson.name || "swallowkit",
|
|
665
|
+
version: packageJson.version || "latest",
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// Fall back to the published package name when package metadata is unavailable.
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
name: "swallowkit",
|
|
673
|
+
version: "latest",
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function buildGeneratedProjectDevDependencies() {
|
|
677
|
+
const { name, version } = getSwallowKitPackageMetadata();
|
|
678
|
+
return {
|
|
679
|
+
[name]: version,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function buildSwallowKitMcpProjectConfigSource(pm = 'pnpm') {
|
|
683
|
+
const { name } = getSwallowKitPackageMetadata();
|
|
684
|
+
return JSON.stringify({
|
|
685
|
+
mcpServers: {
|
|
686
|
+
swallowkit: {
|
|
687
|
+
command: pm === 'pnpm' ? 'pnpm' : 'npx',
|
|
688
|
+
args: pm === 'pnpm'
|
|
689
|
+
? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
|
|
690
|
+
: ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
|
|
691
|
+
cwd: ".",
|
|
692
|
+
env: {
|
|
693
|
+
SWALLOWKIT_MCP_VERSION: "latest",
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
}, null, 2);
|
|
698
|
+
}
|
|
699
|
+
async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
|
|
289
700
|
console.log('📦 Adding SwallowKit files...\n');
|
|
290
701
|
const projectName = options.name;
|
|
291
|
-
// 1. Update package.json to add
|
|
702
|
+
// 1. Update package.json to add runtime dependencies for generated projects
|
|
292
703
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
293
704
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
294
|
-
//
|
|
295
|
-
// zod is in the shared workspace package, not here
|
|
705
|
+
// Root BFF/auth code imports Zod directly; declare it as well as the shared package.
|
|
296
706
|
packageJson.dependencies = {
|
|
297
707
|
...packageJson.dependencies,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
708
|
+
...buildGeneratedProjectDependencies(projectName, pm),
|
|
709
|
+
};
|
|
710
|
+
packageJson.devDependencies = {
|
|
711
|
+
...packageJson.devDependencies,
|
|
712
|
+
...buildGeneratedProjectDevDependencies(),
|
|
302
713
|
};
|
|
303
714
|
packageJson.scripts = {
|
|
304
715
|
...packageJson.scripts,
|
|
305
716
|
'build': (0, package_manager_1.getBuildScript)(pm),
|
|
306
717
|
'start': 'next start',
|
|
307
|
-
'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm),
|
|
718
|
+
'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
|
|
308
719
|
};
|
|
309
|
-
if (pm === 'pnpm') {
|
|
310
|
-
packageJson.packageManager = 'pnpm@latest';
|
|
311
|
-
}
|
|
312
720
|
packageJson.engines = {
|
|
313
|
-
node: '
|
|
721
|
+
node: '>=22.14.0',
|
|
722
|
+
...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
|
|
314
723
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
724
|
+
packageJson.devEngines = {
|
|
725
|
+
packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
|
|
726
|
+
};
|
|
727
|
+
delete packageJson.packageManager;
|
|
728
|
+
// Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
|
|
729
|
+
const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
|
|
730
|
+
const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
|
|
731
|
+
packageJson.workspaces = workspacePackages;
|
|
732
|
+
if (wsConfig.type !== 'file') {
|
|
323
733
|
packageJson.workspaces = wsConfig.value;
|
|
324
734
|
}
|
|
325
735
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
326
|
-
// Create workspace
|
|
736
|
+
// Create pnpm-workspace.yaml for local pnpm development when needed.
|
|
327
737
|
if (wsConfig.type === 'file') {
|
|
328
738
|
fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
|
|
329
739
|
}
|
|
@@ -336,11 +746,9 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
|
|
|
336
746
|
}
|
|
337
747
|
if (fs.existsSync(nextConfigPath)) {
|
|
338
748
|
let nextConfigContent = fs.readFileSync(nextConfigPath, 'utf-8');
|
|
339
|
-
// Add output
|
|
749
|
+
// Add output, transpiled workspace package, and server externals for standalone deployment
|
|
340
750
|
if (!nextConfigContent.includes("output:") && !nextConfigContent.includes('output =')) {
|
|
341
|
-
|
|
342
|
-
// Handle JavaScript config format: const nextConfig = {
|
|
343
|
-
nextConfigContent = nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n experimental: {\n turbopackUseSystemTlsCerts: true,\n },\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
|
|
751
|
+
nextConfigContent = injectSwallowKitNextConfig(nextConfigContent, projectName);
|
|
344
752
|
fs.writeFileSync(nextConfigPath, nextConfigContent);
|
|
345
753
|
}
|
|
346
754
|
}
|
|
@@ -360,17 +768,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
|
|
|
360
768
|
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
361
769
|
}
|
|
362
770
|
// 3. Create SwallowKit config
|
|
363
|
-
const swallowkitConfig =
|
|
364
|
-
module.exports = {
|
|
365
|
-
functions: {
|
|
366
|
-
baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
367
|
-
},
|
|
368
|
-
deployment: {
|
|
369
|
-
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
370
|
-
swaName: process.env.AZURE_SWA_NAME || '',
|
|
371
|
-
},
|
|
372
|
-
}
|
|
373
|
-
`;
|
|
771
|
+
const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
|
|
374
772
|
fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
|
|
375
773
|
// 4. Create shared workspace package for Zod models (Single Source of Truth)
|
|
376
774
|
await createSharedPackage(projectDir, projectName);
|
|
@@ -382,7 +780,14 @@ module.exports = {
|
|
|
382
780
|
// Create backend utility for calling Azure Functions
|
|
383
781
|
const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
|
|
384
782
|
function getFunctionsBaseUrl(): string {
|
|
385
|
-
return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
783
|
+
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
787
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
788
|
+
if (key) return { 'x-functions-key': key };
|
|
789
|
+
if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
|
|
790
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
386
791
|
}
|
|
387
792
|
|
|
388
793
|
/**
|
|
@@ -392,10 +797,11 @@ function getFunctionsBaseUrl(): string {
|
|
|
392
797
|
async function request<T>(
|
|
393
798
|
endpoint: string,
|
|
394
799
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
395
|
-
body?:
|
|
800
|
+
body?: unknown,
|
|
396
801
|
queryParams?: Record<string, string>
|
|
397
802
|
): Promise<T> {
|
|
398
803
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
804
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
399
805
|
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
400
806
|
if (queryParams) {
|
|
401
807
|
const params = new URLSearchParams(queryParams);
|
|
@@ -407,6 +813,7 @@ async function request<T>(
|
|
|
407
813
|
method,
|
|
408
814
|
headers: {
|
|
409
815
|
'Content-Type': 'application/json',
|
|
816
|
+
...keyHeaders,
|
|
410
817
|
},
|
|
411
818
|
body: body ? JSON.stringify(body) : undefined,
|
|
412
819
|
});
|
|
@@ -462,14 +869,14 @@ export const api = {
|
|
|
462
869
|
/**
|
|
463
870
|
* Make a POST request
|
|
464
871
|
*/
|
|
465
|
-
post: <T>(endpoint: string, body?:
|
|
872
|
+
post: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
466
873
|
return request<T>(endpoint, 'POST', body);
|
|
467
874
|
},
|
|
468
875
|
|
|
469
876
|
/**
|
|
470
877
|
* Make a PUT request
|
|
471
878
|
*/
|
|
472
|
-
put: <T>(endpoint: string, body?:
|
|
879
|
+
put: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
473
880
|
return request<T>(endpoint, 'PUT', body);
|
|
474
881
|
},
|
|
475
882
|
|
|
@@ -487,7 +894,8 @@ export const api = {
|
|
|
487
894
|
fs.mkdirSync(componentsDir, { recursive: true });
|
|
488
895
|
// 6. Create .env.example
|
|
489
896
|
const envExample = `# Azure Functions Backend URL
|
|
490
|
-
|
|
897
|
+
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
898
|
+
BACKEND_FUNCTIONS_KEY=
|
|
491
899
|
|
|
492
900
|
# Azure Configuration
|
|
493
901
|
AZURE_RESOURCE_GROUP=your-resource-group
|
|
@@ -526,7 +934,7 @@ export async function register() {
|
|
|
526
934
|
const originalConsoleError = console.error;
|
|
527
935
|
const originalConsoleWarn = console.warn;
|
|
528
936
|
|
|
529
|
-
console.log = function(...args:
|
|
937
|
+
console.log = function(...args: unknown[]) {
|
|
530
938
|
originalConsoleLog.apply(console, args);
|
|
531
939
|
const message = args.map(arg =>
|
|
532
940
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -537,7 +945,7 @@ export async function register() {
|
|
|
537
945
|
});
|
|
538
946
|
};
|
|
539
947
|
|
|
540
|
-
console.error = function(...args:
|
|
948
|
+
console.error = function(...args: unknown[]) {
|
|
541
949
|
originalConsoleError.apply(console, args);
|
|
542
950
|
const message = args.map(arg =>
|
|
543
951
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -548,7 +956,7 @@ export async function register() {
|
|
|
548
956
|
});
|
|
549
957
|
};
|
|
550
958
|
|
|
551
|
-
console.warn = function(...args:
|
|
959
|
+
console.warn = function(...args: unknown[]) {
|
|
552
960
|
originalConsoleWarn.apply(console, args);
|
|
553
961
|
const message = args.map(arg =>
|
|
554
962
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -570,7 +978,8 @@ export async function register() {
|
|
|
570
978
|
// 8. Create .env.local for local development
|
|
571
979
|
const envLocalContent = [
|
|
572
980
|
'# Azure Functions Backend URL (Local)',
|
|
573
|
-
'
|
|
981
|
+
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
982
|
+
'BACKEND_FUNCTIONS_KEY=',
|
|
574
983
|
''
|
|
575
984
|
].join('\n');
|
|
576
985
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -596,7 +1005,7 @@ export async function register() {
|
|
|
596
1005
|
};
|
|
597
1006
|
fs.writeFileSync(path.join(projectDir, 'staticwebapp.config.json'), JSON.stringify(swaConfig, null, 2));
|
|
598
1007
|
// 14. Create Azure Functions project
|
|
599
|
-
await createAzureFunctionsProject(projectDir, pm);
|
|
1008
|
+
await createAzureFunctionsProject(projectDir, pm, backendLanguage);
|
|
600
1009
|
// 15. Create BFF API route to call Azure Functions
|
|
601
1010
|
await createBffApiRoute(projectDir);
|
|
602
1011
|
// 16. Create home page
|
|
@@ -606,9 +1015,9 @@ export async function register() {
|
|
|
606
1015
|
await installDependencies(projectDir, pm);
|
|
607
1016
|
console.log('✅ Project structure created\n');
|
|
608
1017
|
// 18. Create README.md
|
|
609
|
-
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm);
|
|
1018
|
+
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
|
|
610
1019
|
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
611
|
-
createAiAgentFiles(projectDir, projectName);
|
|
1020
|
+
createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
|
|
612
1021
|
}
|
|
613
1022
|
async function createSharedPackage(projectDir, projectName) {
|
|
614
1023
|
console.log('📦 Creating shared workspace package for Zod models...\n');
|
|
@@ -617,6 +1026,7 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
617
1026
|
fs.mkdirSync(modelsDir, { recursive: true });
|
|
618
1027
|
// shared/package.json
|
|
619
1028
|
const sharedPackageJson = {
|
|
1029
|
+
private: true,
|
|
620
1030
|
name: `@${projectName}/shared`,
|
|
621
1031
|
version: '1.0.0',
|
|
622
1032
|
description: 'Shared Zod models — Single Source of Truth for validation schemas',
|
|
@@ -635,65 +1045,131 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
635
1045
|
};
|
|
636
1046
|
fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
|
|
637
1047
|
// shared/tsconfig.json
|
|
638
|
-
|
|
639
|
-
compilerOptions: {
|
|
640
|
-
target: 'ES2020',
|
|
641
|
-
module: 'commonjs',
|
|
642
|
-
moduleResolution: 'node',
|
|
643
|
-
lib: ['ES2020'],
|
|
644
|
-
outDir: 'dist',
|
|
645
|
-
rootDir: '.',
|
|
646
|
-
declaration: true,
|
|
647
|
-
declarationMap: true,
|
|
648
|
-
sourceMap: true,
|
|
649
|
-
strict: true,
|
|
650
|
-
esModuleInterop: true,
|
|
651
|
-
skipLibCheck: true,
|
|
652
|
-
forceConsistentCasingInFileNames: true,
|
|
653
|
-
},
|
|
654
|
-
include: ['index.ts', 'models/**/*'],
|
|
655
|
-
exclude: ['node_modules', 'dist'],
|
|
656
|
-
};
|
|
657
|
-
fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(sharedTsConfig, null, 2));
|
|
1048
|
+
fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(buildSharedTsConfig(), null, 2));
|
|
658
1049
|
// shared/index.ts (empty re-export file, scaffold will add entries)
|
|
659
1050
|
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`);
|
|
660
1051
|
// shared/.gitignore
|
|
661
1052
|
fs.writeFileSync(path.join(sharedDir, '.gitignore'), `node_modules\ndist\n`);
|
|
662
1053
|
console.log('✅ Shared package created\n');
|
|
663
1054
|
}
|
|
664
|
-
async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
|
|
665
|
-
console.log(
|
|
1055
|
+
async function createAzureFunctionsProject(projectDir, pm = 'pnpm', backendLanguage = 'typescript') {
|
|
1056
|
+
console.log(`📦 Creating Azure Functions project (${getBackendLanguageLabel(backendLanguage)})...\n`);
|
|
666
1057
|
const functionsDir = path.join(projectDir, 'functions');
|
|
667
1058
|
fs.mkdirSync(functionsDir, { recursive: true });
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1059
|
+
const projectName = path.basename(projectDir);
|
|
1060
|
+
const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
|
|
1061
|
+
createFunctionsHostFiles(functionsDir, databaseName, backendLanguage);
|
|
1062
|
+
if (backendLanguage === 'typescript') {
|
|
1063
|
+
createTypeScriptFunctionsProject(projectDir, functionsDir, pm);
|
|
1064
|
+
}
|
|
1065
|
+
else if (backendLanguage === 'csharp') {
|
|
1066
|
+
createCSharpFunctionsProject(projectDir, functionsDir);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
createPythonFunctionsProject(projectDir, functionsDir);
|
|
1070
|
+
}
|
|
1071
|
+
console.log('✅ Azure Functions project created\n');
|
|
1072
|
+
}
|
|
1073
|
+
function createFunctionsHostFiles(functionsDir, databaseName, backendLanguage) {
|
|
1074
|
+
const hostJson = {
|
|
1075
|
+
version: '2.0',
|
|
1076
|
+
logging: {
|
|
1077
|
+
applicationInsights: {
|
|
1078
|
+
samplingSettings: {
|
|
1079
|
+
isEnabled: true,
|
|
1080
|
+
maxTelemetryItemsPerSecond: 20,
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
678
1083
|
},
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
'@azure/identity': '^4.0.0',
|
|
683
|
-
'zod': '>=3.25.0',
|
|
684
|
-
[`@${path.basename(projectDir)}/shared`]: '*',
|
|
1084
|
+
extensionBundle: {
|
|
1085
|
+
id: 'Microsoft.Azure.Functions.ExtensionBundle',
|
|
1086
|
+
version: '[4.0.0, 4.10.0)',
|
|
685
1087
|
},
|
|
686
|
-
devDependencies: {
|
|
687
|
-
'@types/node': '^20.0.0',
|
|
688
|
-
'typescript': '^5.0.0'
|
|
689
|
-
}
|
|
690
1088
|
};
|
|
691
|
-
fs.writeFileSync(path.join(functionsDir, '
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1089
|
+
fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
|
|
1090
|
+
const localSettings = {
|
|
1091
|
+
IsEncrypted: false,
|
|
1092
|
+
Values: {
|
|
1093
|
+
AzureWebJobsStorage: '',
|
|
1094
|
+
FUNCTIONS_WORKER_RUNTIME: getFunctionsWorkerRuntime(backendLanguage),
|
|
1095
|
+
AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
|
|
1096
|
+
CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
|
|
1097
|
+
COSMOS_DB_DATABASE_NAME: databaseName,
|
|
1098
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
|
|
1102
|
+
const gitignoreLines = [
|
|
1103
|
+
'local.settings.json',
|
|
1104
|
+
'*.log',
|
|
1105
|
+
'.vscode',
|
|
1106
|
+
'.DS_Store',
|
|
1107
|
+
];
|
|
1108
|
+
if (backendLanguage === 'typescript') {
|
|
1109
|
+
gitignoreLines.unshift('node_modules', 'dist');
|
|
1110
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `node_modules
|
|
1111
|
+
.git
|
|
1112
|
+
.vscode
|
|
1113
|
+
local.settings.json
|
|
1114
|
+
test
|
|
1115
|
+
tsconfig.json
|
|
1116
|
+
*.ts
|
|
1117
|
+
!dist/**/*.js
|
|
1118
|
+
`);
|
|
1119
|
+
}
|
|
1120
|
+
else if (backendLanguage === 'python') {
|
|
1121
|
+
gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
|
|
1122
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
|
|
1123
|
+
.codegen-venv
|
|
1124
|
+
__pycache__
|
|
1125
|
+
.pytest_cache
|
|
1126
|
+
.mypy_cache
|
|
1127
|
+
.ruff_cache
|
|
1128
|
+
local.settings.json
|
|
1129
|
+
tests
|
|
1130
|
+
`);
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
gitignoreLines.unshift('bin', 'obj');
|
|
1134
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `bin
|
|
1135
|
+
obj
|
|
1136
|
+
local.settings.json
|
|
1137
|
+
tests
|
|
1138
|
+
`);
|
|
1139
|
+
}
|
|
1140
|
+
fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
|
|
1141
|
+
}
|
|
1142
|
+
function buildTypeScriptFunctionsPackageJson(projectName, pm = 'pnpm') {
|
|
1143
|
+
return {
|
|
1144
|
+
name: 'functions',
|
|
1145
|
+
version: '1.0.0',
|
|
1146
|
+
description: 'Azure Functions backend',
|
|
1147
|
+
main: 'dist/*.js',
|
|
1148
|
+
scripts: {
|
|
1149
|
+
start: 'func start',
|
|
1150
|
+
build: 'tsc',
|
|
1151
|
+
prestart: (0, package_manager_1.getFunctionsPrestart)(pm),
|
|
1152
|
+
},
|
|
1153
|
+
dependencies: {
|
|
1154
|
+
'@azure/functions': '~4.5.0',
|
|
1155
|
+
'@azure/cosmos': '^4.0.0',
|
|
1156
|
+
'@azure/identity': '^4.0.0',
|
|
1157
|
+
zod: '>=3.25.0',
|
|
1158
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
|
|
1159
|
+
},
|
|
1160
|
+
devDependencies: {
|
|
1161
|
+
'@types/node': '^20.0.0',
|
|
1162
|
+
typescript: '^5.0.0',
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
|
|
1167
|
+
const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
|
|
1168
|
+
fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
|
|
1169
|
+
const sharedPkgName = `@${path.basename(projectDir)}/shared`;
|
|
1170
|
+
const functionsTsConfig = {
|
|
1171
|
+
compilerOptions: {
|
|
1172
|
+
target: 'ES2020',
|
|
697
1173
|
module: 'commonjs',
|
|
698
1174
|
moduleResolution: 'node',
|
|
699
1175
|
lib: ['ES2020'],
|
|
@@ -709,69 +1185,14 @@ async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
|
|
|
709
1185
|
},
|
|
710
1186
|
},
|
|
711
1187
|
include: ['src/**/*'],
|
|
712
|
-
exclude: ['node_modules', 'dist']
|
|
1188
|
+
exclude: ['node_modules', 'dist'],
|
|
713
1189
|
};
|
|
714
1190
|
fs.writeFileSync(path.join(functionsDir, 'tsconfig.json'), JSON.stringify(functionsTsConfig, null, 2));
|
|
715
|
-
// Create host.json
|
|
716
|
-
const hostJson = {
|
|
717
|
-
version: '2.0',
|
|
718
|
-
logging: {
|
|
719
|
-
applicationInsights: {
|
|
720
|
-
samplingSettings: {
|
|
721
|
-
isEnabled: true,
|
|
722
|
-
maxTelemetryItemsPerSecond: 20
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
},
|
|
726
|
-
extensionBundle: {
|
|
727
|
-
id: 'Microsoft.Azure.Functions.ExtensionBundle',
|
|
728
|
-
version: '[4.0.0, 4.10.0)'
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
|
|
732
|
-
// Create .funcignore
|
|
733
|
-
const funcignore = `node_modules
|
|
734
|
-
.git
|
|
735
|
-
.vscode
|
|
736
|
-
local.settings.json
|
|
737
|
-
test
|
|
738
|
-
tsconfig.json
|
|
739
|
-
*.ts
|
|
740
|
-
!dist/**/*.js
|
|
741
|
-
`;
|
|
742
|
-
fs.writeFileSync(path.join(functionsDir, '.funcignore'), funcignore);
|
|
743
|
-
// Create .gitignore for functions directory
|
|
744
|
-
const functionsGitignore = `node_modules
|
|
745
|
-
dist
|
|
746
|
-
local.settings.json
|
|
747
|
-
*.log
|
|
748
|
-
.vscode
|
|
749
|
-
.DS_Store
|
|
750
|
-
`;
|
|
751
|
-
fs.writeFileSync(path.join(functionsDir, '.gitignore'), functionsGitignore);
|
|
752
|
-
// Create local.settings.json
|
|
753
|
-
const projectName = path.basename(projectDir);
|
|
754
|
-
const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
|
|
755
|
-
const localSettings = {
|
|
756
|
-
IsEncrypted: false,
|
|
757
|
-
Values: {
|
|
758
|
-
AzureWebJobsStorage: '',
|
|
759
|
-
FUNCTIONS_WORKER_RUNTIME: 'node',
|
|
760
|
-
AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
|
|
761
|
-
CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
|
|
762
|
-
COSMOS_DB_DATABASE_NAME: databaseName,
|
|
763
|
-
NODE_TLS_REJECT_UNAUTHORIZED: '0'
|
|
764
|
-
}
|
|
765
|
-
};
|
|
766
|
-
fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
|
|
767
|
-
// Create src directory
|
|
768
1191
|
const srcDir = path.join(functionsDir, 'src');
|
|
769
1192
|
fs.mkdirSync(srcDir, { recursive: true });
|
|
770
|
-
|
|
771
|
-
const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
1193
|
+
fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
772
1194
|
import { z } from 'zod/v4';
|
|
773
1195
|
|
|
774
|
-
// Zod schema for request validation
|
|
775
1196
|
const greetRequestSchema = z.object({
|
|
776
1197
|
name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
|
|
777
1198
|
});
|
|
@@ -780,12 +1201,9 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
780
1201
|
context.log('HTTP trigger function processed a request.');
|
|
781
1202
|
|
|
782
1203
|
try {
|
|
783
|
-
// Get name from query or body
|
|
784
1204
|
const name = request.query.get('name') || (await request.text());
|
|
785
|
-
|
|
786
|
-
// Validate with Zod
|
|
787
1205
|
const result = greetRequestSchema.safeParse({ name });
|
|
788
|
-
|
|
1206
|
+
|
|
789
1207
|
if (!result.success) {
|
|
790
1208
|
return {
|
|
791
1209
|
status: 400,
|
|
@@ -796,7 +1214,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
796
1214
|
}
|
|
797
1215
|
|
|
798
1216
|
const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
|
|
799
|
-
|
|
1217
|
+
|
|
800
1218
|
return {
|
|
801
1219
|
status: 200,
|
|
802
1220
|
jsonBody: {
|
|
@@ -817,13 +1235,109 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
817
1235
|
|
|
818
1236
|
app.http('greet', {
|
|
819
1237
|
methods: ['GET', 'POST'],
|
|
820
|
-
authLevel: '
|
|
1238
|
+
authLevel: 'function',
|
|
821
1239
|
handler: greet
|
|
822
1240
|
});
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1241
|
+
`);
|
|
1242
|
+
}
|
|
1243
|
+
function createCSharpFunctionsProject(projectDir, functionsDir) {
|
|
1244
|
+
const projectBaseName = path.basename(projectDir);
|
|
1245
|
+
const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
|
|
1246
|
+
const csprojName = `${projectPascal}.Functions.csproj`;
|
|
1247
|
+
fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
|
|
1248
|
+
fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
|
|
1249
|
+
fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
|
|
1250
|
+
fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
|
|
1251
|
+
const crudDir = path.join(functionsDir, 'Crud');
|
|
1252
|
+
fs.mkdirSync(crudDir, { recursive: true });
|
|
1253
|
+
fs.writeFileSync(path.join(crudDir, 'GreetFunction.cs'), `using System.Net;
|
|
1254
|
+
using Microsoft.Azure.Functions.Worker;
|
|
1255
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
1256
|
+
|
|
1257
|
+
namespace SwallowKit.Functions;
|
|
1258
|
+
|
|
1259
|
+
public sealed class GreetFunction
|
|
1260
|
+
{
|
|
1261
|
+
[Function("greet")]
|
|
1262
|
+
public async Task<HttpResponseData> Run(
|
|
1263
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
|
|
1264
|
+
{
|
|
1265
|
+
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
1266
|
+
var name = "SwallowKit";
|
|
1267
|
+
foreach (var segment in query)
|
|
1268
|
+
{
|
|
1269
|
+
var parts = segment.Split('=', 2);
|
|
1270
|
+
if (parts.Length == 2 && parts[0] == "name")
|
|
1271
|
+
{
|
|
1272
|
+
name = Uri.UnescapeDataString(parts[1]);
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
var response = request.CreateResponse(HttpStatusCode.OK);
|
|
1277
|
+
await response.WriteAsJsonAsync(new
|
|
1278
|
+
{
|
|
1279
|
+
message = $"Hello, {name}! This message is from Azure Functions.",
|
|
1280
|
+
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
|
1281
|
+
});
|
|
1282
|
+
return response;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
`);
|
|
1286
|
+
}
|
|
1287
|
+
function createPythonFunctionsProject(projectDir, functionsDir) {
|
|
1288
|
+
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1289
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1290
|
+
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
1291
|
+
azure-cosmos>=4.9.0
|
|
1292
|
+
azure-identity>=1.19.0
|
|
1293
|
+
`);
|
|
1294
|
+
fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
|
|
1295
|
+
const blueprintsDir = path.join(functionsDir, 'blueprints');
|
|
1296
|
+
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
1297
|
+
fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
|
|
1298
|
+
fs.writeFileSync(path.join(blueprintsDir, 'greet.py'), `import json
|
|
1299
|
+
from datetime import datetime, timezone
|
|
1300
|
+
|
|
1301
|
+
import azure.functions as func
|
|
1302
|
+
|
|
1303
|
+
bp = func.Blueprint()
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
@bp.route(route="greet", methods=["GET", "POST"])
|
|
1307
|
+
def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
1308
|
+
name = req.params.get("name") or "SwallowKit"
|
|
1309
|
+
payload = {
|
|
1310
|
+
"message": f"Hello, {name}! This message is from Azure Functions.",
|
|
1311
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1312
|
+
}
|
|
1313
|
+
return func.HttpResponse(
|
|
1314
|
+
body=json.dumps(payload, ensure_ascii=False),
|
|
1315
|
+
status_code=200,
|
|
1316
|
+
mimetype="application/json",
|
|
1317
|
+
)
|
|
1318
|
+
`);
|
|
1319
|
+
fs.writeFileSync(path.join(functionsDir, 'function_app.py'), `import azure.functions as func
|
|
1320
|
+
|
|
1321
|
+
from blueprints.greet import bp as greet_bp
|
|
1322
|
+
|
|
1323
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
|
1324
|
+
|
|
1325
|
+
app.register_blueprint(greet_bp)
|
|
1326
|
+
# SwallowKit scaffold registrations
|
|
1327
|
+
`);
|
|
1328
|
+
}
|
|
1329
|
+
function ensureProjectGitignoreEntry(projectDir, entry) {
|
|
1330
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
1331
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const current = fs.readFileSync(gitignorePath, 'utf8');
|
|
1335
|
+
const lines = current.split(/\r?\n/);
|
|
1336
|
+
if (lines.includes(entry)) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const normalized = current.endsWith('\n') ? current : `${current}\n`;
|
|
1340
|
+
fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
|
|
827
1341
|
}
|
|
828
1342
|
async function createBffApiRoute(projectDir) {
|
|
829
1343
|
console.log('📦 Creating BFF API route...\n');
|
|
@@ -874,17 +1388,17 @@ export async function POST(request: NextRequest) {
|
|
|
874
1388
|
}
|
|
875
1389
|
`;
|
|
876
1390
|
fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
|
|
877
|
-
// Update .env.example to include
|
|
1391
|
+
// Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
|
|
878
1392
|
const envExamplePath = path.join(projectDir, '.env.example');
|
|
879
1393
|
let envExample = fs.readFileSync(envExamplePath, 'utf-8');
|
|
880
|
-
if (!envExample.includes('
|
|
1394
|
+
if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
|
|
881
1395
|
envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
|
|
882
1396
|
fs.writeFileSync(envExamplePath, envExample);
|
|
883
1397
|
}
|
|
884
1398
|
// Update .env.local
|
|
885
1399
|
const envLocalPath = path.join(projectDir, '.env.local');
|
|
886
1400
|
let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
|
|
887
|
-
if (!envLocal.includes('
|
|
1401
|
+
if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
|
|
888
1402
|
envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
|
|
889
1403
|
fs.writeFileSync(envLocalPath, envLocal);
|
|
890
1404
|
}
|
|
@@ -985,7 +1499,7 @@ export default function Home() {
|
|
|
985
1499
|
Create your first model with Zod and generate CRUD operations automatically.
|
|
986
1500
|
</p>
|
|
987
1501
|
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
988
|
-
${pmCmd.
|
|
1502
|
+
${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
|
|
989
1503
|
</code>
|
|
990
1504
|
</div>
|
|
991
1505
|
</section>
|
|
@@ -1021,13 +1535,29 @@ export const scaffoldConfig = {
|
|
|
1021
1535
|
fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
|
|
1022
1536
|
console.log('✅ Scaffold config created\n');
|
|
1023
1537
|
}
|
|
1024
|
-
function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm) {
|
|
1538
|
+
function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage) {
|
|
1025
1539
|
console.log('📝 Creating README.md...\n');
|
|
1026
1540
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
1027
1541
|
const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
|
|
1028
1542
|
const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
|
|
1029
1543
|
const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
|
|
1030
1544
|
'Outbound VNet (Cosmos DB Private Endpoint)';
|
|
1545
|
+
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1546
|
+
const schemaBridgeDescription = backendLanguage === 'typescript'
|
|
1547
|
+
? 'Zod (shared between frontend and backend)'
|
|
1548
|
+
: `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
|
|
1549
|
+
const functionsTree = backendLanguage === 'typescript'
|
|
1550
|
+
? `│ └── src/\n│ └── greet.ts # Sample function`
|
|
1551
|
+
: backendLanguage === 'csharp'
|
|
1552
|
+
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
|
|
1553
|
+
: `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
|
|
1554
|
+
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1555
|
+
? '- Azure Functions CRUD endpoints'
|
|
1556
|
+
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1557
|
+
const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
|
|
1558
|
+
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1559
|
+
? `\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`
|
|
1560
|
+
: '';
|
|
1031
1561
|
const readme = `# ${projectName}
|
|
1032
1562
|
|
|
1033
1563
|
A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
|
|
@@ -1036,9 +1566,9 @@ A full-stack application built with **SwallowKit** - Next.js on Azure Static Web
|
|
|
1036
1566
|
|
|
1037
1567
|
- **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
|
|
1038
1568
|
- **BFF (Backend for Frontend)**: Next.js API Routes
|
|
1039
|
-
- **Backend**: Azure Functions (
|
|
1569
|
+
- **Backend**: Azure Functions (${backendLanguageLabel})
|
|
1040
1570
|
- **Database**: Azure Cosmos DB
|
|
1041
|
-
- **Schema Validation**:
|
|
1571
|
+
- **Schema Validation**: ${schemaBridgeDescription}
|
|
1042
1572
|
- **Infrastructure**: Bicep (Infrastructure as Code)
|
|
1043
1573
|
- **CI/CD**: ${cicdLabel}
|
|
1044
1574
|
|
|
@@ -1055,7 +1585,7 @@ This project was initialized with the following settings:
|
|
|
1055
1585
|
|
|
1056
1586
|
Before you begin, ensure you have the following installed:
|
|
1057
1587
|
|
|
1058
|
-
1. **Node.js
|
|
1588
|
+
1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1059
1589
|
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1060
1590
|
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1061
1591
|
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
@@ -1063,7 +1593,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
|
|
|
1063
1593
|
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1064
1594
|
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1065
1595
|
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1066
|
-
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**:
|
|
1596
|
+
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
|
|
1067
1597
|
|
|
1068
1598
|
## 📁 Project Structure
|
|
1069
1599
|
|
|
@@ -1073,11 +1603,8 @@ ${projectName}/
|
|
|
1073
1603
|
│ ├── api/ # BFF API routes (proxy to Functions)
|
|
1074
1604
|
│ └── page.tsx # Home page
|
|
1075
1605
|
├── functions/ # Azure Functions (backend)
|
|
1076
|
-
|
|
1077
|
-
│ ├── models/ # Data models (copied from lib/models)
|
|
1078
|
-
│ └── hello.ts # Sample function
|
|
1606
|
+
${functionsTree}
|
|
1079
1607
|
├── lib/
|
|
1080
|
-
│ ├── models/ # Shared Zod schemas
|
|
1081
1608
|
│ └── api/ # API client utilities
|
|
1082
1609
|
├── infra/ # Bicep infrastructure files
|
|
1083
1610
|
│ ├── main.bicep
|
|
@@ -1092,21 +1619,21 @@ ${projectName}/
|
|
|
1092
1619
|
Define your data model with Zod schema:
|
|
1093
1620
|
|
|
1094
1621
|
\`\`\`bash
|
|
1095
|
-
${pmCmd.
|
|
1622
|
+
${pmCmd.exec} swallowkit create-model <model-name>
|
|
1096
1623
|
\`\`\`
|
|
1097
1624
|
|
|
1098
|
-
This creates a model file in \`
|
|
1625
|
+
This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
|
|
1099
1626
|
|
|
1100
1627
|
### 2. Generate CRUD Code
|
|
1101
1628
|
|
|
1102
1629
|
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1103
1630
|
|
|
1104
1631
|
\`\`\`bash
|
|
1105
|
-
${pmCmd.
|
|
1632
|
+
${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
|
|
1106
1633
|
\`\`\`
|
|
1107
1634
|
|
|
1108
1635
|
This generates:
|
|
1109
|
-
|
|
1636
|
+
${backendScaffoldNote}
|
|
1110
1637
|
- Next.js BFF API routes
|
|
1111
1638
|
- React UI components (list, detail, create, edit)
|
|
1112
1639
|
- Navigation menu integration
|
|
@@ -1114,7 +1641,7 @@ This generates:
|
|
|
1114
1641
|
### 3. Start Development Servers
|
|
1115
1642
|
|
|
1116
1643
|
\`\`\`bash
|
|
1117
|
-
${pmCmd.
|
|
1644
|
+
${pmCmd.exec} swallowkit dev
|
|
1118
1645
|
\`\`\`
|
|
1119
1646
|
|
|
1120
1647
|
This starts:
|
|
@@ -1123,6 +1650,7 @@ This starts:
|
|
|
1123
1650
|
- Cosmos DB Emulator check (must be running separately)
|
|
1124
1651
|
|
|
1125
1652
|
**Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
|
|
1653
|
+
${pythonLocalDevNote}
|
|
1126
1654
|
|
|
1127
1655
|
## ☁️ Deploy to Azure
|
|
1128
1656
|
|
|
@@ -1131,7 +1659,7 @@ This starts:
|
|
|
1131
1659
|
Create all required Azure resources using Bicep:
|
|
1132
1660
|
|
|
1133
1661
|
\`\`\`bash
|
|
1134
|
-
${pmCmd.
|
|
1662
|
+
${pmCmd.exec} swallowkit provision --resource-group <rg-name>
|
|
1135
1663
|
\`\`\`
|
|
1136
1664
|
|
|
1137
1665
|
This creates:
|
|
@@ -1187,10 +1715,10 @@ func azure functionapp publish func-${projectName}
|
|
|
1187
1715
|
|
|
1188
1716
|
## 🔧 Available Commands
|
|
1189
1717
|
|
|
1190
|
-
- \`${pmCmd.
|
|
1191
|
-
- \`${pmCmd.
|
|
1192
|
-
- \`${pmCmd.
|
|
1193
|
-
- \`${pmCmd.
|
|
1718
|
+
- \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
|
|
1719
|
+
- \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
|
|
1720
|
+
- \`${pmCmd.exec} swallowkit dev\` - Start development servers
|
|
1721
|
+
- \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
|
|
1194
1722
|
${azureConfig.vnetOption !== 'none' ? `
|
|
1195
1723
|
## 🔒 Network Security (VNet Configuration)
|
|
1196
1724
|
|
|
@@ -1237,415 +1765,71 @@ This project was generated by SwallowKit. If you encounter any issues or have su
|
|
|
1237
1765
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1238
1766
|
console.log('✅ README.md created\n');
|
|
1239
1767
|
}
|
|
1240
|
-
function createAiAgentFiles(projectDir, projectName) {
|
|
1768
|
+
function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
|
|
1241
1769
|
console.log('🤖 Creating AI agent instruction files...\n');
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
### Project Structure
|
|
1263
|
-
|
|
1264
|
-
\`\`\`
|
|
1265
|
-
${projectName}/
|
|
1266
|
-
├── app/ # Next.js App Router
|
|
1267
|
-
│ ├── api/ # BFF API routes (proxy to Azure Functions)
|
|
1268
|
-
│ └── {model}/ # UI pages per model (list, detail, create, edit)
|
|
1269
|
-
├── functions/ # Azure Functions (backend)
|
|
1270
|
-
│ └── src/ # HTTP trigger handlers with Cosmos DB bindings
|
|
1271
|
-
├── shared/ # Shared workspace package
|
|
1272
|
-
│ ├── models/ # Zod schema definitions (single source of truth)
|
|
1273
|
-
│ └── index.ts # Re-exports all models
|
|
1274
|
-
├── lib/
|
|
1275
|
-
│ └── api/ # API client utilities (backend.ts, call-function.ts)
|
|
1276
|
-
├── components/ # Shared React components
|
|
1277
|
-
├── infra/ # Bicep infrastructure-as-code files
|
|
1278
|
-
│ ├── main.bicep
|
|
1279
|
-
│ └── modules/
|
|
1280
|
-
└── .github/workflows/ # CI/CD workflows (if configured)
|
|
1281
|
-
\`\`\`
|
|
1282
|
-
|
|
1283
|
-
## Critical Design Principles
|
|
1284
|
-
|
|
1285
|
-
### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
|
|
1286
|
-
|
|
1287
|
-
- \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
|
|
1288
|
-
- **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
|
|
1289
|
-
- The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
|
|
1290
|
-
- Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
|
|
1291
|
-
|
|
1292
|
-
Example BFF route pattern:
|
|
1293
|
-
|
|
1294
|
-
\`\`\`typescript
|
|
1295
|
-
// app/api/{model}/route.ts
|
|
1296
|
-
import { callFunction } from '@/lib/api/call-function';
|
|
1297
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1298
|
-
import { z } from 'zod/v4';
|
|
1299
|
-
|
|
1300
|
-
export async function GET() {
|
|
1301
|
-
return callFunction({
|
|
1302
|
-
method: 'GET',
|
|
1303
|
-
path: '/api/{model}',
|
|
1304
|
-
responseSchema: z.array(ModelSchema),
|
|
1305
|
-
});
|
|
1770
|
+
const runCmd = (0, package_manager_1.getCommands)(pm).exec;
|
|
1771
|
+
(0, agent_instructions_1.writeAgentInstructionFiles)(projectDir, {
|
|
1772
|
+
projectName,
|
|
1773
|
+
backendLanguage,
|
|
1774
|
+
packageManager: pm,
|
|
1775
|
+
});
|
|
1776
|
+
console.log(' ✅ AGENTS.md (canonical project contract)');
|
|
1777
|
+
console.log(' ✅ CLAUDE.md (Claude Code adapter)');
|
|
1778
|
+
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
|
|
1779
|
+
console.log(' ✅ .github/instructions/ (path-specific instructions)');
|
|
1780
|
+
fs.writeFileSync(path.join(projectDir, '.mcp.json'), buildSwallowKitMcpProjectConfigSource(pm));
|
|
1781
|
+
console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
|
|
1782
|
+
(0, workflows_1.writeWorkflowDocs)(projectDir, runCmd);
|
|
1783
|
+
console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
|
|
1784
|
+
const skillPaths = (0, workflows_1.writeAgentSkills)(projectDir, runCmd);
|
|
1785
|
+
console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
|
|
1786
|
+
for (const skillPath of skillPaths) {
|
|
1787
|
+
console.log(` - ${skillPath}`);
|
|
1788
|
+
}
|
|
1789
|
+
console.log('\n✅ AI agent files created\n');
|
|
1306
1790
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const body = await request.json();
|
|
1310
|
-
return callFunction({
|
|
1311
|
-
method: 'POST',
|
|
1312
|
-
path: '/api/{model}',
|
|
1313
|
-
body,
|
|
1314
|
-
inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
|
|
1315
|
-
responseSchema: ModelSchema,
|
|
1316
|
-
successStatus: 201,
|
|
1317
|
-
});
|
|
1791
|
+
function buildFunctionsHostKeyBicepExpression() {
|
|
1792
|
+
return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
|
|
1318
1793
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
- All data models are defined **once** as Zod schemas in \`shared/models/\`.
|
|
1324
|
-
- TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
|
|
1325
|
-
- The same schema is used in **all three layers**: frontend (validation), BFF (input/output validation), and Azure Functions (request/response validation + Cosmos DB documents).
|
|
1326
|
-
- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.
|
|
1327
|
-
|
|
1328
|
-
Model definition pattern:
|
|
1329
|
-
|
|
1330
|
-
\`\`\`typescript
|
|
1331
|
-
// shared/models/{model}.ts
|
|
1332
|
-
import { z } from 'zod/v4';
|
|
1333
|
-
|
|
1334
|
-
export const Todo = z.object({
|
|
1335
|
-
id: z.string(),
|
|
1336
|
-
name: z.string().min(1),
|
|
1337
|
-
// ... your fields
|
|
1338
|
-
createdAt: z.string().optional(),
|
|
1339
|
-
updatedAt: z.string().optional(),
|
|
1340
|
-
});
|
|
1341
|
-
|
|
1342
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1343
|
-
export const displayName = 'Todo';
|
|
1344
|
-
\`\`\`
|
|
1345
|
-
|
|
1346
|
-
Key rules:
|
|
1347
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1348
|
-
- \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
|
|
1349
|
-
- Always re-export models from \`shared/index.ts\`.
|
|
1350
|
-
|
|
1351
|
-
### 3. Azure Functions Own All Business Logic and Data Access
|
|
1352
|
-
|
|
1353
|
-
- All CRUD operations and business logic live in \`functions/src/\`.
|
|
1354
|
-
- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
|
|
1355
|
-
- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).
|
|
1356
|
-
- Validate all data against Zod schemas before writing to Cosmos DB.
|
|
1357
|
-
- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.
|
|
1358
|
-
|
|
1359
|
-
Azure Functions handler pattern:
|
|
1360
|
-
|
|
1361
|
-
\`\`\`typescript
|
|
1362
|
-
// functions/src/{model}.ts
|
|
1363
|
-
import { app } from '@azure/functions';
|
|
1364
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1365
|
-
|
|
1366
|
-
const containerName = 'Models'; // PascalCase + 's'
|
|
1367
|
-
|
|
1368
|
-
app.http('{model}-get-all', {
|
|
1369
|
-
methods: ['GET'],
|
|
1370
|
-
route: '{model}',
|
|
1371
|
-
authLevel: 'anonymous',
|
|
1372
|
-
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
1373
|
-
handler: async (request, context) => {
|
|
1374
|
-
const documents = context.extraInputs.get('cosmosInput');
|
|
1375
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
1376
|
-
return { status: 200, jsonBody: validated };
|
|
1377
|
-
},
|
|
1378
|
-
});
|
|
1379
|
-
\`\`\`
|
|
1380
|
-
|
|
1381
|
-
## Naming Conventions
|
|
1382
|
-
|
|
1383
|
-
| Item | Convention | Example |
|
|
1384
|
-
|------|-----------|---------|
|
|
1385
|
-
| Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
|
|
1386
|
-
| Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
|
|
1387
|
-
| Functions handler file | \`functions/src/{kebab-case}.ts\` | \`functions/src/todo.ts\` |
|
|
1388
|
-
| Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
|
|
1389
|
-
| API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
|
|
1390
|
-
| BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
|
|
1391
|
-
| BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
|
|
1392
|
-
| UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
|
|
1393
|
-
| React component | PascalCase | \`TodoForm.tsx\` |
|
|
1394
|
-
| Cosmos DB container | PascalCase + 's' | \`Todos\` |
|
|
1395
|
-
| Cosmos DB partition key | \`/id\` | Always \`/id\` |
|
|
1396
|
-
| Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
|
|
1397
|
-
|
|
1398
|
-
## Adding New Models (SwallowKit CLI Skills)
|
|
1399
|
-
|
|
1400
|
-
Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
|
|
1401
|
-
|
|
1402
|
-
### Skill: Create a new data model
|
|
1403
|
-
|
|
1404
|
-
\`\`\`bash
|
|
1405
|
-
npx swallowkit create-model <name>
|
|
1406
|
-
# Multiple models at once:
|
|
1407
|
-
npx swallowkit create-model user post comment
|
|
1408
|
-
\`\`\`
|
|
1409
|
-
|
|
1410
|
-
Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
|
|
1411
|
-
Edit the generated file to add your domain-specific fields, then run scaffold.
|
|
1412
|
-
|
|
1413
|
-
### Skill: Generate full CRUD from a model
|
|
1414
|
-
|
|
1415
|
-
\`\`\`bash
|
|
1416
|
-
npx swallowkit scaffold shared/models/<name>.ts
|
|
1417
|
-
\`\`\`
|
|
1418
|
-
|
|
1419
|
-
Generates:
|
|
1420
|
-
- Azure Functions handlers (\`functions/src/<name>.ts\`)
|
|
1421
|
-
- BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
|
|
1422
|
-
- UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
|
|
1423
|
-
- Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
|
|
1424
|
-
|
|
1425
|
-
### Skill: Start development servers
|
|
1426
|
-
|
|
1427
|
-
\`\`\`bash
|
|
1428
|
-
npx swallowkit dev
|
|
1429
|
-
\`\`\`
|
|
1430
|
-
|
|
1431
|
-
Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
|
|
1432
|
-
Checks for Cosmos DB Emulator availability.
|
|
1433
|
-
|
|
1434
|
-
### Skill: Provision Azure resources
|
|
1435
|
-
|
|
1436
|
-
\`\`\`bash
|
|
1437
|
-
npx swallowkit provision --resource-group <name> --location <region>
|
|
1438
|
-
\`\`\`
|
|
1439
|
-
|
|
1440
|
-
Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
|
|
1441
|
-
|
|
1442
|
-
### Typical workflow for "add a new feature/model"
|
|
1443
|
-
|
|
1444
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1445
|
-
2. Edit \`shared/models/<name>.ts\` — add fields
|
|
1446
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1447
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1448
|
-
|
|
1449
|
-
## Do NOT
|
|
1450
|
-
|
|
1451
|
-
- **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
|
|
1452
|
-
- **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
|
|
1453
|
-
- **Do not** manually duplicate model definitions across layers. Use the shared package.
|
|
1454
|
-
- **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
|
|
1455
|
-
- **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
|
|
1456
|
-
- **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
|
|
1457
|
-
|
|
1458
|
-
## Technology Stack
|
|
1459
|
-
|
|
1460
|
-
- **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
|
|
1461
|
-
- **BFF**: Next.js API Routes (proxy only)
|
|
1462
|
-
- **Backend**: Azure Functions (TypeScript, Node.js)
|
|
1463
|
-
- **Database**: Azure Cosmos DB (NoSQL)
|
|
1464
|
-
- **Schema**: Zod (shared across all layers via workspace package)
|
|
1465
|
-
- **Infrastructure**: Bicep (IaC)
|
|
1466
|
-
- **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
|
|
1467
|
-
- **Auth**: Azure Managed Identity (no connection strings in production)
|
|
1468
|
-
- **Monitoring**: Application Insights
|
|
1469
|
-
`;
|
|
1470
|
-
fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
|
|
1471
|
-
console.log(' ✅ AGENTS.md (Codex / generic agents)');
|
|
1472
|
-
// ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
|
|
1473
|
-
const claudeMd = `# CLAUDE.md
|
|
1474
|
-
|
|
1475
|
-
This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
|
|
1476
|
-
|
|
1477
|
-
## Quick Reference
|
|
1478
|
-
|
|
1479
|
-
- **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
|
|
1480
|
-
- **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
|
|
1481
|
-
- **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
|
|
1482
|
-
- **Backend rule**: All business logic and Cosmos DB access lives in \`functions/src/\`.
|
|
1483
|
-
|
|
1484
|
-
## SwallowKit CLI Commands
|
|
1485
|
-
|
|
1486
|
-
| Task | Command |
|
|
1487
|
-
|------|---------|
|
|
1488
|
-
| Create model | \`npx swallowkit create-model <name>\` |
|
|
1489
|
-
| Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
|
|
1490
|
-
| Dev servers | \`npx swallowkit dev\` |
|
|
1491
|
-
| Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
|
|
1492
|
-
|
|
1493
|
-
## Workflow: Add a new model
|
|
1494
|
-
|
|
1495
|
-
1. \`npx swallowkit create-model <name>\`
|
|
1496
|
-
2. Edit \`shared/models/<name>.ts\` — add your fields
|
|
1497
|
-
3. \`npx swallowkit scaffold shared/models/<name>.ts\`
|
|
1498
|
-
4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1499
|
-
`;
|
|
1500
|
-
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
|
|
1501
|
-
console.log(' ✅ CLAUDE.md (Claude Code)');
|
|
1502
|
-
// ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
|
|
1503
|
-
const ghDir = path.join(projectDir, '.github');
|
|
1504
|
-
fs.mkdirSync(ghDir, { recursive: true });
|
|
1505
|
-
const copilotInstructions = `# Copilot Instructions
|
|
1506
|
-
|
|
1507
|
-
This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
|
|
1508
|
-
|
|
1509
|
-
## Architecture (3-layer)
|
|
1510
|
-
|
|
1511
|
-
\`\`\`
|
|
1512
|
-
Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
|
|
1513
|
-
\`\`\`
|
|
1794
|
+
function buildStaticWebAppConfigBicepSource() {
|
|
1795
|
+
return `@description('Static Web App name')
|
|
1796
|
+
param staticWebAppName string
|
|
1514
1797
|
|
|
1515
|
-
|
|
1798
|
+
@description('Functions App default hostname for backend API calls')
|
|
1799
|
+
param functionsDefaultHostname string
|
|
1516
1800
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
3. **Backend owns data** — All CRUD, business logic, and Cosmos DB access is in \`functions/src/\`.
|
|
1520
|
-
4. **Use the CLI** — Run \`npx swallowkit create-model <name>\` then \`npx swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
|
|
1801
|
+
@description('Functions App name used to resolve its host key after deployment')
|
|
1802
|
+
param functionsAppName string
|
|
1521
1803
|
|
|
1522
|
-
|
|
1804
|
+
@description('Application Insights connection string for SWA')
|
|
1805
|
+
param appInsightsConnectionString string
|
|
1523
1806
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1807
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
1808
|
+
name: staticWebAppName
|
|
1809
|
+
}
|
|
1527
1810
|
|
|
1528
|
-
|
|
1811
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1812
|
+
parent: staticWebApp
|
|
1813
|
+
name: 'appsettings'
|
|
1814
|
+
properties: {
|
|
1815
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1816
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1817
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
1818
|
+
BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1529
1821
|
|
|
1530
|
-
|
|
1531
|
-
`;
|
|
1532
|
-
fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
|
|
1533
|
-
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
|
|
1534
|
-
// ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
|
|
1535
|
-
const instructionsDir = path.join(ghDir, 'instructions');
|
|
1536
|
-
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
1537
|
-
// 4a. shared/models — Zod schema layer
|
|
1538
|
-
const sharedModelsInstructions = `---
|
|
1539
|
-
applyTo: "shared/models/**"
|
|
1540
|
-
---
|
|
1541
|
-
|
|
1542
|
-
# Shared Models — Zod Schema Rules
|
|
1543
|
-
|
|
1544
|
-
Files in this directory are the **single source of truth** for data models across the entire application.
|
|
1545
|
-
|
|
1546
|
-
## Rules
|
|
1547
|
-
|
|
1548
|
-
- Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
|
|
1549
|
-
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1550
|
-
\`\`\`typescript
|
|
1551
|
-
export const Todo = z.object({ ... });
|
|
1552
|
-
export type Todo = z.infer<typeof Todo>;
|
|
1553
|
-
\`\`\`
|
|
1554
|
-
- Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
|
|
1555
|
-
- Export a \`displayName\` string constant for UI display.
|
|
1556
|
-
- Re-export every model from \`shared/index.ts\`.
|
|
1557
|
-
- For relationships, use **nested schemas** (import and embed the related schema), not ID references.
|
|
1558
|
-
- After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
|
|
1822
|
+
output configName string = staticWebAppConfig.name
|
|
1559
1823
|
`;
|
|
1560
|
-
fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
|
|
1561
|
-
// 4b. app/api — BFF layer
|
|
1562
|
-
const bffInstructions = `---
|
|
1563
|
-
applyTo: "app/api/**"
|
|
1564
|
-
---
|
|
1565
|
-
|
|
1566
|
-
# BFF API Routes — Rules
|
|
1567
|
-
|
|
1568
|
-
Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
|
|
1569
|
-
|
|
1570
|
-
## Rules
|
|
1571
|
-
|
|
1572
|
-
- **Never** put business logic, database access, or direct Cosmos DB calls here.
|
|
1573
|
-
- Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
|
|
1574
|
-
- You may validate input/output with Zod schemas before forwarding.
|
|
1575
|
-
- Import schemas from \`@${projectName}/shared\`.
|
|
1576
|
-
|
|
1577
|
-
## Pattern
|
|
1578
|
-
|
|
1579
|
-
\`\`\`typescript
|
|
1580
|
-
import { callFunction } from '@/lib/api/call-function';
|
|
1581
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1582
|
-
import { z } from 'zod/v4';
|
|
1583
|
-
|
|
1584
|
-
export async function GET() {
|
|
1585
|
-
return callFunction({
|
|
1586
|
-
method: 'GET',
|
|
1587
|
-
path: '/api/{model}',
|
|
1588
|
-
responseSchema: z.array(ModelSchema),
|
|
1589
|
-
});
|
|
1590
1824
|
}
|
|
1591
|
-
|
|
1592
|
-
`;
|
|
1593
|
-
fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
|
|
1594
|
-
// 4c. functions — Azure Functions backend layer
|
|
1595
|
-
const functionsInstructions = `---
|
|
1596
|
-
applyTo: "functions/**"
|
|
1597
|
-
---
|
|
1598
|
-
|
|
1599
|
-
# Azure Functions — Backend Rules
|
|
1600
|
-
|
|
1601
|
-
Files in \`functions/src/\` contain all business logic and data access for this application.
|
|
1602
|
-
|
|
1603
|
-
## Rules
|
|
1604
|
-
|
|
1605
|
-
- Use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
|
|
1606
|
-
- Use the Cosmos DB SDK client directly **only** for delete operations.
|
|
1607
|
-
- Validate all request data against Zod schemas from \`@${projectName}/shared\` before writing.
|
|
1608
|
-
- Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
|
|
1609
|
-
- Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
|
|
1610
|
-
|
|
1611
|
-
## Handler Pattern
|
|
1612
|
-
|
|
1613
|
-
\`\`\`typescript
|
|
1614
|
-
import { app } from '@azure/functions';
|
|
1615
|
-
import { ModelSchema } from '@${projectName}/shared';
|
|
1616
|
-
|
|
1617
|
-
app.http('{model}-get-all', {
|
|
1618
|
-
methods: ['GET'],
|
|
1619
|
-
route: '{model}',
|
|
1620
|
-
authLevel: 'anonymous',
|
|
1621
|
-
extraInputs: [cosmosInput],
|
|
1622
|
-
handler: async (request, context) => {
|
|
1623
|
-
const documents = context.extraInputs.get(cosmosInput);
|
|
1624
|
-
const validated = z.array(ModelSchema).parse(documents);
|
|
1625
|
-
return { status: 200, jsonBody: validated };
|
|
1626
|
-
},
|
|
1627
|
-
});
|
|
1628
|
-
\`\`\`
|
|
1629
|
-
`;
|
|
1630
|
-
fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
|
|
1631
|
-
console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
|
|
1632
|
-
console.log(' - shared-models.instructions.md');
|
|
1633
|
-
console.log(' - bff-routes.instructions.md');
|
|
1634
|
-
console.log(' - azure-functions.instructions.md');
|
|
1635
|
-
console.log('\n✅ AI agent files created\n');
|
|
1636
|
-
console.log(' Supported agents:');
|
|
1637
|
-
console.log(' - OpenAI Codex → AGENTS.md');
|
|
1638
|
-
console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
|
|
1639
|
-
console.log(' - GitHub Copilot → .github/copilot-instructions.md');
|
|
1640
|
-
console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
|
|
1641
|
-
console.log('');
|
|
1642
|
-
}
|
|
1643
|
-
async function createInfrastructure(projectDir, projectName, azureConfig) {
|
|
1825
|
+
async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
|
|
1644
1826
|
console.log('📦 Creating infrastructure files (Bicep)...\n');
|
|
1645
1827
|
const infraDir = path.join(projectDir, 'infra');
|
|
1646
1828
|
const modulesDir = path.join(infraDir, 'modules');
|
|
1647
1829
|
fs.mkdirSync(modulesDir, { recursive: true });
|
|
1648
1830
|
const enableVNet = azureConfig.vnetOption !== 'none';
|
|
1831
|
+
const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
|
|
1832
|
+
const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
|
|
1649
1833
|
// main.bicep
|
|
1650
1834
|
const mainBicep = `targetScope = 'resourceGroup'
|
|
1651
1835
|
|
|
@@ -1658,6 +1842,13 @@ param location string = resourceGroup().location
|
|
|
1658
1842
|
@description('Location for Static Web App (must be explicitly provided)')
|
|
1659
1843
|
param swaLocation string
|
|
1660
1844
|
|
|
1845
|
+
@description('Static Web Apps hosting plan')
|
|
1846
|
+
@allowed([
|
|
1847
|
+
'Free'
|
|
1848
|
+
'Standard'
|
|
1849
|
+
])
|
|
1850
|
+
param staticWebAppSku string = '${staticWebAppSku}'
|
|
1851
|
+
|
|
1661
1852
|
@description('Cosmos DB mode')
|
|
1662
1853
|
@allowed(['freetier', 'serverless'])
|
|
1663
1854
|
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
@@ -1700,7 +1891,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
|
1700
1891
|
params: {
|
|
1701
1892
|
name: 'swa-\${projectName}'
|
|
1702
1893
|
location: swaLocation
|
|
1703
|
-
sku:
|
|
1894
|
+
sku: staticWebAppSku
|
|
1704
1895
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1705
1896
|
}
|
|
1706
1897
|
}
|
|
@@ -1741,35 +1932,30 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
|
|
|
1741
1932
|
params: {
|
|
1742
1933
|
name: 'pe-cosmos-\${projectName}'
|
|
1743
1934
|
location: location
|
|
1744
|
-
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1745
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1746
|
-
subnetId: vnet
|
|
1747
|
-
vnetId: vnet
|
|
1935
|
+
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
|
|
1936
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
1937
|
+
subnetId: vnet!.outputs.privateEndpointSubnetId
|
|
1938
|
+
vnetId: vnet!.outputs.id
|
|
1748
1939
|
}
|
|
1749
|
-
dependsOn: [
|
|
1750
|
-
cosmosDbFreeTier
|
|
1751
|
-
cosmosDbServerless
|
|
1752
|
-
vnet
|
|
1753
|
-
]
|
|
1754
1940
|
}
|
|
1755
1941
|
|
|
1756
1942
|
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
1757
1943
|
module functionsFlex 'modules/functions-flex.bicep' = {
|
|
1758
1944
|
name: 'functionsApp'
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1945
|
+
params: {
|
|
1946
|
+
name: 'func-\${projectName}'
|
|
1947
|
+
location: location
|
|
1948
|
+
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
1949
|
+
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
1950
|
+
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
1951
|
+
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
1952
|
+
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
1953
|
+
functionsRuntimeName: '${functionsRuntime.name}'
|
|
1954
|
+
functionsRuntimeVersion: '${functionsRuntime.version}'
|
|
1955
|
+
enableVNet: enableVNet
|
|
1956
|
+
vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
|
|
1769
1957
|
}
|
|
1770
1958
|
dependsOn: [
|
|
1771
|
-
cosmosDbFreeTier
|
|
1772
|
-
cosmosDbServerless
|
|
1773
1959
|
cosmosPrivateEndpoint
|
|
1774
1960
|
]
|
|
1775
1961
|
}
|
|
@@ -1778,23 +1964,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
1778
1964
|
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
1779
1965
|
name: 'cosmosDbRoleAssignment'
|
|
1780
1966
|
params: {
|
|
1781
|
-
cosmosAccountName: cosmosDbFreeTier
|
|
1967
|
+
cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
|
|
1782
1968
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1783
1969
|
}
|
|
1784
|
-
dependsOn: [
|
|
1785
|
-
functionsFlex
|
|
1786
|
-
]
|
|
1787
1970
|
}
|
|
1788
1971
|
|
|
1789
1972
|
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
1790
1973
|
name: 'cosmosDbRoleAssignment'
|
|
1791
1974
|
params: {
|
|
1792
|
-
cosmosAccountName: cosmosDbServerless
|
|
1975
|
+
cosmosAccountName: cosmosDbServerless!.outputs.accountName
|
|
1793
1976
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1794
1977
|
}
|
|
1795
|
-
dependsOn: [
|
|
1796
|
-
functionsFlex
|
|
1797
|
-
]
|
|
1798
1978
|
}
|
|
1799
1979
|
|
|
1800
1980
|
// Update SWA config with Functions hostname (after Functions deployment)
|
|
@@ -1803,20 +1983,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
|
1803
1983
|
params: {
|
|
1804
1984
|
staticWebAppName: staticWebApp.outputs.name
|
|
1805
1985
|
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
1986
|
+
functionsAppName: functionsFlex.outputs.name
|
|
1806
1987
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1807
1988
|
}
|
|
1808
|
-
dependsOn: [
|
|
1809
|
-
functionsFlex
|
|
1810
|
-
]
|
|
1811
1989
|
}
|
|
1812
1990
|
|
|
1813
1991
|
output staticWebAppName string = staticWebApp.outputs.name
|
|
1814
1992
|
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
1815
1993
|
output functionsAppName string = functionsFlex.outputs.name
|
|
1816
1994
|
output functionsAppUrl string = functionsFlex.outputs.defaultHostname
|
|
1817
|
-
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1818
|
-
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1819
|
-
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1995
|
+
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
1996
|
+
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
1997
|
+
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
1820
1998
|
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
1821
1999
|
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
1822
2000
|
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
@@ -1824,7 +2002,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
|
|
|
1824
2002
|
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
1825
2003
|
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
1826
2004
|
output vnetEnabled bool = enableVNet
|
|
1827
|
-
output vnetName string = enableVNet ? vnet
|
|
2005
|
+
output vnetName string = enableVNet ? vnet!.outputs.name : ''
|
|
1828
2006
|
`;
|
|
1829
2007
|
fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
|
|
1830
2008
|
// main.parameters.json
|
|
@@ -1838,6 +2016,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
|
1838
2016
|
"cosmosDbMode": {
|
|
1839
2017
|
"value": "${azureConfig.cosmosDbMode}"
|
|
1840
2018
|
},
|
|
2019
|
+
"staticWebAppSku": {
|
|
2020
|
+
"value": "${staticWebAppSku}"
|
|
2021
|
+
},
|
|
1841
2022
|
"enableVNet": {
|
|
1842
2023
|
"value": ${enableVNet}
|
|
1843
2024
|
}
|
|
@@ -1892,38 +2073,14 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
|
1892
2073
|
`;
|
|
1893
2074
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
1894
2075
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
1895
|
-
const staticWebAppConfigBicep =
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
param
|
|
1900
|
-
|
|
1901
|
-
@description('Application Insights connection string for SWA')
|
|
1902
|
-
param appInsightsConnectionString string
|
|
2076
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
2077
|
+
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
2078
|
+
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
2079
|
+
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
2080
|
+
param name string
|
|
1903
2081
|
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1909
|
-
parent: staticWebApp
|
|
1910
|
-
name: 'appsettings'
|
|
1911
|
-
properties: {
|
|
1912
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1913
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1914
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
output configName string = staticWebAppConfig.name
|
|
1919
|
-
`;
|
|
1920
|
-
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
1921
|
-
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
1922
|
-
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
1923
|
-
param name string
|
|
1924
|
-
|
|
1925
|
-
@description('Location for Log Analytics workspace')
|
|
1926
|
-
param location string
|
|
2082
|
+
@description('Location for Log Analytics workspace')
|
|
2083
|
+
param location string
|
|
1927
2084
|
|
|
1928
2085
|
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
1929
2086
|
name: name
|
|
@@ -1996,6 +2153,12 @@ param enableVNet bool = false
|
|
|
1996
2153
|
@description('VNet subnet ID for Functions (required if enableVNet is true)')
|
|
1997
2154
|
param vnetSubnetId string = ''
|
|
1998
2155
|
|
|
2156
|
+
@description('Functions runtime name')
|
|
2157
|
+
param functionsRuntimeName string
|
|
2158
|
+
|
|
2159
|
+
@description('Functions runtime version')
|
|
2160
|
+
param functionsRuntimeVersion string
|
|
2161
|
+
|
|
1999
2162
|
// Storage Account for Functions
|
|
2000
2163
|
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
|
2001
2164
|
name: storageAccountName
|
|
@@ -2066,8 +2229,8 @@ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
|
|
|
2066
2229
|
instanceMemoryMB: 2048
|
|
2067
2230
|
}
|
|
2068
2231
|
runtime: {
|
|
2069
|
-
name:
|
|
2070
|
-
version:
|
|
2232
|
+
name: functionsRuntimeName
|
|
2233
|
+
version: functionsRuntimeVersion
|
|
2071
2234
|
}
|
|
2072
2235
|
}
|
|
2073
2236
|
siteConfig: {
|
|
@@ -2129,63 +2292,7 @@ output principalId string = functionApp.identity.principalId
|
|
|
2129
2292
|
`;
|
|
2130
2293
|
fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
|
|
2131
2294
|
// modules/cosmosdb-freetier.bicep (Free Tier)
|
|
2132
|
-
const cosmosDbFreeTierBicep =
|
|
2133
|
-
param accountName string
|
|
2134
|
-
|
|
2135
|
-
@description('Database name')
|
|
2136
|
-
param databaseName string
|
|
2137
|
-
|
|
2138
|
-
@description('Location for Cosmos DB')
|
|
2139
|
-
param location string
|
|
2140
|
-
|
|
2141
|
-
@description('Public network access')
|
|
2142
|
-
@allowed(['Enabled', 'Disabled'])
|
|
2143
|
-
param publicNetworkAccess string = 'Enabled'
|
|
2144
|
-
|
|
2145
|
-
// Cosmos DB Account (Free Tier)
|
|
2146
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
2147
|
-
name: accountName
|
|
2148
|
-
location: location
|
|
2149
|
-
kind: 'GlobalDocumentDB'
|
|
2150
|
-
properties: {
|
|
2151
|
-
databaseAccountOfferType: 'Standard'
|
|
2152
|
-
enableAutomaticFailover: false
|
|
2153
|
-
enableFreeTier: true
|
|
2154
|
-
publicNetworkAccess: publicNetworkAccess
|
|
2155
|
-
disableLocalAuth: true
|
|
2156
|
-
consistencyPolicy: {
|
|
2157
|
-
defaultConsistencyLevel: 'Session'
|
|
2158
|
-
}
|
|
2159
|
-
locations: [
|
|
2160
|
-
{
|
|
2161
|
-
locationName: location
|
|
2162
|
-
failoverPriority: 0
|
|
2163
|
-
isZoneRedundant: false
|
|
2164
|
-
}
|
|
2165
|
-
]
|
|
2166
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
// Cosmos DB Database
|
|
2171
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
2172
|
-
parent: cosmosAccount
|
|
2173
|
-
name: databaseName
|
|
2174
|
-
properties: {
|
|
2175
|
-
resource: {
|
|
2176
|
-
id: databaseName
|
|
2177
|
-
}
|
|
2178
|
-
options: {
|
|
2179
|
-
throughput: 1000
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
output id string = cosmosAccount.id
|
|
2185
|
-
output accountName string = cosmosAccount.name
|
|
2186
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
2187
|
-
output databaseName string = database.name
|
|
2188
|
-
`;
|
|
2295
|
+
const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
|
|
2189
2296
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
|
|
2190
2297
|
// modules/cosmosdb-serverless.bicep (Serverless)
|
|
2191
2298
|
const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
|
|
@@ -2275,10 +2382,11 @@ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignment
|
|
|
2275
2382
|
output roleAssignmentId string = roleAssignment.id
|
|
2276
2383
|
`;
|
|
2277
2384
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
|
|
2278
|
-
//
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2385
|
+
// Bicep resolves module files at compile time even when their deployment condition is false,
|
|
2386
|
+
// so these definitions must always be generated. main.bicep still controls deployment with
|
|
2387
|
+
// `if (enableVNet)`.
|
|
2388
|
+
// modules/vnet.bicep
|
|
2389
|
+
const vnetBicep = `@description('VNet name')
|
|
2282
2390
|
param name string
|
|
2283
2391
|
|
|
2284
2392
|
@description('Location for VNet')
|
|
@@ -2324,9 +2432,9 @@ output name string = vnet.name
|
|
|
2324
2432
|
output functionsSubnetId string = vnet.properties.subnets[0].id
|
|
2325
2433
|
output privateEndpointSubnetId string = vnet.properties.subnets[1].id
|
|
2326
2434
|
`;
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2435
|
+
fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
|
|
2436
|
+
// modules/private-endpoint-cosmos.bicep
|
|
2437
|
+
const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
|
|
2330
2438
|
param name string
|
|
2331
2439
|
|
|
2332
2440
|
@description('Location')
|
|
@@ -2404,73 +2512,85 @@ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneG
|
|
|
2404
2512
|
output privateEndpointId string = privateEndpoint.id
|
|
2405
2513
|
output privateDnsZoneId string = privateDnsZone.id
|
|
2406
2514
|
`;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
}
|
|
2515
|
+
fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
|
|
2516
|
+
console.log('✅ VNet module definitions created\n');
|
|
2410
2517
|
console.log('✅ Infrastructure files created\n');
|
|
2411
2518
|
}
|
|
2412
|
-
|
|
2413
|
-
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
2519
|
+
function getGitHubFunctionsWorkflow(pm, backendLanguage, projectPm = pm) {
|
|
2414
2520
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2415
2521
|
const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
|
|
2416
|
-
const
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2522
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2523
|
+
const commonSetup = ` - uses: actions/checkout@v4
|
|
2524
|
+
|
|
2525
|
+
${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
|
|
2526
|
+
- name: Setup Node.js
|
|
2527
|
+
uses: actions/setup-node@v4
|
|
2528
|
+
with:
|
|
2529
|
+
node-version: '22'
|
|
2530
|
+
cache: ${pm}
|
|
2531
|
+
cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
2532
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
2533
|
+
- name: Install dependencies
|
|
2534
|
+
run: |
|
|
2535
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2536
|
+
|
|
2537
|
+
- name: Build shared package
|
|
2538
|
+
run: |
|
|
2539
|
+
${pmCmd.runFilter('shared')} build
|
|
2540
|
+
`;
|
|
2541
|
+
if (backendLanguage === 'typescript') {
|
|
2542
|
+
return `name: Deploy Azure Functions
|
|
2420
2543
|
|
|
2421
2544
|
on:
|
|
2422
2545
|
push:
|
|
2423
2546
|
branches:
|
|
2424
2547
|
- main
|
|
2425
2548
|
paths:
|
|
2426
|
-
- '
|
|
2427
|
-
- 'components/**'
|
|
2428
|
-
- 'lib/**'
|
|
2549
|
+
- 'functions/**'
|
|
2429
2550
|
- 'shared/**'
|
|
2430
|
-
- 'public/**'
|
|
2431
|
-
- 'package.json'
|
|
2432
|
-
- 'next.config.js'
|
|
2433
|
-
- 'next.config.ts'
|
|
2434
|
-
workflow_dispatch:
|
|
2435
2551
|
pull_request:
|
|
2436
2552
|
branches:
|
|
2437
2553
|
- main
|
|
2438
2554
|
paths:
|
|
2439
|
-
- '
|
|
2440
|
-
- 'components/**'
|
|
2441
|
-
- 'lib/**'
|
|
2555
|
+
- 'functions/**'
|
|
2442
2556
|
- 'shared/**'
|
|
2443
|
-
|
|
2444
|
-
- 'package.json'
|
|
2445
|
-
- 'next.config.js'
|
|
2446
|
-
- 'next.config.ts'
|
|
2557
|
+
workflow_dispatch:
|
|
2447
2558
|
|
|
2448
2559
|
jobs:
|
|
2449
2560
|
build-and-deploy:
|
|
2450
2561
|
runs-on: ubuntu-latest
|
|
2451
|
-
name: Build and Deploy
|
|
2562
|
+
name: Build and Deploy Functions
|
|
2452
2563
|
|
|
2453
2564
|
steps:
|
|
2454
|
-
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2565
|
+
${commonSetup} - name: Build Functions
|
|
2566
|
+
run: |
|
|
2567
|
+
${pmCmd.runFilter('functions')} build
|
|
2457
2568
|
|
|
2458
|
-
- name:
|
|
2569
|
+
- name: Prepare functions for deployment
|
|
2570
|
+
run: |
|
|
2571
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2572
|
+
mkdir -p /tmp/fn-deps
|
|
2573
|
+
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2574
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2575
|
+
rm -rf ./functions/node_modules
|
|
2576
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2577
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2578
|
+
mkdir -p "$SHARED_DEST"
|
|
2579
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2580
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2581
|
+
|
|
2582
|
+
- name: Deploy to Azure Functions
|
|
2459
2583
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2460
|
-
uses: Azure/
|
|
2584
|
+
uses: Azure/functions-action@v1
|
|
2461
2585
|
with:
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
api_location: ''
|
|
2467
|
-
output_location: ''
|
|
2468
|
-
env:
|
|
2469
|
-
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
2586
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2587
|
+
package: './functions'
|
|
2588
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2589
|
+
sku: flexconsumption
|
|
2470
2590
|
`;
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2591
|
+
}
|
|
2592
|
+
if (backendLanguage === 'csharp') {
|
|
2593
|
+
return `name: Deploy Azure Functions
|
|
2474
2594
|
|
|
2475
2595
|
on:
|
|
2476
2596
|
push:
|
|
@@ -2493,37 +2613,57 @@ jobs:
|
|
|
2493
2613
|
name: Build and Deploy Functions
|
|
2494
2614
|
|
|
2495
2615
|
steps:
|
|
2496
|
-
-
|
|
2497
|
-
|
|
2498
|
-
- name: Setup Node.js
|
|
2499
|
-
uses: actions/setup-node@v4
|
|
2616
|
+
${commonSetup} - name: Setup .NET
|
|
2617
|
+
uses: actions/setup-dotnet@v4
|
|
2500
2618
|
with:
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
- name:
|
|
2504
|
-
run: |
|
|
2505
|
-
${pmCmd.ci}
|
|
2506
|
-
|
|
2507
|
-
- name: Build shared package
|
|
2508
|
-
run: |
|
|
2509
|
-
${pmCmd.runFilter('shared')} build
|
|
2510
|
-
|
|
2511
|
-
- name: Build Functions
|
|
2619
|
+
dotnet-version: '10.0.x'
|
|
2620
|
+
|
|
2621
|
+
- name: Publish Functions
|
|
2512
2622
|
run: |
|
|
2513
|
-
|
|
2623
|
+
dotnet publish ./functions -c Release -o ./functions/publish
|
|
2514
2624
|
|
|
2515
|
-
- name:
|
|
2625
|
+
- name: Deploy to Azure Functions
|
|
2626
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2627
|
+
uses: Azure/functions-action@v1
|
|
2628
|
+
with:
|
|
2629
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2630
|
+
package: './functions/publish'
|
|
2631
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2632
|
+
sku: flexconsumption
|
|
2633
|
+
`;
|
|
2634
|
+
}
|
|
2635
|
+
return `name: Deploy Azure Functions
|
|
2636
|
+
|
|
2637
|
+
on:
|
|
2638
|
+
push:
|
|
2639
|
+
branches:
|
|
2640
|
+
- main
|
|
2641
|
+
paths:
|
|
2642
|
+
- 'functions/**'
|
|
2643
|
+
- 'shared/**'
|
|
2644
|
+
pull_request:
|
|
2645
|
+
branches:
|
|
2646
|
+
- main
|
|
2647
|
+
paths:
|
|
2648
|
+
- 'functions/**'
|
|
2649
|
+
- 'shared/**'
|
|
2650
|
+
workflow_dispatch:
|
|
2651
|
+
|
|
2652
|
+
jobs:
|
|
2653
|
+
build-and-deploy:
|
|
2654
|
+
runs-on: ubuntu-latest
|
|
2655
|
+
name: Build and Deploy Functions
|
|
2656
|
+
|
|
2657
|
+
steps:
|
|
2658
|
+
${commonSetup} - name: Setup Python
|
|
2659
|
+
uses: actions/setup-python@v5
|
|
2660
|
+
with:
|
|
2661
|
+
python-version: '3.11'
|
|
2662
|
+
|
|
2663
|
+
- name: Install Functions dependencies
|
|
2516
2664
|
run: |
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2520
|
-
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2521
|
-
rm -rf ./functions/node_modules
|
|
2522
|
-
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2523
|
-
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2524
|
-
mkdir -p "$SHARED_DEST"
|
|
2525
|
-
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2526
|
-
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2665
|
+
python -m pip install --upgrade pip
|
|
2666
|
+
python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
|
|
2527
2667
|
|
|
2528
2668
|
- name: Deploy to Azure Functions
|
|
2529
2669
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
@@ -2534,29 +2674,34 @@ ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
|
|
|
2534
2674
|
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2535
2675
|
sku: flexconsumption
|
|
2536
2676
|
`;
|
|
2537
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
2538
|
-
console.log('✅ GitHub Actions workflows created\n');
|
|
2539
2677
|
}
|
|
2540
|
-
|
|
2541
|
-
console.log('📦 Creating Azure Pipelines...\n');
|
|
2678
|
+
function getAzureFunctionsPipeline(pm, backendLanguage, projectPm = pm) {
|
|
2542
2679
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2543
2680
|
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2681
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2682
|
+
const commonSetup = ` - task: NodeTool@0
|
|
2683
|
+
inputs:
|
|
2684
|
+
versionSpec: '22.x'
|
|
2685
|
+
displayName: 'Install Node.js'
|
|
2686
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2687
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
2688
|
+
- script: |
|
|
2689
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2690
|
+
displayName: 'Install workspace dependencies'
|
|
2691
|
+
|
|
2692
|
+
- script: |
|
|
2693
|
+
${pmCmd.runFilter('shared')} build
|
|
2694
|
+
displayName: 'Build shared package'
|
|
2695
|
+
`;
|
|
2696
|
+
if (backendLanguage === 'typescript') {
|
|
2697
|
+
return `trigger:
|
|
2548
2698
|
branches:
|
|
2549
2699
|
include:
|
|
2550
2700
|
- main
|
|
2551
2701
|
paths:
|
|
2552
2702
|
include:
|
|
2553
|
-
-
|
|
2554
|
-
- components/**
|
|
2555
|
-
- lib/**
|
|
2703
|
+
- functions/**
|
|
2556
2704
|
- shared/**
|
|
2557
|
-
- public/**
|
|
2558
|
-
- package.json
|
|
2559
|
-
- next.config.js
|
|
2560
2705
|
|
|
2561
2706
|
pr:
|
|
2562
2707
|
branches:
|
|
@@ -2564,13 +2709,8 @@ pr:
|
|
|
2564
2709
|
- main
|
|
2565
2710
|
paths:
|
|
2566
2711
|
include:
|
|
2567
|
-
-
|
|
2568
|
-
- components/**
|
|
2569
|
-
- lib/**
|
|
2712
|
+
- functions/**
|
|
2570
2713
|
- shared/**
|
|
2571
|
-
- public/**
|
|
2572
|
-
- package.json
|
|
2573
|
-
- next.config.js
|
|
2574
2714
|
|
|
2575
2715
|
pool:
|
|
2576
2716
|
vmImage: 'ubuntu-latest'
|
|
@@ -2579,33 +2719,51 @@ variables:
|
|
|
2579
2719
|
- group: azure-deployment
|
|
2580
2720
|
|
|
2581
2721
|
steps:
|
|
2582
|
-
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
displayName: 'Install Node.js'
|
|
2586
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2587
|
-
- script: |
|
|
2588
|
-
${pmCmd.ci}
|
|
2589
|
-
displayName: 'Install dependencies'
|
|
2722
|
+
${commonSetup} - script: |
|
|
2723
|
+
${pmCmd.runFilter('functions')} build
|
|
2724
|
+
displayName: 'Build Functions'
|
|
2590
2725
|
|
|
2591
2726
|
- script: |
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2727
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2728
|
+
mkdir -p /tmp/fn-deps
|
|
2729
|
+
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2730
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2731
|
+
rm -rf ./functions/node_modules
|
|
2732
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2733
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2734
|
+
mkdir -p "$SHARED_DEST"
|
|
2735
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2736
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2737
|
+
displayName: 'Prepare functions for deployment'
|
|
2738
|
+
|
|
2739
|
+
- task: ArchiveFiles@2
|
|
2740
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2741
|
+
inputs:
|
|
2742
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
|
|
2743
|
+
includeRootFolder: false
|
|
2744
|
+
archiveType: 'zip'
|
|
2745
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2746
|
+
displayName: 'Archive Functions'
|
|
2747
|
+
|
|
2748
|
+
- task: PublishBuildArtifacts@1
|
|
2749
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2750
|
+
inputs:
|
|
2751
|
+
PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2752
|
+
ArtifactName: 'functions'
|
|
2753
|
+
displayName: 'Publish Functions artifact'
|
|
2754
|
+
|
|
2755
|
+
- task: AzureFunctionApp@2
|
|
2756
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2757
|
+
inputs:
|
|
2758
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
2759
|
+
appType: 'functionAppLinux'
|
|
2760
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
2761
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2762
|
+
displayName: 'Deploy to Azure Functions'
|
|
2605
2763
|
`;
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2764
|
+
}
|
|
2765
|
+
if (backendLanguage === 'csharp') {
|
|
2766
|
+
return `trigger:
|
|
2609
2767
|
branches:
|
|
2610
2768
|
include:
|
|
2611
2769
|
- main
|
|
@@ -2630,35 +2788,68 @@ variables:
|
|
|
2630
2788
|
- group: azure-deployment
|
|
2631
2789
|
|
|
2632
2790
|
steps:
|
|
2633
|
-
- task:
|
|
2791
|
+
${commonSetup} - task: UseDotNet@2
|
|
2634
2792
|
inputs:
|
|
2635
|
-
|
|
2636
|
-
displayName: 'Install
|
|
2637
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2638
|
-
- script: |
|
|
2639
|
-
${pmCmd.ci}
|
|
2640
|
-
displayName: 'Install workspace dependencies'
|
|
2793
|
+
version: '10.0.x'
|
|
2794
|
+
displayName: 'Install .NET SDK'
|
|
2641
2795
|
|
|
2642
2796
|
- script: |
|
|
2643
|
-
|
|
2644
|
-
displayName: '
|
|
2797
|
+
dotnet publish ./functions -c Release -o ./functions/publish
|
|
2798
|
+
displayName: 'Publish Functions'
|
|
2645
2799
|
|
|
2646
|
-
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2800
|
+
- task: ArchiveFiles@2
|
|
2801
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2802
|
+
inputs:
|
|
2803
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions/publish'
|
|
2804
|
+
includeRootFolder: false
|
|
2805
|
+
archiveType: 'zip'
|
|
2806
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2807
|
+
displayName: 'Archive Functions'
|
|
2808
|
+
|
|
2809
|
+
- task: AzureFunctionApp@2
|
|
2810
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2811
|
+
inputs:
|
|
2812
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
2813
|
+
appType: 'functionAppLinux'
|
|
2814
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
2815
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2816
|
+
displayName: 'Deploy to Azure Functions'
|
|
2817
|
+
`;
|
|
2818
|
+
}
|
|
2819
|
+
return `trigger:
|
|
2820
|
+
branches:
|
|
2821
|
+
include:
|
|
2822
|
+
- main
|
|
2823
|
+
paths:
|
|
2824
|
+
include:
|
|
2825
|
+
- functions/**
|
|
2826
|
+
- shared/**
|
|
2827
|
+
|
|
2828
|
+
pr:
|
|
2829
|
+
branches:
|
|
2830
|
+
include:
|
|
2831
|
+
- main
|
|
2832
|
+
paths:
|
|
2833
|
+
include:
|
|
2834
|
+
- functions/**
|
|
2835
|
+
- shared/**
|
|
2836
|
+
|
|
2837
|
+
pool:
|
|
2838
|
+
vmImage: 'ubuntu-latest'
|
|
2839
|
+
|
|
2840
|
+
variables:
|
|
2841
|
+
- group: azure-deployment
|
|
2842
|
+
|
|
2843
|
+
steps:
|
|
2844
|
+
${commonSetup} - task: UsePythonVersion@0
|
|
2845
|
+
inputs:
|
|
2846
|
+
versionSpec: '3.11'
|
|
2847
|
+
displayName: 'Install Python'
|
|
2649
2848
|
|
|
2650
2849
|
- script: |
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2655
|
-
rm -rf ./functions/node_modules
|
|
2656
|
-
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2657
|
-
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2658
|
-
mkdir -p "$SHARED_DEST"
|
|
2659
|
-
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2660
|
-
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2661
|
-
displayName: 'Prepare functions for deployment'
|
|
2850
|
+
python -m pip install --upgrade pip
|
|
2851
|
+
python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
|
|
2852
|
+
displayName: 'Install Functions dependencies'
|
|
2662
2853
|
|
|
2663
2854
|
- task: ArchiveFiles@2
|
|
2664
2855
|
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
@@ -2669,13 +2860,6 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
|
2669
2860
|
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2670
2861
|
displayName: 'Archive Functions'
|
|
2671
2862
|
|
|
2672
|
-
- task: PublishBuildArtifacts@1
|
|
2673
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2674
|
-
inputs:
|
|
2675
|
-
PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2676
|
-
ArtifactName: 'functions'
|
|
2677
|
-
displayName: 'Publish Functions artifact'
|
|
2678
|
-
|
|
2679
2863
|
- task: AzureFunctionApp@2
|
|
2680
2864
|
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2681
2865
|
inputs:
|
|
@@ -2685,6 +2869,268 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
|
2685
2869
|
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2686
2870
|
displayName: 'Deploy to Azure Functions'
|
|
2687
2871
|
`;
|
|
2872
|
+
}
|
|
2873
|
+
function buildAzureSwaPipeline(pm, projectPm = pm) {
|
|
2874
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2875
|
+
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
2876
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2877
|
+
return `trigger:
|
|
2878
|
+
branches:
|
|
2879
|
+
include:
|
|
2880
|
+
- main
|
|
2881
|
+
paths:
|
|
2882
|
+
include:
|
|
2883
|
+
- app/**
|
|
2884
|
+
- components/**
|
|
2885
|
+
- lib/**
|
|
2886
|
+
- shared/**
|
|
2887
|
+
- public/**
|
|
2888
|
+
- package.json
|
|
2889
|
+
- next.config.js
|
|
2890
|
+
|
|
2891
|
+
pr:
|
|
2892
|
+
branches:
|
|
2893
|
+
include:
|
|
2894
|
+
- main
|
|
2895
|
+
paths:
|
|
2896
|
+
include:
|
|
2897
|
+
- app/**
|
|
2898
|
+
- components/**
|
|
2899
|
+
- lib/**
|
|
2900
|
+
- shared/**
|
|
2901
|
+
- public/**
|
|
2902
|
+
- package.json
|
|
2903
|
+
- next.config.js
|
|
2904
|
+
|
|
2905
|
+
pool:
|
|
2906
|
+
vmImage: 'ubuntu-latest'
|
|
2907
|
+
|
|
2908
|
+
variables:
|
|
2909
|
+
- group: azure-deployment
|
|
2910
|
+
|
|
2911
|
+
steps:
|
|
2912
|
+
- task: NodeTool@0
|
|
2913
|
+
inputs:
|
|
2914
|
+
versionSpec: '22.x'
|
|
2915
|
+
displayName: 'Install Node.js'
|
|
2916
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2917
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
2918
|
+
- script: |
|
|
2919
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2920
|
+
displayName: 'Install dependencies'
|
|
2921
|
+
|
|
2922
|
+
- script: |
|
|
2923
|
+
${pmCmd.run} build
|
|
2924
|
+
env:
|
|
2925
|
+
NODE_ENV: production
|
|
2926
|
+
displayName: 'Build Next.js app'
|
|
2927
|
+
|
|
2928
|
+
- task: AzureStaticWebApp@0
|
|
2929
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2930
|
+
inputs:
|
|
2931
|
+
app_location: '.'
|
|
2932
|
+
output_location: '.next/standalone'
|
|
2933
|
+
skip_app_build: true
|
|
2934
|
+
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
2935
|
+
displayName: 'Deploy to Azure Static Web Apps'
|
|
2936
|
+
`;
|
|
2937
|
+
}
|
|
2938
|
+
function getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm) {
|
|
2939
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
2940
|
+
return '';
|
|
2941
|
+
}
|
|
2942
|
+
return ` - script: |
|
|
2943
|
+
node <<'NODE'
|
|
2944
|
+
const fs = require('fs');
|
|
2945
|
+
|
|
2946
|
+
function readJson(path) {
|
|
2947
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function writeJson(path, value) {
|
|
2951
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
const sharedPackagePath = 'shared/package.json';
|
|
2955
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
2956
|
+
|
|
2957
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
2958
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
2959
|
+
if (!pkg[section]) continue;
|
|
2960
|
+
|
|
2961
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
2962
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
2963
|
+
pkg[section][name] = replacement;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
const root = readJson('package.json');
|
|
2970
|
+
root.workspaces = ['shared', 'functions'];
|
|
2971
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
2972
|
+
root.scripts = root.scripts || {};
|
|
2973
|
+
root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
|
|
2974
|
+
writeJson('package.json', root);
|
|
2975
|
+
|
|
2976
|
+
if (fs.existsSync('functions/package.json')) {
|
|
2977
|
+
const functionsPkg = readJson('functions/package.json');
|
|
2978
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
2979
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
2980
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
2981
|
+
}
|
|
2982
|
+
writeJson('functions/package.json', functionsPkg);
|
|
2983
|
+
}
|
|
2984
|
+
NODE
|
|
2985
|
+
|
|
2986
|
+
npm install --package-lock-only --ignore-scripts
|
|
2987
|
+
displayName: 'Normalize pnpm workspace for npm CI'`;
|
|
2988
|
+
}
|
|
2989
|
+
function getGitHubNpmWorkspaceNormalizationStep(pm, projectPm) {
|
|
2990
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
2991
|
+
return '';
|
|
2992
|
+
}
|
|
2993
|
+
return ` - name: Normalize pnpm workspace for npm CI
|
|
2994
|
+
shell: bash
|
|
2995
|
+
run: |
|
|
2996
|
+
node <<'NODE'
|
|
2997
|
+
const fs = require('fs');
|
|
2998
|
+
|
|
2999
|
+
function readJson(path) {
|
|
3000
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
function writeJson(path, value) {
|
|
3004
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
const sharedPackagePath = 'shared/package.json';
|
|
3008
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
3009
|
+
|
|
3010
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
3011
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
3012
|
+
if (!pkg[section]) continue;
|
|
3013
|
+
|
|
3014
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
3015
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
3016
|
+
pkg[section][name] = replacement;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
const root = readJson('package.json');
|
|
3023
|
+
root.workspaces = ['shared', 'functions'];
|
|
3024
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
3025
|
+
root.scripts = root.scripts || {};
|
|
3026
|
+
root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
|
|
3027
|
+
writeJson('package.json', root);
|
|
3028
|
+
|
|
3029
|
+
if (fs.existsSync('functions/package.json')) {
|
|
3030
|
+
const functionsPkg = readJson('functions/package.json');
|
|
3031
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
3032
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
3033
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
3034
|
+
}
|
|
3035
|
+
writeJson('functions/package.json', functionsPkg);
|
|
3036
|
+
}
|
|
3037
|
+
NODE
|
|
3038
|
+
|
|
3039
|
+
npm install --package-lock-only --ignore-scripts
|
|
3040
|
+
`;
|
|
3041
|
+
}
|
|
3042
|
+
async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
|
|
3043
|
+
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
3044
|
+
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
3045
|
+
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
3046
|
+
const actionsPm = pm;
|
|
3047
|
+
const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
|
|
3048
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
3049
|
+
// deploy-functions.yml
|
|
3050
|
+
const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
|
|
3051
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
3052
|
+
console.log('✅ GitHub Actions workflows created\n');
|
|
3053
|
+
}
|
|
3054
|
+
function buildGitHubSwaWorkflow(pm, projectPm = pm) {
|
|
3055
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
3056
|
+
return `name: Deploy Static Web App
|
|
3057
|
+
|
|
3058
|
+
on:
|
|
3059
|
+
push:
|
|
3060
|
+
branches:
|
|
3061
|
+
- main
|
|
3062
|
+
paths:
|
|
3063
|
+
- 'app/**'
|
|
3064
|
+
- 'components/**'
|
|
3065
|
+
- 'lib/**'
|
|
3066
|
+
- 'shared/**'
|
|
3067
|
+
- 'public/**'
|
|
3068
|
+
- 'package.json'
|
|
3069
|
+
- '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3070
|
+
- 'next.config.js'
|
|
3071
|
+
- 'next.config.ts'
|
|
3072
|
+
workflow_dispatch:
|
|
3073
|
+
pull_request:
|
|
3074
|
+
branches:
|
|
3075
|
+
- main
|
|
3076
|
+
paths:
|
|
3077
|
+
- 'app/**'
|
|
3078
|
+
- 'components/**'
|
|
3079
|
+
- 'lib/**'
|
|
3080
|
+
- 'shared/**'
|
|
3081
|
+
- 'public/**'
|
|
3082
|
+
- 'package.json'
|
|
3083
|
+
- '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3084
|
+
- 'next.config.js'
|
|
3085
|
+
- 'next.config.ts'
|
|
3086
|
+
|
|
3087
|
+
jobs:
|
|
3088
|
+
build-and-deploy:
|
|
3089
|
+
runs-on: ubuntu-latest
|
|
3090
|
+
name: Build and Deploy Static Web App
|
|
3091
|
+
|
|
3092
|
+
steps:
|
|
3093
|
+
- uses: actions/checkout@v4
|
|
3094
|
+
with:
|
|
3095
|
+
submodules: true
|
|
3096
|
+
|
|
3097
|
+
${(0, package_manager_1.getCiSetupStep)(pm) ? `${(0, package_manager_1.getCiSetupStep)(pm)}\n` : ''}
|
|
3098
|
+
- name: Setup Node.js
|
|
3099
|
+
uses: actions/setup-node@v4
|
|
3100
|
+
with:
|
|
3101
|
+
node-version: '22'
|
|
3102
|
+
cache: ${pm}
|
|
3103
|
+
cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
3104
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
3105
|
+
- name: Install and build app
|
|
3106
|
+
run: |
|
|
3107
|
+
${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}
|
|
3108
|
+
|
|
3109
|
+
- name: Deploy to Azure Static Web Apps
|
|
3110
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
3111
|
+
uses: Azure/static-web-apps-deploy@v1
|
|
3112
|
+
with:
|
|
3113
|
+
azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
|
3114
|
+
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
3115
|
+
action: 'upload'
|
|
3116
|
+
app_location: '/'
|
|
3117
|
+
api_location: ''
|
|
3118
|
+
output_location: ''
|
|
3119
|
+
app_build_command: '${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}'
|
|
3120
|
+
env:
|
|
3121
|
+
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
3122
|
+
`;
|
|
3123
|
+
}
|
|
3124
|
+
async function createAzurePipelines(projectDir, pm, backendLanguage) {
|
|
3125
|
+
console.log('📦 Creating Azure Pipelines...\n');
|
|
3126
|
+
const pipelinesPm = pm;
|
|
3127
|
+
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
3128
|
+
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
3129
|
+
// swa.yml
|
|
3130
|
+
const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
|
|
3131
|
+
fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
|
|
3132
|
+
// functions.yml
|
|
3133
|
+
const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
|
|
2688
3134
|
fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
|
|
2689
3135
|
console.log('✅ Azure Pipelines created\n');
|
|
2690
3136
|
}
|