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