@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 +9 -0
- package/dist/node/{chunk-QIAMD2Q6.js → chunk-3CW4MOO4.js} +23 -0
- package/dist/node/chunk-AJBCHKBP.js +105 -0
- package/dist/node/{chunk-GGUDZD4K.js → chunk-JU53JE4P.js} +28 -9
- package/dist/node/chunk-NGWAMBXS.js +264 -0
- package/dist/node/{chunk-IF6GI4WU.js → chunk-PFX7LHRZ.js} +19 -10
- package/dist/node/{chunk-PQMKRFBQ.js → chunk-PVTUVUBK.js} +100 -29
- package/dist/node/{chunk-QNC5QRIO.js → chunk-QTGT76RY.js} +35 -14
- package/dist/node/cli/index.js +19 -3
- package/dist/node/core/config.d.ts +1 -1
- package/dist/node/core/config.js +2 -1
- package/dist/node/core/navigation.d.ts +1 -1
- package/dist/node/core/navigation.js +1 -1
- package/dist/node/core/remote.d.ts +18 -0
- package/dist/node/core/remote.js +8 -0
- package/dist/node/core/search.d.ts +6 -2
- package/dist/node/node/build.js +5 -4
- package/dist/node/node/check.d.ts +9 -2
- package/dist/node/node/check.js +6 -3
- package/dist/node/node/index.d.ts +1 -1
- package/dist/node/node/index.js +28 -9
- package/dist/node/node/init.js +1 -1
- package/dist/node/types/index.d.ts +19 -1
- package/dist/node/vite-plugin/index.js +4 -3
- package/package.json +10 -2
- package/src/theme/Sidebar.tsx +13 -11
- package/dist/node/chunk-77VHQV7W.js +0 -136
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-
|
|
11
|
+
} from "./chunk-PFX7LHRZ.js";
|
|
12
12
|
import {
|
|
13
13
|
loadUserConfig,
|
|
14
14
|
resolveConfig,
|
|
15
15
|
resolveSections
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-PVTUVUBK.js";
|
|
17
17
|
import {
|
|
18
18
|
buildSectionData
|
|
19
|
-
} from "./chunk-
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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,
|
|
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-
|
|
9
|
+
} from "./chunk-PVTUVUBK.js";
|
|
10
10
|
import {
|
|
11
11
|
buildSectionData
|
|
12
|
-
} from "./chunk-
|
|
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
|
|
59
|
+
const rootFiles = getRootFiles();
|
|
54
60
|
sectionDataList = visibleSections.map(
|
|
55
|
-
(section) => buildSectionData(section,
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
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(() => {
|