oct
Loading...
Searching...
No Matches
typedoc.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/docs/typedoc.py
4
5"""
6Purpose
7-------
8The TypeDoc tier of ``oct docs``: API documentation for TypeScript
9subprojects, wired in the same shape as the Doxygen tier so the two are
10learned once.
11
12Doxygen has no TypeScript parser, and Sphinx autodoc has no TypeScript
13either, so before this module a TypeScript subproject (``oc_explorer``)
14had a Sphinx tier for its markdown and nothing for its API. TypeDoc runs
15the real TypeScript compiler and documents the type-checked program, which
16is why it beats any text-level tool for this job: generics, unions,
17overloads, inferred return types and re-exports all come from the checker.
18
19The declaration is a ``typedoc.json`` at the subproject root, mirroring
20"the Doxyfile is the declaration": the single-project build runs on it,
21the workspace fan-out includes the subproject on it, the hub shows a
22TypeDoc card on it, and ``--clean`` sweeps the output regardless.
23
24Responsibilities
25----------------
26- Own :data:`TYPEDOC_CONFIG_REL` and :func:`has_typedoc`, the declaration
27 every consumer resolves.
28- :func:`typedoc_output_dir` - read ``out`` from the config the way
29 TypeDoc resolves it (relative to the config's directory).
30- :func:`run_docs_typedoc` - clear the previous output, run TypeDoc via
31 ``npx`` with the subproject as working directory, report the result.
32
33Diagnostics
34-----------
35Domain: OCT-DOCS
36Levels:
37 L2 - build entry / exit
38 L3 - tool resolution, output directory decision
39 L4 - subprocess argv
40
41Contracts
42---------
43- Nothing here installs anything. TypeDoc is a dev dependency of the
44 TypeScript subproject (``npm install --save-dev typedoc``) and ``npx``
45 resolves it from that subproject's ``node_modules``. A missing ``npx``
46 is reported and returns non-zero from a direct ``--typedoc`` request,
47 because the operator asked for that build by name; the workspace
48 fan-out is the caller that decides whether that is fatal.
49- Output is cleared before each run for the same reason Doxygen's is:
50 a generator does not delete pages for inputs it no longer sees. The
51 same two guards apply - the directory must resolve inside the project
52 and must carry TypeDoc's own marker - so a mis-set ``out`` cannot turn
53 a docs build into a recursive delete.
54- No shell. The subprocess is an argv list; on Windows ``npx`` is a
55 ``.cmd`` shim and is resolved through :func:`shutil.which` so the call
56 works without ``shell=True``.
57
58Dependencies
59------------
60- oct.core.diagnostics (_dbg structured logger)
61- oct.tools.fs_remove (remove_tree_or_warn)
62- oct.docs.workspace_docs (lazy; is_workspace_root, run_workspace_docs - the workspace fan-out)
63"""
64
65from __future__ import annotations
66
67import json
68import shutil
69import subprocess
70import sys
71from pathlib import Path
72
73from oct.core.diagnostics import _dbg
74from oct.tools.fs_remove import remove_tree_or_warn
75
76
77#: Where a project declares TypeDoc, relative to the project root.
78TYPEDOC_CONFIG_REL: Path = Path("typedoc.json")
79
80#: TypeDoc's default ``out`` when the config does not set one.
81_DEFAULT_OUT: str = "docs"
82
83#: Files TypeDoc writes into every HTML output directory it owns. One must
84#: be present before the pre-run clear will delete the directory.
85_TYPEDOC_HTML_MARKERS: tuple[str, ...] = ("index.html", "hierarchy.html", "assets")
86
87
88def has_typedoc(project_dir: Path) -> bool:
89 """Return True when *project_dir* declares TypeDoc documentation."""
90 return (Path(project_dir) / TYPEDOC_CONFIG_REL).is_file()
91
92
93def typedoc_output_dir(project_dir: Path) -> Path:
94 """Resolve the HTML output directory the project's ``typedoc.json`` names.
95
96 TypeDoc resolves a relative ``out`` against the directory holding the
97 config file. A config that cannot be parsed, or that sets no ``out``,
98 resolves to TypeDoc's default.
99 """
100 func = "typedoc_output_dir"
101 config_path = Path(project_dir) / TYPEDOC_CONFIG_REL
102 out = _DEFAULT_OUT
103 try:
104 data = json.loads(config_path.read_text(encoding="utf-8"))
105 if isinstance(data, dict) and isinstance(data.get("out"), str) and data["out"]:
106 out = data["out"]
107 except (OSError, json.JSONDecodeError) as exc:
108 _dbg("OCT-DOCS", func, f"config unreadable ({exc}); default out", 3)
109 resolved = (config_path.parent / out).resolve()
110 _dbg("OCT-DOCS", func, f"out={resolved}", 3)
111 return resolved
112
113
114def _clear_typedoc_output(project_dir: Path) -> None:
115 """Delete the previous TypeDoc output, guarded the same way as Doxygen's."""
116 func = "_clear_typedoc_output"
117 html_dir = typedoc_output_dir(project_dir)
118 if not html_dir.is_dir():
119 return
120 try:
121 html_dir.relative_to(Path(project_dir).resolve())
122 except ValueError:
123 print(f"Warning: TypeDoc output {html_dir} is outside the project; "
124 f"not clearing it.")
125 _dbg("OCT-DOCS", func, f"refused: {html_dir} outside project", 2)
126 return
127 if not any((html_dir / m).exists() for m in _TYPEDOC_HTML_MARKERS):
128 print(f"Warning: {html_dir} does not look like TypeDoc output "
129 f"(no {_TYPEDOC_HTML_MARKERS[0]}); not clearing it.")
130 _dbg("OCT-DOCS", func, f"refused: {html_dir} has no marker", 2)
131 return
132 if remove_tree_or_warn(html_dir, label="previous TypeDoc output"):
133 _dbg("OCT-DOCS", func, f"cleared {html_dir}", 3)
134
135
136def find_npx() -> str | None:
137 """Return the ``npx`` executable path, or ``None`` when Node is absent.
138
139 On Windows the launcher is ``npx.cmd``; :func:`shutil.which` finds
140 either, and returning the resolved path lets the subprocess run as a
141 plain argv list with no shell.
142 """
143 return shutil.which("npx")
144
145
146def run_docs_typedoc(root: Path, *, no_workspace: bool = False) -> int:
147 """Build the TypeDoc tier for the project at *root*.
148
149 Returns 0 on success; 1 when the project declares no ``typedoc.json``,
150 when ``npx`` cannot be found, or when TypeDoc itself fails.
151
152 Workspace mode: from a workspace root, dispatches to
153 :func:`oct.docs.workspace_docs.run_workspace_docs` with
154 ``mode="typedoc"`` unless *no_workspace* is set.
155 """
156 func = "run_docs_typedoc"
157 if not no_workspace:
158 from oct.docs.workspace_docs import is_workspace_root, run_workspace_docs
159 if is_workspace_root(root):
160 return run_workspace_docs(root, mode="typedoc")
161
162 _dbg("OCT-DOCS", func, f"start root={root}", 2)
163 if not has_typedoc(root):
164 print(
165 f"Error: no {TYPEDOC_CONFIG_REL} at {root}.\n"
166 f"A TypeScript project declares TypeDoc by carrying that file; "
167 f"see 'oct docs --help'.",
168 file=sys.stderr,
169 )
170 return 1
171
172 npx = find_npx()
173 if npx is None:
174 print(
175 "Error: npx is not on PATH. TypeDoc runs through Node.js; "
176 "install Node and run 'npm install' in the project so its "
177 "typedoc dev dependency is present.",
178 file=sys.stderr,
179 )
180 _dbg("OCT-DOCS", func, "npx missing", 1)
181 return 1
182
184
185 # --no-install: npx must find typedoc in the project's node_modules and
186 # never fetch it from the registry mid-build. A missing dev dependency
187 # is a project setup problem and should fail as one, not be papered
188 # over by a network download that may or may not happen.
189 argv = [npx, "--no-install", "typedoc"]
190 _dbg("OCT-DOCS", func, f"argv={argv} cwd={root}", 4)
191 print(f"Running typedoc (cwd {root})...")
192 try:
193 result = subprocess.run(argv, cwd=str(root))
194 except OSError as exc:
195 print(f"Error: could not run npx: {exc}", file=sys.stderr)
196 return 1
197
198 if result.returncode == 0:
199 index = typedoc_output_dir(root) / "index.html"
200 if index.exists():
201 print(f"\nTypeDoc build complete. Open: {index}")
202 else:
203 print(f"\nTypeDoc build complete. Output: {typedoc_output_dir(root)}")
204 _dbg("OCT-DOCS", func, f"end rc={result.returncode}", 2)
205 return result.returncode
206
207
208__all__ = [
209 "TYPEDOC_CONFIG_REL",
210 "find_npx",
211 "has_typedoc",
212 "run_docs_typedoc",
213 "typedoc_output_dir",
214]
bool has_typedoc(Path project_dir)
Definition typedoc.py:88
int run_docs_typedoc(Path root, *, bool no_workspace=False)
Definition typedoc.py:146
Path typedoc_output_dir(Path project_dir)
Definition typedoc.py:93
None _clear_typedoc_output(Path project_dir)
Definition typedoc.py:114
str|None find_npx()
Definition typedoc.py:136