Merge pull request #157 from MervinPraison/claude/issue-150-20260624-000000

fix(compiler): write all build output as UTF-8 (Windows cp1252)
This commit is contained in:
Mervin Praison authored and GitHub committed 2026-06-24 23:18:20 +01:00
commit 5f37843e3d
7 files changed
+74 -15

No files matched your search

+8 -6
View File
@@ -157,7 +157,7 @@ routes:
template: "docs"
"""
config_path.write_text(minimal_config)
config_path.write_text(minimal_config, encoding="utf-8")
console.print(Panel(f"[green]✓[/green] Created {config_path}", title="Success"))
@@ -598,7 +598,7 @@ def dev(
if dark_mode is not None:
config["site"]["theme"]["darkMode"] = dark_mode
tmp_cfg = example_path / ".aiui.tmp.yaml"
with open(tmp_cfg, "w") as f:
with open(tmp_cfg, "w", encoding="utf-8") as f:
yaml.dump(config, f, default_flow_style=False)
build_config = str(tmp_cfg)
@@ -1025,7 +1025,7 @@ def dev(
if example_name and example_name in examples:
yaml_path = examples_dir / example_name / "aiui.template.yaml"
# Write the new YAML
with open(yaml_path, "w") as f:
with open(yaml_path, "w", encoding="utf-8") as f:
f.write(yaml_content)
console.print(
f"[yellow]Rebuilding {example_name} with edited YAML...[/yellow]"
@@ -1520,12 +1520,14 @@ def run(
},
"style": style,
}
with open(ui_config_path, "w") as f:
with open(ui_config_path, "w", encoding="utf-8") as f:
_json.dump(ui_cfg, f, indent=2)
# Write minimal route-manifest and docs-nav so the SPA doesn't error
(output / "route-manifest.json").write_text('{"routes": []}')
(output / "docs-nav.json").write_text('{"items": []}')
(output / "route-manifest.json").write_text(
'{"routes": []}', encoding="utf-8"
)
(output / "docs-nav.json").write_text('{"items": []}', encoding="utf-8")
# Find available port
def find_available_port(start_port: int, max_attempts: int = 10) -> int:
+8 -4
View File
@@ -422,7 +422,9 @@ class Compiler:
def _write_json(self, path: Path, data: dict, indent: int | None) -> None:
"""Write JSON data to file."""
path.write_text(json.dumps(data, indent=indent, ensure_ascii=False))
path.write_text(
json.dumps(data, indent=indent, ensure_ascii=False), encoding="utf-8"
)
def _copy_viewer(self, output_dir: Path) -> None:
"""Copy frontend bundle to output directory."""
@@ -478,7 +480,7 @@ class Compiler:
"document.documentElement.classList.add('dark');",
"/* light mode — no dark class */",
)
html_path.write_text(html)
html_path.write_text(html, encoding="utf-8")
def _copy_plugins(self, output_dir: Path, frontend_dir: Path) -> None:
"""Copy frontend plugins and generate plugins.json config."""
@@ -519,7 +521,9 @@ class Compiler:
ordered.append(p)
plugins_config = {"plugins": ordered}
(plugins_dst / "plugins.json").write_text(json.dumps(plugins_config, indent=2))
(plugins_dst / "plugins.json").write_text(
json.dumps(plugins_config, indent=2), encoding="utf-8"
)
def _generate_route_pages(self, output_dir: Path, nav: dict) -> list[str]:
"""Generate per-route HTML files for SPA fallback and SEO.
@@ -593,7 +597,7 @@ class Compiler:
f'<div id="root"><noscript>{noscript_content}</noscript></div>',
)
page_file.write_text(html)
page_file.write_text(html, encoding="utf-8")
files.append(f"{relative}/index.html")
return files
+1 -1
View File
@@ -171,7 +171,7 @@ class YAMLConfigStore:
# Write to temp file in same directory, then atomic rename
fd, tmp_path = tempfile.mkstemp(dir=str(self._path.parent), suffix=".yaml.tmp")
try:
with os.fdopen(fd, "w") as f:
with os.fdopen(fd, "w", encoding="utf-8") as f:
yaml.dump(
self._data,
f,
+1 -1
View File
@@ -278,7 +278,7 @@ def _save_data() -> None:
"saved_at": time.time(),
}
_data_file.parent.mkdir(parents=True, exist_ok=True)
with open(_data_file, "w") as f:
with open(_data_file, "w", encoding="utf-8") as f:
json.dump(data, f)
except Exception:
pass # Silently fail
+1 -1
View File
@@ -1466,7 +1466,7 @@ async def api_config_handler(request: Request) -> JSONResponse:
config_data = body.get("config", body)
import yaml
with open(_config_path, "w") as f:
with open(_config_path, "w", encoding="utf-8") as f:
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True)
_config_cache = config_data
return JSONResponse({"status": "saved", "config_path": str(_config_path)})
+2 -2
View File
@@ -256,7 +256,7 @@ def _write_disk_cache(data: dict) -> None:
try:
path = _disk_cache_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data))
path.write_text(json.dumps(data), encoding="utf-8")
except OSError:
pass # Non-critical — build still works
@@ -357,7 +357,7 @@ def inject_theme_css(
# Write to theme.css
theme_file = output_dir / "assets" / "theme.css"
theme_file.parent.mkdir(parents=True, exist_ok=True)
theme_file.write_text(css)
theme_file.write_text(css, encoding="utf-8")
def get_available_themes() -> list[str]:
+53
View File
@@ -599,3 +599,56 @@ class TestCompositionResolver:
assert len(left_sidebar_widgets) == 1
assert left_sidebar_widgets[0]["props"]["searchable"] is True
assert "collapsible" not in left_sidebar_widgets[0]["props"]
class TestCompilerUtf8Writes:
"""Regression tests for issue #150 — UTF-8 on write (Windows cp1252)."""
SPARKLE = "\u2728"
def test_doc_page_written_as_utf8(self, tmp_path: Path):
docs_dir = tmp_path / "docs"
docs_dir.mkdir()
(docs_dir / "index.md").write_text(
f"# Welcome\n\n## {self.SPARKLE} Features\n\nHello!", encoding="utf-8"
)
config = Config(
site=SiteConfig(title="Unicode Site"),
content=ContentConfig(docs=ContentSourceConfig(dir=str(docs_dir))),
templates={"docs": TemplateConfig(layout="Default", slots={})},
routes=[RouteConfig(match="/docs/**", template="docs")],
)
compiler = Compiler(config, base_path=tmp_path)
result = compiler.compile(tmp_path / "output")
assert result.success is True
page = tmp_path / "output" / "docs" / "index" / "index.html"
assert page.exists()
raw = page.read_bytes()
assert self.SPARKLE in raw.decode("utf-8")
assert b"\xe2\x9c\xa8" in raw
def test_write_json_uses_utf8(self, minimal_config: Config, tmp_path: Path):
compiler = Compiler(minimal_config, base_path=tmp_path)
target = tmp_path / "out.json"
compiler._write_json(target, {"sparkle": self.SPARKLE}, indent=2)
raw = target.read_bytes()
assert self.SPARKLE in raw.decode("utf-8")
assert b"\xe2\x9c\xa8" in raw
def test_plugins_json_uses_utf8(self, tmp_path: Path):
frontend_dir = tmp_path / "frontend"
plugins_dst = frontend_dir / "plugins"
plugins_dst.mkdir(parents=True)
config = Config(
site=SiteConfig(title="Plugin Site"),
templates={"docs": TemplateConfig(layout="Default", slots={})},
routes=[RouteConfig(match="/docs/**", template="docs")],
)
compiler = Compiler(config, base_path=tmp_path)
compiler._copy_plugins(tmp_path / "output", frontend_dir)
plugins_file = tmp_path / "output" / "plugins" / "plugins.json"
assert plugins_file.exists()
assert plugins_file.read_bytes().decode("utf-8")