sizuku 0.6.1 → 0.6.2

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 (2) hide show
  1. package/dist/index.mjs +538 -1304
  2. package/package.json +7 -7
package/dist/index.mjs CHANGED
@@ -1,357 +1,73 @@
1
- import fs from "node:fs";
2
- import fsp from "node:fs/promises";
3
1
  import path, { dirname } from "node:path";
4
- import { format } from "oxfmt";
5
2
  import { Node, Project } from "ts-morph";
3
+ import { format } from "oxfmt";
4
+ import fs from "node:fs";
5
+ import fsp from "node:fs/promises";
6
6
  import { Resvg } from "@resvg/resvg-js";
7
7
  import { run } from "@softwaretechnik/dbml-renderer";
8
- //#region src/fsp/index.ts
9
- function readFileSync(path) {
10
- try {
11
- return {
12
- ok: true,
13
- value: fs.readFileSync(path, "utf-8")
14
- };
15
- } catch (e) {
16
- return {
17
- ok: false,
18
- error: e instanceof Error ? e.message : String(e)
19
- };
20
- }
21
- }
22
- /**
23
- * Creates a directory if it does not already exist.
24
- *
25
- * @param dir - Directory path to create.
26
- * @returns A `Result` that is `ok` on success, otherwise an error message.
27
- */
28
- async function mkdir(dir) {
29
- try {
30
- await fsp.mkdir(dir, { recursive: true });
31
- return {
32
- ok: true,
33
- value: void 0
34
- };
35
- } catch (e) {
36
- return {
37
- ok: false,
38
- error: e instanceof Error ? e.message : String(e)
39
- };
40
- }
41
- }
42
- /**
43
- * Writes UTF-8 text to a file, creating it if necessary.
44
- *
45
- * @param path - File path to write.
46
- * @param data - Text data to write.
47
- * @returns A `Result` that is `ok` on success, otherwise an error message.
48
- */
49
- async function writeFile(path, data) {
50
- try {
51
- await fsp.writeFile(path, data, "utf-8");
52
- return {
53
- ok: true,
54
- value: void 0
55
- };
56
- } catch (e) {
57
- return {
58
- ok: false,
59
- error: e instanceof Error ? e.message : String(e)
60
- };
61
- }
62
- }
63
- /**
64
- * Writes binary data to a file, creating it if necessary.
65
- *
66
- * @param path - File path to write.
67
- * @param data - Binary data to write.
68
- * @returns A `Result` that is `ok` on success, otherwise an error message.
69
- */
70
- async function writeFileBinary(path, data) {
71
- try {
72
- await fsp.writeFile(path, data);
73
- return {
74
- ok: true,
75
- value: void 0
76
- };
77
- } catch (e) {
78
- return {
79
- ok: false,
80
- error: e instanceof Error ? e.message : String(e)
81
- };
82
- }
83
- }
84
- //#endregion
85
- //#region src/format/index.ts
86
- /**
87
- * Format TypeScript code using oxfmt
88
- * @param input - The code to format
89
- * @returns The formatted code, or throws on format errors (I/O boundary)
90
- */
91
- async function fmt(input) {
92
- const { code, errors } = await format("<stdin>.ts", input, {
93
- printWidth: 100,
94
- singleQuote: true,
95
- semi: false
96
- });
97
- if (errors.length > 0) throw new Error(errors.map((e) => e.message).join("\n"));
98
- return code;
99
- }
100
- //#endregion
101
8
  //#region src/utils/index.ts
102
- /**
103
- * Capitalize the first character of a string.
104
- *
105
- * @param str - The input string.
106
- * @returns String with first character capitalized.
107
- */
108
9
  function makeCapitalized(str) {
109
10
  return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
110
11
  }
111
- /**
112
- * Resolve schema object wrapper type from objectType annotation.
113
- */
12
+ function stripImports(content) {
13
+ const lines = content.split("\n");
14
+ const codeStart = lines.findIndex((line) => !line.trim().startsWith("import") && line.trim() !== "");
15
+ return lines.slice(codeStart);
16
+ }
114
17
  function resolveWrapperType(objectType) {
115
18
  if (objectType === "strict") return "strictObject";
116
19
  if (objectType === "loose") return "looseObject";
117
20
  return "object";
118
21
  }
119
- /**
120
- * Resolve ArkType undeclared key handling prefix.
121
- *
122
- * ArkType uses `"+"` key to control unknown property behavior:
123
- * - strict → `"+": "reject"` (reject unknown keys)
124
- * - loose → `"+": "ignore"` (preserve unknown keys, which is ArkType's default)
125
- * - undefined → no prefix (default behavior: preserve unknown keys)
126
- */
127
- function resolveArktypeUndeclared(objectType) {
128
- if (objectType === "strict") return "\"+\":\"reject\",";
129
- if (objectType === "loose") return "\"+\":\"ignore\",";
130
- return "";
131
- }
132
- /**
133
- * Join relation schema fields into a comma-separated string.
134
- */
135
22
  function makeRelationFields(fields) {
136
23
  return fields.map((f) => `${f.name}:${f.definition}`).join(",");
137
24
  }
138
- /**
139
- * Generates a Zod object wrapper.
140
- *
141
- * @param inner - The inner field definitions string.
142
- * @param wrapperType - The object wrapper type.
143
- * @returns The generated Zod object string.
144
- */
145
- function makeZodObject(inner, wrapperType = "object") {
146
- switch (wrapperType) {
147
- case "strictObject": return `z.strictObject({${inner}})`;
148
- case "looseObject": return `z.looseObject({${inner}})`;
149
- default: return `z.object({${inner}})`;
150
- }
151
- }
152
- /**
153
- * Generates a Valibot object wrapper.
154
- *
155
- * @param inner - The inner field definitions string.
156
- * @param wrapperType - The object wrapper type.
157
- * @returns The generated Valibot object string.
158
- */
159
- function makeValibotObject(inner, wrapperType = "object") {
160
- switch (wrapperType) {
161
- case "strictObject": return `v.strictObject({${inner}})`;
162
- case "looseObject": return `v.looseObject({${inner}})`;
163
- default: return `v.object({${inner}})`;
164
- }
165
- }
166
- /**
167
- * Check if a string starts with a prefix.
168
- *
169
- * @param str - The input string.
170
- * @param prefix - The prefix to check.
171
- * @returns True if string starts with prefix.
172
- */
173
- function startsWith(str, prefix) {
174
- return str.indexOf(prefix) === 0;
175
- }
176
- /**
177
- * Trim whitespace from string.
178
- *
179
- * @param str - The input string.
180
- * @returns Trimmed string.
181
- */
182
- function trimString(str) {
183
- return str.trim();
184
- }
185
- /**
186
- * Clean comment lines by removing triple slash prefix and trimming.
187
- *
188
- * @param commentLines - Raw comment lines
189
- * @returns Cleaned comment lines
190
- */
191
25
  function cleanCommentLines(commentLines) {
192
26
  return commentLines.map((line) => line.replace(/^\/\/\/\s*/, "").trim()).filter(Boolean);
193
27
  }
194
- /**
195
- * Extract object type from comment lines.
196
- *
197
- * @param cleaned - Cleaned comment lines
198
- * @param tag - The tag to look for
199
- * @returns The object type if found
200
- */
201
- function extractObjectType(cleaned, tag) {
28
+ function parseFieldComments(commentLines, tag) {
29
+ const cleaned = cleanCommentLines(commentLines);
202
30
  const tagWithoutAt = tag.slice(1);
203
31
  const objectTypeLine = cleaned.find((line) => line.includes(`${tagWithoutAt}strictObject`) || line.includes(`${tagWithoutAt}looseObject`));
204
- if (!objectTypeLine) return void 0;
205
- if (objectTypeLine.includes("strictObject")) return "strict";
206
- if (objectTypeLine.includes("looseObject")) return "loose";
207
- }
208
- /**
209
- * Extract definition from comment lines.
210
- *
211
- * @param cleaned - Cleaned comment lines
212
- * @param tag - The tag to look for
213
- * @returns The definition string
214
- */
215
- function extractDefinition(cleaned, tag) {
32
+ const objectType = !objectTypeLine ? void 0 : objectTypeLine.includes("strictObject") ? "strict" : "loose";
216
33
  const definitionLine = cleaned.find((line) => line.startsWith(tag) && !line.includes("strictObject") && !line.includes("looseObject"));
217
- if (!definitionLine) return "";
218
- const withoutAt = definitionLine.startsWith("@") ? definitionLine.substring(1) : definitionLine;
219
- if (tag === "@a." || tag === "@e.") {
220
- const prefix = tag.substring(1);
221
- return withoutAt.startsWith(prefix) ? withoutAt.substring(prefix.length) : withoutAt;
222
- }
223
- return withoutAt;
224
- }
225
- /**
226
- * Extract description from comment lines.
227
- *
228
- * @param cleaned - Cleaned comment lines
229
- * @returns The description if found
230
- */
231
- function extractDescription(cleaned) {
232
- const descriptionLines = cleaned.filter((line) => !(line.includes("@z.") || line.includes("@v.") || line.includes("@a.") || line.includes("@e.") || line.includes("@relation.")));
233
- return descriptionLines.length > 0 ? descriptionLines.join(" ") : void 0;
234
- }
235
- /**
236
- * Parse field comments and extract definition line and description.
237
- *
238
- * @param commentLines - Raw comment lines (e.g., from source text)
239
- * @param tag - The tag to look for (e.g., '@v.', '@z.', '@a.', or '@e.')
240
- * @returns Parsed definition and description
241
- */
242
- function parseFieldComments(commentLines, tag) {
243
- const cleaned = cleanCommentLines(commentLines);
244
- const objectType = extractObjectType(cleaned, tag);
34
+ const definition = (() => {
35
+ if (!definitionLine) return "";
36
+ const withoutAt = definitionLine.startsWith("@") ? definitionLine.substring(1) : definitionLine;
37
+ if (tag === "@a." || tag === "@e.") {
38
+ const prefix = tag.substring(1);
39
+ return withoutAt.startsWith(prefix) ? withoutAt.substring(prefix.length) : withoutAt;
40
+ }
41
+ return withoutAt;
42
+ })();
43
+ const descriptionLines = cleaned.filter((line) => !line.includes("@z.") && !line.includes("@v.") && !line.includes("@a.") && !line.includes("@e.") && !line.includes("@relation."));
245
44
  return {
246
- definition: extractDefinition(cleaned, tag),
247
- description: extractDescription(cleaned),
45
+ definition,
46
+ description: descriptionLines.length > 0 ? descriptionLines.join(" ") : void 0,
248
47
  objectType
249
48
  };
250
49
  }
251
- /**
252
- * Process a single line during comment extraction.
253
- *
254
- * @param acc - The accumulator
255
- * @param line - The line to process
256
- * @returns Updated accumulator
257
- */
258
- function processCommentLine(acc, line) {
259
- if (acc.shouldStop) return acc;
260
- if (line.startsWith("///")) return {
261
- commentLines: [line, ...acc.commentLines],
262
- shouldStop: false
263
- };
264
- if (line === "") return acc;
265
- return {
266
- commentLines: acc.commentLines,
267
- shouldStop: true
268
- };
269
- }
270
- /**
271
- * Extract field comments from source text.
272
- *
273
- * @param sourceText - The source text to extract comments from.
274
- * @param fieldStartPos - The position of the field in the source text.
275
- * @returns An array of comment lines.
276
- */
277
50
  function extractFieldComments(sourceText, fieldStartPos) {
278
- return sourceText.substring(0, fieldStartPos).split("\n").map((line, index) => ({
279
- line: line.trim(),
280
- index
281
- })).reverse().reduce((acc, { line }) => processCommentLine(acc, line), {
282
- commentLines: [],
283
- shouldStop: false
284
- }).commentLines;
285
- }
286
- /**
287
- * Creates `z.infer` type for the specified model.
288
- *
289
- * @param name - The model name
290
- * @returns The generated TypeScript type definition line using Zod.
291
- */
292
- function infer(name) {
293
- const modelName = makeCapitalized(name);
294
- return `export type ${modelName} = z.infer<typeof ${modelName}Schema>`;
295
- }
296
- /**
297
- * Creates `v.InferOutput` type for the specified model.
298
- *
299
- * @param name - The model name
300
- * @returns The generated TypeScript type definition line using Valibot.
301
- */
302
- function inferOutput(name) {
303
- const modelName = makeCapitalized(name);
304
- return `export type ${modelName} = v.InferOutput<typeof ${modelName}Schema>`;
305
- }
306
- /**
307
- * Generate a JSDoc comment block from a description
308
- * @param description - The description text
309
- * @returns The formatted JSDoc comment block
310
- */
311
- function makeCommentBlock(description) {
312
- if (!description) return "";
313
- return `/**\n * ${description}\n */\n`;
51
+ const reversed = sourceText.substring(0, fieldStartPos).split("\n").map((l) => l.trim()).reverse();
52
+ const stopIdx = reversed.findIndex((l) => l !== "" && !l.startsWith("///"));
53
+ return (stopIdx === -1 ? reversed : reversed.slice(0, stopIdx)).filter((l) => l.startsWith("///")).reverse();
314
54
  }
315
- /**
316
- * Generate field definitions with optional JSDoc comments
317
- * @param schema - The schema with fields to generate definitions for
318
- * @param comment - Whether to include JSDoc comments
319
- */
320
55
  function fieldDefinitions(schema, comment) {
321
56
  return schema.fields.map(({ name, definition, description }) => {
322
- return `${description && comment ? makeCommentBlock(description) : ""}${name}:${definition}`;
57
+ return `${description && comment ? `/**\n * ${description}\n */\n` : ""}${name}:${definition}`;
323
58
  }).join(",\n");
324
59
  }
325
- /**
326
- * Creates ArkType infer type for the specified model.
327
- *
328
- * @param name - The model name
329
- * @returns The generated TypeScript type definition line using ArkType.
330
- */
331
- function inferArktype(name) {
332
- const capitalized = makeCapitalized(name);
333
- return `export type ${capitalized} = typeof ${capitalized}Schema.infer`;
334
- }
335
- /**
336
- * Creates Effect Schema infer type for the specified model.
337
- *
338
- * @param name - The model name
339
- * @returns The generated TypeScript type definition line using Effect Schema.
340
- */
341
- function inferEffect(name) {
342
- const capitalized = makeCapitalized(name);
343
- return `export type ${capitalized}Encoded = typeof ${capitalized}Schema.Encoded`;
344
- }
345
60
  //#endregion
346
61
  //#region src/helper/extract-schemas.ts
347
- /**
348
- * Generates relation definition based on function name and reference table.
349
- *
350
- * @param fnName - The relation function name ('many' or 'one')
351
- * @param refTable - The referenced table name
352
- * @param prefix - Schema prefix for validation library
353
- * @returns The generated relation definition string
354
- */
62
+ function makeSourceFile(sourceCode) {
63
+ return new Project({
64
+ useInMemoryFileSystem: true,
65
+ compilerOptions: {
66
+ allowJs: true,
67
+ skipLibCheck: true
68
+ }
69
+ }).createSourceFile("temp.ts", sourceCode);
70
+ }
355
71
  function generateRelationDefinition(fnName, refTable, prefix) {
356
72
  const schema = `${makeCapitalized(refTable)}Schema`;
357
73
  if (fnName === "many") {
@@ -362,469 +78,196 @@ function generateRelationDefinition(fnName, refTable, prefix) {
362
78
  if (fnName === "one") return schema;
363
79
  return "";
364
80
  }
365
- /**
366
- * Processes arrow function body to find object literal expression.
367
- *
368
- * @param body - The arrow function body node
369
- * @returns The found object literal expression, or null if not found
370
- */
371
- function processArrowFunctionBody(body) {
372
- if (Node.isObjectLiteralExpression(body)) return body;
373
- if (Node.isParenthesizedExpression(body)) return findObjectLiteralExpression(body.getExpression());
374
- if (Node.isBlock(body)) {
375
- const ret = body.getStatements().find(Node.isReturnStatement);
376
- if (ret && Node.isReturnStatement(ret)) {
377
- const re = ret.getExpression();
378
- return re && Node.isObjectLiteralExpression(re) ? re : null;
379
- }
380
- }
381
- return null;
382
- }
383
- /**
384
- * Recursively extracts an `ObjectLiteralExpression` from a given AST node.
385
- *
386
- * @param expr - The root `Node` to search for object literals
387
- * @returns The found `ObjectLiteralExpression`, or `null` if not found
388
- */
389
81
  function findObjectLiteralExpression(expr) {
390
82
  if (Node.isObjectLiteralExpression(expr)) return expr;
391
83
  if (Node.isParenthesizedExpression(expr)) return findObjectLiteralExpression(expr.getExpression());
392
- if (Node.isArrowFunction(expr)) return processArrowFunctionBody(expr.getBody());
84
+ if (Node.isArrowFunction(expr)) {
85
+ const body = expr.getBody();
86
+ if (Node.isObjectLiteralExpression(body)) return body;
87
+ if (Node.isParenthesizedExpression(body)) return findObjectLiteralExpression(body.getExpression());
88
+ if (Node.isBlock(body)) {
89
+ const re = body.getStatements().find(Node.isReturnStatement)?.getExpression();
90
+ return re && Node.isObjectLiteralExpression(re) ? re : null;
91
+ }
92
+ }
393
93
  return null;
394
94
  }
395
- /**
396
- * Finds an object literal expression in call expression arguments.
397
- *
398
- * @param call - The call expression to search for object literals in its arguments
399
- * @param finder - Function to find object literal in a node
400
- * @returns The found object literal, or `null` if not found in any argument
401
- */
402
- function findObjectLiteralInArgs(call, finder) {
95
+ function findObjectLiteralInArgs(call) {
403
96
  for (const arg of call.getArguments()) {
404
- const obj = finder(arg);
97
+ const obj = findObjectLiteralExpression(arg);
405
98
  if (obj) return obj;
406
99
  }
407
100
  return null;
408
101
  }
409
- /**
410
- * Determines whether a given `CallExpression` is a relation-related function call.
411
- *
412
- * @param callExpr - The call expression node to check for relation functions
413
- * @returns `true` if the function is a relation function; otherwise, `false`
414
- */
415
102
  function isRelationFunctionCall(callExpr) {
416
103
  const expression = callExpr.getExpression();
417
104
  if (!Node.isIdentifier(expression)) return false;
418
- const functionName = expression.getText();
419
- return functionName === "relations" || functionName.includes("relation");
420
- }
421
- /**
422
- * Creates a field extractor function using a custom parseFieldComments implementation.
423
- *
424
- * @param parseFieldComments - A function that parses comment lines into { definition, description, objectType }
425
- * @returns A property node extractor function
426
- */
427
- function createExtractFieldFromProperty(parseFieldComments) {
428
- return (property, sourceText) => {
429
- if (!Node.isPropertyAssignment(property)) return null;
430
- const name = property.getName();
431
- if (!name) return null;
432
- const { definition, description } = parseFieldComments(extractFieldComments(sourceText, property.getStart()));
433
- return {
434
- name,
435
- definition,
436
- description
437
- };
438
- };
439
- }
440
- /**
441
- * Creates a relation field extractor function.
442
- *
443
- * @param parseFieldComments - Function to parse field comments
444
- * @param prefix - Schema prefix for validation library
445
- * @returns Function that extracts relation fields from property
446
- */
447
- function createExtractRelationFieldFromProperty(parseFieldComments, prefix) {
448
- return (property, sourceText) => {
449
- if (!Node.isPropertyAssignment(property)) return null;
450
- const name = property.getName();
451
- if (!name) return null;
452
- const init = property.getInitializer();
453
- if (!Node.isCallExpression(init)) return {
454
- name,
455
- definition: "",
456
- description: void 0
457
- };
458
- const expr = init.getExpression();
459
- if (!Node.isIdentifier(expr)) return {
460
- name,
461
- definition: "",
462
- description: void 0
463
- };
464
- const fnName = expr.getText();
465
- const args = init.getArguments();
466
- if (!(args.length && Node.isIdentifier(args[0]))) return {
467
- name,
468
- definition: "",
469
- description: void 0
470
- };
471
- const definition = generateRelationDefinition(fnName, args[0].getText(), prefix);
472
- const { description } = parseFieldComments(extractFieldComments(sourceText, property.getStart()));
473
- return {
474
- name,
475
- definition,
476
- description
477
- };
478
- };
479
- }
480
- /**
481
- * Extracts fields from object literal properties using appropriate extractor.
482
- *
483
- * @param properties - Array of object literal properties
484
- * @param isRelation - Whether this is a relation function call
485
- * @param extractFieldFromProperty - Function to extract regular fields
486
- * @param extractRelationFieldFromProperty - Function to extract relation fields
487
- * @param sourceText - The source text for comment extraction
488
- * @returns Array of extracted field results
489
- */
490
- function extractFieldsFromProperties(properties, isRelation, extractFieldFromProperty, extractRelationFieldFromProperty, sourceText) {
491
- return properties.map((prop) => isRelation ? extractRelationFieldFromProperty(prop, sourceText) : extractFieldFromProperty(prop, sourceText)).filter((field) => field !== null);
492
- }
493
- /**
494
- * Creates a field extractor for call expressions with customizable strategies.
495
- *
496
- * @param extractFieldFromProperty - Function to extract field from property
497
- * @param extractRelationFieldFromProperty - Function to extract relation field from property
498
- * @param findObjectLiteralExpression - Function to find object literal expression
499
- * @param findObjectLiteralInArgs - Function to find object literal in call arguments
500
- * @param isRelationFunctionCall - Function to check if call is relation function
501
- * @returns Function that extracts fields from call expression
502
- */
503
- function createExtractFieldsFromCallExpression(extractFieldFromProperty, extractRelationFieldFromProperty, findObjectLiteralExpression, findObjectLiteralInArgs, isRelationFunctionCall) {
504
- return (callExpr, sourceText) => {
505
- const objectLiteral = findObjectLiteralInArgs(callExpr, findObjectLiteralExpression);
506
- if (!objectLiteral) return [];
507
- const isRelation = isRelationFunctionCall(callExpr);
508
- return extractFieldsFromProperties(objectLiteral.getProperties(), isRelation, extractFieldFromProperty, extractRelationFieldFromProperty, sourceText);
509
- };
510
- }
511
- /**
512
- * Creates a schema extractor from customizable strategies.
513
- *
514
- * @param extractFieldsFromCall - Function to extract fields from a call expression
515
- * @param extractFieldFromProperty - Function to extract a single field from an object literal property
516
- * @param parseFieldComments - Function to parse field comments with object type support
517
- * @param commentPrefix - The comment prefix to use for parsing
518
- * @returns A function that extracts a schema from a variable declaration node
519
- */
520
- function makeSchemaExtractor(extractFieldsFromCall, extractFieldFromProperty, parseFieldComments, commentPrefix) {
521
- return (variableStatement, sourceText, originalSourceCode) => {
522
- if (!Node.isVariableStatement(variableStatement)) return null;
523
- const declarations = variableStatement.getDeclarations();
524
- if (declarations.length === 0) return null;
525
- const declaration = declarations[0];
526
- const name = declaration.getName();
527
- if (!name) return null;
528
- const statementStart = variableStatement.getStart();
529
- const originalSourceLines = originalSourceCode.split("\n");
530
- const commentLines = [];
531
- const lineNumber = originalSourceLines.reduce((state, line, i) => state.found ? state : state.acc >= statementStart ? {
532
- acc: state.acc,
533
- index: i,
534
- found: true
535
- } : {
536
- acc: state.acc + line.length + 1,
537
- index: 0,
538
- found: false
539
- }, {
540
- acc: 0,
541
- index: 0,
542
- found: false
543
- }).index;
544
- for (let i = lineNumber - 1; i >= 0; i--) {
545
- const line = originalSourceLines[i];
546
- const trimmedLine = trimString(line);
547
- if (trimmedLine === "") continue;
548
- if (startsWith(trimmedLine, "///")) commentLines.unshift(line);
549
- else break;
550
- }
551
- const { objectType } = parseFieldComments(commentLines, commentPrefix);
552
- const initializer = declaration.getInitializer();
553
- if (Node.isCallExpression(initializer)) {
554
- if (isRelationFunctionCall(initializer)) return null;
555
- return {
556
- name,
557
- fields: extractFieldsFromCall(initializer, sourceText),
558
- objectType
559
- };
560
- }
561
- if (Node.isObjectLiteralExpression(initializer)) return {
562
- name,
563
- fields: initializer.getProperties().map((prop) => extractFieldFromProperty(prop, sourceText)).filter((field) => field !== null),
564
- objectType
565
- };
105
+ const fnName = expression.getText();
106
+ return fnName === "relations" || fnName.includes("relation");
107
+ }
108
+ function extractFieldFromProperty(property, sourceText, tag) {
109
+ if (!Node.isPropertyAssignment(property)) return null;
110
+ const name = property.getName();
111
+ if (!name) return null;
112
+ const { definition, description } = parseFieldComments(extractFieldComments(sourceText, property.getStart()), tag);
113
+ return {
114
+ name,
115
+ definition,
116
+ description
117
+ };
118
+ }
119
+ function extractRelationFieldFromProperty(property, sourceText, tag, prefix) {
120
+ if (!Node.isPropertyAssignment(property)) return null;
121
+ const name = property.getName();
122
+ if (!name) return null;
123
+ const init = property.getInitializer();
124
+ if (!Node.isCallExpression(init)) return {
125
+ name,
126
+ definition: "",
127
+ description: void 0
128
+ };
129
+ const expr = init.getExpression();
130
+ if (!Node.isIdentifier(expr)) return {
131
+ name,
132
+ definition: "",
133
+ description: void 0
134
+ };
135
+ const fnName = expr.getText();
136
+ const args = init.getArguments();
137
+ if (!(args.length && Node.isIdentifier(args[0]))) return {
138
+ name,
139
+ definition: "",
140
+ description: void 0
141
+ };
142
+ const definition = generateRelationDefinition(fnName, args[0].getText(), prefix);
143
+ const { description } = parseFieldComments(extractFieldComments(sourceText, property.getStart()), tag);
144
+ return {
145
+ name,
146
+ definition,
147
+ description
148
+ };
149
+ }
150
+ function extractFieldsFromCall(call, sourceText, tag, prefix) {
151
+ const objectLiteral = findObjectLiteralInArgs(call);
152
+ if (!objectLiteral) return [];
153
+ const isRelation = isRelationFunctionCall(call);
154
+ return objectLiteral.getProperties().map((prop) => isRelation ? extractRelationFieldFromProperty(prop, sourceText, tag, prefix) : extractFieldFromProperty(prop, sourceText, tag)).filter((f) => f !== null);
155
+ }
156
+ function findLeadingComments(sourceCode, statementStart) {
157
+ const lines = sourceCode.split("\n");
158
+ const lineNumber = sourceCode.slice(0, statementStart).split("\n").length - 1;
159
+ const reversed = lines.slice(0, lineNumber).reverse();
160
+ const stopIdx = reversed.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("///"));
161
+ return (stopIdx === -1 ? reversed : reversed.slice(0, stopIdx)).filter((l) => l.trim().startsWith("///")).reverse();
162
+ }
163
+ function extractSchema(variableStatement, sourceText, sourceCode, tag, prefix) {
164
+ if (!Node.isVariableStatement(variableStatement)) return null;
165
+ const declaration = variableStatement.getDeclarations()[0];
166
+ if (!declaration) return null;
167
+ const name = declaration.getName();
168
+ if (!name) return null;
169
+ const { objectType } = parseFieldComments(findLeadingComments(sourceCode, variableStatement.getStart()), tag);
170
+ const initializer = declaration.getInitializer();
171
+ if (Node.isCallExpression(initializer)) {
172
+ if (isRelationFunctionCall(initializer)) return null;
566
173
  return {
567
174
  name,
568
- fields: [],
175
+ fields: extractFieldsFromCall(initializer, sourceText, tag, prefix),
569
176
  objectType
570
177
  };
178
+ }
179
+ if (Node.isObjectLiteralExpression(initializer)) return {
180
+ name,
181
+ fields: initializer.getProperties().map((prop) => extractFieldFromProperty(prop, sourceText, tag)).filter((f) => f !== null),
182
+ objectType
183
+ };
184
+ return {
185
+ name,
186
+ fields: [],
187
+ objectType
571
188
  };
572
189
  }
573
- /**
574
- * Extracts schemas from TypeScript source code using AST analysis.
575
- *
576
- * This function processes exported variable declarations to extract table schemas
577
- * with their field definitions and comments. It supports both Zod and Valibot schema extraction.
578
- *
579
- * @param lines - Array of source code lines to process
580
- * @param library - The validation library to extract schemas for ('zod' or 'valibot')
581
- * @returns Array of extracted schemas with field definitions
582
- *
583
- * @example
584
- * ```typescript
585
- * // For Zod schemas
586
- * const zodSchemas = extractSchemas(sourceLines, 'zod')
587
- *
588
- * // For Valibot schemas
589
- * const valibotSchemas = extractSchemas(sourceLines, 'valibot')
590
- * ```
591
- */
592
190
  function extractSchemas(lines, library) {
593
191
  const sourceCode = lines.join("\n");
594
- const sourceFile = new Project({
595
- useInMemoryFileSystem: true,
596
- compilerOptions: {
597
- allowJs: true,
598
- skipLibCheck: true
599
- }
600
- }).createSourceFile("temp.ts", sourceCode);
192
+ const sourceFile = makeSourceFile(sourceCode);
601
193
  const sourceText = sourceFile.getFullText();
602
- const commentPrefixMap = {
194
+ const tag = {
603
195
  zod: "@z.",
604
196
  valibot: "@v.",
605
197
  arktype: "@a.",
606
198
  effect: "@e."
607
- };
608
- const schemaPrefixMap = {
199
+ }[library];
200
+ const prefix = {
609
201
  zod: "z",
610
202
  valibot: "v",
611
203
  arktype: "type",
612
204
  effect: "Schema"
613
- };
614
- const commentPrefix = commentPrefixMap[library];
615
- const schemaPrefix = schemaPrefixMap[library];
616
- const extractField = createExtractFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix));
617
- const extractSchema = makeSchemaExtractor(createExtractFieldsFromCallExpression(extractField, createExtractRelationFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix), schemaPrefix), findObjectLiteralExpression, findObjectLiteralInArgs, isRelationFunctionCall), extractField, parseFieldComments, commentPrefix);
618
- return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).map((stmt) => extractSchema(stmt, sourceText, sourceCode)).filter((schema) => schema !== null);
205
+ }[library];
206
+ return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).map((stmt) => extractSchema(stmt, sourceText, sourceCode, tag, prefix)).filter((schema) => schema !== null);
619
207
  }
620
- /**
621
- * Extracts relation schemas from `relations(...)` declarations using AST analysis.
622
- *
623
- * This returns entries like `userRelations` and `postRelations` with fields
624
- * resolved to either `z.array(OtherSchema)` / `v.array(OtherSchema)` or direct
625
- * `OtherSchema` based on `many`/`one`.
626
- *
627
- * Note: Base table schemas are not included here; use `extractSchemas` for those.
628
- */
629
208
  function extractRelationSchemas(lines, library) {
630
- const sourceCode = lines.join("\n");
631
- const sourceFile = new Project({
632
- useInMemoryFileSystem: true,
633
- compilerOptions: {
634
- allowJs: true,
635
- skipLibCheck: true
636
- }
637
- }).createSourceFile("temp.ts", sourceCode);
209
+ const sourceFile = makeSourceFile(lines.join("\n"));
638
210
  const sourceText = sourceFile.getFullText();
639
- const commentPrefixMap = {
211
+ const tag = {
640
212
  zod: "@z.",
641
213
  valibot: "@v.",
642
214
  arktype: "@a.",
643
215
  effect: "@e."
644
- };
645
- const schemaPrefixMap = {
216
+ }[library];
217
+ const prefix = {
646
218
  zod: "z",
647
219
  valibot: "v",
648
220
  arktype: "type",
649
221
  effect: "Schema"
650
- };
651
- const commentPrefix = commentPrefixMap[library];
652
- const schemaPrefix = schemaPrefixMap[library];
653
- const baseSchemas = extractSchemas(lines, library);
654
- const baseSchemaMap = new Map(baseSchemas.map((schema) => [schema.name, schema.objectType]));
655
- const extractFieldsFromCall = createExtractFieldsFromCallExpression(createExtractFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix)), createExtractRelationFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix), schemaPrefix), findObjectLiteralExpression, findObjectLiteralInArgs, isRelationFunctionCall);
656
- function extract(declaration) {
222
+ }[library];
223
+ const baseSchemaMap = new Map(extractSchemas(lines, library).map((schema) => [schema.name, schema.objectType]));
224
+ return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).flatMap((stmt) => stmt.getDeclarations()).map((declaration) => {
657
225
  if (!Node.isVariableDeclaration(declaration)) return null;
658
226
  const name = declaration.getName();
659
227
  if (!name) return null;
660
228
  const initializer = declaration.getInitializer();
661
229
  if (!Node.isCallExpression(initializer)) return null;
662
230
  if (!isRelationFunctionCall(initializer)) return null;
663
- const relArgs = initializer.getArguments();
664
- const baseIdentifier = relArgs.length && Node.isIdentifier(relArgs[0]) ? relArgs[0] : void 0;
665
- if (!baseIdentifier) return null;
231
+ const baseIdentifier = initializer.getArguments()[0];
232
+ if (!baseIdentifier || !Node.isIdentifier(baseIdentifier)) return null;
666
233
  const baseName = baseIdentifier.getText();
667
234
  return {
668
235
  name,
669
236
  baseName,
670
- fields: extractFieldsFromCall(initializer, sourceText),
237
+ fields: extractFieldsFromCall(initializer, sourceText, tag, prefix),
671
238
  objectType: baseSchemaMap.get(baseName)
672
239
  };
673
- }
674
- return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).flatMap((stmt) => stmt.getDeclarations()).map((decl) => extract(decl)).filter((schema) => schema !== null);
675
- }
676
- //#endregion
677
- //#region src/generator/arktype/index.ts
678
- /**
679
- * Generate ArkType type() definition from a schema
680
- * @param schema - The schema to generate ArkType definition from
681
- * @param comment - Whether to include JSDoc comments in the generated code
682
- */
683
- function arktype(schema, comment) {
684
- const inner = fieldDefinitions(schema, comment);
685
- const undeclared = resolveArktypeUndeclared(schema.objectType);
686
- return `export const ${makeCapitalized(schema.name)}Schema = type({${undeclared}${inner}})`;
687
- }
688
- /**
689
- * Generate ArkType schema code with optional type inference
690
- * @param schema - The schema to generate code from
691
- * @param comment - Whether to include JSDoc comments in the generated code
692
- * @param type - Whether to include type inference
693
- */
694
- function arktypeCode(schema, comment, type) {
695
- const arktypeSchema = arktype(schema, comment);
696
- if (type) return `${arktypeSchema}\n\n${inferArktype(schema.name)}\n`;
697
- return `${arktypeSchema}\n`;
698
- }
699
- /**
700
- * Generate ArkType relation schema code with spread base schema
701
- * @param schema - The relation schema with base model reference
702
- * @param withType - Whether to include type inference
703
- */
704
- function makeRelationArktypeCode(schema, withType) {
705
- const relName = `${schema.name}Schema`;
706
- const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
707
- const fields = makeRelationFields(schema.fields);
708
- const undeclared = resolveArktypeUndeclared(schema.objectType);
709
- const obj = `\nexport const ${makeCapitalized(relName)} = type({${undeclared}...${baseSchema}.t,${fields}})`;
710
- if (withType) return `${obj}\n\n${inferArktype(schema.name)}\n`;
711
- return `${obj}`;
712
- }
713
- /**
714
- * Generate ArkType schema
715
- * @param code - The code to generate ArkType schema from
716
- * @param output - The output file path
717
- * @param comment - Whether to include comments in the generated code
718
- * @param type - Whether to include type information in the generated code
719
- * @param relation - Whether to include relation schemas in the generated code
720
- */
721
- async function sizukuArktype(code, output, comment, type, relation) {
722
- const importLine = `import { type } from 'arktype'`;
723
- const baseSchemas = extractSchemas(code, "arktype");
724
- const relationSchemas = extractRelationSchemas(code, "arktype");
725
- const arktypeGeneratedCode = [
726
- importLine,
727
- "",
728
- ...baseSchemas.map((schema) => arktypeCode(schema, comment ?? false, type ?? false)),
729
- ...relation ? relationSchemas.map((schema) => makeRelationArktypeCode(schema, type ?? false)) : []
730
- ].join("\n");
731
- const mkdirResult = await mkdir(path.dirname(output));
732
- if (!mkdirResult.ok) return {
733
- ok: false,
734
- error: mkdirResult.error
735
- };
736
- const writeFileResult = await writeFile(output, await fmt(arktypeGeneratedCode));
737
- if (!writeFileResult.ok) return {
738
- ok: false,
739
- error: writeFileResult.error
740
- };
741
- return {
742
- ok: true,
743
- value: void 0
744
- };
240
+ }).filter((schema) => schema !== null);
745
241
  }
