docs(v4): withExtraConfig is extends, plus the registry name bug it uncovered - #823
Conversation
extends, plus the registry name bug it uncovered
Code ReviewRisk: Low — The registry now uses merged component configuration for names and lazy name-mismatch diagnostics; the change is safe to merge. Documents the v4 replacement for Review usage: 87,966 in (77,069 cached) / 1,239 out tokens — $0.0144 (openrouter/openai/gpt-5.6-luna, thinking: low) Reviewed by @weareikko/code-review v0.9.5 for commit 66f3e16. |
Export sizeBundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped. @studiometa/js-toolkit-v4
Unchanged (385)@studiometa/js-toolkit
@studiometa/js-toolkit-v4
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #823 +/- ##
=======================================
Coverage 97.16% 97.16%
=======================================
Files 170 170
Lines 4133 4133
Branches 1152 1151 -1
=======================================
Hits 4016 4016
Misses 106 106
Partials 11 11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will regress 1 benchmark
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | all transforms |
299.3 µs | 333 µs | -10.13% |
| ⚡ | create tween with smooth mode |
181.8 µs | 102.9 µs | +76.71% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing feature/v4-extra-config (8984b60) with main (bdea82a)2
Footnotes
-
141 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
-
No successful run was found on
main(8984b60) during the generation of this report, so bdea82a was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
`registerComponent()` read the class's own static `config.name`, while the instance mounts under the merged one — the name `Base` publishes on `__base__`, and the name `resolveConfig()` already gives the family walk, the mount strategy and `@on(Class, type)`. A subclass extending a component with extra config and no rename therefore registered under `undefined`: no mount, no collision warning, and an `undefined` key left in the registry map. It now collides with the name it inherited, which is the loud first-wins path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
v3's `withExtraConfig()` existed because v3 read a class's own static `config`. v4 merges config along the prototype chain, so extending a component with different config is `extends` plus a `static config`, and `@component()` already takes that config object. Adds the spec proving it against the three `@studiometa/ui` call sites, and records the two v3 behaviours v4 refuses: the auto-rename on collision, and the deep merge of the config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
66f3e16 to
8984b60
Compare
Verdict: no new API.
withExtraConfigisextendsin v4.v3's
withExtraConfig(Class, config, deepmergeOptions)existed for one reason: v3 read a class's own staticconfig, so a subclass could not add a single option without restating everything its parent declared. v4 mergesconfigalong the prototype chain (resolveConfig(), #627), which removes the reason. The operation is a class declaration, and@component()already takes the config object — there is nothing to extend on it, and no mixin to add.This PR is therefore one bug fix, one spec and one documentation section. No new public symbol, so
index.ts,exports.spec.tsand the generated subpaths are untouched.The three
@studiometa/uicall sitesAll three live in
packages/ui-mapbox, all three extendAbstractMapboxControl, and all three overridecreateControl()— so each needs a class body regardless of how the config gets there.class MapboxNavigationControl extends withExtraConfig(AbstractMapboxControl, { name: 'MapboxNavigationControl', options: { showCompass: Boolean, showZoom: Boolean } }) { … }class MapboxNavigationControl extends AbstractMapboxControl { static config = { name: 'MapboxNavigationControl', options: { showCompass: Boolean, showZoom: Boolean } }; … }class MapboxGeolocateControl extends withExtraConfig(AbstractMapboxControl, { name: …, options: { positionOptions: Object, … } }) { … }static config.optionsclass MapboxFullscreenControl extends withExtraConfig(AbstractMapboxControl, { name: 'MapboxFullscreenControl' }) { … }class MapboxFullscreenControl extends AbstractMapboxControl { static config = { name: 'MapboxFullscreenControl' }; … }In every case the base's
positionoption keeps its'top-right'default, the base's refs andconfig.componentscome along, andAbstractMapboxControl.configis left untouched.src/config-extension.spec.tsruns each of those three shapes in Chromium, plus@component()on a subclass and the expression-position form for a class you cannot edit:The bug this turned up
Writing the collision case failed, and not for the reason expected:
registerComponent()readComponentClass.config.name— the class's own static — while every other consumer of a name reads the merged one (Base's__base__key and$id, the family walk, the mount strategy,@on(Class, type)). A subclass that extends a component with extra config and forgets to rename therefore registered underundefined: no mount, no collision warning, and anundefinedkey left in the registry map. Silent, and aimed squarely at v4's plain-JS audience, who get no type error for the missingname. It now collides with the inherited name and takes the loud first-wins path. Same fix applied to the lazy name-mismatch diagnostic.What I rejected
withExtraConfigclone. Nothing to wrap. The three call sites need a class body anyway, and a helper returning an anonymous subclass costs a name in the registry and a frame in the prototype chain for zero saved lines.createServiceMixin()is the shape v4 uses when there is state to attach; there is none here.@componentaccepting a class to extend. It would be a second way to spellextends, decorator-only, and v4 promises a non-decorator equivalent for everything.utils/deepmerge.tsnow, but config is not where it goes.Basemerges config one level on purpose, and an option definition is a unit: restatingthemerestates its type and its default, which is what "this option is different here" means. Deep-merging would also have to reach intodefaultfactory functions, which it can only treat as opaque values. v3's caller-supplieddeepmergeOptionshas no v4 equivalent because the merge it tuned is gone. A spec pins the replace-whole semantics.<Name>WithExtraConfig— a token nobody writes in HTML.nameis required byBaseConfig, the registry is first-wins-and-warn (DESIGN §11f item 3), and all three ui call sites already name their extension, so the branch was dead there.Verification
npm run lint,npm run lint:types,npm run test:v4(982 tests, 73 files) andnpm run check:packageall green, and each commit is green on its own.🤖 Generated with Claude Code
https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9