Skip to content

Update to webpack 5 / Node.js 22 / A-Frame master / three r184#218

Open
vincentfretin wants to merge 35 commits into
supermedium:masterfrom
vincentfretin:update-webpack5
Open

Update to webpack 5 / Node.js 22 / A-Frame master / three r184#218
vincentfretin wants to merge 35 commits into
supermedium:masterfrom
vincentfretin:update-webpack5

Conversation

@vincentfretin
Copy link
Copy Markdown

@vincentfretin vincentfretin commented Dec 29, 2025

I didn't manage to run that project on an old version of Node.js with nvm, so I updated webpack from version 4 to 5 so we can run the project on latest Node.js LTS 22.
I removed all babel transforms, none were used actually, also removed the unused zip-loader dependency.
webpack 5 doesn't include nodejs polyfills anymore, so it was easier to rewrite the zip worker, Claude Claude Opus 4.5 replaced unzip-js + Node.js polyfills with fflate - a modern, fast, pure JS zip library that works natively in browsers without polyfills.

Things that were fixed:

  • the star alpha blending, combined with a fix in aframe master Expose premultipliedAlpha on material component aframevr/aframe#5810
    Before:
    star_before
    After:
    star_after
  • controllers reconnection that lose ability to click and haptics not working on reconnect (fixed with the update to aframe master)
    That was an issue in aframe 1.3.0 that moonrider was using, with the way that version get the gamepads, it adds a 500ms delay to get an updated list and because of it there is a race condition that you end up with controllers having a generic gamepad with 10 buttons instead of 12 buttons and no joysticks. This issue was actually fixed in aframe 1.7.0 aframevr/aframe@fd90434 to not have a delay when getting the controllers. Haptics lost when controllers reconnects is fixed by Fix tracked-controls.controller undefined on controllerconnected after reconnect aframevr/aframe#5804 in aframe master
  • the hover on the keyboard letters
  • the song cover, removed usage of the cors proxy that don't exist anymore and proxying is not needed today
  • better perf running at 90 fps (because latest aframe contains the pool component optimization, that was in Dec 24 2022 included in 1.4.0 aframe release)
  • remove the subscribe to newsletter that doesn't exist anymore
  • don't trigger a second fetch of the zip because of the abort: true postMessage that the zip worker didn't honor since 787f82d
  • better cleanup of audio sources

Nice to have before merging:

Deployed

https://vincentfretin.github.io/moonrider/

Note for me, to deploy, remove the CNAME file from the predeploy command, change the deploy command to use git@github.com:vincentfretin/moonrider.git and run npm run deploy

