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