@waveso/docs 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/dist/frontmatter.d.ts +55 -0
  4. package/dist/frontmatter.js +80 -0
  5. package/dist/highlighter.d.ts +99 -0
  6. package/dist/highlighter.js +183 -0
  7. package/dist/meta.d.ts +75 -0
  8. package/dist/meta.js +183 -0
  9. package/dist/next.d.ts +256 -0
  10. package/dist/next.js +365 -0
  11. package/dist/plugins/rehype-capture-toc.d.ts +18 -0
  12. package/dist/plugins/rehype-capture-toc.js +69 -0
  13. package/dist/plugins/remark-doc-links.d.ts +63 -0
  14. package/dist/plugins/remark-doc-links.js +122 -0
  15. package/dist/plugins/remark-unwrap-images.d.ts +11 -0
  16. package/dist/plugins/remark-unwrap-images.js +25 -0
  17. package/dist/plugins/remark-youtube.d.ts +22 -0
  18. package/dist/plugins/remark-youtube.js +84 -0
  19. package/dist/react/callout.d.ts +37 -0
  20. package/dist/react/callout.js +113 -0
  21. package/dist/react/doc-content.d.ts +29 -0
  22. package/dist/react/doc-content.js +30 -0
  23. package/dist/react/markdown-components.d.ts +84 -0
  24. package/dist/react/markdown-components.js +122 -0
  25. package/dist/react/search-dialog.d.ts +41 -0
  26. package/dist/react/search-dialog.js +404 -0
  27. package/dist/react/sidebar.d.ts +29 -0
  28. package/dist/react/sidebar.js +196 -0
  29. package/dist/react/skip-link.d.ts +37 -0
  30. package/dist/react/skip-link.js +37 -0
  31. package/dist/react/toc.d.ts +35 -0
  32. package/dist/react/toc.js +87 -0
  33. package/dist/react/youtube.d.ts +27 -0
  34. package/dist/react/youtube.js +75 -0
  35. package/dist/render.d.ts +72 -0
  36. package/dist/render.js +279 -0
  37. package/dist/search-index.d.ts +51 -0
  38. package/dist/search-index.js +274 -0
  39. package/dist/search-options.d.ts +18 -0
  40. package/dist/search-options.js +40 -0
  41. package/dist/source.d.ts +67 -0
  42. package/dist/source.js +332 -0
  43. package/dist/styles.css +1033 -0
  44. package/dist/types.d.ts +334 -0
  45. package/dist/types.js +0 -0
  46. package/package.json +166 -0
