@pyreon/rocketstyle 0.15.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +65 -10
- package/lib/index.js +91 -45
- package/package.json +10 -10
- package/src/__tests__/attrs-overloads.test.ts +97 -0
- package/src/__tests__/cache-key-boolean-collision.test.ts +54 -0
- package/src/__tests__/reactive-props-preservation.test.ts +195 -0
- package/src/__tests__/rocketstyle.browser.test.tsx +275 -0
- package/src/__tests__/rocketstyleIntegration.test.ts +134 -0
- package/src/hoc/rocketstyleAttrsHoc.ts +16 -10
- package/src/index.ts +2 -1
- package/src/rocketstyle.ts +122 -38
- package/src/types/attrs.ts +20 -10
- package/src/types/rocketstyle.ts +68 -19
- package/src/types/styles.ts +1 -1
- package/src/types/utils.ts +45 -2
- package/src/utils/attrs.ts +50 -3
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Rocketstyle } from './init'
|
|
|
4
4
|
import rocketstyle from './init'
|
|
5
5
|
import type { IsRocketComponent } from './isRocketComponent'
|
|
6
6
|
import isRocketComponent from './isRocketComponent'
|
|
7
|
-
import type { AttrsCb } from './types/attrs'
|
|
7
|
+
import type { AttrsCb, AttrsHelpers } from './types/attrs'
|
|
8
8
|
import type {
|
|
9
9
|
ConfigAttrs,
|
|
10
10
|
ConsumerCb,
|
|
@@ -37,6 +37,7 @@ import type { ComponentFn, ElementType, ExtractProps, MergeTypes, TObj } from '.
|
|
|
37
37
|
|
|
38
38
|
export type {
|
|
39
39
|
AttrsCb,
|
|
40
|
+
AttrsHelpers,
|
|
40
41
|
ComponentFn,
|
|
41
42
|
ComposeParam,
|
|
42
43
|
ConfigAttrs,
|
package/src/rocketstyle.ts
CHANGED
|
@@ -9,7 +9,12 @@ import type { Configuration, ExtendedConfiguration } from './types/configuration
|
|
|
9
9
|
import type { RocketComponent } from './types/rocketComponent'
|
|
10
10
|
import type { InnerComponentProps, RocketStyleComponent } from './types/rocketstyle'
|
|
11
11
|
import type { ComponentFn } from './types/utils'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
calculateChainOptions,
|
|
14
|
+
calculateStylingAttrs,
|
|
15
|
+
mergeDescriptors,
|
|
16
|
+
pickStyledAttrs,
|
|
17
|
+
} from './utils/attrs'
|
|
13
18
|
import { chainOptions, chainOrOptions, chainReservedKeyOptions } from './utils/chaining'
|
|
14
19
|
import { calculateHocsFuncs } from './utils/compose'
|
|
15
20
|
import { getDimensionsMap } from './utils/dimensions'
|
|
@@ -38,18 +43,42 @@ type CloneAndEnhance = (
|
|
|
38
43
|
opts: Partial<ExtendedConfiguration>,
|
|
39
44
|
) => ReturnType<typeof rocketComponent>
|
|
40
45
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Clones the current configuration and merges new options, returning a fresh
|
|
48
|
+
* rocketComponent.
|
|
49
|
+
*
|
|
50
|
+
* Component-swap reset: when `opts.component` is set AND differs from the
|
|
51
|
+
* current `defaultOpts.component`, the prior `attrs`, `priorityAttrs`,
|
|
52
|
+
* `filterAttrs`, and `compose` chains are dropped — they were tailored to the
|
|
53
|
+
* previous component's prop shape, and applying them to a different component
|
|
54
|
+
* silently leaks invalid props through to the DOM (e.g. `disabled` on an
|
|
55
|
+
* `<a>`). Callers who want to preserve them must re-chain explicitly:
|
|
56
|
+
*
|
|
57
|
+
* const NewBtn = Button.config({ component: 'a' }).attrs(sharedAttrs)
|
|
58
|
+
*/
|
|
59
|
+
const cloneAndEnhance: CloneAndEnhance = (defaultOpts, opts) => {
|
|
60
|
+
const componentChanged =
|
|
61
|
+
opts.component != null && opts.component !== defaultOpts.component
|
|
62
|
+
|
|
63
|
+
return rocketComponent({
|
|
44
64
|
...defaultOpts,
|
|
45
|
-
attrs:
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
attrs: componentChanged
|
|
66
|
+
? chainOptions(opts.attrs, [])
|
|
67
|
+
: chainOptions(opts.attrs, defaultOpts.attrs),
|
|
68
|
+
filterAttrs: componentChanged
|
|
69
|
+
? [...(opts.filterAttrs ?? [])]
|
|
70
|
+
: [...(defaultOpts.filterAttrs ?? []), ...(opts.filterAttrs ?? [])],
|
|
71
|
+
priorityAttrs: componentChanged
|
|
72
|
+
? chainOptions(opts.priorityAttrs, [])
|
|
73
|
+
: chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
|
|
48
74
|
statics: { ...defaultOpts.statics, ...opts.statics },
|
|
49
|
-
compose:
|
|
75
|
+
compose: componentChanged
|
|
76
|
+
? { ...opts.compose }
|
|
77
|
+
: { ...defaultOpts.compose, ...opts.compose },
|
|
50
78
|
...chainOrOptions(CONFIG_KEYS, opts, defaultOpts),
|
|
51
79
|
...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts),
|
|
52
80
|
} as Parameters<typeof rocketComponent>[0])
|
|
81
|
+
}
|
|
53
82
|
|
|
54
83
|
// --------------------------------------------------------
|
|
55
84
|
// rocketComponent
|
|
@@ -239,23 +268,46 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
239
268
|
// Read reactive inputs (tracks theme + mode signals)
|
|
240
269
|
const theme = themeAttrs.theme
|
|
241
270
|
const mode = themeAttrs.mode
|
|
271
|
+
const propsRec = props as Record<string, unknown>
|
|
272
|
+
|
|
273
|
+
// Resolve active dimensions FIRST so the cache key uses the RESOLVED
|
|
274
|
+
// dimension values, not the raw prop names. Under `useBooleans: true`
|
|
275
|
+
// the user writes `<X primary />` / `<X secondary />` — both map to
|
|
276
|
+
// `state="primary"` / `state="secondary"` after _calculateStylingAttrs
|
|
277
|
+
// resolves the boolean shorthand. Keying off `propsRec[dimName]` would
|
|
278
|
+
// read `undefined` for both (the dimension prop itself was never set)
|
|
279
|
+
// and collide every variant onto the first cached entry. Reading
|
|
280
|
+
// `rocketstateRaw[dimName]` gives the resolved string and partitions
|
|
281
|
+
// them correctly.
|
|
282
|
+
// Resolved from props (not localCtx which has pseudo getters).
|
|
283
|
+
const rocketstateRaw = _calculateStylingAttrs({
|
|
284
|
+
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
285
|
+
dimensions,
|
|
286
|
+
})
|
|
242
287
|
|
|
243
|
-
// Build key: mode |
|
|
288
|
+
// Build key: mode | dimensionValues | pseudoState. Reading dimension
|
|
244
289
|
// props + pseudo signals here tracks them in the surrounding computed
|
|
245
290
|
// so any change re-runs us with a different key.
|
|
246
291
|
let key = mode as string
|
|
247
|
-
const propsRec = props as Record<string, unknown>
|
|
248
292
|
for (const dimName in dimensions) {
|
|
249
|
-
const v =
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
?
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
293
|
+
const v = rocketstateRaw[dimName]
|
|
294
|
+
// Multi-key dimensions (e.g. variant={['primary', 'rounded']}) are
|
|
295
|
+
// arrays. Sort + join so equivalent sets hash identically; without
|
|
296
|
+
// this both `['a','b']` and `['b','a']` would produce different keys.
|
|
297
|
+
if (Array.isArray(v)) {
|
|
298
|
+
key +=
|
|
299
|
+
'|' + (v.length === 0 ? '' : (v as unknown[]).slice().sort().join(','))
|
|
300
|
+
} else {
|
|
301
|
+
// String/number/boolean serialize directly. Anything else (including
|
|
302
|
+
// undefined / objects) gets a typeof tag so we don't collide.
|
|
303
|
+
key +=
|
|
304
|
+
'|' +
|
|
305
|
+
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'
|
|
306
|
+
? String(v)
|
|
307
|
+
: v === undefined
|
|
308
|
+
? ''
|
|
309
|
+
: '~' + typeof v)
|
|
310
|
+
}
|
|
259
311
|
}
|
|
260
312
|
for (const k of ALL_PSEUDO_KEYS) {
|
|
261
313
|
const propV = propsRec[k]
|
|
@@ -307,12 +359,6 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
307
359
|
}
|
|
308
360
|
const themes = dimHelper.get(theme)
|
|
309
361
|
|
|
310
|
-
// Resolve active dimensions from props (not localCtx which has pseudo getters)
|
|
311
|
-
const rocketstateRaw = _calculateStylingAttrs({
|
|
312
|
-
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
313
|
-
dimensions,
|
|
314
|
-
})
|
|
315
|
-
|
|
316
362
|
// Resolve mode-specific theme
|
|
317
363
|
const modeBaseHelper = ThemeManager.modeBaseTheme[mode]
|
|
318
364
|
if (modeBaseHelper.has(baseTheme)) {
|
|
@@ -375,30 +421,68 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
375
421
|
_omitSetCache.set(RESERVED_STYLING_PROPS_KEYS, omitSet)
|
|
376
422
|
}
|
|
377
423
|
|
|
378
|
-
// Merge localCtx + props
|
|
379
|
-
//
|
|
380
|
-
|
|
424
|
+
// Merge localCtx + props via descriptor-copy so reactive getter
|
|
425
|
+
// props on `props` (compiler-emitted `_rp(() => signal())` wrappers
|
|
426
|
+
// converted to getters by `makeReactiveProps`) survive the merge.
|
|
427
|
+
// A plain `{ ...localCtx, ...props }` spread would fire every getter
|
|
428
|
+
// and collapse to static values, defeating reactivity for any
|
|
429
|
+
// downstream JSX accessor reading `props.x`.
|
|
430
|
+
const mergeProps = localCtx ? mergeDescriptors(localCtx, props) : props
|
|
381
431
|
|
|
382
|
-
// omit()
|
|
383
|
-
//
|
|
432
|
+
// omit() preserves descriptors (since ui-core's omit was updated to
|
|
433
|
+
// copy descriptors), so reactive getters carry through to finalProps.
|
|
384
434
|
const finalProps = omit(mergeProps as Record<string, unknown>, omitSet) as Record<string, any>
|
|
385
435
|
|
|
386
436
|
if (options.passProps) {
|
|
387
437
|
const passed = pick(mergeProps, options.passProps)
|
|
388
|
-
|
|
438
|
+
// Copy descriptors so any reactive getters in passProps survive.
|
|
439
|
+
// Plain `finalProps[k] = passed[k]` would fire getters at setup time
|
|
440
|
+
// AND silently fail when finalProps[k] is already a getter-only
|
|
441
|
+
// descriptor (assignment to a getter-only property is a no-op in
|
|
442
|
+
// non-strict mode, throws in strict mode).
|
|
443
|
+
const passedDescriptors = Object.getOwnPropertyDescriptors(passed)
|
|
444
|
+
for (const k of Object.keys(passedDescriptors)) {
|
|
445
|
+
Object.defineProperty(finalProps, k, passedDescriptors[k]!)
|
|
446
|
+
}
|
|
389
447
|
}
|
|
390
448
|
|
|
391
|
-
|
|
449
|
+
// Use defineProperty for these last writes too — if props.ref or
|
|
450
|
+
// an existing finalProps slot happened to carry a getter-only
|
|
451
|
+
// descriptor, plain assignment would silently fail. defineProperty
|
|
452
|
+
// explicitly replaces the descriptor regardless of shape.
|
|
453
|
+
const refDescriptor = Object.getOwnPropertyDescriptor(props, 'ref')
|
|
454
|
+
if (refDescriptor) {
|
|
455
|
+
Object.defineProperty(finalProps, 'ref', refDescriptor)
|
|
456
|
+
}
|
|
392
457
|
// Function accessors — DynamicStyled wraps them in a computed() so
|
|
393
458
|
// mode/dimension changes produce a new CSS class reactively. The
|
|
394
459
|
// computed tracks only these two accessors; the resolve itself runs
|
|
395
460
|
// untracked to prevent exponential cascade from theme deep-reads.
|
|
396
|
-
finalProps
|
|
397
|
-
|
|
461
|
+
Object.defineProperty(finalProps, '$rocketstyle', {
|
|
462
|
+
value: $rocketstyleAccessor,
|
|
463
|
+
writable: true,
|
|
464
|
+
enumerable: true,
|
|
465
|
+
configurable: true,
|
|
466
|
+
})
|
|
467
|
+
Object.defineProperty(finalProps, '$rocketstate', {
|
|
468
|
+
value: $rocketstateAccessor,
|
|
469
|
+
writable: true,
|
|
470
|
+
enumerable: true,
|
|
471
|
+
configurable: true,
|
|
472
|
+
})
|
|
398
473
|
|
|
399
474
|
// development debugging — tree-shaken in production via import.meta.env.DEV
|
|
400
475
|
if (__DEV__) {
|
|
401
|
-
|
|
476
|
+
// defineProperty rather than `=` to be safe against any preserved
|
|
477
|
+
// descriptor in this slot (defense-in-depth — `data-rocketstyle`
|
|
478
|
+
// is unlikely to be passed as a user prop, but the writes above
|
|
479
|
+
// use defineProperty for the same reason).
|
|
480
|
+
Object.defineProperty(finalProps, 'data-rocketstyle', {
|
|
481
|
+
value: componentName,
|
|
482
|
+
writable: true,
|
|
483
|
+
enumerable: true,
|
|
484
|
+
configurable: true,
|
|
485
|
+
})
|
|
402
486
|
|
|
403
487
|
if (options.DEBUG) {
|
|
404
488
|
const debugPayload = {
|
|
@@ -529,8 +613,8 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
529
613
|
{
|
|
530
614
|
render,
|
|
531
615
|
mode,
|
|
532
|
-
isDark: mode === '
|
|
533
|
-
isLight: mode === '
|
|
616
|
+
isDark: mode === 'dark',
|
|
617
|
+
isLight: mode === 'light',
|
|
534
618
|
},
|
|
535
619
|
]),
|
|
536
620
|
})
|
package/src/types/attrs.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import type { render } from '@pyreon/ui-core'
|
|
2
2
|
import type { ThemeModeKeys } from './theme'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
) =>
|
|
4
|
+
/** Helpers object passed as the 3rd arg to every `.attrs(callback)`. */
|
|
5
|
+
export type AttrsHelpers = {
|
|
6
|
+
mode?: ThemeModeKeys
|
|
7
|
+
isDark?: boolean
|
|
8
|
+
isLight?: boolean
|
|
9
|
+
createElement: typeof render
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Callback signature for `.attrs((props, theme, helpers) => …)`.
|
|
14
|
+
*
|
|
15
|
+
* `Partial<A>` on the return is for the strict-typing form when callers
|
|
16
|
+
* pass `AttrsCb<DFP, Theme<T>>` directly. In the rocketstyle `.attrs()`
|
|
17
|
+
* callback overload itself we use a different shape that decouples the
|
|
18
|
+
* props arg (narrow, full DFP) from the return type (loose — only the
|
|
19
|
+
* user's explicit `<P>` generic is checked, with `Record<string,
|
|
20
|
+
* unknown>` allowing runtime extras like `_documentProps`). See
|
|
21
|
+
* `IRocketStyleComponent.attrs` for the call-site shape.
|
|
22
|
+
*/
|
|
23
|
+
export type AttrsCb<A, T> = (props: Partial<A>, theme: T, helpers: AttrsHelpers) => Partial<A>
|
package/src/types/rocketstyle.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
-
import type {
|
|
2
|
+
import type { AttrsHelpers } from './attrs'
|
|
3
3
|
import type { ConfigAttrs } from './config'
|
|
4
4
|
import type { DefaultProps } from './configuration'
|
|
5
5
|
import type {
|
|
@@ -76,7 +76,27 @@ export interface IRocketStyleComponent<
|
|
|
76
76
|
// dimension key props
|
|
77
77
|
DKP extends TDKP = TDKP,
|
|
78
78
|
// calculated final props
|
|
79
|
-
|
|
79
|
+
//
|
|
80
|
+
// `OA extends infer O` distributes over OA's union branches — future-proofs
|
|
81
|
+
// against the overload-aware `ExtractProps` (follow-up PR); for today's
|
|
82
|
+
// single-typed OA the distribution collapses to a single branch.
|
|
83
|
+
//
|
|
84
|
+
// **OA-overlapping EA keys are widened**. When `.attrs({ tag: 'a' })` sets
|
|
85
|
+
// a default for `tag` on a wrapper whose OA has `tag: HTMLTags`, we strip
|
|
86
|
+
// `tag` from OA and re-add it as `Partial<Pick<O, 'tag'>>` — taking O's
|
|
87
|
+
// WIDER type (`HTMLTags`), not EA's narrow literal `'a'`. The runtime
|
|
88
|
+
// default is `'a'`; the JSX call site still accepts any `HTMLTags` value
|
|
89
|
+
// (so `<Btn tag="span" />` is valid). `Partial<Omit<EA, keyof O>>` handles
|
|
90
|
+
// net-new EA-only keys (no OA conflict) and marks them optional too —
|
|
91
|
+
// every `.attrs()` value is semantically a default, never required of the
|
|
92
|
+
// consumer. Mirrors vitus-labs/ui-system PR #225.
|
|
93
|
+
DFP = OA extends infer O
|
|
94
|
+
? Omit<O, keyof EA & keyof O> &
|
|
95
|
+
Partial<Pick<O, keyof EA & keyof O>> &
|
|
96
|
+
MergeTypes<
|
|
97
|
+
[Partial<Omit<EA, keyof O>>, DefaultProps, ExtractDimensionProps<D, DKP, UB>]
|
|
98
|
+
>
|
|
99
|
+
: never,
|
|
80
100
|
> {
|
|
81
101
|
// The component is callable — Pyreon components are plain functions
|
|
82
102
|
(props: DFP): VNodeChild
|
|
@@ -95,23 +115,52 @@ export interface IRocketStyleComponent<
|
|
|
95
115
|
: RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>
|
|
96
116
|
|
|
97
117
|
// ATTRS chaining method
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
//
|
|
119
|
+
// Two overloads. TS resolves in declaration order; the callback form is
|
|
120
|
+
// listed FIRST so a function argument matches it before falling through to
|
|
121
|
+
// the object form. Mirrors vitus-labs/ui-system PR #227 Issue 2 (callback
|
|
122
|
+
// overload split).
|
|
123
|
+
//
|
|
124
|
+
// **Callback overload — asymmetric props/return**: the `props` arg is
|
|
125
|
+
// typed strictly as `Partial<DFP & P>` (so reading `props.x` narrows
|
|
126
|
+
// against the wrapped component's full surface), while the RETURN is
|
|
127
|
+
// `Partial<P> & Record<string, unknown>` — only the user's explicit `<P>`
|
|
128
|
+
// generic is checked, with a wildcard for runtime extras. This preserves
|
|
129
|
+
// Pyreon's documented convention where `.attrs(callback)` returns can
|
|
130
|
+
// carry runtime-only fields (`_documentProps` markers for the document-
|
|
131
|
+
// export pipeline, `tag: 'a'` overrides on Text-based components where
|
|
132
|
+
// the rendered DOM tag is outside the component's narrow `tag` union,
|
|
133
|
+
// etc.). The runtime is loose by design — `.attrs(cb, { filter })`
|
|
134
|
+
// strips keys before forwarding to the DOM. Consumers wanting strict
|
|
135
|
+
// typed extras pass `<P>` with the precise keys they expect.
|
|
136
|
+
//
|
|
137
|
+
// **Object overload — `P & Partial<NoInfer<DFP>>`**: TS infers P from
|
|
138
|
+
// the param's keys. `NoInfer<DFP>` (TS 5.4+) prevents DFP from
|
|
139
|
+
// contributing to P inference. Combined with DFP widening above, this
|
|
140
|
+
// makes `.attrs({ tag: 'a' })` keys optional at the JSX call site —
|
|
141
|
+
// every `.attrs()` value is a default, not a required prop. Mirrors
|
|
142
|
+
// vitus-labs PR #225.
|
|
143
|
+
attrs: {
|
|
144
|
+
<P extends TObj = {}>(
|
|
145
|
+
param: (
|
|
146
|
+
props: Partial<DFP & P>,
|
|
147
|
+
theme: Theme<T>,
|
|
148
|
+
helpers: AttrsHelpers,
|
|
149
|
+
) => Partial<P> & Record<string, unknown>,
|
|
150
|
+
config?: Partial<{
|
|
151
|
+
priority: boolean
|
|
152
|
+
filter: (keyof MergeTypes<[EA, P]>)[]
|
|
153
|
+
}>,
|
|
154
|
+
): RocketStyleComponent<OA, MergeTypes<[EA, P]>, T, CSS, S, HOC, D, UB, DKP>
|
|
155
|
+
|
|
156
|
+
<P extends TObj = {}>(
|
|
157
|
+
param: P & Partial<NoInfer<DFP>>,
|
|
158
|
+
config?: Partial<{
|
|
159
|
+
priority: boolean
|
|
160
|
+
filter: (keyof MergeTypes<[EA, P]>)[]
|
|
161
|
+
}>,
|
|
162
|
+
): RocketStyleComponent<OA, MergeTypes<[EA, P]>, T, CSS, S, HOC, D, UB, DKP>
|
|
163
|
+
}
|
|
115
164
|
|
|
116
165
|
// THEME chaining method
|
|
117
166
|
theme: <P extends TObj = TObj>(
|
package/src/types/styles.ts
CHANGED
package/src/types/utils.ts
CHANGED
|
@@ -51,5 +51,48 @@ export type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R
|
|
|
51
51
|
export type MergeTypes<A extends readonly [...any]> = ExtractNullableKeys<Spread<A>>
|
|
52
52
|
|
|
53
53
|
// ─── ExtractProps ─────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Extracts the props type from a Pyreon component function — or passes
|
|
56
|
+
* through the input unchanged when it's already a props type.
|
|
57
|
+
*
|
|
58
|
+
* Multi-overload aware: matches up to 4 call signatures and produces the
|
|
59
|
+
* UNION of their first-argument types. A single-overload function still
|
|
60
|
+
* works (the union of 4 copies of the same props type dedupes back to
|
|
61
|
+
* the single shape).
|
|
62
|
+
*
|
|
63
|
+
* **Why this shape**. `T extends (props: infer P) => any ? P : never` only
|
|
64
|
+
* captures the LAST overload of a multi-overload function — TS's overload-
|
|
65
|
+
* resolution-against-conditional-types semantics. Iterator / List / Element
|
|
66
|
+
* are 3-overload primitives where the LAST overload (`ChildrenProps`) is the
|
|
67
|
+
* loosest; without overload-aware extraction, `ExtractProps<Iterator>`
|
|
68
|
+
* returned just `ChildrenProps` and lost both `SimpleProps<T>` and
|
|
69
|
+
* `ObjectProps<T>` — wrapping Iterator through `rocketstyle()` /
|
|
70
|
+
* `attrs()` silently downgraded the public prop surface.
|
|
71
|
+
*
|
|
72
|
+
* The pattern-match shape `T extends { (props: infer P1, ...args: any): any;
|
|
73
|
+
* (props: infer P2, ...args: any): any; ... }` is the canonical TS trick
|
|
74
|
+
* for extracting overload sets — see also `Parameters<T>` semantics.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors vitus-labs PR #222.
|
|
77
|
+
*/
|
|
78
|
+
export type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends {
|
|
79
|
+
(props: infer P1, ...args: any): any
|
|
80
|
+
(props: infer P2, ...args: any): any
|
|
81
|
+
(props: infer P3, ...args: any): any
|
|
82
|
+
(props: infer P4, ...args: any): any
|
|
83
|
+
}
|
|
84
|
+
? P1 | P2 | P3 | P4
|
|
85
|
+
: TComponentOrTProps extends {
|
|
86
|
+
(props: infer P1, ...args: any): any
|
|
87
|
+
(props: infer P2, ...args: any): any
|
|
88
|
+
(props: infer P3, ...args: any): any
|
|
89
|
+
}
|
|
90
|
+
? P1 | P2 | P3
|
|
91
|
+
: TComponentOrTProps extends {
|
|
92
|
+
(props: infer P1, ...args: any): any
|
|
93
|
+
(props: infer P2, ...args: any): any
|
|
94
|
+
}
|
|
95
|
+
? P1 | P2
|
|
96
|
+
: TComponentOrTProps extends ComponentFn<infer TProps>
|
|
97
|
+
? TProps
|
|
98
|
+
: TComponentOrTProps
|
package/src/utils/attrs.ts
CHANGED
|
@@ -3,13 +3,60 @@ import type { MultiKeys } from '../types/dimensions'
|
|
|
3
3
|
// --------------------------------------------------------
|
|
4
4
|
// remove undefined props
|
|
5
5
|
// --------------------------------------------------------
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Strips keys with `undefined` values so they don't shadow default props during merging.
|
|
8
|
+
*
|
|
9
|
+
* Copies own property DESCRIPTORS rather than values so that reactive
|
|
10
|
+
* getter-shaped props (compiler-emitted `_rp(() => signal())` converted
|
|
11
|
+
* to getters by `makeReactiveProps`) survive the pipeline with their
|
|
12
|
+
* subscription intact. Reading `props[key]` here would fire the getter
|
|
13
|
+
* at HOC setup time (outside any tracking scope) and collapse the prop
|
|
14
|
+
* to a static value — every downstream JSX accessor that reads
|
|
15
|
+
* `props.x` would see the captured-once value, not the live signal.
|
|
16
|
+
*
|
|
17
|
+
* For getter descriptors we keep the descriptor as-is (the
|
|
18
|
+
* undefined-filter doesn't apply — we can't peek into the getter
|
|
19
|
+
* without firing it). For data descriptors we drop entries whose
|
|
20
|
+
* value is `undefined` to preserve the original merge semantics.
|
|
21
|
+
*/
|
|
7
22
|
type RemoveUndefinedProps = <T extends Record<string, any>>(props: T) => Partial<T>
|
|
8
23
|
|
|
9
24
|
export const removeUndefinedProps: RemoveUndefinedProps = (props) => {
|
|
10
25
|
const result: Partial<typeof props> = {}
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
const descriptors = Object.getOwnPropertyDescriptors(props)
|
|
27
|
+
for (const key of Object.keys(descriptors)) {
|
|
28
|
+
const d = descriptors[key]!
|
|
29
|
+
if (d.get || d.value !== undefined) {
|
|
30
|
+
Object.defineProperty(result, key, d)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --------------------------------------------------------
|
|
37
|
+
// merge descriptors
|
|
38
|
+
// --------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* Like `Object.assign(target, ...sources)` but copies own property
|
|
41
|
+
* DESCRIPTORS instead of reading + writing values. Later sources
|
|
42
|
+
* override earlier ones (same semantics as spread / Object.assign).
|
|
43
|
+
*
|
|
44
|
+
* Required for reactive-prop preservation through the rocketstyle
|
|
45
|
+
* pipeline: a plain `{ ...A, ...B }` spread fires every getter on A
|
|
46
|
+
* and B and stores the resolved value, breaking the reactive
|
|
47
|
+
* subscription. This helper copies descriptors so getters survive
|
|
48
|
+
* the merge.
|
|
49
|
+
*/
|
|
50
|
+
export const mergeDescriptors = (
|
|
51
|
+
...sources: ReadonlyArray<Record<string, any> | null | undefined>
|
|
52
|
+
): Record<string, any> => {
|
|
53
|
+
const result: Record<string, any> = {}
|
|
54
|
+
for (const source of sources) {
|
|
55
|
+
if (!source) continue
|
|
56
|
+
const descriptors = Object.getOwnPropertyDescriptors(source)
|
|
57
|
+
for (const key of Object.keys(descriptors)) {
|
|
58
|
+
Object.defineProperty(result, key, descriptors[key]!)
|
|
59
|
+
}
|
|
13
60
|
}
|
|
14
61
|
return result
|
|
15
62
|
}
|