Skip to main content

Developing this repository

For changes to the theme itself. If you only consume it, you do not need this page.

Layout

The repository is a moon workspace holding a single package. The workspace root carries the moon configuration, this documentation, and nothing else.

PathWhat it is
modules/voraus-docusaurus-theme/src/The published theme
modules/voraus-docusaurus-theme/test/A Vitest suite that reproduces the documentation against a freshly scaffolded site
docs/This documentation, published through the theme by that suite
.moon/Workspace and toolchain configuration. Node.js and npm versions are pinned here

Tasks

moon downloads the pinned Node.js and npm itself, so installing moon is the only prerequisite:

moon run :install # npm ci - run this first
moon run :build # build the theme
moon run :lint # eslint + prettier
moon run :spellcheck # cspell, US English
moon run :typography # no em dashes or other typographic decoration
moon run :typecheck # tsc
moon run :test # the integration test
moon run :preview # build the fixture site and serve it

preview depends on test, so a single command gets you a browsable site.

Dependencies are installed only by that explicit install task, and only ever with npm ci, which installs exactly what the lockfile says and fails rather than rewriting it. moon's implicit install is switched off, along with its package.json syncing and lockfile deduping, so nothing mutates the lockfile behind your back.

Adding or removing a dependency is therefore two steps, and the only place a bare npm install belongs. Edit package.json, then:

npm install --package-lock-only # resolve the new tree into package-lock.json
moon run :install # apply it

--package-lock-only writes the lockfile and nothing else, so the installed node_modules still only ever comes from npm ci.

That also has to stay a separate step rather than a dependency of the other tasks: CI runs the test and lint stages in parallel, and two moon processes in one workspace would each start npm ci, which deletes node_modules before repopulating it, so the second wipes what the first just built.

Spelling

moon run :spellcheck runs cspell over every file in the workspace that carries prose: the docs, the README, the pipeline and the sources.

cspell.json sits at the workspace root, because that is its scope. language: "en-US" is what enforces American spelling: cspell's US dictionary does not contain the British spellings of words like color or initializing, so they come back as unknown words.

A handful of British forms are also valid US words in their own right and pass that check. Those are listed in flagWords, which rejects a word outright rather than asking a dictionary about it. Read the list in cspell.json to see which.

Keep it to forms that are unambiguously wrong here. Two kinds of word do not belong in it: names from the voraus palette, which are spelled the way the design system spells them, and identifiers from other tools, such as the GitHub Actions function used in test.yml.

The two per-repository parts are therefore words, the project's own vocabulary, and flagWords. To share the rest across repositories, publish the config as a package and import it:

{ "import": ["@voraus/cspell-config"], "words": ["..."] }

Typography

moon run :typography rejects em dashes, en dashes, ellipsis characters, middle dots and decorative arrows anywhere in the tracked tree. Write ... and a plain -, or reach for a comma, a colon or parentheses.

It is a git grep rather than a lint rule because neither of the other tools can do it: Prettier has no opinion on these characters, and cspell tokenizes text into words, so punctuation never reaches flagWords. Symbols that carry meaning are unaffected, which is why © in the footer copyright and the box-drawing characters in diagrams pass.

CHANGELOG.md is excluded. Release Please generates it from commit subjects, so it is not ours to format.

Previewing

moon run :preview

The site is served at http://localhost:3000/voraus-docusaurus-theme/, not at /. GitHub Pages publishes it as a project page, so the fixture is built for that path and the test covers the same URL shape production uses. Requests to / redirect there.

Append ?vdoc-theme=dark to see what a reader inside vdoc sees. See Inside vdoc.

A full run takes about 45 seconds, and almost all of it is the fixture site's own npm install of around 1400 packages. The suite deletes and rebuilds the site from scratch every time, on purpose: it reproduces what a consuming repository does, and an incremental run would stop proving that.

The @theme-original trap

In a swizzled site component, @theme-original/X reaches the previous implementation. That is the documented way and it works.

Inside this theme package it is a trap. Docusaurus points @theme-original/X at the last theme providing X, which, for this theme's own components, is this theme itself. A component wrapping with it renders itself forever and the build dies with:

FATAL ERROR: Reached heap limit Allocation failed

