Skip to content

Commit 41c17d1

Browse files
mishig25claude
andauthored
refactor: modernize build and preview commands (#798)
- extract the duplicated kit-staging logic (~80 lines in each command) into shared helpers in build.py: stage_kit_routes(), docs_node_env(), run_npm(), resolve_npm_sibling() - skip node_modules/.svelte-kit/build when copying the kit folder to the temp dir: npm ci recreates node_modules anyway (locally this avoided copying ~0.5GB per build) - fix a preview bug: `p.rmdir if p.is_dir() else p.unlink()` never called rmdir, so reserved `__`-named directories (e.g. __pycache__) were never removed before the dev build - resolve node/npm via shutil.which: replaces preview's shell=True-on- Windows workaround and makes `build --html` work on Windows too - raise the node version check to >= 20 (vite 7 requirement) and make the error message command-agnostic (preview reused it with a message about the --html flag) - run the dev-server thread as a daemon so Ctrl-C exits cleanly - replace `env["package_name"] or x if cond else x` precedence puzzle with env.get(); drop the markdown re-read in build (the content is already returned by write_markdown_route_file); pathlib throughout Verified: byte-identical accelerate e2e output (48/48 pages + identical file inventory, llms.txt, markdown routes, objects.inv); preview smoke tested end-to-end (dev server serves pages, watchdog rebuild reflects an edited md file in the served page); ruff and pytest clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 2f7376e commit 41c17d1

2 files changed

Lines changed: 145 additions & 153 deletions

File tree

src/doc_builder/commands/build.py

Lines changed: 109 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,119 @@
3333
write_markdown_route_file,
3434
)
3535

36+
# The kit (SvelteKit) build currently runs on Vite 7, which requires Node.js >= 20.
37+
MIN_NODE_MAJOR_VERSION = 20
38+
39+
# Build artifacts and dependencies do not need to be staged: `npm ci` recreates
40+
# node_modules and the build regenerates `.svelte-kit` and `build`.
41+
KIT_COPY_IGNORE = shutil.ignore_patterns("node_modules", ".svelte-kit", "build")
42+
3643

3744
def check_node_is_available():
45+
"""
46+
Raises if Node.js is missing or older than `MIN_NODE_MAJOR_VERSION`.
47+
"""
3848
try:
3949
p = subprocess.run(
40-
["node", "-v"],
50+
[resolve_npm_sibling("node"), "-v"],
4151
capture_output=True,
4252
check=True,
4353
encoding="utf-8",
4454
)
4555
version = p.stdout.strip()
4656
except Exception as e:
4757
raise OSError(
48-
"Using the --html flag requires node v14 to be installed, but it was not found in your system."
58+
f"This command requires Node.js v{MIN_NODE_MAJOR_VERSION} or newer, but node was not found in your system."
4959
) from e
5060

51-
major = int(version[1:].split(".")[0])
52-
if major < 14:
61+
major = int(version.removeprefix("v").split(".")[0])
62+
if major < MIN_NODE_MAJOR_VERSION:
5363
raise OSError(
54-
"Using the --html flag requires node v14 to be installed, but the version in your system is lower "
55-
f"({version[1:]})"
64+
f"This command requires Node.js v{MIN_NODE_MAJOR_VERSION} or newer, but the version in your system is "
65+
f"lower ({version.removeprefix('v')})."
5666
)
5767

5868

