Files
FabledScribe/tests/test_frontend_shared_styles.py
T
bvandeusenandClaude Opus 5.5 1774ee3696
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m40s
CI & Build / Python tests (push) Successful in 2m37s
CI & Build / Build & push image (push) Successful in 56s
fix(frontend): each shared <style src> sheet sits at one block index everywhere, so vite build cannot depend on transform order (#4988)
Run 8271 failed in the image build: "[vite:vue] Cannot read properties of undefined (reading 'scoped')" on moments-shared.css. plugin-vue caches one descriptor per src file and answers ?index=N from whichever component registered it last. RuleEditorSlideOver had moments-shared at block 2, while its other two importers have it at block 1. Step 2's new imports changed the transform order and exposed the bug.

- RuleEditorSlideOver: moments-shared is now loaded with an @import instead of a third <style src>.
- rules-shared.css sat at block 0 in the five panes and at block 1 in LessonDetailView, LessonEditorView and RuleEditorSlideOver, which is the same latent hazard. The three move to block 0.
- tests/test_frontend_shared_styles.py: a guard that every <style src> sheet sits at one index, plus a test that the guard can fail. vue-tsc cannot see this hazard, and vite build only runs after every test has passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:58:41 -04:00

54 lines
2.2 KiB
Python

"""A stylesheet shared by `<style src>` must sit at the same block index in
every component that loads it that way.
plugin-vue caches one SFC descriptor per `src` file and resolves a request for
`moments-shared.css?vue&type=style&index=N` against whichever component last
registered it. When two components carry the same src at different indexes,
the lookup can land on a component with fewer style blocks, and the production
build dies with "Cannot read properties of undefined (reading 'scoped')". It
depends on transform order, so an unrelated change, such as a new import in a
view, is enough to flip it. vue-tsc cannot see it; only `vite build` can, and
that runs at image-build time, after every test has passed.
"""
from __future__ import annotations
import pathlib
import re
from collections import defaultdict
ROOT = pathlib.Path(__file__).resolve().parents[1] / "frontend" / "src"
STYLE_TAG = re.compile(r"<style\b([^>]*)>")
SRC_ATTR = re.compile(r'\bsrc="([^"]+)"')
def src_indexes(sources: dict[str, str]) -> dict[str, dict[int, list[str]]]:
"""{src: {block index: [component, ...]}} over a set of SFC sources."""
seen: dict[str, dict[int, list[str]]] = defaultdict(lambda: defaultdict(list))
for name, text in sources.items():
for index, attrs in enumerate(STYLE_TAG.findall(text)):
match = SRC_ATTR.search(attrs)
if match:
seen[match.group(1)][index].append(name)
return seen
def disagreements(sources: dict[str, str]) -> dict[str, dict[int, list[str]]]:
return {src: dict(at) for src, at in src_indexes(sources).items() if len(at) > 1}
def test_every_shared_style_src_sits_at_one_block_index():
sources = {str(p.relative_to(ROOT)): p.read_text() for p in ROOT.rglob("*.vue")}
assert sources, f"no .vue files under {ROOT}"
assert disagreements(sources) == {}, (
"a <style src> sheet sits at different block indexes across components — "
"load it with @import in the odd one out"
)
def test_the_guard_can_fail():
sources = {
"A.vue": '<style scoped></style>\n<style src="@/x.css" />',
"B.vue": '<style scoped></style>\n<style src="@/y.css" />\n<style src="@/x.css" />',
}
assert disagreements(sources) == {"@/x.css": {1: ["A.vue"], 2: ["B.vue"]}}