From a8f02c9f3f4f3d71ed03f4987ceab40d62a628d8 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 16 Mar 2026 04:52:15 +0000 Subject: [PATCH] Fix studio frontend build producing empty Tailwind CSS Two issues caused the studio frontend to render without any styling when installed via `pip install` (non-editable): 1. `pyproject.toml` package-data only included `frontend/dist/**/*`. The `include-package-data = true` setting relies on `git ls-files`, which fails in isolated builds (pip/uv copy source to a temp dir without `.git`). This meant `frontend/src/`, `package.json`, `vite.config.ts`, and other build files were missing from the installed package. Tailwind had no source files to scan. 2. Python venvs auto-create a `.gitignore` with a bare `*` pattern. Tailwind v4's oxide scanner walks parent directories and respects `.gitignore` -- so even when source files are present, the venv's `*` pattern causes the scanner to skip all `.tsx` files. The result is a 34KB CSS skeleton with zero utility classes instead of the expected 265KB. Additionally, Vite adds `crossorigin` to script/link tags by default. This forces CORS mode on font subresource loads, which Firefox HTTPS-Only Mode does not exempt -- causing all @font-face downloads to fail silently when Studio is served over HTTP. Changes: - pyproject.toml: Expand package-data to include frontend source, config files, setup scripts, and backend requirements using glob patterns (no node_modules) - studio/setup.sh: Temporarily hide parent .gitignore files containing a bare `*` during `npm run build`, with trap-based restoration - studio/backend/main.py: Strip `crossorigin` attributes from HTML at serve time so fonts load correctly on any protocol --- pyproject.toml | 19 ++++++++++++++++++- studio/backend/main.py | 18 ++++++++++++++++++ studio/setup.sh | 25 +++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7317fc8b0..1e46cc92d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,24 @@ version = {attr = "unsloth.models._utils.__version__"} include-package-data = true [tool.setuptools.package-data] -studio = ["frontend/dist/**/*"] +studio = [ + "*.sh", + "*.ps1", + "*.bat", + "frontend/dist/**/*", + "frontend/public/**/*", + "frontend/src/**/*", + "frontend/*.json", + "frontend/*.ts", + "frontend/*.js", + "frontend/*.lock", + "frontend/*.html", + "frontend/*.yaml", + "frontend/.git*", + "backend/requirements/**/*", + "backend/core/data_recipe/oxc-validator/*.json", + "backend/core/data_recipe/oxc-validator/*.mjs", +] [tool.setuptools.packages.find] exclude = ["images*", "tests*", "kernels/moe*"] diff --git a/studio/backend/main.py b/studio/backend/main.py index be46272847..3ab846306e 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -255,6 +255,22 @@ async def get_hardware_info(): # ============ Serve Frontend (Optional) ============ +def _strip_crossorigin(html_bytes: bytes) -> bytes: + """Remove ``crossorigin`` attributes from script/link tags. + + Vite adds ``crossorigin`` by default which forces CORS mode on font + subresource loads. When Studio is served over plain HTTP, Firefox + HTTPS-Only Mode does not exempt CORS font requests -- causing all + @font-face downloads to fail silently. Stripping the attribute + makes them regular same-origin fetches that work on any protocol. + """ + import re as _re + + html = html_bytes.decode("utf-8") + html = _re.sub(r'\s+crossorigin(?:="[^"]*")?', "", html) + return html.encode("utf-8") + + def _inject_bootstrap(html_bytes: bytes, app: FastAPI) -> bytes: """Inject bootstrap credentials into HTML when password change is required. @@ -296,6 +312,7 @@ def setup_frontend(app: FastAPI, build_path: Path): @app.get("/") async def serve_root(): content = (build_path / "index.html").read_bytes() + content = _strip_crossorigin(content) content = _inject_bootstrap(content, app) return Response( content = content, @@ -319,6 +336,7 @@ def setup_frontend(app: FastAPI, build_path: Path): # Serve index.html as bytes — avoids Content-Length mismatch content = (build_path / "index.html").read_bytes() + content = _strip_crossorigin(content) content = _inject_bootstrap(content, app) return Response( content = content, diff --git a/studio/setup.sh b/studio/setup.sh index a9e3085efd..3fc01feb2b 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -121,8 +121,33 @@ echo "✅ Node $(node -v) | npm $(npm -v)" echo "" echo "Building frontend..." cd "$SCRIPT_DIR/frontend" + +# Tailwind v4's oxide scanner respects .gitignore in parent directories. +# Python venvs create a .gitignore with "*" (ignore everything), which +# prevents Tailwind from scanning .tsx source files for class names. +# Temporarily hide any such .gitignore during the build, then restore it. +_HIDDEN_GITIGNORES=() +_dir="$(pwd)" +while [ "$_dir" != "/" ]; do + _dir="$(dirname "$_dir")" + if [ -f "$_dir/.gitignore" ] && grep -qx '\*' "$_dir/.gitignore" 2>/dev/null; then + mv "$_dir/.gitignore" "$_dir/.gitignore._twbuild" + _HIDDEN_GITIGNORES+=("$_dir/.gitignore") + fi +done + +_restore_gitignores() { + for _gi in "${_HIDDEN_GITIGNORES[@]}"; do + mv "${_gi}._twbuild" "$_gi" 2>/dev/null || true + done +} +trap _restore_gitignores EXIT + run_quiet "npm install" npm install run_quiet "npm run build" npm run build + +_restore_gitignores +trap - EXIT cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" run_quiet "npm install (oxc validator runtime)" npm install cd "$SCRIPT_DIR"