Skip to content

docs(v4): withExtraConfig is extends, plus the registry name bug it uncovered - #823

Merged
titouanmathis merged 2 commits into
mainfrom
feature/v4-extra-config
Aug 16, 2026
Merged

titouanmathis merged 2 commits into
mainfrom
feature/v4-extra-config

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

Verdict: no new API. withExtraConfig is extends in v4.

v3's withExtraConfig(Class, config, deepmergeOptions) existed for one reason: v3 read a class's own static config, so a subclass could not add a single option without restating everything its parent declared. v4 merges config along 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.ts and the generated subpaths are untouched.

The three @studiometa/ui call sites

All three live in packages/ui-mapbox, all three extend AbstractMapboxControl, and all three override createControl() — so each needs a class body regardless of how the config gets there.

v3 v4
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, … } }) { … } same shape — the six added options sit in static config.options
class MapboxFullscreenControl extends withExtraConfig(AbstractMapboxControl, { name: 'MapboxFullscreenControl' }) { … } class MapboxFullscreenControl extends AbstractMapboxControl { static config = { name: 'MapboxFullscreenControl' }; … }

In every case the base's position option keeps its 'top-right' default, the base's refs and config.components come along, and AbstractMapboxControl.config is left untouched. src/config-extension.spec.ts runs each of those three shapes in Chromium, plus @component() on a subclass and the expression-position form for a class you cannot edit:

registerComponent(class extends Vendor {
  static config = { name: 'CompactVendor', options: { compact: Boolean } };
});

The bug this turned up

Writing the collision case failed, and not for the reason expected: registerComponent() read ComponentClass.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 under undefined: no mount, no collision warning, and an undefined key left in the registry map. Silent, and aimed squarely at v4's plain-JS audience, who get no type error for the missing name. 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

  • A withExtraConfig clone. 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.
  • A mixin. A mixin exists to add behaviour a subclass cannot inherit. Config is inherited. createServiceMixin() is the shape v4 uses when there is state to attach; there is none here.
  • @component accepting a class to extend. It would be a second way to spell extends, decorator-only, and v4 promises a non-decorator equivalent for everything.
  • Deep-merging config. Core ships utils/deepmerge.ts now, but config is not where it goes. Base merges config one level on purpose, and an option definition is a unit: restating theme restates its type and its default, which is what "this option is different here" means. Deep-merging would also have to reach into default factory functions, which it can only treat as opaque values. v3's caller-supplied deepmergeOptions has no v4 equivalent because the merge it tuned is gone. A spec pins the replace-whole semantics.
  • v3's auto-rename to <Name>WithExtraConfig — a token nobody writes in HTML. name is required by BaseConfig, 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) and npm run check:package all green, and each commit is green on its own.

🤖 Generated with Claude Code

https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9

@titouanmathis titouanmathis changed the title feat(v4): answer withExtraConfig with plain subclassing docs(v4): withExtraConfig is extends, plus the registry name bug it uncovered Aug 16, 2026
@github-actions

Copy link
Copy Markdown

Code Review

Risk: 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 withExtraConfig using normal subclassing with static configuration, and adds coverage for inherited config, collisions, decorators, and non-deep option merging. Updates registry name resolution to consistently use resolveConfig() before registration and lazy-component validation.


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.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Export size

Bundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped.

@studiometa/js-toolkit-v4

Export Size (gzip) Diff
registerManifest 10.56 kB +1 B (+0.0%)
component 10.55 kB -1 B (-0.0%)
registerComponents 10.48 kB -2 B (-0.0%)
Unchanged (385)

@studiometa/js-toolkit