69+
def resolve_npm_sibling(executable):
70+
"""
71+
Resolves `node`/`npm` to a full path, which also works on Windows where
72+
`npm` is `npm.cmd` (not directly runnable by `subprocess` without a shell).
73+
"""
74+
resolved = shutil.which(executable)
75+
if resolved is None:
76+
raise OSError(f"`{executable}` was not found in your system (PATH).")
77+
return resolved
78+
79+
80+
def run_npm(npm_args, cwd, env=None, quiet=True):
81+
"""
82+
Runs `npm <npm_args>` in `cwd`. With `quiet=True`, stdout is captured
83+
(stderr passes through so errors stay visible).
84+
"""
85+
subprocess.run(
86+
[resolve_npm_sibling("npm"), *npm_args],
87+
stdout=subprocess.PIPE if quiet else None,
88+
check=True,
89+
encoding="utf-8",
90+
cwd=str(cwd),
91+
env=env,
92+
)
93+
94+
95+
def docs_node_env(library_name, version, language):
96+
"""
97+
Environment for the kit build/dev server: DOCS_* variables end up as compile-time
98+
constants (see kit/vite.config.ts) and in the base path (see kit/svelte.config.js).
99+
"""
100+
env = os.environ.copy()
101+
env["DOCS_LIBRARY"] = env.get("package_name") or library_name
102+
env["DOCS_VERSION"] = version
103+
env["DOCS_LANGUAGE"] = language
104+
return env
105+
106+
107+
def stage_kit_routes(kit_folder, tmp_dir, output_path):
108+
"""
109+
Stages the SvelteKit app in `tmp_dir` with the generated doc files as routes:
110+
111+
1. copies the kit folder (without dependencies/build artifacts),
112+
2. copies the generated files from `output_path` into `src/routes`,
113+
3. renames `.mdx` files to the SvelteKit routing convention (`foo.mdx` ->
114+
`foo/+page.svelte`) and writes the markdown (`foo.md`) route files.
115+
116+
Returns `(kit_dir, routes_dir, markdown_exports)` where `markdown_exports` is a
117+
list of `(relative_posix_path, markdown_content)` tuples.
118+
"""
119+
kit_dir = Path(tmp_dir) / "kit"
120+
routes_dir = kit_dir / "src" / "routes"
121+
122+
shutil.copytree(kit_folder, kit_dir, ignore=KIT_COPY_IGNORE)
123+
124+
# Manual copy and overwrite from output_path to src/routes: we don't use
125+
# shutil.copytree as it exists and contains important files.
126+
for f in Path(output_path).iterdir():
127+
dest = routes_dir / f.name
128+
if f.is_dir():
129+
if dest.is_dir():
130+
shutil.rmtree(dest)
131+
shutil.copytree(f, dest)
132+
else:
133+
shutil.copy(f, dest)
134+
135+
# make mdx file paths comply with the sveltekit routing mechanism
136+
# see more: https://svelte.dev/docs/kit/routing
137+
markdown_exports = []
138+
for mdx_file_path in routes_dir.rglob("*.mdx"):
139+
new_page_svelte = sveltify_file_route(mdx_file_path)
140+
new_markdown = markdownify_file_route(mdx_file_path)
141+
content = write_markdown_route_file(mdx_file_path, new_markdown)
142+
markdown_exports.append((Path(new_markdown).relative_to(routes_dir).as_posix(), content))
143+
Path(new_page_svelte).parent.mkdir(parents=True, exist_ok=True)
144+
shutil.move(mdx_file_path, new_page_svelte)
145+
146+
return kit_dir, routes_dir, markdown_exports
147+
148+
59149
def build_command(args):
60150
read_doc_config(args.path_to_docs)
61151
if args.html:
@@ -117,94 +207,45 @@ def build_command(args):
117207
)
118208

119209
# dev build should not update _versions.yml
120-
package_doc_path = os.path.join(args.build_dir, args.library_name)
121-
if "pr_" not in version and os.path.isfile(os.path.join(package_doc_path, "_versions.yml")):
122-
update_versions_file(os.path.join(args.build_dir, args.library_name), version, args.path_to_docs)
210+
package_doc_path = Path(args.build_dir) / args.library_name
211+
if "pr_" not in version and (package_doc_path / "_versions.yml").is_file():
212+
update_versions_file(package_doc_path, version, args.path_to_docs)
123213

