@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.
- package/package.json +8 -10
- package/src/__tests__/attrs-overloads.test.ts +0 -97
- package/src/__tests__/attrs.test.ts +0 -190
- package/src/__tests__/cache-key-boolean-collision.test.ts +0 -54
- package/src/__tests__/chaining.test.ts +0 -86
- package/src/__tests__/collection.test.ts +0 -35
- package/src/__tests__/compose.test.ts +0 -36
- package/src/__tests__/context.test.ts +0 -200
- package/src/__tests__/createLocalProvider.test.ts +0 -280
- package/src/__tests__/dimensions.test.ts +0 -183
- package/src/__tests__/e2e-styler.test.ts +0 -299
- package/src/__tests__/hooks.test.ts +0 -178
- package/src/__tests__/isRocketComponent.test.ts +0 -48
- package/src/__tests__/memo-cap.test.ts +0 -174
- package/src/__tests__/minimal-theme.test.ts +0 -62
- package/src/__tests__/misc.test.ts +0 -204
- package/src/__tests__/native-marker.test.ts +0 -9
- package/src/__tests__/providerConsumer.test.ts +0 -183
- package/src/__tests__/reactive-props-preservation.test.ts +0 -195
- package/src/__tests__/rocketstyle.browser.test.tsx +0 -481
- package/src/__tests__/rocketstyleIntegration.test.ts +0 -711
- package/src/__tests__/theme-integration.test.tsx +0 -254
- package/src/__tests__/themeUtils.test.ts +0 -463
- package/src/cache/LocalThemeManager.ts +0 -14
- package/src/cache/index.ts +0 -3
- package/src/constants/booleanTags.ts +0 -32
- package/src/constants/defaultDimensions.ts +0 -23
- package/src/constants/index.ts +0 -59
- package/src/context/context.ts +0 -70
- package/src/context/createLocalProvider.ts +0 -97
- package/src/context/localContext.ts +0 -37
- package/src/env.d.ts +0 -6
- package/src/hoc/index.ts +0 -3
- package/src/hoc/rocketstyleAttrsHoc.ts +0 -76
- package/src/hooks/index.ts +0 -4
- package/src/hooks/usePseudoState.ts +0 -79
- package/src/hooks/useTheme.ts +0 -48
- package/src/index.ts +0 -95
- package/src/init.ts +0 -93
- package/src/isRocketComponent.ts +0 -16
- package/src/rocketstyle.ts +0 -640
- package/src/types/attrs.ts +0 -23
- package/src/types/config.ts +0 -48
- package/src/types/configuration.ts +0 -69
- package/src/types/dimensions.ts +0 -109
- package/src/types/hoc.ts +0 -5
- package/src/types/pseudo.ts +0 -19
- package/src/types/rocketComponent.ts +0 -24
- package/src/types/rocketstyle.ts +0 -220
- package/src/types/styles.ts +0 -61
- package/src/types/theme.ts +0 -18
- package/src/types/utils.ts +0 -98
- package/src/utils/attrs.ts +0 -181
- package/src/utils/chaining.ts +0 -58
- package/src/utils/collection.ts +0 -9
- package/src/utils/compose.ts +0 -11
- package/src/utils/dimensions.ts +0 -126
- package/src/utils/statics.ts +0 -44
- package/src/utils/styles.ts +0 -18
- package/src/utils/theme.ts +0 -211
package/src/rocketstyle.ts
DELETED
|
@@ -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
|
package/src/types/attrs.ts
DELETED
|
@@ -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>
|