mikro-orm-markdown 0.1.0-alpha.2 → 0.1.0-alpha.4

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,17 +1,20 @@
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) {
14
- return { entities, props };
15
+ const classNames = /* @__PURE__ */ new Set();
16
+ if (filePaths.length === 0) {
17
+ return { entities, props, sourceFileCount: 0, classNames };
15
18
  }
16
19
  const project = new Project({
17
20
  skipAddingFilesFromTsConfig: true,
@@ -21,34 +24,44 @@ function loadJsDoc(srcGlobs) {
21
24
  skipLibCheck: true
22
25
  }
23
26
  });
24
- project.addSourceFilesAtPaths(srcGlobs);
25
- for (const sourceFile of project.getSourceFiles()) {
26
- for (const cls of sourceFile.getClasses()) {
27
- const className = cls.getName();
28
- if (!className) {
29
- continue;
30
- }
31
- const classDocs = cls.getJsDocs();
32
- if (classDocs.length > 0) {
33
- entities.set(className, parseEntityJsDoc(classDocs));
34
- }
35
- const propMap = /* @__PURE__ */ new Map();
36
- for (const prop of cls.getProperties()) {
37
- const propDocs = prop.getJsDocs();
38
- if (propDocs.length === 0) {
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
39
  continue;
40
40
  }
41
- const desc = extractDescription(propDocs);
42
- if (desc !== void 0) {
43
- propMap.set(prop.getName(), { description: desc });
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);
44
59
  }
45
60
  }
46
- if (propMap.size > 0) {
47
- props.set(className, propMap);
48
- }
61
+ } catch {
49
62
  }
50
63
  }
51
- return { entities, props };
64
+ return { entities, props, sourceFileCount: sourceFiles.length, classNames };
52
65
  }
53
66
  function parseEntityJsDoc(jsDocs) {
54
67
  const namespaces = [];
@@ -83,18 +96,26 @@ function parseEntityJsDoc(jsDocs) {
83
96
  hidden
84
97
  };
85
98
  }
86
- function extractDescription(jsDocs) {
99
+ function parsePropJsDoc(jsDocs) {
100
+ let description;
101
+ let atLeastOne = false;
87
102
  for (const doc of jsDocs) {
88
103
  const desc = doc.getDescription().trim();
89
- if (desc) {
90
- 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
+ }
91
111
  }
92
112
  }
93
- return void 0;
113
+ return { ...description !== void 0 && { description }, atLeastOne };
94
114
  }
95
115
 
96
116
  // src/metadata/load.ts
