prev-cli 0.4.0 → 0.6.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
@@ -71,45 +71,103 @@ async function ensureCacheDir(rootDir) {
71
71
  import fg from "fast-glob";
72
72
  import { readFile } from "fs/promises";
73
73
  import path2 from "path";
74
+ function parseFrontmatter(content) {
75
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
76
+ const match = content.match(frontmatterRegex);
77
+ if (!match) {
78
+ return { frontmatter: {}, content };
79
+ }
80
+ const frontmatterStr = match[1];
81
+ const restContent = content.slice(match[0].length);
82
+ const frontmatter = {};
83
+ for (const line of frontmatterStr.split(`
84
+ `)) {
85
+ const colonIndex = line.indexOf(":");
86
+ if (colonIndex === -1)
87
+ continue;
88
+ 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);
99
+ if (key) {
100
+ frontmatter[key] = value;
101
+ }
102
+ }
103
+ return { frontmatter, content: restContent };
104
+ }
105
+ function isIndexFile(basename) {
106
+ const lower = basename.toLowerCase();
107
+ return lower === "index" || lower === "readme";
108
+ }
74
109
  function fileToRoute(file) {
75
110
  const withoutExt = file.replace(/\.mdx?$/, "");
76
- if (withoutExt === "index") {
77
- return "/";
78
- }
79
- if (withoutExt.endsWith("/index")) {
80
- return "/" + withoutExt.slice(0, -6);
111
+ const basename = path2.basename(withoutExt).toLowerCase();
112
+ if (basename === "index" || basename === "readme") {
113
+ const dir = path2.dirname(withoutExt);
114
+ if (dir === ".") {
115
+ return "/";
116
+ }
117
+ return "/" + dir;
81
118
  }
82
119
  return "/" + withoutExt;
83
120
  }
84
121
  async function scanPages(rootDir) {
85
- const files = await fg.glob("**/*.mdx", {
122
+ const files = await fg.glob("**/*.{md,mdx}", {
86
123
  cwd: rootDir,
87
124
  ignore: ["node_modules/**", "dist/**", ".cache/**"]
88
125
  });
89
- const pages = [];
126
+ const routeMap = new Map;
90
127
  for (const file of files) {
128
+ const route = fileToRoute(file);
129
+ const basename = path2.basename(file, path2.extname(file)).toLowerCase();
130
+ const priority = basename === "index" ? 1 : basename === "readme" ? 2 : 0;
131
+ const existing = routeMap.get(route);
132
+ if (!existing || priority > 0 && priority < existing.priority) {
133
+ routeMap.set(route, { file, priority });
134
+ }
135
+ }
136
+ const pages = [];
137
+ for (const { file } of routeMap.values()) {
91
138
  const fullPath = path2.join(rootDir, file);
92
- const content = await readFile(fullPath, "utf-8");
93
- const title = extractTitle(content, file);
94
- pages.push({
139
+ const rawContent = await readFile(fullPath, "utf-8");
140
+ const { frontmatter, content } = parseFrontmatter(rawContent);
141
+ const title = extractTitle(content, file, frontmatter);
142
+ const page = {
95
143
  route: fileToRoute(file),
96
144
  title,
97
145
  file
98
- });
146
+ };
147
+ if (frontmatter.description) {
148
+ page.description = frontmatter.description;
149
+ }
150
+ if (Object.keys(frontmatter).length > 0) {
151
+ page.frontmatter = frontmatter;
152
+ }
153
+ pages.push(page);
99
154
  }
100
155
  return pages.sort((a, b) => a.route.localeCompare(b.route));
101
156
  }
102
- function extractTitle(content, file) {
157
+ function extractTitle(content, file, frontmatter) {
158
+ if (frontmatter?.title && typeof frontmatter.title === "string") {
159
+ return frontmatter.title;
160
+ }
103
161
  const match = content.match(/^#\s+(.+)$/m);
104
162
  if (match) {
105
163
  return match[1].trim();
106
164
  }
107
- const basename = path2.basename(file, path2.extname(file));
108
- if (basename === "index") {
165
+ const basename = path2.basename(file, path2.extname(file)).toLowerCase();
166
+ if (isIndexFile(basename)) {
109
167
  const dirname = path2.dirname(file);
110
168
  return dirname === "." ? "Home" : capitalize(path2.basename(dirname));
111
169
  }
112
- return capitalize(basename);
170
+ return capitalize(path2.basename(file, path2.extname(file)));
113
171
  }
114
172
  function capitalize(str) {
115
173
  return str.charAt(0).toUpperCase() + str.slice(1).replace(/-/g, " ");
@@ -164,7 +222,7 @@ function pagesPlugin(rootDir) {
164
222
  }
165
223
  },
166
224
  handleHotUpdate({ file, server }) {
167
- if (file.endsWith(".mdx")) {
225
+ if (file.endsWith(".mdx") || file.endsWith(".md")) {
168
226
  const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
169
227
  if (mod) {
170
228
  server.moduleGraph.invalidateModule(mod);
@@ -482,7 +540,7 @@ function printWelcome(type) {
482
540
  }
483
541
  function printReady() {
484
542
  console.log();
485
- console.log(" Edit your .mdx files and see changes instantly.");
543
+ console.log(" Edit your .md/.mdx files and see changes instantly.");
486
544
  console.log(" Press Ctrl+C to stop.");
487
545
  console.log();
488
546
  }
@@ -537,12 +595,13 @@ var { values, positionals } = parseArgs({
537
595
  options: {
538
596
  port: { type: "string", short: "p" },
539
597
  days: { type: "string", short: "d" },
598
+ cwd: { type: "string", short: "c" },
540
599
  help: { type: "boolean", short: "h" }
541
600
  },
542
601
  allowPositionals: true
543
602
  });
544
603
  var command = positionals[0] || "dev";
545
- var rootDir = positionals[1] || process.cwd();
604
+ var rootDir = values.cwd || positionals[1] || process.cwd();
546
605
  function printHelp() {
547
606
  console.log(`
548
607
  prev - Zero-config documentation site generator
@@ -557,6 +616,7 @@ Commands:
557
616
  clean Remove old cache directories
558
617
 
559
618
  Options:
619
+ -c, --cwd <path> Set working directory
560
620
  -p, --port <port> Specify port (dev/preview)
561
621
  -d, --days <days> Cache age threshold for clean (default: 30)
562
622
  -h, --help Show this help message
@@ -1,13 +1,27 @@
1
+ export interface Frontmatter {
2
+ title?: string;
3
+ description?: string;
4
+ [key: string]: unknown;
5
+ }
1
6
  export interface Page {
2
7
  route: string;
3
8
  title: string;
4
9
  file: string;
10
+ description?: string;
11
+ frontmatter?: Frontmatter;
5
12
  }
6
13
  export interface SidebarItem {
7
14
  title: string;
8
15
  route?: string;
9
16
  children?: SidebarItem[];
10
17
  }
18
+ /**
19
+ * Parse YAML frontmatter from markdown content
20
+ */
21
+ export declare function parseFrontmatter(content: string): {
22
+ frontmatter: Frontmatter;
23
+ content: string;
24
+ };
11
25
  export declare function fileToRoute(file: string): string;
12
26
  export declare function scanPages(rootDir: string): Promise<Page[]>;
13
27
  export declare function buildSidebarTree(pages: Page[]): SidebarItem[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,7 @@ function convertToPageTree(items: any[]): PageTree.Root {
54
54
  }
55
55
 
56
56
  // Dynamic imports for MDX pages
57
- const pageModules = import.meta.glob('/**/*.mdx', { eager: true })
57
+ const pageModules = import.meta.glob('/**/*.{md,mdx}', { eager: true })
58
58
 
59
59
  function getPageComponent(file: string): React.ComponentType | null {
60
60
  const mod = pageModules[`/${file}`] as { default: React.ComponentType } | undefined