swallowkit 1.0.0-beta.4 → 1.0.0-beta.40
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/LICENSE +21 -21
- package/README.ja.md +153 -215
- package/README.md +153 -216
- package/dist/__tests__/fixtures.d.ts +22 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +146 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/add-auth.d.ts +10 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +426 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +163 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +21 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +57 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +470 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +33 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +628 -146
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +45 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2851 -1754
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +439 -130
- 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 +200 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +8 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +94 -5
- 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/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +171 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +32 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +225 -0
- package/dist/core/operations/runtime.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 +63 -0
- package/dist/core/operations/scaffold-machine.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/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +209 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +38 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1244 -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 +1027 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +920 -220
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +328 -135
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +696 -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 +314 -182
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +10 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +768 -663
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +16 -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 +11 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +34 -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 +156 -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 +186 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +110 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +224 -0
- package/dist/utils/package-manager.js.map +1 -0
- 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/package.json +84 -73
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/config.test.ts +274 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/dev-seeds.test.ts +173 -0
- package/src/__tests__/dev.test.ts +252 -0
- package/src/__tests__/fixtures.ts +144 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +197 -0
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +356 -0
- package/src/__tests__/mcp.test.ts +124 -0
- package/src/__tests__/model-parser.test.ts +52 -0
- package/src/__tests__/nextjs-generator.test.ts +97 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +201 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +191 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +482 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +614 -0
- package/src/cli/commands/dev.ts +1134 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3600 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +991 -0
- package/src/cli/index.ts +196 -0
- package/src/core/config.ts +312 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/create-model.ts +174 -0
- package/src/core/operations/runtime.ts +235 -0
- package/src/core/operations/scaffold-machine.ts +91 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/validation.ts +229 -0
- package/src/core/scaffold/auth-generator.ts +1284 -0
- package/src/core/scaffold/connector-functions-generator.ts +1128 -0
- package/src/core/scaffold/functions-generator.ts +970 -0
- package/src/core/scaffold/model-parser.ts +841 -0
- package/src/core/scaffold/native-schema-generator.ts +822 -0
- package/src/core/scaffold/nextjs-generator.ts +370 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1061 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +17 -0
- package/src/machine/errors.ts +34 -0
- package/src/machine/index.ts +173 -0
- package/src/mcp/index.ts +191 -0
- package/src/types/index.ts +134 -0
- package/src/utils/package-manager.ts +238 -0
- package/src/utils/python-uv.ts +96 -0
|
@@ -36,11 +36,148 @@ 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.buildCosmosDbFreeTierBicepSource = buildCosmosDbFreeTierBicepSource;
|
|
40
|
+
exports.buildSharedTsConfig = buildSharedTsConfig;
|
|
39
41
|
exports.initCommand = initCommand;
|
|
42
|
+
exports.parseIgnoredBuilds = parseIgnoredBuilds;
|
|
43
|
+
exports.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
|
|
44
|
+
exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
|
|
45
|
+
exports.buildCSharpFunctionsProjectSource = buildCSharpFunctionsProjectSource;
|
|
46
|
+
exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
|
|
47
|
+
exports.buildGeneratedProjectDependencies = buildGeneratedProjectDependencies;
|
|
48
|
+
exports.buildGeneratedProjectDevDependencies = buildGeneratedProjectDevDependencies;
|
|
49
|
+
exports.buildSwallowKitMcpProjectConfigSource = buildSwallowKitMcpProjectConfigSource;
|
|
50
|
+
exports.getGitHubFunctionsWorkflow = getGitHubFunctionsWorkflow;
|
|
51
|
+
exports.getAzureFunctionsPipeline = getAzureFunctionsPipeline;
|
|
52
|
+
exports.buildAzureSwaPipeline = buildAzureSwaPipeline;
|
|
53
|
+
exports.buildGitHubSwaWorkflow = buildGitHubSwaWorkflow;
|
|
40
54
|
const fs = __importStar(require("fs"));
|
|
41
55
|
const path = __importStar(require("path"));
|
|
42
56
|
const child_process_1 = require("child_process");
|
|
43
57
|
const prompts_1 = __importDefault(require("prompts"));
|
|
58
|
+
const package_manager_1 = require("../../utils/package-manager");
|
|
59
|
+
const manifest_1 = require("../../core/project/manifest");
|
|
60
|
+
const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
|
|
61
|
+
const python_uv_1 = require("../../utils/python-uv");
|
|
62
|
+
const BACKEND_LANGUAGE_CHOICES = [
|
|
63
|
+
{ title: "TypeScript", value: "typescript" },
|
|
64
|
+
{ title: "C#", value: "csharp" },
|
|
65
|
+
{ title: "Python", value: "python" },
|
|
66
|
+
];
|
|
67
|
+
function usesNodeFunctionsProject(backendLanguage) {
|
|
68
|
+
return backendLanguage === "typescript";
|
|
69
|
+
}
|
|
70
|
+
function getBackendLanguageLabel(backendLanguage) {
|
|
71
|
+
return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
|
|
72
|
+
}
|
|
73
|
+
function buildCosmosDbFreeTierBicepSource() {
|
|
74
|
+
return `@description('Cosmos DB account name')
|
|
75
|
+
param accountName string
|
|
76
|
+
|
|
77
|
+
@description('Database name')
|
|
78
|
+
param databaseName string
|
|
79
|
+
|
|
80
|
+
@description('Location for Cosmos DB')
|
|
81
|
+
param location string
|
|
82
|
+
|
|
83
|
+
@description('Public network access')
|
|
84
|
+
@allowed(['Enabled', 'Disabled'])
|
|
85
|
+
param publicNetworkAccess string = 'Enabled'
|
|
86
|
+
|
|
87
|
+
// Cosmos DB Account (Free Tier)
|
|
88
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
89
|
+
name: accountName
|
|
90
|
+
location: location
|
|
91
|
+
kind: 'GlobalDocumentDB'
|
|
92
|
+
properties: {
|
|
93
|
+
databaseAccountOfferType: 'Standard'
|
|
94
|
+
enableAutomaticFailover: false
|
|
95
|
+
enableFreeTier: true
|
|
96
|
+
publicNetworkAccess: publicNetworkAccess
|
|
97
|
+
disableLocalAuth: true
|
|
98
|
+
consistencyPolicy: {
|
|
99
|
+
defaultConsistencyLevel: 'Session'
|
|
100
|
+
}
|
|
101
|
+
locations: [
|
|
102
|
+
{
|
|
103
|
+
locationName: location
|
|
104
|
+
failoverPriority: 0
|
|
105
|
+
isZoneRedundant: false
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cosmos DB Database
|
|
113
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
114
|
+
parent: cosmosAccount
|
|
115
|
+
name: databaseName
|
|
116
|
+
properties: {
|
|
117
|
+
resource: {
|
|
118
|
+
id: databaseName
|
|
119
|
+
}
|
|
120
|
+
options: {
|
|
121
|
+
throughput: 1000
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
output id string = cosmosAccount.id
|
|
127
|
+
output accountName string = cosmosAccount.name
|
|
128
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
129
|
+
output databaseName string = database.name
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
function getFunctionsWorkerRuntime(backendLanguage) {
|
|
133
|
+
if (backendLanguage === "csharp") {
|
|
134
|
+
return "dotnet-isolated";
|
|
135
|
+
}
|
|
136
|
+
if (backendLanguage === "python") {
|
|
137
|
+
return "python";
|
|
138
|
+
}
|
|
139
|
+
return "node";
|
|
140
|
+
}
|
|
141
|
+
function getFunctionsRuntimeConfig(backendLanguage) {
|
|
142
|
+
if (backendLanguage === "csharp") {
|
|
143
|
+
return { name: "dotnet-isolated", version: "10.0" };
|
|
144
|
+
}
|
|
145
|
+
if (backendLanguage === "python") {
|
|
146
|
+
return { name: "python", version: "3.11" };
|
|
147
|
+
}
|
|
148
|
+
return { name: "node", version: "22" };
|
|
149
|
+
}
|
|
150
|
+
function buildSharedTsConfig() {
|
|
151
|
+
return {
|
|
152
|
+
compilerOptions: {
|
|
153
|
+
target: "ES2020",
|
|
154
|
+
module: "Node16",
|
|
155
|
+
moduleResolution: "node16",
|
|
156
|
+
lib: ["ES2020"],
|
|
157
|
+
outDir: "dist",
|
|
158
|
+
rootDir: ".",
|
|
159
|
+
declaration: true,
|
|
160
|
+
declarationMap: true,
|
|
161
|
+
sourceMap: true,
|
|
162
|
+
strict: true,
|
|
163
|
+
esModuleInterop: true,
|
|
164
|
+
skipLibCheck: true,
|
|
165
|
+
forceConsistentCasingInFileNames: true,
|
|
166
|
+
},
|
|
167
|
+
include: ["index.ts", "models/**/*"],
|
|
168
|
+
exclude: ["node_modules", "dist"],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async function promptBackendLanguage() {
|
|
172
|
+
const response = await (0, prompts_1.default)({
|
|
173
|
+
type: "select",
|
|
174
|
+
name: "backendLanguage",
|
|
175
|
+
message: "Azure Functions backend language:",
|
|
176
|
+
choices: BACKEND_LANGUAGE_CHOICES,
|
|
177
|
+
initial: 0,
|
|
178
|
+
});
|
|
179
|
+
return response.backendLanguage || "typescript";
|
|
180
|
+
}
|
|
44
181
|
async function promptCiCd() {
|
|
45
182
|
const response = await (0, prompts_1.default)({
|
|
46
183
|
type: 'select',
|
|
@@ -81,9 +218,37 @@ async function promptAzureConfig() {
|
|
|
81
218
|
vnetOption: vnetResponse.vnet || 'outbound'
|
|
82
219
|
};
|
|
83
220
|
}
|
|
221
|
+
const VALID_CICD = ['github', 'azure', 'skip'];
|
|
222
|
+
const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
|
|
223
|
+
const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
|
|
224
|
+
const VALID_VNET = ['none', 'outbound'];
|
|
225
|
+
function validateInitFlags(options) {
|
|
226
|
+
if (options.cicd && !VALID_CICD.includes(options.cicd)) {
|
|
227
|
+
console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
if (options.backendLanguage && !VALID_BACKEND_LANGUAGE.includes(options.backendLanguage)) {
|
|
231
|
+
console.error(`❌ Invalid --backend-language value: "${options.backendLanguage}". Must be: ${VALID_BACKEND_LANGUAGE.join(', ')}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
if (options.cosmosDbMode && !VALID_COSMOS_DB_MODE.includes(options.cosmosDbMode)) {
|
|
235
|
+
console.error(`❌ Invalid --cosmos-db-mode value: "${options.cosmosDbMode}". Must be: ${VALID_COSMOS_DB_MODE.join(', ')}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
if (options.vnet && !VALID_VNET.includes(options.vnet)) {
|
|
239
|
+
console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
84
243
|
async function initCommand(options) {
|
|
244
|
+
// Validate flag values before doing anything
|
|
245
|
+
validateInitFlags(options);
|
|
85
246
|
console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
|
|
86
247
|
console.log(`📋 Template: ${options.template}`);
|
|
248
|
+
// Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
|
|
249
|
+
const pm = (0, package_manager_1.detectFromUserAgent)();
|
|
250
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
251
|
+
console.log(`📦 Package manager: ${pm}`);
|
|
87
252
|
const projectDir = path.join(process.cwd(), options.name);
|
|
88
253
|
try {
|
|
89
254
|
// Check if directory already exists
|
|
@@ -91,25 +256,28 @@ async function initCommand(options) {
|
|
|
91
256
|
console.error(`❌ Directory "${options.name}" already exists.`);
|
|
92
257
|
process.exit(1);
|
|
93
258
|
}
|
|
94
|
-
//
|
|
95
|
-
const cicdProvider = await promptCiCd();
|
|
96
|
-
|
|
97
|
-
const azureConfig =
|
|
259
|
+
// Use flag values if provided, otherwise prompt interactively
|
|
260
|
+
const cicdProvider = options.cicd || await promptCiCd();
|
|
261
|
+
const backendLanguage = options.backendLanguage || await promptBackendLanguage();
|
|
262
|
+
const azureConfig = (options.cosmosDbMode && options.vnet)
|
|
263
|
+
? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
|
|
264
|
+
: await promptAzureConfig();
|
|
98
265
|
// Create Next.js project with create-next-app
|
|
99
|
-
await createNextJsProject(options.name);
|
|
266
|
+
await createNextJsProject(options.name, pm);
|
|
100
267
|
// Upgrade Next.js to specified version (or latest) to avoid cached old versions
|
|
101
|
-
await upgradeNextJs(projectDir, options.nextVersion || 'latest');
|
|
268
|
+
await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
|
|
102
269
|
// Add SwallowKit specific files
|
|
103
|
-
await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig);
|
|
270
|
+
await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
|
|
104
271
|
// Create infrastructure files (Bicep)
|
|
105
|
-
await createInfrastructure(projectDir, options.name, azureConfig);
|
|
272
|
+
await createInfrastructure(projectDir, options.name, azureConfig, backendLanguage);
|
|
106
273
|
// Create CI/CD files based on choice
|
|
107
274
|
if (cicdProvider === 'github') {
|
|
108
|
-
await createGitHubActionsWorkflows(projectDir, azureConfig);
|
|
275
|
+
await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
|
|
109
276
|
}
|
|
110
277
|
else if (cicdProvider === 'azure') {
|
|
111
|
-
await createAzurePipelines(projectDir);
|
|
278
|
+
await createAzurePipelines(projectDir, pm, backendLanguage);
|
|
112
279
|
}
|
|
280
|
+
await (0, manifest_1.syncProjectManifest)(projectDir);
|
|
113
281
|
// Initialize Git repository and create initial commit
|
|
114
282
|
try {
|
|
115
283
|
// Try git init with -b main (Git 2.28+), fallback to git init
|
|
@@ -153,11 +321,11 @@ async function initCommand(options) {
|
|
|
153
321
|
console.log(`\n✅ Project "${options.name}" created successfully!`);
|
|
154
322
|
console.log("\n📝 Next steps:");
|
|
155
323
|
console.log(` cd ${options.name}`);
|
|
156
|
-
console.log(
|
|
157
|
-
console.log(
|
|
158
|
-
console.log(
|
|
324
|
+
console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
|
|
325
|
+
console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
|
|
326
|
+
console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
|
|
159
327
|
console.log("\n🚀 Deploy to Azure:");
|
|
160
|
-
console.log(
|
|
328
|
+
console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
|
|
161
329
|
if (cicdProvider !== 'skip') {
|
|
162
330
|
console.log(" Configure CI/CD secrets and push to repository");
|
|
163
331
|
}
|
|
@@ -171,12 +339,17 @@ async function initCommand(options) {
|
|
|
171
339
|
process.exit(1);
|
|
172
340
|
}
|
|
173
341
|
}
|
|
174
|
-
async function createNextJsProject(projectName) {
|
|
342
|
+
async function createNextJsProject(projectName, pm) {
|
|
175
343
|
return new Promise((resolve, reject) => {
|
|
176
344
|
console.log('\n📦 Creating Next.js project with create-next-app...\n');
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
345
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
346
|
+
// Build args: for pnpm use "pnpm dlx create-next-app@latest ... --use-pnpm"
|
|
347
|
+
// for npm use "npx create-next-app@latest ..."
|
|
348
|
+
const baseArgs = pm === 'pnpm'
|
|
349
|
+
? ['dlx', 'create-next-app@latest']
|
|
350
|
+
: ['create-next-app@latest'];
|
|
351
|
+
const args = [
|
|
352
|
+
...baseArgs,
|
|
180
353
|
projectName,
|
|
181
354
|
'--typescript',
|
|
182
355
|
'--tailwind',
|
|
@@ -185,9 +358,11 @@ async function createNextJsProject(projectName) {
|
|
|
185
358
|
'--disable-git',
|
|
186
359
|
'--import-alias',
|
|
187
360
|
'@/*',
|
|
188
|
-
|
|
361
|
+
...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
|
|
189
362
|
'--yes'
|
|
190
|
-
]
|
|
363
|
+
];
|
|
364
|
+
// Run create-next-app with recommended options for Azure
|
|
365
|
+
const createNextApp = (0, child_process_1.spawn)(pm === 'pnpm' ? 'pnpm' : 'npx', args, {
|
|
191
366
|
stdio: 'inherit',
|
|
192
367
|
shell: true,
|
|
193
368
|
});
|
|
@@ -205,83 +380,273 @@ async function createNextJsProject(projectName) {
|
|
|
205
380
|
});
|
|
206
381
|
});
|
|
207
382
|
}
|
|
208
|
-
async function upgradeNextJs(projectDir, version) {
|
|
383
|
+
async function upgradeNextJs(projectDir, version, pm) {
|
|
384
|
+
console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
|
|
385
|
+
// pnpm: pnpm add next@... ; npm: npm install next@...
|
|
386
|
+
const args = pm === 'pnpm'
|
|
387
|
+
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
|
|
388
|
+
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
|
|
389
|
+
await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
|
|
390
|
+
console.log(`\n✅ Next.js ${version} installed\n`);
|
|
391
|
+
}
|
|
392
|
+
async function installDependencies(projectDir, pm = 'pnpm') {
|
|
393
|
+
console.log('\n📦 Installing dependencies...\n');
|
|
394
|
+
await runPackageManagerCommand(pm, ['install'], projectDir, `${pm} install`);
|
|
395
|
+
console.log('\n✅ Dependencies installed\n');
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Run a package-manager command with stdio passed through to the user.
|
|
399
|
+
*
|
|
400
|
+
* For pnpm, additionally tee stdout/stderr to a buffer so that if the command
|
|
401
|
+
* fails due to `ERR_PNPM_IGNORED_BUILDS`, we can:
|
|
402
|
+
* 1. detect which packages were ignored,
|
|
403
|
+
* 2. interactively ask the user whether to approve them, and
|
|
404
|
+
* 3. run `pnpm approve-builds` followed by a retry of the original command.
|
|
405
|
+
*/
|
|
406
|
+
async function runPackageManagerCommand(pm, args, projectDir, label) {
|
|
407
|
+
const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
|
|
408
|
+
if (code === 0)
|
|
409
|
+
return;
|
|
410
|
+
if (pm === 'pnpm') {
|
|
411
|
+
const ignoredBuilds = parseIgnoredBuilds(output);
|
|
412
|
+
if (ignoredBuilds.length > 0) {
|
|
413
|
+
const approved = await maybeApproveBuilds(projectDir, ignoredBuilds);
|
|
414
|
+
if (approved) {
|
|
415
|
+
// Retry the original command now that build scripts are approved.
|
|
416
|
+
const retry = await spawnAndCapture(pm, args, projectDir, true);
|
|
417
|
+
if (retry.code === 0)
|
|
418
|
+
return;
|
|
419
|
+
throw new Error(`${label} exited with code ${retry.code} after approving builds`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`${label} exited with code ${code}`);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Spawn a child process inheriting stdin (so interactive prompts still work)
|
|
427
|
+
* while either inheriting or capturing+teeing stdout/stderr.
|
|
428
|
+
*/
|
|
429
|
+
function spawnAndCapture(pm, args, projectDir, capture) {
|
|
209
430
|
return new Promise((resolve, reject) => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'
|
|
213
|
-
|
|
214
|
-
`react@latest`,
|
|
215
|
-
`react-dom@latest`,
|
|
216
|
-
'--save-exact'
|
|
217
|
-
], {
|
|
431
|
+
const stdio = capture
|
|
432
|
+
? ['inherit', 'pipe', 'pipe']
|
|
433
|
+
: ['inherit', 'inherit', 'inherit'];
|
|
434
|
+
const child = (0, child_process_1.spawn)(pm, args, {
|
|
218
435
|
cwd: projectDir,
|
|
219
|
-
stdio
|
|
436
|
+
stdio,
|
|
220
437
|
shell: true,
|
|
221
438
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
439
|
+
let combined = '';
|
|
440
|
+
if (capture) {
|
|
441
|
+
child.stdout?.on('data', (chunk) => {
|
|
442
|
+
process.stdout.write(chunk);
|
|
443
|
+
combined += chunk.toString('utf8');
|
|
444
|
+
});
|
|
445
|
+
child.stderr?.on('data', (chunk) => {
|
|
446
|
+
process.stderr.write(chunk);
|
|
447
|
+
combined += chunk.toString('utf8');
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
|
|
451
|
+
child.on('error', reject);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
|
|
456
|
+
* return the bare package names (without versions), de-duplicated.
|
|
457
|
+
*/
|
|
458
|
+
function parseIgnoredBuilds(output) {
|
|
459
|
+
const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
|
|
460
|
+
if (!match)
|
|
461
|
+
return [];
|
|
462
|
+
const list = match[1]
|
|
463
|
+
.split(',')
|
|
464
|
+
.map((entry) => entry.trim())
|
|
465
|
+
// strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
|
|
466
|
+
.map((entry) => entry.replace(/@[^@]+$/, ''))
|
|
467
|
+
.filter((name) => name.length > 0);
|
|
468
|
+
return Array.from(new Set(list));
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Prompt the user and, if approved, run `pnpm approve-builds` followed by
|
|
472
|
+
* `pnpm rebuild <packages>`. Returns true if the user approved (regardless of
|
|
473
|
+
* which packages they actually selected inside approve-builds).
|
|
474
|
+
*/
|
|
475
|
+
async function maybeApproveBuilds(projectDir, ignoredBuilds) {
|
|
476
|
+
console.log(`\n⚠️ pnpm refused to run build scripts for: ${ignoredBuilds.join(', ')}\n` +
|
|
477
|
+
' These packages (e.g. sharp) need their build scripts to run correctly.\n');
|
|
478
|
+
const response = await (0, prompts_1.default)({
|
|
479
|
+
type: 'confirm',
|
|
480
|
+
name: 'approve',
|
|
481
|
+
message: 'Run `pnpm approve-builds` now to approve these build scripts?',
|
|
482
|
+
initial: true,
|
|
234
483
|
});
|
|
484
|
+
if (!response.approve) {
|
|
485
|
+
console.log('\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n');
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
await runSimple('pnpm', ['approve-builds'], projectDir);
|
|
489
|
+
await runSimple('pnpm', ['rebuild', ...ignoredBuilds], projectDir);
|
|
490
|
+
console.log('\n✅ Build scripts approved and packages rebuilt\n');
|
|
491
|
+
return true;
|
|
235
492
|
}
|
|
236
|
-
|
|
493
|
+
function runSimple(command, args, cwd) {
|
|
237
494
|
return new Promise((resolve, reject) => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
});
|
|
244
|
-
npmInstall.on('close', (code) => {
|
|
245
|
-
if (code !== 0) {
|
|
246
|
-
reject(new Error(`npm install exited with code ${code}`));
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
console.log('\n✅ Dependencies installed\n');
|
|
495
|
+
const child = (0, child_process_1.spawn)(command, args, { cwd, stdio: 'inherit', shell: true });
|
|
496
|
+
child.on('close', (code) => {
|
|
497
|
+
if (code !== 0)
|
|
498
|
+
reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
|
|
499
|
+
else
|
|
250
500
|
resolve();
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
npmInstall.on('error', (error) => {
|
|
254
|
-
reject(error);
|
|
255
501
|
});
|
|
502
|
+
child.on('error', reject);
|
|
256
503
|
});
|
|
257
504
|
}
|
|
258
|
-
|
|
505
|
+
function injectSwallowKitNextConfig(nextConfigContent, projectName) {
|
|
506
|
+
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`);
|
|
507
|
+
}
|
|
508
|
+
function buildCSharpFunctionsProgramSource() {
|
|
509
|
+
return `using Microsoft.Azure.Functions.Worker;
|
|
510
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
511
|
+
using Microsoft.Extensions.Hosting;
|
|
512
|
+
using Microsoft.Azure.Functions.Worker.ApplicationInsights;
|
|
513
|
+
|
|
514
|
+
var host = new HostBuilder()
|
|
515
|
+
.ConfigureFunctionsWorkerDefaults()
|
|
516
|
+
.ConfigureServices(services =>
|
|
517
|
+
{
|
|
518
|
+
services.AddApplicationInsightsTelemetryWorkerService();
|
|
519
|
+
services.ConfigureFunctionsApplicationInsights();
|
|
520
|
+
})
|
|
521
|
+
.Build();
|
|
522
|
+
|
|
523
|
+
host.Run();
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
function buildCSharpFunctionsProjectSource() {
|
|
527
|
+
return `<Project Sdk="Microsoft.NET.Sdk">
|
|
528
|
+
<PropertyGroup>
|
|
529
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
530
|
+
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
|
531
|
+
<OutputType>Exe</OutputType>
|
|
532
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
533
|
+
<Nullable>enable</Nullable>
|
|
534
|
+
</PropertyGroup>
|
|
535
|
+
<ItemGroup>
|
|
536
|
+
<Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />
|
|
537
|
+
<EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
|
|
538
|
+
<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
|
|
539
|
+
</ItemGroup>
|
|
540
|
+
<ItemGroup>
|
|
541
|
+
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
|
|
542
|
+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
543
|
+
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
|
544
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
|
545
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
|
|
546
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
|
|
547
|
+
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
|
|
548
|
+
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
|
549
|
+
</ItemGroup>
|
|
550
|
+
</Project>
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
function buildSwallowKitConfigSource(backendLanguage) {
|
|
554
|
+
return `module.exports = {
|
|
555
|
+
backend: {
|
|
556
|
+
language: '${backendLanguage}',
|
|
557
|
+
},
|
|
558
|
+
functions: {
|
|
559
|
+
baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
560
|
+
},
|
|
561
|
+
deployment: {
|
|
562
|
+
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
563
|
+
swaName: process.env.AZURE_SWA_NAME || '',
|
|
564
|
+
},
|
|
565
|
+
}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
function buildGeneratedProjectDependencies(projectName) {
|
|
569
|
+
return {
|
|
570
|
+
'@azure/cosmos': '^4.0.0',
|
|
571
|
+
'applicationinsights': '^3.3.0',
|
|
572
|
+
[`@${projectName}/shared`]: 'workspace:*',
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function getSwallowKitPackageMetadata() {
|
|
576
|
+
const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
|
|
577
|
+
try {
|
|
578
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
579
|
+
return {
|
|
580
|
+
name: packageJson.name || "swallowkit",
|
|
581
|
+
version: packageJson.version || "latest",
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// Fall back to the published package name when package metadata is unavailable.
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
name: "swallowkit",
|
|
589
|
+
version: "latest",
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function buildGeneratedProjectDevDependencies() {
|
|
593
|
+
const { name, version } = getSwallowKitPackageMetadata();
|
|
594
|
+
return {
|
|
595
|
+
[name]: version,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function buildSwallowKitMcpProjectConfigSource() {
|
|
599
|
+
const { name } = getSwallowKitPackageMetadata();
|
|
600
|
+
return JSON.stringify({
|
|
601
|
+
mcpServers: {
|
|
602
|
+
swallowkit: {
|
|
603
|
+
command: "node",
|
|
604
|
+
args: [`./node_modules/${name}/dist/mcp/index.js`],
|
|
605
|
+
cwd: ".",
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
}, null, 2);
|
|
609
|
+
}
|
|
610
|
+
async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
|
|
259
611
|
console.log('📦 Adding SwallowKit files...\n');
|
|
260
612
|
const projectName = options.name;
|
|
261
|
-
// 1. Update package.json to add
|
|
613
|
+
// 1. Update package.json to add runtime dependencies for generated projects
|
|
262
614
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
263
615
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
264
|
-
// Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
|
|
265
616
|
// zod is in the shared workspace package, not here
|
|
266
617
|
packageJson.dependencies = {
|
|
267
618
|
...packageJson.dependencies,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
619
|
+
...buildGeneratedProjectDependencies(projectName),
|
|
620
|
+
};
|
|
621
|
+
packageJson.devDependencies = {
|
|
622
|
+
...packageJson.devDependencies,
|
|
623
|
+
...buildGeneratedProjectDevDependencies(),
|
|
272
624
|
};
|
|
273
625
|
packageJson.scripts = {
|
|
274
626
|
...packageJson.scripts,
|
|
275
|
-
'build':
|
|
627
|
+
'build': (0, package_manager_1.getBuildScript)(pm),
|
|
276
628
|
'start': 'next start',
|
|
277
|
-
'functions:start':
|
|
629
|
+
'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
|
|
278
630
|
};
|
|
279
631
|
packageJson.engines = {
|
|
280
632
|
node: '20.x',
|
|
281
633
|
};
|
|
282
|
-
//
|
|
283
|
-
|
|
634
|
+
// Workspace configuration depends on package manager
|
|
635
|
+
const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
|
|
636
|
+
const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
|
|
637
|
+
if (wsConfig.type === 'file') {
|
|
638
|
+
// pnpm: workspaces are defined in pnpm-workspace.yaml
|
|
639
|
+
delete packageJson.workspaces;
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// npm: workspaces are defined in package.json
|
|
643
|
+
packageJson.workspaces = wsConfig.value;
|
|
644
|
+
}
|
|
284
645
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
646
|
+
// Create workspace config file if needed (pnpm-workspace.yaml)
|
|
647
|
+
if (wsConfig.type === 'file') {
|
|
648
|
+
fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
|
|
649
|
+
}
|
|
285
650
|
// Don't install yet — wait until all workspace packages (shared, functions) are created
|
|
286
651
|
// 2. Update next.config to add standalone output
|
|
287
652
|
// Check for both .ts and .js variants
|
|
@@ -291,11 +656,9 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig)
|
|
|
291
656
|
}
|
|
292
657
|
if (fs.existsSync(nextConfigPath)) {
|
|
293
658
|
let nextConfigContent = fs.readFileSync(nextConfigPath, 'utf-8');
|
|
294
|
-
// Add output
|
|
659
|
+
// Add output, transpiled workspace package, and server externals for standalone deployment
|
|
295
660
|
if (!nextConfigContent.includes("output:") && !nextConfigContent.includes('output =')) {
|
|
296
|
-
|
|
297
|
-
// Handle JavaScript config format: const nextConfig = {
|
|
298
|
-
nextConfigContent = nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n experimental: {\n turbopackUseSystemTlsCerts: true,\n },\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
|
|
661
|
+
nextConfigContent = injectSwallowKitNextConfig(nextConfigContent, projectName);
|
|
299
662
|
fs.writeFileSync(nextConfigPath, nextConfigContent);
|
|
300
663
|
}
|
|
301
664
|
}
|
|
@@ -315,17 +678,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig)
|
|
|
315
678
|
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
316
679
|
}
|
|
317
680
|
// 3. Create SwallowKit config
|
|
318
|
-
const swallowkitConfig =
|
|
319
|
-
module.exports = {
|
|
320
|
-
functions: {
|
|
321
|
-
baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
|
|
322
|
-
},
|
|
323
|
-
deployment: {
|
|
324
|
-
resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
|
|
325
|
-
swaName: process.env.AZURE_SWA_NAME || '',
|
|
326
|
-
},
|
|
327
|
-
}
|
|
328
|
-
`;
|
|
681
|
+
const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
|
|
329
682
|
fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
|
|
330
683
|
// 4. Create shared workspace package for Zod models (Single Source of Truth)
|
|
331
684
|
await createSharedPackage(projectDir, projectName);
|
|
@@ -335,197 +688,197 @@ module.exports = {
|
|
|
335
688
|
const apiLibDir = path.join(libDir, 'api');
|
|
336
689
|
fs.mkdirSync(apiLibDir, { recursive: true });
|
|
337
690
|
// Create backend utility for calling Azure Functions
|
|
338
|
-
const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
|
|
339
|
-
function getFunctionsBaseUrl(): string {
|
|
340
|
-
return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Simple HTTP client for calling backend APIs
|
|
345
|
-
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
346
|
-
*/
|
|
347
|
-
async function request<T>(
|
|
348
|
-
endpoint: string,
|
|
349
|
-
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
350
|
-
body?: any,
|
|
351
|
-
queryParams?: Record<string, string>
|
|
352
|
-
): Promise<T> {
|
|
353
|
-
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
354
|
-
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
355
|
-
if (queryParams) {
|
|
356
|
-
const params = new URLSearchParams(queryParams);
|
|
357
|
-
url += \`?\${params.toString()}\`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
const response = await fetch(url, {
|
|
362
|
-
method,
|
|
363
|
-
headers: {
|
|
364
|
-
'Content-Type': 'application/json',
|
|
365
|
-
},
|
|
366
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
if (!response.ok) {
|
|
370
|
-
const text = await response.text();
|
|
371
|
-
let errorMessage = text || 'Failed to call backend function';
|
|
372
|
-
try {
|
|
373
|
-
const error = JSON.parse(text);
|
|
374
|
-
errorMessage = error.error || error.message || text;
|
|
375
|
-
} catch {
|
|
376
|
-
// If not JSON, use text as-is
|
|
377
|
-
}
|
|
378
|
-
throw new Error(errorMessage);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const contentType = response.headers.get('content-type');
|
|
382
|
-
if (!contentType?.includes('application/json')) {
|
|
383
|
-
const text = await response.text();
|
|
384
|
-
return text as T;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return await response.json();
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error('Error calling backend:', error);
|
|
390
|
-
throw error;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Generic API client for making HTTP requests
|
|
396
|
-
* Simply calls endpoints - no DB dependencies, no schema validation
|
|
397
|
-
* Validation happens on the backend (BFF/Functions)
|
|
398
|
-
*
|
|
399
|
-
* @example
|
|
400
|
-
* // Call custom endpoint
|
|
401
|
-
* await api.get('/api/greet?name=World')
|
|
402
|
-
*
|
|
403
|
-
* // Call scaffolded CRUD endpoints
|
|
404
|
-
* await api.get('/api/todos')
|
|
405
|
-
* await api.post('/api/todos', { title: 'New task' })
|
|
406
|
-
* await api.put('/api/todos/123', { title: 'Updated' })
|
|
407
|
-
* await api.delete('/api/todos/123')
|
|
408
|
-
*/
|
|
409
|
-
export const api = {
|
|
410
|
-
/**
|
|
411
|
-
* Make a GET request
|
|
412
|
-
*/
|
|
413
|
-
get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
|
|
414
|
-
return request<T>(endpoint, 'GET', undefined, params);
|
|
415
|
-
},
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Make a POST request
|
|
419
|
-
*/
|
|
420
|
-
post: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
421
|
-
return request<T>(endpoint, 'POST', body);
|
|
422
|
-
},
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Make a PUT request
|
|
426
|
-
*/
|
|
427
|
-
put: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
428
|
-
return request<T>(endpoint, 'PUT', body);
|
|
429
|
-
},
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Make a DELETE request
|
|
433
|
-
*/
|
|
434
|
-
delete: <T>(endpoint: string): Promise<T> => {
|
|
435
|
-
return request<T>(endpoint, 'DELETE');
|
|
436
|
-
},
|
|
437
|
-
};
|
|
691
|
+
const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
|
|
692
|
+
function getFunctionsBaseUrl(): string {
|
|
693
|
+
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Simple HTTP client for calling backend APIs
|
|
698
|
+
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
699
|
+
*/
|
|
700
|
+
async function request<T>(
|
|
701
|
+
endpoint: string,
|
|
702
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
703
|
+
body?: any,
|
|
704
|
+
queryParams?: Record<string, string>
|
|
705
|
+
): Promise<T> {
|
|
706
|
+
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
707
|
+
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
708
|
+
if (queryParams) {
|
|
709
|
+
const params = new URLSearchParams(queryParams);
|
|
710
|
+
url += \`?\${params.toString()}\`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
const response = await fetch(url, {
|
|
715
|
+
method,
|
|
716
|
+
headers: {
|
|
717
|
+
'Content-Type': 'application/json',
|
|
718
|
+
},
|
|
719
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
const text = await response.text();
|
|
724
|
+
let errorMessage = text || 'Failed to call backend function';
|
|
725
|
+
try {
|
|
726
|
+
const error = JSON.parse(text);
|
|
727
|
+
errorMessage = error.error || error.message || text;
|
|
728
|
+
} catch {
|
|
729
|
+
// If not JSON, use text as-is
|
|
730
|
+
}
|
|
731
|
+
throw new Error(errorMessage);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const contentType = response.headers.get('content-type');
|
|
735
|
+
if (!contentType?.includes('application/json')) {
|
|
736
|
+
const text = await response.text();
|
|
737
|
+
return text as T;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return await response.json();
|
|
741
|
+
} catch (error) {
|
|
742
|
+
console.error('Error calling backend:', error);
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Generic API client for making HTTP requests
|
|
749
|
+
* Simply calls endpoints - no DB dependencies, no schema validation
|
|
750
|
+
* Validation happens on the backend (BFF/Functions)
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* // Call custom endpoint
|
|
754
|
+
* await api.get('/api/greet?name=World')
|
|
755
|
+
*
|
|
756
|
+
* // Call scaffolded CRUD endpoints
|
|
757
|
+
* await api.get('/api/todos')
|
|
758
|
+
* await api.post('/api/todos', { title: 'New task' })
|
|
759
|
+
* await api.put('/api/todos/123', { title: 'Updated' })
|
|
760
|
+
* await api.delete('/api/todos/123')
|
|
761
|
+
*/
|
|
762
|
+
export const api = {
|
|
763
|
+
/**
|
|
764
|
+
* Make a GET request
|
|
765
|
+
*/
|
|
766
|
+
get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
|
|
767
|
+
return request<T>(endpoint, 'GET', undefined, params);
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Make a POST request
|
|
772
|
+
*/
|
|
773
|
+
post: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
774
|
+
return request<T>(endpoint, 'POST', body);
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Make a PUT request
|
|
779
|
+
*/
|
|
780
|
+
put: <T>(endpoint: string, body?: any): Promise<T> => {
|
|
781
|
+
return request<T>(endpoint, 'PUT', body);
|
|
782
|
+
},
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Make a DELETE request
|
|
786
|
+
*/
|
|
787
|
+
delete: <T>(endpoint: string): Promise<T> => {
|
|
788
|
+
return request<T>(endpoint, 'DELETE');
|
|
789
|
+
},
|
|
790
|
+
};
|
|
438
791
|
`;
|
|
439
792
|
fs.writeFileSync(path.join(apiLibDir, 'backend.ts'), backendUtilContent);
|
|
440
793
|
// 5. Create components directory
|
|
441
794
|
const componentsDir = path.join(projectDir, 'components');
|
|
442
795
|
fs.mkdirSync(componentsDir, { recursive: true });
|
|
443
796
|
// 6. Create .env.example
|
|
444
|
-
const envExample = `# Azure Functions Backend URL
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
# Azure Configuration
|
|
448
|
-
AZURE_RESOURCE_GROUP=your-resource-group
|
|
449
|
-
AZURE_SWA_NAME=your-static-web-app-name
|
|
797
|
+
const envExample = `# Azure Functions Backend URL
|
|
798
|
+
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
799
|
+
|
|
800
|
+
# Azure Configuration
|
|
801
|
+
AZURE_RESOURCE_GROUP=your-resource-group
|
|
802
|
+
AZURE_SWA_NAME=your-static-web-app-name
|
|
450
803
|
`;
|
|
451
804
|
fs.writeFileSync(path.join(projectDir, '.env.example'), envExample);
|
|
452
805
|
// 7. Create instrumentation.ts for Application Insights (Next.js official way)
|
|
453
|
-
const instrumentationContent = `// Application Insights instrumentation for Next.js
|
|
454
|
-
// This file is automatically loaded by Next.js when instrumentationHook is enabled
|
|
455
|
-
export async function register() {
|
|
456
|
-
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
457
|
-
// Only run on server-side
|
|
458
|
-
const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
|
|
459
|
-
|
|
460
|
-
if (connectionString) {
|
|
461
|
-
const appInsights = await import('applicationinsights');
|
|
462
|
-
|
|
463
|
-
appInsights
|
|
464
|
-
.setup(connectionString)
|
|
465
|
-
.setAutoCollectConsole(true)
|
|
466
|
-
.setAutoCollectDependencies(true)
|
|
467
|
-
.setAutoCollectExceptions(true)
|
|
468
|
-
.setAutoCollectHeartbeat(true)
|
|
469
|
-
.setAutoCollectPerformance(true, true)
|
|
470
|
-
.setAutoCollectRequests(true)
|
|
471
|
-
.setAutoDependencyCorrelation(true)
|
|
472
|
-
.setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
|
|
473
|
-
.setSendLiveMetrics(true)
|
|
474
|
-
.setUseDiskRetryCaching(true);
|
|
475
|
-
|
|
476
|
-
appInsights.defaultClient.setAutoPopulateAzureProperties();
|
|
477
|
-
appInsights.start();
|
|
478
|
-
|
|
479
|
-
// Override console methods to send to Application Insights
|
|
480
|
-
const originalConsoleLog = console.log;
|
|
481
|
-
const originalConsoleError = console.error;
|
|
482
|
-
const originalConsoleWarn = console.warn;
|
|
483
|
-
|
|
484
|
-
console.log = function(...args: any[]) {
|
|
485
|
-
originalConsoleLog.apply(console, args);
|
|
486
|
-
const message = args.map(arg =>
|
|
487
|
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
488
|
-
).join(' ');
|
|
489
|
-
appInsights.defaultClient.trackTrace({
|
|
490
|
-
message: message,
|
|
491
|
-
severity: '1'
|
|
492
|
-
});
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
console.error = function(...args: any[]) {
|
|
496
|
-
originalConsoleError.apply(console, args);
|
|
497
|
-
const message = args.map(arg =>
|
|
498
|
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
499
|
-
).join(' ');
|
|
500
|
-
appInsights.defaultClient.trackTrace({
|
|
501
|
-
message: message,
|
|
502
|
-
severity: '3'
|
|
503
|
-
});
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
console.warn = function(...args: any[]) {
|
|
507
|
-
originalConsoleWarn.apply(console, args);
|
|
508
|
-
const message = args.map(arg =>
|
|
509
|
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
510
|
-
).join(' ');
|
|
511
|
-
appInsights.defaultClient.trackTrace({
|
|
512
|
-
message: message,
|
|
513
|
-
severity: '2'
|
|
514
|
-
});
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
|
|
518
|
-
} else {
|
|
519
|
-
console.log('[App Insights] Not configured (skipped in development mode)');
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
806
|
+
const instrumentationContent = `// Application Insights instrumentation for Next.js
|
|
807
|
+
// This file is automatically loaded by Next.js when instrumentationHook is enabled
|
|
808
|
+
export async function register() {
|
|
809
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
810
|
+
// Only run on server-side
|
|
811
|
+
const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
|
|
812
|
+
|
|
813
|
+
if (connectionString) {
|
|
814
|
+
const appInsights = await import('applicationinsights');
|
|
815
|
+
|
|
816
|
+
appInsights
|
|
817
|
+
.setup(connectionString)
|
|
818
|
+
.setAutoCollectConsole(true)
|
|
819
|
+
.setAutoCollectDependencies(true)
|
|
820
|
+
.setAutoCollectExceptions(true)
|
|
821
|
+
.setAutoCollectHeartbeat(true)
|
|
822
|
+
.setAutoCollectPerformance(true, true)
|
|
823
|
+
.setAutoCollectRequests(true)
|
|
824
|
+
.setAutoDependencyCorrelation(true)
|
|
825
|
+
.setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
|
|
826
|
+
.setSendLiveMetrics(true)
|
|
827
|
+
.setUseDiskRetryCaching(true);
|
|
828
|
+
|
|
829
|
+
appInsights.defaultClient.setAutoPopulateAzureProperties();
|
|
830
|
+
appInsights.start();
|
|
831
|
+
|
|
832
|
+
// Override console methods to send to Application Insights
|
|
833
|
+
const originalConsoleLog = console.log;
|
|
834
|
+
const originalConsoleError = console.error;
|
|
835
|
+
const originalConsoleWarn = console.warn;
|
|
836
|
+
|
|
837
|
+
console.log = function(...args: any[]) {
|
|
838
|
+
originalConsoleLog.apply(console, args);
|
|
839
|
+
const message = args.map(arg =>
|
|
840
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
841
|
+
).join(' ');
|
|
842
|
+
appInsights.defaultClient.trackTrace({
|
|
843
|
+
message: message,
|
|
844
|
+
severity: '1'
|
|
845
|
+
});
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
console.error = function(...args: any[]) {
|
|
849
|
+
originalConsoleError.apply(console, args);
|
|
850
|
+
const message = args.map(arg =>
|
|
851
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
852
|
+
).join(' ');
|
|
853
|
+
appInsights.defaultClient.trackTrace({
|
|
854
|
+
message: message,
|
|
855
|
+
severity: '3'
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
console.warn = function(...args: any[]) {
|
|
860
|
+
originalConsoleWarn.apply(console, args);
|
|
861
|
+
const message = args.map(arg =>
|
|
862
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
863
|
+
).join(' ');
|
|
864
|
+
appInsights.defaultClient.trackTrace({
|
|
865
|
+
message: message,
|
|
866
|
+
severity: '2'
|
|
867
|
+
});
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
|
|
871
|
+
} else {
|
|
872
|
+
console.log('[App Insights] Not configured (skipped in development mode)');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
523
876
|
`;
|
|
524
877
|
fs.writeFileSync(path.join(projectDir, 'instrumentation.ts'), instrumentationContent);
|
|
525
878
|
// 8. Create .env.local for local development
|
|
526
879
|
const envLocalContent = [
|
|
527
880
|
'# Azure Functions Backend URL (Local)',
|
|
528
|
-
'
|
|
881
|
+
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
529
882
|
''
|
|
530
883
|
].join('\n');
|
|
531
884
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -551,17 +904,19 @@ export async function register() {
|
|
|
551
904
|
};
|
|
552
905
|
fs.writeFileSync(path.join(projectDir, 'staticwebapp.config.json'), JSON.stringify(swaConfig, null, 2));
|
|
553
906
|
// 14. Create Azure Functions project
|
|
554
|
-
await createAzureFunctionsProject(projectDir);
|
|
907
|
+
await createAzureFunctionsProject(projectDir, pm, backendLanguage);
|
|
555
908
|
// 15. Create BFF API route to call Azure Functions
|
|
556
909
|
await createBffApiRoute(projectDir);
|
|
557
910
|
// 16. Create home page
|
|
558
|
-
await createHomePage(projectDir);
|
|
911
|
+
await createHomePage(projectDir, pm);
|
|
559
912
|
// 17. Install all workspace dependencies (root + shared + functions)
|
|
560
913
|
console.log('📦 Installing workspace dependencies...\n');
|
|
561
|
-
await installDependencies(projectDir);
|
|
914
|
+
await installDependencies(projectDir, pm);
|
|
562
915
|
console.log('✅ Project structure created\n');
|
|
563
916
|
// 18. Create README.md
|
|
564
|
-
createReadme(projectDir, projectName, cicdChoice, azureConfig);
|
|
917
|
+
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
|
|
918
|
+
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
919
|
+
createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
|
|
565
920
|
}
|
|
566
921
|
async function createSharedPackage(projectDir, projectName) {
|
|
567
922
|
console.log('📦 Creating shared workspace package for Zod models...\n');
|
|
@@ -570,6 +925,7 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
570
925
|
fs.mkdirSync(modelsDir, { recursive: true });
|
|
571
926
|
// shared/package.json
|
|
572
927
|
const sharedPackageJson = {
|
|
928
|
+
private: true,
|
|
573
929
|
name: `@${projectName}/shared`,
|
|
574
930
|
version: '1.0.0',
|
|
575
931
|
description: 'Shared Zod models — Single Source of Truth for validation schemas',
|
|
@@ -588,37 +944,101 @@ async function createSharedPackage(projectDir, projectName) {
|
|
|
588
944
|
};
|
|
589
945
|
fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
|
|
590
946
|
// shared/tsconfig.json
|
|
591
|
-
|
|
592
|
-
compilerOptions: {
|
|
593
|
-
target: 'ES2020',
|
|
594
|
-
module: 'commonjs',
|
|
595
|
-
moduleResolution: 'node',
|
|
596
|
-
lib: ['ES2020'],
|
|
597
|
-
outDir: 'dist',
|
|
598
|
-
rootDir: '.',
|
|
599
|
-
declaration: true,
|
|
600
|
-
declarationMap: true,
|
|
601
|
-
sourceMap: true,
|
|
602
|
-
strict: true,
|
|
603
|
-
esModuleInterop: true,
|
|
604
|
-
skipLibCheck: true,
|
|
605
|
-
forceConsistentCasingInFileNames: true,
|
|
606
|
-
},
|
|
607
|
-
include: ['index.ts', 'models/**/*'],
|
|
608
|
-
exclude: ['node_modules', 'dist'],
|
|
609
|
-
};
|
|
610
|
-
fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(sharedTsConfig, null, 2));
|
|
947
|
+
fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(buildSharedTsConfig(), null, 2));
|
|
611
948
|
// shared/index.ts (empty re-export file, scaffold will add entries)
|
|
612
949
|
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`);
|
|
613
950
|
// shared/.gitignore
|
|
614
951
|
fs.writeFileSync(path.join(sharedDir, '.gitignore'), `node_modules\ndist\n`);
|
|
615
952
|
console.log('✅ Shared package created\n');
|
|
616
953
|
}
|
|
617
|
-
async function createAzureFunctionsProject(projectDir) {
|
|
618
|
-
console.log(
|
|
954
|
+
async function createAzureFunctionsProject(projectDir, pm = 'pnpm', backendLanguage = 'typescript') {
|
|
955
|
+
console.log(`📦 Creating Azure Functions project (${getBackendLanguageLabel(backendLanguage)})...\n`);
|
|
619
956
|
const functionsDir = path.join(projectDir, 'functions');
|
|
620
957
|
fs.mkdirSync(functionsDir, { recursive: true });
|
|
621
|
-
|
|
958
|
+
const projectName = path.basename(projectDir);
|
|
959
|
+
const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
|
|
960
|
+
createFunctionsHostFiles(functionsDir, databaseName, backendLanguage);
|
|
961
|
+
if (backendLanguage === 'typescript') {
|
|
962
|
+
createTypeScriptFunctionsProject(projectDir, functionsDir, pm);
|
|
963
|
+
}
|
|
964
|
+
else if (backendLanguage === 'csharp') {
|
|
965
|
+
createCSharpFunctionsProject(projectDir, functionsDir);
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
createPythonFunctionsProject(projectDir, functionsDir);
|
|
969
|
+
}
|
|
970
|
+
console.log('✅ Azure Functions project created\n');
|
|
971
|
+
}
|
|
972
|
+
function createFunctionsHostFiles(functionsDir, databaseName, backendLanguage) {
|
|
973
|
+
const hostJson = {
|
|
974
|
+
version: '2.0',
|
|
975
|
+
logging: {
|
|
976
|
+
applicationInsights: {
|
|
977
|
+
samplingSettings: {
|
|
978
|
+
isEnabled: true,
|
|
979
|
+
maxTelemetryItemsPerSecond: 20,
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
extensionBundle: {
|
|
984
|
+
id: 'Microsoft.Azure.Functions.ExtensionBundle',
|
|
985
|
+
version: '[4.0.0, 4.10.0)',
|
|
986
|
+
},
|
|
987
|
+
};
|
|
988
|
+
fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
|
|
989
|
+
const localSettings = {
|
|
990
|
+
IsEncrypted: false,
|
|
991
|
+
Values: {
|
|
992
|
+
AzureWebJobsStorage: '',
|
|
993
|
+
FUNCTIONS_WORKER_RUNTIME: getFunctionsWorkerRuntime(backendLanguage),
|
|
994
|
+
AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
|
|
995
|
+
CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
|
|
996
|
+
COSMOS_DB_DATABASE_NAME: databaseName,
|
|
997
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
|
|
1001
|
+
const gitignoreLines = [
|
|
1002
|
+
'local.settings.json',
|
|
1003
|
+
'*.log',
|
|
1004
|
+
'.vscode',
|
|
1005
|
+
'.DS_Store',
|
|
1006
|
+
];
|
|
1007
|
+
if (backendLanguage === 'typescript') {
|
|
1008
|
+
gitignoreLines.unshift('node_modules', 'dist');
|
|
1009
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `node_modules
|
|
1010
|
+
.git
|
|
1011
|
+
.vscode
|
|
1012
|
+
local.settings.json
|
|
1013
|
+
test
|
|
1014
|
+
tsconfig.json
|
|
1015
|
+
*.ts
|
|
1016
|
+
!dist/**/*.js
|
|
1017
|
+
`);
|
|
1018
|
+
}
|
|
1019
|
+
else if (backendLanguage === 'python') {
|
|
1020
|
+
gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
|
|
1021
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
|
|
1022
|
+
.codegen-venv
|
|
1023
|
+
__pycache__
|
|
1024
|
+
.pytest_cache
|
|
1025
|
+
.mypy_cache
|
|
1026
|
+
.ruff_cache
|
|
1027
|
+
local.settings.json
|
|
1028
|
+
tests
|
|
1029
|
+
`);
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
gitignoreLines.unshift('bin', 'obj');
|
|
1033
|
+
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `bin
|
|
1034
|
+
obj
|
|
1035
|
+
local.settings.json
|
|
1036
|
+
tests
|
|
1037
|
+
`);
|
|
1038
|
+
}
|
|
1039
|
+
fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
|
|
1040
|
+
}
|
|
1041
|
+
function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
|
|
622
1042
|
const functionsPackageJson = {
|
|
623
1043
|
name: 'functions',
|
|
624
1044
|
version: '1.0.0',
|
|
@@ -627,22 +1047,21 @@ async function createAzureFunctionsProject(projectDir) {
|
|
|
627
1047
|
scripts: {
|
|
628
1048
|
start: 'func start',
|
|
629
1049
|
build: 'tsc',
|
|
630
|
-
prestart:
|
|
1050
|
+
prestart: (0, package_manager_1.getFunctionsPrestart)(pm),
|
|
631
1051
|
},
|
|
632
1052
|
dependencies: {
|
|
633
1053
|
'@azure/functions': '~4.5.0',
|
|
634
1054
|
'@azure/cosmos': '^4.0.0',
|
|
635
1055
|
'@azure/identity': '^4.0.0',
|
|
636
|
-
|
|
637
|
-
[`@${path.basename(projectDir)}/shared`]: '
|
|
1056
|
+
zod: '>=3.25.0',
|
|
1057
|
+
[`@${path.basename(projectDir)}/shared`]: 'workspace:*',
|
|
638
1058
|
},
|
|
639
1059
|
devDependencies: {
|
|
640
1060
|
'@types/node': '^20.0.0',
|
|
641
|
-
|
|
642
|
-
}
|
|
1061
|
+
typescript: '^5.0.0',
|
|
1062
|
+
},
|
|
643
1063
|
};
|
|
644
1064
|
fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
|
|
645
|
-
// Create functions tsconfig.json
|
|
646
1065
|
const sharedPkgName = `@${path.basename(projectDir)}/shared`;
|
|
647
1066
|
const functionsTsConfig = {
|
|
648
1067
|
compilerOptions: {
|
|
@@ -662,294 +1081,333 @@ async function createAzureFunctionsProject(projectDir) {
|
|
|
662
1081
|
},
|
|
663
1082
|
},
|
|
664
1083
|
include: ['src/**/*'],
|
|
665
|
-
exclude: ['node_modules', 'dist']
|
|
1084
|
+
exclude: ['node_modules', 'dist'],
|
|
666
1085
|
};
|
|
667
1086
|
fs.writeFileSync(path.join(functionsDir, 'tsconfig.json'), JSON.stringify(functionsTsConfig, null, 2));
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1087
|
+
const srcDir = path.join(functionsDir, 'src');
|
|
1088
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
1089
|
+
fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
|
|
1090
|
+
import { z } from 'zod/v4';
|
|
1091
|
+
|
|
1092
|
+
const greetRequestSchema = z.object({
|
|
1093
|
+
name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
|
1097
|
+
context.log('HTTP trigger function processed a request.');
|
|
1098
|
+
|
|
1099
|
+
try {
|
|
1100
|
+
const name = request.query.get('name') || (await request.text());
|
|
1101
|
+
const result = greetRequestSchema.safeParse({ name });
|
|
1102
|
+
|
|
1103
|
+
if (!result.success) {
|
|
1104
|
+
return {
|
|
1105
|
+
status: 400,
|
|
1106
|
+
jsonBody: {
|
|
1107
|
+
error: result.error.issues[0].message
|
|
682
1108
|
}
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
|
|
1113
|
+
|
|
1114
|
+
return {
|
|
1115
|
+
status: 200,
|
|
1116
|
+
jsonBody: {
|
|
1117
|
+
message: greeting,
|
|
1118
|
+
timestamp: new Date().toISOString()
|
|
1119
|
+
}
|
|
683
1120
|
};
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
tsconfig.json
|
|
692
|
-
*.ts
|
|
693
|
-
!dist/**/*.js
|
|
694
|
-
`;
|
|
695
|
-
fs.writeFileSync(path.join(functionsDir, '.funcignore'), funcignore);
|
|
696
|
-
// Create .gitignore for functions directory
|
|
697
|
-
const functionsGitignore = `node_modules
|
|
698
|
-
dist
|
|
699
|
-
local.settings.json
|
|
700
|
-
*.log
|
|
701
|
-
.vscode
|
|
702
|
-
.DS_Store
|
|
703
|
-
`;
|
|
704
|
-
fs.writeFileSync(path.join(functionsDir, '.gitignore'), functionsGitignore);
|
|
705
|
-
// Create local.settings.json
|
|
706
|
-
const projectName = path.basename(projectDir);
|
|
707
|
-
const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
|
|
708
|
-
const localSettings = {
|
|
709
|
-
IsEncrypted: false,
|
|
710
|
-
Values: {
|
|
711
|
-
AzureWebJobsStorage: '',
|
|
712
|
-
FUNCTIONS_WORKER_RUNTIME: 'node',
|
|
713
|
-
AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
|
|
714
|
-
CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
|
|
715
|
-
COSMOS_DB_DATABASE_NAME: databaseName,
|
|
716
|
-
NODE_TLS_REJECT_UNAUTHORIZED: '0'
|
|
717
|
-
}
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
context.error('Error processing request:', error);
|
|
1123
|
+
return {
|
|
1124
|
+
status: 500,
|
|
1125
|
+
jsonBody: {
|
|
1126
|
+
error: 'Internal server error'
|
|
1127
|
+
}
|
|
718
1128
|
};
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
app.http('greet', {
|
|
1133
|
+
methods: ['GET', 'POST'],
|
|
1134
|
+
authLevel: 'anonymous',
|
|
1135
|
+
handler: greet
|
|
1136
|
+
});
|
|
1137
|
+
`);
|
|
1138
|
+
}
|
|
1139
|
+
function createCSharpFunctionsProject(projectDir, functionsDir) {
|
|
1140
|
+
const projectBaseName = path.basename(projectDir);
|
|
1141
|
+
const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
|
|
1142
|
+
const csprojName = `${projectPascal}.Functions.csproj`;
|
|
1143
|
+
fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
|
|
1144
|
+
fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
|
|
1145
|
+
fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
|
|
1146
|
+
fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
|
|
1147
|
+
const crudDir = path.join(functionsDir, 'Crud');
|
|
1148
|
+
fs.mkdirSync(crudDir, { recursive: true });
|
|
1149
|
+
fs.writeFileSync(path.join(crudDir, 'GreetFunction.cs'), `using System.Net;
|
|
1150
|
+
using Microsoft.Azure.Functions.Worker;
|
|
1151
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
1152
|
+
|
|
1153
|
+
namespace SwallowKit.Functions;
|
|
1154
|
+
|
|
1155
|
+
public sealed class GreetFunction
|
|
1156
|
+
{
|
|
1157
|
+
[Function("greet")]
|
|
1158
|
+
public async Task<HttpResponseData> Run(
|
|
1159
|
+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
|
|
1160
|
+
{
|
|
1161
|
+
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
1162
|
+
var name = "SwallowKit";
|
|
1163
|
+
foreach (var segment in query)
|
|
1164
|
+
{
|
|
1165
|
+
var parts = segment.Split('=', 2);
|
|
1166
|
+
if (parts.Length == 2 && parts[0] == "name")
|
|
1167
|
+
{
|
|
1168
|
+
name = Uri.UnescapeDataString(parts[1]);
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
var response = request.CreateResponse(HttpStatusCode.OK);
|
|
1173
|
+
await response.WriteAsJsonAsync(new
|
|
1174
|
+
{
|
|
1175
|
+
message = $"Hello, {name}! This message is from Azure Functions.",
|
|
1176
|
+
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
|
1177
|
+
});
|
|
1178
|
+
return response;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
`);
|
|
1182
|
+
}
|
|
1183
|
+
function createPythonFunctionsProject(projectDir, functionsDir) {
|
|
1184
|
+
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1185
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1186
|
+
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
1187
|
+
azure-cosmos>=4.9.0
|
|
1188
|
+
azure-identity>=1.19.0
|
|
1189
|
+
`);
|
|
1190
|
+
fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
|
|
1191
|
+
const blueprintsDir = path.join(functionsDir, 'blueprints');
|
|
1192
|
+
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
1193
|
+
fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
|
|
1194
|
+
fs.writeFileSync(path.join(blueprintsDir, 'greet.py'), `import json
|
|
1195
|
+
from datetime import datetime, timezone
|
|
1196
|
+
|
|
1197
|
+
import azure.functions as func
|
|
1198
|
+
|
|
1199
|
+
bp = func.Blueprint()
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
@bp.route(route="greet", methods=["GET", "POST"])
|
|
1203
|
+
def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
1204
|
+
name = req.params.get("name") or "SwallowKit"
|
|
1205
|
+
payload = {
|
|
1206
|
+
"message": f"Hello, {name}! This message is from Azure Functions.",
|
|
1207
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
1208
|
+
}
|
|
1209
|
+
return func.HttpResponse(
|
|
1210
|
+
body=json.dumps(payload, ensure_ascii=False),
|
|
1211
|
+
status_code=200,
|
|
1212
|
+
mimetype="application/json",
|
|
1213
|
+
)
|
|
1214
|
+
`);
|
|
1215
|
+
fs.writeFileSync(path.join(functionsDir, 'function_app.py'), `import azure.functions as func
|
|
1216
|
+
|
|
1217
|
+
from blueprints.greet import bp as greet_bp
|
|
1218
|
+
|
|
1219
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
|
|
1220
|
+
|
|
1221
|
+
app.register_blueprint(greet_bp)
|
|
1222
|
+
# SwallowKit scaffold registrations
|
|
1223
|
+
`);
|
|
1224
|
+
}
|
|
1225
|
+
function ensureProjectGitignoreEntry(projectDir, entry) {
|
|
1226
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
1227
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const current = fs.readFileSync(gitignorePath, 'utf8');
|
|
1231
|
+
const lines = current.split(/\r?\n/);
|
|
1232
|
+
if (lines.includes(entry)) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const normalized = current.endsWith('\n') ? current : `${current}\n`;
|
|
1236
|
+
fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
|
|
780
1237
|
}
|
|
781
1238
|
async function createBffApiRoute(projectDir) {
|
|
782
1239
|
console.log('📦 Creating BFF API route...\n');
|
|
783
1240
|
const apiDir = path.join(projectDir, 'app', 'api', 'greet');
|
|
784
1241
|
fs.mkdirSync(apiDir, { recursive: true });
|
|
785
1242
|
// Create API route that calls Azure Functions using shared utility
|
|
786
|
-
const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
|
|
787
|
-
import { api } from '@/lib/api/backend';
|
|
788
|
-
|
|
789
|
-
interface GreetResponse {
|
|
790
|
-
message: string;
|
|
791
|
-
timestamp: string;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
export async function GET(request: NextRequest) {
|
|
795
|
-
try {
|
|
796
|
-
const { searchParams } = new URL(request.url);
|
|
797
|
-
const name = searchParams.get('name') || 'World';
|
|
798
|
-
|
|
799
|
-
const data = await api.get<GreetResponse>('/api/greet', { name });
|
|
800
|
-
|
|
801
|
-
return NextResponse.json(data);
|
|
802
|
-
} catch (error) {
|
|
803
|
-
console.error('Error calling Azure Functions:', error);
|
|
804
|
-
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
805
|
-
return NextResponse.json(
|
|
806
|
-
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
807
|
-
{ status: 500 }
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
export async function POST(request: NextRequest) {
|
|
813
|
-
try {
|
|
814
|
-
const body = await request.json();
|
|
815
|
-
|
|
816
|
-
const data = await api.post<GreetResponse>('/api/greet', body);
|
|
817
|
-
|
|
818
|
-
return NextResponse.json(data);
|
|
819
|
-
} catch (error) {
|
|
820
|
-
console.error('Error calling Azure Functions:', error);
|
|
821
|
-
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
822
|
-
return NextResponse.json(
|
|
823
|
-
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
824
|
-
{ status: 500 }
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
1243
|
+
const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
|
|
1244
|
+
import { api } from '@/lib/api/backend';
|
|
1245
|
+
|
|
1246
|
+
interface GreetResponse {
|
|
1247
|
+
message: string;
|
|
1248
|
+
timestamp: string;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
export async function GET(request: NextRequest) {
|
|
1252
|
+
try {
|
|
1253
|
+
const { searchParams } = new URL(request.url);
|
|
1254
|
+
const name = searchParams.get('name') || 'World';
|
|
1255
|
+
|
|
1256
|
+
const data = await api.get<GreetResponse>('/api/greet', { name });
|
|
1257
|
+
|
|
1258
|
+
return NextResponse.json(data);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
console.error('Error calling Azure Functions:', error);
|
|
1261
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
1262
|
+
return NextResponse.json(
|
|
1263
|
+
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
1264
|
+
{ status: 500 }
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export async function POST(request: NextRequest) {
|
|
1270
|
+
try {
|
|
1271
|
+
const body = await request.json();
|
|
1272
|
+
|
|
1273
|
+
const data = await api.post<GreetResponse>('/api/greet', body);
|
|
1274
|
+
|
|
1275
|
+
return NextResponse.json(data);
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
console.error('Error calling Azure Functions:', error);
|
|
1278
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
|
|
1279
|
+
return NextResponse.json(
|
|
1280
|
+
{ error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
|
|
1281
|
+
{ status: 500 }
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
828
1285
|
`;
|
|
829
1286
|
fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
|
|
830
|
-
// Update .env.example to include
|
|
1287
|
+
// Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
|
|
831
1288
|
const envExamplePath = path.join(projectDir, '.env.example');
|
|
832
1289
|
let envExample = fs.readFileSync(envExamplePath, 'utf-8');
|
|
833
|
-
if (!envExample.includes('
|
|
1290
|
+
if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
|
|
834
1291
|
envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
|
|
835
1292
|
fs.writeFileSync(envExamplePath, envExample);
|
|
836
1293
|
}
|
|
837
1294
|
// Update .env.local
|
|
838
1295
|
const envLocalPath = path.join(projectDir, '.env.local');
|
|
839
1296
|
let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
|
|
840
|
-
if (!envLocal.includes('
|
|
1297
|
+
if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
|
|
841
1298
|
envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
|
|
842
1299
|
fs.writeFileSync(envLocalPath, envLocal);
|
|
843
1300
|
}
|
|
844
1301
|
console.log('✅ BFF API route created\n');
|
|
845
1302
|
}
|
|
846
|
-
async function createHomePage(projectDir) {
|
|
1303
|
+
async function createHomePage(projectDir, pm = 'pnpm') {
|
|
847
1304
|
console.log('📦 Creating home page...\n');
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
import {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const [
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
<p className="text-green-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
<p className="text-red-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
{
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
<
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
<
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
<
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1305
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
1306
|
+
const pageContent = `'use client'
|
|
1307
|
+
|
|
1308
|
+
export const dynamic = 'force-dynamic';
|
|
1309
|
+
|
|
1310
|
+
import { useState } from 'react';
|
|
1311
|
+
import { scaffoldConfig } from '@/lib/scaffold-config';
|
|
1312
|
+
|
|
1313
|
+
export default function Home() {
|
|
1314
|
+
const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
1315
|
+
const [message, setMessage] = useState('');
|
|
1316
|
+
|
|
1317
|
+
const testConnection = async () => {
|
|
1318
|
+
setGreetingStatus('loading');
|
|
1319
|
+
try {
|
|
1320
|
+
const response = await fetch('/api/greet?name=SwallowKit');
|
|
1321
|
+
const data = await response.json();
|
|
1322
|
+
if (!response.ok) {
|
|
1323
|
+
throw new Error(data.error || \`Server error: \${response.status}\`);
|
|
1324
|
+
}
|
|
1325
|
+
setMessage(data.message);
|
|
1326
|
+
setGreetingStatus('success');
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
|
|
1329
|
+
setGreetingStatus('error');
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
return (
|
|
1334
|
+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
|
|
1335
|
+
<div className="container mx-auto px-4 py-12">
|
|
1336
|
+
<header className="text-center mb-16">
|
|
1337
|
+
<h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
|
|
1338
|
+
Welcome to SwallowKit
|
|
1339
|
+
</h1>
|
|
1340
|
+
<p className="text-xl text-gray-600 dark:text-gray-400">
|
|
1341
|
+
Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
|
|
1342
|
+
</p>
|
|
1343
|
+
</header>
|
|
1344
|
+
|
|
1345
|
+
{/* Connection Test */}
|
|
1346
|
+
<section className="max-w-2xl mx-auto mb-12">
|
|
1347
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
|
|
1348
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
|
1349
|
+
Test BFF → Functions Connection
|
|
1350
|
+
</h2>
|
|
1351
|
+
<button
|
|
1352
|
+
onClick={testConnection}
|
|
1353
|
+
disabled={greetingStatus === 'loading'}
|
|
1354
|
+
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
|
|
1355
|
+
>
|
|
1356
|
+
{greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
|
|
1357
|
+
</button>
|
|
1358
|
+
{greetingStatus === 'success' && (
|
|
1359
|
+
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
1360
|
+
<p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
|
|
1361
|
+
<p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
|
|
1362
|
+
</div>
|
|
1363
|
+
)}
|
|
1364
|
+
{greetingStatus === 'error' && (
|
|
1365
|
+
<div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
1366
|
+
<p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
|
|
1367
|
+
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
|
|
1368
|
+
</div>
|
|
1369
|
+
)}
|
|
1370
|
+
</div>
|
|
1371
|
+
</section>
|
|
1372
|
+
|
|
1373
|
+
{/* Scaffolded Models Menu */}
|
|
1374
|
+
{scaffoldConfig.models.length > 0 ? (
|
|
1375
|
+
<section className="max-w-6xl mx-auto">
|
|
1376
|
+
<h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
|
|
1377
|
+
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
1378
|
+
{scaffoldConfig.models.map((model) => (
|
|
1379
|
+
<a
|
|
1380
|
+
key={model.name}
|
|
1381
|
+
href={model.path}
|
|
1382
|
+
className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
|
|
1383
|
+
>
|
|
1384
|
+
<h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
|
|
1385
|
+
<p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
|
|
1386
|
+
</a>
|
|
1387
|
+
))}
|
|
1388
|
+
</div>
|
|
1389
|
+
</section>
|
|
1390
|
+
) : (
|
|
1391
|
+
<section className="max-w-2xl mx-auto text-center">
|
|
1392
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
|
|
1393
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
|
|
1394
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
1395
|
+
Create your first model with Zod and generate CRUD operations automatically.
|
|
1396
|
+
</p>
|
|
1397
|
+
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
1398
|
+
${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
|
|
1399
|
+
</code>
|
|
1400
|
+
</div>
|
|
1401
|
+
</section>
|
|
1402
|
+
)}
|
|
1403
|
+
|
|
1404
|
+
<footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
|
|
1405
|
+
<p>Built with SwallowKit</p>
|
|
1406
|
+
</footer>
|
|
1407
|
+
</div>
|
|
1408
|
+
</div>
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
953
1411
|
`;
|
|
954
1412
|
fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), pageContent);
|
|
955
1413
|
console.log('✅ Home page created\n');
|
|
@@ -958,1277 +1416,1916 @@ export default function Home() {
|
|
|
958
1416
|
if (!fs.existsSync(scaffoldConfigDir)) {
|
|
959
1417
|
fs.mkdirSync(scaffoldConfigDir, { recursive: true });
|
|
960
1418
|
}
|
|
961
|
-
const scaffoldConfigContent = `export interface ScaffoldModel {
|
|
962
|
-
name: string;
|
|
963
|
-
path: string;
|
|
964
|
-
label: string;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
export const scaffoldConfig = {
|
|
968
|
-
models: [
|
|
969
|
-
// Scaffolded models will be added here by '
|
|
970
|
-
] as ScaffoldModel[]
|
|
971
|
-
};
|
|
1419
|
+
const scaffoldConfigContent = `export interface ScaffoldModel {
|
|
1420
|
+
name: string;
|
|
1421
|
+
path: string;
|
|
1422
|
+
label: string;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
export const scaffoldConfig = {
|
|
1426
|
+
models: [
|
|
1427
|
+
// Scaffolded models will be added here by 'swallowkit scaffold' command
|
|
1428
|
+
] as ScaffoldModel[]
|
|
1429
|
+
};
|
|
972
1430
|
`;
|
|
973
1431
|
fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
|
|
974
1432
|
console.log('✅ Scaffold config created\n');
|
|
975
1433
|
}
|
|
976
|
-
function createReadme(projectDir, projectName, cicdChoice, azureConfig) {
|
|
1434
|
+
function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage) {
|
|
977
1435
|
console.log('📝 Creating README.md...\n');
|
|
1436
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
978
1437
|
const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
|
|
979
1438
|
const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
|
|
980
1439
|
const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
|
|
981
1440
|
'Outbound VNet (Cosmos DB Private Endpoint)';
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
-
|
|
991
|
-
|
|
992
|
-
-
|
|
993
|
-
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
- **Azure Functions
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
\`\`\`
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
-
|
|
1182
|
-
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1441
|
+
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1442
|
+
const schemaBridgeDescription = backendLanguage === 'typescript'
|
|
1443
|
+
? 'Zod (shared between frontend and backend)'
|
|
1444
|
+
: `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
|
|
1445
|
+
const functionsTree = backendLanguage === 'typescript'
|
|
1446
|
+
? `│ └── src/\n│ └── greet.ts # Sample function`
|
|
1447
|
+
: backendLanguage === 'csharp'
|
|
1448
|
+
? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
|
|
1449
|
+
: `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
|
|
1450
|
+
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1451
|
+
? '- Azure Functions CRUD endpoints'
|
|
1452
|
+
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1453
|
+
const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
|
|
1454
|
+
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1455
|
+
? `\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`
|
|
1456
|
+
: '';
|
|
1457
|
+
const readme = `# ${projectName}
|
|
1458
|
+
|
|
1459
|
+
A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
|
|
1460
|
+
|
|
1461
|
+
## 🚀 Tech Stack
|
|
1462
|
+
|
|
1463
|
+
- **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
|
|
1464
|
+
- **BFF (Backend for Frontend)**: Next.js API Routes
|
|
1465
|
+
- **Backend**: Azure Functions (${backendLanguageLabel})
|
|
1466
|
+
- **Database**: Azure Cosmos DB
|
|
1467
|
+
- **Schema Validation**: ${schemaBridgeDescription}
|
|
1468
|
+
- **Infrastructure**: Bicep (Infrastructure as Code)
|
|
1469
|
+
- **CI/CD**: ${cicdLabel}
|
|
1470
|
+
|
|
1471
|
+
## 📋 Project Configuration
|
|
1472
|
+
|
|
1473
|
+
This project was initialized with the following settings:
|
|
1474
|
+
|
|
1475
|
+
- **Azure Functions Plan**: Flex Consumption
|
|
1476
|
+
- **Cosmos DB Mode**: ${cosmosDbModeLabel}
|
|
1477
|
+
- **Network Security**: ${vnetLabel}
|
|
1478
|
+
- **CI/CD**: ${cicdLabel}
|
|
1479
|
+
|
|
1480
|
+
## ✅ Prerequisites
|
|
1481
|
+
|
|
1482
|
+
Before you begin, ensure you have the following installed:
|
|
1483
|
+
|
|
1484
|
+
1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1485
|
+
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1486
|
+
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1487
|
+
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
1488
|
+
${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local development
|
|
1489
|
+
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1490
|
+
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1491
|
+
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1492
|
+
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
|
|
1493
|
+
|
|
1494
|
+
## 📁 Project Structure
|
|
1495
|
+
|
|
1496
|
+
\`\`\`
|
|
1497
|
+
${projectName}/
|
|
1498
|
+
├── app/ # Next.js App Router (frontend)
|
|
1499
|
+
│ ├── api/ # BFF API routes (proxy to Functions)
|
|
1500
|
+
│ └── page.tsx # Home page
|
|
1501
|
+
├── functions/ # Azure Functions (backend)
|
|
1502
|
+
${functionsTree}
|
|
1503
|
+
├── lib/
|
|
1504
|
+
│ └── api/ # API client utilities
|
|
1505
|
+
├── infra/ # Bicep infrastructure files
|
|
1506
|
+
│ ├── main.bicep
|
|
1507
|
+
│ └── modules/ # Bicep modules for each resource
|
|
1508
|
+
└── .github/workflows/ # CI/CD workflows
|
|
1509
|
+
\`\`\`
|
|
1510
|
+
|
|
1511
|
+
## 🏗️ Getting Started
|
|
1512
|
+
|
|
1513
|
+
### 1. Create Your First Model
|
|
1514
|
+
|
|
1515
|
+
Define your data model with Zod schema:
|
|
1516
|
+
|
|
1517
|
+
\`\`\`bash
|
|
1518
|
+
${pmCmd.dlx} swallowkit create-model <model-name>
|
|
1519
|
+
\`\`\`
|
|
1520
|
+
|
|
1521
|
+
This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
|
|
1522
|
+
|
|
1523
|
+
### 2. Generate CRUD Code
|
|
1524
|
+
|
|
1525
|
+
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1526
|
+
|
|
1527
|
+
\`\`\`bash
|
|
1528
|
+
${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
|
|
1529
|
+
\`\`\`
|
|
1530
|
+
|
|
1531
|
+
This generates:
|
|
1532
|
+
${backendScaffoldNote}
|
|
1533
|
+
- Next.js BFF API routes
|
|
1534
|
+
- React UI components (list, detail, create, edit)
|
|
1535
|
+
- Navigation menu integration
|
|
1536
|
+
|
|
1537
|
+
### 3. Start Development Servers
|
|
1538
|
+
|
|
1539
|
+
\`\`\`bash
|
|
1540
|
+
${pmCmd.dlx} swallowkit dev
|
|
1541
|
+
\`\`\`
|
|
1542
|
+
|
|
1543
|
+
This starts:
|
|
1544
|
+
- Next.js dev server (http://localhost:3000)
|
|
1545
|
+
- Azure Functions (http://localhost:7071)
|
|
1546
|
+
- Cosmos DB Emulator check (must be running separately)
|
|
1547
|
+
|
|
1548
|
+
**Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
|
|
1549
|
+
${pythonLocalDevNote}
|
|
1550
|
+
|
|
1551
|
+
## ☁️ Deploy to Azure
|
|
1552
|
+
|
|
1553
|
+
### Provision Azure Resources
|
|
1554
|
+
|
|
1555
|
+
Create all required Azure resources using Bicep:
|
|
1556
|
+
|
|
1557
|
+
\`\`\`bash
|
|
1558
|
+
${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
|
|
1559
|
+
\`\`\`
|
|
1560
|
+
|
|
1561
|
+
This creates:
|
|
1562
|
+
- Static Web App (\`swa-${projectName}\`)
|
|
1563
|
+
- Azure Functions (\`func-${projectName}\`)
|
|
1564
|
+
- Cosmos DB (\`cosmos-${projectName}\`)
|
|
1565
|
+
- Storage Account
|
|
1566
|
+
|
|
1567
|
+
You will be prompted to select Azure regions:
|
|
1568
|
+
1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
|
|
1569
|
+
2. **Static Web App location**: Limited availability (default: East Asia)
|
|
1570
|
+
|
|
1571
|
+
### CI/CD Setup
|
|
1572
|
+
|
|
1573
|
+
${cicdChoice === 'github' ? `#### GitHub Actions
|
|
1574
|
+
|
|
1575
|
+
1. Get Static Web App deployment token:
|
|
1576
|
+
\`\`\`bash
|
|
1577
|
+
az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
|
|
1578
|
+
\`\`\`
|
|
1579
|
+
|
|
1580
|
+
2. Get Function App publish profile:
|
|
1581
|
+
\`\`\`bash
|
|
1582
|
+
az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
|
|
1583
|
+
\`\`\`
|
|
1584
|
+
|
|
1585
|
+
3. Add secrets to GitHub repository:
|
|
1586
|
+
- \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
|
|
1587
|
+
- \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
|
|
1588
|
+
- \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
|
|
1589
|
+
|
|
1590
|
+
4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
|
|
1591
|
+
|
|
1592
|
+
1. Set up service connection in Azure DevOps
|
|
1593
|
+
2. Update \`azure-pipelines.yml\` with your resource names
|
|
1594
|
+
3. Configure pipeline variables:
|
|
1595
|
+
- \`azureSubscription\`: Service connection name
|
|
1596
|
+
- \`resourceGroupName\`: Resource group name
|
|
1597
|
+
4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
|
|
1598
|
+
|
|
1599
|
+
**Deploy Static Web App:**
|
|
1600
|
+
\`\`\`bash
|
|
1601
|
+
${pmCmd.run} build
|
|
1602
|
+
az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
|
|
1603
|
+
\`\`\`
|
|
1604
|
+
|
|
1605
|
+
**Deploy Functions:**
|
|
1606
|
+
\`\`\`bash
|
|
1607
|
+
cd functions
|
|
1608
|
+
${pmCmd.run} build
|
|
1609
|
+
func azure functionapp publish func-${projectName}
|
|
1610
|
+
\`\`\``}
|
|
1611
|
+
|
|
1612
|
+
## 🔧 Available Commands
|
|
1613
|
+
|
|
1614
|
+
- \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
|
|
1615
|
+
- \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
|
|
1616
|
+
- \`${pmCmd.dlx} swallowkit dev\` - Start development servers
|
|
1617
|
+
- \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
|
|
1618
|
+
${azureConfig.vnetOption !== 'none' ? `
|
|
1619
|
+
## 🔒 Network Security (VNet Configuration)
|
|
1620
|
+
|
|
1621
|
+
This project is configured with **${vnetLabel}**.
|
|
1622
|
+
|
|
1623
|
+
### Architecture
|
|
1624
|
+
|
|
1625
|
+
\`\`\`
|
|
1626
|
+
Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
|
|
1627
|
+
│
|
|
1628
|
+
VNet Integration
|
|
1629
|
+
(outbound only)
|
|
1630
|
+
\`\`\`
|
|
1631
|
+
|
|
1632
|
+
- **Functions → Cosmos DB**: Connected via Private Endpoint (private connection)
|
|
1633
|
+
- **SWA → Functions**: Connected via public endpoint (secured with CORS + IP restrictions)
|
|
1634
|
+
|
|
1635
|
+
### VNet Resources
|
|
1636
|
+
|
|
1637
|
+
| Resource | Purpose |
|
|
1638
|
+
|----------|---------|
|
|
1639
|
+
| \`vnet-${projectName}\` | Virtual Network (10.0.0.0/16) |
|
|
1640
|
+
| \`snet-functions\` | Functions subnet (10.0.1.0/24) |
|
|
1641
|
+
| \`snet-private-endpoints\` | Private Endpoints subnet (10.0.2.0/24) |
|
|
1642
|
+
| \`pe-cosmos-${projectName}\` | Cosmos DB Private Endpoint |
|
|
1643
|
+
|
|
1644
|
+
### Private DNS Zones
|
|
1645
|
+
|
|
1646
|
+
- \`privatelink.documents.azure.com\` (Cosmos DB)
|
|
1647
|
+
` : ''}
|
|
1648
|
+
## 📚 Learn More
|
|
1649
|
+
|
|
1650
|
+
- [SwallowKit Documentation](https://github.com/himanago/swallowkit)
|
|
1651
|
+
- [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
|
|
1652
|
+
- [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
|
|
1653
|
+
- [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
|
|
1654
|
+
- [Next.js](https://nextjs.org/)
|
|
1655
|
+
- [Zod](https://zod.dev/)
|
|
1656
|
+
|
|
1657
|
+
## 💭 Feedback
|
|
1658
|
+
|
|
1659
|
+
This project was generated by SwallowKit. If you encounter any issues or have suggestions for improvements, please open an issue on the [SwallowKit repository](https://github.com/himanago/swallowkit).
|
|
1187
1660
|
`;
|
|
1188
1661
|
fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
|
|
1189
1662
|
console.log('✅ README.md created\n');
|
|
1190
1663
|
}
|
|
1191
|
-
|
|
1664
|
+
function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
|
|
1665
|
+
console.log('🤖 Creating AI agent instruction files...\n');
|
|
1666
|
+
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1667
|
+
const runCmd = pm === 'pnpm' ? 'pnpm' : 'npx';
|
|
1668
|
+
const projectMcpConfigSource = buildSwallowKitMcpProjectConfigSource();
|
|
1669
|
+
const functionsStructureLine = backendLanguage === 'typescript'
|
|
1670
|
+
? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
|
|
1671
|
+
: backendLanguage === 'csharp'
|
|
1672
|
+
? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # Native-generated C# schema assets`
|
|
1673
|
+
: `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # Native-generated Python schema assets`;
|
|
1674
|
+
const backendSchemaNote = backendLanguage === 'typescript'
|
|
1675
|
+
? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
|
|
1676
|
+
: `- The frontend/BFF source of truth stays in \`shared/models/\` as Zod schemas.\n- \`swallowkit scaffold\` exports OpenAPI into \`functions/openapi/\` and generates ${backendLanguageLabel} schema assets into \`functions/generated/\` with native ${backendLanguageLabel} tooling.`;
|
|
1677
|
+
const backendRulesNote = backendLanguage === 'typescript'
|
|
1678
|
+
? `- All CRUD operations and business logic live in \`functions/src/\`.\n- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.\n- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).\n- Validate all data against Zod schemas before writing to Cosmos DB.\n- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.`
|
|
1679
|
+
: `- All business logic lives in \`functions/\` and the generated handlers perform real Cosmos DB CRUD.\n- Keep Zod schemas in \`shared/models/\` as the source of truth.\n- Regenerate backend contracts with \`swallowkit scaffold shared/models/<name>.ts\` whenever a schema changes.\n- Use the native-generated schema assets in \`functions/generated/\` to keep backend contracts aligned.\n- The backend should still own \`id\`, \`createdAt\`, and \`updatedAt\`.`;
|
|
1680
|
+
// ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
|
|
1681
|
+
const agentsMd = `# AGENTS.md
|
|
1682
|
+
|
|
1683
|
+
This project was generated by **SwallowKit**.
|
|
1684
|
+
All coding agents **must** follow the architecture and conventions described below.
|
|
1685
|
+
|
|
1686
|
+
## Architecture Overview
|
|
1687
|
+
|
|
1688
|
+
This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
|
|
1689
|
+
|
|
1690
|
+
\`\`\`
|
|
1691
|
+
Frontend (React / Next.js App Router)
|
|
1692
|
+
↓ fetch('/api/{model}', ...)
|
|
1693
|
+
BFF Layer (Next.js API Routes)
|
|
1694
|
+
↓ HTTP → Azure Functions
|
|
1695
|
+
Backend (Azure Functions)
|
|
1696
|
+
↓
|
|
1697
|
+
Azure Cosmos DB (Document Database)
|
|
1698
|
+
\`\`\`
|
|
1699
|
+
|
|
1700
|
+
### Project Structure
|
|
1701
|
+
|
|
1702
|
+
\`\`\`
|
|
1703
|
+
${projectName}/
|
|
1704
|
+
├── app/ # Next.js App Router
|
|
1705
|
+
│ ├── api/ # BFF API routes (proxy to Azure Functions)
|
|
1706
|
+
│ └── {model}/ # UI pages per model (list, detail, create, edit)
|
|
1707
|
+
├── functions/ # Azure Functions (backend)
|
|
1708
|
+
${functionsStructureLine}
|
|
1709
|
+
├── shared/ # Shared workspace package
|
|
1710
|
+
│ ├── models/ # Zod schema definitions (single source of truth)
|
|
1711
|
+
│ └── index.ts # Re-exports all models
|
|
1712
|
+
├── lib/
|
|
1713
|
+
│ └── api/ # API client utilities (backend.ts, call-function.ts)
|
|
1714
|
+
├── components/ # Shared React components
|
|
1715
|
+
├── infra/ # Bicep infrastructure-as-code files
|
|
1716
|
+
│ ├── main.bicep
|
|
1717
|
+
│ └── modules/
|
|
1718
|
+
├── .mcp.json # Project-scoped MCP bootstrap using local installed SwallowKit
|
|
1719
|
+
└── .github/workflows/ # CI/CD workflows (if configured)
|
|
1720
|
+
\`\`\`
|
|
1721
|
+
|
|
1722
|
+
## SwallowKit MCP / Machine Workflow
|
|
1723
|
+
|
|
1724
|
+
- This repository includes a project-scoped \`.mcp.json\` file that starts the locally installed SwallowKit MCP server on runtimes that auto-load project MCP configurations.
|
|
1725
|
+
- Prefer the \`swallowkit_*\` MCP tools for framework-owned inspection, validation, and generation when they are available.
|
|
1726
|
+
- If MCP is unavailable in your runtime, fall back to the machine CLI:
|
|
1727
|
+
- \`${runCmd} swallowkit machine inspect project\`
|
|
1728
|
+
- \`${runCmd} swallowkit machine validate project\`
|
|
1729
|
+
- \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`
|
|
1730
|
+
- Do not hand-edit framework-owned artifacts when the MCP or machine interface can generate or validate them for you.
|
|
1731
|
+
- The local MCP bootstrap depends on project dependencies already being installed.
|
|
1732
|
+
- **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
|
|
1733
|
+
|
|
1734
|
+
## Critical Design Principles
|
|
1735
|
+
|
|
1736
|
+
### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
|
|
1737
|
+
|
|
1738
|
+
- \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
|
|
1739
|
+
- **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
|
|
1740
|
+
- The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
|
|
1741
|
+
- Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
|
|
1742
|
+
|
|
1743
|
+
Example BFF route pattern:
|
|
1744
|
+
|
|
1745
|
+
\`\`\`typescript
|
|
1746
|
+
// app/api/{model}/route.ts
|
|
1747
|
+
import { callFunction } from '@/lib/api/call-function';
|
|
1748
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
1749
|
+
import { z } from 'zod/v4';
|
|
1750
|
+
|
|
1751
|
+
export async function GET() {
|
|
1752
|
+
return callFunction({
|
|
1753
|
+
method: 'GET',
|
|
1754
|
+
path: '/api/{model}',
|
|
1755
|
+
responseSchema: z.array(ModelSchema),
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
export async function POST(request: NextRequest) {
|
|
1760
|
+
const body = await request.json();
|
|
1761
|
+
return callFunction({
|
|
1762
|
+
method: 'POST',
|
|
1763
|
+
path: '/api/{model}',
|
|
1764
|
+
body,
|
|
1765
|
+
inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
|
|
1766
|
+
responseSchema: ModelSchema,
|
|
1767
|
+
successStatus: 201,
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
\`\`\`
|
|
1771
|
+
|
|
1772
|
+
### 2. Zod Schemas Are the Single Source of Truth
|
|
1773
|
+
|
|
1774
|
+
- All data models are defined **once** as Zod schemas in \`shared/models/\`.
|
|
1775
|
+
- TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
|
|
1776
|
+
- ${backendSchemaNote}
|
|
1777
|
+
|
|
1778
|
+
Model definition pattern:
|
|
1779
|
+
|
|
1780
|
+
\`\`\`typescript
|
|
1781
|
+
// shared/models/{model}.ts
|
|
1782
|
+
import { z } from 'zod/v4';
|
|
1783
|
+
|
|
1784
|
+
export const Todo = z.object({
|
|
1785
|
+
id: z.string(),
|
|
1786
|
+
name: z.string().min(1),
|
|
1787
|
+
// ... your fields
|
|
1788
|
+
createdAt: z.string().optional(),
|
|
1789
|
+
updatedAt: z.string().optional(),
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
export type Todo = z.infer<typeof Todo>;
|
|
1793
|
+
export const displayName = 'Todo';
|
|
1794
|
+
\`\`\`
|
|
1795
|
+
|
|
1796
|
+
Key rules:
|
|
1797
|
+
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
1798
|
+
- \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
|
|
1799
|
+
- Always re-export models from \`shared/index.ts\`.
|
|
1800
|
+
|
|
1801
|
+
### 3. Azure Functions Own All Business Logic and Data Access
|
|
1802
|
+
|
|
1803
|
+
- ${backendRulesNote}
|
|
1804
|
+
|
|
1805
|
+
${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 the native schema assets under \`functions/generated/\` in sync.`}
|
|
1806
|
+
|
|
1807
|
+
${backendLanguage === 'typescript' ? `\`\`\`typescript
|
|
1808
|
+
// functions/src/{model}.ts
|
|
1809
|
+
import { app } from '@azure/functions';
|
|
1810
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
1811
|
+
|
|
1812
|
+
const containerName = 'Models'; // PascalCase + 's'
|
|
1813
|
+
|
|
1814
|
+
app.http('{model}-get-all', {
|
|
1815
|
+
methods: ['GET'],
|
|
1816
|
+
route: '{model}',
|
|
1817
|
+
authLevel: 'anonymous',
|
|
1818
|
+
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
1819
|
+
handler: async (request, context) => {
|
|
1820
|
+
const documents = context.extraInputs.get('cosmosInput');
|
|
1821
|
+
const validated = z.array(ModelSchema).parse(documents);
|
|
1822
|
+
return { status: 200, jsonBody: validated };
|
|
1823
|
+
},
|
|
1824
|
+
});
|
|
1825
|
+
\`\`\`` : ''}
|
|
1826
|
+
|
|
1827
|
+
## Naming Conventions
|
|
1828
|
+
|
|
1829
|
+
| Item | Convention | Example |
|
|
1830
|
+
|------|-----------|---------|
|
|
1831
|
+
| Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
|
|
1832
|
+
| Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
|
|
1833
|
+
| Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoCrudFunctions.cs' : 'functions/blueprints/todo.py'}\` |
|
|
1834
|
+
| Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
|
|
1835
|
+
| API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
|
|
1836
|
+
| BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
|
|
1837
|
+
| BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
|
|
1838
|
+
| UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
|
|
1839
|
+
| React component | PascalCase | \`TodoForm.tsx\` |
|
|
1840
|
+
| Cosmos DB container | PascalCase + 's' | \`Todos\` |
|
|
1841
|
+
| Cosmos DB partition key | \`/id\` (default) | Custom: \`export const partitionKey = '/field'\` |
|
|
1842
|
+
| Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
|
|
1843
|
+
|
|
1844
|
+
## Adding New Models (SwallowKit CLI Skills)
|
|
1845
|
+
|
|
1846
|
+
Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
|
|
1847
|
+
|
|
1848
|
+
### Skill: Create a new data model
|
|
1849
|
+
|
|
1850
|
+
\`\`\`bash
|
|
1851
|
+
${runCmd} swallowkit create-model <name>
|
|
1852
|
+
# Multiple models at once:
|
|
1853
|
+
${runCmd} swallowkit create-model user post comment
|
|
1854
|
+
\`\`\`
|
|
1855
|
+
|
|
1856
|
+
Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
|
|
1857
|
+
Edit the generated file to add your domain-specific fields, then run scaffold.
|
|
1858
|
+
|
|
1859
|
+
### Skill: Generate full CRUD from a model
|
|
1860
|
+
|
|
1861
|
+
\`\`\`bash
|
|
1862
|
+
${runCmd} swallowkit scaffold shared/models/<name>.ts
|
|
1863
|
+
\`\`\`
|
|
1864
|
+
|
|
1865
|
+
Generates:
|
|
1866
|
+
- Azure Functions handlers (${backendLanguage === 'typescript' ? '`functions/src/<name>.ts`' : '`functions/` language-specific CRUD files + `functions/generated/` schema assets'})
|
|
1867
|
+
- BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
|
|
1868
|
+
- UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
|
|
1869
|
+
- Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
|
|
1870
|
+
|
|
1871
|
+
### Skill: Start development servers
|
|
1872
|
+
|
|
1873
|
+
\`\`\`bash
|
|
1874
|
+
${runCmd} swallowkit dev
|
|
1875
|
+
\`\`\`
|
|
1876
|
+
|
|
1877
|
+
Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
|
|
1878
|
+
Checks for Cosmos DB Emulator availability.
|
|
1879
|
+
|
|
1880
|
+
### Skill: Provision Azure resources
|
|
1881
|
+
|
|
1882
|
+
\`\`\`bash
|
|
1883
|
+
${runCmd} swallowkit provision --resource-group <name> --location <region>
|
|
1884
|
+
\`\`\`
|
|
1885
|
+
|
|
1886
|
+
Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
|
|
1887
|
+
|
|
1888
|
+
### Typical workflow for "add a new feature/model"
|
|
1889
|
+
|
|
1890
|
+
1. \`${runCmd} swallowkit create-model <name>\`
|
|
1891
|
+
2. Edit \`shared/models/<name>.ts\` — add fields
|
|
1892
|
+
3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
|
|
1893
|
+
4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1894
|
+
5. If \`dev-seeds/\` already exists, update the seed JSON files to include realistic data for the new model and adjust existing seeds if relationships changed.
|
|
1895
|
+
|
|
1896
|
+
## Do NOT
|
|
1897
|
+
|
|
1898
|
+
- **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
|
|
1899
|
+
- **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
|
|
1900
|
+
- **Do not** manually duplicate model definitions across layers. Use the shared package.
|
|
1901
|
+
- **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
|
|
1902
|
+
- **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
|
|
1903
|
+
- By default, all containers use \`/id\` as the partition key. To use a custom partition key, add \`export const partitionKey = '/yourField'\` to the model file. The scaffold command will apply it across all layers.
|
|
1904
|
+
|
|
1905
|
+
## Technology Stack
|
|
1906
|
+
|
|
1907
|
+
- **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
|
|
1908
|
+
- **BFF**: Next.js API Routes (proxy only)
|
|
1909
|
+
- **Backend**: Azure Functions (${backendLanguageLabel})
|
|
1910
|
+
- **Database**: Azure Cosmos DB (NoSQL)
|
|
1911
|
+
- **Schema**: Zod (shared across all layers via workspace package)
|
|
1912
|
+
- **Infrastructure**: Bicep (IaC)
|
|
1913
|
+
- **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
|
|
1914
|
+
- **Auth**: Azure Managed Identity (no connection strings in production)
|
|
1915
|
+
- **Monitoring**: Application Insights
|
|
1916
|
+
`;
|
|
1917
|
+
fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
|
|
1918
|
+
console.log(' ✅ AGENTS.md (Codex / generic agents)');
|
|
1919
|
+
// ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
|
|
1920
|
+
const claudeMd = `# CLAUDE.md
|
|
1921
|
+
|
|
1922
|
+
This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
|
|
1923
|
+
|
|
1924
|
+
## Quick Reference
|
|
1925
|
+
|
|
1926
|
+
- **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
|
|
1927
|
+
- **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
|
|
1928
|
+
- **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
|
|
1929
|
+
- **Backend language**: ${backendLanguageLabel}
|
|
1930
|
+
- **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
|
|
1931
|
+
|
|
1932
|
+
## SwallowKit MCP
|
|
1933
|
+
|
|
1934
|
+
- This repository includes a project-scoped \`.mcp.json\` that registers the locally installed SwallowKit MCP server for runtimes that support project MCP files.
|
|
1935
|
+
- When the \`swallowkit_*\` tools are available, prefer them for inspect / validate / generate tasks.
|
|
1936
|
+
- If MCP is unavailable, use \`${runCmd} swallowkit machine ...\` instead.
|
|
1937
|
+
- **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
|
|
1938
|
+
|
|
1939
|
+
## SwallowKit CLI Commands
|
|
1940
|
+
|
|
1941
|
+
| Task | Command |
|
|
1942
|
+
|------|---------|
|
|
1943
|
+
| Create model | \`${runCmd} swallowkit create-model <name>\` |
|
|
1944
|
+
| Generate CRUD | \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` |
|
|
1945
|
+
| Dev servers | \`${runCmd} swallowkit dev\` |
|
|
1946
|
+
| Provision Azure | \`${runCmd} swallowkit provision --resource-group <rg> --location <region>\` |
|
|
1947
|
+
|
|
1948
|
+
## Workflow: Add a new model
|
|
1949
|
+
|
|
1950
|
+
1. \`${runCmd} swallowkit create-model <name>\`
|
|
1951
|
+
2. Edit \`shared/models/<name>.ts\` — add your fields
|
|
1952
|
+
3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
|
|
1953
|
+
4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
1954
|
+
5. If \`dev-seeds/\` exists, update the seed JSON files to include data for the new model and adjust existing seeds if relationships changed.
|
|
1955
|
+
`;
|
|
1956
|
+
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
|
|
1957
|
+
console.log(' ✅ CLAUDE.md (Claude Code)');
|
|
1958
|
+
fs.writeFileSync(path.join(projectDir, '.mcp.json'), projectMcpConfigSource);
|
|
1959
|
+
console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
|
|
1960
|
+
// ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
|
|
1961
|
+
const ghDir = path.join(projectDir, '.github');
|
|
1962
|
+
fs.mkdirSync(ghDir, { recursive: true });
|
|
1963
|
+
const copilotInstructions = `# Copilot Instructions
|
|
1964
|
+
|
|
1965
|
+
This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
|
|
1966
|
+
|
|
1967
|
+
## Architecture (3-layer)
|
|
1968
|
+
|
|
1969
|
+
\`\`\`
|
|
1970
|
+
Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
|
|
1971
|
+
\`\`\`
|
|
1972
|
+
|
|
1973
|
+
## Key Rules
|
|
1974
|
+
|
|
1975
|
+
1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
|
|
1976
|
+
2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
|
|
1977
|
+
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/\`.
|
|
1978
|
+
4. **Use the CLI** — Run \`${runCmd} swallowkit create-model <name>\` then \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
|
|
1979
|
+
5. **Maintain seed data** — When adding models or changing schemas, update the JSON files under \`dev-seeds/\` to keep seed data consistent with the current schema.
|
|
1980
|
+
|
|
1981
|
+
## SwallowKit Framework Operations
|
|
1982
|
+
|
|
1983
|
+
- Prefer the SwallowKit MCP or machine interface for framework-owned inspection, validation, and generation instead of hand-editing generated files.
|
|
1984
|
+
- If your runtime loads project-scoped MCP config from \`.mcp.json\`, use the \`swallowkit_*\` tools.
|
|
1985
|
+
- Otherwise use \`${runCmd} swallowkit machine inspect project\`, \`${runCmd} swallowkit machine validate project\`, and \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`.
|
|
1986
|
+
- **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
|
|
1987
|
+
|
|
1988
|
+
## Naming
|
|
1989
|
+
|
|
1990
|
+
- Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
|
|
1991
|
+
- Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
|
|
1992
|
+
- Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key default \`/id\` (customizable via \`export const partitionKey\`)
|
|
1993
|
+
|
|
1994
|
+
## Managed Fields
|
|
1995
|
+
|
|
1996
|
+
\`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
|
|
1997
|
+
`;
|
|
1998
|
+
fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
|
|
1999
|
+
console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
|
|
2000
|
+
// ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
|
|
2001
|
+
const instructionsDir = path.join(ghDir, 'instructions');
|
|
2002
|
+
fs.mkdirSync(instructionsDir, { recursive: true });
|
|
2003
|
+
// 4a. shared/models — Zod schema layer
|
|
2004
|
+
const sharedModelsInstructions = `---
|
|
2005
|
+
applyTo: "shared/models/**"
|
|
2006
|
+
---
|
|
2007
|
+
|
|
2008
|
+
# Shared Models — Zod Schema Rules
|
|
2009
|
+
|
|
2010
|
+
Files in this directory are the **single source of truth** for data models across the entire application.
|
|
2011
|
+
|
|
2012
|
+
## Rules
|
|
2013
|
+
|
|
2014
|
+
- Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
|
|
2015
|
+
- Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
|
|
2016
|
+
\`\`\`typescript
|
|
2017
|
+
export const Todo = z.object({ ... });
|
|
2018
|
+
export type Todo = z.infer<typeof Todo>;
|
|
2019
|
+
\`\`\`
|
|
2020
|
+
- Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
|
|
2021
|
+
- Export a \`displayName\` string constant for UI display.
|
|
2022
|
+
- Re-export every model from \`shared/index.ts\`.
|
|
2023
|
+
- For relationships, use **nested schemas** (import and embed the related schema), not ID references.
|
|
2024
|
+
- After editing a model, run \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
|
|
2025
|
+
`;
|
|
2026
|
+
fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
|
|
2027
|
+
// 4b. app/api — BFF layer
|
|
2028
|
+
const bffInstructions = `---
|
|
2029
|
+
applyTo: "app/api/**"
|
|
2030
|
+
---
|
|
2031
|
+
|
|
2032
|
+
# BFF API Routes — Rules
|
|
2033
|
+
|
|
2034
|
+
Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
|
|
2035
|
+
|
|
2036
|
+
## Rules
|
|
2037
|
+
|
|
2038
|
+
- **Never** put business logic, database access, or direct Cosmos DB calls here.
|
|
2039
|
+
- Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
|
|
2040
|
+
- You may validate input/output with Zod schemas before forwarding.
|
|
2041
|
+
- Import schemas from \`@${projectName}/shared\`.
|
|
2042
|
+
|
|
2043
|
+
## Pattern
|
|
2044
|
+
|
|
2045
|
+
\`\`\`typescript
|
|
2046
|
+
import { callFunction } from '@/lib/api/call-function';
|
|
2047
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
2048
|
+
import { z } from 'zod/v4';
|
|
2049
|
+
|
|
2050
|
+
export async function GET() {
|
|
2051
|
+
return callFunction({
|
|
2052
|
+
method: 'GET',
|
|
2053
|
+
path: '/api/{model}',
|
|
2054
|
+
responseSchema: z.array(ModelSchema),
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
\`\`\`
|
|
2058
|
+
`;
|
|
2059
|
+
fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
|
|
2060
|
+
// 4c. functions — Azure Functions backend layer
|
|
2061
|
+
const functionsInstructions = `---
|
|
2062
|
+
applyTo: "functions/**"
|
|
2063
|
+
---
|
|
2064
|
+
|
|
2065
|
+
# Azure Functions — Backend Rules
|
|
2066
|
+
|
|
2067
|
+
Files in \`functions/\` contain all business logic and data access for this application.
|
|
2068
|
+
|
|
2069
|
+
## Rules
|
|
2070
|
+
|
|
2071
|
+
- Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
|
|
2072
|
+
- For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
|
|
2073
|
+
- For C#/Python backends, consume the native-generated assets in \`functions/generated/\`.
|
|
2074
|
+
- Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
|
|
2075
|
+
- Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key defaults to \`/id\` but can be customized per model.
|
|
2076
|
+
|
|
2077
|
+
## Handler Pattern
|
|
2078
|
+
|
|
2079
|
+
\`\`\`typescript
|
|
2080
|
+
import { app } from '@azure/functions';
|
|
2081
|
+
import { ModelSchema } from '@${projectName}/shared';
|
|
2082
|
+
|
|
2083
|
+
app.http('{model}-get-all', {
|
|
2084
|
+
methods: ['GET'],
|
|
2085
|
+
route: '{model}',
|
|
2086
|
+
authLevel: 'anonymous',
|
|
2087
|
+
extraInputs: [cosmosInput],
|
|
2088
|
+
handler: async (request, context) => {
|
|
2089
|
+
const documents = context.extraInputs.get(cosmosInput);
|
|
2090
|
+
const validated = z.array(ModelSchema).parse(documents);
|
|
2091
|
+
return { status: 200, jsonBody: validated };
|
|
2092
|
+
},
|
|
2093
|
+
});
|
|
2094
|
+
\`\`\`
|
|
2095
|
+
`;
|
|
2096
|
+
fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
|
|
2097
|
+
console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
|
|
2098
|
+
console.log(' - shared-models.instructions.md');
|
|
2099
|
+
console.log(' - bff-routes.instructions.md');
|
|
2100
|
+
console.log(' - azure-functions.instructions.md');
|
|
2101
|
+
console.log('\n✅ AI agent files created\n');
|
|
2102
|
+
console.log(' Supported agents:');
|
|
2103
|
+
console.log(' - OpenAI Codex → AGENTS.md');
|
|
2104
|
+
console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
|
|
2105
|
+
console.log(' - Project MCP runtimes → .mcp.json');
|
|
2106
|
+
console.log(' - GitHub Copilot → .github/copilot-instructions.md');
|
|
2107
|
+
console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
|
|
2108
|
+
console.log('');
|
|
2109
|
+
}
|
|
2110
|
+
async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
|
|
1192
2111
|
console.log('📦 Creating infrastructure files (Bicep)...\n');
|
|
1193
2112
|
const infraDir = path.join(projectDir, 'infra');
|
|
1194
2113
|
const modulesDir = path.join(infraDir, 'modules');
|
|
1195
2114
|
fs.mkdirSync(modulesDir, { recursive: true });
|
|
1196
2115
|
const enableVNet = azureConfig.vnetOption !== 'none';
|
|
2116
|
+
const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
|
|
1197
2117
|
// main.bicep
|
|
1198
|
-
const mainBicep = `targetScope = 'resourceGroup'
|
|
1199
|
-
|
|
1200
|
-
@description('Project name')
|
|
1201
|
-
param projectName string
|
|
1202
|
-
|
|
1203
|
-
@description('Location for Functions and Cosmos DB')
|
|
1204
|
-
param location string = resourceGroup().location
|
|
1205
|
-
|
|
1206
|
-
@description('Location for Static Web App (must be explicitly provided)')
|
|
1207
|
-
param swaLocation string
|
|
1208
|
-
|
|
1209
|
-
@description('Cosmos DB mode')
|
|
1210
|
-
@allowed(['freetier', 'serverless'])
|
|
1211
|
-
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
1212
|
-
|
|
1213
|
-
@description('Enable VNet integration')
|
|
1214
|
-
param enableVNet bool = ${enableVNet}
|
|
1215
|
-
|
|
1216
|
-
// Shared Log Analytics Workspace (in Functions region for data residency)
|
|
1217
|
-
module logAnalytics 'modules/loganalytics.bicep' = {
|
|
1218
|
-
name: 'logAnalytics'
|
|
1219
|
-
params: {
|
|
1220
|
-
name: 'log-\${projectName}'
|
|
1221
|
-
location: location
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// Application Insights for Static Web App (must be in same region as SWA)
|
|
1226
|
-
module appInsightsSwa 'modules/appinsights.bicep' = {
|
|
1227
|
-
name: 'appInsightsSwa'
|
|
1228
|
-
params: {
|
|
1229
|
-
name: 'appi-\${projectName}-swa'
|
|
1230
|
-
location: swaLocation
|
|
1231
|
-
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// Application Insights for Functions (in same region as Functions)
|
|
1236
|
-
module appInsightsFunctions 'modules/appinsights.bicep' = {
|
|
1237
|
-
name: 'appInsightsFunctions'
|
|
1238
|
-
params: {
|
|
1239
|
-
name: 'appi-\${projectName}-func'
|
|
1240
|
-
location: location
|
|
1241
|
-
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Static Web App
|
|
1246
|
-
module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
1247
|
-
name: 'staticWebApp'
|
|
1248
|
-
params: {
|
|
1249
|
-
name: 'swa-\${projectName}'
|
|
1250
|
-
location: swaLocation
|
|
1251
|
-
sku: 'Standard'
|
|
1252
|
-
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// VNet (conditional)
|
|
1257
|
-
module vnet 'modules/vnet.bicep' = if (enableVNet) {
|
|
1258
|
-
name: 'vnet'
|
|
1259
|
-
params: {
|
|
1260
|
-
name: 'vnet-\${projectName}'
|
|
1261
|
-
location: location
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
|
|
1266
|
-
module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
|
|
1267
|
-
name: 'cosmosDb'
|
|
1268
|
-
params: {
|
|
1269
|
-
accountName: 'cosmos-\${projectName}'
|
|
1270
|
-
databaseName: '\${projectName}Database'
|
|
1271
|
-
location: location
|
|
1272
|
-
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
|
|
1277
|
-
name: 'cosmosDb'
|
|
1278
|
-
params: {
|
|
1279
|
-
accountName: 'cosmos-\${projectName}'
|
|
1280
|
-
databaseName: '\${projectName}Database'
|
|
1281
|
-
location: location
|
|
1282
|
-
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// Cosmos DB Private Endpoint (conditional)
|
|
1287
|
-
module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
|
|
1288
|
-
name: 'cosmosPrivateEndpoint'
|
|
1289
|
-
params: {
|
|
1290
|
-
name: 'pe-cosmos-\${projectName}'
|
|
1291
|
-
location: location
|
|
1292
|
-
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
|
|
1293
|
-
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
1294
|
-
subnetId: vnet.outputs.privateEndpointSubnetId
|
|
1295
|
-
vnetId: vnet.outputs.id
|
|
1296
|
-
}
|
|
1297
|
-
dependsOn: [
|
|
1298
|
-
cosmosDbFreeTier
|
|
1299
|
-
cosmosDbServerless
|
|
1300
|
-
vnet
|
|
1301
|
-
]
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
1305
|
-
module functionsFlex 'modules/functions-flex.bicep' = {
|
|
1306
|
-
name: 'functionsApp'
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
output
|
|
1364
|
-
output
|
|
1365
|
-
output
|
|
1366
|
-
output
|
|
1367
|
-
output
|
|
1368
|
-
output
|
|
1369
|
-
output
|
|
1370
|
-
output
|
|
1371
|
-
output
|
|
1372
|
-
output
|
|
1373
|
-
output
|
|
1374
|
-
output
|
|
1375
|
-
output
|
|
2118
|
+
const mainBicep = `targetScope = 'resourceGroup'
|
|
2119
|
+
|
|
2120
|
+
@description('Project name')
|
|
2121
|
+
param projectName string
|
|
2122
|
+
|
|
2123
|
+
@description('Location for Functions and Cosmos DB')
|
|
2124
|
+
param location string = resourceGroup().location
|
|
2125
|
+
|
|
2126
|
+
@description('Location for Static Web App (must be explicitly provided)')
|
|
2127
|
+
param swaLocation string
|
|
2128
|
+
|
|
2129
|
+
@description('Cosmos DB mode')
|
|
2130
|
+
@allowed(['freetier', 'serverless'])
|
|
2131
|
+
param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
2132
|
+
|
|
2133
|
+
@description('Enable VNet integration')
|
|
2134
|
+
param enableVNet bool = ${enableVNet}
|
|
2135
|
+
|
|
2136
|
+
// Shared Log Analytics Workspace (in Functions region for data residency)
|
|
2137
|
+
module logAnalytics 'modules/loganalytics.bicep' = {
|
|
2138
|
+
name: 'logAnalytics'
|
|
2139
|
+
params: {
|
|
2140
|
+
name: 'log-\${projectName}'
|
|
2141
|
+
location: location
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// Application Insights for Static Web App (must be in same region as SWA)
|
|
2146
|
+
module appInsightsSwa 'modules/appinsights.bicep' = {
|
|
2147
|
+
name: 'appInsightsSwa'
|
|
2148
|
+
params: {
|
|
2149
|
+
name: 'appi-\${projectName}-swa'
|
|
2150
|
+
location: swaLocation
|
|
2151
|
+
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// Application Insights for Functions (in same region as Functions)
|
|
2156
|
+
module appInsightsFunctions 'modules/appinsights.bicep' = {
|
|
2157
|
+
name: 'appInsightsFunctions'
|
|
2158
|
+
params: {
|
|
2159
|
+
name: 'appi-\${projectName}-func'
|
|
2160
|
+
location: location
|
|
2161
|
+
logAnalyticsWorkspaceId: logAnalytics.outputs.id
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Static Web App
|
|
2166
|
+
module staticWebApp 'modules/staticwebapp.bicep' = {
|
|
2167
|
+
name: 'staticWebApp'
|
|
2168
|
+
params: {
|
|
2169
|
+
name: 'swa-\${projectName}'
|
|
2170
|
+
location: swaLocation
|
|
2171
|
+
sku: 'Standard'
|
|
2172
|
+
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// VNet (conditional)
|
|
2177
|
+
module vnet 'modules/vnet.bicep' = if (enableVNet) {
|
|
2178
|
+
name: 'vnet'
|
|
2179
|
+
params: {
|
|
2180
|
+
name: 'vnet-\${projectName}'
|
|
2181
|
+
location: location
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
|
|
2186
|
+
module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
|
|
2187
|
+
name: 'cosmosDb'
|
|
2188
|
+
params: {
|
|
2189
|
+
accountName: 'cosmos-\${projectName}'
|
|
2190
|
+
databaseName: '\${projectName}Database'
|
|
2191
|
+
location: location
|
|
2192
|
+
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
|
|
2197
|
+
name: 'cosmosDb'
|
|
2198
|
+
params: {
|
|
2199
|
+
accountName: 'cosmos-\${projectName}'
|
|
2200
|
+
databaseName: '\${projectName}Database'
|
|
2201
|
+
location: location
|
|
2202
|
+
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// Cosmos DB Private Endpoint (conditional)
|
|
2207
|
+
module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
|
|
2208
|
+
name: 'cosmosPrivateEndpoint'
|
|
2209
|
+
params: {
|
|
2210
|
+
name: 'pe-cosmos-\${projectName}'
|
|
2211
|
+
location: location
|
|
2212
|
+
cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
|
|
2213
|
+
cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
2214
|
+
subnetId: vnet.outputs.privateEndpointSubnetId
|
|
2215
|
+
vnetId: vnet.outputs.id
|
|
2216
|
+
}
|
|
2217
|
+
dependsOn: [
|
|
2218
|
+
cosmosDbFreeTier
|
|
2219
|
+
cosmosDbServerless
|
|
2220
|
+
vnet
|
|
2221
|
+
]
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
|
|
2225
|
+
module functionsFlex 'modules/functions-flex.bicep' = {
|
|
2226
|
+
name: 'functionsApp'
|
|
2227
|
+
params: {
|
|
2228
|
+
name: 'func-\${projectName}'
|
|
2229
|
+
location: location
|
|
2230
|
+
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
2231
|
+
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
2232
|
+
swaDefaultHostname: staticWebApp.outputs.defaultHostname
|
|
2233
|
+
cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
|
|
2234
|
+
cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
|
|
2235
|
+
functionsRuntimeName: '${functionsRuntime.name}'
|
|
2236
|
+
functionsRuntimeVersion: '${functionsRuntime.version}'
|
|
2237
|
+
enableVNet: enableVNet
|
|
2238
|
+
vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
|
|
2239
|
+
}
|
|
2240
|
+
dependsOn: [
|
|
2241
|
+
cosmosDbFreeTier
|
|
2242
|
+
cosmosDbServerless
|
|
2243
|
+
cosmosPrivateEndpoint
|
|
2244
|
+
]
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// Cosmos DB role assignment for Functions (after Functions is created)
|
|
2248
|
+
module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
|
|
2249
|
+
name: 'cosmosDbRoleAssignment'
|
|
2250
|
+
params: {
|
|
2251
|
+
cosmosAccountName: cosmosDbFreeTier.outputs.accountName
|
|
2252
|
+
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2253
|
+
}
|
|
2254
|
+
dependsOn: [
|
|
2255
|
+
functionsFlex
|
|
2256
|
+
]
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
|
|
2260
|
+
name: 'cosmosDbRoleAssignment'
|
|
2261
|
+
params: {
|
|
2262
|
+
cosmosAccountName: cosmosDbServerless.outputs.accountName
|
|
2263
|
+
functionsPrincipalId: functionsFlex.outputs.principalId
|
|
2264
|
+
}
|
|
2265
|
+
dependsOn: [
|
|
2266
|
+
functionsFlex
|
|
2267
|
+
]
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// Update SWA config with Functions hostname (after Functions deployment)
|
|
2271
|
+
module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
2272
|
+
name: 'staticWebAppConfig'
|
|
2273
|
+
params: {
|
|
2274
|
+
staticWebAppName: staticWebApp.outputs.name
|
|
2275
|
+
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
2276
|
+
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2277
|
+
}
|
|
2278
|
+
dependsOn: [
|
|
2279
|
+
functionsFlex
|
|
2280
|
+
]
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
output staticWebAppName string = staticWebApp.outputs.name
|
|
2284
|
+
output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
|
|
2285
|
+
output functionsAppName string = functionsFlex.outputs.name
|
|
2286
|
+
output functionsAppUrl string = functionsFlex.outputs.defaultHostname
|
|
2287
|
+
output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
|
|
2288
|
+
output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
|
|
2289
|
+
output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
|
|
2290
|
+
output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
|
|
2291
|
+
output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
|
|
2292
|
+
output appInsightsSwaName string = appInsightsSwa.outputs.name
|
|
2293
|
+
output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
|
|
2294
|
+
output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
|
|
2295
|
+
output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
|
|
2296
|
+
output vnetEnabled bool = enableVNet
|
|
2297
|
+
output vnetName string = enableVNet ? vnet.outputs.name : ''
|
|
1376
2298
|
`;
|
|
1377
2299
|
fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
|
|
1378
2300
|
// main.parameters.json
|
|
1379
|
-
const params = `{
|
|
1380
|
-
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
|
1381
|
-
"contentVersion": "1.0.0.0",
|
|
1382
|
-
"parameters": {
|
|
1383
|
-
"projectName": {
|
|
1384
|
-
"value": "${projectName}"
|
|
1385
|
-
},
|
|
1386
|
-
"cosmosDbMode": {
|
|
1387
|
-
"value": "${azureConfig.cosmosDbMode}"
|
|
1388
|
-
},
|
|
1389
|
-
"enableVNet": {
|
|
1390
|
-
"value": ${enableVNet}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
2301
|
+
const params = `{
|
|
2302
|
+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
|
|
2303
|
+
"contentVersion": "1.0.0.0",
|
|
2304
|
+
"parameters": {
|
|
2305
|
+
"projectName": {
|
|
2306
|
+
"value": "${projectName}"
|
|
2307
|
+
},
|
|
2308
|
+
"cosmosDbMode": {
|
|
2309
|
+
"value": "${azureConfig.cosmosDbMode}"
|
|
2310
|
+
},
|
|
2311
|
+
"enableVNet": {
|
|
2312
|
+
"value": ${enableVNet}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
1394
2316
|
`;
|
|
1395
2317
|
fs.writeFileSync(path.join(infraDir, 'main.parameters.json'), params);
|
|
1396
2318
|
// modules/staticwebapp.bicep
|
|
1397
|
-
const staticWebAppBicep = `@description('Static Web App name')
|
|
1398
|
-
param name string
|
|
1399
|
-
|
|
1400
|
-
@description('Location for the Static Web App')
|
|
1401
|
-
param location string
|
|
1402
|
-
|
|
1403
|
-
@description('SKU name (Free or Standard)')
|
|
1404
|
-
@allowed([
|
|
1405
|
-
'Free'
|
|
1406
|
-
'Standard'
|
|
1407
|
-
])
|
|
1408
|
-
param sku string = 'Standard'
|
|
1409
|
-
|
|
1410
|
-
@description('Application Insights connection string')
|
|
1411
|
-
param appInsightsConnectionString string
|
|
1412
|
-
|
|
1413
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
|
|
1414
|
-
name: name
|
|
1415
|
-
location: location
|
|
1416
|
-
sku: {
|
|
1417
|
-
name: sku
|
|
1418
|
-
tier: sku
|
|
1419
|
-
}
|
|
1420
|
-
properties: {
|
|
1421
|
-
buildProperties: {
|
|
1422
|
-
skipGithubActionWorkflowGeneration: true
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// Link Application Insights to Static Web App (for both client and server-side telemetry)
|
|
1428
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1429
|
-
parent: staticWebApp
|
|
1430
|
-
name: 'appsettings'
|
|
1431
|
-
properties: {
|
|
1432
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1433
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
output id string = staticWebApp.id
|
|
1438
|
-
output name string = staticWebApp.name
|
|
1439
|
-
output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
2319
|
+
const staticWebAppBicep = `@description('Static Web App name')
|
|
2320
|
+
param name string
|
|
2321
|
+
|
|
2322
|
+
@description('Location for the Static Web App')
|
|
2323
|
+
param location string
|
|
2324
|
+
|
|
2325
|
+
@description('SKU name (Free or Standard)')
|
|
2326
|
+
@allowed([
|
|
2327
|
+
'Free'
|
|
2328
|
+
'Standard'
|
|
2329
|
+
])
|
|
2330
|
+
param sku string = 'Standard'
|
|
2331
|
+
|
|
2332
|
+
@description('Application Insights connection string')
|
|
2333
|
+
param appInsightsConnectionString string
|
|
2334
|
+
|
|
2335
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
|
|
2336
|
+
name: name
|
|
2337
|
+
location: location
|
|
2338
|
+
sku: {
|
|
2339
|
+
name: sku
|
|
2340
|
+
tier: sku
|
|
2341
|
+
}
|
|
2342
|
+
properties: {
|
|
2343
|
+
buildProperties: {
|
|
2344
|
+
skipGithubActionWorkflowGeneration: true
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// Link Application Insights to Static Web App (for both client and server-side telemetry)
|
|
2350
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2351
|
+
parent: staticWebApp
|
|
2352
|
+
name: 'appsettings'
|
|
2353
|
+
properties: {
|
|
2354
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2355
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
output id string = staticWebApp.id
|
|
2360
|
+
output name string = staticWebApp.name
|
|
2361
|
+
output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
1440
2362
|
`;
|
|
1441
2363
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
1442
2364
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
1443
|
-
const staticWebAppConfigBicep = `@description('Static Web App name')
|
|
1444
|
-
param staticWebAppName string
|
|
1445
|
-
|
|
1446
|
-
@description('Functions App default hostname for backend API calls')
|
|
1447
|
-
param functionsDefaultHostname string
|
|
1448
|
-
|
|
1449
|
-
@description('Application Insights connection string for SWA')
|
|
1450
|
-
param appInsightsConnectionString string
|
|
1451
|
-
|
|
1452
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
1453
|
-
name: staticWebAppName
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
1457
|
-
parent: staticWebApp
|
|
1458
|
-
name: 'appsettings'
|
|
1459
|
-
properties: {
|
|
1460
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
1461
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
1462
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
output configName string = staticWebAppConfig.name
|
|
2365
|
+
const staticWebAppConfigBicep = `@description('Static Web App name')
|
|
2366
|
+
param staticWebAppName string
|
|
2367
|
+
|
|
2368
|
+
@description('Functions App default hostname for backend API calls')
|
|
2369
|
+
param functionsDefaultHostname string
|
|
2370
|
+
|
|
2371
|
+
@description('Application Insights connection string for SWA')
|
|
2372
|
+
param appInsightsConnectionString string
|
|
2373
|
+
|
|
2374
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2375
|
+
name: staticWebAppName
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2379
|
+
parent: staticWebApp
|
|
2380
|
+
name: 'appsettings'
|
|
2381
|
+
properties: {
|
|
2382
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2383
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2384
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
output configName string = staticWebAppConfig.name
|
|
1467
2389
|
`;
|
|
1468
2390
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
1469
2391
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
1470
|
-
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
1471
|
-
param name string
|
|
1472
|
-
|
|
1473
|
-
@description('Location for Log Analytics workspace')
|
|
1474
|
-
param location string
|
|
1475
|
-
|
|
1476
|
-
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
1477
|
-
name: name
|
|
1478
|
-
location: location
|
|
1479
|
-
properties: {
|
|
1480
|
-
sku: {
|
|
1481
|
-
name: 'PerGB2018'
|
|
1482
|
-
}
|
|
1483
|
-
retentionInDays: 30
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
output id string = logAnalytics.id
|
|
1488
|
-
output name string = logAnalytics.name
|
|
2392
|
+
const logAnalyticsBicep = `@description('Log Analytics workspace name')
|
|
2393
|
+
param name string
|
|
2394
|
+
|
|
2395
|
+
@description('Location for Log Analytics workspace')
|
|
2396
|
+
param location string
|
|
2397
|
+
|
|
2398
|
+
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
2399
|
+
name: name
|
|
2400
|
+
location: location
|
|
2401
|
+
properties: {
|
|
2402
|
+
sku: {
|
|
2403
|
+
name: 'PerGB2018'
|
|
2404
|
+
}
|
|
2405
|
+
retentionInDays: 30
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
output id string = logAnalytics.id
|
|
2410
|
+
output name string = logAnalytics.name
|
|
1489
2411
|
`;
|
|
1490
2412
|
fs.writeFileSync(path.join(modulesDir, 'loganalytics.bicep'), logAnalyticsBicep);
|
|
1491
2413
|
// modules/appinsights.bicep (Application Insights only, connects to shared Log Analytics)
|
|
1492
|
-
const appInsightsBicep = `@description('Application Insights name')
|
|
1493
|
-
param name string
|
|
1494
|
-
|
|
1495
|
-
@description('Location for Application Insights')
|
|
1496
|
-
param location string
|
|
1497
|
-
|
|
1498
|
-
@description('Log Analytics workspace resource ID')
|
|
1499
|
-
param logAnalyticsWorkspaceId string
|
|
1500
|
-
|
|
1501
|
-
// Application Insights
|
|
1502
|
-
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
|
1503
|
-
name: name
|
|
1504
|
-
location: location
|
|
1505
|
-
kind: 'web'
|
|
1506
|
-
properties: {
|
|
1507
|
-
Application_Type: 'web'
|
|
1508
|
-
WorkspaceResourceId: logAnalyticsWorkspaceId
|
|
1509
|
-
RetentionInDays: 30
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
output id string = appInsights.id
|
|
1514
|
-
output name string = appInsights.name
|
|
1515
|
-
output connectionString string = appInsights.properties.ConnectionString
|
|
1516
|
-
output instrumentationKey string = appInsights.properties.InstrumentationKey
|
|
2414
|
+
const appInsightsBicep = `@description('Application Insights name')
|
|
2415
|
+
param name string
|
|
2416
|
+
|
|
2417
|
+
@description('Location for Application Insights')
|
|
2418
|
+
param location string
|
|
2419
|
+
|
|
2420
|
+
@description('Log Analytics workspace resource ID')
|
|
2421
|
+
param logAnalyticsWorkspaceId string
|
|
2422
|
+
|
|
2423
|
+
// Application Insights
|
|
2424
|
+
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
|
2425
|
+
name: name
|
|
2426
|
+
location: location
|
|
2427
|
+
kind: 'web'
|
|
2428
|
+
properties: {
|
|
2429
|
+
Application_Type: 'web'
|
|
2430
|
+
WorkspaceResourceId: logAnalyticsWorkspaceId
|
|
2431
|
+
RetentionInDays: 30
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
output id string = appInsights.id
|
|
2436
|
+
output name string = appInsights.name
|
|
2437
|
+
output connectionString string = appInsights.properties.ConnectionString
|
|
2438
|
+
output instrumentationKey string = appInsights.properties.InstrumentationKey
|
|
1517
2439
|
`;
|
|
1518
2440
|
fs.writeFileSync(path.join(modulesDir, 'appinsights.bicep'), appInsightsBicep);
|
|
1519
2441
|
// modules/functions-flex.bicep (Flex Consumption)
|
|
1520
|
-
const functionsFlexBicep = `@description('Functions App name')
|
|
1521
|
-
param name string
|
|
1522
|
-
|
|
1523
|
-
@description('Location for the Functions App')
|
|
1524
|
-
param location string
|
|
1525
|
-
|
|
1526
|
-
@description('Storage account name for Functions')
|
|
1527
|
-
param storageAccountName string
|
|
1528
|
-
|
|
1529
|
-
@description('Application Insights connection string')
|
|
1530
|
-
param appInsightsConnectionString string
|
|
1531
|
-
|
|
1532
|
-
@description('Static Web App default hostname for CORS')
|
|
1533
|
-
param swaDefaultHostname string
|
|
1534
|
-
|
|
1535
|
-
@description('Cosmos DB endpoint')
|
|
1536
|
-
param cosmosDbEndpoint string
|
|
1537
|
-
|
|
1538
|
-
@description('Cosmos DB database name')
|
|
1539
|
-
param cosmosDbDatabaseName string
|
|
1540
|
-
|
|
1541
|
-
@description('Enable VNet integration')
|
|
1542
|
-
param enableVNet bool = false
|
|
1543
|
-
|
|
1544
|
-
@description('VNet subnet ID for Functions (required if enableVNet is true)')
|
|
1545
|
-
param vnetSubnetId string = ''
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
//
|
|
1568
|
-
resource
|
|
1569
|
-
parent:
|
|
1570
|
-
name: '
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
'
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2442
|
+
const functionsFlexBicep = `@description('Functions App name')
|
|
2443
|
+
param name string
|
|
2444
|
+
|
|
2445
|
+
@description('Location for the Functions App')
|
|
2446
|
+
param location string
|
|
2447
|
+
|
|
2448
|
+
@description('Storage account name for Functions')
|
|
2449
|
+
param storageAccountName string
|
|
2450
|
+
|
|
2451
|
+
@description('Application Insights connection string')
|
|
2452
|
+
param appInsightsConnectionString string
|
|
2453
|
+
|
|
2454
|
+
@description('Static Web App default hostname for CORS')
|
|
2455
|
+
param swaDefaultHostname string
|
|
2456
|
+
|
|
2457
|
+
@description('Cosmos DB endpoint')
|
|
2458
|
+
param cosmosDbEndpoint string
|
|
2459
|
+
|
|
2460
|
+
@description('Cosmos DB database name')
|
|
2461
|
+
param cosmosDbDatabaseName string
|
|
2462
|
+
|
|
2463
|
+
@description('Enable VNet integration')
|
|
2464
|
+
param enableVNet bool = false
|
|
2465
|
+
|
|
2466
|
+
@description('VNet subnet ID for Functions (required if enableVNet is true)')
|
|
2467
|
+
param vnetSubnetId string = ''
|
|
2468
|
+
|
|
2469
|
+
@description('Functions runtime name')
|
|
2470
|
+
param functionsRuntimeName string
|
|
2471
|
+
|
|
2472
|
+
@description('Functions runtime version')
|
|
2473
|
+
param functionsRuntimeVersion string
|
|
2474
|
+
|
|
2475
|
+
// Storage Account for Functions
|
|
2476
|
+
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
|
|
2477
|
+
name: storageAccountName
|
|
2478
|
+
location: location
|
|
2479
|
+
sku: {
|
|
2480
|
+
name: 'Standard_LRS'
|
|
2481
|
+
}
|
|
2482
|
+
kind: 'StorageV2'
|
|
2483
|
+
properties: {
|
|
2484
|
+
supportsHttpsTrafficOnly: true
|
|
2485
|
+
minimumTlsVersion: 'TLS1_2'
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// Blob Service for deployment package container
|
|
2490
|
+
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
|
|
2491
|
+
parent: storageAccount
|
|
2492
|
+
name: 'default'
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// Deployment package container
|
|
2496
|
+
resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
|
|
2497
|
+
parent: blobService
|
|
2498
|
+
name: 'deploymentpackage'
|
|
2499
|
+
properties: {
|
|
2500
|
+
publicAccess: 'None'
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// App Service Plan (Flex Consumption)
|
|
2505
|
+
resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
|
|
2506
|
+
name: '\${name}-plan'
|
|
2507
|
+
location: location
|
|
2508
|
+
sku: {
|
|
2509
|
+
name: 'FC1'
|
|
2510
|
+
tier: 'FlexConsumption'
|
|
2511
|
+
}
|
|
2512
|
+
properties: {
|
|
2513
|
+
reserved: true // Required for Linux
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// Azure Functions App
|
|
2518
|
+
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
|
|
2519
|
+
name: name
|
|
2520
|
+
location: location
|
|
2521
|
+
kind: 'functionapp,linux'
|
|
2522
|
+
identity: {
|
|
2523
|
+
type: 'SystemAssigned'
|
|
2524
|
+
}
|
|
2525
|
+
properties: {
|
|
2526
|
+
serverFarmId: hostingPlan.id
|
|
2527
|
+
reserved: true
|
|
2528
|
+
virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
|
|
2529
|
+
vnetContentShareEnabled: enableVNet
|
|
2530
|
+
functionAppConfig: {
|
|
2531
|
+
deployment: {
|
|
2532
|
+
storage: {
|
|
2533
|
+
type: 'blobContainer'
|
|
2534
|
+
value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
|
|
2535
|
+
authentication: {
|
|
2536
|
+
type: 'SystemAssignedIdentity'
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
scaleAndConcurrency: {
|
|
2541
|
+
maximumInstanceCount: 100
|
|
2542
|
+
instanceMemoryMB: 2048
|
|
2543
|
+
}
|
|
2544
|
+
runtime: {
|
|
2545
|
+
name: functionsRuntimeName
|
|
2546
|
+
version: functionsRuntimeVersion
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
siteConfig: {
|
|
2550
|
+
appSettings: [
|
|
2551
|
+
{
|
|
2552
|
+
name: 'AzureWebJobsStorage__accountName'
|
|
2553
|
+
value: storageAccount.name
|
|
2554
|
+
}
|
|
2555
|
+
{
|
|
2556
|
+
name: 'FUNCTIONS_EXTENSION_VERSION'
|
|
2557
|
+
value: '~4'
|
|
2558
|
+
}
|
|
2559
|
+
{
|
|
2560
|
+
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
|
2561
|
+
value: appInsightsConnectionString
|
|
2562
|
+
}
|
|
2563
|
+
{
|
|
2564
|
+
name: 'CosmosDBConnection__accountEndpoint'
|
|
2565
|
+
value: cosmosDbEndpoint
|
|
2566
|
+
}
|
|
2567
|
+
{
|
|
2568
|
+
name: 'COSMOS_DB_DATABASE_NAME'
|
|
2569
|
+
value: cosmosDbDatabaseName
|
|
2570
|
+
}
|
|
2571
|
+
]
|
|
2572
|
+
cors: {
|
|
2573
|
+
allowedOrigins: [
|
|
2574
|
+
'https://\${swaDefaultHostname}'
|
|
2575
|
+
]
|
|
2576
|
+
}
|
|
2577
|
+
ipSecurityRestrictions: [
|
|
2578
|
+
{
|
|
2579
|
+
action: 'Allow'
|
|
2580
|
+
ipAddress: 'AzureCloud'
|
|
2581
|
+
tag: 'ServiceTag'
|
|
2582
|
+
priority: 100
|
|
2583
|
+
}
|
|
2584
|
+
]
|
|
2585
|
+
}
|
|
2586
|
+
httpsOnly: true
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// Role Assignment: Storage Blob Data Contributor
|
|
2591
|
+
resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
2592
|
+
name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
2593
|
+
scope: storageAccount
|
|
2594
|
+
properties: {
|
|
2595
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
|
|
2596
|
+
principalId: functionApp.identity.principalId
|
|
2597
|
+
principalType: 'ServicePrincipal'
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
output id string = functionApp.id
|
|
2602
|
+
output name string = functionApp.name
|
|
2603
|
+
output defaultHostname string = functionApp.properties.defaultHostName
|
|
2604
|
+
output principalId string = functionApp.identity.principalId
|
|
1677
2605
|
`;
|
|
1678
2606
|
fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
|
|
1679
2607
|
// modules/cosmosdb-freetier.bicep (Free Tier)
|
|
1680
|
-
const cosmosDbFreeTierBicep =
|
|
1681
|
-
param accountName string
|
|
1682
|
-
|
|
1683
|
-
@description('Database name')
|
|
1684
|
-
param databaseName string
|
|
1685
|
-
|
|
1686
|
-
@description('Location for Cosmos DB')
|
|
1687
|
-
param location string
|
|
1688
|
-
|
|
1689
|
-
@description('Public network access')
|
|
1690
|
-
@allowed(['Enabled', 'Disabled'])
|
|
1691
|
-
param publicNetworkAccess string = 'Enabled'
|
|
1692
|
-
|
|
1693
|
-
// Cosmos DB Account (Free Tier)
|
|
1694
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
1695
|
-
name: accountName
|
|
1696
|
-
location: location
|
|
1697
|
-
kind: 'GlobalDocumentDB'
|
|
1698
|
-
properties: {
|
|
1699
|
-
databaseAccountOfferType: 'Standard'
|
|
1700
|
-
enableAutomaticFailover: false
|
|
1701
|
-
enableFreeTier: true
|
|
1702
|
-
publicNetworkAccess: publicNetworkAccess
|
|
1703
|
-
disableLocalAuth: true
|
|
1704
|
-
consistencyPolicy: {
|
|
1705
|
-
defaultConsistencyLevel: 'Session'
|
|
1706
|
-
}
|
|
1707
|
-
locations: [
|
|
1708
|
-
{
|
|
1709
|
-
locationName: location
|
|
1710
|
-
failoverPriority: 0
|
|
1711
|
-
isZoneRedundant: false
|
|
1712
|
-
}
|
|
1713
|
-
]
|
|
1714
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
// Cosmos DB Database
|
|
1719
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
1720
|
-
parent: cosmosAccount
|
|
1721
|
-
name: databaseName
|
|
1722
|
-
properties: {
|
|
1723
|
-
resource: {
|
|
1724
|
-
id: databaseName
|
|
1725
|
-
}
|
|
1726
|
-
options: {
|
|
1727
|
-
throughput: 1000
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
output id string = cosmosAccount.id
|
|
1733
|
-
output accountName string = cosmosAccount.name
|
|
1734
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
1735
|
-
output databaseName string = database.name
|
|
1736
|
-
`;
|
|
2608
|
+
const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
|
|
1737
2609
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
|
|
1738
2610
|
// modules/cosmosdb-serverless.bicep (Serverless)
|
|
1739
|
-
const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
|
|
1740
|
-
param accountName string
|
|
1741
|
-
|
|
1742
|
-
@description('Database name')
|
|
1743
|
-
param databaseName string
|
|
1744
|
-
|
|
1745
|
-
@description('Location for Cosmos DB')
|
|
1746
|
-
param location string
|
|
1747
|
-
|
|
1748
|
-
@description('Public network access')
|
|
1749
|
-
@allowed(['Enabled', 'Disabled'])
|
|
1750
|
-
param publicNetworkAccess string = 'Enabled'
|
|
1751
|
-
|
|
1752
|
-
// Cosmos DB Account (Serverless)
|
|
1753
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
1754
|
-
name: accountName
|
|
1755
|
-
location: location
|
|
1756
|
-
kind: 'GlobalDocumentDB'
|
|
1757
|
-
properties: {
|
|
1758
|
-
databaseAccountOfferType: 'Standard'
|
|
1759
|
-
enableAutomaticFailover: false
|
|
1760
|
-
publicNetworkAccess: publicNetworkAccess
|
|
1761
|
-
disableLocalAuth: true
|
|
1762
|
-
consistencyPolicy: {
|
|
1763
|
-
defaultConsistencyLevel: 'Session'
|
|
1764
|
-
}
|
|
1765
|
-
locations: [
|
|
1766
|
-
{
|
|
1767
|
-
locationName: location
|
|
1768
|
-
failoverPriority: 0
|
|
1769
|
-
isZoneRedundant: false
|
|
1770
|
-
}
|
|
1771
|
-
]
|
|
1772
|
-
capabilities: [
|
|
1773
|
-
{
|
|
1774
|
-
name: 'EnableServerless'
|
|
1775
|
-
}
|
|
1776
|
-
]
|
|
1777
|
-
disableKeyBasedMetadataWriteAccess: true
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
// Cosmos DB Database
|
|
1782
|
-
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
1783
|
-
parent: cosmosAccount
|
|
1784
|
-
name: databaseName
|
|
1785
|
-
properties: {
|
|
1786
|
-
resource: {
|
|
1787
|
-
id: databaseName
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
output id string = cosmosAccount.id
|
|
1793
|
-
output accountName string = cosmosAccount.name
|
|
1794
|
-
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
1795
|
-
output databaseName string = database.name
|
|
2611
|
+
const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
|
|
2612
|
+
param accountName string
|
|
2613
|
+
|
|
2614
|
+
@description('Database name')
|
|
2615
|
+
param databaseName string
|
|
2616
|
+
|
|
2617
|
+
@description('Location for Cosmos DB')
|
|
2618
|
+
param location string
|
|
2619
|
+
|
|
2620
|
+
@description('Public network access')
|
|
2621
|
+
@allowed(['Enabled', 'Disabled'])
|
|
2622
|
+
param publicNetworkAccess string = 'Enabled'
|
|
2623
|
+
|
|
2624
|
+
// Cosmos DB Account (Serverless)
|
|
2625
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
|
|
2626
|
+
name: accountName
|
|
2627
|
+
location: location
|
|
2628
|
+
kind: 'GlobalDocumentDB'
|
|
2629
|
+
properties: {
|
|
2630
|
+
databaseAccountOfferType: 'Standard'
|
|
2631
|
+
enableAutomaticFailover: false
|
|
2632
|
+
publicNetworkAccess: publicNetworkAccess
|
|
2633
|
+
disableLocalAuth: true
|
|
2634
|
+
consistencyPolicy: {
|
|
2635
|
+
defaultConsistencyLevel: 'Session'
|
|
2636
|
+
}
|
|
2637
|
+
locations: [
|
|
2638
|
+
{
|
|
2639
|
+
locationName: location
|
|
2640
|
+
failoverPriority: 0
|
|
2641
|
+
isZoneRedundant: false
|
|
2642
|
+
}
|
|
2643
|
+
]
|
|
2644
|
+
capabilities: [
|
|
2645
|
+
{
|
|
2646
|
+
name: 'EnableServerless'
|
|
2647
|
+
}
|
|
2648
|
+
]
|
|
2649
|
+
disableKeyBasedMetadataWriteAccess: true
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// Cosmos DB Database
|
|
2654
|
+
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
|
|
2655
|
+
parent: cosmosAccount
|
|
2656
|
+
name: databaseName
|
|
2657
|
+
properties: {
|
|
2658
|
+
resource: {
|
|
2659
|
+
id: databaseName
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
output id string = cosmosAccount.id
|
|
2665
|
+
output accountName string = cosmosAccount.name
|
|
2666
|
+
output endpoint string = cosmosAccount.properties.documentEndpoint
|
|
2667
|
+
output databaseName string = database.name
|
|
1796
2668
|
`;
|
|
1797
2669
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-serverless.bicep'), cosmosDbServerlessBicep);
|
|
1798
2670
|
// modules/cosmosdb-role-assignment.bicep (Role Assignment Module)
|
|
1799
|
-
const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
|
|
1800
|
-
param cosmosAccountName string
|
|
1801
|
-
|
|
1802
|
-
@description('Functions App Managed Identity Principal ID')
|
|
1803
|
-
param functionsPrincipalId string
|
|
1804
|
-
|
|
1805
|
-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
1806
|
-
name: cosmosAccountName
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// Built-in Cosmos DB Data Contributor role definition
|
|
1810
|
-
var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
|
|
1811
|
-
|
|
1812
|
-
// Role assignment for Functions to access Cosmos DB
|
|
1813
|
-
resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
|
|
1814
|
-
parent: cosmosAccount
|
|
1815
|
-
name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
|
|
1816
|
-
properties: {
|
|
1817
|
-
roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
|
|
1818
|
-
principalId: functionsPrincipalId
|
|
1819
|
-
scope: cosmosAccount.id
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
output roleAssignmentId string = roleAssignment.id
|
|
2671
|
+
const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
|
|
2672
|
+
param cosmosAccountName string
|
|
2673
|
+
|
|
2674
|
+
@description('Functions App Managed Identity Principal ID')
|
|
2675
|
+
param functionsPrincipalId string
|
|
2676
|
+
|
|
2677
|
+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
2678
|
+
name: cosmosAccountName
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// Built-in Cosmos DB Data Contributor role definition
|
|
2682
|
+
var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
|
|
2683
|
+
|
|
2684
|
+
// Role assignment for Functions to access Cosmos DB
|
|
2685
|
+
resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
|
|
2686
|
+
parent: cosmosAccount
|
|
2687
|
+
name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
|
|
2688
|
+
properties: {
|
|
2689
|
+
roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
|
|
2690
|
+
principalId: functionsPrincipalId
|
|
2691
|
+
scope: cosmosAccount.id
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
output roleAssignmentId string = roleAssignment.id
|
|
1824
2696
|
`;
|
|
1825
2697
|
fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
|
|
1826
2698
|
// VNet modules (only generate if VNet is enabled)
|
|
1827
2699
|
if (enableVNet) {
|
|
1828
2700
|
// modules/vnet.bicep
|
|
1829
|
-
const vnetBicep = `@description('VNet name')
|
|
1830
|
-
param name string
|
|
1831
|
-
|
|
1832
|
-
@description('Location for VNet')
|
|
1833
|
-
param location string
|
|
1834
|
-
|
|
1835
|
-
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
|
|
1836
|
-
name: name
|
|
1837
|
-
location: location
|
|
1838
|
-
properties: {
|
|
1839
|
-
addressSpace: {
|
|
1840
|
-
addressPrefixes: [
|
|
1841
|
-
'10.0.0.0/16'
|
|
1842
|
-
]
|
|
1843
|
-
}
|
|
1844
|
-
subnets: [
|
|
1845
|
-
{
|
|
1846
|
-
name: 'snet-functions'
|
|
1847
|
-
properties: {
|
|
1848
|
-
addressPrefix: '10.0.1.0/24'
|
|
1849
|
-
delegations: [
|
|
1850
|
-
{
|
|
1851
|
-
name: 'delegation'
|
|
1852
|
-
properties: {
|
|
1853
|
-
serviceName: 'Microsoft.App/environments'
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
]
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
{
|
|
1860
|
-
name: 'snet-private-endpoints'
|
|
1861
|
-
properties: {
|
|
1862
|
-
addressPrefix: '10.0.2.0/24'
|
|
1863
|
-
privateEndpointNetworkPolicies: 'Disabled'
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
]
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
output id string = vnet.id
|
|
1871
|
-
output name string = vnet.name
|
|
1872
|
-
output functionsSubnetId string = vnet.properties.subnets[0].id
|
|
1873
|
-
output privateEndpointSubnetId string = vnet.properties.subnets[1].id
|
|
2701
|
+
const vnetBicep = `@description('VNet name')
|
|
2702
|
+
param name string
|
|
2703
|
+
|
|
2704
|
+
@description('Location for VNet')
|
|
2705
|
+
param location string
|
|
2706
|
+
|
|
2707
|
+
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
|
|
2708
|
+
name: name
|
|
2709
|
+
location: location
|
|
2710
|
+
properties: {
|
|
2711
|
+
addressSpace: {
|
|
2712
|
+
addressPrefixes: [
|
|
2713
|
+
'10.0.0.0/16'
|
|
2714
|
+
]
|
|
2715
|
+
}
|
|
2716
|
+
subnets: [
|
|
2717
|
+
{
|
|
2718
|
+
name: 'snet-functions'
|
|
2719
|
+
properties: {
|
|
2720
|
+
addressPrefix: '10.0.1.0/24'
|
|
2721
|
+
delegations: [
|
|
2722
|
+
{
|
|
2723
|
+
name: 'delegation'
|
|
2724
|
+
properties: {
|
|
2725
|
+
serviceName: 'Microsoft.App/environments'
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
]
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
{
|
|
2732
|
+
name: 'snet-private-endpoints'
|
|
2733
|
+
properties: {
|
|
2734
|
+
addressPrefix: '10.0.2.0/24'
|
|
2735
|
+
privateEndpointNetworkPolicies: 'Disabled'
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
]
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
output id string = vnet.id
|
|
2743
|
+
output name string = vnet.name
|
|
2744
|
+
output functionsSubnetId string = vnet.properties.subnets[0].id
|
|
2745
|
+
output privateEndpointSubnetId string = vnet.properties.subnets[1].id
|
|
1874
2746
|
`;
|
|
1875
2747
|
fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
|
|
1876
2748
|
// modules/private-endpoint-cosmos.bicep
|
|
1877
|
-
const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
|
|
1878
|
-
param name string
|
|
1879
|
-
|
|
1880
|
-
@description('Location')
|
|
1881
|
-
param location string
|
|
1882
|
-
|
|
1883
|
-
@description('Cosmos DB account resource ID')
|
|
1884
|
-
param cosmosAccountId string
|
|
1885
|
-
|
|
1886
|
-
@description('Cosmos DB account name')
|
|
1887
|
-
param cosmosAccountName string
|
|
1888
|
-
|
|
1889
|
-
@description('Subnet ID for private endpoint')
|
|
1890
|
-
param subnetId string
|
|
1891
|
-
|
|
1892
|
-
@description('VNet ID for DNS zone link')
|
|
1893
|
-
param vnetId string
|
|
1894
|
-
|
|
1895
|
-
// Private DNS Zone for Cosmos DB
|
|
1896
|
-
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
|
|
1897
|
-
name: 'privatelink.documents.azure.com'
|
|
1898
|
-
location: 'global'
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
// Link DNS Zone to VNet
|
|
1902
|
-
resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
|
|
1903
|
-
parent: privateDnsZone
|
|
1904
|
-
name: '\${cosmosAccountName}-vnet-link'
|
|
1905
|
-
location: 'global'
|
|
1906
|
-
properties: {
|
|
1907
|
-
virtualNetwork: {
|
|
1908
|
-
id: vnetId
|
|
1909
|
-
}
|
|
1910
|
-
registrationEnabled: false
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
// Private Endpoint for Cosmos DB
|
|
1915
|
-
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
|
|
1916
|
-
name: name
|
|
1917
|
-
location: location
|
|
1918
|
-
properties: {
|
|
1919
|
-
subnet: {
|
|
1920
|
-
id: subnetId
|
|
1921
|
-
}
|
|
1922
|
-
privateLinkServiceConnections: [
|
|
1923
|
-
{
|
|
1924
|
-
name: '\${cosmosAccountName}-connection'
|
|
1925
|
-
properties: {
|
|
1926
|
-
privateLinkServiceId: cosmosAccountId
|
|
1927
|
-
groupIds: [
|
|
1928
|
-
'Sql'
|
|
1929
|
-
]
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
]
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
// DNS Zone Group
|
|
1937
|
-
resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
|
|
1938
|
-
parent: privateEndpoint
|
|
1939
|
-
name: 'default'
|
|
1940
|
-
properties: {
|
|
1941
|
-
privateDnsZoneConfigs: [
|
|
1942
|
-
{
|
|
1943
|
-
name: 'cosmos-dns-config'
|
|
1944
|
-
properties: {
|
|
1945
|
-
privateDnsZoneId: privateDnsZone.id
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
]
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
output privateEndpointId string = privateEndpoint.id
|
|
1953
|
-
output privateDnsZoneId string = privateDnsZone.id
|
|
2749
|
+
const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
|
|
2750
|
+
param name string
|
|
2751
|
+
|
|
2752
|
+
@description('Location')
|
|
2753
|
+
param location string
|
|
2754
|
+
|
|
2755
|
+
@description('Cosmos DB account resource ID')
|
|
2756
|
+
param cosmosAccountId string
|
|
2757
|
+
|
|
2758
|
+
@description('Cosmos DB account name')
|
|
2759
|
+
param cosmosAccountName string
|
|
2760
|
+
|
|
2761
|
+
@description('Subnet ID for private endpoint')
|
|
2762
|
+
param subnetId string
|
|
2763
|
+
|
|
2764
|
+
@description('VNet ID for DNS zone link')
|
|
2765
|
+
param vnetId string
|
|
2766
|
+
|
|
2767
|
+
// Private DNS Zone for Cosmos DB
|
|
2768
|
+
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
|
|
2769
|
+
name: 'privatelink.documents.azure.com'
|
|
2770
|
+
location: 'global'
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// Link DNS Zone to VNet
|
|
2774
|
+
resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
|
|
2775
|
+
parent: privateDnsZone
|
|
2776
|
+
name: '\${cosmosAccountName}-vnet-link'
|
|
2777
|
+
location: 'global'
|
|
2778
|
+
properties: {
|
|
2779
|
+
virtualNetwork: {
|
|
2780
|
+
id: vnetId
|
|
2781
|
+
}
|
|
2782
|
+
registrationEnabled: false
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// Private Endpoint for Cosmos DB
|
|
2787
|
+
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
|
|
2788
|
+
name: name
|
|
2789
|
+
location: location
|
|
2790
|
+
properties: {
|
|
2791
|
+
subnet: {
|
|
2792
|
+
id: subnetId
|
|
2793
|
+
}
|
|
2794
|
+
privateLinkServiceConnections: [
|
|
2795
|
+
{
|
|
2796
|
+
name: '\${cosmosAccountName}-connection'
|
|
2797
|
+
properties: {
|
|
2798
|
+
privateLinkServiceId: cosmosAccountId
|
|
2799
|
+
groupIds: [
|
|
2800
|
+
'Sql'
|
|
2801
|
+
]
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
]
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// DNS Zone Group
|
|
2809
|
+
resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
|
|
2810
|
+
parent: privateEndpoint
|
|
2811
|
+
name: 'default'
|
|
2812
|
+
properties: {
|
|
2813
|
+
privateDnsZoneConfigs: [
|
|
2814
|
+
{
|
|
2815
|
+
name: 'cosmos-dns-config'
|
|
2816
|
+
properties: {
|
|
2817
|
+
privateDnsZoneId: privateDnsZone.id
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
]
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
output privateEndpointId string = privateEndpoint.id
|
|
2825
|
+
output privateDnsZoneId string = privateDnsZone.id
|
|
1954
2826
|
`;
|
|
1955
2827
|
fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
|
|
1956
2828
|
console.log('✅ VNet modules created\n');
|
|
1957
2829
|
}
|
|
1958
2830
|
console.log('✅ Infrastructure files created\n');
|
|
1959
2831
|
}
|
|
1960
|
-
|
|
2832
|
+
function getGitHubFunctionsWorkflow(pm, backendLanguage) {
|
|
2833
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2834
|
+
const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
|
|
2835
|
+
const commonSetup = ` - uses: actions/checkout@v4
|
|
2836
|
+
|
|
2837
|
+
- name: Setup Node.js
|
|
2838
|
+
uses: actions/setup-node@v4
|
|
2839
|
+
with:
|
|
2840
|
+
node-version: '22'
|
|
2841
|
+
${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
|
|
2842
|
+
- name: Install dependencies
|
|
2843
|
+
run: |
|
|
2844
|
+
${pmCmd.ci}
|
|
2845
|
+
|
|
2846
|
+
- name: Build shared package
|
|
2847
|
+
run: |
|
|
2848
|
+
${pmCmd.runFilter('shared')} build
|
|
2849
|
+
`;
|
|
2850
|
+
if (backendLanguage === 'typescript') {
|
|
2851
|
+
return `name: Deploy Azure Functions
|
|
2852
|
+
|
|
2853
|
+
on:
|
|
2854
|
+
push:
|
|
2855
|
+
branches:
|
|
2856
|
+
- main
|
|
2857
|
+
paths:
|
|
2858
|
+
- 'functions/**'
|
|
2859
|
+
- 'shared/**'
|
|
2860
|
+
pull_request:
|
|
2861
|
+
branches:
|
|
2862
|
+
- main
|
|
2863
|
+
paths:
|
|
2864
|
+
- 'functions/**'
|
|
2865
|
+
- 'shared/**'
|
|
2866
|
+
workflow_dispatch:
|
|
2867
|
+
|
|
2868
|
+
jobs:
|
|
2869
|
+
build-and-deploy:
|
|
2870
|
+
runs-on: ubuntu-latest
|
|
2871
|
+
name: Build and Deploy Functions
|
|
2872
|
+
|
|
2873
|
+
steps:
|
|
2874
|
+
${commonSetup} - name: Build Functions
|
|
2875
|
+
run: |
|
|
2876
|
+
${pmCmd.runFilter('functions')} build
|
|
2877
|
+
|
|
2878
|
+
- name: Prepare functions for deployment
|
|
2879
|
+
run: |
|
|
2880
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2881
|
+
mkdir -p /tmp/fn-deps
|
|
2882
|
+
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2883
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
2884
|
+
rm -rf ./functions/node_modules
|
|
2885
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2886
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2887
|
+
mkdir -p "$SHARED_DEST"
|
|
2888
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2889
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2890
|
+
|
|
2891
|
+
- name: Deploy to Azure Functions
|
|
2892
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2893
|
+
uses: Azure/functions-action@v1
|
|
2894
|
+
with:
|
|
2895
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2896
|
+
package: './functions'
|
|
2897
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2898
|
+
sku: flexconsumption
|
|
2899
|
+
`;
|
|
2900
|
+
}
|
|
2901
|
+
if (backendLanguage === 'csharp') {
|
|
2902
|
+
return `name: Deploy Azure Functions
|
|
2903
|
+
|
|
2904
|
+
on:
|
|
2905
|
+
push:
|
|
2906
|
+
branches:
|
|
2907
|
+
- main
|
|
2908
|
+
paths:
|
|
2909
|
+
- 'functions/**'
|
|
2910
|
+
- 'shared/**'
|
|
2911
|
+
pull_request:
|
|
2912
|
+
branches:
|
|
2913
|
+
- main
|
|
2914
|
+
paths:
|
|
2915
|
+
- 'functions/**'
|
|
2916
|
+
- 'shared/**'
|
|
2917
|
+
workflow_dispatch:
|
|
2918
|
+
|
|
2919
|
+
jobs:
|
|
2920
|
+
build-and-deploy:
|
|
2921
|
+
runs-on: ubuntu-latest
|
|
2922
|
+
name: Build and Deploy Functions
|
|
2923
|
+
|
|
2924
|
+
steps:
|
|
2925
|
+
${commonSetup} - name: Setup .NET
|
|
2926
|
+
uses: actions/setup-dotnet@v4
|
|
2927
|
+
with:
|
|
2928
|
+
dotnet-version: '10.0.x'
|
|
2929
|
+
|
|
2930
|
+
- name: Publish Functions
|
|
2931
|
+
run: |
|
|
2932
|
+
dotnet publish ./functions -c Release -o ./functions/publish
|
|
2933
|
+
|
|
2934
|
+
- name: Deploy to Azure Functions
|
|
2935
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2936
|
+
uses: Azure/functions-action@v1
|
|
2937
|
+
with:
|
|
2938
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2939
|
+
package: './functions/publish'
|
|
2940
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2941
|
+
sku: flexconsumption
|
|
2942
|
+
`;
|
|
2943
|
+
}
|
|
2944
|
+
return `name: Deploy Azure Functions
|
|
2945
|
+
|
|
2946
|
+
on:
|
|
2947
|
+
push:
|
|
2948
|
+
branches:
|
|
2949
|
+
- main
|
|
2950
|
+
paths:
|
|
2951
|
+
- 'functions/**'
|
|
2952
|
+
- 'shared/**'
|
|
2953
|
+
pull_request:
|
|
2954
|
+
branches:
|
|
2955
|
+
- main
|
|
2956
|
+
paths:
|
|
2957
|
+
- 'functions/**'
|
|
2958
|
+
- 'shared/**'
|
|
2959
|
+
workflow_dispatch:
|
|
2960
|
+
|
|
2961
|
+
jobs:
|
|
2962
|
+
build-and-deploy:
|
|
2963
|
+
runs-on: ubuntu-latest
|
|
2964
|
+
name: Build and Deploy Functions
|
|
2965
|
+
|
|
2966
|
+
steps:
|
|
2967
|
+
${commonSetup} - name: Setup Python
|
|
2968
|
+
uses: actions/setup-python@v5
|
|
2969
|
+
with:
|
|
2970
|
+
python-version: '3.11'
|
|
2971
|
+
|
|
2972
|
+
- name: Install Functions dependencies
|
|
2973
|
+
run: |
|
|
2974
|
+
python -m pip install --upgrade pip
|
|
2975
|
+
python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
|
|
2976
|
+
|
|
2977
|
+
- name: Deploy to Azure Functions
|
|
2978
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2979
|
+
uses: Azure/functions-action@v1
|
|
2980
|
+
with:
|
|
2981
|
+
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2982
|
+
package: './functions'
|
|
2983
|
+
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2984
|
+
sku: flexconsumption
|
|
2985
|
+
`;
|
|
2986
|
+
}
|
|
2987
|
+
function getAzureFunctionsPipeline(pm, backendLanguage) {
|
|
2988
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
2989
|
+
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
2990
|
+
const commonSetup = ` - task: NodeTool@0
|
|
2991
|
+
inputs:
|
|
2992
|
+
versionSpec: '22.x'
|
|
2993
|
+
displayName: 'Install Node.js'
|
|
2994
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
2995
|
+
- script: |
|
|
2996
|
+
${pmCmd.ci}
|
|
2997
|
+
displayName: 'Install workspace dependencies'
|
|
2998
|
+
|
|
2999
|
+
- script: |
|
|
3000
|
+
${pmCmd.runFilter('shared')} build
|
|
3001
|
+
displayName: 'Build shared package'
|
|
3002
|
+
`;
|
|
3003
|
+
if (backendLanguage === 'typescript') {
|
|
3004
|
+
return `trigger:
|
|
3005
|
+
branches:
|
|
3006
|
+
include:
|
|
3007
|
+
- main
|
|
3008
|
+
paths:
|
|
3009
|
+
include:
|
|
3010
|
+
- functions/**
|
|
3011
|
+
- shared/**
|
|
3012
|
+
|
|
3013
|
+
pr:
|
|
3014
|
+
branches:
|
|
3015
|
+
include:
|
|
3016
|
+
- main
|
|
3017
|
+
paths:
|
|
3018
|
+
include:
|
|
3019
|
+
- functions/**
|
|
3020
|
+
- shared/**
|
|
3021
|
+
|
|
3022
|
+
pool:
|
|
3023
|
+
vmImage: 'ubuntu-latest'
|
|
3024
|
+
|
|
3025
|
+
variables:
|
|
3026
|
+
- group: azure-deployment
|
|
3027
|
+
|
|
3028
|
+
steps:
|
|
3029
|
+
${commonSetup} - script: |
|
|
3030
|
+
${pmCmd.runFilter('functions')} build
|
|
3031
|
+
displayName: 'Build Functions'
|
|
3032
|
+
|
|
3033
|
+
- script: |
|
|
3034
|
+
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
3035
|
+
mkdir -p /tmp/fn-deps
|
|
3036
|
+
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
3037
|
+
cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
|
|
3038
|
+
rm -rf ./functions/node_modules
|
|
3039
|
+
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
3040
|
+
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
3041
|
+
mkdir -p "$SHARED_DEST"
|
|
3042
|
+
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
3043
|
+
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
3044
|
+
displayName: 'Prepare functions for deployment'
|
|
3045
|
+
|
|
3046
|
+
- task: ArchiveFiles@2
|
|
3047
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3048
|
+
inputs:
|
|
3049
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
|
|
3050
|
+
includeRootFolder: false
|
|
3051
|
+
archiveType: 'zip'
|
|
3052
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
3053
|
+
displayName: 'Archive Functions'
|
|
3054
|
+
|
|
3055
|
+
- task: PublishBuildArtifacts@1
|
|
3056
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3057
|
+
inputs:
|
|
3058
|
+
PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
3059
|
+
ArtifactName: 'functions'
|
|
3060
|
+
displayName: 'Publish Functions artifact'
|
|
3061
|
+
|
|
3062
|
+
- task: AzureFunctionApp@2
|
|
3063
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3064
|
+
inputs:
|
|
3065
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
3066
|
+
appType: 'functionAppLinux'
|
|
3067
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
3068
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
3069
|
+
displayName: 'Deploy to Azure Functions'
|
|
3070
|
+
`;
|
|
3071
|
+
}
|
|
3072
|
+
if (backendLanguage === 'csharp') {
|
|
3073
|
+
return `trigger:
|
|
3074
|
+
branches:
|
|
3075
|
+
include:
|
|
3076
|
+
- main
|
|
3077
|
+
paths:
|
|
3078
|
+
include:
|
|
3079
|
+
- functions/**
|
|
3080
|
+
- shared/**
|
|
3081
|
+
|
|
3082
|
+
pr:
|
|
3083
|
+
branches:
|
|
3084
|
+
include:
|
|
3085
|
+
- main
|
|
3086
|
+
paths:
|
|
3087
|
+
include:
|
|
3088
|
+
- functions/**
|
|
3089
|
+
- shared/**
|
|
3090
|
+
|
|
3091
|
+
pool:
|
|
3092
|
+
vmImage: 'ubuntu-latest'
|
|
3093
|
+
|
|
3094
|
+
variables:
|
|
3095
|
+
- group: azure-deployment
|
|
3096
|
+
|
|
3097
|
+
steps:
|
|
3098
|
+
${commonSetup} - task: UseDotNet@2
|
|
3099
|
+
inputs:
|
|
3100
|
+
version: '10.0.x'
|
|
3101
|
+
displayName: 'Install .NET SDK'
|
|
3102
|
+
|
|
3103
|
+
- script: |
|
|
3104
|
+
dotnet publish ./functions -c Release -o ./functions/publish
|
|
3105
|
+
displayName: 'Publish Functions'
|
|
3106
|
+
|
|
3107
|
+
- task: ArchiveFiles@2
|
|
3108
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3109
|
+
inputs:
|
|
3110
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions/publish'
|
|
3111
|
+
includeRootFolder: false
|
|
3112
|
+
archiveType: 'zip'
|
|
3113
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
3114
|
+
displayName: 'Archive Functions'
|
|
3115
|
+
|
|
3116
|
+
- task: AzureFunctionApp@2
|
|
3117
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3118
|
+
inputs:
|
|
3119
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
3120
|
+
appType: 'functionAppLinux'
|
|
3121
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
3122
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
3123
|
+
displayName: 'Deploy to Azure Functions'
|
|
3124
|
+
`;
|
|
3125
|
+
}
|
|
3126
|
+
return `trigger:
|
|
3127
|
+
branches:
|
|
3128
|
+
include:
|
|
3129
|
+
- main
|
|
3130
|
+
paths:
|
|
3131
|
+
include:
|
|
3132
|
+
- functions/**
|
|
3133
|
+
- shared/**
|
|
3134
|
+
|
|
3135
|
+
pr:
|
|
3136
|
+
branches:
|
|
3137
|
+
include:
|
|
3138
|
+
- main
|
|
3139
|
+
paths:
|
|
3140
|
+
include:
|
|
3141
|
+
- functions/**
|
|
3142
|
+
- shared/**
|
|
3143
|
+
|
|
3144
|
+
pool:
|
|
3145
|
+
vmImage: 'ubuntu-latest'
|
|
3146
|
+
|
|
3147
|
+
variables:
|
|
3148
|
+
- group: azure-deployment
|
|
3149
|
+
|
|
3150
|
+
steps:
|
|
3151
|
+
${commonSetup} - task: UsePythonVersion@0
|
|
3152
|
+
inputs:
|
|
3153
|
+
versionSpec: '3.11'
|
|
3154
|
+
displayName: 'Install Python'
|
|
3155
|
+
|
|
3156
|
+
- script: |
|
|
3157
|
+
python -m pip install --upgrade pip
|
|
3158
|
+
python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
|
|
3159
|
+
displayName: 'Install Functions dependencies'
|
|
3160
|
+
|
|
3161
|
+
- task: ArchiveFiles@2
|
|
3162
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3163
|
+
inputs:
|
|
3164
|
+
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
|
|
3165
|
+
includeRootFolder: false
|
|
3166
|
+
archiveType: 'zip'
|
|
3167
|
+
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
3168
|
+
displayName: 'Archive Functions'
|
|
3169
|
+
|
|
3170
|
+
- task: AzureFunctionApp@2
|
|
3171
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3172
|
+
inputs:
|
|
3173
|
+
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
3174
|
+
appType: 'functionAppLinux'
|
|
3175
|
+
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
3176
|
+
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
3177
|
+
displayName: 'Deploy to Azure Functions'
|
|
3178
|
+
`;
|
|
3179
|
+
}
|
|
3180
|
+
function buildAzureSwaPipeline(pm) {
|
|
3181
|
+
const pmCmd = (0, package_manager_1.getCommands)(pm);
|
|
3182
|
+
const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
|
|
3183
|
+
return `trigger:
|
|
3184
|
+
branches:
|
|
3185
|
+
include:
|
|
3186
|
+
- main
|
|
3187
|
+
paths:
|
|
3188
|
+
include:
|
|
3189
|
+
- app/**
|
|
3190
|
+
- components/**
|
|
3191
|
+
- lib/**
|
|
3192
|
+
- shared/**
|
|
3193
|
+
- public/**
|
|
3194
|
+
- package.json
|
|
3195
|
+
- next.config.js
|
|
3196
|
+
|
|
3197
|
+
pr:
|
|
3198
|
+
branches:
|
|
3199
|
+
include:
|
|
3200
|
+
- main
|
|
3201
|
+
paths:
|
|
3202
|
+
include:
|
|
3203
|
+
- app/**
|
|
3204
|
+
- components/**
|
|
3205
|
+
- lib/**
|
|
3206
|
+
- shared/**
|
|
3207
|
+
- public/**
|
|
3208
|
+
- package.json
|
|
3209
|
+
- next.config.js
|
|
3210
|
+
|
|
3211
|
+
pool:
|
|
3212
|
+
vmImage: 'ubuntu-latest'
|
|
3213
|
+
|
|
3214
|
+
variables:
|
|
3215
|
+
- group: azure-deployment
|
|
3216
|
+
|
|
3217
|
+
steps:
|
|
3218
|
+
- task: NodeTool@0
|
|
3219
|
+
inputs:
|
|
3220
|
+
versionSpec: '22.x'
|
|
3221
|
+
displayName: 'Install Node.js'
|
|
3222
|
+
${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
|
|
3223
|
+
- script: |
|
|
3224
|
+
${pmCmd.ci}
|
|
3225
|
+
displayName: 'Install dependencies'
|
|
3226
|
+
|
|
3227
|
+
- script: |
|
|
3228
|
+
${pmCmd.run} build
|
|
3229
|
+
env:
|
|
3230
|
+
NODE_ENV: production
|
|
3231
|
+
displayName: 'Build Next.js app'
|
|
3232
|
+
|
|
3233
|
+
- task: AzureStaticWebApp@0
|
|
3234
|
+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
3235
|
+
inputs:
|
|
3236
|
+
app_location: '.'
|
|
3237
|
+
output_location: '.next/standalone'
|
|
3238
|
+
skip_app_build: true
|
|
3239
|
+
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
3240
|
+
displayName: 'Deploy to Azure Static Web Apps'
|
|
3241
|
+
`;
|
|
3242
|
+
}
|
|
3243
|
+
async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
|
|
1961
3244
|
console.log('📦 Creating GitHub Actions workflows...\n');
|
|
1962
3245
|
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
1963
3246
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
1964
|
-
|
|
1965
|
-
const swaWorkflow =
|
|
1966
|
-
|
|
1967
|
-
on:
|
|
1968
|
-
push:
|
|
1969
|
-
branches:
|
|
1970
|
-
- main
|
|
1971
|
-
paths:
|
|
1972
|
-
- 'app/**'
|
|
1973
|
-
- 'components/**'
|
|
1974
|
-
- 'lib/**'
|
|
1975
|
-
- 'shared/**'
|
|
1976
|
-
- 'public/**'
|
|
1977
|
-
- 'package.json'
|
|
1978
|
-
- 'next.config.js'
|
|
1979
|
-
- 'next.config.ts'
|
|
1980
|
-
workflow_dispatch:
|
|
1981
|
-
pull_request:
|
|
1982
|
-
branches:
|
|
1983
|
-
- main
|
|
1984
|
-
paths:
|
|
1985
|
-
- 'app/**'
|
|
1986
|
-
- 'components/**'
|
|
1987
|
-
- 'lib/**'
|
|
1988
|
-
- 'shared/**'
|
|
1989
|
-
- 'public/**'
|
|
1990
|
-
- 'package.json'
|
|
1991
|
-
- 'next.config.js'
|
|
1992
|
-
- 'next.config.ts'
|
|
1993
|
-
|
|
1994
|
-
jobs:
|
|
1995
|
-
build-and-deploy:
|
|
1996
|
-
runs-on: ubuntu-latest
|
|
1997
|
-
name: Build and Deploy Static Web App
|
|
1998
|
-
|
|
1999
|
-
steps:
|
|
2000
|
-
- uses: actions/checkout@v4
|
|
2001
|
-
with:
|
|
2002
|
-
submodules: true
|
|
2003
|
-
|
|
2004
|
-
- name: Deploy to Azure Static Web Apps
|
|
2005
|
-
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2006
|
-
uses: Azure/static-web-apps-deploy@v1
|
|
2007
|
-
with:
|
|
2008
|
-
azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
|
2009
|
-
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
2010
|
-
action: 'upload'
|
|
2011
|
-
app_location: '/'
|
|
2012
|
-
api_location: ''
|
|
2013
|
-
output_location: ''
|
|
2014
|
-
env:
|
|
2015
|
-
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
2016
|
-
`;
|
|
3247
|
+
const actionsPm = 'npm';
|
|
3248
|
+
const swaWorkflow = buildGitHubSwaWorkflow(actionsPm);
|
|
2017
3249
|
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
2018
3250
|
// deploy-functions.yml
|
|
2019
|
-
const functionsWorkflow =
|
|
2020
|
-
|
|
2021
|
-
on:
|
|
2022
|
-
push:
|
|
2023
|
-
branches:
|
|
2024
|
-
- main
|
|
2025
|
-
paths:
|
|
2026
|
-
- 'functions/**'
|
|
2027
|
-
- 'shared/**'
|
|
2028
|
-
pull_request:
|
|
2029
|
-
branches:
|
|
2030
|
-
- main
|
|
2031
|
-
paths:
|
|
2032
|
-
- 'functions/**'
|
|
2033
|
-
- 'shared/**'
|
|
2034
|
-
workflow_dispatch:
|
|
2035
|
-
|
|
2036
|
-
jobs:
|
|
2037
|
-
build-and-deploy:
|
|
2038
|
-
runs-on: ubuntu-latest
|
|
2039
|
-
name: Build and Deploy Functions
|
|
2040
|
-
|
|
2041
|
-
steps:
|
|
2042
|
-
- uses: actions/checkout@v4
|
|
2043
|
-
|
|
2044
|
-
- name: Setup Node.js
|
|
2045
|
-
uses: actions/setup-node@v4
|
|
2046
|
-
with:
|
|
2047
|
-
node-version: '22'
|
|
2048
|
-
|
|
2049
|
-
- name: Install dependencies
|
|
2050
|
-
run: |
|
|
2051
|
-
npm ci
|
|
2052
|
-
|
|
2053
|
-
- name: Build shared package
|
|
2054
|
-
run: |
|
|
2055
|
-
npm run build -w shared
|
|
2056
|
-
|
|
2057
|
-
- name: Build Functions
|
|
2058
|
-
run: |
|
|
2059
|
-
npm run build -w functions
|
|
2060
|
-
|
|
2061
|
-
- name: Prepare functions for deployment
|
|
2062
|
-
run: |
|
|
2063
|
-
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2064
|
-
mkdir -p /tmp/fn-deps
|
|
2065
|
-
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2066
|
-
cd /tmp/fn-deps && npm install --omit=dev && cd -
|
|
2067
|
-
rm -rf ./functions/node_modules
|
|
2068
|
-
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2069
|
-
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2070
|
-
mkdir -p "$SHARED_DEST"
|
|
2071
|
-
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2072
|
-
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2073
|
-
|
|
2074
|
-
- name: Deploy to Azure Functions
|
|
2075
|
-
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
2076
|
-
uses: Azure/functions-action@v1
|
|
2077
|
-
with:
|
|
2078
|
-
app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
|
|
2079
|
-
package: './functions'
|
|
2080
|
-
publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
|
|
2081
|
-
sku: flexconsumption
|
|
2082
|
-
`;
|
|
3251
|
+
const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage);
|
|
2083
3252
|
fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
|
|
2084
3253
|
console.log('✅ GitHub Actions workflows created\n');
|
|
2085
3254
|
}
|
|
2086
|
-
|
|
3255
|
+
function buildGitHubSwaWorkflow(pm) {
|
|
3256
|
+
return `name: Deploy Static Web App
|
|
3257
|
+
|
|
3258
|
+
on:
|
|
3259
|
+
push:
|
|
3260
|
+
branches:
|
|
3261
|
+
- main
|
|
3262
|
+
paths:
|
|
3263
|
+
- 'app/**'
|
|
3264
|
+
- 'components/**'
|
|
3265
|
+
- 'lib/**'
|
|
3266
|
+
- 'shared/**'
|
|
3267
|
+
- 'public/**'
|
|
3268
|
+
- 'package.json'
|
|
3269
|
+
- 'next.config.js'
|
|
3270
|
+
- 'next.config.ts'
|
|
3271
|
+
workflow_dispatch:
|
|
3272
|
+
pull_request:
|
|
3273
|
+
branches:
|
|
3274
|
+
- main
|
|
3275
|
+
paths:
|
|
3276
|
+
- 'app/**'
|
|
3277
|
+
- 'components/**'
|
|
3278
|
+
- 'lib/**'
|
|
3279
|
+
- 'shared/**'
|
|
3280
|
+
- 'public/**'
|
|
3281
|
+
- 'package.json'
|
|
3282
|
+
- 'next.config.js'
|
|
3283
|
+
- 'next.config.ts'
|
|
3284
|
+
|
|
3285
|
+
jobs:
|
|
3286
|
+
build-and-deploy:
|
|
3287
|
+
runs-on: ubuntu-latest
|
|
3288
|
+
name: Build and Deploy Static Web App
|
|
3289
|
+
|
|
3290
|
+
steps:
|
|
3291
|
+
- uses: actions/checkout@v4
|
|
3292
|
+
with:
|
|
3293
|
+
submodules: true
|
|
3294
|
+
|
|
3295
|
+
- name: Setup Node.js
|
|
3296
|
+
uses: actions/setup-node@v4
|
|
3297
|
+
with:
|
|
3298
|
+
node-version: '22'
|
|
3299
|
+
${(0, package_manager_1.getCiSetupStep)(pm) ? `\n${(0, package_manager_1.getCiSetupStep)(pm)}\n` : ''}
|
|
3300
|
+
- name: Install and build app
|
|
3301
|
+
run: |
|
|
3302
|
+
${(0, package_manager_1.getSwaAppBuildCommand)(pm)}
|
|
3303
|
+
|
|
3304
|
+
- name: Deploy to Azure Static Web Apps
|
|
3305
|
+
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
|
3306
|
+
uses: Azure/static-web-apps-deploy@v1
|
|
3307
|
+
with:
|
|
3308
|
+
azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
|
3309
|
+
repo_token: \${{ secrets.GITHUB_TOKEN }}
|
|
3310
|
+
action: 'upload'
|
|
3311
|
+
app_location: '/'
|
|
3312
|
+
api_location: ''
|
|
3313
|
+
output_location: ''
|
|
3314
|
+
app_build_command: '${(0, package_manager_1.getSwaAppBuildCommand)(pm)}'
|
|
3315
|
+
env:
|
|
3316
|
+
NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
|
|
3317
|
+
`;
|
|
3318
|
+
}
|
|
3319
|
+
async function createAzurePipelines(projectDir, pm, backendLanguage) {
|
|
2087
3320
|
console.log('📦 Creating Azure Pipelines...\n');
|
|
3321
|
+
const pipelinesPm = 'npm';
|
|
2088
3322
|
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
2089
3323
|
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
2090
3324
|
// swa.yml
|
|
2091
|
-
const swaPipeline =
|
|
2092
|
-
branches:
|
|
2093
|
-
include:
|
|
2094
|
-
- main
|
|
2095
|
-
paths:
|
|
2096
|
-
include:
|
|
2097
|
-
- app/**
|
|
2098
|
-
- components/**
|
|
2099
|
-
- lib/**
|
|
2100
|
-
- shared/**
|
|
2101
|
-
- public/**
|
|
2102
|
-
- package.json
|
|
2103
|
-
- next.config.js
|
|
2104
|
-
|
|
2105
|
-
pr:
|
|
2106
|
-
branches:
|
|
2107
|
-
include:
|
|
2108
|
-
- main
|
|
2109
|
-
paths:
|
|
2110
|
-
include:
|
|
2111
|
-
- app/**
|
|
2112
|
-
- components/**
|
|
2113
|
-
- lib/**
|
|
2114
|
-
- shared/**
|
|
2115
|
-
- public/**
|
|
2116
|
-
- package.json
|
|
2117
|
-
- next.config.js
|
|
2118
|
-
|
|
2119
|
-
pool:
|
|
2120
|
-
vmImage: 'ubuntu-latest'
|
|
2121
|
-
|
|
2122
|
-
variables:
|
|
2123
|
-
- group: azure-deployment
|
|
2124
|
-
|
|
2125
|
-
steps:
|
|
2126
|
-
- task: NodeTool@0
|
|
2127
|
-
inputs:
|
|
2128
|
-
versionSpec: '22.x'
|
|
2129
|
-
displayName: 'Install Node.js'
|
|
2130
|
-
|
|
2131
|
-
- script: |
|
|
2132
|
-
npm ci
|
|
2133
|
-
displayName: 'Install dependencies'
|
|
2134
|
-
|
|
2135
|
-
- script: |
|
|
2136
|
-
npm run build
|
|
2137
|
-
env:
|
|
2138
|
-
NODE_ENV: production
|
|
2139
|
-
displayName: 'Build Next.js app'
|
|
2140
|
-
|
|
2141
|
-
- task: AzureStaticWebApp@0
|
|
2142
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2143
|
-
inputs:
|
|
2144
|
-
app_location: '.'
|
|
2145
|
-
output_location: '.next/standalone'
|
|
2146
|
-
skip_app_build: true
|
|
2147
|
-
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
|
|
2148
|
-
displayName: 'Deploy to Azure Static Web Apps'
|
|
2149
|
-
`;
|
|
3325
|
+
const swaPipeline = buildAzureSwaPipeline(pipelinesPm);
|
|
2150
3326
|
fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
|
|
2151
3327
|
// functions.yml
|
|
2152
|
-
const functionsPipeline =
|
|
2153
|
-
branches:
|
|
2154
|
-
include:
|
|
2155
|
-
- main
|
|
2156
|
-
paths:
|
|
2157
|
-
include:
|
|
2158
|
-
- functions/**
|
|
2159
|
-
- shared/**
|
|
2160
|
-
|
|
2161
|
-
pr:
|
|
2162
|
-
branches:
|
|
2163
|
-
include:
|
|
2164
|
-
- main
|
|
2165
|
-
paths:
|
|
2166
|
-
include:
|
|
2167
|
-
- functions/**
|
|
2168
|
-
- shared/**
|
|
2169
|
-
|
|
2170
|
-
pool:
|
|
2171
|
-
vmImage: 'ubuntu-latest'
|
|
2172
|
-
|
|
2173
|
-
variables:
|
|
2174
|
-
- group: azure-deployment
|
|
2175
|
-
|
|
2176
|
-
steps:
|
|
2177
|
-
- task: NodeTool@0
|
|
2178
|
-
inputs:
|
|
2179
|
-
versionSpec: '22.x'
|
|
2180
|
-
displayName: 'Install Node.js'
|
|
2181
|
-
|
|
2182
|
-
- script: |
|
|
2183
|
-
npm ci
|
|
2184
|
-
displayName: 'Install workspace dependencies'
|
|
2185
|
-
|
|
2186
|
-
- script: |
|
|
2187
|
-
npm run build -w shared
|
|
2188
|
-
displayName: 'Build shared package'
|
|
2189
|
-
|
|
2190
|
-
- script: |
|
|
2191
|
-
npm run build -w functions
|
|
2192
|
-
displayName: 'Build Functions'
|
|
2193
|
-
|
|
2194
|
-
- script: |
|
|
2195
|
-
SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
|
|
2196
|
-
mkdir -p /tmp/fn-deps
|
|
2197
|
-
node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
|
|
2198
|
-
cd /tmp/fn-deps && npm install --omit=dev && cd -
|
|
2199
|
-
rm -rf ./functions/node_modules
|
|
2200
|
-
mv /tmp/fn-deps/node_modules ./functions/node_modules
|
|
2201
|
-
SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
|
|
2202
|
-
mkdir -p "$SHARED_DEST"
|
|
2203
|
-
cp -r ./shared/dist "$SHARED_DEST/dist"
|
|
2204
|
-
cp ./shared/package.json "$SHARED_DEST/package.json"
|
|
2205
|
-
displayName: 'Prepare functions for deployment'
|
|
2206
|
-
|
|
2207
|
-
- task: ArchiveFiles@2
|
|
2208
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2209
|
-
inputs:
|
|
2210
|
-
rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
|
|
2211
|
-
includeRootFolder: false
|
|
2212
|
-
archiveType: 'zip'
|
|
2213
|
-
archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2214
|
-
displayName: 'Archive Functions'
|
|
2215
|
-
|
|
2216
|
-
- task: PublishBuildArtifacts@1
|
|
2217
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2218
|
-
inputs:
|
|
2219
|
-
PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2220
|
-
ArtifactName: 'functions'
|
|
2221
|
-
displayName: 'Publish Functions artifact'
|
|
2222
|
-
|
|
2223
|
-
- task: AzureFunctionApp@2
|
|
2224
|
-
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
|
2225
|
-
inputs:
|
|
2226
|
-
azureSubscription: '$(AZURE_SUBSCRIPTION)'
|
|
2227
|
-
appType: 'functionAppLinux'
|
|
2228
|
-
appName: '$(AZURE_FUNCTIONAPP_NAME)'
|
|
2229
|
-
package: '$(Build.ArtifactStagingDirectory)/functions.zip'
|
|
2230
|
-
displayName: 'Deploy to Azure Functions'
|
|
2231
|
-
`;
|
|
3328
|
+
const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage);
|
|
2232
3329
|
fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
|
|
2233
3330
|
console.log('✅ Azure Pipelines created\n');
|
|
2234
3331
|
}
|