Skip to content

Commit 09dc4d6

Browse files
committed
astro(feat[builder]): Emit src/content.config.ts with collection wiring
why: Cycle 22 / Step 6 of notes/plans/astro.md. Pydantic owns the canonical data shapes, so the Astro side's collection wiring should also be generated rather than hand-maintained — that keeps the defineCollection() registration in lockstep with the emitted artefact paths (src/content/docs/**/*.json, src/content/api/ symbols.json, xref-index.json) and the parity-tested theme schemas (documentSchema, symbolSchema, xrefEntrySchema). The Astro side now inherits zero collection-wiring maintenance: drop the file in, content collections work. what: - Add content_config.py with render_content_config() returning the TypeScript source as a single string. The template registers three collections: * docs — glob({pattern: '**/*.json', base: './src/content/docs'}) + documentSchema * api — file('src/content/api/symbols.json') + symbolSchema * xrefs — file('xref-index.json') + xrefEntrySchema Each import path points at the @gp-sphinx-astro/theme package schemas the parity tests already exercise. Header comment marks the file as auto-generated. - Re-export render_content_config from gp_sphinx_astro_builder.__init__ - AstroBuilder.finish() writes <outdir>/src/content.config.ts (creates src/ directory if missing) - Add 6 pytest tests: * returns a non-empty string * imports from each of the three theme schema modules * defines all three collections * uses glob() for docs and file() for api/xrefs * exports `const collections` * full source is byte-stable against a syrupy snapshot - Add integration test asserting <outdir>/src/content.config.ts exists with the three collection blocks present after a real Sphinx build Verification: Python 1391 passed (was 1383) / 3 skipped, 17 syrupy snapshots green; ruff format / check / mypy strict all green. Steps 0–6 of notes/plans/astro.md are now done.
1 parent 183c1af commit 09dc4d6

6 files changed

Lines changed: 189 additions & 0 deletions

File tree

packages/gp-sphinx-astro-builder/src/gp_sphinx_astro_builder/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import typing as t
1818

1919
from gp_sphinx_astro_builder.builder import AstroBuilder
20+
from gp_sphinx_astro_builder.content_config import render_content_config
2021
from gp_sphinx_astro_builder.models import (
2122
AdmonitionNode,
2223
AdmonitionVariant,
@@ -93,6 +94,7 @@
9394
"export_doctree_schema",
9495
"export_symbol_schema",
9596
"export_xref_index_schema",
97+
"render_content_config",
9698
"setup",
9799
]
98100

packages/gp-sphinx-astro-builder/src/gp_sphinx_astro_builder/builder.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from sphinx.util.inventory import InventoryFile
1919
from sphinx.util.osutil import _last_modified_time
2020

21+
from gp_sphinx_astro_builder.content_config import render_content_config
2122
from gp_sphinx_astro_builder.intersphinx import build_xref_index_entries
2223
from gp_sphinx_astro_builder.schemas import (
2324
export_doctree_schema,
@@ -144,6 +145,16 @@ def finish(self) -> None:
144145
encoding="utf-8",
145146
)
146147

148+
# Astro content collection wiring: the TypeScript file that imports
149+
# the parity-tested theme schemas and registers them with
150+
# defineCollection() against the canonical artefact paths.
151+
content_config_path = self.outdir / "src" / "content.config.ts"
152+
content_config_path.parent.mkdir(parents=True, exist_ok=True)
153+
content_config_path.write_text(
154+
render_content_config(),
155+
encoding="utf-8",
156+
)
157+
147158
def _target_path(self, docname: str): # type: ignore[no-untyped-def]
148159
"""Return the absolute path for the JSON file emitted for ``docname``."""
149160
return self.outdir / "src" / "content" / "docs" / (docname + self.out_suffix)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Generated ``src/content.config.ts`` rendering.
2+
3+
The Astro side wires its content collections through a TypeScript file
4+
that imports Zod schemas from ``@gp-sphinx-astro/theme`` and registers
5+
them with ``defineCollection``. Pydantic owns the canonical data shapes,
6+
so it makes sense to also own the collection wiring — the TypeScript
7+
file is generated by the builder during ``finish()`` rather than
8+
hand-maintained, which keeps the schema registration in lockstep with
9+
the emitted artefacts (`src/content/docs/**/*.json`,
10+
`src/content/api/symbols.json`, `xref-index.json`).
11+
"""
12+
13+
from __future__ import annotations
14+
15+
_TEMPLATE = """\
16+
// AUTO-GENERATED by gp-sphinx-astro-builder. Do not edit by hand;
17+
// regenerate by running `sphinx-build -b astro <src> <out>`.
18+
19+
import { defineCollection } from 'astro:content'
20+
import { file, glob } from 'astro/loaders'
21+
import { documentSchema } from '@gp-sphinx-astro/theme/schemas/doctree'
22+
import { symbolSchema } from '@gp-sphinx-astro/theme/schemas/symbol'
23+
import { xrefEntrySchema } from '@gp-sphinx-astro/theme/schemas/xref'
24+
25+
export const collections = {
26+
docs: defineCollection({
27+
loader: glob({ pattern: '**/*.json', base: './src/content/docs' }),
28+
schema: documentSchema,
29+
}),
30+
api: defineCollection({
31+
loader: file('src/content/api/symbols.json'),
32+
schema: symbolSchema,
33+
}),
34+
xrefs: defineCollection({
35+
loader: file('xref-index.json'),
36+
schema: xrefEntrySchema,
37+
}),
38+
}
39+
"""
40+
41+
42+
def render_content_config() -> str:
43+
"""Return the canonical ``content.config.ts`` source.
44+
45+
The function is intentionally parameter-less for now — every consumer
46+
of the builder needs the same three collections (``docs``, ``api``,
47+
``xrefs``), each pointed at the canonical artefact paths the builder
48+
emits. Parametrise when a real consumer needs to disable or rename
49+
one of the three.
50+
51+
Returns
52+
-------
53+
str
54+
TypeScript source as a single string, newline-terminated.
55+
56+
Examples
57+
--------
58+
>>> from gp_sphinx_astro_builder.content_config import render_content_config
59+
>>> source = render_content_config()
60+
>>> "documentSchema" in source
61+
True
62+
"""
63+
return _TEMPLATE
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# serializer version: 1
2+
# name: test_render_content_config_matches_snapshot
3+
'''
4+
// AUTO-GENERATED by gp-sphinx-astro-builder. Do not edit by hand;
5+
// regenerate by running `sphinx-build -b astro <src> <out>`.
6+
7+
import { defineCollection } from 'astro:content'
8+
import { file, glob } from 'astro/loaders'
9+
import { documentSchema } from '@gp-sphinx-astro/theme/schemas/doctree'
10+
import { symbolSchema } from '@gp-sphinx-astro/theme/schemas/symbol'
11+
import { xrefEntrySchema } from '@gp-sphinx-astro/theme/schemas/xref'
12+
13+
export const collections = {
14+
docs: defineCollection({
15+
loader: glob({ pattern: '**/*.json', base: './src/content/docs' }),
16+
schema: documentSchema,
17+
}),
18+
api: defineCollection({
19+
loader: file('src/content/api/symbols.json'),
20+
schema: symbolSchema,
21+
}),
22+
xrefs: defineCollection({
23+
loader: file('xref-index.json'),
24+
schema: xrefEntrySchema,
25+
}),
26+
}
27+
28+
'''
29+
# ---
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests for :mod:`gp_sphinx_astro_builder.content_config`."""
2+
3+
from __future__ import annotations
4+
5+
import typing as t
6+
7+
from gp_sphinx_astro_builder.content_config import render_content_config
8+
9+
if t.TYPE_CHECKING:
10+
from syrupy.assertion import SnapshotAssertion
11+
12+
13+
def test_render_content_config_returns_string() -> None:
14+
"""``render_content_config`` returns a non-empty TypeScript source string."""
15+
source = render_content_config()
16+
assert isinstance(source, str)
17+
assert source.strip()
18+
19+
20+
def test_render_content_config_imports_canonical_zod_schemas() -> None:
21+
"""The generated source imports the parity-tested theme schemas."""
22+
source = render_content_config()
23+
assert "from '@gp-sphinx-astro/theme/schemas/doctree'" in source
24+
assert "from '@gp-sphinx-astro/theme/schemas/symbol'" in source
25+
assert "from '@gp-sphinx-astro/theme/schemas/xref'" in source
26+
27+
28+
def test_render_content_config_defines_three_collections() -> None:
29+
"""The generated source declares ``docs``, ``api``, and ``xrefs`` collections."""
30+
source = render_content_config()
31+
assert "docs: defineCollection" in source
32+
assert "api: defineCollection" in source
33+
assert "xrefs: defineCollection" in source
34+
35+
36+
def test_render_content_config_uses_glob_for_docs_and_file_for_data() -> None:
37+
"""The generated source wires ``glob()`` for docs and ``file()`` for symbols/xrefs."""
38+
source = render_content_config()
39+
# glob() for the per-document JSON
40+
assert "glob({ pattern: '**/*.json', base: './src/content/docs' })" in source
41+
# file() for the flat-array data collections
42+
assert "file('src/content/api/symbols.json')" in source
43+
assert "file('xref-index.json')" in source
44+
45+
46+
def test_render_content_config_emits_export_const_collections() -> None:
47+
"""The generated source ends with the canonical ``export const collections``."""
48+
source = render_content_config()
49+
assert "export const collections" in source
50+
51+
52+
def test_render_content_config_matches_snapshot(
53+
snapshot: SnapshotAssertion,
54+
) -> None:
55+
"""The full generated source is byte-stable against a syrupy snapshot."""
56+
assert render_content_config() == snapshot

tests/ext/astro_builder/test_integration.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,34 @@ def test_astro_builder_emits_objects_inv_round_trippable(
332332
), f"expected demo_api.merge_demo in inventory; got types: {list(inventory.keys())}"
333333

334334

335+
@pytest.mark.integration
336+
def test_astro_builder_emits_content_config_ts(
337+
tmp_path: pathlib.Path,
338+
) -> None:
339+
"""``finish()`` writes ``src/content.config.ts`` with the canonical wiring."""
340+
cache_root = derive_sphinx_scenario_cache_root(tmp_path)
341+
scenario = SphinxScenario(
342+
buildername="astro",
343+
files=(
344+
ScenarioFile("conf.py", _CONF_PY),
345+
ScenarioFile("index.rst", _INDEX_RST),
346+
),
347+
)
348+
result = build_isolated_sphinx_result(cache_root, tmp_path, scenario)
349+
350+
config_path = result.outdir / "src" / "content.config.ts"
351+
assert config_path.exists(), (
352+
f"expected {config_path} to be emitted; "
353+
f"outdir contents: {list(result.outdir.rglob('*'))}"
354+
)
355+
356+
source = config_path.read_text("utf-8")
357+
assert "export const collections" in source
358+
assert "docs: defineCollection" in source
359+
assert "api: defineCollection" in source
360+
assert "xrefs: defineCollection" in source
361+
362+
335363
@pytest.mark.integration
336364
def test_astro_builder_emission_matches_snapshot(
337365
tmp_path: pathlib.Path,

0 commit comments

Comments
 (0)