97
- import { MikroORM } from "@mikro-orm/core";
117
+ import * as path from "path";
118
+ import { EntitySchema, MikroORM } from "@mikro-orm/core";
98
119
  var MetadataLoadError = class extends Error {
99
120
  constructor(message, cause) {
100
121
  super(message);
@@ -102,13 +123,46 @@ var MetadataLoadError = class extends Error {
102
123
  this.name = "MetadataLoadError";
103
124
  }
104
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
+ }
105
154
  async function loadEntityMetadata(options) {
155
+ assertNoEntitySchemaEntities(options);
106
156
  let orm;
107
157
  try {
108
158
  orm = await MikroORM.init({
109
159
  ...options,
110
160
  debug: false,
111
- connect: 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 }
112
166
  });
113
167
  } catch (cause) {
114
168
  throw new MetadataLoadError(
@@ -116,23 +170,80 @@ async function loadEntityMetadata(options) {
116
170
  cause
117
171
  );
118
172
  }
119
- const all = Object.values(orm.getMetadata().getAll());
120
- if (all.length === 0) {
121
- throw new MetadataLoadError(
122
- "No entities were discovered. Check that your config specifies at least one entity path or class."
123
- );
173
+ try {
174
+ const all = Object.values(orm.getMetadata().getAll());
175
+ if (all.length === 0) {
176
+ throw new MetadataLoadError(
177
+ "No entities were discovered. Check that your config specifies at least one entity path or class."
178
+ );
179
+ }
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 };
183
+ } finally {
184
+ await closeDiscoveryResources(orm);
124
185
  }
125
- return all;
126
186
  }
127
187
 
188
+ // src/model/build.ts
189
+ import { ReferenceKind as ReferenceKind2 } from "@mikro-orm/core";
190
+
128
191
  // src/render/mermaid.ts
129
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
130
240
  var FORMULA_DUMMY_TABLE = {
131
241
  alias: "e0",
132
242
  name: "",
133
243
  qualifiedName: "",
134
244
  toString: () => "e0"
135
245
  };
246
+ var UNRESOLVED_FORMULA = "<unresolved>";
136
247
  function buildDiagramModel(metas) {
137
248
  const metaByClass = new Map(metas.map((m) => [m.className, m]));
138
249
  const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
@@ -140,17 +251,14 @@ function buildDiagramModel(metas) {
140
251
  return { entities, relations };
141
252
  }
142
253
  function buildEntityModel(meta, metaByClass) {
143
- const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.discriminatorValue;
254
+ const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
144
255
  const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
145
256
  const columns = [];
146
257
  for (const prop of Object.values(meta.properties)) {
147
258
  if (isStiRoot && prop.inherited === true) {
148
259
  continue;
149
260
  }
150
- const col = buildColumn(prop, metaByClass, meta);
151
- if (col !== null) {
152
- columns.push(col);
153
- }
261
+ columns.push(...buildColumns(prop, metaByClass, meta));
154
262
  }
155
263
  return {
156
264
  className: meta.className,
@@ -160,47 +268,73 @@ function buildEntityModel(meta, metaByClass) {
160
268
  isEmbeddable: meta.embeddable === true,
161
269
  ...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
162
270
  ...isStiChild && { extendsEntity: meta.extends },
271
+ ...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
163
272
  constraints: buildConstraints(meta)
164
273
  };
165
274
  }
166
- function buildColumn(prop, metaByClass, owningMeta) {
275
+ function buildColumns(prop, metaByClass, owningMeta) {
167
276
  if (prop.kind === ReferenceKind.EMBEDDED) {
168
- return null;
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 [];
169
293
  }
170
294
  if (prop.kind === ReferenceKind.SCALAR) {
295
+ if (prop.object === true && prop.embedded !== void 0) {
296
+ return [];
297
+ }
171
298
  const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
172
299
  let embeddedIn;
300
+ let embeddedPropName;
173
301
  if (prop.embedded !== void 0) {
174
302
  const parentPropName = prop.embedded[0];
175
303
  embeddedIn = owningMeta.properties[parentPropName]?.type;
304
+ embeddedPropName = prop.embedded[1];
176
305
  }
177
306
  const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
178
- return {
179
- propName: prop.name,
180
- fieldName: prop.fieldNames?.[0] ?? prop.name,
181
- type: normalizeType(prop.type),
182
- isPrimary: prop.primary === true,
183
- isForeignKey: false,
184
- isUnique: prop.unique === true,
185
- isNullable: prop.nullable === true,
186
- ...formulaExpr !== void 0 && { formula: formulaExpr },
187
- ...embeddedIn !== void 0 && { embeddedIn },
188
- ...isDiscriminator && { isDiscriminator: true }
189
- };
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
+ ];
190
329
  }
191
330
  if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner === true) {
192
- const fkType = resolveFkType(prop.type, metaByClass);
193
- return {
194
- propName: prop.name,
195
- fieldName: prop.fieldNames?.[0] ?? `${prop.name}_id`,
196
- type: fkType,
197
- isPrimary: false,
198
- isForeignKey: true,
199
- isUnique: prop.unique === true,
200
- isNullable: prop.nullable === true
201
- };
331
+ const cols = buildForeignKeyColumns(prop, metaByClass);
332
+ if (prop.type === owningMeta.className) {
333
+ return cols.map((col) => ({ ...col, isSelfReference: true }));
334
+ }
335
+ return cols;
202
336
  }
203
- return null;
337
+ return [];
204
338
  }
205
339
  function resolveFormulaExpr(cb) {
206
340
  try {
@@ -210,18 +344,69 @@ function resolveFormulaExpr(cb) {
210
344
  get: (_target, key) => typeof key === "string" ? key : ""
211
345
  }
212
346
  );
213
- return cb(FORMULA_DUMMY_TABLE, cols);
347
+ const result = cb(FORMULA_DUMMY_TABLE, cols);
348
+ return typeof result === "string" ? result : String(result);
214
349
  } catch {
215
- return "";
350
+ return UNRESOLVED_FORMULA;
216
351
  }
217
352
  }
218
- function resolveFkType(referencedClassName, metaByClass) {
219
- const refMeta = metaByClass.get(referencedClassName);
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);
220
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
+ const resolvedProp = pkProp ?? primaryProps[index] ?? primaryProps[0];
379
+ const rawType = resolvedProp?.type ?? "integer";
380
+ const pkIndex = resolvedProp !== void 0 ? primaryProps.indexOf(resolvedProp) : 0;
381
+ return resolveScalarType(rawType, metaByClass, pkIndex);
382
+ });
383
+ }
384
+ function resolveScalarType(type, metaByClass, pkIndex = 0, depth = 0) {
385
+ if (depth >= 5) {
221
386
  return "integer";
222
387
  }
223
- const pkProp = Object.values(refMeta.properties).find((p) => p.primary === true);
224
- return pkProp ? normalizeType(pkProp.type) : "integer";
388
+ const refMeta = metaByClass.get(type);
389
+ if (!refMeta) {
390
+ return type;
391
+ }
392
+ const primaryProps = getPrimaryProps(refMeta);
393
+ if (primaryProps.length === 0) {
394
+ return type;
395
+ }
396
+ const targetProp = primaryProps[pkIndex] ?? primaryProps[0];
397
+ const nextType = targetProp?.type ?? type;
398
+ if (nextType === type) {
399
+ return type;
400
+ }
401
+ return resolveScalarType(nextType, metaByClass, pkIndex, depth + 1);
402
+ }
403
+ function getPrimaryProps(meta) {
404
+ const primaryKeys = meta.primaryKeys ?? [];
405
+ const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
406
+ if (orderedPrimaryProps.length > 0) {
407
+ return orderedPrimaryProps;
408
+ }
409
+ return Object.values(meta.properties).filter((prop) => prop.primary === true);
225
410
  }
