swallowkit 1.0.0-beta.5 → 1.0.0-beta.7

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.
Files changed (97) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +251 -242
  3. package/README.md +252 -243
  4. package/dist/__tests__/fixtures.d.ts +14 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +85 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/create-model.js +14 -14
  9. package/dist/cli/commands/dev.d.ts +8 -0
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +238 -30
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +5 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2507 -1664
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/scaffold.d.ts +3 -0
  18. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  19. package/dist/cli/commands/scaffold.js +281 -117
  20. package/dist/cli/commands/scaffold.js.map +1 -1
  21. package/dist/cli/index.js +2 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/core/config.d.ts +2 -1
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +28 -0
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/scaffold/functions-generator.d.ts +5 -0
  28. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  29. package/dist/core/scaffold/functions-generator.js +649 -218
  30. package/dist/core/scaffold/functions-generator.js.map +1 -1
  31. package/dist/core/scaffold/model-parser.d.ts +1 -1
  32. package/dist/core/scaffold/model-parser.js +99 -99
  33. package/dist/core/scaffold/nextjs-generator.js +181 -181
  34. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  35. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  36. package/dist/core/scaffold/openapi-generator.js +190 -0
  37. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  38. package/dist/core/scaffold/ui-generator.js +656 -656
  39. package/dist/database/base-model.d.ts +3 -3
  40. package/dist/database/base-model.js +3 -3
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/types/index.d.ts +4 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/utils/package-manager.d.ts +2 -1
  48. package/dist/utils/package-manager.d.ts.map +1 -1
  49. package/dist/utils/package-manager.js +14 -10
  50. package/dist/utils/package-manager.js.map +1 -1
  51. package/package.json +81 -74
  52. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
  53. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  54. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
  55. package/src/__tests__/config.test.ts +122 -0
  56. package/src/__tests__/dev.test.ts +42 -0
  57. package/src/__tests__/fixtures.ts +83 -0
  58. package/src/__tests__/functions-generator.test.ts +101 -0
  59. package/src/__tests__/init.test.ts +59 -0
  60. package/src/__tests__/nextjs-generator.test.ts +97 -0
  61. package/src/__tests__/openapi-generator.test.ts +43 -0
  62. package/src/__tests__/package-manager.test.ts +189 -0
  63. package/src/__tests__/scaffold.test.ts +39 -0
  64. package/src/__tests__/string-utils.test.ts +75 -0
  65. package/src/__tests__/ui-generator.test.ts +144 -0
  66. package/src/cli/commands/create-model.ts +141 -0
  67. package/src/cli/commands/dev.ts +794 -0
  68. package/src/cli/commands/index.ts +8 -0
  69. package/src/cli/commands/init.ts +3363 -0
  70. package/src/cli/commands/provision.ts +193 -0
  71. package/src/cli/commands/scaffold.ts +786 -0
  72. package/src/cli/index.ts +73 -0
  73. package/src/core/config.ts +244 -0
  74. package/src/core/scaffold/functions-generator.ts +674 -0
  75. package/src/core/scaffold/model-parser.ts +627 -0
  76. package/src/core/scaffold/nextjs-generator.ts +217 -0
  77. package/src/core/scaffold/openapi-generator.ts +212 -0
  78. package/src/core/scaffold/ui-generator.ts +945 -0
  79. package/src/database/base-model.ts +184 -0
  80. package/src/database/client.ts +140 -0
  81. package/src/database/repository.ts +104 -0
  82. package/src/database/runtime-check.ts +25 -0
  83. package/src/index.ts +27 -0
  84. package/src/types/index.ts +45 -0
  85. package/src/utils/package-manager.ts +229 -0
  86. package/dist/cli/commands/build.d.ts +0 -6
  87. package/dist/cli/commands/build.d.ts.map +0 -1
  88. package/dist/cli/commands/build.js +0 -177
  89. package/dist/cli/commands/build.js.map +0 -1
  90. package/dist/cli/commands/deploy.d.ts +0 -3
  91. package/dist/cli/commands/deploy.d.ts.map +0 -1
  92. package/dist/cli/commands/deploy.js +0 -147
  93. package/dist/cli/commands/deploy.js.map +0 -1
  94. package/dist/cli/commands/setup.d.ts +0 -6
  95. package/dist/cli/commands/setup.d.ts.map +0 -1
  96. package/dist/cli/commands/setup.js +0 -254
  97. package/dist/cli/commands/setup.js.map +0 -1
