mikro-orm-markdown 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import * as syncFs from "fs";
4
5
  import * as fs from "fs/promises";
5
- import * as path from "path";
6
+ import * as path2 from "path";
7
+ import { fileURLToPath, pathToFileURL } from "url";
6
8
  import { Command } from "commander";
7
9
 
8
10
  // src/docs/jsdoc.ts
9
11
  import { Project } from "ts-morph";
10
- function loadJsDoc(srcGlobs) {
12
+ function loadJsDoc(filePaths) {
11
13
  const entities = /* @__PURE__ */ new Map();
12
14
  const props = /* @__PURE__ */ new Map();
13
- if (srcGlobs.length === 0) return { entities, props };
15
+ const classNames = /* @__PURE__ */ new Set();
16
+ if (filePaths.length === 0) {
17
+ return { entities, props, sourceFileCount: 0, classNames };
18
+ }
14
19
  const project = new Project({
15
20
  skipAddingFilesFromTsConfig: true,
16
21
  skipLoadingLibFiles: true,
@@ -19,30 +24,44 @@ function loadJsDoc(srcGlobs) {
19
24
  skipLibCheck: true
20
25
  }
21
26
  });
22
- project.addSourceFilesAtPaths(srcGlobs);
23
- for (const sourceFile of project.getSourceFiles()) {
24
- for (const cls of sourceFile.getClasses()) {
25
- const className = cls.getName();
26
- if (!className) continue;
27
- const classDocs = cls.getJsDocs();
28
- if (classDocs.length > 0) {
29
- entities.set(className, parseEntityJsDoc(classDocs));
30
- }
31
- const propMap = /* @__PURE__ */ new Map();
32
- for (const prop of cls.getProperties()) {
33
- const propDocs = prop.getJsDocs();
34
- if (propDocs.length === 0) continue;
35
- const desc = extractDescription(propDocs);
36
- if (desc !== void 0) {
37
- propMap.set(prop.getName(), { description: desc });
27
+ for (const filePath of filePaths) {
28
+ try {
29
+ project.addSourceFilesAtPaths(filePath);
30
+ } catch {
31
+ }
32
+ }
33
+ const sourceFiles = project.getSourceFiles();
34
+ for (const sourceFile of sourceFiles) {
35
+ try {
36
+ for (const cls of sourceFile.getClasses()) {
37
+ const className = cls.getName();
38
+ if (!className) {
39
+ continue;
40
+ }
41
+ classNames.add(className);
42
+ const classDocs = cls.getJsDocs();
43
+ if (classDocs.length > 0) {
44
+ entities.set(className, parseEntityJsDoc(classDocs));
45
+ }
46
+ const propMap = /* @__PURE__ */ new Map();
47
+ for (const prop of cls.getProperties()) {
48
+ const propDocs = prop.getJsDocs();
49
+ if (propDocs.length === 0) {
50
+ continue;
51
+ }
52
+ const info = parsePropJsDoc(propDocs);
53
+ if (info.description !== void 0 || info.atLeastOne) {
54
+ propMap.set(prop.getName(), info);
55
+ }
56
+ }
57
+ if (propMap.size > 0) {
58
+ props.set(className, propMap);
38
59
  }
39
60
  }
40
- if (propMap.size > 0) {
41
- props.set(className, propMap);
42
- }
61
+ } catch {
43
62
  }
44
63
  }
45
- return { entities, props };
64
+ return { entities, props, sourceFileCount: sourceFiles.length, classNames };
46
65
  }
47
66
  function parseEntityJsDoc(jsDocs) {
48
67
  const namespaces = [];
@@ -52,14 +71,21 @@ function parseEntityJsDoc(jsDocs) {
52
71
  let description;
53
72
  for (const doc of jsDocs) {
54
73
  const desc = doc.getDescription().trim();
55
- if (desc && description === void 0) description = desc;
74
+ if (desc && description === void 0) {
75
+ description = desc;
76
+ }
56
77
  for (const tag of doc.getTags()) {
57
78
  const tagName = tag.getTagName();
58
79
  const comment = tag.getCommentText()?.trim();
59
- if (tagName === "namespace" && comment) namespaces.push(comment);
60
- else if (tagName === "erd" && comment) erdNamespaces.push(comment);
61
- else if (tagName === "describe" && comment) describeNamespaces.push(comment);
62
- else if (tagName === "hidden") hidden = true;
80
+ if (tagName === "namespace" && comment) {
81
+ namespaces.push(comment);
82
+ } else if (tagName === "erd" && comment) {
83
+ erdNamespaces.push(comment);
84
+ } else if (tagName === "describe" && comment) {
85
+ describeNamespaces.push(comment);
86
+ } else if (tagName === "hidden") {
87
+ hidden = true;
88
+ }
63
89
  }
64
90
  }
65
91
  return {
@@ -70,16 +96,26 @@ function parseEntityJsDoc(jsDocs) {
70
96
  hidden
71
97
  };
72
98
  }
73
- function extractDescription(jsDocs) {
99
+ function parsePropJsDoc(jsDocs) {
100
+ let description;
101
+ let atLeastOne = false;
74
102
  for (const doc of jsDocs) {
75
103
  const desc = doc.getDescription().trim();
76
- if (desc) return desc;
104
+ if (desc && description === void 0) {
105
+ description = desc;
106
+ }
107
+ for (const tag of doc.getTags()) {
108
+ if (tag.getTagName() === "atLeastOne") {
109
+ atLeastOne = true;
110
+ }
111
+ }
77
112
  }
78
- return void 0;
113
+ return { ...description !== void 0 && { description }, atLeastOne };
79
114
  }
80
115
 
81
116
  // src/metadata/load.ts
82
- import { MikroORM } from "@mikro-orm/core";
117
+ import * as path from "path";
118
+ import { EntitySchema, MikroORM } from "@mikro-orm/core";
83
119
  var MetadataLoadError = class extends Error {
84
120
  constructor(message, cause) {
85
121
  super(message);
@@ -87,12 +123,46 @@ var MetadataLoadError = class extends Error {
87
123
  this.name = "MetadataLoadError";
88
124
  }
89
125
  };
126
+ async function closeDiscoveryResources(orm) {
127
+ await orm.config.getMetadataCacheAdapter()?.close?.();
128
+ await orm.config.getResultCacheAdapter()?.close?.();
129
+ }
130
+ function collectEntitySchemaNames(options) {
131
+ const configuredEntities = [...options.entities ?? [], ...options.entitiesTs ?? []];
132
+ const names = [];
133
+ for (const entity of configuredEntities) {
134
+ if (entity instanceof EntitySchema) {
135
+ names.push(entity.meta.className);
136
+ continue;
137
+ }
138
+ if (entity !== null && typeof entity === "object" && "schema" in entity && entity.schema instanceof EntitySchema) {
139
+ names.push(entity.schema.meta.className);
140
+ }
141
+ }
142
+ return names;
143
+ }
144
+ function assertNoEntitySchemaEntities(options) {
145
+ const schemaNames = collectEntitySchemaNames(options);
146
+ if (schemaNames.length === 0) {
147
+ return;
148
+ }
149
+ throw new MetadataLoadError(
150
+ `EntitySchema-defined entities are not currently supported: ${schemaNames.join(", ")}.
151
+ Use decorator-based @Entity() classes instead.`
152
+ );
153
+ }
90
154
  async function loadEntityMetadata(options) {
155
+ assertNoEntitySchemaEntities(options);
91
156
  let orm;
92
157
  try {
93
158
  orm = await MikroORM.init({
94
159
  ...options,
95
- debug: false
160
+ debug: false,
161
+ connect: false,
162
+ // Always disable the metadata cache for one-shot doc runs so the project
163
+ // is never littered with a temp/ folder, regardless of how metadataProvider
164
+ // was configured.
165
+ metadataCache: { ...options.metadataCache, enabled: false }
96
166
  });
97
167
  } catch (cause) {
98
168
  throw new MetadataLoadError(
@@ -107,20 +177,73 @@ async function loadEntityMetadata(options) {
107
177
  "No entities were discovered. Check that your config specifies at least one entity path or class."
108
178
  );
109
179
  }
110
- return all;
180
+ const baseDir = orm.config.get("baseDir");
181
+ const sourcePaths = [...new Set(all.filter((m) => m.path).map((m) => path.resolve(baseDir, m.path)))];
182
+ return { metas: all, sourcePaths };
111
183
  } finally {
112
- await orm.close(true);
184
+ await closeDiscoveryResources(orm);
113
185
  }
114
186
  }
115
187
 
188
+ // src/model/build.ts
189
+ import { ReferenceKind as ReferenceKind2 } from "@mikro-orm/core";
190
+
116
191
  // src/render/mermaid.ts
117
192
  import { ReferenceKind } from "@mikro-orm/core";
193
+
194
+ // src/render/escape.ts
195
+ var MARKDOWN_INLINE_SPECIAL_CHARS = /[\\`|*#]/g;
196
+ var MERMAID_IDENTIFIER_INVALID_CHARS = /[^a-zA-Z0-9_]/g;
197
+ function normalizeInlineText(value) {
198
+ return value.replace(/\r\n?/g, "\n").replace(/[\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
199
+ }
200
+ function splitNormalizedLines(value) {
201
+ return value.replace(/\r\n?/g, "\n").split("\n");
202
+ }
203
+ function escapeHtmlText(value) {
204
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
205
+ }
206
+ function escapeMarkdownInline(value) {
207
+ return escapeHtmlText(normalizeInlineText(value)).replace(MARKDOWN_INLINE_SPECIAL_CHARS, "\\$&");
208
+ }
209
+ function escapeMarkdownTableCell(value) {
210
+ return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join("<br>");
211
+ }
212
+ function escapeMarkdownParagraph(value) {
213
+ return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join(" \n");
214
+ }
215
+ function renderMarkdownBlockQuote(value) {
216
+ return splitNormalizedLines(value).map((line) => `> ${escapeMarkdownInline(line)}`).join("\n");
217
+ }
218
+ function renderMarkdownInlineCode(value) {
219
+ const normalized = normalizeInlineText(value);
220
+ const backtickRuns = normalized.match(/`+/g) ?? [];
221
+ const longestRun = Math.max(0, ...backtickRuns.map((run2) => run2.length));
222
+ const fence = "`".repeat(longestRun + 1);
223
+ const needsPadding = normalized.startsWith("`") || normalized.endsWith("`");
224
+ const content = needsPadding ? ` ${normalized} ` : normalized;
225
+ return `${fence}${content}${fence}`;
226
+ }
227
+ function toMermaidIdentifier(value) {
228
+ const normalized = normalizeInlineText(value).replace(MERMAID_IDENTIFIER_INVALID_CHARS, "_").replace(/_+/g, "_");
229
+ const identifier = normalized === "" ? "_" : normalized;
230
+ return /^[a-zA-Z_]/.test(identifier) ? identifier : `_${identifier}`;
231
+ }
232
+ function escapeMermaidQuotedText(value) {
233
+ return normalizeInlineText(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
234
+ }
235
+ function toMarkdownAnchor(value) {
236
+ return normalizeInlineText(value).toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s+/g, "-");
237
+ }
238
+
239
+ // src/render/mermaid.ts
118
240
  var FORMULA_DUMMY_TABLE = {
119
241
  alias: "e0",
120
242
  name: "",
121
243
  qualifiedName: "",
122
244
  toString: () => "e0"
123
245
  };
246
+ var UNRESOLVED_FORMULA = "<unresolved>";
124
247
  function buildDiagramModel(metas) {
125
248
  const metaByClass = new Map(metas.map((m) => [m.className, m]));
126
249
  const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
@@ -128,13 +251,14 @@ function buildDiagramModel(metas) {
128
251
  return { entities, relations };
129
252
  }
130
253
  function buildEntityModel(meta, metaByClass) {
131
- const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.discriminatorValue;
254
+ const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
132
255
  const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
133
256
  const columns = [];
134
257
  for (const prop of Object.values(meta.properties)) {
135
- if (isStiRoot && prop.inherited === true) continue;
136
- const col = buildColumn(prop, metaByClass, meta);
137
- if (col !== null) columns.push(col);
258
+ if (isStiRoot && prop.inherited === true) {
259
+ continue;
260
+ }
261
+ columns.push(...buildColumns(prop, metaByClass, meta));
138
262
  }
139
263
  return {
140
264
  className: meta.className,
@@ -144,61 +268,123 @@ function buildEntityModel(meta, metaByClass) {
144
268
  isEmbeddable: meta.embeddable === true,
145
269
  ...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
146
270
  ...isStiChild && { extendsEntity: meta.extends },
271
+ ...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
147
272
  constraints: buildConstraints(meta)
148
273
  };
149
274
  }
150
- function buildColumn(prop, metaByClass, owningMeta) {
151
- if (prop.kind === ReferenceKind.EMBEDDED) return null;
275
+ function buildColumns(prop, metaByClass, owningMeta) {
276
+ if (prop.kind === ReferenceKind.EMBEDDED) {
277
+ if (prop.object === true || prop.array === true) {
278
+ return [
279
+ {
280
+ propName: prop.name,
281
+ fieldName: prop.fieldNames?.[0] ?? prop.name,
282
+ type: "json",
283
+ isPrimary: false,
284
+ isForeignKey: false,
285
+ isUnique: prop.unique === true,
286
+ isNullable: prop.nullable === true,
287
+ ...prop.comment !== void 0 && { comment: prop.comment },
288
+ embeddedIn: prop.array === true ? `${prop.type}[]` : prop.type
289
+ }
290
+ ];
291
+ }
292
+ return [];
293
+ }
152
294
  if (prop.kind === ReferenceKind.SCALAR) {
295
+ if (prop.object === true && prop.embedded !== void 0) {
296
+ return [];
297
+ }
153
298
  const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
154
299
  let embeddedIn;
300
+ let embeddedPropName;
155
301
  if (prop.embedded !== void 0) {
156
302
  const parentPropName = prop.embedded[0];
157
303
  embeddedIn = owningMeta.properties[parentPropName]?.type;
304
+ embeddedPropName = prop.embedded[1];
158
305
  }
159
306
  const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
160
- return {
161
- propName: prop.name,
162
- fieldName: prop.fieldNames?.[0] ?? prop.name,
163
- type: normalizeType(prop.type),
164
- isPrimary: prop.primary === true,
165
- isForeignKey: false,
166
- isUnique: prop.unique === true,
167
- isNullable: prop.nullable === true,
168
- ...formulaExpr !== void 0 && { formula: formulaExpr },
169
- ...embeddedIn !== void 0 && { embeddedIn },
170
- ...isDiscriminator && { isDiscriminator: true }
171
- };
307
+ const enumItems = prop.enum === true && Array.isArray(prop.items) && prop.items.length > 0 ? prop.items.map((item) => String(item)) : void 0;
308
+ return [
309
+ {
310
+ propName: prop.name,
311
+ fieldName: prop.fieldNames?.[0] ?? prop.name,
312
+ // Store the original type (e.g. `varchar(255)`); the Mermaid renderer
313
+ // sanitizes it for diagram identifiers, while the markdown table shows
314
+ // it verbatim. Guard against a missing type so downstream string
315
+ // handling never sees undefined (matches the FK path's defaulting).
316
+ type: prop.type ?? "unknown",
317
+ isPrimary: prop.primary === true,
318
+ isForeignKey: false,
319
+ isUnique: prop.unique === true,
320
+ isNullable: prop.nullable === true,
321
+ ...prop.comment !== void 0 && { comment: prop.comment },
322
+ ...formulaExpr !== void 0 && { formula: formulaExpr },
323
+ ...embeddedIn !== void 0 && { embeddedIn },
324
+ ...embeddedPropName !== void 0 && { embeddedPropName },
325
+ ...isDiscriminator && { isDiscriminator: true },
326
+ ...enumItems !== void 0 && { enumItems }
327
+ }
328
+ ];
172
329
  }
173
330
  if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner === true) {
174
- const fkType = resolveFkType(prop.type, metaByClass);
175
- return {
176
- propName: prop.name,
177
- fieldName: prop.fieldNames?.[0] ?? `${prop.name}_id`,
178
- type: fkType,
179
- isPrimary: false,
180
- isForeignKey: true,
181
- isUnique: prop.unique === true,
182
- isNullable: prop.nullable === true
183
- };
331
+ const cols = buildForeignKeyColumns(prop, metaByClass);
332
+ if (prop.type === owningMeta.className) {
333
+ return cols.map((col) => ({ ...col, isSelfReference: true }));
334
+ }
335
+ return cols;
184
336
  }
185
- return null;
337
+ return [];
186
338
  }
187
339
  function resolveFormulaExpr(cb) {
188
340
  try {
189
- const cols = new Proxy({}, {
190
- get: (_target, key) => typeof key === "string" ? key : ""
191
- });
192
- return cb(FORMULA_DUMMY_TABLE, cols);
341
+ const cols = new Proxy(
342
+ {},
343
+ {
344
+ get: (_target, key) => typeof key === "string" ? key : ""
345
+ }
346
+ );
347
+ const result = cb(FORMULA_DUMMY_TABLE, cols);
348
+ return typeof result === "string" ? result : String(result);
193
349
  } catch {
194
- return "";
350
+ return UNRESOLVED_FORMULA;
195
351
  }
196
352
  }
197
- function resolveFkType(referencedClassName, metaByClass) {
198
- const refMeta = metaByClass.get(referencedClassName);
199
- if (!refMeta) return "integer";
200
- const pkProp = Object.values(refMeta.properties).find((p) => p.primary === true);
201
- return pkProp ? normalizeType(pkProp.type) : "integer";
353
+ function buildForeignKeyColumns(prop, metaByClass) {
354
+ const fieldNames = prop.fieldNames && prop.fieldNames.length > 0 ? prop.fieldNames : [`${prop.name}_id`];
355
+ const fkTypes = resolveFkTypes(prop, metaByClass, fieldNames.length);
356
+ return fieldNames.map((fieldName, index) => ({
357
+ propName: prop.name,
358
+ fieldName,
359
+ type: fkTypes[index] ?? fkTypes[0] ?? "integer",
360
+ isPrimary: prop.primary === true,
361
+ isForeignKey: true,
362
+ isUnique: prop.unique === true,
363
+ isNullable: prop.nullable === true,
364
+ ...prop.comment !== void 0 && { comment: prop.comment },
365
+ referencedEntity: prop.type
366
+ }));
367
+ }
368
+ function resolveFkTypes(prop, metaByClass, fieldNameCount) {
369
+ const refMeta = metaByClass.get(prop.type);
370
+ if (!refMeta) {
371
+ return Array.from({ length: fieldNameCount }, () => "integer");
372
+ }
373
+ const primaryProps = getPrimaryProps(refMeta);
374
+ const referencedColumnNames = prop.referencedColumnNames ?? [];
375
+ return Array.from({ length: fieldNameCount }, (_value, index) => {
376
+ const referencedColumnName = referencedColumnNames[index];
377
+ const pkProp = referencedColumnName !== void 0 ? primaryProps.find((candidate) => candidate.fieldNames.includes(referencedColumnName)) : void 0;
378
+ return (pkProp ?? primaryProps[index] ?? primaryProps[0])?.type ?? "integer";
379
+ });
380
+ }
381
+ function getPrimaryProps(meta) {
382
+ const primaryKeys = meta.primaryKeys ?? [];
383
+ const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
384
+ if (orderedPrimaryProps.length > 0) {
385
+ return orderedPrimaryProps;
386
+ }
387
+ return Object.values(meta.properties).filter((prop) => prop.primary === true);
202
388
  }
203
389
  function buildConstraints(meta) {
204
390
  const result = [];
@@ -206,7 +392,7 @@ function buildConstraints(meta) {
206
392
  const props = idx.properties;
207
393
  result.push({
208
394
  type: "index",
209
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
395
+ properties: resolveConstraintProperties(meta, props),
210
396
  ...idx.name !== void 0 && { name: idx.name }
211
397
  });
212
398
  }
@@ -214,12 +400,14 @@ function buildConstraints(meta) {
214
400
  const props = uniq.properties;
215
401
  result.push({
216
402
  type: "unique",
217
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
403
+ properties: resolveConstraintProperties(meta, props),
218
404
  ...uniq.name !== void 0 && { name: uniq.name }
219
405
  });
220
406
  }
221
407
  for (const check of meta.checks ?? []) {
222
- if (typeof check.expression !== "string") continue;
408
+ if (typeof check.expression !== "string") {
409
+ continue;
410
+ }
223
411
  result.push({
224
412
  type: "check",
225
413
  properties: [],
@@ -229,13 +417,30 @@ function buildConstraints(meta) {
229
417
  }
230
418
  return result;
231
419
  }
420
+ function resolveConstraintProperties(meta, props) {
421
+ const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
422
+ return propNames.flatMap((propName) => {
423
+ const prop = meta.properties[String(propName)];
424
+ if (prop === void 0) {
425
+ return [String(propName)];
426
+ }
427
+ if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
428
+ return prop.fieldNames;
429
+ }
430
+ return [prop.name];
431
+ });
432
+ }
232
433
  function buildRelationEdges(metas) {
233
434
  const edges = [];
234
435
  for (const meta of metas) {
235
- if (meta.pivotTable || meta.embeddable) continue;
436
+ if (meta.pivotTable || meta.embeddable) {
437
+ continue;
438
+ }
236
439
  for (const prop of Object.values(meta.properties)) {
237
440
  const edge = buildEdge(meta.className, prop);
238
- if (edge !== null) edges.push(edge);
441
+ if (edge !== null) {
442
+ edges.push(edge);
443
+ }
239
444
  }
240
445
  }
241
446
  return edges;
@@ -243,6 +448,9 @@ function buildRelationEdges(metas) {
243
448
  function buildEdge(fromEntity, prop) {
244
449
  const isNullable = prop.nullable === true;
245
450
  if (prop.kind === ReferenceKind.MANY_TO_ONE) {
451
+ if (prop.type === fromEntity) {
452
+ return null;
453
+ }
246
454
  return {
247
455
  fromEntity,
248
456
  toEntity: prop.type,
@@ -274,7 +482,7 @@ function buildEdge(fromEntity, prop) {
274
482
  function renderErDiagram(model) {
275
483
  const lines = ["erDiagram"];
276
484
  for (const entity of model.entities) {
277
- lines.push(` ${entity.className} {`);
485
+ lines.push(` ${toMermaidIdentifier(entity.className)} {`);
278
486
  for (const col of entity.columns) {
279
487
  lines.push(` ${renderColumnLine(col)}`);
280
488
  }
@@ -282,16 +490,18 @@ function renderErDiagram(model) {
282
490
  }
283
491
  for (const rel of model.relations) {
284
492
  lines.push(
285
- ` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`
493
+ ` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
286
494
  );
287
495
  }
288
496
  return lines.join("\n");
289
497
  }
290
498
  function renderColumnLine(col) {
291
499
  let qualifier = "";
292
- if (col.isPrimary) qualifier = " PK";
293
- else if (col.isForeignKey) qualifier = " FK";
294
- else if (col.isUnique) qualifier = " UK";
500
+ if (col.isPrimary) {
501
+ qualifier = " PK";
502
+ } else if (col.isUnique) {
503
+ qualifier = " UK";
504
+ }
295
505
  let comment;
296
506
  if (col.formula !== void 0) {
297
507
  comment = col.formula ? `formula: ${col.formula}` : "formula";
@@ -299,41 +509,54 @@ function renderColumnLine(col) {
299
509
  comment = "discriminator";
300
510
  } else if (col.embeddedIn !== void 0) {
301
511
  comment = `[${col.embeddedIn}]`;
302
- } else if (col.fieldName !== col.propName) {
303
- comment = col.propName;
512
+ } else if (col.isSelfReference) {
513
+ comment = "self-ref";
304
514
  }
305
- const commentStr = comment !== void 0 ? ` "${comment}"` : "";
306
- return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
307
- }
308
- function normalizeType(type) {
309
- return type.replace(/[^a-zA-Z0-9_]/g, "_");
515
+ const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
516
+ return `${toMermaidIdentifier(col.type)} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
310
517
  }
311
518
 
312
519
  // src/model/build.ts
313
- function buildDocumentModel(metas, jsDocResult, title, description) {
314
- const { entities: diagramEntities, relations: allRelations } = buildDiagramModel(metas);
520
+ var FROM_ONE_OR_MORE = "}|";
521
+ var TO_ONE_OR_MORE = "|{";
522
+ function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
523
+ const { entities: diagramEntities, relations } = buildDiagramModel(metas);
524
+ const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
525
+ const hiddenClasses = /* @__PURE__ */ new Set();
526
+ for (const model of diagramEntities) {
527
+ if (jsDocResult.entities.get(model.className)?.hidden) {
528
+ hiddenClasses.add(model.className);
529
+ }
530
+ }
315
531
  const enrichedByClass = /* @__PURE__ */ new Map();
316
532
  for (const model of diagramEntities) {
317
533
  const jsDoc = jsDocResult.entities.get(model.className);
318
- if (jsDoc?.hidden) continue;
319
- const propDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
320
- enrichedByClass.set(model.className, { model, jsDoc, propDocs });
534
+ if (jsDoc?.hidden) {
535
+ continue;
536
+ }
537
+ const columns = model.columns.filter(
538
+ (col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
539
+ );
540
+ const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
541
+ const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
542
+ const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
543
+ enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
321
544
  }
322
545
  const groupNames = /* @__PURE__ */ new Set();
323
546
  let anyUntagged = false;
324
547
  for (const { jsDoc } of enrichedByClass.values()) {
325
- const allNs = [
326
- ...jsDoc?.namespaces ?? [],
327
- ...jsDoc?.erdNamespaces ?? [],
328
- ...jsDoc?.describeNamespaces ?? []
329
- ];
548
+ const allNs = [...jsDoc?.namespaces ?? [], ...jsDoc?.erdNamespaces ?? [], ...jsDoc?.describeNamespaces ?? []];
330
549
  if (allNs.length === 0) {
331
550
  anyUntagged = true;
332
551
  } else {
333
- for (const ns of allNs) groupNames.add(ns);
552
+ for (const ns of allNs) {
553
+ groupNames.add(ns);
554
+ }
334
555
  }
335
556
  }
336
- if (anyUntagged) groupNames.add("default");
557
+ if (anyUntagged) {
558
+ groupNames.add("default");
559
+ }
337
560
  const groups = [];
338
561
  for (const groupName of groupNames) {
339
562
  const isDefault = groupName === "default";
@@ -344,46 +567,153 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
344
567
  ({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
345
568
  );
346
569
  const erdClassNames = new Set(erdEntities.map((e) => e.model.className));
347
- const erdRelations = allRelations.filter(
348
- (r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity)
349
- );
570
+ const erdRelations = allRelations.filter((r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity));
350
571
  groups.push({ name: groupName, erdEntities, textEntities, erdRelations });
351
572
  }
352
573
  groups.sort((a, b) => {
353
- if (a.name === "default") return 1;
354
- if (b.name === "default") return -1;
574
+ if (a.name === "default") {
575
+ return 1;
576
+ }
577
+ if (b.name === "default") {
578
+ return -1;
579
+ }
355
580
  return a.name.localeCompare(b.name);
356
581
  });
357
582
  return { title, groups, ...description !== void 0 && { description } };
358
583
  }
584
+ function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
585
+ const merged = new Map(ownPropDocs);
586
+ for (const col of columns) {
587
+ if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
588
+ continue;
589
+ }
590
+ const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
591
+ if (info) {
592
+ merged.set(col.propName, info);
593
+ }
594
+ }
595
+ return merged;
596
+ }
597
+ function applyAtLeastOne(relations, metas, props, onWarn) {
598
+ const adjusted = relations.map((edge) => ({ ...edge }));
599
+ const metaByClass = new Map(metas.map((m) => [m.className, m]));
600
+ for (const [className, propMap] of props) {
601
+ const meta = metaByClass.get(className);
602
+ if (!meta) {
603
+ continue;
604
+ }
605
+ for (const [propName, info] of propMap) {
606
+ if (!info.atLeastOne) {
607
+ continue;
608
+ }
609
+ const prop = meta.properties[propName];
610
+ if (!prop) {
611
+ continue;
612
+ }
613
+ let edge;
614
+ if (prop.kind === ReferenceKind2.ONE_TO_MANY && prop.mappedBy) {
615
+ edge = adjusted.find(
616
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
617
+ );
618
+ if (edge) {
619
+ edge.fromCardinality = FROM_ONE_OR_MORE;
620
+ }
621
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.owner === true) {
622
+ edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
623
+ if (edge) {
624
+ edge.toCardinality = TO_ONE_OR_MORE;
625
+ }
626
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.mappedBy) {
627
+ edge = adjusted.find(
628
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
629
+ );
630
+ if (edge) {
631
+ edge.fromCardinality = FROM_ONE_OR_MORE;
632
+ }
633
+ }
634
+ if (!edge) {
635
+ onWarn?.(
636
+ `@atLeastOne on ${className}.${propName} had no effect: no matching relation edge was found. It applies only to collection relations that can be matched to a rendered edge: @OneToMany with mappedBy, or @ManyToMany on either the owning side or an inverse mappedBy side.`
637
+ );
638
+ }
639
+ }
640
+ }
641
+ return adjusted;
642
+ }
359
643
  function hasNoNamespaceTags(jsDoc) {
360
- if (!jsDoc) return true;
644
+ if (!jsDoc) {
645
+ return true;
646
+ }
361
647
  return jsDoc.namespaces.length === 0 && jsDoc.erdNamespaces.length === 0 && jsDoc.describeNamespaces.length === 0;
362
648
  }
363
649
  function belongsToGroupForErd(jsDoc, groupName, isDefault) {
364
- if (isDefault) return hasNoNamespaceTags(jsDoc);
365
- if (!jsDoc) return false;
650
+ if (isDefault) {
651
+ return hasNoNamespaceTags(jsDoc);
652
+ }
653
+ if (!jsDoc) {
654
+ return false;
655
+ }
366
656
  return jsDoc.namespaces.includes(groupName) || jsDoc.erdNamespaces.includes(groupName);
367
657
  }
368
658
  function belongsToGroupForText(jsDoc, groupName, isDefault) {
369
- if (isDefault) return hasNoNamespaceTags(jsDoc);
370
- if (!jsDoc) return false;
659
+ if (isDefault) {
660
+ return hasNoNamespaceTags(jsDoc);
661
+ }
662
+ if (!jsDoc) {
663
+ return false;
664
+ }
371
665
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
372
666
  }
373
667
 
668
+ // src/provider.ts
669
+ async function withTsMorphMetadataProvider(options, onWarn) {
670
+ if (options.metadataProvider !== void 0) {
671
+ return options;
672
+ }
673
+ try {
674
+ const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
675
+ return { ...options, metadataProvider: TsMorphMetadataProvider };
676
+ } catch (err) {
677
+ const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
678
+ const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
679
+ if (!isNotInstalled && onWarn) {
680
+ onWarn(
681
+ `@mikro-orm/reflection is installed but failed to load: ${err instanceof Error ? err.message : String(err)}. Ensure all @mikro-orm/* packages are installed at the same version.`
682
+ );
683
+ }
684
+ return options;
685
+ }
686
+ }
687
+
374
688
  // src/render/markdown.ts
375
689
  function renderMarkdown(docModel) {
376
- const sections = [`# ${docModel.title}`];
690
+ const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
377
691
  if (docModel.description) {
378
- sections.push(docModel.description);
692
+ sections.push(escapeMarkdownParagraph(docModel.description));
693
+ }
694
+ if (docModel.groups.length > 1) {
695
+ sections.push(renderTableOfContents(docModel.groups));
379
696
  }
380
697
  for (const group of docModel.groups) {
381
698
  sections.push(renderGroupSection(group));
382
699
  }
383
700
  return sections.join("\n\n");
384
701
  }
702
+ function renderBulletSection(header, items, renderItem) {
703
+ const lines = [header, ""];
704
+ for (const item of items) {
705
+ lines.push(renderItem(item));
706
+ }
707
+ return lines.join("\n");
708
+ }
709
+ function renderTableOfContents(groups) {
710
+ return renderBulletSection("## Contents", groups, (group) => {
711
+ const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
712
+ return `- [${label}](#${toMarkdownAnchor(group.name)})`;
713
+ });
714
+ }
385
715
  function renderGroupSection(group) {
386
- const parts = [`## ${group.name}`];
716
+ const parts = [`## ${escapeMarkdownInline(group.name)}`];
387
717
  if (group.erdEntities.length > 0) {
388
718
  const diagramModel = {
389
719
  entities: group.erdEntities.map((e) => e.model),
@@ -397,16 +727,18 @@ function renderGroupSection(group) {
397
727
  return parts.join("\n\n");
398
728
  }
399
729
  function renderEntitySection(entity) {
400
- const parts = [`### ${entity.model.className}`];
730
+ const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
731
+ parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
401
732
  if (entity.jsDoc?.description) {
402
- parts.push(`> ${entity.jsDoc.description}`);
733
+ parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
403
734
  }
404
735
  if (entity.model.discriminatorColumn) {
736
+ parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
737
+ } else if (entity.model.extendsEntity) {
738
+ const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
405
739
  parts.push(
406
- `*STI root \u2014 discriminator column: \`${entity.model.discriminatorColumn}\`*`
740
+ `*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
407
741
  );
408
- } else if (entity.model.extendsEntity) {
409
- parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
410
742
  }
411
743
  if (entity.model.columns.length > 0) {
412
744
  parts.push(renderColumnTable(entity));
@@ -414,6 +746,10 @@ function renderEntitySection(entity) {
414
746
  if (entity.model.constraints.length > 0) {
415
747
  parts.push(renderConstraints(entity.model.constraints));
416
748
  }
749
+ const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
750
+ if (computedColumns.length > 0) {
751
+ parts.push(renderComputedColumns(computedColumns));
752
+ }
417
753
  return parts.join("\n\n");
418
754
  }
419
755
  function renderColumnTable(entity) {
@@ -422,59 +758,256 @@ function renderColumnTable(entity) {
422
758
  const rows = entity.model.columns.map((col) => {
423
759
  const key = resolveColumnKey(col);
424
760
  const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
425
- const desc = entity.propDocs.get(col.propName)?.description ?? "";
426
- return `| ${col.fieldName} | ${col.type} | ${key} | ${nullable} | ${desc} |`;
761
+ const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
762
+ const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
763
+ const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
764
+ return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(col.type)} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
427
765
  });
428
766
  return [header, sep, ...rows].join("\n");
429
767
  }
430
768
  function resolveColumnKey(col) {
431
- if (col.isPrimary) return "PK";
769
+ const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
770
+ if (col.isPrimary && col.isForeignKey) {
771
+ return `PK, ${fkKey}`;
772
+ }
773
+ if (col.isPrimary) {
774
+ return "PK";
775
+ }
432
776
  if (col.isForeignKey) {
433
- return col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
777
+ return fkKey;
778
+ }
779
+ if (col.isUnique) {
780
+ return "UK";
781
+ }
782
+ if (col.isDiscriminator) {
783
+ return "discriminator";
784
+ }
785
+ if (col.embeddedIn !== void 0) {
786
+ return `[${col.embeddedIn}]`;
434
787
  }
435
- if (col.isUnique) return "UK";
436
- if (col.formula !== void 0) return `formula: ${col.formula}`;
437
- if (col.isDiscriminator) return "discriminator";
438
- if (col.embeddedIn !== void 0) return `[${col.embeddedIn}]`;
439
788
  return "";
440
789
  }
790
+ function renderComputedColumns(columns) {
791
+ return renderBulletSection("**Computed columns:**", columns, (col) => {
792
+ const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
793
+ return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
794
+ });
795
+ }
441
796
  function renderConstraints(constraints) {
442
- const lines = ["**Constraints:**", ""];
443
- for (const c of constraints) {
444
- const name = c.name ? ` \`${c.name}\`` : "";
797
+ return renderBulletSection("**Constraints:**", constraints, (c) => {
798
+ const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
799
+ const properties = c.properties.map(escapeMarkdownInline).join(", ");
445
800
  if (c.type === "index") {
446
- lines.push(`- Index${name}: (${c.properties.join(", ")})`);
447
- } else if (c.type === "unique") {
448
- lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
449
- } else if (c.type === "check") {
450
- lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
801
+ return `- Index${name}: (${properties})`;
451
802
  }
452
- }
453
- return lines.join("\n");
803
+ if (c.type === "unique") {
804
+ return `- Unique${name}: (${properties})`;
805
+ }
806
+ return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
807
+ });
454
808
  }
455
809
 
456
810
  // src/index.ts
811
+ var COMPILED_JS = /\.(c|m)?js$/i;
812
+ function resolveJsDocSources(sourcePaths, src, onWarn) {
813
+ if (src !== void 0 && src.length > 0) {
814
+ return src;
815
+ }
816
+ if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
817
+ onWarn(
818
+ 'Entities were discovered from compiled JavaScript, so JSDoc descriptions and @namespace/@hidden tags cannot be read (build tools strip comments). Hidden entities may be exposed. Pass --src "<glob to your .ts sources>" (or the `src` option) to read JSDoc from the original TypeScript files.'
819
+ );
820
+ }
821
+ return sourcePaths;
822
+ }
823
+ function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
824
+ if (jsDocResult.sourceFileCount === 0) {
825
+ throw new Error(
826
+ `No source files matched the explicit src paths: ${src.join(", ")}
827
+ Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
828
+ );
829
+ }
830
+ const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
831
+ const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
832
+ if (missingConcrete.length > 0) {
833
+ throw new Error(
834
+ `Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
835
+ Check that --src (or the \`src\` option) points at all TypeScript entity files. JSDoc tags such as @namespace and @hidden for missing entities cannot be read.`
836
+ );
837
+ }
838
+ if (onWarn) {
839
+ const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
840
+ if (missingAbstract.length > 0) {
841
+ onWarn(
842
+ `Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
843
+ @hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
844
+ );
845
+ }
846
+ }
847
+ }
848
+ function errorMessages(err) {
849
+ const messages = [];
850
+ const seen = /* @__PURE__ */ new Set();
851
+ let current = err;
852
+ while (current instanceof Error && !seen.has(current)) {
853
+ seen.add(current);
854
+ messages.push(current.message);
855
+ current = current.cause;
856
+ }
857
+ return messages;
858
+ }
859
+ function isMissingTsMorphSourceFile(err) {
860
+ return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
861
+ }
862
+ async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
863
+ try {
864
+ return await loadEntityMetadata(effectiveOrm);
865
+ } catch (err) {
866
+ const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
867
+ if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
868
+ throw err;
869
+ }
870
+ try {
871
+ return await loadEntityMetadata(originalOrm);
872
+ } catch {
873
+ throw err;
874
+ }
875
+ }
876
+ }
457
877
  async function generateMarkdown(options) {
458
- const { orm, title = "Database Schema", description, src = [] } = options;
459
- const metas = await loadEntityMetadata(orm);
460
- const jsDocResult = loadJsDoc(src);
461
- const docModel = buildDocumentModel(metas, jsDocResult, title, description);
878
+ const { orm, title = "Database Schema", description, src, onWarn } = options;
879
+ const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
880
+ const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
881
+ const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
882
+ if (src !== void 0 && src.length > 0) {
883
+ assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
884
+ }
885
+ const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
462
886
  return renderMarkdown(docModel);
463
887
  }
464
888
 
465
889
  // src/cli.ts
890
+ function toConfigImportSpecifier(configPath) {
891
+ return pathToFileURL(path2.resolve(configPath)).href;
892
+ }
893
+ function findNearestTsconfig(fromPath) {
894
+ let dir = path2.dirname(path2.resolve(fromPath));
895
+ for (; ; ) {
896
+ const candidate = path2.join(dir, "tsconfig.json");
897
+ if (syncFs.existsSync(candidate)) {
898
+ return candidate;
899
+ }
900
+ const parent = path2.dirname(dir);
901
+ if (parent === dir) {
902
+ return void 0;
903
+ }
904
+ dir = parent;
905
+ }
906
+ }
907
+ async function loadOrmOptions(configPath, tsconfigPath) {
908
+ const isTypeScriptConfig = configPath.endsWith(".ts");
909
+ if (isTypeScriptConfig) {
910
+ let register;
911
+ try {
912
+ ({ register } = await import("tsx/esm/api"));
913
+ } catch {
914
+ throw new Error('TypeScript config files require the "tsx" package.\nInstall it with: npm install -D tsx');
915
+ }
916
+ let tsconfig;
917
+ if (tsconfigPath !== void 0) {
918
+ tsconfig = path2.resolve(tsconfigPath);
919
+ if (!syncFs.existsSync(tsconfig)) {
920
+ throw new Error(`--tsconfig file not found: ${tsconfig}`);
921
+ }
922
+ } else {
923
+ tsconfig = findNearestTsconfig(configPath);
924
+ }
925
+ register(tsconfig !== void 0 ? { tsconfig } : {});
926
+ }
927
+ const configUrl = toConfigImportSpecifier(configPath);
928
+ let mod;
929
+ try {
930
+ mod = await import(
931
+ /* @vite-ignore */
932
+ configUrl
933
+ );
934
+ } catch (cause) {
935
+ if (isTypeScriptConfig) {
936
+ const detail = cause instanceof Error ? cause.message : String(cause);
937
+ throw new Error(
938
+ `Failed to load TypeScript config.
939
+ ${detail}
940
+
941
+ If this looks like a decorator/metadata error, the tsconfig applied to your entity files is likely missing "experimentalDecorators" / "emitDecoratorMetadata".
942
+ Make sure a tsconfig.json with those options sits next to your config file, or pass one explicitly with --tsconfig <path>.`,
943
+ { cause }
944
+ );
945
+ }
946
+ throw cause;
947
+ }
948
+ if (mod.default === void 0) {
949
+ throw new Error("Config file must use a default export, e.g. `export default defineConfig({ ... })`.");
950
+ }
951
+ const config = mod.default;
952
+ if (typeof config === "function" || config instanceof Promise) {
953
+ throw new Error(
954
+ "Config file default export must be a configuration object, not a function or Promise.\nResolve it first, or use the programmatic API instead (see README)."
955
+ );
956
+ }
957
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
958
+ throw new Error(
959
+ "Config file default export must be a configuration object, not a primitive value or array.\nExport a plain MikroORM options object instead."
960
+ );
961
+ }
962
+ const options = config;
963
+ const withPreferTs = isTypeScriptConfig && options.preferTs === void 0 ? { ...options, preferTs: true } : options;
964
+ return withPreferTs;
965
+ }
966
+ function formatErrorChain(err) {
967
+ const lines = [];
968
+ const seen = /* @__PURE__ */ new Set();
969
+ let current = err;
970
+ while (current instanceof Error && !seen.has(current)) {
971
+ seen.add(current);
972
+ lines.push(current.message);
973
+ current = current.cause;
974
+ }
975
+ if (current !== void 0 && !(current instanceof Error)) {
976
+ lines.push(String(current));
977
+ }
978
+ return lines.map((line, i) => i === 0 ? line : ` \u21B3 caused by: ${line}`).join("\n");
979
+ }
980
+ var REFLECTION_METADATA_HINT = "\n\nNote: this tool loads configs via tsx (esbuild), which does not emit 'emitDecoratorMetadata' reflection data, so enabling it will not help here.\nEither give each entity property an explicit 'type:'/'entity:' attribute, or install '@mikro-orm/reflection' \u2014 this tool then reads types from your TypeScript sources automatically.";
981
+ function formatDiscoveryError(err) {
982
+ const chain = formatErrorChain(err);
983
+ return chain.includes("emitDecoratorMetadata") ? chain + REFLECTION_METADATA_HINT : chain;
984
+ }
985
+ function formatFileSystemError(cause) {
986
+ if (cause instanceof Error) {
987
+ const code = "code" in cause && typeof cause.code === "string" ? ` (${cause.code})` : "";
988
+ return `${cause.message}${code}`;
989
+ }
990
+ return String(cause);
991
+ }
992
+ async function writeMarkdownFile(outPath, markdown) {
993
+ try {
994
+ await fs.mkdir(path2.dirname(outPath), { recursive: true });
995
+ await fs.writeFile(outPath, markdown, "utf-8");
996
+ } catch (cause) {
997
+ throw new Error(`Cannot write output file: ${outPath}
998
+ ${formatFileSystemError(cause)}`, { cause });
999
+ }
1000
+ }
466
1001
  async function run(opts) {
467
- const configPath = path.resolve(opts.config);
468
- const outPath = path.resolve(opts.out);
1002
+ const configPath = path2.resolve(opts.config);
1003
+ const outPath = path2.resolve(opts.out);
469
1004
  let ormOptions;
470
1005
  try {
471
- const mod = await import(configPath);
472
- ormOptions = mod.default ?? mod;
1006
+ ormOptions = await loadOrmOptions(configPath, opts.tsconfig);
473
1007
  } catch (err) {
474
- const hint = configPath.endsWith(".ts") ? "\nHint: TypeScript configs require tsx or ts-node:\n npx tsx ./node_modules/.bin/mikro-orm-markdown ..." : "";
475
1008
  process.stderr.write(
476
1009
  `Error: Cannot load config: ${configPath}
477
- ${err instanceof Error ? err.message : String(err)}${hint}
1010
+ ${err instanceof Error ? err.message : String(err)}
478
1011
  `
479
1012
  );
480
1013
  process.exit(1);
@@ -484,28 +1017,57 @@ ${err instanceof Error ? err.message : String(err)}${hint}
484
1017
  markdown = await generateMarkdown({
485
1018
  orm: ormOptions,
486
1019
  title: opts.title,
487
- src: opts.src,
488
- ...opts.description !== void 0 && { description: opts.description }
1020
+ ...opts.description !== void 0 && { description: opts.description },
1021
+ ...opts.src !== void 0 && { src: opts.src },
1022
+ onWarn: (message) => void process.stderr.write(`Warning: ${message}
1023
+ `)
489
1024
  });
490
1025
  } catch (err) {
491
- const msg = err instanceof MetadataLoadError ? err.message : err instanceof Error ? err.message : String(err);
492
- process.stderr.write(`Error: ${msg}
1026
+ process.stderr.write(`Error: ${formatDiscoveryError(err)}
493
1027
  `);
494
1028
  process.exit(1);
495
1029
  }
496
- await fs.writeFile(outPath, markdown, "utf-8");
497
- process.stdout.write(`\u2713 Written to ${path.relative(process.cwd(), outPath)}
1030
+ try {
1031
+ await writeMarkdownFile(outPath, markdown);
1032
+ } catch (err) {
1033
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1034
+ `);
1035
+ process.exit(1);
1036
+ }
1037
+ process.stdout.write(`\u2713 Written to ${path2.relative(process.cwd(), outPath)}
498
1038
  `);
499
1039
  }
500
1040
  var program = new Command().name("mikro-orm-markdown").description("Generate Mermaid ERD + markdown docs from MikroORM entities").requiredOption("-c, --config <path>", "MikroORM config file path").option("-o, --out <path>", "Output markdown file path", "./ERD.md").option("-t, --title <string>", "Document title", "Database Schema").option("-d, --description <string>", "Optional description paragraph shown below the title").option(
501
- "-s, --src <glob>",
502
- "Glob pattern for entity source files, repeatable (for JSDoc extraction)",
503
- (val, prev) => [...prev, val],
504
- []
1041
+ "--tsconfig <path>",
1042
+ "tsconfig.json to use when loading a .ts config (defaults to the nearest one beside the config file)"
1043
+ ).option(
1044
+ "--src <paths...>",
1045
+ "Source .ts file globs to read JSDoc from when entities run from compiled .js (comments are stripped at build time)"
505
1046
  ).action(run);
506
- program.parseAsync(process.argv).catch((err) => {
507
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}
1047
+ function isDirectCliExecution() {
1048
+ const entryPoint = process.argv[1];
1049
+ if (entryPoint === void 0) {
1050
+ return false;
1051
+ }
1052
+ try {
1053
+ return syncFs.realpathSync(path2.resolve(entryPoint)) === syncFs.realpathSync(fileURLToPath(import.meta.url));
1054
+ } catch {
1055
+ return false;
1056
+ }
1057
+ }
1058
+ if (isDirectCliExecution()) {
1059
+ program.parseAsync(process.argv).catch((err) => {
1060
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
508
1061
  `);
509
- process.exit(1);
510
- });
1062
+ process.exit(1);
1063
+ });
1064
+ }
1065
+ export {
1066
+ findNearestTsconfig,
1067
+ formatDiscoveryError,
1068
+ formatErrorChain,
1069
+ loadOrmOptions,
1070
+ toConfigImportSpecifier,
1071
+ writeMarkdownFile
1072
+ };
511
1073
  //# sourceMappingURL=cli.js.map