746
242
  //#endregion
747
- //#region src/generator/mermaid-er/validator/index.ts
748
- /**
749
- * Create a unique key for relation deduplication.
750
- */
751
- function relationKey(r) {
752
- return `${r.fromModel}.${r.fromField}->${r.toModel}.${r.toField}`;
753
- }
754
- /**
755
- * Extract base builder name from expression.
756
- *
757
- * @param expr - The expression to extract name from
758
- * @returns The base builder name
759
- */
243
+ //#region src/helper/extract-tables.ts
760
244
  function baseBuilderName(expr) {
761
245
  if (Node.isIdentifier(expr)) return expr.getText();
762
246
  if (Node.isCallExpression(expr) || Node.isPropertyAccessExpression(expr)) return baseBuilderName(expr.getExpression());
763
247
  return "";
764
248
  }
765
- /**
766
- * Type guard for FieldInfo.
767
- *
768
- * @param v - The value to check
769
- * @returns True if value is FieldInfo
770
- */
771
- function isFieldInfo(v) {
772
- return v !== null;
773
- }
774
- /**
775
- * Extract key type based on field definition.
776
- *
777
- * @param initText - The initializer text
778
- * @returns The key type (PK, FK, or null)
779
- */
780
249
  function extractKeyType(initText) {
781
250
  if (initText.includes(".primaryKey()")) return "PK";
782
251
  if (initText.includes(".references(")) return "FK";
783
252
  return null;
784
253
  }
785
- /**
786
- * Find immediate comment for a field.
787
- *
788
- * @param code - The source code lines
789
- * @param lineIdx - The line index of the field
790
- * @returns The immediate comment or empty string
791
- */
792
254
  function findImmediateComment(code, lineIdx) {
793
255
  return code.slice(0, lineIdx).reverse().find((line) => {
794
256
  const t = line.trim();
795
257
  return t.startsWith("///") && !t.includes("@z.") && !t.includes("@v.") && !t.includes("@a.") && !t.includes("@e.") && !t.includes("@relation");
796
258
  })?.replace(/^\s*\/\/\/\s*/, "") ?? "";
797
259
  }
