Skip to content

Entry points

There is no root export, and no wildcard. Every entry point is enumerated in exports, so an import always names the file it came from, and a name that is not documented is not importable — see Stability.

The subpaths

SubpathEnvironmentContents
@waveso/docs/nextNodecreateDocsRoute, createDocsSitemap, createDocsRedirects
@waveso/docs/viteNodewaveDocs, WAVE_DOCS_MODULE_ID — see Vite
@waveso/docs/vite-client—Ambient types for virtual:wave-docs. Type-only, and there is no JavaScript file at all
@waveso/docs/sourceNodecreateDocsSource, resolveDocsConfig
@waveso/docs/renderNodecreateDocsRenderer, resolveMarkdownLink
@waveso/docs/highlighterNodecreateDocsHighlighter, DEFAULT_DOCS_LANGS, DEFAULT_DOCS_THEMES
@waveso/docs/search-indexNodeextractSearchRecords, buildSearchIndex
@waveso/docs/llms-txtNodebuildLlmsTxt, buildLlmsFullTxt, toPortableMarkdown
@waveso/docs/react/<name>Browser + RSCEleven components, one per subpath — enumerated below
@waveso/docs/frontmatterAnydocFrontmatterSchema, parseFrontmatter, z
@waveso/docs/errorsAnyisDocsError, DOCS_ERROR_PREFIX, and the DocsError and DocsErrorCode types
@waveso/docs/typesAnySeventeen shared types. Type-only — it compiles to an empty JavaScript file
@waveso/docs/styles.css—The stylesheet
@waveso/docs/package.json—The manifest, for tooling that resolves it

Each entry also exports the types that go with its function — DocsRouteOptions from next, DocsRendererOptions and DocsRenderer from render, DocsHighlighterOptions from highlighter, DocsSource from source. The shapes those are written against live in types, which imports nothing at runtime and can therefore be imported from a Node module, a Server Component and a browser bundle alike.

sideEffects lists *.css and nothing else, so a bundler may drop any module you do not import — and must keep the stylesheet, which is imported for its effect rather than for a name.

The React subpaths

Ten components, one file each. The boundary column is the directive the module actually ships, which is what decides whose bundle it lands in:

SubpathExports
react/doc-contentDocContentServer Component. Mounts the copy runtime only when the tree contains a code frame
react/markdown-componentscreateMarkdownComponents, defaultMarkdownComponentsNo client boundary anywhere under the map
react/calloutCallout, CALLOUT_TYPESServer Component
react/youtubeYouTubeServer Component
react/skip-linkSkipLink, DOCS_CONTENT_IDServer Component
react/sidebarDocsSidebar'use client'. Takes pathname as a prop
react/tocDocsToc'use client'. Scrollspy via IntersectionObserver
react/search-dialogSearchDialog'use client'. Host-agnostic
react/next-searchDocsSearch'use client'. Imports next/link and next/navigation
react/next-linkDocsLink'use client'. Imports next/link

react/youtube being a Server Component is load-bearing, and server-boundary.test.ts walks the whole map for a directive rather than trusting the column above. Internals has the regression that put the test there.

The props are in Components.

"browser": null

The five Node-only subpaths carry "browser": null in the map, so importing one from client code fails with a located module not found rather than resolving.

That is about weight, not about node:fs. Three of the five would bundle without complaint: render, highlighter and search-index require no Node builtins at all. The markdown pipeline runs wherever JavaScript does, and Shiki is loaded through its JavaScript regex engine rather than WASM on purpose. What a bundler would do with them is succeed, and ship unified, remark-parse and every Shiki grammar to a reader — the exact outcome this package exists to prevent, arriving with no error to notice. Only source and next genuinely need the filesystem.

Runtimes

What each entry requires, measured by bundling it with platform: 'neutral' so nothing is resolved away as assumed-present, and asserted exactly — with toEqual, never toContain — by src/entry-runtime.test.ts:

EntryNode builtins
@waveso/docs/typesnone
@waveso/docs/frontmatternone
@waveso/docs/highlighternone
@waveso/docs/rendernone
@waveso/docs/search-indexnone
@waveso/docs/llms-txtnone
@waveso/docs/sourcenode:fs/promises, node:path
@waveso/docs/nextnode:crypto, node:fs/promises, node:path
@waveso/docs/vitenode:fs/promises, node:path

source needs the filesystem because reading a content directory is what it is for; next inherits its two and adds node:crypto for the search index's ETag. vite inherits the same two and adds nothing — it emits the index as a file rather than serving it, so there is no ETag to compute, and it imports vite itself nowhere. A subset assertion would pass the moment a new builtin appeared, which defeats the point of the file — one node:crypto import three modules deep costs nothing locally and rules out every non-Node runtime at once. Both spellings count, too: gray-matter@4 did require('fs') rather than node:fs, for a matter.read() this package never called, and a check that only looked for the node: prefix would have reported the tree clean while it was not.

errors and the ten React subpaths are outside that set. Neither reaches a builtin today — errors imports nothing at all, and the React modules import react, each other, and in two cases next — but no assertion pins it.

The next peer

next is an optional peer, and @waveso/docs/next is the only entry that reaches for it. It does so through a dynamic import() with a literal specifier, so a consumer's bundler still resolves it statically while next/link and next/navigation stay off the module graph of anyone who never touches this entry point — including next.config.ts, which loads it for createDocsSitemap and createDocsRedirects outside the Next runtime.

When the peer is absent the failure is missing-peer, whose message names the specifier and the alternative. Node's own ERR_MODULE_NOT_FOUND names a file inside this package instead, which reads as our bug rather than a missing install. The codes are in Errors.

Two React subpaths import Next statically, and are named so the exception is visible in the file list: react/next-link and react/next-search. Every other component takes its link and image components as props, which is what keeps the renderer testable without a router.

There is no Vite peer at all, which is the difference between the two adapters rather than an oversight. @waveso/docs/next has to reach for next/link and next/image because it renders them; @waveso/docs/vite renders nothing of Vite's — it returns a plain object with the hook names Vite calls, and its types are declared structurally rather than imported. So nothing here pins a Vite major, and there is no version of Vite for it to be incompatible with.

No barrel file

lib/docs.ts
import { createDocsRoute } from '@waveso/docs/next';
import type { DocFile } from '@waveso/docs/types';

Subpaths rather than a root index, so every import names its origin and a bundler can drop what you did not ask for. A root export would defeat both: one module graph for the whole package, and @waveso/docs in the import line saying nothing about which half of it you pulled in.

Two subpaths were deleted rather than kept for symmetry, and are not coming back. ./markdown-links froze six names to make one of them reachable, and the plugin among those six throws unless the caller sets an undocumented field — surface with no use. ./search-options froze tokenizeSearchText, an Intl.Segmenter policy with a feature-detect fallback, i.e. the function most likely to change. resolveMarkdownLink survived, from render: six public names became one, and it is the only one a linkResolver author cannot hand-roll. What that policy does and does not promise is Stability.