@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,145 @@
1
+ /**
2
+ * The virtual `@suzumiyaaoba/mdxr`(/components) module used inside hydration
3
+ * bundles.
4
+ * A fresh module is generated per importer containing only the re-exports
5
+ * that importer requests: esbuild eagerly resolves every
6
+ * `export { x } from "m"` target, and the leaf modules' top-level
7
+ * `defineComponent(...)` calls count as side effects — so a module exporting
8
+ * the whole catalog would drag the whole catalog into the bundle.
9
+ */
10
+
11
+ import path from "node:path";
12
+
13
+ import type { Plugin } from "esbuild";
14
+ import * as valibot from "valibot";
15
+
16
+ import { srcDir } from "../paths.js";
17
+ import type { ExportIndex } from "./export-index.js";
18
+ import { scanMdxrImports } from "./import-scan.js";
19
+ import type { MdxrImports } from "./import-scan.js";
20
+
21
+ /**
22
+ * Names that live outside the leaf map: `builtinComponents` is defined in the
23
+ * catalog barrel itself (pulling it is intentional — the whole catalog is the
24
+ * value) and `defineConfig` is part of the `@suzumiyaaoba/mdxr` API surface.
25
+ * They resolve
26
+ * only when a module actually imports them.
27
+ */
28
+ export const EXTRA_MODULES: Record<string, string> = {
29
+ builtinComponents: path.join(srcDir, "ui/index.js"),
30
+ defineConfig: path.join(srcDir, "config.js"),
31
+ mountDocument: path.join(srcDir, "hydrate-runtime.js"),
32
+ };
33
+
34
+ /**
35
+ * The virtual module's `v` export. `export * as v` would materialize the
36
+ * entire valibot namespace — a plain object built from named imports ships
37
+ * only the schemas actually written. Unknown `v.x` accesses stay absent,
38
+ * matching `undefined` semantics. The `surface` gate keeps
39
+ * `@suzumiyaaoba/mdxr/components` honest: that specifier has no `v` export,
40
+ * so shipping one would diverge from SSR.
41
+ */
42
+ const vExportLines = (imports: MdxrImports, surface: Set<string>): string[] => {
43
+ if (!surface.has("v") || imports.vProps === null) {
44
+ return [];
45
+ }
46
+ if (imports.vProps === "all" || imports.names === "all") {
47
+ return ['export * as v from "valibot";'];
48
+ }
49
+ const props: string[] = [];
50
+ for (const p of imports.vProps) {
51
+ if (p in valibot) {
52
+ props.push(p);
53
+ }
54
+ }
55
+ return [
56
+ props.length > 0
57
+ ? `import { ${props.join(", ")} } from "valibot";\nexport const v = { ${props.join(", ")} };`
58
+ : "export const v = {};",
59
+ ];
60
+ };
61
+
62
+ /**
63
+ * Virtual stand-in for `@suzumiyaaoba/mdxr`(/components) inside the bundle.
64
+ */
65
+ // Exported for tests — pure codegen, so the emitted export list is easy to
66
+ // assert without running esbuild.
67
+ export const runtimeModuleContents = (
68
+ map: Map<string, string>,
69
+ imports: MdxrImports,
70
+ surface: Set<string>
71
+ ): string => {
72
+ const lines = [
73
+ `export { defineComponent, textOf } from ${JSON.stringify(path.join(srcDir, "define.js"))};`,
74
+ ...vExportLines(imports, surface),
75
+ ];
76
+ // "all" (namespace/default/dynamic imports) exports exactly the specifier's
77
+ // real surface — leaf internals and EXTRA names like `mountDocument` stay
78
+ // hidden, matching what the actual package would hand SSR. Named imports
79
+ // beyond the surface are skipped: SSR's link error is then mirrored by an
80
+ // esbuild "no matching export" failure instead of a silent divergence.
81
+ const wanted = imports.names === "all" ? [...surface] : [...imports.names];
82
+ const byModule = new Map<string, string[]>();
83
+ for (const name of wanted) {
84
+ if (!surface.has(name)) {
85
+ continue;
86
+ }
87
+ const mod = map.get(name) ?? EXTRA_MODULES[name];
88
+ if (mod === undefined) {
89
+ continue;
90
+ }
91
+ const list = byModule.get(mod) ?? [];
92
+ list.push(name);
93
+ byModule.set(mod, list);
94
+ }
95
+ for (const [mod, mods] of byModule) {
96
+ lines.push(`export { ${mods.join(", ")} } from ${JSON.stringify(mod)};`);
97
+ }
98
+ return lines.join("\n");
99
+ };
100
+
101
+ /* oxlint-disable require-unicode-regexp -- esbuild onResolve/onLoad filters forbid `u` */
102
+ export const runtimeModule = (index: ExportIndex): Plugin => ({
103
+ name: "mdxr:runtime",
104
+ setup(b) {
105
+ const importers = new Map<
106
+ string,
107
+ { importer: string; surface: Set<string> }
108
+ >();
109
+ let seq = 0;
110
+ b.onResolve(
111
+ { filter: /^@suzumiyaaoba\/mdxr(?:\/components)?$/ },
112
+ (args) => {
113
+ const virtual = `mdxr:runtime:${seq}`;
114
+ seq += 1;
115
+ importers.set(virtual, {
116
+ importer: args.importer,
117
+ // Each specifier gets its own real surface — the root entry (config
118
+ // API) and `/components` (the catalog) are deliberately different
119
+ // sets.
120
+ surface:
121
+ args.path === "@suzumiyaaoba/mdxr"
122
+ ? index.surfaces.mdxr
123
+ : index.surfaces.components,
124
+ });
125
+ return { namespace: "mdxr-runtime", path: virtual };
126
+ }
127
+ );
128
+ b.onLoad(
129
+ { filter: /^mdxr:runtime:/, namespace: "mdxr-runtime" },
130
+ async (args) => {
131
+ const v = importers.get(args.path);
132
+ return {
133
+ contents: runtimeModuleContents(
134
+ index.map,
135
+ await scanMdxrImports(v?.importer ?? ""),
136
+ v?.surface ?? index.surfaces.components
137
+ ),
138
+ loader: "js",
139
+ resolveDir: srcDir,
140
+ };
141
+ }
142
+ );
143
+ },
144
+ });
145
+ /* oxlint-enable require-unicode-regexp */
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Client-side mount for hydrated documents. Bundled into the generated
3
+ * hydration entry by `buildHydrateScript` — keeping this as a real module
4
+ * means TypeScript checks the vnode wiring and the component-merge rules are
5
+ * literally shared with the SSR path (component-map.ts).
6
+ */
7
+
8
+ import { createElement, Fragment } from "react";
9
+ import { hydrateRoot } from "react-dom/client";
10
+
11
+ import { mergeUserComponents } from "./component-map.js";
12
+ import type { AnyComponent, ComponentMap } from "./define.js";
13
+ import { DocContext } from "./doc-context.js";
14
+
15
+ export interface MountSpec {
16
+ /** Catalog components assembled by the generated entry. */
17
+ components: ComponentMap;
18
+ /** The compiled MDX module's default export. */
19
+ doc: AnyComponent;
20
+ /** `fileLink(rel, line)` results recorded during SSR (`rel\0line` → url). */
21
+ fileLinks: Record<string, string>;
22
+ /** PlanHeader props, present iff the document header was rendered. */
23
+ headerProps?: Record<string, string | undefined>;
24
+ /** SSR render timestamp (ISO) — replayed into `DocContext.now`. */
25
+ now?: string;
26
+ /** The PlanHeader component — only bound when `headerProps` is set. */
27
+ planHeader?: AnyComponent;
28
+ /** The user's components module namespace, when configured. */
29
+ userModule?: Record<string, unknown>;
30
+ }
31
+
32
+ /**
33
+ * Rebuild the exact vnode tree SSR produced — `DocContext.Provider` wrapping
34
+ * the Plan header + the compiled MDX module — and `hydrateRoot` it onto
35
+ * `<main id="mdxr-root">`. `fileLink` is replayed from the recorded SSR
36
+ * answers, so components see identical data without filesystem access.
37
+ */
38
+ export const mountDocument = (spec: MountSpec): void => {
39
+ if (spec.userModule !== undefined) {
40
+ mergeUserComponents(spec.userModule, spec.components);
41
+ }
42
+ const root = document.querySelector("#mdxr-root");
43
+ if (root === null) {
44
+ return;
45
+ }
46
+ hydrateRoot(
47
+ root,
48
+ createElement(
49
+ DocContext.Provider,
50
+ {
51
+ value: {
52
+ fileLink: (rel: string, line?: string) =>
53
+ spec.fileLinks[`${rel}\0${line ?? ""}`],
54
+ now: spec.now === undefined ? undefined : new Date(spec.now),
55
+ },
56
+ },
57
+ createElement(
58
+ Fragment,
59
+ null,
60
+ spec.headerProps === undefined || spec.planHeader === undefined
61
+ ? null
62
+ : createElement(spec.planHeader, spec.headerProps),
63
+ createElement(spec.doc, { components: spec.components })
64
+ )
65
+ )
66
+ );
67
+ };
package/src/hydrate.ts ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Hydration bundle assembly: generates the inlined client script that
3
+ * rebuilds the exact vnode tree SSR produced and `hydrateRoot`s it onto
4
+ * `<main id="mdxr-root">`. Implementation details live in `src/hydrate/`:
5
+ * `export-index` (export surface scanning), `import-scan` (user-module
6
+ * import analysis), `runtime-module` (virtual package module codegen), and
7
+ * `plugins` (esbuild plugins for shared packages, the doc module, icons).
8
+ */
9
+
10
+ import { build } from "esbuild";
11
+
12
+ import { exportIndex } from "./hydrate/export-index.js";
13
+ import { docModule, iconSets, pinShared } from "./hydrate/plugins.js";
14
+ import { EXTRA_MODULES, runtimeModule } from "./hydrate/runtime-module.js";
15
+ import { pkgRoot } from "./paths.js";
16
+ import { builtinComponents } from "./ui/index.js";
17
+
18
+ export { exportIndex } from "./hydrate/export-index.js";
19
+ export { runtimeModuleContents } from "./hydrate/runtime-module.js";
20
+
21
+ export interface HydrateSpec {
22
+ /** Compiled MDX module source (`program` format; default export = content). */
23
+ code: string;
24
+ /** Resolved entry of the user's components module, when configured. */
25
+ componentsPath?: string;
26
+ /** `fileLink(rel, line)` results recorded during SSR (`rel\0line` → url). */
27
+ fileLinks: Record<string, string>;
28
+ /** PlanHeader props, present iff the document header was rendered. */
29
+ header?: Record<string, string | undefined>;
30
+ /** SSR render timestamp (ISO) — replayed into `DocContext.now`. */
31
+ now?: string;
32
+ /** Catalog keys the document referenced — only these get imported. */
33
+ usedComponents: string[];
34
+ /** Iconify names (`prefix:name`) resolved during SSR. */
35
+ usedIcons: string[];
36
+ }
37
+
38
+ /**
39
+ * Build the inlined client script: it rebuilds the exact vnode tree SSR
40
+ * produced — `DocContext.Provider` wrapping the Plan header + the compiled
41
+ * MDX module — and `hydrateRoot`s it onto `<main id="mdxr-root">`.
42
+ * `fileLink` is replayed from the recorded SSR answers, so components see
43
+ * identical data without filesystem access.
44
+ */
45
+ export const buildHydrateScript = async (
46
+ spec: HydrateSpec
47
+ ): Promise<string> => {
48
+ const index = await exportIndex();
49
+ const { map } = index;
50
+ const imports: string[] = [];
51
+ const entries: string[] = [];
52
+ let seq = 0;
53
+ // One aliased named import per leaf module keeps the generated code simple
54
+ // and collision-free; esbuild dedupes repeated module loads.
55
+ const bind = (exported: string): string => {
56
+ const mod = map.get(exported) ?? EXTRA_MODULES[exported];
57
+ if (mod === undefined) {
58
+ throw new Error(
59
+ `hydration: no module provides catalog export "${exported}"`
60
+ );
61
+ }
62
+ const local = `__mdxr_c${seq}`;
63
+ seq += 1;
64
+ imports.push(
65
+ `import { ${exported} as ${local} } from ${JSON.stringify(mod)};`
66
+ );
67
+ return local;
68
+ };
69
+
70
+ const mountDocument = bind("mountDocument");
71
+ // PlanHeader is only needed when a frontmatter header was rendered —
72
+ // binding it unconditionally would drag Meta/Due/StatusBadge into every
73
+ // bundle.
74
+ const planHeader =
75
+ spec.header === undefined ? "undefined" : bind("PlanHeader");
76
+
77
+ for (const name of spec.usedComponents) {
78
+ // User components merge via their namespace below; the `hasOwn` check must
79
+ // be own-property only — `in` alone treats `constructor` etc. as catalog
80
+ // names. `pre` (the markdown override) is exported as `Pre`.
81
+ if (!Object.hasOwn(builtinComponents, name)) {
82
+ continue;
83
+ }
84
+ const exported = name === "pre" ? "Pre" : name;
85
+ entries.push(`${JSON.stringify(name)}: ${bind(exported)}`);
86
+ }
87
+
88
+ const userImport =
89
+ spec.componentsPath === undefined
90
+ ? ""
91
+ : `import * as __mdxrUser from ${JSON.stringify(spec.componentsPath)};`;
92
+
93
+ // Mount/merge details live in hydrate-runtime (a real, type-checked module
94
+ // bundled by esbuild); the generated entry only binds names and data.
95
+ const entry = `${imports.join("\n")}
96
+ ${userImport}
97
+ import __mdxrDoc from "mdxr:doc";
98
+ ${mountDocument}({
99
+ components: { ${entries.join(", ")} },
100
+ doc: __mdxrDoc,
101
+ fileLinks: ${JSON.stringify(spec.fileLinks)},
102
+ headerProps: ${JSON.stringify(spec.header)},
103
+ now: ${JSON.stringify(spec.now)},
104
+ planHeader: ${planHeader},
105
+ userModule: ${spec.componentsPath === undefined ? "undefined" : "__mdxrUser"},
106
+ });
107
+ `;
108
+
109
+ const result = await build({
110
+ absWorkingDir: pkgRoot,
111
+ bundle: true,
112
+ define: { "process.env.NODE_ENV": '"production"' },
113
+ format: "iife",
114
+ jsx: "automatic",
115
+ jsxImportSource: "react",
116
+ logLevel: "silent",
117
+ metafile: true,
118
+ minify: true,
119
+ platform: "browser",
120
+ plugins: [
121
+ pinShared,
122
+ docModule(spec.code),
123
+ runtimeModule(index),
124
+ iconSets(spec.usedIcons),
125
+ ],
126
+ stdin: {
127
+ contents: entry,
128
+ loader: "js",
129
+ resolveDir: pkgRoot,
130
+ sourcefile: "mdxr-hydrate.js",
131
+ },
132
+ target: "es2022",
133
+ write: false,
134
+ });
135
+ // `MDXR_BUNDLE_METAFILE=<path>` dumps the esbuild metafile for size work.
136
+ if (process.env.MDXR_BUNDLE_METAFILE !== undefined) {
137
+ const { writeFile } = await import("node:fs/promises");
138
+ await writeFile(
139
+ process.env.MDXR_BUNDLE_METAFILE,
140
+ JSON.stringify(result.metafile)
141
+ );
142
+ await writeFile(`${process.env.MDXR_BUNDLE_METAFILE}.entry.js`, entry);
143
+ }
144
+ // htmlDocument applies `inlineScript` escaping at embed time — return the
145
+ // raw bundle here.
146
+ return result.outputFiles[0]?.text ?? "";
147
+ };
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ // Public entry: `@suzumiyaaoba/mdxr`
2
+ // API surface for mdxr.config.ts and custom component modules.
3
+ export { defineConfig, type MdxrConfig } from "./config.js";
4
+ export {
5
+ defineComponent,
6
+ textOf,
7
+ type ComponentMeta,
8
+ type MdxrComponent,
9
+ } from "./define.js";
10
+ export { DocContext } from "./doc-context.js";
11
+ // Also part of the root surface: config modules may want the catalog
12
+ // without importing `@suzumiyaaoba/mdxr/components`. The hydration bundle
13
+ // mirrors each
14
+ // specifier's real surface, so the two entries stay distinct.
15
+ export { builtinComponents } from "./ui/index.js";
16
+ export * as v from "valibot";
package/src/init.ts ADDED
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { own } from "./guards.js";
7
+ import { pkgRoot } from "./paths.js";
8
+
9
+ const SKILL_DIRS: Record<string, { global: string; local: string }> = {
10
+ agents: { global: ".agents/skills/mdxr", local: ".agents/skills/mdxr" },
11
+ claude: { global: ".claude/skills/mdxr", local: ".claude/skills/mdxr" },
12
+ devin: { global: ".config/devin/skills/mdxr", local: ".devin/skills/mdxr" },
13
+ };
14
+
15
+ export interface InitOptions {
16
+ tool?: string;
17
+ global?: boolean;
18
+ force?: boolean;
19
+ }
20
+
21
+ export const installSkill = async (opts: InitOptions): Promise<string[]> => {
22
+ const tool = opts.tool ?? "agents";
23
+ const global = opts.global === true;
24
+ const base = global ? os.homedir() : process.cwd();
25
+
26
+ const tools = tool === "all" ? Object.keys(SKILL_DIRS) : [tool];
27
+ // `own`: `--tool toString` would otherwise pull a function off the
28
+ // prototype and crash on `path.join(base, undefined)`.
29
+ const dests = tools.map((t) => {
30
+ const dirs = own(SKILL_DIRS, t);
31
+ if (dirs === undefined) {
32
+ throw new Error(`unknown tool: ${t} (expected agents|claude|devin|all)`);
33
+ }
34
+ return path.join(base, global ? dirs.global : dirs.local);
35
+ });
36
+ for (const dest of dests) {
37
+ if (fs.existsSync(dest) && opts.force !== true) {
38
+ throw new Error(`${dest} already exists (use --force to overwrite)`);
39
+ }
40
+ }
41
+
42
+ return await Promise.all(
43
+ dests.map(async (dest) => {
44
+ await fsp.mkdir(path.dirname(dest), { recursive: true });
45
+ if (opts.force === true) {
46
+ // fs.cp only overwrites same-named files — files deleted or renamed
47
+ // upstream would linger in dest. A force install replaces wholesale.
48
+ await fsp.rm(dest, { force: true, recursive: true });
49
+ }
50
+ await fsp.cp(path.join(pkgRoot, "skill"), dest, {
51
+ force: true,
52
+ recursive: true,
53
+ });
54
+ return dest;
55
+ })
56
+ );
57
+ };
package/src/langs.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { own } from "./guards.js";
2
+
3
+ /**
4
+ * Fence-language sets shared by the rehype highlighter and the `Terminal`
5
+ * component. `Terminal` renders these as transcripts; shiki must skip them.
6
+ */
7
+
8
+ /** Session/transcript fence languages rendered by the `Terminal` component. */
9
+ export const TERMINAL_LANGS: ReadonlySet<string> = new Set([
10
+ "console",
11
+ "shell-session",
12
+ "shellsession",
13
+ "terminal",
14
+ ]);
15
+
16
+ /** Languages with no grammar — left as plain `<pre>` text. */
17
+ export const PLAIN_LANGS: ReadonlySet<string> = new Set([
18
+ "plain",
19
+ "plaintext",
20
+ "text",
21
+ "txt",
22
+ ]);
23
+
24
+ /** Fences the shiki highlighter skips entirely (terminal + mermaid + plain). */
25
+ export const SKIP_LANGS: ReadonlySet<string> = new Set([
26
+ ...TERMINAL_LANGS,
27
+ ...PLAIN_LANGS,
28
+ "mermaid",
29
+ ]);
30
+
31
+ /** Exact basenames (lowercased) → shiki language id, for files with no
32
+ * meaningful extension. */
33
+ const FILE_LANGS: Record<string, string> = {
34
+ ".babelrc": "json",
35
+ ".editorconfig": "ini",
36
+ ".eslintrc": "json",
37
+ ".gitignore": "ini",
38
+ ".npmrc": "ini",
39
+ ".prettierrc": "json",
40
+ brewfile: "ruby",
41
+ "cmakelists.txt": "cmake",
42
+ containerfile: "dockerfile",
43
+ dockerfile: "dockerfile",
44
+ gemfile: "ruby",
45
+ gnumakefile: "makefile",
46
+ guardfile: "ruby",
47
+ jenkinsfile: "groovy",
48
+ makefile: "makefile",
49
+ podfile: "ruby",
50
+ rakefile: "ruby",
51
+ vagrantfile: "ruby",
52
+ };
53
+
54
+ /** Extensions (lowercase, no dot) → shiki language id. Unknown ids are safe:
55
+ * the highlighter's lazy loader rejects names outside shiki's bundle and the
56
+ * caller falls back to unhighlighted rows. */
57
+ const EXT_LANGS: Record<string, string> = {
58
+ astro: "astro",
59
+ bash: "bash",
60
+ c: "c",
61
+ cc: "cpp",
62
+ cfg: "ini",
63
+ cjs: "javascript",
64
+ clj: "clojure",
65
+ cljs: "clojure",
66
+ cmake: "cmake",
67
+ cpp: "cpp",
68
+ cs: "csharp",
69
+ css: "css",
70
+ cts: "typescript",
71
+ cxx: "cpp",
72
+ dart: "dart",
73
+ dockerfile: "dockerfile",
74
+ ex: "elixir",
75
+ exs: "elixir",
76
+ fish: "fish",
77
+ fs: "fsharp",
78
+ fsx: "fsharp",
79
+ gql: "graphql",
80
+ graphql: "graphql",
81
+ h: "c",
82
+ hh: "cpp",
83
+ hpp: "cpp",
84
+ hs: "haskell",
85
+ htm: "html",
86
+ html: "html",
87
+ hxx: "cpp",
88
+ ini: "ini",
89
+ java: "java",
90
+ jl: "julia",
91
+ js: "javascript",
92
+ json: "json",
93
+ json5: "json5",
94
+ jsonc: "jsonc",
95
+ jsx: "jsx",
96
+ kt: "kotlin",
97
+ kts: "kotlin",
98
+ less: "less",
99
+ lua: "lua",
100
+ md: "markdown",
101
+ mdx: "markdown",
102
+ mjs: "javascript",
103
+ ml: "ocaml",
104
+ mts: "typescript",
105
+ php: "php",
106
+ pl: "perl",
107
+ pm: "perl",
108
+ prisma: "prisma",
109
+ proto: "proto",
110
+ ps1: "powershell",
111
+ py: "python",
112
+ pyi: "python",
113
+ r: "r",
114
+ rb: "ruby",
115
+ rs: "rust",
116
+ sc: "scala",
117
+ scala: "scala",
118
+ scss: "scss",
119
+ sh: "bash",
120
+ sql: "sql",
121
+ svelte: "svelte",
122
+ svg: "xml",
123
+ swift: "swift",
124
+ tf: "terraform",
125
+ toml: "toml",
126
+ ts: "typescript",
127
+ tsx: "tsx",
128
+ txt: "text",
129
+ vb: "vb",
130
+ vue: "vue",
131
+ xml: "xml",
132
+ yaml: "yaml",
133
+ yml: "yaml",
134
+ zig: "zig",
135
+ zsh: "zsh",
136
+ };
137
+
138
+ /** Best-guess shiki language for a file path — basename first, then the
139
+ * extension. `x.d.ts` keeps `ts` (the declaration grammar is the same). */
140
+ export const langForPath = (path: string): string | undefined => {
141
+ const base = path.split(/[\\/]/u).at(-1)?.toLowerCase() ?? "";
142
+ const named = own(FILE_LANGS, base);
143
+ if (named !== undefined) {
144
+ return named;
145
+ }
146
+ const ext = /\.(?<e>[^.]+)$/u.exec(base)?.groups?.e;
147
+ return ext === undefined ? undefined : own(EXT_LANGS, ext);
148
+ };
package/src/lines.ts ADDED
@@ -0,0 +1,53 @@
1
+ /** The shared `path:lines` grammar (`src/x.ts:40-52`, `…:40`, `…:40-`). */
2
+
3
+ /** "`src/a.ts:40-52`" → `{ path: "src/a.ts", lines: "40-52" }`. */
4
+ export const splitPathLines = (
5
+ value: string
6
+ ): { lines?: string; path: string } => {
7
+ const m = /^(?<p>.+):(?<ls>\d+(?:-\d*)?)$/u.exec(value);
8
+ return m?.groups === undefined
9
+ ? { path: value }
10
+ : { lines: m.groups.ls, path: m.groups.p };
11
+ };
12
+
13
+ /**
14
+ * `lines="40-52"`, `lines="40"`, `lines="40-"` → 1-based inclusive range
15
+ * (`end` undefined means "to EOF"). Invalid specs return undefined.
16
+ */
17
+ export const parseLineRange = (
18
+ spec: string
19
+ ): { end: number | undefined; start: number } | undefined => {
20
+ const m = /^(?<start>\d+)(?:-(?<end>\d*))?$/u.exec(spec.trim());
21
+ if (m?.groups === undefined) {
22
+ return undefined;
23
+ }
24
+ const start = Number(m.groups.start);
25
+ let end: number | undefined;
26
+ if (m.groups.end === undefined) {
27
+ end = start;
28
+ } else if (m.groups.end !== "") {
29
+ end = Number(m.groups.end);
30
+ }
31
+ if (start < 1 || (end !== undefined && end < start)) {
32
+ return undefined;
33
+ }
34
+ return { end, start };
35
+ };
36
+
37
+ /** "40-52" → "40" — the first line of a range spec, used for link targets. */
38
+ export const firstLine = (lines: string | undefined): string | undefined =>
39
+ /^(?<n>\d+)/u.exec(lines ?? "")?.groups?.n;
40
+
41
+ /**
42
+ * `title="x.ts"` / `filename='x.ts'` / `title=x.ts` inside a code-fence meta
43
+ * string → the filename. Boundary-anchored: `data-title="x"` must not yield a
44
+ * filename. Shared by `Pre`'s header and the diff highlighter's lang guess.
45
+ */
46
+ export const fenceFilename = (meta: string): string | undefined =>
47
+ /(?:^|\s)(?:title|filename)="(?<name>[^"]+)"/u.exec(meta)?.groups?.name ??
48
+ /(?:^|\s)(?:title|filename)='(?<sq>[^']+)'/u.exec(meta)?.groups?.sq ??
49
+ /(?:^|\s)(?:title|filename)=(?<name>[^\s"']+)/u.exec(meta)?.groups?.name;
50
+
51
+ /** `lang=ts` inside a fence meta string → the explicit language override. */
52
+ export const fenceLang = (meta: string): string | undefined =>
53
+ /(?:^|\s)lang=(?<l>[\w+.#-]+)/u.exec(meta)?.groups?.l;