aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_spa_imports.py
blob: 230f33f96b92d9bf5538f0d452230dc05f959127 (plain) (blame)
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)