Export Size (gzip) Diff
(barrel) 17.44 kB
AbstractService 598 B
Base 9.06 kB
ComponentLoader 2.31 kB
DEFAULT_DIAGNOSTIC_PREFIX 102 B
DragService 2.02 kB
IDLE_TIMEOUT 57 B
KeyService 935 B
LoadService 666 B
MutationService 849 B
PointerService 1.13 kB
RafService 1020 B
ResizeService 1.12 kB
ScrollService 1.36 kB
VISIBLE_ROOT_MARGIN 72 B
autoload 2.4 kB
closestComponent 419 B
composeManifests 119 B
createApp 996 B
defineFeatures 326 B
defineManifest 512 B
fromMetaGlob 228 B
fromWebpackContext 131 B
getClosestParent 197 B
getDirectChildren 202 B
getInstanceFromElement 125 B
getInstances 187 B
getScopedGroups 104 B
importOnInteraction 926 B
importOnMediaQuery 243 B
importWhenIdle 225 B
importWhenPrefersMotion 271 B
importWhenVisible 935 B
isDirectChild 218 B
logTree 551 B
queryComponent 594 B
queryComponentAll 601 B
readEagerTokens 201 B
registerComponent 305 B
registerComponents 356 B
registerManifest 2.87 kB
registerManifests 2.89 kB
useDrag 2.05 kB
useKey 943 B
useLoad 676 B
useMutation 876 B
usePointer 1.15 kB
useRaf 1 kB
useResize 1.13 kB
useScroll 1.36 kB
utils 10.05 kB
utils/Queue 269 B
utils/SmartQueue 440 B
utils/addClass 240 B
utils/addStyle 239 B
utils/animate 3.34 kB
utils/boundingRectToCircle 206 B
utils/cache 208 B
utils/camelCase 405 B
utils/clamp 98 B
utils/clamp01 114 B
utils/collideCircleCircle 129 B
utils/collideCircleRect 192 B
utils/collidePointCircle 128 B
utils/collidePointRect 122 B
utils/collideRectRect 128 B
utils/createEaseInOut 123 B
utils/createEaseOut 91 B
utils/createElement 635 B
utils/createLocalStorage 1.32 kB
utils/createLocalStorageProvider 296 B
utils/createMemoryStorageProvider 174 B
utils/createNoopProvider 128 B
utils/createRange 115 B
utils/createSessionStorage 1.32 kB
utils/createSessionStorageProvider 288 B
utils/createStorage 1.3 kB
utils/createUrlSearchParamsInHashProvider 461 B
utils/createUrlSearchParamsInHashStorage 1.35 kB
utils/createUrlSearchParamsProvider 429 B
utils/createUrlSearchParamsStorage 1.34 kB
utils/damp 106 B
utils/dashCase 404 B
utils/debounce 122 B
utils/domScheduler 310 B
utils/ease 519 B
utils/easeInCirc 285 B
utils/easeInCubic 287 B
utils/easeInExpo 286 B
utils/easeInOutCirc 288 B
utils/easeInOutCubic 289 B
utils/easeInOutExpo 288 B
utils/easeInOutQuad 288 B
utils/easeInOutQuart 289 B
utils/easeInOutQuint 289 B
utils/easeInOutSine 288 B
utils/easeInQuad 285 B
utils/easeInQuart 286 B
utils/easeInQuint 286 B
utils/easeInSine 285 B
utils/easeLinear 77 B
utils/easeOutCirc 286 B
utils/easeOutCubic 288 B
utils/easeOutExpo 286 B
utils/easeOutQuad 286 B
utils/easeOutQuart 286 B
utils/easeOutQuint 286 B
utils/easeOutSine 286 B
utils/endsWith 128 B
utils/fold 168 B
utils/getAncestorWhere 123 B
utils/getAncestorWhereUntil 148 B
utils/getComponentResolver 140 B
utils/getOffsetSizes 194 B
utils/hasWindow 88 B
utils/historyPush 524 B
utils/historyReplace 526 B
utils/inertiaFinalValue 169 B
utils/isArray 63 B
utils/isBoolean 78 B
utils/isDefined 75 B
utils/isDev 78 B
utils/isEmpty 206 B
utils/isEmptyString 108 B
utils/isFunction 79 B
utils/isNull 68 B
utils/isNumber 91 B
utils/isObject 108 B
utils/isString 77 B
utils/keyCodes 122 B
utils/lerp 84 B
utils/loadElement 220 B
utils/loadIframe 241 B
utils/loadImage 241 B
utils/loadLink 237 B
utils/loadScript 251 B
utils/localStorageProvider 839 B
utils/lowerCase 404 B
utils/map 93 B
utils/matrix 136 B
utils/mean 126 B
utils/memo 130 B
utils/memoize 228 B
utils/memoryStorageProvider 843 B
utils/nextFrame 179 B
utils/nextMicrotask 133 B
utils/nextTick 148 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 322 B
utils/pascalCase 407 B
utils/random 93 B
utils/randomInt 113 B
utils/randomItem 234 B
utils/removeClass 242 B
utils/removeStyle 243 B
utils/round 95 B
utils/saveActiveElement 92 B
utils/scrollTo 2.31 kB
utils/sessionStorageProvider 838 B
utils/smoothTo 476 B
utils/snakeCase 406 B
utils/spring 154 B
utils/startsWith 125 B
utils/throttle 125 B
utils/toggleClass 242 B
utils/transform 347 B
utils/transition 1010 B
utils/trapFocus 441 B
utils/tween 1.72 kB
utils/untrapFocus 120 B
utils/upperCase 404 B
utils/urlSearchParamsInHashProvider 845 B
utils/urlSearchParamsProvider 839 B
utils/useScheduler 309 B
utils/wait 103 B
utils/withLeadingCharacters 135 B
utils/withLeadingSlash 142 B
utils/withTrailingCharacters 135 B
utils/withTrailingSlash 142 B
utils/withoutLeadingCharacters 122 B
utils/withoutLeadingCharactersRecursive 165 B
utils/withoutLeadingSlash 133 B
utils/withoutTrailingCharacters 122 B
utils/withoutTrailingCharactersRecursive 165 B
utils/withoutTrailingSlash 133 B
utils/wrap 122 B
version 56 B
withBreakpointManager 1.54 kB
withBreakpointObserver 1.71 kB
withDrag 2.18 kB
withExtraConfig 163 B
withFreezedOptions 187 B
withGroup 455 B
withIntersectionObserver 303 B
withMountOnMediaQuery 393 B
withMountWhenInView 347 B
withMountWhenPrefersMotion 431 B
withMutation 1010 B
withName 109 B
withRelativePointer 1.29 kB
withResponsiveOptions 2.4 kB
withScrolledInView 3.05 kB

