mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
`emailLibrary/index.js` goes from 11,375 lines to 6,187. What came out:
- `settingsPage.js` (879) — the Email Settings view, its form and controls,
away/auto-reply including the calendar-event sync, and the display
preferences. The inline-image preference lives here rather than with the
renderer because the settings form owns writing it.
- `unsubscribe.js` (1,224) — the bulk-unsubscribe review flow. One exported
entry point, its own localStorage keys, its own agent-tool-output listeners.
- `reader.js` (676) — opening an email as a docked tab or a floating window,
plus the AI summary panel both of them share.
- `menus.js` (855) — the reader More menu, the card kebab menu, the bulk
Actions menu and `_bulkAction`.
- `bodyRender.js` (871) — plain and threaded body rendering, inline MIME
images, quote folding.
- `attachments.js` (491) — attachment chips and the deferred load for messages
whose attachment list was not in the list response.
- `aiReply.js` (449) — AI-reply entry points, the per-message context draft,
the translate and remind submenus.
The extracted modules import back from `index.js`, so the graph has cycles.
That is safe for hoisted function declarations and unsafe for a value read
during evaluation, so nothing crosses a module boundary except functions:
`API_BASE` is re-declared per module, the way `emailInbox.js` and
`emailShared.js` already do it, and `_autoReplyRefreshSeq` moves into
`state.js` because the settings page and the unread-badge refresh both write
it and an imported binding is read-only. The module-graph test enters the
package at each module in turn, which is the order that would expose a
dead-zone read.
Two test helpers grew while doing this. `js_function_source` replaces three
marker-pair slices ("from this signature down to that comment") whose end
marker had moved into another module — the slice ran past the function and
kept passing against the wrong text. Its first implementation balanced braces
by walking characters and ended `_toggleCardPreview` 18 lines early, because
the apostrophe in `// that's a scroll, not a nav` opened a string that ate the
braces after it. It now keys on the invariant the file actually holds: a
top-level declaration starts at column 0, so its closing brace is the next
lone `}` at column 0.
84 lines
3.4 KiB
Python
84 lines
3.4 KiB
Python
"""Read a split JS subpackage the way the module graph does.
|
|
|
|
``static/js/emailLibrary.js`` is a re-export wrapper; the implementation lives
|
|
in ``static/js/emailLibrary/``. A test that asserts on email-library behaviour
|
|
has to look at every module in that package, because reading one file ties the
|
|
test to whichever module a function happens to sit in today — it goes red the
|
|
next time something moves without any behaviour changing.
|
|
|
|
That is the mistake the stylesheet split made, which is why
|
|
``tests/helpers/stylesheets.py`` exists. This is the same helper for JS.
|
|
|
|
Order is deterministic: the entry module first, then the rest alphabetically.
|
|
Tests that assert "A appears before B" are asserting about one module's source,
|
|
not about the package, so the concatenation order only has to be stable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
_STATIC_JS = Path(__file__).resolve().parents[2] / "static" / "js"
|
|
|
|
EMAIL_LIBRARY_WRAPPER = _STATIC_JS / "emailLibrary.js"
|
|
EMAIL_LIBRARY_PACKAGE = _STATIC_JS / "emailLibrary"
|
|
EMAIL_LIBRARY_ENTRY = EMAIL_LIBRARY_PACKAGE / "index.js"
|
|
|
|
|
|
def _package_paths(package: Path, entry: Path) -> list[Path]:
|
|
if not entry.is_file():
|
|
raise AssertionError(f"missing package entry module: {entry}")
|
|
rest = sorted(p for p in package.glob("*.js") if p != entry)
|
|
return [entry, *rest]
|
|
|
|
|
|
def email_library_paths(include_wrapper: bool = False) -> list[Path]:
|
|
"""Every module of the email-library package, entry module first.
|
|
|
|
``include_wrapper`` adds the compatibility file at the old top-level path.
|
|
Leave it off for assertions about implementation code: the wrapper holds
|
|
only an ``export … from`` list.
|
|
"""
|
|
paths = _package_paths(EMAIL_LIBRARY_PACKAGE, EMAIL_LIBRARY_ENTRY)
|
|
return [EMAIL_LIBRARY_WRAPPER, *paths] if include_wrapper else paths
|
|
|
|
|
|
def email_library_source(include_wrapper: bool = False) -> str:
|
|
"""The whole email-library package as one string."""
|
|
return "\n".join(
|
|
p.read_text(encoding="utf-8") for p in email_library_paths(include_wrapper)
|
|
)
|
|
|
|
|
|
def js_function_source(name: str, source: str | None = None) -> str:
|
|
"""One top-level JS function, from its signature to its closing brace.
|
|
|
|
Two things this does not do, on purpose.
|
|
|
|
It does not slice between a signature and a marker further down ("from
|
|
``_toggleCardPreview`` to the ``Wrap a probable signature`` comment"). That
|
|
is what a split breaks: the marker ends up in another module, the slice runs
|
|
past the end of the function without failing, and the assertions keep
|
|
passing against the wrong text.
|
|
|
|
It does not balance braces by walking characters either. The obvious version
|
|
of that walker treats the apostrophe in a ``// that's a scroll`` comment as
|
|
an open quote and swallows every brace until the next one, which ends the
|
|
function early — silently, again.
|
|
|
|
Instead it uses the invariant the file actually holds: a top-level
|
|
declaration starts at column 0, so its closing brace is the next lone ``}``
|
|
at column 0.
|
|
"""
|
|
text = email_library_source() if source is None else source
|
|
signature = re.compile(
|
|
r"^(?:export\s+)?(?:async\s+)?function\s+" + re.escape(name) + r"\s*\(",
|
|
re.M,
|
|
)
|
|
match = signature.search(text)
|
|
assert match, f"no top-level declaration of {name}"
|
|
closing = re.compile(r"^\}", re.M).search(text, match.end())
|
|
assert closing, f"unterminated function {name}"
|
|
return text[match.start():closing.end()]
|