226
411
  function buildConstraints(meta) {
227
412
  const result = [];
@@ -229,7 +414,7 @@ function buildConstraints(meta) {
229
414
  const props = idx.properties;
230
415
  result.push({
231
416
  type: "index",
232
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
417
+ properties: resolveConstraintProperties(meta, props),
233
418
  ...idx.name !== void 0 && { name: idx.name }
234
419
  });
235
420
  }
@@ -237,7 +422,7 @@ function buildConstraints(meta) {
237
422
  const props = uniq.properties;
238
423
  result.push({
239
424
  type: "unique",
240
- properties: Array.isArray(props) ? props.map(String) : props ? [String(props)] : [],
425
+ properties: resolveConstraintProperties(meta, props),
241
426
  ...uniq.name !== void 0 && { name: uniq.name }
242
427
  });
243
428
  }
@@ -254,6 +439,19 @@ function buildConstraints(meta) {
254
439
  }
255
440
  return result;
256
441
  }
442
+ function resolveConstraintProperties(meta, props) {
443
+ const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
444
+ return propNames.flatMap((propName) => {
445
+ const prop = meta.properties[String(propName)];
446
+ if (prop === void 0) {
447
+ return [String(propName)];
448
+ }
449
+ if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
450
+ return prop.fieldNames;
451
+ }
452
+ return [prop.name];
453
+ });
454
+ }
257
455
  function buildRelationEdges(metas) {
258
456
  const edges = [];
259
457
  for (const meta of metas) {
@@ -272,6 +470,9 @@ function buildRelationEdges(metas) {
272
470
  function buildEdge(fromEntity, prop) {
273
471
  const isNullable = prop.nullable === true;
274
472
  if (prop.kind === ReferenceKind.MANY_TO_ONE) {
473
+ if (prop.type === fromEntity) {
474
+ return null;
475
+ }
275
476
  return {
276
477
  fromEntity,
277
478
  toEntity: prop.type,
@@ -300,17 +501,41 @@ function buildEdge(fromEntity, prop) {
300
501
  }
301
502
  return null;
302
503
  }
504
+ function normalizeType(type) {
505
+ const t = type.toLowerCase().trim();
506
+ if (t === "uuid" || t === "text" || t === "string" || t.startsWith("varchar")) {
507
+ return "string";
508
+ }
509
+ if (t === "timestamptz" || t === "timestamp" || t === "datetime") {
510
+ return "datetime";
511
+ }
512
+ if (t === "integer" || t === "int" || t === "bigint" || t === "smallint") {
513
+ return "integer";
514
+ }
515
+ if (t === "doubletype" || t === "double precision" || t === "double" || t === "float" || t === "decimal") {
516
+ return "float";
517
+ }
518
+ if (t === "boolean" || t === "bool") {
519
+ return "boolean";
520
+ }
521
+ if (t === "jsonb") {
522
+ return "json";
523
+ }
524
+ return type;
525
+ }
303
526
  function renderErDiagram(model) {
304
527
  const lines = ["erDiagram"];
305
528
  for (const entity of model.entities) {
306
- lines.push(` ${entity.className} {`);
529
+ lines.push(` ${toMermaidIdentifier(entity.className)} {`);
307
530
  for (const col of entity.columns) {
308
531
  lines.push(` ${renderColumnLine(col)}`);
309
532
  }
310
533
  lines.push(" }");
311
534
  }
312
535
  for (const rel of model.relations) {
313
- lines.push(` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`);
536
+ lines.push(
537
+ ` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
538
+ );
314
539
  }
315
540
  return lines.join("\n");
316
541
  }
@@ -318,8 +543,6 @@ function renderColumnLine(col) {
318
543
  let qualifier = "";
319
544
  if (col.isPrimary) {
320
545
  qualifier = " PK";
321
- } else if (col.isForeignKey) {
322
- qualifier = " FK";
323
546
  } else if (col.isUnique) {
324
547
  qualifier = " UK";
325
548
  }
@@ -330,27 +553,38 @@ function renderColumnLine(col) {
330
553
  comment = "discriminator";
331
554
  } else if (col.embeddedIn !== void 0) {
332
555
  comment = `[${col.embeddedIn}]`;
333
- } else if (col.fieldName !== col.propName) {
334
- comment = col.propName;
556
+ } else if (col.isSelfReference) {
557
+ comment = "self-ref";
335
558
  }
336
- const commentStr = comment !== void 0 ? ` "${comment}"` : "";
337
- return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
338
- }
339
- function normalizeType(type) {
340
- return type.replace(/[^a-zA-Z0-9_]/g, "_");
559
+ const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
560
+ return `${toMermaidIdentifier(normalizeType(col.type))} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
341
561
  }
342
562
 
343
563
  // src/model/build.ts
344
- function buildDocumentModel(metas, jsDocResult, title, description) {
345
- const { entities: diagramEntities, relations: allRelations } = buildDiagramModel(metas);
564
+ var FROM_ONE_OR_MORE = "}|";
565
+ var TO_ONE_OR_MORE = "|{";
566
+ function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
567
+ const { entities: diagramEntities, relations } = buildDiagramModel(metas);
568
+ const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
569
+ const hiddenClasses = /* @__PURE__ */ new Set();
570
+ for (const model of diagramEntities) {
571
+ if (jsDocResult.entities.get(model.className)?.hidden) {
572
+ hiddenClasses.add(model.className);
573
+ }
574
+ }
346
575
  const enrichedByClass = /* @__PURE__ */ new Map();
347
576
  for (const model of diagramEntities) {
348
577
  const jsDoc = jsDocResult.entities.get(model.className);
349
578
  if (jsDoc?.hidden) {
350
579
  continue;
351
580
  }
352
- const propDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
353
- enrichedByClass.set(model.className, { model, jsDoc, propDocs });
581
+ const columns = model.columns.filter(
582
+ (col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
583
+ );
584
+ const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
585
+ const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
586
+ const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
587
+ enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
354
588
  }
355
589
  const groupNames = /* @__PURE__ */ new Set();
356
590
  let anyUntagged = false;
@@ -370,9 +604,16 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
370
604
  const groups = [];
371
605
  for (const groupName of groupNames) {
372
606
  const isDefault = groupName === "default";
373
- const erdEntities = [...enrichedByClass.values()].filter(
374
- ({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)
375
- );
607
+ const erdEntities = [...enrichedByClass.values()].filter(({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)).map((entity) => {
608
+ if (isCrossNamespaceInGroup(entity.jsDoc, groupName, isDefault)) {
609
+ const pkColumns = entity.model.columns.filter((col) => col.isPrimary);
610
+ if (pkColumns.length === 0) {
611
+ return null;
612
+ }
613
+ return { ...entity, model: { ...entity.model, columns: pkColumns } };
614
+ }
615
+ return entity;
616
+ }).filter((entity) => entity !== null);
376
617
  const textEntities = [...enrichedByClass.values()].filter(
377
618
  ({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
378
619
  );
@@ -391,6 +632,65 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
391
632
  });
392
633
  return { title, groups, ...description !== void 0 && { description } };
393
634
  }
635
+ function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
636
+ const merged = new Map(ownPropDocs);
637
+ for (const col of columns) {
638
+ if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
639
+ continue;
640
+ }
641
+ const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
642
+ if (info) {
643
+ merged.set(col.propName, info);
644
+ }
645
+ }
646
+ return merged;
647
+ }
648
+ function applyAtLeastOne(relations, metas, props, onWarn) {
649
+ const adjusted = relations.map((edge) => ({ ...edge }));
650
+ const metaByClass = new Map(metas.map((m) => [m.className, m]));
651
+ for (const [className, propMap] of props) {
652
+ const meta = metaByClass.get(className);
653
+ if (!meta) {
654
+ continue;
655
+ }
656
+ for (const [propName, info] of propMap) {
657
+ if (!info.atLeastOne) {
658
+ continue;
659
+ }
660
+ const prop = meta.properties[propName];
661
+ if (!prop) {
662
+ continue;
663
+ }
664
+ let edge;
665
+ if (prop.kind === ReferenceKind2.ONE_TO_MANY && prop.mappedBy) {
666
+ edge = adjusted.find(
667
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
668
+ );
669
+ if (edge) {
670
+ edge.fromCardinality = FROM_ONE_OR_MORE;
671
+ }
672
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.owner === true) {
673
+ edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
674
+ if (edge) {
675
+ edge.toCardinality = TO_ONE_OR_MORE;
676
+ }
677
+ } else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.mappedBy) {
678
+ edge = adjusted.find(
679
+ (e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
680
+ );
681
+ if (edge) {
682
+ edge.fromCardinality = FROM_ONE_OR_MORE;
683
+ }
684
+ }
685
+ if (!edge) {
686
+ onWarn?.(
687
+ `@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.`
688
+ );
689
+ }
690
+ }
691
+ }
692
+ return adjusted;
693
+ }
394
694
  function hasNoNamespaceTags(jsDoc) {
395
695
  if (!jsDoc) {
396
696
  return true;
@@ -415,20 +715,62 @@ function belongsToGroupForText(jsDoc, groupName, isDefault) {
415
715
  }
416
716
  return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
417
717
  }
718
+ function isCrossNamespaceInGroup(jsDoc, groupName, isDefault) {
719
+ if (isDefault || !jsDoc) {
720
+ return false;
721
+ }
722
+ return jsDoc.erdNamespaces.includes(groupName) && !jsDoc.namespaces.includes(groupName) && !jsDoc.describeNamespaces.includes(groupName);
723
+ }
724
+
725
+ // src/provider.ts
726
+ async function withTsMorphMetadataProvider(options, onWarn) {
727
+ if (options.metadataProvider !== void 0) {
728
+ return options;
729
+ }
730
+ try {
731
+ const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
732
+ return { ...options, metadataProvider: TsMorphMetadataProvider };
733
+ } catch (err) {
734
+ const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
735
+ const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
736
+ if (!isNotInstalled && onWarn) {
737
+ onWarn(
738
+ `@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.`
739
+ );
740
+ }
741
+ return options;
742
+ }
743
+ }
418
744
 
419
745
  // src/render/markdown.ts
420
746
  function renderMarkdown(docModel) {
421
- const sections = [`# ${docModel.title}`];
747
+ const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
422
748
  if (docModel.description) {
423
- sections.push(docModel.description);
749
+ sections.push(escapeMarkdownParagraph(docModel.description));
750
+ }
751
+ if (docModel.groups.length > 1) {
752
+ sections.push(renderTableOfContents(docModel.groups));
424
753
  }
425
754
  for (const group of docModel.groups) {
426
755
  sections.push(renderGroupSection(group));
427
756
  }
428
757
  return sections.join("\n\n");
429
758
  }
759
+ function renderBulletSection(header, items, renderItem) {
760
+ const lines = [header, ""];
761
+ for (const item of items) {
762
+ lines.push(renderItem(item));
763
+ }
764
+ return lines.join("\n");
765
+ }
766
+ function renderTableOfContents(groups) {
767
+ return renderBulletSection("## Contents", groups, (group) => {
768
+ const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
769
+ return `- [${label}](#${toMarkdownAnchor(group.name)})`;
770
+ });
771
+ }
430
772
  function renderGroupSection(group) {
431
- const parts = [`## ${group.name}`];
773
+ const parts = [`## ${escapeMarkdownInline(group.name)}`];
432
774
  if (group.erdEntities.length > 0) {
433
775
  const diagramModel = {
434
776
  entities: group.erdEntities.map((e) => e.model),
@@ -442,14 +784,18 @@ function renderGroupSection(group) {
442
784
  return parts.join("\n\n");
443
785
  }
444
786
  function renderEntitySection(entity) {
445
- const parts = [`### ${entity.model.className}`];
787
+ const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
788
+ parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
446
789
  if (entity.jsDoc?.description) {
447
- parts.push(`> ${entity.jsDoc.description}`);
790
+ parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
448
791
  }
449
792
  if (entity.model.discriminatorColumn) {
450
- parts.push(`*STI root \u2014 discriminator column: \`${entity.model.discriminatorColumn}\`*`);
793
+ parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
451
794
  } else if (entity.model.extendsEntity) {
452
- parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
795
+ const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
796
+ parts.push(
797
+ `*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
798
+ );
453
799
  }
454
800
  if (entity.model.columns.length > 0) {
455
801
  parts.push(renderColumnTable(entity));
@@ -457,6 +803,10 @@ function renderEntitySection(entity) {
457
803
  if (entity.model.constraints.length > 0) {
458
804
  parts.push(renderConstraints(entity.model.constraints));
459
805
  }
806
+ const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
807
+ if (computedColumns.length > 0) {
808
+ parts.push(renderComputedColumns(computedColumns));
809
+ }
460
810
  return parts.join("\n\n");
461
811
  }
462
812
  function renderColumnTable(entity) {
@@ -465,24 +815,27 @@ function renderColumnTable(entity) {
465
815
  const rows = entity.model.columns.map((col) => {
466
816
  const key = resolveColumnKey(col);
467
817
  const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
468
- const desc = entity.propDocs.get(col.propName)?.description ?? "";
469
- return `| ${col.fieldName} | ${col.type} | ${key} | ${nullable} | ${desc} |`;
818
+ const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
819
+ const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
820
+ const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
821
+ return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(normalizeType(col.type))} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
470
822
  });
471
823
  return [header, sep, ...rows].join("\n");
472
824
  }
473
825
  function resolveColumnKey(col) {
826
+ const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
827
+ if (col.isPrimary && col.isForeignKey) {
828
+ return `PK, ${fkKey}`;
829
+ }
474
830
  if (col.isPrimary) {
475
831
  return "PK";
476
832
  }
477
833
  if (col.isForeignKey) {
478
- return col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
834
+ return fkKey;
479
835
  }
480
836
  if (col.isUnique) {
481
837
  return "UK";
482
838
  }
483
- if (col.formula !== void 0) {
484
- return `formula: ${col.formula}`;
485
- }
486
839
  if (col.isDiscriminator) {
487
840
  return "discriminator";
488
841
  }
@@ -491,43 +844,227 @@ function resolveColumnKey(col) {
491
844
  }
492
845
  return "";
493
846
  }
847
+ function renderComputedColumns(columns) {
848
+ return renderBulletSection("**Computed columns:**", columns, (col) => {
849
+ const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
850
+ return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
851
+ });
852
+ }
494
853
  function renderConstraints(constraints) {
495
- const lines = ["**Constraints:**", ""];
496
- for (const c of constraints) {
497
- const name = c.name ? ` \`${c.name}\`` : "";
854
+ return renderBulletSection("**Constraints:**", constraints, (c) => {
855
+ const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
856
+ const properties = c.properties.map(escapeMarkdownInline).join(", ");
498
857
  if (c.type === "index") {
499
- lines.push(`- Index${name}: (${c.properties.join(", ")})`);
500
- } else if (c.type === "unique") {
501
- lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
502
- } else if (c.type === "check") {
503
- lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
858
+ return `- Index${name}: (${properties})`;
504
859
  }
505
- }
506
- return lines.join("\n");
860
+ if (c.type === "unique") {
861
+ return `- Unique${name}: (${properties})`;
862
+ }
863
+ return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
864
+ });
507
865
  }
508
866
 
509
867
  // src/index.ts
868
+ var COMPILED_JS = /\.(c|m)?js$/i;
869
+ function resolveJsDocSources(sourcePaths, src, onWarn) {
870
+ if (src !== void 0 && src.length > 0) {
871
+ return src;
872
+ }
873
+ if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
874
+ onWarn(
875
+ '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.'
876
+ );
877
+ }
878
+ return sourcePaths;
879
+ }
880
+ function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
881
+ if (jsDocResult.sourceFileCount === 0) {
882
+ throw new Error(
883
+ `No source files matched the explicit src paths: ${src.join(", ")}
884
+ Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
885
+ );
886
+ }
887
+ const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
888
+ const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
889
+ if (missingConcrete.length > 0) {
890
+ throw new Error(
891
+ `Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
892
+ 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.`
893
+ );
894
+ }
895
+ if (onWarn) {
896
+ const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
897
+ if (missingAbstract.length > 0) {
898
+ onWarn(
899
+ `Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
900
+ @hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
901
+ );
902
+ }
903
+ }
904
+ }
905
+ function errorMessages(err) {
906
+ const messages = [];
907
+ const seen = /* @__PURE__ */ new Set();
908
+ let current = err;
909
+ while (current instanceof Error && !seen.has(current)) {
910
+ seen.add(current);
911
+ messages.push(current.message);
912
+ current = current.cause;
913
+ }
914
+ return messages;
915
+ }
916
+ function isMissingTsMorphSourceFile(err) {
917
+ return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
918
+ }
919
+ async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
920
+ try {
921
+ return await loadEntityMetadata(effectiveOrm);
922
+ } catch (err) {
923
+ const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
924
+ if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
925
+ throw err;
926
+ }
927
+ try {
928
+ return await loadEntityMetadata(originalOrm);
929
+ } catch {
930
+ throw err;
931
+ }
932
+ }
933
+ }
510
934
  async function generateMarkdown(options) {
511
- const { orm, title = "Database Schema", description, src = [] } = options;
512
- const metas = await loadEntityMetadata(orm);
513
- const jsDocResult = loadJsDoc(src);
514
- const docModel = buildDocumentModel(metas, jsDocResult, title, description);
935
+ const { orm, title = "Database Schema", description, src, onWarn } = options;
936
+ const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
937
+ const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
938
+ const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
939
+ if (src !== void 0 && src.length > 0) {
940
+ assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
941
+ }
942
+ const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
515
943
  return renderMarkdown(docModel);
516
944
  }
517
945
 
518
946
  // src/cli.ts
947
+ function toConfigImportSpecifier(configPath) {
948
+ return pathToFileURL(path2.resolve(configPath)).href;
949
+ }
950
+ function findNearestTsconfig(fromPath) {
951
+ let dir = path2.dirname(path2.resolve(fromPath));
952
+ for (; ; ) {
953
+ const candidate = path2.join(dir, "tsconfig.json");
954
+ if (syncFs.existsSync(candidate)) {
955
+ return candidate;
956
+ }
957
+ const parent = path2.dirname(dir);
958
+ if (parent === dir) {
959
+ return void 0;
960
+ }
961
+ dir = parent;
962
+ }
963
+ }
964
+ async function loadOrmOptions(configPath, tsconfigPath) {
965
+ const isTypeScriptConfig = configPath.endsWith(".ts");
966
+ if (isTypeScriptConfig) {
967
+ let register;
968
+ try {
969
+ ({ register } = await import("tsx/esm/api"));
970
+ } catch {
971
+ throw new Error('TypeScript config files require the "tsx" package.\nInstall it with: npm install -D tsx');
972
+ }
973
+ let tsconfig;
974
+ if (tsconfigPath !== void 0) {
975
+ tsconfig = path2.resolve(tsconfigPath);
976
+ if (!syncFs.existsSync(tsconfig)) {
977
+ throw new Error(`--tsconfig file not found: ${tsconfig}`);
978
+ }
979
+ } else {
980
+ tsconfig = findNearestTsconfig(configPath);
981
+ }
982
+ register(tsconfig !== void 0 ? { tsconfig } : {});
983
+ }
984
+ const configUrl = toConfigImportSpecifier(configPath);
985
+ let mod;
986
+ try {
987
+ mod = await import(
988
+ /* @vite-ignore */
989
+ configUrl
990
+ );
991
+ } catch (cause) {
992
+ if (isTypeScriptConfig) {
993
+ const detail = cause instanceof Error ? cause.message : String(cause);
994
+ throw new Error(
995
+ `Failed to load TypeScript config.
996
+ ${detail}
997
+
998
+ If this looks like a decorator/metadata error, the tsconfig applied to your entity files is likely missing "experimentalDecorators" / "emitDecoratorMetadata".
999
+ Make sure a tsconfig.json with those options sits next to your config file, or pass one explicitly with --tsconfig <path>.`,
1000
+ { cause }
1001
+ );
1002
+ }
1003
+ throw cause;
1004
+ }
1005
+ if (mod.default === void 0) {
1006
+ throw new Error("Config file must use a default export, e.g. `export default defineConfig({ ... })`.");
1007
+ }
1008
+ const config = mod.default;
1009
+ if (typeof config === "function" || config instanceof Promise) {
1010
+ throw new Error(
1011
+ "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)."
1012
+ );
1013
+ }
1014
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
1015
+ throw new Error(
1016
+ "Config file default export must be a configuration object, not a primitive value or array.\nExport a plain MikroORM options object instead."
1017
+ );
1018
+ }
1019
+ const options = config;
1020
+ const withPreferTs = isTypeScriptConfig && options.preferTs === void 0 ? { ...options, preferTs: true } : options;
1021
+ return withPreferTs;
1022
+ }
1023
+ function formatErrorChain(err) {
1024
+ const lines = [];
1025
+ const seen = /* @__PURE__ */ new Set();
1026
+ let current = err;
1027
+ while (current instanceof Error && !seen.has(current)) {
1028
+ seen.add(current);
1029
+ lines.push(current.message);
1030
+ current = current.cause;
1031
+ }
1032
+ if (current !== void 0 && !(current instanceof Error)) {
1033
+ lines.push(String(current));
1034
+ }
1035
+ return lines.map((line, i) => i === 0 ? line : ` \u21B3 caused by: ${line}`).join("\n");
1036
+ }
1037
+ 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.";
1038
+ function formatDiscoveryError(err) {
1039
+ const chain = formatErrorChain(err);
1040
+ return chain.includes("emitDecoratorMetadata") ? chain + REFLECTION_METADATA_HINT : chain;
1041
+ }
1042
+ function formatFileSystemError(cause) {
1043
+ if (cause instanceof Error) {
1044
+ const code = "code" in cause && typeof cause.code === "string" ? ` (${cause.code})` : "";
1045
+ return `${cause.message}${code}`;
1046
+ }
1047
+ return String(cause);
1048
+ }
1049
+ async function writeMarkdownFile(outPath, markdown) {
1050
+ try {
1051
+ await fs.mkdir(path2.dirname(outPath), { recursive: true });
1052
+ await fs.writeFile(outPath, markdown, "utf-8");
1053
+ } catch (cause) {
1054
+ throw new Error(`Cannot write output file: ${outPath}
1055
+ ${formatFileSystemError(cause)}`, { cause });
1056
+ }
1057
+ }
519
1058
  async function run(opts) {
520
- const configPath = path.resolve(opts.config);
521
- const outPath = path.resolve(opts.out);
1059
+ const configPath = path2.resolve(opts.config);
1060
+ const outPath = path2.resolve(opts.out);
522
1061
  let ormOptions;
523
1062
  try {
524
- const mod = await import(configPath);
525
- ormOptions = mod.default ?? mod;
1063
+ ormOptions = await loadOrmOptions(configPath, opts.tsconfig);
526
1064
  } catch (err) {
527
- const hint = configPath.endsWith(".ts") ? "\nHint: TypeScript configs require tsx or ts-node:\n npx tsx ./node_modules/.bin/mikro-orm-markdown ..." : "";
528
1065
  process.stderr.write(
529
1066
  `Error: Cannot load config: ${configPath}
530
- ${err instanceof Error ? err.message : String(err)}${hint}
1067
+ ${err instanceof Error ? err.message : String(err)}
531
1068
  `
532
1069
  );
533
1070
  process.exit(1);
@@ -537,23 +1074,57 @@ ${err instanceof Error ? err.message : String(err)}${hint}
537
1074
  markdown = await generateMarkdown({
538
1075
  orm: ormOptions,
539
1076
  title: opts.title,
540
- src: opts.src ?? [],
541
- ...opts.description !== void 0 && { description: opts.description }
1077
+ ...opts.description !== void 0 && { description: opts.description },
1078
+ ...opts.src !== void 0 && { src: opts.src },
1079
+ onWarn: (message) => void process.stderr.write(`Warning: ${message}
1080
+ `)
542
1081
  });
543
1082
  } catch (err) {
544
- const msg = err instanceof MetadataLoadError ? err.message : err instanceof Error ? err.message : String(err);
545
- process.stderr.write(`Error: ${msg}
1083
+ process.stderr.write(`Error: ${formatDiscoveryError(err)}
546
1084
  `);
547
1085
  process.exit(1);
548
1086
  }
549
- await fs.writeFile(outPath, markdown, "utf-8");
550
- process.stdout.write(`\u2713 Written to ${path.relative(process.cwd(), outPath)}
1087
+ try {
1088
+ await writeMarkdownFile(outPath, markdown);
1089
+ } catch (err) {
1090
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
551
1091
  `);
1092
+ process.exit(1);
1093
+ }
1094
+ process.stdout.write(`\u2713 Written to ${path2.relative(process.cwd(), outPath)}
1095
+ `);
1096
+ }
1097
+ 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(
1098
+ "--tsconfig <path>",
1099
+ "tsconfig.json to use when loading a .ts config (defaults to the nearest one beside the config file)"
1100
+ ).option(
1101
+ "--src <paths...>",
1102
+ "Source .ts file globs to read JSDoc from when entities run from compiled .js (comments are stripped at build time)"
1103
+ ).action(run);
1104
+ function isDirectCliExecution() {
1105
+ const entryPoint = process.argv[1];
1106
+ if (entryPoint === void 0) {
1107
+ return false;
1108
+ }
1109
+ try {
1110
+ return syncFs.realpathSync(path2.resolve(entryPoint)) === syncFs.realpathSync(fileURLToPath(import.meta.url));
1111
+ } catch {
1112
+ return false;
1113
+ }
552
1114
  }
553
- 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("-s, --src <glob>", "JSDoc source glob (repeatable)", (val, prev = []) => [...prev, val]).action(run);
554
- program.parseAsync(process.argv).catch((err) => {
555
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}
1115
+ if (isDirectCliExecution()) {
1116
+ program.parseAsync(process.argv).catch((err) => {
1117
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
556
1118
  `);
557
- process.exit(1);
558
- });
1119
+ process.exit(1);
1120
+ });
1121
+ }
1122
+ export {
1123
+ findNearestTsconfig,
1124
+ formatDiscoveryError,
1125
+ formatErrorChain,
1126
+ loadOrmOptions,
1127
+ toConfigImportSpecifier,
1128
+ writeMarkdownFile
1129
+ };
559
1130
  //# sourceMappingURL=cli.js.map