Theme components here wrap through @theme-init/X instead, which is pinned to the original Docusaurus implementation. The integration test greps every built component for the wrong specifier, because nothing else would notice.

@theme/Mermaid is the exception, and it fails quietly rather than loudly. theme-classic declares that component as a no-op, purely so that MDX can register mermaid without knowing whether a renderer is installed, and @docusaurus/theme-mermaid then replaces it. @theme-init is bound to the first theme that declared a component, so @theme-init/Mermaid is that no-op and not the renderer. Nothing errors; every diagram simply renders nothing. It is therefore wrapped by importing @docusaurus/theme-mermaid/lib/theme/Mermaid directly, and the suite asserts that the real renderer is in the bundle.

How the package is built

Docusaurus asks theme authors to strip type annotations and nothing else, because the consuming site's bundler compiles theme components with that site's browser targets. The build therefore has two halves:

  • Everything directly under src/ - the plugin entry, the config factory, the remark plugin - is compiled by tsc to CommonJS, because Docusaurus require()s a theme's main entry while loading docusaurus.config.ts. That also rules out await import() of an ESM-only package there: this half is emitted for module: CommonJS, which rewrites a dynamic import to require.
  • src/theme/** is processed by Babel with @babel/preset-typescript only, so the emitted components keep their JSX and ESM syntax.

Both halves are asserted by the integration test, so an accidental "helpful" transpile step fails the build.

src/ is published alongside lib/ so that docusaurus swizzle --typescript can emit TSX.

The integration test

The suite under test/ is not a unit test of the source tree. It:

  1. packs the theme with npm pack, which runs prepack, so the tarball is always built from current sources
  2. scaffolds a brand new Docusaurus site with create-docusaurus
  3. installs the tarball into it
  4. applies the configuration this documentation prescribes
  5. copies everything in docs/ in as that site's content, verbatim
  6. builds it
  7. asserts on the built HTML, CSS and JavaScript

Its one deliberate deviation from what a consuming repository does is installing from the local tarball rather than from npm, since the version under test has not been published yet.

This makes the documentation executable. Every page under docs/ is a page of the built site, with index.md as its landing page, so if a link breaks, a heading anchor moves or a Markdown construct stops compiling, the test fails. If the usage instructions stop being correct, the test fails.

The repository README.md is not published. It exists to point at this documentation, so framing it as a page of the same documentation would only duplicate it. Prettier still checks it.

The suite also checks that the options table in docs/03-configuration.md lists exactly the members of the VorausSiteOptions interface, so an option cannot be added, renamed or removed without the documentation following.

DOCS_URL and DOCS_BASE_URL override the URL the fixture site is built for. The documentation workflow passes both in from actions/configure-pages, so changing the Pages URL (adding a custom domain, say) is a repository setting rather than an edit here. Unset, they fall back to the URL that setting currently produces, so a local run builds exactly what CI deploys.

Dependency updates

The org's self-hosted Renovate picks this repository up because renovate.json5 exists, and applies the shared baseline: Conventional Commit titles, off-hours schedule, one grouped PR per package manager. Three things are overridden here:

  • No dev-dependency pinning. This is a published library, and consumers can only dedupe a single Docusaurus and React tree if it keeps expressing ranges. The committed lockfiles are what make CI reproducible.
  • All @docusaurus/* packages update together, majors included. They depend on each other by exact version, so a major arriving one PR per package would be several individually unbuildable changes.
  • A custom manager keeps the docusaurusVersion constant the integration test scaffolds its site with in step with the packages, since no package manager sees it.

.moon/ is left alone on purpose: the Node.js and npm pinned there are the toolchain every task runs on, locally and in CI alike, so they are moved deliberately rather than on Renovate's schedule.

Validate changes to it with:

npx --yes --package renovate -- renovate-config-validator

Releasing

Commits follow Conventional Commits. Release Please maintains a release pull request that bumps the version and the changelog. Merging it creates the git tag and the GitHub release, and the same release.yml run then publishes the package to npm.

That publish carries no npm token. The workflow mints an OIDC token, which npm exchanges for a short-lived credential. This is trusted publishing, configured on the npm package to accept this repository and this workflow file. Nothing to rotate, and a release cannot be published from anywhere else.

This documentation is published separately, by docs.yml on every push to main, to GitHub Pages.