create-dox 0.3.4 → 0.5.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.
@@ -19,9 +19,9 @@ function shouldInclude(path) {
19
19
  }
20
20
  return true;
21
21
  }
22
- async function downloadTemplate(targetDir) {
22
+ async function downloadTemplate(targetDir, siteName) {
23
23
  console.log("");
24
- console.log(" \u23F3 Downloading Dox template...");
24
+ console.log(` \u23F3 Creating ${siteName?.trim() || "your docs site"}...`);
25
25
  const response = await fetch(TARBALL_URL);
26
26
  if (!response.ok) {
27
27
  throw new Error(`Failed to download template: ${response.status} ${response.statusText}`);
@@ -135,6 +135,11 @@ function buildStarterDocsJson({
135
135
  i18nLocales
136
136
  }) {
137
137
  const config = {};
138
+ config.theme = "sharp";
139
+ config.fonts = {
140
+ body: { family: "Plus Jakarta Sans", weight: ["400", "500", "600", "700"] },
141
+ heading: { family: "Outfit", weight: ["600", "700"] }
142
+ };
138
143
  if (enableAiChat) {
139
144
  config.ai = { chat: true };
140
145
  }
@@ -201,20 +206,15 @@ function updateSiteConfig(targetDir, projectName, description, brandPreset, repo
201
206
  /const brandPreset:\s*BrandPresetKey\s*=\s*'[^']*'/,
202
207
  `const brandPreset: BrandPresetKey = '${brandPreset}'`
203
208
  );
204
- if (repoUrl) {
205
- source = source.replace(
206
- /repoUrl:\s*'[^']*'/,
207
- `repoUrl: '${repoUrl}'`
208
- );
209
- source = source.replace(
210
- /\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
211
- `{ label: 'GitHub', href: '${repoUrl}' }`
212
- );
213
- source = source.replace(
214
- /\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
215
- `{ label: 'Support', href: '${repoUrl}/issues/new' }`
216
- );
217
- }
209
+ source = source.replace(/repoUrl:\s*'[^']*'/, `repoUrl: '${repoUrl}'`);
210
+ source = source.replace(
211
+ /\{\s*label:\s*'GitHub',\s*href:\s*'[^']*'\s*\}/,
212
+ `{ label: 'GitHub', href: '${repoUrl}' }`
213
+ );
214
+ source = source.replace(
215
+ /\{\s*label:\s*'Support',\s*href:\s*'[^']*'\s*\}/,
216
+ `{ label: 'Support', href: '${repoUrl ? `${repoUrl}/issues/new` : ""}' }`
217
+ );
218
218
  writeFileSync(siteFile, source, "utf8");
219
219
  }
220
220
  function patchApiReferenceGuard(targetDir) {
@@ -366,7 +366,7 @@ async function scaffold(options) {
366
366
  }
367
367
  mkdirSync2(targetDir, { recursive: true });
368
368
  const slug = slugify(projectName);
369
- await downloadTemplate(targetDir);
369
+ await downloadTemplate(targetDir, projectName);
370
370
  writeStarterContent(targetDir, projectName, slug, enableAiChat, repoUrl, i18nLocales);
371
371
  updateSiteConfig(targetDir, projectName, description, brandPreset, repoUrl);
372
372
  patchApiReferenceGuard(targetDir);
@@ -3,7 +3,7 @@ import {
3
3
  initGit,
4
4
  installDeps,
5
5
  scaffold
6
- } from "./chunk-YY5QPPAG.js";
6
+ } from "./chunk-23YWNWTH.js";
7
7
 
8
8
  // src/migrate/index.ts
9
9
  import { mkdirSync as mkdirSync2, copyFileSync as copyFileSync2, readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdtempSync, rmSync } from "fs";
package/dist/index.js CHANGED
@@ -2,13 +2,13 @@
2
2
  import {
3
3
  migrateDocs,
4
4
  parseGitHubUrl
5
- } from "./chunk-QB4U3KFX.js";
5
+ } from "./chunk-GVYVHLAP.js";
6
6
  import {
7
7
  logo,
8
8
  scaffold,
9
9
  slugify,
10
10
  success
11
- } from "./chunk-YY5QPPAG.js";
11
+ } from "./chunk-23YWNWTH.js";
12
12
 