Comment thread src/workers/zip.js
const difficulty = difficultyBeatmap._difficulty;
const beatmapFilename = difficultyBeatmap._beatmapFilename;
if (beatFiles[beatmapFilename] === undefined) {
continue;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I asked about that particular change Claude did.

Original behavior: If beatFiles[beatmapFilename] was undefined, it would return from the callback and never call postMessage. This meant silent failure - the worker would just hang and never respond.

New behavior: With continue, we skip missing beatmaps but always call postMessage. If data.beats ends up empty, zip-loader properly emits songloaderror.

So the new code has better error handling - it fails gracefully with an error message instead of silently hanging.

Windswipe added a commit to Windswipe/Moon-Rider-REBORN that referenced this pull request Apr 17, 2026
vincentfretin and others added 26 commits April 19, 2026 11:15
beat-generator.onClearGame iterates #beatContainer.children, which per
scene.html is the container attribute for every beat and plume pool —
i.e. it holds all pooled entities, in-use and already-returned.
Calling returnToPool on the already-returned ones produced "returned
entity was not previously pooled" warnings from A-Frame's pool (30+
per pool on every game clear, matching each pool's initial size).

Move the "am I currently in use?" check into returnToPool itself so
the component owns that question and any caller can safely call it
more than once. Drop the position.set and visible=false fixups along
the way: a returned entity is paused and invisible, and its position
is set fresh the next time it's handed back out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Forward-fix for A-Frame 1.6.0+: since the component update rework in
aframevr/aframe#5474, Component.destroy() actively nulls this.data.
play-sound's handler reads this.data.enabled on every event, so a
lingering listener after destroy fires with this.data === undefined
and throws "Cannot read properties of undefined (reading 'enabled')".

Store the bound handler on the component and detach it in a new
remove hook.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dispose is idempotent on three.js BufferGeometry, but nulling out
this.geometry afterwards makes the `if (this.geometry)` check actually
mean "there's a live geometry to dispose" and releases the stale
reference so GC can collect the disposed instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A-Frame 1.6.0 (commit 4a89bb6e) defers component/system init until
document.readyState === 'complete', so the envmap template image is
already fully loaded by the time this system runs. The
addEventListener('load', ...) callback was attaching after the load
event had fired and never executed, leaving envmapImg.src empty and
wall shaders sampling a blank texture — wall interiors rendered black
instead of reflecting the cyan/pink gradient.

Drop the load listener and the DOMContentLoaded guard around
createMaterials() while here — both are dead code under the new
timing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-normal material blending modes only take effect in three.js's
transparent render pass, so the additive bloom around the merkaba
star at the horizon was silently dropped and the planes rendered
opaquely, exposing the underlying pink merkaba as a small pink blob
instead of the bright white glow. Adding transparent: true puts them
back in the transparent pass where their additive blending and
depthTest: false are actually honored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A-Frame 1.5.0 (commit c8e8241b, "Redesign enter session UI") renamed
the component from vr-mode-ui to xr-mode-ui. Without the rename no
component is registered, so the querySelector + click-listener code
that wires up our custom #vrButton never runs and the "Click Here to
Enter VR" button is dead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A-Frame 1.7 renamed oculus-touch-controls to meta-touch-controls,
removed the orientationOffset schema property (no longer honored), and
switched tracked-controls to hardcoded gripSpace. A-Frame also
dropped daydream-controls and gearvr-controls some releases ago.

- controller.js: setAttribute 'meta-touch-controls' instead of
  'oculus-touch-controls'; drop the no-op orientationOffset; drop
  daydream-controls registration and cursor config.
- debug-controller.js, super-keyboard.js, state/index.js: reflect the
  same rename; drop daydream-controls/gearvr-controls entries (the
  has3DOFVR list collapses to a single value).
- scene.html: the old per-controller blade rotation
  (controllerType.indexOf('oculus') !== -1 ? '135 0 0' : '90 0 0')
  was compensating for targetRaySpace pose. With gripSpace as the new
  default, '90 0 0' is correct for every controller — verified in VR.
  Drop the conditional.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
We load A-Frame from a CDN <script> tag in index.html; the local
vendor/aframe-master{,.min}.js{,.map} copies haven't been referenced
since that switch and just added noise to the repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A-Frame already attaches THREE.BufferGeometryUtils (see
aframe/src/lib/three.js), so the r103-era trimmed copy in
vendor/BufferGeometryUtils.js is redundant. It also broke under
webpack 5 because it referenced THREE as an implicit global.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Audited vendor/Curve.js and vendor/CatmullRomCurve3.js against modern
three.js: the CatmullRomCurve3 fixes (intPoint modulo, this.points
reset in fromJSON) are upstream already, and the Curve.js
optional-target GC patches for getPoints/getSpacedPoints/getLengths
are never exercised on a hot path in supercurve.js — the per-frame
getPointAt/getTangentAt calls already go through the upstream
optional-target overloads on getPoint/getTangent.

Use upstream THREE.Curve / THREE.CatmullRomCurve3 directly via the
THREE global A-Frame exposes and drop the two require() lines at the
top of supercurve.js.

Add `externals: { three: 'THREE' }` to webpack.config.js so the one
`import ... from 'three'` in src/lib/FontLoader.js resolves to
window.THREE instead of bundling a second copy of three.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
THREE.Math was removed from three.js in r125 — the helpers live on
THREE.MathUtils now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- panel-shader.js, supercurve-shader.js: A-Frame aframevr/aframe#5310
  removed updateVariables and the attributes array from the Shader
  base class. Delegate the uniform-update work to the inherited base
  update via the prototype chain so our registerPanel/registerCurve
  side effect still runs.
- trail.js: THREE.VertexColors was removed in three.js r125 — the
  material property is now a plain boolean.
- scene.html: set renderer="colorManagement: false" so colors and
  textures keep the pre-1.3.0 behavior our custom GLSL was authored
  against. Without this A-Frame enables THREE.ColorManagement and
  switches outputColorSpace to sRGB, which throws off every shader
  that mixes raw color uniforms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The *BufferGeometry aliases were removed from three.js:
- tail.js: THREE.PlaneBufferGeometry → THREE.PlaneGeometry
- wall.js: THREE.BoxBufferGeometry  → THREE.BoxGeometry

aframe-slice9-component still references THREE.PlaneBufferGeometry
internally, so alias it back to THREE.PlaneGeometry in src/index.js
before the require.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
beatsaver now serves cover images with permissive CORS headers, so
the beatproxy rewrite is dead weight. Use the raw coverURL from
versions[0] directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The supermedium-sync in 8456119 dropped two pieces that are required
for keyboards that ship a hover sprite (our `superkeyboard` model
does — `keyboard-hover.png`):

1. Setting `src: hoverImg` on the color plane material (normal
   blending, sampled texture) instead of the additive-solid-color
   glow.
2. UV manipulation so the per-key plane samples only that key's
   region of the sprite. Since modern three.js replaced
   `geometry.faceVertexUvs` with BufferGeometry, switch to
   `texture.offset` + `texture.repeat` which gives the same result.

Without this, the hover plane was drawn as a flat white (or
`keyHoverColor`) rectangle on top of each key — an opaque white
square with the letter erased behind it.

The additive-glow path is preserved for keyboards without a hover
sprite, and setAttribute declares `blending: 'additive'` directly
instead of relying on a componentinitialized listener (which now
fires synchronously and was being attached too late).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vincentfretin and others added 2 commits April 19, 2026 11:25
- package.json: vendor/ was removed from the repo, so drop it from
  the predeploy cp list.
- webpack.config.js: set publicPath to 'auto'. On moonrider.xyz it
  still resolves to /build/ (same as the old '/build'), and it also
  lets the build work when served from a subpath (e.g. a GitHub
  Pages fork at /moonrider/).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop the "Get Updates on Our Upcoming VR Projects!" email capture
form — the endpoint (supermedium.com/mail/subscribe) and associated
tracking no longer make sense here. Keep a small Discord link in the
bottom-left corner so the community pointer is still visible.

Removes: the #subscribeForm block in index.html, the associated CSS
(#subscribeForm, inputs, buttons), and initSubscribeForm() plus its
DOMContentLoaded listener in src/index.js.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vincentfretin vincentfretin changed the title Update to webpack 5 to work on latest Node.js 22 Update to webpack 5 / Node.js 22 / A-Frame master / three r184 Apr 19, 2026
vincentfretin and others added 7 commits April 19, 2026 11:47
The absolute /assets/img/banner.jpg path only resolves correctly
when the site is served from the root of a domain. A relative path
works regardless of the base path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously paused or naturally-ended buffer sources stayed connected to
the gain node: stopAudio skipped source.stop() when isAudioPlaying was
false, onRestart only disconnected the old source, and the victory
branch overwrote this.source without cleaning up the just-ended game
source. Sources could accumulate across BEGIN cycles, and the lingering
onended on a restarted source could fire songcomplete during the next
song.

- stopAudio: always call source.stop() (wrapped in try/catch for
  already-ended buffers) so paused/ended sources get fully torn down.
- onRestart: call stopAudio() for full teardown; drop the duplicate
  audioanalyserbuffersource listener and dead songloadfinish emit.
  Explicit refreshSource() stays — audioanalyser's update is a no-op
  when src hasn't changed, so the bind path produces no new source.
- Victory branch: call stopAudio() before swapping in the
  background-music source.
- JSDoc: songloadfinish is no longer emitted; document the actual
  songprocessfinish / beatloaderpreloadfinish events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The worker-side abort logic was removed in 787f82d ('use old format
beatsaver from cache') but the sender in zip-loader.js stayed. The
unhonored abort message has been triggering a redundant zip fetch on
every song switch and again whenever ziploaderend clears the version
(the directDownload field carries the *new* selection, so the worker
just re-downloaded the same zip).

Drop the abort branch in zip-loader.update, the abort flag in
fetchZip, and the now-unused abort field. One fetch per selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pressing BEGIN on the same song after exit-to-menu (which leaves
state.challenge.audio holding the previous valid blob URL) would hang
on the loading screen. setAttribute on the audioanalyser was a no-op
because the merged data matched current data, so audioanalyser.update
never ran, refreshSource was never called, no audioanalyserbuffersource
event fired, and getAudio's Promise never resolved.

Detect that case (current audioanalyser src matches the requested
audio) and call refreshSource directly, mirroring what onRestart
already does for restart-of-the-same-song.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ziploaderend clears menuSelectedChallenge.version when the zip finishes
loading, and gamemenuexit then restores menuSelectedChallenge.id
without restoring version. song-preview sees the id flip to non-empty
and assembles 'https://cdn.beatsaver.com/' + '' + '.mp3', firing a
404 fetch and an "Uncaught (in promise) NotSupportedError" each time
the user exits to menu.

Bail out of the play branch when selectedChallengeVersion is empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues conspired to leak the menu music into the first game:

- The pause branch in update sat after the isSearching early-return,
  so a state update that flipped both isPlaying false and isSearching
  true in the same tick would skip the pause.
- A-Frame's lifecycle play() unconditionally called fadeInAudio, so
  scene-play/resume started the intro audio regardless of the bound
  state (and at boot, before update's play branch ever wires the
  audio through the audioanalyser, the element played straight to
  native browser output).
- audio.play() returns a Promise; a pause() that lands before the
  Promise settles can lose to a still-decoding menu.ogg, leaving the
  element playing after the user has already moved on to a song.

Fixes:

- Run the pause branch before the isSearching early-return.
- Gate lifecycle play() on this.data.isPlaying && !this.data.isSearching.
- Chain off the play promise in fadeInAudio and re-check the bound
  state when it settles, pausing if it flipped while play was pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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