prev-cli 0.7.1 → 0.7.2

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,6 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
+ import path5 from "path";
5
6
 
6
7
  // src/vite/start.ts
7
8
  import { createServer as createServer2, build, preview } from "vite";
@@ -71,6 +72,32 @@ async function ensureCacheDir(rootDir) {
71
72
  import fg from "fast-glob";
72
73
  import { readFile } from "fs/promises";
73
74
  import path2 from "path";
75
+ function parseValue(value) {
76
+ const trimmed = value.trim();
77
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
78
+ const inner = trimmed.slice(1, -1);
79
+ if (inner.trim() === "")
80
+ return [];
81
+ return inner.split(",").map((item) => {
82
+ let v2 = item.trim();
83
+ if (v2.startsWith('"') && v2.endsWith('"') || v2.startsWith("'") && v2.endsWith("'")) {
84
+ v2 = v2.slice(1, -1);
85
+ }
86
+ return v2;
87
+ });
88
+ }
89
+ let v = trimmed;
90
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
91
+ v = v.slice(1, -1);
92
+ }
93
+ if (v === "true")
94
+ return true;
95
+ if (v === "false")
96
+ return false;
97
+ if (!isNaN(Number(v)) && v !== "")
98
+ return Number(v);
99
+ return v;
100
+ }
74
101
  function parseFrontmatter(content) {
75
102
  const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
76
103
  const match = content.match(frontmatterRegex);
@@ -86,18 +113,9 @@ function parseFrontmatter(content) {
86
113
  if (colonIndex === -1)
87
114
  continue;
88
115
  const key = line.slice(0, colonIndex).trim();
89
- let value = line.slice(colonIndex + 1).trim();
90
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
91
- value = value.slice(1, -1);
92
- }
93
- if (value === "true")
94
- value = true;
95
- else if (value === "false")
96
- value = false;
97
- else if (!isNaN(Number(value)) && value !== "")
98
- value = Number(value);
116
+ const rawValue = line.slice(colonIndex + 1);
99
117
  if (key) {
100
- frontmatter[key] = value;
118
+ frontmatter[key] = parseValue(rawValue);
101
119
  }
102
120
  }
103
121
  return { frontmatter, content: restContent };
@@ -602,7 +620,7 @@ var { values, positionals } = parseArgs({
602
620
  allowPositionals: true
603
621
  });
604
622
  var command = positionals[0] || "dev";
