@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,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;
|