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