From 65c77155d8d04034c3b6f1bd4559c288f49adcc3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 12:31:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF=20a?= =?UTF-8?q?ssets=20=E6=B8=85=E7=90=86=E8=84=9A=E6=9C=AC=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20Rolldown=20=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- deploy/deploy-frontend.sh | 3 +- deploy/prune_frontend_assets.py | 81 +++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/deploy/deploy-frontend.sh b/deploy/deploy-frontend.sh index 1b68449f..f78c5556 100644 --- a/deploy/deploy-frontend.sh +++ b/deploy/deploy-frontend.sh @@ -26,7 +26,8 @@ echo "✓ Upload complete" # 4. Extract on server + prune orphaned legacy chunks echo "▶ Deploying on server..." ssh nexus "cd /www/wwwroot/api.synaglobal.vip/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz" -echo "▶ Pruning orphaned assets..." +echo "▶ Pruning orphaned assets (dry-run first)..." +ssh nexus "python3 /www/wwwroot/api.synaglobal.vip/deploy/prune_frontend_assets.py --dry-run" ssh nexus "python3 /www/wwwroot/api.synaglobal.vip/deploy/prune_frontend_assets.py" echo "✓ Extracted and pruned" diff --git a/deploy/prune_frontend_assets.py b/deploy/prune_frontend_assets.py index fa3c728c..a76f9e77 100644 --- a/deploy/prune_frontend_assets.py +++ b/deploy/prune_frontend_assets.py @@ -1,15 +1,32 @@ #!/usr/bin/env python3 -"""Remove orphaned files under web/app/assets/ not referenced by current Vite build.""" +"""Remove orphaned files under web/app/assets/ not referenced by current Vite build. + +Vite/Rolldown minified bundles reference chunks by bare filename (e.g. ServersPage-xxx.js) +without import() paths — scan file contents for known asset basenames. +""" from __future__ import annotations import re import sys from pathlib import Path -IMPORT_RE = re.compile( - r"""(?:from\s*["']|import\s*\(\s*["']|import\s*["'])\./([^"']+\.(?:js|css))""" +ASSET_NAME_RE = re.compile( + r"[A-Za-z0-9][A-Za-z0-9_.-]*\.(?:js|css|woff2?|ttf|eot)" ) -URL_RE = re.compile(r"""url\(["']?([^"')]+\.(?:woff2?|ttf|eot))["']?\)""") +URL_RE = re.compile(r"""url\(["']?([^"')]+)["']?\)""") + + +def _referenced_basenames(text: str, known: set[str]) -> set[str]: + found: set[str] = set() + for m in ASSET_NAME_RE.finditer(text): + name = m.group(0) + if name in known: + found.add(name) + for raw in URL_RE.findall(text): + name = Path(raw).name + if name in known: + found.add(name) + return found def collect(app_dir: Path) -> set[Path]: @@ -17,42 +34,48 @@ def collect(app_dir: Path) -> set[Path]: index = app_dir / "index.html" if not index.is_file(): raise SystemExit(f"missing {index}") - keep: set[Path] = set() - queue: list[Path] = [] + + all_names = {p.name for p in assets.iterdir() if p.is_file()} + keep_names: set[str] = set() for m in re.finditer(r"/app/assets/([^\s\"']+)", index.read_text(encoding="utf-8")): - p = assets / m.group(1) - if p.is_file(): - queue.append(p) + name = m.group(1).split("/")[-1] + if name in all_names: + keep_names.add(name) - while queue: - path = queue.pop() - if path in keep or not path.is_file(): - continue - keep.add(path) - text = path.read_text(encoding="utf-8", errors="ignore") - for rel in IMPORT_RE.findall(text): - nxt = assets / rel - if nxt.is_file(): - queue.append(nxt) - if path.suffix == ".css": - for rel in URL_RE.findall(text): - rel = rel.lstrip("./") - nxt = assets / rel - if nxt.is_file(): - keep.add(nxt) - return keep + if not keep_names: + raise SystemExit("no entry assets found in index.html") + + changed = True + while changed: + changed = False + for name in list(keep_names): + path = assets / name + if not path.is_file(): + continue + text = path.read_text(encoding="utf-8", errors="ignore") + for ref in _referenced_basenames(text, all_names): + if ref not in keep_names: + keep_names.add(ref) + changed = True + + return {assets / n for n in keep_names} def main() -> int: - app_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "/www/wwwroot/api.synaglobal.vip/web/app") + dry_run = "--dry-run" in sys.argv + args = [a for a in sys.argv[1:] if a != "--dry-run"] + app_dir = Path(args[0] if args else "/www/wwwroot/api.synaglobal.vip/web/app") assets = app_dir / "assets" if not assets.is_dir(): raise SystemExit(f"not a directory: {assets}") keep = collect(app_dir) - all_files = {p for p in assets.rglob("*") if p.is_file()} - orphan = sorted(all_files - keep) + all_files = {p for p in assets.iterdir() if p.is_file()} + orphan = sorted(all_files - keep, key=lambda p: p.name) + if dry_run: + print(f"dry-run kept={len(keep)} would_remove={len(orphan)} total={len(all_files)}") + return 0 for p in orphan: p.unlink() print(f"kept={len(keep)} removed={len(orphan)} total_before={len(all_files)}")