prev-cli 0.9.1 → 0.10.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/dist/cli.js +136 -28
- package/dist/vite/plugins/previews-plugin.d.ts +2 -0
- package/dist/vite/previews.d.ts +6 -0
- package/package.json +2 -1
- package/src/theme/Preview.tsx +105 -0
- package/src/theme/entry.tsx +4 -2
- package/src/theme/mdx-components.tsx +5 -0
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
-
import
|
|
5
|
+
import path7 from "path";
|
|
6
6
|
|
|
7
7
|
// src/vite/start.ts
|
|
8
8
|
import { createServer as createServer2, build, preview } from "vite";
|
|
@@ -13,9 +13,9 @@ import react from "@vitejs/plugin-react-swc";
|
|
|
13
13
|
import mdx from "@mdx-js/rollup";
|
|
14
14
|
import remarkGfm from "remark-gfm";
|
|
15
15
|
import rehypeHighlight from "rehype-highlight";
|
|
16
|
-
import
|
|
16
|
+
import path6 from "path";
|
|
17
17
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
18
|
-
import { existsSync as
|
|
18
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
19
19
|
|
|
20
20
|
// src/utils/cache.ts
|
|
21
21
|
import { createHash } from "crypto";
|
|
@@ -324,10 +324,15 @@ function entryPlugin(rootDir) {
|
|
|
324
324
|
if (command === "build" && rootDir) {
|
|
325
325
|
tempHtmlPath = path3.join(rootDir, "index.html");
|
|
326
326
|
writeFileSync(tempHtmlPath, getHtml(entryPath, true));
|
|
327
|
+
const existingInput = config.build?.rollupOptions?.input || {};
|
|
328
|
+
const inputObj = typeof existingInput === "string" ? { _original: existingInput } : Array.isArray(existingInput) ? Object.fromEntries(existingInput.map((f, i) => [`entry${i}`, f])) : existingInput;
|
|
327
329
|
return {
|
|
328
330
|
build: {
|
|
329
331
|
rollupOptions: {
|
|
330
|
-
input:
|
|
332
|
+
input: {
|
|
333
|
+
...inputObj,
|
|
334
|
+
main: tempHtmlPath
|
|
335
|
+
}
|
|
331
336
|
}
|
|
332
337
|
}
|
|
333
338
|
};
|
|
@@ -362,6 +367,73 @@ function entryPlugin(rootDir) {
|
|
|
362
367
|
};
|
|
363
368
|
}
|
|
364
369
|
|
|
370
|
+
// src/vite/previews.ts
|
|
371
|
+
import fg2 from "fast-glob";
|
|
372
|
+
import path4 from "path";
|
|
373
|
+
import { existsSync as existsSync2 } from "fs";
|
|
374
|
+
async function scanPreviews(rootDir) {
|
|
375
|
+
const previewsDir = path4.join(rootDir, "previews");
|
|
376
|
+
if (!existsSync2(previewsDir)) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const htmlFiles = await fg2.glob("**/index.html", {
|
|
380
|
+
cwd: previewsDir,
|
|
381
|
+
ignore: ["node_modules/**"]
|
|
382
|
+
});
|
|
383
|
+
return htmlFiles.map((file) => {
|
|
384
|
+
const dir = path4.dirname(file);
|
|
385
|
+
const name = dir === "." ? path4.basename(path4.dirname(path4.join(previewsDir, file))) : dir;
|
|
386
|
+
return {
|
|
387
|
+
name,
|
|
388
|
+
route: `/_preview/${name}`,
|
|
389
|
+
htmlPath: path4.join(previewsDir, file)
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/vite/plugins/previews-plugin.ts
|
|
395
|
+
import { existsSync as existsSync3, renameSync, mkdirSync, rmSync } from "fs";
|
|
396
|
+
import path5 from "path";
|
|
397
|
+
var VIRTUAL_MODULE_ID2 = "virtual:prev-previews";
|
|
398
|
+
var RESOLVED_VIRTUAL_MODULE_ID2 = "\x00" + VIRTUAL_MODULE_ID2;
|
|
399
|
+
function previewsPlugin(rootDir) {
|
|
400
|
+
return {
|
|
401
|
+
name: "prev-previews",
|
|
402
|
+
resolveId(id) {
|
|
403
|
+
if (id === VIRTUAL_MODULE_ID2) {
|
|
404
|
+
return RESOLVED_VIRTUAL_MODULE_ID2;
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async load(id) {
|
|
408
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID2) {
|
|
409
|
+
const previews = await scanPreviews(rootDir);
|
|
410
|
+
return `export const previews = ${JSON.stringify(previews)};`;
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
handleHotUpdate({ file, server }) {
|
|
414
|
+
if (file.includes("/previews/") && file.endsWith(".html")) {
|
|
415
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID2);
|
|
416
|
+
if (mod) {
|
|
417
|
+
server.moduleGraph.invalidateModule(mod);
|
|
418
|
+
return [mod];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
closeBundle() {
|
|
423
|
+
const distDir = path5.join(rootDir, "dist");
|
|
424
|
+
const previewsDir = path5.join(distDir, "previews");
|
|
425
|
+
const targetDir = path5.join(distDir, "_preview");
|
|
426
|
+
if (existsSync3(previewsDir)) {
|
|
427
|
+
mkdirSync(path5.dirname(targetDir), { recursive: true });
|
|
428
|
+
if (existsSync3(targetDir)) {
|
|
429
|
+
rmSync(targetDir, { recursive: true });
|
|
430
|
+
}
|
|
431
|
+
renameSync(previewsDir, targetDir);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
365
437
|
// src/vite/config.ts
|
|
366
438
|
function createFriendlyLogger() {
|
|
367
439
|
const logger = createLogger("info", { allowClearScreen: false });
|
|
@@ -417,10 +489,10 @@ function createFriendlyLogger() {
|
|
|
417
489
|
};
|
|
418
490
|
}
|
|
419
491
|
function findCliRoot2() {
|
|
420
|
-
let dir =
|
|
492
|
+
let dir = path6.dirname(fileURLToPath2(import.meta.url));
|
|
421
493
|
for (let i = 0;i < 10; i++) {
|
|
422
|
-
const pkgPath =
|
|
423
|
-
if (
|
|
494
|
+
const pkgPath = path6.join(dir, "package.json");
|
|
495
|
+
if (existsSync4(pkgPath)) {
|
|
424
496
|
try {
|
|
425
497
|
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
426
498
|
if (pkg.name === "prev-cli") {
|
|
@@ -428,24 +500,24 @@ function findCliRoot2() {
|
|
|
428
500
|
}
|
|
429
501
|
} catch {}
|
|
430
502
|
}
|
|
431
|
-
const parent =
|
|
503
|
+
const parent = path6.dirname(dir);
|
|
432
504
|
if (parent === dir)
|
|
433
505
|
break;
|
|
434
506
|
dir = parent;
|
|
435
507
|
}
|
|
436
|
-
return
|
|
508
|
+
return path6.dirname(path6.dirname(fileURLToPath2(import.meta.url)));
|
|
437
509
|
}
|
|
438
510
|
function findNodeModules(cliRoot2) {
|
|
439
|
-
const localNodeModules =
|
|
440
|
-
if (
|
|
511
|
+
const localNodeModules = path6.join(cliRoot2, "node_modules");
|
|
512
|
+
if (existsSync4(path6.join(localNodeModules, "react"))) {
|
|
441
513
|
return localNodeModules;
|
|
442
514
|
}
|
|
443
515
|
let dir = cliRoot2;
|
|
444
516
|
for (let i = 0;i < 10; i++) {
|
|
445
|
-
const parent =
|
|
517
|
+
const parent = path6.dirname(dir);
|
|
446
518
|
if (parent === dir)
|
|
447
519
|
break;
|
|
448
|
-
if (
|
|
520
|
+
if (path6.basename(parent) === "node_modules" && existsSync4(path6.join(parent, "react"))) {
|
|
449
521
|
return parent;
|
|
450
522
|
}
|
|
451
523
|
dir = parent;
|
|
@@ -454,10 +526,12 @@ function findNodeModules(cliRoot2) {
|
|
|
454
526
|
}
|
|
455
527
|
var cliRoot2 = findCliRoot2();
|
|
456
528
|
var cliNodeModules = findNodeModules(cliRoot2);
|
|
457
|
-
var srcRoot2 =
|
|
529
|
+
var srcRoot2 = path6.join(cliRoot2, "src");
|
|
458
530
|
async function createViteConfig(options) {
|
|
459
531
|
const { rootDir, mode, port, include } = options;
|
|
460
532
|
const cacheDir = await ensureCacheDir(rootDir);
|
|
533
|
+
const previews = await scanPreviews(rootDir);
|
|
534
|
+
const previewInputs = Object.fromEntries(previews.map((p) => [`_preview/${p.name}`, p.htmlPath]));
|
|
461
535
|
return {
|
|
462
536
|
root: rootDir,
|
|
463
537
|
mode,
|
|
@@ -471,18 +545,46 @@ async function createViteConfig(options) {
|
|
|
471
545
|
}),
|
|
472
546
|
react(),
|
|
473
547
|
pagesPlugin(rootDir, { include }),
|
|
474
|
-
entryPlugin(rootDir)
|
|
548
|
+
entryPlugin(rootDir),
|
|
549
|
+
previewsPlugin(rootDir),
|
|
550
|
+
{
|
|
551
|
+
name: "prev-preview-server",
|
|
552
|
+
configureServer(server) {
|
|
553
|
+
server.middlewares.use(async (req, res, next) => {
|
|
554
|
+
if (req.url?.startsWith("/_preview/")) {
|
|
555
|
+
const previewName = decodeURIComponent(req.url.slice("/_preview/".length).split("?")[0]);
|
|
556
|
+
const previewsDir = path6.join(rootDir, "previews");
|
|
557
|
+
const htmlPath = path6.resolve(previewsDir, previewName, "index.html");
|
|
558
|
+
if (!htmlPath.startsWith(previewsDir)) {
|
|
559
|
+
return next();
|
|
560
|
+
}
|
|
561
|
+
if (existsSync4(htmlPath)) {
|
|
562
|
+
try {
|
|
563
|
+
const html = await server.transformIndexHtml(req.url, readFileSync2(htmlPath, "utf-8"));
|
|
564
|
+
res.setHeader("Content-Type", "text/html");
|
|
565
|
+
res.end(html);
|
|
566
|
+
return;
|
|
567
|
+
} catch (err) {
|
|
568
|
+
console.error("Error serving preview:", err);
|
|
569
|
+
return next();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
next();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
475
577
|
],
|
|
476
578
|
resolve: {
|
|
477
579
|
alias: {
|
|
478
|
-
"@prev/ui":
|
|
479
|
-
"@prev/theme":
|
|
480
|
-
react:
|
|
481
|
-
"react-dom":
|
|
482
|
-
"@tanstack/react-router":
|
|
483
|
-
mermaid:
|
|
484
|
-
dayjs:
|
|
485
|
-
"@terrastruct/d2":
|
|
580
|
+
"@prev/ui": path6.join(srcRoot2, "ui"),
|
|
581
|
+
"@prev/theme": path6.join(srcRoot2, "theme"),
|
|
582
|
+
react: path6.join(cliNodeModules, "react"),
|
|
583
|
+
"react-dom": path6.join(cliNodeModules, "react-dom"),
|
|
584
|
+
"@tanstack/react-router": path6.join(cliNodeModules, "@tanstack/react-router"),
|
|
585
|
+
mermaid: path6.join(cliNodeModules, "mermaid"),
|
|
586
|
+
dayjs: path6.join(cliNodeModules, "dayjs"),
|
|
587
|
+
"@terrastruct/d2": path6.join(cliNodeModules, "@terrastruct/d2")
|
|
486
588
|
},
|
|
487
589
|
dedupe: [
|
|
488
590
|
"react",
|
|
@@ -515,8 +617,8 @@ async function createViteConfig(options) {
|
|
|
515
617
|
},
|
|
516
618
|
warmup: {
|
|
517
619
|
clientFiles: [
|
|
518
|
-
|
|
519
|
-
|
|
620
|
+
path6.join(srcRoot2, "theme/entry.tsx"),
|
|
621
|
+
path6.join(srcRoot2, "theme/styles.css")
|
|
520
622
|
]
|
|
521
623
|
}
|
|
522
624
|
},
|
|
@@ -525,9 +627,15 @@ async function createViteConfig(options) {
|
|
|
525
627
|
strictPort: false
|
|
526
628
|
},
|
|
527
629
|
build: {
|
|
528
|
-
outDir:
|
|
630
|
+
outDir: path6.join(rootDir, "dist"),
|
|
529
631
|
reportCompressedSize: false,
|
|
530
|
-
chunkSizeWarningLimit: 1e4
|
|
632
|
+
chunkSizeWarningLimit: 1e4,
|
|
633
|
+
rollupOptions: {
|
|
634
|
+
input: {
|
|
635
|
+
main: path6.join(srcRoot2, "theme/index.html"),
|
|
636
|
+
...previewInputs
|
|
637
|
+
}
|
|
638
|
+
}
|
|
531
639
|
}
|
|
532
640
|
};
|
|
533
641
|
}
|
|
@@ -641,7 +749,7 @@ var { values, positionals } = parseArgs({
|
|
|
641
749
|
allowPositionals: true
|
|
642
750
|
});
|
|
643
751
|
var command = positionals[0] || "dev";
|
|
644
|
-
var rootDir =
|
|
752
|
+
var rootDir = path7.resolve(values.cwd || positionals[1] || ".");
|
|
645
753
|
function printHelp() {
|
|
646
754
|
console.log(`
|
|
647
755
|
prev - Zero-config documentation site generator
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prev-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Transform MDX directories into beautiful documentation websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"test:all": "bun run test && bun run test:integration"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@mdx-js/react": "^3.1.1",
|
|
43
44
|
"@mdx-js/rollup": "^3.0.0",
|
|
44
45
|
"@tailwindcss/vite": "^4.0.0",
|
|
45
46
|
"@tanstack/react-router": "^1.145.7",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { Maximize2, Minimize2, ExternalLink } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface PreviewProps {
|
|
5
|
+
src: string
|
|
6
|
+
height?: string | number
|
|
7
|
+
title?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Preview({ src, height = 400, title }: PreviewProps) {
|
|
11
|
+
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
12
|
+
const previewUrl = `/_preview/${src}`
|
|
13
|
+
const displayTitle = title || src
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isFullscreen) return
|
|
17
|
+
|
|
18
|
+
// Lock body scroll
|
|
19
|
+
document.body.style.overflow = 'hidden'
|
|
20
|
+
|
|
21
|
+
// Handle escape key
|
|
22
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
23
|
+
if (e.key === 'Escape') {
|
|
24
|
+
setIsFullscreen(false)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
document.body.style.overflow = ''
|
|
31
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
32
|
+
}
|
|
33
|
+
}, [isFullscreen])
|
|
34
|
+
|
|
35
|
+
if (isFullscreen) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="fixed inset-0 z-50 bg-white dark:bg-zinc-900">
|
|
38
|
+
<div className="flex items-center justify-between p-2 border-b border-zinc-200 dark:border-zinc-700">
|
|
39
|
+
<span className="text-sm font-medium">{displayTitle}</span>
|
|
40
|
+
<div className="flex gap-2">
|
|
41
|
+
<a
|
|
42
|
+
href={previewUrl}
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noopener noreferrer"
|
|
45
|
+
className="p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded"
|
|
46
|
+
title="Open in new tab"
|
|
47
|
+
aria-label="Open in new tab"
|
|
48
|
+
>
|
|
49
|
+
<ExternalLink className="w-4 h-4" />
|
|
50
|
+
</a>
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => setIsFullscreen(false)}
|
|
53
|
+
className="p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded"
|
|
54
|
+
title="Exit fullscreen"
|
|
55
|
+
aria-label="Exit fullscreen"
|
|
56
|
+
>
|
|
57
|
+
<Minimize2 className="w-4 h-4" />
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<iframe
|
|
62
|
+
src={previewUrl}
|
|
63
|
+
className="w-full h-[calc(100vh-49px)]"
|
|
64
|
+
title={displayTitle}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="my-4 border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden">
|
|
72
|
+
<div className="flex items-center justify-between px-3 py-2 bg-zinc-50 dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700">
|
|
73
|
+
<span className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
|
74
|
+
{displayTitle}
|
|
75
|
+
</span>
|
|
76
|
+
<div className="flex gap-1">
|
|
77
|
+
<a
|
|
78
|
+
href={previewUrl}
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noopener noreferrer"
|
|
81
|
+
className="p-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded"
|
|
82
|
+
title="Open in new tab"
|
|
83
|
+
aria-label="Open in new tab"
|
|
84
|
+
>
|
|
85
|
+
<ExternalLink className="w-4 h-4 text-zinc-500" />
|
|
86
|
+
</a>
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => setIsFullscreen(true)}
|
|
89
|
+
className="p-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded"
|
|
90
|
+
title="Fullscreen"
|
|
91
|
+
aria-label="Enter fullscreen"
|
|
92
|
+
>
|
|
93
|
+
<Maximize2 className="w-4 h-4 text-zinc-500" />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<iframe
|
|
98
|
+
src={previewUrl}
|
|
99
|
+
style={{ height: typeof height === 'number' ? `${height}px` : height }}
|
|
100
|
+
className="w-full"
|
|
101
|
+
title={displayTitle}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|
package/src/theme/entry.tsx
CHANGED
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
createRoute,
|
|
8
8
|
Outlet,
|
|
9
9
|
} from '@tanstack/react-router'
|
|
10
|
+
import { MDXProvider } from '@mdx-js/react'
|
|
10
11
|
import { pages, sidebar } from 'virtual:prev-pages'
|
|
11
12
|
import { useDiagrams } from './diagrams'
|
|
12
13
|
import { Layout } from './Layout'
|
|
13
14
|
import { MetadataBlock } from './MetadataBlock'
|
|
15
|
+
import { mdxComponents } from './mdx-components'
|
|
14
16
|
import './styles.css'
|
|
15
17
|
|
|
16
18
|
// PageTree types (simplified from fumadocs-core)
|
|
@@ -72,12 +74,12 @@ interface PageMeta {
|
|
|
72
74
|
function PageWrapper({ Component, meta }: { Component: React.ComponentType; meta: PageMeta }) {
|
|
73
75
|
useDiagrams()
|
|
74
76
|
return (
|
|
75
|
-
|
|
77
|
+
<MDXProvider components={mdxComponents}>
|
|
76
78
|
{meta.frontmatter && Object.keys(meta.frontmatter).length > 0 && (
|
|
77
79
|
<MetadataBlock frontmatter={meta.frontmatter} />
|
|
78
80
|
)}
|
|
79
81
|
<Component />
|
|
80
|
-
|
|
82
|
+
</MDXProvider>
|
|
81
83
|
)
|
|
82
84
|
}
|
|
83
85
|
|