@studiometa/js-toolkit-v4

Export Size (gzip) Diff
(barrel) 20.51 kB
BREAKPOINTS 776 B
Base 8.09 kB
DIAGNOSTICS 629 B
DRAG_MODES 162 B
EVENTS 155 B
MOUNT_ATTRIBUTE 69 B
SWAP_MODES 129 B
children 243 B
createContext 472 B
createFallbackProvider 1.34 kB
createGroup 1.06 kB
createLocalStorage 2.34 kB
createLocalStorageProvider 1.23 kB
createMemoryStorageProvider 1.23 kB
createService 630 B
createServiceMixin 509 B
createSessionStorage 2.34 kB
createSessionStorageProvider 1.23 kB
createStorage 2.32 kB
createUrlSearchParamsInHashProvider 1.23 kB
createUrlSearchParamsInHashStorage 2.36 kB
createUrlSearchParamsProvider 1.23 kB
createUrlSearchParamsStorage 2.36 kB
defaultScheduler 1.5 kB
defineManifest 983 B
domUpdate 1.23 kB
emitExtendable 1.09 kB
fromMetaGlob 203 B
fromWebpackContext 131 B
getBreakpoints 776 B
getInstances 2.77 kB
inject 175 B
injectContext 675 B
injectContextSync 634 B
jsonSerializer 95 B
localStorageProvider 1.22 kB
memoryStorageProvider 1.23 kB
nextFrame 115 B
on 8.42 kB
perTarget 176 B
provide 178 B
provideContext 704 B
provideRootContext 748 B
read 127 B
registerComponent 10.47 kB
sessionStorageProvider 1.22 kB
setBreakpoints 805 B
signal 920 B
subscribeContext 1.45 kB
swap 2.63 kB
toggle 176 B
until 172 B
urlSearchParamsInHashProvider 1.22 kB
urlSearchParamsProvider 1.22 kB
useBreakpoint 1.44 kB
useDrag 3.22 kB
useInView 1.31 kB
useMediaQuery 1.05 kB
useMutation 1.29 kB
usePointer 1.72 kB
usePrefersReducedMotion 1.08 kB
useRaf 1.93 kB
useResize 1.3 kB
useScroll 2.54 kB
useScrollProgress 3.51 kB
useWindowScroll 2.52 kB
useWindowSize 1.3 kB
utils 8.65 kB
utils/DEFAULT_DAMP_FACTOR 109 B
utils/INERTIA_FRAME 97 B
utils/MAX_SPRING_RATIO 100 B
utils/SCROLL_AXES 100 B
utils/TRANSFORM_PROPS 137 B
utils/TRANSITION_OPTIONS 132 B
utils/camelCase 449 B
utils/capitalize 119 B
utils/clamp 133 B
utils/clamp01 149 B
utils/clampDampFactor 157 B
utils/createEaseInOut 120 B
utils/createEaseOut 91 B
utils/createElement 638 B
utils/createRange 205 B
utils/damp 211 B
utils/debounce 121 B
utils/decayOver 162 B
utils/deepmerge 312 B
utils/easeInCirc 94 B
utils/easeInCubic 81 B
utils/easeInExpo 97 B
utils/easeInOutCirc 150 B
utils/easeInOutCubic 141 B
utils/easeInOutExpo 150 B
utils/easeInOutQuad 139 B
utils/easeInOutQuart 140 B
utils/easeInOutQuint 140 B
utils/easeInOutSine 156 B
utils/easeInQuad 80 B
utils/easeInQuart 81 B
utils/easeInQuint 81 B
utils/easeInSine 104 B
utils/easeLinear 77 B
utils/easeOutCirc 121 B
utils/easeOutCubic 111 B
utils/easeOutExpo 124 B
utils/easeOutQuad 110 B
utils/easeOutQuart 112 B
utils/easeOutQuint 111 B
utils/easeOutSine 132 B
utils/enterTransition 679 B
utils/fold 200 B
utils/getOffsetSizes 268 B
utils/historyPush 380 B
utils/historyReplace 381 B
utils/inertiaDecay 199 B
utils/inertiaFinalValue 187 B
utils/inertiaStep 232 B
utils/inertiaTimeConstant 178 B
utils/isBoolean 90 B
utils/isDefined 87 B
utils/isFunction 86 B
utils/isNull 78 B
utils/isNumber 103 B
utils/isObject 115 B
utils/isString 89 B
utils/kebabCase 421 B
utils/leaveTransition 679 B
utils/lerp 120 B
utils/loadImage 245 B
utils/loadLink 776 B
utils/loadScript 697 B
utils/lowerCase 84 B
utils/map 128 B
utils/matrix 150 B
utils/mean 147 B
utils/memo 217 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 253 B
utils/pascalCase 434 B
utils/random 93 B
utils/randomInt 132 B
utils/randomItem 163 B
utils/round 130 B
utils/saveActiveElement 571 B
utils/scrollTo 1.72 kB
utils/selectorFor 2.72 kB
utils/setClassesOrStyles 218 B
utils/smoothTo 2.56 kB
utils/snakeCase 421 B
utils/spring 343 B
utils/throttle 151 B
utils/transform 286 B
utils/transition 577 B
utils/trapFocus 717 B
utils/untrapFocus 587 B
utils/upperCase 84 B
utils/wait 103 B
utils/withLeadingCharacters 142 B
utils/withLeadingSlash 152 B
utils/withTrailingCharacters 143 B
utils/withTrailingSlash 153 B
utils/withoutLeadingCharacters 127 B
utils/withoutLeadingCharactersRecursive 144 B
utils/withoutLeadingSlash 138 B
utils/withoutTrailingCharacters 129 B
utils/withoutTrailingCharactersRecursive 147 B
utils/withoutTrailingSlash 140 B
utils/wrap 154 B
viewTransition 1.66 kB
watchAttributes 1.98 kB
whenDOMSettled 2.31 kB
withDrag 3.61 kB
withInView 1.76 kB
withMutation 1.78 kB
withPointer 2.11 kB
withRaf 2.31 kB
withResize 1.7 kB
withScroll 2.93 kB
withScrollProgress 3.94 kB
write 125 B

@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.16%. Comparing base (555405f) to head (8984b60).
⚠️ Report is 3 commits behind head on main.

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           
Flag Coverage Δ
eslint-plugin-js-toolkit 93.79% <ø> (ø)
js-toolkit 97.92% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed

codspeed Bot commented Aug 16, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 139 untouched benchmarks
⏩ 141 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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

Open in CodSpeed

Footnotes

  1. 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.

  2. 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.

titouanmathis and others added 2 commits August 16, 2026 12:12
`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
@titouanmathis
titouanmathis force-pushed the feature/v4-extra-config branch from 66f3e16 to 8984b60 Compare August 16, 2026 12:12
@titouanmathis
titouanmathis merged commit bdea82a into main Aug 16, 2026
8 of 9 checks passed
@titouanmathis
titouanmathis deleted the feature/v4-extra-config branch August 16, 2026 12:13
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