Merge dev: family canon, milestone 463 steps 1-6 #206

Merged
bvandeusen merged 10 commits from dev into main 2026-10-06 14:11:29 -04:00
4 changed files with 77 additions and 13 deletions
Showing only changes of commit 1774ee3696 - Show all commits
@@ -414,6 +414,12 @@ watch(() => props.ruleId, load);
</div>
</template>
<!-- `.rule-chip` for the "suggested" marker on a lesson — the chip the rule
panes already use, loaded here because the slide-over can open with no
pane that loads it (a link straight to ?rule=N).
Block 0, as in every pane that loads it: a shared src must sit at
one index everywhere (tests/test_frontend_shared_styles.py). -->
<style src="@/assets/rules-shared.css" />
<style scoped>
.kind { border: 1px solid var(--fs-border-color); border-radius: var(--fs-radius-md); padding: var(--fs-space-3); margin: var(--fs-space-3) 0; }
.kind legend { font-size: var(--fs-size-tiny); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); color: var(--fs-text-tertiary); padding: 0 var(--fs-space-2); }
@@ -518,8 +524,11 @@ legend { padding: 0 0.35rem; font-size: 0.8rem; color: var(--fs-text-tertiary);
.trash:hover, .close:hover { opacity: 1; }
</style>
<!-- `.rule-chip` for the "suggested" marker on a lesson — the chip the rule
panes already use, loaded here because the slide-over can open with no
pane that loads it (a link straight to ?rule=N). -->
<style src="@/assets/rules-shared.css" />
<style src="@/assets/moments-shared.css" />
<!-- An @import, not a second <style src>: plugin-vue caches ONE descriptor per
src file, and every other importer of moments-shared.css has it as style
block 1. Here it would be block 2, so whichever SFC registered last decided
the lookup and the production build failed with "reading 'scoped'" when
the transform order changed (tests/test_frontend_shared_styles.py). -->
<style>
@import "@/assets/moments-shared.css";
</style>
+5 -4
View File
@@ -313,6 +313,11 @@ onMounted(load);
</div>
</template>
<!-- `.rule-chip` and its preference variant: the marker a rule's kind wears
everywhere else, reused rather than re-spelled.
Block 0, as in every pane that loads it: a shared src must sit at
one index everywhere (tests/test_frontend_shared_styles.py). -->
<style src="@/assets/rules-shared.css" />
<style scoped>
.lesson-detail {
max-width: 780px;
@@ -459,7 +464,3 @@ onMounted(load);
font-size: 0.88rem;
}
</style>
<!-- `.rule-chip` and its preference variant: the marker a rule's kind wears
everywhere else, reused rather than re-spelled. -->
<style src="@/assets/rules-shared.css" />
+5 -4
View File
@@ -459,6 +459,11 @@ onMounted(() => {
</div>
</template>
<!-- `.rule-chip-preference` marks a preference among the rules offered, as it
does everywhere else a rule's kind is shown.
Block 0, as in every pane that loads it: a shared src must sit at
one index everywhere (tests/test_frontend_shared_styles.py). -->
<style src="@/assets/rules-shared.css" />
<style scoped>
.lesson-editor {
max-width: 820px;
@@ -606,7 +611,3 @@ onMounted(() => {
.le-small { padding: 0.2rem 0.7rem; font-size: var(--fs-size-label); }
.le-ghost:disabled { opacity: var(--fs-disabled-opacity); cursor: default; }
</style>
<!-- `.rule-chip-preference` marks a preference among the rules offered, as it
does everywhere else a rule's kind is shown. -->
<style src="@/assets/rules-shared.css" />
+53
View File
@@ -0,0 +1,53 @@
"""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"]}}