Skip to content

fix(scripts): restore class-name lookup for nameless registered scripts#8840

Closed
mvaligursky wants to merge 2 commits into
mainfrom
mv-fix-script-classname-alias
Closed

fix(scripts): restore class-name lookup for nameless registered scripts#8840
mvaligursky wants to merge 2 commits into
mainfrom
mv-fix-script-classname-alias

Conversation

@mvaligursky

Copy link
Copy Markdown
Contributor

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.ScriptType and registering them… only adding the second argument to pc.registerScript with 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:

// before (≤ 2.19.2)
name = name || script.__name || ScriptType.__getScriptName(script); // verbatim class name → "PlayerController"
// after (2.19.3, #8831)
name = name || getScriptRegistryName(script);                       // lowerCamelCase → "playerController"

That matches the ESM asset-loader convention, but it changed the long-standing manual-registration behavior. ScriptRegistry (a Map since #8835) is keyed solely by the new lowerCamelCase name with no alias, so an entity that references the script by its original PlayerController name 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:

const className = getScriptName(script);
const aliasName = (className && className !== scriptName &&
    toLowerCamelCase(className) === scriptName &&
    !reservedScriptNames.has(className) && !this._scripts.has(className)) ? className : null;
  • The canonical __name stays the lowerCamelCase form (no change to the ESM/asset path).
  • The alias is added only when toLowerCamelCase(className) === name — so explicitly named scripts get no alias, and createScript('foo') is unaffected.
  • The alias is skipped if reserved or already taken, so it never clobbers an existing registration.
  • 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):

  • nameless script resolves under both playerController and PlayerController;
  • explicitly named scripts get no class-name alias;
  • alias never overwrites an existing registration;
  • remove() clears both names;
  • end-to-end: an entity referencing the script by its class name attaches the instance.

Fixes the 2.19.3 script-registration regression reported on the forum.

🤖 Generated with Claude Code

#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>

Copilot AI 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.

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.

Comment on lines +154 to 161
// 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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +287 to +300
// 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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9eaace8remove() now fires remove:${className} when an alias was present, mirroring the add:${aliasName} event.

Comment thread src/framework/script/script-registry.js Outdated
Comment on lines +208 to +212
let attributes;
const scriptInstances = [];
const scriptInstancesInitialized = [];

for (components.loopIndex = 0; components.loopIndex < components.length; components.loopIndex++) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9eaace8attributes 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.)

Comment on lines +226 to +239
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);
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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>

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@mvaligursky

Copy link
Copy Markdown
Contributor Author

Closing in favor of an alternative, simpler approach: restore the verbatim class name as the default registration name for nameless registerScript(Class) (pre-2.19.3 behavior), instead of maintaining a dual-name alias. The dual-name shim added long-term complexity (and review surfaced edge cases around swap/remove). New PR to follow.

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.

2 participants