Merged
Conversation
Resolves on first WebSocket open. External scripts (test harnesses, IDE integrations) calling window.ema.switchRoute or other future WS-dependent APIs can now await the promise instead of racing the handshake or polling for connection state. Reconnects don't reset it — it's a one-shot "client is past first handshake" gate. Motivated by the e2e test cases for Emanote (srid/emanote#667), where a Playwright step needs to programmatically navigate via switchRoute and the natural alternative (sleep an arbitrary 200-1000ms) is flaky.
srid
added a commit
to srid/emanote
that referenced
this pull request
Apr 26, 2026
The previous e2e setup spawned emanote with --no-ws, so every scenario navigated via page.goto (fresh full load) and the morph code path was completely uncovered. That's why issue #667 (toc-spy dying after morph nav) escaped detection. Drop --no-ws and add a @morph-tagged scenario that navigates via window.ema.switchRoute. The test is gated to live mode only; static mode has no WS so the @morph Before hook returns 'skipped'. Awaiting window.ema.ready (PR srid/ema#181 — pinned via flake input to feat/ema-ready-promise) avoids racing the WS handshake. The shim's EMAHotReload event lets the step block until morph + script-reload completes, so the next assertion sees stable DOM. Coverage now: 16 scenarios; live=16/16, static=15/16 (1 skipped).
srid
added a commit
to srid/emanote
that referenced
this pull request
Apr 26, 2026
ema PR srid/ema#181 merged. Drop the temporary feat-branch pin in flake.nix and pull the merge commit via flake.lock — no functional change since the feat branch contained exactly the merged content.
srid
added a commit
to srid/emanote
that referenced
this pull request
Apr 26, 2026
**Site-authored interactive behaviors no longer live as inline `<script>` IIFEs scattered across Heist templates.** Four small ES modules under `_emanote-static/js/` (`theme-toggle`, `code-copy`, `toc-spy`, `footnote-popup`) replace the per-template IIFEs, plus a `morph.js` helper providing `ready()` and `onElement(selector, fn)` (MutationObserver-backed). Closes #643. *Side effect*: code-copy buttons now wire onto `<pre>` blocks added by live-server DOM patches, not only those present at first paint. Live-mode cache busting works **per file** via an inline `<script type="importmap">` mapping bare specifiers (`@emanote/morph` etc.) to URLs through `siteRouteUrl`, which appends `?t=<mtime>`. Plain ES module imports (`import './morph.js'`) would have stripped the query during URL resolution and stale-cached transitive deps; the bare-specifier indirection is what makes per-file invalidation propagate. The importmap itself is tagged `im-preserve` + `data-ema-skip` so it survives Ema's morph navigation — without that, the second importmap in the morphed `<head>` hit the HTML spec's *already-resolved specifier* rule and emitted a console warning per entry. The `toc-spy` module captures DOM refs in a closure, so it now also subscribes to Ema's `EMAHotReload` event to tear down + re-init on each morph (closes #667). Static mode also gained a fix: the production Tailwind compile now adds `_emanote-static/js` as an `@source`, so utility classes applied by JS at runtime (`POPOVER_CLASS`, `BODY_CLASS` in `footnote-popup.js`) are visible to the scanner and don't drop out of `tailwind.css`. Pre-fix, the popup rendered as plain text inside a thin border on `emanote gen` output. Also strips Tailwind's `//# sourceMappingURL=/sm/<hash>.map` trailer to silence the per-pageload 404 in dev logs. > **Depends on srid/ema#181** (merged) — `window.ema.ready` Promise so the morph-nav e2e step can await the WS handshake without racing. > > **Out of scope, kept inline by design**: FOUC theme applier in `base.tpl` (pre-paint, can't defer), Stork controller (`onclick="window.emanote.stork.toggleSearch()"` call sites span four templates — separate refactor), breadcrumbs `toggleHidden` (5 lines; section 1 of #632 deletes via native `<details>`). > > **Known limitation, documented**: editing a *bare-imported* JS module mid-session needs a hard reload to pick up the change via morph nav (the preserved importmap holds the original URLs). `main.js` itself still cache-busts via morph since it's loaded directly. Issue #666 tracks the broader layer-URL cache-bust gap (CSS, fonts, Stork's own assets). E2e: 16 scenarios, live=16/16, static=15+1-skipped (`@morph` tag gates morph-nav for live only). New scenarios cover theme toggle, code-copy initial wiring, TOC scroll-spy, popup body styling (catches the Tailwind class-drop regression class), and morph-nav of toc-spy. Module split into `Emanote.View.JsBundle` mirrors the existing `Emanote.View.Tailwind` shape. ### Try it locally ```sh nix run github:srid/emanote/js-module-boundary -- -L $YOUR_NOTEBOOK run ```
srid
added a commit
to srid/emanote
that referenced
this pull request
Apr 26, 2026
Quality review flagged narration ("this file owns…", "extracted because…",
"step definitions stay mode-agnostic by going through this helper") and
WHAT-restating doc blocks across the new modules. Trimmed each to the
WHY-only nucleus or the bare purpose; kept the bug references that
encode hidden constraints (srid/ema#181, EMAHotReload) since those
inform reviewers about non-obvious gates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
window.ema.readynow resolves on first WebSocket handshake — a one-shot gate for external scripts that need to callwindow.ema.switchRoute(or any other WS-dependent API we add later) without racing the connection open. Reconnects don't reset it; it's a first-open signal, not a current-state flag.Motivated by Emanote's e2e suite (srid/emanote#667), where a Playwright step needs to programmatically navigate via
switchRouteto exercise the morph code path. The natural alternative — `page.waitForTimeout` — is flaky against cold-start emanote. Polling forWebSocket.OPENrequires exposing thewsinstance, which is closure-scoped on purpose. A one-line Promise on the public API surface is the cheapest, broadest fix.Tiny behavior addition; no breaking change. The existing
window.ema = { switchRoute }shape gets one new field; nothing existing moves.