@@ -0,0 +1,334 @@
1
+ import { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { Root } from "hast";
3
+ //#region src/types.d.ts
4
+ /**
5
+ * The frontmatter fields the package itself understands.
6
+ *
7
+ * Consumers extend this with their own schema — {@link DocsConfig.frontmatterSchema} —
8
+ * and the extra fields flow through the generic parameter on {@link DocFile}
9
+ * and friends rather than widening this interface.
10
+ */
11
+ interface DocFrontmatter {
12
+ /** Page title. Used for `<h1>` fallbacks, `<title>`, and search. */
13
+ title: string;
14
+ /** One-line summary. Used for `<meta name="description">` and search. */
15
+ description?: string;
16
+ /**
17
+ * Sidebar label, when it should differ from {@link DocFrontmatter.title}.
18
+ * Sidebars are narrow; page ancestors are not.
19
+ */
20
+ label?: string;
21
+ /**
22
+ * Excluded from navigation, search and `generateStaticParams`.
23
+ *
24
+ * Deliberately not tied to `NODE_ENV`: Vercel preview deployments are
25
+ * production builds, so branching on it would hide drafts in exactly the
26
+ * place reviewers look. Gate on an explicit config flag instead.
27
+ */
28
+ draft?: boolean;
29
+ /**
30
+ * Previous URLs for this page, relative to the docs base path
31
+ * (e.g. `['old-name', 'legacy/old-name']`). The Next adapter turns these
32
+ * into permanent redirects so a rename never becomes a silent 404.
33
+ */
34
+ aliases?: string[];
35
+ /**
36
+ * Sort weight within its directory, for directories without a `meta.json`.
37
+ * Lower sorts first; pages without an order sort last, alphabetically.
38
+ */
39
+ order?: number;
40
+ }
41
+ /**
42
+ * A single documentation page discovered on disk.
43
+ *
44
+ * `segments` is the canonical identity; `slug` and `href` are derived and
45
+ * cached for convenience. An `index.md` at the content root has an empty
46
+ * `segments` array and maps to the base path itself.
47
+ */
48
+ interface DocFile<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
49
+ /** Route segments, e.g. `['api', 'auth']`. Empty for the root index page. */
50
+ segments: string[];
51
+ /** `segments.join('/')`, e.g. `'api/auth'`. Empty string for the index. */
52
+ slug: string;
53
+ /** Fully-qualified route, e.g. `'/docs/api/auth'`. */
54
+ href: string;
55
+ /** Absolute path on disk. */
56
+ filePath: string;
57
+ /** Path relative to the content root, e.g. `'api/auth.md'`. */
58
+ relativePath: string;
59
+ /** Validated frontmatter. */
60
+ frontmatter: TFrontmatter;
61
+ /** Markdown body with the frontmatter block removed. */
62
+ content: string;
63
+ }
64
+ /** A link to a documentation page. */
65
+ interface DocNavPage {
66
+ type: 'page';
67
+ title: string;
68
+ href: string;
69
+ slug: string;
70
+ }
71
+ /**
72
+ * A directory. `href` is present when the directory has an `index.md`, in
73
+ * which case the group heading is itself a link.
74
+ */
75
+ interface DocNavGroup {
76
+ type: 'group';
77
+ title: string;
78
+ href?: string;
79
+ children: DocNavNode[];
80
+ }
81
+ /** A non-interactive heading between groups, from `"---Label---"` in meta.json. */
82
+ interface DocNavSeparator {
83
+ type: 'separator';
84
+ title: string;
85
+ }
86
+ /** An arbitrary external link, from an object entry in meta.json. */
87
+ interface DocNavLink {
88
+ type: 'link';
89
+ title: string;
90
+ href: string;
91
+ external: boolean;
92
+ }
93
+ type DocNavNode = DocNavPage | DocNavGroup | DocNavSeparator | DocNavLink;
94
+ /**
95
+ * Per-directory ordering and labelling, read from `meta.json`.
96
+ *
97
+ * Chosen over numeric filename prefixes because a filename cannot express
98
+ * separators, external links, or a directory title — all of which a real
99
+ * docs sidebar needs — and because `01_mental_model.md` violates kebab-case
100
+ * twice over.
101
+ *
102
+ * `pages` accepts:
103
+ * - `"getting-started"` — a file or subdirectory in this directory
104
+ * - `"---Reference---"` — a separator with the enclosed label
105
+ * - `"..."` — everything not named explicitly, alphabetically
106
+ * - `"...api"` — expand the `api` subdirectory inline
107
+ * - `{ title, href }` — an arbitrary link
108
+ */
109
+ interface DocsMeta {
110
+ /** Directory title, shown as the group heading. Defaults to the dirname. */
111
+ title?: string;
112
+ /** Ordered entries. Omit to sort by frontmatter `order`, then alphabetically. */
113
+ pages?: Array<string | {
114
+ title: string;
115
+ href: string;
116
+ }>;
117
+ }
118
+ /**
119
+ * A heading captured from the rendered tree.
120
+ *
121
+ * These ids come from the same `rehype-slug` pass that annotated the document,
122
+ * so anchors match by construction rather than by a second parse with a
123
+ * separately-seeded slugger — which drifts precisely where it hurts, on
124
+ * duplicate headings that get `-1` collision suffixes.
125
+ */
126
+ interface TocEntry {
127
+ id: string;
128
+ text: string;
129
+ /** Heading level, 2–6. `h1` is the page title and is never in the TOC. */
130
+ depth: number;
131
+ children: TocEntry[];
132
+ }
133
+ /**
134
+ * A parsed, highlighted document, ready to be turned into React elements.
135
+ *
136
+ * `hast` is a plain serialisable tree — it survives the RSC boundary, a
137
+ * `JSON.stringify` round-trip through any build-time artifact — a cache file,
138
+ * a bundler virtual module — equally well. Rendering it is the consumer's
139
+ * business.
140
+ */
141
+ interface RenderedDoc<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
142
+ frontmatter: TFrontmatter;
143
+ hast: Root;
144
+ toc: TocEntry[];
145
+ /** Route segments of the document this was rendered from. */
146
+ segments: string[];
147
+ href: string;
148
+ }
149
+ /**
150
+ * One indexable unit: a heading and the prose beneath it.
151
+ *
152
+ * Section-scoped rather than page-scoped so a hit can deep-link to the right
153
+ * heading instead of dropping the reader at the top of a 2,000-word page.
154
+ * The byte saving over full-page records is real but modest (~1.4x) — the
155
+ * deep link is the point.
156
+ */
157
+ interface SearchRecord {
158
+ /**
159
+ * Stable id: `slug#anchor`, or the bare slug for a page's lead section.
160
+ *
161
+ * ⚠️ NOT THE `href`, WHICH IS WHAT IT USED TO BE. Two reasons, and both are
162
+ * about the shipped `search-index.json` rather than about tidiness. It
163
+ * carried the whole route twice per record, on the one artifact the README
164
+ * sells on download size. And an href embeds `basePath`, so a site moving
165
+ * from `/docs` to `/reference` changed the identity of every record for no
166
+ * reason. A slug survives that.
167
+ */
168
+ id: string;
169
+ /** Page title. */
170
+ title: string;
171
+ /** Heading text for this section; equals `title` for the lead section. */
172
+ heading: string;
173
+ /**
174
+ * Ancestor headings, outermost first.
175
+ *
176
+ * NAMED FOR WHAT IT IS, not for what the dialog does with it. This was
177
+ * `titles`, which sat between `title` and `heading` and meant neither — three
178
+ * fields whose names differed by a plural. The UI turning these into
179
+ * breadcrumbs is the UI's decision; the record is data.
180
+ */
181
+ ancestors: string[];
182
+ /** Route including the anchor, e.g. `/docs/api/auth#bearer-token`. */
183
+ href: string;
184
+ /** Plain text of the section, truncated to the configured excerpt length. */
185
+ text: string;
186
+ }
187
+ /**
188
+ * Where a link or an image was authored: the route AND the directory.
189
+ *
190
+ * ⚠️ BOTH, AND THE DIFFERENCE IS THE ENTIRE REASON THIS IS AN OBJECT. `api.md`
191
+ * and `api/index.md` produce IDENTICAL route segments — `['api']` — and resolve
192
+ * `./auth.md` to different pages, because one folds against the content root
193
+ * and the other against `api/`. Only the on-disk path separates them.
194
+ *
195
+ * The resolvers used to receive route segments alone, so a custom resolver was
196
+ * handed strictly less than the built-in one had and could not get
197
+ * `./sibling.md` right on any directory index page. No rule recovers the
198
+ * directory from the route, which is why the fix had to be the argument rather
199
+ * than a note in the docs.
200
+ */
201
+ interface DocLinkContext {
202
+ /** Route segments of the containing document, e.g. `['api', 'auth']`. */
203
+ segments: string[];
204
+ /**
205
+ * Directory segments of the SOURCE FILE, relative to the content root:
206
+ * `['api']` for both `api/auth.md` and `api/auth/index.md`. This is what a
207
+ * relative href folds against.
208
+ */
209
+ dirSegments: string[];
210
+ /** The source path, e.g. `'api/auth.md'`. For error messages. */
211
+ relativePath: string;
212
+ }
213
+ /**
214
+ * Resolve an internal markdown link target to a route.
215
+ *
216
+ * Called for every relative link found in the source. Returning `undefined`
217
+ * signals "not a documentation page", which — with `assertLinks` on — fails
218
+ * the build rather than shipping a 404 that was valid on GitHub.
219
+ */
220
+ type LinkResolver = (
221
+ /** The raw href as authored, e.g. `'./api/auth.md'`. */
222
+ href: string, from: DocLinkContext) => string | undefined;
223
+ /**
224
+ * Resolve an image `src` to a public URL and its intrinsic dimensions.
225
+ *
226
+ * Dimensions are read at build time because markdown carries none and
227
+ * `next/image` refuses to render without them (short of `fill`).
228
+ *
229
+ * ⚠️ `src` ARRIVES FOLDED AND CONTAINED. A `../` chain is resolved against
230
+ * `from.dirSegments` before this is called, and one that climbs above the
231
+ * content root throws instead of reaching you — so an implementation that joins
232
+ * this onto a filesystem path is not handing a document author a way to read
233
+ * `../../../../.env`. That was not true before: images skipped folding
234
+ * entirely and arrived exactly as authored.
235
+ *
236
+ * Two shapes are the exception, and they still reach you: an absolute
237
+ * `/logo.png` and a schemed `https://…` are passed through UNFOLDED, because the
238
+ * first is already a public URL and the second belongs to someone else. Folding
239
+ * them would be meaningless, but they are not filtered out — a host that wants
240
+ * to rewrite `/logo.png` onto a CDN needs the call. So an implementation that
241
+ * blindly prefixes what it is handed produces `/cdn//logo.png` and
242
+ * `/cdn/https://example.com/a.png`. Branch on them, or return `undefined` to
243
+ * leave the src as authored.
244
+ */
245
+ type ImageResolver = (src: string, from: DocLinkContext) => Promise<{
246
+ src: string;
247
+ width?: number;
248
+ height?: number;
249
+ } | undefined> | {
250
+ src: string;
251
+ width?: number;
252
+ height?: number;
253
+ } | undefined;
254
+ /**
255
+ * How a documentation tree is read.
256
+ *
257
+ * The type parameter is inferred from
258
+ * {@link DocsConfig.frontmatterSchema} — pass one and every `DocFile` and
259
+ * `RenderedDoc` the host hands back carries your fields, with no explicit type
260
+ * argument anywhere. Omit it and the parameter defaults to
261
+ * {@link DocFrontmatter}, which is what every existing call site gets.
262
+ */
263
+ interface DocsConfig<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
264
+ /**
265
+ * Content root. Relative paths resolve against `process.cwd()`, which is
266
+ * the project root during both `next build` and `vite build`.
267
+ */
268
+ contentDir: string;
269
+ /** URL prefix the docs are mounted at. Defaults to `'/docs'`. */
270
+ basePath?: string;
271
+ /**
272
+ * Include pages marked `draft: true`. Defaults to `false`.
273
+ *
274
+ * Drive this from your own env check — deliberately not `NODE_ENV`.
275
+ */
276
+ includeDrafts?: boolean;
277
+ /**
278
+ * Fail the build when an internal link resolves to a page that does not
279
+ * exist. Defaults to `true`; there is no good reason to turn it off.
280
+ */
281
+ assertLinks?: boolean;
282
+ /**
283
+ * Validates every page's frontmatter. Defaults to `docFrontmatterSchema`
284
+ * from `@waveso/docs/frontmatter`.
285
+ *
286
+ * Any [Standard Schema](https://standardschema.dev) validator is accepted —
287
+ * Zod, Valibot, ArkType — rather than a Zod type specifically. That keeps the
288
+ * package from dictating a validator, and a schema handed over through the
289
+ * spec interface cannot hit the cross-instance mismatch two copies of Zod in
290
+ * one `node_modules` otherwise produce.
291
+ *
292
+ * ```ts
293
+ * // content/docs-schema.ts — one module, imported by every route file
294
+ * import { docFrontmatterSchema } from '@waveso/docs/frontmatter';
295
+ * import { z } from 'zod';
296
+ *
297
+ * export const frontmatterSchema = docFrontmatterSchema.extend({
298
+ * audience: z.enum(['user', 'operator']).exactOptional(),
299
+ * });
300
+ * ```
301
+ *
302
+ * Three things are worth knowing before you write one:
303
+ *
304
+ * - **The output must still satisfy {@link DocFrontmatter}.** `title` drives
305
+ * the `<h1>` fallback and `<title>`, `draft` the visibility filter,
306
+ * `aliases` the redirects, `order`/`label` the sidebar. A schema that
307
+ * drops them is a compile error here, not a mystery at render time.
308
+ * - **Unknown keys are stripped, by every validator worth using.** The
309
+ * parsed frontmatter is exactly what the schema declares, so declare every
310
+ * field you intend to read — extending
311
+ * `docFrontmatterSchema` is the shortest way to keep the built-ins.
312
+ * - **Identity is load-bearing.** The filesystem scan is memoised per
313
+ * resolved config, and two schema objects are only "the same schema" when
314
+ * they are the same object. Export one from a shared module (as above)
315
+ * rather than building it inline in each route file, or each file pays for
316
+ * its own scan.
317
+ */
318
+ frontmatterSchema?: StandardSchemaV1<unknown, TFrontmatter>;
319
+ }
320
+ /** {@link DocsConfig} with defaults applied. */
321
+ interface ResolvedDocsConfig<TFrontmatter extends DocFrontmatter = DocFrontmatter> {
322
+ contentDir: string;
323
+ basePath: string;
324
+ includeDrafts: boolean;
325
+ assertLinks: boolean;
326
+ /**
327
+ * As supplied. Absent — never explicitly `undefined`, per
328
+ * `exactOptionalPropertyTypes` — when the built-in `docFrontmatterSchema`
329
+ * applies, so the default lives in one place: `parseFrontmatter`.
330
+ */
331
+ frontmatterSchema?: StandardSchemaV1<unknown, TFrontmatter>;
332
+ }
333
+ //#endregion
334
+ export { DocFile, DocFrontmatter, DocLinkContext, DocNavGroup, DocNavLink, DocNavNode, DocNavPage, DocNavSeparator, DocsConfig, DocsMeta, ImageResolver, LinkResolver, RenderedDoc, ResolvedDocsConfig, SearchRecord, TocEntry };
package/dist/types.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,166 @@
1
+ {
2
+ "name": "@waveso/docs",
3
+ "version": "0.1.0",
4
+ "description": "Markdown documentation for Next.js — parsed to hast in Node at build time, rendered as your own React components, zero parser bytes in the browser",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "*.css"
8
+ ],
9
+ "license": "MIT",
10
+ "keywords": [
11
+ "documentation",
12
+ "docs",
13
+ "markdown",
14
+ "remark",
15
+ "rehype",
16
+ "shiki",
17
+ "nextjs",
18
+ "react-server-components",
19
+ "static-site"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/wavedotso/wave-docs.git"
24
+ },
25
+ "homepage": "https://github.com/wavedotso/wave-docs#readme",
26
+ "bugs": "https://github.com/wavedotso/wave-docs/issues",
27
+ "engines": {
28
+ "node": ">=20.19.0"
29
+ },
30
+ "exports": {
31
+ "./package.json": "./package.json",
32
+ "./styles.css": "./dist/styles.css",
33
+ "./types": {
34
+ "types": "./dist/types.d.ts",
35
+ "default": "./dist/types.js"
36
+ },
37
+ "./frontmatter": {
38
+ "types": "./dist/frontmatter.d.ts",
39
+ "default": "./dist/frontmatter.js"
40
+ },
41
+ "./source": {
42
+ "types": "./dist/source.d.ts",
43
+ "browser": null,
44
+ "default": "./dist/source.js"
45
+ },
46
+ "./render": {
47
+ "types": "./dist/render.d.ts",
48
+ "browser": null,
49
+ "default": "./dist/render.js"
50
+ },
51
+ "./highlighter": {
52
+ "types": "./dist/highlighter.d.ts",
53
+ "browser": null,
54
+ "default": "./dist/highlighter.js"
55
+ },
56
+ "./search-index": {
57
+ "types": "./dist/search-index.d.ts",
58
+ "browser": null,
59
+ "default": "./dist/search-index.js"
60
+ },
61
+ "./search-options": {
62
+ "types": "./dist/search-options.d.ts",
63
+ "default": "./dist/search-options.js"
64
+ },
65
+ "./markdown-links": {
66
+ "types": "./dist/plugins/remark-doc-links.d.ts",
67
+ "default": "./dist/plugins/remark-doc-links.js"
68
+ },
69
+ "./next": {
70
+ "types": "./dist/next.d.ts",
71
+ "browser": null,
72
+ "default": "./dist/next.js"
73
+ },
74
+ "./react/*": {
75
+ "types": "./dist/react/*.d.ts",
76
+ "default": "./dist/react/*.js"
77
+ }
78
+ },
79
+ "files": [
80
+ "dist",
81
+ "package.json",
82
+ "README.md",
83
+ "LICENSE"
84
+ ],
85
+ "dependencies": {
86
+ "@shikijs/langs": "4.4.3",
87
+ "@shikijs/rehype": "4.4.3",
88
+ "@shikijs/themes": "4.4.3",
89
+ "@standard-schema/spec": "^1.1.0",
90
+ "@types/hast": "^3.0.5",
91
+ "@types/mdast": "^4.0.4",
92
+ "gray-matter": "^4.0.3",
93
+ "hast-util-to-jsx-runtime": "^2.3.6",
94
+ "hast-util-to-string": "^3.0.1",
95
+ "minisearch": "^7.2.0",
96
+ "rehype-autolink-headings": "^7.1.0",
97
+ "rehype-github-alerts": "^4.2.0",
98
+ "rehype-slug": "^6.0.0",
99
+ "remark-gfm": "^4.0.1",
100
+ "remark-parse": "^11.0.0",
101
+ "remark-rehype": "^11.1.2",
102
+ "shiki": "4.4.3",
103
+ "unified": "^11.0.5",
104
+ "unist-util-visit": "^5.1.0",
105
+ "vfile": "^6.0.3"
106
+ },
107
+ "peerDependencies": {
108
+ "next": "^16.0.0",
109
+ "react": "^19.0.0",
110
+ "react-dom": "^19.0.0",
111
+ "tailwindcss": "^4.0.0",
112
+ "zod": "^4.4.3"
113
+ },
114
+ "peerDependenciesMeta": {
115
+ "next": {
116
+ "optional": true
117
+ },
118
+ "tailwindcss": {
119
+ "optional": true
120
+ }
121
+ },
122
+ "devDependencies": {
123
+ "@arethetypeswrong/cli": "^0.18.5",
124
+ "@biomejs/biome": "^2.5.7",
125
+ "@changesets/cli": "^2.31.1",
126
+ "@testing-library/jest-dom": "^7.0.1",
127
+ "@testing-library/react": "^16.3.2",
128
+ "@testing-library/user-event": "^14.6.3",
129
+ "@types/node": "^24.0.0",
130
+ "@types/react": "^19.2.18",
131
+ "@types/react-dom": "^19.2.4",
132
+ "image-size": "^2.0.2",
133
+ "jsdom": "^30.0.1",
134
+ "next": "^16.3.0",
135
+ "publint": "^0.3.23",
136
+ "react": "^19.2.3",
137
+ "react-dom": "^19.2.3",
138
+ "tsdown": "^0.22.14",
139
+ "typescript": "^5.9.3",
140
+ "unrun": "^0.3.1",
141
+ "vitest": "^4.1.10",
142
+ "zod": "^4.4.3"
143
+ },
144
+ "devEngines": {
145
+ "runtime": {
146
+ "name": "node",
147
+ "version": ">=22.18.0",
148
+ "onFail": "warn"
149
+ }
150
+ },
151
+ "scripts": {
152
+ "build": "tsdown",
153
+ "dev": "tsdown --watch",
154
+ "typecheck": "tsc --noEmit",
155
+ "lint": "biome check",
156
+ "lint:fix": "biome check --write",
157
+ "format": "biome format --write",
158
+ "test": "vitest run",
159
+ "test:watch": "vitest",
160
+ "check:package": "publint && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm",
161
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
162
+ "changeset": "changeset",
163
+ "version": "changeset version",
164
+ "release": "pnpm run build && changeset publish"
165
+ }
166
+ }