@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,26 @@
|
|
|
1
|
+
//#region src/map-pooled.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Bounded-concurrency `Promise.all`.
|
|
4
|
+
*
|
|
5
|
+
* Private — deliberately not an entry point in `package.json`.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Like `Promise.all(items.map(fn))`, but with at most `limit` calls in flight.
|
|
9
|
+
*
|
|
10
|
+
* Results keep input order, and the first rejection rejects the whole call,
|
|
11
|
+
* exactly as `Promise.all` does. Workers also stop pulling new items once one
|
|
12
|
+
* has thrown: a build that is going to fail on page 3 should not render the
|
|
13
|
+
* other 1,997 first. In-flight calls are not cancelled — there is nothing to
|
|
14
|
+
* cancel them with — so up to `limit - 1` may still settle after the rejection.
|
|
15
|
+
*
|
|
16
|
+
* `Promise.all` over a whole documentation set is fine only while the mapped
|
|
17
|
+
* function is effectively synchronous — which is true of the markdown pipeline
|
|
18
|
+
* today, and stops being true the moment a consumer wires the async
|
|
19
|
+
* `imageResolver` the docs recommend. At that point every page's tree is live
|
|
20
|
+
* at once, and a 2,000-page site holds 2,000 hast trees plus 2,000 in-flight
|
|
21
|
+
* resolver calls in memory to produce output that was always going to be
|
|
22
|
+
* written in order.
|
|
23
|
+
*/
|
|
24
|
+
declare function mapPooled<TItem, TResult>(items: readonly TItem[], limit: number, fn: (item: TItem, index: number) => Promise<TResult>): Promise<TResult[]>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { mapPooled };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/map-pooled.ts
|
|
2
|
+
/**
|
|
3
|
+
* Bounded-concurrency `Promise.all`.
|
|
4
|
+
*
|
|
5
|
+
* Private — deliberately not an entry point in `package.json`.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Like `Promise.all(items.map(fn))`, but with at most `limit` calls in flight.
|
|
9
|
+
*
|
|
10
|
+
* Results keep input order, and the first rejection rejects the whole call,
|
|
11
|
+
* exactly as `Promise.all` does. Workers also stop pulling new items once one
|
|
12
|
+
* has thrown: a build that is going to fail on page 3 should not render the
|
|
13
|
+
* other 1,997 first. In-flight calls are not cancelled — there is nothing to
|
|
14
|
+
* cancel them with — so up to `limit - 1` may still settle after the rejection.
|
|
15
|
+
*
|
|
16
|
+
* `Promise.all` over a whole documentation set is fine only while the mapped
|
|
17
|
+
* function is effectively synchronous — which is true of the markdown pipeline
|
|
18
|
+
* today, and stops being true the moment a consumer wires the async
|
|
19
|
+
* `imageResolver` the docs recommend. At that point every page's tree is live
|
|
20
|
+
* at once, and a 2,000-page site holds 2,000 hast trees plus 2,000 in-flight
|
|
21
|
+
* resolver calls in memory to produce output that was always going to be
|
|
22
|
+
* written in order.
|
|
23
|
+
*/
|
|
24
|
+
async function mapPooled(items, limit, fn) {
|
|
25
|
+
if (items.length <= limit) return Promise.all(items.map((item, index) => fn(item, index)));
|
|
26
|
+
const results = new Array(items.length);
|
|
27
|
+
let next = 0;
|
|
28
|
+
let failed = false;
|
|
29
|
+
const worker = async () => {
|
|
30
|
+
while (next < items.length && !failed) {
|
|
31
|
+
const index = next++;
|
|
32
|
+
try {
|
|
33
|
+
results[index] = await fn(items[index], index);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
failed = true;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const workers = Math.max(1, Math.min(limit, items.length));
|
|
41
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { mapPooled };
|
package/dist/meta.d.ts
CHANGED
|
@@ -67,9 +67,13 @@ declare function readDocsMeta(dirPath: string): Promise<DocsMeta | undefined>;
|
|
|
67
67
|
* @param entries - The directory's children.
|
|
68
68
|
* @param meta - Its validated `meta.json`, if any.
|
|
69
69
|
* @param metaPath - Path to that `meta.json`, used in error messages.
|
|
70
|
-
* @
|
|
71
|
-
*
|
|
70
|
+
* @param depth - How far this directory sits below the content root, which
|
|
71
|
+
* decides whether its own `index.md` is listed by default. See
|
|
72
|
+
* {@link isListedByDefault}.
|
|
73
|
+
* @throws When `pages` names a child that does not exist, or names one twice —
|
|
74
|
+
* always a typo, and catching it here is the entire reason `meta.json` is
|
|
75
|
+
* validated at build.
|
|
72
76
|
*/
|
|
73
|
-
declare function orderNavEntries(entries: readonly MetaDirEntry[], meta: DocsMeta | undefined, metaPath: string): DocNavNode[];
|
|
77
|
+
declare function orderNavEntries(entries: readonly MetaDirEntry[], meta: DocsMeta | undefined, metaPath: string, depth: number): DocNavNode[];
|
|
74
78
|
//#endregion
|
|
75
79
|
export { MetaDirEntry, docsMetaSchema, orderNavEntries, parseDocsMeta, readDocsMeta };
|
package/dist/meta.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { docsError } from "./docs-error.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -40,7 +41,7 @@ function parseDocsMeta(raw, filePath) {
|
|
|
40
41
|
const result = docsMetaSchema.safeParse(raw);
|
|
41
42
|
if (result.success) return result.data;
|
|
42
43
|
const details = result.error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
43
|
-
throw
|
|
44
|
+
throw docsError("invalid-meta", `Invalid meta.json at ${filePath}:\n${details}`);
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* Read and validate `<dirPath>/meta.json`. Resolves to `undefined` when the
|
|
@@ -60,7 +61,7 @@ async function readDocsMeta(dirPath) {
|
|
|
60
61
|
parsed = JSON.parse(raw);
|
|
61
62
|
} catch (err) {
|
|
62
63
|
const reason = err instanceof Error ? err.message : String(err);
|
|
63
|
-
throw
|
|
64
|
+
throw docsError("invalid-meta", `Could not parse ${filePath} as JSON: ${reason}`, { cause: err });
|
|
64
65
|
}
|
|
65
66
|
return parseDocsMeta(parsed, filePath);
|
|
66
67
|
}
|
|
@@ -75,12 +76,16 @@ async function readDocsMeta(dirPath) {
|
|
|
75
76
|
* @param entries - The directory's children.
|
|
76
77
|
* @param meta - Its validated `meta.json`, if any.
|
|
77
78
|
* @param metaPath - Path to that `meta.json`, used in error messages.
|
|
78
|
-
* @
|
|
79
|
-
*
|
|
79
|
+
* @param depth - How far this directory sits below the content root, which
|
|
80
|
+
* decides whether its own `index.md` is listed by default. See
|
|
81
|
+
* {@link isListedByDefault}.
|
|
82
|
+
* @throws When `pages` names a child that does not exist, or names one twice —
|
|
83
|
+
* always a typo, and catching it here is the entire reason `meta.json` is
|
|
84
|
+
* validated at build.
|
|
80
85
|
*/
|
|
81
|
-
function orderNavEntries(entries, meta, metaPath) {
|
|
86
|
+
function orderNavEntries(entries, meta, metaPath, depth) {
|
|
82
87
|
const pages = meta?.pages;
|
|
83
|
-
if (pages === void 0) return dropEmptyGroups(sortEntries(entries.filter((entry) => isListedByDefault(entry))).map((entry) => entry.node));
|
|
88
|
+
if (pages === void 0) return dropEmptyGroups(sortEntries(entries.filter((entry) => isListedByDefault(entry, depth))).map((entry) => entry.node));
|
|
84
89
|
const byName = indexByName(entries, metaPath);
|
|
85
90
|
const used = /* @__PURE__ */ new Set();
|
|
86
91
|
const nodes = [];
|
|
@@ -104,29 +109,42 @@ function orderNavEntries(entries, meta, metaPath) {
|
|
|
104
109
|
continue;
|
|
105
110
|
}
|
|
106
111
|
if (page === REST) {
|
|
107
|
-
if (restAt !== -1) throw
|
|
112
|
+
if (restAt !== -1) throw docsError("invalid-meta", `${metaPath} has more than one "..." entry. A directory has a single set of unnamed pages, so only one wildcard can be honoured — remove the extra.`);
|
|
108
113
|
restAt = nodes.length;
|
|
109
114
|
continue;
|
|
110
115
|
}
|
|
111
116
|
if (page.startsWith(REST)) {
|
|
112
117
|
const name = page.slice(3);
|
|
113
118
|
const target = byName.get(name);
|
|
114
|
-
if (!target?.inlineChildren) throw
|
|
119
|
+
if (!target?.inlineChildren) throw docsError("invalid-meta", `${metaPath} entry "${page}" expands a directory named "${name}", which is not a subdirectory here. ${describeAvailable(entries, true)}`);
|
|
120
|
+
assertUnused(used, name, page, metaPath);
|
|
115
121
|
used.add(name);
|
|
116
122
|
if (target.indexNode !== void 0) nodes.push(target.indexNode);
|
|
117
123
|
nodes.push(...target.inlineChildren);
|
|
118
124
|
continue;
|
|
119
125
|
}
|
|
120
126
|
const target = byName.get(page);
|
|
121
|
-
if (!target) throw
|
|
127
|
+
if (!target) throw docsError("invalid-meta", `${metaPath} lists "${page}", which does not exist. ${describeAvailable(entries, false)}`);
|
|
128
|
+
assertUnused(used, page, page, metaPath);
|
|
122
129
|
used.add(page);
|
|
123
130
|
if (!target.hidden) nodes.push(target.node);
|
|
124
131
|
}
|
|
125
132
|
if (restAt !== -1) {
|
|
126
|
-
const rest = sortEntries(entries.filter((entry) => isListedByDefault(entry) && !used.has(entry.name))).map((entry) => entry.node);
|
|
133
|
+
const rest = sortEntries(entries.filter((entry) => isListedByDefault(entry, depth) && !used.has(entry.name))).map((entry) => entry.node);
|
|
127
134
|
nodes.splice(restAt, 0, ...rest);
|
|
128
135
|
}
|
|
129
|
-
return dropEmptyGroups(nodes);
|
|
136
|
+
return dropDanglingSeparators(dropEmptyGroups(nodes));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A child named twice is a typo, not an instruction.
|
|
140
|
+
*
|
|
141
|
+
* It renders the page twice, both copies highlighted on the page they link to,
|
|
142
|
+
* and the sidebar keys its items positionally — so there is no duplicate-key
|
|
143
|
+
* warning either. Every other ambiguity in this file is a build error; this
|
|
144
|
+
* one only looked deliberate because nothing checked for it.
|
|
145
|
+
*/
|
|
146
|
+
function assertUnused(used, name, written, metaPath) {
|
|
147
|
+
if (used.has(name)) throw docsError("invalid-meta", `${metaPath} lists "${written}" more than once. It would appear twice in the sidebar, both copies marked as the current page. Remove the duplicate.`);
|
|
130
148
|
}
|
|
131
149
|
/**
|
|
132
150
|
* Index the directory's children by the name `pages` addresses them with.
|
|
@@ -141,7 +159,7 @@ function indexByName(entries, metaPath) {
|
|
|
141
159
|
const byName = /* @__PURE__ */ new Map();
|
|
142
160
|
for (const entry of entries) {
|
|
143
161
|
const clash = byName.get(entry.name);
|
|
144
|
-
if (clash !== void 0) throw
|
|
162
|
+
if (clash !== void 0) throw docsError("invalid-meta", `${metaPath} cannot address "${entry.name}": ${describeEntry(clash)} and ${describeEntry(entry)} both claim that name. Rename one.`);
|
|
145
163
|
byName.set(entry.name, entry);
|
|
146
164
|
}
|
|
147
165
|
return byName;
|
|
@@ -157,9 +175,37 @@ function describeEntry(entry) {
|
|
|
157
175
|
function dropEmptyGroups(nodes) {
|
|
158
176
|
return nodes.filter((node) => node.type !== "group" || node.children.length > 0 || node.href !== void 0);
|
|
159
177
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
178
|
+
/**
|
|
179
|
+
* A separator labels whatever follows it. When that turned out to be nothing —
|
|
180
|
+
* every page in the next group a draft, or two separators in a row after one
|
|
181
|
+
* of them emptied — the heading is left standing over nothing.
|
|
182
|
+
*
|
|
183
|
+
* Runs after {@link dropEmptyGroups}, which is what empties them, and cannot be
|
|
184
|
+
* left to the author to notice: `includeDrafts` is how a docs site is
|
|
185
|
+
* previewed, and it is exactly the mode where the group is not empty.
|
|
186
|
+
*/
|
|
187
|
+
function dropDanglingSeparators(nodes) {
|
|
188
|
+
const kept = [];
|
|
189
|
+
for (const node of [...nodes].reverse()) {
|
|
190
|
+
const next = kept.at(-1);
|
|
191
|
+
if (node.type === "separator" && (next === void 0 || next.type === "separator")) continue;
|
|
192
|
+
kept.push(node);
|
|
193
|
+
}
|
|
194
|
+
return kept.reverse();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* An entry appears without being named: not a draft, and not the directory's
|
|
198
|
+
* own `index.md` — which is the group heading's link, and would be a second
|
|
199
|
+
* entry for a page the sidebar already shows.
|
|
200
|
+
*
|
|
201
|
+
* The content root is the exception, and the case the rule never considered:
|
|
202
|
+
* nothing encloses it, so its `index.md` is the one page that has no heading
|
|
203
|
+
* to carry its href. Without this, the site's landing page is missing from its
|
|
204
|
+
* own sidebar — a reader who follows any link cannot get back — and every
|
|
205
|
+
* install writes a `meta.json` whose only purpose is to undo the default.
|
|
206
|
+
*/
|
|
207
|
+
function isListedByDefault(entry, depth) {
|
|
208
|
+
return entry.hidden !== true && (entry.isIndex !== true || depth === 0);
|
|
163
209
|
}
|
|
164
210
|
function sortEntries(entries) {
|
|
165
211
|
return [...entries].sort(compareEntries);
|
package/dist/next.d.ts
CHANGED
|
@@ -1,48 +1,55 @@
|
|
|
1
|
-
import { DocFile, DocFrontmatter, DocsConfig, ImageResolver, LinkResolver, RenderedDoc } from "./types.js";
|
|
1
|
+
import { DocFile, DocFrontmatter, DocsConfig, ImageResolver, LinkResolver, RenderedDoc, SearchRecord } from "./types.js";
|
|
2
2
|
import { DocsHighlighter, DocsLang, DocsTheme, DocsThemes } from "./highlighter.js";
|
|
3
3
|
import { MarkdownComponents } from "./react/markdown-components.js";
|
|
4
|
+
import { DocsLabels } from "./react/shell-labels.js";
|
|
5
|
+
import { DocsLayoutSearchProps } from "./react/layout.js";
|
|
4
6
|
import { DocsSource } from "./source.js";
|
|
5
7
|
import { ReactNode } from "react";
|
|
8
|
+
import { PluggableList } from "unified";
|
|
9
|
+
import { Options } from "minisearch";
|
|
6
10
|
//#region src/next.d.ts
|
|
7
11
|
interface DocsRouteOptions<TFrontmatter extends DocFrontmatter = DocFrontmatter> extends DocsConfig<TFrontmatter> {
|
|
8
12
|
/** Overrides merged over the Next-flavoured defaults (`next/link` + `next/image`). */
|
|
9
|
-
components?: MarkdownComponents;
|
|
13
|
+
components?: MarkdownComponents | undefined;
|
|
10
14
|
/** Reuse an existing Shiki highlighter. */
|
|
11
|
-
highlighter?: DocsHighlighter | Promise<DocsHighlighter
|
|
15
|
+
highlighter?: DocsHighlighter | Promise<DocsHighlighter> | undefined;
|
|
12
16
|
/** Grammars to load, when building the default highlighter. */
|
|
13
|
-
langs?: readonly DocsLang[];
|
|
17
|
+
langs?: readonly DocsLang[] | undefined;
|
|
14
18
|
/** Theme pair. */
|
|
15
|
-
themes?: DocsThemes;
|
|
19
|
+
themes?: DocsThemes | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Fence languages Shiki must not touch, e.g. `['mermaid']`.
|
|
22
|
+
*
|
|
23
|
+
* The `<pre><code class="language-mermaid">` then reaches your `pre`/`code`
|
|
24
|
+
* component untouched, which is what lets you render a diagram instead of a
|
|
25
|
+
* monochrome block of DSL.
|
|
26
|
+
*/
|
|
27
|
+
excludeLangs?: readonly string[] | undefined;
|
|
16
28
|
/**
|
|
17
29
|
* Prepend an `<h1>` from `frontmatter.title` when the markdown has none.
|
|
18
30
|
* Defaults to `true`; turn it off if your layout renders the title itself.
|
|
19
31
|
*/
|
|
20
|
-
titleHeading?: boolean;
|
|
32
|
+
titleHeading?: boolean | undefined;
|
|
21
33
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
34
|
+
* Extra remark plugins, attached after `remarkGfm` and before link
|
|
35
|
+
* resolution — so anything they emit is folded, contained and asserted like
|
|
36
|
+
* authored markdown.
|
|
25
37
|
*/
|
|
26
|
-
|
|
38
|
+
remarkPlugins?: PluggableList | undefined;
|
|
27
39
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Next's module graph, so nothing re-evaluates a route module when one
|
|
32
|
-
* changes: without this, `next dev` serves whatever it read on the first
|
|
33
|
-
* request until the server restarts, and a file added afterwards is never
|
|
34
|
-
* found. A rescan of a few hundred small files costs single-digit
|
|
35
|
-
* milliseconds; a production build reads the tree once, as it should.
|
|
40
|
+
* Extra rehype plugins, attached after heading ids and permalinks exist and
|
|
41
|
+
* before the code steps — so a `<pre>` is still the author's text rather
|
|
42
|
+
* than Shiki's token spans.
|
|
36
43
|
*/
|
|
37
|
-
|
|
44
|
+
rehypePlugins?: PluggableList | undefined;
|
|
38
45
|
/** Replaces the built-in markdown-link resolution. */
|
|
39
|
-
linkResolver?: LinkResolver;
|
|
46
|
+
linkResolver?: LinkResolver | undefined;
|
|
40
47
|
/**
|
|
41
48
|
* Resolves image `src` to a public URL and intrinsic dimensions. Without one,
|
|
42
49
|
* markdown images render as a plain `<img>`: `next/image` refuses to render
|
|
43
50
|
* without dimensions, and markdown carries none.
|
|
44
51
|
*/
|
|
45
|
-
imageResolver?: ImageResolver;
|
|
52
|
+
imageResolver?: ImageResolver | undefined;
|
|
46
53
|
/**
|
|
47
54
|
* Absolute site origin, e.g. `'https://example.com'`.
|
|
48
55
|
*
|
|
@@ -50,7 +57,65 @@ interface DocsRouteOptions<TFrontmatter extends DocFrontmatter = DocFrontmatter>
|
|
|
50
57
|
* root-relative path, which Next resolves against `metadataBase` — so if you
|
|
51
58
|
* set neither, you ship pages with no usable canonical.
|
|
52
59
|
*/
|
|
53
|
-
siteUrl?: string;
|
|
60
|
+
siteUrl?: string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* MiniSearch overrides for the index {@link DocsRoute.searchIndex} builds.
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ THE IDENTICAL OBJECT MUST REACH THE DIALOG — pass it to `DocsSearch`'s
|
|
65
|
+
* (or `SearchDialog`'s) `miniSearchOptions`. MiniSearch reads `tokenize` and
|
|
66
|
+
* `processTerm` both when indexing and when querying, so applying one here
|
|
67
|
+
* and not there produces an index whose terms no query can spell: zero
|
|
68
|
+
* results, no error, nothing in the console.
|
|
69
|
+
*/
|
|
70
|
+
miniSearchOptions?: Partial<Options<SearchRecord>> | undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Props for {@link DocsRoute.Layout}.
|
|
74
|
+
*
|
|
75
|
+
* Four, and the fourth is a boolean. Everything else a docs shell is asked for
|
|
76
|
+
* turned out to be reachable already: an announcement banner renders *above*
|
|
77
|
+
* `<docs.Layout>` in your own `layout.tsx`, because this does not own `<body>`;
|
|
78
|
+
* a content footer goes inside `children`; and sidebar links, social icons and
|
|
79
|
+
* separators are `DocNavNode`s authored in `meta.json`. The header bar is the
|
|
80
|
+
* one region nothing else can reach, which is what `actions` is for.
|
|
81
|
+
*
|
|
82
|
+
* A `slots` map was the alternative, and it can still be added later — two node
|
|
83
|
+
* props can become a slots map, a slots map cannot become two props.
|
|
84
|
+
*/
|
|
85
|
+
interface DocsLayoutProps {
|
|
86
|
+
children: ReactNode;
|
|
87
|
+
/**
|
|
88
|
+
* Brand at the header start. A string, or your own logo component.
|
|
89
|
+
*
|
|
90
|
+
* `ReactNode`, so it cannot also serve as the `<title>` or as the header's
|
|
91
|
+
* accessible name; the landmark carries a fixed label instead.
|
|
92
|
+
*/
|
|
93
|
+
title?: ReactNode;
|
|
94
|
+
/** Header end, after search: a theme toggle, a version switcher, a link. */
|
|
95
|
+
actions?: ReactNode;
|
|
96
|
+
/**
|
|
97
|
+
* The search trigger. Defaults to on, and the URL is always derived.
|
|
98
|
+
*
|
|
99
|
+
* `false` omits it. An object configures the dialog — `placeholder`,
|
|
100
|
+
* `hotkey`, `miniSearchOptions` and the rest of `DocsSearch`'s surface,
|
|
101
|
+
* minus `indexUrl`.
|
|
102
|
+
*
|
|
103
|
+
* You do not need to pass `miniSearchOptions` here to match what
|
|
104
|
+
* `createDocsRoute` was given: the route's own value is forwarded, so the
|
|
105
|
+
* object that built the index is the object that queries it. Pass one only
|
|
106
|
+
* to override that.
|
|
107
|
+
*/
|
|
108
|
+
search?: boolean | DocsLayoutSearchProps | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* The four strings the shell renders itself: the navigation landmark's name,
|
|
111
|
+
* the drawer's open and close buttons, and the skip link.
|
|
112
|
+
*
|
|
113
|
+
* Everything else a reader sees is your markdown or your `title`. This is the
|
|
114
|
+
* whole of what a non-English site has to say — and it is the fifth prop,
|
|
115
|
+
* added deliberately: a documentation shell nobody can translate is not a
|
|
116
|
+
* shell for the whole ecosystem.
|
|
117
|
+
*/
|
|
118
|
+
labels?: DocsLabels | undefined;
|
|
54
119
|
}
|
|
55
120
|
/** Props Next hands a page in the App Router. */
|
|
56
121
|
interface DocsPageProps {
|
|
@@ -142,13 +207,17 @@ interface DocsRoute<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
|
|
|
142
207
|
* is documented in one place and typed as `false`, not so it can be
|
|
143
208
|
* forwarded.
|
|
144
209
|
*
|
|
145
|
-
* Declaring it is not optional. Next defaults `dynamicParams` to `true`,
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
210
|
+
* Declaring it is not optional. Next defaults `dynamicParams` to `true`, so
|
|
211
|
+
* a URL `generateStaticParams` never listed is still invoked on demand: the
|
|
212
|
+
* route runs on a server at request time to produce a 404 that was already
|
|
213
|
+
* knowable at build time. `output: 'export'` refuses to build without it at
|
|
214
|
+
* all. With the full page set known ahead of time there is nothing to render
|
|
215
|
+
* on demand anyway, and a prerendered 404 is both faster and cacheable.
|
|
216
|
+
*
|
|
217
|
+
* (An earlier version of this note claimed an unlisted URL reached
|
|
218
|
+
* `fs.readFile` and returned HTTP 500. It does not: `find()` is a lookup in
|
|
219
|
+
* the map built by the directory walk, so a miss is just `undefined`. The
|
|
220
|
+
* export is still required, for the reasons above.)
|
|
152
221
|
*/
|
|
153
222
|
dynamicParams: false;
|
|
154
223
|
/**
|
|
@@ -163,11 +232,89 @@ interface DocsRoute<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
|
|
|
163
232
|
*/
|
|
164
233
|
getPage: (segments: string[]) => Promise<RenderedDoc<TFrontmatter> | undefined>;
|
|
165
234
|
/**
|
|
166
|
-
* Every published page, rendered. The
|
|
167
|
-
*
|
|
168
|
-
*
|
|
235
|
+
* Every published page, rendered. The escape hatch behind
|
|
236
|
+
* {@link DocsRoute.searchIndex}, for anyone building their own artifact out
|
|
237
|
+
* of `extractSearchRecords`.
|
|
169
238
|
*/
|
|
170
239
|
renderAll: () => Promise<Array<RenderedDoc<TFrontmatter>>>;
|
|
240
|
+
/**
|
|
241
|
+
* `GET` handler for `app/<basePath>/search-index.json/route.ts`, serving the
|
|
242
|
+
* MiniSearch index the dialog fetches.
|
|
243
|
+
*
|
|
244
|
+
* ```ts
|
|
245
|
+
* // app/docs/search-index.json/route.ts — the whole file
|
|
246
|
+
* import { docs } from '@/lib/docs';
|
|
247
|
+
*
|
|
248
|
+
* export const GET = docs.searchIndex;
|
|
249
|
+
* export const dynamic = 'force-static'; // a literal, see below
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* **`dynamic = 'force-static'` is not optional and must be a literal**, for
|
|
253
|
+
* the same reason as {@link DocsRoute.dynamicParams}: route segment config is
|
|
254
|
+
* parsed out of the module before any of it runs. Without it Next marks the
|
|
255
|
+
* route `ƒ` (Dynamic) and re-renders your entire corpus on every request —
|
|
256
|
+
* from markdown that output tracing did not put in the deployment bundle, so
|
|
257
|
+
* on a serverless host it does not merely get slow, it throws, at the reader,
|
|
258
|
+
* inside the search dialog. The build prints no warning for this, so the
|
|
259
|
+
* handler detects it at runtime and throws with `code:
|
|
260
|
+
* 'search-index-dynamic'` instead of failing quietly.
|
|
261
|
+
*
|
|
262
|
+
* The index is built from the same `renderAll()` → `extractSearchRecords` →
|
|
263
|
+
* `buildSearchIndex` pipeline you could write by hand, with the `charset`-free
|
|
264
|
+
* `application/json` content type, a strong `ETag` and
|
|
265
|
+
* `cache-control: public, max-age=0, must-revalidate` — Next's default for a
|
|
266
|
+
* prerendered route is a year of `s-maxage` with no validator, which on a
|
|
267
|
+
* stable URL means a CDN serving last year's index until someone purges it.
|
|
268
|
+
*/
|
|
269
|
+
searchIndex: () => Promise<Response>;
|
|
270
|
+
/**
|
|
271
|
+
* Default export for `app/<basePath>/layout.tsx` — the entire docs shell.
|
|
272
|
+
*
|
|
273
|
+
* ```tsx
|
|
274
|
+
* // app/docs/layout.tsx — the whole file
|
|
275
|
+
* import '@waveso/docs/styles.css';
|
|
276
|
+
* import { docs } from '@/lib/docs';
|
|
277
|
+
*
|
|
278
|
+
* export default docs.Layout;
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* Or, with your own chrome in the header:
|
|
282
|
+
*
|
|
283
|
+
* ```tsx
|
|
284
|
+
* export default function DocsLayout({ children }: { children: ReactNode }) {
|
|
285
|
+
* return (
|
|
286
|
+
* <docs.Layout title={<Logo />} actions={<ThemeToggle />}>
|
|
287
|
+
* {children}
|
|
288
|
+
* </docs.Layout>
|
|
289
|
+
* );
|
|
290
|
+
* }
|
|
291
|
+
* ```
|
|
292
|
+
*
|
|
293
|
+
* It owns the skip link, the header, the sidebar column, the mobile drawer
|
|
294
|
+
* and the grid, and it reads `source.nav()` and `searchIndexUrl` itself — so
|
|
295
|
+
* there is no nav to fetch and no URL to pass. It does **not** own the table
|
|
296
|
+
* of contents: a Next layout receives `{children, params}` and cannot know
|
|
297
|
+
* which page is rendering, so `docs.Page` emits the TOC as its second child
|
|
298
|
+
* and the grid places it.
|
|
299
|
+
*
|
|
300
|
+
* Your `layout.tsx` stays a Server Component. The two pieces that need a
|
|
301
|
+
* client — the nav's `usePathname`, the search dialog — carry their own
|
|
302
|
+
* `'use client'` boundaries inside the package.
|
|
303
|
+
*
|
|
304
|
+
* Next passes `{ children, params }`; the extra `params` is ignored, which is
|
|
305
|
+
* why `export default docs.Layout` type-checks as a layout.
|
|
306
|
+
*/
|
|
307
|
+
Layout: (props: DocsLayoutProps) => Promise<ReactNode>;
|
|
308
|
+
/**
|
|
309
|
+
* `${basePath}/search-index.json` — hand it to `DocsSearch`'s `indexUrl`.
|
|
310
|
+
*
|
|
311
|
+
* Derived from the route's own `basePath`, so it is right when the docs are
|
|
312
|
+
* mounted at `/`, at `/docs`, or under a nested prefix. It is *not* prefixed
|
|
313
|
+
* with Next's `basePath` config, which Next applies to `<Link>` and to
|
|
314
|
+
* navigation but never to a client `fetch()` — on a site setting that, prefix
|
|
315
|
+
* it yourself.
|
|
316
|
+
*/
|
|
317
|
+
searchIndexUrl: string;
|
|
171
318
|
}
|
|
172
319
|
/**
|
|
173
320
|
* Create the route handlers for a documentation tree.
|
|
@@ -191,9 +338,9 @@ interface DocsSitemapOptions<TFrontmatter extends DocFrontmatter = DocFrontmatte
|
|
|
191
338
|
*/
|
|
192
339
|
siteUrl: string;
|
|
193
340
|
/** Applied to every entry. Omitted by default — Google ignores it anyway. */
|
|
194
|
-
changeFrequency?: DocsSitemapEntry['changeFrequency'];
|
|
341
|
+
changeFrequency?: DocsSitemapEntry['changeFrequency'] | undefined;
|
|
195
342
|
/** Applied to every entry. Omitted by default. */
|
|
196
|
-
priority?: number;
|
|
343
|
+
priority?: number | undefined;
|
|
197
344
|
/**
|
|
198
345
|
* Override the last-modified date per page.
|
|
199
346
|
*
|
|
@@ -253,4 +400,4 @@ interface DocsRedirect {
|
|
|
253
400
|
*/
|
|
254
401
|
declare function createDocsRedirects(config: DocsConfig): Promise<DocsRedirect[]>;
|
|
255
402
|
//#endregion
|
|
256
|
-
export { type DocsLang, DocsPageMetadata, DocsPageProps, DocsRedirect, DocsRoute, DocsRouteOptions, DocsSitemapEntry, DocsSitemapOptions, type DocsTheme, type DocsThemes, createDocsRedirects, createDocsRoute, createDocsSitemap };
|
|
403
|
+
export { type DocsLang, DocsLayoutProps, DocsPageMetadata, DocsPageProps, DocsRedirect, DocsRoute, DocsRouteOptions, DocsSitemapEntry, DocsSitemapOptions, type DocsTheme, type DocsThemes, createDocsRedirects, createDocsRoute, createDocsSitemap };
|