@suzumiyaaoba/mdxr 0.1.0

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 (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +2902 -0
  5. package/dist/components.d.mts +791 -0
  6. package/dist/components.mjs +2 -0
  7. package/dist/config-SI9IyFiC.mjs +184 -0
  8. package/dist/doc-context-CEqzMYKv.d.mts +568 -0
  9. package/dist/index.d.mts +20 -0
  10. package/dist/index.mjs +4 -0
  11. package/dist/ui-GKFD0mx5.mjs +15986 -0
  12. package/package.json +131 -0
  13. package/skill/SKILL.md +49 -0
  14. package/skill/references/components/charts.md +172 -0
  15. package/skill/references/components/document.md +98 -0
  16. package/skill/references/components/forms.md +35 -0
  17. package/skill/references/components/investigation.md +198 -0
  18. package/skill/references/components/layout.md +68 -0
  19. package/skill/references/components/output.md +158 -0
  20. package/skill/references/components/planning.md +155 -0
  21. package/skill/references/components/reports.md +252 -0
  22. package/skill/references/components/shadcn.md +19 -0
  23. package/skill/references/components.md +186 -0
  24. package/skill/references/extending.md +48 -0
  25. package/src/ask-sheet.ts +40 -0
  26. package/src/assets/css.ts +392 -0
  27. package/src/assets/scripts.ts +78 -0
  28. package/src/catalog.ts +287 -0
  29. package/src/cli.ts +178 -0
  30. package/src/client/doc-events.ts +1108 -0
  31. package/src/client/entry.ts +27 -0
  32. package/src/client-js.ts +47 -0
  33. package/src/component-map.ts +47 -0
  34. package/src/components/ui/accordion.tsx +77 -0
  35. package/src/components/ui/alert-dialog.tsx +185 -0
  36. package/src/components/ui/alert.tsx +76 -0
  37. package/src/components/ui/aspect-ratio.tsx +22 -0
  38. package/src/components/ui/attachment.tsx +208 -0
  39. package/src/components/ui/avatar.tsx +106 -0
  40. package/src/components/ui/badge.tsx +52 -0
  41. package/src/components/ui/breadcrumb.tsx +121 -0
  42. package/src/components/ui/bubble.tsx +128 -0
  43. package/src/components/ui/button-group.tsx +88 -0
  44. package/src/components/ui/button.tsx +58 -0
  45. package/src/components/ui/calendar.tsx +226 -0
  46. package/src/components/ui/card.tsx +102 -0
  47. package/src/components/ui/carousel.tsx +246 -0
  48. package/src/components/ui/chart.tsx +379 -0
  49. package/src/components/ui/checkbox.tsx +27 -0
  50. package/src/components/ui/collapsible.tsx +19 -0
  51. package/src/components/ui/combobox.tsx +298 -0
  52. package/src/components/ui/command.tsx +193 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/dialog.tsx +159 -0
  55. package/src/components/ui/direction.tsx +4 -0
  56. package/src/components/ui/drawer.tsx +227 -0
  57. package/src/components/ui/dropdown-menu.tsx +269 -0
  58. package/src/components/ui/empty.tsx +104 -0
  59. package/src/components/ui/field.tsx +237 -0
  60. package/src/components/ui/fieldset.tsx +32 -0
  61. package/src/components/ui/frame.tsx +87 -0
  62. package/src/components/ui/hover-card.tsx +50 -0
  63. package/src/components/ui/input-group.tsx +159 -0
  64. package/src/components/ui/input-otp.tsx +83 -0
  65. package/src/components/ui/input.tsx +19 -0
  66. package/src/components/ui/item.tsx +202 -0
  67. package/src/components/ui/kbd.tsx +26 -0
  68. package/src/components/ui/label.tsx +19 -0
  69. package/src/components/ui/marker.tsx +71 -0
  70. package/src/components/ui/menubar.tsx +284 -0
  71. package/src/components/ui/message-scroller.tsx +128 -0
  72. package/src/components/ui/message.tsx +91 -0
  73. package/src/components/ui/meter.tsx +80 -0
  74. package/src/components/ui/native-select.tsx +64 -0
  75. package/src/components/ui/navigation-menu.tsx +170 -0
  76. package/src/components/ui/pagination.tsx +133 -0
  77. package/src/components/ui/popover.tsx +87 -0
  78. package/src/components/ui/progress.tsx +82 -0
  79. package/src/components/ui/questionnaire.tsx +328 -0
  80. package/src/components/ui/radio-group.tsx +35 -0
  81. package/src/components/ui/resizable.tsx +49 -0
  82. package/src/components/ui/scroll-area.tsx +50 -0
  83. package/src/components/ui/select.tsx +201 -0
  84. package/src/components/ui/separator.tsx +22 -0
  85. package/src/components/ui/sheet.tsx +135 -0
  86. package/src/components/ui/sidebar.tsx +730 -0
  87. package/src/components/ui/skeleton.tsx +13 -0
  88. package/src/components/ui/slider.tsx +51 -0
  89. package/src/components/ui/spinner.tsx +16 -0
  90. package/src/components/ui/switch.tsx +31 -0
  91. package/src/components/ui/table.tsx +113 -0
  92. package/src/components/ui/tabs.tsx +82 -0
  93. package/src/components/ui/textarea.tsx +17 -0
  94. package/src/components/ui/toast.tsx +229 -0
  95. package/src/components/ui/toggle-group.tsx +87 -0
  96. package/src/components/ui/toggle.tsx +43 -0
  97. package/src/components/ui/tooltip.tsx +65 -0
  98. package/src/components.ts +78 -0
  99. package/src/config.ts +56 -0
  100. package/src/define.ts +122 -0
  101. package/src/doc-context.ts +20 -0
  102. package/src/editor.ts +94 -0
  103. package/src/format-error.ts +78 -0
  104. package/src/guards.ts +84 -0
  105. package/src/hooks/use-mobile.ts +21 -0
  106. package/src/html.ts +91 -0
  107. package/src/hydrate/export-index.ts +232 -0
  108. package/src/hydrate/import-scan.ts +169 -0
  109. package/src/hydrate/plugins.ts +118 -0
  110. package/src/hydrate/runtime-module.ts +145 -0
  111. package/src/hydrate-runtime.ts +67 -0
  112. package/src/hydrate.ts +147 -0
  113. package/src/index.ts +16 -0
  114. package/src/init.ts +57 -0
  115. package/src/langs.ts +148 -0
  116. package/src/lines.ts +53 -0
  117. package/src/load-user-module.ts +191 -0
  118. package/src/mdx.ts +248 -0
  119. package/src/paths.ts +36 -0
  120. package/src/rehype/shiki.ts +533 -0
  121. package/src/remark/alerts.ts +53 -0
  122. package/src/remark/ast.ts +96 -0
  123. package/src/remark/callouts.ts +25 -0
  124. package/src/remark/code-file.ts +85 -0
  125. package/src/remark/code-meta.ts +21 -0
  126. package/src/remark/directives.ts +170 -0
  127. package/src/remark/file-paths.ts +64 -0
  128. package/src/remark/headings.ts +131 -0
  129. package/src/remark/no-js.ts +40 -0
  130. package/src/render.ts +337 -0
  131. package/src/serve.ts +322 -0
  132. package/src/styles/globals.css +134 -0
  133. package/src/styles/shadcn.css +641 -0
  134. package/src/tailwind.ts +119 -0
  135. package/src/ui/approvals.tsx +76 -0
  136. package/src/ui/ask-question.tsx +386 -0
  137. package/src/ui/ask.tsx +206 -0
  138. package/src/ui/attrs.ts +51 -0
  139. package/src/ui/audit.tsx +139 -0
  140. package/src/ui/bar-chart.tsx +334 -0
  141. package/src/ui/benchmarks.tsx +143 -0
  142. package/src/ui/bits.tsx +537 -0
  143. package/src/ui/board.tsx +173 -0
  144. package/src/ui/bridge.tsx +207 -0
  145. package/src/ui/bumps.tsx +178 -0
  146. package/src/ui/callout.tsx +106 -0
  147. package/src/ui/changes.tsx +89 -0
  148. package/src/ui/chart-bits.tsx +52 -0
  149. package/src/ui/chart.ts +577 -0
  150. package/src/ui/checks.tsx +203 -0
  151. package/src/ui/child-index.tsx +44 -0
  152. package/src/ui/children.ts +43 -0
  153. package/src/ui/chips.tsx +49 -0
  154. package/src/ui/cmd.tsx +27 -0
  155. package/src/ui/columns.tsx +31 -0
  156. package/src/ui/comments.tsx +544 -0
  157. package/src/ui/compare.tsx +67 -0
  158. package/src/ui/decision.tsx +79 -0
  159. package/src/ui/deps.tsx +78 -0
  160. package/src/ui/details.tsx +43 -0
  161. package/src/ui/diff-parse.ts +307 -0
  162. package/src/ui/diff.tsx +458 -0
  163. package/src/ui/diffstat.tsx +58 -0
  164. package/src/ui/due.tsx +65 -0
  165. package/src/ui/effort.tsx +29 -0
  166. package/src/ui/endpoints.tsx +118 -0
  167. package/src/ui/envvars.tsx +96 -0
  168. package/src/ui/figure.tsx +37 -0
  169. package/src/ui/file-icon.ts +1020 -0
  170. package/src/ui/file-link.ts +32 -0
  171. package/src/ui/file-ref.tsx +39 -0
  172. package/src/ui/files.tsx +112 -0
  173. package/src/ui/findings.tsx +85 -0
  174. package/src/ui/flow.tsx +73 -0
  175. package/src/ui/funnel.tsx +121 -0
  176. package/src/ui/gantt.tsx +443 -0
  177. package/src/ui/gauges.tsx +135 -0
  178. package/src/ui/glossary.tsx +29 -0
  179. package/src/ui/graph-layout.ts +149 -0
  180. package/src/ui/graph-specs.tsx +102 -0
  181. package/src/ui/graph.tsx +278 -0
  182. package/src/ui/grid.tsx +119 -0
  183. package/src/ui/hypothesis.tsx +94 -0
  184. package/src/ui/icon.tsx +80 -0
  185. package/src/ui/incident.tsx +130 -0
  186. package/src/ui/index.ts +342 -0
  187. package/src/ui/ins-del.tsx +41 -0
  188. package/src/ui/json.tsx +190 -0
  189. package/src/ui/layout.ts +9 -0
  190. package/src/ui/line-chart.tsx +251 -0
  191. package/src/ui/matrix.tsx +208 -0
  192. package/src/ui/meta.tsx +73 -0
  193. package/src/ui/option.tsx +64 -0
  194. package/src/ui/owner.tsx +40 -0
  195. package/src/ui/packages.tsx +104 -0
  196. package/src/ui/pathway.tsx +96 -0
  197. package/src/ui/phase.tsx +41 -0
  198. package/src/ui/pie-chart.tsx +170 -0
  199. package/src/ui/plan.tsx +77 -0
  200. package/src/ui/pre.tsx +175 -0
  201. package/src/ui/priority.tsx +51 -0
  202. package/src/ui/props.tsx +77 -0
  203. package/src/ui/quadrant.tsx +172 -0
  204. package/src/ui/radar.tsx +200 -0
  205. package/src/ui/ref.tsx +160 -0
  206. package/src/ui/release.tsx +178 -0
  207. package/src/ui/req.tsx +42 -0
  208. package/src/ui/review.tsx +133 -0
  209. package/src/ui/risk.tsx +67 -0
  210. package/src/ui/sankey.tsx +287 -0
  211. package/src/ui/scatter.tsx +237 -0
  212. package/src/ui/schema.tsx +113 -0
  213. package/src/ui/score.tsx +105 -0
  214. package/src/ui/search.tsx +120 -0
  215. package/src/ui/series.tsx +38 -0
  216. package/src/ui/severity.tsx +69 -0
  217. package/src/ui/shadcn.tsx +216 -0
  218. package/src/ui/spark.tsx +86 -0
  219. package/src/ui/stack.tsx +57 -0
  220. package/src/ui/stats.tsx +63 -0
  221. package/src/ui/status-badge.tsx +66 -0
  222. package/src/ui/statuspage.tsx +238 -0
  223. package/src/ui/steps.tsx +74 -0
  224. package/src/ui/summary.tsx +41 -0
  225. package/src/ui/symbol-ref.tsx +73 -0
  226. package/src/ui/terminal.tsx +117 -0
  227. package/src/ui/tests.tsx +218 -0
  228. package/src/ui/timeline.tsx +63 -0
  229. package/src/ui/toc.tsx +56 -0
  230. package/src/ui/tones.ts +187 -0
  231. package/src/ui/trace.tsx +69 -0
  232. package/src/ui/tree.tsx +281 -0
  233. package/src/ui/treemap.tsx +128 -0
  234. package/src/ui/venn.tsx +258 -0
  235. package/src/ui/verdict.tsx +78 -0
  236. package/src/ui/waterfall.tsx +142 -0
@@ -0,0 +1,191 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { build } from "esbuild";
8
+ import type { Plugin } from "esbuild";
9
+
10
+ import { isRecord } from "./guards.js";
11
+ import { cacheDir } from "./paths.js";
12
+
13
+ /**
14
+ * Packages that must resolve to *this* package's dependencies — a second React
15
+ * copy would break hooks/context, and `valibot` instances mix across schemas.
16
+ * Shared with hydrate.ts's pinShared plugin (which adds no `@suzumiyaaoba/mdxr`
17
+ * since the runtimeModule plugin maps that specifier onto the catalog).
18
+ */
19
+ export const SHARED_PACKAGES = ["react", "react-dom", "valibot"];
20
+
21
+ // `@suzumiyaaoba/mdxr`(/components) reach this same package via Node's
22
+ // self-reference (the cache file lives inside the package root).
23
+ /* oxlint-disable require-unicode-regexp -- esbuild serializes onResolve filter
24
+ regexes to Go's RE2, which rejects the `u` flag. */
25
+ const SHARED_EXTERNALS = new RegExp(
26
+ `^(?:${[...SHARED_PACKAGES, "@suzumiyaaoba/mdxr"].join("|")})(?:/.*)?$`
27
+ );
28
+ /* oxlint-enable require-unicode-regexp */
29
+
30
+ const sharedExternals: Plugin = {
31
+ name: "shared-externals",
32
+ setup(b) {
33
+ b.onResolve({ filter: SHARED_EXTERNALS }, (a) => ({
34
+ external: true,
35
+ path: a.path,
36
+ }));
37
+ },
38
+ };
39
+
40
+ export interface BundledModule {
41
+ module: Record<string, unknown>;
42
+ /** Bundled source — also used as a Tailwind scan source. */
43
+ code: string;
44
+ }
45
+
46
+ const INDEX_FILES = ["index.tsx", "index.ts", "index.jsx", "index.js"];
47
+
48
+ /** Files written but not yet imported — a prune must never unlink these. */
49
+ const inFlight = new Set<string>();
50
+
51
+ /**
52
+ * `mdxr serve` writes a fresh `doc-<hash>.mjs` on every rebuild, so the cache
53
+ * would grow unboundedly over a session. Keep only the newest few files per
54
+ * prefix — imported modules live in the module registry, not on disk, so
55
+ * deleting an already-imported file is safe. Best-effort: failures are fine.
56
+ */
57
+ const pruneCache = async (prefix: string): Promise<void> => {
58
+ try {
59
+ const KEEP = 8;
60
+ // Never touch files younger than this: another process may have written
61
+ // one and not yet called import() (inFlight only guards this process).
62
+ const STALE_MS = 10_000;
63
+ const now = Date.now();
64
+ const ents = await readdir(cacheDir);
65
+ const mine = ents.filter(
66
+ (f) =>
67
+ f.startsWith(`${prefix}-`) &&
68
+ f.endsWith(".mjs") &&
69
+ !inFlight.has(path.join(cacheDir, f))
70
+ );
71
+ if (mine.length <= KEEP) {
72
+ return;
73
+ }
74
+ const withMtime = await Promise.all(
75
+ mine.map(async (f) => {
76
+ const s = await stat(path.join(cacheDir, f));
77
+ return { f, mtime: s.mtimeMs };
78
+ })
79
+ );
80
+ withMtime.sort((a, b) => b.mtime - a.mtime);
81
+ const keep = new Set(withMtime.slice(0, KEEP).map(({ f }) => f));
82
+ await Promise.all(
83
+ withMtime
84
+ .filter(({ f, mtime }) => now - mtime > STALE_MS && !keep.has(f))
85
+ .map(async ({ f, mtime }) => {
86
+ const p = path.join(cacheDir, f);
87
+ // TOCTOU: another process may have rewritten the file since our
88
+ // readdir/stat (a rewrite refreshes mtime, but we judged staleness
89
+ // on the old one). Re-stat right before unlinking.
90
+ const s = await stat(p);
91
+ if (s.mtimeMs !== mtime || now - s.mtimeMs <= STALE_MS) {
92
+ return;
93
+ }
94
+ await unlink(p);
95
+ })
96
+ );
97
+ } catch {
98
+ // Cache hygiene is best-effort — never fail a render over it.
99
+ }
100
+ };
101
+
102
+ /** `await import` of a missing module: Node's ERR_MODULE_NOT_FOUND, possibly
103
+ * wrapped by a module runner that only preserves the message. */
104
+ const isModuleNotFound = (e: unknown): boolean =>
105
+ isRecord(e) &&
106
+ (e.code === "ERR_MODULE_NOT_FOUND" ||
107
+ e.code === "ENOENT" ||
108
+ (typeof e.message === "string" &&
109
+ e.message.includes("Cannot find module")));
110
+
111
+ /**
112
+ * Write bundled ESM `code` into this package's cache dir (content-hashed, so
113
+ * repeat imports are free) and import it. Living inside the package makes
114
+ * bare imports (`react`, `valibot`, `@suzumiyaaoba/mdxr`) resolve to *our*
115
+ * copies — one React instance shared with renderToStaticMarkup and the
116
+ * hydrate bundle.
117
+ */
118
+ export const importBundledCode = async (
119
+ code: string,
120
+ prefix: string
121
+ ): Promise<Record<string, unknown>> => {
122
+ await mkdir(cacheDir, { recursive: true });
123
+ const hash = createHash("sha256").update(code).digest("hex").slice(0, 12);
124
+ const out = path.join(cacheDir, `${prefix}-${hash}.mjs`);
125
+ // Always rewrite: refreshing the mtime keeps the live file out of prune's
126
+ // reach (a stale-mtime hit could otherwise be unlinked while still in use).
127
+ await writeFile(out, code);
128
+ inFlight.add(out);
129
+ let raw: unknown;
130
+ try {
131
+ try {
132
+ raw = await import(pathToFileURL(out).href);
133
+ } catch (error) {
134
+ if (!isModuleNotFound(error)) {
135
+ throw error;
136
+ }
137
+ // A concurrent process's pruneCache may have unlinked the file between
138
+ // our writeFile and the module runner's read (it judged staleness on a
139
+ // pre-rewrite mtime). Rewrite and retry once on a fresh URL — a `?`
140
+ // suffix also escapes any cached miss in the importing module runner.
141
+ await writeFile(out, code);
142
+ raw = await import(`${pathToFileURL(out).href}?retry`);
143
+ }
144
+ } finally {
145
+ inFlight.delete(out);
146
+ }
147
+ void pruneCache(prefix);
148
+ return isRecord(raw) ? raw : {};
149
+ };
150
+
151
+ /**
152
+ * Resolve a user module path: directories collapse to their index file.
153
+ * Used both when importing the module (SSR) and when bundling it for the
154
+ * hydration script.
155
+ */
156
+ export const resolveModuleEntry = (entryPath: string): string => {
157
+ if (!(existsSync(entryPath) && statSync(entryPath).isDirectory())) {
158
+ return entryPath;
159
+ }
160
+ for (const name of INDEX_FILES) {
161
+ const candidate = path.join(entryPath, name);
162
+ if (existsSync(candidate)) {
163
+ return candidate;
164
+ }
165
+ }
166
+ return entryPath;
167
+ };
168
+
169
+ /**
170
+ * Bundle a user-authored TS/TSX module and import it.
171
+ * The result is written inside this package's directory so that externals
172
+ * resolve against our node_modules (single React instance).
173
+ */
174
+ export const loadUserModule = async (
175
+ entryPath: string
176
+ ): Promise<BundledModule> => {
177
+ const result = await build({
178
+ bundle: true,
179
+ entryPoints: [entryPath],
180
+ format: "esm",
181
+ jsx: "automatic",
182
+ jsxImportSource: "react",
183
+ logLevel: "silent",
184
+ platform: "node",
185
+ plugins: [sharedExternals],
186
+ target: "node20",
187
+ write: false,
188
+ });
189
+ const code = result.outputFiles[0].text;
190
+ return { code, module: await importBundledCode(code, "components") };
191
+ };
package/src/mdx.ts ADDED
@@ -0,0 +1,248 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { compile } from "@mdx-js/mdx";
5
+ import { createElement, Fragment } from "react";
6
+ import type { ReactElement } from "react";
7
+ import { renderToStaticMarkup, renderToString } from "react-dom/server";
8
+ import rehypeKatex from "rehype-katex";
9
+ import remarkDirective from "remark-directive";
10
+ import remarkFrontmatter from "remark-frontmatter";
11
+ import remarkGfm from "remark-gfm";
12
+ import remarkMath from "remark-math";
13
+ import { VFile } from "vfile";
14
+ import { matter } from "vfile-matter";
15
+
16
+ import type { ComponentMap } from "./define.js";
17
+ import type { DocContextValue } from "./doc-context.js";
18
+ import { DocContext } from "./doc-context.js";
19
+ import { editorUrl } from "./editor.js";
20
+ import { enhanceRenderError, formatError } from "./format-error.js";
21
+ import { isComponent, isRecord } from "./guards.js";
22
+ import { importBundledCode } from "./load-user-module.js";
23
+ import { rehypeShiki } from "./rehype/shiki.js";
24
+ import { remarkMdxrAlerts } from "./remark/alerts.js";
25
+ import { remarkCodeFile } from "./remark/code-file.js";
26
+ import { remarkCodeMeta } from "./remark/code-meta.js";
27
+ import { remarkMdxrDirectives } from "./remark/directives.js";
28
+ import { remarkFilePaths } from "./remark/file-paths.js";
29
+ import { remarkMdxrHeadings } from "./remark/headings.js";
30
+ import { remarkNoJs } from "./remark/no-js.js";
31
+ import { takeUsedIcons } from "./ui/icon.js";
32
+
33
+ export interface MdxResult {
34
+ body: string;
35
+ frontmatter: Record<string, unknown>;
36
+ /**
37
+ * Compiled MDX module source (ESM, `react/jsx-runtime` imports). The same
38
+ * module is rendered here and re-bundled into the document's hydration
39
+ * script, so client and server evaluate identical code.
40
+ */
41
+ code: string;
42
+ /**
43
+ * Every `fileLink(rel, line)` call made during SSR, keyed `rel\0line`. The
44
+ * hydration bundle replays these so components see identical link results
45
+ * (no `existsSync` in the browser).
46
+ */
47
+ fileLinks: Record<string, string>;
48
+ /**
49
+ * Iconify names (`prefix:name`) resolved while rendering — the hydration
50
+ * bundle registers exactly this subset instead of the full icon sets.
51
+ */
52
+ usedIcons: string[];
53
+ /**
54
+ * Catalog keys the compiled document references — the hydrate import list.
55
+ * Extracted from the emitted module rather than observed at render: the
56
+ * module spreads `props.components`, so proxy-based tracking would record
57
+ * every catalog key.
58
+ */
59
+ usedComponents: string[];
60
+ /**
61
+ * ISO timestamp captured at render time and put in `DocContext.now` —
62
+ * serialized into the hydration spec so relative-time components render
63
+ * identically on the client.
64
+ */
65
+ renderedAt: string;
66
+ /**
67
+ * The context value the body was rendered under — anything rendered next to
68
+ * it (the frontmatter PlanHeader) must use the same Provider or hydration
69
+ * sees different context.
70
+ */
71
+ context: DocContextValue;
72
+ /** True when the body was rendered for hydration (renderToString). */
73
+ hydrated: boolean;
74
+ /**
75
+ * Re-render the document with `header` as the first child of the same
76
+ * Provider > Fragment > [header|null, doc] tree the hydration client
77
+ * mounts (hydrate-runtime's mountDocument). useId() encodes tree position,
78
+ * so the header must be rendered inside that shape — prepending separately
79
+ * rendered markup would shift every hydrated id/name/htmlFor. Returns
80
+ * fresh snapshots too: the pass replays every fileLink/icon lookup.
81
+ */
82
+ renderWithHeader: (header: ReactElement) => {
83
+ fileLinks: Record<string, string>;
84
+ html: string;
85
+ usedIcons: string[];
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Which catalog entries the document references is visible in the compiled
91
+ * module itself: JSX identifiers become `_missingMdxReference("Name", …)`
92
+ * checks, markdown element overrides read `_components.name`, and a `wrapper`
93
+ * entry is picked straight off `props.components`. (Spreading `components`
94
+ * into `_components` makes runtime tracking see every key — hence static
95
+ * extraction here instead.)
96
+ */
97
+ const extractUsedComponents = (
98
+ code: string,
99
+ components: ComponentMap
100
+ ): string[] => {
101
+ const catalogKeys = new Set(Object.keys(components));
102
+ const used = new Set<string>();
103
+ for (const m of code.matchAll(/_missingMdxReference\("(?<name>[^"]+)"/gu)) {
104
+ const name = m.groups?.name;
105
+ if (name !== undefined && catalogKeys.has(name)) {
106
+ used.add(name);
107
+ }
108
+ }
109
+ for (const m of code.matchAll(
110
+ /_components\.(?<dot>\w+)|_components\["(?<bracket>[^"]+)"\]/gu
111
+ )) {
112
+ const name = m.groups?.dot ?? m.groups?.bracket;
113
+ if (name !== undefined && catalogKeys.has(name)) {
114
+ used.add(name);
115
+ }
116
+ }
117
+ if (catalogKeys.has("wrapper")) {
118
+ used.add("wrapper");
119
+ }
120
+ return [...used];
121
+ };
122
+
123
+ export const mdxToHtml = async (
124
+ source: string,
125
+ components: ComponentMap,
126
+ filePath = "document.mdx",
127
+ opts: { editor?: string; hydrate?: boolean } = {}
128
+ ): Promise<MdxResult> => {
129
+ const file = new VFile({ path: filePath, value: source });
130
+ matter(file);
131
+ // vfile-matter sets `file.data.matter`, but its types don't declare it.
132
+ const fmRaw: unknown = isRecord(file.data) ? file.data.matter : undefined;
133
+ const frontmatter: Record<string, unknown> = isRecord(fmRaw) ? fmRaw : {};
134
+
135
+ // Frontmatter `editor:` overrides the mdxr.config.ts default; "none" or an
136
+ // unresolvable path disables the link. Only existing files get links.
137
+ const editor =
138
+ typeof frontmatter.editor === "string" && frontmatter.editor !== ""
139
+ ? frontmatter.editor
140
+ : opts.editor;
141
+ const dir = file.dirname ?? ".";
142
+ // Calls are recorded so the hydration bundle can replay identical results —
143
+ // the client has no filesystem, so a missing map entry means "no link".
144
+ const fileLinks = new Map<string, string>();
145
+ const fileLink = (rel: string, line?: string): string | undefined => {
146
+ const abs = path.resolve(dir, rel);
147
+ const url = existsSync(abs) ? editorUrl(editor, abs, line) : undefined;
148
+ if (url !== undefined) {
149
+ // NUL separator — `rel` may legitimately end in digits, and the client
150
+ // lookup in hydrate.ts uses the same key shape.
151
+ fileLinks.set(`${rel}\0${line ?? ""}`, url);
152
+ }
153
+ return url;
154
+ };
155
+
156
+ // Compile once: the emitted module is imported for SSR *and* inlined into
157
+ // the hydration bundle, so both sides run byte-identical document code.
158
+ const compiled = await compile(file, {
159
+ baseUrl: import.meta.url,
160
+ format: "mdx",
161
+ rehypePlugins: [rehypeKatex, rehypeShiki],
162
+ remarkPlugins: [
163
+ remarkFrontmatter,
164
+ remarkGfm,
165
+ remarkMath,
166
+ remarkDirective,
167
+ remarkMdxrDirectives,
168
+ remarkMdxrAlerts,
169
+ remarkNoJs,
170
+ remarkMdxrHeadings,
171
+ remarkCodeFile,
172
+ remarkCodeMeta,
173
+ remarkFilePaths,
174
+ ],
175
+ });
176
+ // Non-fatal plugin diagnostics (unknown directives, …) reach the user here.
177
+ for (const m of compiled.messages) {
178
+ process.stderr.write(`mdxr: warning: ${formatError(m)}\n`);
179
+ }
180
+ const code = String(compiled);
181
+ // The compiled module is imported from this package's cache dir so its
182
+ // `react/jsx-runtime` resolves to our copy — the same instance the
183
+ // hydration bundle pins via its resolve plugin.
184
+ const mod = await importBundledCode(code, "doc");
185
+
186
+ const used = extractUsedComponents(code, components);
187
+
188
+ const docComponent = mod.default;
189
+ if (!isComponent(docComponent)) {
190
+ throw new Error("Compiled document has no default export component.");
191
+ }
192
+
193
+ takeUsedIcons();
194
+ const renderedAt = new Date();
195
+ const context: DocContextValue = { fileLink, now: renderedAt };
196
+ // hydrateRoot reconciles against markup produced by renderToString — its
197
+ // `<!-- -->` text-boundary comments keep adjacent text expressions from
198
+ // merging in the DOM. renderToStaticMarkup omits them, so only purely
199
+ // static documents use it.
200
+ const hydrated = (opts.hydrate ?? true) && used.length > 0;
201
+ const renderToMarkup = hydrated ? renderToString : renderToStaticMarkup;
202
+ // The client mounts Provider > Fragment > [header|null, doc]. SSR must emit
203
+ // that exact shape: useId() seeds encode a component's position in the
204
+ // tree, so rendering `doc` directly would diverge from hydration even with
205
+ // no header (the extra Fragment level changes the generated ids).
206
+ const renderDocument = (header: ReactElement | null): string =>
207
+ renderToMarkup(
208
+ createElement(
209
+ DocContext.Provider,
210
+ { value: context },
211
+ createElement(
212
+ Fragment,
213
+ null,
214
+ header,
215
+ createElement(docComponent, { components })
216
+ )
217
+ )
218
+ );
219
+ let body: string;
220
+ try {
221
+ body = renderDocument(null);
222
+ } catch (error) {
223
+ throw enhanceRenderError(error, Object.keys(components));
224
+ }
225
+ return {
226
+ body,
227
+ code,
228
+ context,
229
+ fileLinks: Object.fromEntries(fileLinks),
230
+ frontmatter,
231
+ hydrated,
232
+ renderWithHeader: (header) => {
233
+ try {
234
+ const html = renderDocument(header);
235
+ return {
236
+ fileLinks: Object.fromEntries(fileLinks),
237
+ html,
238
+ usedIcons: takeUsedIcons(),
239
+ };
240
+ } catch (error) {
241
+ throw enhanceRenderError(error, Object.keys(components));
242
+ }
243
+ },
244
+ renderedAt: renderedAt.toISOString(),
245
+ usedComponents: used,
246
+ usedIcons: takeUsedIcons(),
247
+ };
248
+ };
package/src/paths.ts ADDED
@@ -0,0 +1,36 @@
1
+ import module from "node:module";
2
+ import path from "node:path";
3
+ import url from "node:url";
4
+
5
+ const require = module.createRequire(import.meta.url);
6
+
7
+ const findPkgRoot = (): string => {
8
+ try {
9
+ return path.dirname(
10
+ url.fileURLToPath(require.resolve("@suzumiyaaoba/mdxr/package.json"))
11
+ );
12
+ } catch {
13
+ // Bundled (dist/*.mjs) and source (src/*.ts) layouts both sit one level
14
+ // below the package root.
15
+ return url.fileURLToPath(new URL("..", import.meta.url));
16
+ }
17
+ };
18
+
19
+ /** Absolute path to this package's root (works bundled, unbundled, and via npx). */
20
+ export const pkgRoot = findPkgRoot();
21
+
22
+ /**
23
+ * This package's `src/` directory. The hydration bundle is always built from
24
+ * the TypeScript sources — they ship with the package, so packed installs
25
+ * and local dev produce identical bundles. Building from sources (rather
26
+ * than the packed `dist/` chunks) is also what makes tree-shaking effective:
27
+ * every used component is imported from its leaf module, and no barrel file
28
+ * is ever loaded.
29
+ */
30
+ export const srcDir = path.join(pkgRoot, "src");
31
+
32
+ /** Directory where bundled user modules are cached. Kept inside this package so
33
+ * bare imports (`react`, `valibot`, `@suzumiyaaoba/mdxr`) resolve to *our*
34
+ * copy —
35
+ * guaranteeing a single React instance shared with renderToStaticMarkup. */
36
+ export const cacheDir = `${pkgRoot}/.mdxr-cache`;