@pyreon/rocketstyle 0.24.4 → 0.24.6

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.
Files changed (60) hide show
  1. package/package.json +8 -10
  2. package/src/__tests__/attrs-overloads.test.ts +0 -97
  3. package/src/__tests__/attrs.test.ts +0 -190
  4. package/src/__tests__/cache-key-boolean-collision.test.ts +0 -54
  5. package/src/__tests__/chaining.test.ts +0 -86
  6. package/src/__tests__/collection.test.ts +0 -35
  7. package/src/__tests__/compose.test.ts +0 -36
  8. package/src/__tests__/context.test.ts +0 -200
  9. package/src/__tests__/createLocalProvider.test.ts +0 -280
  10. package/src/__tests__/dimensions.test.ts +0 -183
  11. package/src/__tests__/e2e-styler.test.ts +0 -299
  12. package/src/__tests__/hooks.test.ts +0 -178
  13. package/src/__tests__/isRocketComponent.test.ts +0 -48
  14. package/src/__tests__/memo-cap.test.ts +0 -174
  15. package/src/__tests__/minimal-theme.test.ts +0 -62
  16. package/src/__tests__/misc.test.ts +0 -204
  17. package/src/__tests__/native-marker.test.ts +0 -9
  18. package/src/__tests__/providerConsumer.test.ts +0 -183
  19. package/src/__tests__/reactive-props-preservation.test.ts +0 -195
  20. package/src/__tests__/rocketstyle.browser.test.tsx +0 -481
  21. package/src/__tests__/rocketstyleIntegration.test.ts +0 -711
  22. package/src/__tests__/theme-integration.test.tsx +0 -254
  23. package/src/__tests__/themeUtils.test.ts +0 -463
  24. package/src/cache/LocalThemeManager.ts +0 -14
  25. package/src/cache/index.ts +0 -3
  26. package/src/constants/booleanTags.ts +0 -32
  27. package/src/constants/defaultDimensions.ts +0 -23
  28. package/src/constants/index.ts +0 -59
  29. package/src/context/context.ts +0 -70
  30. package/src/context/createLocalProvider.ts +0 -97
  31. package/src/context/localContext.ts +0 -37
  32. package/src/env.d.ts +0 -6
  33. package/src/hoc/index.ts +0 -3
  34. package/src/hoc/rocketstyleAttrsHoc.ts +0 -76
  35. package/src/hooks/index.ts +0 -4
  36. package/src/hooks/usePseudoState.ts +0 -79
  37. package/src/hooks/useTheme.ts +0 -48
  38. package/src/index.ts +0 -95
  39. package/src/init.ts +0 -93
  40. package/src/isRocketComponent.ts +0 -16
  41. package/src/rocketstyle.ts +0 -640
  42. package/src/types/attrs.ts +0 -23
  43. package/src/types/config.ts +0 -48
  44. package/src/types/configuration.ts +0 -69
  45. package/src/types/dimensions.ts +0 -109
  46. package/src/types/hoc.ts +0 -5
  47. package/src/types/pseudo.ts +0 -19
  48. package/src/types/rocketComponent.ts +0 -24
  49. package/src/types/rocketstyle.ts +0 -220
  50. package/src/types/styles.ts +0 -61
  51. package/src/types/theme.ts +0 -18
  52. package/src/types/utils.ts +0 -98
  53. package/src/utils/attrs.ts +0 -181
  54. package/src/utils/chaining.ts +0 -58
  55. package/src/utils/collection.ts +0 -9
  56. package/src/utils/compose.ts +0 -11
  57. package/src/utils/dimensions.ts +0 -126
  58. package/src/utils/statics.ts +0 -44
  59. package/src/utils/styles.ts +0 -18
  60. package/src/utils/theme.ts +0 -211
