swallowkit 1.0.0-beta.6 → 1.0.0-beta.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +86 -177
- package/README.md +86 -178
- package/dist/__tests__/fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +101 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/add-auth.d.ts +15 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +790 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +163 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
- package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
- package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +19 -81
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +62 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +474 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +38 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +733 -155
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +78 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +1453 -898
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +24 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +473 -90
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +333 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +14 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +179 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/auth-operations.d.ts +61 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +272 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +185 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/file-session.d.ts +56 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +203 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +37 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +248 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +51 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +187 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/agent-instructions.d.ts +29 -0
- package/dist/core/project/agent-instructions.d.ts.map +1 -0
- package/dist/core/project/agent-instructions.js +159 -0
- package/dist/core/project/agent-instructions.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +274 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +299 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1774 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +771 -69
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +21 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +243 -43
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +753 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +153 -9
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +17 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +174 -34
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +69 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +412 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +24 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +14 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +42 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +566 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +425 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +109 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +5 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +30 -9
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -5
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +206 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/agent-instructions.test.ts +237 -0
- package/src/__tests__/agent-loop-phase34.test.ts +513 -0
- package/src/__tests__/auth.test.ts +770 -0
- package/src/__tests__/bff-swa-auth.test.ts +275 -0
- package/src/__tests__/config.test.ts +295 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
- package/src/__tests__/dev-seeds.test.ts +236 -0
- package/src/__tests__/dev.test.ts +296 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/fixtures.ts +184 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +411 -0
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +371 -0
- package/src/__tests__/mcp.test.ts +253 -0
- package/src/__tests__/model-parser.test.ts +81 -0
- package/src/__tests__/multi-auth.test.ts +160 -0
- package/src/__tests__/nextjs-generator.test.ts +105 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +227 -0
- package/src/__tests__/plan-apply.test.ts +499 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +302 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +248 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +853 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +629 -0
- package/src/cli/commands/dev.ts +1252 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3541 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1081 -0
- package/src/cli/index.ts +350 -0
- package/src/core/config.ts +380 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/auth-operations.ts +360 -0
- package/src/core/operations/create-model.ts +191 -0
- package/src/core/operations/file-session.ts +222 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +258 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +77 -0
- package/src/core/operations/scaffold-plan.ts +215 -0
- package/src/core/project/agent-instructions.ts +167 -0
- package/src/core/project/agent-templates/AGENTS.md +502 -0
- package/src/core/project/agent-templates/CLAUDE.md +31 -0
- package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
- package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
- package/src/core/project/agent-templates/copilot-instructions.md +27 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/validation.ts +295 -0
- package/src/core/project/workflows.ts +295 -0
- package/src/core/scaffold/auth-generator.ts +1824 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +972 -0
- package/src/core/scaffold/model-parser.ts +854 -0
- package/src/core/scaffold/native-schema-generator.ts +904 -0
- package/src/core/scaffold/nextjs-generator.ts +382 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1100 -0
- package/src/core/verify/index.ts +468 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +33 -0
- package/src/machine/errors.ts +45 -0
- package/src/machine/index.ts +690 -0
- package/src/mcp/index.ts +423 -0
- package/src/types/index.ts +182 -0
- package/src/utils/package-manager.ts +246 -0
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
|
@@ -36,12 +36,160 @@ 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.approveBuildsViaWorkspaceYaml = approveBuildsViaWorkspaceYaml;
|
|
46
|
+
exports.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
|
|
47
|
+
exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
|
|
48
|
+
exports.buildCSharpFunctionsProjectSource = buildCSharpFunctionsProjectSource;
|
|
49
|
+
exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
|
|
50
|
+
exports.buildGeneratedProjectDependencies = buildGeneratedProjectDependencies;
|
|
51
|
+
exports.buildGeneratedProjectDevDependencies = buildGeneratedProjectDevDependencies;
|
|
52
|
+
exports.buildSwallowKitMcpProjectConfigSource = buildSwallowKitMcpProjectConfigSource;
|
|
53
|
+
exports.buildTypeScriptFunctionsPackageJson = buildTypeScriptFunctionsPackageJson;
|
|
54
|
+
exports.buildFunctionsHostKeyBicepExpression = buildFunctionsHostKeyBicepExpression;
|
|
55
|
+
exports.buildStaticWebAppConfigBicepSource = buildStaticWebAppConfigBicepSource;
|
|
56
|
+
exports.createInfrastructure = createInfrastructure;
|
|
57
|
+
exports.getGitHubFunctionsWorkflow = getGitHubFunctionsWorkflow;
|
|
58
|
+
exports.getAzureFunctionsPipeline = getAzureFunctionsPipeline;
|
|
59
|
+
exports.buildAzureSwaPipeline = buildAzureSwaPipeline;
|
|
60
|
+
exports.buildGitHubSwaWorkflow = buildGitHubSwaWorkflow;
|
|
40
61
|
const fs = __importStar(require("fs"));
|
|
41
62
|
const path = __importStar(require("path"));
|
|
42
63
|
const child_process_1 = require("child_process");
|
|
43
64
|
const prompts_1 = __importDefault(require("prompts"));
|
|
44
65
|
const package_manager_1 = require("../../utils/package-manager");
|
|
66
|
+
const manifest_1 = require("../../core/project/manifest");
|
|
67
|
+
const workflows_1 = require("../../core/project/workflows");
|
|
68
|
+
const agent_instructions_1 = require("../../core/project/agent-instructions");
|
|
69
|
+
const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
|
|
70
|
+
const python_uv_1 = require("../../utils/python-uv");
|
|
71
|
+
function getStaticWebAppSku(plan) {
|
|
72
|
+
return plan === 'free' ? 'Free' : 'Standard';
|
|
73
|
+
}
|
|
74
|
+
const BACKEND_LANGUAGE_CHOICES = [
|
|
75
|
+
{ title: "TypeScript", value: "typescript" },
|
|
76
|
+
{ title: "C#", value: "csharp" },
|
|
77
|
+
{ title: "Python", value: "python" },
|
|
78
|
+
];
|
|
79
|
+
function usesNodeFunctionsProject(backendLanguage) {
|
|
80
|
+
return backendLanguage === "typescript";
|
|
81
|
+
}
|
|
82
|
+
function getBackendLanguageLabel(backendLanguage) {
|
|
83
|
+
return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
|
|
84
|
+
}
|
|
85
|
+
function buildCosmosDbFreeTierBicepSource() {
|
|
86
|
+
return `@description('Cosmos DB account name')
|
|
87
|
+
param accountName string
|
|
88
|
+
|
|
89
|
+
@description('Database name')
|
|
90
|
+
param databaseName string
|
|
91
|
+
|
|
92
|
+
@description('Location for Cosmos DB')
|
|
93
|
+
param location string
|
|
94
|
+
|
|
95
|
+
@description('Public network access')
|
|
96
|
+
@allowed(['Enabled', 'Disabled'])
|
|
97
|
+
param publicNetworkAccess string = 'Enabled'
|
|
98
|
+
|
|
99
|
+
// Cosmos DB Account (Free Tier)
|
|
100
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
101
|
+
name: accountName
|
|
102
|
+
location: location
|
|
103
|
+
kind: 'GlobalDocumentDB'
|
|
104
|
+
properties: {
|
|
105
|
+
databaseAccountOfferType: 'Standard'
|
|
106
|
+
enableAutomaticFailover: false
|
|
107
|
+
enableFreeTier: true
|
|
108
|
+
publicNetworkAccess: publicNetworkAccess
|
|
109
|
+
disableLocalAuth: true
|
|
110
|
+
consistencyPolicy: {
|
|
111
|
+
defaultConsistencyLevel: 'Session'
|
|
112
|
+
}
|
|
113
|
+
locations: [
|
|
114
|
+
{
|
|
115
|
+
locationName: location
|
|
116
|
+
failoverPriority: 0
|
|
117
|
+
isZoneRedundant: false
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cosmos DB Database
|
|
125
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
126
|
+
parent: cosmosAccount
|
|
127
|
+
name: databaseName
|
|
128
|
+
properties: {
|
|
129
|
+
resource: {
|
|
130
|
+
id: databaseName
|
|
131
|
+
}
|
|
132
|
+
options: {
|
|
133
|
+
throughput: 1000
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
output id string = cosmosAccount.id
|
|
139
|
+
output accountName string = cosmosAccount.name
|
|
140
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
141
|
+
output databaseName string = database.name
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
function getFunctionsWorkerRuntime(backendLanguage) {
|
|
145
|
+
if (backendLanguage === "csharp") {
|
|
146
|
+
return "dotnet-isolated";
|
|
147
|
+
}
|
|
148
|
+
if (backendLanguage === "python") {
|
|
149
|
+
return "python";
|
|
150
|
+
}
|
|
151
|
+
return "node";
|
|
152
|
+
}
|
|
153
|
+
function getFunctionsRuntimeConfig(backendLanguage) {
|
|
154
|
+
if (backendLanguage === "csharp") {
|
|
155
|
+
return { name: "dotnet-isolated", version: "10.0" };
|
|
156
|
+
}
|
|
157
|
+
if (backendLanguage === "python") {
|
|
158
|
+
return { name: "python", version: "3.11" };
|
|
159
|
+
}
|
|
160
|
+
return { name: "node", version: "22" };
|
|
161
|
+
}
|
|
162
|
+
function buildSharedTsConfig() {
|
|
163
|
+
return {
|
|
164
|
+
compilerOptions: {
|
|
165
|
+
target: "ES2020",
|
|
166
|
+
module: "Node16",
|
|
167
|
+
moduleResolution: "node16",
|
|
168
|
+
lib: ["ES2020"],
|
|
169
|
+
outDir: "dist",
|
|
170
|
+
rootDir: ".",
|
|
171
|
+
declaration: true,
|
|
172
|
+
declarationMap: true,
|
|
173
|
+
sourceMap: true,
|
|
174
|
+
strict: true,
|
|
175
|
+
esModuleInterop: true,
|
|
176
|
+
skipLibCheck: true,
|
|
177
|
+
forceConsistentCasingInFileNames: true,
|
|
178
|
+
},
|
|
179
|
+
include: ["index.ts", "models/**/*"],
|
|
180
|
+
exclude: ["node_modules", "dist"],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function promptBackendLanguage() {
|
|
184
|
+
const response = await (0, prompts_1.default)({
|
|
185
|
+
type: "select",
|
|
186
|
+
name: "backendLanguage",
|
|
187
|
+
message: "Azure Functions backend language:",
|
|
188
|
+
choices: BACKEND_LANGUAGE_CHOICES,
|
|
189
|
+
initial: 0,
|
|
190
|
+
});
|
|
191
|
+
return response.backendLanguage || "typescript";
|
|
192
|
+
}
|
|
45
193
|
async function promptCiCd() {
|
|
46
194
|
const response = await (0, prompts_1.default)({
|
|
47
195
|
type: 'select',
|
|
@@ -56,40 +204,69 @@ async function promptCiCd() {
|
|
|
56
204
|
});
|
|
57
205
|
return response.cicd || 'skip';
|
|
58
206
|
}
|
|
59
|
-
async function promptAzureConfig() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
207
|
+
async function promptAzureConfig(options) {
|
|
208
|
+
let swaPlan = options.swaPlan;
|
|
209
|
+
if (!swaPlan) {
|
|
210
|
+
const response = await (0, prompts_1.default)({
|
|
211
|
+
type: 'select',
|
|
212
|
+
name: 'plan',
|
|
213
|
+
message: 'Azure Static Web Apps plan:',
|
|
214
|
+
choices: [
|
|
215
|
+
{ title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
|
|
216
|
+
{ title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
|
|
217
|
+
],
|
|
218
|
+
initial: 0
|
|
219
|
+
});
|
|
220
|
+
swaPlan = response.plan || 'standard';
|
|
221
|
+
}
|
|
222
|
+
let cosmosDbMode = options.cosmosDbMode;
|
|
223
|
+
if (!cosmosDbMode) {
|
|
224
|
+
const response = await (0, prompts_1.default)({
|
|
225
|
+
type: 'select',
|
|
226
|
+
name: 'mode',
|
|
227
|
+
message: 'Cosmos DB mode (affects cost):',
|
|
228
|
+
choices: [
|
|
229
|
+
{ title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
|
|
230
|
+
{ title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
|
|
231
|
+
],
|
|
232
|
+
initial: 0
|
|
233
|
+
});
|
|
234
|
+
cosmosDbMode = response.mode || 'freetier';
|
|
235
|
+
}
|
|
236
|
+
let vnetOption = options.vnet;
|
|
237
|
+
if (!vnetOption) {
|
|
238
|
+
const response = await (0, prompts_1.default)({
|
|
239
|
+
type: 'select',
|
|
240
|
+
name: 'vnet',
|
|
241
|
+
message: 'Network security:',
|
|
242
|
+
choices: [
|
|
243
|
+
{ title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
|
|
244
|
+
{ title: 'None - Public endpoints, simplest but least secure', value: 'none' }
|
|
245
|
+
],
|
|
246
|
+
initial: 0
|
|
247
|
+
});
|
|
248
|
+
vnetOption = response.vnet || 'outbound';
|
|
249
|
+
}
|
|
80
250
|
return {
|
|
81
|
-
cosmosDbMode:
|
|
82
|
-
vnetOption:
|
|
251
|
+
cosmosDbMode: cosmosDbMode || 'freetier',
|
|
252
|
+
vnetOption: vnetOption || 'outbound',
|
|
253
|
+
swaPlan: swaPlan || 'standard'
|
|
83
254
|
};
|
|
84
255
|
}
|
|
85
256
|
const VALID_CICD = ['github', 'azure', 'skip'];
|
|
257
|
+
const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
|
|
86
258
|
const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
|
|
87
259
|
const VALID_VNET = ['none', 'outbound'];
|
|
260
|
+
const VALID_SWA_PLAN = ['free', 'standard'];
|
|
88
261
|
function validateInitFlags(options) {
|
|
89
262
|
if (options.cicd && !VALID_CICD.includes(options.cicd)) {
|
|
90
263
|
console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
|
|
91
264
|
process.exit(1);
|
|
92
265
|
}
|
|
266
|
+
if (options.backendLanguage && !VALID_BACKEND_LANGUAGE.includes(options.backendLanguage)) {
|
|
267
|
+
console.error(`❌ Invalid --backend-language value: "${options.backendLanguage}". Must be: ${VALID_BACKEND_LANGUAGE.join(', ')}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
93
270
|
if (options.cosmosDbMode && !VALID_COSMOS_DB_MODE.includes(options.cosmosDbMode)) {
|
|
94
271
|
console.error(`❌ Invalid --cosmos-db-mode value: "${options.cosmosDbMode}". Must be: ${VALID_COSMOS_DB_MODE.join(', ')}`);
|
|
95
272
|
process.exit(1);
|
|
@@ -98,6 +275,10 @@ function validateInitFlags(options) {
|
|
|
98
275
|
console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
|
|
99
276
|
process.exit(1);
|
|
100
277
|
}
|
|
278
|
+
if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
|
|
279
|
+
console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
101
282
|
}
|
|
102
283
|
async function initCommand(options) {
|
|
103
284
|
// Validate flag values before doing anything
|
|
@@ -115,26 +296,46 @@ async function initCommand(options) {
|
|
|
115
296
|
console.error(`❌ Directory "${options.name}" already exists.`);
|
|
116
297
|
process.exit(1);
|
|
117
298
|
}
|
|
299
|
+
// 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
|
|
300
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
|
|
301
|
+
if (!interactive) {
|
|
302
|
+
const missing = [];
|
|
303
|
+
if (!options.cicd)
|
|
304
|
+
missing.push('--cicd <github|azure|skip>');
|
|
305
|
+
if (!options.backendLanguage)
|
|
306
|
+
missing.push('--backend-language <typescript|csharp|python>');
|
|
307
|
+
if (!options.cosmosDbMode)
|
|
308
|
+
missing.push('--cosmos-db-mode <freetier|serverless>');
|
|
309
|
+
if (!options.vnet)
|
|
310
|
+
missing.push('--vnet <outbound|none>');
|
|
311
|
+
if (!options.swaPlan)
|
|
312
|
+
missing.push('--swa-plan <free|standard>');
|
|
313
|
+
if (missing.length > 0) {
|
|
314
|
+
console.error(`❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
|
|
315
|
+
missing.map((flag) => ` ${flag}`).join('\n'));
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
118
319
|
// Use flag values if provided, otherwise prompt interactively
|
|
119
320
|
const cicdProvider = options.cicd || await promptCiCd();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
: await promptAzureConfig();
|
|
321
|
+
const backendLanguage = options.backendLanguage || await promptBackendLanguage();
|
|
322
|
+
const azureConfig = await promptAzureConfig(options);
|
|
123
323
|
// Create Next.js project with create-next-app
|
|
124
324
|
await createNextJsProject(options.name, pm);
|
|
125
325
|
// Upgrade Next.js to specified version (or latest) to avoid cached old versions
|
|
126
326
|
await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
|
|
127
327
|
// Add SwallowKit specific files
|
|
128
|
-
await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm);
|
|
328
|
+
await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
|
|
129
329
|
// Create infrastructure files (Bicep)
|
|
130
|
-
await createInfrastructure(projectDir, options.name, azureConfig);
|
|
330
|
+
await createInfrastructure(projectDir, options.name, azureConfig, backendLanguage);
|
|
131
331
|
// Create CI/CD files based on choice
|
|
132
332
|
if (cicdProvider === 'github') {
|
|
133
|
-
await createGitHubActionsWorkflows(projectDir, azureConfig, pm);
|
|
333
|
+
await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
|
|
134
334
|
}
|
|
135
335
|
else if (cicdProvider === 'azure') {
|
|
136
|
-
await createAzurePipelines(projectDir, pm);
|
|
336
|
+
await createAzurePipelines(projectDir, pm, backendLanguage);
|
|
137
337
|
}
|
|
338
|
+
await (0, manifest_1.syncProjectManifest)(projectDir);
|
|
138
339
|
// Initialize Git repository and create initial commit
|
|
139
340
|
try {
|
|
140
341
|
// Try git init with -b main (Git 2.28+), fallback to git init
|
|
@@ -179,7 +380,7 @@ async function initCommand(options) {
|
|
|
179
380
|
console.log("\n📝 Next steps:");
|
|
180
381
|
console.log(` cd ${options.name}`);
|
|
181
382
|
console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
|
|
182
|
-
console.log(` ${pmCmd.dlx} swallowkit scaffold
|
|
383
|
+
console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
|
|
183
384
|
console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
|
|
184
385
|
console.log("\n🚀 Deploy to Azure:");
|
|
185
386
|
console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
|
|
@@ -238,92 +439,416 @@ async function createNextJsProject(projectName, pm) {
|
|
|
238
439
|
});
|
|
239
440
|
}
|
|
240
441
|
async function upgradeNextJs(projectDir, version, pm) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
442
|
+
console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
|
|
443
|
+
// pnpm: pnpm add next@... ; npm: npm install next@...
|
|
444
|
+
const args = pm === 'pnpm'
|
|
445
|
+
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
|
|
446
|
+
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
|
|
447
|
+
await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
|
|
448
|
+
console.log(`\n✅ Next.js ${version} installed\n`);
|
|
449
|
+
}
|
|
450
|
+
async function installDependencies(projectDir, pm = 'pnpm') {
|
|
451
|
+
console.log('\n📦 Installing dependencies...\n');
|
|
452
|
+
await runPackageManagerCommand(pm, ['install'], projectDir, `${pm} install`);
|
|
453
|
+
if (pm === 'pnpm') {
|
|
454
|
+
console.log('\n📦 Generating npm lockfile for CI/CD...\n');
|
|
455
|
+
await withNpmLockfileSafeManifests(projectDir, async () => {
|
|
456
|
+
await runPackageManagerCommand('npm', ['install', '--package-lock-only', '--ignore-scripts'], projectDir, 'npm install --package-lock-only');
|
|
251
457
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
458
|
+
}
|
|
459
|
+
console.log('\n✅ Dependencies installed\n');
|
|
460
|
+
}
|
|
461
|
+
async function withNpmLockfileSafeManifests(projectDir, action) {
|
|
462
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
463
|
+
const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
|
|
464
|
+
const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
|
|
465
|
+
const originalFiles = new Map();
|
|
466
|
+
const hiddenNodeModules = new Map();
|
|
467
|
+
function rememberOriginal(filePath) {
|
|
468
|
+
if (!originalFiles.has(filePath)) {
|
|
469
|
+
originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function readJson(filePath) {
|
|
473
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
474
|
+
}
|
|
475
|
+
function writeJson(filePath, value) {
|
|
476
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
477
|
+
}
|
|
478
|
+
const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
|
|
479
|
+
? readJson(sharedPackageJsonPath).name
|
|
480
|
+
: undefined;
|
|
481
|
+
function replaceSharedWorkspaceDependency(packageJson, replacement) {
|
|
482
|
+
let changed = false;
|
|
483
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
484
|
+
const dependencies = packageJson[section];
|
|
485
|
+
if (!dependencies)
|
|
486
|
+
continue;
|
|
487
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
488
|
+
if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
489
|
+
dependencies[name] = replacement;
|
|
490
|
+
changed = true;
|
|
491
|
+
}
|
|
255
492
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
493
|
+
}
|
|
494
|
+
return changed;
|
|
495
|
+
}
|
|
496
|
+
function normalizePackageJson(filePath, replacement) {
|
|
497
|
+
if (!fs.existsSync(filePath))
|
|
498
|
+
return;
|
|
499
|
+
const packageJson = readJson(filePath);
|
|
500
|
+
if (!replaceSharedWorkspaceDependency(packageJson, replacement))
|
|
501
|
+
return;
|
|
502
|
+
rememberOriginal(filePath);
|
|
503
|
+
writeJson(filePath, packageJson);
|
|
504
|
+
}
|
|
505
|
+
function hideNodeModules(directory) {
|
|
506
|
+
const nodeModulesPath = path.join(directory, 'node_modules');
|
|
507
|
+
if (!fs.existsSync(nodeModulesPath))
|
|
508
|
+
return;
|
|
509
|
+
const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
|
|
510
|
+
fs.renameSync(nodeModulesPath, hiddenPath);
|
|
511
|
+
hiddenNodeModules.set(nodeModulesPath, hiddenPath);
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
normalizePackageJson(packageJsonPath, 'file:shared');
|
|
515
|
+
normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
|
|
516
|
+
hideNodeModules(projectDir);
|
|
517
|
+
hideNodeModules(path.join(projectDir, 'shared'));
|
|
518
|
+
hideNodeModules(path.join(projectDir, 'functions'));
|
|
519
|
+
await action();
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
|
|
523
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
524
|
+
fs.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
259
525
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
526
|
+
fs.renameSync(hiddenPath, nodeModulesPath);
|
|
527
|
+
}
|
|
528
|
+
for (const [filePath, content] of originalFiles) {
|
|
529
|
+
fs.writeFileSync(filePath, content);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
265
532
|
}
|
|
266
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Run a package-manager command with stdio passed through to the user.
|
|
535
|
+
*
|
|
536
|
+
* For pnpm, additionally tee stdout/stderr to a buffer so that if the command
|
|
537
|
+
* fails due to `ERR_PNPM_IGNORED_BUILDS`, we can:
|
|
538
|
+
* 1. detect which packages were ignored,
|
|
539
|
+
* 2. approve them (interactively or automatically),
|
|
540
|
+
* 3. clean-reinstall and retry — repeating if pnpm reports further ignored
|
|
541
|
+
* packages (pnpm does not always list every unapproved package at once).
|
|
542
|
+
*/
|
|
543
|
+
async function runPackageManagerCommand(pm, args, projectDir, label) {
|
|
544
|
+
let { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
|
|
545
|
+
if (code === 0)
|
|
546
|
+
return;
|
|
547
|
+
if (pm === 'pnpm') {
|
|
548
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
549
|
+
const ignoredBuilds = parseIgnoredBuilds(output);
|
|
550
|
+
if (ignoredBuilds.length === 0)
|
|
551
|
+
break;
|
|
552
|
+
const approved = await maybeApproveBuilds(projectDir, ignoredBuilds);
|
|
553
|
+
if (!approved)
|
|
554
|
+
break;
|
|
555
|
+
// 中断された install はパッケージ抽出が不完全なまま "Already up to date"
|
|
556
|
+
// と判定されることがあるため、node_modules を消して決定論的に入れ直す
|
|
557
|
+
removeWorkspaceNodeModules(projectDir);
|
|
558
|
+
const retry = await spawnAndCapture(pm, args, projectDir, true);
|
|
559
|
+
code = retry.code;
|
|
560
|
+
output = retry.output;
|
|
561
|
+
if (code === 0)
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (parseIgnoredBuilds(output).length > 0) {
|
|
565
|
+
throw new Error(`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
|
|
566
|
+
'Run "pnpm approve-builds" inside the project, then re-run the command.');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
throw new Error(`${label} exited with code ${code}`);
|
|
570
|
+
}
|
|
571
|
+
/** workspace root と直下パッケージの node_modules を削除する(クリーン再インストール用)。 */
|
|
572
|
+
function removeWorkspaceNodeModules(projectDir) {
|
|
573
|
+
const targets = [projectDir, ...fs.readdirSync(projectDir, { withFileTypes: true })
|
|
574
|
+
.filter((entry) => entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.'))
|
|
575
|
+
.map((entry) => path.join(projectDir, entry.name))];
|
|
576
|
+
for (const dir of targets) {
|
|
577
|
+
const nodeModules = path.join(dir, 'node_modules');
|
|
578
|
+
if (fs.existsSync(nodeModules)) {
|
|
579
|
+
fs.rmSync(nodeModules, { recursive: true, force: true });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Spawn a child process inheriting stdin (so interactive prompts still work)
|
|
585
|
+
* while either inheriting or capturing+teeing stdout/stderr.
|
|
586
|
+
*/
|
|
587
|
+
function spawnAndCapture(pm, args, projectDir, capture) {
|
|
267
588
|
return new Promise((resolve, reject) => {
|
|
268
|
-
|
|
269
|
-
|
|
589
|
+
const stdio = capture
|
|
590
|
+
? ['inherit', 'pipe', 'pipe']
|
|
591
|
+
: ['inherit', 'inherit', 'inherit'];
|
|
592
|
+
const child = (0, child_process_1.spawn)(pm, args, {
|
|
270
593
|
cwd: projectDir,
|
|
271
|
-
stdio
|
|
594
|
+
stdio,
|
|
272
595
|
shell: true,
|
|
273
596
|
});
|
|
597
|
+
let combined = '';
|
|
598
|
+
if (capture) {
|
|
599
|
+
child.stdout?.on('data', (chunk) => {
|
|
600
|
+
process.stdout.write(chunk);
|
|
601
|
+
combined += chunk.toString('utf8');
|
|
602
|
+
});
|
|
603
|
+
child.stderr?.on('data', (chunk) => {
|
|
604
|
+
process.stderr.write(chunk);
|
|
605
|
+
combined += chunk.toString('utf8');
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
|
|
609
|
+
child.on('error', reject);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
|
|
614
|
+
* return the bare package names (without versions), de-duplicated.
|
|
615
|
+
*/
|
|
616
|
+
function parseIgnoredBuilds(output) {
|
|
617
|
+
const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
|
|
618
|
+
if (!match)
|
|
619
|
+
return [];
|
|
620
|
+
const list = match[1]
|
|
621
|
+
.split(',')
|
|
622
|
+
.map((entry) => entry.trim())
|
|
623
|
+
// strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
|
|
624
|
+
.map((entry) => entry.replace(/@[^@]+$/, ''))
|
|
625
|
+
.filter((name) => name.length > 0);
|
|
626
|
+
return Array.from(new Set(list));
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Prompt the user and, if approved, run `pnpm approve-builds` followed by
|
|
630
|
+
* `pnpm rebuild <packages>`. Returns true if the user approved (regardless of
|
|
631
|
+
* which packages they actually selected inside approve-builds).
|
|
632
|
+
*
|
|
633
|
+
* In non-interactive environments (CI, coding agents, piped output) prompting
|
|
634
|
+
* would hang forever, so build scripts are approved automatically via
|
|
635
|
+
* package.json `pnpm.onlyBuiltDependencies` instead.
|
|
636
|
+
*/
|
|
637
|
+
async function maybeApproveBuilds(projectDir, ignoredBuilds) {
|
|
638
|
+
console.log(`\n⚠️ pnpm refused to run build scripts for: ${ignoredBuilds.join(', ')}\n` +
|
|
639
|
+
' These packages (e.g. sharp) need their build scripts to run correctly.\n');
|
|
640
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
|
|
641
|
+
if (!interactive) {
|
|
642
|
+
console.log('ℹ️ Non-interactive environment detected — approving build scripts automatically\n' +
|
|
643
|
+
' via "allowBuilds" in pnpm-workspace.yaml.\n');
|
|
644
|
+
// rebuild は不要: 呼び出し側がクリーン再インストールで build script を実行する
|
|
645
|
+
approveBuildsViaWorkspaceYaml(projectDir, ignoredBuilds);
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
const response = await (0, prompts_1.default)({
|
|
649
|
+
type: 'confirm',
|
|
650
|
+
name: 'approve',
|
|
651
|
+
message: 'Run `pnpm approve-builds` now to approve these build scripts?',
|
|
652
|
+
initial: true,
|
|
653
|
+
});
|
|
654
|
+
if (!response.approve) {
|
|
655
|
+
console.log('\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n');
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
await runSimple('pnpm', ['approve-builds'], projectDir);
|
|
659
|
+
await runSimple('pnpm', ['rebuild', ...ignoredBuilds], projectDir);
|
|
660
|
+
console.log('\n✅ Build scripts approved and packages rebuilt\n');
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* pnpm-workspace.yaml の allowBuilds にパッケージを true で登録して build script を許可する。
|
|
665
|
+
* (pnpm 11+ の正準設定。失敗時に pnpm が追記する placeholder 行も true に置き換える)
|
|
666
|
+
*/
|
|
667
|
+
function approveBuildsViaWorkspaceYaml(projectDir, packages) {
|
|
668
|
+
const workspaceYamlPath = path.join(projectDir, 'pnpm-workspace.yaml');
|
|
669
|
+
const existing = fs.existsSync(workspaceYamlPath)
|
|
670
|
+
? fs.readFileSync(workspaceYamlPath, 'utf-8')
|
|
671
|
+
: '';
|
|
672
|
+
const yamlKey = (pkg) => (/^[A-Za-z0-9_.-]+$/.test(pkg) ? pkg : `'${pkg}'`);
|
|
673
|
+
const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
|
|
674
|
+
const keyIndex = lines.findIndex((line) => /^allowBuilds\s*:/.test(line));
|
|
675
|
+
if (keyIndex === -1) {
|
|
676
|
+
const block = `allowBuilds:\n${packages.map((pkg) => ` ${yamlKey(pkg)}: true`).join('\n')}\n`;
|
|
677
|
+
const base = existing.length > 0 && !existing.endsWith('\n') ? `${existing}\n` : existing;
|
|
678
|
+
fs.writeFileSync(workspaceYamlPath, `${base}${block}`, 'utf-8');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// 既存マップ内の対象パッケージ(placeholder 含む)を true に更新し、無いものは追記する
|
|
682
|
+
let endIndex = keyIndex + 1;
|
|
683
|
+
const updated = new Set();
|
|
684
|
+
while (endIndex < lines.length && /^\s+\S/.test(lines[endIndex])) {
|
|
685
|
+
const entry = lines[endIndex].match(/^\s+['"]?([A-Za-z0-9@/_.-]+)['"]?\s*:/);
|
|
686
|
+
if (entry && packages.includes(entry[1])) {
|
|
687
|
+
lines[endIndex] = ` ${yamlKey(entry[1])}: true`;
|
|
688
|
+
updated.add(entry[1]);
|
|
689
|
+
}
|
|
690
|
+
endIndex++;
|
|
691
|
+
}
|
|
692
|
+
const additions = packages.filter((pkg) => !updated.has(pkg)).map((pkg) => ` ${yamlKey(pkg)}: true`);
|
|
693
|
+
lines.splice(endIndex, 0, ...additions);
|
|
694
|
+
fs.writeFileSync(workspaceYamlPath, lines.join('\n'), 'utf-8');
|
|
695
|
+
}
|
|
696
|
+
function runSimple(command, args, cwd) {
|
|
697
|
+
return new Promise((resolve, reject) => {
|
|
698
|
+
const child = (0, child_process_1.spawn)(command, args, { cwd, stdio: 'inherit', shell: true });
|
|
274
699
|
child.on('close', (code) => {
|
|
275
|
-
if (code !== 0)
|
|
276
|
-
reject(new Error(`${
|
|
277
|
-
|
|
278
|
-
else {
|
|
279
|
-
console.log('\n✅ Dependencies installed\n');
|
|
700
|
+
if (code !== 0)
|
|
701
|
+
reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
|
|
702
|
+
else
|
|
280
703
|
resolve();
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
child.on('error', (error) => {
|
|
284
|
-
reject(error);
|
|
285
704
|
});
|
|
705
|
+
child.on('error', reject);
|
|
286
706
|
});
|
|
287
707
|
}
|
|
288
|
-
|
|
708
|
+
function injectSwallowKitNextConfig(nextConfigContent, projectName) {
|
|
709
|
+
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`);
|
|
710
|
+
}
|
|
711
|
+
function buildCSharpFunctionsProgramSource() {
|
|
712
|
+
return `using Microsoft.Azure.Functions.Worker;
|
|
713
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
714
|
+
using Microsoft.Extensions.Hosting;
|
|
715
|
+
using Microsoft.Azure.Functions.Worker.ApplicationInsights;
|
|
716
|
+
|
|
717
|
+
var host = new HostBuilder()
|
|
718
|
+
.ConfigureFunctionsWorkerDefaults()
|
|
719
|
+
.ConfigureServices(services =>
|
|
720
|
+
{
|
|
721
|
+
services.AddApplicationInsightsTelemetryWorkerService();
|
|
722
|
+
services.ConfigureFunctionsApplicationInsights();
|
|
723
|
+
})
|
|
724
|
+
.Build();
|
|
725
|
+
|
|
726
|
+
host.Run();
|
|
727
|
+
`;
|
|
728
|
+
}
|
|
729
|
+
function buildCSharpFunctionsProjectSource() {
|
|
730
|
+
return `<Project Sdk="Microsoft.NET.Sdk">
|
|
731
|
+
<PropertyGroup>
|
|
732
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
733
|
+
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
|
734
|
+
<OutputType>Exe</OutputType>
|
|
735
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
736
|
+
<Nullable>enable</Nullable>
|
|
737
|
+
</PropertyGroup>
|
|
738
|
+
<ItemGroup>
|
|
739
|
+
<Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />
|
|
740
|
+
<EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
|
|
741
|
+
<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
|
|
742
|
+
</ItemGroup>
|
|
743
|
+
<ItemGroup>
|
|
744
|
+
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
|
|
745
|
+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
746
|
+
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
|
747
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
|
748
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
|
749
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
|
|
750
|
+
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
|
|
751
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
|
752
|
+
</ItemGroup>
|
|
753
|
+
</Project>
|
|
754
|
+
`;
|
|
755
|
+
}
|
|
756
|
+
function buildSwallowKitConfigSource(backendLanguage) {
|
|
757
|
+
return `module.exports = {
|
|
758
|
+
backend: {
|
|
759
|
+
language: '${backendLanguage}',
|
|
760
|
+
},
|
|
761
|
+
functions: {
|
|
762
|
+
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
763
|
+
},
|
|
764
|
+
deployment: {
|
|
765
|
+
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
766
|
+
swaName: process.env.AZURE_SWA_NAME || '',
|
|
767
|
+
},
|
|
768
|
+
}
|
|
769
|
+
`;
|
|
770
|
+
}
|
|
771
|
+
function buildGeneratedProjectDependencies(projectName, pm = 'pnpm') {
|
|
772
|
+
return {
|
|
773
|
+
'@azure/cosmos': '^4.0.0',
|
|
774
|
+
'applicationinsights': '^3.3.0',
|
|
775
|
+
'zod': '^4.0.0',
|
|
776
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function getSwallowKitPackageMetadata() {
|
|
780
|
+
const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
|
|
781
|
+
try {
|
|
782
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
783
|
+
return {
|
|
784
|
+
name: packageJson.name || "swallowkit",
|
|
785
|
+
version: packageJson.version || "latest",
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
// Fall back to the published package name when package metadata is unavailable.
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
name: "swallowkit",
|
|
793
|
+
version: "latest",
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function buildGeneratedProjectDevDependencies() {
|
|
797
|
+
const { name, version } = getSwallowKitPackageMetadata();
|
|
798
|
+
return {
|
|
799
|
+
[name]: version,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function buildSwallowKitMcpProjectConfigSource(pm = 'pnpm') {
|
|
803
|
+
const { name } = getSwallowKitPackageMetadata();
|
|
804
|
+
return JSON.stringify({
|
|
805
|
+
mcpServers: {
|
|
806
|
+
swallowkit: {
|
|
807
|
+
command: pm === 'pnpm' ? 'pnpm' : 'npx',
|
|
808
|
+
args: pm === 'pnpm'
|
|
809
|
+
? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
|
|
810
|
+
: ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
|
|
811
|
+
cwd: ".",
|
|
812
|
+
env: {
|
|
813
|
+
SWALLOWKIT_MCP_VERSION: "latest",
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
}, null, 2);
|
|
818
|
+
}
|
|
819
|
+
async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
|
|
289
820
|
console.log('📦 Adding SwallowKit files...\n');
|
|
290
821
|
const projectName = options.name;
|
|
291
|
-
// 1. Update package.json to add
|
|
822
|
+
// 1. Update package.json to add runtime dependencies for generated projects
|
|
292
823
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
293
824
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
294
|
-
// Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
|
|
295
825
|
// zod is in the shared workspace package, not here
|
|
296
826
|
packageJson.dependencies = {
|
|
297
827
|
...packageJson.dependencies,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
828
|
+
...buildGeneratedProjectDependencies(projectName, pm),
|
|
829
|
+
};
|
|
830
|
+
packageJson.devDependencies = {
|
|
831
|
+
...packageJson.devDependencies,
|
|
832
|
+
...buildGeneratedProjectDevDependencies(),
|
|
302
833
|
};
|
|
303
834
|
packageJson.scripts = {
|
|
304
835
|
...packageJson.scripts,
|
|
305
836
|
'build': (0, package_manager_1.getBuildScript)(pm),
|
|
306
837
|
'start': 'next start',
|
|
307
|
-
'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm),
|
|
838
|
+
'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
|
|
308
839
|
};
|
|
309
|
-
if (pm === 'pnpm') {
|
|
310
|
-
packageJson.packageManager = 'pnpm@latest';
|
|
311
|
-
}
|
|
312
840
|
packageJson.engines = {
|
|
313
841
|
node: '20.x',
|
|
314
842
|
};
|
|
315
|
-
//
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
// npm: workspaces are defined in package.json
|
|
843
|
+
// Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
|
|
844
|
+
const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
|
|
845
|
+
const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
|
|
846
|
+
packageJson.workspaces = workspacePackages;
|
|
847
|
+
if (wsConfig.type !== 'file') {
|
|
323
848
|
packageJson.workspaces = wsConfig.value;
|
|
324
849
|
}
|
|
325
850
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
326
|
-
// Create workspace
|
|
851
|
+
// Create pnpm-workspace.yaml for local pnpm development when needed.
|
|
327
852
|
if (wsConfig.type === 'file') {
|
|
328
853
|
fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
|
|
329
854
|
}
|
|
@@ -336,11 +861,9 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
|
|
|
336
861
|
}
|
|
337
862
|
if (fs.existsSync(nextConfigPath)) {
|
|
338
863
|
let nextConfigContent = fs.readFileSync(nextConfigPath, 'utf-8');
|
|
339
|
-
// Add output
|
|
864
|
+
// Add output, transpiled workspace package, and server externals for standalone deployment
|
|
340
865
|
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`);
|
|
866
|
+
nextConfigContent = injectSwallowKitNextConfig(nextConfigContent, projectName);
|
|
344
867
|
fs.writeFileSync(nextConfigPath, nextConfigContent);
|
|
345
868
|
}
|
|
346
869
|
}
|
|
@@ -360,17 +883,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
|
|
|
360
883
|
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
361
884
|
}
|
|
362
885
|
// 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
|
-
`;
|
|
886
|
+
const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
|
|
374
887
|
fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
|
|
375
888
|
// 4. Create shared workspace package for Zod models (Single Source of Truth)
|
|
376
889
|
await createSharedPackage(projectDir, projectName);
|
|
@@ -382,7 +895,14 @@ module.exports = {
|
|
|
382
895
|
// Create backend utility for calling Azure Functions
|
|
383
896
|
const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
|
|
384
897
|
function getFunctionsBaseUrl(): string {
|
|
385
|
-
return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
898
|
+
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
902
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
903
|
+
if (key) return { 'x-functions-key': key };
|
|
904
|
+
if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
|
|
905
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
386
906
|
}
|
|
387
907
|
|
|
388
908
|
/**
|
|
@@ -392,10 +912,11 @@ function getFunctionsBaseUrl(): string {
|
|
|
392
912
|
async function request<T>(
|
|
393
913
|
endpoint: string,
|
|
394
914
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
395
|
-
body?:
|
|
915
|
+
body?: unknown,
|
|
396
916
|
queryParams?: Record<string, string>
|
|
397
917
|
): Promise<T> {
|
|
398
918
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
919
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
399
920
|
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
400
921
|
if (queryParams) {
|
|
401
922
|
const params = new URLSearchParams(queryParams);
|
|
@@ -407,6 +928,7 @@ async function request<T>(
|
|
|
407
928
|
method,
|
|
408
929
|
headers: {
|
|
409
930
|
'Content-Type': 'application/json',
|
|
931
|
+
...keyHeaders,
|
|
410
932
|
},
|
|
411
933
|
body: body ? JSON.stringify(body) : undefined,
|
|
412
934
|
});
|
|
@@ -462,14 +984,14 @@ export const api = {
|
|
|
462
984
|
/**
|
|
463
985
|
* Make a POST request
|
|
464
986
|
*/
|
|
465
|
-
post: <T>(endpoint: string, body?:
|
|
987
|
+
post: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
466
988
|
return request<T>(endpoint, 'POST', body);
|
|
467
989
|
},
|
|
468
990
|
|
|
469
991
|
/**
|
|
470
992
|
* Make a PUT request
|
|
471
993
|
*/
|
|
472
|
-
put: <T>(endpoint: string, body?:
|
|
994
|
+
put: <T>(endpoint: string, body?: unknown): Promise<T> => {
|
|
473
995
|
return request<T>(endpoint, 'PUT', body);
|
|
474
996
|
},
|
|
475
997
|
|
|
@@ -487,7 +1009,8 @@ export const api = {
|
|
|
487
1009
|
fs.mkdirSync(componentsDir, { recursive: true });
|
|
488
1010
|
// 6. Create .env.example
|
|
489
1011
|
const envExample = `# Azure Functions Backend URL
|
|
490
|
-
|
|
1012
|
+
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
1013
|
+
BACKEND_FUNCTIONS_KEY=
|
|
491
1014
|
|
|
492
1015
|
# Azure Configuration
|
|
493
1016
|
AZURE_RESOURCE_GROUP=your-resource-group
|
|
@@ -526,7 +1049,7 @@ export async function register() {
|
|
|
526
1049
|
const originalConsoleError = console.error;
|
|
527
1050
|
const originalConsoleWarn = console.warn;
|
|
528
1051
|
|
|
529
|
-
console.log = function(...args:
|
|
1052
|
+
console.log = function(...args: unknown[]) {
|
|
530
1053
|
originalConsoleLog.apply(console, args);
|
|
531
1054
|
const message = args.map(arg =>
|
|
532
1055
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -537,7 +1060,7 @@ export async function register() {
|
|
|
537
1060
|
});
|
|
538
1061
|
};
|
|
539
1062
|
|
|
540
|
-
console.error = function(...args:
|
|
1063
|
+
console.error = function(...args: unknown[]) {
|
|
541
1064
|
originalConsoleError.apply(console, args);
|
|
542
1065
|
const message = args.map(arg =>
|
|
543
1066
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -548,7 +1071,7 @@ export async function register() {
|
|
|
548
1071
|
});
|
|
549
1072
|
};
|
|
550
1073
|
|
|
551
|
-
console.warn = function(...args:
|
|
1074
|
+
console.warn = function(...args: unknown[]) {
|
|
552
1075
|
originalConsoleWarn.apply(console, args);
|
|
553
1076
|
const message = args.map(arg =>
|
|
554
1077
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
@@ -570,7 +1093,8 @@ export async function register() {
|
|
|
570
1093
|
// 8. Create .env.local for local development
|
|
571
1094
|
const envLocalContent = [
|
|
572
1095
|
'# Azure Functions Backend URL (Local)',
|
|
573
|
-
'
|
|
1096
|
+
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
1097
|
+
'BACKEND_FUNCTIONS_KEY=',
|
|
574
1098
|
''
|
|
575
1099
|
].join('\n');
|
|
576
1100
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -596,7 +1120,7 @@ export async function register() {
|
|
|
596
1120
|
};
|
|
597
1121
|
fs.writeFileSync(path.join(projectDir, 'staticwebapp.config.json'), JSON.stringify(swaConfig, null, 2));
|
|
598
1122
|
// 14. Create Azure Functions project
|
|
599
|
-
await createAzureFunctionsProject(projectDir, pm);
|
|
1123
|
+
await createAzureFunctionsProject(projectDir, pm, backendLanguage);
|
|
600
1124
|
// 15. Create BFF API route to call Azure Functions
|
|
601
1125
|
await createBffApiRoute(projectDir);
|
|
602
1126
|
// 16. Create home page
|
|
@@ -606,9 +1130,9 @@ export async function register() {
|
|
|
606
1130
|
await installDependencies(projectDir, pm);
|
|
607
1131
|
console.log('✅ Project structure created\n');
|
|
608
1132
|
// 18. Create README.md
|
|
609
|
-
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm);
|
|
1133
|
+
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
|
|
610
1134
|
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
611
|
-
createAiAgentFiles(projectDir, projectName);
|
|
1135
|
+
createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
|
|
612
1136
|
}
|
|
613
1137
|
async function createSharedPackage(projectDir, projectName) {
|
|
614
1138
|
console.log('📦 Creating shared workspace package for Zod models...\n');
|
|
@@ -617,6 +1141,7 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
617
1141
|
fs.mkdirSync(modelsDir, { recursive: true });
|
|
618
1142
|
// shared/package.json
|
|
619
1143
|
const sharedPackageJson = {
|
|
1144
|
+
private: true,
|
|
620
1145
|
name: `@${projectName}/shared`,
|
|
621
1146
|
version: '1.0.0',
|
|
622
1147
|
description: 'Shared Zod models — Single Source of Truth for validation schemas',
|
|
@@ -635,61 +1160,127 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
635
1160
|
};
|
|
636
1161
|
fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
|
|
637
1162
|
// 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));
|
|
1163
|
+
fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(buildSharedTsConfig(), null, 2));
|
|
658
1164
|
// shared/index.ts (empty re-export file, scaffold will add entries)
|
|
659
1165
|
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
1166
|
// shared/.gitignore
|
|
661
1167
|
fs.writeFileSync(path.join(sharedDir, '.gitignore'), `node_modules\ndist\n`);
|
|
662
1168
|
console.log('✅ Shared package created\n');
|
|
663
1169
|
}
|
|
664
|
-
async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
|
|
665
|
-
console.log(
|
|
1170
|
+
async function createAzureFunctionsProject(projectDir, pm = 'pnpm', backendLanguage = 'typescript') {
|
|
1171
|
+
console.log(`📦 Creating Azure Functions project (${getBackendLanguageLabel(backendLanguage)})...\n`);
|
|
666
1172
|
const functionsDir = path.join(projectDir, 'functions');
|
|
667
1173
|
fs.mkdirSync(functionsDir, { recursive: true });
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1174
|
+
const projectName = path.basename(projectDir);
|
|
1175
|
+
const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
|
|
1176
|
+
createFunctionsHostFiles(functionsDir, databaseName, backendLanguage);
|
|
1177
|
+
if (backendLanguage === 'typescript') {
|
|
1178
|
+
createTypeScriptFunctionsProject(projectDir, functionsDir, pm);
|
|
1179
|
+
}
|
|
1180
|
+
else if (backendLanguage === 'csharp') {
|
|
1181
|
+
createCSharpFunctionsProject(projectDir, functionsDir);
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
createPythonFunctionsProject(projectDir, functionsDir);
|
|
1185
|
+
}
|
|
1186
|
+
console.log('✅ Azure Functions project created\n');
|
|
1187
|
+
}
|
|
1188
|
+
function createFunctionsHostFiles(functionsDir, databaseName, backendLanguage) {
|
|
1189
|
+
const hostJson = {
|
|
1190
|
+
version: '2.0',
|
|
1191
|
+
logging: {
|
|
1192
|
+
applicationInsights: {
|
|
1193
|
+
samplingSettings: {
|
|
1194
|
+
isEnabled: true,
|
|
1195
|
+
maxTelemetryItemsPerSecond: 20,
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
extensionBundle: {
|
|
1200
|
+
id: 'Microsoft.Azure.Functions.ExtensionBundle',
|
|
1201
|
+
version: '[4.0.0, 4.10.0)',
|
|
1202
|
+
},
|
|
1203
|
+
};
|
|
1204
|
+
fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
|
|
1205
|
+
const localSettings = {
|
|
1206
|
+
IsEncrypted: false,
|
|
1207
|
+
Values: {
|
|
1208
|
+
AzureWebJobsStorage: '',
|
|
1209
|
+
FUNCTIONS_WORKER_RUNTIME: getFunctionsWorkerRuntime(backendLanguage),
|
|
1210
|
+
AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
|
|
1211
|
+
CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
|
|
1212
|
+
COSMOS_DB_DATABASE_NAME: databaseName,
|
|
1213
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
|
|
1217
|
+
const gitignoreLines = [
|
|
1218
|
+
'local.settings.json',
|
|
1219
|
+
'*.log',
|
|
1220
|
+
'.vscode',
|
|
1221
|
+
'.DS_Store',
|
|
1222
|
+
];
|
|
1223
|
+
if (backendLanguage === 'typescript') {
|
|
1224
|
+
gitignoreLines.unshift('node_modules', 'dist');
|
|
1225
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `node_modules
|
|
1226
|
+
.git
|
|
1227
|
+
.vscode
|
|
1228
|
+
local.settings.json
|
|
1229
|
+
test
|
|
1230
|
+
tsconfig.json
|
|
1231
|
+
*.ts
|
|
1232
|
+
!dist/**/*.js
|
|
1233
|
+
`);
|
|
1234
|
+
}
|
|
1235
|
+
else if (backendLanguage === 'python') {
|
|
1236
|
+
gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
|
|
1237
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
|
|
1238
|
+
.codegen-venv
|
|
1239
|
+
__pycache__
|
|
1240
|
+
.pytest_cache
|
|
1241
|
+
.mypy_cache
|
|
1242
|
+
.ruff_cache
|
|
1243
|
+
local.settings.json
|
|
1244
|
+
tests
|
|
1245
|
+
`);
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
gitignoreLines.unshift('bin', 'obj');
|
|
1249
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `bin
|
|
1250
|
+
obj
|
|
1251
|
+
local.settings.json
|
|
1252
|
+
tests
|
|
1253
|
+
`);
|
|
1254
|
+
}
|
|
1255
|
+
fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
|
|
1256
|
+
}
|
|
1257
|
+
function buildTypeScriptFunctionsPackageJson(projectName, pm = 'pnpm') {
|
|
1258
|
+
return {
|
|
1259
|
+
name: 'functions',
|
|
1260
|
+
version: '1.0.0',
|
|
1261
|
+
description: 'Azure Functions backend',
|
|
1262
|
+
main: 'dist/*.js',
|
|
1263
|
+
scripts: {
|
|
1264
|
+
start: 'func start',
|
|
1265
|
+
build: 'tsc',
|
|
1266
|
+
prestart: (0, package_manager_1.getFunctionsPrestart)(pm),
|
|
1267
|
+
},
|
|
1268
|
+
dependencies: {
|
|
1269
|
+
'@azure/functions': '~4.5.0',
|
|
1270
|
+
'@azure/cosmos': '^4.0.0',
|
|
1271
|
+
'@azure/identity': '^4.0.0',
|
|
1272
|
+
zod: '>=3.25.0',
|
|
1273
|
+
[`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
|
|
1274
|
+
},
|
|
1275
|
+
devDependencies: {
|
|
1276
|
+
'@types/node': '^20.0.0',
|
|
1277
|
+
typescript: '^5.0.0',
|
|
1278
|
+
},
|
|
690
1279
|
};
|
|
1280
|
+
}
|
|
1281
|
+
function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
|
|
1282
|
+
const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
|
|
691
1283
|
fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
|
|
692
|
-
// Create functions tsconfig.json
|
|
693
1284
|
const sharedPkgName = `@${path.basename(projectDir)}/shared`;
|
|
694
1285
|
const functionsTsConfig = {
|
|
695
1286
|
compilerOptions: {
|
|
@@ -709,69 +1300,14 @@ async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
|
|
|
709
1300
|
},
|
|
710
1301
|
},
|
|
711
1302
|
include: ['src/**/*'],
|
|
712
|
-
exclude: ['node_modules', 'dist']
|
|
1303
|
+
exclude: ['node_modules', 'dist'],
|
|
713
1304
|
};
|
|
714
1305
|
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
1306
|
const srcDir = path.join(functionsDir, 'src');
|
|
769
1307
|
fs.mkdirSync(srcDir, { recursive: true });
|
|
770
|
-
|
|
771
|
-
const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
1308
|
+
fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
772
1309
|
import { z } from 'zod/v4';
|
|
773
1310
|
|
|
774
|
-
// Zod schema for request validation
|
|
775
1311
|
const greetRequestSchema = z.object({
|
|
776
1312
|
name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
|
|
777
1313
|
});
|
|
@@ -780,12 +1316,9 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
780
1316
|
context.log('HTTP trigger function processed a request.');
|
|
781
1317
|
|
|
782
1318
|
try {
|
|
783
|
-
// Get name from query or body
|
|
784
1319
|
const name = request.query.get('name') || (await request.text());
|
|
785
|
-
|
|
786
|
-
// Validate with Zod
|
|
787
1320
|
const result = greetRequestSchema.safeParse({ name });
|
|
788
|
-
|
|
1321
|
+
|
|
789
1322
|
if (!result.success) {
|
|
790
1323
|
return {
|
|
791
1324
|
status: 400,
|
|
@@ -796,7 +1329,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
796
1329
|
}
|
|
797
1330
|
|
|
798
1331
|
const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
|
|
799
|
-
|
|
1332
|
+
|
|
800
1333
|
return {
|
|
801
1334
|
status: 200,
|
|
802
1335
|
jsonBody: {
|
|
@@ -817,13 +1350,109 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
817
1350
|
|
|
818
1351
|
app.http('greet', {
|
|
819
1352
|
methods: ['GET', 'POST'],
|
|
820
|
-
authLevel: '
|
|
1353
|
+
authLevel: 'function',
|
|
821
1354
|
handler: greet
|
|
822
1355
|
});
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1356
|
+
`);
|
|
1357
|
+
}
|
|
1358
|
+
function createCSharpFunctionsProject(projectDir, functionsDir) {
|
|
1359
|
+
const projectBaseName = path.basename(projectDir);
|
|
1360
|
+
const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
|
|
1361
|
+
const csprojName = `${projectPascal}.Functions.csproj`;
|
|
1362
|
+
fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
|
|
1363
|
+
fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
|
|
1364
|
+
fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
|
|
1365
|
+
fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
|
|
1366
|
+
const crudDir = path.join(functionsDir, 'Crud');
|
|
1367
|
+
fs.mkdirSync(crudDir, { recursive: true });
|
|
1368
|
+
fs.writeFileSync(path.join(crudDir, 'GreetFunction.cs'), `using System.Net;
|
|
1369
|
+
using Microsoft.Azure.Functions.Worker;
|
|
1370
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
1371
|
+
|
|
1372
|
+
namespace SwallowKit.Functions;
|
|
1373
|
+
|
|
1374
|
+
public sealed class GreetFunction
|
|
1375
|
+
{
|
|
1376
|
+
[Function("greet")]
|
|
1377
|
+
public async Task<HttpResponseData> Run(
|
|
1378
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
|
|
1379
|
+
{
|
|
1380
|
+
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
1381
|
+
var name = "SwallowKit";
|
|
1382
|
+
foreach (var segment in query)
|
|
1383
|
+
{
|
|
1384
|
+
var parts = segment.Split('=', 2);
|
|
1385
|
+
if (parts.Length == 2 && parts[0] == "name")
|
|
1386
|
+
{
|
|
1387
|
+
name = Uri.UnescapeDataString(parts[1]);
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
var response = request.CreateResponse(HttpStatusCode.OK);
|
|
1392
|
+
await response.WriteAsJsonAsync(new
|
|
1393
|
+
{
|
|
1394
|
+
message = $"Hello, {name}! This message is from Azure Functions.",
|
|
1395
|
+
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
|
1396
|
+
});
|
|
1397
|
+
return response;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
`);
|
|
1401
|
+
}
|
|
1402
|
+
function createPythonFunctionsProject(projectDir, functionsDir) {
|
|
1403
|
+
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1404
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1405
|
+
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
1406
|
+
azure-cosmos>=4.9.0
|
|
1407
|
+
azure-identity>=1.19.0
|
|
1408
|
+
`);
|
|
1409
|
+
fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
|
|
1410
|
+
const blueprintsDir = path.join(functionsDir, 'blueprints');
|
|
1411
|
+
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
1412
|
+
fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
|
|
1413
|
+
fs.writeFileSync(path.join(blueprintsDir, 'greet.py'), `import json
|
|
1414
|
+
from datetime import datetime, timezone
|
|
1415
|
+
|
|
1416
|
+
import azure.functions as func
|
|
1417
|
+
|
|
1418
|
+
bp = func.Blueprint()
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
@bp.route(route="greet", methods=["GET", "POST"])
|
|
1422
|
+
def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
1423
|
+
name = req.params.get("name") or "SwallowKit"
|
|
1424
|
+
payload = {
|
|
1425
|
+
"message": f"Hello, {name}! This message is from Azure Functions.",
|
|
1426
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1427
|
+
}
|
|
1428
|
+
return func.HttpResponse(
|
|
1429
|
+
body=json.dumps(payload, ensure_ascii=False),
|
|
1430
|
+
status_code=200,
|
|
1431
|
+
mimetype="application/json",
|
|
1432
|
+
)
|
|
1433
|
+
`);
|
|
1434
|
+
fs.writeFileSync(path.join(functionsDir, 'function_app.py'), `import azure.functions as func
|
|
1435
|
+
|
|
1436
|
+
from blueprints.greet import bp as greet_bp
|
|
1437
|
+
|
|
1438
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
|
1439
|
+
|
|
1440
|
+
app.register_blueprint(greet_bp)
|
|
1441
|
+
# SwallowKit scaffold registrations
|
|
1442
|
+
`);
|
|
1443
|
+
}
|
|
1444
|
+
function ensureProjectGitignoreEntry(projectDir, entry) {
|
|
1445
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
1446
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const current = fs.readFileSync(gitignorePath, 'utf8');
|
|
1450
|
+
const lines = current.split(/\r?\n/);
|
|
1451
|
+
if (lines.includes(entry)) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const normalized = current.endsWith('\n') ? current : `${current}\n`;
|
|
1455
|
+
fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
|
|
827
1456
|
}
|
|
828
1457
|
async function createBffApiRoute(projectDir) {
|
|
829
1458
|
console.log('📦 Creating BFF API route...\n');
|
|
@@ -874,17 +1503,17 @@ export async function POST(request: NextRequest) {
|
|
|
874
1503
|
}
|
|
875
1504
|
`;
|
|
876
1505
|
fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
|
|
877
|
-
// Update .env.example to include
|
|
1506
|
+
// Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
|
|
878
1507
|
const envExamplePath = path.join(projectDir, '.env.example');
|
|
879
1508
|
let envExample = fs.readFileSync(envExamplePath, 'utf-8');
|
|
880
|
-
if (!envExample.includes('
|
|
1509
|
+
if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
|
|
881
1510
|
envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
|
|
882
1511
|
fs.writeFileSync(envExamplePath, envExample);
|
|
883
1512
|
}
|
|
884
1513
|
// Update .env.local
|
|
885
1514
|
const envLocalPath = path.join(projectDir, '.env.local');
|
|
886
1515
|
let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
|
|
887
|
-
if (!envLocal.includes('
|
|
1516
|
+
if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
|
|
888
1517
|
envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
|
|
889
1518
|
fs.writeFileSync(envLocalPath, envLocal);
|
|
890
1519
|
}
|
|
@@ -985,7 +1614,7 @@ export default function Home() {
|
|
|
985
1614
|
Create your first model with Zod and generate CRUD operations automatically.
|
|
986
1615
|
</p>
|
|
987
1616
|
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
988
|
-
${pmCmd.dlx} swallowkit scaffold
|
|
1617
|
+
${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
|
|
989
1618
|
</code>
|
|
990
1619
|
</div>
|
|
991
1620
|
</section>
|
|
@@ -1021,13 +1650,29 @@ export const scaffoldConfig = {
|
|
|
1021
1650
|
fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
|
|
1022
1651
|
console.log('✅ Scaffold config created\n');
|
|
1023
1652
|
}
|
|
1024
|
-
function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm) {
|
|
1653
|
+
function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage) {
|
|
1025
1654
|
console.log('📝 Creating README.md...\n');
|
|
1026
1655
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
1027
1656
|
const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
|
|
1028
1657
|
const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
|
|
1029
1658
|
const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
|
|
1030
1659
|
'Outbound VNet (Cosmos DB Private Endpoint)';
|
|
1660
|
+
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1661
|
+
const schemaBridgeDescription = backendLanguage === 'typescript'
|
|
1662
|
+
? 'Zod (shared between frontend and backend)'
|
|
1663
|
+
: `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
|
|
1664
|
+
const functionsTree = backendLanguage === 'typescript'
|
|
1665
|
+
? `│ └── src/\n│ └── greet.ts # Sample function`
|
|
1666
|
+
: backendLanguage === 'csharp'
|
|
1667
|
+
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
|
|
1668
|
+
: `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
|
|
1669
|
+
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1670
|
+
? '- Azure Functions CRUD endpoints'
|
|
1671
|
+
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1672
|
+
const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
|
|
1673
|
+
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1674
|
+
? `\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`
|
|
1675
|
+
: '';
|
|
1031
1676
|
const readme = `# ${projectName}
|
|
1032
1677
|
|
|
1033
1678
|
A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
|
|
@@ -1036,9 +1681,9 @@ A full-stack application built with **SwallowKit** - Next.js on Azure Static Web
|
|
|
1036
1681
|
|
|
1037
1682
|
- **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
|
|
1038
1683
|
- **BFF (Backend for Frontend)**: Next.js API Routes
|
|
1039
|
-
- **Backend**: Azure Functions (
|
|
1684
|
+
- **Backend**: Azure Functions (${backendLanguageLabel})
|
|
1040
1685
|
- **Database**: Azure Cosmos DB
|
|
1041
|
-
- **Schema Validation**:
|
|
1686
|
+
- **Schema Validation**: ${schemaBridgeDescription}
|
|
1042
1687
|
- **Infrastructure**: Bicep (Infrastructure as Code)
|
|
1043
1688
|
- **CI/CD**: ${cicdLabel}
|
|
1044
1689
|
|
|
@@ -1073,11 +1718,8 @@ ${projectName}/
|
|
|
1073
1718
|
│ ├── api/ # BFF API routes (proxy to Functions)
|
|
1074
1719
|
│ └── page.tsx # Home page
|
|
1075
1720
|
├── functions/ # Azure Functions (backend)
|
|
1076
|
-
|
|
1077
|
-
│ ├── models/ # Data models (copied from lib/models)
|
|
1078
|
-
│ └── hello.ts # Sample function
|
|
1721
|
+
${functionsTree}
|
|
1079
1722
|
├── lib/
|
|
1080
|
-
│ ├── models/ # Shared Zod schemas
|
|
1081
1723
|
│ └── api/ # API client utilities
|
|
1082
1724
|
├── infra/ # Bicep infrastructure files
|
|
1083
1725
|
│ ├── main.bicep
|
|
@@ -1095,18 +1737,18 @@ Define your data model with Zod schema:
|
|
|
1095
1737
|
${pmCmd.dlx} swallowkit create-model <model-name>
|
|
1096
1738
|
\`\`\`
|
|
1097
1739
|
|
|
1098
|
-
This creates a model file in \`
|
|
1740
|
+
This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
|
|
1099
1741
|
|
|
1100
1742
|
### 2. Generate CRUD Code
|
|
1101
1743
|
|
|
1102
1744
|
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1103
1745
|
|
|
1104
1746
|
\`\`\`bash
|
|
1105
|
-
${pmCmd.dlx} swallowkit scaffold
|
|
1747
|
+
${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
|
|
1106
1748
|
\`\`\`
|
|
1107
1749
|
|
|
1108
1750
|
This generates:
|
|
1109
|
-
|
|
1751
|
+
${backendScaffoldNote}
|
|
1110
1752
|
- Next.js BFF API routes
|
|
1111
1753
|
- React UI components (list, detail, create, edit)
|
|
1112
1754
|
- Navigation menu integration
|
|
@@ -1123,6 +1765,7 @@ This starts:
|
|
|
1123
1765
|
- Cosmos DB Emulator check (must be running separately)
|
|
1124
1766
|
|
|
1125
1767
|
**Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
|
|
1768
|
+
${pythonLocalDevNote}
|
|
1126
1769
|
|
|
1127
1770
|
## ☁️ Deploy to Azure
|
|
1128
1771
|
|
|
@@ -1237,415 +1880,71 @@ This project was generated by SwallowKit. If you encounter any issues or have su
|
|
|
1237
1880
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1238
1881
|
console.log('✅ README.md created\n');
|
|
1239
1882
|
}
|
|
1240
|
-
function createAiAgentFiles(projectDir, projectName) {
|
|
1883
|
+
function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
|
|
1241
1884
|
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
|
-
});
|
|
1885
|
+
const runCmd = pm === 'pnpm' ? 'pnpm' : 'npx';
|
|
1886
|
+
(0, agent_instructions_1.writeAgentInstructionFiles)(projectDir, {
|
|
1887
|
+
projectName,
|
|
1888
|
+
backendLanguage,
|
|
1889
|
+
packageManager: pm,
|
|
1890
|
+
});
|
|
1891
|
+
console.log(' ✅ AGENTS.md (canonical project contract)');
|
|
1892
|
+
console.log(' ✅ CLAUDE.md (Claude Code adapter)');
|
|
1893
|
+
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
|
|
1894
|
+
console.log(' ✅ .github/instructions/ (path-specific instructions)');
|
|
1895
|
+
fs.writeFileSync(path.join(projectDir, '.mcp.json'), buildSwallowKitMcpProjectConfigSource(pm));
|
|
1896
|
+
console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
|
|
1897
|
+
(0, workflows_1.writeWorkflowDocs)(projectDir, runCmd);
|
|
1898
|
+
console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
|
|
1899
|
+
const skillPaths = (0, workflows_1.writeAgentSkills)(projectDir, runCmd);
|
|
1900
|
+
console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
|
|
1901
|
+
for (const skillPath of skillPaths) {
|
|
1902
|
+
console.log(` - ${skillPath}`);
|
|
1903
|
+
}
|
|
1904
|
+
console.log('\n✅ AI agent files created\n');
|
|
1306
1905
|
}
|
|
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
|
-
});
|
|
1906
|
+
function buildFunctionsHostKeyBicepExpression() {
|
|
1907
|
+
return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
|
|
1318
1908
|
}
|
|
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
|
-
\`\`\`
|
|
1909
|
+
function buildStaticWebAppConfigBicepSource() {
|
|
1910
|
+
return `@description('Static Web App name')
|
|
1911
|
+
param staticWebAppName string
|
|
1514
1912
|
|
|
1515
|
-
|
|
1913
|
+
@description('Functions App default hostname for backend API calls')
|
|
1914
|
+
param functionsDefaultHostname string
|
|
1516
1915
|
|
|
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.
|
|
1916
|
+
@description('Functions App name used to resolve its host key after deployment')
|
|
1917
|
+
param functionsAppName string
|
|
1521
1918
|
|
|
1522
|
-
|
|
1919
|
+
@description('Application Insights connection string for SWA')
|
|
1920
|
+
param appInsightsConnectionString string
|
|
1523
1921
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1922
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
1923
|
+
name: staticWebAppName
|
|
1924
|
+
}
|
|
1527
1925
|
|
|
1528
|
-
|
|
1926
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1927
|
+
parent: staticWebApp
|
|
1928
|
+
name: 'appsettings'
|
|
1929
|
+
properties: {
|
|
1930
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1931
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1932
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
1933
|
+
BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1529
1936
|
|
|
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.
|
|
1937
|
+
output configName string = staticWebAppConfig.name
|
|
1559
1938
|
`;
|
|
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
1939
|
}
|
|
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) {
|
|
1940
|
+
async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
|
|
1644
1941
|
console.log('📦 Creating infrastructure files (Bicep)...\n');
|
|
1645
1942
|
const infraDir = path.join(projectDir, 'infra');
|
|
1646
1943
|
const modulesDir = path.join(infraDir, 'modules');
|
|
1647
1944
|
fs.mkdirSync(modulesDir, { recursive: true });
|
|
1648
1945
|
const enableVNet = azureConfig.vnetOption !== 'none';
|
|
1946
|
+
const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
|
|
1947
|
+
const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
|
|
1649
1948
|
// main.bicep
|
|
1650
1949
|
const mainBicep = `targetScope = 'resourceGroup'
|
|
1651
1950
|
|
|
@@ -1658,6 +1957,13 @@ param location string = resourceGroup().location
|
|
|
1658
1957
|
@description('Location for Static Web App (must be explicitly provided)')
|
|
1659
1958
|
param swaLocation string
|
|
1660
1959
|
|
|
1960
|
+
@description('Static Web Apps hosting plan')
|
|
1961
|
+
@allowed([
|
|
1962
|
+
'Free'
|
|
1963
|
+
'Standard'
|
|
1964
|
+
])
|
|
1965
|
+
param staticWebAppSku string = '${staticWebAppSku}'
|
|
1966
|
+
|
|
1661
1967
|
@description('Cosmos DB mode')
|
|
1662
1968
|
@allowed(['freetier', 'serverless'])
|
|
1663
1969
|
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
@@ -1700,7 +2006,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
|
1700
2006
|
params: {
|
|
1701
2007
|
name: 'swa-\${projectName}'
|
|
1702
2008
|
location: swaLocation
|
|
1703
|
-
sku:
|
|
2009
|
+
sku: staticWebAppSku
|
|
1704
2010
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1705
2011
|
}
|
|
1706
2012
|
}
|
|
@@ -1741,35 +2047,30 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
|
|
|
1741
2047
|
params: {
|
|
1742
2048
|
name: 'pe-cosmos-\${projectName}'
|
|
1743
2049
|
location: location
|
|
1744
|
-
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1745
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier
|
|
1746
|
-
subnetId: vnet
|
|
1747
|
-
vnetId: vnet
|
|
2050
|
+
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
|
|
2051
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
2052
|
+
subnetId: vnet!.outputs.privateEndpointSubnetId
|
|
2053
|
+
vnetId: vnet!.outputs.id
|
|
1748
2054
|
}
|
|
1749
|
-
dependsOn: [
|
|
1750
|
-
cosmosDbFreeTier
|
|
1751
|
-
cosmosDbServerless
|
|
1752
|
-
vnet
|
|
1753
|
-
]
|
|
1754
2055
|
}
|
|
1755
2056
|
|
|
1756
2057
|
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
1757
2058
|
module functionsFlex 'modules/functions-flex.bicep' = {
|
|
1758
2059
|
name: 'functionsApp'
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2060
|
+
params: {
|
|
2061
|
+
name: 'func-\${projectName}'
|
|
2062
|
+
location: location
|
|
2063
|
+
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
2064
|
+
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
2065
|
+
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
2066
|
+
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
2067
|
+
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
2068
|
+
functionsRuntimeName: '${functionsRuntime.name}'
|
|
2069
|
+
functionsRuntimeVersion: '${functionsRuntime.version}'
|
|
2070
|
+
enableVNet: enableVNet
|
|
2071
|
+
vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
|
|
1769
2072
|
}
|
|
1770
2073
|
dependsOn: [
|
|
1771
|
-
cosmosDbFreeTier
|
|
1772
|
-
cosmosDbServerless
|
|
1773
2074
|
cosmosPrivateEndpoint
|
|
1774
2075
|
]
|
|
1775
2076
|
}
|
|
@@ -1778,23 +2079,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
|
|
|
1778
2079
|
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
1779
2080
|
name: 'cosmosDbRoleAssignment'
|
|
1780
2081
|
params: {
|
|
1781
|
-
cosmosAccountName: cosmosDbFreeTier
|
|
2082
|
+
cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
|
|
1782
2083
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1783
2084
|
}
|
|
1784
|
-
dependsOn: [
|
|
1785
|
-
functionsFlex
|
|
1786
|
-
]
|
|
1787
2085
|
}
|
|
1788
2086
|
|
|
1789
2087
|
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
1790
2088
|
name: 'cosmosDbRoleAssignment'
|
|
1791
2089
|
params: {
|
|
1792
|
-
cosmosAccountName: cosmosDbServerless
|
|
2090
|
+
cosmosAccountName: cosmosDbServerless!.outputs.accountName
|
|
1793
2091
|
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
1794
2092
|
}
|
|
1795
|
-
dependsOn: [
|
|
1796
|
-
functionsFlex
|
|
1797
|
-
]
|
|
1798
2093
|
}
|
|
1799
2094
|
|
|
1800
2095
|
// Update SWA config with Functions hostname (after Functions deployment)
|
|
@@ -1803,20 +2098,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
|
1803
2098
|
params: {
|
|
1804
2099
|
staticWebAppName: staticWebApp.outputs.name
|
|
1805
2100
|
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
2101
|
+
functionsAppName: functionsFlex.outputs.name
|
|
1806
2102
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1807
2103
|
}
|
|
1808
|
-
dependsOn: [
|
|
1809
|
-
functionsFlex
|
|
1810
|
-
]
|
|
1811
2104
|
}
|
|
1812
2105
|
|
|
1813
2106
|
output staticWebAppName string = staticWebApp.outputs.name
|
|
1814
2107
|
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
1815
2108
|
output functionsAppName string = functionsFlex.outputs.name
|
|
1816
2109
|
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
|
|
2110
|
+
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
|
|
2111
|
+
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
|
|
2112
|
+
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
|
|
1820
2113
|
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
1821
2114
|
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
1822
2115
|
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
@@ -1824,7 +2117,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
|
|
|
1824
2117
|
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
1825
2118
|
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
1826
2119
|
output vnetEnabled bool = enableVNet
|
|
1827
|
-
output vnetName string = enableVNet ? vnet
|
|
2120
|
+
output vnetName string = enableVNet ? vnet!.outputs.name : ''
|
|
1828
2121
|
`;
|
|
1829
2122
|
fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
|
|
1830
2123
|
// main.parameters.json
|
|
@@ -1838,6 +2131,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
|
1838
2131
|
"cosmosDbMode": {
|
|
1839
2132
|
"value": "${azureConfig.cosmosDbMode}"
|
|
1840
2133
|
},
|
|
2134
|
+
"staticWebAppSku": {
|
|
2135
|
+
"value": "${staticWebAppSku}"
|
|
2136
|
+
},
|
|
1841
2137
|
"enableVNet": {
|
|
1842
2138
|
"value": ${enableVNet}
|
|
1843
2139
|
}
|
|
@@ -1892,31 +2188,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
|
1892
2188
|
`;
|
|
1893
2189
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
1894
2190
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
1895
|
-
const staticWebAppConfigBicep =
|
|
1896
|
-
param staticWebAppName string
|
|
1897
|
-
|
|
1898
|
-
@description('Functions App default hostname for backend API calls')
|
|
1899
|
-
param functionsDefaultHostname string
|
|
1900
|
-
|
|
1901
|
-
@description('Application Insights connection string for SWA')
|
|
1902
|
-
param appInsightsConnectionString string
|
|
1903
|
-
|
|
1904
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
1905
|
-
name: staticWebAppName
|
|
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
|
-
`;
|
|
2191
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
1920
2192
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
1921
2193
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
1922
2194
|
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
@@ -1996,6 +2268,12 @@ param enableVNet bool = false
|
|
|
1996
2268
|
@description('VNet subnet ID for Functions (required if enableVNet is true)')
|
|
1997
2269
|
param vnetSubnetId string = ''
|
|
1998
2270
|
|
|
2271
|
+
@description('Functions runtime name')
|
|
2272
|
+
param functionsRuntimeName string
|
|
2273
|
+
|
|
2274
|
+
@description('Functions runtime version')
|
|
2275
|
+
param functionsRuntimeVersion string
|
|
2276
|
+
|
|
1999
2277
|
// Storage Account for Functions
|
|
2000
2278
|
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
|
2001
2279
|
name: storageAccountName
|
|
@@ -2066,8 +2344,8 @@ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
|
|
|
2066
2344
|
instanceMemoryMB: 2048
|
|
2067
2345
|
}
|
|
2068
2346
|
runtime: {
|
|
2069
|
-
name:
|
|
2070
|
-
version:
|
|
2347
|
+
name: functionsRuntimeName
|
|
2348
|
+
version: functionsRuntimeVersion
|
|
2071
2349
|
}
|
|
2072
2350
|
}
|
|
2073
2351
|
siteConfig: {
|
|
@@ -2129,63 +2407,7 @@ output principalId string = functionApp.identity.principalId
|
|
|
2129
2407
|
`;
|
|
2130
2408
|
fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
|
|
2131
2409
|
// 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
|
-
`;
|
|
2410
|
+
const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
|
|
2189
2411
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
|
|
2190
2412
|
// modules/cosmosdb-serverless.bicep (Serverless)
|
|
2191
2413
|
const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
|
|
@@ -2275,10 +2497,11 @@ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignment
|
|
|
2275
2497
|
output roleAssignmentId string = roleAssignment.id
|
|
2276
2498
|
`;
|
|
2277
2499
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
|
|
2278
|
-
//
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2500
|
+
// Bicep resolves module files at compile time even when their deployment condition is false,
|
|
2501
|
+
// so these definitions must always be generated. main.bicep still controls deployment with
|
|
2502
|
+
// `if (enableVNet)`.
|
|
2503
|
+
// modules/vnet.bicep
|
|
2504
|
+
const vnetBicep = `@description('VNet name')
|
|
2282
2505
|
param name string
|
|
2283
2506
|
|
|
2284
2507
|
@description('Location for VNet')
|
|
@@ -2324,9 +2547,9 @@ output name string = vnet.name
|
|
|
2324
2547
|
output functionsSubnetId string = vnet.properties.subnets[0].id
|
|
2325
2548
|
output privateEndpointSubnetId string = vnet.properties.subnets[1].id
|
|
2326
2549
|
`;
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2550
|
+
fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
|
|
2551
|
+
// modules/private-endpoint-cosmos.bicep
|
|
2552
|
+
const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
|
|
2330
2553
|
param name string
|
|
2331
2554
|
|
|
2332
2555
|
@description('Location')
|
|
@@ -2404,73 +2627,83 @@ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneG
|
|
|
2404
2627
|
output privateEndpointId string = privateEndpoint.id
|
|
2405
2628
|
output privateDnsZoneId string = privateDnsZone.id
|
|
2406
2629
|
`;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
}
|
|
2630
|
+
fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
|
|
2631
|
+
console.log('✅ VNet module definitions created\n');
|
|
2410
2632
|
console.log('✅ Infrastructure files created\n');
|
|
2411
2633
|
}
|
|
2412
|
-
|
|
2413
|
-
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
2634
|
+
function getGitHubFunctionsWorkflow(pm, backendLanguage, projectPm = pm) {
|
|
2414
2635
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2415
2636
|
const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
|
|
2416
|
-
const
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2637
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2638
|
+
const commonSetup = ` - uses: actions/checkout@v4
|
|
2639
|
+
|
|
2640
|
+
- name: Setup Node.js
|
|
2641
|
+
uses: actions/setup-node@v4
|
|
2642
|
+
with:
|
|
2643
|
+
node-version: '22'
|
|
2644
|
+
${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
|
|
2645
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
2646
|
+
- name: Install dependencies
|
|
2647
|
+
run: |
|
|
2648
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2649
|
+
|
|
2650
|
+
- name: Build shared package
|
|
2651
|
+
run: |
|
|
2652
|
+
${pmCmd.runFilter('shared')} build
|
|
2653
|
+
`;
|
|
2654
|
+
if (backendLanguage === 'typescript') {
|
|
2655
|
+
return `name: Deploy Azure Functions
|
|
2420
2656
|
|
|
2421
2657
|
on:
|
|
2422
2658
|
push:
|
|
2423
2659
|
branches:
|
|
2424
2660
|
- main
|
|
2425
2661
|
paths:
|
|
2426
|
-
- '
|
|
2427
|
-
- 'components/**'
|
|
2428
|
-
- 'lib/**'
|
|
2662
|
+
- 'functions/**'
|
|
2429
2663
|
- 'shared/**'
|
|
2430
|
-
- 'public/**'
|
|
2431
|
-
- 'package.json'
|
|
2432
|
-
- 'next.config.js'
|
|
2433
|
-
- 'next.config.ts'
|
|
2434
|
-
workflow_dispatch:
|
|
2435
2664
|
pull_request:
|
|
2436
2665
|
branches:
|
|
2437
2666
|
- main
|
|
2438
2667
|
paths:
|
|
2439
|
-
- '
|
|
2440
|
-
- 'components/**'
|
|
2441
|
-
- 'lib/**'
|
|
2668
|
+
- 'functions/**'
|
|
2442
2669
|
- 'shared/**'
|
|
2443
|
-
|
|
2444
|
-
- 'package.json'
|
|
2445
|
-
- 'next.config.js'
|
|
2446
|
-
- 'next.config.ts'
|
|
2670
|
+
workflow_dispatch:
|
|
2447
2671
|
|
|
2448
2672
|
jobs:
|
|
2449
2673
|
build-and-deploy:
|
|
2450
2674
|
runs-on: ubuntu-latest
|
|
2451
|
-
name: Build and Deploy
|
|
2675
|
+
name: Build and Deploy Functions
|
|
2452
2676
|
|
|
2453
2677
|
steps:
|
|
2454
|
-
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2678
|
+
${commonSetup} - name: Build Functions
|
|
2679
|
+
run: |
|
|
2680
|
+
${pmCmd.runFilter('functions')} build
|
|
2457
2681
|
|
|
2458
|
-
- name:
|
|
2682
|
+
- name: Prepare functions for deployment
|
|
2683
|
+
run: |
|
|
2684
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2685
|
+
mkdir -p /tmp/fn-deps
|
|
2686
|
+
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));"
|
|
2687
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2688
|
+
rm -rf ./functions/node_modules
|
|
2689
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2690
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2691
|
+
mkdir -p "$SHARED_DEST"
|
|
2692
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2693
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2694
|
+
|
|
2695
|
+
- name: Deploy to Azure Functions
|
|
2459
2696
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2460
|
-
uses: Azure/
|
|
2697
|
+
uses: Azure/functions-action@v1
|
|
2461
2698
|
with:
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
api_location: ''
|
|
2467
|
-
output_location: ''
|
|
2468
|
-
env:
|
|
2469
|
-
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
2699
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2700
|
+
package: './functions'
|
|
2701
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2702
|
+
sku: flexconsumption
|
|
2470
2703
|
`;
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2704
|
+
}
|
|
2705
|
+
if (backendLanguage === 'csharp') {
|
|
2706
|
+
return `name: Deploy Azure Functions
|
|
2474
2707
|
|
|
2475
2708
|
on:
|
|
2476
2709
|
push:
|
|
@@ -2493,37 +2726,57 @@ jobs:
|
|
|
2493
2726
|
name: Build and Deploy Functions
|
|
2494
2727
|
|
|
2495
2728
|
steps:
|
|
2496
|
-
-
|
|
2497
|
-
|
|
2498
|
-
- name: Setup Node.js
|
|
2499
|
-
uses: actions/setup-node@v4
|
|
2729
|
+
${commonSetup} - name: Setup .NET
|
|
2730
|
+
uses: actions/setup-dotnet@v4
|
|
2500
2731
|
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
|
|
2732
|
+
dotnet-version: '10.0.x'
|
|
2733
|
+
|
|
2734
|
+
- name: Publish Functions
|
|
2512
2735
|
run: |
|
|
2513
|
-
|
|
2736
|
+
dotnet publish ./functions -c Release -o ./functions/publish
|
|
2514
2737
|
|
|
2515
|
-
- name:
|
|
2738
|
+
- name: Deploy to Azure Functions
|
|
2739
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2740
|
+
uses: Azure/functions-action@v1
|
|
2741
|
+
with:
|
|
2742
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2743
|
+
package: './functions/publish'
|
|
2744
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2745
|
+
sku: flexconsumption
|
|
2746
|
+
`;
|
|
2747
|
+
}
|
|
2748
|
+
return `name: Deploy Azure Functions
|
|
2749
|
+
|
|
2750
|
+
on:
|
|
2751
|
+
push:
|
|
2752
|
+
branches:
|
|
2753
|
+
- main
|
|
2754
|
+
paths:
|
|
2755
|
+
- 'functions/**'
|
|
2756
|
+
- 'shared/**'
|
|
2757
|
+
pull_request:
|
|
2758
|
+
branches:
|
|
2759
|
+
- main
|
|
2760
|
+
paths:
|
|
2761
|
+
- 'functions/**'
|
|
2762
|
+
- 'shared/**'
|
|
2763
|
+
workflow_dispatch:
|
|
2764
|
+
|
|
2765
|
+
jobs:
|
|
2766
|
+
build-and-deploy:
|
|
2767
|
+
runs-on: ubuntu-latest
|
|
2768
|
+
name: Build and Deploy Functions
|
|
2769
|
+
|
|
2770
|
+
steps:
|
|
2771
|
+
${commonSetup} - name: Setup Python
|
|
2772
|
+
uses: actions/setup-python@v5
|
|
2773
|
+
with:
|
|
2774
|
+
python-version: '3.11'
|
|
2775
|
+
|
|
2776
|
+
- name: Install Functions dependencies
|
|
2516
2777
|
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"
|
|
2778
|
+
python -m pip install --upgrade pip
|
|
2779
|
+
python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
|
|
2527
2780
|
|
|
2528
2781
|
- name: Deploy to Azure Functions
|
|
2529
2782
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
@@ -2534,29 +2787,34 @@ ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
|
|
|
2534
2787
|
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2535
2788
|
sku: flexconsumption
|
|
2536
2789
|
`;
|
|
2537
|
-
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
2538
|
-
console.log('✅ GitHub Actions workflows created\n');
|
|
2539
2790
|
}
|
|
2540
|
-
|
|
2541
|
-
console.log('📦 Creating Azure Pipelines...\n');
|
|
2791
|
+
function getAzureFunctionsPipeline(pm, backendLanguage, projectPm = pm) {
|
|
2542
2792
|
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2543
2793
|
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2794
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2795
|
+
const commonSetup = ` - task: NodeTool@0
|
|
2796
|
+
inputs:
|
|
2797
|
+
versionSpec: '22.x'
|
|
2798
|
+
displayName: 'Install Node.js'
|
|
2799
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2800
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
2801
|
+
- script: |
|
|
2802
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
2803
|
+
displayName: 'Install workspace dependencies'
|
|
2804
|
+
|
|
2805
|
+
- script: |
|
|
2806
|
+
${pmCmd.runFilter('shared')} build
|
|
2807
|
+
displayName: 'Build shared package'
|
|
2808
|
+
`;
|
|
2809
|
+
if (backendLanguage === 'typescript') {
|
|
2810
|
+
return `trigger:
|
|
2548
2811
|
branches:
|
|
2549
2812
|
include:
|
|
2550
2813
|
- main
|
|
2551
2814
|
paths:
|
|
2552
2815
|
include:
|
|
2553
|
-
-
|
|
2554
|
-
- components/**
|
|
2555
|
-
- lib/**
|
|
2816
|
+
- functions/**
|
|
2556
2817
|
- shared/**
|
|
2557
|
-
- public/**
|
|
2558
|
-
- package.json
|
|
2559
|
-
- next.config.js
|
|
2560
2818
|
|
|
2561
2819
|
pr:
|
|
2562
2820
|
branches:
|
|
@@ -2564,13 +2822,8 @@ pr:
|
|
|
2564
2822
|
- main
|
|
2565
2823
|
paths:
|
|
2566
2824
|
include:
|
|
2567
|
-
-
|
|
2568
|
-
- components/**
|
|
2569
|
-
- lib/**
|
|
2825
|
+
- functions/**
|
|
2570
2826
|
- shared/**
|
|
2571
|
-
- public/**
|
|
2572
|
-
- package.json
|
|
2573
|
-
- next.config.js
|
|
2574
2827
|
|
|
2575
2828
|
pool:
|
|
2576
2829
|
vmImage: 'ubuntu-latest'
|
|
@@ -2579,33 +2832,51 @@ variables:
|
|
|
2579
2832
|
- group: azure-deployment
|
|
2580
2833
|
|
|
2581
2834
|
steps:
|
|
2582
|
-
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
displayName: 'Install Node.js'
|
|
2586
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2587
|
-
- script: |
|
|
2588
|
-
${pmCmd.ci}
|
|
2589
|
-
displayName: 'Install dependencies'
|
|
2835
|
+
${commonSetup} - script: |
|
|
2836
|
+
${pmCmd.runFilter('functions')} build
|
|
2837
|
+
displayName: 'Build Functions'
|
|
2590
2838
|
|
|
2591
2839
|
- script: |
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2840
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2841
|
+
mkdir -p /tmp/fn-deps
|
|
2842
|
+
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));"
|
|
2843
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2844
|
+
rm -rf ./functions/node_modules
|
|
2845
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2846
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2847
|
+
mkdir -p "$SHARED_DEST"
|
|
2848
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2849
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2850
|
+
displayName: 'Prepare functions for deployment'
|
|
2596
2851
|
|
|
2597
|
-
- task:
|
|
2852
|
+
- task: ArchiveFiles@2
|
|
2598
2853
|
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2599
2854
|
inputs:
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
displayName: '
|
|
2855
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
|
|
2856
|
+
includeRootFolder: false
|
|
2857
|
+
archiveType: 'zip'
|
|
2858
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2859
|
+
displayName: 'Archive Functions'
|
|
2860
|
+
|
|
2861
|
+
- task: PublishBuildArtifacts@1
|
|
2862
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2863
|
+
inputs:
|
|
2864
|
+
PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2865
|
+
ArtifactName: 'functions'
|
|
2866
|
+
displayName: 'Publish Functions artifact'
|
|
2867
|
+
|
|
2868
|
+
- task: AzureFunctionApp@2
|
|
2869
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2870
|
+
inputs:
|
|
2871
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
2872
|
+
appType: 'functionAppLinux'
|
|
2873
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
2874
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2875
|
+
displayName: 'Deploy to Azure Functions'
|
|
2605
2876
|
`;
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2877
|
+
}
|
|
2878
|
+
if (backendLanguage === 'csharp') {
|
|
2879
|
+
return `trigger:
|
|
2609
2880
|
branches:
|
|
2610
2881
|
include:
|
|
2611
2882
|
- main
|
|
@@ -2630,35 +2901,68 @@ variables:
|
|
|
2630
2901
|
- group: azure-deployment
|
|
2631
2902
|
|
|
2632
2903
|
steps:
|
|
2633
|
-
- task:
|
|
2904
|
+
${commonSetup} - task: UseDotNet@2
|
|
2634
2905
|
inputs:
|
|
2635
|
-
|
|
2636
|
-
displayName: 'Install
|
|
2637
|
-
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2638
|
-
- script: |
|
|
2639
|
-
${pmCmd.ci}
|
|
2640
|
-
displayName: 'Install workspace dependencies'
|
|
2906
|
+
version: '10.0.x'
|
|
2907
|
+
displayName: 'Install .NET SDK'
|
|
2641
2908
|
|
|
2642
2909
|
- script: |
|
|
2643
|
-
|
|
2644
|
-
displayName: '
|
|
2910
|
+
dotnet publish ./functions -c Release -o ./functions/publish
|
|
2911
|
+
displayName: 'Publish Functions'
|
|
2645
2912
|
|
|
2646
|
-
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2913
|
+
- task: ArchiveFiles@2
|
|
2914
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2915
|
+
inputs:
|
|
2916
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions/publish'
|
|
2917
|
+
includeRootFolder: false
|
|
2918
|
+
archiveType: 'zip'
|
|
2919
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2920
|
+
displayName: 'Archive Functions'
|
|
2921
|
+
|
|
2922
|
+
- task: AzureFunctionApp@2
|
|
2923
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2924
|
+
inputs:
|
|
2925
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
2926
|
+
appType: 'functionAppLinux'
|
|
2927
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
2928
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2929
|
+
displayName: 'Deploy to Azure Functions'
|
|
2930
|
+
`;
|
|
2931
|
+
}
|
|
2932
|
+
return `trigger:
|
|
2933
|
+
branches:
|
|
2934
|
+
include:
|
|
2935
|
+
- main
|
|
2936
|
+
paths:
|
|
2937
|
+
include:
|
|
2938
|
+
- functions/**
|
|
2939
|
+
- shared/**
|
|
2940
|
+
|
|
2941
|
+
pr:
|
|
2942
|
+
branches:
|
|
2943
|
+
include:
|
|
2944
|
+
- main
|
|
2945
|
+
paths:
|
|
2946
|
+
include:
|
|
2947
|
+
- functions/**
|
|
2948
|
+
- shared/**
|
|
2949
|
+
|
|
2950
|
+
pool:
|
|
2951
|
+
vmImage: 'ubuntu-latest'
|
|
2952
|
+
|
|
2953
|
+
variables:
|
|
2954
|
+
- group: azure-deployment
|
|
2955
|
+
|
|
2956
|
+
steps:
|
|
2957
|
+
${commonSetup} - task: UsePythonVersion@0
|
|
2958
|
+
inputs:
|
|
2959
|
+
versionSpec: '3.11'
|
|
2960
|
+
displayName: 'Install Python'
|
|
2649
2961
|
|
|
2650
2962
|
- 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'
|
|
2963
|
+
python -m pip install --upgrade pip
|
|
2964
|
+
python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
|
|
2965
|
+
displayName: 'Install Functions dependencies'
|
|
2662
2966
|
|
|
2663
2967
|
- task: ArchiveFiles@2
|
|
2664
2968
|
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
@@ -2669,13 +2973,6 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
|
2669
2973
|
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2670
2974
|
displayName: 'Archive Functions'
|
|
2671
2975
|
|
|
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
2976
|
- task: AzureFunctionApp@2
|
|
2680
2977
|
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2681
2978
|
inputs:
|
|
@@ -2685,6 +2982,264 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
|
2685
2982
|
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2686
2983
|
displayName: 'Deploy to Azure Functions'
|
|
2687
2984
|
`;
|
|
2985
|
+
}
|
|
2986
|
+
function buildAzureSwaPipeline(pm, projectPm = pm) {
|
|
2987
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2988
|
+
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
2989
|
+
const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
2990
|
+
return `trigger:
|
|
2991
|
+
branches:
|
|
2992
|
+
include:
|
|
2993
|
+
- main
|
|
2994
|
+
paths:
|
|
2995
|
+
include:
|
|
2996
|
+
- app/**
|
|
2997
|
+
- components/**
|
|
2998
|
+
- lib/**
|
|
2999
|
+
- shared/**
|
|
3000
|
+
- public/**
|
|
3001
|
+
- package.json
|
|
3002
|
+
- next.config.js
|
|
3003
|
+
|
|
3004
|
+
pr:
|
|
3005
|
+
branches:
|
|
3006
|
+
include:
|
|
3007
|
+
- main
|
|
3008
|
+
paths:
|
|
3009
|
+
include:
|
|
3010
|
+
- app/**
|
|
3011
|
+
- components/**
|
|
3012
|
+
- lib/**
|
|
3013
|
+
- shared/**
|
|
3014
|
+
- public/**
|
|
3015
|
+
- package.json
|
|
3016
|
+
- next.config.js
|
|
3017
|
+
|
|
3018
|
+
pool:
|
|
3019
|
+
vmImage: 'ubuntu-latest'
|
|
3020
|
+
|
|
3021
|
+
variables:
|
|
3022
|
+
- group: azure-deployment
|
|
3023
|
+
|
|
3024
|
+
steps:
|
|
3025
|
+
- task: NodeTool@0
|
|
3026
|
+
inputs:
|
|
3027
|
+
versionSpec: '22.x'
|
|
3028
|
+
displayName: 'Install Node.js'
|
|
3029
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
3030
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
|
|
3031
|
+
- script: |
|
|
3032
|
+
${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
|
|
3033
|
+
displayName: 'Install dependencies'
|
|
3034
|
+
|
|
3035
|
+
- script: |
|
|
3036
|
+
${pmCmd.run} build
|
|
3037
|
+
env:
|
|
3038
|
+
NODE_ENV: production
|
|
3039
|
+
displayName: 'Build Next.js app'
|
|
3040
|
+
|
|
3041
|
+
- task: AzureStaticWebApp@0
|
|
3042
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3043
|
+
inputs:
|
|
3044
|
+
app_location: '.'
|
|
3045
|
+
output_location: '.next/standalone'
|
|
3046
|
+
skip_app_build: true
|
|
3047
|
+
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
3048
|
+
displayName: 'Deploy to Azure Static Web Apps'
|
|
3049
|
+
`;
|
|
3050
|
+
}
|
|
3051
|
+
function getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm) {
|
|
3052
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
3053
|
+
return '';
|
|
3054
|
+
}
|
|
3055
|
+
return ` - script: |
|
|
3056
|
+
node <<'NODE'
|
|
3057
|
+
const fs = require('fs');
|
|
3058
|
+
|
|
3059
|
+
function readJson(path) {
|
|
3060
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
function writeJson(path, value) {
|
|
3064
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
const sharedPackagePath = 'shared/package.json';
|
|
3068
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
3069
|
+
|
|
3070
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
3071
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
3072
|
+
if (!pkg[section]) continue;
|
|
3073
|
+
|
|
3074
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
3075
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
3076
|
+
pkg[section][name] = replacement;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
const root = readJson('package.json');
|
|
3083
|
+
root.workspaces = ['shared', 'functions'];
|
|
3084
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
3085
|
+
root.scripts = root.scripts || {};
|
|
3086
|
+
root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
|
|
3087
|
+
writeJson('package.json', root);
|
|
3088
|
+
|
|
3089
|
+
if (fs.existsSync('functions/package.json')) {
|
|
3090
|
+
const functionsPkg = readJson('functions/package.json');
|
|
3091
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
3092
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
3093
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
3094
|
+
}
|
|
3095
|
+
writeJson('functions/package.json', functionsPkg);
|
|
3096
|
+
}
|
|
3097
|
+
NODE
|
|
3098
|
+
|
|
3099
|
+
npm install --package-lock-only --ignore-scripts
|
|
3100
|
+
displayName: 'Normalize pnpm workspace for npm CI'`;
|
|
3101
|
+
}
|
|
3102
|
+
function getGitHubNpmWorkspaceNormalizationStep(pm, projectPm) {
|
|
3103
|
+
if (pm !== 'npm' || projectPm !== 'pnpm') {
|
|
3104
|
+
return '';
|
|
3105
|
+
}
|
|
3106
|
+
return ` - name: Normalize pnpm workspace for npm CI
|
|
3107
|
+
shell: bash
|
|
3108
|
+
run: |
|
|
3109
|
+
node <<'NODE'
|
|
3110
|
+
const fs = require('fs');
|
|
3111
|
+
|
|
3112
|
+
function readJson(path) {
|
|
3113
|
+
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
function writeJson(path, value) {
|
|
3117
|
+
fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
const sharedPackagePath = 'shared/package.json';
|
|
3121
|
+
const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
|
|
3122
|
+
|
|
3123
|
+
function replaceSharedWorkspaceDep(pkg, replacement) {
|
|
3124
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
3125
|
+
if (!pkg[section]) continue;
|
|
3126
|
+
|
|
3127
|
+
for (const [name, version] of Object.entries(pkg[section])) {
|
|
3128
|
+
if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
|
|
3129
|
+
pkg[section][name] = replacement;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
const root = readJson('package.json');
|
|
3136
|
+
root.workspaces = ['shared', 'functions'];
|
|
3137
|
+
replaceSharedWorkspaceDep(root, 'file:shared');
|
|
3138
|
+
root.scripts = root.scripts || {};
|
|
3139
|
+
root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
|
|
3140
|
+
writeJson('package.json', root);
|
|
3141
|
+
|
|
3142
|
+
if (fs.existsSync('functions/package.json')) {
|
|
3143
|
+
const functionsPkg = readJson('functions/package.json');
|
|
3144
|
+
replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
|
|
3145
|
+
if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
|
|
3146
|
+
functionsPkg.scripts.prestart = 'npm run build';
|
|
3147
|
+
}
|
|
3148
|
+
writeJson('functions/package.json', functionsPkg);
|
|
3149
|
+
}
|
|
3150
|
+
NODE
|
|
3151
|
+
|
|
3152
|
+
npm install --package-lock-only --ignore-scripts
|
|
3153
|
+
`;
|
|
3154
|
+
}
|
|
3155
|
+
async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
|
|
3156
|
+
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
3157
|
+
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
3158
|
+
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
3159
|
+
const actionsPm = 'npm';
|
|
3160
|
+
const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
|
|
3161
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
3162
|
+
// deploy-functions.yml
|
|
3163
|
+
const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
|
|
3164
|
+
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
3165
|
+
console.log('✅ GitHub Actions workflows created\n');
|
|
3166
|
+
}
|
|
3167
|
+
function buildGitHubSwaWorkflow(pm, projectPm = pm) {
|
|
3168
|
+
const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
|
|
3169
|
+
return `name: Deploy Static Web App
|
|
3170
|
+
|
|
3171
|
+
on:
|
|
3172
|
+
push:
|
|
3173
|
+
branches:
|
|
3174
|
+
- main
|
|
3175
|
+
paths:
|
|
3176
|
+
- 'app/**'
|
|
3177
|
+
- 'components/**'
|
|
3178
|
+
- 'lib/**'
|
|
3179
|
+
- 'shared/**'
|
|
3180
|
+
- 'public/**'
|
|
3181
|
+
- 'package.json'
|
|
3182
|
+
- 'next.config.js'
|
|
3183
|
+
- 'next.config.ts'
|
|
3184
|
+
workflow_dispatch:
|
|
3185
|
+
pull_request:
|
|
3186
|
+
branches:
|
|
3187
|
+
- main
|
|
3188
|
+
paths:
|
|
3189
|
+
- 'app/**'
|
|
3190
|
+
- 'components/**'
|
|
3191
|
+
- 'lib/**'
|
|
3192
|
+
- 'shared/**'
|
|
3193
|
+
- 'public/**'
|
|
3194
|
+
- 'package.json'
|
|
3195
|
+
- 'next.config.js'
|
|
3196
|
+
- 'next.config.ts'
|
|
3197
|
+
|
|
3198
|
+
jobs:
|
|
3199
|
+
build-and-deploy:
|
|
3200
|
+
runs-on: ubuntu-latest
|
|
3201
|
+
name: Build and Deploy Static Web App
|
|
3202
|
+
|
|
3203
|
+
steps:
|
|
3204
|
+
- uses: actions/checkout@v4
|
|
3205
|
+
with:
|
|
3206
|
+
submodules: true
|
|
3207
|
+
|
|
3208
|
+
- name: Setup Node.js
|
|
3209
|
+
uses: actions/setup-node@v4
|
|
3210
|
+
with:
|
|
3211
|
+
node-version: '22'
|
|
3212
|
+
${(0, package_manager_1.getCiSetupStep)(pm) ? `\n${(0, package_manager_1.getCiSetupStep)(pm)}\n` : ''}
|
|
3213
|
+
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
3214
|
+
- name: Install and build app
|
|
3215
|
+
run: |
|
|
3216
|
+
${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}
|
|
3217
|
+
|
|
3218
|
+
- name: Deploy to Azure Static Web Apps
|
|
3219
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
3220
|
+
uses: Azure/static-web-apps-deploy@v1
|
|
3221
|
+
with:
|
|
3222
|
+
azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
|
3223
|
+
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
3224
|
+
action: 'upload'
|
|
3225
|
+
app_location: '/'
|
|
3226
|
+
api_location: ''
|
|
3227
|
+
output_location: ''
|
|
3228
|
+
app_build_command: '${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}'
|
|
3229
|
+
env:
|
|
3230
|
+
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
3231
|
+
`;
|
|
3232
|
+
}
|
|
3233
|
+
async function createAzurePipelines(projectDir, pm, backendLanguage) {
|
|
3234
|
+
console.log('📦 Creating Azure Pipelines...\n');
|
|
3235
|
+
const pipelinesPm = 'npm';
|
|
3236
|
+
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
3237
|
+
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
3238
|
+
// swa.yml
|
|
3239
|
+
const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
|
|
3240
|
+
fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
|
|
3241
|
+
// functions.yml
|
|
3242
|
+
const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
|
|
2688
3243
|
fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
|
|
2689
3244
|
console.log('✅ Azure Pipelines created\n');
|
|
2690
3245
|
}
|