@dogsbay/format-mdx 0.2.0-beta.1 → 0.2.0-beta.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/adapter.d.ts +159 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +95 -0
  4. package/dist/adapter.js.map +1 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +269 -19
  7. package/dist/cli.js.map +1 -1
  8. package/dist/extract.d.ts +32 -0
  9. package/dist/extract.d.ts.map +1 -0
  10. package/dist/extract.js +152 -0
  11. package/dist/extract.js.map +1 -0
  12. package/dist/fumadocs.d.ts +48 -0
  13. package/dist/fumadocs.d.ts.map +1 -0
  14. package/dist/fumadocs.js +470 -0
  15. package/dist/fumadocs.js.map +1 -0
  16. package/dist/index.d.ts +11 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +11 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/js-scan.d.ts +35 -0
  21. package/dist/js-scan.d.ts.map +1 -0
  22. package/dist/js-scan.js +124 -0
  23. package/dist/js-scan.js.map +1 -0
  24. package/dist/meta-nav.d.ts +29 -0
  25. package/dist/meta-nav.d.ts.map +1 -0
  26. package/dist/meta-nav.js +194 -0
  27. package/dist/meta-nav.js.map +1 -0
  28. package/dist/mintlify-nav.d.ts +31 -0
  29. package/dist/mintlify-nav.d.ts.map +1 -0
  30. package/dist/mintlify-nav.js +418 -0
  31. package/dist/mintlify-nav.js.map +1 -0
  32. package/dist/mintlify-openapi.d.ts +107 -0
  33. package/dist/mintlify-openapi.d.ts.map +1 -0
  34. package/dist/mintlify-openapi.js +357 -0
  35. package/dist/mintlify-openapi.js.map +1 -0
  36. package/dist/mintlify.d.ts +3 -0
  37. package/dist/mintlify.d.ts.map +1 -0
  38. package/dist/mintlify.js +196 -0
  39. package/dist/mintlify.js.map +1 -0
  40. package/dist/nav-extract.d.ts +43 -0
  41. package/dist/nav-extract.d.ts.map +1 -0
  42. package/dist/nav-extract.js +135 -0
  43. package/dist/nav-extract.js.map +1 -0
  44. package/dist/parse.d.ts +100 -4
  45. package/dist/parse.d.ts.map +1 -1
  46. package/dist/parse.js +1228 -149
  47. package/dist/parse.js.map +1 -1
  48. package/dist/ts-type-block.d.ts +43 -0
  49. package/dist/ts-type-block.d.ts.map +1 -0
  50. package/dist/ts-type-block.js +164 -0
  51. package/dist/ts-type-block.js.map +1 -0
  52. package/package.json +10 -8
