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
|
"""
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 / "apps.js").read_text(encoding="utf-8")
found = IMPORT.findall(source)
assert len(found) >= 5, (
"the import pattern no longer matches apps.js — this test is then "
"checking nothing")
assert "configurableApps" in _exported(source)
|