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.
- package/LICENSE +21 -21
- package/README.ja.md +251 -242
- package/README.md +252 -243
- package/dist/__tests__/fixtures.d.ts +14 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +85 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/create-model.js +14 -14
- package/dist/cli/commands/dev.d.ts +8 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +238 -30
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2507 -1664
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +3 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +281 -117
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +28 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +5 -0
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +649 -218
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +1 -1
- package/dist/core/scaffold/model-parser.js +99 -99
- package/dist/core/scaffold/nextjs-generator.js +181 -181
- 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.js +656 -656
- 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/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +2 -1
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +14 -10
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +81 -74
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
- package/src/__tests__/config.test.ts +122 -0
- package/src/__tests__/dev.test.ts +42 -0
- package/src/__tests__/fixtures.ts +83 -0
- package/src/__tests__/functions-generator.test.ts +101 -0
- package/src/__tests__/init.test.ts +59 -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__/scaffold.test.ts +39 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/cli/commands/create-model.ts +141 -0
- package/src/cli/commands/dev.ts +794 -0
- package/src/cli/commands/index.ts +8 -0
- package/src/cli/commands/init.ts +3363 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +786 -0
- package/src/cli/index.ts +73 -0
- package/src/core/config.ts +244 -0
- package/src/core/scaffold/functions-generator.ts +674 -0
- package/src/core/scaffold/model-parser.ts +627 -0
- package/src/core/scaffold/nextjs-generator.ts +217 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +945 -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/types/index.ts +45 -0
- package/src/utils/package-manager.ts +229 -0
- package/dist/cli/commands/build.d.ts +0 -6
- package/dist/cli/commands/build.d.ts.map +0 -1
- package/dist/cli/commands/build.js +0 -177
- package/dist/cli/commands/build.js.map +0 -1
- package/dist/cli/commands/deploy.d.ts +0 -3
- package/dist/cli/commands/deploy.d.ts.map +0 -1
- package/dist/cli/commands/deploy.js +0 -147
- package/dist/cli/commands/deploy.js.map +0 -1
- package/dist/cli/commands/setup.d.ts +0 -6
- package/dist/cli/commands/setup.d.ts.map +0 -1
- package/dist/cli/commands/setup.js +0 -254
- 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
|
+
}
|