13
13
  // src/index.ts
14
14
  import { existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
@@ -99,7 +99,9 @@ async function gatherAnswers(dirArg, useDefaults) {
99
99
  // src/check.ts
100
100
  import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
101
101
  import { join as join2, extname, relative } from "path";
102
+ import { execFileSync } from "child_process";
102
103
  import matter from "gray-matter";
104
+ import { parse as parseYaml } from "yaml";
103
105
 
104
106
  // src/docs-json.ts
105
107
  import { readFileSync, writeFileSync } from "fs";
@@ -115,14 +117,62 @@ function writeDocsJson(projectDir, config) {
115
117
  }
116
118
 
117
119
  // src/check.ts
120
+ function gitLocal(projectDir, args2) {
121
+ try {
122
+ const out = execFileSync("git", args2, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
123
+ return { ok: true, out: out.trim() };
124
+ } catch {
125
+ return { ok: false, out: "" };
126
+ }
127
+ }
128
+ function checkDrift(projectDir, file, data, issues) {
129
+ const sources = data.sources;
130
+ const verifiedCommit = data.verifiedCommit;
131
+ if (!Array.isArray(sources) || sources.length === 0 || typeof verifiedCommit !== "string" || !verifiedCommit.trim()) {
132
+ return;
133
+ }
134
+ const commit = verifiedCommit.trim();
135
+ if (!gitLocal(projectDir, ["cat-file", "-e", `${commit}^{commit}`]).ok) {
136
+ issues.push({
137
+ severity: "warning",
138
+ message: `Cannot verify freshness: verifiedCommit "${commit.slice(0, 8)}" is not in git history \u2014 run with a full clone (fetch-depth: 0).`,
139
+ file
140
+ });
141
+ return;
142
+ }
143
+ for (const src of sources) {
144
+ if (typeof src !== "string" || !src.trim()) continue;
145
+ const colon = src.indexOf(":");
146
+ let filePath = src;
147
+ if (colon > 0) {
148
+ const alias = src.slice(0, colon);
149
+ if (alias !== "." && alias !== "self") {
150
+ issues.push({
151
+ severity: "warning",
152
+ message: `Cross-repo source "${src}" \u2014 drift check skipped (needs the referenced repo; see multi-repo setup).`,
153
+ file
154
+ });
155
+ continue;
156
+ }
157
+ filePath = src.slice(colon + 1);
158
+ }
159
+ filePath = filePath.replace(/^\.\//, "").replace(/#.*$/, "");
160
+ const changed = gitLocal(projectDir, ["log", "--format=%H", `${commit}..HEAD`, "--", filePath]).out;
161
+ if (changed) {
162
+ const n = changed.split("\n").filter(Boolean).length;
163
+ issues.push({
164
+ severity: "warning",
165
+ message: `Drift: source "${src}" changed in ${n} commit(s) since it was verified \u2014 this page may be stale.`,
166
+ file
167
+ });
168
+ }
169
+ }
170
+ }
118
171
  function collectNavPageIds(groups, seen, duplicates) {
119
172
  for (const page of groups) {
120
173
  if (typeof page === "string") {
121
- if (seen.has(page)) {
122
- duplicates.add(page);
123
- } else {
124
- seen.add(page);
125
- }
174
+ if (seen.has(page)) duplicates.add(page);
175
+ else seen.add(page);
126
176
  } else if (page.pages) {
127
177
  collectNavPageIds(page.pages, seen, duplicates);
128
178
  }
@@ -139,11 +189,8 @@ function scanMdx(dir, results) {
139
189
  const fullPath = join2(dir, entry);
140
190
  try {
141
191
  const stat = statSync(fullPath);
142
- if (stat.isDirectory()) {
143
- scanMdx(fullPath, results);
144
- } else if (extname(entry).toLowerCase() === ".mdx") {
145
- results.push(fullPath);
146
- }
192
+ if (stat.isDirectory()) scanMdx(fullPath, results);
193
+ else if (extname(entry).toLowerCase() === ".mdx") results.push(fullPath);
147
194
  } catch {
148
195
  }
149
196
  }
@@ -159,7 +206,80 @@ function addOrphanToNav(projectDir, pageId) {
159
206
  writeDocsJson(projectDir, config);
160
207
  }
161
208
  }
162
- async function runCheck(projectDir, fix) {
209
+ function slugify2(text) {
210
+ return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
211
+ }
212
+ function extractHeadingAnchors(content) {
213
+ const anchors = /* @__PURE__ */ new Set();
214
+ for (const line of content.split("\n")) {
215
+ const m = /^#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
216
+ if (m) anchors.add(slugify2(m[1]));
217
+ }
218
+ return anchors;
219
+ }
220
+ function extractLinks(content) {
221
+ const links = [];
222
+ const lines = content.split("\n");
223
+ let inFence = false;
224
+ for (let i = 0; i < lines.length; i++) {
225
+ if (/^\s*(```|~~~)/.test(lines[i])) {
226
+ inFence = !inFence;
227
+ continue;
228
+ }
229
+ if (inFence) continue;
230
+ const line = lines[i].replace(/`[^`]*`/g, "");
231
+ for (const m of line.matchAll(/\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
232
+ links.push({ target: m[1], line: i + 1 });
233
+ }
234
+ for (const m of line.matchAll(/href=["']([^"']+)["']/g)) {
235
+ links.push({ target: m[1], line: i + 1 });
236
+ }
237
+ }
238
+ return links;
239
+ }
240
+ function pageIdToPath(pageId) {
241
+ return pageId === "introduction" ? "/" : `/${pageId}`;
242
+ }
243
+ function validateOpenApi(projectDir, source, issues) {
244
+ const specPath = join2(projectDir, source);
245
+ if (!existsSync(specPath)) {
246
+ issues.push({ severity: "error", message: `API reference points at "${source}" but the file does not exist`, file: source });
247
+ return;
248
+ }
249
+ let spec;
250
+ try {
251
+ const raw = readFileSync2(specPath, "utf8");
252
+ spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml(raw);
253
+ } catch (err) {
254
+ issues.push({ severity: "error", message: `OpenAPI spec is not valid ${source.endsWith(".json") ? "JSON" : "YAML"}: ${err.message}`, file: source });
255
+ return;
256
+ }
257
+ const s = spec;
258
+ if (typeof s?.openapi !== "string" && typeof s?.swagger !== "string") {
259
+ issues.push({ severity: "error", message: 'OpenAPI spec is missing the "openapi" (or "swagger") version field', file: source });
260
+ }
261
+ if (typeof s?.info !== "object" || s.info === null) {
262
+ issues.push({ severity: "error", message: 'OpenAPI spec is missing the "info" object', file: source });
263
+ }
264
+ const paths = s?.paths;
265
+ if (typeof paths !== "object" || paths === null) {
266
+ issues.push({ severity: "error", message: 'OpenAPI spec is missing the "paths" object', file: source });
267
+ } else {
268
+ const methods = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
269
+ for (const [p, ops] of Object.entries(paths)) {
270
+ if (typeof ops !== "object" || ops === null) {
271
+ issues.push({ severity: "error", message: `OpenAPI path "${p}" is not an object`, file: source });
272
+ continue;
273
+ }
274
+ const hasOp = Object.keys(ops).some((k) => methods.has(k.toLowerCase()));
275
+ if (!hasOp) {
276
+ issues.push({ severity: "warning", message: `OpenAPI path "${p}" has no operations`, file: source });
277
+ }
278
+ }
279
+ }
280
+ }
281
+ async function runCheck(projectDir, options) {
282
+ const { fix, ci } = options;
163
283
  if (!existsSync(join2(projectDir, "docs.json"))) {
164
284
  console.error(`
165
285
  \u274C Not a Dox project: docs.json not found in ${projectDir}
@@ -172,7 +292,11 @@ async function runCheck(projectDir, fix) {
172
292
  const navPageIds = /* @__PURE__ */ new Set();
173
293
  const duplicates = /* @__PURE__ */ new Set();
174
294
  for (const tab of config.tabs) {
175
- if (tab.href || tab.api) continue;
295
+ if (tab.href) {
296
+ if (tab.href.startsWith("/")) navPageIds.add(tab.href.slice(1) || "introduction");
297
+ continue;
298
+ }
299
+ if (tab.api) continue;
176
300
  if (!tab.groups || tab.groups.length === 0) {
177
301
  issues.push({ severity: "error", message: `Tab "${tab.tab}" has no groups and no href \u2014 it will render empty` });
178
302
  continue;
@@ -183,21 +307,17 @@ async function runCheck(projectDir, fix) {
183
307
  issues.push({ severity: "error", message: `[duplicate] "${dup}" appears more than once in docs.json` });
184
308
  }
185
309
  for (const pageId of navPageIds) {
186
- const candidates = [
187
- join2(contentDir, `${pageId}.mdx`),
188
- join2(contentDir, `${pageId}/index.mdx`)
189
- ];
310
+ const candidates = [join2(contentDir, `${pageId}.mdx`), join2(contentDir, `${pageId}/index.mdx`)];
190
311
  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
- });
312
+ issues.push({ severity: "error", message: `"${pageId}" is in docs.json but has no MDX file`, file: `src/content/${pageId}.mdx` });
196
313
  }
197
314
  }
198
315
  const allFiles = [];
199
316
  if (existsSync(contentDir)) scanMdx(contentDir, allFiles);
200
317
  const fixedOrphans = [];
318
+ const validPaths = /* @__PURE__ */ new Set(["/"]);
319
+ const anchorsByPath = /* @__PURE__ */ new Map();
320
+ const linksByFile = [];
201
321
  for (const filePath of allFiles) {
202
322
  const rel = filePath.slice(contentDir.length + 1).replace(/\.mdx$/, "").replace(/\\/g, "/");
203
323
  const pageId = rel.endsWith("/index") ? rel.slice(0, -6) : rel;
@@ -206,37 +326,70 @@ async function runCheck(projectDir, fix) {
206
326
  addOrphanToNav(projectDir, pageId);
207
327
  fixedOrphans.push(pageId);
208
328
  } else {
209
- issues.push({
210
- severity: "warning",
211
- message: `"${pageId}" is not in docs.json nav (orphan)`,
212
- file: relative(projectDir, filePath)
213
- });
329
+ issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative(projectDir, filePath) });
214
330
  }
215
331
  }
216
332
  let data = {};
217
333
  let content = "";
334
+ let lineOffset = 0;
218
335
  try {
219
336
  const raw = readFileSync2(filePath, "utf8");
220
337
  const parsed = matter(raw);
221
338
  data = parsed.data;
222
339
  content = parsed.content;
340
+ lineOffset = raw.slice(0, raw.indexOf(content)).split("\n").length - 1;
223
341
  } catch {
224
342
  issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
225
343
  continue;
226
344
  }
227
345
  const rel2 = relative(projectDir, filePath);
228
- if (!data.title) {
229
- issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
230
- }
231
- if (!data.description) {
232
- issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
233
- }
234
- if (content.trim().length < 50) {
235
- issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
346
+ if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
347
+ if (!data.description) issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
348
+ if (content.trim().length < 50) issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
349
+ if (options.drift) checkDrift(projectDir, rel2, data, issues);
350
+ const path = pageIdToPath(pageId);
351
+ const anchors = extractHeadingAnchors(content);
352
+ validPaths.add(path);
353
+ anchorsByPath.set(path, anchors);
354
+ linksByFile.push({ file: rel2, path, anchors, links: extractLinks(content), offset: lineOffset });
355
+ }
356
+ for (const { file, anchors, links, offset } of linksByFile) {
357
+ for (const { target, line: contentLine } of links) {
358
+ const line = contentLine + offset;
359
+ if (/^(https?:|mailto:|tel:)/i.test(target)) continue;
360
+ if (target.startsWith("#")) {
361
+ const anchor2 = target.slice(1);
362
+ if (anchor2 && !anchors.has(anchor2)) {
363
+ issues.push({ severity: "warning", message: `Broken anchor: "${target}" not found on this page`, file, line });
364
+ }
365
+ continue;
366
+ }
367
+ if (!target.startsWith("/")) continue;
368
+ const [beforeHash, anchor] = target.split("#");
369
+ let path = beforeHash.split("?")[0];
370
+ if (path.length > 1) path = path.replace(/\/$/, "");
371
+ if (path.startsWith("/api") || path.startsWith("/_next") || /\.[a-z0-9]+$/i.test(path)) continue;
372
+ if (!validPaths.has(path)) {
373
+ issues.push({ severity: "error", message: `Broken link: "${target}" \u2014 no page at "${path}"`, file, line });
374
+ } else if (anchor && !anchorsByPath.get(path)?.has(anchor)) {
375
+ issues.push({ severity: "warning", message: `Broken anchor: "${target}" \u2014 no heading "#${anchor}" on that page`, file, line });
376
+ }
236
377
  }
237
378
  }
379
+ for (const tab of config.tabs) {
380
+ if (tab.api?.source) validateOpenApi(projectDir, tab.api.source, issues);
381
+ }
238
382
  const errors = issues.filter((i) => i.severity === "error");
239
383
  const warnings = issues.filter((i) => i.severity === "warning");
384
+ if (ci) {
385
+ for (const issue of issues) {
386
+ const loc = issue.file ? `file=${issue.file}${issue.line ? `,line=${issue.line}` : ""}` : "";
387
+ console.log(`::${issue.severity} ${loc}::${issue.message}`);
388
+ }
389
+ console.log(`
390
+ dox check: ${errors.length} error(s), ${warnings.length} warning(s)`);
391
+ return errors.length > 0 ? 1 : 0;
392
+ }
240
393
  console.log(`
241
394
  Linting ${projectDir}...
242
395
  `);
@@ -250,7 +403,7 @@ async function runCheck(projectDir, fix) {
250
403
  console.log(" ERRORS:");
251
404
  for (const issue of errors) {
252
405
  console.log(` ${issue.message}`);
253
- if (issue.file) console.log(` \u2192 ${issue.file}`);
406
+ if (issue.file) console.log(` \u2192 ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
254
407
  }
255
408
  console.log("");
256
409
  }
@@ -258,7 +411,7 @@ async function runCheck(projectDir, fix) {
258
411
  console.log(" WARNINGS:");
259
412
  for (const issue of warnings) {
260
413
  console.log(` ${issue.message}`);
261
- if (issue.file) console.log(` \u2192 ${issue.file}`);
414
+ if (issue.file) console.log(` \u2192 ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
262
415
  }
263
416
  console.log("");
264
417
  }
@@ -601,8 +754,12 @@ async function runScaffoldCommand() {
601
754
  }
602
755
  async function runCheckCommand() {
603
756
  const projectDir = resolve2(positional[1] ?? ".");
604
- const fix = flags.includes("--fix");
605
- const exitCode = await runCheck(projectDir, fix);
757
+ const exitCode = await runCheck(projectDir, {
758
+ fix: flags.includes("--fix"),
759
+ ci: flags.includes("--ci"),
760
+ external: flags.includes("--external"),
761
+ drift: flags.includes("--drift")
762
+ });
606
763
  process.exit(exitCode);
607
764
  }
608
765
  async function runTranslateSubcommand() {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "../chunk-QB4U3KFX.js";
5
- import "../chunk-YY5QPPAG.js";
4
+ } from "../chunk-GVYVHLAP.js";
5
+ import "../chunk-23YWNWTH.js";
6
6
  export {
7
7
  migrateDocs
8
8
  };
package/dist/scaffold.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scaffold
4
- } from "./chunk-YY5QPPAG.js";
4
+ } from "./chunk-23YWNWTH.js";
5
5
  export {
6
6
  scaffold
7
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dox",
3
- "version": "0.3.4",
3
+ "version": "0.5.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",