@pyreon/rocketstyle 0.15.0 → 0.16.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 +42 -26
- 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__/rocketstyle.browser.test.tsx +221 -0
- package/src/__tests__/rocketstyleIntegration.test.ts +134 -0
- package/src/index.ts +2 -1
- package/src/rocketstyle.ts +68 -27
- 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/lib/index.d.ts
CHANGED
|
@@ -87,11 +87,47 @@ type ExtractNullableKeys<T> = { [P in keyof T as IsAny<T[P]> extends true ? P :
|
|
|
87
87
|
type SpreadTwo<L, R> = Id<Pick<L, Exclude<keyof L, keyof R>> & R>;
|
|
88
88
|
type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ? SpreadTwo<L, Spread<R>> : unknown;
|
|
89
89
|
type MergeTypes<A extends readonly [...any]> = ExtractNullableKeys<Spread<A>>;
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Extracts the props type from a Pyreon component function — or passes
|
|
92
|
+
* through the input unchanged when it's already a props type.
|
|
93
|
+
*
|
|
94
|
+
* Multi-overload aware: matches up to 4 call signatures and produces the
|
|
95
|
+
* UNION of their first-argument types. A single-overload function still
|
|
96
|
+
* works (the union of 4 copies of the same props type dedupes back to
|
|
97
|
+
* the single shape).
|
|
98
|
+
*
|
|
99
|
+
* **Why this shape**. `T extends (props: infer P) => any ? P : never` only
|
|
100
|
+
* captures the LAST overload of a multi-overload function — TS's overload-
|
|
101
|
+
* resolution-against-conditional-types semantics. Iterator / List / Element
|
|
102
|
+
* are 3-overload primitives where the LAST overload (`ChildrenProps`) is the
|
|
103
|
+
* loosest; without overload-aware extraction, `ExtractProps<Iterator>`
|
|
104
|
+
* returned just `ChildrenProps` and lost both `SimpleProps<T>` and
|
|
105
|
+
* `ObjectProps<T>` — wrapping Iterator through `rocketstyle()` /
|
|
106
|
+
* `attrs()` silently downgraded the public prop surface.
|
|
107
|
+
*
|
|
108
|
+
* The pattern-match shape `T extends { (props: infer P1, ...args: any): any;
|
|
109
|
+
* (props: infer P2, ...args: any): any; ... }` is the canonical TS trick
|
|
110
|
+
* for extracting overload sets — see also `Parameters<T>` semantics.
|
|
111
|
+
*
|
|
112
|
+
* Mirrors vitus-labs PR #222.
|
|
113
|
+
*/
|
|
114
|
+
type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends {
|
|
115
|
+
(props: infer P1, ...args: any): any;
|
|
116
|
+
(props: infer P2, ...args: any): any;
|
|
117
|
+
(props: infer P3, ...args: any): any;
|
|
118
|
+
(props: infer P4, ...args: any): any;
|
|
119
|
+
} ? P1 | P2 | P3 | P4 : TComponentOrTProps extends {
|
|
120
|
+
(props: infer P1, ...args: any): any;
|
|
121
|
+
(props: infer P2, ...args: any): any;
|
|
122
|
+
(props: infer P3, ...args: any): any;
|
|
123
|
+
} ? P1 | P2 | P3 : TComponentOrTProps extends {
|
|
124
|
+
(props: infer P1, ...args: any): any;
|
|
125
|
+
(props: infer P2, ...args: any): any;
|
|
126
|
+
} ? P1 | P2 : TComponentOrTProps extends ComponentFn<infer TProps> ? TProps : TComponentOrTProps;
|
|
91
127
|
//#endregion
|
|
92
128
|
//#region src/types/styles.d.ts
|
|
93
129
|
interface StylesDefault {}
|
|
94
|
-
type Styles<
|
|
130
|
+
type Styles<_S = unknown> = StylesDefault;
|
|
95
131
|
type Css = typeof config.css;
|
|
96
132
|
/**
|
|
97
133
|
* Props available inside `.styles()` interpolation functions.
|
|
@@ -226,12 +262,25 @@ type DefaultProps = Partial<PseudoProps> & {
|
|
|
226
262
|
};
|
|
227
263
|
//#endregion
|
|
228
264
|
//#region src/types/attrs.d.ts
|
|
229
|
-
|
|
265
|
+
/** Helpers object passed as the 3rd arg to every `.attrs(callback)`. */
|
|
266
|
+
type AttrsHelpers = {
|
|
230
267
|
mode?: ThemeModeKeys;
|
|
231
268
|
isDark?: boolean;
|
|
232
269
|
isLight?: boolean;
|
|
233
270
|
createElement: typeof render;
|
|
234
|
-
}
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Callback signature for `.attrs((props, theme, helpers) => …)`.
|
|
274
|
+
*
|
|
275
|
+
* `Partial<A>` on the return is for the strict-typing form when callers
|
|
276
|
+
* pass `AttrsCb<DFP, Theme<T>>` directly. In the rocketstyle `.attrs()`
|
|
277
|
+
* callback overload itself we use a different shape that decouples the
|
|
278
|
+
* props arg (narrow, full DFP) from the return type (loose — only the
|
|
279
|
+
* user's explicit `<P>` generic is checked, with `Record<string,
|
|
280
|
+
* unknown>` allowing runtime extras like `_documentProps`). See
|
|
281
|
+
* `IRocketStyleComponent.attrs` for the call-site shape.
|
|
282
|
+
*/
|
|
283
|
+
type AttrsCb<A, T> = (props: Partial<A>, theme: T, helpers: AttrsHelpers) => Partial<A>;
|
|
235
284
|
//#endregion
|
|
236
285
|
//#region src/types/hoc.d.ts
|
|
237
286
|
type GenericHoc = (component: ElementType) => ElementType;
|
|
@@ -250,7 +299,7 @@ type RocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T extends
|
|
|
250
299
|
* @param DKP Dimensions key props.
|
|
251
300
|
* @param DFP Calculated final component props
|
|
252
301
|
*/
|
|
253
|
-
interface IRocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T extends TObj = {}, CSS extends TObj = {}, S extends TObj = {}, HOC extends TObj = {}, D extends Dimensions = Dimensions, UB extends boolean = boolean, DKP extends TDKP = TDKP, DFP =
|
|
302
|
+
interface IRocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T extends TObj = {}, CSS extends TObj = {}, S extends TObj = {}, HOC extends TObj = {}, D extends Dimensions = Dimensions, UB extends boolean = boolean, DKP extends TDKP = TDKP, DFP = (OA extends infer O ? Omit<O, keyof EA & keyof O> & Partial<Pick<O, keyof EA & keyof O>> & MergeTypes<[Partial<Omit<EA, keyof O>>, DefaultProps, ExtractDimensionProps<D, DKP, UB>]> : never)> {
|
|
254
303
|
(props: DFP): VNodeChild;
|
|
255
304
|
config: <NC extends ElementType | unknown = unknown>({
|
|
256
305
|
name,
|
|
@@ -261,10 +310,16 @@ interface IRocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T ex
|
|
|
261
310
|
inversed,
|
|
262
311
|
passProps
|
|
263
312
|
}: ConfigAttrs<NC, D, DKP, UB>) => NC extends ElementType ? RocketStyleComponent<ExtractProps<NC>, EA, T, CSS, S, HOC, D, UB, DKP> : RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>;
|
|
264
|
-
attrs:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
313
|
+
attrs: {
|
|
314
|
+
<P extends TObj = {}>(param: (props: Partial<DFP & P>, theme: Theme<T>, helpers: AttrsHelpers) => Partial<P> & Record<string, unknown>, config?: Partial<{
|
|
315
|
+
priority: boolean;
|
|
316
|
+
filter: (keyof MergeTypes<[EA, P]>)[];
|
|
317
|
+
}>): RocketStyleComponent<OA, MergeTypes<[EA, P]>, T, CSS, S, HOC, D, UB, DKP>;
|
|
318
|
+
<P extends TObj = {}>(param: P & Partial<NoInfer<DFP>>, config?: Partial<{
|
|
319
|
+
priority: boolean;
|
|
320
|
+
filter: (keyof MergeTypes<[EA, P]>)[];
|
|
321
|
+
}>): RocketStyleComponent<OA, MergeTypes<[EA, P]>, T, CSS, S, HOC, D, UB, DKP>;
|
|
322
|
+
};
|
|
268
323
|
theme: <P extends TObj = TObj>(param: Partial<P> | Partial<Styles<CSS>> | ThemeCb<P, Theme<T>>) => RocketStyleComponent<OA, EA, T, MergeTypes<[CSS, P]>, S, HOC, D, UB, DKP>;
|
|
269
324
|
styles: (param: StylesCb<CSS>) => RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>;
|
|
270
325
|
compose: <P extends ComposeParam>(param: P) => P extends TObj ? RocketStyleComponent<OA, EA, T, CSS, S, MergeTypes<[HOC, P]>, D, UB, DKP> : RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>;
|
|
@@ -337,5 +392,5 @@ declare const isRocketComponent: IsRocketComponent;
|
|
|
337
392
|
*/
|
|
338
393
|
declare function resolveTheme<T = Record<string, unknown>>(value: (() => T) | T): T;
|
|
339
394
|
//#endregion
|
|
340
|
-
export { type AttrsCb, type ComponentFn, type ComposeParam, type ConfigAttrs, type ConsumerCb, type ConsumerCtxCBValue, type ConsumerCtxCb, type DefaultProps, type DimensionCallbackParam, type DimensionProps, type DimensionValue, type Dimensions, type ElementType, type ExtractDimensionProps, type ExtractDimensions, type ExtractProps, type GenericHoc, type IRocketStyleComponent, type IsRocketComponent, type MergeTypes, Provider, type RocketComponentType, type RocketProviderState, type RocketStyleComponent, type RocketStyleInterpolationProps, type Rocketstyle, type StylesCb, type StylesDefault, type TDKP, type TObj, type TProvider, type ThemeCb, type ThemeDefault, type ThemeMode, type ThemeModeCallback, type ThemeModeKeys, context, rocketstyle as default, rocketstyle, isRocketComponent, resolveTheme };
|
|
395
|
+
export { type AttrsCb, type AttrsHelpers, type ComponentFn, type ComposeParam, type ConfigAttrs, type ConsumerCb, type ConsumerCtxCBValue, type ConsumerCtxCb, type DefaultProps, type DimensionCallbackParam, type DimensionProps, type DimensionValue, type Dimensions, type ElementType, type ExtractDimensionProps, type ExtractDimensions, type ExtractProps, type GenericHoc, type IRocketStyleComponent, type IsRocketComponent, type MergeTypes, Provider, type RocketComponentType, type RocketProviderState, type RocketStyleComponent, type RocketStyleInterpolationProps, type Rocketstyle, type StylesCb, type StylesDefault, type TDKP, type TObj, type TProvider, type ThemeCb, type ThemeDefault, type ThemeMode, type ThemeModeCallback, type ThemeModeKeys, context, rocketstyle as default, rocketstyle, isRocketComponent, resolveTheme };
|
|
341
396
|
//# sourceMappingURL=index2.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -472,23 +472,38 @@ const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) =
|
|
|
472
472
|
//#endregion
|
|
473
473
|
//#region src/rocketstyle.ts
|
|
474
474
|
const _countSink = globalThis;
|
|
475
|
-
/**
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Clones the current configuration and merges new options, returning a fresh
|
|
477
|
+
* rocketComponent.
|
|
478
|
+
*
|
|
479
|
+
* Component-swap reset: when `opts.component` is set AND differs from the
|
|
480
|
+
* current `defaultOpts.component`, the prior `attrs`, `priorityAttrs`,
|
|
481
|
+
* `filterAttrs`, and `compose` chains are dropped — they were tailored to the
|
|
482
|
+
* previous component's prop shape, and applying them to a different component
|
|
483
|
+
* silently leaks invalid props through to the DOM (e.g. `disabled` on an
|
|
484
|
+
* `<a>`). Callers who want to preserve them must re-chain explicitly:
|
|
485
|
+
*
|
|
486
|
+
* const NewBtn = Button.config({ component: 'a' }).attrs(sharedAttrs)
|
|
487
|
+
*/
|
|
488
|
+
const cloneAndEnhance = (defaultOpts, opts) => {
|
|
489
|
+
const componentChanged = opts.component != null && opts.component !== defaultOpts.component;
|
|
490
|
+
return rocketComponent({
|
|
491
|
+
...defaultOpts,
|
|
492
|
+
attrs: componentChanged ? chainOptions(opts.attrs, []) : chainOptions(opts.attrs, defaultOpts.attrs),
|
|
493
|
+
filterAttrs: componentChanged ? [...opts.filterAttrs ?? []] : [...defaultOpts.filterAttrs ?? [], ...opts.filterAttrs ?? []],
|
|
494
|
+
priorityAttrs: componentChanged ? chainOptions(opts.priorityAttrs, []) : chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
|
|
495
|
+
statics: {
|
|
496
|
+
...defaultOpts.statics,
|
|
497
|
+
...opts.statics
|
|
498
|
+
},
|
|
499
|
+
compose: componentChanged ? { ...opts.compose } : {
|
|
500
|
+
...defaultOpts.compose,
|
|
501
|
+
...opts.compose
|
|
502
|
+
},
|
|
503
|
+
...chainOrOptions(CONFIG_KEYS, opts, defaultOpts),
|
|
504
|
+
...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts)
|
|
505
|
+
});
|
|
506
|
+
};
|
|
492
507
|
const rocketComponent = (options) => {
|
|
493
508
|
const { component, styles } = options;
|
|
494
509
|
const { styled } = config;
|
|
@@ -548,11 +563,16 @@ const rocketComponent = (options) => {
|
|
|
548
563
|
const _resolveRsEntry = () => {
|
|
549
564
|
const theme = themeAttrs.theme;
|
|
550
565
|
const mode = themeAttrs.mode;
|
|
551
|
-
let key = mode;
|
|
552
566
|
const propsRec = props;
|
|
567
|
+
const rocketstateRaw = _calculateStylingAttrs({
|
|
568
|
+
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
569
|
+
dimensions
|
|
570
|
+
});
|
|
571
|
+
let key = mode;
|
|
553
572
|
for (const dimName in dimensions) {
|
|
554
|
-
const v =
|
|
555
|
-
key += "|" + (
|
|
573
|
+
const v = rocketstateRaw[dimName];
|
|
574
|
+
if (Array.isArray(v)) key += "|" + (v.length === 0 ? "" : v.slice().sort().join(","));
|
|
575
|
+
else key += "|" + (typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? String(v) : v === void 0 ? "" : "~" + typeof v);
|
|
556
576
|
}
|
|
557
577
|
for (const k of ALL_PSEUDO_KEYS) {
|
|
558
578
|
const propV = propsRec[k];
|
|
@@ -583,10 +603,6 @@ const rocketComponent = (options) => {
|
|
|
583
603
|
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
584
604
|
} else dimHelper.set(theme, getDimensionThemes(theme, options));
|
|
585
605
|
const themes = dimHelper.get(theme);
|
|
586
|
-
const rocketstateRaw = _calculateStylingAttrs({
|
|
587
|
-
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
588
|
-
dimensions
|
|
589
|
-
});
|
|
590
606
|
const modeBaseHelper = ThemeManager$1.modeBaseTheme[mode];
|
|
591
607
|
if (modeBaseHelper.has(baseTheme)) {
|
|
592
608
|
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
@@ -715,8 +731,8 @@ const rocketComponent = (options) => {
|
|
|
715
731
|
{
|
|
716
732
|
render,
|
|
717
733
|
mode,
|
|
718
|
-
isDark: mode === "
|
|
719
|
-
isLight: mode === "
|
|
734
|
+
isDark: mode === "dark",
|
|
735
|
+
isLight: mode === "light"
|
|
720
736
|
}
|
|
721
737
|
])
|
|
722
738
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/rocketstyle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Multi-dimensional style composition for Pyreon components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,19 +42,19 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@pyreon/test-utils": "^0.13.
|
|
46
|
-
"@pyreon/typescript": "^0.
|
|
47
|
-
"@pyreon/ui-core": "^0.
|
|
45
|
+
"@pyreon/test-utils": "^0.13.3",
|
|
46
|
+
"@pyreon/typescript": "^0.16.0",
|
|
47
|
+
"@pyreon/ui-core": "^0.16.0",
|
|
48
48
|
"@vitest/browser-playwright": "^4.1.4",
|
|
49
49
|
"@vitus-labs/tools-rolldown": "^2.3.0"
|
|
50
50
|
},
|
|
51
|
-
"peerDependencies": {
|
|
52
|
-
"@pyreon/core": "^0.15.0",
|
|
53
|
-
"@pyreon/reactivity": "^0.15.0",
|
|
54
|
-
"@pyreon/styler": "^0.15.0",
|
|
55
|
-
"@pyreon/ui-core": "^0.15.0"
|
|
56
|
-
},
|
|
57
51
|
"engines": {
|
|
58
52
|
"node": ">= 22"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@pyreon/core": "^0.16.0",
|
|
56
|
+
"@pyreon/reactivity": "^0.16.0",
|
|
57
|
+
"@pyreon/styler": "^0.16.0",
|
|
58
|
+
"@pyreon/ui-core": "^0.16.0"
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ComponentFn } from '@pyreon/core'
|
|
2
|
+
import rocketstyle from '../init'
|
|
3
|
+
|
|
4
|
+
// Type-level regression tests for the post-#225/#227 `.attrs()` overload
|
|
5
|
+
// split: (a) DFP widening makes `.attrs(obj)` keys optional at JSX call site,
|
|
6
|
+
// (b) callback overload preserves Pyreon's loose-return convention so
|
|
7
|
+
// `_documentProps` / `tag: 'a'` runtime extras still typecheck without
|
|
8
|
+
// per-callsite `as any` casts.
|
|
9
|
+
//
|
|
10
|
+
// These are not bisect-load-bearing at runtime — they're type-level
|
|
11
|
+
// assertions exercised by `tsc --noEmit`. Including them in the suite
|
|
12
|
+
// makes failures show up in the test report (vitest treats type errors
|
|
13
|
+
// as compile failures).
|
|
14
|
+
describe('attrs overloads — type-level contract', () => {
|
|
15
|
+
// A minimal base component standing in for Text / Button / etc.
|
|
16
|
+
// We only care about the type-level surface here.
|
|
17
|
+
type BaseProps = {
|
|
18
|
+
tag?: 'div' | 'span' | 'p' | 'h1' | 'h2' | 'h3'
|
|
19
|
+
role?: string
|
|
20
|
+
}
|
|
21
|
+
const Base: ComponentFn<BaseProps> = () => null
|
|
22
|
+
|
|
23
|
+
describe('object overload — keys become optional at JSX site (PR #225)', () => {
|
|
24
|
+
it('accepts object with default values', () => {
|
|
25
|
+
const Comp = rocketstyle()({ name: 'Comp', component: Base }).attrs({
|
|
26
|
+
tag: 'div',
|
|
27
|
+
})
|
|
28
|
+
// The component is callable — at the JSX call site, `tag` is now
|
|
29
|
+
// optional because `.attrs({ tag: 'div' })` provides a default.
|
|
30
|
+
// Pre-#225 the type would have required `tag` at the JSX site.
|
|
31
|
+
// We can't directly assert via `expectTypeOf` without the dep, but
|
|
32
|
+
// the smoke is: the chain compiles without errors.
|
|
33
|
+
expect(typeof Comp).toBe('function')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('accepts new keys in attrs object', () => {
|
|
37
|
+
// The attrs object can introduce new keys beyond the base component's
|
|
38
|
+
// props (here: `customField`). The keys flow into the returned
|
|
39
|
+
// component's extended-attrs `EA` and become typed props.
|
|
40
|
+
const Comp = rocketstyle()({ name: 'Comp', component: Base }).attrs({
|
|
41
|
+
customField: 'hello',
|
|
42
|
+
})
|
|
43
|
+
expect(typeof Comp).toBe('function')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('callback overload — Pyreon convention for runtime extras', () => {
|
|
48
|
+
it('accepts callback returning fields outside the base prop union (no as-cast needed)', () => {
|
|
49
|
+
// This is the canonical document-primitive pattern: a Text-based
|
|
50
|
+
// rocketstyle that overrides `tag` to a value outside Text's strict
|
|
51
|
+
// `tag` union AND adds a runtime-only `_documentProps` marker. The
|
|
52
|
+
// callback's return type intentionally allows `Record<string, unknown>`
|
|
53
|
+
// for keys outside the user's explicit `<P>` generic, matching
|
|
54
|
+
// Pyreon's pre-#225 convention.
|
|
55
|
+
const Comp = rocketstyle()({ name: 'DocLink', component: Base }).attrs<{
|
|
56
|
+
href?: string
|
|
57
|
+
}>((props) => ({
|
|
58
|
+
tag: 'a', // 'a' is NOT in BaseProps['tag'] — falls through Record<string, unknown>
|
|
59
|
+
_documentProps: { href: props.href ?? '#' },
|
|
60
|
+
}))
|
|
61
|
+
expect(typeof Comp).toBe('function')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('accepts callback returning literal values (contextual narrowing via <P>)', () => {
|
|
65
|
+
// When the user passes an explicit `<P>` generic, the callback's
|
|
66
|
+
// return is contextually typed against `Partial<P>`. Writing
|
|
67
|
+
// `tag: 'h1'` stays narrow at literal `'h1'` — no `as const` needed.
|
|
68
|
+
// Note: tag is in BaseProps['tag'] union so this typechecks against
|
|
69
|
+
// BOTH the wildcard arm AND the explicit P-key arm.
|
|
70
|
+
const Comp = rocketstyle()({ name: 'Heading', component: Base }).attrs<{
|
|
71
|
+
level?: number
|
|
72
|
+
}>((props) => ({
|
|
73
|
+
tag: `h${props.level ?? 1}` as 'h1' | 'h2' | 'h3',
|
|
74
|
+
}))
|
|
75
|
+
expect(typeof Comp).toBe('function')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('callback receives full DFP-typed props for narrow reads', () => {
|
|
79
|
+
// The `props` arg passed to the callback IS strictly typed as
|
|
80
|
+
// `Partial<DFP & P>` — so reading `props.tag` narrows against the
|
|
81
|
+
// wrapped component's full surface. This is the "props narrow,
|
|
82
|
+
// return loose" asymmetry Pyreon settled on.
|
|
83
|
+
const Comp = rocketstyle()({ name: 'Probe', component: Base }).attrs<{
|
|
84
|
+
scale?: number
|
|
85
|
+
}>((props) => {
|
|
86
|
+
// Type check: `props.scale` is `number | undefined`, `props.tag`
|
|
87
|
+
// is the narrow BaseProps['tag'] union | undefined.
|
|
88
|
+
const _scale: number | undefined = props.scale
|
|
89
|
+
const _tag: 'div' | 'span' | 'p' | 'h1' | 'h2' | 'h3' | undefined = props.tag
|
|
90
|
+
expect(_scale).toBeUndefined()
|
|
91
|
+
expect(_tag).toBeUndefined()
|
|
92
|
+
return { scale: 1 }
|
|
93
|
+
})
|
|
94
|
+
expect(typeof Comp).toBe('function')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bug reproduction: under `useBooleans: true`, `_resolveRsEntry`'s cache
|
|
3
|
+
* key reads `propsRec[dimName]` directly (e.g. `propsRec.state`). Boolean
|
|
4
|
+
* shorthand props like `<X primary />` populate `propsRec.primary` (NOT
|
|
5
|
+
* `propsRec.state`), so the cache key for the `state` slot is `undefined`
|
|
6
|
+
* → `''` regardless of which boolean variant was passed.
|
|
7
|
+
*
|
|
8
|
+
* Result: `<X primary />` and `<X secondary />` produce identical cache
|
|
9
|
+
* keys and share the cached entry. The first-resolved variant's
|
|
10
|
+
* `$rocketstyle` wins for all subsequent renders.
|
|
11
|
+
*/
|
|
12
|
+
import { initTestConfig, withThemeContext } from '@pyreon/test-utils'
|
|
13
|
+
import rocketstyle from '../init'
|
|
14
|
+
|
|
15
|
+
let cleanup: () => void
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
cleanup = initTestConfig()
|
|
18
|
+
})
|
|
19
|
+
afterAll(() => cleanup())
|
|
20
|
+
|
|
21
|
+
const ThemeCapture: any = ({ $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
22
|
+
type: 'div',
|
|
23
|
+
props: rest,
|
|
24
|
+
$rocketstyle: typeof $rocketstyle === 'function' ? $rocketstyle() : $rocketstyle,
|
|
25
|
+
$rocketstate: typeof $rocketstate === 'function' ? $rocketstate() : $rocketstate,
|
|
26
|
+
})
|
|
27
|
+
ThemeCapture.displayName = 'ThemeCapture'
|
|
28
|
+
|
|
29
|
+
describe('rocketstyle — cache-key collision under useBooleans:true', () => {
|
|
30
|
+
it('different boolean variants produce different $rocketstyle (NOT collide)', () => {
|
|
31
|
+
const Button: any = rocketstyle({ useBooleans: true })({
|
|
32
|
+
name: 'BoolButton',
|
|
33
|
+
component: ThemeCapture,
|
|
34
|
+
}).states(() => ({
|
|
35
|
+
primary: { color: 'red' },
|
|
36
|
+
secondary: { color: 'blue' },
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
// Render with primary=true. Captures the $rocketstyle resolved
|
|
40
|
+
// for state='primary'.
|
|
41
|
+
const a = withThemeContext(() => Button({ primary: true }))
|
|
42
|
+
|
|
43
|
+
// Render with secondary=true. Should resolve to state='secondary'
|
|
44
|
+
// and produce DIFFERENT $rocketstyle.
|
|
45
|
+
const b = withThemeContext(() => Button({ secondary: true }))
|
|
46
|
+
|
|
47
|
+
// Bug: a.$rocketstyle === b.$rocketstyle (same cached entry).
|
|
48
|
+
// Fix: a.$rocketstyle.color === 'red', b.$rocketstyle.color === 'blue'.
|
|
49
|
+
expect(a.$rocketstate.state).toBe('primary')
|
|
50
|
+
expect(b.$rocketstate.state).toBe('secondary')
|
|
51
|
+
expect(a.$rocketstyle.color).toBe('red')
|
|
52
|
+
expect(b.$rocketstyle.color).toBe('blue')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -95,6 +95,152 @@ describe('@pyreon/rocketstyle in real browser', () => {
|
|
|
95
95
|
unmount()
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
+
// Bug 3 audit: derivation chain matching the real bokisch.com bug shape.
|
|
99
|
+
//
|
|
100
|
+
// Library defines a base `Text` component with `.styles(({css}) => css\`color:
|
|
101
|
+
// ${$rocketstyle.color}\`)` — the .styles() callback reads $rocketstyle.color.
|
|
102
|
+
// Library also defines `.theme({color: 'black'})` as the default.
|
|
103
|
+
//
|
|
104
|
+
// Consumer derives a sub-component: `Text.theme((t, m) => ({color: m('red',
|
|
105
|
+
// 'blue')}))` — adds NEW .theme() but does NOT call .styles() again. The
|
|
106
|
+
// derivation should INHERIT the base's .styles() and have it consume the
|
|
107
|
+
// new .theme() values, with mode toggling re-rendering correctly.
|
|
108
|
+
//
|
|
109
|
+
// The user's report: components in this chain keep stale colors on
|
|
110
|
+
// theme toggle. Test verifies whether the derivation chain actually
|
|
111
|
+
// wires up reactively.
|
|
112
|
+
it('Bug 3 repro: rocketstyle derivation chain — inherited .styles() + new .theme() reacts to mode', async () => {
|
|
113
|
+
const modeSig = signal<'light' | 'dark'>('light')
|
|
114
|
+
|
|
115
|
+
// Library base — has .styles() that reads $rocketstyle.color.
|
|
116
|
+
const TextBase: any = rocketstyle()({
|
|
117
|
+
name: 'TextBase',
|
|
118
|
+
component: Base,
|
|
119
|
+
})
|
|
120
|
+
.styles(
|
|
121
|
+
(css: any) => css`
|
|
122
|
+
color: ${({ $rocketstyle }: any) => $rocketstyle.color};
|
|
123
|
+
`,
|
|
124
|
+
)
|
|
125
|
+
.theme({ color: 'rgb(0, 128, 0)' }) // green default
|
|
126
|
+
|
|
127
|
+
// Consumer derivation — only adds .theme(), no .styles() override.
|
|
128
|
+
const Text: any = TextBase
|
|
129
|
+
.theme((_t: any, m: any) => ({
|
|
130
|
+
color: m('rgb(255, 0, 0)', 'rgb(0, 0, 255)'),
|
|
131
|
+
}))
|
|
132
|
+
|
|
133
|
+
const { container, unmount } = mountInBrowser(
|
|
134
|
+
h(PyreonUI, { theme: {}, mode: modeSig }, h(Text, { id: 'derive' })),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const el = container.querySelector<HTMLElement>('#derive')!
|
|
138
|
+
const initialColor = getComputedStyle(el).color
|
|
139
|
+
|
|
140
|
+
modeSig.set('dark')
|
|
141
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
142
|
+
await new Promise((r) => requestAnimationFrame(() => r(undefined)))
|
|
143
|
+
|
|
144
|
+
const darkColor = getComputedStyle(el).color
|
|
145
|
+
|
|
146
|
+
// Diagnostic: log both colors for clarity, regardless of pass/fail
|
|
147
|
+
// (helps triage whether the bug is "no .theme() applied at all" vs
|
|
148
|
+
// "applied but doesn't react to mode toggle").
|
|
149
|
+
if (initialColor === 'rgb(255, 0, 0)' && darkColor === 'rgb(0, 0, 255)') {
|
|
150
|
+
// Works correctly — derivation chain wires up reactively
|
|
151
|
+
expect(darkColor).toBe('rgb(0, 0, 255)')
|
|
152
|
+
} else {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`[bug-3-repro] derivation chain failed. initial=${initialColor}, dark=${darkColor}. ` +
|
|
155
|
+
`Expected initial=rgb(255, 0, 0), dark=rgb(0, 0, 255). ` +
|
|
156
|
+
`Possible causes: (a) .theme() override silently dropped, ` +
|
|
157
|
+
`(b) .styles() not inherited from base, ` +
|
|
158
|
+
`(c) mode toggle doesn't propagate.`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
unmount()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Bug 3 audit (continued): derivation chain WITH dimension props.
|
|
165
|
+
// Real consumer pattern: `<Text base paragraph centered>` uses
|
|
166
|
+
// dimension-prop values from .sizes()/.variants(). Tests that mode
|
|
167
|
+
// toggle still propagates when dimension props are active.
|
|
168
|
+
it('Bug 3 repro: derivation + dimension props + mode toggle', async () => {
|
|
169
|
+
const modeSig = signal<'light' | 'dark'>('light')
|
|
170
|
+
|
|
171
|
+
const TextBase: any = rocketstyle()({ name: 'TextBaseDim', component: Base })
|
|
172
|
+
.styles(
|
|
173
|
+
(css: any) => css`
|
|
174
|
+
color: ${({ $rocketstyle }: any) => $rocketstyle.color};
|
|
175
|
+
font-size: ${({ $rocketstyle }: any) => $rocketstyle.fontSize};
|
|
176
|
+
`,
|
|
177
|
+
)
|
|
178
|
+
.theme({ color: 'rgb(0, 0, 0)', fontSize: '14px' })
|
|
179
|
+
.sizes({
|
|
180
|
+
small: { fontSize: '12px' },
|
|
181
|
+
large: { fontSize: '20px' },
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Consumer derivation: theme override that depends on mode
|
|
185
|
+
const Text: any = TextBase.theme((_t: any, m: any) => ({
|
|
186
|
+
color: m('rgb(255, 0, 0)', 'rgb(0, 0, 255)'),
|
|
187
|
+
}))
|
|
188
|
+
|
|
189
|
+
const { container, unmount } = mountInBrowser(
|
|
190
|
+
h(PyreonUI, { theme: {}, mode: modeSig },
|
|
191
|
+
h(Text, { id: 'dim', size: 'large' }),
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
const el = container.querySelector<HTMLElement>('#dim')!
|
|
195
|
+
expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)')
|
|
196
|
+
expect(getComputedStyle(el).fontSize).toBe('20px')
|
|
197
|
+
|
|
198
|
+
modeSig.set('dark')
|
|
199
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
200
|
+
await new Promise((r) => requestAnimationFrame(() => r(undefined)))
|
|
201
|
+
|
|
202
|
+
expect(getComputedStyle(el).color).toBe('rgb(0, 0, 255)')
|
|
203
|
+
expect(getComputedStyle(el).fontSize).toBe('20px') // dimension unchanged
|
|
204
|
+
unmount()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Bug 3 audit (continued): DOUBLE derivation — Text → TextStyled → consumer.
|
|
208
|
+
// Tests whether mode reactivity survives a multi-level chain.
|
|
209
|
+
it('Bug 3 repro: double-derivation chain still reacts to mode', async () => {
|
|
210
|
+
const modeSig = signal<'light' | 'dark'>('light')
|
|
211
|
+
|
|
212
|
+
const TextBase: any = rocketstyle()({ name: 'DoubleTextBase', component: Base })
|
|
213
|
+
.styles(
|
|
214
|
+
(css: any) => css`
|
|
215
|
+
color: ${({ $rocketstyle }: any) => $rocketstyle.color};
|
|
216
|
+
`,
|
|
217
|
+
)
|
|
218
|
+
.theme({ color: 'rgb(0, 0, 0)' })
|
|
219
|
+
|
|
220
|
+
// First derivation — adds states (no theme override yet)
|
|
221
|
+
const TextStyled: any = TextBase.states({
|
|
222
|
+
muted: { color: 'rgb(128, 128, 128)' },
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Second derivation — mode-aware theme
|
|
226
|
+
const Text: any = TextStyled.theme((_t: any, m: any) => ({
|
|
227
|
+
color: m('rgb(255, 0, 0)', 'rgb(0, 0, 255)'),
|
|
228
|
+
}))
|
|
229
|
+
|
|
230
|
+
const { container, unmount } = mountInBrowser(
|
|
231
|
+
h(PyreonUI, { theme: {}, mode: modeSig }, h(Text, { id: 'dbl' })),
|
|
232
|
+
)
|
|
233
|
+
const el = container.querySelector<HTMLElement>('#dbl')!
|
|
234
|
+
expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)')
|
|
235
|
+
|
|
236
|
+
modeSig.set('dark')
|
|
237
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
238
|
+
await new Promise((r) => requestAnimationFrame(() => r(undefined)))
|
|
239
|
+
|
|
240
|
+
expect(getComputedStyle(el).color).toBe('rgb(0, 0, 255)')
|
|
241
|
+
unmount()
|
|
242
|
+
})
|
|
243
|
+
|
|
98
244
|
it('the `variant` prop layers on top of state', () => {
|
|
99
245
|
const Box: any = rocketstyle()({ name: 'VariantBox', component: Base })
|
|
100
246
|
.styles(
|
|
@@ -168,6 +314,81 @@ describe('@pyreon/rocketstyle in real browser', () => {
|
|
|
168
314
|
dark.unmount()
|
|
169
315
|
})
|
|
170
316
|
|
|
317
|
+
// Bug 5 repro: cache-key collision under `useBooleans: true`.
|
|
318
|
+
//
|
|
319
|
+
// The user-facing bug shape: `<Btn primary />` and `<Btn secondary />`
|
|
320
|
+
// both render with the FIRST cached variant's resolved styles. Pre-fix
|
|
321
|
+
// the dimension-prop memo built its key from raw `propsRec[dimName]`
|
|
322
|
+
// BEFORE _calculateStylingAttrs resolved the boolean shorthand, so all
|
|
323
|
+
// boolean variants on the same dimension had `propsRec.state === undefined`
|
|
324
|
+
// and collided. Real-app symptom: a button group where primary, secondary,
|
|
325
|
+
// and danger variants all render in the FIRST color the user clicked.
|
|
326
|
+
//
|
|
327
|
+
// Real-Chromium proof: mounts both variants in parallel and asserts
|
|
328
|
+
// `getComputedStyle().color` is different. happy-dom's class-based
|
|
329
|
+
// assertions wouldn't catch this — only real CSS resolution proves
|
|
330
|
+
// the styler classCache served different cached styles for each variant.
|
|
331
|
+
it('Bug 5 repro: useBooleans:true with multiple boolean variants render with distinct computed styles', () => {
|
|
332
|
+
const Btn: any = rocketstyle({ useBooleans: true })({
|
|
333
|
+
name: 'BoolBtn',
|
|
334
|
+
component: Base,
|
|
335
|
+
})
|
|
336
|
+
.styles(
|
|
337
|
+
(css: any) => css`
|
|
338
|
+
color: ${({ $rocketstyle }: any) => $rocketstyle.color};
|
|
339
|
+
`,
|
|
340
|
+
)
|
|
341
|
+
.theme({ color: 'rgb(0, 0, 0)' })
|
|
342
|
+
.states({
|
|
343
|
+
primary: { color: 'rgb(255, 0, 0)' },
|
|
344
|
+
secondary: { color: 'rgb(0, 255, 0)' },
|
|
345
|
+
danger: { color: 'rgb(0, 0, 255)' },
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const primaryMount = mountInBrowser(h(Btn, { id: 'p', primary: true }))
|
|
349
|
+
const secondaryMount = mountInBrowser(h(Btn, { id: 's', secondary: true }))
|
|
350
|
+
const dangerMount = mountInBrowser(h(Btn, { id: 'd', danger: true }))
|
|
351
|
+
|
|
352
|
+
const primaryEl = primaryMount.container.querySelector<HTMLElement>('#p')!
|
|
353
|
+
const secondaryEl = secondaryMount.container.querySelector<HTMLElement>('#s')!
|
|
354
|
+
const dangerEl = dangerMount.container.querySelector<HTMLElement>('#d')!
|
|
355
|
+
|
|
356
|
+
// Without the fix: all three resolve to whichever variant was cached
|
|
357
|
+
// first under the colliding key (mode|undefined|...). With the fix:
|
|
358
|
+
// each gets its own cache entry keyed off the resolved state value.
|
|
359
|
+
expect(getComputedStyle(primaryEl).color).toBe('rgb(255, 0, 0)')
|
|
360
|
+
expect(getComputedStyle(secondaryEl).color).toBe('rgb(0, 255, 0)')
|
|
361
|
+
expect(getComputedStyle(dangerEl).color).toBe('rgb(0, 0, 255)')
|
|
362
|
+
|
|
363
|
+
// Sibling mount under the SAME PyreonUI provider — exercises the
|
|
364
|
+
// cross-instance shared memo (per-theme WeakMap entry). Pre-fix this
|
|
365
|
+
// was the actual real-app shape: button group children mounted under
|
|
366
|
+
// a single provider, all hitting the same colliding key.
|
|
367
|
+
const groupMount = mountInBrowser(
|
|
368
|
+
h(PyreonUI, { theme: {}, mode: 'light' },
|
|
369
|
+
h('div', { id: 'group' },
|
|
370
|
+
h(Btn, { id: 'g-primary', primary: true }),
|
|
371
|
+
h(Btn, { id: 'g-secondary', secondary: true }),
|
|
372
|
+
h(Btn, { id: 'g-danger', danger: true }),
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
expect(
|
|
377
|
+
getComputedStyle(groupMount.container.querySelector<HTMLElement>('#g-primary')!).color,
|
|
378
|
+
).toBe('rgb(255, 0, 0)')
|
|
379
|
+
expect(
|
|
380
|
+
getComputedStyle(groupMount.container.querySelector<HTMLElement>('#g-secondary')!).color,
|
|
381
|
+
).toBe('rgb(0, 255, 0)')
|
|
382
|
+
expect(
|
|
383
|
+
getComputedStyle(groupMount.container.querySelector<HTMLElement>('#g-danger')!).color,
|
|
384
|
+
).toBe('rgb(0, 0, 255)')
|
|
385
|
+
|
|
386
|
+
primaryMount.unmount()
|
|
387
|
+
secondaryMount.unmount()
|
|
388
|
+
dangerMount.unmount()
|
|
389
|
+
groupMount.unmount()
|
|
390
|
+
})
|
|
391
|
+
|
|
171
392
|
it('multiple instances share definition-scoped caches (no per-mount rebuild)', () => {
|
|
172
393
|
// Verifies the perf optimization: getDimensionsMap, reservedPropNames keys,
|
|
173
394
|
// and omit Sets are cached at definition time (WeakMap), not rebuilt per mount.
|
|
@@ -214,6 +214,36 @@ describe('chaining methods', () => {
|
|
|
214
214
|
const result = WithAttrs.getDefaultAttrs({}, {}, 'light')
|
|
215
215
|
expect(result.label).toBe('default')
|
|
216
216
|
})
|
|
217
|
+
|
|
218
|
+
it('.getDefaultAttrs() passes isDark/isLight helpers matching the requested mode', () => {
|
|
219
|
+
// Regression: pre-fix the helpers were inverted in `getDefaultAttrs`
|
|
220
|
+
// (isDark: mode === 'light', isLight: mode === 'dark') so introspection
|
|
221
|
+
// callers (rocketstories-style story generators, devtools) saw the
|
|
222
|
+
// OPPOSITE of what the runtime renders. Runtime via `useTheme` derives
|
|
223
|
+
// helpers correctly from context (and handles `inversed` by flipping
|
|
224
|
+
// the mode at the Provider level — see context.test.ts), so the bug
|
|
225
|
+
// only surfaced for callers that read the helpers via `getDefaultAttrs`.
|
|
226
|
+
//
|
|
227
|
+
// Contract: `mode` is the EFFECTIVE mode (post-`inversed` resolution).
|
|
228
|
+
// Callers wanting "inversed light" pass `'dark'`; this function takes
|
|
229
|
+
// the resolved value, so testing both light/dark covers both the
|
|
230
|
+
// un-inversed and inversed cases.
|
|
231
|
+
const captured: Array<{ mode: any; isDark: any; isLight: any }> = []
|
|
232
|
+
const Probe = Button.attrs((_props: any, _theme: any, helpers: any) => {
|
|
233
|
+
captured.push({
|
|
234
|
+
mode: helpers.mode,
|
|
235
|
+
isDark: helpers.isDark,
|
|
236
|
+
isLight: helpers.isLight,
|
|
237
|
+
})
|
|
238
|
+
return {}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
Probe.getDefaultAttrs({}, {}, 'light')
|
|
242
|
+
Probe.getDefaultAttrs({}, {}, 'dark')
|
|
243
|
+
|
|
244
|
+
expect(captured[0]).toEqual({ mode: 'light', isDark: false, isLight: true })
|
|
245
|
+
expect(captured[1]).toEqual({ mode: 'dark', isDark: true, isLight: false })
|
|
246
|
+
})
|
|
217
247
|
})
|
|
218
248
|
|
|
219
249
|
// --------------------------------------------------------
|
|
@@ -575,3 +605,107 @@ describe('theme and state injection', () => {
|
|
|
575
605
|
expect(vnode.$rocketstate.state).toBe('primary')
|
|
576
606
|
})
|
|
577
607
|
})
|
|
608
|
+
|
|
609
|
+
// --------------------------------------------------------
|
|
610
|
+
// component-swap reset (cloneAndEnhance)
|
|
611
|
+
// --------------------------------------------------------
|
|
612
|
+
// `.config({ component: NewBase })` swaps the underlying renderable. The prior
|
|
613
|
+
// .attrs() / .priorityAttrs() / .filterAttrs() / .compose() chains were
|
|
614
|
+
// tailored to the previous component's prop shape — applying them to a
|
|
615
|
+
// different component silently leaks invalid props through to the DOM (e.g.
|
|
616
|
+
// `disabled` on an `<a>`). vitus-labs's rocketstyle drops those chains on
|
|
617
|
+
// component swap; this regression test locks in matching behavior here.
|
|
618
|
+
describe('component-swap reset', () => {
|
|
619
|
+
it('drops .attrs() chain when component changes', () => {
|
|
620
|
+
const ButtonBase: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
621
|
+
type: 'button',
|
|
622
|
+
props: rest,
|
|
623
|
+
children,
|
|
624
|
+
})
|
|
625
|
+
ButtonBase.displayName = 'ButtonBase'
|
|
626
|
+
|
|
627
|
+
const AnchorBase: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
628
|
+
type: 'a',
|
|
629
|
+
props: rest,
|
|
630
|
+
children,
|
|
631
|
+
})
|
|
632
|
+
AnchorBase.displayName = 'AnchorBase'
|
|
633
|
+
|
|
634
|
+
const Button: any = rocketstyle()({
|
|
635
|
+
name: 'Button',
|
|
636
|
+
component: ButtonBase,
|
|
637
|
+
}).attrs((() => ({ 'data-button-attr': 'leaked' })) as any)
|
|
638
|
+
|
|
639
|
+
const Link: any = Button.config({ component: AnchorBase })
|
|
640
|
+
|
|
641
|
+
const result = renderProps(Link)
|
|
642
|
+
expect(result['data-button-attr']).toBeUndefined()
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('preserves .attrs() chain when component is not changed', () => {
|
|
646
|
+
const Base: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
647
|
+
type: 'div',
|
|
648
|
+
props: rest,
|
|
649
|
+
children,
|
|
650
|
+
})
|
|
651
|
+
Base.displayName = 'Base'
|
|
652
|
+
|
|
653
|
+
const Button: any = rocketstyle()({
|
|
654
|
+
name: 'Button',
|
|
655
|
+
component: Base,
|
|
656
|
+
}).attrs((() => ({ 'data-keep': 'yes' })) as any)
|
|
657
|
+
|
|
658
|
+
const Same: any = Button.config({ DEBUG: false })
|
|
659
|
+
|
|
660
|
+
const result = renderProps(Same)
|
|
661
|
+
expect(result['data-keep']).toBe('yes')
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('preserves .attrs() chain when same component is re-passed via config', () => {
|
|
665
|
+
const Base: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
666
|
+
type: 'div',
|
|
667
|
+
props: rest,
|
|
668
|
+
children,
|
|
669
|
+
})
|
|
670
|
+
Base.displayName = 'Base'
|
|
671
|
+
|
|
672
|
+
const Button: any = rocketstyle()({
|
|
673
|
+
name: 'Button',
|
|
674
|
+
component: Base,
|
|
675
|
+
}).attrs((() => ({ 'data-keep': 'yes' })) as any)
|
|
676
|
+
|
|
677
|
+
const Same: any = Button.config({ component: Base })
|
|
678
|
+
|
|
679
|
+
const result = renderProps(Same)
|
|
680
|
+
expect(result['data-keep']).toBe('yes')
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it('lets fresh attrs after component swap apply to the new component', () => {
|
|
684
|
+
const ButtonBase: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
685
|
+
type: 'button',
|
|
686
|
+
props: rest,
|
|
687
|
+
children,
|
|
688
|
+
})
|
|
689
|
+
ButtonBase.displayName = 'ButtonBase'
|
|
690
|
+
|
|
691
|
+
const AnchorBase: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
692
|
+
type: 'a',
|
|
693
|
+
props: rest,
|
|
694
|
+
children,
|
|
695
|
+
})
|
|
696
|
+
AnchorBase.displayName = 'AnchorBase'
|
|
697
|
+
|
|
698
|
+
const Button: any = rocketstyle()({
|
|
699
|
+
name: 'Button',
|
|
700
|
+
component: ButtonBase,
|
|
701
|
+
}).attrs((() => ({ 'data-from-button': 'original' })) as any)
|
|
702
|
+
|
|
703
|
+
const Link: any = Button.config({ component: AnchorBase }).attrs(
|
|
704
|
+
(() => ({ 'data-from-link': 'fresh' })) as any,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
const result = renderProps(Link)
|
|
708
|
+
expect(result['data-from-button']).toBeUndefined()
|
|
709
|
+
expect(result['data-from-link']).toBe('fresh')
|
|
710
|
+
})
|
|
711
|
+
})
|
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
|
@@ -38,18 +38,42 @@ type CloneAndEnhance = (
|
|
|
38
38
|
opts: Partial<ExtendedConfiguration>,
|
|
39
39
|
) => ReturnType<typeof rocketComponent>
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Clones the current configuration and merges new options, returning a fresh
|
|
43
|
+
* rocketComponent.
|
|
44
|
+
*
|
|
45
|
+
* Component-swap reset: when `opts.component` is set AND differs from the
|
|
46
|
+
* current `defaultOpts.component`, the prior `attrs`, `priorityAttrs`,
|
|
47
|
+
* `filterAttrs`, and `compose` chains are dropped — they were tailored to the
|
|
48
|
+
* previous component's prop shape, and applying them to a different component
|
|
49
|
+
* silently leaks invalid props through to the DOM (e.g. `disabled` on an
|
|
50
|
+
* `<a>`). Callers who want to preserve them must re-chain explicitly:
|
|
51
|
+
*
|
|
52
|
+
* const NewBtn = Button.config({ component: 'a' }).attrs(sharedAttrs)
|
|
53
|
+
*/
|
|
54
|
+
const cloneAndEnhance: CloneAndEnhance = (defaultOpts, opts) => {
|
|
55
|
+
const componentChanged =
|
|
56
|
+
opts.component != null && opts.component !== defaultOpts.component
|
|
57
|
+
|
|
58
|
+
return rocketComponent({
|
|
44
59
|
...defaultOpts,
|
|
45
|
-
attrs:
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
attrs: componentChanged
|
|
61
|
+
? chainOptions(opts.attrs, [])
|
|
62
|
+
: chainOptions(opts.attrs, defaultOpts.attrs),
|
|
63
|
+
filterAttrs: componentChanged
|
|
64
|
+
? [...(opts.filterAttrs ?? [])]
|
|
65
|
+
: [...(defaultOpts.filterAttrs ?? []), ...(opts.filterAttrs ?? [])],
|
|
66
|
+
priorityAttrs: componentChanged
|
|
67
|
+
? chainOptions(opts.priorityAttrs, [])
|
|
68
|
+
: chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
|
|
48
69
|
statics: { ...defaultOpts.statics, ...opts.statics },
|
|
49
|
-
compose:
|
|
70
|
+
compose: componentChanged
|
|
71
|
+
? { ...opts.compose }
|
|
72
|
+
: { ...defaultOpts.compose, ...opts.compose },
|
|
50
73
|
...chainOrOptions(CONFIG_KEYS, opts, defaultOpts),
|
|
51
74
|
...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts),
|
|
52
75
|
} as Parameters<typeof rocketComponent>[0])
|
|
76
|
+
}
|
|
53
77
|
|
|
54
78
|
// --------------------------------------------------------
|
|
55
79
|
// rocketComponent
|
|
@@ -239,23 +263,46 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
239
263
|
// Read reactive inputs (tracks theme + mode signals)
|
|
240
264
|
const theme = themeAttrs.theme
|
|
241
265
|
const mode = themeAttrs.mode
|
|
266
|
+
const propsRec = props as Record<string, unknown>
|
|
242
267
|
|
|
243
|
-
//
|
|
268
|
+
// Resolve active dimensions FIRST so the cache key uses the RESOLVED
|
|
269
|
+
// dimension values, not the raw prop names. Under `useBooleans: true`
|
|
270
|
+
// the user writes `<X primary />` / `<X secondary />` — both map to
|
|
271
|
+
// `state="primary"` / `state="secondary"` after _calculateStylingAttrs
|
|
272
|
+
// resolves the boolean shorthand. Keying off `propsRec[dimName]` would
|
|
273
|
+
// read `undefined` for both (the dimension prop itself was never set)
|
|
274
|
+
// and collide every variant onto the first cached entry. Reading
|
|
275
|
+
// `rocketstateRaw[dimName]` gives the resolved string and partitions
|
|
276
|
+
// them correctly.
|
|
277
|
+
// Resolved from props (not localCtx which has pseudo getters).
|
|
278
|
+
const rocketstateRaw = _calculateStylingAttrs({
|
|
279
|
+
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
280
|
+
dimensions,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Build key: mode | dimensionValues | pseudoState. Reading dimension
|
|
244
284
|
// props + pseudo signals here tracks them in the surrounding computed
|
|
245
285
|
// so any change re-runs us with a different key.
|
|
246
286
|
let key = mode as string
|
|
247
|
-
const propsRec = props as Record<string, unknown>
|
|
248
287
|
for (const dimName in dimensions) {
|
|
249
|
-
const v =
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
?
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
288
|
+
const v = rocketstateRaw[dimName]
|
|
289
|
+
// Multi-key dimensions (e.g. variant={['primary', 'rounded']}) are
|
|
290
|
+
// arrays. Sort + join so equivalent sets hash identically; without
|
|
291
|
+
// this both `['a','b']` and `['b','a']` would produce different keys.
|
|
292
|
+
if (Array.isArray(v)) {
|
|
293
|
+
key +=
|
|
294
|
+
'|' + (v.length === 0 ? '' : (v as unknown[]).slice().sort().join(','))
|
|
295
|
+
} else {
|
|
296
|
+
// String/number/boolean serialize directly. Anything else (including
|
|
297
|
+
// undefined / objects) gets a typeof tag so we don't collide.
|
|
298
|
+
key +=
|
|
299
|
+
'|' +
|
|
300
|
+
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'
|
|
301
|
+
? String(v)
|
|
302
|
+
: v === undefined
|
|
303
|
+
? ''
|
|
304
|
+
: '~' + typeof v)
|
|
305
|
+
}
|
|
259
306
|
}
|
|
260
307
|
for (const k of ALL_PSEUDO_KEYS) {
|
|
261
308
|
const propV = propsRec[k]
|
|
@@ -307,12 +354,6 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
307
354
|
}
|
|
308
355
|
const themes = dimHelper.get(theme)
|
|
309
356
|
|
|
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
357
|
// Resolve mode-specific theme
|
|
317
358
|
const modeBaseHelper = ThemeManager.modeBaseTheme[mode]
|
|
318
359
|
if (modeBaseHelper.has(baseTheme)) {
|
|
@@ -529,8 +570,8 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
529
570
|
{
|
|
530
571
|
render,
|
|
531
572
|
mode,
|
|
532
|
-
isDark: mode === '
|
|
533
|
-
isLight: mode === '
|
|
573
|
+
isDark: mode === 'dark',
|
|
574
|
+
isLight: mode === 'light',
|
|
534
575
|
},
|
|
535
576
|
]),
|
|
536
577
|
})
|
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
|