swallowkit 1.0.0-beta.6 → 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 (78) hide show
  1. package/README.ja.md +12 -6
  2. package/README.md +12 -6
  3. package/dist/cli/commands/dev.d.ts +8 -0
  4. package/dist/cli/commands/dev.d.ts.map +1 -1
  5. package/dist/cli/commands/dev.js +238 -30
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts +5 -0
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +723 -285
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/scaffold.d.ts +3 -0
  12. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  13. package/dist/cli/commands/scaffold.js +181 -17
  14. package/dist/cli/commands/scaffold.js.map +1 -1
  15. package/dist/cli/index.js +2 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/core/config.d.ts +2 -1
  18. package/dist/core/config.d.ts.map +1 -1
  19. package/dist/core/config.js +28 -0
  20. package/dist/core/config.js.map +1 -1
  21. package/dist/core/scaffold/functions-generator.d.ts +5 -0
  22. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  23. package/dist/core/scaffold/functions-generator.js +431 -0
  24. package/dist/core/scaffold/functions-generator.js.map +1 -1
  25. package/dist/core/scaffold/model-parser.d.ts +1 -1
  26. package/dist/core/scaffold/model-parser.js +1 -1
  27. package/dist/core/scaffold/nextjs-generator.js +1 -1
  28. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  29. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  30. package/dist/core/scaffold/openapi-generator.js +190 -0
  31. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  32. package/dist/database/base-model.d.ts +3 -3
  33. package/dist/database/base-model.js +3 -3
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/types/index.d.ts +4 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/utils/package-manager.d.ts +2 -1
  41. package/dist/utils/package-manager.d.ts.map +1 -1
  42. package/dist/utils/package-manager.js +10 -6
  43. package/dist/utils/package-manager.js.map +1 -1
  44. package/package.json +2 -1
  45. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
  46. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  47. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
  48. package/src/__tests__/config.test.ts +122 -0
  49. package/src/__tests__/dev.test.ts +42 -0
  50. package/src/__tests__/fixtures.ts +83 -0
  51. package/src/__tests__/functions-generator.test.ts +101 -0
  52. package/src/__tests__/init.test.ts +59 -0
  53. package/src/__tests__/nextjs-generator.test.ts +97 -0
  54. package/src/__tests__/openapi-generator.test.ts +43 -0
  55. package/src/__tests__/package-manager.test.ts +189 -0
  56. package/src/__tests__/scaffold.test.ts +39 -0
  57. package/src/__tests__/string-utils.test.ts +75 -0
  58. package/src/__tests__/ui-generator.test.ts +144 -0
  59. package/src/cli/commands/create-model.ts +141 -0
  60. package/src/cli/commands/dev.ts +794 -0
  61. package/src/cli/commands/index.ts +8 -0
  62. package/src/cli/commands/init.ts +3363 -0
  63. package/src/cli/commands/provision.ts +193 -0
  64. package/src/cli/commands/scaffold.ts +786 -0
  65. package/src/cli/index.ts +73 -0
  66. package/src/core/config.ts +244 -0
  67. package/src/core/scaffold/functions-generator.ts +674 -0
  68. package/src/core/scaffold/model-parser.ts +627 -0
  69. package/src/core/scaffold/nextjs-generator.ts +217 -0
  70. package/src/core/scaffold/openapi-generator.ts +212 -0
  71. package/src/core/scaffold/ui-generator.ts +945 -0
  72. package/src/database/base-model.ts +184 -0
  73. package/src/database/client.ts +140 -0
  74. package/src/database/repository.ts +104 -0
  75. package/src/database/runtime-check.ts +25 -0
  76. package/src/index.ts +27 -0
  77. package/src/types/index.ts +45 -0
  78. package/src/utils/package-manager.ts +229 -0
