@waveso/docs 0.1.0 → 0.3.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 +194 -0
- package/README.md +592 -88
- package/dist/code-frame.d.ts +29 -0
- package/dist/code-frame.js +41 -0
- package/dist/code-meta.d.ts +48 -0
- package/dist/code-meta.js +72 -0
- package/dist/docs-content-id.d.ts +19 -0
- package/dist/docs-content-id.js +19 -0
- package/dist/docs-error.d.ts +19 -0
- package/dist/docs-error.js +28 -0
- package/dist/errors.d.ts +94 -0
- package/dist/errors.js +45 -0
- package/dist/frontmatter.d.ts +39 -7
- package/dist/frontmatter.js +51 -24
- package/dist/highlighter.d.ts +2 -2
- package/dist/highlighter.js +3 -2
- package/dist/map-pooled.d.ts +26 -0
- package/dist/map-pooled.js +45 -0
- package/dist/meta.d.ts +7 -3
- package/dist/meta.js +61 -15
- package/dist/next.d.ts +182 -35
- package/dist/next.js +177 -49
- package/dist/plugins/rehype-capture-toc.js +52 -20
- package/dist/plugins/rehype-code-frame.d.ts +10 -0
- package/dist/plugins/rehype-code-frame.js +88 -0
- package/dist/plugins/rehype-code-language.d.ts +24 -0
- package/dist/plugins/rehype-code-language.js +54 -0
- package/dist/plugins/rehype-fallback-heading-ids.d.ts +6 -0
- package/dist/plugins/rehype-fallback-heading-ids.js +51 -0
- package/dist/plugins/rehype-flatten-roots.d.ts +7 -0
- package/dist/plugins/rehype-flatten-roots.js +39 -0
- package/dist/plugins/remark-doc-links.d.ts +12 -1
- package/dist/plugins/remark-doc-links.js +147 -20
- package/dist/react/code-runtime.d.ts +14 -0
- package/dist/react/code-runtime.js +161 -0
- package/dist/react/doc-content.d.ts +39 -2
- package/dist/react/doc-content.js +42 -10
- package/dist/react/layout.d.ts +44 -0
- package/dist/react/layout.js +65 -0
- package/dist/react/markdown-components.js +71 -6
- package/dist/react/nav.d.ts +28 -0
- package/dist/react/nav.js +70 -0
- package/dist/react/nearest-scroll-top.d.ts +45 -0
- package/dist/react/nearest-scroll-top.js +44 -0
- package/dist/react/next-link.d.ts +34 -0
- package/dist/react/next-link.js +30 -0
- package/dist/react/next-nav.d.ts +11 -0
- package/dist/react/next-nav.js +32 -0
- package/dist/react/next-search.d.ts +22 -0
- package/dist/react/next-search.js +52 -0
- package/dist/react/search-dialog.d.ts +35 -7
- package/dist/react/search-dialog.js +55 -33
- package/dist/react/shell-labels.d.ts +43 -0
- package/dist/react/shell-labels.js +27 -0
- package/dist/react/sidebar.d.ts +38 -3
- package/dist/react/sidebar.js +104 -12
- package/dist/react/skip-link.d.ts +1 -9
- package/dist/react/skip-link.js +6 -5
- package/dist/react/toc.d.ts +12 -4
- package/dist/react/toc.js +46 -12
- package/dist/react/youtube.d.ts +31 -5
- package/dist/react/youtube.js +76 -52
- package/dist/render.d.ts +78 -10
- package/dist/render.js +137 -54
- package/dist/route-path.d.ts +46 -0
- package/dist/route-path.js +51 -0
- package/dist/search-index.d.ts +22 -21
- package/dist/search-index.js +27 -78
- package/dist/search-options.d.ts +32 -1
- package/dist/search-options.js +66 -3
- package/dist/section-boundary.d.ts +17 -0
- package/dist/section-boundary.js +43 -0
- package/dist/sitemap-limit.d.ts +34 -0
- package/dist/sitemap-limit.js +37 -0
- package/dist/source.d.ts +12 -22
- package/dist/source.js +165 -72
- package/dist/styles.css +1117 -125
- package/dist/types.d.ts +52 -29
- package/package.json +70 -34
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Root } from "hast";
|
|
2
|
+
//#region src/code-frame.d.ts
|
|
3
|
+
/** Marks the `<figure>` a code block is wrapped in. */
|
|
4
|
+
declare const CODE_FRAME_ATTRIBUTE = "data-wave-docs-code";
|
|
5
|
+
/** The copy button inside that figure. */
|
|
6
|
+
declare const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
|
|
7
|
+
/**
|
|
8
|
+
* Set on `<html>` by the runtime once its listener is attached.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ THIS IS WHAT CLOSES THE INERT-MARKUP TRAP, AND IT IS STRUCTURAL RATHER
|
|
11
|
+
* THAN DOCUMENTED. The button is in the HTML whether or not any JavaScript
|
|
12
|
+
* runs — a `renderToStaticMarkup` consumer, a reader with scripts off, anyone
|
|
13
|
+
* rendering the hast by hand. The stylesheet keeps it `visibility: hidden`
|
|
14
|
+
* until this attribute appears, which also takes it out of the tab order, so
|
|
15
|
+
* none of those readers meets a button that silently does nothing or a dead
|
|
16
|
+
* tab stop where a control should be.
|
|
17
|
+
*/
|
|
18
|
+
declare const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
19
|
+
/**
|
|
20
|
+
* Whether a tree contains a code frame.
|
|
21
|
+
*
|
|
22
|
+
* `DocContent` asks before rendering the runtime, so a page with no fences
|
|
23
|
+
* ships zero extra bytes rather than a client component that finds nothing to
|
|
24
|
+
* do. The server has the tree in hand and this is one pass over it, so the
|
|
25
|
+
* check costs nothing the render was not already paying.
|
|
26
|
+
*/
|
|
27
|
+
declare function hasCodeFrame(tree: Root): boolean;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/code-frame.ts
|
|
2
|
+
/** Marks the `<figure>` a code block is wrapped in. */
|
|
3
|
+
const CODE_FRAME_ATTRIBUTE = "data-wave-docs-code";
|
|
4
|
+
/** The copy button inside that figure. */
|
|
5
|
+
const CODE_COPY_ATTRIBUTE = "data-wave-docs-copy";
|
|
6
|
+
/**
|
|
7
|
+
* Set on `<html>` by the runtime once its listener is attached.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ THIS IS WHAT CLOSES THE INERT-MARKUP TRAP, AND IT IS STRUCTURAL RATHER
|
|
10
|
+
* THAN DOCUMENTED. The button is in the HTML whether or not any JavaScript
|
|
11
|
+
* runs — a `renderToStaticMarkup` consumer, a reader with scripts off, anyone
|
|
12
|
+
* rendering the hast by hand. The stylesheet keeps it `visibility: hidden`
|
|
13
|
+
* until this attribute appears, which also takes it out of the tab order, so
|
|
14
|
+
* none of those readers meets a button that silently does nothing or a dead
|
|
15
|
+
* tab stop where a control should be.
|
|
16
|
+
*/
|
|
17
|
+
const CODE_READY_ATTRIBUTE = "data-wave-docs-code-ready";
|
|
18
|
+
/**
|
|
19
|
+
* Whether a tree contains a code frame.
|
|
20
|
+
*
|
|
21
|
+
* `DocContent` asks before rendering the runtime, so a page with no fences
|
|
22
|
+
* ships zero extra bytes rather than a client component that finds nothing to
|
|
23
|
+
* do. The server has the tree in hand and this is one pass over it, so the
|
|
24
|
+
* check costs nothing the render was not already paying.
|
|
25
|
+
*/
|
|
26
|
+
function hasCodeFrame(tree) {
|
|
27
|
+
return containsFrame(tree.children);
|
|
28
|
+
}
|
|
29
|
+
function containsFrame(nodes) {
|
|
30
|
+
for (const node of nodes) {
|
|
31
|
+
if (node.type !== "element") continue;
|
|
32
|
+
if (isCodeFrame(node)) return true;
|
|
33
|
+
if (containsFrame(node.children)) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function isCodeFrame(node) {
|
|
38
|
+
return node.properties[CODE_FRAME_ATTRIBUTE] !== void 0;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { CODE_COPY_ATTRIBUTE, CODE_FRAME_ATTRIBUTE, CODE_READY_ATTRIBUTE, hasCodeFrame };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/code-meta.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The bit after the language on a fence: ```` ```ts title="app/page.tsx" ````.
|
|
4
|
+
*
|
|
5
|
+
* Private — deliberately not an entry point. What a consumer sees is the
|
|
6
|
+
* `<figcaption>` it produces; the grammar is ours to extend.
|
|
7
|
+
*
|
|
8
|
+
* ## Read, never mutate
|
|
9
|
+
*
|
|
10
|
+
* `parseCodeMeta` takes a string and returns a value. It does not rewrite
|
|
11
|
+
* `code.data.meta`, and that restraint is the whole design: Shiki forwards the
|
|
12
|
+
* raw meta string to its transformers as `meta.__raw`, which is how
|
|
13
|
+
* `{1,3-5}` line highlighting and `showLineNumbers` will work when those land.
|
|
14
|
+
* A parser that consumed what it recognised would silently disable every
|
|
15
|
+
* feature it had not been taught about yet.
|
|
16
|
+
*
|
|
17
|
+
* ## Why `title=`
|
|
18
|
+
*
|
|
19
|
+
* Not a design question — a survey. Fumadocs, Starlight (via Expressive Code)
|
|
20
|
+
* and Docusaurus all spell it `title="…"`. Nextra's `filename=` and VitePress's
|
|
21
|
+
* `[filename]` are the minority, and `[…]` has nowhere to grow, so adopting it
|
|
22
|
+
* would mean two grammars in one meta string. `title` collides with frontmatter
|
|
23
|
+
* `title` as a word, which one comment fixes; making three of the five nearest
|
|
24
|
+
* comparators' users learn a second spelling does not have a fix.
|
|
25
|
+
*
|
|
26
|
+
* ## Unknown keys are ignored, malformed ones are not
|
|
27
|
+
*
|
|
28
|
+
* Shiki's meta string is an open namespace shared with its own transformers, so
|
|
29
|
+
* an author pasting `twoslash` or `{1,3}` from another site's documentation must
|
|
30
|
+
* not fail the build. But `title=app/page.tsx` — unquoted — is a *malformed*
|
|
31
|
+
* `title`, and ignoring it silently ships a caption reading `app/page.tsx` only
|
|
32
|
+
* up to the first space, or no caption at all. That throws, naming the file.
|
|
33
|
+
*/
|
|
34
|
+
/** What a fence's meta string asked for. */
|
|
35
|
+
interface CodeMeta {
|
|
36
|
+
/** The `<figcaption>` text, and part of the copy button's accessible name. */
|
|
37
|
+
title?: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read {@link CodeMeta} out of a fence's meta string.
|
|
41
|
+
*
|
|
42
|
+
* `path` is only ever used to name the offending document in an error. The
|
|
43
|
+
* alternative — an error saying `title=` was malformed, on a site with four
|
|
44
|
+
* hundred pages — is a grep.
|
|
45
|
+
*/
|
|
46
|
+
declare function parseCodeMeta(meta: string | undefined, path: string): CodeMeta;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { CodeMeta, parseCodeMeta };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { docsError } from "./docs-error.js";
|
|
2
|
+
//#region src/code-meta.ts
|
|
3
|
+
/**
|
|
4
|
+
* The bit after the language on a fence: ```` ```ts title="app/page.tsx" ````.
|
|
5
|
+
*
|
|
6
|
+
* Private — deliberately not an entry point. What a consumer sees is the
|
|
7
|
+
* `<figcaption>` it produces; the grammar is ours to extend.
|
|
8
|
+
*
|
|
9
|
+
* ## Read, never mutate
|
|
10
|
+
*
|
|
11
|
+
* `parseCodeMeta` takes a string and returns a value. It does not rewrite
|
|
12
|
+
* `code.data.meta`, and that restraint is the whole design: Shiki forwards the
|
|
13
|
+
* raw meta string to its transformers as `meta.__raw`, which is how
|
|
14
|
+
* `{1,3-5}` line highlighting and `showLineNumbers` will work when those land.
|
|
15
|
+
* A parser that consumed what it recognised would silently disable every
|
|
16
|
+
* feature it had not been taught about yet.
|
|
17
|
+
*
|
|
18
|
+
* ## Why `title=`
|
|
19
|
+
*
|
|
20
|
+
* Not a design question — a survey. Fumadocs, Starlight (via Expressive Code)
|
|
21
|
+
* and Docusaurus all spell it `title="…"`. Nextra's `filename=` and VitePress's
|
|
22
|
+
* `[filename]` are the minority, and `[…]` has nowhere to grow, so adopting it
|
|
23
|
+
* would mean two grammars in one meta string. `title` collides with frontmatter
|
|
24
|
+
* `title` as a word, which one comment fixes; making three of the five nearest
|
|
25
|
+
* comparators' users learn a second spelling does not have a fix.
|
|
26
|
+
*
|
|
27
|
+
* ## Unknown keys are ignored, malformed ones are not
|
|
28
|
+
*
|
|
29
|
+
* Shiki's meta string is an open namespace shared with its own transformers, so
|
|
30
|
+
* an author pasting `twoslash` or `{1,3}` from another site's documentation must
|
|
31
|
+
* not fail the build. But `title=app/page.tsx` — unquoted — is a *malformed*
|
|
32
|
+
* `title`, and ignoring it silently ships a caption reading `app/page.tsx` only
|
|
33
|
+
* up to the first space, or no caption at all. That throws, naming the file.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* `title="…"`, anywhere in the string, with the value in double quotes.
|
|
37
|
+
*
|
|
38
|
+
* A filename containing a double quote is inexpressible, and gets no escape
|
|
39
|
+
* mechanism: no real path has one, and an escape grammar is a permanent tax on
|
|
40
|
+
* every reader of this regex to serve a case that does not occur.
|
|
41
|
+
*/
|
|
42
|
+
const TITLE = /(^|\s)title="([^"]*)"/;
|
|
43
|
+
/**
|
|
44
|
+
* A `title=` at a word boundary, however it is spelled.
|
|
45
|
+
*
|
|
46
|
+
* Only consulted once {@link TITLE} has already failed, so reaching this means
|
|
47
|
+
* the author wrote a title the one grammar cannot read — unquoted,
|
|
48
|
+
* single-quoted, or opened and never closed. An earlier version excluded
|
|
49
|
+
* anything followed by a quote, which let `title="a.ts` (no closing quote)
|
|
50
|
+
* fall through and produce no caption at all, silently. That is the failure
|
|
51
|
+
* this function exists to make loud.
|
|
52
|
+
*/
|
|
53
|
+
const MALFORMED_TITLE = /(^|\s)title=/;
|
|
54
|
+
/**
|
|
55
|
+
* Read {@link CodeMeta} out of a fence's meta string.
|
|
56
|
+
*
|
|
57
|
+
* `path` is only ever used to name the offending document in an error. The
|
|
58
|
+
* alternative — an error saying `title=` was malformed, on a site with four
|
|
59
|
+
* hundred pages — is a grep.
|
|
60
|
+
*/
|
|
61
|
+
function parseCodeMeta(meta, path) {
|
|
62
|
+
if (meta === void 0 || meta.trim() === "") return {};
|
|
63
|
+
const matched = TITLE.exec(meta);
|
|
64
|
+
if (matched === null) {
|
|
65
|
+
if (MALFORMED_TITLE.test(meta)) throw docsError("invalid-code-meta", `${path}: a code fence has a malformed \`title\`. Write it as \`title="app/page.tsx"\`, with double quotes — an unquoted title stops at the first space, and a title containing a double quote cannot be expressed. Got: \`${meta.trim()}\``);
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
const title = matched[2];
|
|
69
|
+
return title === void 0 || title === "" ? {} : { title };
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { parseCodeMeta };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/docs-content-id.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The id of the rendered `<article>`, and the target of the skip link.
|
|
4
|
+
*
|
|
5
|
+
* Private — deliberately not an entry point in `package.json`. The documented
|
|
6
|
+
* import is `@waveso/docs/react/skip-link`, which re-exports it; this module
|
|
7
|
+
* exists so `src/next.ts` can have the constant without importing a
|
|
8
|
+
* `'use client'` module for a string.
|
|
9
|
+
*
|
|
10
|
+
* One constant for both halves, and no way to change it. The two files spelled
|
|
11
|
+
* it independently once, and a skip link pointing at an id nothing carries
|
|
12
|
+
* scrolls nowhere and focuses nothing — a failure with no symptom until a
|
|
13
|
+
* keyboard user hits it. `createDocsRoute` used to accept a `contentId` option
|
|
14
|
+
* that broke exactly that pairing, silently, because `SkipLink` had no matching
|
|
15
|
+
* option to follow it with.
|
|
16
|
+
*/
|
|
17
|
+
declare const DOCS_CONTENT_ID = "docs-content";
|
|
18
|
+
//#endregion
|
|
19
|
+
export { DOCS_CONTENT_ID };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/docs-content-id.ts
|
|
2
|
+
/**
|
|
3
|
+
* The id of the rendered `<article>`, and the target of the skip link.
|
|
4
|
+
*
|
|
5
|
+
* Private — deliberately not an entry point in `package.json`. The documented
|
|
6
|
+
* import is `@waveso/docs/react/skip-link`, which re-exports it; this module
|
|
7
|
+
* exists so `src/next.ts` can have the constant without importing a
|
|
8
|
+
* `'use client'` module for a string.
|
|
9
|
+
*
|
|
10
|
+
* One constant for both halves, and no way to change it. The two files spelled
|
|
11
|
+
* it independently once, and a skip link pointing at an id nothing carries
|
|
12
|
+
* scrolls nowhere and focuses nothing — a failure with no symptom until a
|
|
13
|
+
* keyboard user hits it. `createDocsRoute` used to accept a `contentId` option
|
|
14
|
+
* that broke exactly that pairing, silently, because `SkipLink` had no matching
|
|
15
|
+
* option to follow it with.
|
|
16
|
+
*/
|
|
17
|
+
const DOCS_CONTENT_ID = "docs-content";
|
|
18
|
+
//#endregion
|
|
19
|
+
export { DOCS_CONTENT_ID };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DocsError, DocsErrorCode } from "./errors.js";
|
|
2
|
+
//#region src/docs-error.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Build an error that names this package and says what class of thing failed.
|
|
5
|
+
*
|
|
6
|
+
* The message is passed through untouched apart from the prefix, which is added
|
|
7
|
+
* only when absent — the wording at each throw site is the part a human reads,
|
|
8
|
+
* and several of them took real effort to get right.
|
|
9
|
+
*
|
|
10
|
+
* `code` is attached non-enumerably so it does not appear in `JSON.stringify`
|
|
11
|
+
* or a spread, which keeps error objects looking exactly as they did while
|
|
12
|
+
* still being branchable.
|
|
13
|
+
*
|
|
14
|
+
* The stack is left alone. Flattening it would hide the `dist/` frames a
|
|
15
|
+
* consumer does not care about, and also the ones a maintainer needs.
|
|
16
|
+
*/
|
|
17
|
+
declare function docsError(code: DocsErrorCode, message: string, options?: ErrorOptions): DocsError;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { docsError };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DOCS_ERROR_PREFIX } from "./errors.js";
|
|
2
|
+
//#region src/docs-error.ts
|
|
3
|
+
/**
|
|
4
|
+
* Build an error that names this package and says what class of thing failed.
|
|
5
|
+
*
|
|
6
|
+
* The message is passed through untouched apart from the prefix, which is added
|
|
7
|
+
* only when absent — the wording at each throw site is the part a human reads,
|
|
8
|
+
* and several of them took real effort to get right.
|
|
9
|
+
*
|
|
10
|
+
* `code` is attached non-enumerably so it does not appear in `JSON.stringify`
|
|
11
|
+
* or a spread, which keeps error objects looking exactly as they did while
|
|
12
|
+
* still being branchable.
|
|
13
|
+
*
|
|
14
|
+
* The stack is left alone. Flattening it would hide the `dist/` frames a
|
|
15
|
+
* consumer does not care about, and also the ones a maintainer needs.
|
|
16
|
+
*/
|
|
17
|
+
function docsError(code, message, options) {
|
|
18
|
+
const error = new Error(message.startsWith("@waveso/docs: ") ? message : `${DOCS_ERROR_PREFIX}${message}`, options);
|
|
19
|
+
Object.defineProperty(error, "code", {
|
|
20
|
+
value: code,
|
|
21
|
+
enumerable: false,
|
|
22
|
+
writable: false,
|
|
23
|
+
configurable: true
|
|
24
|
+
});
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { docsError };
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The error taxonomy, as public API.
|
|
4
|
+
*
|
|
5
|
+
* Every failure this package raises is an `Error` whose message names the
|
|
6
|
+
* package and which carries a `code` off this union. That exists so a host can
|
|
7
|
+
* branch — downgrade broken-link failures in a preview build, say, or report
|
|
8
|
+
* `invalid-frontmatter` differently from `missing-peer` — and until this
|
|
9
|
+
* module was an entry point there was no supported way to do it. The taxonomy
|
|
10
|
+
* said "branch on this" while the module said "you cannot import me", and the
|
|
11
|
+
* two paragraphs contradicted each other for nineteen codes and forty-five
|
|
12
|
+
* call sites.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { isDocsError } from '@waveso/docs/errors';
|
|
16
|
+
*
|
|
17
|
+
* try {
|
|
18
|
+
* await docs.renderAll();
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* if (isDocsError(error) && error.code === 'draft-link') {
|
|
21
|
+
* console.warn(error.message);
|
|
22
|
+
* } else {
|
|
23
|
+
* throw error;
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* No class is exported, and that is deliberate: `instanceof` against a copy of
|
|
29
|
+
* this module resolved twice — a monorepo with two versions installed, a
|
|
30
|
+
* bundler that duplicates it — silently answers `false`. A string `code` and a
|
|
31
|
+
* structural guard have no such failure mode.
|
|
32
|
+
*/
|
|
33
|
+
/** Prefix every message carries, so a stack trace names the culprit package. */
|
|
34
|
+
declare const DOCS_ERROR_PREFIX = "@waveso/docs: ";
|
|
35
|
+
/**
|
|
36
|
+
* What went wrong, as something a consumer can branch on.
|
|
37
|
+
*
|
|
38
|
+
* Before this existed, every failure was a bare `Error` and roughly half the
|
|
39
|
+
* messages omitted the package prefix, so a host wanting to downgrade (say)
|
|
40
|
+
* broken-link failures in dev had nothing to test but the message text — and
|
|
41
|
+
* `message.startsWith('@waveso/docs:')` was not even a reliable filter.
|
|
42
|
+
*/
|
|
43
|
+
type DocsErrorCode =
|
|
44
|
+
/** A link resolves to a route no published page owns. */
|
|
45
|
+
'broken-link' |
|
|
46
|
+
/** A link resolves to a page that exists but is `draft: true`. */
|
|
47
|
+
'draft-link' |
|
|
48
|
+
/** A link resolves to an alias, which is a redirect and not a page. */
|
|
49
|
+
'alias-link' |
|
|
50
|
+
/** An `aliases` entry is empty, escapes the root, or is not URL-safe. */
|
|
51
|
+
'invalid-alias' |
|
|
52
|
+
/** Two pages claim one alias, or an alias shadows a real route. */
|
|
53
|
+
'alias-collision' |
|
|
54
|
+
/** Two files resolve to the same route. */
|
|
55
|
+
'route-collision' |
|
|
56
|
+
/** A page's frontmatter is missing, malformed, or rejected by the schema. */
|
|
57
|
+
'invalid-frontmatter' |
|
|
58
|
+
/** A `meta.json` is malformed, or names something that is not there. */
|
|
59
|
+
'invalid-meta' |
|
|
60
|
+
/** An option passed to this package cannot be used as given. */
|
|
61
|
+
'invalid-config' |
|
|
62
|
+
/** `contentDir` does not point at a readable directory. */
|
|
63
|
+
'missing-content-dir' |
|
|
64
|
+
/** A markdown page is reachable only through a broken symbolic link. */
|
|
65
|
+
'broken-symlink' |
|
|
66
|
+
/** An `imageResolver` returned an unusable shape, threw, or was needed. */
|
|
67
|
+
'invalid-image' |
|
|
68
|
+
/** A theme name outside the supported set. */
|
|
69
|
+
'unknown-theme' |
|
|
70
|
+
/** A fence language outside the loaded set. */
|
|
71
|
+
'unknown-language' |
|
|
72
|
+
/** An optional peer (`next`) is absent or not the expected shape. */
|
|
73
|
+
'missing-peer' |
|
|
74
|
+
/** The search index could not be fetched or parsed. */
|
|
75
|
+
'search-index-unavailable' |
|
|
76
|
+
/** The search-index route ran at request time instead of being prerendered. */
|
|
77
|
+
'search-index-dynamic' |
|
|
78
|
+
/** A code fence's meta string has a `title` that cannot be read. */
|
|
79
|
+
'invalid-code-meta' |
|
|
80
|
+
/** A plugin ran without the context this package always supplies. */
|
|
81
|
+
'internal';
|
|
82
|
+
/** An {@link Error} carrying a {@link DocsErrorCode}. */
|
|
83
|
+
interface DocsError extends Error {
|
|
84
|
+
readonly code: DocsErrorCode;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Narrow an unknown caught value to one of this package's errors.
|
|
88
|
+
*
|
|
89
|
+
* Checks the prefix as well as the shape, so an unrelated `Error` that happens
|
|
90
|
+
* to carry a `code` — Node's own `ENOENT`, for one — is not mistaken for ours.
|
|
91
|
+
*/
|
|
92
|
+
declare function isDocsError(value: unknown): value is DocsError;
|
|
93
|
+
//#endregion
|
|
94
|
+
export { DOCS_ERROR_PREFIX, DocsError, DocsErrorCode, isDocsError };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* The error taxonomy, as public API.
|
|
4
|
+
*
|
|
5
|
+
* Every failure this package raises is an `Error` whose message names the
|
|
6
|
+
* package and which carries a `code` off this union. That exists so a host can
|
|
7
|
+
* branch — downgrade broken-link failures in a preview build, say, or report
|
|
8
|
+
* `invalid-frontmatter` differently from `missing-peer` — and until this
|
|
9
|
+
* module was an entry point there was no supported way to do it. The taxonomy
|
|
10
|
+
* said "branch on this" while the module said "you cannot import me", and the
|
|
11
|
+
* two paragraphs contradicted each other for nineteen codes and forty-five
|
|
12
|
+
* call sites.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { isDocsError } from '@waveso/docs/errors';
|
|
16
|
+
*
|
|
17
|
+
* try {
|
|
18
|
+
* await docs.renderAll();
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* if (isDocsError(error) && error.code === 'draft-link') {
|
|
21
|
+
* console.warn(error.message);
|
|
22
|
+
* } else {
|
|
23
|
+
* throw error;
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* No class is exported, and that is deliberate: `instanceof` against a copy of
|
|
29
|
+
* this module resolved twice — a monorepo with two versions installed, a
|
|
30
|
+
* bundler that duplicates it — silently answers `false`. A string `code` and a
|
|
31
|
+
* structural guard have no such failure mode.
|
|
32
|
+
*/
|
|
33
|
+
/** Prefix every message carries, so a stack trace names the culprit package. */
|
|
34
|
+
const DOCS_ERROR_PREFIX = "@waveso/docs: ";
|
|
35
|
+
/**
|
|
36
|
+
* Narrow an unknown caught value to one of this package's errors.
|
|
37
|
+
*
|
|
38
|
+
* Checks the prefix as well as the shape, so an unrelated `Error` that happens
|
|
39
|
+
* to carry a `code` — Node's own `ENOENT`, for one — is not mistaken for ours.
|
|
40
|
+
*/
|
|
41
|
+
function isDocsError(value) {
|
|
42
|
+
return value instanceof Error && typeof value.code === "string" && value.message.startsWith("@waveso/docs: ");
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { DOCS_ERROR_PREFIX, isDocsError };
|
package/dist/frontmatter.d.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
|
6
6
|
* The frontmatter fields the package itself understands, as a Zod schema.
|
|
7
7
|
*
|
|
8
8
|
* Optional fields use `.exactOptional()` rather than `.optional()` so the
|
|
9
|
-
* inferred type is `{ description?: string }
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* inferred type is `{ description?: string }`: absent stays absent, which is
|
|
10
|
+
* the only thing YAML can express — there is no way to author an explicit
|
|
11
|
+
* `undefined` in a frontmatter block. A consumer's schema may still use plain
|
|
12
|
+
* `.optional()`; {@link DocFrontmatter} accepts both.
|
|
13
13
|
*
|
|
14
14
|
* `description` is deliberately not length-capped. The 155–160 character
|
|
15
15
|
* advice for `<meta name="description">` is a pixel-width heuristic about
|
|
@@ -39,6 +39,17 @@ declare const docFrontmatterSchema: z.ZodObject<{
|
|
|
39
39
|
* `raw` is whatever the YAML parser produced — `unknown` by construction, so
|
|
40
40
|
* every field is checked rather than trusted.
|
|
41
41
|
*
|
|
42
|
+
* The six fields this package reads are always parsed from the raw block by
|
|
43
|
+
* {@link docFrontmatterSchema} and laid back over the result, so a custom
|
|
44
|
+
* `schema` can only ever *add* fields. It cannot drop `draft`, `aliases`,
|
|
45
|
+
* `order`, `label`, `description` or `title` — which a bare
|
|
46
|
+
* `z.object({ title, audience })` silently does — and it cannot redefine them
|
|
47
|
+
* into a shape the sidebar, the redirects and the sitemap do not expect. The
|
|
48
|
+
* price is that a `.default()`, `.transform()` or `.coerce` aimed at one of the
|
|
49
|
+
* six is not honoured: the YAML wins. The return type intersects
|
|
50
|
+
* {@link DocFrontmatter} for the same reason — the built-ins are there whether
|
|
51
|
+
* or not the schema declared them.
|
|
52
|
+
*
|
|
42
53
|
* Async because `~standard.validate` is allowed to return a promise and some
|
|
43
54
|
* validators do (any schema with an async refinement). The source layer is
|
|
44
55
|
* already async, so awaiting here costs nothing and refusing promises would
|
|
@@ -47,9 +58,30 @@ declare const docFrontmatterSchema: z.ZodObject<{
|
|
|
47
58
|
* @param raw - Parsed YAML frontmatter block.
|
|
48
59
|
* @param filePath - Path reported in the error. Pass the path the author
|
|
49
60
|
* would recognise (relative to the content root), not an absolute one.
|
|
50
|
-
|
|
61
|
+
*/
|
|
62
|
+
declare function parseFrontmatter(raw: unknown, filePath: string): Promise<DocFrontmatter>;
|
|
63
|
+
/**
|
|
64
|
+
* Validate one file's frontmatter against `schema`, or throw an error that
|
|
65
|
+
* names the file. See the two-argument overload for the contract.
|
|
66
|
+
*
|
|
67
|
+
* `TFrontmatter` comes from the schema and must never be named at the call
|
|
68
|
+
* site. That is what the two overloads buy: `parseFrontmatter<Mine>(raw, file)`
|
|
69
|
+
* with no schema is a compile error, where before it type-checked and then lied
|
|
70
|
+
* — nothing validated `Mine`, so its fields were `undefined` at runtime while
|
|
71
|
+
* typed as present.
|
|
72
|
+
*
|
|
73
|
+
* `undefined` is accepted here rather than the schema being optional so that a
|
|
74
|
+
* `DocsConfig<T>`'s own optional schema passes straight through. An earlier
|
|
75
|
+
* attempt made it required and broke exactly that, at `createDocsRoute` and at
|
|
76
|
+
* any consumer holding a typed config.
|
|
77
|
+
*
|
|
78
|
+
* @param raw - Parsed YAML frontmatter block.
|
|
79
|
+
* @param filePath - Path reported in the error. Pass the path the author
|
|
80
|
+
* would recognise (relative to the content root), not an absolute one.
|
|
81
|
+
* @param schema - Replacement schema, normally
|
|
51
82
|
* `docFrontmatterSchema.extend(...)`. Any Standard Schema validator works.
|
|
83
|
+
* `undefined` selects {@link docFrontmatterSchema}.
|
|
52
84
|
*/
|
|
53
|
-
declare function parseFrontmatter<TFrontmatter extends DocFrontmatter
|
|
85
|
+
declare function parseFrontmatter<TFrontmatter extends DocFrontmatter>(raw: unknown, filePath: string, schema: StandardSchemaV1<unknown, TFrontmatter> | undefined): Promise<TFrontmatter & DocFrontmatter>;
|
|
54
86
|
//#endregion
|
|
55
|
-
export { docFrontmatterSchema, parseFrontmatter };
|
|
87
|
+
export { docFrontmatterSchema, parseFrontmatter, z };
|
package/dist/frontmatter.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { docsError } from "./docs-error.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
//#region src/frontmatter.ts
|
|
3
4
|
/**
|
|
4
5
|
* The frontmatter fields the package itself understands, as a Zod schema.
|
|
5
6
|
*
|
|
6
7
|
* Optional fields use `.exactOptional()` rather than `.optional()` so the
|
|
7
|
-
* inferred type is `{ description?: string }
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* inferred type is `{ description?: string }`: absent stays absent, which is
|
|
9
|
+
* the only thing YAML can express — there is no way to author an explicit
|
|
10
|
+
* `undefined` in a frontmatter block. A consumer's schema may still use plain
|
|
11
|
+
* `.optional()`; {@link DocFrontmatter} accepts both.
|
|
11
12
|
*
|
|
12
13
|
* `description` is deliberately not length-capped. The 155–160 character
|
|
13
14
|
* advice for `<meta name="description">` is a pixel-width heuristic about
|
|
@@ -32,34 +33,60 @@ const docFrontmatterSchema = z.object({
|
|
|
32
33
|
order: z.number().exactOptional()
|
|
33
34
|
});
|
|
34
35
|
/**
|
|
35
|
-
*
|
|
36
|
+
* The package's own fields, every one of them optional, for the overlay pass
|
|
37
|
+
* in {@link parseFrontmatter}.
|
|
36
38
|
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* already async, so awaiting here costs nothing and refusing promises would
|
|
43
|
-
* have made a documented half of the spec silently unsupported.
|
|
44
|
-
*
|
|
45
|
-
* @param raw - Parsed YAML frontmatter block.
|
|
46
|
-
* @param filePath - Path reported in the error. Pass the path the author
|
|
47
|
-
* would recognise (relative to the content root), not an absolute one.
|
|
48
|
-
* @param schema - Optional replacement schema, normally
|
|
49
|
-
* `docFrontmatterSchema.extend(...)`. Any Standard Schema validator works.
|
|
39
|
+
* `title` is optional here too, even though the package requires it: a custom
|
|
40
|
+
* schema may legitimately produce a title the YAML does not carry (a
|
|
41
|
+
* `.default()`), and demanding one of the raw block would fail that file. The
|
|
42
|
+
* check after the merge reads the merged title instead, so both routes to a
|
|
43
|
+
* title are accepted and no route to a missing one is.
|
|
50
44
|
*/
|
|
45
|
+
const builtInFields = docFrontmatterSchema.partial();
|
|
51
46
|
async function parseFrontmatter(raw, filePath, schema) {
|
|
52
47
|
const active = schema ?? docFrontmatterSchema;
|
|
53
48
|
let result;
|
|
54
49
|
try {
|
|
55
50
|
result = await active["~standard"].validate(raw ?? {});
|
|
56
51
|
} catch (error) {
|
|
57
|
-
throw
|
|
52
|
+
throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}: the schema threw while validating it. This is a bug in the schema, not in the YAML — check any \`refine\`/\`transform\`/\`check\` it declares.`, { cause: error });
|
|
58
53
|
}
|
|
59
|
-
if (result.issues === void 0) return result.value;
|
|
60
|
-
if (result.issues.length === 0) throw
|
|
61
|
-
const
|
|
62
|
-
throw
|
|
54
|
+
if (result.issues === void 0) return active === docFrontmatterSchema ? result.value : applyBuiltIns(result.value, raw, filePath);
|
|
55
|
+
if (result.issues.length === 0) throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}: the schema rejected it but reported no issues, so there is nothing to act on. This is a bug in the schema.`);
|
|
56
|
+
const source = schema === void 0 ? "Fix the YAML block at the top of that file." : "Fix the YAML block at the top of that file, or the `frontmatterSchema` in your docs config.";
|
|
57
|
+
throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}:\n${formatIssues(result.issues)}\n${source}`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Re-read the package's own fields from the raw block and lay them back over a
|
|
61
|
+
* custom schema's output.
|
|
62
|
+
*
|
|
63
|
+
* Without this, the guarantee that a `frontmatterSchema` keeps the built-ins
|
|
64
|
+
* rests on `TFrontmatter extends DocFrontmatter` — which constrains `title` and
|
|
65
|
+
* nothing else, because the other five fields are optional. So
|
|
66
|
+
* `z.object({ title, audience })`, written from scratch instead of as
|
|
67
|
+
* `docFrontmatterSchema.extend(...)`, type-checks and then strips `draft`,
|
|
68
|
+
* `aliases`, `order` and `label` on the way through. The package reads the
|
|
69
|
+
* absence as "not a draft, no redirects, no order": an unpublished page ships,
|
|
70
|
+
* is linked from the sidebar and is submitted to Google in the sitemap, and
|
|
71
|
+
* every renamed page 404s. `tsc` reports nothing, at either end.
|
|
72
|
+
*
|
|
73
|
+
* The overlay runs after the custom schema rather than instead of it, so a
|
|
74
|
+
* stricter rule on a built-in (`title: z.string().max(60)`) still fails the
|
|
75
|
+
* build — it just cannot change the value the package then reads.
|
|
76
|
+
*/
|
|
77
|
+
async function applyBuiltIns(value, raw, filePath) {
|
|
78
|
+
const result = await builtInFields["~standard"].validate(raw ?? {});
|
|
79
|
+
if (result.issues !== void 0) throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}:\n${formatIssues(result.issues)}\n@waveso/docs reads these fields itself, so they are validated even when your \`frontmatterSchema\` does not declare them. Fix the YAML block at the top of that file.`);
|
|
80
|
+
const merged = {
|
|
81
|
+
...value,
|
|
82
|
+
...result.value
|
|
83
|
+
};
|
|
84
|
+
if (typeof merged.title !== "string" || merged.title.length === 0) throw docsError("invalid-frontmatter", `Invalid frontmatter in ${filePath}: no \`title\`. Every page needs one — it drives the \`<h1>\` fallback, \`<title>\`, the sidebar and search. Add \`title:\` to the YAML block at the top of that file, or give your \`frontmatterSchema\` a title it can supply.`);
|
|
85
|
+
return merged;
|
|
86
|
+
}
|
|
87
|
+
/** Render a validator's issues as the bullet list under the file name. */
|
|
88
|
+
function formatIssues(issues) {
|
|
89
|
+
return issues.map((issue) => ` - ${formatIssuePath(issue.path)}: ${issue.message}`).join("\n");
|
|
63
90
|
}
|
|
64
91
|
/**
|
|
65
92
|
* Render an issue path as `aliases[0]` / `title`, never as `""`.
|
|
@@ -77,4 +104,4 @@ function formatIssuePath(path) {
|
|
|
77
104
|
}, "");
|
|
78
105
|
}
|
|
79
106
|
//#endregion
|
|
80
|
-
export { docFrontmatterSchema, parseFrontmatter };
|
|
107
|
+
export { docFrontmatterSchema, parseFrontmatter, z };
|
package/dist/highlighter.d.ts
CHANGED
|
@@ -81,9 +81,9 @@ interface DocsHighlighterOptions {
|
|
|
81
81
|
* than a build-time throw. The runtime check below stays for JavaScript
|
|
82
82
|
* callers and for values that arrive from JSON config.
|
|
83
83
|
*/
|
|
84
|
-
langs?: readonly DocsLang[];
|
|
84
|
+
langs?: readonly DocsLang[] | undefined;
|
|
85
85
|
/** Theme pair. Defaults to {@link DEFAULT_DOCS_THEMES}. */
|
|
86
|
-
themes?: DocsThemes;
|
|
86
|
+
themes?: DocsThemes | undefined;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
89
|
* Create (or reuse) the process-wide highlighter for a given option set.
|
package/dist/highlighter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { docsError } from "./docs-error.js";
|
|
1
2
|
import { createHighlighterCore } from "shiki/core";
|
|
2
3
|
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
3
4
|
//#region src/highlighter.ts
|
|
@@ -155,11 +156,11 @@ function isDocsTheme(name) {
|
|
|
155
156
|
function createDocsHighlighter(options = {}) {
|
|
156
157
|
const themes = options.themes ?? DEFAULT_DOCS_THEMES;
|
|
157
158
|
const requested = options.langs ?? DEFAULT_DOCS_LANGS;
|
|
158
|
-
for (const theme of [themes.light, themes.dark]) if (!isDocsTheme(theme)) throw
|
|
159
|
+
for (const theme of [themes.light, themes.dark]) if (!isDocsTheme(theme)) throw docsError("unknown-theme", `@waveso/docs: unknown Shiki theme '${theme}'. Supported themes: ${Object.keys(THEME_LOADERS).sort().join(", ")}. To use another theme, pass your own highlighter to createDocsRenderer().`);
|
|
159
160
|
const langs = [...new Set(requested)].sort();
|
|
160
161
|
const loaders = /* @__PURE__ */ new Set();
|
|
161
162
|
for (const lang of langs) {
|
|
162
|
-
if (!isDocsLang(lang)) throw
|
|
163
|
+
if (!isDocsLang(lang)) throw docsError("unknown-language", `@waveso/docs: unknown code language '${lang}'. Supported languages: ${Object.keys(LANG_LOADERS).sort().join(", ")}. To use another grammar, pass your own highlighter to createDocsRenderer().`);
|
|
163
164
|
loaders.add(LANG_LOADERS[lang]);
|
|
164
165
|
}
|
|
165
166
|
const key = JSON.stringify({
|