sphinx-vite-builder 0.0.1a16.dev2__tar.gz → 0.0.1a16.dev4__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.
Files changed (20) hide show
  1. sphinx_vite_builder-0.0.1a16.dev4/PKG-INFO +344 -0
  2. sphinx_vite_builder-0.0.1a16.dev4/README.md +317 -0
  3. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/pyproject.toml +8 -1
  4. sphinx_vite_builder-0.0.1a16.dev4/src/sphinx_vite_builder/__init__.py +93 -0
  5. sphinx_vite_builder-0.0.1a16.dev4/src/sphinx_vite_builder/_internal/config.py +190 -0
  6. sphinx_vite_builder-0.0.1a16.dev4/src/sphinx_vite_builder/_internal/hooks.py +297 -0
  7. sphinx_vite_builder-0.0.1a16.dev4/src/sphinx_vite_builder/hatch_plugin.py +92 -0
  8. sphinx_vite_builder-0.0.1a16.dev2/PKG-INFO +0 -200
  9. sphinx_vite_builder-0.0.1a16.dev2/README.md +0 -173
  10. sphinx_vite_builder-0.0.1a16.dev2/src/sphinx_vite_builder/__init__.py +0 -60
  11. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/.gitignore +0 -0
  12. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/AGENTS.md +0 -0
  13. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/CLAUDE.md +0 -0
  14. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/src/sphinx_vite_builder/_internal/__init__.py +0 -0
  15. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/src/sphinx_vite_builder/_internal/bus.py +0 -0
  16. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/src/sphinx_vite_builder/_internal/errors.py +0 -0
  17. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/src/sphinx_vite_builder/_internal/process.py +0 -0
  18. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/src/sphinx_vite_builder/_internal/vite.py +0 -0
  19. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/src/sphinx_vite_builder/build.py +0 -0
  20. {sphinx_vite_builder-0.0.1a16.dev2 → sphinx_vite_builder-0.0.1a16.dev4}/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.dev4
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.dev2"
3
+ version = "0.0.1a16.dev4"
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