124214
# If asked, convert the MDX files into HTML files.
125215
if args.html:
126216
with tempfile.TemporaryDirectory() as tmp_dir:
127-
tmp_dir = Path(tmp_dir)
128-
# Copy everything in a tmp dir
129-
shutil.copytree(kit_folder, tmp_dir / "kit")
130-
# Manual copy and overwrite from output_path to tmp_dir / "kit" / "src" / "routes"
131-
# We don't use shutil.copytree as tmp_dir / "kit" / "src" / "routes" exists and contains important files.
132-
svelte_kit_routes_dir = tmp_dir / "kit" / "src" / "routes"
133-
for f in output_path.iterdir():
134-
dest = svelte_kit_routes_dir / f.name
135-
if f.is_dir():
136-
# Remove the dest folder if it exists
137-
if dest.is_dir():
138-
shutil.rmtree(dest)
139-
shutil.copytree(f, dest)
140-
else:
141-
shutil.copy(f, dest)
142-
# make mdx file paths comply with the sveltekit 1.0 routing mechanism
143-
# see more: https://learn.svelte.dev/tutorial/pages
144-
markdown_exports = []
145-
for mdx_file_path in svelte_kit_routes_dir.rglob("*.mdx"):
146-
new_page_svelte = sveltify_file_route(mdx_file_path)
147-
new_markdown = markdownify_file_route(mdx_file_path)
148-
write_markdown_route_file(mdx_file_path, new_markdown)
149-
markdown_exports.append((new_markdown, os.path.relpath(new_markdown, svelte_kit_routes_dir)))
150-
parent_path = os.path.dirname(new_page_svelte)
151-
os.makedirs(parent_path, exist_ok=True)
152-
shutil.move(mdx_file_path, new_page_svelte)
217+
kit_dir, _, markdown_exports = stage_kit_routes(kit_folder, tmp_dir, output_path)
153218

154219
# Move the objects.inv file at the root
155220
if not args.not_python_module:
156-
shutil.move(tmp_dir / "kit" / "src" / "routes" / "objects.inv", tmp_dir / "objects.inv")
221+
shutil.move(kit_dir / "src" / "routes" / "objects.inv", Path(tmp_dir) / "objects.inv")
157222

158223
# Build doc with node
159-
working_dir = str(tmp_dir / "kit")
160224
print("Installing node dependencies")
161-
subprocess.run(
162-
["npm", "ci"],
163-
stdout=subprocess.PIPE,
164-
check=True,
165-
encoding="utf-8",
166-
cwd=working_dir,
167-
)
225+
run_npm(["ci"], cwd=kit_dir)
168226

169-
env = os.environ.copy()
170-
env["DOCS_LIBRARY"] = (
171-
env["package_name"] or args.library_name if "package_name" in env else args.library_name
172-
)
173-
env["DOCS_VERSION"] = version
174-
env["DOCS_LANGUAGE"] = args.language
175227
print("Building HTML files. This will take a while :-)")
176-
subprocess.run(
177-
["npm", "run", "build"],
178-
stdout=subprocess.PIPE,
179-
check=True,
180-
encoding="utf-8",
181-
cwd=working_dir,
182-
env=env,
183-
)
228+
run_npm(["run", "build"], cwd=kit_dir, env=docs_node_env(args.library_name, version, args.language))
184229

185230
# Copy result back in the build_dir.
186231
shutil.rmtree(output_path)
187-
shutil.copytree(tmp_dir / "kit" / "build", output_path)
188-
# copy markdown routes alongside the generated html output
189-
markdown_data = []
190-
for markdown_file, relative_path in markdown_exports:
191-
markdown_source = Path(markdown_file)
232+
shutil.copytree(kit_dir / "build", output_path)
233+
# write markdown routes alongside the generated html output
234+
for relative_path, content in markdown_exports:
192235
markdown_dest = output_path / relative_path
193236
markdown_dest.parent.mkdir(parents=True, exist_ok=True)
194-
shutil.copy(markdown_source, markdown_dest)
195-
with open(markdown_source, encoding="utf-8") as f:
196-
markdown_data.append((relative_path, f.read()))
237+
markdown_dest.write_text(content, encoding="utf-8", newline="\n")
197238
write_llms_feeds(
198239
output_path,
199-
markdown_data,
240+
markdown_exports,
200241
package_name=args.library_name,
201242
version=version,
202243
language=args.language,
203244
is_python_module=not args.not_python_module,
204245
)
205246
# Move the objects.inv file back
206247
if not args.not_python_module:
207-
shutil.move(tmp_dir / "objects.inv", output_path / "objects.inv")
248+
shutil.move(Path(tmp_dir) / "objects.inv", output_path / "objects.inv")
208249

209250

210251
def build_command_parser(subparsers=None):

0 commit comments

Comments
 (0)