mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Network failure of the editor runtime is no longer terminal: the loader
re-arms to idle and the next explicit press starts a fresh cycle, while a
fingerprint mismatch on either attempt stays terminal. The toast now says
what actually helps — retry advice for the network, refresh advice for a
foreign build — via one shared lazyLoadFailureMessage helper (new i18n key
editor.retry_advice in en/ru/de).
The field smoke caught a second, deeper bug on the way: Chromium records a
FAILED module in the page module map permanently, so retrying the same URL
(even the cache-busted one) never touched the network again. Every retry
now carries a per-cycle nonce and becomes a genuinely new module request.
A proxy-cached stale entry no longer kills the card silently: the entry
facade is rewritten at build time from a static re-export into a top-level
`try{await import(...)}catch{...}` — importers keep the happy-path
guarantee (await import(entry) still resolves only after
customElements.define), and the catch defines a fallback element with a
localized "reload the page" panel. Content-hashed chunks are served with
`public, max-age=31536000, immutable`, and verifyBundleTree now fails on
orphan chunks that the manifest does not name.
Proofs: loader units for re-arm/terminality/toast wording + an AST check
that both loaders forward the terminality flag; smoke_entry_stale (en/ru)
against a tree without the main chunk; smoke_lazy_editor_chunk extended —
second press after network failure now really opens the editor; pytest for
the immutable header; orphan-tree unit; five new registry mutants.
TESTING.md budget line updated to the #352 ceiling alongside.
Issue: #353
User-Visible: yes
42 lines
1.5 KiB
Python
42 lines
1.5 KiB
Python
"""Pure manifest/path resolver for generated frontend assets."""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
_MANIFEST = "houseplan-assets.json"
|
||
_ASSET_DIR = "houseplan-assets"
|
||
|
||
# Content-hashed chunk bodies never change under their URL (#353 К4). The
|
||
# stable entry `houseplan-card.js` is served elsewhere and stays uncached.
|
||
ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable"
|
||
|
||
|
||
def resolve_frontend_asset(frontend_root: Path, filename: object) -> Path | None:
|
||
"""Resolve one generated JS asset only when the current manifest lists it."""
|
||
if not isinstance(filename, str) or not filename or filename != Path(filename).name:
|
||
return None
|
||
if "/" in filename or "\\" in filename or filename in {".", ".."}:
|
||
return None
|
||
if not filename.endswith(".js"):
|
||
return None
|
||
try:
|
||
manifest = json.loads((frontend_root / _MANIFEST).read_text(encoding="utf-8"))
|
||
except (OSError, ValueError, TypeError):
|
||
return None
|
||
if not isinstance(manifest, dict) or manifest.get("schema") != 1:
|
||
return None
|
||
expected = f"{_ASSET_DIR}/{filename}"
|
||
listed = {
|
||
item.get("path")
|
||
for item in manifest.get("files", [])
|
||
if isinstance(item, dict)
|
||
}
|
||
if expected not in listed:
|
||
return None
|
||
asset_root = (frontend_root / _ASSET_DIR).resolve()
|
||
candidate = (asset_root / filename).resolve()
|
||
if candidate.parent != asset_root or not candidate.is_file():
|
||
return None
|
||
return candidate
|