@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/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
|
@@ -239,7 +239,31 @@ const useThemeAttrs = ({ inversed }) => {
|
|
|
239
239
|
//#region src/utils/attrs.ts
|
|
240
240
|
const removeUndefinedProps = (props) => {
|
|
241
241
|
const result = {};
|
|
242
|
-
|
|
242
|
+
const descriptors = Object.getOwnPropertyDescriptors(props);
|
|
243
|
+
for (const key of Object.keys(descriptors)) {
|
|
244
|
+
const d = descriptors[key];
|
|
245
|
+
if (d.get || d.value !== void 0) Object.defineProperty(result, key, d);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Like `Object.assign(target, ...sources)` but copies own property
|
|
251
|
+
* DESCRIPTORS instead of reading + writing values. Later sources
|
|
252
|
+
* override earlier ones (same semantics as spread / Object.assign).
|
|
253
|
+
*
|
|
254
|
+
* Required for reactive-prop preservation through the rocketstyle
|
|
255
|
+
* pipeline: a plain `{ ...A, ...B }` spread fires every getter on A
|
|
256
|
+
* and B and stores the resolved value, breaking the reactive
|
|
257
|
+
* subscription. This helper copies descriptors so getters survive
|
|
258
|
+
* the merge.
|
|
259
|
+
*/
|
|
260
|
+
const mergeDescriptors = (...sources) => {
|
|
261
|
+
const result = {};
|
|
262
|
+
for (const source of sources) {
|
|
263
|
+
if (!source) continue;
|
|
264
|
+
const descriptors = Object.getOwnPropertyDescriptors(source);
|
|
265
|
+
for (const key of Object.keys(descriptors)) Object.defineProperty(result, key, descriptors[key]);
|
|
266
|
+
}
|
|
243
267
|
return result;
|
|
244
268
|
};
|
|
245
269
|
/** Picks only the props whose keys exist in the dimension keywords lookup and have truthy values. */
|
|
@@ -301,15 +325,7 @@ const rocketStyleHOC = ({ inversed, attrs, priorityAttrs }) => {
|
|
|
301
325
|
isLight: themeAttrs.isLight
|
|
302
326
|
}];
|
|
303
327
|
const prioritizedAttrs = calculatePriorityAttrs([filteredProps, ...callbackParams]);
|
|
304
|
-
|
|
305
|
-
...prioritizedAttrs,
|
|
306
|
-
...filteredProps
|
|
307
|
-
}, ...callbackParams]);
|
|
308
|
-
return WrappedComponent({
|
|
309
|
-
...prioritizedAttrs,
|
|
310
|
-
...finalAttrs,
|
|
311
|
-
...filteredProps
|
|
312
|
-
});
|
|
328
|
+
return WrappedComponent(mergeDescriptors(prioritizedAttrs, calculateAttrs([mergeDescriptors(prioritizedAttrs, filteredProps), ...callbackParams]), filteredProps));
|
|
313
329
|
};
|
|
314
330
|
return HOCComponent;
|
|
315
331
|
};
|
|
@@ -472,23 +488,38 @@ const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) =
|
|
|
472
488
|
//#endregion
|
|
473
489
|
//#region src/rocketstyle.ts
|
|
474
490
|
const _countSink = globalThis;
|
|
475
|
-
/**
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Clones the current configuration and merges new options, returning a fresh
|
|
493
|
+
* rocketComponent.
|
|
494
|
+
*
|
|
495
|
+
* Component-swap reset: when `opts.component` is set AND differs from the
|
|
496
|
+
* current `defaultOpts.component`, the prior `attrs`, `priorityAttrs`,
|
|
497
|
+
* `filterAttrs`, and `compose` chains are dropped — they were tailored to the
|
|
498
|
+
* previous component's prop shape, and applying them to a different component
|
|
499
|
+
* silently leaks invalid props through to the DOM (e.g. `disabled` on an
|
|
500
|
+
* `<a>`). Callers who want to preserve them must re-chain explicitly:
|
|
501
|
+
*
|
|
502
|
+
* const NewBtn = Button.config({ component: 'a' }).attrs(sharedAttrs)
|
|
503
|
+
*/
|
|
504
|
+
const cloneAndEnhance = (defaultOpts, opts) => {
|
|
505
|
+
const componentChanged = opts.component != null && opts.component !== defaultOpts.component;
|
|
506
|
+
return rocketComponent({
|
|
507
|
+
...defaultOpts,
|
|
508
|
+
attrs: componentChanged ? chainOptions(opts.attrs, []) : chainOptions(opts.attrs, defaultOpts.attrs),
|
|
509
|
+
filterAttrs: componentChanged ? [...opts.filterAttrs ?? []] : [...defaultOpts.filterAttrs ?? [], ...opts.filterAttrs ?? []],
|
|
510
|
+
priorityAttrs: componentChanged ? chainOptions(opts.priorityAttrs, []) : chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
|
|
511
|
+
statics: {
|
|
512
|
+
...defaultOpts.statics,
|
|
513
|
+
...opts.statics
|
|
514
|
+
},
|
|
515
|
+
compose: componentChanged ? { ...opts.compose } : {
|
|
516
|
+
...defaultOpts.compose,
|
|
517
|
+
...opts.compose
|
|
518
|
+
},
|
|
519
|
+
...chainOrOptions(CONFIG_KEYS, opts, defaultOpts),
|
|
520
|
+
...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts)
|
|
521
|
+
});
|
|
522
|
+
};
|
|
492
523
|
const rocketComponent = (options) => {
|
|
493
524
|
const { component, styles } = options;
|
|
494
525
|
const { styled } = config;
|
|
@@ -548,11 +579,16 @@ const rocketComponent = (options) => {
|
|
|
548
579
|
const _resolveRsEntry = () => {
|
|
549
580
|
const theme = themeAttrs.theme;
|
|
550
581
|
const mode = themeAttrs.mode;
|
|
551
|
-
let key = mode;
|
|
552
582
|
const propsRec = props;
|
|
583
|
+
const rocketstateRaw = _calculateStylingAttrs({
|
|
584
|
+
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
585
|
+
dimensions
|
|
586
|
+
});
|
|
587
|
+
let key = mode;
|
|
553
588
|
for (const dimName in dimensions) {
|
|
554
|
-
const v =
|
|
555
|
-
key += "|" + (
|
|
589
|
+
const v = rocketstateRaw[dimName];
|
|
590
|
+
if (Array.isArray(v)) key += "|" + (v.length === 0 ? "" : v.slice().sort().join(","));
|
|
591
|
+
else key += "|" + (typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? String(v) : v === void 0 ? "" : "~" + typeof v);
|
|
556
592
|
}
|
|
557
593
|
for (const k of ALL_PSEUDO_KEYS) {
|
|
558
594
|
const propV = propsRec[k];
|
|
@@ -583,10 +619,6 @@ const rocketComponent = (options) => {
|
|
|
583
619
|
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
584
620
|
} else dimHelper.set(theme, getDimensionThemes(theme, options));
|
|
585
621
|
const themes = dimHelper.get(theme);
|
|
586
|
-
const rocketstateRaw = _calculateStylingAttrs({
|
|
587
|
-
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
588
|
-
dimensions
|
|
589
|
-
});
|
|
590
622
|
const modeBaseHelper = ThemeManager$1.modeBaseTheme[mode];
|
|
591
623
|
if (modeBaseHelper.has(baseTheme)) {
|
|
592
624
|
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
@@ -631,20 +663,34 @@ const rocketComponent = (options) => {
|
|
|
631
663
|
omitSet = new Set([...RESERVED_STYLING_PROPS_KEYS, ...STATIC_OMIT_KEYS]);
|
|
632
664
|
_omitSetCache.set(RESERVED_STYLING_PROPS_KEYS, omitSet);
|
|
633
665
|
}
|
|
634
|
-
const mergeProps = localCtx ?
|
|
635
|
-
...localCtx,
|
|
636
|
-
...props
|
|
637
|
-
} : props;
|
|
666
|
+
const mergeProps = localCtx ? mergeDescriptors(localCtx, props) : props;
|
|
638
667
|
const finalProps = omit(mergeProps, omitSet);
|
|
639
668
|
if (options.passProps) {
|
|
640
669
|
const passed = pick(mergeProps, options.passProps);
|
|
641
|
-
|
|
670
|
+
const passedDescriptors = Object.getOwnPropertyDescriptors(passed);
|
|
671
|
+
for (const k of Object.keys(passedDescriptors)) Object.defineProperty(finalProps, k, passedDescriptors[k]);
|
|
642
672
|
}
|
|
643
|
-
|
|
644
|
-
finalProps
|
|
645
|
-
finalProps
|
|
673
|
+
const refDescriptor = Object.getOwnPropertyDescriptor(props, "ref");
|
|
674
|
+
if (refDescriptor) Object.defineProperty(finalProps, "ref", refDescriptor);
|
|
675
|
+
Object.defineProperty(finalProps, "$rocketstyle", {
|
|
676
|
+
value: $rocketstyleAccessor,
|
|
677
|
+
writable: true,
|
|
678
|
+
enumerable: true,
|
|
679
|
+
configurable: true
|
|
680
|
+
});
|
|
681
|
+
Object.defineProperty(finalProps, "$rocketstate", {
|
|
682
|
+
value: $rocketstateAccessor,
|
|
683
|
+
writable: true,
|
|
684
|
+
enumerable: true,
|
|
685
|
+
configurable: true
|
|
686
|
+
});
|
|
646
687
|
if (__DEV__) {
|
|
647
|
-
finalProps
|
|
688
|
+
Object.defineProperty(finalProps, "data-rocketstyle", {
|
|
689
|
+
value: componentName,
|
|
690
|
+
writable: true,
|
|
691
|
+
enumerable: true,
|
|
692
|
+
configurable: true
|
|
693
|
+
});
|
|
648
694
|
if (options.DEBUG) {
|
|
649
695
|
const debugPayload = {
|
|
650
696
|
component: componentName,
|
|
@@ -715,8 +761,8 @@ const rocketComponent = (options) => {
|
|
|
715
761
|
{
|
|
716
762
|
render,
|
|
717
763
|
mode,
|
|
718
|
-
isDark: mode === "
|
|
719
|
-
isLight: mode === "
|
|
764
|
+
isDark: mode === "dark",
|
|
765
|
+
isLight: mode === "light"
|
|
720
766
|
}
|
|
721
767
|
])
|
|
722
768
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/rocketstyle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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.5",
|
|
46
|
+
"@pyreon/typescript": "^0.18.0",
|
|
47
|
+
"@pyreon/ui-core": "^0.18.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.18.0",
|
|
56
|
+
"@pyreon/reactivity": "^0.18.0",
|
|
57
|
+
"@pyreon/styler": "^0.18.0",
|
|
58
|
+
"@pyreon/ui-core": "^0.18.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
|
+
})
|