dwar Examples

dwar is internal. You don't call render in a normal docs build — the JSDoc and TypeDoc bridges call it for you. You reach for it directly only when you're building a custom bridge, or when you want to see the renderer in isolation.

The best starting point is the package's own runnable example: the smoke script. It exercises the whole setu → dwar → disk path against a fixture, and because it's real code that runs, it's the most honest example in this doc.

If you just want to configure the theme, see Configuration instead.

Run the smoke script

CODE
pnpm --filter @clean-jsdoc-theme/dwar run smoke

scripts/smoke.ts pulls setu's JSDoc taffy fixture, runs it through generateSite() to get a SiteManifest, hands the manifest to dwar's render(), writes the returned files into packages/dwar/preview/, and — if pagefind is installed — builds the search index against that directory. It exists for visual sanity-checking.

The flow, end to end, is: manifest in → files out → you write them → preview/.

The render(manifest, opts) call

render takes the SiteManifest and a RenderOptions. The only required field of RenderOptions is theme; everything else is optional. The smoke script uses the minimal form (smoke.ts):

CODE
import { render, runPagefindAgainstDir } from '@clean-jsdoc-theme/dwar';
import type { ThemeConfig } from '@clean-jsdoc-theme/dwar';
import { generateSite } from '@clean-jsdoc-theme/setu';

const theme: ThemeConfig = {
  tokens: {
    colors: { bg: '#ffffff', bgMuted: '#f3f4f6', fg: '#0f172a', /* … */ border: '#e5e7eb' },
    fonts: { heading: 'Source Serif 4', body: 'Roboto', mono: 'ui-monospace, monospace' },
    shiki: { light: 'github-light', dark: 'github-dark' },
    siteName: 'clean-jsdoc-theme (smoke)',
  },
  basePath: '/',
};

const manifest = generateSite(collection, { pkg: { name: 'clean-jsdoc-theme', version: '…' } });

const result = await render(manifest, { theme });
//                              ^ only `theme` is required

The RenderOptions fields dwar reads

Every field below is on RenderOptions in render.ts — nothing is invented.

FieldRequiredWhat it does
themeyesThe ThemeConfigtokens (colors, fonts, shiki, siteName, …), basePath, and the optional copyPage / pageNav / aiPrompt / customCss(File) / customJs(File) knobs the render reads.
destinationnoThe destination directory. Used only for path resolution context — dwar never writes there itself.
islandCacheDirnoOpt-in on-disk cache for the esbuild island bundle. Supplying it lets a warm rebuild skip the ~0.4s bundle step; omitting it keeps render() pure. The bridges pass <project>/node_modules/.cache/clean-jsdoc-theme.
inlineSvgsnoMap from a doc-image src to that SVG's raw markup, so rang inlines theme-aware SVGs instead of <img>-ing them. The bridge reads the files; render() just looks them up.

The bridges build a fuller theme (palette overrides, fonts, copyPage, pageNav, custom CSS/JS hrefs) and pass all four options, but the contract is the same: theme is required, the rest is what the bridge happened to find.

What RenderResult contains, and how a bridge persists it

render resolves to a RenderResult (render.ts):

CODE
interface RenderResult {
  files: OutputFile[];        // { path, contents } — everything to write
  search?: SearchEntry[];     // per-page entries (the JSON index is already in `files`)
  errors?: RenderError[];     // { slug, message } — pages skipped, present only on failure
  stats: {
    pageCount: number;        // pages rendered successfully (excludes errors)
    assetCount: number;       // non-HTML files (CSS + JS chunks + search index)
    cssBytes: number;
    jsBytes: number;
    durationMs: number;
  };
}

The purity contract in practice: render() returns files in memory; you write them. The smoke script does exactly that — a plain write loop, then the optional Pagefind step (smoke.ts):

CODE
const result = await render(manifest, { theme });

// Fresh output dir, then write every OutputFile (string or Uint8Array).
await rm(previewDir, { recursive: true, force: true });
await mkdir(previewDir, { recursive: true });
for (const file of result.files) {
  const out = resolve(previewDir, file.path);
  await mkdir(dirname(out), { recursive: true });
  await writeFile(out, typeof file.contents === 'string' ? file.contents : Buffer.from(file.contents));
}

// Pagefind is a SEPARATE post-write step, against the written directory.
try {
  await runPagefindAgainstDir(previewDir);
} catch (err) {
  console.warn(`[smoke] pagefind skipped: ${(err as Error).message}`);
}

The real bridges follow the identical shape. The JSDoc bridge (publish.ts) calls render, concatenates dwar's files with the assets it copied (logos, custom CSS/JS, doc images), writes them all, then runs Pagefind:

CODE
const result = await render(manifest, {
  theme: { ...resolveTheme(opts, siteName, fonts, basePath), ...customAssets.theme },
  destination: absoluteDestination,
  islandCacheDir,
  inlineSvgs,
});

const outputFiles = [...result.files, ...logoFiles, ...customAssets.files, ...docImageFiles];
await writeOutputFiles(absoluteDestination, outputFiles);

// Render failures are reported, never fatal.
if (result.errors && result.errors.length > 0) {
  for (const e of result.errors) console.warn(`  - ${e.slug}: ${e.message}`);
}

// Pagefind is optional — a missing/failing index must not break the build.
try {
  await runPagefindAgainstDir(absoluteDestination);
} catch (err) {
  console.warn(`pagefind step skipped (optional) — ${(err as Error).message}`);
}

The TypeDoc bridge (write-site.ts) does the same thing in ESM: render(manifest, { theme, destination, islandCacheDir }), then writeOutputFiles, then runPagefindAgainstDir. Both treat the errors array as a warning and the Pagefind step as best-effort.

Note the division of labor in the write loop: dwar's result.files are the HTML, the companion .md, the stylesheet, the island chunks, and the fuzzy-search JSON. The logos, custom CSS/JS, and doc images are copied by the bridge (the I/O layer) and concatenated in — that's why render() stays pure and just links the resulting hrefs.

The contract, restated

  • render(manifest, opts) is pure — it allocates files in memory and returns them. It never writes to disk.
  • You write result.files to the destination.
  • runPagefindAgainstDir(dir) is a separate function you call after writing, against the destination directory. It's the only filesystem touch in the whole package, and it's optional.

Read the source

These are the canonical, working usages — read them rather than trusting any snippet above:

Next

  • dwar Overview — why dwar exists, the purity and resilience guarantees, and what render() emits.
  • setu Overview — where the SiteManifest comes from.
  • rang Overview — the components and islands dwar bundles into the page.