PEP 517 build backend + Sphinx extension that orchestrates Vite via pnpm
Project description
sphinx-vite-builder
PEP 517 build backend and Sphinx extension that transparently orchestrates Vite builds via pnpm for Sphinx-theme packages whose static assets (CSS / JS) are produced by a JavaScript toolchain.
What it solves
A common pattern for modern Sphinx themes is a Python package whose
theme/<name>/static/ directory ships built CSS and JS that were
produced by a JS build tool (Vite, webpack, …). The build artefacts are
gitignored — they're reproducibly built, not source code. But that
creates two friction points:
- Editable installs and source-tree builds crash with confusing
errors when the static dir is empty (e.g. hatchling's
Forced include not found). - CI workflows must duplicate
pnpm install + vite buildsetup steps in every job that touches the package.
sphinx-vite-builder owns the Vite invocation end-to-end — exactly the
way maturin owns Cargo for
Rust+Python packages, or
sphinx-theme-builder
owns webpack for older Sphinx themes.
The contract
Sources should check for node, pnpm, etc and error if it's not good, then build. Wheels should have the build files baked in and not need node and pnpm at all.
This is the central invariant of the package. The two install paths behave asymmetrically by design:
Wheel installs — zero toolchain required
A user running pip install <package> from PyPI gets a wheel that
already contains the vite-built static/ tree, populated by this
backend at release time. The PEP 517 chain doesn't run on the
consumer side. No backend invocation. No pnpm. No Node. The end
user sees Python and only Python.
No pnpm, no Node — just Python:
$ pip install gp-furo-theme
Source builds — fail loud, fail informatively
A contributor (or downstream packager building from source) goes
through the PEP 517 chain. The backend runs pnpm exec vite build
to produce static/, and that requires pnpm + Node on PATH. If the
toolchain is missing, the backend raises a typed exception with a
multi-line, copy-pasteable hint:
sphinx-vite-builder: cannot bootstrap the vite toolchain.
`pnpm` is not on PATH. Install it via one of:
corepack enable # Node 16.10+ ships corepack
curl -fsSL https://get.pnpm.io/install.sh | sh -
See https://pnpm.io/installation
…
Detected CI provider: GitHub Actions. Add the following to your pipeline
config (before the Python build step that triggers this backend):
- uses: pnpm/action-setup@v6
with:
version: 10
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
The error includes the resolved vite-root path, the platform-specific
CI setup recipe (GitHub Actions, CircleCI, Azure Pipelines, GitLab CI,
or generic), and the SPHINX_VITE_BUILDER_SKIP=1 escape hatch for
environments that genuinely don't need vite to run.
The web/-absent short-circuit (sdist install bridge)
A user running pip install <pkg>.tar.gz from an sdist runs the
PEP 517 chain too — but the sdist excludes web/ (the Vite source
tree). The backend detects the absence, short-circuits cleanly, and
hatchling packs the pre-baked static/ (carried in the sdist via
[tool.hatch.build] artifacts) into the wheel. Sdist installs
need no toolchain either.
The asymmetry is the whole product: the same backend is strict
(running and failing loudly) when there's a web/ to act on, and
silent (skipping cleanly) when there's no web/ to begin with. The
two shapes match the two consumer worlds.
Quick start — two activation variants
sphinx-vite-builder ships two orthogonal ways to wire vite into a
hatchling-built Python package. Pick whichever fits the consumer's
existing build setup; they are mutually exclusive at the
[build-system].build-backend level.
Variant 1 — PEP 517 backend (drop-in replacement)
The simplest activation. Replace hatchling.build with
sphinx_vite_builder.build and you're done.
# packages/your-theme/pyproject.toml
[build-system]
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
build-backend = "sphinx_vite_builder.build"
[tool.hatch.build.targets.sdist]
exclude = ["web/"] # so the sdist→wheel chain hits the short-circuit
[tool.hatch.build]
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
Variant 2 — Hatchling build hook (composable)
For projects that want to keep build-backend = "hatchling.build" and
layer vite on top of an existing hatchling hook stack
(version, custom build scripts, etc.):
# packages/your-theme/pyproject.toml
[build-system]
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
build-backend = "hatchling.build"
[tool.hatch.build.hooks.vite]
[tool.hatch.build.targets.sdist]
exclude = ["web/"]
[tool.hatch.build]
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
Both variants share the same orchestration core — same SKIP env var,
same web/-absent short-circuit, same fast-fail diagnostics. Pick
variant 1 for simplicity, variant 2 for composability.
Sphinx extension (orthogonal to either build variant)
The Sphinx-extension head is independent of the backend / hook
choice. Wire it into a docs build to get vite running automatically
during sphinx-build (one-shot) and sphinx-autobuild (watched).
# docs/conf.py
extensions = ["sphinx_vite_builder"]
sphinx_vite_builder_mode = "auto" # "auto" | "dev" | "prod"
sphinx_vite_builder_root = "/abs/path/to/web"
"auto" resolves to "dev" when the build is running under
sphinx-autobuild (detected via SPHINX_AUTOBUILD env var, argv[0],
or parent-process inspection on Linux), "prod" otherwise. Setting
sphinx_vite_builder_root to None (the default) makes the extension
a complete no-op — useful when the consumer is installed from a wheel
where the static tree is already pre-baked.
Comparison with similar tools
| Tool | Toolchain owned | Activation strategy | Bootstrap |
|---|---|---|---|
maturin |
Rust (Cargo) | self-hosting via bootstrap shim |
puccinialin auto-installs Rust |
sphinx-theme-builder |
Node (webpack) | rolls own ZIP packing | nodeenv (isolated Node env) |
hatch-jupyter-builder |
Node (npm/yarn) | hatchling build hook | user-managed Node |
sphinx-vite-builder |
Node (vite) | PEP 517 backend or hatchling hook | user-managed pnpm (corepack) |
sphinx-vite-builder deliberately diverges from sphinx-theme-builder's
nodeenv approach: pnpm via corepack is the modern Node convention, and
auto-installing Node into the project tree pulls in significant friction
for editable workflows. Compared to maturin, the Rust analog,
sphinx-vite-builder doesn't auto-install pnpm — pnpm isn't pip-installable,
so the failure mode is "user runs corepack enable" rather than "backend
bootstraps a Node env."
Migrating from manual orchestration
If your project currently runs pnpm exec vite build from a CI step,
a justfile recipe, or a Makefile target, you can drop those:
| Was | Now |
|---|---|
tests.yml step: pnpm install && pnpm exec vite build |
The backend / hook handles it; only keep pnpm/Node setup |
release.yml step: pnpm install && pnpm exec vite build |
Same — keep pnpm/Node setup, drop the manual build call |
justfile recipe _assets-build as prerequisite of html |
Drop the recipe; the Sphinx extension's PROD-mode hook runs vite |
Hatchling [tool.hatch.build] force-include for static/ |
Drop; the backend produces the static tree before hatchling packs |
Keep your CI's pnpm + Node setup steps — the backend needs them at build time even though the wheel installation doesn't.
Fast-fail diagnostics — error reference
| Error | When | Hint includes |
|---|---|---|
PnpmMissingError |
pnpm not on PATH during a source build |
corepack enable, pnpm.io/installation, per-CI YAML recipe, SPHINX_VITE_BUILDER_SKIP=1 |
NodeModulesInstallError |
pnpm install exited non-zero |
cd <vite-root> && pnpm install --frozen-lockfile rerun command, captured stderr |
ViteFailedError |
pnpm exec vite build exited non-zero |
invocation context (cwd, exit code), captured stderr |
All three inherit from SphinxViteBuilderError, so consumers can
except SphinxViteBuilderError for a single catch surface.
CI recipe gallery
The PnpmMissingError hint is self-healing when the backend
detects a CI environment — it embeds the platform-specific setup
recipe in the error message. They are also reproduced here for
search-discoverability.
GitHub Actions (GITHUB_ACTIONS=true)
- uses: pnpm/action-setup@v6
with:
version: 10
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
CircleCI (CIRCLECI=true)
- run:
name: Install pnpm via corepack
command: |
corepack enable
corepack prepare pnpm@latest-10 --activate
Azure Pipelines (TF_BUILD=True)
- task: NodeTool@0
inputs:
versionSpec: '22.x'
- script: |
corepack enable
corepack prepare pnpm@latest-10 --activate
GitLab CI (GITLAB_CI=true)
before_script:
- corepack enable
- corepack prepare pnpm@latest-10 --activate
Generic CI (any other CI=true)
Use your CI's package-manager setup mechanism to put pnpm (>=10)
and node (>=22) on PATH before the Python build step runs.
Detection precedence (most-specific wins): each provider's canonical
env var per the pnpm
Continuous Integration docs
is checked first; the generic CI=true is the fallback for "we know
we're in CI but don't recognise the provider."
Troubleshooting
PnpmMissingError: pnpm is not on PATH — install pnpm via
corepack enable (Node 16.10+) or follow the per-CI recipe in the
error message. If you're in an environment that genuinely doesn't
need vite to run (e.g. building from a pre-baked sdist), set
SPHINX_VITE_BUILDER_SKIP=1 in the environment.
NodeModulesInstallError — pnpm install --frozen-lockfile
exited non-zero. The error surfaces the captured stderr. Common
causes: stale pnpm-lock.yaml (run pnpm install interactively to
refresh), network/registry timeout (retry), or engines mismatch
(check the project's package.json engines field against the
installed Node/pnpm versions).
ViteFailedError — pnpm exec vite build exited non-zero.
Captured stderr is included in the hint. This is usually a
project-side compile error (TypeScript type check, SCSS syntax,
missing import) rather than a tooling problem. Reproduce with
(cd <vite-root> && pnpm exec vite build) to see vite's full output.
Wheel ships without static/ content — the backend ran but the
artefacts didn't make it into the wheel. Verify your pyproject.toml
has the [tool.hatch.build] artifacts = ["src/.../static/"]
declaration. Hatchling's documented "VCS-ignored include" mechanism
requires this even when the path is on disk; force-include does
not work for editable builds.
just html (or any plain sphinx-build) doesn't rebuild assets —
make sure sphinx_vite_builder is loaded in extensions and that
sphinx_vite_builder_root points at your web/ directory. The
extension's PROD-mode hook runs pnpm exec vite build once before
the build proceeds; without it, you'd need a manual orchestration
step.
License
MIT — see LICENSE.
Agent / contributor guidance
See AGENTS.md for the design contract, architecture
map, and conventions agents and contributors should follow when
making changes. (CLAUDE.md is a passthrough to
AGENTS.md for Claude Code.)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sphinx_vite_builder-0.0.1a16.tar.gz.
File metadata
- Download URL: sphinx_vite_builder-0.0.1a16.tar.gz
- Upload date:
- Size: 29.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f37c175090dc56645abf61ce91227839e53caef15ab29ef1eb88757f5b7608f5
|
|
| MD5 |
c66f54b6c5f7693c879dcd9941f3150e
|
|
| BLAKE2b-256 |
1aa040b3af4ddab871a356a9ec6a6648c704ee5085b1393bdb82aee3a3412888
|
Provenance
The following attestation bundles were made for sphinx_vite_builder-0.0.1a16.tar.gz:
Publisher:
release.yml on git-pull/gp-sphinx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sphinx_vite_builder-0.0.1a16.tar.gz -
Subject digest:
f37c175090dc56645abf61ce91227839e53caef15ab29ef1eb88757f5b7608f5 - Sigstore transparency entry: 1436075430
- Sigstore integration time:
-
Permalink:
git-pull/gp-sphinx@81eb49b33bf4a57020fd77e2289af19eb0940a60 -
Branch / Tag:
refs/tags/v0.0.1a16 - Owner: https://github.com/git-pull
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@81eb49b33bf4a57020fd77e2289af19eb0940a60 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sphinx_vite_builder-0.0.1a16-py3-none-any.whl.
File metadata
- Download URL: sphinx_vite_builder-0.0.1a16-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
520a35bd7bf588e39e7453670f9b966786199f4746fe53c6638c33730a5a034c
|
|
| MD5 |
39614181113c6a53042377252ef06287
|
|
| BLAKE2b-256 |
7daec85d7ae51f8d017053f4409c4058be628cb9e6af14446776c4a601ab5e2e
|
Provenance
The following attestation bundles were made for sphinx_vite_builder-0.0.1a16-py3-none-any.whl:
Publisher:
release.yml on git-pull/gp-sphinx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sphinx_vite_builder-0.0.1a16-py3-none-any.whl -
Subject digest:
520a35bd7bf588e39e7453670f9b966786199f4746fe53c6638c33730a5a034c - Sigstore transparency entry: 1436075456
- Sigstore integration time:
-
Permalink:
git-pull/gp-sphinx@81eb49b33bf4a57020fd77e2289af19eb0940a60 -
Branch / Tag:
refs/tags/v0.0.1a16 - Owner: https://github.com/git-pull
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@81eb49b33bf4a57020fd77e2289af19eb0940a60 -
Trigger Event:
push
-
Statement type: