1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
"""
Every import in the SPA points at a file that exports what it names.
The failure this catches has a distinctive shape: nothing errors at build time,
because there is no build; the browser resolves the module graph at load, finds
a binding that is not there, and the page renders blank or the component simply
does not appear. `node --check` cannot see it — it parses one file at a time —
and neither can the source-reading guards, which look inside a file rather than
between two.
Written after the settings-page split, which moved two shared components into a
new module and rewired eight files to import them from there. That is exactly
the change where a rename lands in one file and not the other.
It is a static check, not a load: it says the name is exported, not that the
value is what the caller expects. `test_spa_syntax` covers parsing;
`test_hook_ordering` covers the ordering fault that also presents as a missing
component.
"""
import re
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
pytestmark = pytest.mark.skipif(not STATIC.exists(),
reason="SPA sources unavailable")
# `import { a, b as c } from './x.js';` / `import * as p from ...` / default.
IMPORT = re.compile(
r"^import\s+(?:\{([^}]*)\}|(\*\s+as\s+\w+)|(\w+))\s+from\s+'([^']+)';", re.M)
EXPORT_BLOCK = re.compile(r"^export\s+\{([^}]*)\};", re.M)
EXPORT_DECL = re.compile(
r"^export\s+(?:default\s+)?(?:async\s+)?(?:function|const|class|let|var)\s+(\w+)",
re.M)
def _exported(source: str) -> set[str]:
names = set(EXPORT_DECL.findall(source))
for block in EXPORT_BLOCK.findall(source):
for raw in block.split(","):
name = raw.strip().split(" as ")[-1].strip()
if name:
names.add(name)
return names
def test_every_named_import_resolves():
problems: list[str] = []
for path in sorted(STATIC.glob("*.js")):
source = path.read_text(encoding="utf-8")
for names, star, default, spec in IMPORT.findall(source):
# vendor/ is third-party, bundled, and does not use a form this
# reads. Its exports are covered by the app failing to start.
if not spec.startswith("./") or "vendor/" in spec:
continue
target = (path.parent / spec[2:]).resolve()
if not target.exists():
problems.append(f"{path.name}: imports {spec} — no such file")
continue
if star or default or not names.strip():
continue
available = _exported(target.read_text(encoding="utf-8"))
for raw in names.split(","):
name = raw.strip().split(" as ")[0].strip()
if name and name not in available:
problems.append(
f"{path.name}: imports {{{name}}} from {spec}, "
f"which does not export it")
assert not problems, "unresolved imports:\n" + "\n".join(problems)
def test_the_check_can_see_a_real_module():
"""
Guard against the parser quietly matching nothing — a regex that stopped
finding imports would make the test above pass over an empty set, which
looks exactly like success.
"""
source = (STATIC / "group-page.js").read_text(encoding="utf-8")
found = IMPORT.findall(source)
assert len(found) >= 5, (
"the import pattern no longer matches group-page.js — this test is then "
"checking nothing")
assert "GroupPage" in _exported(source)
# `lazy(() => import('./x.js'), 'Name')` (lazy.js), and the hand-written form
# `import('./x.js').then(m => { _Page = m.Name; ...`.
LAZY = re.compile(r"lazy\(\s*\(\)\s*=>\s*import\('\./([^']+)'\),\s*'(\w+)'")
THEN = re.compile(r"import\('\./([^']+)'\)\.then\(\s*m\s*=>\s*\{\s*\w+\s*=\s*m\.(\w+)")
def test_every_module_loaded_on_demand_exports_what_is_asked_for():
"""
The same fault as above, one step later: a component fetched on demand
under a name its module does not export renders a spinner that never
ends — no error, since the fetch itself succeeded.
"""
problems, seen = [], 0
for path in sorted(STATIC.glob("*.js")):
source = path.read_text(encoding="utf-8")
for spec, name in LAZY.findall(source) + THEN.findall(source):
seen += 1
target = STATIC / spec
if not target.exists():
problems.append(f"{path.name}: loads ./{spec} — no such file")
elif name not in _exported(target.read_text(encoding="utf-8")):
problems.append(f"{path.name}: loads {{{name}}} from ./{spec}, "
f"which does not export it")
assert seen >= 15, f"only {seen} on-demand loads found — is the pattern stale?"
assert not problems, "unresolved on-demand loads:\n" + "\n".join(problems)
|