swallowkit 1.0.0-beta.34 → 1.0.0-beta.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +84 -284
- package/README.md +85 -338
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +136 -32
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +4 -1
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/project/manifest.js +1 -1
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/scaffold/functions-generator.js +1 -1
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +22 -3
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/machine/index.js +1 -1
- package/dist/machine/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +3 -3
- package/src/__tests__/functions-generator.test.ts +1 -1
- package/src/__tests__/init.test.ts +21 -0
- package/src/__tests__/machine.test.ts +105 -0
- package/src/__tests__/scaffold.test.ts +94 -0
- package/src/cli/commands/init.ts +150 -39
- package/src/cli/commands/scaffold.ts +5 -1
- package/src/cli/index.ts +1 -1
- package/src/core/project/manifest.ts +1 -1
- package/src/core/scaffold/functions-generator.ts +1 -1
- package/src/core/scaffold/native-schema-generator.ts +27 -3
- package/src/machine/index.ts +1 -1
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
import * as childProcess from "child_process";
|
|
4
6
|
import { runMachineCli } from "../machine";
|
|
5
7
|
|
|
8
|
+
jest.mock("child_process", () => {
|
|
9
|
+
const actual = jest.requireActual<typeof import("child_process")>("child_process");
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
spawn: jest.fn(),
|
|
13
|
+
spawnSync: jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
6
17
|
const repoRoot = process.cwd();
|
|
7
18
|
|
|
8
19
|
function writeFile(filePath: string, content: string): void {
|
|
@@ -68,6 +79,59 @@ function createProjectFixture(rootDir: string, options: { includeGeneratedArtifa
|
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
82
|
+
function createCSharpScaffoldFixture(rootDir: string): void {
|
|
83
|
+
writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
|
|
84
|
+
writeFile(
|
|
85
|
+
path.join(rootDir, "swallowkit.config.js"),
|
|
86
|
+
`module.exports = {
|
|
87
|
+
backend: {
|
|
88
|
+
language: 'csharp',
|
|
89
|
+
},
|
|
90
|
+
api: {
|
|
91
|
+
endpoint: '/api/_swallowkit',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
`
|
|
95
|
+
);
|
|
96
|
+
writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
|
|
97
|
+
writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
|
|
98
|
+
writeFile(path.join(rootDir, "shared", "models", "estimate.ts"), createModelSource("Estimate"));
|
|
99
|
+
writeFile(path.join(rootDir, "shared", "models", "member.ts"), createModelSource("Member"));
|
|
100
|
+
writeFile(path.join(rootDir, "shared", "models", "team.ts"), createModelSource("Team"));
|
|
101
|
+
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
|
102
|
+
fs.symlinkSync(
|
|
103
|
+
path.join(repoRoot, "node_modules", "zod"),
|
|
104
|
+
path.join(rootDir, "node_modules", "zod"),
|
|
105
|
+
"junction"
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
writeFile(
|
|
109
|
+
path.join(rootDir, "functions", "Crud", "EstimateFunctions.cs"),
|
|
110
|
+
"namespace SwallowKit.Functions;\npublic sealed class EstimateFunctions { public void Approve() {} public void Submit() {} public void Remand() {} }\n"
|
|
111
|
+
);
|
|
112
|
+
writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "README.md"), "keep generated readme\n");
|
|
113
|
+
writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.csproj"), "<Project />\n");
|
|
114
|
+
writeFile(path.join(rootDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.sln"), "solution\n");
|
|
115
|
+
writeFile(
|
|
116
|
+
path.join(rootDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Member.cs"),
|
|
117
|
+
"// existing member\n"
|
|
118
|
+
);
|
|
119
|
+
writeFile(
|
|
120
|
+
path.join(rootDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Team.cs"),
|
|
121
|
+
"// existing team\n"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function mockSuccessfulSpawn() {
|
|
126
|
+
const spawnMock = childProcess.spawn as unknown as any;
|
|
127
|
+
spawnMock.mockImplementation(() => {
|
|
128
|
+
const child = new EventEmitter() as childProcess.ChildProcess;
|
|
129
|
+
process.nextTick(() => child.emit("close", 0));
|
|
130
|
+
return child;
|
|
131
|
+
});
|
|
132
|
+
return spawnMock;
|
|
133
|
+
}
|
|
134
|
+
|
|
71
135
|
function createFreshInitProjectFixture(rootDir: string, backendLanguage: "csharp" | "python" = "csharp"): void {
|
|
72
136
|
writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
|
|
73
137
|
writeFile(
|
|
@@ -248,4 +312,45 @@ describe("machine CLI", () => {
|
|
|
248
312
|
);
|
|
249
313
|
expect(fs.existsSync(path.join(tempDir, ".swallowkit", "project.json"))).toBe(true);
|
|
250
314
|
});
|
|
315
|
+
|
|
316
|
+
it("preserves C# custom Functions and shared generated assets when api-only scaffolding one model", async () => {
|
|
317
|
+
createCSharpScaffoldFixture(tempDir);
|
|
318
|
+
const spawnSyncMock = childProcess.spawnSync as unknown as any;
|
|
319
|
+
spawnSyncMock.mockReturnValue({ status: 0 });
|
|
320
|
+
const spawnMock = mockSuccessfulSpawn();
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const { response, exitCode } = await runMachine([
|
|
324
|
+
"node",
|
|
325
|
+
"swallowkit",
|
|
326
|
+
"machine",
|
|
327
|
+
"generate",
|
|
328
|
+
"scaffold",
|
|
329
|
+
"estimate",
|
|
330
|
+
"--api-only",
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
expect(exitCode).toBe(0);
|
|
334
|
+
expect(response.ok).toBe(true);
|
|
335
|
+
expect(response.command).toBe("generate-scaffold");
|
|
336
|
+
expect(response.data.createdFiles).toEqual(
|
|
337
|
+
expect.arrayContaining([
|
|
338
|
+
"functions/Crud/EstimateCrudFunctions.cs",
|
|
339
|
+
"app/api/estimate/route.ts",
|
|
340
|
+
"app/api/estimate/[id]/route.ts",
|
|
341
|
+
])
|
|
342
|
+
);
|
|
343
|
+
expect(response.data.deletedFiles).toEqual([]);
|
|
344
|
+
expect(fs.readFileSync(path.join(tempDir, "functions", "Crud", "EstimateFunctions.cs"), "utf-8")).toContain("Approve");
|
|
345
|
+
expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "README.md"))).toBe(true);
|
|
346
|
+
expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.csproj"))).toBe(true);
|
|
347
|
+
expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "SwallowKitBackendModels.sln"))).toBe(true);
|
|
348
|
+
expect(fs.readFileSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Member.cs"), "utf-8")).toBe("// existing member\n");
|
|
349
|
+
expect(fs.readFileSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Team.cs"), "utf-8")).toBe("// existing team\n");
|
|
350
|
+
expect(fs.existsSync(path.join(tempDir, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model", "Estimate.cs"))).toBe(true);
|
|
351
|
+
} finally {
|
|
352
|
+
spawnMock.mockReset();
|
|
353
|
+
spawnSyncMock.mockReset();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
251
356
|
});
|
|
@@ -1,14 +1,44 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
import * as childProcess from "child_process";
|
|
2
6
|
import {
|
|
3
7
|
NSWAG_CONSOLECORE_VERSION,
|
|
4
8
|
buildCSharpCodegenToolManifestSource,
|
|
5
9
|
buildPythonCodegenRequirementsSource,
|
|
10
|
+
generateLanguageSchemaArtifacts,
|
|
6
11
|
getCSharpNativeGeneratorArgs,
|
|
7
12
|
getCSharpSchemaModelPath,
|
|
8
13
|
getCSharpSchemaOptionPath,
|
|
9
14
|
getPythonNativeGeneratorArgs,
|
|
10
15
|
getPythonSchemaModelPath,
|
|
11
16
|
} from "../core/scaffold/native-schema-generator";
|
|
17
|
+
import { createBasicModelInfo } from "./fixtures";
|
|
18
|
+
|
|
19
|
+
jest.mock("child_process", () => {
|
|
20
|
+
const actual = jest.requireActual<typeof import("child_process")>("child_process");
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
spawn: jest.fn(),
|
|
24
|
+
spawnSync: jest.fn(),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function writeFile(filePath: string, content: string): void {
|
|
29
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
30
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function mockSuccessfulSpawn() {
|
|
34
|
+
const spawnMock = childProcess.spawn as unknown as any;
|
|
35
|
+
spawnMock.mockImplementation(() => {
|
|
36
|
+
const child = new EventEmitter() as childProcess.ChildProcess;
|
|
37
|
+
process.nextTick(() => child.emit("close", 0));
|
|
38
|
+
return child;
|
|
39
|
+
});
|
|
40
|
+
return spawnMock;
|
|
41
|
+
}
|
|
12
42
|
|
|
13
43
|
describe("native schema generators", () => {
|
|
14
44
|
it("writes a dotnet tool manifest for NSwag", () => {
|
|
@@ -64,4 +94,68 @@ describe("native schema generators", () => {
|
|
|
64
94
|
path.join("C:\\temp\\generated\\python-models", "backend_models", "models", "product.py")
|
|
65
95
|
);
|
|
66
96
|
});
|
|
97
|
+
|
|
98
|
+
describe("non-destructive native schema generation", () => {
|
|
99
|
+
const originalCwd = process.cwd();
|
|
100
|
+
let tempDir: string;
|
|
101
|
+
let spawnSyncMock: any;
|
|
102
|
+
let spawnMock: any;
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-native-schema-"));
|
|
106
|
+
process.chdir(tempDir);
|
|
107
|
+
fs.mkdirSync(path.join(tempDir, "functions"), { recursive: true });
|
|
108
|
+
spawnSyncMock = childProcess.spawnSync as unknown as any;
|
|
109
|
+
spawnSyncMock.mockReturnValue({ status: 0 });
|
|
110
|
+
spawnMock = mockSuccessfulSpawn();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
spawnMock.mockReset();
|
|
115
|
+
spawnSyncMock.mockReset();
|
|
116
|
+
process.chdir(originalCwd);
|
|
117
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("preserves existing C# generated directory contents while writing only requested model files", async () => {
|
|
121
|
+
const outputDir = path.join(tempDir, "functions", "generated", "csharp-models");
|
|
122
|
+
writeFile(path.join(outputDir, "README.md"), "keep me\n");
|
|
123
|
+
writeFile(path.join(outputDir, "SwallowKitBackendModels.csproj"), "<Project />\n");
|
|
124
|
+
writeFile(path.join(outputDir, "src", "SwallowKitBackendModels", "Model", "Member.cs"), "// existing member\n");
|
|
125
|
+
|
|
126
|
+
const estimate = createBasicModelInfo({ name: "Estimate", filePath: path.join(tempDir, "shared", "models", "estimate.ts") });
|
|
127
|
+
|
|
128
|
+
await generateLanguageSchemaArtifacts([estimate], estimate, "functions", "csharp");
|
|
129
|
+
|
|
130
|
+
expect(fs.readFileSync(path.join(outputDir, "README.md"), "utf-8")).toBe("keep me\n");
|
|
131
|
+
expect(fs.existsSync(path.join(outputDir, "SwallowKitBackendModels.csproj"))).toBe(true);
|
|
132
|
+
expect(fs.readFileSync(getCSharpSchemaModelPath(outputDir, "Member"), "utf-8")).toBe("// existing member\n");
|
|
133
|
+
expect(fs.existsSync(getCSharpSchemaModelPath(outputDir, "Todo"))).toBe(false);
|
|
134
|
+
expect(fs.existsSync(getCSharpSchemaModelPath(outputDir, "Estimate"))).toBe(true);
|
|
135
|
+
expect(fs.existsSync(getCSharpSchemaOptionPath(outputDir))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("preserves existing Python generated directory contents while appending requested model exports", async () => {
|
|
139
|
+
const outputDir = path.join(tempDir, "functions", "generated", "python-models");
|
|
140
|
+
writeFile(path.join(outputDir, "README.md"), "keep python\n");
|
|
141
|
+
writeFile(path.join(outputDir, "backend_models", "models", "member.py"), "# existing member\n");
|
|
142
|
+
writeFile(path.join(outputDir, "backend_models", "__init__.py"), "from .models.member import Member\n");
|
|
143
|
+
writeFile(path.join(outputDir, "backend_models", "models", "__init__.py"), "from .member import Member\n");
|
|
144
|
+
|
|
145
|
+
const estimate = createBasicModelInfo({ name: "Estimate", filePath: path.join(tempDir, "shared", "models", "estimate.ts") });
|
|
146
|
+
|
|
147
|
+
await generateLanguageSchemaArtifacts([estimate], estimate, "functions", "python");
|
|
148
|
+
|
|
149
|
+
expect(fs.readFileSync(path.join(outputDir, "README.md"), "utf-8")).toBe("keep python\n");
|
|
150
|
+
expect(fs.readFileSync(path.join(outputDir, "backend_models", "models", "member.py"), "utf-8")).toBe("# existing member\n");
|
|
151
|
+
expect(fs.existsSync(getPythonSchemaModelPath(outputDir, "Todo"))).toBe(false);
|
|
152
|
+
expect(fs.existsSync(getPythonSchemaModelPath(outputDir, "Estimate"))).toBe(true);
|
|
153
|
+
const packageInit = fs.readFileSync(path.join(outputDir, "backend_models", "__init__.py"), "utf-8");
|
|
154
|
+
const modelsInit = fs.readFileSync(path.join(outputDir, "backend_models", "models", "__init__.py"), "utf-8");
|
|
155
|
+
expect(packageInit).toContain("from .models.member import Member");
|
|
156
|
+
expect(packageInit).toContain("from .models.estimate import Estimate");
|
|
157
|
+
expect(modelsInit).toContain("from .member import Member");
|
|
158
|
+
expect(modelsInit).toContain("from .estimate import Estimate");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
67
161
|
});
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -375,32 +375,136 @@ async function upgradeNextJs(projectDir: string, version: string, pm: PackageMan
|
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
async function installDependencies(projectDir: string, pm: PackageManager = 'pnpm'): Promise<void> {
|
|
378
|
+
console.log('\n📦 Installing dependencies...\n');
|
|
379
|
+
|
|
380
|
+
const { ignoredBuilds } = await runInstallAndDetectIgnoredBuilds(projectDir, pm);
|
|
381
|
+
|
|
382
|
+
console.log('\n✅ Dependencies installed\n');
|
|
383
|
+
|
|
384
|
+
if (pm === 'pnpm' && ignoredBuilds.length > 0) {
|
|
385
|
+
await maybeApproveBuilds(projectDir, ignoredBuilds);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Run `<pm> install` while passing through stdio so the user sees progress.
|
|
391
|
+
* For pnpm, also tee stdout/stderr to detect the ERR_PNPM_IGNORED_BUILDS warning
|
|
392
|
+
* and extract the affected package names.
|
|
393
|
+
*/
|
|
394
|
+
async function runInstallAndDetectIgnoredBuilds(
|
|
395
|
+
projectDir: string,
|
|
396
|
+
pm: PackageManager,
|
|
397
|
+
): Promise<{ ignoredBuilds: string[] }> {
|
|
378
398
|
return new Promise((resolve, reject) => {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
pm,
|
|
383
|
-
['install'],
|
|
384
|
-
{
|
|
399
|
+
// For npm, we don't need to detect anything — keep simple inherit behavior.
|
|
400
|
+
if (pm !== 'pnpm') {
|
|
401
|
+
const child = spawn(pm, ['install'], {
|
|
385
402
|
cwd: projectDir,
|
|
386
403
|
stdio: 'inherit',
|
|
387
404
|
shell: true,
|
|
388
|
-
}
|
|
389
|
-
|
|
405
|
+
});
|
|
406
|
+
child.on('close', (code) => {
|
|
407
|
+
if (code !== 0) reject(new Error(`${pm} install exited with code ${code}`));
|
|
408
|
+
else resolve({ ignoredBuilds: [] });
|
|
409
|
+
});
|
|
410
|
+
child.on('error', reject);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
390
413
|
|
|
391
|
-
|
|
414
|
+
// pnpm: capture output while teeing to the user's terminal.
|
|
415
|
+
const child = spawn(pm, ['install'], {
|
|
416
|
+
cwd: projectDir,
|
|
417
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
418
|
+
shell: true,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
let combined = '';
|
|
422
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
423
|
+
process.stdout.write(chunk);
|
|
424
|
+
combined += chunk.toString('utf8');
|
|
425
|
+
});
|
|
426
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
427
|
+
process.stderr.write(chunk);
|
|
428
|
+
combined += chunk.toString('utf8');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
child.on('close', (code) => {
|
|
392
432
|
if (code !== 0) {
|
|
393
433
|
reject(new Error(`${pm} install exited with code ${code}`));
|
|
394
|
-
|
|
395
|
-
console.log('\n✅ Dependencies installed\n');
|
|
396
|
-
resolve();
|
|
434
|
+
return;
|
|
397
435
|
}
|
|
436
|
+
resolve({ ignoredBuilds: parseIgnoredBuilds(combined) });
|
|
398
437
|
});
|
|
438
|
+
child.on('error', reject);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
399
441
|
|
|
400
|
-
|
|
401
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning and return
|
|
444
|
+
* the bare package names (without versions), de-duplicated.
|
|
445
|
+
*
|
|
446
|
+
* Matches both `ERR_PNPM_IGNORED_BUILDS` and the plain `Ignored build scripts:` line.
|
|
447
|
+
*/
|
|
448
|
+
export function parseIgnoredBuilds(output: string): string[] {
|
|
449
|
+
const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
|
|
450
|
+
if (!match) return [];
|
|
451
|
+
const list = match[1]
|
|
452
|
+
.split(',')
|
|
453
|
+
.map((entry) => entry.trim())
|
|
454
|
+
// strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
|
|
455
|
+
.map((entry) => entry.replace(/@[^@]+$/, ''))
|
|
456
|
+
.filter((name) => name.length > 0);
|
|
457
|
+
return Array.from(new Set(list));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function maybeApproveBuilds(projectDir: string, ignoredBuilds: string[]): Promise<void> {
|
|
461
|
+
console.log(
|
|
462
|
+
`\n⚠️ pnpm skipped build scripts for: ${ignoredBuilds.join(', ')}\n` +
|
|
463
|
+
' These packages (e.g. sharp) need their build scripts to run correctly.\n',
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const response = await prompts({
|
|
467
|
+
type: 'confirm',
|
|
468
|
+
name: 'approve',
|
|
469
|
+
message: 'Run `pnpm approve-builds` now to approve these build scripts?',
|
|
470
|
+
initial: true,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (!response.approve) {
|
|
474
|
+
console.log(
|
|
475
|
+
'\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n',
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
await new Promise<void>((resolve, reject) => {
|
|
481
|
+
const child = spawn('pnpm', ['approve-builds'], {
|
|
482
|
+
cwd: projectDir,
|
|
483
|
+
stdio: 'inherit',
|
|
484
|
+
shell: true,
|
|
485
|
+
});
|
|
486
|
+
child.on('close', (code) => {
|
|
487
|
+
if (code !== 0) reject(new Error(`pnpm approve-builds exited with code ${code}`));
|
|
488
|
+
else resolve();
|
|
402
489
|
});
|
|
490
|
+
child.on('error', reject);
|
|
403
491
|
});
|
|
492
|
+
|
|
493
|
+
// After approval, rebuild the approved packages so their native binaries are present.
|
|
494
|
+
await new Promise<void>((resolve, reject) => {
|
|
495
|
+
const child = spawn('pnpm', ['rebuild', ...ignoredBuilds], {
|
|
496
|
+
cwd: projectDir,
|
|
497
|
+
stdio: 'inherit',
|
|
498
|
+
shell: true,
|
|
499
|
+
});
|
|
500
|
+
child.on('close', (code) => {
|
|
501
|
+
if (code !== 0) reject(new Error(`pnpm rebuild exited with code ${code}`));
|
|
502
|
+
else resolve();
|
|
503
|
+
});
|
|
504
|
+
child.on('error', reject);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
console.log('\n✅ Build scripts approved and packages rebuilt\n');
|
|
404
508
|
}
|
|
405
509
|
|
|
406
510
|
export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
|
|
@@ -876,7 +980,7 @@ export async function register() {
|
|
|
876
980
|
createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
|
|
877
981
|
|
|
878
982
|
// 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
879
|
-
createAiAgentFiles(projectDir, projectName, backendLanguage);
|
|
983
|
+
createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
|
|
880
984
|
}
|
|
881
985
|
|
|
882
986
|
async function createSharedPackage(projectDir: string, projectName: string) {
|
|
@@ -1700,9 +1804,10 @@ This project was generated by SwallowKit. If you encounter any issues or have su
|
|
|
1700
1804
|
console.log('✅ README.md created\n');
|
|
1701
1805
|
}
|
|
1702
1806
|
|
|
1703
|
-
function createAiAgentFiles(projectDir: string, projectName: string, backendLanguage: BackendLanguage) {
|
|
1807
|
+
function createAiAgentFiles(projectDir: string, projectName: string, backendLanguage: BackendLanguage, pm: PackageManager) {
|
|
1704
1808
|
console.log('🤖 Creating AI agent instruction files...\n');
|
|
1705
1809
|
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1810
|
+
const runCmd = pm === 'pnpm' ? 'pnpm' : 'npx';
|
|
1706
1811
|
const projectMcpConfigSource = buildSwallowKitMcpProjectConfigSource();
|
|
1707
1812
|
const functionsStructureLine = backendLanguage === 'typescript'
|
|
1708
1813
|
? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
|
|
@@ -1764,11 +1869,12 @@ ${functionsStructureLine}
|
|
|
1764
1869
|
- 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.
|
|
1765
1870
|
- Prefer the \`swallowkit_*\` MCP tools for framework-owned inspection, validation, and generation when they are available.
|
|
1766
1871
|
- If MCP is unavailable in your runtime, fall back to the machine CLI:
|
|
1767
|
-
-
|
|
1768
|
-
-
|
|
1769
|
-
-
|
|
1872
|
+
- \`${runCmd} swallowkit machine inspect project\`
|
|
1873
|
+
- \`${runCmd} swallowkit machine validate project\`
|
|
1874
|
+
- \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`
|
|
1770
1875
|
- Do not hand-edit framework-owned artifacts when the MCP or machine interface can generate or validate them for you.
|
|
1771
1876
|
- The local MCP bootstrap depends on project dependencies already being installed.
|
|
1877
|
+
- **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
|
|
1772
1878
|
|
|
1773
1879
|
## Critical Design Principles
|
|
1774
1880
|
|
|
@@ -1869,7 +1975,7 @@ app.http('{model}-get-all', {
|
|
|
1869
1975
|
|------|-----------|---------|
|
|
1870
1976
|
| Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
|
|
1871
1977
|
| Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
|
|
1872
|
-
| Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/
|
|
1978
|
+
| Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoCrudFunctions.cs' : 'functions/blueprints/todo.py'}\` |
|
|
1873
1979
|
| Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
|
|
1874
1980
|
| API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
|
|
1875
1981
|
| BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
|
|
@@ -1887,9 +1993,9 @@ Use the SwallowKit CLI — do **not** manually create model files or CRUD boiler
|
|
|
1887
1993
|
### Skill: Create a new data model
|
|
1888
1994
|
|
|
1889
1995
|
\`\`\`bash
|
|
1890
|
-
|
|
1996
|
+
${runCmd} swallowkit create-model <name>
|
|
1891
1997
|
# Multiple models at once:
|
|
1892
|
-
|
|
1998
|
+
${runCmd} swallowkit create-model user post comment
|
|
1893
1999
|
\`\`\`
|
|
1894
2000
|
|
|
1895
2001
|
Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
|
|
@@ -1898,7 +2004,7 @@ Edit the generated file to add your domain-specific fields, then run scaffold.
|
|
|
1898
2004
|
### Skill: Generate full CRUD from a model
|
|
1899
2005
|
|
|
1900
2006
|
\`\`\`bash
|
|
1901
|
-
|
|
2007
|
+
${runCmd} swallowkit scaffold shared/models/<name>.ts
|
|
1902
2008
|
\`\`\`
|
|
1903
2009
|
|
|
1904
2010
|
Generates:
|
|
@@ -1910,7 +2016,7 @@ Generates:
|
|
|
1910
2016
|
### Skill: Start development servers
|
|
1911
2017
|
|
|
1912
2018
|
\`\`\`bash
|
|
1913
|
-
|
|
2019
|
+
${runCmd} swallowkit dev
|
|
1914
2020
|
\`\`\`
|
|
1915
2021
|
|
|
1916
2022
|
Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
|
|
@@ -1919,17 +2025,18 @@ Checks for Cosmos DB Emulator availability.
|
|
|
1919
2025
|
### Skill: Provision Azure resources
|
|
1920
2026
|
|
|
1921
2027
|
\`\`\`bash
|
|
1922
|
-
|
|
2028
|
+
${runCmd} swallowkit provision --resource-group <name> --location <region>
|
|
1923
2029
|
\`\`\`
|
|
1924
2030
|
|
|
1925
2031
|
Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
|
|
1926
2032
|
|
|
1927
2033
|
### Typical workflow for "add a new feature/model"
|
|
1928
2034
|
|
|
1929
|
-
1.
|
|
2035
|
+
1. \`${runCmd} swallowkit create-model <name>\`
|
|
1930
2036
|
2. Edit \`shared/models/<name>.ts\` — add fields
|
|
1931
|
-
3.
|
|
1932
|
-
4.
|
|
2037
|
+
3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
|
|
2038
|
+
4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
2039
|
+
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.
|
|
1933
2040
|
|
|
1934
2041
|
## Do NOT
|
|
1935
2042
|
|
|
@@ -1974,23 +2081,25 @@ This file is for Claude Code. Read AGENTS.md in the project root for the full ar
|
|
|
1974
2081
|
|
|
1975
2082
|
- This repository includes a project-scoped \`.mcp.json\` that registers the locally installed SwallowKit MCP server for runtimes that support project MCP files.
|
|
1976
2083
|
- When the \`swallowkit_*\` tools are available, prefer them for inspect / validate / generate tasks.
|
|
1977
|
-
- If MCP is unavailable, use
|
|
2084
|
+
- If MCP is unavailable, use \`${runCmd} swallowkit machine ...\` instead.
|
|
2085
|
+
- **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
|
|
1978
2086
|
|
|
1979
2087
|
## SwallowKit CLI Commands
|
|
1980
2088
|
|
|
1981
2089
|
| Task | Command |
|
|
1982
2090
|
|------|---------|
|
|
1983
|
-
| Create model |
|
|
1984
|
-
| Generate CRUD |
|
|
1985
|
-
| Dev servers |
|
|
1986
|
-
| Provision Azure |
|
|
2091
|
+
| Create model | \`${runCmd} swallowkit create-model <name>\` |
|
|
2092
|
+
| Generate CRUD | \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` |
|
|
2093
|
+
| Dev servers | \`${runCmd} swallowkit dev\` |
|
|
2094
|
+
| Provision Azure | \`${runCmd} swallowkit provision --resource-group <rg> --location <region>\` |
|
|
1987
2095
|
|
|
1988
2096
|
## Workflow: Add a new model
|
|
1989
2097
|
|
|
1990
|
-
1.
|
|
2098
|
+
1. \`${runCmd} swallowkit create-model <name>\`
|
|
1991
2099
|
2. Edit \`shared/models/<name>.ts\` — add your fields
|
|
1992
|
-
3.
|
|
1993
|
-
4.
|
|
2100
|
+
3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
|
|
2101
|
+
4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
|
|
2102
|
+
5. If \`dev-seeds/\` exists, update the seed JSON files to include data for the new model and adjust existing seeds if relationships changed.
|
|
1994
2103
|
`;
|
|
1995
2104
|
|
|
1996
2105
|
fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
|
|
@@ -2019,13 +2128,15 @@ Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Fu
|
|
|
2019
2128
|
1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
|
|
2020
2129
|
2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
|
|
2021
2130
|
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/\`.
|
|
2022
|
-
4. **Use the CLI** — Run
|
|
2131
|
+
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.
|
|
2132
|
+
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.
|
|
2023
2133
|
|
|
2024
2134
|
## SwallowKit Framework Operations
|
|
2025
2135
|
|
|
2026
2136
|
- Prefer the SwallowKit MCP or machine interface for framework-owned inspection, validation, and generation instead of hand-editing generated files.
|
|
2027
2137
|
- If your runtime loads project-scoped MCP config from \`.mcp.json\`, use the \`swallowkit_*\` tools.
|
|
2028
|
-
- Otherwise use
|
|
2138
|
+
- Otherwise use \`${runCmd} swallowkit machine inspect project\`, \`${runCmd} swallowkit machine validate project\`, and \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`.
|
|
2139
|
+
- **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
|
|
2029
2140
|
|
|
2030
2141
|
## Naming
|
|
2031
2142
|
|
|
@@ -2067,7 +2178,7 @@ Files in this directory are the **single source of truth** for data models acros
|
|
|
2067
2178
|
- Export a \`displayName\` string constant for UI display.
|
|
2068
2179
|
- Re-export every model from \`shared/index.ts\`.
|
|
2069
2180
|
- For relationships, use **nested schemas** (import and embed the related schema), not ID references.
|
|
2070
|
-
- After editing a model, run
|
|
2181
|
+
- After editing a model, run \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
|
|
2071
2182
|
`;
|
|
2072
2183
|
|
|
2073
2184
|
fs.writeFileSync(
|
|
@@ -120,6 +120,10 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
120
120
|
? [modelInfo]
|
|
121
121
|
: await collectModelGraph(modelPath);
|
|
122
122
|
|
|
123
|
+
if (options.apiOnly) {
|
|
124
|
+
console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
// 5. Generate BFF callFunction helper
|
|
124
128
|
await generateCallFunctionHelper();
|
|
125
129
|
|
|
@@ -351,7 +355,7 @@ async function generateFunctionsCode(
|
|
|
351
355
|
|
|
352
356
|
if (backendLanguage === "csharp") {
|
|
353
357
|
const crudDir = path.join(process.cwd(), functionsDir, "Crud");
|
|
354
|
-
const functionFilePath = path.join(crudDir, `${modelInfo.name}
|
|
358
|
+
const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
|
|
355
359
|
fs.mkdirSync(crudDir, { recursive: true });
|
|
356
360
|
|
|
357
361
|
// Remove init-generated template (singular) to avoid route conflicts
|
package/src/cli/index.ts
CHANGED
|
@@ -142,7 +142,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
142
142
|
.description("Generate CRUD code for Azure Functions and Next.js BFF from Zod models")
|
|
143
143
|
.option("--functions-dir <dir>", "Azure Functions directory", "functions")
|
|
144
144
|
.option("--api-dir <dir>", "Next.js API routes directory", "app/api")
|
|
145
|
-
.option("--api-only", "
|
|
145
|
+
.option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
|
|
146
146
|
.action((model, options) => {
|
|
147
147
|
scaffoldCommand({
|
|
148
148
|
model,
|
|
@@ -198,7 +198,7 @@ function buildEntityRoutes(
|
|
|
198
198
|
: backendLanguage === "csharp"
|
|
199
199
|
? isConnectorEntity
|
|
200
200
|
? `functions/Connectors/${entity.name}ConnectorFunctions.cs`
|
|
201
|
-
: `functions/Crud/${entity.name}
|
|
201
|
+
: `functions/Crud/${entity.name}CrudFunctions.cs`
|
|
202
202
|
: `functions/blueprints/${modelKebab.replace(/-/g, "_")}.py`;
|
|
203
203
|
|
|
204
204
|
const operations = entity.connectorConfig?.operations ?? ["getAll", "getById", "create", "update", "delete"];
|
|
@@ -418,7 +418,7 @@ ${authCatchBlock} context.error(\`Error deleting item from \${containerName
|
|
|
418
418
|
export function generateCSharpAzureFunctionsCRUD(model: ModelInfo, authPolicy?: ModelAuthPolicy): string {
|
|
419
419
|
const modelName = model.name;
|
|
420
420
|
const modelCamel = toCamelCase(modelName);
|
|
421
|
-
const className = `${modelName}
|
|
421
|
+
const className = `${modelName}CrudFunctions`;
|
|
422
422
|
const containerName = modelName.endsWith('s') ? modelName : `${modelName}s`;
|
|
423
423
|
|
|
424
424
|
const partitionKeyPath = model.partitionKey;
|
|
@@ -601,6 +601,31 @@ function buildGeneratedPythonModelsInitSource(models: ModelInfo[]): string {
|
|
|
601
601
|
return models.map((model) => `from .${toSnakeCase(model.name)} import ${model.name}`).join("\n") + "\n";
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
function mergePythonInitSource(existingSource: string | undefined, generatedSource: string): string {
|
|
605
|
+
if (!existingSource) {
|
|
606
|
+
return generatedSource;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const existingLines = existingSource.split(/\r?\n/);
|
|
610
|
+
const lineSet = new Set(existingLines);
|
|
611
|
+
const missingLines = generatedSource
|
|
612
|
+
.trim()
|
|
613
|
+
.split(/\r?\n/)
|
|
614
|
+
.filter((line) => line.length > 0 && !lineSet.has(line));
|
|
615
|
+
|
|
616
|
+
if (missingLines.length === 0) {
|
|
617
|
+
return existingSource.endsWith("\n") ? existingSource : `${existingSource}\n`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const normalizedExisting = existingSource.endsWith("\n") ? existingSource : `${existingSource}\n`;
|
|
621
|
+
return `${normalizedExisting}${missingLines.join("\n")}\n`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function writeMergedPythonInitFile(filePath: string, generatedSource: string): void {
|
|
625
|
+
const existingSource = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined;
|
|
626
|
+
fs.writeFileSync(filePath, mergePythonInitSource(existingSource, generatedSource), "utf-8");
|
|
627
|
+
}
|
|
628
|
+
|
|
604
629
|
function ensureCSharpCodegenProjectFiles(functionsRoot: string): void {
|
|
605
630
|
const toolManifestPath = path.join(functionsRoot, ".config", "dotnet-tools.json");
|
|
606
631
|
fs.mkdirSync(path.dirname(toolManifestPath), { recursive: true });
|
|
@@ -752,8 +777,8 @@ async function generatePythonSchemaArtifacts(
|
|
|
752
777
|
const packageRoot = path.join(outputDir, "backend_models");
|
|
753
778
|
const modelsRoot = path.join(packageRoot, "models");
|
|
754
779
|
fs.mkdirSync(modelsRoot, { recursive: true });
|
|
755
|
-
|
|
756
|
-
|
|
780
|
+
writeMergedPythonInitFile(path.join(packageRoot, "__init__.py"), buildGeneratedPythonPackageInitSource(models));
|
|
781
|
+
writeMergedPythonInitFile(path.join(modelsRoot, "__init__.py"), buildGeneratedPythonModelsInitSource(models));
|
|
757
782
|
|
|
758
783
|
for (const model of models) {
|
|
759
784
|
const modelPath = getPythonSchemaModelPath(outputDir, model.name);
|
|
@@ -785,7 +810,6 @@ export async function generateLanguageSchemaArtifacts(
|
|
|
785
810
|
backendLanguage === "csharp" ? "csharp-models" : "python-models"
|
|
786
811
|
);
|
|
787
812
|
|
|
788
|
-
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
789
813
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
790
814
|
|
|
791
815
|
if (backendLanguage === "csharp") {
|
package/src/machine/index.ts
CHANGED
|
@@ -134,7 +134,7 @@ function createMachineProgram(): Command {
|
|
|
134
134
|
.argument("<model>", "Model file or model name")
|
|
135
135
|
.option("--functions-dir <dir>", "Functions directory", "functions")
|
|
136
136
|
.option("--api-dir <dir>", "API routes directory", "app/api")
|
|
137
|
-
.option("--api-only", "
|
|
137
|
+
.option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
|
|
138
138
|
.action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
|
|
139
139
|
await handleMachineAction("generate-scaffold", async () => runMachineScaffoldOperation({
|
|
140
140
|
model,
|