@@ -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
+ });
@@ -0,0 +1,144 @@
1
+ import {
2
+ generateListPage,
3
+ generateDetailPage,
4
+ generateFormComponent,
5
+ generateNewPage,
6
+ generateEditPage,
7
+ } from "../core/scaffold/ui-generator";
8
+ import {
9
+ createBasicModelInfo,
10
+ createModelInfoWithForeignKey,
11
+ createModelInfoWithEnum,
12
+ } from "./fixtures";
13
+
14
+ describe("generateListPage", () => {
15
+ it("generates list page for basic model (snapshot)", () => {
16
+ const model = createBasicModelInfo();
17
+ const code = generateListPage(model, "@myapp/shared");
18
+ expect(code).toMatchSnapshot();
19
+ });
20
+
21
+ it("contains 'use client' directive", () => {
22
+ const model = createBasicModelInfo();
23
+ const code = generateListPage(model, "@myapp/shared");
24
+ expect(code).toContain("'use client'");
25
+ });
26
+
27
+ it("imports schema from shared package", () => {
28
+ const model = createBasicModelInfo();
29
+ const code = generateListPage(model, "@myapp/shared");
30
+ expect(code).toContain("@myapp/shared");
31
+ });
32
+
33
+ it("fetches from correct API endpoint", () => {
34
+ const model = createBasicModelInfo();
35
+ const code = generateListPage(model, "@myapp/shared");
36
+ expect(code).toContain("/api/todo");
37
+ });
38
+
39
+ it("displays up to 3 non-id fields", () => {
40
+ const model = createBasicModelInfo();
41
+ const code = generateListPage(model, "@myapp/shared");
42
+ // title, description, completed — first 3 non-id fields
43
+ expect(code).toContain("title");
44
+ });
45
+
46
+ it("handles foreign key fields", () => {
47
+ const model = createModelInfoWithForeignKey();
48
+ const code = generateListPage(model, "@myapp/shared");
49
+ expect(code).toContain("categoryId");
50
+ });
51
+ });
52
+
53
+ describe("generateDetailPage", () => {
54
+ it("generates detail page (snapshot)", () => {
55
+ const model = createBasicModelInfo();
56
+ const code = generateDetailPage(model, "@myapp/shared");
57
+ expect(code).toMatchSnapshot();
58
+ });
59
+
60
+ it("contains 'use client' directive", () => {
61
+ const model = createBasicModelInfo();
62
+ const code = generateDetailPage(model, "@myapp/shared");
63
+ expect(code).toContain("'use client'");
64
+ });
65
+
66
+ it("includes delete button", () => {
67
+ const model = createBasicModelInfo();
68
+ const code = generateDetailPage(model, "@myapp/shared");
69
+ expect(code).toContain("DELETE");
70
+ });
71
+ });
72
+
73
+ describe("generateFormComponent", () => {
74
+ it("generates form component (snapshot)", () => {
75
+ const model = createBasicModelInfo();
76
+ const code = generateFormComponent(model, "@myapp/shared");
77
+ expect(code).toMatchSnapshot();
78
+ });
79
+
80
+ it("generates inputs for non-managed fields", () => {
81
+ const model = createBasicModelInfo();
82
+ const code = generateFormComponent(model, "@myapp/shared");
83
+ // title and description should have form inputs
84
+ expect(code).toContain("title");
85
+ expect(code).toContain("description");
86
+ });
87
+
88
+ it("generates enum select for enum fields", () => {
89
+ const model = createModelInfoWithEnum();
90
+ const code = generateFormComponent(model, "@myapp/shared");
91
+ expect(code).toContain("select");
92
+ expect(code).toContain("open");
93
+ expect(code).toContain("in_progress");
94
+ expect(code).toContain("closed");
95
+ });
96
+
97
+ it("generates checkbox for boolean fields", () => {
98
+ const model = createBasicModelInfo();
99
+ const code = generateFormComponent(model, "@myapp/shared");
100
+ expect(code).toContain("checkbox");
101
+ });
102
+ });
103
+
104
+ describe("generateNewPage", () => {
105
+ it("generates new page (snapshot)", () => {
106
+ const model = createBasicModelInfo();
107
+ const code = generateNewPage(model);
108
+ expect(code).toMatchSnapshot();
109
+ });
110
+
111
+ it("contains form component reference", () => {
112
+ const model = createBasicModelInfo();
113
+ const code = generateNewPage(model);
114
+ expect(code).toContain("Form");
115
+ });
116
+
117
+ it("references the form component", () => {
118
+ const model = createBasicModelInfo();
119
+ const code = generateNewPage(model);
120
+ expect(code).toContain("TodoForm");
121
+ expect(code).toContain("Create New Todo");
122
+ });
123
+ });
124
+
125
+ describe("generateEditPage", () => {
126
+ it("generates edit page (snapshot)", () => {
127
+ const model = createBasicModelInfo();
128
+ const code = generateEditPage(model, "@myapp/shared");
129
+ expect(code).toMatchSnapshot();
130
+ });
131
+
132
+ it("loads existing data and passes to form", () => {
133
+ const model = createBasicModelInfo();
134
+ const code = generateEditPage(model, "@myapp/shared");
135
+ expect(code).toContain("initialData");
136
+ expect(code).toContain("isEdit={true}");
137
+ });
138
+
139
+ it("fetches existing data for editing", () => {
140
+ const model = createBasicModelInfo();
141
+ const code = generateEditPage(model, "@myapp/shared");
142
+ expect(code).toContain("/api/todo/");
143
+ });
144
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * SwallowKit Create-Model コマンド
3
+ * Zod モデルの雛形を生成
4
+ */
5
+
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import * as readline from "readline";
9
+ import { toPascalCase } from "../../core/scaffold/model-parser";
10
+ import { ensureSwallowKitProject } from "../../core/config";
11
+ import { detectFromProject, getCommands } from "../../utils/package-manager";
12
+
13
+ interface CreateModelOptions {
14
+ names: string[]; // モデル名のリスト(例: ["todo", "user", "post"])
15
+ modelsDir?: string; // モデルディレクトリ(デフォルト: "shared/models")
16
+ }
17
+
18
+ /**
19
+ * モデルテンプレートを生成
20
+ */
21
+ function generateModelTemplate(modelName: string): string {
22
+ const pascalName = toPascalCase(modelName);
23
+
24
+ return `import { z } from 'zod/v4';
25
+
26
+ // ${pascalName} model (Zod official pattern: same name for value and type)
27
+ export const ${pascalName} = z.object({
28
+ id: z.string(),
29
+ name: z.string().min(1),
30
+ createdAt: z.string().optional(),
31
+ updatedAt: z.string().optional(),
32
+ });
33
+
34
+ export type ${pascalName} = z.infer<typeof ${pascalName}>;
35
+
36
+ // Display name for UI
37
+ export const displayName = '${pascalName}';
38
+ `;
39
+ }
40
+
41
+ /**
42
+ * ユーザーに確認を求める
43
+ */
44
+ function askConfirmation(question: string): Promise<boolean> {
45
+ const rl = readline.createInterface({
46
+ input: process.stdin,
47
+ output: process.stdout,
48
+ });
49
+
50
+ return new Promise((resolve) => {
51
+ rl.question(question, (answer) => {
52
+ rl.close();
53
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
54
+ });
55
+ });
56
+ }
57
+
58
+ /**
59
+ * create-model コマンド
60
+ */
61
+ export async function createModelCommand(options: CreateModelOptions) {
62
+ // SwallowKit プロジェクトディレクトリかどうかを検証
63
+ ensureSwallowKitProject("create-model");
64
+
65
+ console.log("🏗️ SwallowKit Create-Model: Generating model templates...\n");
66
+
67
+ const modelsDir = options.modelsDir || "shared/models";
68
+
69
+ // shared/models ディレクトリが存在しなければ作成
70
+ if (!fs.existsSync(modelsDir)) {
71
+ console.log(`📁 Creating directory: ${modelsDir}`);
72
+ fs.mkdirSync(modelsDir, { recursive: true });
73
+ }
74
+
75
+ const created: string[] = [];
76
+ const skipped: string[] = [];
77
+
78
+ for (const name of options.names) {
79
+ const kebabName = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
80
+ const filePath = path.join(modelsDir, `${kebabName}.ts`);
81
+ const pascalName = toPascalCase(name);
82
+
83
+ // 既存ファイルチェック
84
+ if (fs.existsSync(filePath)) {
85
+ const shouldOverwrite = await askConfirmation(
86
+ `⚠️ File ${filePath} already exists. Overwrite? (y/N): `
87
+ );
88
+
89
+ if (!shouldOverwrite) {
90
+ console.log(`⏭️ Skipped: ${kebabName}.ts`);
91
+ skipped.push(kebabName);
92
+ continue;
93
+ }
94
+ }
95
+
96
+ // モデルファイルを生成
97
+ const content = generateModelTemplate(name);
98
+ fs.writeFileSync(filePath, content);
99
+ console.log(`✅ Created: ${filePath}`);
100
+ created.push(kebabName);
101
+
102
+ // shared/index.ts に re-export を追加
103
+ updateSharedIndex(kebabName, pascalName);
104
+ }
105
+
106
+ // サマリー表示
107
+ console.log("\n📋 Summary:");
108
+ if (created.length > 0) {
109
+ console.log(` ✅ Created ${created.length} model(s): ${created.join(', ')}.ts`);
110
+ }
111
+ if (skipped.length > 0) {
112
+ console.log(` ⏭️ Skipped ${skipped.length} model(s): ${skipped.join(', ')}.ts`);
113
+ }
114
+
115
+ if (created.length > 0) {
116
+ console.log("\n📝 Next steps:");
117
+ console.log(" 1. Customize the generated model fields in shared/models/");
118
+ console.log(` 2. Run '${getCommands(detectFromProject()).dlx} swallowkit scaffold <model>' to generate CRUD code`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * shared/index.ts に re-export エントリを追加
124
+ */
125
+ function updateSharedIndex(kebabName: string, pascalName: string): void {
126
+ const indexPath = path.join("shared", "index.ts");
127
+
128
+ if (!fs.existsSync(indexPath)) {
129
+ return;
130
+ }
131
+
132
+ const content = fs.readFileSync(indexPath, "utf-8");
133
+ const exportLine = `export { ${pascalName} } from './models/${kebabName}';`;
134
+
135
+ // 既に存在する場合はスキップ
136
+ if (content.includes(exportLine)) {
137
+ return;
138
+ }
139
+
140
+ fs.appendFileSync(indexPath, exportLine + "\n");
141
+ }