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,627 @@
1
+ /**
2
+ * Zod モデルファイルを解析して、スキーマ情報を抽出する
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { pathToFileURL } from "url";
8
+
9
+ export interface ModelInfo {
10
+ name: string; // モデル名(例: "Todo")
11
+ displayName: string; // 表示名(例: "Todo" または "タスク")
12
+ schemaName: string; // スキーマ変数名(例: "todoSchema")
13
+ filePath: string; // モデルファイルの絶対パス
14
+ fields: FieldInfo[]; // フィールド情報
15
+ hasId: boolean; // id フィールドがあるか
16
+ hasCreatedAt: boolean; // createdAt フィールドがあるか
17
+ hasUpdatedAt: boolean; // updatedAt フィールドがあるか
18
+ nestedSchemaRefs: NestedSchemaRef[]; // ネストしたスキーマ参照
19
+ }
20
+
21
+ export interface FieldInfo {
22
+ name: string;
23
+ type: string; // "string" | "number" | "boolean" | "date" | "object" | "array"
24
+ isOptional: boolean;
25
+ isArray: boolean;
26
+ enumValues?: string[]; // enum の場合の選択肢
27
+ isForeignKey?: boolean; // 外部キーかどうか
28
+ referencedModel?: string; // 参照先のモデル名(例: "Category")
29
+ isNestedSchema?: boolean; // ネストしたスキーマ参照かどうか
30
+ nestedSchemaName?: string; // 参照先のスキーマ名(例: "categorySchema")
31
+ nestedModelName?: string; // 参照先のモデル名(例: "Category")
32
+ nestedDisplayField?: string; // 参照先の表示フィールド(例: "name")
33
+ }
34
+
35
+ /**
36
+ * ネストしたスキーマ参照の情報
37
+ */
38
+ export interface NestedSchemaRef {
39
+ fieldName: string; // フィールド名(例: "category")
40
+ schemaName: string; // スキーマ変数名(例: "categorySchema")
41
+ modelName: string; // モデル名(例: "Category")
42
+ importPath: string; // インポートパス(例: "./category")
43
+ isArray: boolean; // 配列参照か
44
+ isOptional: boolean; // オプショナルか
45
+ displayField: string; // 表示用フィールド(例: "name")
46
+ }
47
+
48
+ /**
49
+ * モデルファイルを解析して ModelInfo を返す
50
+ */
51
+ export async function parseModelFile(modelPath: string): Promise<ModelInfo> {
52
+ if (!fs.existsSync(modelPath)) {
53
+ throw new Error(`Model file not found: ${modelPath}`);
54
+ }
55
+
56
+ const content = fs.readFileSync(modelPath, "utf-8");
57
+ const fileName = path.basename(modelPath, ".ts");
58
+
59
+ // モデル名を推定(ファイル名を PascalCase に変換)
60
+ const modelName = toPascalCase(fileName);
61
+
62
+ // スキーマ変数名を抽出
63
+ // パターン1: export const todoSchema = z.object({ ... }) (camelCase + Schema接尾辞)
64
+ // パターン2: export const Todo = z.object({ ... }) (Zod公式パターン)
65
+ let schemaMatch = content.match(/export\s+const\s+(\w+Schema)\s*=/);
66
+ if (!schemaMatch) {
67
+ schemaMatch = content.match(/export\s+const\s+(\w+)\s*=\s*z\.object\s*\(/);
68
+ }
69
+ if (!schemaMatch) {
70
+ throw new Error(`Could not find exported schema in ${modelPath}. Expected patterns:\n - export const xxxSchema = z.object({ ... })\n - export const Xxx = z.object({ ... })`);
71
+ }
72
+
73
+ const schemaName = schemaMatch[1];
74
+
75
+ // displayName を抽出(例: export const displayName = 'Task')
76
+ const displayNameMatch = content.match(/export\s+const\s+displayName\s*=\s*['"]([^'"]+)['"]/);
77
+ const displayName = displayNameMatch ? displayNameMatch[1] : modelName;
78
+
79
+ // ネストしたスキーマ参照を検出
80
+ const nestedSchemaRefs = detectNestedSchemaRefs(modelPath, content, schemaName);
81
+
82
+ // フィールド情報を抽出(動的インポートを使用)
83
+ const fields = await extractFieldsFromSchema(modelPath, schemaName);
84
+
85
+ // ネストスキーマ情報をフィールドにマージ
86
+ mergeNestedSchemaInfo(fields, nestedSchemaRefs);
87
+
88
+ // id フィールドの存在確認
89
+ const hasId = fields.some(f => f.name === "id");
90
+ const hasCreatedAt = fields.some(f => f.name === "createdAt");
91
+ const hasUpdatedAt = fields.some(f => f.name === "updatedAt");
92
+
93
+ return {
94
+ name: modelName,
95
+ displayName,
96
+ schemaName,
97
+ filePath: modelPath,
98
+ fields,
99
+ hasId,
100
+ hasCreatedAt,
101
+ hasUpdatedAt,
102
+ nestedSchemaRefs,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * import文とフィールド定義を解析し、ネストしたスキーマ参照を検出
108
+ */
109
+ function detectNestedSchemaRefs(
110
+ modelPath: string,
111
+ content: string,
112
+ schemaName: string
113
+ ): NestedSchemaRef[] {
114
+ const refs: NestedSchemaRef[] = [];
115
+
116
+ // 1. import文を解析して外部スキーマ変数を収集
117
+ // パターン: import { categorySchema } from './category'
118
+ // import { tagSchema, Tag } from './tag'
119
+ const importMap = new Map<string, string>(); // schemaVarName -> importPath
120
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
121
+ let importMatch;
122
+ while ((importMatch = importRegex.exec(content)) !== null) {
123
+ const imports = importMatch[1].split(',').map(s => s.trim());
124
+ const importPath = importMatch[2];
125
+ for (const imp of imports) {
126
+ // 'as' エイリアスに対応
127
+ const name = imp.split(/\s+as\s+/).pop()!.trim();
128
+ if (name.endsWith('Schema')) {
129
+ importMap.set(name, importPath);
130
+ } else {
131
+ // Zod公式パターン: インポート先ファイルで z.object として定義されているか確認
132
+ const dir = path.dirname(modelPath);
133
+ let targetPath = path.resolve(dir, importPath);
134
+ if (!targetPath.endsWith('.ts') && !targetPath.endsWith('.js')) {
135
+ targetPath += '.ts';
136
+ }
137
+ if (fs.existsSync(targetPath)) {
138
+ const targetContent = fs.readFileSync(targetPath, 'utf-8');
139
+ const isZodSchema = new RegExp(`(?:export\\s+)?const\\s+${name}\\s*=\\s*z\\.object\\s*\\(`).test(targetContent);
140
+ if (isZodSchema) {
141
+ importMap.set(name, importPath);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ if (importMap.size === 0) {
149
+ return refs;
150
+ }
151
+
152
+ // 2. z.object({ ... }) の中身を取得
153
+ const objectContent = extractObjectContent(content, schemaName);
154
+ if (!objectContent) {
155
+ return refs;
156
+ }
157
+
158
+ // 3. 各フィールドでインポートしたスキーマ変数が使われているか検出
159
+ for (const [schemaVarName, importPath] of importMap) {
160
+ // スキーマ名からモデル名を推定: categorySchema -> Category
161
+ const modelName = schemaVarName.replace(/Schema$/, '');
162
+ const pascalModelName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
163
+
164
+ // フィールド定義内でこのスキーマが使われているパターンを検出
165
+ // パターン1: fieldName: schemaVarName
166
+ // パターン2: fieldName: schemaVarName.optional()
167
+ // パターン3: fieldName: z.array(schemaVarName)
168
+ // パターン4: fieldName: z.array(schemaVarName).optional()
169
+ const patterns = [
170
+ // 単一オブジェクト参照
171
+ new RegExp(`(\\w+)\\s*:\\s*${schemaVarName}(?:\\.optional\\(\\))?`, 'g'),
172
+ // 配列参照
173
+ new RegExp(`(\\w+)\\s*:\\s*z\\.array\\(\\s*${schemaVarName}\\s*\\)(?:\\.optional\\(\\))?`, 'g'),
174
+ ];
175
+
176
+ for (let patIdx = 0; patIdx < patterns.length; patIdx++) {
177
+ const pattern = patterns[patIdx];
178
+ let fieldMatch;
179
+ while ((fieldMatch = pattern.exec(objectContent)) !== null) {
180
+ const fieldName = fieldMatch[1];
181
+ const fullMatch = fieldMatch[0];
182
+ const isArray = patIdx === 1;
183
+ const isOptional = fullMatch.includes('.optional()');
184
+
185
+ // 参照先スキーマの表示用フィールドを推定
186
+ const displayField = detectDisplayField(modelPath, importPath);
187
+
188
+ refs.push({
189
+ fieldName,
190
+ schemaName: schemaVarName,
191
+ modelName: pascalModelName,
192
+ importPath,
193
+ isArray,
194
+ isOptional,
195
+ displayField,
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ return refs;
202
+ }
203
+
204
+ /**
205
+ * z.object({ ... }) の内容部分を抽出
206
+ */
207
+ function extractObjectContent(content: string, schemaName: string): string | null {
208
+ const objectRegex = new RegExp(`${schemaName}\\s*=\\s*z\\.object\\(\\{`, "s");
209
+ const objectStart = content.search(objectRegex);
210
+
211
+ if (objectStart === -1) {
212
+ return null;
213
+ }
214
+
215
+ const braceStart = content.indexOf('{', objectStart);
216
+ let braceCount = 1;
217
+ let objectEnd = braceStart + 1;
218
+
219
+ while (braceCount > 0 && objectEnd < content.length) {
220
+ if (content[objectEnd] === '{') braceCount++;
221
+ if (content[objectEnd] === '}') braceCount--;
222
+ objectEnd++;
223
+ }
224
+
225
+ return content.substring(braceStart + 1, objectEnd - 1);
226
+ }
227
+
228
+ /**
229
+ * 参照先スキーマファイルから主要な表示フィールドを推定
230
+ */
231
+ function detectDisplayField(currentModelPath: string, importPath: string): string {
232
+ try {
233
+ const dir = path.dirname(currentModelPath);
234
+ let targetPath = path.resolve(dir, importPath);
235
+
236
+ // .ts 拡張子を補完
237
+ if (!targetPath.endsWith('.ts')) {
238
+ targetPath += '.ts';
239
+ }
240
+
241
+ if (!fs.existsSync(targetPath)) {
242
+ return 'name';
243
+ }
244
+
245
+ const targetContent = fs.readFileSync(targetPath, 'utf-8');
246
+
247
+ // 'name' フィールドがあれば最優先
248
+ if (/\bname\s*:\s*z\./.test(targetContent)) {
249
+ return 'name';
250
+ }
251
+ // 'title' フィールドがあれば次点
252
+ if (/\btitle\s*:\s*z\./.test(targetContent)) {
253
+ return 'title';
254
+ }
255
+ // 'label' フィールドがあれば
256
+ if (/\blabel\s*:\s*z\./.test(targetContent)) {
257
+ return 'label';
258
+ }
259
+
260
+ return 'name';
261
+ } catch {
262
+ return 'name';
263
+ }
264
+ }
265
+
266
+ /**
267
+ * ネストスキーマ情報をフィールド情報にマージ
268
+ */
269
+ function mergeNestedSchemaInfo(fields: FieldInfo[], nestedRefs: NestedSchemaRef[]): void {
270
+ for (const ref of nestedRefs) {
271
+ const field = fields.find(f => f.name === ref.fieldName);
272
+ if (field) {
273
+ field.isNestedSchema = true;
274
+ field.nestedSchemaName = ref.schemaName;
275
+ field.nestedModelName = ref.modelName;
276
+ field.nestedDisplayField = ref.displayField;
277
+ // object/array 型のままにする(動的解析が 'object' を返す)
278
+ } else {
279
+ // 動的解析で検出されなかったフィールドを追加
280
+ fields.push({
281
+ name: ref.fieldName,
282
+ type: ref.isArray ? 'array' : 'object',
283
+ isOptional: ref.isOptional,
284
+ isArray: ref.isArray,
285
+ isNestedSchema: true,
286
+ nestedSchemaName: ref.schemaName,
287
+ nestedModelName: ref.modelName,
288
+ nestedDisplayField: ref.displayField,
289
+ });
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Zodスキーマから動的にフィールド情報を抽出
296
+ */
297
+ async function extractFieldsFromSchema(modelPath: string, schemaName: string): Promise<FieldInfo[]> {
298
+ const fields: FieldInfo[] = [];
299
+
300
+ try {
301
+ // child_processでtsxを実行
302
+ const { execSync } = require('child_process');
303
+ const { tmpdir } = require('os');
304
+ const { join } = require('path');
305
+ const { writeFileSync, unlinkSync, readFileSync } = require('fs');
306
+
307
+ // モデルファイルの内容を読み込む
308
+ let modelContent = readFileSync(modelPath, 'utf8');
309
+ const modelDir = path.dirname(path.resolve(modelPath));
310
+
311
+ // ローカル相対インポートを絶対パスの .mjs import 文に変換して保持
312
+ // パターン: import { categorySchema } from './category'
313
+ const localImports: string[] = [];
314
+ modelContent = modelContent.replace(
315
+ /import\s*\{([^}]+)\}\s*from\s*['"](\.[^'"]+)['"]\s*;?/g,
316
+ (_match: string, imports: string, importPath: string) => {
317
+ // インポートされた変数名を取得
318
+ const importedNames = imports.split(',').map(s => {
319
+ const trimmed = s.trim();
320
+ // 'as' エイリアスに対応
321
+ return trimmed.split(/\s+as\s+/).pop()!.trim();
322
+ }).filter(s => s.length > 0);
323
+
324
+ // 相対パスを絶対パスに解決
325
+ let resolvedPath = path.resolve(modelDir, importPath);
326
+ if (!resolvedPath.endsWith('.ts') && !resolvedPath.endsWith('.js')) {
327
+ resolvedPath += '.ts';
328
+ }
329
+ if (fs.existsSync(resolvedPath)) {
330
+ // インポート先の内容を読み込み、インライン化する
331
+ let refContent = readFileSync(resolvedPath, 'utf8');
332
+ // インポート先の import 文を除去
333
+ refContent = refContent.replace(/import\s+.*?\s+from\s+['"](?!\.).*?['"];?\s*/g, '');
334
+ refContent = refContent.replace(/import\s+.*?\s+from\s+['"]\..*?['"];?\s*/g, '');
335
+ refContent = refContent.replace(/export\s+(const|type|interface|class|function)\s+/g, '$1 ');
336
+ refContent = refContent.replace(/^type\s+\w+\s*=\s*[^;]+;/gm, '');
337
+ refContent = refContent.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
338
+ refContent = refContent.replace(/\/\*[\s\S]*?\*\//g, '');
339
+ refContent = refContent.replace(/\/\/.*/g, '');
340
+
341
+ // インポートされていない const 宣言を除去(displayName 等の重複を防ぐ)
342
+ const importedNameSet = new Set(importedNames);
343
+ refContent = refContent.replace(/^const\s+(\w+)\s*=\s*[^;]+;/gm, (constMatch: string, varName: string) => {
344
+ return importedNameSet.has(varName) ? constMatch : '';
345
+ });
346
+
347
+ localImports.push(refContent.trim());
348
+ }
349
+ return ''; // 元のインポート文は除去
350
+ }
351
+ );
352
+
353
+ // zod 以外のパッケージインポートを除去
354
+ modelContent = modelContent.replace(/import\s+.*?\s+from\s+['"].*?['"];?\s*/g, '');
355
+ modelContent = modelContent.replace(/export\s+(const|type|interface|class|function)\s+/g, '$1 ');
356
+ // type宣言を削除(ランタイムでは不要)
357
+ modelContent = modelContent.replace(/^type\s+\w+\s*=\s*[^;]+;/gm, '');
358
+ // interface宣言を削除(ランタイムでは不要、複数行対応)
359
+ modelContent = modelContent.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
360
+ // コメントを削除
361
+ modelContent = modelContent.replace(/\/\*[\s\S]*?\*\//g, '');
362
+ modelContent = modelContent.replace(/\/\/.*/g, '');
363
+
364
+ // インライン化したローカルインポートを先頭に追加
365
+ const inlinedDeps = localImports.length > 0 ? localImports.join('\n\n') + '\n\n' : '';
366
+
367
+ // プロジェクトルートを探す(package.jsonがある場所)
368
+ let projectRoot = path.dirname(modelPath);
369
+ while (projectRoot !== path.dirname(projectRoot)) {
370
+ if (fs.existsSync(path.join(projectRoot, 'package.json'))) {
371
+ break;
372
+ }
373
+ projectRoot = path.dirname(projectRoot);
374
+ }
375
+
376
+ // プロジェクトルート内に一時スクリプトファイルを作成
377
+ const tempScript = path.join(projectRoot, `.swallowkit-parser-${Date.now()}.mjs`);
378
+ const scriptCode = `
379
+ import { z } from 'zod/v4';
380
+
381
+ // インライン化した依存スキーマ
382
+ ${inlinedDeps}
383
+ // モデルファイルの内容を評価
384
+ ${modelContent}
385
+
386
+ const schema = ${schemaName};
387
+
388
+ // Zod v3とv4の両方に対応
389
+ const isObject = (schema && schema._def &&
390
+ (schema._def.typeName === 'ZodObject' || schema.constructor?.name === 'ZodObject' || typeof schema._def.shape === 'function'));
391
+
392
+ if (isObject) {
393
+ const shape = typeof schema._def.shape === 'function' ? schema._def.shape() : schema._def.shape;
394
+ const fields = Object.keys(shape).map(key => {
395
+ const field = shape[key];
396
+ let type = 'string';
397
+ let isOptional = false;
398
+ let isArray = false;
399
+ let enumValues = undefined;
400
+
401
+ // ZodOptional, ZodDefault, ZodEffects を unwrap
402
+ let fieldDef = field;
403
+ const getTypeName = (def) => def?._def?.typeName || def?.constructor?.name || '';
404
+
405
+ // 繰り返し unwrap(複数のラッパーがある場合に対応)
406
+ let unwrapped = false;
407
+ do {
408
+ unwrapped = false;
409
+ const typeName = getTypeName(fieldDef);
410
+
411
+ if (typeName === 'ZodOptional') {
412
+ isOptional = true;
413
+ fieldDef = fieldDef._def.innerType;
414
+ unwrapped = true;
415
+ } else if (typeName === 'ZodDefault') {
416
+ // .default() は optional と同様に扱う
417
+ isOptional = true;
418
+ fieldDef = fieldDef._def.innerType;
419
+ unwrapped = true;
420
+ } else if (typeName === 'ZodEffects') {
421
+ // .min(), .max(), .regex() などの Effects を unwrap
422
+ fieldDef = fieldDef._def.schema;
423
+ unwrapped = true;
424
+ }
425
+ } while (unwrapped);
426
+
427
+ // ZodArray をチェック
428
+ if (getTypeName(fieldDef) === 'ZodArray') {
429
+ isArray = true;
430
+ fieldDef = fieldDef._def.type || fieldDef._def.element;
431
+ }
432
+
433
+ // 基本型を判定
434
+ const typeName = getTypeName(fieldDef);
435
+ if (typeName === 'ZodString') type = 'string';
436
+ else if (typeName === 'ZodNumber') type = 'number';
437
+ else if (typeName === 'ZodBoolean') type = 'boolean';
438
+ else if (typeName === 'ZodDate') type = 'date';
439
+ else if (typeName === 'ZodObject') type = 'object';
440
+ else if (typeName === 'ZodEnum' || typeName === 'ZodNativeEnum') {
441
+ type = 'string';
442
+ // enum の選択肢を取得(複数のZodバージョンに対応)
443
+ if (fieldDef.options) {
444
+ // Zod v3.23+ では options プロパティを使用
445
+ enumValues = Array.isArray(fieldDef.options)
446
+ ? fieldDef.options
447
+ : Object.values(fieldDef.options);
448
+ } else if (fieldDef._def.values) {
449
+ // 古いバージョンでは _def.values を使用
450
+ enumValues = Array.isArray(fieldDef._def.values)
451
+ ? fieldDef._def.values
452
+ : Object.values(fieldDef._def.values);
453
+ } else if (fieldDef._def.entries) {
454
+ // さらに古いバージョンでは _def.entries を使用
455
+ enumValues = Array.isArray(fieldDef._def.entries)
456
+ ? fieldDef._def.entries
457
+ : Object.values(fieldDef._def.entries);
458
+ }
459
+ }
460
+
461
+ // 外部キー検出: フィールド名が <ModelName>Id のパターンの場合
462
+ let isForeignKey = false;
463
+ let referencedModel = undefined;
464
+ if (key.endsWith('Id') && key.length > 2 && type === 'string') {
465
+ // categoryId -> Category, userId -> User など
466
+ const modelName = key.slice(0, -2); // "Id" を除去
467
+ referencedModel = modelName.charAt(0).toUpperCase() + modelName.slice(1); // 先頭を大文字に
468
+ isForeignKey = true;
469
+ }
470
+
471
+ return { name: key, type, isOptional, isArray, enumValues, isForeignKey, referencedModel };
472
+ });
473
+
474
+ console.log(JSON.stringify(fields));
475
+ }
476
+ `;
477
+
478
+ writeFileSync(tempScript, scriptCode, 'utf8');
479
+
480
+ try {
481
+ // プロジェクトルートでtsxを実行 (npm/pnpm 両対応)
482
+ // Detect package manager from project lockfile
483
+ const { detectFromProject, getCommands } = require('../../utils/package-manager');
484
+ const pm = detectFromProject(projectRoot);
485
+ const pmCmd = getCommands(pm);
486
+ const result = execSync(`${pmCmd.exec} tsx "${tempScript}"`, {
487
+ encoding: 'utf8',
488
+ cwd: projectRoot,
489
+ });
490
+
491
+ const stdout = result;
492
+
493
+ if (stdout) {
494
+ const parsedFields = JSON.parse(stdout.trim());
495
+ fields.push(...parsedFields);
496
+ }
497
+ } finally {
498
+ // 一時ファイルを削除
499
+ try {
500
+ unlinkSync(tempScript);
501
+ } catch (e) {
502
+ // ファイル削除失敗は無視
503
+ }
504
+ }
505
+ } catch (error) {
506
+ // tsxが使えない場合は正規表現フォールバック
507
+ console.warn('Failed to use dynamic import, falling back to regex parsing');
508
+ console.warn('Error:', error);
509
+ return extractFieldsWithRegex(modelPath, schemaName);
510
+ }
511
+
512
+ return fields;
513
+ }
514
+
515
+ /**
516
+ * 正規表現でフィールド情報を抽出(フォールバック用)
517
+ */
518
+ function extractFieldsWithRegex(modelPath: string, schemaName: string): FieldInfo[] {
519
+ const fields: FieldInfo[] = [];
520
+ const content = fs.readFileSync(modelPath, "utf-8");
521
+
522
+ // z.object({ ... }) の内容を抽出(ネストした括弧に対応)
523
+ const objectContent = extractObjectContent(content, schemaName);
524
+
525
+ if (!objectContent) {
526
+ return fields;
527
+ }
528
+
529
+ // 各フィールドを解析
530
+ const fieldRegex = /(\w+)\s*:\s*(z\.\w+)/g;
531
+ let match;
532
+
533
+ while ((match = fieldRegex.exec(objectContent)) !== null) {
534
+ const fieldName = match[1];
535
+ const zodDef = match[2];
536
+ const zodType = zodDef.split('.')[1];
537
+
538
+ const fieldStart = match.index;
539
+ const fieldEnd = objectContent.indexOf(',', fieldStart);
540
+ const fieldDef = fieldEnd > -1
541
+ ? objectContent.substring(fieldStart, fieldEnd)
542
+ : objectContent.substring(fieldStart);
543
+
544
+ fields.push({
545
+ name: fieldName,
546
+ type: mapZodTypeToTs(zodType),
547
+ isOptional: fieldDef.includes(".optional()"),
548
+ isArray: fieldDef.includes(".array()"),
549
+ });
550
+ }
551
+
552
+ return fields;
553
+ }
554
+
555
+ /**
556
+ * Zod 型を TypeScript 型にマッピング
557
+ */
558
+ function mapZodTypeToTs(zodType: string): string {
559
+ const typeMap: Record<string, string> = {
560
+ string: "string",
561
+ number: "number",
562
+ boolean: "boolean",
563
+ date: "Date",
564
+ object: "object",
565
+ array: "array",
566
+ };
567
+
568
+ return typeMap[zodType] || "any";
569
+ }
570
+
571
+ /**
572
+ * 文字列を PascalCase に変換
573
+ */
574
+ export function toPascalCase(str: string): string {
575
+ return str
576
+ .split(/[-_]/)
577
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
578
+ .join("");
579
+ }
580
+
581
+ /**
582
+ * 文字列を camelCase に変換
583
+ */
584
+ export function toCamelCase(str: string): string {
585
+ const pascal = toPascalCase(str);
586
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
587
+ }
588
+
589
+ /**
590
+ * 文字列を kebab-case に変換
591
+ */
592
+ export function toKebabCase(str: string): string {
593
+ return str
594
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
595
+ .toLowerCase();
596
+ }
597
+
598
+ /**
599
+ * models ディレクトリから全てのモデル情報を取得
600
+ * @param modelsDir モデルディレクトリのパス(デフォルト: "shared/models")
601
+ * @returns モデル情報の配列
602
+ */
603
+ export async function getAllModels(modelsDir: string = "shared/models"): Promise<ModelInfo[]> {
604
+ const cwd = process.cwd();
605
+ const fullModelsDir = path.join(cwd, modelsDir);
606
+
607
+ if (!fs.existsSync(fullModelsDir)) {
608
+ return [];
609
+ }
610
+
611
+ const files = fs.readdirSync(fullModelsDir);
612
+ const modelFiles = files.filter(file => file.endsWith('.ts'));
613
+
614
+ const models: ModelInfo[] = [];
615
+
616
+ for (const file of modelFiles) {
617
+ try {
618
+ const modelPath = path.join(fullModelsDir, file);
619
+ const modelInfo = await parseModelFile(modelPath);
620
+ models.push(modelInfo);
621
+ } catch (error: any) {
622
+ console.warn(`⚠️ Failed to parse model file ${file}: ${error.message}`);
623
+ }
624
+ }
625
+
626
+ return models;
627
+ }