-
Notifications
You must be signed in to change notification settings - Fork 45
packages utils examples
Be honest with yourself first:
@clean-jsdoc-theme/utilsis an internal building block. You almost never install it on its own. You reach for it when you're writing a custom bridge (your own entry point that drivessetu → dwar), a tool that produces or inspects aSiteManifest, or a component override that needs the boundary types. If you just want docs, install an entry point and set options instead.
Everything is exported from the single package entry — there are no subpath
exports (package.json exposes only .), so all imports look like this:
import { slugifyHeading, validateThemeOpts } from '@clean-jsdoc-theme/utils';
import type { SiteManifest, Page, ThemeConfig } from '@clean-jsdoc-theme/utils';When you build something that hands a SiteManifest to dwar (or consumes one),
import the boundary types. These are the exact shapes setu emits and dwar
renders — see
manifest.ts
and
page.ts.
import type { SiteManifest, Page, NavNode } from '@clean-jsdoc-theme/utils';
// A hand-built manifest — what setu would normally produce for you.
const manifest: SiteManifest = {
pages: [
{
slug: 'guides/intro',
frontmatter: { title: 'Intro', kind: 'guide', group: 'Guides', order: 1 },
body: '# Intro\n\nWelcome.',
} satisfies Page,
],
nav: [
{ label: 'Guides', children: [{ label: 'Intro', slug: 'guides/intro' }] },
] satisfies NavNode[],
buildId: '2026-06-12-abc123',
};A few things the types enforce, straight from the source:
-
Page.slugis relative to the site root — no leading slash, no.html. -
Page.bodyis always an MDX string;mdastandheadingsare optional. -
Frontmatter.kindis aPageKind(class|module|namespace|mixin|interface|typedef|global|index|guide|source). -
NavNodeis recursive: leaves carry aslug, branches carrychildren.
The theme contract lives in
theme.ts:
import type { ThemeConfig } from '@clean-jsdoc-theme/utils';
const theme: ThemeConfig = {
tokens: {
colors: {
bg: 'oklch(1 0 0)',
bgMuted: 'oklch(0.97 0 0)',
fg: 'oklch(0.2 0 0)',
fgMuted: 'oklch(0.5 0 0)',
accent: 'oklch(0.6 0.2 250)',
accentFg: 'oklch(1 0 0)',
border: 'oklch(0.9 0 0)',
},
fonts: { heading: 'Source Serif 4', body: 'Roboto', mono: 'ui-monospace, monospace' },
shiki: { light: 'github-light', dark: 'github-dark' },
},
basePath: '/docs/',
};Both setu and dwar slugify through the same functions so a sidebar link and the
heading anchor it points at can never disagree. Verified behavior from
slug-rules.ts:
import { slugifyHeading, slugifyPath, slugifySourcePath } from '@clean-jsdoc-theme/utils';
slugifyHeading('Hello, World!'); // 'hello-world' (lowercased, punctuation dropped)
slugifyPath(['Foo Bar', 'Baz!']); // 'foo-bar/baz'
slugifySourcePath('src/Foo.js'); // 'src/foo-js' (extension folded in, not stripped)
slugifySourcePath('lib\\util\\index.ts'); // 'lib/util/index-ts' (backslashes normalized)slugifyHeading takes an optional registry to dedupe repeated headings on a
page — pass one Map and reuse it across every heading:
const reg = new Map<string, number>();
slugifyHeading('Options', reg); // 'options'
slugifyHeading('Options', reg); // 'options-1'
slugifyHeading('Options', reg); // 'options-2'To serve from a sub-directory, normalize the base path once and join links
through withBase (both are pure and fail safe to '/' — see
base-path.ts):
import { normalizeBasePath, withBase } from '@clean-jsdoc-theme/utils';
const base = normalizeBasePath('https://example.com/docs/api/'); // '/docs/api'
withBase(base, '/guides/intro'); // '/docs/api/guides/intro'
withBase('/', '/guides/intro'); // '/guides/intro' (unchanged at the root)validateThemeOpts is the orchestrator a bridge runs before building. It never
throws: it returns normalized value plus a DiagnosticBag you decide what to
do with. See
validate-opts.ts.
The Google-Fonts existence check is the only networked piece, and it's
injected — you build a resolver with createGoogleFontResolver() and pass
it in. This is what keeps utils browser-safe (it never imports fetch itself).
Omit the resolver and font checks are skipped gracefully.
import {
validateThemeOpts,
createGoogleFontResolver,
formatDiagnostics,
} from '@clean-jsdoc-theme/utils';
const fontResolver = createGoogleFontResolver(); // uses global fetch; fail-open + cached
const { value, diagnostics } = await validateThemeOpts({
opts: { siteName: 'My API', fonts: { heading: 'Roboto', body: 'Roboto' } },
fontResolver,
unknownKeyPolicy: 'suggest-typos', // only flag near-miss typos of a known key
});
// value.siteName / value.fonts are normalized; decide your own policy on findings:
if (diagnostics.list.length > 0) console.log(formatDiagnostics(diagnostics, { color: true }));
if (diagnostics.hasErrors()) {
// strict mode is the *caller's* choice — utils never throws
}This is exactly how the real bridges wire it. The JSDoc bridge in
publish.ts
builds the resolver, calls validateThemeOpts with unknownKeyPolicy: 'suggest-typos' (JSDoc's own opts share the flat namespace), then logs the bag
and only throws when opts.strict is set and diagnostics.hasErrors(). The
TypeDoc plugin in
write-site.ts
does the same but with unknownKeyPolicy: 'warn-all', since its options live in
a dedicated namespaced block rather than a shared flat one.
For tests or a custom runtime you can inject a fake fetch instead of hitting
the network — the resolver only needs the minimal FetchLike slice
(url, optional signal/headers, returns { status }):
const resolver = createGoogleFontResolver({
fetch: async (url) => ({ status: url.includes('Roboto') ? 200 : 400 }),
});
await resolver('Roboto'); // 'exists'
await resolver('Made Up Font'); // 'missing' (200 → exists, 400 → missing, else → 'unknown')After writing files, the bridge formats a Next.js-style summary with
formatBuildReport. Like the font check, the node-only dependency (gzip) is
injected so utils stays node-free — pass a gzipSizer and the gzip column
appears. See
report.ts.
import { formatBuildReport } from '@clean-jsdoc-theme/utils';
import { gzipSync } from 'node:zlib'; // the *caller* imports node, not utils
console.log(
formatBuildReport({
files: result.files, // OutputFile[] from dwar.render
stats: result.stats,
destination: './out',
gzipSizer: (b) => gzipSync(b).length, // omit to hide the gzip column
color: true,
})
);Both publish.ts and write-site.ts call it exactly this way — see lines
around the formatBuildReport({ … gzipSizer }) calls in each bridge.
- utils Overview — what each export is for and why the package exists.
-
Packages — how utils fits the
setu → dwarpipeline. -
setu Overview — produces the
SiteManifestyou see typed here. -
dwar Overview — consumes it via
render().
This wiki is auto-generated from docs-site/docs. Edit there — changes sync automatically. Full docs: https://ankdev.me/clean-jsdoc-theme/