@pyreon/rocketstyle 0.14.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 +67 -12
- package/lib/index.js +94 -45
- package/package.json +12 -11
- package/src/__tests__/attrs-overloads.test.ts +97 -0
- package/src/__tests__/cache-key-boolean-collision.test.ts +54 -0
- package/src/__tests__/native-marker.test.ts +9 -0
- package/src/__tests__/rocketstyle.browser.test.tsx +221 -0
- package/src/__tests__/rocketstyleIntegration.test.ts +134 -0
- package/src/constants/index.ts +1 -4
- package/src/context/context.ts +5 -1
- package/src/env.d.ts +6 -0
- package/src/index.ts +2 -1
- package/src/rocketstyle.ts +177 -67
- 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.map +0 -1
- package/lib/index.js.map +0 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as _pyreon_core0 from "@pyreon/core";
|
|
1
|
+
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
2
|
import { VNodeChild } from "@pyreon/core";
|
|
3
3
|
import { config, context, render } from "@pyreon/ui-core";
|
|
4
4
|
|
|
@@ -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.
|
|
@@ -222,16 +258,29 @@ type Configuration<C = ElementType | unknown, D extends Dimensions = Dimensions>
|
|
|
222
258
|
statics: Record<string, any>;
|
|
223
259
|
} & Record<string, any>;
|
|
224
260
|
type DefaultProps = Partial<PseudoProps> & {
|
|
225
|
-
children?: _pyreon_core0.VNodeChild;
|
|
261
|
+
children?: _$_pyreon_core0.VNodeChild;
|
|
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
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createContext, provide, useContext } from "@pyreon/core";
|
|
1
|
+
import { createContext, nativeCompat, provide, useContext } from "@pyreon/core";
|
|
2
2
|
import { Provider as Provider$1, compose, config, context, get, hoistNonReactStatics, isEmpty, merge, omit, pick, render, set } from "@pyreon/ui-core";
|
|
3
3
|
import { signal } from "@pyreon/reactivity";
|
|
4
4
|
|
|
5
5
|
//#region src/constants/index.ts
|
|
6
6
|
/** Tree-shakeable dev-mode flag. `true` in dev, `false` (dead code eliminated) in prod. */
|
|
7
|
-
const __DEV__ =
|
|
7
|
+
const __DEV__ = process.env.NODE_ENV !== "production";
|
|
8
8
|
/** Default theme mode used when no mode is provided via context. */
|
|
9
9
|
const MODE_DEFAULT = "light";
|
|
10
10
|
/** Pseudo-state interaction keys tracked for styling (hover, active, focus, pressed). */
|
|
@@ -74,6 +74,7 @@ const Provider = ({ provider = Provider$1, inversed, ...props }) => {
|
|
|
74
74
|
children
|
|
75
75
|
}) ?? null;
|
|
76
76
|
};
|
|
77
|
+
nativeCompat(Provider);
|
|
77
78
|
|
|
78
79
|
//#endregion
|
|
79
80
|
//#region src/constants/defaultDimensions.ts
|
|
@@ -471,23 +472,38 @@ const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) =
|
|
|
471
472
|
//#endregion
|
|
472
473
|
//#region src/rocketstyle.ts
|
|
473
474
|
const _countSink = globalThis;
|
|
474
|
-
/**
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
};
|
|
491
507
|
const rocketComponent = (options) => {
|
|
492
508
|
const { component, styles } = options;
|
|
493
509
|
const { styled } = config;
|
|
@@ -510,6 +526,8 @@ const rocketComponent = (options) => {
|
|
|
510
526
|
...options.filterAttrs ?? []
|
|
511
527
|
];
|
|
512
528
|
const _omitSetCache = /* @__PURE__ */ new WeakMap();
|
|
529
|
+
const _rsMemo = /* @__PURE__ */ new WeakMap();
|
|
530
|
+
const RS_MEMO_CAP = 32;
|
|
513
531
|
const hocsFuncs = [rocketStyleHOC(options), ...calculateHocsFuncs(options.compose)];
|
|
514
532
|
const EnhancedComponent = (props) => {
|
|
515
533
|
const localCtx = useLocalContext(options.consumer);
|
|
@@ -527,7 +545,7 @@ const rocketComponent = (options) => {
|
|
|
527
545
|
})();
|
|
528
546
|
let dimResult = _dimensionsCache.get(initialDimensionThemes);
|
|
529
547
|
if (dimResult) {
|
|
530
|
-
if (
|
|
548
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.dimensionsMap.hit");
|
|
531
549
|
} else {
|
|
532
550
|
dimResult = getDimensionsMap({
|
|
533
551
|
themes: initialDimensionThemes,
|
|
@@ -541,59 +559,90 @@ const rocketComponent = (options) => {
|
|
|
541
559
|
RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames);
|
|
542
560
|
_reservedKeysCache.set(reservedPropNames, RESERVED_STYLING_PROPS_KEYS);
|
|
543
561
|
}
|
|
544
|
-
const
|
|
545
|
-
|
|
562
|
+
const localPseudo = localCtx?.pseudo;
|
|
563
|
+
const _resolveRsEntry = () => {
|
|
546
564
|
const theme = themeAttrs.theme;
|
|
547
565
|
const mode = themeAttrs.mode;
|
|
566
|
+
const propsRec = props;
|
|
567
|
+
const rocketstateRaw = _calculateStylingAttrs({
|
|
568
|
+
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
569
|
+
dimensions
|
|
570
|
+
});
|
|
571
|
+
let key = mode;
|
|
572
|
+
for (const dimName in dimensions) {
|
|
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);
|
|
576
|
+
}
|
|
577
|
+
for (const k of ALL_PSEUDO_KEYS) {
|
|
578
|
+
const propV = propsRec[k];
|
|
579
|
+
const localV = localPseudo?.[k];
|
|
580
|
+
const v = propV !== void 0 ? propV : localV;
|
|
581
|
+
key += "|" + (v === void 0 ? "" : v ? "1" : "0");
|
|
582
|
+
}
|
|
583
|
+
let themeMemo = _rsMemo.get(theme);
|
|
584
|
+
if (!themeMemo) {
|
|
585
|
+
themeMemo = /* @__PURE__ */ new Map();
|
|
586
|
+
_rsMemo.set(theme, themeMemo);
|
|
587
|
+
}
|
|
588
|
+
const cached = themeMemo.get(key);
|
|
589
|
+
if (cached) {
|
|
590
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.dimensionMemo.hit");
|
|
591
|
+
themeMemo.delete(key);
|
|
592
|
+
themeMemo.set(key, cached);
|
|
593
|
+
return cached;
|
|
594
|
+
}
|
|
595
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.getTheme");
|
|
548
596
|
const baseThemeHelper = ThemeManager$1.baseTheme;
|
|
549
597
|
if (baseThemeHelper.has(theme)) {
|
|
550
|
-
if (
|
|
598
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
551
599
|
} else baseThemeHelper.set(theme, getThemeFromChain(options.theme, theme));
|
|
552
600
|
const baseTheme = baseThemeHelper.get(theme);
|
|
553
601
|
const dimHelper = ThemeManager$1.dimensionsThemes;
|
|
554
602
|
if (dimHelper.has(theme)) {
|
|
555
|
-
if (
|
|
603
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
556
604
|
} else dimHelper.set(theme, getDimensionThemes(theme, options));
|
|
557
605
|
const themes = dimHelper.get(theme);
|
|
558
|
-
const rocketstate = _calculateStylingAttrs({
|
|
559
|
-
props: pickStyledAttrs(props, reservedPropNames),
|
|
560
|
-
dimensions
|
|
561
|
-
});
|
|
562
606
|
const modeBaseHelper = ThemeManager$1.modeBaseTheme[mode];
|
|
563
607
|
if (modeBaseHelper.has(baseTheme)) {
|
|
564
|
-
if (
|
|
608
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
565
609
|
} else modeBaseHelper.set(baseTheme, getThemeByMode(baseTheme, mode));
|
|
566
610
|
const currentModeBaseTheme = modeBaseHelper.get(baseTheme);
|
|
567
611
|
const modeDimHelper = ThemeManager$1.modeDimensionTheme[mode];
|
|
568
612
|
if (modeDimHelper.has(themes)) {
|
|
569
|
-
if (
|
|
613
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
570
614
|
} else modeDimHelper.set(themes, getThemeByMode(themes, mode));
|
|
571
|
-
|
|
572
|
-
rocketstate,
|
|
615
|
+
const rocketstyle = getTheme({
|
|
616
|
+
rocketstate: rocketstateRaw,
|
|
573
617
|
themes: modeDimHelper.get(themes),
|
|
574
618
|
baseTheme: currentModeBaseTheme,
|
|
575
619
|
transformKeys: options.transformKeys,
|
|
576
620
|
appTheme: theme
|
|
577
621
|
});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const rocketstate = _calculateStylingAttrs({
|
|
582
|
-
props: pickStyledAttrs(props, reservedPropNames),
|
|
583
|
-
dimensions
|
|
584
|
-
});
|
|
585
|
-
const propPseudo = pick(props, ALL_PSEUDO_KEYS);
|
|
586
|
-
return {
|
|
587
|
-
...rocketstate,
|
|
622
|
+
const propPseudo = pick(propsRec, ALL_PSEUDO_KEYS);
|
|
623
|
+
const rocketstate = {
|
|
624
|
+
...rocketstateRaw,
|
|
588
625
|
pseudo: {
|
|
589
626
|
...localPseudo,
|
|
590
627
|
...propPseudo
|
|
591
628
|
}
|
|
592
629
|
};
|
|
630
|
+
if (themeMemo.size >= RS_MEMO_CAP) {
|
|
631
|
+
const oldestKey = themeMemo.keys().next().value;
|
|
632
|
+
if (oldestKey !== void 0) themeMemo.delete(oldestKey);
|
|
633
|
+
}
|
|
634
|
+
const entry = {
|
|
635
|
+
rocketstyle,
|
|
636
|
+
rocketstate
|
|
637
|
+
};
|
|
638
|
+
themeMemo.set(key, entry);
|
|
639
|
+
return entry;
|
|
593
640
|
};
|
|
641
|
+
const $rocketstyleAccessor = () => _resolveRsEntry().rocketstyle;
|
|
642
|
+
const $rocketstateAccessor = () => _resolveRsEntry().rocketstate;
|
|
594
643
|
let omitSet = _omitSetCache.get(RESERVED_STYLING_PROPS_KEYS);
|
|
595
644
|
if (omitSet) {
|
|
596
|
-
if (
|
|
645
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.omitSet.hit");
|
|
597
646
|
} else {
|
|
598
647
|
omitSet = new Set([...RESERVED_STYLING_PROPS_KEYS, ...STATIC_OMIT_KEYS]);
|
|
599
648
|
_omitSetCache.set(RESERVED_STYLING_PROPS_KEYS, omitSet);
|
|
@@ -682,8 +731,8 @@ const rocketComponent = (options) => {
|
|
|
682
731
|
{
|
|
683
732
|
render,
|
|
684
733
|
mode,
|
|
685
|
-
isDark: mode === "
|
|
686
|
-
isLight: mode === "
|
|
734
|
+
isDark: mode === "dark",
|
|
735
|
+
isLight: mode === "light"
|
|
687
736
|
}
|
|
688
737
|
])
|
|
689
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": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"lib",
|
|
13
|
+
"!lib/**/*.map",
|
|
13
14
|
"!lib/analysis",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE",
|
|
@@ -41,19 +42,19 @@
|
|
|
41
42
|
"typecheck": "tsc --noEmit"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@pyreon/test-utils": "^0.13.
|
|
45
|
-
"@pyreon/typescript": "^0.
|
|
46
|
-
"@pyreon/ui-core": "^0.
|
|
45
|
+
"@pyreon/test-utils": "^0.13.3",
|
|
46
|
+
"@pyreon/typescript": "^0.16.0",
|
|
47
|
+
"@pyreon/ui-core": "^0.16.0",
|
|
47
48
|
"@vitest/browser-playwright": "^4.1.4",
|
|
48
|
-
"@vitus-labs/tools-rolldown": "^
|
|
49
|
-
},
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"@pyreon/core": "^0.14.0",
|
|
52
|
-
"@pyreon/reactivity": "^0.14.0",
|
|
53
|
-
"@pyreon/styler": "^0.14.0",
|
|
54
|
-
"@pyreon/ui-core": "^0.14.0"
|
|
49
|
+
"@vitus-labs/tools-rolldown": "^2.3.0"
|
|
55
50
|
},
|
|
56
51
|
"engines": {
|
|
57
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"
|
|
58
59
|
}
|
|
59
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
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isNativeCompat } from '@pyreon/core'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import RocketstyleProvider from '../context/context'
|
|
4
|
+
|
|
5
|
+
describe('native-compat marker — @pyreon/rocketstyle', () => {
|
|
6
|
+
it('Provider is marked native', () => {
|
|
7
|
+
expect(isNativeCompat(RocketstyleProvider)).toBe(true)
|
|
8
|
+
})
|
|
9
|
+
})
|