@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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2902 -0
- package/dist/components.d.mts +791 -0
- package/dist/components.mjs +2 -0
- package/dist/config-SI9IyFiC.mjs +184 -0
- package/dist/doc-context-CEqzMYKv.d.mts +568 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +4 -0
- package/dist/ui-GKFD0mx5.mjs +15986 -0
- package/package.json +131 -0
- package/skill/SKILL.md +49 -0
- package/skill/references/components/charts.md +172 -0
- package/skill/references/components/document.md +98 -0
- package/skill/references/components/forms.md +35 -0
- package/skill/references/components/investigation.md +198 -0
- package/skill/references/components/layout.md +68 -0
- package/skill/references/components/output.md +158 -0
- package/skill/references/components/planning.md +155 -0
- package/skill/references/components/reports.md +252 -0
- package/skill/references/components/shadcn.md +19 -0
- package/skill/references/components.md +186 -0
- package/skill/references/extending.md +48 -0
- package/src/ask-sheet.ts +40 -0
- package/src/assets/css.ts +392 -0
- package/src/assets/scripts.ts +78 -0
- package/src/catalog.ts +287 -0
- package/src/cli.ts +178 -0
- package/src/client/doc-events.ts +1108 -0
- package/src/client/entry.ts +27 -0
- package/src/client-js.ts +47 -0
- package/src/component-map.ts +47 -0
- package/src/components/ui/accordion.tsx +77 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +208 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +121 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +88 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +226 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +246 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/combobox.tsx +298 -0
- package/src/components/ui/command.tsx +193 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +237 -0
- package/src/components/ui/fieldset.tsx +32 -0
- package/src/components/ui/frame.tsx +87 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +159 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +202 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/menubar.tsx +284 -0
- package/src/components/ui/message-scroller.tsx +128 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/meter.tsx +80 -0
- package/src/components/ui/native-select.tsx +64 -0
- package/src/components/ui/navigation-menu.tsx +170 -0
- package/src/components/ui/pagination.tsx +133 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +328 -0
- package/src/components/ui/radio-group.tsx +35 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +201 -0
- package/src/components/ui/separator.tsx +22 -0
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +229 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/components.ts +78 -0
- package/src/config.ts +56 -0
- package/src/define.ts +122 -0
- package/src/doc-context.ts +20 -0
- package/src/editor.ts +94 -0
- package/src/format-error.ts +78 -0
- package/src/guards.ts +84 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/html.ts +91 -0
- package/src/hydrate/export-index.ts +232 -0
- package/src/hydrate/import-scan.ts +169 -0
- package/src/hydrate/plugins.ts +118 -0
- package/src/hydrate/runtime-module.ts +145 -0
- package/src/hydrate-runtime.ts +67 -0
- package/src/hydrate.ts +147 -0
- package/src/index.ts +16 -0
- package/src/init.ts +57 -0
- package/src/langs.ts +148 -0
- package/src/lines.ts +53 -0
- package/src/load-user-module.ts +191 -0
- package/src/mdx.ts +248 -0
- package/src/paths.ts +36 -0
- package/src/rehype/shiki.ts +533 -0
- package/src/remark/alerts.ts +53 -0
- package/src/remark/ast.ts +96 -0
- package/src/remark/callouts.ts +25 -0
- package/src/remark/code-file.ts +85 -0
- package/src/remark/code-meta.ts +21 -0
- package/src/remark/directives.ts +170 -0
- package/src/remark/file-paths.ts +64 -0
- package/src/remark/headings.ts +131 -0
- package/src/remark/no-js.ts +40 -0
- package/src/render.ts +337 -0
- package/src/serve.ts +322 -0
- package/src/styles/globals.css +134 -0
- package/src/styles/shadcn.css +641 -0
- package/src/tailwind.ts +119 -0
- package/src/ui/approvals.tsx +76 -0
- package/src/ui/ask-question.tsx +386 -0
- package/src/ui/ask.tsx +206 -0
- package/src/ui/attrs.ts +51 -0
- package/src/ui/audit.tsx +139 -0
- package/src/ui/bar-chart.tsx +334 -0
- package/src/ui/benchmarks.tsx +143 -0
- package/src/ui/bits.tsx +537 -0
- package/src/ui/board.tsx +173 -0
- package/src/ui/bridge.tsx +207 -0
- package/src/ui/bumps.tsx +178 -0
- package/src/ui/callout.tsx +106 -0
- package/src/ui/changes.tsx +89 -0
- package/src/ui/chart-bits.tsx +52 -0
- package/src/ui/chart.ts +577 -0
- package/src/ui/checks.tsx +203 -0
- package/src/ui/child-index.tsx +44 -0
- package/src/ui/children.ts +43 -0
- package/src/ui/chips.tsx +49 -0
- package/src/ui/cmd.tsx +27 -0
- package/src/ui/columns.tsx +31 -0
- package/src/ui/comments.tsx +544 -0
- package/src/ui/compare.tsx +67 -0
- package/src/ui/decision.tsx +79 -0
- package/src/ui/deps.tsx +78 -0
- package/src/ui/details.tsx +43 -0
- package/src/ui/diff-parse.ts +307 -0
- package/src/ui/diff.tsx +458 -0
- package/src/ui/diffstat.tsx +58 -0
- package/src/ui/due.tsx +65 -0
- package/src/ui/effort.tsx +29 -0
- package/src/ui/endpoints.tsx +118 -0
- package/src/ui/envvars.tsx +96 -0
- package/src/ui/figure.tsx +37 -0
- package/src/ui/file-icon.ts +1020 -0
- package/src/ui/file-link.ts +32 -0
- package/src/ui/file-ref.tsx +39 -0
- package/src/ui/files.tsx +112 -0
- package/src/ui/findings.tsx +85 -0
- package/src/ui/flow.tsx +73 -0
- package/src/ui/funnel.tsx +121 -0
- package/src/ui/gantt.tsx +443 -0
- package/src/ui/gauges.tsx +135 -0
- package/src/ui/glossary.tsx +29 -0
- package/src/ui/graph-layout.ts +149 -0
- package/src/ui/graph-specs.tsx +102 -0
- package/src/ui/graph.tsx +278 -0
- package/src/ui/grid.tsx +119 -0
- package/src/ui/hypothesis.tsx +94 -0
- package/src/ui/icon.tsx +80 -0
- package/src/ui/incident.tsx +130 -0
- package/src/ui/index.ts +342 -0
- package/src/ui/ins-del.tsx +41 -0
- package/src/ui/json.tsx +190 -0
- package/src/ui/layout.ts +9 -0
- package/src/ui/line-chart.tsx +251 -0
- package/src/ui/matrix.tsx +208 -0
- package/src/ui/meta.tsx +73 -0
- package/src/ui/option.tsx +64 -0
- package/src/ui/owner.tsx +40 -0
- package/src/ui/packages.tsx +104 -0
- package/src/ui/pathway.tsx +96 -0
- package/src/ui/phase.tsx +41 -0
- package/src/ui/pie-chart.tsx +170 -0
- package/src/ui/plan.tsx +77 -0
- package/src/ui/pre.tsx +175 -0
- package/src/ui/priority.tsx +51 -0
- package/src/ui/props.tsx +77 -0
- package/src/ui/quadrant.tsx +172 -0
- package/src/ui/radar.tsx +200 -0
- package/src/ui/ref.tsx +160 -0
- package/src/ui/release.tsx +178 -0
- package/src/ui/req.tsx +42 -0
- package/src/ui/review.tsx +133 -0
- package/src/ui/risk.tsx +67 -0
- package/src/ui/sankey.tsx +287 -0
- package/src/ui/scatter.tsx +237 -0
- package/src/ui/schema.tsx +113 -0
- package/src/ui/score.tsx +105 -0
- package/src/ui/search.tsx +120 -0
- package/src/ui/series.tsx +38 -0
- package/src/ui/severity.tsx +69 -0
- package/src/ui/shadcn.tsx +216 -0
- package/src/ui/spark.tsx +86 -0
- package/src/ui/stack.tsx +57 -0
- package/src/ui/stats.tsx +63 -0
- package/src/ui/status-badge.tsx +66 -0
- package/src/ui/statuspage.tsx +238 -0
- package/src/ui/steps.tsx +74 -0
- package/src/ui/summary.tsx +41 -0
- package/src/ui/symbol-ref.tsx +73 -0
- package/src/ui/terminal.tsx +117 -0
- package/src/ui/tests.tsx +218 -0
- package/src/ui/timeline.tsx +63 -0
- package/src/ui/toc.tsx +56 -0
- package/src/ui/tones.ts +187 -0
- package/src/ui/trace.tsx +69 -0
- package/src/ui/tree.tsx +281 -0
- package/src/ui/treemap.tsx +128 -0
- package/src/ui/venn.tsx +258 -0
- package/src/ui/verdict.tsx +78 -0
- 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`;
|