@izumisy/md-react-preview 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 +116 -0
- package/app/index.html +41 -0
- package/app/src/app.tsx +219 -0
- package/app/src/code-block.tsx +182 -0
- package/app/src/main.tsx +9 -0
- package/app/src/mdx-components.tsx +103 -0
- package/app/src/preview-block.tsx +217 -0
- package/app/src/theme.tsx +45 -0
- package/app/src/virtual-modules.d.ts +26 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +59 -0
- package/dist/index.d.mts +3370 -0
- package/dist/index.mjs +2 -0
- package/dist/server-C2ZxWhHj.mjs +275 -0
- package/package.json +60 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as escapeJsString, c as DEFAULT_GLOB, i as createPreviewerViteConfig, l as defineConfig, n as runPreview, o as extractPreviewBlocks, r as startDev, s as hasPreviewBlocks, t as runBuild } from "./server-C2ZxWhHj.mjs";
|
|
2
|
+
export { DEFAULT_GLOB, createPreviewerViteConfig, defineConfig, escapeJsString, extractPreviewBlocks, hasPreviewBlocks, runBuild, runPreview, startDev };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { build, createServer, preview } from "vite";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import react from "@vitejs/plugin-react";
|
|
6
|
+
import mdx from "@mdx-js/rollup";
|
|
7
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
8
|
+
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
|
9
|
+
import remarkGfm from "remark-gfm";
|
|
10
|
+
import { REGISTRY_MODULE_ID, VIRTUAL_PREFIX, createBasePreviewPlugin, createPreviewBuildPlugin, parseMeta, resolveCssImportPath, simpleHash } from "@izumisy/vite-plugin-react-preview";
|
|
11
|
+
//#region src/config.ts
|
|
12
|
+
const DEFAULT_GLOB = "docs/**/*.md";
|
|
13
|
+
function defineConfig(config) {
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/preview-transform.ts
|
|
18
|
+
const PREVIEW_FENCE_RE = /```tsx preview(.*?)\n([\s\S]*?)```/g;
|
|
19
|
+
/**
|
|
20
|
+
* Extract all ` ```tsx preview ` fenced code blocks from a markdown/mdx string.
|
|
21
|
+
* Returns an array of blocks with their code and positions.
|
|
22
|
+
*/
|
|
23
|
+
function extractPreviewBlocks(source) {
|
|
24
|
+
const blocks = [];
|
|
25
|
+
const re = new RegExp(PREVIEW_FENCE_RE.source, "g");
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = re.exec(source)) !== null) blocks.push({
|
|
28
|
+
code: match[2].replace(/\n$/, ""),
|
|
29
|
+
start: match.index,
|
|
30
|
+
end: match.index + match[0].length,
|
|
31
|
+
meta: parseMeta(match[1])
|
|
32
|
+
});
|
|
33
|
+
return blocks;
|
|
34
|
+
}
|
|
35
|
+
function escapeJsString(s) {
|
|
36
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check whether a source string contains ` ```tsx preview ` blocks.
|
|
40
|
+
*/
|
|
41
|
+
function hasPreviewBlocks(source) {
|
|
42
|
+
return source.includes("tsx preview");
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/vite-plugins/preview.ts
|
|
46
|
+
/**
|
|
47
|
+
* Vite plugin that:
|
|
48
|
+
* 1. Transforms ` ```tsx preview ` fenced code blocks in .preview.mdx files
|
|
49
|
+
* into `<PreviewBlock>` components (runs before the MDX compiler).
|
|
50
|
+
* 2. Provides virtual modules for each preview block so they can be rendered
|
|
51
|
+
* in isolated iframe containers.
|
|
52
|
+
* 3. Handles HMR invalidation when preview files change.
|
|
53
|
+
* 4. In build mode, scans preview files upfront to populate the block registry.
|
|
54
|
+
*/
|
|
55
|
+
function previewPlugin(resolveFiles, options) {
|
|
56
|
+
const blockRegistry = /* @__PURE__ */ new Map();
|
|
57
|
+
let devServer = null;
|
|
58
|
+
const basePlugins = createBasePreviewPlugin("mrp-preview-base", {
|
|
59
|
+
blockRegistry,
|
|
60
|
+
cssImport: options?.css ? resolveCssImportPath(options.css, options.hostRoot) : void 0
|
|
61
|
+
});
|
|
62
|
+
const transformPlugin = {
|
|
63
|
+
name: "mrp-preview",
|
|
64
|
+
enforce: "pre",
|
|
65
|
+
async transform(code, id) {
|
|
66
|
+
if (!id.endsWith(".md")) return;
|
|
67
|
+
let transformed;
|
|
68
|
+
if (hasPreviewBlocks(code)) {
|
|
69
|
+
const blocks = extractPreviewBlocks(code);
|
|
70
|
+
transformed = code;
|
|
71
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
72
|
+
const block = blocks[i];
|
|
73
|
+
const blockId = simpleHash(`${id}:${i}`);
|
|
74
|
+
const escaped = escapeJsString(block.code);
|
|
75
|
+
const isStandalone = block.meta.standalone === "true";
|
|
76
|
+
blockRegistry.set(blockId, {
|
|
77
|
+
code: block.code,
|
|
78
|
+
sourceFile: id,
|
|
79
|
+
wrap: block.meta.wrap,
|
|
80
|
+
height: block.meta.height,
|
|
81
|
+
standalone: isStandalone
|
|
82
|
+
});
|
|
83
|
+
const replacement = `<PreviewBlock code={"${escaped}"} blockId={"${blockId}"}${block.meta.height ? ` height={"${block.meta.height}"}` : ""}${block.meta.wrap ? ` wrap={"${block.meta.wrap}"}` : ""}${block.meta.align ? ` align={"${block.meta.align}"}` : ""}${isStandalone ? ` standalone={true}` : ""} />`;
|
|
84
|
+
transformed = transformed.slice(0, block.start) + replacement + transformed.slice(block.end);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (devServer) {
|
|
88
|
+
for (const [blockId, entry] of blockRegistry) if (entry.sourceFile === id) {
|
|
89
|
+
const mod = devServer.moduleGraph.getModuleById("\0" + VIRTUAL_PREFIX + blockId + ".tsx");
|
|
90
|
+
if (mod) devServer.moduleGraph.invalidateModule(mod);
|
|
91
|
+
}
|
|
92
|
+
const registryMod = devServer.moduleGraph.getModuleById("\0" + REGISTRY_MODULE_ID);
|
|
93
|
+
if (registryMod) devServer.moduleGraph.invalidateModule(registryMod);
|
|
94
|
+
}
|
|
95
|
+
if (transformed) return {
|
|
96
|
+
code: transformed,
|
|
97
|
+
map: null
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
configureServer(server) {
|
|
101
|
+
devServer = server;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
async function scanBlocks() {
|
|
105
|
+
const { readFile } = await import("node:fs/promises");
|
|
106
|
+
const files = await resolveFiles();
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const blocks = extractPreviewBlocks(await readFile(file, "utf-8"));
|
|
109
|
+
if (blocks.length === 0) continue;
|
|
110
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
111
|
+
const blockId = simpleHash(`${file}:${i}`);
|
|
112
|
+
blockRegistry.set(blockId, {
|
|
113
|
+
code: blocks[i].code,
|
|
114
|
+
sourceFile: file,
|
|
115
|
+
wrap: blocks[i].meta.wrap,
|
|
116
|
+
height: blocks[i].meta.height
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
plugins: [
|
|
123
|
+
transformPlugin,
|
|
124
|
+
...basePlugins,
|
|
125
|
+
createPreviewBuildPlugin({
|
|
126
|
+
blockRegistry,
|
|
127
|
+
scanBlocks
|
|
128
|
+
})
|
|
129
|
+
],
|
|
130
|
+
blockRegistry
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/vite-plugins/entries.ts
|
|
135
|
+
/**
|
|
136
|
+
* Virtual module `virtual:previewer-entries` — exports all discovered
|
|
137
|
+
* *.md files as an array of { name, Component } objects.
|
|
138
|
+
*/
|
|
139
|
+
function previewerEntriesPlugin(hostRoot, resolveFiles) {
|
|
140
|
+
const MODULE_ID = "virtual:previewer-entries";
|
|
141
|
+
const RESOLVED_ID = "\0" + MODULE_ID;
|
|
142
|
+
return {
|
|
143
|
+
name: "previewer-entries",
|
|
144
|
+
resolveId(id) {
|
|
145
|
+
if (id === MODULE_ID) return RESOLVED_ID;
|
|
146
|
+
},
|
|
147
|
+
async load(id) {
|
|
148
|
+
if (id !== RESOLVED_ID) return;
|
|
149
|
+
const files = await resolveFiles();
|
|
150
|
+
const entries = await Promise.all(files.map(async (file, i) => {
|
|
151
|
+
return {
|
|
152
|
+
varName: `Mod${i}`,
|
|
153
|
+
name: basename(file).replace(/\.md$/, ""),
|
|
154
|
+
file,
|
|
155
|
+
filePath: relative(hostRoot, file)
|
|
156
|
+
};
|
|
157
|
+
}));
|
|
158
|
+
return [
|
|
159
|
+
...entries.map((e) => `import ${e.varName}, { frontmatter as ${e.varName}Fm } from ${JSON.stringify(e.file)};`),
|
|
160
|
+
"",
|
|
161
|
+
"export const entries = [",
|
|
162
|
+
...entries.map((e) => ` { name: ${JSON.stringify(e.name)}, Component: ${e.varName}, frontmatter: ${e.varName}Fm ?? {}, filePath: ${JSON.stringify(e.filePath)} },`),
|
|
163
|
+
"];"
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/vite-plugins/css.ts
|
|
170
|
+
/**
|
|
171
|
+
* Virtual module `virtual:previewer-css` — imports the host project's
|
|
172
|
+
* CSS file if configured, or exports nothing.
|
|
173
|
+
*/
|
|
174
|
+
function previewerCssPlugin(hostRoot, css) {
|
|
175
|
+
const MODULE_ID = "virtual:previewer-css";
|
|
176
|
+
const RESOLVED_ID = "\0" + MODULE_ID + ".css";
|
|
177
|
+
return {
|
|
178
|
+
name: "previewer-css",
|
|
179
|
+
resolveId(id) {
|
|
180
|
+
if (id === MODULE_ID) return RESOLVED_ID;
|
|
181
|
+
},
|
|
182
|
+
load(id) {
|
|
183
|
+
if (id !== RESOLVED_ID) return;
|
|
184
|
+
if (!css) return "";
|
|
185
|
+
const cssPath = resolveCssImportPath(css, hostRoot);
|
|
186
|
+
return `@import ${JSON.stringify(cssPath)};`;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/vite-config.ts
|
|
192
|
+
const APP_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "app");
|
|
193
|
+
const mdxReactEntry = createRequire(import.meta.url).resolve("@mdx-js/react");
|
|
194
|
+
function createPreviewerViteConfig(options) {
|
|
195
|
+
const preview = previewPlugin(options.resolveFiles, {
|
|
196
|
+
css: options.css,
|
|
197
|
+
hostRoot: options.root
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
configFile: false,
|
|
201
|
+
root: APP_DIR,
|
|
202
|
+
publicDir: false,
|
|
203
|
+
define: { __PREVIEWER_TITLE__: JSON.stringify(options.title) },
|
|
204
|
+
resolve: { alias: { "@mdx-js/react": mdxReactEntry } },
|
|
205
|
+
server: {
|
|
206
|
+
port: 3040,
|
|
207
|
+
fs: { allow: [options.root, APP_DIR] }
|
|
208
|
+
},
|
|
209
|
+
plugins: [
|
|
210
|
+
...preview.plugins,
|
|
211
|
+
{
|
|
212
|
+
enforce: "pre",
|
|
213
|
+
...mdx({
|
|
214
|
+
remarkPlugins: [
|
|
215
|
+
remarkGfm,
|
|
216
|
+
remarkFrontmatter,
|
|
217
|
+
[remarkMdxFrontmatter, { name: "frontmatter" }],
|
|
218
|
+
...options.mdx?.remarkPlugins ?? []
|
|
219
|
+
],
|
|
220
|
+
rehypePlugins: [...options.mdx?.rehypePlugins ?? []],
|
|
221
|
+
providerImportSource: "@mdx-js/react",
|
|
222
|
+
format: "mdx",
|
|
223
|
+
mdxExtensions: [".mdx", ".md"],
|
|
224
|
+
include: /\.(md|mdx)$/
|
|
225
|
+
})
|
|
226
|
+
},
|
|
227
|
+
react({ include: /\.(jsx|tsx)$/ }),
|
|
228
|
+
...options.vite?.plugins ?? [],
|
|
229
|
+
previewerEntriesPlugin(options.root, options.resolveFiles),
|
|
230
|
+
previewerCssPlugin(options.root, options.css)
|
|
231
|
+
]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/server.ts
|
|
236
|
+
function buildViteConfig({ cwd, config, resolveFiles }) {
|
|
237
|
+
return createPreviewerViteConfig({
|
|
238
|
+
root: cwd,
|
|
239
|
+
title: config.title,
|
|
240
|
+
resolveFiles,
|
|
241
|
+
css: config.previewCss,
|
|
242
|
+
vite: config.vite,
|
|
243
|
+
mdx: config.mdx
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async function startDev(opts) {
|
|
247
|
+
const server = await createServer(buildViteConfig(opts));
|
|
248
|
+
await server.listen();
|
|
249
|
+
server.printUrls();
|
|
250
|
+
return server;
|
|
251
|
+
}
|
|
252
|
+
async function runBuild(opts) {
|
|
253
|
+
const viteConfig = buildViteConfig(opts);
|
|
254
|
+
await build({
|
|
255
|
+
...viteConfig,
|
|
256
|
+
build: {
|
|
257
|
+
...viteConfig.build,
|
|
258
|
+
outDir: resolve(opts.cwd, "dist")
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function runPreview(opts) {
|
|
263
|
+
const viteConfig = buildViteConfig(opts);
|
|
264
|
+
const server = await preview({
|
|
265
|
+
...viteConfig,
|
|
266
|
+
build: {
|
|
267
|
+
...viteConfig.build,
|
|
268
|
+
outDir: resolve(opts.cwd, "dist")
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
server.printUrls();
|
|
272
|
+
return server;
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export { escapeJsString as a, DEFAULT_GLOB as c, createPreviewerViteConfig as i, defineConfig as l, runPreview as n, extractPreviewBlocks as o, startDev as r, hasPreviewBlocks as s, runBuild as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@izumisy/md-react-preview",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "md-react-preview — component previewer powered by MDX",
|
|
5
|
+
"bin": {
|
|
6
|
+
"mrp": "./dist/cli.mjs"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/**",
|
|
10
|
+
"app/**"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"keywords": [],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@mdx-js/react": "^3.1.0",
|
|
24
|
+
"@mdx-js/rollup": "^3.1.0",
|
|
25
|
+
"@shikijs/langs": "^4.0.2",
|
|
26
|
+
"@shikijs/themes": "^4.0.2",
|
|
27
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
28
|
+
"c12": "^3.3.3",
|
|
29
|
+
"citty": "^0.2.1",
|
|
30
|
+
"fast-glob": "^3.3.3",
|
|
31
|
+
"react": "^19.2.4",
|
|
32
|
+
"react-dom": "^19.2.4",
|
|
33
|
+
"remark-frontmatter": "^5.0.0",
|
|
34
|
+
"remark-gfm": "^4.0.1",
|
|
35
|
+
"remark-mdx-frontmatter": "^5.2.0",
|
|
36
|
+
"shiki": "^4.0.2",
|
|
37
|
+
"vite": "^6.3.5",
|
|
38
|
+
"@izumisy/vite-plugin-react-preview": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22",
|
|
42
|
+
"@types/react": "^19",
|
|
43
|
+
"@types/react-dom": "^19",
|
|
44
|
+
"oxfmt": "^0.41.0",
|
|
45
|
+
"oxlint": "^1.56.0",
|
|
46
|
+
"tsdown": "^0.21.3",
|
|
47
|
+
"typescript": "~5.9.3",
|
|
48
|
+
"unified": "^11.0.5",
|
|
49
|
+
"vitest": "^4.1.2"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"dev": "tsdown --watch",
|
|
53
|
+
"build": "tsdown",
|
|
54
|
+
"type-check": "tsc --incremental",
|
|
55
|
+
"lint": "oxlint",
|
|
56
|
+
"fmt": "oxfmt --write src app/src",
|
|
57
|
+
"fmt:check": "oxfmt --check src app/src",
|
|
58
|
+
"test": "vitest run"
|
|
59
|
+
}
|
|
60
|
+
}
|