prev-cli 0.9.0 → 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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
- import path5 from "path";
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 path4 from "path";
16
+ import path6 from "path";
17
17
  import { fileURLToPath as fileURLToPath2 } from "url";
18
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
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: tempHtmlPath
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 = path4.dirname(fileURLToPath2(import.meta.url));
492
+ let dir = path6.dirname(fileURLToPath2(import.meta.url));
421
493
  for (let i = 0;i < 10; i++) {
422
- const pkgPath = path4.join(dir, "package.json");
423
- if (existsSync2(pkgPath)) {
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 = path4.dirname(dir);
503
+ const parent = path6.dirname(dir);
432
504
  if (parent === dir)
433
505
  break;
434
506
  dir = parent;
435
507
  }
436
- return path4.dirname(path4.dirname(fileURLToPath2(import.meta.url)));
508
+ return path6.dirname(path6.dirname(fileURLToPath2(import.meta.url)));
437
509
  }
438
510
  function findNodeModules(cliRoot2) {
439
- const localNodeModules = path4.join(cliRoot2, "node_modules");
440
- if (existsSync2(path4.join(localNodeModules, "react"))) {
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 = path4.dirname(dir);
517
+ const parent = path6.dirname(dir);
446
518
  if (parent === dir)
447
519
  break;
448
- if (path4.basename(parent) === "node_modules" && existsSync2(path4.join(parent, "react"))) {
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 = path4.join(cliRoot2, "src");
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": path4.join(srcRoot2, "ui"),
479
- "@prev/theme": path4.join(srcRoot2, "theme"),
480
- react: path4.join(cliNodeModules, "react"),
481
- "react-dom": path4.join(cliNodeModules, "react-dom"),
482
- "@tanstack/react-router": path4.join(cliNodeModules, "@tanstack/react-router"),
483
- mermaid: path4.join(cliNodeModules, "mermaid"),
484
- dayjs: path4.join(cliNodeModules, "dayjs"),
485
- "@terrastruct/d2": path4.join(cliNodeModules, "@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
- path4.join(srcRoot2, "theme/entry.tsx"),
519
- path4.join(srcRoot2, "theme/styles.css")
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: path4.join(rootDir, "dist"),
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 = path5.resolve(values.cwd || positionals[1] || ".");
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
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function previewsPlugin(rootDir: string): Plugin;
@@ -0,0 +1,6 @@
1
+ export interface Preview {
2
+ name: string;
3
+ route: string;
4
+ htmlPath: string;
5
+ }
6
+ export declare function scanPreviews(rootDir: string): Promise<Preview[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.9.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
+ }
@@ -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)
@@ -54,8 +56,8 @@ function convertToPageTree(items: any[]): PageTree.Root {
54
56
  }
55
57
  }
56
58
 
57
- // Dynamic imports for MDX pages
58
- const pageModules = import.meta.glob('/**/*.{md,mdx}', { eager: true })
59
+ // Dynamic imports for MDX pages (include dot directories for --include flag)
60
+ const pageModules = import.meta.glob(['/**/*.{md,mdx}', '/.*/**/*.{md,mdx}'], { eager: true })
59
61
 
60
62
  function getPageComponent(file: string): React.ComponentType | null {
61
63
  const mod = pageModules[`/${file}`] as { default: React.ComponentType } | undefined
@@ -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
 
@@ -0,0 +1,5 @@
1
+ import { Preview } from './Preview'
2
+
3
+ export const mdxComponents = {
4
+ Preview,
5
+ }