swallowkit 1.0.0-beta.3 → 1.0.0-beta.31
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 +353 -215
- package/README.md +406 -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 +444 -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 +15 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2696 -1706
- 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 +448 -129
- 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 -213
- 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 +677 -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 +184 -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 +109 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +215 -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 +85 -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 +115 -0
- package/src/__tests__/machine.test.ts +251 -0
- package/src/__tests__/mcp.test.ts +117 -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 +189 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +67 -0
- package/src/__tests__/string-utils.test.ts +75 -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 +500 -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 +3480 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1001 -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 +798 -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 +185 -0
- package/src/types/index.ts +134 -0
- package/src/utils/package-manager.ts +229 -0
- package/src/utils/python-uv.ts +96 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { buildSwallowKitToolDefinitions } from "../mcp";
|
|
5
|
+
|
|
6
|
+
describe("SwallowKit MCP tool definitions", () => {
|
|
7
|
+
it("delegates inspect_project to the machine CLI", async () => {
|
|
8
|
+
const runner = jest.fn().mockResolvedValue({
|
|
9
|
+
stdout: JSON.stringify({
|
|
10
|
+
ok: true,
|
|
11
|
+
command: "inspect-project",
|
|
12
|
+
data: { manifestSource: "file" },
|
|
13
|
+
}),
|
|
14
|
+
stderr: "",
|
|
15
|
+
exitCode: 0,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const tool = buildSwallowKitToolDefinitions(runner).find((candidate) => candidate.name === "swallowkit_inspect_project");
|
|
19
|
+
expect(tool).toBeDefined();
|
|
20
|
+
|
|
21
|
+
const result = await tool!.handler({});
|
|
22
|
+
expect(runner).toHaveBeenCalledWith(["inspect", "project"]);
|
|
23
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ manifestSource: "file" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("delegates scaffold_model with explicit args", async () => {
|
|
27
|
+
const runner = jest.fn().mockResolvedValue({
|
|
28
|
+
stdout: JSON.stringify({
|
|
29
|
+
ok: true,
|
|
30
|
+
command: "generate-scaffold",
|
|
31
|
+
data: { createdFiles: ["functions/src/todo.ts"] },
|
|
32
|
+
}),
|
|
33
|
+
stderr: "",
|
|
34
|
+
exitCode: 0,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const tool = buildSwallowKitToolDefinitions(runner).find((candidate) => candidate.name === "swallowkit_scaffold_model");
|
|
38
|
+
expect(tool).toBeDefined();
|
|
39
|
+
|
|
40
|
+
const result = await tool!.handler({
|
|
41
|
+
model: "todo",
|
|
42
|
+
functionsDir: "functions",
|
|
43
|
+
apiDir: "app/api",
|
|
44
|
+
apiOnly: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(runner).toHaveBeenCalledWith([
|
|
48
|
+
"generate",
|
|
49
|
+
"scaffold",
|
|
50
|
+
"todo",
|
|
51
|
+
"--functions-dir",
|
|
52
|
+
"functions",
|
|
53
|
+
"--api-dir",
|
|
54
|
+
"app/api",
|
|
55
|
+
"--api-only",
|
|
56
|
+
]);
|
|
57
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ createdFiles: ["functions/src/todo.ts"] });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps the built MCP entrypoint alive long enough to complete the handshake", async () => {
|
|
61
|
+
const entrypoint = path.resolve(__dirname, "..", "..", "dist", "mcp", "index.js");
|
|
62
|
+
expect(fs.existsSync(entrypoint)).toBe(true);
|
|
63
|
+
|
|
64
|
+
await new Promise<void>((resolve, reject) => {
|
|
65
|
+
const child = spawn(process.execPath, [entrypoint], {
|
|
66
|
+
cwd: path.resolve(__dirname, "..", ".."),
|
|
67
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
let settled = false;
|
|
70
|
+
let expectedShutdown = false;
|
|
71
|
+
let stderr = "";
|
|
72
|
+
|
|
73
|
+
const finishResolve = () => {
|
|
74
|
+
if (!settled) {
|
|
75
|
+
settled = true;
|
|
76
|
+
resolve();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const finishReject = (error: Error) => {
|
|
81
|
+
if (!settled) {
|
|
82
|
+
settled = true;
|
|
83
|
+
reject(error);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
expectedShutdown = true;
|
|
89
|
+
child.kill();
|
|
90
|
+
}, 1000);
|
|
91
|
+
|
|
92
|
+
child.stderr.on("data", (chunk) => {
|
|
93
|
+
stderr += chunk.toString();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.on("error", (error) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
finishReject(error);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
child.on("close", (code, signal) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
|
|
104
|
+
if (expectedShutdown) {
|
|
105
|
+
finishResolve();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
finishReject(
|
|
110
|
+
new Error(
|
|
111
|
+
`Built MCP entrypoint exited early with code ${code ?? "null"} and signal ${signal ?? "null"}${stderr ? `: ${stderr}` : ""}`
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { parseModelFile } from "../core/scaffold/model-parser";
|
|
4
|
+
|
|
5
|
+
describe("parseModelFile", () => {
|
|
6
|
+
it("preserves array and enum metadata for schemas with defaults", async () => {
|
|
7
|
+
const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
|
|
8
|
+
const modelPath = path.join(tempDir, "product.ts");
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
fs.writeFileSync(
|
|
12
|
+
modelPath,
|
|
13
|
+
`import { z } from 'zod/v4';
|
|
14
|
+
|
|
15
|
+
export const Product = z.object({
|
|
16
|
+
id: z.string(),
|
|
17
|
+
name: z.string().min(1),
|
|
18
|
+
price: z.number().min(0),
|
|
19
|
+
tags: z.array(z.string()).default([]),
|
|
20
|
+
status: z.enum(['draft', 'active', 'archived']).default('draft'),
|
|
21
|
+
createdAt: z.string().optional(),
|
|
22
|
+
updatedAt: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type Product = z.infer<typeof Product>;
|
|
26
|
+
`,
|
|
27
|
+
"utf-8"
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const model = await parseModelFile(modelPath);
|
|
31
|
+
expect(model.fields).toEqual(
|
|
32
|
+
expect.arrayContaining([
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
name: "tags",
|
|
35
|
+
type: "string",
|
|
36
|
+
isArray: true,
|
|
37
|
+
isOptional: true,
|
|
38
|
+
}),
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
name: "status",
|
|
41
|
+
type: "string",
|
|
42
|
+
isArray: false,
|
|
43
|
+
isOptional: true,
|
|
44
|
+
enumValues: ["draft", "active", "archived"],
|
|
45
|
+
}),
|
|
46
|
+
])
|
|
47
|
+
);
|
|
48
|
+
} finally {
|
|
49
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateBFFCallFunction,
|
|
3
|
+
generateCompactBFFRoutes,
|
|
4
|
+
} from "../core/scaffold/nextjs-generator";
|
|
5
|
+
import { createBasicModelInfo } from "./fixtures";
|
|
6
|
+
|
|
7
|
+
describe("generateBFFCallFunction", () => {
|
|
8
|
+
it("generates call function helper code", () => {
|
|
9
|
+
const code = generateBFFCallFunction();
|
|
10
|
+
expect(code).toMatchSnapshot();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("includes getFunctionsBaseUrl helper", () => {
|
|
14
|
+
const code = generateBFFCallFunction();
|
|
15
|
+
expect(code).toContain("getFunctionsBaseUrl");
|
|
16
|
+
expect(code).toContain("BACKEND_FUNCTIONS_BASE_URL");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("includes callFunction export", () => {
|
|
20
|
+
const code = generateBFFCallFunction();
|
|
21
|
+
expect(code).toContain("export async function callFunction");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("includes z.ZodSchema typings", () => {
|
|
25
|
+
const code = generateBFFCallFunction();
|
|
26
|
+
expect(code).toContain("z.ZodSchema");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("generateCompactBFFRoutes", () => {
|
|
31
|
+
it("generates list and detail routes", () => {
|
|
32
|
+
const model = createBasicModelInfo();
|
|
33
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
34
|
+
|
|
35
|
+
expect(routes.listRoute).toMatchSnapshot();
|
|
36
|
+
expect(routes.detailRoute).toMatchSnapshot();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("imports the correct schema", () => {
|
|
40
|
+
const model = createBasicModelInfo();
|
|
41
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
42
|
+
|
|
43
|
+
expect(routes.listRoute).toContain(
|
|
44
|
+
"import { todoSchema } from '@myapp/shared'"
|
|
45
|
+
);
|
|
46
|
+
expect(routes.detailRoute).toContain(
|
|
47
|
+
"import { todoSchema } from '@myapp/shared'"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses correct API paths (camelCase)", () => {
|
|
52
|
+
const model = createBasicModelInfo();
|
|
53
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
54
|
+
|
|
55
|
+
expect(routes.listRoute).toContain("path: '/api/todo'");
|
|
56
|
+
expect(routes.detailRoute).toContain("/api/todo/");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("list route has GET and POST handlers", () => {
|
|
60
|
+
const model = createBasicModelInfo();
|
|
61
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
62
|
+
|
|
63
|
+
expect(routes.listRoute).toContain("export async function GET()");
|
|
64
|
+
expect(routes.listRoute).toContain("export async function POST(");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("detail route has GET, PUT, and DELETE handlers", () => {
|
|
68
|
+
const model = createBasicModelInfo();
|
|
69
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
70
|
+
|
|
71
|
+
expect(routes.detailRoute).toContain("export async function GET(");
|
|
72
|
+
expect(routes.detailRoute).toContain("export async function PUT(");
|
|
73
|
+
expect(routes.detailRoute).toContain("export async function DELETE(");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("creates InputSchema that omits managed fields", () => {
|
|
77
|
+
const model = createBasicModelInfo();
|
|
78
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
79
|
+
|
|
80
|
+
expect(routes.listRoute).toContain(
|
|
81
|
+
"todoSchema.omit({ id: true, createdAt: true, updatedAt: true })"
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("handles multi-word model names", () => {
|
|
86
|
+
const model = createBasicModelInfo({
|
|
87
|
+
name: "BlogPost",
|
|
88
|
+
schemaName: "blogPostSchema",
|
|
89
|
+
});
|
|
90
|
+
const routes = generateCompactBFFRoutes(model, "@myapp/shared");
|
|
91
|
+
|
|
92
|
+
expect(routes.listRoute).toContain("path: '/api/blogPost'");
|
|
93
|
+
expect(routes.listRoute).toContain(
|
|
94
|
+
"import { blogPostSchema } from '@myapp/shared'"
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
|
|
2
|
+
import { createBasicModelInfo } from "./fixtures";
|
|
3
|
+
|
|
4
|
+
describe("generateOpenApiDocument", () => {
|
|
5
|
+
it("emits an OpenAPI document for the root model", () => {
|
|
6
|
+
const model = createBasicModelInfo();
|
|
7
|
+
const document = JSON.parse(generateOpenApiDocument([model], model));
|
|
8
|
+
|
|
9
|
+
expect(document.openapi).toBe("3.0.3");
|
|
10
|
+
expect(document.components.schemas.Todo.properties.title.type).toBe("string");
|
|
11
|
+
expect(document.paths["/api/todo"].post.requestBody.content["application/json"].schema.$ref)
|
|
12
|
+
.toBe("#/components/schemas/Todo");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("emits nested schema references when present", () => {
|
|
16
|
+
const todo = createBasicModelInfo({
|
|
17
|
+
fields: [
|
|
18
|
+
{ name: "id", type: "string", isOptional: false, isArray: false },
|
|
19
|
+
{ name: "title", type: "string", isOptional: false, isArray: false },
|
|
20
|
+
{
|
|
21
|
+
name: "category",
|
|
22
|
+
type: "object",
|
|
23
|
+
isOptional: false,
|
|
24
|
+
isArray: false,
|
|
25
|
+
isNestedSchema: true,
|
|
26
|
+
nestedModelName: "Category",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
const category = createBasicModelInfo({
|
|
31
|
+
name: "Category",
|
|
32
|
+
schemaName: "categorySchema",
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: "id", type: "string", isOptional: false, isArray: false },
|
|
35
|
+
{ name: "name", type: "string", isOptional: false, isArray: false },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const document = JSON.parse(generateOpenApiDocument([todo, category], todo));
|
|
40
|
+
expect(document.components.schemas.Todo.properties.category.$ref).toBe("#/components/schemas/Category");
|
|
41
|
+
expect(document.components.schemas.Category.properties.name.type).toBe("string");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCommands,
|
|
3
|
+
getWorkspaceConfig,
|
|
4
|
+
getCiSetupStep,
|
|
5
|
+
getAzurePipelinesSetup,
|
|
6
|
+
getBuildScript,
|
|
7
|
+
getFunctionsPrestart,
|
|
8
|
+
getFunctionsStartScript,
|
|
9
|
+
spawnArgs,
|
|
10
|
+
} from "../utils/package-manager";
|
|
11
|
+
|
|
12
|
+
describe("getCommands", () => {
|
|
13
|
+
describe("pnpm", () => {
|
|
14
|
+
const cmds = getCommands("pnpm");
|
|
15
|
+
|
|
16
|
+
it("returns correct name", () => {
|
|
17
|
+
expect(cmds.name).toBe("pnpm");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns correct install command", () => {
|
|
21
|
+
expect(cmds.install).toBe("pnpm install");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns correct ci command", () => {
|
|
25
|
+
expect(cmds.ci).toBe("pnpm install --frozen-lockfile");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns correct add command", () => {
|
|
29
|
+
expect(cmds.add).toBe("pnpm add");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns correct addDev command", () => {
|
|
33
|
+
expect(cmds.addDev).toBe("pnpm add -D");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns correct exec command", () => {
|
|
37
|
+
expect(cmds.exec).toBe("pnpm exec");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns correct dlx command", () => {
|
|
41
|
+
expect(cmds.dlx).toBe("pnpm dlx");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns correct runFilter for workspace", () => {
|
|
45
|
+
expect(cmds.runFilter("shared")).toBe("pnpm run --filter shared");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns --use-pnpm flag for create-next-app", () => {
|
|
49
|
+
expect(cmds.createNextAppFlag).toBe("--use-pnpm");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("npm", () => {
|
|
54
|
+
const cmds = getCommands("npm");
|
|
55
|
+
|
|
56
|
+
it("returns correct name", () => {
|
|
57
|
+
expect(cmds.name).toBe("npm");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns correct install command", () => {
|
|
61
|
+
expect(cmds.install).toBe("npm install");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns correct ci command", () => {
|
|
65
|
+
expect(cmds.ci).toBe("npm ci");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns correct add command", () => {
|
|
69
|
+
expect(cmds.add).toBe("npm install");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns correct addDev command", () => {
|
|
73
|
+
expect(cmds.addDev).toBe("npm install -D");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns correct exec command", () => {
|
|
77
|
+
expect(cmds.exec).toBe("npx");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns correct dlx command", () => {
|
|
81
|
+
expect(cmds.dlx).toBe("npx");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns correct runFilter for workspace", () => {
|
|
85
|
+
expect(cmds.runFilter("shared")).toBe("npm run --workspace=shared");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns null for create-next-app flag", () => {
|
|
89
|
+
expect(cmds.createNextAppFlag).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("getWorkspaceConfig", () => {
|
|
95
|
+
it("returns pnpm-workspace.yaml config for pnpm", () => {
|
|
96
|
+
const config = getWorkspaceConfig("pnpm", ["packages/*", "apps/*"]);
|
|
97
|
+
expect(config.type).toBe("file");
|
|
98
|
+
if (config.type === "file") {
|
|
99
|
+
expect(config.filename).toBe("pnpm-workspace.yaml");
|
|
100
|
+
expect(config.content).toContain("packages:");
|
|
101
|
+
expect(config.content).toContain(" - packages/*");
|
|
102
|
+
expect(config.content).toContain(" - apps/*");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns packageJson config for npm", () => {
|
|
107
|
+
const config = getWorkspaceConfig("npm", ["packages/*", "apps/*"]);
|
|
108
|
+
expect(config.type).toBe("packageJson");
|
|
109
|
+
if (config.type === "packageJson") {
|
|
110
|
+
expect(config.field).toBe("workspaces");
|
|
111
|
+
expect(config.value).toEqual(["packages/*", "apps/*"]);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("getCiSetupStep", () => {
|
|
117
|
+
it("returns pnpm setup step for pnpm", () => {
|
|
118
|
+
const step = getCiSetupStep("pnpm");
|
|
119
|
+
expect(step).toContain("pnpm/action-setup");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns empty string for npm", () => {
|
|
123
|
+
expect(getCiSetupStep("npm")).toBe("");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("getAzurePipelinesSetup", () => {
|
|
128
|
+
it("returns corepack step for pnpm", () => {
|
|
129
|
+
const step = getAzurePipelinesSetup("pnpm");
|
|
130
|
+
expect(step).toContain("corepack enable");
|
|
131
|
+
expect(step).toContain("pnpm");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns empty string for npm", () => {
|
|
135
|
+
expect(getAzurePipelinesSetup("npm")).toBe("");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("getBuildScript", () => {
|
|
140
|
+
it("uses pnpm run --filter for pnpm", () => {
|
|
141
|
+
expect(getBuildScript("pnpm")).toContain("pnpm run --filter shared build");
|
|
142
|
+
expect(getBuildScript("pnpm")).toContain("fs.cpSync");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("uses npm run --workspace for npm", () => {
|
|
146
|
+
expect(getBuildScript("npm")).toContain("npm run --workspace=shared build");
|
|
147
|
+
expect(getBuildScript("npm")).toContain("fs.cpSync");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("getFunctionsPrestart", () => {
|
|
152
|
+
it("uses pnpm run build for pnpm", () => {
|
|
153
|
+
expect(getFunctionsPrestart("pnpm")).toBe("pnpm run build");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("uses npm run build for npm", () => {
|
|
157
|
+
expect(getFunctionsPrestart("npm")).toBe("npm run build");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("getFunctionsStartScript", () => {
|
|
162
|
+
it("uses pnpm start for pnpm", () => {
|
|
163
|
+
expect(getFunctionsStartScript("pnpm")).toContain("pnpm start");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("uses npm start for npm", () => {
|
|
167
|
+
expect(getFunctionsStartScript("npm")).toContain("npm start");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("uses func start for csharp backends", () => {
|
|
171
|
+
expect(getFunctionsStartScript("pnpm", "csharp")).toBe("cd functions && func start");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("uses func start for python backends", () => {
|
|
175
|
+
expect(getFunctionsStartScript("npm", "python")).toBe("cd functions && func start");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("spawnArgs", () => {
|
|
180
|
+
it("returns cmd and args for pnpm", () => {
|
|
181
|
+
const result = spawnArgs("pnpm", ["add", "next@latest"]);
|
|
182
|
+
expect(result).toEqual({ cmd: "pnpm", args: ["add", "next@latest"] });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns cmd and args for npm", () => {
|
|
186
|
+
const result = spawnArgs("npm", ["install", "next@latest"]);
|
|
187
|
+
expect(result).toEqual({ cmd: "npm", args: ["install", "next@latest"] });
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import {
|
|
3
|
+
buildProjectLocalUvEnv,
|
|
4
|
+
buildUvPipInstallArgs,
|
|
5
|
+
buildUvVenvArgs,
|
|
6
|
+
getProjectLocalUvPaths,
|
|
7
|
+
getPythonProjectRoot,
|
|
8
|
+
} from "../utils/python-uv";
|
|
9
|
+
|
|
10
|
+
describe("python uv helpers", () => {
|
|
11
|
+
it("keeps uv-managed state inside the project", () => {
|
|
12
|
+
const projectRoot = "C:\\repo";
|
|
13
|
+
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
14
|
+
|
|
15
|
+
expect(uvPaths.stateDir).toBe(path.join(projectRoot, ".uv"));
|
|
16
|
+
expect(uvPaths.cacheDir).toBe(path.join(projectRoot, ".uv", "cache"));
|
|
17
|
+
expect(uvPaths.pythonInstallDir).toBe(path.join(projectRoot, ".uv", "python"));
|
|
18
|
+
expect(uvPaths.localUvExecutable.endsWith(process.platform === "win32" ? path.join(".uv", "bin", "uv.exe") : path.join(".uv", "bin", "uv"))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("derives the project root from the functions directory", () => {
|
|
22
|
+
expect(getPythonProjectRoot(path.join("C:\\repo", "functions"))).toBe("C:\\repo");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("builds project-local uv environment variables", () => {
|
|
26
|
+
const env = buildProjectLocalUvEnv({ PATH: "C:\\Windows\\System32" }, "C:\\repo");
|
|
27
|
+
|
|
28
|
+
expect(env.UV_CACHE_DIR).toBe(path.join("C:\\repo", ".uv", "cache"));
|
|
29
|
+
expect(env.UV_PYTHON_INSTALL_DIR).toBe(path.join("C:\\repo", ".uv", "python"));
|
|
30
|
+
expect(env.UV_TOOL_DIR).toBe(path.join("C:\\repo", ".uv", "tools"));
|
|
31
|
+
expect(env.UV_TOOL_BIN_DIR).toBe(path.join("C:\\repo", ".uv", "tools", "bin"));
|
|
32
|
+
expect(env.UV_MANAGED_PYTHON).toBeUndefined();
|
|
33
|
+
expect(env.UV_PYTHON_PREFERENCE).toBe("only-managed");
|
|
34
|
+
expect(env.UV_PYTHON_NO_REGISTRY).toBe(process.platform === "win32" ? "true" : undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("builds uv commands for venv creation and requirements installation", () => {
|
|
38
|
+
expect(buildUvVenvArgs(".venv")).toEqual(["venv", ".venv", "--python", "3.11"]);
|
|
39
|
+
expect(buildUvPipInstallArgs(path.join("C:\\repo", "functions", ".venv", "Scripts", "python.exe"), "requirements.txt")).toEqual([
|
|
40
|
+
"pip",
|
|
41
|
+
"install",
|
|
42
|
+
"--python",
|
|
43
|
+
path.join("C:\\repo", "functions", ".venv", "Scripts", "python.exe"),
|
|
44
|
+
"-r",
|
|
45
|
+
"requirements.txt",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import {
|
|
3
|
+
NSWAG_CONSOLECORE_VERSION,
|
|
4
|
+
buildCSharpCodegenToolManifestSource,
|
|
5
|
+
buildPythonCodegenRequirementsSource,
|
|
6
|
+
getCSharpNativeGeneratorArgs,
|
|
7
|
+
getCSharpSchemaModelPath,
|
|
8
|
+
getCSharpSchemaOptionPath,
|
|
9
|
+
getPythonNativeGeneratorArgs,
|
|
10
|
+
getPythonSchemaModelPath,
|
|
11
|
+
} from "../core/scaffold/native-schema-generator";
|
|
12
|
+
|
|
13
|
+
describe("native schema generators", () => {
|
|
14
|
+
it("writes a dotnet tool manifest for NSwag", () => {
|
|
15
|
+
const manifest = JSON.parse(buildCSharpCodegenToolManifestSource());
|
|
16
|
+
|
|
17
|
+
expect(manifest.version).toBe(1);
|
|
18
|
+
expect(manifest.tools["nswag.consolecore"].version).toBe(NSWAG_CONSOLECORE_VERSION);
|
|
19
|
+
expect(manifest.tools["nswag.consolecore"].commands).toEqual(["nswag"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("pins python schema generation requirements without Java", () => {
|
|
23
|
+
expect(buildPythonCodegenRequirementsSource()).toContain("datamodel-code-generator");
|
|
24
|
+
expect(buildPythonCodegenRequirementsSource()).not.toContain("openapi-generator");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("builds NSwag arguments for model-only C# generation", () => {
|
|
28
|
+
const outputPath = path.join("C:\\temp\\generated\\csharp-models", ".native-temp", "Contracts.cs");
|
|
29
|
+
const args = getCSharpNativeGeneratorArgs("C:\\temp\\todo.openapi.json", outputPath);
|
|
30
|
+
|
|
31
|
+
expect(args.slice(0, 4)).toEqual(["tool", "run", "nswag", "openapi2csclient"]);
|
|
32
|
+
expect(args).toContain("/GenerateClientClasses:false");
|
|
33
|
+
expect(args).toContain("/GenerateDtoTypes:true");
|
|
34
|
+
expect(args).toContain("/GenerateNullableReferenceTypes:true");
|
|
35
|
+
expect(args).toContain(`/output:${outputPath}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("builds datamodel-code-generator arguments for python assets", () => {
|
|
39
|
+
const outputPath = path.join("C:\\temp\\generated\\python-models", ".native-temp", "models.py");
|
|
40
|
+
const args = getPythonNativeGeneratorArgs("C:\\temp\\todo.openapi.json", outputPath);
|
|
41
|
+
|
|
42
|
+
expect(args).toEqual(
|
|
43
|
+
expect.arrayContaining([
|
|
44
|
+
"-m",
|
|
45
|
+
"datamodel_code_generator",
|
|
46
|
+
"--input-file-type",
|
|
47
|
+
"openapi",
|
|
48
|
+
"--output-model-type",
|
|
49
|
+
"pydantic_v2.BaseModel",
|
|
50
|
+
"--disable-timestamp",
|
|
51
|
+
outputPath,
|
|
52
|
+
])
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("keeps generated asset paths under functions/generated", () => {
|
|
57
|
+
expect(getCSharpSchemaModelPath("C:\\temp\\generated\\csharp-models", "Product")).toBe(
|
|
58
|
+
path.join("C:\\temp\\generated\\csharp-models", "src", "SwallowKitBackendModels", "Model", "Product.cs")
|
|
59
|
+
);
|
|
60
|
+
expect(getCSharpSchemaOptionPath("C:\\temp\\generated\\csharp-models")).toBe(
|
|
61
|
+
path.join("C:\\temp\\generated\\csharp-models", "src", "SwallowKitBackendModels", "Client", "Option.cs")
|
|
62
|
+
);
|
|
63
|
+
expect(getPythonSchemaModelPath("C:\\temp\\generated\\python-models", "Product")).toBe(
|
|
64
|
+
path.join("C:\\temp\\generated\\python-models", "backend_models", "models", "product.py")
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { toPascalCase, toCamelCase, toKebabCase } from "../core/scaffold/model-parser";
|
|
2
|
+
|
|
3
|
+
describe("toPascalCase", () => {
|
|
4
|
+
it("converts kebab-case to PascalCase", () => {
|
|
5
|
+
expect(toPascalCase("todo-item")).toBe("TodoItem");
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("converts snake_case to PascalCase", () => {
|
|
9
|
+
expect(toPascalCase("todo_item")).toBe("TodoItem");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("handles single word", () => {
|
|
13
|
+
expect(toPascalCase("todo")).toBe("Todo");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("handles already PascalCase (no separators)", () => {
|
|
17
|
+
expect(toPascalCase("Todo")).toBe("Todo");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("handles multiple segments", () => {
|
|
21
|
+
expect(toPascalCase("my-cool-model")).toBe("MyCoolModel");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("handles empty string", () => {
|
|
25
|
+
expect(toPascalCase("")).toBe("");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("toCamelCase", () => {
|
|
30
|
+
it("converts kebab-case to camelCase", () => {
|
|
31
|
+
expect(toCamelCase("todo-item")).toBe("todoItem");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("converts snake_case to camelCase", () => {
|
|
35
|
+
expect(toCamelCase("todo_item")).toBe("todoItem");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("handles single word", () => {
|
|
39
|
+
expect(toCamelCase("Todo")).toBe("todo");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("handles PascalCase input (lowercases first char)", () => {
|
|
43
|
+
expect(toCamelCase("TodoItem")).toBe("todoItem");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles empty string", () => {
|
|
47
|
+
expect(toCamelCase("")).toBe("");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("toKebabCase", () => {
|
|
52
|
+
it("converts PascalCase to kebab-case", () => {
|
|
53
|
+
expect(toKebabCase("TodoItem")).toBe("todo-item");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("converts camelCase to kebab-case", () => {
|
|
57
|
+
expect(toKebabCase("todoItem")).toBe("todo-item");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("handles single lowercase word", () => {
|
|
61
|
+
expect(toKebabCase("todo")).toBe("todo");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles multiple capitals", () => {
|
|
65
|
+
expect(toKebabCase("MyCoolModel")).toBe("my-cool-model");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("handles already kebab-case", () => {
|
|
69
|
+
expect(toKebabCase("todo-item")).toBe("todo-item");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles empty string", () => {
|
|
73
|
+
expect(toKebabCase("")).toBe("");
|
|
74
|
+
});
|
|
75
|
+
});
|