@zxsylph/dbml-formatter 1.0.3 → 1.0.8

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.
@@ -1,3 +1,6 @@
1
1
  #!/usr/bin/env node
2
- require('ts-node').register();
3
- require('../src/cli.ts');
2
+ const path = require("path");
3
+ require("ts-node").register({
4
+ project: path.join(__dirname, "../tsconfig.json"),
5
+ });
6
+ require("../src/cli.ts");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zxsylph/dbml-formatter",
3
- "version": "1.0.3",
3
+ "version": "1.0.8",
4
4
  "description": "",
5
5
  "files": [
6
6
  "bin",
package/src/cli.ts CHANGED
@@ -6,23 +6,92 @@ import { format } from './formatter/formatter';
6
6
  const args = process.argv.slice(2);
7
7
 
8
8
  if (args.length === 0) {
9
- console.error('Usage: dbml-fmt <file>');
9
+ console.error('Usage: dbml-formatter <file> | --folder <path> [--dry-run]');
10
10
  process.exit(1);
11
11
  }
12
12
 
13
- const filePath = args[0];
14
- const absPath = path.resolve(process.cwd(), filePath);
13
+ const folderIndex = args.indexOf('--folder');
14
+ const dryRunIndex = args.indexOf('--dry-run');
15
+ const orderFieldIndex = args.indexOf('--order-field');
16
+ const isDryRun = dryRunIndex !== -1;
17
+ const orderField = orderFieldIndex !== -1;
15
18
 
16
- if (!fs.existsSync(absPath)) {
17
- console.error(`File not found: ${absPath}`);
18
- process.exit(1);
19
- }
19
+ if (folderIndex !== -1) {
20
+ // Folder mode
21
+ const folderPath = args[folderIndex + 1];
22
+ if (!folderPath || folderPath.startsWith('--')) {
23
+ console.error('Error: --folder requires a path argument');
24
+ process.exit(1);
25
+ }
20
26
 
21
- try {
22
- const content = fs.readFileSync(absPath, 'utf-8');
23
- const formatted = format(content);
24
- console.log(formatted);
25
- } catch (error) {
26
- console.error('Error formatting file:', error);
27
- process.exit(1);
27
+ const absFolderPath = path.resolve(process.cwd(), folderPath);
28
+ if (!fs.existsSync(absFolderPath) || !fs.statSync(absFolderPath).isDirectory()) {
29
+ console.error(`Directory not found: ${absFolderPath}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const getDbmlFiles = (dir: string): string[] => {
34
+ let results: string[] = [];
35
+ const list = fs.readdirSync(dir);
36
+ list.forEach(file => {
37
+ const filePath = path.join(dir, file);
38
+ const stat = fs.statSync(filePath);
39
+ if (stat && stat.isDirectory()) {
40
+ results = results.concat(getDbmlFiles(filePath));
41
+ } else {
42
+ if (file.endsWith('.dbml')) {
43
+ results.push(filePath);
44
+ }
45
+ }
46
+ });
47
+ return results;
48
+ };
49
+
50
+ const files = getDbmlFiles(absFolderPath);
51
+
52
+ if (files.length === 0) {
53
+ console.log(`No .dbml files found in ${absFolderPath}`);
54
+ } else {
55
+ files.forEach(file => {
56
+ try {
57
+ const content = fs.readFileSync(file, 'utf-8');
58
+ const formatted = format(content, { orderField });
59
+
60
+ if (isDryRun) {
61
+ console.log(`\n--- Dry Run: ${file} ---`);
62
+ console.log(formatted);
63
+ } else {
64
+ fs.writeFileSync(file, formatted, 'utf-8');
65
+ console.log(`Formatted: ${file}`);
66
+ }
67
+ } catch (err) {
68
+ console.error(`Error formatting ${file}:`, err);
69
+ }
70
+ });
71
+ }
72
+
73
+ } else {
74
+ // Single file mode
75
+ const filePath = args[0];
76
+
77
+ if (filePath.startsWith('--')) {
78
+ console.error('Usage: dbml-formatter <file> | --folder <path> [--dry-run]');
79
+ process.exit(1);
80
+ }
81
+
82
+ const absPath = path.resolve(process.cwd(), filePath);
83
+
84
+ if (!fs.existsSync(absPath)) {
85
+ console.error(`File not found: ${absPath}`);
86
+ process.exit(1);
87
+ }
88
+
89
+ try {
90
+ const content = fs.readFileSync(absPath, 'utf-8');
91
+ const formatted = format(content, { orderField });
92
+ console.log(formatted);
93
+ } catch (error) {
94
+ console.error('Error formatting file:', error);
95
+ process.exit(1);
96
+ }
28
97
  }
@@ -3,6 +3,7 @@ import { Token, TokenType, tokenize } from './tokenizer';
3
3
  export interface FormatterOptions {
4
4
  indentSize?: number;
5
5
  useTabs?: boolean;
6
+ orderField?: boolean;
6
7
  }
7
8
 
8
9
  export function format(input: string, options: FormatterOptions = {}): string {
@@ -161,6 +162,91 @@ export function format(input: string, options: FormatterOptions = {}): string {
161
162
  } else {
162
163
  output += '\n\n';
163
164
  }
165
+ } else {
166
+ // NEW: If no table note, add empty Note: ""
167
+ output += getIndent() + 'Note: ""\n\n';
168
+ }
169
+
170
+ // OPTIONAL: Sort Fields within groups
171
+ if (options.orderField) {
172
+ // 1. Normalize groups: Detach "Gap" (extra newlines) from content lines.
173
+ // If a line ends with > 1 newline, split it into [ContentLine] + [EmptyLine]s.
174
+
175
+ const normalized: Token[][] = [];
176
+
177
+ for (const line of otherLinesGroups) {
178
+ const last = line[line.length - 1];
179
+ let hasExtraNewline = false;
180
+
181
+ if (last && last.type === TokenType.Whitespace) {
182
+ const newlineCount = (last.value.match(/\n/g) || []).length;
183
+ if (newlineCount > 1) {
184
+ hasExtraNewline = true;
185
+
186
+ // Create stripped line (1 newline)
187
+ const newLineTokens = [...line];
188
+ newLineTokens[newLineTokens.length - 1] = {
189
+ ...last,
190
+ value: last.value.replace(/\n+/g, '\n')
191
+ };
192
+ normalized.push(newLineTokens);
193
+
194
+ // Add spacer lines
195
+ for(let k=1; k < newlineCount; k++) {
196
+ normalized.push([{ type: TokenType.Whitespace, value: '\n', line: 0, column: 0 }]);
197
+ }
198
+ }
199
+ }
200
+
201
+ if (!hasExtraNewline) {
202
+ normalized.push(line);
203
+ }
204
+ }
205
+
206
+ // Replace otherLinesGroups with normalized version
207
+ otherLinesGroups.splice(0, otherLinesGroups.length, ...normalized);
208
+
209
+ // 2. Group lines by "is content" and Sort
210
+
211
+ // Helper to check if line is content
212
+ const isContentLine = (line: Token[]) => {
213
+ const m = line.filter(x => x.type !== TokenType.Whitespace && x.type !== TokenType.Comment);
214
+ return m.length > 0;
215
+ };
216
+
217
+ let i = 0;
218
+ while(i < otherLinesGroups.length) {
219
+ if (isContentLine(otherLinesGroups[i])) {
220
+ // Start of a block
221
+ let j = i + 1;
222
+ while(j < otherLinesGroups.length && isContentLine(otherLinesGroups[j])) {
223
+ j++;
224
+ }
225
+ // Range [i, j) is a content block to sort
226
+ const block = otherLinesGroups.slice(i, j);
227
+ // Sort block
228
+ block.sort((a, b) => {
229
+ const getFirstWord = (toks: Token[]) => {
230
+ const t = toks.find(x => x.type === TokenType.Word || x.type === TokenType.String);
231
+ return t ? t.value.replace(/^"|"$/g, '').toLowerCase() : '';
232
+ };
233
+ const wa = getFirstWord(a);
234
+ const wb = getFirstWord(b);
235
+ if (wa < wb) return -1;
236
+ if (wa > wb) return 1;
237
+ return 0;
238
+ });
239
+
240
+ // Put back
241
+ for(let k=0; k<block.length; k++) {
242
+ otherLinesGroups[i+k] = block[k];
243
+ }
244
+
245
+ i = j;
246
+ } else {
247
+ i++;
248
+ }
249
+ }
164
250
  }
165
251
 
166
252
  // 5. Print other lines (Process Fields)
@@ -221,7 +307,88 @@ export function format(input: string, options: FormatterOptions = {}): string {
221
307
  }
222
308
  }
223
309
 
224
- // 6. Apply "Quote Data Types" logic
310
+ // 6. Apply "Empty Field Note" logic
311
+ const meaningful = lineTokens.filter(t => t.type !== TokenType.Whitespace && t.type !== TokenType.Comment);
312
+ // Heuristic: Is this a field?
313
+ // It should have at least 2 tokens (Name, Type).
314
+ // It should NOT be 'indexes'.
315
+ // It should NOT contain '{' (which would imply a block start like indexes { )
316
+
317
+ let isField = false;
318
+ if (meaningful.length >= 2) {
319
+ const firstWord = meaningful[0].value.toLowerCase();
320
+ if (firstWord !== 'indexes' && firstWord !== 'note') {
321
+ // Check for braces in original lineTokens to avoid sub-blocks
322
+ const hasBrace = lineTokens.some(t => t.type === TokenType.Symbol && t.value === '{');
323
+ if (!hasBrace) {
324
+ isField = true;
325
+ }
326
+ }
327
+ }
328
+
329
+ if (isField) {
330
+ // Find settings block
331
+ let openBracketIdx = -1;
332
+ let closeBracketIdx = -1;
333
+ for(let idx=0; idx<lineTokens.length; idx++) {
334
+ if (lineTokens[idx].type === TokenType.Symbol && lineTokens[idx].value === '[') openBracketIdx = idx;
335
+ if (lineTokens[idx].type === TokenType.Symbol && lineTokens[idx].value === ']') closeBracketIdx = idx;
336
+ }
337
+
338
+ if (openBracketIdx !== -1 && closeBracketIdx !== -1 && closeBracketIdx > openBracketIdx) {
339
+ // Settings exist. Check if 'note' is present.
340
+ const inside = lineTokens.slice(openBracketIdx + 1, closeBracketIdx);
341
+ let hasNote = false;
342
+
343
+ // Simple token scan for 'note' word
344
+ // Ideally we should parse comma groups, but 'note' keyword is reserved in settings.
345
+ for (const t of inside) {
346
+ if (t.type === TokenType.Word && t.value.toLowerCase() === 'note') {
347
+ hasNote = true;
348
+ break;
349
+ }
350
+ }
351
+
352
+ if (!hasNote) {
353
+ // Insert `note: ""` at the beginning of settings
354
+ // We need to insert: "note", ":", "\"\"", ","
355
+ // REMOVED explicit spaces
356
+ const newTokens: Token[] = [
357
+ { type: TokenType.Word, value: 'note', line: 0, column: 0 },
358
+ { type: TokenType.Symbol, value: ':', line: 0, column: 0 },
359
+ { type: TokenType.String, value: '""', line: 0, column: 0 },
360
+ { type: TokenType.Symbol, value: ',', line: 0, column: 0 }
361
+ ];
362
+ lineTokens.splice(openBracketIdx + 1, 0, ...newTokens);
363
+ }
364
+ } else {
365
+ // No settings exist. Append ` [note: ""]`.
366
+ // REMOVED explicit spaces
367
+
368
+ // Find last meaningful token index
369
+ let lastMeaningfulIdx = -1;
370
+ for (let idx = lineTokens.length - 1; idx >= 0; idx--) {
371
+ if (lineTokens[idx].type !== TokenType.Whitespace && lineTokens[idx].type !== TokenType.Comment) {
372
+ lastMeaningfulIdx = idx;
373
+ break;
374
+ }
375
+ }
376
+
377
+ if (lastMeaningfulIdx !== -1) {
378
+ const appendTokens: Token[] = [
379
+ { type: TokenType.Symbol, value: '[', line: 0, column: 0 },
380
+ { type: TokenType.Word, value: 'note', line: 0, column: 0 },
381
+ { type: TokenType.Symbol, value: ':', line: 0, column: 0 },
382
+ { type: TokenType.String, value: '""', line: 0, column: 0 },
383
+ { type: TokenType.Symbol, value: ']', line: 0, column: 0 }
384
+ ];
385
+ lineTokens.splice(lastMeaningfulIdx + 1, 0, ...appendTokens);
386
+ }
387
+ }
388
+ }
389
+
390
+ // 7. Apply "Quote Data Types" logic
391
+
225
392
  let wordCount = 0;
226
393
  for (const t of lineTokens) {
227
394
  // Only count words before `[`?
@@ -0,0 +1,34 @@
1
+ import { format } from './formatter/formatter';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ const filePath = path.join(__dirname, '../test_folder/repro_empty_note_field.dbml');
6
+ const content = fs.readFileSync(filePath, 'utf-8');
7
+
8
+ const formatted = format(content);
9
+
10
+ console.log('--- Formatted Output ---');
11
+ console.log(formatted);
12
+
13
+ // Robust check:
14
+ // We just check if the output contains `Note: ""` or `note: ""` for each field name.
15
+ // Since the output might be multiline, we can't just filter lines.
16
+ // We scan the text.
17
+
18
+ const requiredFields = ['id', 'username', 'email'];
19
+ let allPresent = true;
20
+
21
+ // Just check globally if we have 3 empty notes + the table note?
22
+ // Table note is also `Note: ""`
23
+ // So we expect 4 `Note: ""` or `note: ""`
24
+ const matches = formatted.match(/Note:\s*""/gi);
25
+ const count = matches ? matches.length : 0;
26
+
27
+ console.log(`Found ${count} empty notes.`);
28
+
29
+ if (count >= 4) { // 1 for table + 3 for fields
30
+ console.log('SUCCESS: Empty notes added to fields.');
31
+ } else {
32
+ console.error('FAILURE: Empty notes NOT added to fields (or not enough).');
33
+ process.exit(1);
34
+ }
@@ -0,0 +1,18 @@
1
+ import { format } from './formatter/formatter';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ const filePath = path.join(__dirname, '../test_folder/repro_empty_note.dbml');
6
+ const content = fs.readFileSync(filePath, 'utf-8');
7
+
8
+ const formatted = format(content);
9
+
10
+ console.log('--- Formatted Output ---');
11
+ console.log(formatted);
12
+
13
+ if (formatted.includes('Note: ""')) {
14
+ console.log('SUCCESS: Empty note added.');
15
+ } else {
16
+ console.error('FAILURE: Empty note NOT added.');
17
+ process.exit(1);
18
+ }