create-dox 0.3.4 → 0.4.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/index.js +141 -38
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -100,6 +100,7 @@ async function gatherAnswers(dirArg, useDefaults) {
|
|
|
100
100
|
import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
101
101
|
import { join as join2, extname, relative } from "path";
|
|
102
102
|
import matter from "gray-matter";
|
|
103
|
+
import { parse as parseYaml } from "yaml";
|
|
103
104
|
|
|
104
105
|
// src/docs-json.ts
|
|
105
106
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -118,11 +119,8 @@ function writeDocsJson(projectDir, config) {
|
|
|
118
119
|
function collectNavPageIds(groups, seen, duplicates) {
|
|
119
120
|
for (const page of groups) {
|
|
120
121
|
if (typeof page === "string") {
|
|
121
|
-
if (seen.has(page))
|
|
122
|
-
|
|
123
|
-
} else {
|
|
124
|
-
seen.add(page);
|
|
125
|
-
}
|
|
122
|
+
if (seen.has(page)) duplicates.add(page);
|
|
123
|
+
else seen.add(page);
|
|
126
124
|
} else if (page.pages) {
|
|
127
125
|
collectNavPageIds(page.pages, seen, duplicates);
|
|
128
126
|
}
|
|
@@ -139,11 +137,8 @@ function scanMdx(dir, results) {
|
|
|
139
137
|
const fullPath = join2(dir, entry);
|
|
140
138
|
try {
|
|
141
139
|
const stat = statSync(fullPath);
|
|
142
|
-
if (stat.isDirectory())
|
|
143
|
-
|
|
144
|
-
} else if (extname(entry).toLowerCase() === ".mdx") {
|
|
145
|
-
results.push(fullPath);
|
|
146
|
-
}
|
|
140
|
+
if (stat.isDirectory()) scanMdx(fullPath, results);
|
|
141
|
+
else if (extname(entry).toLowerCase() === ".mdx") results.push(fullPath);
|
|
147
142
|
} catch {
|
|
148
143
|
}
|
|
149
144
|
}
|
|
@@ -159,7 +154,80 @@ function addOrphanToNav(projectDir, pageId) {
|
|
|
159
154
|
writeDocsJson(projectDir, config);
|
|
160
155
|
}
|
|
161
156
|
}
|
|
162
|
-
|
|
157
|
+
function slugify2(text) {
|
|
158
|
+
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
|
|
159
|
+
}
|
|
160
|
+
function extractHeadingAnchors(content) {
|
|
161
|
+
const anchors = /* @__PURE__ */ new Set();
|
|
162
|
+
for (const line of content.split("\n")) {
|
|
163
|
+
const m = /^#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
|
|
164
|
+
if (m) anchors.add(slugify2(m[1]));
|
|
165
|
+
}
|
|
166
|
+
return anchors;
|
|
167
|
+
}
|
|
168
|
+
function extractLinks(content) {
|
|
169
|
+
const links = [];
|
|
170
|
+
const lines = content.split("\n");
|
|
171
|
+
let inFence = false;
|
|
172
|
+
for (let i = 0; i < lines.length; i++) {
|
|
173
|
+
if (/^\s*(```|~~~)/.test(lines[i])) {
|
|
174
|
+
inFence = !inFence;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (inFence) continue;
|
|
178
|
+
const line = lines[i].replace(/`[^`]*`/g, "");
|
|
179
|
+
for (const m of line.matchAll(/\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
|
|
180
|
+
links.push({ target: m[1], line: i + 1 });
|
|
181
|
+
}
|
|
182
|
+
for (const m of line.matchAll(/href=["']([^"']+)["']/g)) {
|
|
183
|
+
links.push({ target: m[1], line: i + 1 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return links;
|
|
187
|
+
}
|
|
188
|
+
function pageIdToPath(pageId) {
|
|
189
|
+
return pageId === "introduction" ? "/" : `/${pageId}`;
|
|
190
|
+
}
|
|
191
|
+
function validateOpenApi(projectDir, source, issues) {
|
|
192
|
+
const specPath = join2(projectDir, source);
|
|
193
|
+
if (!existsSync(specPath)) {
|
|
194
|
+
issues.push({ severity: "error", message: `API reference points at "${source}" but the file does not exist`, file: source });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let spec;
|
|
198
|
+
try {
|
|
199
|
+
const raw = readFileSync2(specPath, "utf8");
|
|
200
|
+
spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml(raw);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
issues.push({ severity: "error", message: `OpenAPI spec is not valid ${source.endsWith(".json") ? "JSON" : "YAML"}: ${err.message}`, file: source });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const s = spec;
|
|
206
|
+
if (typeof s?.openapi !== "string" && typeof s?.swagger !== "string") {
|
|
207
|
+
issues.push({ severity: "error", message: 'OpenAPI spec is missing the "openapi" (or "swagger") version field', file: source });
|
|
208
|
+
}
|
|
209
|
+
if (typeof s?.info !== "object" || s.info === null) {
|
|
210
|
+
issues.push({ severity: "error", message: 'OpenAPI spec is missing the "info" object', file: source });
|
|
211
|
+
}
|
|
212
|
+
const paths = s?.paths;
|
|
213
|
+
if (typeof paths !== "object" || paths === null) {
|
|
214
|
+
issues.push({ severity: "error", message: 'OpenAPI spec is missing the "paths" object', file: source });
|
|
215
|
+
} else {
|
|
216
|
+
const methods = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
217
|
+
for (const [p, ops] of Object.entries(paths)) {
|
|
218
|
+
if (typeof ops !== "object" || ops === null) {
|
|
219
|
+
issues.push({ severity: "error", message: `OpenAPI path "${p}" is not an object`, file: source });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const hasOp = Object.keys(ops).some((k) => methods.has(k.toLowerCase()));
|
|
223
|
+
if (!hasOp) {
|
|
224
|
+
issues.push({ severity: "warning", message: `OpenAPI path "${p}" has no operations`, file: source });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function runCheck(projectDir, options) {
|
|
230
|
+
const { fix, ci } = options;
|
|
163
231
|
if (!existsSync(join2(projectDir, "docs.json"))) {
|
|
164
232
|
console.error(`
|
|
165
233
|
\u274C Not a Dox project: docs.json not found in ${projectDir}
|
|
@@ -172,7 +240,11 @@ async function runCheck(projectDir, fix) {
|
|
|
172
240
|
const navPageIds = /* @__PURE__ */ new Set();
|
|
173
241
|
const duplicates = /* @__PURE__ */ new Set();
|
|
174
242
|
for (const tab of config.tabs) {
|
|
175
|
-
if (tab.href
|
|
243
|
+
if (tab.href) {
|
|
244
|
+
if (tab.href.startsWith("/")) navPageIds.add(tab.href.slice(1) || "introduction");
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (tab.api) continue;
|
|
176
248
|
if (!tab.groups || tab.groups.length === 0) {
|
|
177
249
|
issues.push({ severity: "error", message: `Tab "${tab.tab}" has no groups and no href \u2014 it will render empty` });
|
|
178
250
|
continue;
|
|
@@ -183,21 +255,17 @@ async function runCheck(projectDir, fix) {
|
|
|
183
255
|
issues.push({ severity: "error", message: `[duplicate] "${dup}" appears more than once in docs.json` });
|
|
184
256
|
}
|
|
185
257
|
for (const pageId of navPageIds) {
|
|
186
|
-
const candidates = [
|
|
187
|
-
join2(contentDir, `${pageId}.mdx`),
|
|
188
|
-
join2(contentDir, `${pageId}/index.mdx`)
|
|
189
|
-
];
|
|
258
|
+
const candidates = [join2(contentDir, `${pageId}.mdx`), join2(contentDir, `${pageId}/index.mdx`)];
|
|
190
259
|
if (!candidates.some((c) => existsSync(c))) {
|
|
191
|
-
issues.push({
|
|
192
|
-
severity: "error",
|
|
193
|
-
message: `"${pageId}" is in docs.json but has no MDX file`,
|
|
194
|
-
file: `src/content/${pageId}.mdx`
|
|
195
|
-
});
|
|
260
|
+
issues.push({ severity: "error", message: `"${pageId}" is in docs.json but has no MDX file`, file: `src/content/${pageId}.mdx` });
|
|
196
261
|
}
|
|
197
262
|
}
|
|
198
263
|
const allFiles = [];
|
|
199
264
|
if (existsSync(contentDir)) scanMdx(contentDir, allFiles);
|
|
200
265
|
const fixedOrphans = [];
|
|
266
|
+
const validPaths = /* @__PURE__ */ new Set(["/"]);
|
|
267
|
+
const anchorsByPath = /* @__PURE__ */ new Map();
|
|
268
|
+
const linksByFile = [];
|
|
201
269
|
for (const filePath of allFiles) {
|
|
202
270
|
const rel = filePath.slice(contentDir.length + 1).replace(/\.mdx$/, "").replace(/\\/g, "/");
|
|
203
271
|
const pageId = rel.endsWith("/index") ? rel.slice(0, -6) : rel;
|
|
@@ -206,37 +274,69 @@ async function runCheck(projectDir, fix) {
|
|
|
206
274
|
addOrphanToNav(projectDir, pageId);
|
|
207
275
|
fixedOrphans.push(pageId);
|
|
208
276
|
} else {
|
|
209
|
-
issues.push({
|
|
210
|
-
severity: "warning",
|
|
211
|
-
message: `"${pageId}" is not in docs.json nav (orphan)`,
|
|
212
|
-
file: relative(projectDir, filePath)
|
|
213
|
-
});
|
|
277
|
+
issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative(projectDir, filePath) });
|
|
214
278
|
}
|
|
215
279
|
}
|
|
216
280
|
let data = {};
|
|
217
281
|
let content = "";
|
|
282
|
+
let lineOffset = 0;
|
|
218
283
|
try {
|
|
219
284
|
const raw = readFileSync2(filePath, "utf8");
|
|
220
285
|
const parsed = matter(raw);
|
|
221
286
|
data = parsed.data;
|
|
222
287
|
content = parsed.content;
|
|
288
|
+
lineOffset = raw.slice(0, raw.indexOf(content)).split("\n").length - 1;
|
|
223
289
|
} catch {
|
|
224
290
|
issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
|
|
225
291
|
continue;
|
|
226
292
|
}
|
|
227
293
|
const rel2 = relative(projectDir, filePath);
|
|
228
|
-
if (!data.title) {
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
294
|
+
if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
|
|
295
|
+
if (!data.description) issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
|
|
296
|
+
if (content.trim().length < 50) issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
|
|
297
|
+
const path = pageIdToPath(pageId);
|
|
298
|
+
const anchors = extractHeadingAnchors(content);
|
|
299
|
+
validPaths.add(path);
|
|
300
|
+
anchorsByPath.set(path, anchors);
|
|
301
|
+
linksByFile.push({ file: rel2, path, anchors, links: extractLinks(content), offset: lineOffset });
|
|
302
|
+
}
|
|
303
|
+
for (const { file, anchors, links, offset } of linksByFile) {
|
|
304
|
+
for (const { target, line: contentLine } of links) {
|
|
305
|
+
const line = contentLine + offset;
|
|
306
|
+
if (/^(https?:|mailto:|tel:)/i.test(target)) continue;
|
|
307
|
+
if (target.startsWith("#")) {
|
|
308
|
+
const anchor2 = target.slice(1);
|
|
309
|
+
if (anchor2 && !anchors.has(anchor2)) {
|
|
310
|
+
issues.push({ severity: "warning", message: `Broken anchor: "${target}" not found on this page`, file, line });
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!target.startsWith("/")) continue;
|
|
315
|
+
const [beforeHash, anchor] = target.split("#");
|
|
316
|
+
let path = beforeHash.split("?")[0];
|
|
317
|
+
if (path.length > 1) path = path.replace(/\/$/, "");
|
|
318
|
+
if (path.startsWith("/api") || path.startsWith("/_next") || /\.[a-z0-9]+$/i.test(path)) continue;
|
|
319
|
+
if (!validPaths.has(path)) {
|
|
320
|
+
issues.push({ severity: "error", message: `Broken link: "${target}" \u2014 no page at "${path}"`, file, line });
|
|
321
|
+
} else if (anchor && !anchorsByPath.get(path)?.has(anchor)) {
|
|
322
|
+
issues.push({ severity: "warning", message: `Broken anchor: "${target}" \u2014 no heading "#${anchor}" on that page`, file, line });
|
|
323
|
+
}
|
|
236
324
|
}
|
|
237
325
|
}
|
|
326
|
+
for (const tab of config.tabs) {
|
|
327
|
+
if (tab.api?.source) validateOpenApi(projectDir, tab.api.source, issues);
|
|
328
|
+
}
|
|
238
329
|
const errors = issues.filter((i) => i.severity === "error");
|
|
239
330
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
331
|
+
if (ci) {
|
|
332
|
+
for (const issue of issues) {
|
|
333
|
+
const loc = issue.file ? `file=${issue.file}${issue.line ? `,line=${issue.line}` : ""}` : "";
|
|
334
|
+
console.log(`::${issue.severity} ${loc}::${issue.message}`);
|
|
335
|
+
}
|
|
336
|
+
console.log(`
|
|
337
|
+
dox check: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
338
|
+
return errors.length > 0 ? 1 : 0;
|
|
339
|
+
}
|
|
240
340
|
console.log(`
|
|
241
341
|
Linting ${projectDir}...
|
|
242
342
|
`);
|
|
@@ -250,7 +350,7 @@ async function runCheck(projectDir, fix) {
|
|
|
250
350
|
console.log(" ERRORS:");
|
|
251
351
|
for (const issue of errors) {
|
|
252
352
|
console.log(` ${issue.message}`);
|
|
253
|
-
if (issue.file) console.log(` \u2192 ${issue.file}`);
|
|
353
|
+
if (issue.file) console.log(` \u2192 ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
|
|
254
354
|
}
|
|
255
355
|
console.log("");
|
|
256
356
|
}
|
|
@@ -258,7 +358,7 @@ async function runCheck(projectDir, fix) {
|
|
|
258
358
|
console.log(" WARNINGS:");
|
|
259
359
|
for (const issue of warnings) {
|
|
260
360
|
console.log(` ${issue.message}`);
|
|
261
|
-
if (issue.file) console.log(` \u2192 ${issue.file}`);
|
|
361
|
+
if (issue.file) console.log(` \u2192 ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
|
|
262
362
|
}
|
|
263
363
|
console.log("");
|
|
264
364
|
}
|
|
@@ -601,8 +701,11 @@ async function runScaffoldCommand() {
|
|
|
601
701
|
}
|
|
602
702
|
async function runCheckCommand() {
|
|
603
703
|
const projectDir = resolve2(positional[1] ?? ".");
|
|
604
|
-
const
|
|
605
|
-
|
|
704
|
+
const exitCode = await runCheck(projectDir, {
|
|
705
|
+
fix: flags.includes("--fix"),
|
|
706
|
+
ci: flags.includes("--ci"),
|
|
707
|
+
external: flags.includes("--external")
|
|
708
|
+
});
|
|
606
709
|
process.exit(exitCode);
|
|
607
710
|
}
|
|
608
711
|
async function runTranslateSubcommand() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-dox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Scaffold a new Dox documentation project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"@inquirer/prompts": "^7.0.0",
|
|
29
29
|
"gray-matter": "^4.0.3",
|
|
30
30
|
"p-limit": "^6.1.0",
|
|
31
|
-
"tar": "^6.2.0"
|
|
31
|
+
"tar": "^6.2.0",
|
|
32
|
+
"yaml": "^2.6.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@types/node": "^22.0.0",
|