diff --git a/CHANGELOG.md b/CHANGELOG.md index 6346bf67c..7037c36ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format ## [Unreleased] +### Changed + +- Warn in development when a component is registered under a name already used by a different component, instead of silently ignoring it — registration stays first-writer-wins, mirroring `customElements.define` ([#736](https://github.com/studiometa/js-toolkit/pull/736)) + ## [v3.6.1](https://github.com/studiometa/js-toolkit/compare/3.6.0..3.6.1) (2026-07-13) ### Fixed diff --git a/packages/js-toolkit/Base/utils.ts b/packages/js-toolkit/Base/utils.ts index 8d69bb0f9..642647568 100644 --- a/packages/js-toolkit/Base/utils.ts +++ b/packages/js-toolkit/Base/utils.ts @@ -1,4 +1,4 @@ -import { isArray, isDefined, SmartQueue, dashCase } from '../utils/index.js'; +import { isArray, isDefined, SmartQueue, dashCase, isDev } from '../utils/index.js'; import type { Base, BaseConfig, BaseConstructor } from './index.js'; import { features } from './features.js'; import { useMutation } from '../services/MutationService.js'; @@ -202,6 +202,14 @@ function mutationCallback() { export function addToRegistry(nameOrSelector: string, ctor: BaseConstructor) { if (registry().has(nameOrSelector)) { + if (isDev && registry().get(nameOrSelector) !== ctor) { + console.warn( + `[${nameOrSelector}] A different component is already registered under this name. ` + + 'Registration is ignored — like `customElements.define`, a name maps to a single ' + + 'component. Give this component a unique `config.name` (or use `withExtraConfig`, ' + + 'which renames automatically).', + ); + } return; } diff --git a/packages/tests/Base/utils.spec.ts b/packages/tests/Base/utils.spec.ts index 5c1384de4..63f95bff8 100644 --- a/packages/tests/Base/utils.spec.ts +++ b/packages/tests/Base/utils.spec.ts @@ -146,6 +146,25 @@ describe('The Base utils', () => { await Promise.all(Array.from(fooInstances).map((i) => i.$destroy())); }); + it('should warn in dev mode when a name is registered with a different component', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const Foo = withName(Base, 'Foo'); + const Bar = withName(Base, 'Bar'); + + addToRegistry('CollisionName', Foo); + // Re-registering the SAME class is idempotent and must not warn. + addToRegistry('CollisionName', Foo); + expect(warn).not.toHaveBeenCalled(); + + // A DIFFERENT class under the same name stays register-once (ignored) but warns. + addToRegistry('CollisionName', Bar); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0][0]).toContain('CollisionName'); + expect(warn.mock.calls[0][0]).toContain('customElements.define'); + + warn.mockRestore(); + }); + it('should register new components', async () => { const TestComponent = withName(Base, 'TestComponent'); const registryKey = '__JS_TOOLKIT_REGISTRY__';