refactor(email): move emailLibrary.js into static/js/emailLibrary/

The email library was 11,375 lines in one file, the second-largest JS
module in the repo. `static/js/emailLibrary/` already held four extracted
helpers, so the package existed; the bulk of the code just was not in it.

The implementation moves to `emailLibrary/index.js` and the old path
becomes a re-export wrapper. Five call sites import that path, four of
them dynamically with a `?v=` string, and `sw.js` caches URLs verbatim,
so a wrapper is what makes the move need no coordinated edit to any of
them.

The test side is the part worth reviewing. 58 tests read
`static/js/emailLibrary.js` as text. Pointing them at
`emailLibrary/index.js` would buy one move and break again on the next
one, which is exactly what happened to the stylesheet tests (they now go
through `tests/helpers/stylesheets.py`). So the same shape:
`tests/helpers/js_modules.py` reads the whole package, and assertions
stop caring which module a function sits in.

`tests/test_email_library_module_graph_js.py` is new. This frontend has
no module-graph validation, and a package fails in ways a single file
cannot: a wrapper that drops an export is `undefined` at call time rather
than an error at load time, and a module that reads a `const` across an
import cycle throws only when that module is entered first. It pins the
wrapper's surface against the entry module's, evaluates every module on
its own in a browser, and requires both import paths to hand out one
instance.

It also caught a live gap while being written:
`emailLibrary/replyRecipients.js` is imported by `emailInbox.js`, an app
shell module, and was never in the `sw.js` precache.
This commit is contained in:
Léo
2026-09-30 09:08:21 +02:00
parent 6105702901
commit ab5a08a6f9
21 changed files with 11729 additions and 11446 deletions
+4 -4
View File
@@ -6,11 +6,11 @@ import subprocess
from pathlib import Path
import pytest
from tests.helpers.js_modules import email_library_source
_REPO = Path(__file__).resolve().parent.parent
_INBOX_JS = _REPO / "static" / "js" / "emailInbox.js"
_LIBRARY_JS = _REPO / "static" / "js" / "emailLibrary.js"
_HAS_NODE = shutil.which("node") is not None
@@ -21,7 +21,7 @@ def _extract_between(source: str, signature: str, next_marker: str) -> str:
def test_library_unread_preview_has_one_authoritative_request_and_rollback():
source = _LIBRARY_JS.read_text(encoding="utf-8")
source = email_library_source()
function = _extract_between(source, "async function _toggleCardPreview", "\n/**\n * Wrap a probable signature block")
assert function.count("/api/email/read/") == 1
@@ -38,7 +38,7 @@ def test_library_unread_preview_has_one_authoritative_request_and_rollback():
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
def test_library_authoritative_success_defeats_newer_rollback_in_either_order():
source = _LIBRARY_JS.read_text(encoding="utf-8")
source = email_library_source()
function = _extract_between(source, "async function _toggleCardPreview", "\n/**\n * Wrap a probable signature block")
settlements = _extract_between(
function,
@@ -101,7 +101,7 @@ console.log(JSON.stringify({{
def test_library_reply_open_carries_immutable_mailbox_context():
library_source = _LIBRARY_JS.read_text(encoding="utf-8")
library_source = email_library_source()
inbox_source = _INBOX_JS.read_text(encoding="utf-8")
assert "const mailboxGeneration = _emailMailboxGeneration;" in library_source