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
This commit is contained in:
Daniel Han 2026-03-16 04:52:15 +00:00
commit a8f02c9f3f
3 changed files with 61 additions and 1 deletions

View file

@ -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*"]

View file

@ -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,

View file

@ -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"