sphinx-vite-builder 0.0.1a16.dev0__tar.gz → 0.0.1a16.dev2__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.dev2/AGENTS.md +208 -0
- sphinx_vite_builder-0.0.1a16.dev2/CLAUDE.md +8 -0
- sphinx_vite_builder-0.0.1a16.dev2/PKG-INFO +200 -0
- sphinx_vite_builder-0.0.1a16.dev2/README.md +173 -0
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/pyproject.toml +1 -2
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/__init__.py +18 -1
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/process.py +23 -5
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/vite.py +21 -4
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/build.py +57 -2
- sphinx_vite_builder-0.0.1a16.dev0/PKG-INFO +0 -106
- sphinx_vite_builder-0.0.1a16.dev0/README.md +0 -78
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/.gitignore +0 -0
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/__init__.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/bus.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/_internal/errors.py +0 -0
- {sphinx_vite_builder-0.0.1a16.dev0 → sphinx_vite_builder-0.0.1a16.dev2}/src/sphinx_vite_builder/py.typed +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# AGENTS.md — `sphinx-vite-builder`
|
|
2
|
+
|
|
3
|
+
Guidance for AI agents (Claude Code, Cursor, Copilot, Codex, …) and
|
|
4
|
+
human contributors working on this package. Mirrors the higher-level
|
|
5
|
+
guidance at `gp-sphinx/CLAUDE.md`; `packages/sphinx-vite-builder/CLAUDE.md`
|
|
6
|
+
points here so Claude Code reads the same content as other agent runners.
|
|
7
|
+
|
|
8
|
+
## What this package is
|
|
9
|
+
|
|
10
|
+
Two orthogonal entry points sharing one subprocess core:
|
|
11
|
+
|
|
12
|
+
1. **PEP 517 build backend** at `sphinx_vite_builder.build`. Runs
|
|
13
|
+
`pnpm exec vite build` before delegating wheel/sdist construction
|
|
14
|
+
to `hatchling.build`. Consumer packages declare it via
|
|
15
|
+
`[build-system].build-backend = "sphinx_vite_builder.build"`.
|
|
16
|
+
2. **Sphinx extension** at `sphinx_vite_builder:setup`. Hooks
|
|
17
|
+
`builder-inited` and `build-finished` so `sphinx-build` /
|
|
18
|
+
`sphinx-autobuild` automatically run vite — one-shot for prod, a
|
|
19
|
+
long-lived `vite build --watch` child process for autobuild — with
|
|
20
|
+
graceful teardown on signal / `atexit`.
|
|
21
|
+
|
|
22
|
+
Both heads consume the smart-subprocess core under
|
|
23
|
+
`sphinx_vite_builder._internal/`: `process.py` (`AsyncProcess` —
|
|
24
|
+
asyncio subprocess wrapper with POSIX session isolation,
|
|
25
|
+
SIGTERM-then-SIGKILL teardown, line-buffered stdout/stderr drainers,
|
|
26
|
+
captured stderr for error surfacing), `bus.py` (`AsyncioBus` — single
|
|
27
|
+
asyncio loop in a daemon thread for sync↔async bridging),
|
|
28
|
+
`vite.py` (orchestration: detect `web/`, check pnpm via `shutil.which`,
|
|
29
|
+
spawn install/build), and `errors.py` (`PnpmMissingError`,
|
|
30
|
+
`NodeModulesInstallError`, `ViteFailedError`).
|
|
31
|
+
|
|
32
|
+
**Phase 1 status:** The PEP 517 backend is fully implemented and
|
|
33
|
+
tested. The Sphinx extension `setup()` is a placeholder — it
|
|
34
|
+
registers cleanly in `conf.py` but doesn't yet hook the docs build
|
|
35
|
+
lifecycle. The full extension implementation (event handlers, vite
|
|
36
|
+
watch, teardown) lands in a follow-up release.
|
|
37
|
+
|
|
38
|
+
## The design contract — keep this invariant
|
|
39
|
+
|
|
40
|
+
> **Sources should check for node, pnpm, etc and error if it's not
|
|
41
|
+
> good, then build. Wheels should have the build files baked in and
|
|
42
|
+
> not need node and pnpm at all.**
|
|
43
|
+
|
|
44
|
+
This is the central invariant. When you change anything in the
|
|
45
|
+
backend, vite orchestration, or release pipeline, ask: "does this
|
|
46
|
+
preserve the source-vs-wheel asymmetry?"
|
|
47
|
+
|
|
48
|
+
### Source builds — fail loud, fail informatively
|
|
49
|
+
|
|
50
|
+
A consumer building from source (cloned tree, `[tool.uv.sources]` git
|
|
51
|
+
URL, `pip install <repo-path>`) goes through the PEP 517 chain. The
|
|
52
|
+
backend's `build_wheel` / `build_editable` / `build_sdist` hooks
|
|
53
|
+
**MUST** run vite, and vite **MUST** be available. If pnpm or Node is
|
|
54
|
+
missing:
|
|
55
|
+
|
|
56
|
+
- Raise the typed exception (`PnpmMissingError`,
|
|
57
|
+
`NodeModulesInstallError`, or `ViteFailedError`) — never a bare
|
|
58
|
+
`subprocess.CalledProcessError` or `FileNotFoundError`. The typed
|
|
59
|
+
exception lets consumers `except` cleanly via the
|
|
60
|
+
`SphinxViteBuilderError` base class.
|
|
61
|
+
- Each error's message MUST be a multi-line, copy-pasteable hint
|
|
62
|
+
formatted by `_format_*_hint()`. Include the canonical install
|
|
63
|
+
paths (`corepack enable`, `https://pnpm.io/installation`), the
|
|
64
|
+
resolved vite-root path, and the `SPHINX_VITE_BUILDER_SKIP=1`
|
|
65
|
+
escape-hatch line.
|
|
66
|
+
- Detect CI via `_detect_ci_provider()`. When CI is detected, append
|
|
67
|
+
the platform-specific YAML/config snippet from `_CI_SETUP_RECIPES`
|
|
68
|
+
so the consumer can copy-paste the fix into their pipeline.
|
|
69
|
+
Supported providers: GitHub Actions, CircleCI, Azure Pipelines,
|
|
70
|
+
GitLab CI, plus a generic fallback for `CI=true`.
|
|
71
|
+
|
|
72
|
+
### Wheel installs — zero toolchain required
|
|
73
|
+
|
|
74
|
+
A consumer doing `pip install <package>==<version>` from PyPI gets a
|
|
75
|
+
wheel that **already contains** the vite-built `static/` tree
|
|
76
|
+
(populated by the backend at release time, packed via hatchling's
|
|
77
|
+
`artifacts` directive). The PEP 517 chain doesn't run. No backend
|
|
78
|
+
invocation. No `_run_vite_build()`. No `shutil.which("pnpm")`. The
|
|
79
|
+
end user sees Python and only Python.
|
|
80
|
+
|
|
81
|
+
### Wheel-from-sdist — the bridge case
|
|
82
|
+
|
|
83
|
+
A consumer doing `pip install <pkg>.tar.gz` (or `--no-binary :all:`)
|
|
84
|
+
runs the wheel-from-sdist chain:
|
|
85
|
+
|
|
86
|
+
1. pip/uv unpacks the sdist into a temp dir
|
|
87
|
+
2. Calls our backend's `build_wheel` against the unpacked tree
|
|
88
|
+
3. The backend's vite-orchestration sees no `web/` (excluded from
|
|
89
|
+
sdist via `[tool.hatch.build.targets.sdist] exclude = ["web/"]`)
|
|
90
|
+
and short-circuits cleanly — `_resolve_vite_root()` returns
|
|
91
|
+
`None`, vite is never invoked
|
|
92
|
+
4. Hatchling packs the pre-baked `static/` (carried in the sdist via
|
|
93
|
+
`[tool.hatch.build] artifacts`) into the wheel
|
|
94
|
+
|
|
95
|
+
Net: **sdist install also requires zero toolchain**. The
|
|
96
|
+
`web/`-absent short-circuit is the load-bearing primitive.
|
|
97
|
+
|
|
98
|
+
## QA permutations — keep them green
|
|
99
|
+
|
|
100
|
+
The install-path permutations every change must keep green:
|
|
101
|
+
|
|
102
|
+
| # | Path | Toolchain | Expected |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| 1 | wheel install from PyPI | none | succeeds; static present |
|
|
105
|
+
| 2 | sdist install from PyPI (`--no-binary :all:`) | none | succeeds; static present (backend short-circuits) |
|
|
106
|
+
| 3 | source build (`uv build` from cloned tree) | with pnpm + Node | succeeds; wheel contains static |
|
|
107
|
+
| 4 | source build (`uv build` from cloned tree) | none | fails with `PnpmMissingError` + CI-platform recipe |
|
|
108
|
+
|
|
109
|
+
When you add a new failure mode or a new short-circuit branch, add a
|
|
110
|
+
new row here AND a corresponding test in
|
|
111
|
+
`tests/test_sphinx_vite_builder_vite.py`.
|
|
112
|
+
|
|
113
|
+
## Architecture map
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
sphinx_vite_builder/
|
|
117
|
+
├── __init__.py Sphinx extension entry: setup(app)
|
|
118
|
+
├── build.py PEP 517/660 hooks (delegate to hatchling)
|
|
119
|
+
├── py.typed
|
|
120
|
+
└── _internal/
|
|
121
|
+
├── errors.py SphinxViteBuilderError + 3 subclasses
|
|
122
|
+
├── process.py AsyncProcess (asyncio subprocess wrapper)
|
|
123
|
+
├── bus.py AsyncioBus (sync↔async bridge)
|
|
124
|
+
└── vite.py run_vite_build() + CI detection + hint formatters
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Neither head calls the other; both consume `_internal/`. The PEP 517
|
|
128
|
+
hooks in `build.py` MUST stay 1:1 mirrors of `flit_core.buildapi` and
|
|
129
|
+
`hatchling.build` — every hook runs `run_vite_build()` then delegates.
|
|
130
|
+
Optional hooks (`get_requires_for_build_*`, `prepare_metadata_for_build_*`)
|
|
131
|
+
alias to hatchling by identity — vite has no influence on dependency
|
|
132
|
+
resolution or distribution metadata, so wrapping them would be wrong.
|
|
133
|
+
|
|
134
|
+
## When you add a new public function
|
|
135
|
+
|
|
136
|
+
- Add doctests. Every public function MUST have working doctests
|
|
137
|
+
per the workspace convention. Use ELLIPSIS for variable output.
|
|
138
|
+
- Add NumPy-style docstrings: short summary, Parameters, Returns,
|
|
139
|
+
Raises, Examples.
|
|
140
|
+
- Add type annotations everywhere, including return types
|
|
141
|
+
(`-> None` on test functions). mypy runs strict mode.
|
|
142
|
+
|
|
143
|
+
## When you add a new error path
|
|
144
|
+
|
|
145
|
+
- Add a new `*Error` subclass in `errors.py` if the failure has a
|
|
146
|
+
distinct semantic meaning. Inherit from `SphinxViteBuilderError`.
|
|
147
|
+
- Add a `_format_*_hint(...)` function in `vite.py` for the
|
|
148
|
+
copy-pasteable diagnostic. Wrap the raise:
|
|
149
|
+
`raise NewError(_format_new_error_hint(...))`.
|
|
150
|
+
- If the new path is reachable in CI, ensure the message includes
|
|
151
|
+
enough context to fix-from-the-message-itself. Add a CI-recipe
|
|
152
|
+
block via `_format_ci_recipe_block()` if relevant.
|
|
153
|
+
- Add tests for: positive case (path triggers correctly), each error
|
|
154
|
+
branch (specific exception, with key strings present in message),
|
|
155
|
+
inheritance from `SphinxViteBuilderError`.
|
|
156
|
+
|
|
157
|
+
## When you change `release.yml` or the workspace
|
|
158
|
+
|
|
159
|
+
The workspace's `release.yml` MUST keep the pnpm + Node setup steps
|
|
160
|
+
that run before `uv build`, otherwise the wheels published to PyPI
|
|
161
|
+
would be empty of static (the prior broken-release pattern that
|
|
162
|
+
motivated this whole package). The required steps are:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
- uses: pnpm/action-setup@v6
|
|
166
|
+
with:
|
|
167
|
+
version: 10
|
|
168
|
+
- uses: actions/setup-node@v6
|
|
169
|
+
with:
|
|
170
|
+
node-version: 22
|
|
171
|
+
cache: pnpm
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If you find yourself removing those, ask: "is the source-build path
|
|
175
|
+
still going to produce a populated `static/` in the wheel?" The
|
|
176
|
+
answer must be yes.
|
|
177
|
+
|
|
178
|
+
## When in doubt
|
|
179
|
+
|
|
180
|
+
- Read the full plan at gp-sphinx issue #28.
|
|
181
|
+
- Look at how PR #29 wired everything together initially.
|
|
182
|
+
- Look at how libtmux-mcp PR #33 exercises both consumer paths in
|
|
183
|
+
CI — the WITH-wheels and WITHOUT-wheels permutations both have
|
|
184
|
+
green runs that are good reference points.
|
|
185
|
+
- Run the local QA matrix: clean venv install of the published
|
|
186
|
+
wheel, clean venv install of the published sdist (`--no-binary`),
|
|
187
|
+
clean clone + `uv build` with toolchain stripped (must fail
|
|
188
|
+
loudly), clean clone + `uv build` with toolchain present (must
|
|
189
|
+
succeed).
|
|
190
|
+
|
|
191
|
+
## What NOT to do
|
|
192
|
+
|
|
193
|
+
- **Do not** add `web/` to the sdist. Excluding it is what makes the
|
|
194
|
+
wheel-from-sdist short-circuit work.
|
|
195
|
+
- **Do not** commit anything from `static/` to git. Build artefacts
|
|
196
|
+
are produced reproducibly; check-in is forbidden by workspace
|
|
197
|
+
policy.
|
|
198
|
+
- **Do not** silently swallow vite/pnpm subprocess failures. Every
|
|
199
|
+
non-zero exit goes through a typed exception with a
|
|
200
|
+
copy-pasteable hint.
|
|
201
|
+
- **Do not** auto-install pnpm via the backend (the maturin
|
|
202
|
+
`puccinialin`-style trick doesn't apply here — pnpm isn't
|
|
203
|
+
pip-installable). The contract is "pnpm is your responsibility,
|
|
204
|
+
here's how to install it on your platform".
|
|
205
|
+
- **Do not** change `SPHINX_VITE_BUILDER_SKIP=1` semantics without
|
|
206
|
+
thinking through the wheel-vs-source asymmetry. The escape hatch
|
|
207
|
+
is correct for wheel-only environments; using it during a real
|
|
208
|
+
source build silently produces broken artefacts.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Claude Code reads this file. Other agent runners (Cursor, Copilot,
|
|
4
|
+
Codex, …) read [`AGENTS.md`](AGENTS.md). The two files have identical
|
|
5
|
+
content via this passthrough — every guideline lives in `AGENTS.md`,
|
|
6
|
+
and edits go there.
|
|
7
|
+
|
|
8
|
+
→ See [`AGENTS.md`](AGENTS.md).
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sphinx-vite-builder
|
|
3
|
+
Version: 0.0.1a16.dev2
|
|
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
|
+
## Two heads, one subprocess core
|
|
129
|
+
|
|
130
|
+
### PEP 517 build backend
|
|
131
|
+
|
|
132
|
+
Drop-in replacement for `hatchling.build`. Runs `pnpm exec vite build`
|
|
133
|
+
before delegating wheel/sdist construction to hatchling.
|
|
134
|
+
|
|
135
|
+
```toml
|
|
136
|
+
# packages/your-theme/pyproject.toml
|
|
137
|
+
[build-system]
|
|
138
|
+
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
139
|
+
build-backend = "sphinx_vite_builder.build"
|
|
140
|
+
|
|
141
|
+
[tool.hatch.build.targets.sdist]
|
|
142
|
+
exclude = ["web/"] # so the sdist→wheel chain hits the short-circuit
|
|
143
|
+
|
|
144
|
+
[tool.hatch.build]
|
|
145
|
+
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Sphinx extension (Phase 1: placeholder)
|
|
149
|
+
|
|
150
|
+
The extension entry point is currently a placeholder registered in
|
|
151
|
+
`conf.py` to prevent import errors. Full lifecycle integration —
|
|
152
|
+
running Vite before the docs build and spawning a watched Vite
|
|
153
|
+
process during `sphinx-autobuild` — lands in a follow-up release.
|
|
154
|
+
|
|
155
|
+
For now, the [PEP 517](https://peps.python.org/pep-0517/) backend
|
|
156
|
+
handles all Vite orchestration during source builds and wheel
|
|
157
|
+
generation; that path is fully implemented and tested.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# docs/conf.py
|
|
161
|
+
extensions = ["sphinx_vite_builder"]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Fast-fail diagnostics — error type reference
|
|
165
|
+
|
|
166
|
+
| Error | When | Hint surface |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `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` |
|
|
169
|
+
| `NodeModulesInstallError` | `pnpm install` exited non-zero | `cd <vite-root> && pnpm install --frozen-lockfile` rerun command, captured stderr |
|
|
170
|
+
| `ViteFailedError` | `pnpm exec vite build` exited non-zero | invocation context (cwd, exit code), captured stderr |
|
|
171
|
+
|
|
172
|
+
All three inherit from `SphinxViteBuilderError`, so consumers can
|
|
173
|
+
`except SphinxViteBuilderError` for a single catch surface.
|
|
174
|
+
|
|
175
|
+
## CI detection
|
|
176
|
+
|
|
177
|
+
The `PnpmMissingError` hint is **self-healing** when the backend
|
|
178
|
+
detects a CI environment. Detection precedence (most-specific wins):
|
|
179
|
+
|
|
180
|
+
| CI provider | Env var | Recipe shape |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| GitHub Actions | `GITHUB_ACTIONS=true` | `pnpm/action-setup@v6` + `actions/setup-node@v6` |
|
|
183
|
+
| CircleCI | `CIRCLECI=true` | `corepack enable && corepack prepare pnpm@latest-10 --activate` step |
|
|
184
|
+
| Azure Pipelines | `TF_BUILD=True` | `NodeTool@0` + corepack script |
|
|
185
|
+
| GitLab CI | `GITLAB_CI=true` | `before_script` corepack invocations |
|
|
186
|
+
| Generic | `CI=true` | "Use your CI's package-manager setup mechanism" |
|
|
187
|
+
|
|
188
|
+
Source: each provider's own canonical detection variable per the pnpm
|
|
189
|
+
[Continuous Integration docs](https://pnpm.io/continuous-integration).
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT — see [LICENSE](LICENSE).
|
|
194
|
+
|
|
195
|
+
## Agent / contributor guidance
|
|
196
|
+
|
|
197
|
+
See [`AGENTS.md`](AGENTS.md) for the design contract, architecture
|
|
198
|
+
map, and conventions agents and contributors should follow when
|
|
199
|
+
making changes. ([`CLAUDE.md`](CLAUDE.md) is a passthrough to
|
|
200
|
+
`AGENTS.md` for Claude Code.)
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
## Two heads, one subprocess core
|
|
102
|
+
|
|
103
|
+
### PEP 517 build backend
|
|
104
|
+
|
|
105
|
+
Drop-in replacement for `hatchling.build`. Runs `pnpm exec vite build`
|
|
106
|
+
before delegating wheel/sdist construction to hatchling.
|
|
107
|
+
|
|
108
|
+
```toml
|
|
109
|
+
# packages/your-theme/pyproject.toml
|
|
110
|
+
[build-system]
|
|
111
|
+
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
112
|
+
build-backend = "sphinx_vite_builder.build"
|
|
113
|
+
|
|
114
|
+
[tool.hatch.build.targets.sdist]
|
|
115
|
+
exclude = ["web/"] # so the sdist→wheel chain hits the short-circuit
|
|
116
|
+
|
|
117
|
+
[tool.hatch.build]
|
|
118
|
+
artifacts = ["src/<your-theme>/theme/<theme-name>/static/"]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Sphinx extension (Phase 1: placeholder)
|
|
122
|
+
|
|
123
|
+
The extension entry point is currently a placeholder registered in
|
|
124
|
+
`conf.py` to prevent import errors. Full lifecycle integration —
|
|
125
|
+
running Vite before the docs build and spawning a watched Vite
|
|
126
|
+
process during `sphinx-autobuild` — lands in a follow-up release.
|
|
127
|
+
|
|
128
|
+
For now, the [PEP 517](https://peps.python.org/pep-0517/) backend
|
|
129
|
+
handles all Vite orchestration during source builds and wheel
|
|
130
|
+
generation; that path is fully implemented and tested.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# docs/conf.py
|
|
134
|
+
extensions = ["sphinx_vite_builder"]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Fast-fail diagnostics — error type reference
|
|
138
|
+
|
|
139
|
+
| Error | When | Hint surface |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `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` |
|
|
142
|
+
| `NodeModulesInstallError` | `pnpm install` exited non-zero | `cd <vite-root> && pnpm install --frozen-lockfile` rerun command, captured stderr |
|
|
143
|
+
| `ViteFailedError` | `pnpm exec vite build` exited non-zero | invocation context (cwd, exit code), captured stderr |
|
|
144
|
+
|
|
145
|
+
All three inherit from `SphinxViteBuilderError`, so consumers can
|
|
146
|
+
`except SphinxViteBuilderError` for a single catch surface.
|
|
147
|
+
|
|
148
|
+
## CI detection
|
|
149
|
+
|
|
150
|
+
The `PnpmMissingError` hint is **self-healing** when the backend
|
|
151
|
+
detects a CI environment. Detection precedence (most-specific wins):
|
|
152
|
+
|
|
153
|
+
| CI provider | Env var | Recipe shape |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| GitHub Actions | `GITHUB_ACTIONS=true` | `pnpm/action-setup@v6` + `actions/setup-node@v6` |
|
|
156
|
+
| CircleCI | `CIRCLECI=true` | `corepack enable && corepack prepare pnpm@latest-10 --activate` step |
|
|
157
|
+
| Azure Pipelines | `TF_BUILD=True` | `NodeTool@0` + corepack script |
|
|
158
|
+
| GitLab CI | `GITLAB_CI=true` | `before_script` corepack invocations |
|
|
159
|
+
| Generic | `CI=true` | "Use your CI's package-manager setup mechanism" |
|
|
160
|
+
|
|
161
|
+
Source: each provider's own canonical detection variable per the pnpm
|
|
162
|
+
[Continuous Integration docs](https://pnpm.io/continuous-integration).
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT — see [LICENSE](LICENSE).
|
|
167
|
+
|
|
168
|
+
## Agent / contributor guidance
|
|
169
|
+
|
|
170
|
+
See [`AGENTS.md`](AGENTS.md) for the design contract, architecture
|
|
171
|
+
map, and conventions agents and contributors should follow when
|
|
172
|
+
making changes. ([`CLAUDE.md`](CLAUDE.md) is a passthrough to
|
|
173
|
+
`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.dev2"
|
|
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 = [
|
|
@@ -31,7 +31,6 @@ keywords = ["sphinx", "extension", "vite", "pnpm", "pep517", "build", "backend"]
|
|
|
31
31
|
# required at build time of consumer packages but not at runtime, so it
|
|
32
32
|
# stays in [build-system].requires of *consumers* rather than here.
|
|
33
33
|
dependencies = [
|
|
34
|
-
"hatchling>=1.0",
|
|
35
34
|
"sphinx>=8.1",
|
|
36
35
|
]
|
|
37
36
|
|
|
@@ -15,9 +15,13 @@ under :mod:`sphinx_vite_builder._internal`.
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import logging
|
|
18
19
|
import typing as t
|
|
19
20
|
|
|
20
|
-
__version__ = "0.0.1a16.
|
|
21
|
+
__version__ = "0.0.1a16.dev2"
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
logger.addHandler(logging.NullHandler())
|
|
21
25
|
|
|
22
26
|
if t.TYPE_CHECKING:
|
|
23
27
|
from sphinx.application import Sphinx
|
|
@@ -31,6 +35,19 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
|
|
|
31
35
|
on ``sphinx-build``) lands in a follow-up commit. For now this
|
|
32
36
|
stub registers the extension so consumers can declare it without
|
|
33
37
|
a no-such-module error, and returns the safety metadata.
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
The Phase-1 stub returns the parallel-safety metadata Sphinx
|
|
42
|
+
expects, regardless of the application object passed in:
|
|
43
|
+
|
|
44
|
+
>>> class FakeApp:
|
|
45
|
+
... pass
|
|
46
|
+
>>> metadata = setup(FakeApp()) # type: ignore[arg-type]
|
|
47
|
+
>>> metadata["parallel_read_safe"]
|
|
48
|
+
True
|
|
49
|
+
>>> metadata["parallel_write_safe"]
|
|
50
|
+
True
|
|
34
51
|
"""
|
|
35
52
|
del app
|
|
36
53
|
return {
|
|
@@ -9,9 +9,11 @@ backend and extension heads need:
|
|
|
9
9
|
- ``PYTHONUNBUFFERED=1`` is forced into the child env so Python tools
|
|
10
10
|
invoked via the package-manager bridge don't withhold their output.
|
|
11
11
|
- On POSIX, the child runs in a new session (``start_new_session=True``)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
and :meth:`AsyncProcess.terminate` signals the whole process group
|
|
13
|
+
via :func:`os.killpg` so ``pnpm exec`` plus every descendant exits
|
|
14
|
+
together. ``asyncio.subprocess.Process.terminate`` would only signal
|
|
15
|
+
the leader's PID, leaving the vite child orphaned (pnpm does not
|
|
16
|
+
forward signals to its ``exec`` target).
|
|
15
17
|
- :meth:`AsyncProcess.terminate` is graceful-then-forceful: SIGTERM,
|
|
16
18
|
await up to ``timeout`` seconds, escalate to SIGKILL if the child is
|
|
17
19
|
still alive. Idempotent: calling on an already-exited process is a
|
|
@@ -32,6 +34,7 @@ import contextlib
|
|
|
32
34
|
import logging
|
|
33
35
|
import os
|
|
34
36
|
import pathlib
|
|
37
|
+
import signal
|
|
35
38
|
import sys
|
|
36
39
|
import typing as t
|
|
37
40
|
|
|
@@ -196,7 +199,19 @@ class AsyncProcess:
|
|
|
196
199
|
if self._process.returncode is not None:
|
|
197
200
|
return self._process.returncode
|
|
198
201
|
|
|
199
|
-
|
|
202
|
+
# POSIX: the child was spawned with ``start_new_session=True``,
|
|
203
|
+
# so ``self._process.pid`` is the leader of its own session and
|
|
204
|
+
# equals its process-group ID. SIGTERM the whole group so
|
|
205
|
+
# ``pnpm exec`` plus all its descendants (including the vite
|
|
206
|
+
# process pnpm doesn't forward signals to) exit. asyncio's
|
|
207
|
+
# ``Process.terminate()`` is PID-only — it would leave vite
|
|
208
|
+
# orphaned. Windows has no ``killpg``; the asyncio default is
|
|
209
|
+
# the right primitive there.
|
|
210
|
+
with contextlib.suppress(ProcessLookupError):
|
|
211
|
+
if sys.platform != "win32":
|
|
212
|
+
os.killpg(self._process.pid, signal.SIGTERM)
|
|
213
|
+
else:
|
|
214
|
+
self._process.terminate()
|
|
200
215
|
try:
|
|
201
216
|
await asyncio.wait_for(self._process.wait(), timeout=timeout)
|
|
202
217
|
except asyncio.TimeoutError:
|
|
@@ -208,7 +223,10 @@ class AsyncProcess:
|
|
|
208
223
|
# ProcessLookupError race: the child can exit between
|
|
209
224
|
# TimeoutError and kill().
|
|
210
225
|
with contextlib.suppress(ProcessLookupError):
|
|
211
|
-
|
|
226
|
+
if sys.platform != "win32":
|
|
227
|
+
os.killpg(self._process.pid, signal.SIGKILL)
|
|
228
|
+
else:
|
|
229
|
+
self._process.kill()
|
|
212
230
|
await self._process.wait()
|
|
213
231
|
|
|
214
232
|
# Wait for drainers to consume their last buffered line before
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
"""Vite + pnpm orchestration: detection, install, one-shot build
|
|
1
|
+
"""Vite + pnpm orchestration: detection, install, one-shot build.
|
|
2
2
|
|
|
3
3
|
This module is the shared orchestration core consumed by both heads:
|
|
4
4
|
|
|
5
5
|
- The PEP 517 backend (:mod:`sphinx_vite_builder.build`) calls
|
|
6
6
|
:func:`run_vite_build` from each of its hooks, before delegating to
|
|
7
7
|
hatchling.
|
|
8
|
-
- The Sphinx extension (:mod:`sphinx_vite_builder`)
|
|
9
|
-
:func:`run_vite_build`
|
|
10
|
-
``builder-inited
|
|
8
|
+
- The Sphinx extension (:mod:`sphinx_vite_builder`) — Phase 1
|
|
9
|
+
placeholder — will call :func:`run_vite_build` from
|
|
10
|
+
``builder-inited`` once the extension head lands in a follow-up
|
|
11
|
+
release.
|
|
11
12
|
|
|
12
13
|
Fast-fail discipline: every prerequisite is checked up front so the
|
|
13
14
|
caller gets an actionable diagnostic instead of a generic spawn-failure
|
|
@@ -340,6 +341,22 @@ def run_vite_build(
|
|
|
340
341
|
non-zero.
|
|
341
342
|
- :class:`ViteFailedError` if ``pnpm exec vite build`` exits
|
|
342
343
|
non-zero.
|
|
344
|
+
|
|
345
|
+
Examples
|
|
346
|
+
--------
|
|
347
|
+
The ``SPHINX_VITE_BUILDER_SKIP`` environment variable
|
|
348
|
+
short-circuits the whole orchestration before any subprocess is
|
|
349
|
+
spawned — exercising it from a doctest verifies the escape hatch
|
|
350
|
+
keeps working without touching pnpm, Node, or the filesystem
|
|
351
|
+
tree:
|
|
352
|
+
|
|
353
|
+
>>> import os, pathlib, tempfile
|
|
354
|
+
>>> os.environ["SPHINX_VITE_BUILDER_SKIP"] = "1"
|
|
355
|
+
>>> try:
|
|
356
|
+
... with tempfile.TemporaryDirectory() as tmp:
|
|
357
|
+
... run_vite_build(pathlib.Path(tmp))
|
|
358
|
+
... finally:
|
|
359
|
+
... del os.environ["SPHINX_VITE_BUILDER_SKIP"]
|
|
343
360
|
"""
|
|
344
361
|
if os.environ.get("SPHINX_VITE_BUILDER_SKIP"):
|
|
345
362
|
logger.info("SPHINX_VITE_BUILDER_SKIP set; skipping vite build")
|
|
@@ -37,7 +37,41 @@ def build_wheel(
|
|
|
37
37
|
config_settings: dict[str, t.Any] | None = None,
|
|
38
38
|
metadata_directory: str | None = None,
|
|
39
39
|
) -> str:
|
|
40
|
-
"""PEP 517 ``build_wheel``: vite-build, then hatchling-pack.
|
|
40
|
+
"""PEP 517 ``build_wheel``: vite-build, then hatchling-pack.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
With ``SPHINX_VITE_BUILDER_SKIP=1`` set, the hook short-circuits
|
|
45
|
+
vite and delegates straight to :mod:`hatchling.build` against a
|
|
46
|
+
minimal synthetic project, producing a real ``.whl``:
|
|
47
|
+
|
|
48
|
+
>>> import os, pathlib, tempfile, textwrap
|
|
49
|
+
>>> os.environ["SPHINX_VITE_BUILDER_SKIP"] = "1"
|
|
50
|
+
>>> cwd = os.getcwd()
|
|
51
|
+
>>> with tempfile.TemporaryDirectory() as tmp:
|
|
52
|
+
... project = pathlib.Path(tmp)
|
|
53
|
+
... _ = (project / "pyproject.toml").write_text(textwrap.dedent('''
|
|
54
|
+
... [build-system]
|
|
55
|
+
... requires = ["hatchling"]
|
|
56
|
+
... build-backend = "hatchling.build"
|
|
57
|
+
... [project]
|
|
58
|
+
... name = "doctest-pkg"
|
|
59
|
+
... version = "0.0.0"
|
|
60
|
+
... ''').lstrip())
|
|
61
|
+
... pkg = project / "doctest_pkg"
|
|
62
|
+
... pkg.mkdir()
|
|
63
|
+
... _ = (pkg / "__init__.py").write_text("")
|
|
64
|
+
... dist = project / "dist"
|
|
65
|
+
... dist.mkdir()
|
|
66
|
+
... os.chdir(project)
|
|
67
|
+
... try:
|
|
68
|
+
... name = build_wheel(str(dist))
|
|
69
|
+
... finally:
|
|
70
|
+
... os.chdir(cwd)
|
|
71
|
+
... del os.environ["SPHINX_VITE_BUILDER_SKIP"]
|
|
72
|
+
>>> name.endswith(".whl")
|
|
73
|
+
True
|
|
74
|
+
"""
|
|
41
75
|
run_vite_build()
|
|
42
76
|
return _hatchling.build_wheel(wheel_directory, config_settings, metadata_directory)
|
|
43
77
|
|
|
@@ -47,7 +81,18 @@ def build_editable(
|
|
|
47
81
|
config_settings: dict[str, t.Any] | None = None,
|
|
48
82
|
metadata_directory: str | None = None,
|
|
49
83
|
) -> str:
|
|
50
|
-
"""PEP 660 ``build_editable``: vite-build, then hatchling-pack-editable.
|
|
84
|
+
"""PEP 660 ``build_editable``: vite-build, then hatchling-pack-editable.
|
|
85
|
+
|
|
86
|
+
The delegation pattern matches :func:`build_wheel`; see that
|
|
87
|
+
function's docstring for an end-to-end example exercising the
|
|
88
|
+
``SPHINX_VITE_BUILDER_SKIP=1`` short-circuit.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> import inspect
|
|
93
|
+
>>> sorted(inspect.signature(build_editable).parameters)
|
|
94
|
+
['config_settings', 'metadata_directory', 'wheel_directory']
|
|
95
|
+
"""
|
|
51
96
|
run_vite_build()
|
|
52
97
|
return _hatchling.build_editable(
|
|
53
98
|
wheel_directory, config_settings, metadata_directory
|
|
@@ -66,6 +111,16 @@ def build_sdist(
|
|
|
66
111
|
the sdist without pnpm or Node — the wheel-from-sdist build will
|
|
67
112
|
skip vite (no ``web/`` in the unpacked tree) and ship the
|
|
68
113
|
pre-baked assets via hatchling's normal file selection.
|
|
114
|
+
|
|
115
|
+
The delegation pattern matches :func:`build_wheel`; see that
|
|
116
|
+
function's docstring for an end-to-end example exercising the
|
|
117
|
+
``SPHINX_VITE_BUILDER_SKIP=1`` short-circuit.
|
|
118
|
+
|
|
119
|
+
Examples
|
|
120
|
+
--------
|
|
121
|
+
>>> import inspect
|
|
122
|
+
>>> sorted(inspect.signature(build_sdist).parameters)
|
|
123
|
+
['config_settings', 'sdist_directory']
|
|
69
124
|
"""
|
|
70
125
|
run_vite_build()
|
|
71
126
|
return _hatchling.build_sdist(sdist_directory, config_settings)
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sphinx-vite-builder
|
|
3
|
-
Version: 0.0.1a16.dev0
|
|
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: hatchling>=1.0
|
|
26
|
-
Requires-Dist: sphinx>=8.1
|
|
27
|
-
Description-Content-Type: text/markdown
|
|
28
|
-
|
|
29
|
-
# sphinx-vite-builder
|
|
30
|
-
|
|
31
|
-
PEP 517 build backend and Sphinx extension that transparently orchestrates
|
|
32
|
-
[Vite](https://vitejs.dev/) builds via [pnpm](https://pnpm.io/) for
|
|
33
|
-
Sphinx-theme packages whose static assets (CSS / JS) are produced by a
|
|
34
|
-
JavaScript toolchain.
|
|
35
|
-
|
|
36
|
-
## What it solves
|
|
37
|
-
|
|
38
|
-
A common pattern for modern Sphinx themes is a Python package whose
|
|
39
|
-
`theme/<name>/static/` directory ships built CSS and JS that were
|
|
40
|
-
produced by a JS build tool (Vite, webpack, …). The build artefacts are
|
|
41
|
-
gitignored — they're reproducibly built, not source code. But that
|
|
42
|
-
creates two friction points:
|
|
43
|
-
|
|
44
|
-
1. **Editable installs and source-tree builds** crash with confusing
|
|
45
|
-
errors when the static dir is empty (e.g. hatchling's
|
|
46
|
-
`Forced include not found`).
|
|
47
|
-
2. **CI workflows** must duplicate `pnpm install + vite build` setup
|
|
48
|
-
steps in every job that touches the package.
|
|
49
|
-
|
|
50
|
-
`sphinx-vite-builder` owns the Vite invocation end-to-end — exactly the
|
|
51
|
-
way [maturin](https://github.com/PyO3/maturin) owns Cargo for
|
|
52
|
-
Rust+Python packages, or
|
|
53
|
-
[sphinx-theme-builder](https://github.com/pradyunsg/sphinx-theme-builder)
|
|
54
|
-
owns webpack for older Sphinx themes.
|
|
55
|
-
|
|
56
|
-
## Two heads, one subprocess core
|
|
57
|
-
|
|
58
|
-
### PEP 517 build backend
|
|
59
|
-
|
|
60
|
-
Drop-in replacement for `hatchling.build`. Runs `pnpm exec vite build`
|
|
61
|
-
before delegating wheel/sdist construction to hatchling.
|
|
62
|
-
|
|
63
|
-
```toml
|
|
64
|
-
# packages/your-theme/pyproject.toml
|
|
65
|
-
[build-system]
|
|
66
|
-
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
67
|
-
build-backend = "sphinx_vite_builder.build"
|
|
68
|
-
backend-path = ["../sphinx-vite-builder/src"] # for in-tree workspace consumption
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
The backend short-circuits when `web/` (the Vite source tree) is absent
|
|
72
|
-
— so `pip install <pkg>.tar.gz` from an unpacked sdist works without
|
|
73
|
-
pnpm or Node, because the sdist already contains pre-baked
|
|
74
|
-
`static/`.
|
|
75
|
-
|
|
76
|
-
### Sphinx extension
|
|
77
|
-
|
|
78
|
-
Loaded from `conf.py`. Runs Vite as part of the docs lifecycle:
|
|
79
|
-
|
|
80
|
-
- `sphinx-build` → `pnpm exec vite build` once before the docs build
|
|
81
|
-
- `sphinx-autobuild` → `pnpm exec vite build --watch` as a child process
|
|
82
|
-
for the lifetime of the autobuild server, with idempotent re-fire on
|
|
83
|
-
rebuilds and graceful teardown on signal / `atexit`
|
|
84
|
-
|
|
85
|
-
```python
|
|
86
|
-
# docs/conf.py
|
|
87
|
-
extensions = ["sphinx_vite_builder"]
|
|
88
|
-
sphinx_vite_root = "../packages/your-theme/web" # path to vite project
|
|
89
|
-
sphinx_vite_mode = "auto" # auto | dev | prod | disabled
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Fast-fail diagnostics
|
|
93
|
-
|
|
94
|
-
When prerequisites are missing the backend / extension raises
|
|
95
|
-
actionable errors rather than producing broken output:
|
|
96
|
-
|
|
97
|
-
- `PnpmMissingError` — `pnpm` not on `PATH`; hint includes
|
|
98
|
-
`corepack enable` and the `pnpm.io` install URL
|
|
99
|
-
- `NodeModulesInstallError` — `pnpm install` exited non-zero; hint
|
|
100
|
-
includes the rerun command
|
|
101
|
-
- `ViteFailedError` — `pnpm exec vite build` exited non-zero; hint
|
|
102
|
-
surfaces the captured stderr
|
|
103
|
-
|
|
104
|
-
## License
|
|
105
|
-
|
|
106
|
-
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
## Two heads, one subprocess core
|
|
29
|
-
|
|
30
|
-
### PEP 517 build backend
|
|
31
|
-
|
|
32
|
-
Drop-in replacement for `hatchling.build`. Runs `pnpm exec vite build`
|
|
33
|
-
before delegating wheel/sdist construction to hatchling.
|
|
34
|
-
|
|
35
|
-
```toml
|
|
36
|
-
# packages/your-theme/pyproject.toml
|
|
37
|
-
[build-system]
|
|
38
|
-
requires = ["hatchling>=1.0", "sphinx-vite-builder"]
|
|
39
|
-
build-backend = "sphinx_vite_builder.build"
|
|
40
|
-
backend-path = ["../sphinx-vite-builder/src"] # for in-tree workspace consumption
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The backend short-circuits when `web/` (the Vite source tree) is absent
|
|
44
|
-
— so `pip install <pkg>.tar.gz` from an unpacked sdist works without
|
|
45
|
-
pnpm or Node, because the sdist already contains pre-baked
|
|
46
|
-
`static/`.
|
|
47
|
-
|
|
48
|
-
### Sphinx extension
|
|
49
|
-
|
|
50
|
-
Loaded from `conf.py`. Runs Vite as part of the docs lifecycle:
|
|
51
|
-
|
|
52
|
-
- `sphinx-build` → `pnpm exec vite build` once before the docs build
|
|
53
|
-
- `sphinx-autobuild` → `pnpm exec vite build --watch` as a child process
|
|
54
|
-
for the lifetime of the autobuild server, with idempotent re-fire on
|
|
55
|
-
rebuilds and graceful teardown on signal / `atexit`
|
|
56
|
-
|
|
57
|
-
```python
|
|
58
|
-
# docs/conf.py
|
|
59
|
-
extensions = ["sphinx_vite_builder"]
|
|
60
|
-
sphinx_vite_root = "../packages/your-theme/web" # path to vite project
|
|
61
|
-
sphinx_vite_mode = "auto" # auto | dev | prod | disabled
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Fast-fail diagnostics
|
|
65
|
-
|
|
66
|
-
When prerequisites are missing the backend / extension raises
|
|
67
|
-
actionable errors rather than producing broken output:
|
|
68
|
-
|
|
69
|
-
- `PnpmMissingError` — `pnpm` not on `PATH`; hint includes
|
|
70
|
-
`corepack enable` and the `pnpm.io` install URL
|
|
71
|
-
- `NodeModulesInstallError` — `pnpm install` exited non-zero; hint
|
|
72
|
-
includes the rerun command
|
|
73
|
-
- `ViteFailedError` — `pnpm exec vite build` exited non-zero; hint
|
|
74
|
-
surfaces the captured stderr
|
|
75
|
-
|
|
76
|
-
## License
|
|
77
|
-
|
|
78
|
-
MIT — see [LICENSE](LICENSE).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|