605
- var rootDir = values.cwd || positionals[1] || process.cwd();
623
+ var rootDir = path5.resolve(values.cwd || positionals[1] || ".");
606
624
  function printHelp() {
607
625
  console.log(`
608
626
  prev - Zero-config documentation site generator
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,146 @@
1
+ import React from 'react'
2
+
3
+ type FrontmatterValue = string | number | boolean | string[] | unknown
4
+
5
+ interface MetadataBlockProps {
6
+ frontmatter: Record<string, FrontmatterValue>
7
+ title?: string
8
+ description?: string
9
+ }
10
+
11
+ // Fields to skip (shown elsewhere or internal)
12
+ const SKIP_FIELDS = new Set(['title', 'description'])
13
+
14
+ // Fields with special icons
15
+ const FIELD_ICONS: Record<string, string> = {
16
+ date: '📅',
17
+ author: '👤',
18
+ tags: '🏷️',
19
+ status: '📌',
20
+ version: '📦',
21
+ repo: '🔗',
22
+ url: '🔗',
23
+ link: '🔗',
24
+ }
25
+
26
+ /**
27
+ * Detect the display type of a value
28
+ */
29
+ function detectType(key: string, value: FrontmatterValue): 'date' | 'url' | 'boolean' | 'array' | 'number' | 'string' {
30
+ if (Array.isArray(value)) return 'array'
31
+ if (typeof value === 'boolean') return 'boolean'
32
+ if (typeof value === 'number') return 'number'
33
+ if (typeof value !== 'string') return 'string'
34
+
35
+ // Check for URL
36
+ if (value.startsWith('http://') || value.startsWith('https://')) return 'url'
37
+
38
+ // Check for date patterns
39
+ if (key === 'date' || key.includes('date') || key.includes('Date')) {
40
+ // ISO date or YYYY-MM-DD
41
+ if (/^\d{4}-\d{2}-\d{2}/.test(value)) return 'date'
42
+ }
43
+
44
+ return 'string'
45
+ }
46
+
47
+ /**
48
+ * Format a date string nicely
49
+ */
50
+ function formatDate(value: string): string {
51
+ try {
52
+ const date = new Date(value)
53
+ if (isNaN(date.getTime())) return value
54
+ return date.toLocaleDateString('en-US', {
55
+ year: 'numeric',
56
+ month: 'short',
57
+ day: 'numeric'
58
+ })
59
+ } catch {
60
+ return value
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Render a single metadata value based on its type
66
+ */
67
+ function renderValue(key: string, value: FrontmatterValue): React.ReactNode {
68
+ const type = detectType(key, value)
69
+
70
+ switch (type) {
71
+ case 'array':
72
+ return (
73
+ <span className="metadata-array">
74
+ {(value as string[]).map((item, i) => (
75
+ <span key={i} className="metadata-chip">{item}</span>
76
+ ))}
77
+ </span>
78
+ )
79
+
80
+ case 'boolean':
81
+ return (
82
+ <span className={`metadata-boolean ${value ? 'is-true' : 'is-false'}`}>
83
+ {value ? '✓' : '✗'}
84
+ </span>
85
+ )
86
+
87
+ case 'url':
88
+ return (
89
+ <a href={value as string} className="metadata-link" target="_blank" rel="noopener noreferrer">
90
+ {(value as string).replace(/^https?:\/\//, '').split('/')[0]}
91
+ </a>
92
+ )
93
+
94
+ case 'date':
95
+ return <span className="metadata-date">{formatDate(value as string)}</span>
96
+
97
+ case 'number':
98
+ return <span className="metadata-number">{(value as number).toLocaleString()}</span>
99
+
100
+ default:
101
+ return <span className="metadata-string">{String(value)}</span>
102
+ }
103
+ }
104
+
105
+ export function MetadataBlock({ frontmatter, title, description }: MetadataBlockProps) {
106
+ // Filter out skipped fields and empty values
107
+ const fields = Object.entries(frontmatter).filter(
108
+ ([key, value]) => !SKIP_FIELDS.has(key) && value !== undefined && value !== null && value !== ''
109
+ )
110
+
111
+ // Check for draft status
112
+ const isDraft = frontmatter.draft === true
113
+
114
+ // Only show title/description if they came from frontmatter (not extracted from H1)
115
+ const hasExplicitTitle = 'title' in frontmatter && frontmatter.title
116
+ const hasExplicitDescription = 'description' in frontmatter && frontmatter.description
117
+
118
+ // If no explicit metadata and no other fields, don't render the block
119
+ if (fields.length === 0 && !hasExplicitTitle && !hasExplicitDescription && !isDraft) {
120
+ return null
121
+ }
122
+
123
+ return (
124
+ <div className="metadata-block">
125
+ {hasExplicitTitle && <h1 className="metadata-title">{title}</h1>}
126
+ {hasExplicitDescription && <p className="metadata-description">{description}</p>}
127
+
128
+ {(fields.length > 0 || isDraft) && (
129
+ <div className="metadata-fields">
130
+ {isDraft && (
131
+ <span className="metadata-field metadata-draft">
132
+ <span className="metadata-chip is-draft">Draft</span>
133
+ </span>
134
+ )}
135
+ {fields.filter(([key]) => key !== 'draft').map(([key, value]) => (
136
+ <span key={key} className="metadata-field">
137
+ {FIELD_ICONS[key] && <span className="metadata-icon">{FIELD_ICONS[key]}</span>}
138
+ <span className="metadata-key">{key}:</span>
139
+ {renderValue(key, value)}
140
+ </span>
141
+ ))}
142
+ </div>
143
+ )}
144
+ </div>
145
+ )
146
+ }
@@ -10,6 +10,7 @@ import {
10
10
  import { pages, sidebar } from 'virtual:prev-pages'
11
11
  import { useDiagrams } from './diagrams'
12
12
  import { Layout } from './Layout'
13
+ import { MetadataBlock } from './MetadataBlock'
13
14
  import './styles.css'
14
15
 
15
16
  // PageTree types (simplified from fumadocs-core)
@@ -61,10 +62,27 @@ function getPageComponent(file: string): React.ComponentType | null {
61
62
  return mod?.default || null
62
63
  }
63
64
 
65
+ interface PageMeta {
66
+ title?: string
67
+ description?: string
68
+ frontmatter?: Record<string, unknown>
69
+ }
70
+
64
71
  // Page wrapper that renders diagrams after content loads
65
- function PageWrapper({ Component }: { Component: React.ComponentType }) {
72
+ function PageWrapper({ Component, meta }: { Component: React.ComponentType; meta: PageMeta }) {
66
73
  useDiagrams()
67
- return <Component />
74
+ return (
75
+ <>
76
+ {meta.frontmatter && Object.keys(meta.frontmatter).length > 0 && (
77
+ <MetadataBlock
78
+ frontmatter={meta.frontmatter}
79
+ title={meta.title}
80
+ description={meta.description}
81
+ />
82
+ )}
83
+ <Component />
84
+ </>
85
+ )
68
86
  }
69
87
 
70
88
  // Root layout with custom lightweight Layout
@@ -86,12 +104,17 @@ const rootRoute = createRootRoute({
86
104
  })
87
105
 
88
106
  // Create routes from pages
89
- const pageRoutes = pages.map((page: { route: string; file: string }) => {
107
+ const pageRoutes = pages.map((page: { route: string; file: string; title?: string; description?: string; frontmatter?: Record<string, unknown> }) => {
90
108
  const Component = getPageComponent(page.file)
109
+ const meta: PageMeta = {
110
+ title: page.title,
111
+ description: page.description,
112
+ frontmatter: page.frontmatter,
113
+ }
91
114
  return createRoute({
92
115
  getParentRoute: () => rootRoute,
93
116
  path: page.route === '/' ? '/' : page.route,
94
- component: Component ? () => <PageWrapper Component={Component} /> : () => null,
117
+ component: Component ? () => <PageWrapper Component={Component} meta={meta} /> : () => null,
95
118
  })
96
119
  })
97
120
 
@@ -591,3 +591,109 @@ body {
591
591
  background: var(--fd-accent);
592
592
  color: var(--fd-accent-foreground);
593
593
  }
594
+
595
+ /* ===========================================
596
+ Metadata Block Styles
597
+ =========================================== */
598
+
599
+ .metadata-block {
600
+ margin-bottom: 2rem;
601
+ padding-bottom: 1.5rem;
602
+ border-bottom: 1px solid var(--fd-border);
603
+ }
604
+
605
+ .metadata-title {
606
+ font-size: 2.25rem;
607
+ font-weight: 700;
608
+ margin: 0 0 0.5rem 0;
609
+ line-height: 1.2;
610
+ letter-spacing: -0.025em;
611
+ color: var(--fd-foreground);
612
+ }
613
+
614
+ .metadata-description {
615
+ font-size: 1.125rem;
616
+ color: var(--fd-muted-foreground);
617
+ margin: 0 0 1rem 0;
618
+ line-height: 1.5;
619
+ }
620
+
621
+ .metadata-fields {
622
+ display: flex;
623
+ flex-wrap: wrap;
624
+ gap: 0.75rem;
625
+ align-items: center;
626
+ }
627
+
628
+ .metadata-field {
629
+ display: inline-flex;
630
+ align-items: center;
631
+ gap: 0.35rem;
632
+ font-size: 0.875rem;
633
+ color: var(--fd-muted-foreground);
634
+ }
635
+
636
+ .metadata-icon {
637
+ font-size: 0.875rem;
638
+ }
639
+
640
+ .metadata-key {
641
+ font-weight: 500;
642
+ color: var(--fd-foreground);
643
+ }
644
+
645
+ .metadata-chip {
646
+ display: inline-block;
647
+ padding: 0.15rem 0.5rem;
648
+ background: var(--fd-muted);
649
+ border-radius: 0.25rem;
650
+ font-size: 0.8rem;
651
+ color: var(--fd-foreground);
652
+ }
653
+
654
+ .metadata-chip.is-draft {
655
+ background: oklch(0.75 0.15 50);
656
+ color: oklch(0.25 0.05 50);
657
+ }
658
+
659
+ .dark .metadata-chip.is-draft {
660
+ background: oklch(0.45 0.12 50);
661
+ color: oklch(0.95 0.02 50);
662
+ }
663
+
664
+ .metadata-array {
665
+ display: inline-flex;
666
+ flex-wrap: wrap;
667
+ gap: 0.35rem;
668
+ }
669
+
670
+ .metadata-boolean {
671
+ font-weight: 600;
672
+ }
673
+
674
+ .metadata-boolean.is-true {
675
+ color: oklch(0.55 0.15 145);
676
+ }
677
+
678
+ .metadata-boolean.is-false {
679
+ color: oklch(0.55 0.15 25);
680
+ }
681
+
682
+ .metadata-link {
683
+ color: var(--fd-primary);
684
+ text-decoration: underline;
685
+ text-decoration-color: var(--fd-border);
686
+ text-underline-offset: 2px;
687
+ }
688
+
689
+ .metadata-link:hover {
690
+ text-decoration-color: var(--fd-primary);
691
+ }
692
+
693
+ .metadata-date {
694
+ font-variant-numeric: tabular-nums;
695
+ }
696
+
697
+ .metadata-number {
698
+ font-variant-numeric: tabular-nums;
699
+ }