sphinx-vite-builder 0.0.1a16.dev2__tar.gz → 0.0.1a16.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sphinx_vite_builder-0.0.1a16.dev3/PKG-INFO +344 -0
- sphinx_vite_builder-0.0.1a16.dev3/README.md +317 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/pyproject.toml +8 -1
- sphinx_vite_builder-0.0.1a16.dev3/src/sphinx_vite_builder/__init__.py +93 -0
- sphinx_vite_builder-0.0.1a16.dev3/src/sphinx_vite_builder/_internal/config.py +190 -0
- sphinx_vite_builder-0.0.1a16.dev3/src/sphinx_vite_builder/_internal/hooks.py +261 -0
- sphinx_vite_builder-0.0.1a16.dev3/src/sphinx_vite_builder/hatch_plugin.py +92 -0
- sphinx_vite_builder-0.0.1a16.dev2/PKG-INFO +0 -200
- sphinx_vite_builder-0.0.1a16.dev2/README.md +0 -173
- sphinx_vite_builder-0.0.1a16.dev2/src/sphinx_vite_builder/__init__.py +0 -60
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/.gitignore +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/AGENTS.md +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/CLAUDE.md +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/src/sphinx_vite_builder/_internal/__init__.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/src/sphinx_vite_builder/_internal/bus.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/src/sphinx_vite_builder/_internal/errors.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/src/sphinx_vite_builder/_internal/process.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/src/sphinx_vite_builder/_internal/vite.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/src/sphinx_vite_builder/build.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev3}/src/sphinx_vite_builder/py.typed +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sphinx-vite-builder
|
|
3
|
+
Version: 0.0.1a16.dev3
|
|
4
|
+
Summary: PEP 517 build backend + Sphinx extension that orchestrates Vite via pnpm
|
|
5
|
+
Project-URL: Repository, https://github.com/git-pull/gp-sphinx
|
|
6
|
+
Author-email: Tony Narlock <tony@git-pull.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: backend,build,extension,pep517,pnpm,sphinx,vite
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: Sphinx
|
|
11
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Documentation
|
|
21
|
+
Classifier: Topic :: Documentation :: Sphinx
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: <4.0,>=3.10
|
|
25
|
+
Requires-Dist: sphinx>=8.1
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# sphinx-vite-builder
|
|
29
|
+
|
|
30
|
+
PEP 517 build backend and Sphinx extension that transparently orchestrates
|
|
31
|
+
[Vite](https://vitejs.dev/) builds via [pnpm](https://pnpm.io/) for
|
|
32
|
+
Sphinx-theme packages whose static assets (CSS / JS) are produced by a
|
|
33
|
+
JavaScript toolchain.
|
|
34
|
+
|
|
35
|
+
## What it solves
|
|
36
|
+
|
|
37
|
+
A common pattern for modern Sphinx themes is a Python package whose
|
|
38
|
+
`theme/<name>/static/` directory ships built CSS and JS that were
|
|
39
|
+
produced by a JS build tool (Vite, webpack, …). The build artefacts are
|
|
40
|
+
gitignored — they're reproducibly built, not source code. But that
|
|
41
|
+
creates two friction points:
|
|
42
|
+
|
|
43
|
+
1. **Editable installs and source-tree builds** crash with confusing
|
|
44
|
+
errors when the static dir is empty (e.g. hatchling's
|
|
45
|
+
`Forced include not found`).
|
|
46
|
+
2. **CI workflows** must duplicate `pnpm install + vite build` setup
|
|
47
|
+
steps in every job that touches the package.
|
|
48
|
+
|
|
49
|
+
`sphinx-vite-builder` owns the Vite invocation end-to-end — exactly the
|
|
50
|
+
way [maturin](https://github.com/PyO3/maturin) owns Cargo for
|
|
51
|
+
Rust+Python packages, or
|
|
52
|
+
[sphinx-theme-builder](https://github.com/pradyunsg/sphinx-theme-builder)
|
|
53
|
+
owns webpack for older Sphinx themes.
|
|
54
|
+
|
|
55
|
+
## The contract
|
|
56
|
+
|
|
57
|
+
> **Sources should check for node, pnpm, etc and error if it's not
|
|
58
|
+
> good, then build. Wheels should have the build files baked in and
|
|
59
|
+
> not need node and pnpm at all.**
|
|
60
|
+
|
|
61
|
+
This is the central invariant of the package. The two install paths
|
|
62
|
+
behave asymmetrically by design:
|
|
63
|
+
|
|
64
|
+
### Wheel installs — zero toolchain required
|
|
65
|
+
|
|
66
|
+
A user running `pip install <package>` from PyPI gets a wheel that
|
|
67
|
+
**already contains** the vite-built `static/` tree, populated by this
|
|
68
|
+
backend at release time. The PEP 517 chain doesn't run on the
|
|
69
|
+
consumer side. No backend invocation. No `pnpm`. No Node. The end
|
|
70
|
+
user sees Python and only Python.
|
|
71
|
+
|
|
72
|
+
No pnpm, no Node — just Python:
|
|
73
|
+
|
|
74
|
+
```console
|
|
75
|
+
$ pip install gp-furo-theme
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Source builds — fail loud, fail informatively
|
|
79
|
+
|
|
80
|
+
A contributor (or downstream packager building from source) goes
|
|
81
|
+
through the PEP 517 chain. The backend runs `pnpm exec vite build`
|
|
82
|
+
to produce `static/`, and that requires pnpm + Node on PATH. If the
|
|
83
|
+
toolchain is missing, the backend raises a typed exception with a
|
|
84
|
+
multi-line, copy-pasteable hint:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
sphinx-vite-builder: cannot bootstrap the vite toolchain.
|
|
88
|
+
`pnpm` is not on PATH. Install it via one of:
|
|
89
|
+
|
|
90
|
+
corepack enable # Node 16.10+ ships corepack
|
|
91
|
+
curl -fsSL https://get.pnpm.io/install.sh | sh -
|
|
92
|
+
|
|
93
|
+
See https://pnpm.io/installation
|
|
94
|
+
|
|
95
|
+
…
|
|
96
|
+
|
|
97
|
+
Detected CI provider: GitHub Actions. Add the following to your pipeline
|
|
98
|
+
config (before the Python build step that triggers this backend):
|
|
99
|
+
|
|
100
|
+
- uses: pnpm/action-setup@v6
|
|
101
|
+
with:
|
|
102
|
+
version: 10
|
|
103
|
+
- uses: actions/setup-node@v6
|
|
104
|
+
with:
|
|
105
|
+
node-version: 22
|
|
106
|
+
cache: pnpm
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The error includes the resolved vite-root path, the platform-specific
|
|
110
|
+
CI setup recipe (GitHub Actions, CircleCI, Azure Pipelines, GitLab CI,
|
|
111
|
+
or generic), and the `SPHINX_VITE_BUILDER_SKIP=1` escape hatch for
|
|
112
|
+
environments that genuinely don't need vite to run.
|
|
113
|
+
|
|
114
|
+
### The `web/`-absent short-circuit (sdist install bridge)
|
|
115
|
+
|
|
116
|
+
A user running `pip install <pkg>.tar.gz` from an sdist runs the
|
|
117
|
+
PEP 517 chain too — but the sdist excludes `web/` (the Vite source
|
|
118
|
+
tree). The backend detects the absence, short-circuits cleanly, and
|
|
119
|
+
hatchling packs the pre-baked `static/` (carried in the sdist via
|
|
120
|
+
`[tool.hatch.build] artifacts`) into the wheel. **Sdist installs
|
|
121
|
+
need no toolchain either.**
|
|
122
|
+
|
|
123
|
+
The asymmetry is the whole product: the same backend is strict
|
|
124
|
+
(running and failing loudly) when there's a `web/` to act on, and
|
|
125
|
+
silent (skipping cleanly) when there's no `web/` to begin with. The
|
|
126
|
+
two shapes match the two consumer worlds.
|
|
127
|
+
|
|
128
|
+
## Quick start — two activation variants
|
|
129
|
+
|
|
130
|
+
`sphinx-vite-builder` ships two orthogonal ways to wire vite into a
|
|
131
|
+
hatchling-built Python package. Pick whichever fits the consumer's
|
|
132
|
+
existing build setup; they are mutually exclusive at the
|
|
133
|
+
`[build-system].build-backend` level.
|
|
134
|
+
|
|
135
|
+
### Variant 1 — PEP 517 backend (drop-in replacement)
|
|
136
|
+
|
|
137
|
+
The simplest activation. Replace `hatchling.build` with
|
|
138
|
+
`sphinx_vite_builder.build` and you're done.
|
|
139
|
+
|
|
140
|
+
```toml
|
|
141
|
+
# packages/your-theme/pyproject.toml
|
|
142
|
+
[build-system]
|
|
143
|
+
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
144
|
+
build-backend = "sphinx_vite_builder.build"
|
|
145
|
+
|
|
146
|
+
[tool.hatch.build.targets.sdist]
|
|
147
|
+
exclude = ["web/"] # so the sdist→wheel chain hits the short-circuit
|
|
148
|
+
|
|
149
|
+
[tool.hatch.build]
|
|
150
|
+
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Variant 2 — Hatchling build hook (composable)
|
|
154
|
+
|
|
155
|
+
For projects that want to keep `build-backend = "hatchling.build"` and
|
|
156
|
+
layer vite on top of an existing hatchling hook stack
|
|
157
|
+
(`version`, custom build scripts, etc.):
|
|
158
|
+
|
|
159
|
+
```toml
|
|
160
|
+
# packages/your-theme/pyproject.toml
|
|
161
|
+
[build-system]
|
|
162
|
+
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
163
|
+
build-backend = "hatchling.build"
|
|
164
|
+
|
|
165
|
+
[tool.hatch.build.hooks.vite]
|
|
166
|
+
|
|
167
|
+
[tool.hatch.build.targets.sdist]
|
|
168
|
+
exclude = ["web/"]
|
|
169
|
+
|
|
170
|
+
[tool.hatch.build]
|
|
171
|
+
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Both variants share the same orchestration core — same SKIP env var,
|
|
175
|
+
same `web/`-absent short-circuit, same fast-fail diagnostics. Pick
|
|
176
|
+
variant 1 for simplicity, variant 2 for composability.
|
|
177
|
+
|
|
178
|
+
### Sphinx extension (orthogonal to either build variant)
|
|
179
|
+
|
|
180
|
+
The Sphinx-extension head is independent of the backend / hook
|
|
181
|
+
choice. Wire it into a docs build to get vite running automatically
|
|
182
|
+
during `sphinx-build` (one-shot) and `sphinx-autobuild` (watched).
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# docs/conf.py
|
|
186
|
+
extensions = ["sphinx_vite_builder"]
|
|
187
|
+
sphinx_vite_builder_mode = "auto" # "auto" | "dev" | "prod"
|
|
188
|
+
sphinx_vite_builder_root = "/abs/path/to/web"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`"auto"` resolves to `"dev"` when the build is running under
|
|
192
|
+
`sphinx-autobuild` (detected via `SPHINX_AUTOBUILD` env var, `argv[0]`,
|
|
193
|
+
or parent-process inspection on Linux), `"prod"` otherwise. Setting
|
|
194
|
+
`sphinx_vite_builder_root` to `None` (the default) makes the extension
|
|
195
|
+
a complete no-op — useful when the consumer is installed from a wheel
|
|
196
|
+
where the static tree is already pre-baked.
|
|
197
|
+
|
|
198
|
+
## Comparison with similar tools
|
|
199
|
+
|
|
200
|
+
| Tool | Toolchain owned | Activation strategy | Bootstrap |
|
|
201
|
+
|---|---|---|---|
|
|
202
|
+
| [`maturin`](https://github.com/PyO3/maturin) | Rust (Cargo) | self-hosting via `bootstrap` shim | `puccinialin` auto-installs Rust |
|
|
203
|
+
| [`sphinx-theme-builder`](https://github.com/pradyunsg/sphinx-theme-builder) | Node (webpack) | rolls own ZIP packing | `nodeenv` (isolated Node env) |
|
|
204
|
+
| [`hatch-jupyter-builder`](https://github.com/jupyterlab/hatch-jupyter-builder) | Node (npm/yarn) | hatchling build hook | user-managed Node |
|
|
205
|
+
| `sphinx-vite-builder` | Node (vite) | PEP 517 backend **or** hatchling hook | user-managed pnpm (corepack) |
|
|
206
|
+
|
|
207
|
+
`sphinx-vite-builder` deliberately diverges from `sphinx-theme-builder`'s
|
|
208
|
+
`nodeenv` approach: pnpm via corepack is the modern Node convention, and
|
|
209
|
+
auto-installing Node into the project tree pulls in significant friction
|
|
210
|
+
for editable workflows. Compared to `maturin`, the Rust analog,
|
|
211
|
+
sphinx-vite-builder doesn't auto-install pnpm — pnpm isn't pip-installable,
|
|
212
|
+
so the failure mode is "user runs `corepack enable`" rather than "backend
|
|
213
|
+
bootstraps a Node env."
|
|
214
|
+
|
|
215
|
+
## Migrating from manual orchestration
|
|
216
|
+
|
|
217
|
+
If your project currently runs `pnpm exec vite build` from a CI step,
|
|
218
|
+
a justfile recipe, or a Makefile target, you can drop those:
|
|
219
|
+
|
|
220
|
+
| Was | Now |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `tests.yml` step: `pnpm install && pnpm exec vite build` | The backend / hook handles it; only keep pnpm/Node setup |
|
|
223
|
+
| `release.yml` step: `pnpm install && pnpm exec vite build` | Same — keep pnpm/Node setup, drop the manual build call |
|
|
224
|
+
| `justfile` recipe `_assets-build` as prerequisite of `html` | Drop the recipe; the Sphinx extension's PROD-mode hook runs vite |
|
|
225
|
+
| Hatchling `[tool.hatch.build] force-include` for `static/` | Drop; the backend produces the static tree before hatchling packs |
|
|
226
|
+
|
|
227
|
+
Keep your CI's pnpm + Node setup steps — the backend needs them at
|
|
228
|
+
build time even though the wheel installation doesn't.
|
|
229
|
+
|
|
230
|
+
## Fast-fail diagnostics — error reference
|
|
231
|
+
|
|
232
|
+
| Error | When | Hint includes |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `PnpmMissingError` | `pnpm` not on `PATH` during a source build | `corepack enable`, [pnpm.io/installation](https://pnpm.io/installation), per-CI YAML recipe, `SPHINX_VITE_BUILDER_SKIP=1` |
|
|
235
|
+
| `NodeModulesInstallError` | `pnpm install` exited non-zero | `cd <vite-root> && pnpm install --frozen-lockfile` rerun command, captured stderr |
|
|
236
|
+
| `ViteFailedError` | `pnpm exec vite build` exited non-zero | invocation context (cwd, exit code), captured stderr |
|
|
237
|
+
|
|
238
|
+
All three inherit from `SphinxViteBuilderError`, so consumers can
|
|
239
|
+
`except SphinxViteBuilderError` for a single catch surface.
|
|
240
|
+
|
|
241
|
+
## CI recipe gallery
|
|
242
|
+
|
|
243
|
+
The `PnpmMissingError` hint is **self-healing** when the backend
|
|
244
|
+
detects a CI environment — it embeds the platform-specific setup
|
|
245
|
+
recipe in the error message. They are also reproduced here for
|
|
246
|
+
search-discoverability.
|
|
247
|
+
|
|
248
|
+
### GitHub Actions (`GITHUB_ACTIONS=true`)
|
|
249
|
+
|
|
250
|
+
```yaml
|
|
251
|
+
- uses: pnpm/action-setup@v6
|
|
252
|
+
with:
|
|
253
|
+
version: 10
|
|
254
|
+
- uses: actions/setup-node@v6
|
|
255
|
+
with:
|
|
256
|
+
node-version: 22
|
|
257
|
+
cache: pnpm
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### CircleCI (`CIRCLECI=true`)
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
- run:
|
|
264
|
+
name: Install pnpm via corepack
|
|
265
|
+
command: |
|
|
266
|
+
corepack enable
|
|
267
|
+
corepack prepare pnpm@latest-10 --activate
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Azure Pipelines (`TF_BUILD=True`)
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
- task: NodeTool@0
|
|
274
|
+
inputs:
|
|
275
|
+
versionSpec: '22.x'
|
|
276
|
+
- script: |
|
|
277
|
+
corepack enable
|
|
278
|
+
corepack prepare pnpm@latest-10 --activate
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### GitLab CI (`GITLAB_CI=true`)
|
|
282
|
+
|
|
283
|
+
```yaml
|
|
284
|
+
before_script:
|
|
285
|
+
- corepack enable
|
|
286
|
+
- corepack prepare pnpm@latest-10 --activate
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Generic CI (any other `CI=true`)
|
|
290
|
+
|
|
291
|
+
Use your CI's package-manager setup mechanism to put `pnpm` (>=10)
|
|
292
|
+
and `node` (>=22) on PATH before the Python build step runs.
|
|
293
|
+
|
|
294
|
+
Detection precedence (most-specific wins): each provider's canonical
|
|
295
|
+
env var per the pnpm
|
|
296
|
+
[Continuous Integration docs](https://pnpm.io/continuous-integration)
|
|
297
|
+
is checked first; the generic `CI=true` is the fallback for "we know
|
|
298
|
+
we're in CI but don't recognise the provider."
|
|
299
|
+
|
|
300
|
+
## Troubleshooting
|
|
301
|
+
|
|
302
|
+
**`PnpmMissingError: pnpm is not on PATH`** — install pnpm via
|
|
303
|
+
`corepack enable` (Node 16.10+) or follow the per-CI recipe in the
|
|
304
|
+
error message. If you're in an environment that genuinely doesn't
|
|
305
|
+
need vite to run (e.g. building from a pre-baked sdist), set
|
|
306
|
+
`SPHINX_VITE_BUILDER_SKIP=1` in the environment.
|
|
307
|
+
|
|
308
|
+
**`NodeModulesInstallError`** — `pnpm install --frozen-lockfile`
|
|
309
|
+
exited non-zero. The error surfaces the captured stderr. Common
|
|
310
|
+
causes: stale `pnpm-lock.yaml` (run `pnpm install` interactively to
|
|
311
|
+
refresh), network/registry timeout (retry), or `engines` mismatch
|
|
312
|
+
(check the project's `package.json` `engines` field against the
|
|
313
|
+
installed Node/pnpm versions).
|
|
314
|
+
|
|
315
|
+
**`ViteFailedError`** — `pnpm exec vite build` exited non-zero.
|
|
316
|
+
Captured stderr is included in the hint. This is usually a
|
|
317
|
+
project-side compile error (TypeScript type check, SCSS syntax,
|
|
318
|
+
missing import) rather than a tooling problem. Reproduce with
|
|
319
|
+
`(cd <vite-root> && pnpm exec vite build)` to see vite's full output.
|
|
320
|
+
|
|
321
|
+
**Wheel ships without `static/` content** — the backend ran but the
|
|
322
|
+
artefacts didn't make it into the wheel. Verify your `pyproject.toml`
|
|
323
|
+
has the `[tool.hatch.build] artifacts = ["src/.../static/"]`
|
|
324
|
+
declaration. Hatchling's documented "VCS-ignored include" mechanism
|
|
325
|
+
requires this even when the path is on disk; `force-include` does
|
|
326
|
+
not work for editable builds.
|
|
327
|
+
|
|
328
|
+
**`just html` (or any plain `sphinx-build`) doesn't rebuild assets** —
|
|
329
|
+
make sure `sphinx_vite_builder` is loaded in `extensions` and that
|
|
330
|
+
`sphinx_vite_builder_root` points at your `web/` directory. The
|
|
331
|
+
extension's PROD-mode hook runs `pnpm exec vite build` once before
|
|
332
|
+
the build proceeds; without it, you'd need a manual orchestration
|
|
333
|
+
step.
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
MIT — see [LICENSE](LICENSE).
|
|
338
|
+
|
|
339
|
+
## Agent / contributor guidance
|
|
340
|
+
|
|
341
|
+
See [`AGENTS.md`](AGENTS.md) for the design contract, architecture
|
|
342
|
+
map, and conventions agents and contributors should follow when
|
|
343
|
+
making changes. ([`CLAUDE.md`](CLAUDE.md) is a passthrough to
|
|
344
|
+
`AGENTS.md` for Claude Code.)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# sphinx-vite-builder
|
|
2
|
+
|
|
3
|
+
PEP 517 build backend and Sphinx extension that transparently orchestrates
|
|
4
|
+
[Vite](https://vitejs.dev/) builds via [pnpm](https://pnpm.io/) for
|
|
5
|
+
Sphinx-theme packages whose static assets (CSS / JS) are produced by a
|
|
6
|
+
JavaScript toolchain.
|
|
7
|
+
|
|
8
|
+
## What it solves
|
|
9
|
+
|
|
10
|
+
A common pattern for modern Sphinx themes is a Python package whose
|
|
11
|
+
`theme/<name>/static/` directory ships built CSS and JS that were
|
|
12
|
+
produced by a JS build tool (Vite, webpack, …). The build artefacts are
|
|
13
|
+
gitignored — they're reproducibly built, not source code. But that
|
|
14
|
+
creates two friction points:
|
|
15
|
+
|
|
16
|
+
1. **Editable installs and source-tree builds** crash with confusing
|
|
17
|
+
errors when the static dir is empty (e.g. hatchling's
|
|
18
|
+
`Forced include not found`).
|
|
19
|
+
2. **CI workflows** must duplicate `pnpm install + vite build` setup
|
|
20
|
+
steps in every job that touches the package.
|
|
21
|
+
|
|
22
|
+
`sphinx-vite-builder` owns the Vite invocation end-to-end — exactly the
|
|
23
|
+
way [maturin](https://github.com/PyO3/maturin) owns Cargo for
|
|
24
|
+
Rust+Python packages, or
|
|
25
|
+
[sphinx-theme-builder](https://github.com/pradyunsg/sphinx-theme-builder)
|
|
26
|
+
owns webpack for older Sphinx themes.
|
|
27
|
+
|
|
28
|
+
## The contract
|
|
29
|
+
|
|
30
|
+
> **Sources should check for node, pnpm, etc and error if it's not
|
|
31
|
+
> good, then build. Wheels should have the build files baked in and
|
|
32
|
+
> not need node and pnpm at all.**
|
|
33
|
+
|
|
34
|
+
This is the central invariant of the package. The two install paths
|
|
35
|
+
behave asymmetrically by design:
|
|
36
|
+
|
|
37
|
+
### Wheel installs — zero toolchain required
|
|
38
|
+
|
|
39
|
+
A user running `pip install <package>` from PyPI gets a wheel that
|
|
40
|
+
**already contains** the vite-built `static/` tree, populated by this
|
|
41
|
+
backend at release time. The PEP 517 chain doesn't run on the
|
|
42
|
+
consumer side. No backend invocation. No `pnpm`. No Node. The end
|
|
43
|
+
user sees Python and only Python.
|
|
44
|
+
|
|
45
|
+
No pnpm, no Node — just Python:
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
$ pip install gp-furo-theme
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Source builds — fail loud, fail informatively
|
|
52
|
+
|
|
53
|
+
A contributor (or downstream packager building from source) goes
|
|
54
|
+
through the PEP 517 chain. The backend runs `pnpm exec vite build`
|
|
55
|
+
to produce `static/`, and that requires pnpm + Node on PATH. If the
|
|
56
|
+
toolchain is missing, the backend raises a typed exception with a
|
|
57
|
+
multi-line, copy-pasteable hint:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
sphinx-vite-builder: cannot bootstrap the vite toolchain.
|
|
61
|
+
`pnpm` is not on PATH. Install it via one of:
|
|
62
|
+
|
|
63
|
+
corepack enable # Node 16.10+ ships corepack
|
|
64
|
+
curl -fsSL https://get.pnpm.io/install.sh | sh -
|
|
65
|
+
|
|
66
|
+
See https://pnpm.io/installation
|
|
67
|
+
|
|
68
|
+
…
|
|
69
|
+
|
|
70
|
+
Detected CI provider: GitHub Actions. Add the following to your pipeline
|
|
71
|
+
config (before the Python build step that triggers this backend):
|
|
72
|
+
|
|
73
|
+
- uses: pnpm/action-setup@v6
|
|
74
|
+
with:
|
|
75
|
+
version: 10
|
|
76
|
+
- uses: actions/setup-node@v6
|
|
77
|
+
with:
|
|
78
|
+
node-version: 22
|
|
79
|
+
cache: pnpm
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The error includes the resolved vite-root path, the platform-specific
|
|
83
|
+
CI setup recipe (GitHub Actions, CircleCI, Azure Pipelines, GitLab CI,
|
|
84
|
+
or generic), and the `SPHINX_VITE_BUILDER_SKIP=1` escape hatch for
|
|
85
|
+
environments that genuinely don't need vite to run.
|
|
86
|
+
|
|
87
|
+
### The `web/`-absent short-circuit (sdist install bridge)
|
|
88
|
+
|
|
89
|
+
A user running `pip install <pkg>.tar.gz` from an sdist runs the
|
|
90
|
+
PEP 517 chain too — but the sdist excludes `web/` (the Vite source
|
|
91
|
+
tree). The backend detects the absence, short-circuits cleanly, and
|
|
92
|
+
hatchling packs the pre-baked `static/` (carried in the sdist via
|
|
93
|
+
`[tool.hatch.build] artifacts`) into the wheel. **Sdist installs
|
|
94
|
+
need no toolchain either.**
|
|
95
|
+
|
|
96
|
+
The asymmetry is the whole product: the same backend is strict
|
|
97
|
+
(running and failing loudly) when there's a `web/` to act on, and
|
|
98
|
+
silent (skipping cleanly) when there's no `web/` to begin with. The
|
|
99
|
+
two shapes match the two consumer worlds.
|
|
100
|
+
|
|
101
|
+
## Quick start — two activation variants
|
|
102
|
+
|
|
103
|
+
`sphinx-vite-builder` ships two orthogonal ways to wire vite into a
|
|
104
|
+
hatchling-built Python package. Pick whichever fits the consumer's
|
|
105
|
+
existing build setup; they are mutually exclusive at the
|
|
106
|
+
`[build-system].build-backend` level.
|
|
107
|
+
|
|
108
|
+
### Variant 1 — PEP 517 backend (drop-in replacement)
|
|
109
|
+
|
|
110
|
+
The simplest activation. Replace `hatchling.build` with
|
|
111
|
+
`sphinx_vite_builder.build` and you're done.
|
|
112
|
+
|
|
113
|
+
```toml
|
|
114
|
+
# packages/your-theme/pyproject.toml
|
|
115
|
+
[build-system]
|
|
116
|
+
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
117
|
+
build-backend = "sphinx_vite_builder.build"
|
|
118
|
+
|
|
119
|
+
[tool.hatch.build.targets.sdist]
|
|
120
|
+
exclude = ["web/"] # so the sdist→wheel chain hits the short-circuit
|
|
121
|
+
|
|
122
|
+
[tool.hatch.build]
|
|
123
|
+
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Variant 2 — Hatchling build hook (composable)
|
|
127
|
+
|
|
128
|
+
For projects that want to keep `build-backend = "hatchling.build"` and
|
|
129
|
+
layer vite on top of an existing hatchling hook stack
|
|
130
|
+
(`version`, custom build scripts, etc.):
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
# packages/your-theme/pyproject.toml
|
|
134
|
+
[build-system]
|
|
135
|
+
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
136
|
+
build-backend = "hatchling.build"
|
|
137
|
+
|
|
138
|
+
[tool.hatch.build.hooks.vite]
|
|
139
|
+
|
|
140
|
+
[tool.hatch.build.targets.sdist]
|
|
141
|
+
exclude = ["web/"]
|
|
142
|
+
|
|
143
|
+
[tool.hatch.build]
|
|
144
|
+
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Both variants share the same orchestration core — same SKIP env var,
|
|
148
|
+
same `web/`-absent short-circuit, same fast-fail diagnostics. Pick
|
|
149
|
+
variant 1 for simplicity, variant 2 for composability.
|
|
150
|
+
|
|
151
|
+
### Sphinx extension (orthogonal to either build variant)
|
|
152
|
+
|
|
153
|
+
The Sphinx-extension head is independent of the backend / hook
|
|
154
|
+
choice. Wire it into a docs build to get vite running automatically
|
|
155
|
+
during `sphinx-build` (one-shot) and `sphinx-autobuild` (watched).
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# docs/conf.py
|
|
159
|
+
extensions = ["sphinx_vite_builder"]
|
|
160
|
+
sphinx_vite_builder_mode = "auto" # "auto" | "dev" | "prod"
|
|
161
|
+
sphinx_vite_builder_root = "/abs/path/to/web"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`"auto"` resolves to `"dev"` when the build is running under
|
|
165
|
+
`sphinx-autobuild` (detected via `SPHINX_AUTOBUILD` env var, `argv[0]`,
|
|
166
|
+
or parent-process inspection on Linux), `"prod"` otherwise. Setting
|
|
167
|
+
`sphinx_vite_builder_root` to `None` (the default) makes the extension
|
|
168
|
+
a complete no-op — useful when the consumer is installed from a wheel
|
|
169
|
+
where the static tree is already pre-baked.
|
|
170
|
+
|
|
171
|
+
## Comparison with similar tools
|
|
172
|
+
|
|
173
|
+
| Tool | Toolchain owned | Activation strategy | Bootstrap |
|
|
174
|
+
|---|---|---|---|
|
|
175
|
+
| [`maturin`](https://github.com/PyO3/maturin) | Rust (Cargo) | self-hosting via `bootstrap` shim | `puccinialin` auto-installs Rust |
|
|
176
|
+
| [`sphinx-theme-builder`](https://github.com/pradyunsg/sphinx-theme-builder) | Node (webpack) | rolls own ZIP packing | `nodeenv` (isolated Node env) |
|
|
177
|
+
| [`hatch-jupyter-builder`](https://github.com/jupyterlab/hatch-jupyter-builder) | Node (npm/yarn) | hatchling build hook | user-managed Node |
|
|
178
|
+
| `sphinx-vite-builder` | Node (vite) | PEP 517 backend **or** hatchling hook | user-managed pnpm (corepack) |
|
|
179
|
+
|
|
180
|
+
`sphinx-vite-builder` deliberately diverges from `sphinx-theme-builder`'s
|
|
181
|
+
`nodeenv` approach: pnpm via corepack is the modern Node convention, and
|
|
182
|
+
auto-installing Node into the project tree pulls in significant friction
|
|
183
|
+
for editable workflows. Compared to `maturin`, the Rust analog,
|
|
184
|
+
sphinx-vite-builder doesn't auto-install pnpm — pnpm isn't pip-installable,
|
|
185
|
+
so the failure mode is "user runs `corepack enable`" rather than "backend
|
|
186
|
+
bootstraps a Node env."
|
|
187
|
+
|
|
188
|
+
## Migrating from manual orchestration
|
|
189
|
+
|
|
190
|
+
If your project currently runs `pnpm exec vite build` from a CI step,
|
|
191
|
+
a justfile recipe, or a Makefile target, you can drop those:
|
|
192
|
+
|
|
193
|
+
| Was | Now |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `tests.yml` step: `pnpm install && pnpm exec vite build` | The backend / hook handles it; only keep pnpm/Node setup |
|
|
196
|
+
| `release.yml` step: `pnpm install && pnpm exec vite build` | Same — keep pnpm/Node setup, drop the manual build call |
|
|
197
|
+
| `justfile` recipe `_assets-build` as prerequisite of `html` | Drop the recipe; the Sphinx extension's PROD-mode hook runs vite |
|
|
198
|
+
| Hatchling `[tool.hatch.build] force-include` for `static/` | Drop; the backend produces the static tree before hatchling packs |
|
|
199
|
+
|
|
200
|
+
Keep your CI's pnpm + Node setup steps — the backend needs them at
|
|
201
|
+
build time even though the wheel installation doesn't.
|
|
202
|
+
|
|
203
|
+
## Fast-fail diagnostics — error reference
|
|
204
|
+
|
|
205
|
+
| Error | When | Hint includes |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| `PnpmMissingError` | `pnpm` not on `PATH` during a source build | `corepack enable`, [pnpm.io/installation](https://pnpm.io/installation), per-CI YAML recipe, `SPHINX_VITE_BUILDER_SKIP=1` |
|
|
208
|
+
| `NodeModulesInstallError` | `pnpm install` exited non-zero | `cd <vite-root> && pnpm install --frozen-lockfile` rerun command, captured stderr |
|
|
209
|
+
| `ViteFailedError` | `pnpm exec vite build` exited non-zero | invocation context (cwd, exit code), captured stderr |
|
|
210
|
+
|
|
211
|
+
All three inherit from `SphinxViteBuilderError`, so consumers can
|
|
212
|
+
`except SphinxViteBuilderError` for a single catch surface.
|
|
213
|
+
|
|
214
|
+
## CI recipe gallery
|
|
215
|
+
|
|
216
|
+
The `PnpmMissingError` hint is **self-healing** when the backend
|
|
217
|
+
detects a CI environment — it embeds the platform-specific setup
|
|
218
|
+
recipe in the error message. They are also reproduced here for
|
|
219
|
+
search-discoverability.
|
|
220
|
+
|
|
221
|
+
### GitHub Actions (`GITHUB_ACTIONS=true`)
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
- uses: pnpm/action-setup@v6
|
|
225
|
+
with:
|
|
226
|
+
version: 10
|
|
227
|
+
- uses: actions/setup-node@v6
|
|
228
|
+
with:
|
|
229
|
+
node-version: 22
|
|
230
|
+
cache: pnpm
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### CircleCI (`CIRCLECI=true`)
|
|
234
|
+
|
|
235
|
+
```yaml
|
|
236
|
+
- run:
|
|
237
|
+
name: Install pnpm via corepack
|
|
238
|
+
command: |
|
|
239
|
+
corepack enable
|
|
240
|
+
corepack prepare pnpm@latest-10 --activate
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Azure Pipelines (`TF_BUILD=True`)
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
- task: NodeTool@0
|
|
247
|
+
inputs:
|
|
248
|
+
versionSpec: '22.x'
|
|
249
|
+
- script: |
|
|
250
|
+
corepack enable
|
|
251
|
+
corepack prepare pnpm@latest-10 --activate
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### GitLab CI (`GITLAB_CI=true`)
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
before_script:
|
|
258
|
+
- corepack enable
|
|
259
|
+
- corepack prepare pnpm@latest-10 --activate
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Generic CI (any other `CI=true`)
|
|
263
|
+
|
|
264
|
+
Use your CI's package-manager setup mechanism to put `pnpm` (>=10)
|
|
265
|
+
and `node` (>=22) on PATH before the Python build step runs.
|
|
266
|
+
|
|
267
|
+
Detection precedence (most-specific wins): each provider's canonical
|
|
268
|
+
env var per the pnpm
|
|
269
|
+
[Continuous Integration docs](https://pnpm.io/continuous-integration)
|
|
270
|
+
is checked first; the generic `CI=true` is the fallback for "we know
|
|
271
|
+
we're in CI but don't recognise the provider."
|
|
272
|
+
|
|
273
|
+
## Troubleshooting
|
|
274
|
+
|
|
275
|
+
**`PnpmMissingError: pnpm is not on PATH`** — install pnpm via
|
|
276
|
+
`corepack enable` (Node 16.10+) or follow the per-CI recipe in the
|
|
277
|
+
error message. If you're in an environment that genuinely doesn't
|
|
278
|
+
need vite to run (e.g. building from a pre-baked sdist), set
|
|
279
|
+
`SPHINX_VITE_BUILDER_SKIP=1` in the environment.
|
|
280
|
+
|
|
281
|
+
**`NodeModulesInstallError`** — `pnpm install --frozen-lockfile`
|
|
282
|
+
exited non-zero. The error surfaces the captured stderr. Common
|
|
283
|
+
causes: stale `pnpm-lock.yaml` (run `pnpm install` interactively to
|
|
284
|
+
refresh), network/registry timeout (retry), or `engines` mismatch
|
|
285
|
+
(check the project's `package.json` `engines` field against the
|
|
286
|
+
installed Node/pnpm versions).
|
|
287
|
+
|
|
288
|
+
**`ViteFailedError`** — `pnpm exec vite build` exited non-zero.
|
|
289
|
+
Captured stderr is included in the hint. This is usually a
|
|
290
|
+
project-side compile error (TypeScript type check, SCSS syntax,
|
|
291
|
+
missing import) rather than a tooling problem. Reproduce with
|
|
292
|
+
`(cd <vite-root> && pnpm exec vite build)` to see vite's full output.
|
|
293
|
+
|
|
294
|
+
**Wheel ships without `static/` content** — the backend ran but the
|
|
295
|
+
artefacts didn't make it into the wheel. Verify your `pyproject.toml`
|
|
296
|
+
has the `[tool.hatch.build] artifacts = ["src/.../static/"]`
|
|
297
|
+
declaration. Hatchling's documented "VCS-ignored include" mechanism
|
|
298
|
+
requires this even when the path is on disk; `force-include` does
|
|
299
|
+
not work for editable builds.
|
|
300
|
+
|
|
301
|
+
**`just html` (or any plain `sphinx-build`) doesn't rebuild assets** —
|
|
302
|
+
make sure `sphinx_vite_builder` is loaded in `extensions` and that
|
|
303
|
+
`sphinx_vite_builder_root` points at your `web/` directory. The
|
|
304
|
+
extension's PROD-mode hook runs `pnpm exec vite build` once before
|
|
305
|
+
the build proceeds; without it, you'd need a manual orchestration
|
|
306
|
+
step.
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
MIT — see [LICENSE](LICENSE).
|
|
311
|
+
|
|
312
|
+
## Agent / contributor guidance
|
|
313
|
+
|
|
314
|
+
See [`AGENTS.md`](AGENTS.md) for the design contract, architecture
|
|
315
|
+
map, and conventions agents and contributors should follow when
|
|
316
|
+
making changes. ([`CLAUDE.md`](CLAUDE.md) is a passthrough to
|
|
317
|
+
`AGENTS.md` for Claude Code.)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sphinx-vite-builder"
|
|
3
|
-
version = "0.0.1a16.
|
|
3
|
+
version = "0.0.1a16.dev3"
|
|
4
4
|
description = "PEP 517 build backend + Sphinx extension that orchestrates Vite via pnpm"
|
|
5
5
|
requires-python = ">=3.10,<4.0"
|
|
6
6
|
authors = [
|
|
@@ -37,6 +37,13 @@ dependencies = [
|
|
|
37
37
|
[project.entry-points."sphinx.extensions"]
|
|
38
38
|
"sphinx-vite-builder" = "sphinx_vite_builder"
|
|
39
39
|
|
|
40
|
+
# Hatchling build-hook variant — consumers can pick this OR the
|
|
41
|
+
# `build-backend = "sphinx_vite_builder.build"` PEP 517 backend
|
|
42
|
+
# variant, never both. See packages/sphinx-vite-builder/AGENTS.md
|
|
43
|
+
# for the trade-off.
|
|
44
|
+
[project.entry-points.hatch]
|
|
45
|
+
vite = "sphinx_vite_builder.hatch_plugin"
|
|
46
|
+
|
|
40
47
|
[project.urls]
|
|
41
48
|
Repository = "https://github.com/git-pull/gp-sphinx"
|
|
42
49
|
|