@pyreon/rocketstyle 0.13.1 → 0.15.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/README.md +17 -1
- package/lib/index.d.ts +16 -2
- package/lib/index.js +151 -82
- package/package.json +10 -9
- package/src/__tests__/context.test.ts +15 -15
- package/src/__tests__/createLocalProvider.test.ts +33 -1
- package/src/__tests__/native-marker.test.ts +9 -0
- package/src/__tests__/rocketstyle.browser.test.tsx +36 -0
- package/src/constants/index.ts +7 -0
- package/src/context/context.ts +5 -1
- package/src/env.d.ts +6 -0
- package/src/init.ts +2 -2
- package/src/rocketstyle.ts +230 -77
- package/src/types/rocketstyle.ts +15 -0
- package/src/utils/attrs.ts +25 -29
- package/src/utils/theme.ts +47 -44
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Organize component styles by dimensions — states, sizes, variants — instead
|
|
|
12
12
|
- **Pseudo-state detection** — hover, focus, pressed tracked via signals and context
|
|
13
13
|
- **Light/dark mode** — theme callbacks receive a mode parameter
|
|
14
14
|
- **Provider/Consumer** — propagate parent state to children through context
|
|
15
|
-
- **WeakMap caching** —
|
|
15
|
+
- **Multi-tier WeakMap caching** — dimension maps, reserved keys, omit Sets, and theme results cached per component definition (shared across all instances). Per-mount allocations near zero for same-definition components
|
|
16
16
|
- **TypeScript inference** — dimension values and prop types inferred through the chain
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
@@ -337,6 +337,22 @@ Button.theme((theme, mode) => ({
|
|
|
337
337
|
|
|
338
338
|
Use `inversed: true` in `.config()` to flip the mode for a component subtree.
|
|
339
339
|
|
|
340
|
+
## Performance
|
|
341
|
+
|
|
342
|
+
Rocketstyle uses a multi-tier caching architecture to minimize per-mount allocations:
|
|
343
|
+
|
|
344
|
+
- **Per-definition caches** (shared across all instances via `WeakMap`):
|
|
345
|
+
- `_dimensionsCache` — `getDimensionsMap` result keyed on dimension-themes identity
|
|
346
|
+
- `_reservedKeysCache` — `Object.keys(reservedPropNames)` keyed on keywords identity
|
|
347
|
+
- `_omitSetCache` — pre-built `Set<string>` for `omit()` (avoids per-mount Set construction)
|
|
348
|
+
- `ALL_PSEUDO_KEYS` / `STATIC_OMIT_KEYS` — merged key arrays computed once
|
|
349
|
+
- **Theme cache** (`LocalThemeManager`): `WeakMap` tiers for baseTheme, dimensionThemes, and per-mode resolved themes
|
|
350
|
+
- **getTheme in-place merge**: dimension slices merged directly onto `finalTheme` instead of allocating a new target per `merge()` call
|
|
351
|
+
- **Frozen `EMPTY_PSEUDO`**: shared frozen `{}` for pseudo-state defaults instead of 6 allocations per call
|
|
352
|
+
- **Dev guard**: uses `__DEV__` (`import.meta.env.DEV`) — tree-shaken to zero bytes in production
|
|
353
|
+
|
|
354
|
+
For a 150-component page with 8 dimensions each: ~1,350 Set allocations, ~300 array spreads, and ~150 map rebuilds eliminated vs naive implementation.
|
|
355
|
+
|
|
340
356
|
## Peer Dependencies
|
|
341
357
|
|
|
342
358
|
| Package | Version |
|
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
|
|
|
@@ -222,7 +222,7 @@ type Configuration<C = ElementType | unknown, D extends Dimensions = Dimensions>
|
|
|
222
222
|
statics: Record<string, any>;
|
|
223
223
|
} & Record<string, any>;
|
|
224
224
|
type DefaultProps = Partial<PseudoProps> & {
|
|
225
|
-
children?: _pyreon_core0.VNodeChild;
|
|
225
|
+
children?: _$_pyreon_core0.VNodeChild;
|
|
226
226
|
};
|
|
227
227
|
//#endregion
|
|
228
228
|
//#region src/types/attrs.d.ts
|
|
@@ -283,6 +283,20 @@ interface IRocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T ex
|
|
|
283
283
|
readonly $$types: DFP;
|
|
284
284
|
IS_ROCKETSTYLE: true;
|
|
285
285
|
displayName: string;
|
|
286
|
+
/**
|
|
287
|
+
* The accumulated `.attrs()` callback chain — hoisted at component-creation
|
|
288
|
+
* time so external inspectors (notably `extractDocumentTree` from
|
|
289
|
+
* `@pyreon/connector-document`) can compute post-attrs props without
|
|
290
|
+
* invoking the full styled wrapper. Each callback maps user props to a
|
|
291
|
+
* partial props object; `chain.reduce(Object.assign, {})` produces the
|
|
292
|
+
* post-attrs result.
|
|
293
|
+
*
|
|
294
|
+
* Stable contract — consumers can rely on this property being present on
|
|
295
|
+
* every rocketstyle-wrapped component. Empty array when no `.attrs()`
|
|
296
|
+
* was ever called on the chain. Treat as read-only; mutating breaks
|
|
297
|
+
* `extractDocumentTree` and any other inspector consuming the hoist.
|
|
298
|
+
*/
|
|
299
|
+
readonly __rs_attrs: ReadonlyArray<(props: Record<string, unknown>) => Record<string, unknown>>;
|
|
286
300
|
}
|
|
287
301
|
//#endregion
|
|
288
302
|
//#region src/types/rocketComponent.d.ts
|
package/lib/index.js
CHANGED
|
@@ -1,8 +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
|
+
/** Tree-shakeable dev-mode flag. `true` in dev, `false` (dead code eliminated) in prod. */
|
|
7
|
+
const __DEV__ = process.env.NODE_ENV !== "production";
|
|
6
8
|
/** Default theme mode used when no mode is provided via context. */
|
|
7
9
|
const MODE_DEFAULT = "light";
|
|
8
10
|
/** Pseudo-state interaction keys tracked for styling (hover, active, focus, pressed). */
|
|
@@ -72,6 +74,7 @@ const Provider = ({ provider = Provider$1, inversed, ...props }) => {
|
|
|
72
74
|
children
|
|
73
75
|
}) ?? null;
|
|
74
76
|
};
|
|
77
|
+
nativeCompat(Provider);
|
|
75
78
|
|
|
76
79
|
//#endregion
|
|
77
80
|
//#region src/constants/defaultDimensions.ts
|
|
@@ -251,31 +254,24 @@ const calculateChainOptions = (options) => (args) => {
|
|
|
251
254
|
};
|
|
252
255
|
const calculateStylingAttrs = ({ useBooleans, multiKeys }) => ({ props, dimensions }) => {
|
|
253
256
|
const result = {};
|
|
254
|
-
|
|
257
|
+
for (const item in dimensions) {
|
|
255
258
|
const pickedProp = props[item];
|
|
256
259
|
const t = typeof pickedProp;
|
|
257
260
|
if (multiKeys?.[item] && Array.isArray(pickedProp)) result[item] = pickedProp;
|
|
258
261
|
else if (t === "string" || t === "number") result[item] = pickedProp;
|
|
259
262
|
else result[item] = void 0;
|
|
260
|
-
}
|
|
261
|
-
if (useBooleans) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
newDimensionValue = k;
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
result[key] = newDimensionValue;
|
|
277
|
-
}
|
|
278
|
-
});
|
|
263
|
+
}
|
|
264
|
+
if (useBooleans) for (const key in result) {
|
|
265
|
+
if (result[key]) continue;
|
|
266
|
+
const dimensionMap = dimensions[key];
|
|
267
|
+
const isMultiKey = multiKeys?.[key];
|
|
268
|
+
let newDimensionValue;
|
|
269
|
+
if (isMultiKey) {
|
|
270
|
+
const matches = [];
|
|
271
|
+
for (const propKey in props) if (propKey in dimensionMap) matches.push(propKey);
|
|
272
|
+
newDimensionValue = matches.length > 0 ? matches : void 0;
|
|
273
|
+
} else for (const k in props) if (k in dimensionMap && props[k]) newDimensionValue = k;
|
|
274
|
+
result[key] = newDimensionValue;
|
|
279
275
|
}
|
|
280
276
|
return result;
|
|
281
277
|
};
|
|
@@ -428,36 +424,41 @@ const getThemeFromChain = (options, theme) => {
|
|
|
428
424
|
return options.reduce((acc, item) => merge(acc, item(theme, themeModeCallback, config.css)), result);
|
|
429
425
|
};
|
|
430
426
|
const getDimensionThemes = (theme, options) => {
|
|
427
|
+
const dims = options.dimensions;
|
|
428
|
+
if (isEmpty(dims)) return {};
|
|
431
429
|
const result = {};
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const [, dimension] = isMultiKey(value);
|
|
430
|
+
for (const key in dims) {
|
|
431
|
+
const [, dimension] = isMultiKey(dims[key]);
|
|
435
432
|
const helper = options[key];
|
|
436
|
-
if (Array.isArray(helper) && helper.length > 0)
|
|
437
|
-
|
|
438
|
-
|
|
433
|
+
if (Array.isArray(helper) && helper.length > 0) result[dimension] = removeNullableValues(getThemeFromChain(helper, theme));
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
439
436
|
};
|
|
437
|
+
const EMPTY_PSEUDO = Object.freeze({});
|
|
440
438
|
const getTheme = ({ rocketstate, themes, baseTheme, transformKeys, appTheme }) => {
|
|
441
|
-
|
|
439
|
+
const finalTheme = { ...baseTheme };
|
|
442
440
|
const deferredTransforms = [];
|
|
443
|
-
|
|
441
|
+
for (const key in rocketstate) {
|
|
442
|
+
const value = rocketstate[key];
|
|
443
|
+
if (value == null) continue;
|
|
444
444
|
const keyTheme = themes[key] ?? {};
|
|
445
445
|
const isTransform = transformKeys?.[key];
|
|
446
446
|
const mergeValue = (item) => {
|
|
447
447
|
const val = keyTheme[item];
|
|
448
|
+
if (val == null) return;
|
|
448
449
|
if (isTransform && typeof val === "function") deferredTransforms.push(val);
|
|
449
|
-
else
|
|
450
|
+
else merge(finalTheme, val);
|
|
450
451
|
};
|
|
451
|
-
if (Array.isArray(value)) value.
|
|
452
|
+
if (Array.isArray(value)) for (let i = 0; i < value.length; i++) mergeValue(value[i]);
|
|
452
453
|
else mergeValue(value);
|
|
453
|
-
}
|
|
454
|
-
for (
|
|
455
|
-
finalTheme.hover ??=
|
|
456
|
-
finalTheme.focus ??=
|
|
457
|
-
finalTheme.active ??=
|
|
458
|
-
finalTheme.disabled ??=
|
|
459
|
-
finalTheme.pressed ??=
|
|
460
|
-
finalTheme.readOnly ??=
|
|
454
|
+
}
|
|
455
|
+
for (let i = 0; i < deferredTransforms.length; i++) merge(finalTheme, deferredTransforms[i](finalTheme, appTheme ?? {}, themeModeCallback, config.css));
|
|
456
|
+
finalTheme.hover ??= EMPTY_PSEUDO;
|
|
457
|
+
finalTheme.focus ??= EMPTY_PSEUDO;
|
|
458
|
+
finalTheme.active ??= EMPTY_PSEUDO;
|
|
459
|
+
finalTheme.disabled ??= EMPTY_PSEUDO;
|
|
460
|
+
finalTheme.pressed ??= EMPTY_PSEUDO;
|
|
461
|
+
finalTheme.readOnly ??= EMPTY_PSEUDO;
|
|
461
462
|
return finalTheme;
|
|
462
463
|
};
|
|
463
464
|
const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) => {
|
|
@@ -470,6 +471,7 @@ const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) =
|
|
|
470
471
|
|
|
471
472
|
//#endregion
|
|
472
473
|
//#region src/rocketstyle.ts
|
|
474
|
+
const _countSink = globalThis;
|
|
473
475
|
/** Clones the current configuration and merges new options, returning a fresh rocketComponent. */
|
|
474
476
|
const cloneAndEnhance = (defaultOpts, opts) => rocketComponent({
|
|
475
477
|
...defaultOpts,
|
|
@@ -500,6 +502,17 @@ const rocketComponent = (options) => {
|
|
|
500
502
|
`;
|
|
501
503
|
const RenderComponent = options.provider ? createLocalProvider(STYLED_COMPONENT) : STYLED_COMPONENT;
|
|
502
504
|
const ThemeManager$1 = new ThemeManager();
|
|
505
|
+
const _dimensionsCache = /* @__PURE__ */ new WeakMap();
|
|
506
|
+
const _reservedKeysCache = /* @__PURE__ */ new WeakMap();
|
|
507
|
+
const ALL_PSEUDO_KEYS = [...PSEUDO_KEYS, ...PSEUDO_META_KEYS];
|
|
508
|
+
const STATIC_OMIT_KEYS = [
|
|
509
|
+
"pseudo",
|
|
510
|
+
...PSEUDO_KEYS,
|
|
511
|
+
...options.filterAttrs ?? []
|
|
512
|
+
];
|
|
513
|
+
const _omitSetCache = /* @__PURE__ */ new WeakMap();
|
|
514
|
+
const _rsMemo = /* @__PURE__ */ new WeakMap();
|
|
515
|
+
const RS_MEMO_CAP = 32;
|
|
503
516
|
const hocsFuncs = [rocketStyleHOC(options), ...calculateHocsFuncs(options.compose)];
|
|
504
517
|
const EnhancedComponent = (props) => {
|
|
505
518
|
const localCtx = useLocalContext(options.consumer);
|
|
@@ -510,72 +523,127 @@ const rocketComponent = (options) => {
|
|
|
510
523
|
if (!helper.has(initialTheme)) helper.set(initialTheme, getThemeFromChain(options.theme, initialTheme));
|
|
511
524
|
return helper.get(initialTheme);
|
|
512
525
|
})();
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
526
|
+
const initialDimensionThemes = (() => {
|
|
527
|
+
const helper = ThemeManager$1.dimensionsThemes;
|
|
528
|
+
if (!helper.has(initialTheme)) helper.set(initialTheme, getDimensionThemes(initialTheme, options));
|
|
529
|
+
return helper.get(initialTheme);
|
|
530
|
+
})();
|
|
531
|
+
let dimResult = _dimensionsCache.get(initialDimensionThemes);
|
|
532
|
+
if (dimResult) {
|
|
533
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.dimensionsMap.hit");
|
|
534
|
+
} else {
|
|
535
|
+
dimResult = getDimensionsMap({
|
|
536
|
+
themes: initialDimensionThemes,
|
|
537
|
+
useBooleans: options.useBooleans
|
|
538
|
+
});
|
|
539
|
+
_dimensionsCache.set(initialDimensionThemes, dimResult);
|
|
540
|
+
}
|
|
541
|
+
const { keysMap: dimensions, keywords: reservedPropNames } = dimResult;
|
|
542
|
+
let RESERVED_STYLING_PROPS_KEYS = _reservedKeysCache.get(reservedPropNames);
|
|
543
|
+
if (!RESERVED_STYLING_PROPS_KEYS) {
|
|
544
|
+
RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames);
|
|
545
|
+
_reservedKeysCache.set(reservedPropNames, RESERVED_STYLING_PROPS_KEYS);
|
|
546
|
+
}
|
|
547
|
+
const localPseudo = localCtx?.pseudo;
|
|
548
|
+
const _resolveRsEntry = () => {
|
|
523
549
|
const theme = themeAttrs.theme;
|
|
524
550
|
const mode = themeAttrs.mode;
|
|
551
|
+
let key = mode;
|
|
552
|
+
const propsRec = props;
|
|
553
|
+
for (const dimName in dimensions) {
|
|
554
|
+
const v = propsRec[dimName];
|
|
555
|
+
key += "|" + (typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? String(v) : v === void 0 ? "" : "~" + typeof v);
|
|
556
|
+
}
|
|
557
|
+
for (const k of ALL_PSEUDO_KEYS) {
|
|
558
|
+
const propV = propsRec[k];
|
|
559
|
+
const localV = localPseudo?.[k];
|
|
560
|
+
const v = propV !== void 0 ? propV : localV;
|
|
561
|
+
key += "|" + (v === void 0 ? "" : v ? "1" : "0");
|
|
562
|
+
}
|
|
563
|
+
let themeMemo = _rsMemo.get(theme);
|
|
564
|
+
if (!themeMemo) {
|
|
565
|
+
themeMemo = /* @__PURE__ */ new Map();
|
|
566
|
+
_rsMemo.set(theme, themeMemo);
|
|
567
|
+
}
|
|
568
|
+
const cached = themeMemo.get(key);
|
|
569
|
+
if (cached) {
|
|
570
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.dimensionMemo.hit");
|
|
571
|
+
themeMemo.delete(key);
|
|
572
|
+
themeMemo.set(key, cached);
|
|
573
|
+
return cached;
|
|
574
|
+
}
|
|
575
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.getTheme");
|
|
525
576
|
const baseThemeHelper = ThemeManager$1.baseTheme;
|
|
526
|
-
if (
|
|
577
|
+
if (baseThemeHelper.has(theme)) {
|
|
578
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
579
|
+
} else baseThemeHelper.set(theme, getThemeFromChain(options.theme, theme));
|
|
527
580
|
const baseTheme = baseThemeHelper.get(theme);
|
|
528
581
|
const dimHelper = ThemeManager$1.dimensionsThemes;
|
|
529
|
-
if (
|
|
582
|
+
if (dimHelper.has(theme)) {
|
|
583
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
584
|
+
} else dimHelper.set(theme, getDimensionThemes(theme, options));
|
|
530
585
|
const themes = dimHelper.get(theme);
|
|
531
|
-
const
|
|
532
|
-
props: pickStyledAttrs(
|
|
586
|
+
const rocketstateRaw = _calculateStylingAttrs({
|
|
587
|
+
props: pickStyledAttrs(propsRec, reservedPropNames),
|
|
533
588
|
dimensions
|
|
534
589
|
});
|
|
535
590
|
const modeBaseHelper = ThemeManager$1.modeBaseTheme[mode];
|
|
536
|
-
if (
|
|
591
|
+
if (modeBaseHelper.has(baseTheme)) {
|
|
592
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
593
|
+
} else modeBaseHelper.set(baseTheme, getThemeByMode(baseTheme, mode));
|
|
537
594
|
const currentModeBaseTheme = modeBaseHelper.get(baseTheme);
|
|
538
595
|
const modeDimHelper = ThemeManager$1.modeDimensionTheme[mode];
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
596
|
+
if (modeDimHelper.has(themes)) {
|
|
597
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
|
|
598
|
+
} else modeDimHelper.set(themes, getThemeByMode(themes, mode));
|
|
599
|
+
const rocketstyle = getTheme({
|
|
600
|
+
rocketstate: rocketstateRaw,
|
|
542
601
|
themes: modeDimHelper.get(themes),
|
|
543
602
|
baseTheme: currentModeBaseTheme,
|
|
544
603
|
transformKeys: options.transformKeys,
|
|
545
604
|
appTheme: theme
|
|
546
605
|
});
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const rocketstate = _calculateStylingAttrs({
|
|
551
|
-
props: pickStyledAttrs(props, reservedPropNames),
|
|
552
|
-
dimensions
|
|
553
|
-
});
|
|
554
|
-
const propPseudo = pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS]);
|
|
555
|
-
return {
|
|
556
|
-
...rocketstate,
|
|
606
|
+
const propPseudo = pick(propsRec, ALL_PSEUDO_KEYS);
|
|
607
|
+
const rocketstate = {
|
|
608
|
+
...rocketstateRaw,
|
|
557
609
|
pseudo: {
|
|
558
610
|
...localPseudo,
|
|
559
611
|
...propPseudo
|
|
560
612
|
}
|
|
561
613
|
};
|
|
614
|
+
if (themeMemo.size >= RS_MEMO_CAP) {
|
|
615
|
+
const oldestKey = themeMemo.keys().next().value;
|
|
616
|
+
if (oldestKey !== void 0) themeMemo.delete(oldestKey);
|
|
617
|
+
}
|
|
618
|
+
const entry = {
|
|
619
|
+
rocketstyle,
|
|
620
|
+
rocketstate
|
|
621
|
+
};
|
|
622
|
+
themeMemo.set(key, entry);
|
|
623
|
+
return entry;
|
|
562
624
|
};
|
|
563
|
-
const
|
|
625
|
+
const $rocketstyleAccessor = () => _resolveRsEntry().rocketstyle;
|
|
626
|
+
const $rocketstateAccessor = () => _resolveRsEntry().rocketstate;
|
|
627
|
+
let omitSet = _omitSetCache.get(RESERVED_STYLING_PROPS_KEYS);
|
|
628
|
+
if (omitSet) {
|
|
629
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.omitSet.hit");
|
|
630
|
+
} else {
|
|
631
|
+
omitSet = new Set([...RESERVED_STYLING_PROPS_KEYS, ...STATIC_OMIT_KEYS]);
|
|
632
|
+
_omitSetCache.set(RESERVED_STYLING_PROPS_KEYS, omitSet);
|
|
633
|
+
}
|
|
634
|
+
const mergeProps = localCtx ? {
|
|
564
635
|
...localCtx,
|
|
565
636
|
...props
|
|
566
|
-
};
|
|
567
|
-
const finalProps =
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
$rocketstate: $rocketstateAccessor
|
|
577
|
-
};
|
|
578
|
-
if (process.env.NODE_ENV !== "production") {
|
|
637
|
+
} : props;
|
|
638
|
+
const finalProps = omit(mergeProps, omitSet);
|
|
639
|
+
if (options.passProps) {
|
|
640
|
+
const passed = pick(mergeProps, options.passProps);
|
|
641
|
+
for (const k in passed) finalProps[k] = passed[k];
|
|
642
|
+
}
|
|
643
|
+
finalProps.ref = props.ref;
|
|
644
|
+
finalProps.$rocketstyle = $rocketstyleAccessor;
|
|
645
|
+
finalProps.$rocketstate = $rocketstateAccessor;
|
|
646
|
+
if (__DEV__) {
|
|
579
647
|
finalProps["data-rocketstyle"] = componentName;
|
|
580
648
|
if (options.DEBUG) {
|
|
581
649
|
const debugPayload = {
|
|
@@ -613,6 +681,7 @@ const rocketComponent = (options) => {
|
|
|
613
681
|
context: FinalComponent,
|
|
614
682
|
options: options.statics
|
|
615
683
|
});
|
|
684
|
+
FinalComponent.__rs_attrs = options.attrs ?? [];
|
|
616
685
|
Object.assign(FinalComponent, {
|
|
617
686
|
attrs: (attrs, { priority, filter } = {}) => {
|
|
618
687
|
const result = {};
|
|
@@ -669,7 +738,7 @@ const validateInit = (name, component, dimensions) => {
|
|
|
669
738
|
if (!isEmpty(errors)) throw Error(JSON.stringify(errors));
|
|
670
739
|
};
|
|
671
740
|
const rocketstyle = (({ dimensions = DEFAULT_DIMENSIONS, useBooleans = false } = {}) => ({ name, component }) => {
|
|
672
|
-
if (
|
|
741
|
+
if (__DEV__) validateInit(name, component, dimensions);
|
|
673
742
|
return rocketComponent({
|
|
674
743
|
name,
|
|
675
744
|
component,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/rocketstyle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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,17 +42,17 @@
|
|
|
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.2",
|
|
46
|
+
"@pyreon/typescript": "^0.15.0",
|
|
47
|
+
"@pyreon/ui-core": "^0.15.0",
|
|
47
48
|
"@vitest/browser-playwright": "^4.1.4",
|
|
48
|
-
"@vitus-labs/tools-rolldown": "^
|
|
49
|
+
"@vitus-labs/tools-rolldown": "^2.3.0"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
|
-
"@pyreon/core": "^0.
|
|
52
|
-
"@pyreon/reactivity": "^0.
|
|
53
|
-
"@pyreon/styler": "^0.
|
|
54
|
-
"@pyreon/ui-core": "^0.
|
|
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"
|
|
55
56
|
},
|
|
56
57
|
"engines": {
|
|
57
58
|
"node": ">= 22"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
-
import { useContext } from '@pyreon/core'
|
|
2
|
+
import { h, useContext } from '@pyreon/core'
|
|
3
3
|
import { Provider as CoreProvider } from '@pyreon/ui-core'
|
|
4
4
|
import Provider from '../context/context'
|
|
5
5
|
|
|
@@ -46,7 +46,7 @@ describe('Provider (context)', () => {
|
|
|
46
46
|
it('uses MODE_DEFAULT (light) when no mode is provided', () => {
|
|
47
47
|
mockedUseContext.mockReturnValue((() => ({})) as any)
|
|
48
48
|
|
|
49
|
-
const children =
|
|
49
|
+
const children = h('span', null, 'Hello')
|
|
50
50
|
Provider({ children })
|
|
51
51
|
|
|
52
52
|
expect(mockedCoreProvider).toHaveBeenCalledTimes(1)
|
|
@@ -59,7 +59,7 @@ describe('Provider (context)', () => {
|
|
|
59
59
|
it('passes mode directly when inversed is false', () => {
|
|
60
60
|
mockedUseContext.mockReturnValue((() => ({ mode: 'dark' })) as any)
|
|
61
61
|
|
|
62
|
-
const children =
|
|
62
|
+
const children = h('span', null, 'Hello')
|
|
63
63
|
Provider({ children, mode: 'dark', inversed: false })
|
|
64
64
|
|
|
65
65
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -69,7 +69,7 @@ describe('Provider (context)', () => {
|
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
it('passes mode directly when inversed is undefined', () => {
|
|
72
|
-
const children =
|
|
72
|
+
const children = h('span', null, 'Hello')
|
|
73
73
|
Provider({ children, mode: 'dark' })
|
|
74
74
|
|
|
75
75
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -79,7 +79,7 @@ describe('Provider (context)', () => {
|
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
it('inverts light to dark when inversed is true', () => {
|
|
82
|
-
const children =
|
|
82
|
+
const children = h('span', null, 'Hello')
|
|
83
83
|
Provider({ children, mode: 'light', inversed: true })
|
|
84
84
|
|
|
85
85
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -89,7 +89,7 @@ describe('Provider (context)', () => {
|
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
it('inverts dark to light when inversed is true', () => {
|
|
92
|
-
const children =
|
|
92
|
+
const children = h('span', null, 'Hello')
|
|
93
93
|
Provider({ children, mode: 'dark', inversed: true })
|
|
94
94
|
|
|
95
95
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -100,7 +100,7 @@ describe('Provider (context)', () => {
|
|
|
100
100
|
|
|
101
101
|
it('passes theme to provider when provided', () => {
|
|
102
102
|
const theme = { rootSize: 16, breakpoints: { sm: 576 } }
|
|
103
|
-
const children =
|
|
103
|
+
const children = h('span', null, 'Hello')
|
|
104
104
|
Provider({ children, theme })
|
|
105
105
|
|
|
106
106
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -108,7 +108,7 @@ describe('Provider (context)', () => {
|
|
|
108
108
|
})
|
|
109
109
|
|
|
110
110
|
it('does not pass theme key when theme is undefined', () => {
|
|
111
|
-
const children =
|
|
111
|
+
const children = h('span', null, 'Hello')
|
|
112
112
|
Provider({ children })
|
|
113
113
|
|
|
114
114
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -123,7 +123,7 @@ describe('Provider (context)', () => {
|
|
|
123
123
|
key: null,
|
|
124
124
|
}))
|
|
125
125
|
|
|
126
|
-
const children =
|
|
126
|
+
const children = h('span', null, 'Hello')
|
|
127
127
|
Provider({ children, provider: customProvider as any })
|
|
128
128
|
|
|
129
129
|
expect(customProvider).toHaveBeenCalledTimes(1)
|
|
@@ -132,7 +132,7 @@ describe('Provider (context)', () => {
|
|
|
132
132
|
})
|
|
133
133
|
|
|
134
134
|
it('defaults to CoreProvider when no provider prop is given', () => {
|
|
135
|
-
const children =
|
|
135
|
+
const children = h('span', null, 'Hello')
|
|
136
136
|
Provider({ children })
|
|
137
137
|
|
|
138
138
|
expect(mockedCoreProvider).toHaveBeenCalledTimes(1)
|
|
@@ -147,7 +147,7 @@ describe('Provider (context)', () => {
|
|
|
147
147
|
})
|
|
148
148
|
|
|
149
149
|
it('passes provider reference to the provider call', () => {
|
|
150
|
-
const children =
|
|
150
|
+
const children = h('span', null, 'Hello')
|
|
151
151
|
Provider({ children })
|
|
152
152
|
|
|
153
153
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -157,7 +157,7 @@ describe('Provider (context)', () => {
|
|
|
157
157
|
it('returns null when provider returns null', () => {
|
|
158
158
|
mockedCoreProvider.mockReturnValue(null as any)
|
|
159
159
|
|
|
160
|
-
const children =
|
|
160
|
+
const children = h('span', null, 'Hello')
|
|
161
161
|
const result = Provider({ children })
|
|
162
162
|
|
|
163
163
|
expect(result).toBeNull()
|
|
@@ -166,7 +166,7 @@ describe('Provider (context)', () => {
|
|
|
166
166
|
it('returns null when provider returns undefined', () => {
|
|
167
167
|
mockedCoreProvider.mockReturnValue(undefined as any)
|
|
168
168
|
|
|
169
|
-
const children =
|
|
169
|
+
const children = h('span', null, 'Hello')
|
|
170
170
|
const result = Provider({ children })
|
|
171
171
|
|
|
172
172
|
expect(result).toBeNull()
|
|
@@ -179,7 +179,7 @@ describe('Provider (context)', () => {
|
|
|
179
179
|
})) as any)
|
|
180
180
|
|
|
181
181
|
const overrideTheme = { rootSize: 20 }
|
|
182
|
-
const children =
|
|
182
|
+
const children = h('span', null, 'Hello')
|
|
183
183
|
Provider({ children, theme: overrideTheme, mode: 'dark' })
|
|
184
184
|
|
|
185
185
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -190,7 +190,7 @@ describe('Provider (context)', () => {
|
|
|
190
190
|
it('uses context mode when no mode prop is given', () => {
|
|
191
191
|
mockedUseContext.mockReturnValue((() => ({ mode: 'dark' })) as any)
|
|
192
192
|
|
|
193
|
-
const children =
|
|
193
|
+
const children = h('span', null, 'Hello')
|
|
194
194
|
Provider({ children })
|
|
195
195
|
|
|
196
196
|
const callArgs = mockedCoreProvider.mock.calls[0]?.[0] as Record<string, unknown>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { provide } from '@pyreon/core'
|
|
1
|
+
import { h, provide } from '@pyreon/core'
|
|
2
2
|
import createLocalProvider from '../context/createLocalProvider'
|
|
3
3
|
|
|
4
4
|
// Mock @pyreon/core provide
|
|
@@ -246,3 +246,35 @@ describe('createLocalProvider', () => {
|
|
|
246
246
|
expect(result).toBe(expectedResult)
|
|
247
247
|
})
|
|
248
248
|
})
|
|
249
|
+
|
|
250
|
+
// ─── createLocalProvider — real h() round-trip ──────────────────────
|
|
251
|
+
//
|
|
252
|
+
// The tests above use a `BaseComponent` mock that returns
|
|
253
|
+
// `{ type, props, children, key }` literals and assert against the
|
|
254
|
+
// shape. This block re-runs key contracts with a base component
|
|
255
|
+
// that returns real h() output — same divergence guard as the
|
|
256
|
+
// attrs / connector-document parallels.
|
|
257
|
+
|
|
258
|
+
describe('createLocalProvider — real h() round-trip', () => {
|
|
259
|
+
it('passes children through a real h() base component', () => {
|
|
260
|
+
const BaseComponentH = vi.fn((props: Record<string, unknown>) =>
|
|
261
|
+
h('div', { 'data-real-h': 'yes', ...props }, props.children as never),
|
|
262
|
+
)
|
|
263
|
+
const HOC = createLocalProvider(BaseComponentH as any)
|
|
264
|
+
const result = HOC({ children: 'hello' } as any) as any
|
|
265
|
+
expect(BaseComponentH).toHaveBeenCalled()
|
|
266
|
+
expect(result.type).toBe('div')
|
|
267
|
+
expect(result.props['data-real-h']).toBe('yes')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('forwards arbitrary props through real h() output', () => {
|
|
271
|
+
const BaseComponentH = vi.fn((props: Record<string, unknown>) =>
|
|
272
|
+
h('button', props, props.label as never),
|
|
273
|
+
)
|
|
274
|
+
const HOC = createLocalProvider(BaseComponentH as any)
|
|
275
|
+
const result = HOC({ label: 'Click', 'data-id': '42' } as any) as any
|
|
276
|
+
expect(result.type).toBe('button')
|
|
277
|
+
expect(result.props.label).toBe('Click')
|
|
278
|
+
expect(result.props['data-id']).toBe('42')
|
|
279
|
+
})
|
|
280
|
+
})
|
|
@@ -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
|
+
})
|