fix(scripts): restore class-name lookup for nameless registered scripts#8840
fix(scripts): restore class-name lookup for nameless registered scripts#8840mvaligursky wants to merge 2 commits into
Conversation
#8831 (2.19.3) changed the name a script is registered under when no explicit name is given - from the verbatim class name (e.g. `PlayerController`) to its lowerCamelCase form (`playerController`) - to match the ESM asset loader convention. This silently broke projects that register ES6 classes via `registerScript(Class)` / `app.scripts.add(Class)` and reference them by the original class name: the entity's lookup no longer matched the registry key, so the script never attached. Register such scripts under a back-compat alias (the verbatim class name) in addition to the canonical lowerCamelCase name, so lookups by either name resolve. The alias is added only when the registry name was derived from the class name (i.e. `toLowerCamelCase(className) === name`), differs from it, is not reserved, and is not already taken - so explicitly named scripts are unaffected and existing registrations are never clobbered. `remove()` and hot-swap keep the alias in sync, and components awaiting either name are instantiated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request restores pre-2.19.3 behavior for manually registered (nameless) ES6 script classes by adding a backward-compatible alias in the script registry, allowing lookups by both the derived lowerCamelCase name and the original verbatim class name.
Changes:
- Add a class-name alias in
ScriptRegistry.add()when the canonical registry name was derived from the class name. - Update awaiting-script instantiation to handle both the canonical name and the alias.
- Extend script-registry tests to cover alias registration, non-aliasing for explicit names, non-overwrite behavior, removal, and entity attachment via class-name references.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/framework/script/script-registry.js |
Adds class-name aliasing for nameless registrations and updates awaiting-instance creation/removal behavior. |
test/framework/script/script-registry.test.mjs |
Adds regression tests validating alias behavior and end-to-end entity attachment via class-name lookups. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // keep the class-name alias pointing at the swapped-in script | ||
| const oldAlias = getScriptName(old); | ||
| if (oldAlias && this._scripts.get(oldAlias) === old) { | ||
| this._scripts.set(oldAlias, script); | ||
| } | ||
|
|
||
| this.fire('swap', scriptName, script); | ||
| this.fire(`swap:${scriptName}`, script); |
There was a problem hiding this comment.
Good catch — fixed in 9eaace8. The swap path now also fires swap:${oldAlias} when the old script had a class-name alias, so scripts attached via the alias hot-swap correctly. Added a regression test for this.
| // delete the canonical registry name and any class-name alias that points at this script | ||
| const canonicalName = scriptType.__name; | ||
| this._scripts.delete(canonicalName); | ||
|
|
||
| const className = getScriptName(scriptType); | ||
| if (className && this._scripts.get(className) === scriptType) { | ||
| this._scripts.delete(className); | ||
| } | ||
|
|
||
| const ind = this._list.indexOf(scriptType); | ||
| this._list.splice(ind, 1); | ||
|
|
||
| this.fire('remove', scriptName, scriptType); | ||
| this.fire(`remove:${scriptName}`, scriptType); | ||
| this.fire('remove', canonicalName, scriptType); | ||
| this.fire(`remove:${canonicalName}`, scriptType); |
There was a problem hiding this comment.
Fixed in 9eaace8 — remove() now fires remove:${className} when an alias was present, mirroring the add:${aliasName} event.
| let attributes; | ||
| const scriptInstances = []; | ||
| const scriptInstancesInitialized = []; | ||
|
|
||
| for (components.loopIndex = 0; components.loopIndex < components.length; components.loopIndex++) { |
There was a problem hiding this comment.
Fixed in 9eaace8 — attributes is now resolved per component inside the loop (component._scriptsData?.[scriptName]?.attributes), so a component without script data no longer inherits a previous iteration's attributes. (This was a pre-existing issue in the code I extracted into the helper; good to fix it here.)
| it('attaches a nameless script to an entity referenced by its class name', function () { | ||
| // mirrors the pre-2.19.3 pattern: register an ES6 class with no explicit name, then | ||
| // reference it on an entity by its (PascalCase) class name | ||
| class PlayerController extends Script {} | ||
| registerScript(PlayerController, undefined, app); | ||
|
|
||
| const e = new Entity(); | ||
| app.root.addChild(e); | ||
| e.addComponent('script'); | ||
|
|
||
| const instance = e.script.create('PlayerController'); | ||
| expect(instance).to.be.an.instanceof(PlayerController); | ||
| expect(e.script.PlayerController).to.equal(instance); | ||
| }); |
There was a problem hiding this comment.
Added in 9eaace8 — a test that re-registers a nameless script (with a swap method) and asserts the swap:<ClassName> alias event fires and the alias now resolves to the new class.
- Fire `swap:<alias>` so scripts attached via the class-name alias (which subscribe to `swap:<ClassName>`) hot-swap correctly. - Fire `remove:<alias>` for symmetry with the `add:<alias>` event. - Reset `attributes` per component in `_createAwaitingInstances` so a component without script data can't inherit a previous one's attributes. - Add a regression test for swapping a script attached via its class-name alias. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Closing in favor of an alternative, simpler approach: restore the verbatim class name as the default registration name for nameless |
Summary
Since 2.19.3, projects that register ES6 script classes without an explicit name and reference them by the class name stopped working — the scripts silently fail to attach. Reported by a user with "hundreds of scripts": "All my scripts stopped working (got not registered)… extending
pc.ScriptTypeand registering them… only adding the second argument topc.registerScriptwith the script name… fixed the problem."Root cause
#8831 changed the name a script is registered under when no explicit name is passed to
registerScript/createScript/app.scripts.add:That matches the ESM asset-loader convention, but it changed the long-standing manual-registration behavior.
ScriptRegistry(aMapsince #8835) is keyed solely by the new lowerCamelCase name with no alias, so an entity that references the script by its originalPlayerControllername no longer resolves it → the script never attaches. Passing an explicit name (registerScript(Class, 'PlayerController')) works because the explicit name wins — but that's painful across hundreds of scripts.Fix (option 1: back-compat alias)
In
ScriptRegistry.add, when the registry name was derived from the class name, also expose the script under its verbatim class name:__namestays the lowerCamelCase form (no change to the ESM/asset path).toLowerCamelCase(className) === name— so explicitly named scripts get no alias, andcreateScript('foo')is unaffected.remove()clears both names; hot-swap keeps the alias pointing at the swapped-in class; components awaiting either name are instantiated.This restores pre-2.19.3 lookups while keeping the lowerCamelCase canonical name.
Tests
Added to
script-registry.test.mjs(105 passing):playerControllerandPlayerController;remove()clears both names;Fixes the 2.19.3 script-registration regression reported on the forum.
🤖 Generated with Claude Code