@marlinjai/clearify 1.9.0 → 1.11.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/README.md CHANGED
@@ -1,3 +1,12 @@
1
+ ---
2
+ title: Clearify
3
+ summary: README for Clearify, an open-source documentation site generator that turns markdown into beautiful docs with zero config, MDX support, Mermaid diagrams, full-text search, OpenAPI rendering, and SSG with SEO.
4
+ category: documentation
5
+ tags: [clearify, documentation, mdx, vite, ssg]
6
+ status: active
7
+ date: 2026-02-07
8
+ ---
9
+
1
10
  # Clearify
2
11
 
3
12
  An open-source documentation site generator. Turn markdown into beautiful docs.
@@ -171,6 +171,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
171
171
  } else {
172
172
  console.log(" CHANGELOG.md already exists, skipping");
173
173
  }
174
+ const roadmapPath = resolve(cwd, "ROADMAP.md");
175
+ if (!existsSync(roadmapPath)) {
176
+ writeFileSync(
177
+ roadmapPath,
178
+ `# Roadmap
179
+
180
+ ## Planned
181
+
182
+ <!-- Decided features, ready to be worked on -->
183
+
184
+ ## In Progress
185
+
186
+ <!-- Currently being implemented -->
187
+
188
+ ## Completed
189
+
190
+ <!-- Done \u2014 move to CHANGELOG.md on release -->
191
+ `
192
+ );
193
+ console.log(" Created ROADMAP.md");
194
+ } else {
195
+ console.log(" ROADMAP.md already exists, skipping");
196
+ }
174
197
  if (!existsSync(configPath)) {
175
198
  if (options.noInternal) {
176
199
  writeFileSync(
@@ -0,0 +1,105 @@
1
+ // src/core/remote.ts
2
+ import { execSync } from "child_process";
3
+ import { existsSync, mkdirSync } from "fs";
4
+ import { resolve } from "path";
5
+ function repoSlug(repo, ref) {
6
+ return repo.replace(/^(https?:\/\/|git@)/, "").replace(/\.git$/, "").replace(/[/:@]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") + "--" + ref;
7
+ }
8
+ function isPinnedRef(ref) {
9
+ return /^[0-9a-f]{40}$/i.test(ref) || /^v?\d+\.\d+/.test(ref);
10
+ }
11
+ async function resolveRemoteSource(source, cacheDir) {
12
+ const ref = source.ref ?? "main";
13
+ const slug = repoSlug(source.repo, ref);
14
+ const localPath = resolve(cacheDir, slug);
15
+ const sparse = source.sparse !== false;
16
+ mkdirSync(cacheDir, { recursive: true });
17
+ if (existsSync(resolve(localPath, ".git"))) {
18
+ if (!isPinnedRef(ref)) {
19
+ try {
20
+ execSync(`git -C ${JSON.stringify(localPath)} fetch origin ${ref} --depth 1`, {
21
+ stdio: "pipe",
22
+ timeout: 3e4
23
+ });
24
+ execSync(`git -C ${JSON.stringify(localPath)} checkout FETCH_HEAD`, {
25
+ stdio: "pipe"
26
+ });
27
+ } catch (err) {
28
+ console.warn(
29
+ ` \u26A0 Failed to update remote "${source.repo}" (ref: ${ref}), using stale cache.`,
30
+ err instanceof Error ? err.message : ""
31
+ );
32
+ }
33
+ }
34
+ const commit2 = getHeadCommit(localPath);
35
+ return { localPath, resolvedCommit: commit2, cached: true };
36
+ }
37
+ try {
38
+ if (sparse && source.path) {
39
+ execSync(
40
+ `git clone --no-checkout --depth 1 --branch ${ref} ${source.repo} ${JSON.stringify(localPath)}`,
41
+ { stdio: "pipe", timeout: 6e4 }
42
+ );
43
+ execSync(`git -C ${JSON.stringify(localPath)} sparse-checkout init --cone`, {
44
+ stdio: "pipe"
45
+ });
46
+ execSync(`git -C ${JSON.stringify(localPath)} sparse-checkout set ${source.path}`, {
47
+ stdio: "pipe"
48
+ });
49
+ execSync(`git -C ${JSON.stringify(localPath)} checkout`, { stdio: "pipe" });
50
+ } else {
51
+ execSync(
52
+ `git clone --depth 1 --branch ${ref} ${source.repo} ${JSON.stringify(localPath)}`,
53
+ { stdio: "pipe", timeout: 6e4 }
54
+ );
55
+ }
56
+ } catch (err) {
57
+ const msg = err instanceof Error ? err.message : String(err);
58
+ throw new Error(
59
+ `Failed to clone remote repo "${source.repo}" (ref: ${ref}): ${msg}`
60
+ );
61
+ }
62
+ const commit = getHeadCommit(localPath);
63
+ return { localPath, resolvedCommit: commit, cached: false };
64
+ }
65
+ function getHeadCommit(repoPath) {
66
+ try {
67
+ return execSync(`git -C ${JSON.stringify(repoPath)} rev-parse HEAD`, {
68
+ encoding: "utf-8",
69
+ stdio: "pipe"
70
+ }).trim();
71
+ } catch {
72
+ return "unknown";
73
+ }
74
+ }
75
+ async function resolveRemoteSections(sections, cacheDir) {
76
+ const results = [];
77
+ for (const section of sections) {
78
+ if (!section.git) {
79
+ results.push(section);
80
+ continue;
81
+ }
82
+ const { localPath, cached } = await resolveRemoteSource(section.git, cacheDir);
83
+ const absoluteDocsDir = resolve(localPath, section.docsDir);
84
+ if (!existsSync(absoluteDocsDir)) {
85
+ throw new Error(
86
+ `Remote section "${section.label}": docs directory "${section.docsDir}" not found in cloned repo "${section.git.repo}".`
87
+ );
88
+ }
89
+ console.log(
90
+ ` Remote section "${section.label}": ${cached ? "cached" : "cloned"} \u2192 ${absoluteDocsDir}`
91
+ );
92
+ results.push({
93
+ ...section,
94
+ docsDir: absoluteDocsDir,
95
+ git: void 0
96
+ // Already resolved — docsDir is now absolute
97
+ });
98
+ }
99
+ return results;
100
+ }
101
+
102
+ export {
103
+ resolveRemoteSource,
104
+ resolveRemoteSections
105
+ };
@@ -8,15 +8,15 @@ import {
8
8
  } from "./chunk-SCZZB7OE.js";
9
9
  import {
10
10
  clearifyPlugin
11
- } from "./chunk-IF6GI4WU.js";
11
+ } from "./chunk-PFX7LHRZ.js";
12
12
  import {
13
13
  loadUserConfig,
14
14
  resolveConfig,
15
15
  resolveSections
16
- } from "./chunk-PQMKRFBQ.js";
16
+ } from "./chunk-PVTUVUBK.js";
17
17
  import {
18
18
  buildSectionData
19
- } from "./chunk-QNC5QRIO.js";
19
+ } from "./chunk-QTGT76RY.js";
20
20
 
21
21
  // src/node/build.ts
22
22
  import { build as viteBuild } from "vite";
@@ -27,10 +27,11 @@ import remarkFrontmatter from "remark-frontmatter";
27
27
  import remarkMdxFrontmatter from "remark-mdx-frontmatter";
28
28
  import rehypeShiki from "@shikijs/rehype";
29
29
  import tailwindcss from "@tailwindcss/vite";
30
- import { resolve } from "path";
30
+ import { resolve, dirname } from "path";
31
31
  import { writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, copyFileSync } from "fs";
32
32
  import { basename } from "path";
33
33
  import { fileURLToPath } from "url";
34
+ import { createRequire } from "module";
34
35
  function findPackageRoot() {
35
36
  let dir = fileURLToPath(new URL(".", import.meta.url));
36
37
  for (let i = 0; i < 10; i++) {
@@ -45,6 +46,18 @@ function resolveClientPath(...segments) {
45
46
  const packageRoot = findPackageRoot();
46
47
  return resolve(packageRoot, "src", "client", ...segments);
47
48
  }
49
+ function dedupeResolve() {
50
+ const require2 = createRequire(import.meta.url);
51
+ const dedupePkgs = ["@mdx-js/react", "react", "react-dom", "react-router-dom"];
52
+ const alias = {};
53
+ for (const pkg of dedupePkgs) {
54
+ try {
55
+ alias[pkg] = dirname(require2.resolve(`${pkg}/package.json`));
56
+ } catch {
57
+ }
58
+ }
59
+ return { alias, dedupe: dedupePkgs };
60
+ }
48
61
  function escapeHtml(str) {
49
62
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
50
63
  }
@@ -140,11 +153,14 @@ async function buildSite() {
140
153
  const config = resolveConfig(userConfig);
141
154
  const outDir = resolve(userRoot, config.outDir);
142
155
  const ssrOutDir = resolve(outDir, ".ssr");
143
- const allSections = resolveSections(config, userRoot);
156
+ const allSections = await resolveSections(config, userRoot);
144
157
  const productionSections = allSections.filter((s) => !s.draft);
145
- const changelogPath = resolve(userRoot, "CHANGELOG.md");
158
+ const rootFiles = {
159
+ changelogPath: resolve(userRoot, "CHANGELOG.md"),
160
+ roadmapPath: resolve(userRoot, "ROADMAP.md")
161
+ };
146
162
  const sectionDataList = productionSections.map(
147
- (section) => buildSectionData(section, changelogPath, config.navigation)
163
+ (section) => buildSectionData(section, rootFiles, config.navigation)
148
164
  );
149
165
  const allRoutes = sectionDataList.flatMap((sd) => sd.routes);
150
166
  if (config.openapi?.spec && config.openapi.generatePages !== false) {
@@ -207,11 +223,13 @@ async function buildSite() {
207
223
  mermaidSvgs,
208
224
  mermaidStrategy
209
225
  };
226
+ const { alias: dedupeAlias, dedupe } = dedupeResolve();
210
227
  const clientConfig = {
211
228
  root,
212
229
  plugins: getSharedPlugins(userRoot, sharedPluginOptions),
213
230
  resolve: {
214
- alias: { "@clearify": resolve(root, "..") }
231
+ alias: { "@clearify": resolve(root, ".."), ...dedupeAlias },
232
+ dedupe
215
233
  },
216
234
  build: {
217
235
  outDir,
@@ -225,7 +243,8 @@ async function buildSite() {
225
243
  root,
226
244
  plugins: getSharedPlugins(userRoot, sharedPluginOptions),
227
245
  resolve: {
228
- alias: { "@clearify": resolve(root, "..") }
246
+ alias: { "@clearify": resolve(root, "..") },
247
+ dedupe
229
248
  },
230
249
  build: {
231
250
  outDir: ssrOutDir,
@@ -0,0 +1,264 @@
1
+ import {
2
+ loadUserConfig,
3
+ resolveConfig,
4
+ resolveSections
5
+ } from "./chunk-PVTUVUBK.js";
6
+ import {
7
+ scanDocs
8
+ } from "./chunk-QTGT76RY.js";
9
+
10
+ // src/node/check.ts
11
+ import { resolve, relative } from "path";
12
+ import { readFileSync, existsSync } from "fs";
13
+ import matter from "gray-matter";
14
+ var VALID_CATEGORIES = [
15
+ "documentation",
16
+ "internal",
17
+ "plan",
18
+ "research",
19
+ "decision",
20
+ "roadmap",
21
+ "changelog"
22
+ ];
23
+ var VALID_STATUSES = ["active", "superseded", "archived"];
24
+ function isStrictPath(filePath) {
25
+ const normalized = filePath.replace(/\\/g, "/");
26
+ return /\/docs\/plans(\/|$)/.test(normalized) || /\/docs\/internal(\/|$)/.test(normalized);
27
+ }
28
+ function validateFrontmatter(doc, userRoot) {
29
+ const issues = [];
30
+ const rel = relative(userRoot, doc.filePath);
31
+ const strict = isStrictPath(doc.filePath);
32
+ let rawFm = {};
33
+ try {
34
+ const content = readFileSync(doc.filePath, "utf-8");
35
+ rawFm = matter(content).data;
36
+ } catch {
37
+ rawFm = {};
38
+ }
39
+ if (strict) {
40
+ if (!rawFm.title) {
41
+ issues.push({ file: rel, field: "title", message: "missing required field", severity: "error" });
42
+ }
43
+ if (!rawFm.summary) {
44
+ issues.push({ file: rel, field: "summary", message: "missing required field", severity: "error" });
45
+ }
46
+ if (!rawFm.category) {
47
+ issues.push({ file: rel, field: "category", message: "missing required field", severity: "error" });
48
+ }
49
+ } else {
50
+ if (!rawFm.title) {
51
+ issues.push({ file: rel, field: "title", message: "missing required field", severity: "error" });
52
+ }
53
+ if (!rawFm.summary) {
54
+ issues.push({ file: rel, field: "summary", message: "recommended field missing", severity: "warning" });
55
+ }
56
+ if (!rawFm.category) {
57
+ issues.push({ file: rel, field: "category", message: "recommended field missing", severity: "warning" });
58
+ }
59
+ }
60
+ if (rawFm.category !== void 0) {
61
+ if (!VALID_CATEGORIES.includes(rawFm.category)) {
62
+ issues.push({
63
+ file: rel,
64
+ field: "category",
65
+ message: `invalid value "${rawFm.category}" (allowed: ${VALID_CATEGORIES.join(", ")})`,
66
+ severity: "error"
67
+ });
68
+ }
69
+ }
70
+ if (rawFm.status !== void 0) {
71
+ if (!VALID_STATUSES.includes(rawFm.status)) {
72
+ issues.push({
73
+ file: rel,
74
+ field: "status",
75
+ message: `invalid value "${rawFm.status}" (allowed: ${VALID_STATUSES.join(", ")})`,
76
+ severity: "error"
77
+ });
78
+ }
79
+ }
80
+ if (rawFm.tags !== void 0 && !Array.isArray(rawFm.tags)) {
81
+ issues.push({ file: rel, field: "tags", message: "must be an array", severity: "error" });
82
+ }
83
+ if (rawFm.projects !== void 0 && !Array.isArray(rawFm.projects)) {
84
+ issues.push({ file: rel, field: "projects", message: "must be an array", severity: "error" });
85
+ }
86
+ return issues;
87
+ }
88
+ async function checkFrontmatter() {
89
+ const userRoot = process.cwd();
90
+ const userConfig = await loadUserConfig(userRoot);
91
+ const config = resolveConfig(userConfig, userRoot);
92
+ const sections = await resolveSections(config, userRoot);
93
+ const allDocs = [];
94
+ for (const section of sections) {
95
+ const docs = scanDocs(section.docsDir, section.exclude, section.basePath);
96
+ allDocs.push(...docs);
97
+ }
98
+ const allIssues = [];
99
+ for (const doc of allDocs) {
100
+ const issues = validateFrontmatter(doc, userRoot);
101
+ allIssues.push(...issues);
102
+ }
103
+ const errors = allIssues.filter((i) => i.severity === "error");
104
+ const warnings = allIssues.filter((i) => i.severity === "warning");
105
+ if (allIssues.length === 0) {
106
+ console.log("\n Frontmatter: all files valid.\n");
107
+ return { errors: 0, warnings: 0 };
108
+ }
109
+ console.log(`
110
+ Frontmatter validation: ${errors.length} error(s), ${warnings.length} warning(s)
111
+ `);
112
+ const fileCol = Math.max(4, ...allIssues.map((i) => i.file.length));
113
+ const fieldCol = Math.max(5, ...allIssues.map((i) => i.field.length));
114
+ const msgCol = Math.max(7, ...allIssues.map((i) => i.message.length));
115
+ const sevCol = 7;
116
+ const header = ` ${"File".padEnd(fileCol)} ${"Field".padEnd(fieldCol)} ${"Severity".padEnd(sevCol)} ${"Message".padEnd(msgCol)}`;
117
+ const separator = ` ${"\u2500".repeat(fileCol)} ${"\u2500".repeat(fieldCol)} ${"\u2500".repeat(sevCol)} ${"\u2500".repeat(msgCol)}`;
118
+ console.log(header);
119
+ console.log(separator);
120
+ for (const issue of allIssues) {
121
+ const sev = issue.severity === "error" ? "ERROR " : "warning";
122
+ console.log(` ${issue.file.padEnd(fileCol)} ${issue.field.padEnd(fieldCol)} ${sev} ${issue.message}`);
123
+ }
124
+ console.log();
125
+ return { errors: errors.length, warnings: warnings.length };
126
+ }
127
+ async function checkLinks(options = {}) {
128
+ const fmResult = await checkFrontmatter();
129
+ if (options.frontmatterOnly) {
130
+ if (fmResult.errors > 0) {
131
+ process.exitCode = 1;
132
+ }
133
+ return;
134
+ }
135
+ const userRoot = process.cwd();
136
+ const userConfig = await loadUserConfig(userRoot);
137
+ const config = resolveConfig(userConfig, userRoot);
138
+ const sections = await resolveSections(config, userRoot);
139
+ const validRoutes = /* @__PURE__ */ new Set();
140
+ const allDocs = [];
141
+ for (const section of sections) {
142
+ const docs = scanDocs(section.docsDir, section.exclude, section.basePath);
143
+ for (const doc of docs) {
144
+ validRoutes.add(doc.routePath);
145
+ allDocs.push(doc);
146
+ }
147
+ }
148
+ validRoutes.add("/");
149
+ const changelogPath = resolve(userRoot, "CHANGELOG.md");
150
+ if (existsSync(changelogPath)) {
151
+ validRoutes.add("/changelog");
152
+ }
153
+ const roadmapPath = resolve(userRoot, "ROADMAP.md");
154
+ if (existsSync(roadmapPath)) {
155
+ validRoutes.add("/roadmap");
156
+ }
157
+ const expandedRoutes = new Set(validRoutes);
158
+ for (const route of validRoutes) {
159
+ expandedRoutes.add(route.replace(/\/$/, ""));
160
+ if (route !== "/") expandedRoutes.add(route + "/");
161
+ }
162
+ const mdLinkRegex = /\[(?:[^\]]*)\]\(([^)]+)\)/g;
163
+ const hrefRegex = /href=["']([^"']+)["']/g;
164
+ const brokenLinks = [];
165
+ for (const doc of allDocs) {
166
+ if (!existsSync(doc.filePath)) continue;
167
+ const content = readFileSync(doc.filePath, "utf-8");
168
+ const lines = content.split("\n");
169
+ for (let i = 0; i < lines.length; i++) {
170
+ const line = lines[i];
171
+ const lineNum = i + 1;
172
+ if (line.trimStart().startsWith("```")) continue;
173
+ for (const regex of [mdLinkRegex, hrefRegex]) {
174
+ regex.lastIndex = 0;
175
+ let match;
176
+ while ((match = regex.exec(line)) !== null) {
177
+ const rawLink = match[1].split(/[\s"'#]/)[0];
178
+ if (!rawLink || rawLink.startsWith("http://") || rawLink.startsWith("https://") || rawLink.startsWith("#") || rawLink.startsWith("mailto:") || rawLink.startsWith("tel:") || rawLink.startsWith("//") || rawLink.startsWith("{") || rawLink.startsWith("data:")) {
179
+ continue;
180
+ }
181
+ if (/\.(png|jpe?g|gif|svg|webp|ico|pdf|zip|tar|gz)$/i.test(rawLink)) {
182
+ continue;
183
+ }
184
+ let normalized = rawLink;
185
+ normalized = normalized.replace(/\.(mdx?|html?)$/, "");
186
+ if (!normalized.startsWith("/")) {
187
+ const docDir = doc.routePath.substring(0, doc.routePath.lastIndexOf("/")) || "/";
188
+ normalized = docDir === "/" ? "/" + normalized : docDir + "/" + normalized;
189
+ }
190
+ const check = normalized === "/" ? "/" : normalized.replace(/\/$/, "");
191
+ const checkNoIndex = check.replace(/\/index$/, "") || "/";
192
+ if (!expandedRoutes.has(check) && !expandedRoutes.has(checkNoIndex)) {
193
+ const suggestion = findClosest(check, validRoutes);
194
+ brokenLinks.push({
195
+ file: doc.filePath,
196
+ line: lineNum,
197
+ link: rawLink,
198
+ ...suggestion ? { suggestion } : {}
199
+ });
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ if (brokenLinks.length === 0) {
206
+ console.log("\n No broken links found.\n");
207
+ } else {
208
+ console.log(`
209
+ Found ${brokenLinks.length} broken link${brokenLinks.length === 1 ? "" : "s"}:
210
+ `);
211
+ const byFile = /* @__PURE__ */ new Map();
212
+ for (const bl of brokenLinks) {
213
+ const rel = bl.file.replace(userRoot + "/", "");
214
+ if (!byFile.has(rel)) byFile.set(rel, []);
215
+ byFile.get(rel).push(bl);
216
+ }
217
+ for (const [file, links] of byFile) {
218
+ console.log(` ${file}`);
219
+ for (const link of links) {
220
+ const suggestion = link.suggestion ? ` (did you mean ${link.suggestion}?)` : "";
221
+ console.log(` line ${link.line}: ${link.link}${suggestion}`);
222
+ }
223
+ console.log();
224
+ }
225
+ }
226
+ if (brokenLinks.length > 0 || fmResult.errors > 0) {
227
+ process.exitCode = 1;
228
+ }
229
+ }
230
+ function findClosest(target, routes) {
231
+ let best;
232
+ let bestScore = Infinity;
233
+ const targetSegments = target.split("/").filter(Boolean);
234
+ for (const route of routes) {
235
+ const routeSegments = route.split("/").filter(Boolean);
236
+ const dist = levenshtein(target, route);
237
+ const common = targetSegments.filter((s) => routeSegments.includes(s)).length;
238
+ const score = dist - common * 2;
239
+ if (score < bestScore) {
240
+ bestScore = score;
241
+ best = route;
242
+ }
243
+ }
244
+ if (best && bestScore < target.length * 0.6) return best;
245
+ return void 0;
246
+ }
247
+ function levenshtein(a, b) {
248
+ const m = a.length;
249
+ const n = b.length;
250
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
251
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
252
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
253
+ for (let i = 1; i <= m; i++) {
254
+ for (let j = 1; j <= n; j++) {
255
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
256
+ }
257
+ }
258
+ return dp[m][n];
259
+ }
260
+
261
+ export {
262
+ checkFrontmatter,
263
+ checkLinks
264
+ };
@@ -6,10 +6,10 @@ import {
6
6
  resolveConfig,
7
7
  resolveSections,
8
8
  scanHubProjects
9
- } from "./chunk-PQMKRFBQ.js";
9
+ } from "./chunk-PVTUVUBK.js";
10
10
  import {
11
11
  buildSectionData
12
- } from "./chunk-QNC5QRIO.js";
12
+ } from "./chunk-QTGT76RY.js";
13
13
 
14
14
  // src/vite-plugin/index.ts
15
15
  import { resolve } from "path";
@@ -49,10 +49,16 @@ function clearifyPlugin(options = {}) {
49
49
  let allSectionNavigations;
50
50
  let allSearchEntries;
51
51
  let mermaidSvgData = options.mermaidSvgs ?? {};
52
+ function getRootFiles() {
53
+ return {
54
+ changelogPath: resolve(userRoot, "CHANGELOG.md"),
55
+ roadmapPath: resolve(userRoot, "ROADMAP.md")
56
+ };
57
+ }
52
58
  async function refreshDocs() {
53
- const changelogPath = resolve(userRoot, "CHANGELOG.md");
59
+ const rootFiles = getRootFiles();
54
60
  sectionDataList = visibleSections.map(
55
- (section) => buildSectionData(section, changelogPath, config.navigation)
61
+ (section) => buildSectionData(section, rootFiles, config.navigation)
56
62
  );
57
63
  allRoutes = sectionDataList.flatMap((sd) => sd.routes);
58
64
  allSectionNavigations = sectionDataList.map((sd) => sd.navigation);
@@ -222,7 +228,7 @@ ${routeEntries.join(",\n")}
222
228
  const userConfig = await loadUserConfig(userRoot);
223
229
  config = resolveConfig(userConfig, userRoot);
224
230
  config = await scanHubProjects(config, userRoot);
225
- const allSections = resolveSections(config, userRoot);
231
+ const allSections = await resolveSections(config, userRoot);
226
232
  const isDev = resolvedViteConfig.command === "serve";
227
233
  visibleSections = isDev ? allSections : allSections.filter((s) => !s.draft);
228
234
  await refreshDocs();
@@ -266,9 +272,11 @@ ${routeEntries.join(",\n")}
266
272
  for (const section of visibleSections) {
267
273
  server.watcher.add(section.docsDir);
268
274
  }
269
- const changelogPath = resolve(userRoot, "CHANGELOG.md");
270
- if (existsSync(changelogPath)) {
271
- server.watcher.add(changelogPath);
275
+ const rootFiles = getRootFiles();
276
+ for (const rootFile of [rootFiles.changelogPath, rootFiles.roadmapPath]) {
277
+ if (rootFile && existsSync(rootFile)) {
278
+ server.watcher.add(rootFile);
279
+ }
272
280
  }
273
281
  if (config.openapi?.spec) {
274
282
  const openapiPath = resolve(userRoot, config.openapi.spec);
@@ -385,9 +393,10 @@ ${routeEntries.join(",\n")}
385
393
  });
386
394
  }
387
395
  server.watcher.on("all", (event, path) => {
388
- const isChangelog = path === changelogPath;
396
+ const isChangelog = path === rootFiles.changelogPath;
397
+ const isRoadmap = path === rootFiles.roadmapPath;
389
398
  const isInSection = visibleSections.some((s) => path.startsWith(s.docsDir));
390
- if (!isChangelog && !isInSection) return;
399
+ if (!isChangelog && !isRoadmap && !isInSection) return;
391
400
  if (!path.endsWith(".md") && !path.endsWith(".mdx")) return;
392
401
  if (event === "add" || event === "unlink" || event === "change") {
393
402
  refreshDocs().then(() => {