refactor(highway): flip highway.js to an ES module (R3c) (#913)

Two lines. index.html: defer -> type="module". highway.js: one explicit assignment.
highway.js can now `import`, which is the whole point — the carve can begin.

━━━ THE ONE THING THE FLIP ACTUALLY BREAKS: window.createHighway ━━━

A top-level `function createHighway()` in a CLASSIC script IMPLICITLY becomes
window.createHighway. In a module it does not — module declarations are module-scoped, and
the name vanishes from the global object the instant the tag grows type="module".

The constitution names window.createHighway as PUBLIC EXTENSION CONTRACT (alongside
window.playSong / showScreen / feedBack). NOTHING IN-TREE CALLS IT. That is exactly why this
would have shipped: the only consumers are third-party plugins rendering their own highway
panel, and I cannot grep those. Green CI, green tests, and a broken plugin API.

Verified by removing the assignment and reloading:

    flip WITHOUT an explicit assignment:  window.createHighway === undefined   <-- gone
    flip WITH it:                         window.createHighway === function

So it is assigned explicitly now — same object, same behaviour, no longer an accident of how
the file happens to be loaded.

━━━ AND A CORRECTION TO #912 ━━━

#912 (merged) rewrote 73 bare `highway.x` -> `window.highway.x` on the stated grounds that
the flip would turn every one of them into a ReferenceError. HAVING NOW ACTUALLY FLIPPED IT,
THAT WAS WRONG. highway.js already did `window.highway = highway`, which puts the name on the
GLOBAL OBJECT — and bare-identifier resolution falls back to the global object whether or not
a lexical global binding exists. Measured on both builds: bare `highway` resolves either way.

#912 is defensible as hygiene and it does not hurt, but it was not a precondition and it fixed
no latent bug. A correction is posted on the PR so its commit message does not mislead. The
real hazard was the factory, not the instance — same class of breakage, wrong name.

ORDERING is unchanged: classic-defer and non-async type="module" share ONE post-parse
execution queue, in document order, so highway.js keeps its position at index.html:1244.

VERIFIED. A/B against origin/main: 15 probes IDENTICAL, zero page errors — window.highway,
window.createHighway, the full API surface, a real song playing, the chart clock advancing,
and the seek->setTime sync. THE PERF GATE PASSES at 1.85ms against its 12ms budget (module
evaluation costs nothing at render time), which is exactly what #910 was built to tell me.

node 1045, pytest 2416, ESLint 0, Codex 0.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-12 12:07:28 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent d9fa6d3f55
commit c6963fdf30
2 changed files with 16 additions and 1 deletions
+15
View File
@@ -4240,6 +4240,21 @@ function createHighway() {
}
const highway = createHighway();
window.highway = highway; // expose for plugins
// THE FACTORY IS PART OF THE PUBLIC CONTRACT, and a classic script gave it to us for free.
//
// A top-level `function createHighway()` in a CLASSIC script implicitly becomes
// window.createHighway. In a MODULE it does not — module declarations are module-scoped, and
// the name would silently vanish from the global object the moment this file grew a
// type="module" attribute. The constitution names window.createHighway as public extension
// contract (alongside window.playSong / showScreen / feedBack), and plugins that render their
// own highway panel construct a second instance with it. Nothing in-tree calls it, which is
// exactly why this would have shipped: the only consumers are third-party, and I cannot grep
// those.
//
// So it is assigned explicitly now. Same object, same behaviour, no longer an accident of how
// the file happens to be loaded.
window.createHighway = createHighway;
highway.setOnLyricsChange(function(visible) {
const btn = document.getElementById('btn-lyrics');
if (btn) {