@@ -0,0 +1,159 @@
1
+ /**
2
+ * MDX adapter — extension points for components and conventions a specific
3
+ * site or docs framework layers on top of *generic* MDX.
4
+ *
5
+ * Mirrors the proven Docusaurus adapter architecture
6
+ * (format-docusaurus/src/adapter.ts): the generic parser handles only the
7
+ * common Mintlify-style vocabulary (callouts, Tabs, Steps, Cards…); anything
8
+ * a framework or site invented is injected via an adapter and/or per-source
9
+ * declarative component config.
10
+ *
11
+ * Three layers (plans/format-fumadocs.md):
12
+ * A. generic parser + open registry + never-drop `mdx-raw` + fidelity ledger
13
+ * B. framework adapters (e.g. `fumadocs`)
14
+ * C. per-docset declarative config (`as` / `props` / `children` / `handler`)
15
+ *
16
+ * Trust boundary: declarative config and built-in named handlers are safe
17
+ * everywhere (no subscriber-supplied code is ever executed). Arbitrary
18
+ * `handler:` JS modules are NOT supported here — a hosted updater must be
19
+ * able to run any docset config without executing its code.
20
+ */
21
+ import type { TreeNode } from "@dogsbay/types";
22
+ /** How a component's inner content is treated. */
23
+ export type ChildrenPolicy = "blocks" | "code" | "raw" | "ignore";
24
+ /** Context handed to function-form component handlers. */
25
+ export interface ComponentHandlerContext {
26
+ /** Parse an MDX source fragment into TreeNode[] with the active registry. */
27
+ parseBlocks: (source: string) => TreeNode[];
28
+ /** Append a fidelity-ledger entry. */
29
+ ledger: (entry: Omit<MdxLedgerEntry, "file">) => void;
30
+ /**
31
+ * Same-file `export const` literals (name → raw source text). Lets
32
+ * handlers statically resolve `attr={identifier}` references — the
33
+ * common "data next to the component" MDX pattern. Never executed.
34
+ */
35
+ moduleData: Record<string, string>;
36
+ }
37
+ export interface ComponentMapping {
38
+ /** TreeNode type to produce (e.g. "callout", "tabs", "endpoint"). */
39
+ type: string;
40
+ /** Extract props from JSX attributes. Bare attributes arrive as "true". */
41
+ propsFromAttrs?: (attrs: Record<string, string>) => Record<string, unknown>;
42
+ /** Whether this component wraps children. */
43
+ container: boolean;
44
+ /** Strip this component entirely (recorded in the ledger). */
45
+ strip?: boolean;
46
+ /**
47
+ * How inner content is treated (default "blocks" for containers):
48
+ * - "blocks" — parsed as nested MDX blocks;
49
+ * - "code" — kept as a single code node (a lone inner fence is
50
+ * unwrapped and keeps its language);
51
+ * - "raw" — kept verbatim as an `mdx-raw` child;
52
+ * - "ignore" — dropped (explicit, unlike the old silent swallow).
53
+ */
54
+ children?: ChildrenPolicy;
55
+ /**
56
+ * Function form — full control over the produced node(s). Return
57
+ * "preserve" to fall back to a byte-exact `mdx-raw` node (+ ledger entry);
58
+ * return null to drop the component (recorded as "stripped").
59
+ * Takes precedence over type/propsFromAttrs/children.
60
+ */
61
+ toNode?: (attrs: Record<string, string>, innerSource: string, ctx: ComponentHandlerContext) => TreeNode | TreeNode[] | "preserve" | null;
62
+ }
63
+ export interface MdxPreprocessContext {
64
+ /** Absolute path of the source file being parsed. */
65
+ filePath?: string;
66
+ /** Absolute path of the docs root directory. */
67
+ rootDir?: string;
68
+ }
69
+ export interface MdxAdapter {
70
+ /** Adapter name (e.g. "fumadocs"). */
71
+ name: string;
72
+ /** Raw-text rewrites applied BEFORE any parsing. */
73
+ preprocess?: (source: string, ctx: MdxPreprocessContext) => string;
74
+ /** Block-level component mappings, merged over the generic builtins. */
75
+ components?: Record<string, ComponentMapping>;
76
+ /** Tags treated as self-closing when the source omits the `/>`. */
77
+ selfClosingTags?: string[];
78
+ /**
79
+ * Inline component transformations applied to source text before parsing.
80
+ * Each entry: [regex, replacement string or function].
81
+ */
82
+ inlineTransforms?: [
83
+ RegExp,
84
+ string | ((substring: string, ...args: string[]) => string)
85
+ ][];
86
+ }
87
+ export interface MdxLedgerEntry {
88
+ /** Component tag name (e.g. "APIMethod"). */
89
+ component: string;
90
+ /**
91
+ * What happened:
92
+ * - "preserved" — kept byte-exact as `mdx-raw` (unknown or parse-failed);
93
+ * - "stripped" — deliberately dropped by a mapping.
94
+ */
95
+ action: "preserved" | "stripped";
96
+ /** 1-based line in the (preamble-stripped) source. Best effort. */
97
+ line: number;
98
+ /** Source file, filled in by the importer. */
99
+ file?: string;
100
+ }
101
+ /**
102
+ * YAML-friendly component mapping — the per-source `mdx.components:` block.
103
+ * Purely declarative: safe to evaluate for untrusted docset configs.
104
+ */
105
+ export interface MdxComponentConfig {
106
+ /** TreeNode type to produce (e.g. "table", "endpoint"). */
107
+ as?: string;
108
+ /**
109
+ * Prop mapping: node prop name → JSX attribute name.
110
+ * `{ title: label }` reads attribute `label` into prop `title`.
111
+ */
112
+ props?: Record<string, string>;
113
+ /** Children policy (see ComponentMapping.children). */
114
+ children?: ChildrenPolicy;
115
+ /**
116
+ * Built-in named handler (e.g. "api-method"). Resolved from the handler
117
+ * registry shipped with this package — never a user-supplied module.
118
+ */
119
+ handler?: string;
120
+ /**
121
+ * HTML template with `{attr}` placeholders (attribute values are
122
+ * HTML-escaped on interpolation). For simple presentational
123
+ * components: `'<p><a href="https://…/{url}">Open ↗</a></p>'`.
124
+ * Adds no capability MDX content doesn't already have (raw HTML).
125
+ */
126
+ html?: string;
127
+ /**
128
+ * Map a component whose data lives in a separate TS/TSX file
129
+ * (statically parsed, never executed). `file` resolves relative to
130
+ * the source directory. Requires the CLI import context (file I/O);
131
+ * plain compileComponentConfig callers must pass an extractResolver.
132
+ */
133
+ extract?: {
134
+ file: string;
135
+ export: string;
136
+ as: "cards" | "table";
137
+ };
138
+ /** Strip the component entirely. */
139
+ strip?: boolean;
140
+ /** Treat as self-closing. */
141
+ selfClosing?: boolean;
142
+ }
143
+ /**
144
+ * Compile declarative per-docset config into ComponentMapping entries.
145
+ *
146
+ * @param handlers registry of built-in named handlers (tag-independent);
147
+ * unknown handler names throw — a config typo must not silently
148
+ * degrade into the default mapping.
149
+ */
150
+ export declare function compileComponentConfig(config: Record<string, MdxComponentConfig>, handlers?: Record<string, ComponentMapping>,
151
+ /**
152
+ * Resolver for `extract:` entries (needs file I/O — supplied by the
153
+ * CLI import layer). Without one, extract entries throw.
154
+ */
155
+ extractResolver?: (spec: NonNullable<MdxComponentConfig["extract"]>, tag: string) => ComponentMapping): {
156
+ components: Record<string, ComponentMapping>;
157
+ selfClosingTags: string[];
158
+ };
159
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * MDX adapter — extension points for components and conventions a specific
3
+ * site or docs framework layers on top of *generic* MDX.
4
+ *
5
+ * Mirrors the proven Docusaurus adapter architecture
6
+ * (format-docusaurus/src/adapter.ts): the generic parser handles only the
7
+ * common Mintlify-style vocabulary (callouts, Tabs, Steps, Cards…); anything
8
+ * a framework or site invented is injected via an adapter and/or per-source
9
+ * declarative component config.
10
+ *
11
+ * Three layers (plans/format-fumadocs.md):
12
+ * A. generic parser + open registry + never-drop `mdx-raw` + fidelity ledger
13
+ * B. framework adapters (e.g. `fumadocs`)
14
+ * C. per-docset declarative config (`as` / `props` / `children` / `handler`)
15
+ *
16
+ * Trust boundary: declarative config and built-in named handlers are safe
17
+ * everywhere (no subscriber-supplied code is ever executed). Arbitrary
18
+ * `handler:` JS modules are NOT supported here — a hosted updater must be
19
+ * able to run any docset config without executing its code.
20
+ */
21
+ /**
22
+ * Compile declarative per-docset config into ComponentMapping entries.
23
+ *
24
+ * @param handlers registry of built-in named handlers (tag-independent);
25
+ * unknown handler names throw — a config typo must not silently
26
+ * degrade into the default mapping.
27
+ */
28
+ export function compileComponentConfig(config, handlers = {},
29
+ /**
30
+ * Resolver for `extract:` entries (needs file I/O — supplied by the
31
+ * CLI import layer). Without one, extract entries throw.
32
+ */
33
+ extractResolver) {
34
+ const components = {};
35
+ const selfClosingTags = [];
36
+ for (const [tag, entry] of Object.entries(config)) {
37
+ if (entry.selfClosing)
38
+ selfClosingTags.push(tag);
39
+ if (entry.extract) {
40
+ if (!extractResolver) {
41
+ throw new Error(`mdx component <${tag}> uses extract:, which requires the CLI ` +
42
+ `import context (file access)`);
43
+ }
44
+ components[tag] = extractResolver(entry.extract, tag);
45
+ continue;
46
+ }
47
+ if (entry.html) {
48
+ const template = entry.html;
49
+ components[tag] = {
50
+ type: "html",
51
+ container: true,
52
+ toNode: (attrs) => ({
53
+ type: "html",
54
+ html: template.replace(/\{(\w+)\}/g, (_, name) => String(attrs[name] ?? "")
55
+ .replace(/&/g, "&amp;")
56
+ .replace(/</g, "&lt;")
57
+ .replace(/>/g, "&gt;")
58
+ .replace(/"/g, "&quot;")),
59
+ }),
60
+ };
61
+ continue;
62
+ }
63
+ if (entry.handler) {
64
+ const handler = handlers[entry.handler];
65
+ if (!handler) {
66
+ throw new Error(`Unknown mdx component handler "${entry.handler}" for <${tag}>. ` +
67
+ `Available: ${Object.keys(handlers).join(", ") || "(none)"}`);
68
+ }
69
+ components[tag] = handler;
70
+ continue;
71
+ }
72
+ if (entry.strip) {
73
+ components[tag] = { type: "", container: true, strip: true };
74
+ continue;
75
+ }
76
+ const propsMap = entry.props;
77
+ components[tag] = {
78
+ type: entry.as ?? "",
79
+ container: true,
80
+ children: entry.children,
81
+ propsFromAttrs: propsMap
82
+ ? (attrs) => {
83
+ const props = {};
84
+ for (const [propName, attrName] of Object.entries(propsMap)) {
85
+ if (attrs[attrName] !== undefined)
86
+ props[propName] = attrs[attrName];
87
+ }
88
+ return props;
89
+ }
90
+ : undefined,
91
+ };
92
+ }
93
+ return { components, selfClosingTags };
94
+ }
95
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AAmIxE,eAAO,MAAM,MAAM,EAAE,YAQpB,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAiC,MAAM,gBAAgB,CAAC;AAialF,eAAO,MAAM,MAAM,EAAE,YAapB,CAAC"}
package/dist/cli.js CHANGED
@@ -1,37 +1,181 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
- import { join, relative } from "node:path";
2
+ import { isAbsolute, join, relative } from "node:path";
3
3
  import YAML from "yaml";
4
4
  import { parseMeta } from "@dogsbay/types";
5
- import { mdxToTree, extractHeadings } from "./parse.js";
5
+ import { parseMdx, extractHeadings } from "./parse.js";
6
+ import { compileComponentConfig, } from "./adapter.js";
7
+ import { MDX_BUILTIN_HANDLERS } from "./fumadocs.js";
8
+ import { buildFumadocsNav, detectFumadocsDir } from "./meta-nav.js";
9
+ import { buildMintlifyNav, detectMintlifyDir } from "./mintlify-nav.js";
10
+ import { mintlifyOpenApiSpecRef, buildMintlifyEndpointMap, readOpenApiSpecSource, collectMintlifyOpenApiSpecRefs, collectMintlifyOpenApiNavPages, openApiPageKey, openApiPageSpecRef, } from "./mintlify-openapi.js";
11
+ import { mintlifyAdapter } from "./mintlify.js";
12
+ import { mintlifyRedirects } from "./mintlify-nav.js";
13
+ import { extractNavFromTsxFile } from "./nav-extract.js";
14
+ import { makeExtractMapping } from "./extract.js";
6
15
  /**
7
- * Detect a pulled MDX docs directory.
8
- *
9
- * Looks for nav.json (written by `dogsbay pull`) + at least one .md file.
16
+ * Detect an MDX docs directory. Two shapes:
17
+ * - a pulled site: nav.json (written by `dogsbay pull`) + .md files;
18
+ * - a Fumadocs content directory: meta.json + .mdx/.md pages.
10
19
  */
11
20
  function detectMdxDir(path) {
12
- return (existsSync(join(path, "nav.json")) &&
13
- existsSync(path) &&
14
- hasMdFiles(path));
21
+ if (existsSync(join(path, "nav.json")) && hasMdFiles(path))
22
+ return true;
23
+ if (detectMintlifyDir(path))
24
+ return true;
25
+ return detectFumadocsDir(path);
15
26
  }
16
27
  function hasMdFiles(dir) {
17
28
  try {
18
29
  const entries = readdirSync(dir);
19
- return entries.some((e) => e.endsWith(".md") && e !== "llms.txt");
30
+ return entries.some((e) => (e.endsWith(".md") || e.endsWith(".mdx")) && e !== "llms.txt");
20
31
  }
21
32
  catch {
22
33
  return false;
23
34
  }
24
35
  }
25
36
  /**
26
- * Import a directory of pulled MDX files into ExportPage[] + NavItem[].
37
+ * Import a directory of MDX files into ExportPage[] + NavItem[].
27
38
  */
28
39
  async function importMdxDir(source, opts) {
29
- // Read nav.json
40
+ // Parse options: adapter instance + declarative per-source components.
41
+ const parseOpts = {};
42
+ if (opts.adapterInstance) {
43
+ parseOpts.adapter = opts.adapterInstance;
44
+ }
45
+ else if (detectMintlifyDir(source)) {
46
+ // A Mintlify CONFIG — not merely a file called `docs.json`, which TypeDoc
47
+ // also emits. `detectMintlifyDir` exists precisely to make that
48
+ // distinction (it requires `navigation`, `anchors`, or a mintlify
49
+ // `$schema`), and keying off bare file existence bypassed it: a Fumadocs
50
+ // corpus that happened to ship TypeDoc output got Mintlify's component map
51
+ // applied silently.
52
+ //
53
+ // Default the adapter instead of making the user pass `--adapter mintlify`
54
+ // to get their own components mapped.
55
+ //
56
+ // Without this, importing Mintlify's own docs left every <HeroCard>,
57
+ // <Update>, <ParamField> and <Expandable> unmapped: preserved as mdx-raw
58
+ // and rendered as CODE BLOCKS of their own JSX on the built site. The
59
+ // never-drop policy meant nothing was lost, but the page looked broken.
60
+ // An explicit `--adapter` still wins.
61
+ parseOpts.adapter = mintlifyAdapter;
62
+ }
63
+ // Redirects the source itself declares. A docs site links to its own old
64
+ // paths and relies on the live redirect; after import nothing serves it, so
65
+ // the link 404s unless it is rewritten here.
66
+ const linkRedirects = mintlifyRedirects(source);
67
+ if (Object.keys(linkRedirects).length > 0)
68
+ parseOpts.linkRedirects = linkRedirects;
69
+ if (opts.componentsConfig) {
70
+ const compiled = compileComponentConfig(opts.componentsConfig, MDX_BUILTIN_HANDLERS,
71
+ // extract: data files resolve like sidebar files — absolute paths
72
+ // arrive pre-rebased (site build stages the source), relative ones
73
+ // resolve against the source dir (the convert path).
74
+ (spec) => makeExtractMapping(spec, (file) => isAbsolute(file) ? file : join(source, file)));
75
+ parseOpts.components = compiled.components;
76
+ parseOpts.selfClosingTags = compiled.selfClosingTags;
77
+ }
78
+ // Nav: hardcoded-page-tree extract > nav.json (pulled sites) >
79
+ // Fumadocs meta.json walk.
80
+ const hrefPrefix = typeof opts.hrefPrefix === "string" ? opts.hrefPrefix : "";
30
81
  const navPath = join(source, "nav.json");
31
- const nav = JSON.parse(readFileSync(navPath, "utf-8"));
32
- // Walk all .md files
82
+ let nav = null;
83
+ const sidebarExtract = opts.sidebarExtract;
84
+ if (sidebarExtract) {
85
+ // Absolute file paths arrive pre-resolved (the site-build path
86
+ // rebases them onto the ORIGINAL source dir — the staged copy the
87
+ // plugin sees can't reach ../ siblings); relative ones resolve
88
+ // against the source dir (the convert path).
89
+ const sidebarFile = isAbsolute(sidebarExtract.file)
90
+ ? sidebarExtract.file
91
+ : join(source, sidebarExtract.file);
92
+ nav = extractNavFromTsxFile(sidebarFile, {
93
+ exportName: sidebarExtract.export,
94
+ stripPrefix: sidebarExtract.stripPrefix,
95
+ hrefPrefix,
96
+ });
97
+ if (!nav) {
98
+ console.warn(`mdx: sidebar extraction from ${sidebarExtract.file} failed — ` +
99
+ `falling back to meta.json nav`);
100
+ }
101
+ }
102
+ if (!nav && existsSync(navPath)) {
103
+ nav = JSON.parse(readFileSync(navPath, "utf-8"));
104
+ }
105
+ // Mintlify source-pull: nav is the authoritative `docs.json` / `mint.json`.
106
+ if (!nav) {
107
+ nav = buildMintlifyNav(source, { hrefPrefix });
108
+ }
109
+ if (!nav) {
110
+ nav = buildFumadocsNav(source, { hrefPrefix });
111
+ }
112
+ // Mintlify OpenAPI pages: an `openapi: <method> <path>` frontmatter page has
113
+ // no body — its content is generated from the spec in docs.json (api.openapi).
114
+ // Resolve the spec once into a (method+path → endpoint node) map so those
115
+ // pages import as real endpoint pages instead of blank.
116
+ const specRef = mintlifyOpenApiSpecRef(source);
117
+ // Specs named by INDIVIDUAL PAGES (`openapi: "/openapi.json GET /x"`). A site
118
+ // whose docs.json declares no `api.openapi` still tells us, page by page,
119
+ // exactly which spec to read — Mintlify's own docs do precisely this, and
120
+ // ignoring it cost 100 pages their endpoint content (20 of them entirely
121
+ // blank) with the spec sitting in the repo root. Scanned up front so each
122
+ // distinct spec is parsed once, not per page.
123
+ const pageSpecRefs = new Set();
124
+ const preScan = walkMdFiles(source, source);
125
+ for (const { filePath } of preScan) {
126
+ const fm = extractFrontmatter(readFileSync(filePath, "utf-8"));
127
+ const ref = openApiPageSpecRef(source, fm?.openapi);
128
+ if (ref && ref !== specRef)
129
+ pageSpecRefs.add(ref);
130
+ }
131
+ // Preserve the ORIGINAL spec for exporters that consume one directly. The
132
+ // spec is a canonical source artifact; rebuilding it from endpoint TreeNodes
133
+ // is lossy, so pass the real thing through when we have it.
134
+ const allSpecRefs = collectMintlifyOpenApiSpecRefs(source, [...pageSpecRefs]);
135
+ if (allSpecRefs.length > 0) {
136
+ const preserved = [];
137
+ for (const ref of allSpecRefs) {
138
+ const spec = await readOpenApiSpecSource(ref);
139
+ if (spec) {
140
+ preserved.push({ ...spec, ref });
141
+ }
142
+ else {
143
+ // A failed fetch must NOT look like "this source had no spec" — that
144
+ // silently downgrades the export to lossy reconstruction and the
145
+ // ledger then reports the wrong reason.
146
+ console.warn(` warning: could not read the OpenAPI spec at ${ref} — export will ` +
147
+ `reconstruct it from endpoint nodes instead (lossy: $ref components, ` +
148
+ `security schemes and callbacks are lost).`);
149
+ }
150
+ }
151
+ if (preserved.length > 0)
152
+ opts.sourceOpenApiSpecs = preserved;
153
+ }
154
+ const endpointResult = specRef
155
+ ? await buildMintlifyEndpointMap(specRef)
156
+ : { map: new Map(), loadError: null };
157
+ const endpointMap = endpointResult.map;
158
+ // One endpoint map per page-declared spec, keyed by resolved spec path.
159
+ const pageSpecMaps = new Map();
160
+ for (const ref of pageSpecRefs) {
161
+ const result = await buildMintlifyEndpointMap(ref);
162
+ if (result.loadError) {
163
+ console.warn(` warning: failed to load OpenAPI spec at ${ref} (named by page ` +
164
+ `frontmatter): ${result.loadError} — those pages import without ` +
165
+ `endpoint content.`);
166
+ }
167
+ pageSpecMaps.set(ref, result.map);
168
+ }
169
+ if (endpointResult.loadError) {
170
+ console.warn(` warning: failed to load OpenAPI spec from ${specRef}: ` +
171
+ `${endpointResult.loadError} — API pages import without endpoint content.`);
172
+ }
173
+ // Walk all .md/.mdx files
33
174
  const mdFiles = walkMdFiles(source, source);
34
175
  const pages = [];
176
+ const ledger = [];
177
+ let openApiResolved = 0;
178
+ let openApiMissed = 0;
35
179
  // Custom taxonomy names from dogsbay.config.yml's `taxonomies:` block.
36
180
  const taxonomyNames = Array.isArray(opts.taxonomyNames)
37
181
  ? opts.taxonomyNames
@@ -39,7 +183,34 @@ async function importMdxDir(source, opts) {
39
183
  for (const { filePath, slug } of mdFiles) {
40
184
  const content = readFileSync(filePath, "utf-8");
41
185
  const frontmatter = extractFrontmatter(content);
42
- const tree = mdxToTree(content);
186
+ const result = parseMdx(content, {
187
+ ...parseOpts,
188
+ file: relative(source, filePath),
189
+ // Needed to resolve `import X from "/snippets/foo.mdx"` — Mintlify
190
+ // resolves a leading `/` against the DOCS ROOT, not the filesystem root.
191
+ rootDir: source,
192
+ });
193
+ let tree = result.tree;
194
+ ledger.push(...result.ledger);
195
+ // Mintlify OpenAPI page: append the spec-generated endpoint node. The MDX
196
+ // body (usually empty, sometimes an intro paragraph) renders before it.
197
+ const oaKey = openApiPageKey(frontmatter?.openapi);
198
+ if (oaKey) {
199
+ // The page's OWN spec wins when it names one — a site can reference
200
+ // several specs (mintlify-docs ships openapi.json, admin-openapi.json,
201
+ // analytics.openapi.json and discovery-openapi.json), so the config-level
202
+ // map is the fallback, not the authority.
203
+ const ownRef = openApiPageSpecRef(source, frontmatter?.openapi);
204
+ const endpoint = (ownRef ? pageSpecMaps.get(ownRef)?.get(oaKey) : undefined) ??
205
+ endpointMap.get(oaKey);
206
+ if (endpoint) {
207
+ tree = [...tree, ...endpoint];
208
+ openApiResolved++;
209
+ }
210
+ else {
211
+ openApiMissed++;
212
+ }
213
+ }
43
214
  const headings = extractHeadings(tree);
44
215
  // Extract title: prefer frontmatter, fall back to first h1
45
216
  const titleNode = tree.find((n) => n.type === "heading" && n.props?.level === 1);
@@ -49,6 +220,44 @@ async function importMdxDir(source, opts) {
49
220
  const meta = parseMeta(frontmatter, { slug, taxonomyNames });
50
221
  pages.push({ slug, title, tree, headings, frontmatter, meta });
51
222
  }
223
+ // Nav-referenced OpenAPI pages: `METHOD /path` nav strings under a container
224
+ // with an `openapi` spec exist in no .mdx file — generate them so the nav
225
+ // links resolve (ollama's "Endpoints" group). Their slugs match the nav hrefs
226
+ // (both via openApiRefSlug); skip any that collide with a real .mdx page.
227
+ const existingSlugs = new Set(pages.map((p) => p.slug));
228
+ const navOa = await collectMintlifyOpenApiNavPages(source);
229
+ let navOaAdded = 0;
230
+ for (const page of navOa.pages) {
231
+ if (existingSlugs.has(page.slug))
232
+ continue;
233
+ existingSlugs.add(page.slug);
234
+ pages.push(page);
235
+ navOaAdded++;
236
+ }
237
+ if (navOa.loadError && navOaAdded === 0) {
238
+ console.warn(` warning: failed to load nav-referenced OpenAPI spec: ${navOa.loadError}`);
239
+ }
240
+ // Report OpenAPI page resolution — never silently leave API pages blank.
241
+ const totalOa = openApiResolved + navOaAdded;
242
+ if (totalOa > 0 || openApiMissed > 0) {
243
+ console.log(` Mintlify OpenAPI: resolved ${totalOa} endpoint page(s)` +
244
+ (specRef ? ` from ${specRef}` : ""));
245
+ }
246
+ if (openApiMissed > 0) {
247
+ const cause = !specRef && pageSpecRefs.size === 0
248
+ ? " (no api.openapi spec found in docs.json/mint.json, and no page named its own spec)"
249
+ : endpointResult.loadError
250
+ ? " (the spec failed to load — see the warning above)"
251
+ : "";
252
+ console.warn(` warning: ${openApiMissed} page(s) with \`openapi:\` frontmatter had no ` +
253
+ `matching operation in the spec${cause}` +
254
+ " — those pages import without endpoint content.");
255
+ }
256
+ // Fidelity ledger: expose programmatically; print with --report.
257
+ opts.importLedger = ledger;
258
+ if (opts.report && ledger.length > 0) {
259
+ printLedgerReport(ledger);
260
+ }
52
261
  // Attach metadata for exporters to consume
53
262
  const llmsTxtPath = join(source, "llms.txt");
54
263
  if (existsSync(llmsTxtPath)) {
@@ -60,6 +269,34 @@ async function importMdxDir(source, opts) {
60
269
  }
61
270
  return { pages, nav };
62
271
  }
272
+ /**
273
+ * Print the fidelity ledger grouped by component: what was preserved
274
+ * as raw MDX (candidates for a component mapping) and what a mapping
275
+ * deliberately stripped. Mirrors the import-ditamap --report pattern.
276
+ */
277
+ function printLedgerReport(ledger) {
278
+ const byComponent = new Map();
279
+ for (const entry of ledger) {
280
+ const key = `${entry.component} ${entry.action}`;
281
+ const agg = byComponent.get(key) ?? {
282
+ action: entry.action,
283
+ count: 0,
284
+ files: new Set(),
285
+ };
286
+ agg.count++;
287
+ if (entry.file)
288
+ agg.files.add(entry.file);
289
+ byComponent.set(key, agg);
290
+ }
291
+ console.log("\nMDX fidelity ledger:");
292
+ const sorted = [...byComponent.entries()].sort((a, b) => b[1].count - a[1].count);
293
+ for (const [key, agg] of sorted) {
294
+ const component = key.split(" ")[0];
295
+ console.log(` <${component}> ${agg.action} ×${agg.count} (${agg.files.size} file${agg.files.size === 1 ? "" : "s"})`);
296
+ }
297
+ console.log(" Preserved components survive byte-exact as raw MDX. Map them via an\n" +
298
+ " adapter or the per-source mdx.components config to convert them.");
299
+ }
63
300
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
64
301
  /**
65
302
  * Lift YAML frontmatter from the raw markdown source. Returns
@@ -85,8 +322,8 @@ function extractFrontmatter(source) {
85
322
  }
86
323
  }
87
324
  /**
88
- * Recursively find all .md files in a directory.
89
- * Returns slug (relative path without .md) and absolute file path.
325
+ * Recursively find all .md/.mdx files in a directory.
326
+ * Returns slug (relative path without extension) and absolute file path.
90
327
  */
91
328
  function walkMdFiles(dir, root) {
92
329
  const results = [];
@@ -95,11 +332,19 @@ function walkMdFiles(dir, root) {
95
332
  const fullPath = join(dir, entry);
96
333
  const stat = statSync(fullPath);
97
334
  if (stat.isDirectory()) {
335
+ // Dot-directories are TOOLING, not documentation. Mintlify's own docs
336
+ // carry `.claude/skills/**/SKILL.md`, `.mintlify/AGENTS.md` and
337
+ // `.github/pull_request_template.md`; all 20 were imported as pages and
338
+ // landed in the exported vault as `CLAUDE.md`, `AGENTS.md` and
339
+ // `SKILL.md` beside the real docs. `node_modules` likewise.
340
+ if (entry.startsWith(".") || entry === "node_modules")
341
+ continue;
98
342
  results.push(...walkMdFiles(fullPath, root));
99
343
  }
100
- else if (entry.endsWith(".md") && entry !== "llms.txt") {
344
+ else if ((entry.endsWith(".md") || entry.endsWith(".mdx")) &&
345
+ entry !== "llms.txt") {
101
346
  const rel = relative(root, fullPath);
102
- const slug = rel.replace(/\.md$/, "").replace(/\\/g, "/");
347
+ const slug = rel.replace(/\.(md|mdx)$/, "").replace(/\\/g, "/");
103
348
  results.push({ filePath: fullPath, slug });
104
349
  }
105
350
  }
@@ -110,7 +355,12 @@ export const plugin = {
110
355
  canImport: true,
111
356
  canExport: false,
112
357
  detectSource: detectMdxDir,
113
- importOptions: [],
358
+ importOptions: [
359
+ { flags: "--adapter <name>", description: "MDX framework adapter (e.g. fumadocs)" },
360
+ { flags: "--components <path>", description: "YAML/JSON file with declarative component mappings" },
361
+ { flags: "--sidebar <path[#export]>", description: "Statically extract a hand-written page tree" },
362
+ { flags: "--report", description: "Print the fidelity ledger (preserved/stripped components)" },
363
+ ],
114
364
  exportOptions: [],
115
365
  import: importMdxDir,
116
366
  };