@waveso/docs 0.5.0 → 0.7.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/CHANGELOG.md +252 -1
- package/README.md +188 -90
- package/dist/anchors.d.ts +44 -0
- package/dist/anchors.js +76 -0
- package/dist/errors.d.ts +2 -0
- package/dist/frontmatter.d.ts +8 -0
- package/dist/frontmatter.js +7 -1
- package/dist/link-suggestion.d.ts +31 -0
- package/dist/link-suggestion.js +94 -0
- package/dist/next.d.ts +34 -27
- package/dist/next.js +40 -13
- package/dist/plugins/rehype-fallback-heading-ids.js +1 -1
- package/dist/plugins/remark-doc-links.d.ts +33 -1
- package/dist/plugins/remark-doc-links.js +24 -8
- package/dist/react/hero.d.ts +19 -0
- package/dist/react/hero.js +44 -0
- package/dist/react/layout.d.ts +1 -3
- package/dist/react/layout.js +17 -54
- package/dist/react/link-adapter.d.ts +34 -0
- package/dist/react/link-adapter.js +30 -0
- package/dist/react/nav.d.ts +11 -11
- package/dist/react/nav.js +106 -58
- package/dist/react/next-link.d.ts +6 -28
- package/dist/react/next-link.js +45 -24
- package/dist/react/next-nav.d.ts +5 -1
- package/dist/react/next-nav.js +5 -3
- package/dist/react/next-search.js +1 -1
- package/dist/react/shell-labels.d.ts +8 -2
- package/dist/react/toc.d.ts +13 -3
- package/dist/react/toc.js +17 -10
- package/dist/render.d.ts +1 -1
- package/dist/render.js +71 -11
- package/dist/source.js +7 -3
- package/dist/styles.css +1079 -237
- package/dist/types.d.ts +102 -6
- package/package.json +10 -3
package/dist/render.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { collectAnchorIds, splitAnchor } from "./anchors.js";
|
|
1
2
|
import { docsError } from "./docs-error.js";
|
|
2
3
|
import { DEFAULT_DOCS_THEMES, createDocsHighlighter } from "./highlighter.js";
|
|
4
|
+
import { describeSuggestion } from "./link-suggestion.js";
|
|
3
5
|
import { rehypeCaptureToc } from "./plugins/rehype-capture-toc.js";
|
|
4
6
|
import { rehypeCodeFrame } from "./plugins/rehype-code-frame.js";
|
|
5
7
|
import { rehypeNormalizeCodeLanguage, rehypeRestoreExcludedCode } from "./plugins/rehype-code-language.js";
|
|
@@ -8,6 +10,7 @@ import { rehypeFlattenRoots } from "./plugins/rehype-flatten-roots.js";
|
|
|
8
10
|
import { foldSegments, remarkDocLinks, resolveMarkdownLink, splitHref } from "./plugins/remark-doc-links.js";
|
|
9
11
|
import { remarkUnwrapImages } from "./plugins/remark-unwrap-images.js";
|
|
10
12
|
import { remarkYouTube } from "./plugins/remark-youtube.js";
|
|
13
|
+
import { visit } from "unist-util-visit";
|
|
11
14
|
import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
|
|
12
15
|
import { rehypeGithubAlerts } from "rehype-github-alerts";
|
|
13
16
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
@@ -16,7 +19,6 @@ import remarkGfm from "remark-gfm";
|
|
|
16
19
|
import remarkParse from "remark-parse";
|
|
17
20
|
import remarkRehype from "remark-rehype";
|
|
18
21
|
import { unified } from "unified";
|
|
19
|
-
import { visit } from "unist-util-visit";
|
|
20
22
|
import { VFile } from "vfile";
|
|
21
23
|
//#region src/render.ts
|
|
22
24
|
/**
|
|
@@ -121,6 +123,18 @@ function withSuffix(src, suffix) {
|
|
|
121
123
|
return `${src}${suffix}`;
|
|
122
124
|
}
|
|
123
125
|
/** Route without its `?query` / `#anchor`, for existence checks. */
|
|
126
|
+
/**
|
|
127
|
+
* Does this route belong to the host application rather than the docs?
|
|
128
|
+
*
|
|
129
|
+
* Prefix match on a segment boundary, so `/api` covers `/api/keys` and does not
|
|
130
|
+
* accidentally cover `/apiary`.
|
|
131
|
+
*/
|
|
132
|
+
function isExternalRoute(route, external) {
|
|
133
|
+
return external.some((prefix) => {
|
|
134
|
+
const trimmed = prefix.replace(/\/+$/, "");
|
|
135
|
+
return route === trimmed || route.startsWith(`${trimmed}/`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
124
138
|
function toRouteKey(href) {
|
|
125
139
|
const cut = href.search(/[?#]/);
|
|
126
140
|
return cut === -1 ? href : href.slice(0, cut);
|
|
@@ -343,21 +357,65 @@ function createDocsRenderer(options) {
|
|
|
343
357
|
}));
|
|
344
358
|
}
|
|
345
359
|
/**
|
|
346
|
-
*
|
|
360
|
+
* Act on a link problem at the severity the site chose.
|
|
361
|
+
*
|
|
362
|
+
* ⚠️ `'throw'` IS THE DEFAULT AND SHOULD STAY IT. The link was valid in the
|
|
363
|
+
* editor and on GitHub, so a warning in a build log is a warning nobody
|
|
364
|
+
* reads. The setting exists because a migration may knowingly run against an
|
|
365
|
+
* incomplete corpus for a while and the tool cannot know that — which is the
|
|
366
|
+
* same reason Docusaurus has one.
|
|
367
|
+
*/
|
|
368
|
+
function report(severity, code, message) {
|
|
369
|
+
if (severity === "ignore") return;
|
|
370
|
+
if (severity === "throw") throw docsError(code, message);
|
|
371
|
+
console.warn(docsError(code, message).message);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Anchors that point into the page being rendered.
|
|
375
|
+
*
|
|
376
|
+
* ⚠️ HERE, NOT IN THE CORPUS PASS, BECAUSE THIS IS WHERE THE LINE NUMBER IS.
|
|
377
|
+
* The recorded refs carry the source line and the tree still has its
|
|
378
|
+
* positions; both are gone from a returned `RenderedDoc`. A same-page anchor
|
|
379
|
+
* is also the one a writer produces most — every "see below" — so it is worth
|
|
380
|
+
* the better message.
|
|
347
381
|
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
382
|
+
* Cross-page anchors need the other page's ids and are checked by
|
|
383
|
+
* `renderAll`, which is the first point at which they exist.
|
|
350
384
|
*/
|
|
385
|
+
function assertOwnAnchors(file, tree, refs) {
|
|
386
|
+
if (config.onBrokenAnchors === "ignore") return;
|
|
387
|
+
let ids;
|
|
388
|
+
for (const ref of refs) {
|
|
389
|
+
const anchor = splitAnchor(ref.href ?? ref.raw);
|
|
390
|
+
if (anchor === void 0) continue;
|
|
391
|
+
if (anchor.route !== "" && anchor.route !== file.href) continue;
|
|
392
|
+
ids ??= collectAnchorIds(tree);
|
|
393
|
+
if (ids.has(anchor.fragment)) continue;
|
|
394
|
+
report(config.onBrokenAnchors, "broken-anchor", `@waveso/docs: ${describeLink(file, ref)} links to '${ref.raw}', and this page has no '#${anchor.fragment}'.${describeSuggestion(anchor.fragment, ids)} Heading ids come from the heading text, so renaming a heading renames its anchor.`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** Check every link a page recorded, and act at the configured severity. */
|
|
351
398
|
function assertLinks(file, refs) {
|
|
352
399
|
for (const ref of refs) {
|
|
353
|
-
if (ref.
|
|
354
|
-
if (
|
|
400
|
+
if (ref.anchorOnly) continue;
|
|
401
|
+
if (ref.href === void 0) {
|
|
402
|
+
report(config.onBrokenLinks, "broken-link", `@waveso/docs: ${describeLink(file, ref)} links to '${ref.raw}', which does not resolve to a documentation page. Use a path relative to this file, or an absolute URL for external links.`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (knownRoutes === void 0 || ref.asset || ref.anchorOnly) continue;
|
|
355
406
|
const route = toRouteKey(ref.href);
|
|
407
|
+
if (isExternalRoute(route, config.externalRoutes)) continue;
|
|
356
408
|
if (knownRoutes.has(route)) continue;
|
|
357
|
-
if (draftRoutes?.has(route))
|
|
409
|
+
if (draftRoutes?.has(route)) {
|
|
410
|
+
report(config.onBrokenLinks, "draft-link", `@waveso/docs: ${describeLink(file, ref)} links to '${ref.raw}', which resolves to '${route}' — a page marked \`draft: true\`, so it is not published and the link would 404. Publish the page, remove the link, or build with \`includeDrafts\`.`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
358
413
|
const aliasTarget = aliasRoutes?.get(route);
|
|
359
|
-
if (aliasTarget !== void 0)
|
|
360
|
-
|
|
414
|
+
if (aliasTarget !== void 0) {
|
|
415
|
+
report(config.onBrokenLinks, "alias-link", `@waveso/docs: ${describeLink(file, ref)} links to '${ref.raw}', which resolves to '${route}' — an alias that redirects to '${aliasTarget}'. An alias is not a page: it 404s unless \`createDocsRedirects\` is wired into \`next.config.ts\`, and it is never prerendered. Link to '${aliasTarget}' directly.`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
report(config.onBrokenLinks, "broken-link", `@waveso/docs: ${describeLink(file, ref)} links to '${ref.raw}', which resolves to '${route}' — no such page exists.${describeSuggestion(route, knownRoutes)} Fix the link, or add an \`aliases\` entry to the page it used to point at.`);
|
|
361
419
|
}
|
|
362
420
|
}
|
|
363
421
|
return { async render(file) {
|
|
@@ -373,9 +431,11 @@ function createDocsRenderer(options) {
|
|
|
373
431
|
relativePath: file.relativePath
|
|
374
432
|
};
|
|
375
433
|
const hast = await processor.run(processor.parse(vfile), vfile);
|
|
376
|
-
|
|
434
|
+
const hasHero = (file.frontmatter.actions?.length ?? 0) > 0;
|
|
435
|
+
if (titleHeading && !hasHero && !hasHeadingOne(hast)) hast.children.unshift(titleHeadingNode(file.frontmatter.title));
|
|
377
436
|
await resolveImages(hast, file, imageResolver);
|
|
378
|
-
|
|
437
|
+
assertLinks(file, vfile.data.docLinks ?? []);
|
|
438
|
+
assertOwnAnchors(file, hast, vfile.data.docLinks ?? []);
|
|
379
439
|
return {
|
|
380
440
|
frontmatter: file.frontmatter,
|
|
381
441
|
hast: stripPositions(hast),
|
package/dist/source.js
CHANGED
|
@@ -18,7 +18,7 @@ const INDEX_NAME = "index";
|
|
|
18
18
|
* ⚠️ THE SCAN USED TO OPEN EVERY MARKDOWN FILE AT ONCE. `scanDir` recursed into
|
|
19
19
|
* its subdirectories in parallel and read that directory's pages with a bare
|
|
20
20
|
* `Promise.all`, so the number of `readFile` calls in flight equalled the number
|
|
21
|
-
* of markdown files in the entire tree. On a 1,
|
|
21
|
+
* of markdown files in the entire tree. On a 1,201-page corpus and the common
|
|
22
22
|
* 1,024-descriptor soft limit, `next build` died with a bare `EMFILE: too many
|
|
23
23
|
* open files` — no error code, no mention that this was the docs scan, and
|
|
24
24
|
* nothing pointing at the fix. Exactly the large content set this package is
|
|
@@ -71,7 +71,9 @@ function resolveDocsConfig(config) {
|
|
|
71
71
|
),
|
|
72
72
|
basePath: normalizeBasePath(config.basePath ?? "/docs"),
|
|
73
73
|
includeDrafts: config.includeDrafts ?? false,
|
|
74
|
-
|
|
74
|
+
onBrokenLinks: config.onBrokenLinks ?? "throw",
|
|
75
|
+
onBrokenAnchors: config.onBrokenAnchors ?? "throw",
|
|
76
|
+
externalRoutes: config.externalRoutes ?? [],
|
|
75
77
|
...config.frontmatterSchema === void 0 ? {} : { frontmatterSchema: config.frontmatterSchema }
|
|
76
78
|
};
|
|
77
79
|
}
|
|
@@ -114,7 +116,9 @@ function createDocsSource(config) {
|
|
|
114
116
|
resolved.contentDir,
|
|
115
117
|
resolved.basePath,
|
|
116
118
|
resolved.includeDrafts,
|
|
117
|
-
resolved.
|
|
119
|
+
resolved.onBrokenLinks,
|
|
120
|
+
resolved.onBrokenAnchors,
|
|
121
|
+
resolved.externalRoutes.join(","),
|
|
118
122
|
schemaKey(resolved.frontmatterSchema)
|
|
119
123
|
].join("\0");
|
|
120
124
|
const existing = sources.get(key);
|