@@ -0,0 +1,786 @@
1
+ /**
2
+ * SwallowKit Scaffold ć‚³ćƒžćƒ³ćƒ‰
3
+ * Zod ćƒ¢ćƒ‡ćƒ«ć‹ć‚‰ Azure Functions と Next.js BFF 恮 CRUD ć‚³ćƒ¼ćƒ‰ć‚’ē”Ÿęˆ
4
+ */
5
+
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import { spawn } from "child_process";
9
+ import { getBackendLanguage, ensureSwallowKitProject } from "../../core/config";
10
+ import { ModelInfo, parseModelFile, toKebabCase, toPascalCase } from "../../core/scaffold/model-parser";
11
+ import {
12
+ generateCSharpAzureFunctionsCRUD,
13
+ generateCompactAzureFunctionsCRUD,
14
+ generatePythonAzureFunctionsCRUD,
15
+ } from "../../core/scaffold/functions-generator";
16
+ import { generateCompactBFFRoutes, generateBFFCallFunction } from "../../core/scaffold/nextjs-generator";
17
+ import { generateOpenApiDocument } from "../../core/scaffold/openapi-generator";
18
+ import {
19
+ generateListPage,
20
+ generateDetailPage,
21
+ generateFormComponent,
22
+ generateNewPage,
23
+ generateEditPage,
24
+ } from "../../core/scaffold/ui-generator";
25
+ import { detectFromProject, getCommands } from "../../utils/package-manager";
26
+ import { BackendLanguage } from "../../types";
27
+
28
+ interface ScaffoldOptions {
29
+ model: string; // ćƒ¢ćƒ‡ćƒ«ćƒ•ć‚”ć‚¤ćƒ«ć®ćƒ‘ć‚¹ļ¼ˆä¾‹: "lib/models/todo.ts" or "todo")
30
+ functionsDir?: string; // Azure Functions ć®ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: "functions")
31
+ apiDir?: string; // Next.js API routes ć®ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: "app/api")
32
+ apiOnly?: boolean; // true ć®å “åˆć€UI ć‚’ē”Ÿęˆć—ćŖć„ļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: false)
33
+ }
34
+
35
+ export async function scaffoldCommand(options: ScaffoldOptions) {
36
+ // SwallowKit ćƒ—ćƒ­ć‚øć‚§ć‚Æćƒˆćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖć‹ć©ć†ć‹ć‚’ę¤œčØ¼
37
+ ensureSwallowKitProject("scaffold");
38
+
39
+ console.log("šŸ—ļø SwallowKit Scaffold: Generating CRUD operations...\n");
40
+
41
+ try {
42
+ // 1. Resolve model file path
43
+ const modelPath = resolveModelPath(options.model);
44
+ console.log(`šŸ“„ Model file: ${modelPath}`);
45
+
46
+ // 2. Parse model file
47
+ console.log("šŸ” Parsing model file...");
48
+ const modelInfo = await parseModelFile(modelPath);
49
+ console.log(`āœ… Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
50
+
51
+ const backendLanguage = getBackendLanguage();
52
+ console.log(`🧠 Backend language: ${backendLanguage}`);
53
+
54
+ // ćƒć‚¹ćƒˆć‚¹ć‚­ćƒ¼ćƒžå‚ē…§ćŒć‚ć‚Œć°č”Øē¤ŗ
55
+ if (modelInfo.nestedSchemaRefs.length > 0) {
56
+ console.log(`šŸ”— Nested schema references detected:`);
57
+ for (const ref of modelInfo.nestedSchemaRefs) {
58
+ const relType = ref.isArray ? 'array' : 'single';
59
+ const optional = ref.isOptional ? ' (optional)' : '';
60
+ console.log(` - ${ref.fieldName}: ${ref.modelName} [${relType}]${optional}`);
61
+ }
62
+ }
63
+
64
+ // 3. Check for ID field
65
+ if (!modelInfo.hasId) {
66
+ console.warn(
67
+ "āš ļø Warning: Model does not have an 'id' field. CRUD operations may not work correctly."
68
+ );
69
+ }
70
+
71
+ // 4. Read shared package name
72
+ const functionsDir = options.functionsDir || "functions";
73
+ const sharedPackageName = readSharedPackageName();
74
+ const relatedModels = backendLanguage === "typescript"
75
+ ? [modelInfo]
76
+ : await collectModelGraph(modelPath);
77
+
78
+ // 5. Generate BFF callFunction helper
79
+ await generateCallFunctionHelper();
80
+
81
+ // 6. Generate Azure Functions code
82
+ await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
83
+
84
+ if (backendLanguage !== "typescript") {
85
+ await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage);
86
+ }
87
+
88
+ // 7. Generate Next.js BFF API Routes
89
+ const apiDir = options.apiDir || "app/api";
90
+ await generateBFFRoutes(modelInfo, apiDir, sharedPackageName);
91
+
92
+ // 8. Generate Cosmos DB container Bicep file
93
+ await generateCosmosContainer(modelInfo);
94
+
95
+ // 9. Generate UI components (unless --api-only)
96
+ if (!options.apiOnly) {
97
+ await generateUIComponents(modelInfo, sharedPackageName);
98
+ await updateNavigationMenu(modelInfo);
99
+ }
100
+
101
+ console.log("\nāœ… Scaffold completed successfully!");
102
+ console.log("\nšŸ“ Next steps:");
103
+ console.log(` 1. Review generated files in ${describeFunctionsOutputPath(functionsDir, backendLanguage)} and ${apiDir}/`);
104
+ if (!options.apiOnly) {
105
+ console.log(` 2. Check the generated UI pages in app/${toKebabCase(modelInfo.name)}/`);
106
+ console.log(" 3. Navigate to the model from the homepage menu");
107
+ }
108
+ if (backendLanguage !== "typescript") {
109
+ console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
110
+ }
111
+ console.log(
112
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
113
+ );
114
+ console.log(
115
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
116
+ );
117
+ console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).dlx} swallowkit dev' to test the generated code`);
118
+ } catch (error: any) {
119
+ console.error("\nāŒ Scaffold failed:", error.message);
120
+ process.exit(1);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * ćƒ¢ćƒ‡ćƒ«ćƒ•ć‚”ć‚¤ćƒ«ć®ćƒ‘ć‚¹ć‚’č§£ę±ŗ
126
+ */
127
+ function resolveModelPath(modelInput: string): string {
128
+ const cwd = process.cwd();
129
+
130
+ // ēµ¶åÆ¾ćƒ‘ć‚¹ć®å “åˆ
131
+ if (path.isAbsolute(modelInput)) {
132
+ return modelInput;
133
+ }
134
+
135
+ // ę‹”å¼µå­ćŒć‚ć‚‹å “åˆļ¼ˆē›øåÆ¾ćƒ‘ć‚¹ļ¼‰
136
+ if (modelInput.endsWith(".ts")) {
137
+ const fullPath = path.join(cwd, modelInput);
138
+ if (fs.existsSync(fullPath)) {
139
+ return fullPath;
140
+ }
141
+ }
142
+
143
+ // ę‹”å¼µå­ćŒćŖć„å “åˆć€shared/models/ ć§ęŽ¢ć™
144
+ const defaultPath = path.join(cwd, "shared", "models", `${modelInput}.ts`);
145
+ if (fs.existsSync(defaultPath)) {
146
+ return defaultPath;
147
+ }
148
+
149
+ // lib/models/ ć§ć‚‚ęŽ¢ć™ļ¼ˆå¾Œę–¹äŗ’ę›ę€§ļ¼‰
150
+ const libPath = path.join(cwd, "lib", "models", `${modelInput}.ts`);
151
+ if (fs.existsSync(libPath)) {
152
+ return libPath;
153
+ }
154
+
155
+ // src/models/ ć§ć‚‚ęŽ¢ć™
156
+ const srcPath = path.join(cwd, "src", "models", `${modelInput}.ts`);
157
+ if (fs.existsSync(srcPath)) {
158
+ return srcPath;
159
+ }
160
+
161
+ // č¦‹ć¤ć‹ć‚‰ćŖć„å “åˆćÆć‚Øćƒ©ćƒ¼
162
+ throw new Error(
163
+ `Model file not found: ${modelInput}\n` +
164
+ ` Tried:\n` +
165
+ ` - ${modelInput}\n` +
166
+ ` - ${defaultPath}\n` +
167
+ ` - ${libPath}\n` +
168
+ ` - ${srcPath}\n` +
169
+ ` Please specify a valid model file path.`
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Read shared package name from shared/package.json
175
+ */
176
+ function readSharedPackageName(): string {
177
+ const cwd = process.cwd();
178
+ const sharedPkgPath = path.join(cwd, "shared", "package.json");
179
+
180
+ if (!fs.existsSync(sharedPkgPath)) {
181
+ throw new Error(
182
+ "shared/package.json not found.\n" +
183
+ "The shared package is required for model imports.\n" +
184
+ `Run "${getCommands(detectFromProject()).dlx} swallowkit init" to set up your project.`
185
+ );
186
+ }
187
+
188
+ const pkg = JSON.parse(fs.readFileSync(sharedPkgPath, "utf-8"));
189
+ return pkg.name;
190
+ }
191
+
192
+ /**
193
+ * Generate BFF callFunction helper (lib/api/call-function.ts)
194
+ */
195
+ async function generateCallFunctionHelper(): Promise<void> {
196
+ console.log("\nšŸ“¦ Generating BFF callFunction helper...");
197
+
198
+ const cwd = process.cwd();
199
+
200
+ const helperDir = path.join(cwd, "lib", "api");
201
+ const helperPath = path.join(helperDir, "call-function.ts");
202
+
203
+ if (!fs.existsSync(helperDir)) {
204
+ fs.mkdirSync(helperDir, { recursive: true });
205
+ }
206
+
207
+ const helperCode = generateBFFCallFunction();
208
+ fs.writeFileSync(helperPath, helperCode, "utf-8");
209
+ console.log(`āœ… Created: ${helperPath}`);
210
+ }
211
+
212
+ /**
213
+ * Generate Azure Functions CRUD code
214
+ */
215
+ async function generateFunctionsCode(
216
+ modelInfo: ModelInfo,
217
+ functionsDir: string,
218
+ sharedPackageName: string,
219
+ backendLanguage: BackendLanguage
220
+ ): Promise<void> {
221
+ console.log("\nšŸ”Ø Generating Azure Functions CRUD code...");
222
+
223
+ const modelKebab = toKebabCase(modelInfo.name);
224
+ if (backendLanguage === "typescript") {
225
+ const functionFilePath = path.join(
226
+ process.cwd(),
227
+ functionsDir,
228
+ "src",
229
+ `${modelKebab}.ts`
230
+ );
231
+
232
+ const functionDir = path.dirname(functionFilePath);
233
+ if (!fs.existsSync(functionDir)) {
234
+ fs.mkdirSync(functionDir, { recursive: true });
235
+ }
236
+
237
+ const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName);
238
+ fs.writeFileSync(functionFilePath, code, "utf-8");
239
+ console.log(`āœ… Created: ${functionFilePath}`);
240
+ return;
241
+ }
242
+
243
+ if (backendLanguage === "csharp") {
244
+ const functionFilePath = path.join(
245
+ process.cwd(),
246
+ functionsDir,
247
+ "Crud",
248
+ `${modelInfo.name}Functions.cs`
249
+ );
250
+ fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
251
+ fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo), "utf-8");
252
+ console.log(`āœ… Created: ${functionFilePath}`);
253
+ return;
254
+ }
255
+
256
+ const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
257
+ const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
258
+ fs.mkdirSync(blueprintsDir, { recursive: true });
259
+
260
+ const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo);
261
+ fs.writeFileSync(blueprintPath, blueprint, "utf-8");
262
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration);
263
+ console.log(`āœ… Created: ${blueprintPath}`);
264
+ }
265
+
266
+ async function collectModelGraph(modelPath: string, seen = new Map<string, ModelInfo>()): Promise<ModelInfo[]> {
267
+ const resolvedPath = path.resolve(modelPath);
268
+ if (seen.has(resolvedPath)) {
269
+ return Array.from(seen.values());
270
+ }
271
+
272
+ const modelInfo = await parseModelFile(resolvedPath);
273
+ seen.set(resolvedPath, modelInfo);
274
+
275
+ for (const ref of modelInfo.nestedSchemaRefs) {
276
+ const nestedPath = resolveNestedModelPath(resolvedPath, ref.importPath);
277
+ await collectModelGraph(nestedPath, seen);
278
+ }
279
+
280
+ return Array.from(seen.values());
281
+ }
282
+
283
+ function resolveNestedModelPath(modelPath: string, importPath: string): string {
284
+ let resolvedPath = path.resolve(path.dirname(modelPath), importPath);
285
+ if (!resolvedPath.endsWith(".ts")) {
286
+ resolvedPath += ".ts";
287
+ }
288
+ return resolvedPath;
289
+ }
290
+
291
+ function describeFunctionsOutputPath(functionsDir: string, backendLanguage: BackendLanguage): string {
292
+ if (backendLanguage === "typescript") {
293
+ return `${functionsDir}/src/`;
294
+ }
295
+ if (backendLanguage === "csharp") {
296
+ return `${functionsDir}/Crud/`;
297
+ }
298
+ return `${functionsDir}/blueprints/`;
299
+ }
300
+
301
+ async function generateLanguageSchemaArtifacts(
302
+ models: ModelInfo[],
303
+ rootModel: ModelInfo,
304
+ functionsDir: string,
305
+ backendLanguage: Exclude<BackendLanguage, "typescript">
306
+ ): Promise<void> {
307
+ console.log("\n🧬 Generating OpenAPI-backed schema artifacts...");
308
+
309
+ const openApiDir = path.join(process.cwd(), functionsDir, "openapi");
310
+ fs.mkdirSync(openApiDir, { recursive: true });
311
+
312
+ const specPath = path.join(openApiDir, `${toKebabCase(rootModel.name)}.openapi.json`);
313
+ fs.writeFileSync(specPath, generateOpenApiDocument(models, rootModel), "utf-8");
314
+ console.log(`āœ… Created: ${specPath}`);
315
+
316
+ const outputDir = path.join(
317
+ process.cwd(),
318
+ functionsDir,
319
+ "generated",
320
+ backendLanguage === "csharp" ? "csharp-models" : "python-models"
321
+ );
322
+
323
+ fs.mkdirSync(outputDir, { recursive: true });
324
+ await runOpenApiGenerator(specPath, outputDir, backendLanguage);
325
+ if (backendLanguage === "csharp") {
326
+ pruneGeneratedCSharpArtifacts(outputDir);
327
+ }
328
+ console.log(`āœ… Generated ${backendLanguage} schema assets: ${outputDir}`);
329
+ }
330
+
331
+ async function runOpenApiGenerator(
332
+ specPath: string,
333
+ outputDir: string,
334
+ backendLanguage: Exclude<BackendLanguage, "typescript">
335
+ ): Promise<void> {
336
+ const pm = detectFromProject();
337
+ const command = pm === "pnpm" ? "pnpm" : "npx";
338
+ const args = pm === "pnpm"
339
+ ? ["exec", "openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)]
340
+ : ["openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)];
341
+
342
+ await new Promise<void>((resolve, reject) => {
343
+ const child = spawn(command, args, {
344
+ cwd: process.cwd(),
345
+ shell: true,
346
+ stdio: "inherit",
347
+ });
348
+
349
+ child.on("close", (code) => {
350
+ if (code === 0) {
351
+ resolve();
352
+ return;
353
+ }
354
+
355
+ reject(new Error(`OpenAPI generator exited with code ${code}`));
356
+ });
357
+
358
+ child.on("error", reject);
359
+ });
360
+ }
361
+
362
+ export function getOpenApiGeneratorArgs(
363
+ specPath: string,
364
+ outputDir: string,
365
+ backendLanguage: Exclude<BackendLanguage, "typescript">
366
+ ): string[] {
367
+ const globalProperty = backendLanguage === "csharp"
368
+ ? "models,supportingFiles,apis=false,modelDocs=false,modelTests=false"
369
+ : "models,apis=false,supportingFiles=false,modelDocs=false,modelTests=false";
370
+
371
+ const baseArgs = [
372
+ "generate",
373
+ "-i",
374
+ specPath,
375
+ "-o",
376
+ outputDir,
377
+ "--global-property",
378
+ globalProperty,
379
+ ];
380
+
381
+ if (backendLanguage === "csharp") {
382
+ return [
383
+ ...baseArgs,
384
+ "-g",
385
+ "csharp",
386
+ "--additional-properties",
387
+ "packageName=SwallowKitBackendModels,targetFramework=net8.0,nullableReferenceTypes=true",
388
+ ];
389
+ }
390
+
391
+ return [
392
+ ...baseArgs,
393
+ "-g",
394
+ "python",
395
+ "--additional-properties",
396
+ "packageName=backend_models,projectName=backend-models",
397
+ ];
398
+ }
399
+
400
+ export function getCSharpSchemaArtifactPruneTargets(outputDir: string): string[] {
401
+ return [
402
+ path.join(outputDir, "src", "SwallowKitBackendModels.Test"),
403
+ path.join(outputDir, "src", "SwallowKitBackendModels", "Api"),
404
+ path.join(outputDir, "src", "SwallowKitBackendModels", "Extensions"),
405
+ ];
406
+ }
407
+
408
+ function pruneGeneratedCSharpArtifacts(outputDir: string): void {
409
+ for (const target of getCSharpSchemaArtifactPruneTargets(outputDir)) {
410
+ if (fs.existsSync(target)) {
411
+ fs.rmSync(target, { recursive: true, force: true });
412
+ }
413
+ }
414
+
415
+ const clientDir = path.join(outputDir, "src", "SwallowKitBackendModels", "Client");
416
+ if (!fs.existsSync(clientDir)) {
417
+ return;
418
+ }
419
+
420
+ for (const entry of fs.readdirSync(clientDir, { withFileTypes: true })) {
421
+ if (!entry.isFile() || entry.name === "Option.cs") {
422
+ continue;
423
+ }
424
+
425
+ fs.rmSync(path.join(clientDir, entry.name), { force: true });
426
+ }
427
+ }
428
+
429
+ function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
430
+ if (!fs.existsSync(functionAppPath)) {
431
+ throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
432
+ }
433
+
434
+ const content = fs.readFileSync(functionAppPath, "utf-8");
435
+ if (content.includes(registration)) {
436
+ return;
437
+ }
438
+
439
+ const marker = "# SwallowKit scaffold registrations";
440
+ if (!content.includes(marker)) {
441
+ throw new Error(`Could not find scaffold registration marker in ${functionAppPath}`);
442
+ }
443
+
444
+ const updated = content.replace(marker, `${registration}\n${marker}`);
445
+ fs.writeFileSync(functionAppPath, updated, "utf-8");
446
+ }
447
+
448
+ /**
449
+ * Next.js BFF API Routes ć‚’ē”Ÿęˆ
450
+ */
451
+ async function generateBFFRoutes(
452
+ modelInfo: any,
453
+ apiDir: string,
454
+ sharedPackageName: string
455
+ ): Promise<void> {
456
+ console.log("\nšŸ”Ø Generating Next.js BFF API routes...");
457
+
458
+ const modelKebab = toKebabCase(modelInfo.name);
459
+ const modelCamel = modelInfo.name.charAt(0).toLowerCase() + modelInfo.name.slice(1);
460
+
461
+ // List route: app/api/[model]/route.ts
462
+ const listRoutePath = path.join(
463
+ process.cwd(),
464
+ apiDir,
465
+ modelCamel,
466
+ "route.ts"
467
+ );
468
+
469
+ // Detail route: app/api/[model]/[id]/route.ts
470
+ const detailRoutePath = path.join(
471
+ process.cwd(),
472
+ apiDir,
473
+ modelCamel,
474
+ "[id]",
475
+ "route.ts"
476
+ );
477
+
478
+ // ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖć‚’ä½œęˆ
479
+ const listRouteDir = path.dirname(listRoutePath);
480
+ const detailRouteDir = path.dirname(detailRoutePath);
481
+
482
+ if (!fs.existsSync(listRouteDir)) {
483
+ fs.mkdirSync(listRouteDir, { recursive: true });
484
+ }
485
+
486
+ if (!fs.existsSync(detailRouteDir)) {
487
+ fs.mkdirSync(detailRouteDir, { recursive: true });
488
+ }
489
+
490
+ // ć‚³ćƒ¼ćƒ‰ć‚’ē”Ÿęˆ
491
+ const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
492
+
493
+ // ćƒ•ć‚”ć‚¤ćƒ«ć«ę›øćč¾¼ćæ
494
+ fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
495
+ fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
496
+
497
+ console.log(`āœ… Created: ${listRoutePath}`);
498
+ console.log(`āœ… Created: ${detailRoutePath}`);
499
+ }
500
+
501
+ /**
502
+ * Generate Cosmos DB container Bicep file
503
+ */
504
+ async function generateCosmosContainer(modelInfo: any): Promise<void> {
505
+ console.log("\nšŸ—„ļø Generating Cosmos DB container Bicep file...");
506
+
507
+ const modelKebab = toKebabCase(modelInfo.name);
508
+ const modelPascal = toPascalCase(modelInfo.name);
509
+ const cwd = process.cwd();
510
+
511
+ // Check if infra directory exists
512
+ const infraDir = path.join(cwd, "infra");
513
+ if (!fs.existsSync(infraDir)) {
514
+ console.log("ā„¹ļø infra directory not found. Skipping Cosmos DB container generation.");
515
+ return;
516
+ }
517
+
518
+ // Check if containers directory exists, if not create it
519
+ const containersDir = path.join(infraDir, "containers");
520
+ if (!fs.existsSync(containersDir)) {
521
+ fs.mkdirSync(containersDir, { recursive: true });
522
+ }
523
+
524
+ // Generate container Bicep file
525
+ const containerFileName = `${modelKebab}-container.bicep`;
526
+ const containerFilePath = path.join(containersDir, containerFileName);
527
+
528
+ const bicepContent = `@description('Cosmos DB account name')
529
+ param cosmosAccountName string
530
+
531
+ @description('Database name')
532
+ param databaseName string
533
+
534
+ @description('Container name')
535
+ param containerName string = '${modelPascal}s'
536
+
537
+ @description('Partition key path')
538
+ param partitionKeyPath string = '/id'
539
+
540
+ @description('Throughput (RU/s) - only used for Free Tier')
541
+ param throughput int = 400
542
+
543
+ @description('Cosmos DB mode: freetier or serverless')
544
+ param cosmosDbMode string
545
+
546
+ // Reference existing Cosmos DB account
547
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
548
+ name: cosmosAccountName
549
+ }
550
+
551
+ // Reference existing database
552
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
553
+ parent: cosmosAccount
554
+ name: databaseName
555
+ }
556
+
557
+ // Container for ${modelPascal}
558
+ resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = {
559
+ parent: database
560
+ name: containerName
561
+ properties: {
562
+ resource: {
563
+ id: containerName
564
+ partitionKey: {
565
+ paths: [
566
+ partitionKeyPath
567
+ ]
568
+ kind: 'Hash'
569
+ }
570
+ indexingPolicy: {
571
+ automatic: true
572
+ indexingMode: 'consistent'
573
+ includedPaths: [
574
+ {
575
+ path: '/*'
576
+ }
577
+ ]
578
+ excludedPaths: [
579
+ {
580
+ path: '/_etag/?'
581
+ }
582
+ ]
583
+ }
584
+ }
585
+ options: cosmosDbMode == 'freetier' ? {
586
+ throughput: throughput
587
+ } : {}
588
+ }
589
+ }
590
+
591
+ output containerName string = container.name
592
+ `;
593
+
594
+ // Write Bicep file (overwrite if exists)
595
+ fs.writeFileSync(containerFilePath, bicepContent, "utf-8");
596
+ console.log(`āœ… Created: ${containerFilePath}`);
597
+
598
+ // Update main.bicep to include this container module
599
+ await updateMainBicepWithContainer(modelKebab, modelPascal);
600
+ }
601
+
602
+ /**
603
+ * Update main.bicep to include new container module
604
+ */
605
+ async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string): Promise<void> {
606
+ const cwd = process.cwd();
607
+ const mainBicepPath = path.join(cwd, "infra", "main.bicep");
608
+
609
+ if (!fs.existsSync(mainBicepPath)) {
610
+ console.log("ā„¹ļø main.bicep not found. Please manually add the container module.");
611
+ return;
612
+ }
613
+
614
+ let mainBicepContent = fs.readFileSync(mainBicepPath, "utf-8");
615
+
616
+ // Check if container module already exists
617
+ const containerModuleName = `${modelKebab.replace(/-/g, '')}Container`;
618
+ if (mainBicepContent.includes(`module ${containerModuleName}`)) {
619
+ console.log(`ā„¹ļø Container module '${containerModuleName}' already exists in main.bicep`);
620
+ return;
621
+ }
622
+
623
+ // Find the position to insert the container module (after cosmosDb modules)
624
+ // Look for the end of both cosmosDb modules (FreeTier and Serverless)
625
+ const cosmosModulePattern = /module cosmosDbServerless ['"]modules\/cosmosdb-serverless\.bicep['"] = if \(cosmosDbMode == 'serverless'\) \{[\s\S]*?\n\}/;
626
+ const cosmosModuleMatch = mainBicepContent.match(cosmosModulePattern);
627
+
628
+ if (!cosmosModuleMatch) {
629
+ console.log("āš ļø Could not find Cosmos DB Serverless module in main.bicep. Please manually add the container module:");
630
+ console.log(`\nmodule ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
631
+ name: '${modelKebab}-container'
632
+ params: {
633
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
634
+ databaseName: databaseName
635
+ cosmosDbMode: cosmosDbMode
636
+ }
637
+ dependsOn: [
638
+ cosmosDbFreeTier
639
+ cosmosDbServerless
640
+ ]
641
+ }\n`);
642
+ return;
643
+ }
644
+
645
+ // Find the end of the cosmosDbServerless module
646
+ const insertPosition = cosmosModuleMatch.index! + cosmosModuleMatch[0].length;
647
+
648
+ // Create the container module declaration
649
+ const containerModule = `
650
+
651
+ // ${modelPascal} Container
652
+ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
653
+ name: '${modelKebab}-container'
654
+ params: {
655
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
656
+ databaseName: '$` + `{projectName}Database'
657
+ cosmosDbMode: cosmosDbMode
658
+ }
659
+ dependsOn: [
660
+ cosmosDbFreeTier
661
+ cosmosDbServerless
662
+ ]
663
+ }`;
664
+
665
+ // Insert the module
666
+ mainBicepContent =
667
+ mainBicepContent.slice(0, insertPosition) +
668
+ containerModule +
669
+ mainBicepContent.slice(insertPosition);
670
+
671
+ fs.writeFileSync(mainBicepPath, mainBicepContent, "utf-8");
672
+ console.log(`āœ… Added container module to main.bicep`);
673
+ }
674
+
675
+ /**
676
+ * Generate UI components (list, detail, form, create, edit pages)
677
+ */
678
+ async function generateUIComponents(modelInfo: any, sharedPackageName: string): Promise<void> {
679
+ console.log("\nšŸŽØ Generating UI components...");
680
+
681
+ const modelKebab = toKebabCase(modelInfo.name);
682
+ const modelName = modelInfo.name;
683
+ const cwd = process.cwd();
684
+
685
+ // Create directory structure: app/[model]/
686
+ const modelDir = path.join(cwd, "app", modelKebab);
687
+ const componentsDir = path.join(modelDir, "_components");
688
+ const newDir = path.join(modelDir, "new");
689
+ const idDir = path.join(modelDir, "[id]");
690
+ const editDir = path.join(idDir, "edit");
691
+
692
+ // Create directories
693
+ [componentsDir, newDir, editDir].forEach(dir => {
694
+ if (!fs.existsSync(dir)) {
695
+ fs.mkdirSync(dir, { recursive: true });
696
+ }
697
+ });
698
+
699
+ // Generate and write files
700
+ const listPage = generateListPage(modelInfo, sharedPackageName);
701
+ const detailPage = generateDetailPage(modelInfo, sharedPackageName);
702
+ const formComponent = generateFormComponent(modelInfo, sharedPackageName);
703
+ const newPage = generateNewPage(modelInfo);
704
+ const editPage = generateEditPage(modelInfo, sharedPackageName);
705
+
706
+ fs.writeFileSync(path.join(modelDir, "page.tsx"), listPage, "utf-8");
707
+ fs.writeFileSync(path.join(idDir, "page.tsx"), detailPage, "utf-8");
708
+ fs.writeFileSync(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, "utf-8");
709
+ fs.writeFileSync(path.join(newDir, "page.tsx"), newPage, "utf-8");
710
+ fs.writeFileSync(path.join(editDir, "page.tsx"), editPage, "utf-8");
711
+
712
+ console.log(`āœ… Created: ${path.join(modelDir, "page.tsx")}`);
713
+ console.log(`āœ… Created: ${path.join(idDir, "page.tsx")}`);
714
+ console.log(`āœ… Created: ${path.join(componentsDir, `${modelName}Form.tsx`)}`);
715
+ console.log(`āœ… Created: ${path.join(newDir, "page.tsx")}`);
716
+ console.log(`āœ… Created: ${path.join(editDir, "page.tsx")}`);
717
+ }
718
+
719
+ /**
720
+ * Update navigation menu on the homepage
721
+ */
722
+ async function updateNavigationMenu(modelInfo: any): Promise<void> {
723
+ console.log("\nšŸ“‹ Updating navigation menu...");
724
+
725
+ const modelKebab = toKebabCase(modelInfo.name);
726
+ const cwd = process.cwd();
727
+
728
+ // Update scaffold config
729
+ const configPath = path.join(cwd, "lib", "scaffold-config.ts");
730
+
731
+ if (!fs.existsSync(configPath)) {
732
+ console.log("āš ļø scaffold-config.ts not found. Skipping navigation menu update.");
733
+ return;
734
+ }
735
+
736
+ // Read existing config
737
+ const configContent = fs.readFileSync(configPath, "utf-8");
738
+
739
+ // Parse models array - extract each model entry
740
+ const models: Array<{ name: string; path: string; label: string }> = [];
741
+ const modelsMatch = configContent.match(/models:\s*\[([\s\S]*?)\]\s*as\s*ScaffoldModel\[\]/);
742
+
743
+ if (modelsMatch) {
744
+ const modelsArrayContent = modelsMatch[1];
745
+ // Match each object in the array: { name: '...', path: '...', label: '...' }
746
+ const modelPattern = /\{\s*name:\s*['"]([^'"]+)['"]\s*,\s*path:\s*['"]([^'"]+)['"]\s*,\s*label:\s*['"]([^'"]+)['"]\s*\}/g;
747
+ let match;
748
+ while ((match = modelPattern.exec(modelsArrayContent)) !== null) {
749
+ models.push({
750
+ name: match[1],
751
+ path: match[2],
752
+ label: match[3]
753
+ });
754
+ }
755
+ }
756
+
757
+ // Check if model already exists
758
+ if (models.find(m => m.name === modelInfo.name)) {
759
+ console.log(`ā„¹ļø ${modelInfo.name} already in navigation menu`);
760
+ return;
761
+ }
762
+
763
+ // Add new model
764
+ models.push({
765
+ name: modelInfo.name,
766
+ path: `/${modelKebab}`,
767
+ label: modelInfo.displayName
768
+ });
769
+
770
+ // Generate new scaffold-config.ts content
771
+ const newConfigContent = `export interface ScaffoldModel {
772
+ name: string;
773
+ path: string;
774
+ label: string;
775
+ }
776
+
777
+ export const scaffoldConfig = {
778
+ models: [
779
+ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label}' },`).join('\n')}
780
+ ] as ScaffoldModel[]
781
+ };
782
+ `;
783
+
784
+ fs.writeFileSync(configPath, newConfigContent, "utf-8");
785
+ console.log(`āœ… Added ${modelInfo.name} to navigation menu`);
786
+ }