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.
| Path | What 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 bytscto CommonJS, because Docusaurusrequire()s a theme's main entry while loadingdocusaurus.config.ts. That also rules outawait import()of an ESM-only package there: this half is emitted formodule: CommonJS, which rewrites a dynamic import torequire. src/theme/**is processed by Babel with@babel/preset-typescriptonly, 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:
- packs the theme with
npm pack, which runsprepack, so the tarball is always built from current sources - scaffolds a brand new Docusaurus site with
create-docusaurus - installs the tarball into it
- applies the configuration this documentation prescribes
- copies everything in
docs/in as that site's content, verbatim - builds it
- 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
docusaurusVersionconstant 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.