798
- /**
799
- * Extract reference info from a .references() call.
800
- *
801
- * @param initExpr - The call expression to analyze
802
- * @returns The reference info or null
803
- */
804
260
  function extractReferenceInfo(initExpr) {
805
261
  const match = initExpr.getText().match(/\.references\(\s*\(\)\s*=>\s*(\w+)\.(\w+)\s*\)/);
806
- if (match) return {
262
+ if (!match) return null;
263
+ return {
807
264
  referencedTable: match[1],
808
265
  referencedField: match[2]
809
266
  };
810
- return null;
811
267
  }
812
- /**
813
- * Check if field is required (notNull).
814
- *
815
- * @param initText - The initializer text
816
- * @returns True if field is required
817
- */
818
268
  function isFieldRequired(initText) {
819
269
  return initText.includes(".notNull()");
820
270
  }
821
- /**
822
- * Extract field info from property assignment.
823
- *
824
- * @param prop - The property assignment node
825
- * @param code - The source code lines
826
- * @returns Field info or null
827
- */
828
271
  function extractFieldInfo(prop, code) {
829
272
  const keyNode = prop.getNameNode();
830
273
  if (!Node.isIdentifier(keyNode)) return null;
@@ -841,13 +284,6 @@ function extractFieldInfo(prop, code) {
841
284
  description: immediateComment || null
842
285
  };
843
286
  }
844
- /**
845
- * Extract relation info from a property assignment with .references().
846
- *
847
- * @param prop - The property assignment node
848
- * @param tableName - The name of the current table
849
- * @returns Relation info or null
850
- */
851
287
  function extractRelationFromField(prop, tableName) {
852
288
  const keyNode = prop.getNameNode();
853
289
  if (!Node.isIdentifier(keyNode)) return null;
@@ -867,139 +303,88 @@ function extractRelationFromField(prop, tableName) {
867
303
  isRequired
868
304
  };
869
305
  }
870
- /**
871
- * Extract relations from foreignKey() constraints in the third argument.
872
- *
873
- * Pattern:
874
- * foreignKey({
875
- * columns: [TableName.fieldName],
876
- * foreignColumns: [OtherTable.fieldName],
877
- * })
878
- *
879
- * @param tableName - The name of the current table
880
- * @param constraintArg - The third argument (arrow function returning constraints object)
881
- * @returns Array of relation info
882
- */
883
306
  function extractRelationsFromForeignKeyConstraints(tableName, constraintArg) {
884
- const relations = [];
885
- if (!Node.isArrowFunction(constraintArg)) return relations;
307
+ if (!Node.isArrowFunction(constraintArg)) return [];
886
308
  const body = constraintArg.getBody();
887
- if (!body) return relations;
309
+ if (!body) return [];
888
310
  const objExpr = Node.isParenthesizedExpression(body) ? body.getExpression() : body;
889
- if (!Node.isObjectLiteralExpression(objExpr)) return relations;
890
- for (const prop of objExpr.getProperties()) {
891
- if (!Node.isPropertyAssignment(prop)) continue;
311
+ if (!Node.isObjectLiteralExpression(objExpr)) return [];
312
+ return objExpr.getProperties().flatMap((prop) => {
313
+ if (!Node.isPropertyAssignment(prop)) return [];
892
314
  const initExpr = prop.getInitializer();
893
- if (!initExpr) continue;
315
+ if (!initExpr) return [];
894
316
  const text = initExpr.getText();
895
- if (!text.includes("foreignKey(")) continue;
317
+ if (!text.includes("foreignKey(")) return [];
896
318
  const columnsMatch = text.match(/columns:\s*\[\s*(\w+)\.(\w+)\s*\]/);
897
319
  const foreignColumnsMatch = text.match(/foreignColumns:\s*\[\s*(\w+)\.(\w+)\s*\]/);
898
- if (columnsMatch && foreignColumnsMatch) {
899
- const toField = columnsMatch[2];
900
- const fromModel = foreignColumnsMatch[1];
901
- const fromField = foreignColumnsMatch[2];
902
- const isRequired = !text.includes(".nullable()");
903
- relations.push({
904
- fromModel,
905
- toModel: tableName,
906
- fromField,
907
- toField,
908
- isRequired
909
- });
910
- }
911
- }
912
- return relations;
320
+ if (!columnsMatch || !foreignColumnsMatch) return [];
321
+ return [{
322
+ fromModel: foreignColumnsMatch[1],
323
+ toModel: tableName,
324
+ fromField: foreignColumnsMatch[2],
325
+ toField: columnsMatch[2],
326
+ isRequired: !text.includes(".nullable()")
327
+ }];
328
+ });
329
+ }
330
+ function isFkLikeName(name) {
331
+ const lower = name.toLowerCase();
332
+ return lower.endsWith("id") && lower !== "id" || lower.endsWith("_id");
913
333
  }
914
- /**
915
- * Extract relations from relations() helper blocks.
916
- *
917
- * Pattern:
918
- * relations(TableRef, ({ one, many }) => ({
919
- * user: one(User, {
920
- * fields: [Post.userId],
921
- * references: [User.id],
922
- * }),
923
- * posts: many(Post),
924
- * }))
925
- *
926
- * @param file - The source file
927
- * @returns Array of relation info
928
- */
929
334
  function extractRelationsFromRelationBlocks(file) {
930
- const relations = [];
931
- for (const stmt of file.getVariableStatements().filter((stmt) => stmt.isExported())) {
335
+ return file.getVariableStatements().filter((stmt) => stmt.isExported()).flatMap((stmt) => {
932
336
  const decl = stmt.getDeclarations()[0];
933
- if (!Node.isVariableDeclaration(decl)) continue;
934
- if (!decl.getName().toLowerCase().includes("relation")) continue;
337
+ if (!Node.isVariableDeclaration(decl)) return [];
338
+ if (!decl.getName().toLowerCase().includes("relation")) return [];
935
339
  const init = decl.getInitializer();
936
- if (!(init && Node.isCallExpression(init))) continue;
937
- if (init.getExpression().getText() !== "relations") continue;
340
+ if (!(init && Node.isCallExpression(init))) return [];
341
+ if (init.getExpression().getText() !== "relations") return [];
938
342
  const args = init.getArguments();
939
- if (args.length < 2) continue;
343
+ if (args.length < 2) return [];
940
344
  const tableRef = args[0];
941
- if (!Node.isIdentifier(tableRef)) continue;
942
- const tableName = tableRef.getText();
943
345
  const arrowFn = args[1];
944
- if (!Node.isArrowFunction(arrowFn)) continue;
346
+ if (!Node.isIdentifier(tableRef) || !Node.isArrowFunction(arrowFn)) return [];
347
+ const tableName = tableRef.getText();
945
348
  const body = arrowFn.getBody();
946
- if (!body) continue;
349
+ if (!body) return [];
947
350
  const objExpr = Node.isParenthesizedExpression(body) ? body.getExpression() : body;
948
- if (!Node.isObjectLiteralExpression(objExpr)) continue;
949
- for (const prop of objExpr.getProperties()) {
950
- if (!Node.isPropertyAssignment(prop)) continue;
351
+ if (!Node.isObjectLiteralExpression(objExpr)) return [];
352
+ return objExpr.getProperties().flatMap((prop) => {
353
+ if (!Node.isPropertyAssignment(prop)) return [];
951
354
  const initExpr = prop.getInitializer();
952
- if (!(initExpr && Node.isCallExpression(initExpr))) continue;
953
- if (initExpr.getExpression().getText() !== "one") continue;
355
+ if (!(initExpr && Node.isCallExpression(initExpr))) return [];
356
+ if (initExpr.getExpression().getText() !== "one") return [];
954
357
  const relArgs = initExpr.getArguments();
955
- if (relArgs.length < 2) continue;
358
+ if (relArgs.length < 2) return [];
956
359
  const refTableArg = relArgs[0];
957
- if (!Node.isIdentifier(refTableArg)) continue;
958
- const refTable = refTableArg.getText();
959
360
  const configArg = relArgs[1];
960
- if (!Node.isObjectLiteralExpression(configArg)) continue;
361
+ if (!Node.isIdentifier(refTableArg) || !Node.isObjectLiteralExpression(configArg)) return [];
362
+ const refTable = refTableArg.getText();
961
363
  const configText = configArg.getText();
962
364
  const fieldsMatch = configText.match(/fields:\s*\[\s*(\w+)\.(\w+)\s*\]/);
963
365
  const referencesMatch = configText.match(/references:\s*\[\s*(\w+)\.(\w+)\s*\]/);
964
- if (fieldsMatch && referencesMatch) {
965
- const currentField = fieldsMatch[2];
966
- const refField = referencesMatch[2];
967
- const currentFieldLower = currentField.toLowerCase();
968
- const refFieldLower = refField.toLowerCase();
969
- const isCurrentFieldFk = currentFieldLower.endsWith("id") && currentFieldLower !== "id" || currentFieldLower.endsWith("_id");
970
- const isRefFieldFk = refFieldLower.endsWith("id") && refFieldLower !== "id" || refFieldLower.endsWith("_id");
971
- if (isCurrentFieldFk && !isRefFieldFk) relations.push({
972
- fromModel: refTable,
973
- toModel: tableName,
974
- fromField: refField,
975
- toField: currentField,
976
- isRequired: true
977
- });
978
- else if (!isCurrentFieldFk && isRefFieldFk) relations.push({
979
- fromModel: tableName,
980
- toModel: refTable,
981
- fromField: currentField,
982
- toField: refField,
983
- isRequired: true
984
- });
985
- else relations.push({
986
- fromModel: refTable,
987
- toModel: tableName,
988
- fromField: refField,
989
- toField: currentField,
990
- isRequired: true
991
- });
992
- }
993
- }
994
- }
995
- return relations;
366
+ if (!fieldsMatch || !referencesMatch) return [];
367
+ const currentField = fieldsMatch[2];
368
+ const refField = referencesMatch[2];
369
+ const currentIsFk = isFkLikeName(currentField);
370
+ const refIsFk = isFkLikeName(refField);
371
+ if (!currentIsFk && refIsFk) return [{
372
+ fromModel: tableName,
373
+ toModel: refTable,
374
+ fromField: currentField,
375
+ toField: refField,
376
+ isRequired: true
377
+ }];
378
+ return [{
379
+ fromModel: refTable,
380
+ toModel: tableName,
381
+ fromField: refField,
382
+ toField: currentField,
383
+ isRequired: true
384
+ }];
385
+ });
386
+ });
996
387
  }
997
- /**
998
- * Parse table information from Drizzle schema code.
999
- *
1000
- * @param code - Array of source code lines
1001
- * @returns Array of table information
1002
- */
1003
388
  function parseTableInfo(code) {
1004
389
  const source = code.join("\n");
1005
390
  return new Project({ useInMemoryFileSystem: true }).createSourceFile("temp.ts", source).getVariableStatements().filter((stmt) => stmt.isExported()).flatMap((stmt) => {
@@ -1015,61 +400,71 @@ function parseTableInfo(code) {
1015
400
  if (!(objLit && Node.isObjectLiteralExpression(objLit))) return [];
1016
401
  return [{
1017
402
  name: varName,
1018
- fields: objLit.getProperties().filter(Node.isPropertyAssignment).map((prop) => extractFieldInfo(prop, code)).filter(isFieldInfo)
403
+ fields: objLit.getProperties().filter(Node.isPropertyAssignment).map((prop) => extractFieldInfo(prop, code)).filter((f) => f !== null)
1019
404
  }];
1020
405
  });
1021
406
  }
1022
- /**
1023
- * Extract relations from Drizzle schema code by analyzing:
1024
- * 1. .references() calls on fields
1025
- * 2. foreignKey() constraints in table definition
1026
- * 3. relations() helper blocks
1027
- *
1028
- * @param code - Array of source code lines
1029
- * @returns Array of relation information (deduplicated)
1030
- */
1031
407
  function extractRelationsFromSchema(code) {
1032
408
  const source = code.join("\n");
1033
409
  const file = new Project({ useInMemoryFileSystem: true }).createSourceFile("temp.ts", source);
1034
- const relations = [];
1035
- for (const stmt of file.getVariableStatements().filter((stmt) => stmt.isExported())) {
410
+ const allRelations = [...file.getVariableStatements().filter((stmt) => stmt.isExported()).flatMap((stmt) => {
1036
411
  const decl = stmt.getDeclarations()[0];
1037
- if (!Node.isVariableDeclaration(decl)) continue;
412
+ if (!Node.isVariableDeclaration(decl)) return [];
1038
413
  const varName = decl.getName();
1039
- if (varName.toLowerCase().includes("relation")) continue;
414
+ if (varName.toLowerCase().includes("relation")) return [];
1040
415
  const init = decl.getInitializer();
1041
- if (!(init && Node.isCallExpression(init))) continue;
416
+ if (!(init && Node.isCallExpression(init))) return [];
1042
417
  const callee = init.getExpression().getText();
1043
- if (!callee.endsWith("Table") || callee === "relations") continue;
418
+ if (!callee.endsWith("Table") || callee === "relations") return [];
1044
419
  const args = init.getArguments();
1045
420
  const objLit = args[1];
1046
- if (objLit && Node.isObjectLiteralExpression(objLit)) for (const prop of objLit.getProperties().filter(Node.isPropertyAssignment)) {
1047
- const relation = extractRelationFromField(prop, varName);
1048
- if (relation) relations.push(relation);
1049
- }
1050
421
  const constraintArg = args[2];
1051
- if (constraintArg && Node.isExpression(constraintArg)) {
1052
- const fkRelations = extractRelationsFromForeignKeyConstraints(varName, constraintArg);
1053
- relations.push(...fkRelations);
1054
- }
1055
- }
1056
- const relationBlockRelations = extractRelationsFromRelationBlocks(file);
1057
- relations.push(...relationBlockRelations);
422
+ const fieldRelations = objLit && Node.isObjectLiteralExpression(objLit) ? objLit.getProperties().filter(Node.isPropertyAssignment).map((prop) => extractRelationFromField(prop, varName)).filter((r) => r !== null) : [];
423
+ const fkRelations = constraintArg && Node.isExpression(constraintArg) ? extractRelationsFromForeignKeyConstraints(varName, constraintArg) : [];
424
+ return [...fieldRelations, ...fkRelations];
425
+ }), ...extractRelationsFromRelationBlocks(file)];
1058
426
  const seen = /* @__PURE__ */ new Set();
1059
- return relations.filter((r) => {
1060
- const key = relationKey(r);
427
+ return allRelations.filter((r) => {
428
+ const key = `${r.fromModel}.${r.fromField}->${r.toModel}.${r.toField}`;
1061
429
  if (seen.has(key)) return false;
1062
430
  seen.add(key);
1063
431
  return true;
1064
432
  });
1065
433
  }
1066
434
  //#endregion
1067
- //#region src/generator/dbml/generator/dbml-content.ts
1068
- /**
1069
- * Map Drizzle column types to DBML types
1070
- */
1071
- function mapDrizzleType(drizzleType) {
1072
- return {
435
+ //#region src/generator/arktype.ts
436
+ function undeclared(objectType) {
437
+ if (objectType === "strict") return "\"+\":\"reject\",";
438
+ if (objectType === "loose") return "\"+\":\"ignore\",";
439
+ return "";
440
+ }
441
+ function arktype(code, comment, type, relation) {
442
+ const c = comment ?? false;
443
+ const t = type ?? false;
444
+ const baseLines = extractSchemas(code, "arktype").map((schema) => {
445
+ const cap = makeCapitalized(schema.name);
446
+ const head = `export const ${cap}Schema = type({${undeclared(schema.objectType)}${fieldDefinitions(schema, c)}})`;
447
+ return t ? `${head}\n\nexport type ${cap} = typeof ${cap}Schema.infer\n` : `${head}\n`;
448
+ });
449
+ const relationLines = relation ? extractRelationSchemas(code, "arktype").map((schema) => {
450
+ const relSchema = makeCapitalized(`${schema.name}Schema`);
451
+ const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
452
+ const obj = `\nexport const ${relSchema} = type({${undeclared(schema.objectType)}...${baseSchema}.t,${makeRelationFields(schema.fields)}})`;
453
+ if (!t) return obj;
454
+ const cap = makeCapitalized(schema.name);
455
+ return `${obj}\n\nexport type ${cap} = typeof ${cap}Schema.infer\n`;
456
+ }) : [];
457
+ return [
458
+ `import { type } from 'arktype'`,
459
+ "",
460
+ ...baseLines,
461
+ ...relationLines
462
+ ].join("\n");
463
+ }
464
+ //#endregion
465
+ //#region src/generator/dbml.ts
466
+ function dbml(code) {
467
+ const TYPE_MAP = {
1073
468
  serial: "serial",
1074
469
  smallserial: "smallserial",
1075
470
  bigserial: "bigserial",
@@ -1095,221 +490,148 @@ function mapDrizzleType(drizzleType) {
1095
490
  uuid: "uuid",
1096
491
  blob: "blob",
1097
492
  bytea: "bytea"
1098
- }[drizzleType] ?? drizzleType;
1099
- }
1100
- function escapeNote(str) {
1101
- return str.replace(/'/g, "\\'");
1102
- }
1103
- function quote(value) {
1104
- return `'${escapeNote(value)}'`;
1105
- }
1106
- function makeColumnConstraints(column) {
1107
- const constraints = [];
1108
- if (column.isPrimaryKey) constraints.push("pk");
1109
- if (column.isIncrement) constraints.push("increment");
1110
- if (column.isUnique) constraints.push("unique");
1111
- if (column.isNotNull && !column.isPrimaryKey) constraints.push("not null");
1112
- if (column.defaultValue !== void 0) constraints.push(`default: ${column.defaultValue}`);
1113
- if (column.note) constraints.push(`note: ${quote(column.note)}`);
1114
- return constraints;
1115
- }
1116
- function formatConstraints(constraints) {
1117
- return constraints.length > 0 ? ` [${constraints.join(", ")}]` : "";
1118
- }
1119
- function makeColumn(column) {
1120
- const constraints = makeColumnConstraints(column);
1121
- return ` ${column.name} ${column.type}${formatConstraints(constraints)}`;
1122
- }
1123
- function makeTable(table) {
1124
- const lines = [];
1125
- lines.push(`Table ${table.name} {`);
1126
- for (const column of table.columns) lines.push(makeColumn(column));
1127
- if (table.note) {
1128
- lines.push("");
1129
- lines.push(` Note: ${quote(table.note)}`);
1130
- }
1131
- lines.push("}");
1132
- return lines.join("\n");
1133
- }
1134
- function makeEnum(enumDef) {
1135
- const lines = [];
1136
- lines.push(`Enum ${enumDef.name} {`);
1137
- for (const value of enumDef.values) lines.push(` ${value}`);
1138
- lines.push("}");
1139
- return lines.join("\n");
1140
- }
1141
- function makeRefName(ref) {
1142
- return ref.name ?? `${ref.fromTable}_${ref.fromColumn}_${ref.toTable}_${ref.toColumn}_fk`;
1143
- }
1144
- function makeRef(ref) {
1145
- const name = makeRefName(ref);
1146
- const operator = ref.type ?? ">";
1147
- const actions = [];
1148
- if (ref.onDelete) actions.push(`delete: ${ref.onDelete}`);
1149
- if (ref.onUpdate) actions.push(`update: ${ref.onUpdate}`);
1150
- const actionStr = actions.length > 0 ? ` [${actions.join(", ")}]` : "";
1151
- return `Ref ${name}: ${ref.fromTable}.${ref.fromColumn} ${operator} ${ref.toTable}.${ref.toColumn}${actionStr}`;
1152
- }
1153
- function makeDBMLContent(options) {
1154
- const sections = [];
1155
- if (options.enums) for (const enumDef of options.enums) sections.push(makeEnum(enumDef));
1156
- for (const table of options.tables) sections.push(makeTable(table));
1157
- if (options.refs) for (const ref of options.refs) sections.push(makeRef(ref));
1158
- return sections.join("\n\n");
1159
- }
1160
- /**
1161
- * Convert internal FieldInfo to DBMLColumn
1162
- */
1163
- function toDBMLColumn(field) {
1164
- return {
1165
- name: field.name,
1166
- type: mapDrizzleType(field.type),
1167
- isPrimaryKey: field.keyType === "PK",
1168
- isIncrement: field.type.includes("serial"),
1169
- note: field.description ?? void 0
1170
- };
1171
- }
1172
- /**
1173
- * Convert internal TableInfo to DBMLTable
1174
- */
1175
- function toDBMLTable(table) {
1176
- return {
1177
- name: table.name,
1178
- columns: table.fields.map(toDBMLColumn)
1179
493
  };
494
+ const tables = parseTableInfo(code);
495
+ const relations = extractRelationsFromSchema(code);
496
+ const tableSections = tables.map((table) => {
497
+ const columns = table.fields.map((field) => {
498
+ const type = TYPE_MAP[field.type] ?? field.type;
499
+ const constraints = [
500
+ field.keyType === "PK" ? "pk" : null,
501
+ field.type.includes("serial") ? "increment" : null,
502
+ field.description ? `note: '${field.description.replace(/'/g, "\\'")}'` : null
503
+ ].filter((c) => c !== null);
504
+ const constraintStr = constraints.length > 0 ? ` [${constraints.join(", ")}]` : "";
505
+ return ` ${field.name} ${type}${constraintStr}`;
506
+ });
507
+ return [
508
+ `Table ${table.name} {`,
509
+ ...columns,
510
+ "}"
511
+ ].join("\n");
512
+ });
513
+ const refSections = relations.map((r) => {
514
+ const fromTable = r.toModel;
515
+ const fromColumn = r.toField;
516
+ const toTable = r.fromModel;
517
+ const toColumn = r.fromField;
518
+ return `Ref ${`${fromTable}_${fromColumn}_${toTable}_${toColumn}_fk`}: ${fromTable}.${fromColumn} > ${toTable}.${toColumn}`;
519
+ });
520
+ return [...tableSections, ...refSections].join("\n\n");
1180
521
  }
1181
- /**
1182
- * Convert internal RelationInfo to DBMLRef
1183
- */
1184
- function toDBMLRef(relation) {
1185
- return {
1186
- fromTable: relation.toModel,
1187
- fromColumn: relation.toField,
1188
- toTable: relation.fromModel,
1189
- toColumn: relation.fromField
1190
- };
1191
- }
1192
- /**
1193
- * Generate complete DBML content
1194
- *
1195
- * @param relations - The relations extracted from the schema
1196
- * @param tables - The tables to generate DBML from
1197
- * @returns The generated DBML content
1198
- */
1199
- function dbmlContent(relations, tables) {
1200
- return makeDBMLContent({
1201
- tables: tables.map(toDBMLTable),
1202
- refs: relations.map(toDBMLRef)
522
+ //#endregion
523
+ //#region src/generator/effect.ts
524
+ function effect(code, comment, type, relation) {
525
+ const c = comment ?? false;
526
+ const t = type ?? false;
527
+ const baseLines = extractSchemas(code, "effect").map((schema) => {
528
+ const cap = makeCapitalized(schema.name);
529
+ const head = `export const ${cap}Schema = Schema.Struct({${fieldDefinitions(schema, c)}})`;
530
+ return t ? `${head}\n\nexport type ${cap}Encoded = typeof ${cap}Schema.Encoded\n` : `${head}\n`;
1203
531
  });
532
+ const relationLines = relation ? extractRelationSchemas(code, "effect").map((schema) => {
533
+ const obj = `\nexport const ${makeCapitalized(`${schema.name}Schema`)} = Schema.Struct({...${`${makeCapitalized(schema.baseName)}Schema`}.fields,${makeRelationFields(schema.fields)}})`;
534
+ if (!t) return obj;
535
+ const cap = makeCapitalized(schema.name);
536
+ return `${obj}\n\nexport type ${cap}Encoded = typeof ${cap}Schema.Encoded\n`;
537
+ }) : [];
538
+ return [
539
+ `import { Schema } from 'effect'`,
540
+ "",
541
+ ...baseLines,
542
+ ...relationLines
543
+ ].join("\n");
1204
544
  }
1205
545
  //#endregion
1206
- //#region src/generator/dbml/index.ts
1207
- /**
1208
- * Generate DBML content string from Drizzle schema code
1209
- */
1210
- function generateContent(code) {
1211
- const tables = parseTableInfo(code);
1212
- return dbmlContent(extractRelationsFromSchema(code), tables);
1213
- }
1214
- /**
1215
- * Generate DBML file from Drizzle schema code
1216
- *
1217
- * @param code - The code to generate DBML from
1218
- * @param output - The output file path (must end with .dbml)
1219
- */
1220
- async function sizukuDbmlFile(code, output) {
1221
- const content = generateContent(code);
1222
- const mkdirResult = await mkdir(dirname(output));
1223
- if (!mkdirResult.ok) return {
1224
- ok: false,
1225
- error: `❌ Failed to create directory: ${mkdirResult.error}`
1226
- };
1227
- const writeResult = await writeFile(output, content);
1228
- if (!writeResult.ok) return {
1229
- ok: false,
1230
- error: `❌ Failed to write DBML: ${writeResult.error}`
1231
- };
1232
- return {
1233
- ok: true,
1234
- value: void 0
1235
- };
1236
- }
1237
- /**
1238
- * Generate PNG ER diagram from Drizzle schema code
1239
- *
1240
- * @param code - The code to generate PNG from
1241
- * @param output - The output file path (must end with .png)
1242
- */
1243
- /**
1244
- * Generate ERD output (DBML or PNG) based on file extension
1245
- */
1246
- async function sizukuDbml(code, output) {
1247
- if (output.endsWith(".png")) return sizukuPng(code, output);
1248
- return sizukuDbmlFile(code, output);
1249
- }
1250
- async function sizukuPng(code, output) {
1251
- const pngBuffer = new Resvg(run(generateContent(code), "svg"), { font: { loadSystemFonts: true } }).render().asPng();
1252
- const mkdirResult = await mkdir(dirname(output));
1253
- if (!mkdirResult.ok) return {
1254
- ok: false,
1255
- error: `❌ Failed to create directory: ${mkdirResult.error}`
1256
- };
1257
- const writeResult = await writeFileBinary(output, pngBuffer);
1258
- if (!writeResult.ok) return {
546
+ //#region src/format/index.ts
547
+ async function fmt(input) {
548
+ const { code, errors } = await format("<stdin>.ts", input, {
549
+ printWidth: 100,
550
+ singleQuote: true,
551
+ semi: false
552
+ });
553
+ if (errors.length > 0) return {
1259
554
  ok: false,
1260
- error: `❌ Failed to write PNG: ${writeResult.error}`
555
+ error: errors.map((e) => e.message).join("\n")
1261
556
  };
1262
557
  return {
1263
558
  ok: true,
1264
- value: void 0
559
+ value: code
1265
560
  };
1266
561
  }
1267
562
  //#endregion
1268
- //#region src/generator/effect/index.ts
1269
- function effect(schema, comment) {
1270
- const inner = fieldDefinitions(schema, comment);
1271
- return `export const ${makeCapitalized(schema.name)}Schema = Schema.Struct({${inner}})`;
563
+ //#region src/fsp/index.ts
564
+ function readFileSync(path) {
565
+ try {
566
+ return {
567
+ ok: true,
568
+ value: fs.readFileSync(path, "utf-8")
569
+ };
570
+ } catch (e) {
571
+ return {
572
+ ok: false,
573
+ error: e instanceof Error ? e.message : String(e)
574
+ };
575
+ }
576
+ }
577
+ async function mkdir(dir) {
578
+ try {
579
+ await fsp.mkdir(dir, { recursive: true });
580
+ return {
581
+ ok: true,
582
+ value: void 0
583
+ };
584
+ } catch (e) {
585
+ return {
586
+ ok: false,
587
+ error: e instanceof Error ? e.message : String(e)
588
+ };
589
+ }
1272
590
  }
1273
- function effectCode(schema, comment, type) {
1274
- const effectSchema = effect(schema, comment);
1275
- if (type) return `${effectSchema}\n\n${inferEffect(schema.name)}\n`;
1276
- return `${effectSchema}\n`;
591
+ async function writeFile(path, data) {
592
+ try {
593
+ await fsp.writeFile(path, data, "utf-8");
594
+ return {
595
+ ok: true,
596
+ value: void 0
597
+ };
598
+ } catch (e) {
599
+ return {
600
+ ok: false,
601
+ error: e instanceof Error ? e.message : String(e)
602
+ };
603
+ }
1277
604
  }
1278
- function makeRelationEffectCode(schema, withType) {
1279
- const relName = `${schema.name}Schema`;
1280
- const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
1281
- const fields = makeRelationFields(schema.fields);
1282
- const obj = `\nexport const ${makeCapitalized(relName)} = Schema.Struct({...${baseSchema}.fields,${fields}})`;
1283
- if (withType) return `${obj}\n\n${inferEffect(schema.name)}\n`;
1284
- return `${obj}`;
605
+ async function writeFileBinary(path, data) {
606
+ try {
607
+ await fsp.writeFile(path, data);
608
+ return {
609
+ ok: true,
610
+ value: void 0
611
+ };
612
+ } catch (e) {
613
+ return {
614
+ ok: false,
615
+ error: e instanceof Error ? e.message : String(e)
616
+ };
617
+ }
1285
618
  }
1286
- /**
1287
- * Generate Effect schema
1288
- * @param code - The code to generate Effect schema from
1289
- * @param output - The output file path
1290
- * @param comment - Whether to include comments in the generated code
1291
- * @param type - Whether to include type information in the generated code
1292
- * @param relation - Whether to include relation schemas in the generated code
1293
- */
1294
- async function sizukuEffect(code, output, comment, type, relation) {
1295
- const importLine = `import { Schema } from 'effect'`;
1296
- const baseSchemas = extractSchemas(code, "effect");
1297
- const relationSchemas = extractRelationSchemas(code, "effect");
1298
- const effectGeneratedCode = [
1299
- importLine,
1300
- "",
1301
- ...baseSchemas.map((schema) => effectCode(schema, comment ?? false, type ?? false)),
1302
- ...relation ? relationSchemas.map((schema) => makeRelationEffectCode(schema, type ?? false)) : []
1303
- ].join("\n");
1304
- const mkdirResult = await mkdir(path.dirname(output));
619
+ //#endregion
620
+ //#region src/generator/emit.ts
621
+ async function emit(code, dir, output) {
622
+ const [fmtResult, mkdirResult] = await Promise.all([fmt(code), mkdir(dir)]);
623
+ if (!fmtResult.ok) return {
624
+ ok: false,
625
+ error: fmtResult.error
626
+ };
1305
627
  if (!mkdirResult.ok) return {
1306
628
  ok: false,
1307
629
  error: mkdirResult.error
1308
630
  };
1309
- const writeFileResult = await writeFile(output, await fmt(effectGeneratedCode));
1310
- if (!writeFileResult.ok) return {
631
+ const writeResult = await writeFile(output, fmtResult.value);
632
+ if (!writeResult.ok) return {
1311
633
  ok: false,
1312
- error: writeFileResult.error
634
+ error: writeResult.error
1313
635
  };
1314
636
  return {
1315
637
  ok: true,
@@ -1317,43 +639,11 @@ async function sizukuEffect(code, output, comment, type, relation) {
1317
639
  };
1318
640
  }
1319
641
  //#endregion
1320
- //#region src/generator/mermaid-er/generator/er-content.ts
1321
- const ER_HEADER = ["```mermaid", "erDiagram"];
1322
- const ER_FOOTER = ["```"];
1323
- const RELATIONSHIPS = {
1324
- "zero-one": "|o",
1325
- one: "||",
1326
- "zero-many": "}o",
1327
- many: "}|"
1328
- };
1329
- /**
1330
- * Generate relation line from relation info.
1331
- *
1332
- * @param relation - The relation info
1333
- * @returns The Mermaid ER relation line
1334
- */
1335
- function makeRelationLine(relation) {
1336
- const fromSymbol = RELATIONSHIPS.one;
1337
- const toSymbol = relation.isRequired ? RELATIONSHIPS.many : RELATIONSHIPS["zero-many"];
1338
- return ` ${relation.fromModel} ${fromSymbol}--${toSymbol} ${relation.toModel} : "(${relation.fromField}) - (${relation.toField})"`;
1339
- }
1340
- /**
1341
- * Remove duplicate relations.
1342
- *
1343
- * @param relations - The relations to deduplicate
1344
- * @returns The deduplicated relations
1345
- */
1346
- function removeDuplicateRelations(relations) {
1347
- return [...new Set(relations)];
1348
- }
1349
- /**
1350
- * Generate ER content
1351
- * @param relations - The relations extracted from .references() calls
1352
- * @param tables - The tables to generate the ER content from
1353
- * @returns The generated ER content
1354
- */
1355
- function erContent(relations, tables) {
1356
- const relationLines = removeDuplicateRelations(relations.map(makeRelationLine));
642
+ //#region src/generator/mermaid-er.ts
643
+ function mermaidER(code) {
644
+ const tables = parseTableInfo(code);
645
+ const relations = extractRelationsFromSchema(code);
646
+ const relationLines = [...new Set(relations.map((r) => ` ${r.fromModel} ||--${r.isRequired ? "}|" : "}o"} ${r.toModel} : "(${r.fromField}) - (${r.toField})"`))];
1357
647
  const tableDefinitions = tables.flatMap((table) => [
1358
648
  ` ${table.name} {`,
1359
649
  ...table.fields.map((field) => {
@@ -1364,83 +654,93 @@ function erContent(relations, tables) {
1364
654
  " }"
1365
655
  ]);
1366
656
  return [
1367
- ...ER_HEADER,
657
+ "```mermaid",
658
+ "erDiagram",
1368
659
  ...relationLines,
1369
660
  ...tableDefinitions,
1370
- ...ER_FOOTER
661
+ "```"
1371
662
  ].join("\n");
1372
663
  }
1373
664
  //#endregion
1374
- //#region src/generator/mermaid-er/index.ts
1375
- /**
1376
- * Generate Mermaid ER diagram
1377
- * @param code - The code to generate Mermaid ER diagram from
1378
- * @param output - The output file path
1379
- */
1380
- async function sizukuMermaidER(code, output) {
1381
- const tables = parseTableInfo(code);
1382
- const ERContent = erContent(extractRelationsFromSchema(code), tables);
1383
- const mkdirResult = await mkdir(path.dirname(output));
1384
- if (!mkdirResult.ok) return {
1385
- ok: false,
1386
- error: mkdirResult.error
1387
- };
1388
- const writeFileResult = await writeFile(output, ERContent);
1389
- if (!writeFileResult.ok) return {
1390
- ok: false,
1391
- error: writeFileResult.error
1392
- };
1393
- return {
1394
- ok: true,
1395
- value: void 0
1396
- };
665
+ //#region src/generator/valibot.ts
666
+ function valibot(code, comment, type, relation) {
667
+ const c = comment ?? false;
668
+ const t = type ?? false;
669
+ const baseLines = extractSchemas(code, "valibot").map((schema) => {
670
+ const cap = makeCapitalized(schema.name);
671
+ const head = `export const ${cap}Schema = v.${resolveWrapperType(schema.objectType)}({${fieldDefinitions(schema, c)}})`;
672
+ return t ? `${head}\n\nexport type ${cap} = v.InferOutput<typeof ${cap}Schema>\n` : `${head}\n`;
673
+ });
674
+ const relationLines = relation ? extractRelationSchemas(code, "valibot").map((schema) => {
675
+ const relSchema = makeCapitalized(`${schema.name}Schema`);
676
+ const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
677
+ const obj = `\nexport const ${relSchema} = v.${resolveWrapperType(schema.objectType)}({...${baseSchema}.entries,${makeRelationFields(schema.fields)}})`;
678
+ if (!t) return obj;
679
+ const cap = makeCapitalized(schema.name);
680
+ return `${obj}\n\nexport type ${cap} = v.InferOutput<typeof ${cap}Schema>\n`;
681
+ }) : [];
682
+ return [
683
+ "import * as v from 'valibot'",
684
+ "",
685
+ ...baseLines,
686
+ ...relationLines
687
+ ].join("\n");
1397
688
  }
1398
689
  //#endregion
1399
- //#region src/generator/valibot/index.ts
1400
- function valibot(schema, comment) {
1401
- const wrapperType = resolveWrapperType(schema.objectType);
1402
- const objectCode = makeValibotObject(fieldDefinitions(schema, comment), wrapperType);
1403
- return `export const ${makeCapitalized(schema.name)}Schema = ${objectCode}`;
1404
- }
1405
- function valibotCode(schema, comment, type) {
1406
- const valibotSchema = valibot(schema, comment);
1407
- if (type) return `${valibotSchema}\n\n${inferOutput(schema.name)}\n`;
1408
- return `${valibotSchema}\n`;
1409
- }
1410
- function relationValibotCode(schema, withType) {
1411
- const relName = `${schema.name}Schema`;
1412
- const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
1413
- const fields = makeRelationFields(schema.fields);
1414
- const objectType = resolveWrapperType(schema.objectType);
1415
- const obj = `\nexport const ${makeCapitalized(relName)} = v.${objectType}({...${baseSchema}.entries,${fields}})`;
1416
- if (withType) return `${obj}\n\n${inferOutput(schema.name)}\n`;
1417
- return `${obj}`;
1418
- }
1419
- /**
1420
- * Generate Valibot schema
1421
- * @param code - The code to generate Valibot schema from
1422
- * @param output - The output file path
1423
- * @param comment - Whether to include comments in the generated code
1424
- * @param type - Whether to include type information in the generated code
1425
- */
1426
- async function sizukuValibot(code, output, comment, type, relation) {
1427
- const baseSchemas = extractSchemas(code, "valibot");
1428
- const relationSchemas = extractRelationSchemas(code, "valibot");
1429
- const valibotGeneratedCode = [
1430
- "import * as v from 'valibot'",
690
+ //#region src/generator/zod.ts
691
+ function zod(code, comment, type, version, relation) {
692
+ const c = comment ?? false;
693
+ const t = type ?? false;
694
+ const importLine = version === "mini" ? `import * as z from 'zod/mini'` : version === "@hono/zod-openapi" ? `import { z } from '@hono/zod-openapi'` : `import * as z from 'zod'`;
695
+ const baseLines = extractSchemas(code, "zod").map((schema) => {
696
+ const cap = makeCapitalized(schema.name);
697
+ const head = `export const ${cap}Schema = z.${resolveWrapperType(schema.objectType)}({${fieldDefinitions(schema, c)}})`;
698
+ return t ? `${head}\n\nexport type ${cap} = z.infer<typeof ${cap}Schema>\n` : `${head}\n`;
699
+ });
700
+ const relationLines = relation ? extractRelationSchemas(code, "zod").map((schema) => {
701
+ const relSchema = makeCapitalized(`${schema.name}Schema`);
702
+ const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
703
+ const obj = `\nexport const ${relSchema} = z.${resolveWrapperType(schema.objectType)}({...${baseSchema}.shape,${makeRelationFields(schema.fields)}})`;
704
+ if (!t) return obj;
705
+ const cap = makeCapitalized(schema.name);
706
+ return `${obj}\n\nexport type ${cap} = z.infer<typeof ${cap}Schema>\n`;
707
+ }) : [];
708
+ return [
709
+ importLine,
1431
710
  "",
1432
- ...baseSchemas.map((schema) => valibotCode(schema, comment ?? false, type ?? false)),
1433
- ...relation ? relationSchemas.map((schema) => relationValibotCode(schema, type ?? false)) : []
711
+ ...baseLines,
712
+ ...relationLines
1434
713
  ].join("\n");
1435
- const mkdirResult = await mkdir(path.dirname(output));
714
+ }
715
+ //#endregion
716
+ //#region src/core/arktype.ts
717
+ function sizukuArktype(code, output, comment, type, relation) {
718
+ return emit(arktype(code, comment, type, relation), path.dirname(output), output);
719
+ }
720
+ //#endregion
721
+ //#region src/core/dbml.ts
722
+ async function sizukuDbml(code, output) {
723
+ const content = dbml(code);
724
+ const mkdirResult = await mkdir(dirname(output));
1436
725
  if (!mkdirResult.ok) return {
1437
726
  ok: false,
1438
- error: mkdirResult.error
727
+ error: `❌ Failed to create directory: ${mkdirResult.error}`
1439
728
  };
1440
- const writeFileResult = await writeFile(output, await fmt(valibotGeneratedCode));
1441
- if (!writeFileResult.ok) return {
729
+ if (output.endsWith(".png")) {
730
+ const writeResult = await writeFileBinary(output, new Resvg(run(content, "svg"), { font: { loadSystemFonts: true } }).render().asPng());
731
+ if (!writeResult.ok) return {
732
+ ok: false,
733
+ error: `❌ Failed to write PNG: ${writeResult.error}`
734
+ };
735
+ return {
736
+ ok: true,
737
+ value: void 0
738
+ };
739
+ }
740
+ const writeResult = await writeFile(output, content);
741
+ if (!writeResult.ok) return {
1442
742
  ok: false,
1443
- error: writeFileResult.error
743
+ error: `❌ Failed to write DBML: ${writeResult.error}`
1444
744
  };
1445
745
  return {
1446
746
  ok: true,
@@ -1448,50 +748,20 @@ async function sizukuValibot(code, output, comment, type, relation) {
1448
748
  };
1449
749
  }
1450
750
  //#endregion
1451
- //#region src/generator/zod/index.ts
1452
- function zod(schema, comment) {
1453
- const wrapperType = resolveWrapperType(schema.objectType);
1454
- const objectCode = makeZodObject(fieldDefinitions(schema, comment), wrapperType);
1455
- return `export const ${makeCapitalized(schema.name)}Schema = ${objectCode}`;
1456
- }
1457
- function zodCode(schema, comment, type) {
1458
- const zodSchema = zod(schema, comment);
1459
- if (type) return `${zodSchema}\n\n${infer(schema.name)}\n`;
1460
- return `${zodSchema}\n`;
751
+ //#region src/core/effect.ts
752
+ function sizukuEffect(code, output, comment, type, relation) {
753
+ return emit(effect(code, comment, type, relation), path.dirname(output), output);
1461
754
  }
1462
- function relationZodCode(schema, withType) {
1463
- const relName = `${schema.name}Schema`;
1464
- const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
1465
- const fields = makeRelationFields(schema.fields);
1466
- const objectType = resolveWrapperType(schema.objectType);
1467
- const obj = `\nexport const ${makeCapitalized(relName)} = z.${objectType}({...${baseSchema}.shape,${fields}})`;
1468
- if (withType) return `${obj}\n\n${infer(schema.name)}\n`;
1469
- return `${obj}`;
1470
- }
1471
- /**
1472
- * Generate Zod schema
1473
- * @param code - The code to generate Zod schema from
1474
- * @param output - The output file path
1475
- * @param comment - Whether to include comments in the generated code
1476
- * @param type - Whether to include type information in the generated code
1477
- * @param zod - The Zod version to use
1478
- */
1479
- async function sizukuZod(code, output, comment, type, zod, relation) {
1480
- const importLine = zod === "mini" ? `import * as z from 'zod/mini'` : zod === "@hono/zod-openapi" ? `import { z } from '@hono/zod-openapi'` : `import * as z from 'zod'`;
1481
- const baseSchemas = extractSchemas(code, "zod");
1482
- const relationSchemas = extractRelationSchemas(code, "zod");
1483
- const zodGeneratedCode = [
1484
- importLine,
1485
- "",
1486
- ...baseSchemas.map((schema) => zodCode(schema, comment ?? false, type ?? false)),
1487
- ...relation ? relationSchemas.map((schema) => relationZodCode(schema, type ?? false)) : []
1488
- ].join("\n");
755
+ //#endregion
756
+ //#region src/core/mermaid-er.ts
757
+ async function sizukuMermaidER(code, output) {
758
+ const content = mermaidER(code);
1489
759
  const mkdirResult = await mkdir(path.dirname(output));
1490
760
  if (!mkdirResult.ok) return {
1491
761
  ok: false,
1492
762
  error: mkdirResult.error
1493
763
  };
1494
- const writeFileResult = await writeFile(output, await fmt(zodGeneratedCode));
764
+ const writeFileResult = await writeFile(output, content);
1495
765
  if (!writeFileResult.ok) return {
1496
766
  ok: false,
1497
767
  error: writeFileResult.error
@@ -1502,6 +772,16 @@ async function sizukuZod(code, output, comment, type, zod, relation) {
1502
772
  };
1503
773
  }
1504
774
  //#endregion
775
+ //#region src/core/valibot.ts
776
+ function sizukuValibot(code, output, comment, type, relation) {
777
+ return emit(valibot(code, comment, type, relation), path.dirname(output), output);
778
+ }
779
+ //#endregion
780
+ //#region src/core/zod.ts
781
+ function sizukuZod(code, output, comment, type, version, relation) {
782
+ return emit(zod(code, comment, type, version, relation), path.dirname(output), output);
783
+ }
784
+ //#endregion
1505
785
  //#region src/cli/index.ts
1506
786
  const HELP_TEXT = `💧 sizuku - Drizzle ORM schema tools
1507
787
 
@@ -1519,124 +799,25 @@ Options:
1519
799
  --no-with-comment Do not add JSDoc comments
1520
800
  --no-with-relation Do not generate relation schemas
1521
801
  -h, --help Display this help message`;
1522
- /**
1523
- * Detect output type from file extension
1524
- */
1525
- function detectOutputType(output) {
1526
- if (output.endsWith(".dbml")) return "dbml";
1527
- if (output.endsWith(".png")) return "png";
1528
- if (output.endsWith(".md")) return "mermaid";
1529
- if (output.endsWith(".ts")) return "typescript";
1530
- return null;
1531
- }
1532
- /**
1533
- * Parse CLI flags from argv (pure function)
1534
- */
1535
802
  function parseFlags(argv) {
1536
- const has = (flag) => argv.includes(flag);
803
+ const ZOD_VERSIONS = [
804
+ "v4",
805
+ "mini",
806
+ "@hono/zod-openapi"
807
+ ];
1537
808
  const zodVersionIndex = argv.findIndex((a) => a === "--zod-version" || a.startsWith("--zod-version="));
1538
- const zodVersion = (() => {
1539
- if (zodVersionIndex === -1) return void 0;
1540
- const arg = argv[zodVersionIndex];
1541
- const value = arg.includes("=") ? arg.split("=")[1] : argv[zodVersionIndex + 1];
1542
- if (value === "v4" || value === "mini" || value === "@hono/zod-openapi") return value;
1543
- })();
1544
- return {
1545
- zod: has("--zod"),
1546
- valibot: has("--valibot"),
1547
- arktype: has("--arktype"),
1548
- effect: has("--effect"),
1549
- zodVersion,
1550
- exportTypes: !has("--no-export-types"),
1551
- withComment: !has("--no-with-comment"),
1552
- withRelation: !has("--no-with-relation")
1553
- };
1554
- }
1555
- /**
1556
- * Strip import lines from source code and return only schema definition lines
1557
- */
1558
- function stripImports(content) {
1559
- const lines = content.split("\n");
1560
- const codeStart = lines.findIndex((line) => !line.trim().startsWith("import") && line.trim() !== "");
1561
- return lines.slice(codeStart);
1562
- }
1563
- /**
1564
- * Run sizuku in direct CLI mode for diagram output
1565
- */
1566
- async function sizukuDirectDiagram(input, output, outputType) {
1567
- const contentResult = readFileSync(input);
1568
- if (!contentResult.ok) return {
1569
- ok: false,
1570
- error: `Failed to read input: ${contentResult.error}`
1571
- };
1572
- const code = stripImports(contentResult.value);
1573
- if (outputType === "dbml" || outputType === "png") {
1574
- const result = await sizukuDbml(code, output);
1575
- if (!result.ok) return result;
1576
- return {
1577
- ok: true,
1578
- value: `💧 Generated ${outputType === "png" ? "PNG" : "DBML"} at: ${output}`
1579
- };
1580
- }
1581
- const result = await sizukuMermaidER(code, output);
1582
- if (!result.ok) return result;
809
+ const zodVersionValue = zodVersionIndex === -1 ? void 0 : argv[zodVersionIndex].includes("=") ? argv[zodVersionIndex].split("=")[1] : argv[zodVersionIndex + 1];
1583
810
  return {
1584
- ok: true,
1585
- value: `💧 Generated Mermaid ER at: ${output}`
811
+ zod: argv.includes("--zod"),
812
+ valibot: argv.includes("--valibot"),
813
+ arktype: argv.includes("--arktype"),
814
+ effect: argv.includes("--effect"),
815
+ zodVersion: ZOD_VERSIONS.find((v) => v === zodVersionValue),
816
+ exportTypes: !argv.includes("--no-export-types"),
817
+ withComment: !argv.includes("--no-with-comment"),
818
+ withRelation: !argv.includes("--no-with-relation")
1586
819
  };
1587
820
  }
1588
- /**
1589
- * Resolve which schema library to use from flags
1590
- */
1591
- function resolveSchemaLibrary(flags) {
1592
- if (flags.zod) return {
1593
- name: "zod",
1594
- label: "Zod"
1595
- };
1596
- if (flags.valibot) return {
1597
- name: "valibot",
1598
- label: "Valibot"
1599
- };
1600
- if (flags.arktype) return {
1601
- name: "arktype",
1602
- label: "ArkType"
1603
- };
1604
- if (flags.effect) return {
1605
- name: "effect",
1606
- label: "Effect"
1607
- };
1608
- return null;
1609
- }
1610
- /**
1611
- * Run sizuku in direct CLI mode for validation schema output
1612
- */
1613
- async function sizukuDirectSchema(input, output, flags) {
1614
- const lib = resolveSchemaLibrary(flags);
1615
- if (!lib) return {
1616
- ok: false,
1617
- error: "Specify --zod, --valibot, --arktype, or --effect for .ts output"
1618
- };
1619
- const contentResult = readFileSync(input);
1620
- if (!contentResult.ok) return {
1621
- ok: false,
1622
- error: `Failed to read input: ${contentResult.error}`
1623
- };
1624
- const code = stripImports(contentResult.value);
1625
- const result = await {
1626
- zod: () => sizukuZod(code, output, flags.withComment, flags.exportTypes, flags.zodVersion, flags.withRelation),
1627
- valibot: () => sizukuValibot(code, output, flags.withComment, flags.exportTypes, flags.withRelation),
1628
- arktype: () => sizukuArktype(code, output, flags.withComment, flags.exportTypes, flags.withRelation),
1629
- effect: () => sizukuEffect(code, output, flags.withComment, flags.exportTypes, flags.withRelation)
1630
- }[lib.name]();
1631
- if (!result.ok) return result;
1632
- return {
1633
- ok: true,
1634
- value: `💧 Generated ${lib.label} schema at: ${output}`
1635
- };
1636
- }
1637
- /**
1638
- * Main entry point
1639
- */
1640
821
  async function sizuku() {
1641
822
  const argv = process.argv.slice(2);
1642
823
  if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) return {
@@ -1658,13 +839,66 @@ async function sizuku() {
1658
839
  ok: false,
1659
840
  error: "Missing output file path after -o"
1660
841
  };
1661
- const outputType = detectOutputType(output);
842
+ const outputType = output.endsWith(".dbml") ? "dbml" : output.endsWith(".png") ? "png" : output.endsWith(".md") ? "mermaid" : output.endsWith(".ts") ? "typescript" : null;
1662
843
  if (!outputType) return {
1663
844
  ok: false,
1664
845
  error: `Unsupported output format: ${output}. Supported: .dbml, .png, .md, .ts`
1665
846
  };
1666
- if (outputType === "typescript") return sizukuDirectSchema(input, output, parseFlags(argv));
1667
- return sizukuDirectDiagram(input, output, outputType);
847
+ const flags = outputType === "typescript" ? parseFlags(argv) : null;
848
+ const lib = flags === null ? null : flags.zod ? {
849
+ name: "zod",
850
+ label: "Zod"
851
+ } : flags.valibot ? {
852
+ name: "valibot",
853
+ label: "Valibot"
854
+ } : flags.arktype ? {
855
+ name: "arktype",
856
+ label: "ArkType"
857
+ } : flags.effect ? {
858
+ name: "effect",
859
+ label: "Effect"
860
+ } : null;
861
+ if (outputType === "typescript" && !lib) return {
862
+ ok: false,
863
+ error: "Specify --zod, --valibot, --arktype, or --effect for .ts output"
864
+ };
865
+ const contentResult = readFileSync(input);
866
+ if (!contentResult.ok) return {
867
+ ok: false,
868
+ error: `Failed to read input: ${contentResult.error}`
869
+ };
870
+ const code = stripImports(contentResult.value);
871
+ if (outputType === "dbml" || outputType === "png") {
872
+ const result = await sizukuDbml(code, output);
873
+ if (!result.ok) return result;
874
+ return {
875
+ ok: true,
876
+ value: `💧 Generated ${outputType === "png" ? "PNG" : "DBML"} at: ${output}`
877
+ };
878
+ }
879
+ if (outputType === "mermaid") {
880
+ const result = await sizukuMermaidER(code, output);
881
+ if (!result.ok) return result;
882
+ return {
883
+ ok: true,
884
+ value: `💧 Generated Mermaid ER at: ${output}`
885
+ };
886
+ }
887
+ if (!lib || !flags) return {
888
+ ok: false,
889
+ error: "internal: missing lib/flags"
890
+ };
891
+ const result = await (() => {
892
+ if (lib.name === "zod") return sizukuZod(code, output, flags.withComment, flags.exportTypes, flags.zodVersion, flags.withRelation);
893
+ if (lib.name === "valibot") return sizukuValibot(code, output, flags.withComment, flags.exportTypes, flags.withRelation);
894
+ if (lib.name === "arktype") return sizukuArktype(code, output, flags.withComment, flags.exportTypes, flags.withRelation);
895
+ return sizukuEffect(code, output, flags.withComment, flags.exportTypes, flags.withRelation);
896
+ })();
897
+ if (!result.ok) return result;
898
+ return {
899
+ ok: true,
900
+ value: `💧 Generated ${lib.label} schema at: ${output}`
901
+ };
1668
902
  }
1669
903
  //#endregion
1670
904
  //#region src/index.ts