@@ -1,640 +0,0 @@
1
- import { compose, config, hoistNonReactStatics, omit, pick, render } from '@pyreon/ui-core'
2
- import { LocalThemeManager } from './cache'
3
- import { CONFIG_KEYS, PSEUDO_AND_META_KEYS, PSEUDO_KEYS, STYLING_KEYS, __DEV__ } from './constants'
4
- import createLocalProvider from './context/createLocalProvider'
5
- import { useLocalContext } from './context/localContext'
6
- import { rocketstyleAttrsHoc } from './hoc'
7
- import { useTheme } from './hooks'
8
- import type { Configuration, ExtendedConfiguration } from './types/configuration'
9
- import type { RocketComponent } from './types/rocketComponent'
10
- import type { InnerComponentProps, RocketStyleComponent } from './types/rocketstyle'
11
- import type { ComponentFn } from './types/utils'
12
- import {
13
- calculateChainOptions,
14
- calculateStylingAttrs,
15
- mergeDescriptors,
16
- pickStyledAttrs,
17
- } from './utils/attrs'
18
- import { chainOptions, chainOrOptions, chainReservedKeyOptions } from './utils/chaining'
19
- import { calculateHocsFuncs } from './utils/compose'
20
- import { getDimensionsMap } from './utils/dimensions'
21
- import { createStaticsChainingEnhancers, createStaticsEnhancers } from './utils/statics'
22
- import { calculateStyles } from './utils/styles'
23
- import { getDimensionThemes, getTheme, getThemeByMode, getThemeFromChain } from './utils/theme'
24
-
25
- // Dev-time counter sink — see packages/internals/perf-harness for contract.
26
- const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
27
-
28
- /**
29
- * Core rocketstyle component factory. Creates a fully-featured Pyreon component
30
- * that integrates theme management (with light/dark mode support), multi-tier
31
- * WeakMap caching, dimension-based styling props, pseudo-state detection, and
32
- * chainable static methods (`.attrs()`, `.theme()`, `.styles()`, `.config()`, etc.).
33
- *
34
- * In Pyreon, components are plain functions that run once per mount.
35
- * No forwardRef, useMemo, useState — ref flows as a normal prop.
36
- */
37
-
38
- // --------------------------------------------------------
39
- // cloneAndEnhance
40
- // --------------------------------------------------------
41
- type CloneAndEnhance = (
42
- defaultOpts: Configuration,
43
- opts: Partial<ExtendedConfiguration>,
44
- ) => ReturnType<typeof rocketComponent>
45
-
46
- /**
47
- * Clones the current configuration and merges new options, returning a fresh
48
- * rocketComponent.
49
- *
50
- * Component-swap reset: when `opts.component` is set AND differs from the
51
- * current `defaultOpts.component`, the prior `attrs`, `priorityAttrs`,
52
- * `filterAttrs`, and `compose` chains are dropped — they were tailored to the
53
- * previous component's prop shape, and applying them to a different component
54
- * silently leaks invalid props through to the DOM (e.g. `disabled` on an
55
- * `<a>`). Callers who want to preserve them must re-chain explicitly:
56
- *
57
- * const NewBtn = Button.config({ component: 'a' }).attrs(sharedAttrs)
58
- */
59
- const cloneAndEnhance: CloneAndEnhance = (defaultOpts, opts) => {
60
- const componentChanged =
61
- opts.component != null && opts.component !== defaultOpts.component
62
-
63
- return rocketComponent({
64
- ...defaultOpts,
65
- attrs: componentChanged
66
- ? chainOptions(opts.attrs, [])
67
- : chainOptions(opts.attrs, defaultOpts.attrs),
68
- filterAttrs: componentChanged
69
- ? [...(opts.filterAttrs ?? [])]
70
- : [...(defaultOpts.filterAttrs ?? []), ...(opts.filterAttrs ?? [])],
71
- priorityAttrs: componentChanged
72
- ? chainOptions(opts.priorityAttrs, [])
73
- : chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
74
- statics: { ...defaultOpts.statics, ...opts.statics },
75
- compose: componentChanged
76
- ? { ...opts.compose }
77
- : { ...defaultOpts.compose, ...opts.compose },
78
- ...chainOrOptions(CONFIG_KEYS, opts, defaultOpts),
79
- ...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts),
80
- } as Parameters<typeof rocketComponent>[0])
81
- }
82
-
83
- // --------------------------------------------------------
84
- // rocketComponent
85
- // --------------------------------------------------------
86
- // @ts-expect-error
87
- const rocketComponent: RocketComponent = (options) => {
88
- const { component, styles } = options
89
- const { styled } = config
90
-
91
- const _calculateStylingAttrs = calculateStylingAttrs({
92
- multiKeys: options.multiKeys,
93
- useBooleans: options.useBooleans,
94
- })
95
-
96
- const componentName = options.name ?? options.component.displayName ?? options.component.name
97
-
98
- // Create styled component with all options.styles if available.
99
- // Rocketstyle CSS lives in `@layer rocketstyle`, which is declared
100
- // AFTER `@layer elements` in the cascade ordering (see sheet.ts).
101
- // This ensures rocketstyle theme styles always override element base
102
- // styles regardless of source order.
103
- const STYLED_COMPONENT =
104
- (component.IS_ROCKETSTYLE ?? options.styled !== true)
105
- ? component
106
- : styled(component, { layer: 'rocketstyle' })`
107
- ${calculateStyles(styles)};
108
- `
109
-
110
- // --------------------------------------------------------
111
- // COMPONENT - Final component to be rendered
112
- // --------------------------------------------------------
113
- const RenderComponent: ComponentFn<any> = options.provider
114
- ? createLocalProvider(STYLED_COMPONENT)
115
- : STYLED_COMPONENT
116
-
117
- // --------------------------------------------------------
118
- // THEME - Cached & Calculated theme(s)
119
- // --------------------------------------------------------
120
- const ThemeManager = new LocalThemeManager()
121
-
122
- // ── Per-definition caches (shared across all instances) ──────────────
123
- // getDimensionsMap + Object.keys(reservedPropNames) are theme-independent
124
- // (dimension structure comes from .sizes()/.states()/.variants() chain,
125
- // not from runtime theme values). Cache them so 50 instances of the same
126
- // component definition skip the rebuild entirely.
127
- const _dimensionsCache = new WeakMap<
128
- object,
129
- { keysMap: Record<string, unknown>; keywords: Record<string, true | undefined> }
130
- >()
131
- const _reservedKeysCache = new WeakMap<object, string[]>()
132
-
133
- // Reuse the module-scope pre-merged constant. Cast away `readonly` for
134
- // the downstream consumers that take a plain `string[]`. Saves the
135
- // 6-element array allocation that fired once per `rocketstyle()`
136
- // definition. Ported from vitus-labs `00fdadc2`.
137
- const ALL_PSEUDO_KEYS = PSEUDO_AND_META_KEYS as unknown as string[]
138
- // Static portion of omit keys — PSEUDO_KEYS + filterAttrs + 'pseudo' are definition-scoped.
139
- // RESERVED_STYLING_PROPS_KEYS is dimension-dependent but also cached per definition.
140
- // 'pseudo' is included here so we can skip the destructuring spread of mergeProps.
141
- const STATIC_OMIT_KEYS = ['pseudo', ...PSEUDO_KEYS, ...(options.filterAttrs ?? [])]
142
- // Pre-built Set for omit() — avoids per-call Set allocation. Built once the
143
- // dimension-dependent reserved keys are known (first mount), then reused.
144
- const _omitSetCache = new WeakMap<string[], Set<string>>()
145
-
146
- // ── Dimension-prop memo (per-definition) ─────────────────────────────
147
- // Keyed on theme identity → Map<keyString, { rocketstyle, rocketstate }>.
148
- // The accessors below build a key from (mode, dimension prop tuple,
149
- // pseudo state tuple) and look up here. On hit they return the SAME
150
- // object identities for both `$rocketstyle` and `$rocketstate`, which
151
- // lets the styler's existing `classCache` (keyed on those identities)
152
- // skip the entire CSS resolve pipeline. On miss they compute fresh
153
- // and store the result.
154
- //
155
- // Why this matters: B-FINDING.md (PR #342) showed every Button mount
156
- // fires 22 styler.resolve calls even when the styler-sheet cache hits
157
- // — the cache catches at the LAST step (insert dedup), but the resolve
158
- // pipeline still runs to compute the hash. Stable accessor identities
159
- // mean the styler's classCache hits earlier and the resolves don't run.
160
- //
161
- // LRU bound prevents unbounded growth from prop-tuple churn (e.g. a
162
- // table where every cell has a unique state). 128 entries per theme
163
- // covers the E2 perf-dashboard reference workload AND high-cardinality
164
- // surfaces (data tables, design systems with many tokens crossed with
165
- // size/variant axes, dashboards rendering many small interactive
166
- // components). The previous cap of 32 was sized for the reference
167
- // workload only and thrashed at higher cardinalities — measured 45%
168
- // cache-miss rate (888/2000 lookups) on a 60-unique-tuple Button
169
- // mount loop, 46% wall-clock regression vs the cap-fits-workload
170
- // case. The rs-precompute spike (closed PR #761, results live on
171
- // `spike/rocketstyle-precompute`) bisect-verified that raising the cap
172
- // 32 → 128 zeroes the cold-resolves counter for that 60-tuple
173
- // workload at zero implementation cost. Memory: ~12KB per definition
174
- // per theme at 128 entries × ~100 bytes per entry — negligible vs
175
- // the 46% runtime win.
176
- type RsMemoEntry = { readonly rocketstyle: object; readonly rocketstate: object }
177
- const _rsMemo = new WeakMap<object, Map<string, RsMemoEntry>>()
178
- const RS_MEMO_CAP = 128
179
-
180
- // --------------------------------------------------------
181
- // COMPOSE - high-order components
182
- // --------------------------------------------------------
183
- const hocsFuncs = [rocketstyleAttrsHoc(options), ...calculateHocsFuncs(options.compose)]
184
-
185
- // --------------------------------------------------------
186
- // ENHANCED COMPONENT
187
- // --------------------------------------------------------
188
- // In Pyreon, components are plain functions — no forwardRef needed.
189
- // Ref flows as a normal prop through the chain.
190
- const EnhancedComponent: ComponentFn<InnerComponentProps> = (props) => {
191
- // --------------------------------------------------
192
- // hover - focus - pressed state passed via context from parent component
193
- // --------------------------------------------------
194
- const localCtx = useLocalContext(options.consumer)
195
-
196
- // --------------------------------------------------
197
- // general theme and theme mode dark / light passed in context
198
- // --------------------------------------------------
199
- // IMPORTANT: Do NOT destructure — useTheme returns getter properties.
200
- // Destructuring calls getters once and captures static values.
201
- // Keep the object reference so mode/isDark/isLight re-evaluate lazily.
202
- const themeAttrs = useTheme(options)
203
-
204
- // --------------------------------------------------
205
- // Dimension KEY structure is theme-independent — dimension names (e.g.
206
- // `level3`, `primary`) come from the .sizes()/.states()/.variants()
207
- // callback structure at component-definition time, not from theme values.
208
- // Compute reservedPropNames + dimensions once using the initial theme;
209
- // they remain stable across theme swaps.
210
- //
211
- // Dimension VALUES (used in $rocketstyleAccessor) DO depend on theme and
212
- // are resolved inside the accessor on each tracked invocation — allowing
213
- // whole-theme swaps (user preference themes) to re-resolve CSS without
214
- // remounting. WeakMap caches in ThemeManager keep the common static-theme
215
- // case O(1).
216
- // --------------------------------------------------
217
- const initialTheme = themeAttrs.theme
218
- const initialBaseTheme = (() => {
219
- const helper = ThemeManager.baseTheme
220
- if (!helper.has(initialTheme)) {
221
- helper.set(initialTheme, getThemeFromChain(options.theme, initialTheme))
222
- }
223
- return helper.get(initialTheme)
224
- })()
225
- const initialDimensionThemes = (() => {
226
- const helper = ThemeManager.dimensionsThemes
227
- if (!helper.has(initialTheme)) {
228
- helper.set(initialTheme, getDimensionThemes(initialTheme, options))
229
- }
230
- return helper.get(initialTheme)
231
- })()
232
-
233
- // Cache getDimensionsMap per dimension-themes identity — all instances
234
- // of the same component definition share the same dimension structure.
235
- let dimResult = _dimensionsCache.get(initialDimensionThemes as object)
236
- if (dimResult) {
237
- if (process.env.NODE_ENV !== 'production')
238
- _countSink.__pyreon_count__?.('rocketstyle.dimensionsMap.hit')
239
- } else {
240
- dimResult = getDimensionsMap({
241
- themes: initialDimensionThemes,
242
- useBooleans: options.useBooleans,
243
- })
244
- _dimensionsCache.set(initialDimensionThemes as object, dimResult)
245
- }
246
- const { keysMap: dimensions, keywords: reservedPropNames } = dimResult
247
-
248
- // Cache Object.keys() result — same dimension structure = same keys
249
- let RESERVED_STYLING_PROPS_KEYS = _reservedKeysCache.get(reservedPropNames as object)
250
- if (!RESERVED_STYLING_PROPS_KEYS) {
251
- RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames)
252
- _reservedKeysCache.set(reservedPropNames as object, RESERVED_STYLING_PROPS_KEYS)
253
- }
254
-
255
- // Silence "unused" warnings for initialBaseTheme / initialDimensionThemes —
256
- // they're eagerly populated into ThemeManager caches so the first accessor
257
- // call hits cache, but not referenced directly.
258
- void initialBaseTheme
259
- void initialDimensionThemes
260
-
261
- // Capture pseudo from localCtx once at setup — pseudo properties are
262
- // getters (from createLocalProvider) that read signals lazily.
263
- // Passing them through preserves reactivity without subscribing here.
264
- const localPseudo = localCtx?.pseudo
265
-
266
- // --------------------------------------------------
267
- // Shared accessor resolver.
268
- //
269
- // Both `$rocketstyleAccessor` and `$rocketstateAccessor` derive from the
270
- // same input set (theme, mode, dimension props, pseudo state). Folding
271
- // them into one resolver lets the dimension-prop memo return the SAME
272
- // object identities for both — which is what the styler's `classCache`
273
- // (keyed on `(rocketstyle, rocketstate)` identity) needs to skip the
274
- // resolve pipeline on cache hit.
275
- //
276
- // Reactive contract: this runs inside the styler's `computed()` (one per
277
- // mounted instance). All signal reads — theme, mode, dimension props,
278
- // pseudo getters from localCtx — are TRACKED, so any change re-runs the
279
- // computed which re-resolves the entry. Same key → cached entry; new key
280
- // → fresh computation, stored under LRU cap.
281
- // --------------------------------------------------
282
- const _resolveRsEntry = (): RsMemoEntry => {
283
- // Read reactive inputs (tracks theme + mode signals)
284
- const theme = themeAttrs.theme
285
- const mode = themeAttrs.mode
286
- const propsRec = props as Record<string, unknown>
287
-
288
- // Resolve active dimensions FIRST so the cache key uses the RESOLVED
289
- // dimension values, not the raw prop names. Under `useBooleans: true`
290
- // the user writes `<X primary />` / `<X secondary />` — both map to
291
- // `state="primary"` / `state="secondary"` after _calculateStylingAttrs
292
- // resolves the boolean shorthand. Keying off `propsRec[dimName]` would
293
- // read `undefined` for both (the dimension prop itself was never set)
294
- // and collide every variant onto the first cached entry. Reading
295
- // `rocketstateRaw[dimName]` gives the resolved string and partitions
296
- // them correctly.
297
- // Resolved from props (not localCtx which has pseudo getters).
298
- const rocketstateRaw = _calculateStylingAttrs({
299
- props: pickStyledAttrs(propsRec, reservedPropNames),
300
- dimensions,
301
- })
302
-
303
- // Build key: mode | dimensionValues | pseudoState. Reading dimension
304
- // props + pseudo signals here tracks them in the surrounding computed
305
- // so any change re-runs us with a different key.
306
- let key = mode as string
307
- for (const dimName in dimensions) {
308
- const v = rocketstateRaw[dimName]
309
- // Multi-key dimensions (e.g. variant={['primary', 'rounded']}) are
310
- // arrays. Sort + join so equivalent sets hash identically; without
311
- // this both `['a','b']` and `['b','a']` would produce different keys.
312
- if (Array.isArray(v)) {
313
- key +=
314
- '|' + (v.length === 0 ? '' : (v as unknown[]).slice().sort().join(','))
315
- } else {
316
- // String/number/boolean serialize directly. Anything else (including
317
- // undefined / objects) gets a typeof tag so we don't collide.
318
- key +=
319
- '|' +
320
- (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'
321
- ? String(v)
322
- : v === undefined
323
- ? ''
324
- : '~' + typeof v)
325
- }
326
- }
327
- for (const k of ALL_PSEUDO_KEYS) {
328
- const propV = propsRec[k]
329
- const localV = localPseudo?.[k as keyof typeof localPseudo]
330
- const v = propV !== undefined ? propV : localV
331
- key += '|' + (v === undefined ? '' : v ? '1' : '0')
332
- }
333
-
334
- // Cache lookup
335
- let themeMemo = _rsMemo.get(theme as object)
336
- if (!themeMemo) {
337
- themeMemo = new Map()
338
- _rsMemo.set(theme as object, themeMemo)
339
- }
340
-
341
- const cached = themeMemo.get(key)
342
- if (cached) {
343
- if (process.env.NODE_ENV !== 'production')
344
- _countSink.__pyreon_count__?.('rocketstyle.dimensionMemo.hit')
345
- // LRU touch: move to end so eviction targets oldest unused entry
346
- themeMemo.delete(key)
347
- themeMemo.set(key, cached)
348
- return cached
349
- }
350
-
351
- // Miss: compute fresh. Counter measures actual theme resolutions
352
- // (not accessor invocations) — see COUNTERS.md.
353
- if (process.env.NODE_ENV !== 'production')
354
- _countSink.__pyreon_count__?.('rocketstyle.getTheme')
355
-
356
- // Resolve base + dimension themes for the CURRENT theme. WeakMap
357
- // keyed on theme identity — stable-theme renders hit cache in O(1),
358
- // theme swaps fall through to recompute (once per new theme).
359
- const baseThemeHelper = ThemeManager.baseTheme
360
- if (baseThemeHelper.has(theme)) {
361
- if (process.env.NODE_ENV !== 'production')
362
- _countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
363
- } else {
364
- baseThemeHelper.set(theme, getThemeFromChain(options.theme, theme))
365
- }
366
- const baseTheme = baseThemeHelper.get(theme)
367
-
368
- const dimHelper = ThemeManager.dimensionsThemes
369
- if (dimHelper.has(theme)) {
370
- if (process.env.NODE_ENV !== 'production')
371
- _countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
372
- } else {
373
- dimHelper.set(theme, getDimensionThemes(theme, options))
374
- }
375
- const themes = dimHelper.get(theme)
376
-
377
- // Resolve mode-specific theme
378
- const modeBaseHelper = ThemeManager.modeBaseTheme[mode]
379
- if (modeBaseHelper.has(baseTheme)) {
380
- if (process.env.NODE_ENV !== 'production')
381
- _countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
382
- } else {
383
- modeBaseHelper.set(baseTheme, getThemeByMode(baseTheme, mode))
384
- }
385
- const currentModeBaseTheme = modeBaseHelper.get(baseTheme)
386
-
387
- const modeDimHelper = ThemeManager.modeDimensionTheme[mode]
388
- if (modeDimHelper.has(themes)) {
389
- if (process.env.NODE_ENV !== 'production')
390
- _countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
391
- } else {
392
- modeDimHelper.set(themes, getThemeByMode(themes, mode))
393
- }
394
- const currentModeThemes = modeDimHelper.get(themes)
395
-
396
- const rocketstyle = getTheme({
397
- rocketstate: rocketstateRaw,
398
- themes: currentModeThemes,
399
- baseTheme: currentModeBaseTheme,
400
- transformKeys: options.transformKeys,
401
- appTheme: theme,
402
- })
403
-
404
- // $rocketstate carries dimension state + pseudo flags so the styler
405
- // emits matching pseudo selectors (`:hover`, `:focus`, etc.).
406
- const propPseudo = pick(propsRec, ALL_PSEUDO_KEYS)
407
- const rocketstate = {
408
- ...rocketstateRaw,
409
- pseudo: { ...localPseudo, ...propPseudo },
410
- }
411
-
412
- // LRU eviction at cap — drop the oldest (first-inserted) entry.
413
- if (themeMemo.size >= RS_MEMO_CAP) {
414
- const oldestKey = themeMemo.keys().next().value
415
- if (oldestKey !== undefined) themeMemo.delete(oldestKey)
416
- }
417
- const entry: RsMemoEntry = { rocketstyle, rocketstate }
418
- themeMemo.set(key, entry)
419
- return entry
420
- }
421
-
422
- const $rocketstyleAccessor = () => _resolveRsEntry().rocketstyle
423
- const $rocketstateAccessor = () => _resolveRsEntry().rocketstate
424
-
425
- // --------------------------------------------------
426
- // final props passed to WrappedComponent
427
- // --------------------------------------------------
428
- // Cache a pre-built Set for omit() — avoids building a new Set from
429
- // the key array on every mount. Same dimension structure = same Set.
430
- let omitSet = _omitSetCache.get(RESERVED_STYLING_PROPS_KEYS)
431
- if (omitSet) {
432
- if (process.env.NODE_ENV !== 'production')
433
- _countSink.__pyreon_count__?.('rocketstyle.omitSet.hit')
434
- } else {
435
- omitSet = new Set([...RESERVED_STYLING_PROPS_KEYS, ...STATIC_OMIT_KEYS])
436
- _omitSetCache.set(RESERVED_STYLING_PROPS_KEYS, omitSet)
437
- }
438
-
439
- // Merge localCtx + props via descriptor-copy so reactive getter
440
- // props on `props` (compiler-emitted `_rp(() => signal())` wrappers
441
- // converted to getters by `makeReactiveProps`) survive the merge.
442
- // A plain `{ ...localCtx, ...props }` spread would fire every getter
443
- // and collapse to static values, defeating reactivity for any
444
- // downstream JSX accessor reading `props.x`.
445
- const mergeProps = localCtx ? mergeDescriptors(localCtx, props) : props
446
-
447
- // omit() preserves descriptors (since ui-core's omit was updated to
448
- // copy descriptors), so reactive getters carry through to finalProps.
449
- const finalProps = omit(mergeProps as Record<string, unknown>, omitSet) as Record<string, any>
450
-
451
- if (options.passProps) {
452
- const passed = pick(mergeProps, options.passProps)
453
- // Copy descriptors so any reactive getters in passProps survive.
454
- // Plain `finalProps[k] = passed[k]` would fire getters at setup time
455
- // AND silently fail when finalProps[k] is already a getter-only
456
- // descriptor (assignment to a getter-only property is a no-op in
457
- // non-strict mode, throws in strict mode).
458
- const passedDescriptors = Object.getOwnPropertyDescriptors(passed)
459
- for (const k of Object.keys(passedDescriptors)) {
460
- Object.defineProperty(finalProps, k, passedDescriptors[k]!)
461
- }
462
- }
463
-
464
- // Use defineProperty for these last writes too — if props.ref or
465
- // an existing finalProps slot happened to carry a getter-only
466
- // descriptor, plain assignment would silently fail. defineProperty
467
- // explicitly replaces the descriptor regardless of shape.
468
- const refDescriptor = Object.getOwnPropertyDescriptor(props, 'ref')
469
- if (refDescriptor) {
470
- Object.defineProperty(finalProps, 'ref', refDescriptor)
471
- }
472
- // Function accessors — DynamicStyled wraps them in a computed() so
473
- // mode/dimension changes produce a new CSS class reactively. The
474
- // computed tracks only these two accessors; the resolve itself runs
475
- // untracked to prevent exponential cascade from theme deep-reads.
476
- Object.defineProperty(finalProps, '$rocketstyle', {
477
- value: $rocketstyleAccessor,
478
- writable: true,
479
- enumerable: true,
480
- configurable: true,
481
- })
482
- Object.defineProperty(finalProps, '$rocketstate', {
483
- value: $rocketstateAccessor,
484
- writable: true,
485
- enumerable: true,
486
- configurable: true,
487
- })
488
-
489
- // development debugging — tree-shaken in production via import.meta.env.DEV
490
- if (__DEV__) {
491
- // defineProperty rather than `=` to be safe against any preserved
492
- // descriptor in this slot (defense-in-depth — `data-rocketstyle`
493
- // is unlikely to be passed as a user prop, but the writes above
494
- // use defineProperty for the same reason).
495
- Object.defineProperty(finalProps, 'data-rocketstyle', {
496
- value: componentName,
497
- writable: true,
498
- enumerable: true,
499
- configurable: true,
500
- })
501
-
502
- if (options.DEBUG) {
503
- const debugPayload = {
504
- component: componentName,
505
- rocketstate: $rocketstateAccessor(),
506
- rocketstyle: $rocketstyleAccessor(),
507
- dimensions,
508
- mode: themeAttrs.mode,
509
- reservedPropNames: RESERVED_STYLING_PROPS_KEYS,
510
- filteredAttrs: options.filterAttrs,
511
- }
512
-
513
- // oxlint-disable-next-line no-console
514
- console.debug(`[rocketstyle] ${componentName} render:`, debugPayload)
515
- }
516
- }
517
-
518
- // STATIC VNode — created once, never remounted on mode change.
519
- // The styled component handles reactive class swaps internally.
520
- return RenderComponent(finalProps)
521
- }
522
-
523
- // ------------------------------------------------------
524
- // Compose HOC chain and create final component
525
- // ------------------------------------------------------
526
- const FinalComponent: RocketStyleComponent = compose(...hocsFuncs)(EnhancedComponent)
527
- FinalComponent.IS_ROCKETSTYLE = true
528
- FinalComponent.displayName = componentName
529
-
530
- hoistNonReactStatics(FinalComponent as Record<string, unknown>, options.component)
531
-
532
- // ------------------------------------------------------
533
- // enhance for chaining methods
534
- // ------------------------------------------------------
535
- createStaticsChainingEnhancers({
536
- context: FinalComponent,
537
- dimensionKeys: options.dimensionKeys,
538
- func: cloneAndEnhance,
539
- options,
540
- })
541
-
542
- FinalComponent.IS_ROCKETSTYLE = true
543
- FinalComponent.displayName = componentName
544
- FinalComponent.meta = {}
545
-
546
- // ------------------------------------------------------
547
- // enhance for statics
548
- // ------------------------------------------------------
549
- createStaticsEnhancers({
550
- context: FinalComponent.meta,
551
- options: options.statics,
552
- })
553
-
554
- // Also assign statics directly onto the component so they are
555
- // discoverable via `"key" in Component` checks (e.g. _documentType).
556
- createStaticsEnhancers({
557
- context: FinalComponent,
558
- options: options.statics,
559
- })
560
-
561
- // ─── Hoisted attrs chain (T3.1) ──────────────────────────────────────
562
- //
563
- // Expose the accumulated `.attrs()` callback chain on the component so
564
- // external inspectors (notably `extractDocumentTree` from
565
- // `@pyreon/connector-document`) can compute the post-attrs props
566
- // without invoking the full component. The previous Path B workaround
567
- // had to run the entire styled wrapper — JSX tree creation, dimension
568
- // resolution, the lot — just to read `_documentProps` off the result.
569
- //
570
- // Typed surface: `RocketStyleComponent.__rs_attrs` is a `readonly
571
- // ReadonlyArray<(props) => Record<string, unknown>>`. Empty when
572
- // no `.attrs()` was ever called. `chain.reduce(Object.assign, {})`
573
- // produces the post-attrs result for a given props bag.
574
- //
575
- // The `readonly` modifier guards external CONSUMERS — internal
576
- // assignment from the factory itself is the only legitimate write,
577
- // hence the cast. Do not drop the readonly on the type.
578
- ;(FinalComponent as unknown as { __rs_attrs: typeof options.attrs }).__rs_attrs =
579
- options.attrs ?? []
580
-
581
- Object.assign(FinalComponent, {
582
- attrs: (attrs: any, { priority, filter }: any = {}) => {
583
- const result: Record<string, any> = {}
584
-
585
- if (filter) {
586
- result.filterAttrs = filter
587
- }
588
-
589
- if (priority) {
590
- result.priorityAttrs = attrs as ExtendedConfiguration['priorityAttrs']
591
-
592
- return cloneAndEnhance(options, result)
593
- }
594
-
595
- result.attrs = attrs as ExtendedConfiguration['attrs']
596
-
597
- return cloneAndEnhance(options, result)
598
- },
599
-
600
- config: (opts: any = {}) => {
601
- const result = pick(opts, CONFIG_KEYS) as ExtendedConfiguration
602
-
603
- return cloneAndEnhance(options, result)
604
- },
605
-
606
- statics: (opts: any) => cloneAndEnhance(options, { statics: opts }),
607
-
608
- getStaticDimensions: (theme: any) => {
609
- const themes = getDimensionThemes(theme, options)
610
-
611
- const { keysMap, keywords } = getDimensionsMap({
612
- themes,
613
- useBooleans: options.useBooleans,
614
- })
615
-
616
- return {
617
- dimensions: keysMap,
618
- keywords,
619
- useBooleans: options.useBooleans,
620
- multiKeys: options.multiKeys,
621
- }
622
- },
623
-
624
- getDefaultAttrs: (props: any, theme: any, mode: any) =>
625
- calculateChainOptions(options.attrs)([
626
- props,
627
- theme,
628
- {
629
- render,
630
- mode,
631
- isDark: mode === 'dark',
632
- isLight: mode === 'light',
633
- },
634
- ]),
635
- })
636
-
637
- return FinalComponent
638
- }
639
-
640
- export default rocketComponent
@@ -1,23 +0,0 @@
1
- import type { render } from '@pyreon/ui-core'
2
- import type { ThemeModeKeys } from './theme'
3
-
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>