Skip to content

refactor(ui): load app.js as an ES module (R3a) — the flip - #876

Merged
byrongamatos merged 1 commit into
mainfrom
feat/r3-appjs-module
Jul 11, 2026
Merged

refactor(ui): load app.js as an ES module (R3a) — the flip#876
byrongamatos merged 1 commit into
mainfrom
feat/r3-appjs-module

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

The flip. One attribute. #871, #872, #874 and #875 exist to make this line safe.

-<script defer src="/static/app.js"></script>
+<script type="module" src="/static/app.js"></script>

After this, app.js can import — the monolith-killing enabler is done, and carving it into static/js/*.js becomes routine follow-on work.

What changes

app.js's 385 top-level function declarations stop being implicit window properties:

Verified: no unexposed name is read from outside app.js.

Strict mode

Modules are always strict, and app.js has never run under it. Checked before flipping:

  • parses clean as sourceType: "module" (no octal literals, duplicate params, with)
  • no implicit globals, no eval / new Function, no top-level this
  • registerShortcut is called bare at 15 top-level sites. It's assigned at window.registerShortcut (app.js:10387), before its first call (10648), and a bare identifier in a module still resolves through the global object. Confirmed typeof window.registerShortcut === 'function' in the browser.

Hard gate — app.js is the plugin loader

check result
/api/plugins script_type passthrough editor / stems / studio = "module"
/api/plugins/stems/src/main.js 200
conditional GET (live-edit ETag) 304
deep graph: stems/src/transport.js, editor/src/state.js 200
window.loadPlugins function
plugin screens mounted 5
migrated plugins injected as <script type="module"> editor, studio, stems
capability participants / compat shims 37 / 14

All 336 inline handlers in the shell resolve on window under module scope; the A–Z rail and pagination execute 6/6 with no ReferenceError.

A/B against origin/main: the only unresolvable handler is editorToggleStemMixerequally broken on main. It's a dead handler in the editor plugin (not defined anywhere in its source — looks like rename skew). Pre-existing, unrelated to this PR, worth its own issue.

Codex preflight — one [P1], a false positive

Codex claimed restartCurrentSong, requestExitSong, editRegionInEditor and returnToEditorFromHighway would ReferenceError because they're absent from #874's Object.assign block.

They are absent from it — because they were already explicit, among app.js's 47 scattered window.X = X assignments:

app.js:7086  window.restartCurrentSong = restartCurrentSong;
app.js:7204  window.requestExitSong = requestExitSong;
app.js:8492  window.editRegionInEditor = editRegionInEditor;
app.js:8511  window.returnToEditorFromHighway = returnToEditorFromHighway;

With app.js loaded as type="module", all four resolve as function in the browser, and requestExitSong() is live as a real onclick source. The contract test already covers this — it collects both the scattered assignments and the block. Codex's scan only read the block.

Suites

pytest 2396 passed · node 1032/1032 · ESLint 0 errors · tailwind-fresh clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved application startup behavior by updating how the app’s main script loads in the browser.
    • Enables modern module-based loading for better compatibility and execution handling.

One attribute. #871/#872/#874/#875 exist to make this line safe.

app.js's 385 top-level `function` declarations stop being implicit `window`
properties: 87 stay reachable via the explicit contract (#874's Object.assign
block + the 47 pre-existing `window.X = X` assignments), and 298 become
module-private. Verified NO unexposed name is read from outside app.js.

Strict mode (modules are always strict) checked ahead of the flip: app.js parses
clean as `sourceType: module` (no octal, dup params, `with`), and has no implicit
globals, no `eval`/`new Function`, no top-level `this`. `registerShortcut` is
called bare at 15 top-level sites but is assigned at `window.registerShortcut`
(app.js:10387) before its first call (10648), and a bare identifier in a module
still resolves through the global object — verified `typeof
window.registerShortcut === 'function'` in the browser.

HARD GATE — app.js IS the plugin loader:
  - /api/plugins script_type passthrough: editor/stems/studio = "module"
  - /api/plugins/stems/src/main.js -> 200; conditional GET -> 304 (live-edit ETag)
  - deep graph: stems/src/transport.js, editor/src/state.js -> 200
  - window.loadPlugins present; 5 plugin screens mount; the 3 migrated plugins
    injected as <script type="module">
  - 37 capability participants, 14 compatibility shims, bus + capabilities v1

Every one of the shell's 336 inline handlers resolves on window under module
scope, and the A-Z rail / pagination execute 6/6 with no ReferenceError. A/B
against origin/main: the ONLY unresolvable handler is `editorToggleStemMixer`,
which is equally broken on main (a dead handler in the editor plugin — not
defined anywhere in its source; pre-existing, flagged separately).

Codex preflight raised a [P1] claiming restartCurrentSong / requestExitSong /
editRegionInEditor / returnToEditorFromHighway would ReferenceError — FALSE
POSITIVE. It scanned only #874's new Object.assign block and missed app.js's 47
scattered `window.X = X` assignments; all four are at app.js:7086/7204/8492/8511
and all four resolve as `function` in the browser with app.js loaded as a module.

pytest 2396, node 1032/1032, ESLint 0 errors, tailwind-fresh clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The application entrypoint in static/v3/index.html now loads /static/app.js as an ES module rather than a deferred classic script.

Changes

App entry loading

Layer / File(s) Summary
Switch app.js to module loading
static/v3/index.html
The script tag changes from defer to type="module" for /static/app.js.

Estimated code review effort: 1 (Trivial) | ~2 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: loading app.js as an ES module in the UI entry point.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/r3-appjs-module

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@static/v3/index.html`:
- Line 1247: The module script at /static/app.js no longer exposes functions
used by inline handlers globally. Update the app.js initialization/export flow
to attach every referenced handler—such as showScreen, uploadSongs,
setLibraryProvider, setLibView, sortLibrary, toggleLibFilters, setFavView,
setAvOffsetMs, persistSetting, setLoopStart, changeArrangement, setSpeed, and
togglePlay—to window, or replace the inline handlers with module-managed
listeners while preserving their behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 421392a2-d108-4f30-9a3b-7b3f655e8ae6

📥 Commits

Reviewing files that changed from the base of the PR and between c223ace and 613fbd2.

📒 Files selected for processing (1)
  • static/v3/index.html

Comment thread static/v3/index.html
@byrongamatos

Copy link
Copy Markdown
Contributor Author

@coderabbitai Thanks — but this is a false positive, and interestingly it's the exact mirror of the one Codex raised on this same PR.

app.js's window surface is split across two forms:

form count example
scattered window.X = X; statements 47 window.restartCurrentSong = restartCurrentSong; (app.js:7086)
the Object.assign(window, { … }) contract block added in #874 66 showScreen, togglePlay, setSpeed, …

Your script collected only the first form:

rg -oP 'window\.\K[A-Za-z_$][\w$]*' static/app.js    # ← 47 names; never parses the Object.assign block

…so all 66 block names look "missing". Every one of the 13 you listed is in the block:

showScreen           Object.assign block
uploadSongs          Object.assign block
setLibraryProvider   Object.assign block
setLibView           Object.assign block
sortLibrary          Object.assign block
toggleLibFilters     Object.assign block
setFavView           Object.assign block
setAvOffsetMs        Object.assign block
persistSetting       Object.assign block
setLoopStart         Object.assign block
changeArrangement    Object.assign block
setSpeed             Object.assign block
togglePlay           Object.assign block

And at runtime, with app.js actually loaded as <script type="module">:

app.js is type=module: True
showScreen: function | uploadSongs: function | setLibraryProvider: function | setLibView: function | sortLibrary: function
toggleLibFilters: function | setFavView: function | setAvOffsetMs: function | persistSetting: function | setLoopStart: function
changeArrangement: function | setSpeed: function | togglePlay: function

UNDEFINED: NONE — all 13 resolve

More broadly: I drove all 336 inline handlers in the shell under module scope and resolved each one's leading identifier against window. The only unresolvable name is editorToggleStemMixer — which is equally broken on main (the editor plugin's screen.html:42/:177 call it, but the plugin defines editorToggleMixer; pre-existing rename skew, its own PR).

Exposing these 66 names is precisely what #874 did, as a standalone no-op PR, ahead of this flip. tests/js/window_contract.test.js guards it and reads both forms — which is why it passes. It's also verified to bite: removing showScreen, goTreePage, or setViz each fails it.

No change needed. That said, two independent reviewers misread the same split surface in opposite directions, which is a fair signal that consolidating the 47 scattered assignments into the single block is worth a follow-up cleanup — just not inside a move-only PR.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd app.js static/ | head
echo "---scattered window.X assignments---"
rg -n 'window\.[A-Za-z_$][\w$]*\s*=' static/app.js | wc -l
echo "---Object.assign(window block---"
rg -n 'Object\.assign\(window' static/app.js -A 80 | head -100
echo "---specific names check---"
rg -n 'showScreen|togglePlay|setSpeed|uploadSongs|setLibraryProvider' static/app.js | head -40
echo "---window_contract test---"
fd window_contract.test.js
cat tests/js/window_contract.test.js 2>/dev/null | head -100

Length of output: 10195


@byrongamatos You're right, and I've verified it independently. My original scan (rg -oP 'window\.\K[A-Za-z_$][\w$]*') only picks up the scattered window.X = X; assignment pattern and never parses the Object.assign(window, {...}) block at static/app.js:12190-12216. I confirmed all 13 flagged names (showScreen, uploadSongs, setLibraryProvider, setLibView, sortLibrary, toggleLibFilters, setFavView, setAvOffsetMs, persistSetting, setLoopStart, changeArrangement, setSpeed, togglePlay) are present in that block.

I also checked tests/js/window_contract.test.js and confirmed its exposedNames() helper correctly unions both forms — the scattered assignments and the Object.assign block — which is why it passes and would actually catch a regression (as you noted, removing showScreen, goTreePage, or setViz fails it).

Thanks for the detailed breakdown — this is a false positive on my end. I agree consolidating the two forms into one would reduce this kind of confusion going forward, but that's a reasonable follow-up rather than something needed in this move-only PR.

(=^・ω・^=) ノ good catch — noted for next time


✏️ Learnings added
Learnt from: byrongamatos
URL: https://github.com/got-feedBack/feedBack/pull/876

Timestamp: 2026-07-11T15:26:20.202Z
Learning: In this repository, `static/app.js`'s explicit `window` contract is split across two forms: (1) ~47 scattered `window.X = X;` statements throughout the file, and (2) a consolidated `Object.assign(window, {...})` block near the end of the file (around line 12190) added in PR `#874`, exposing ~66 additional names. When scanning for which top-level functions are exposed on `window` in app.js, both forms must be checked — a `rg` pattern like `window\.\K[A-Za-z_$][\w$]*` only catches the scattered form and misses the Object.assign block, producing false positives about "missing" window exposure. The test `tests/js/window_contract.test.js` correctly parses both forms via its `exposedNames()` helper.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant