@pyreon/elements 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/lib/index.d.ts +8 -8
- package/lib/index.js +172 -148
- package/package.json +12 -11
- package/src/Element/component.tsx +59 -16
- package/src/Overlay/component.tsx +6 -1
- package/src/Overlay/context.tsx +5 -1
- package/src/__tests__/Element.test.ts +95 -46
- package/src/__tests__/Overlay.test.ts +8 -1
- package/src/__tests__/Wrapper.test.tsx +44 -0
- package/src/__tests__/elements.browser.test.tsx +30 -4
- package/src/__tests__/equalBeforeAfter.test.ts +4 -4
- package/src/__tests__/integration.test.tsx +36 -5
- package/src/__tests__/internElementBundle.test.ts +102 -0
- package/src/__tests__/native-markers.test.ts +13 -0
- package/src/__tests__/perf-stress.browser.test.tsx +119 -0
- package/src/__tests__/responsiveProps.test.ts +59 -28
- package/src/__tests__/useOverlay.test.ts +7 -1
- package/src/env.d.ts +6 -0
- package/src/helpers/Wrapper/component.tsx +35 -30
- package/src/helpers/internElementBundle.ts +37 -0
- package/src/utils.ts +1 -4
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock stress benchmark for the Element + Wrapper + Styled stack.
|
|
3
|
+
*
|
|
4
|
+
* Runs in real Chromium. Goal: surface a measurable wall-clock delta that
|
|
5
|
+
* synthetic counter probes (happy-dom + mountChild) miss. Specifically
|
|
6
|
+
* targets the path where Pyreon's 9ms benchmark numbers come from — the
|
|
7
|
+
* mount-pipeline + styler-resolve composition.
|
|
8
|
+
*
|
|
9
|
+
* Each test mounts N components, disposes, and reports median wall-clock
|
|
10
|
+
* across 5 measured iterations after warmup. Variance ≤ 15% on stable runs.
|
|
11
|
+
*/
|
|
12
|
+
import { h, type VNodeChild } from '@pyreon/core'
|
|
13
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
14
|
+
import { flush, mountInBrowser } from '@pyreon/test-utils/browser'
|
|
15
|
+
import { describe, expect, it } from 'vitest'
|
|
16
|
+
import Element from '../Element/component'
|
|
17
|
+
|
|
18
|
+
interface Bench {
|
|
19
|
+
median: number
|
|
20
|
+
min: number
|
|
21
|
+
max: number
|
|
22
|
+
runs: number[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function benchmark(N: number, mountFn: (root: Element, i: number) => () => void): Promise<Bench> {
|
|
26
|
+
const { container, unmount: cleanup } = mountInBrowser(h('div', { id: 'bench-root' }))
|
|
27
|
+
const root = container.querySelector('#bench-root')!
|
|
28
|
+
|
|
29
|
+
// Warmup — primes the styler sheet cache + GC any dead objects from prior runs.
|
|
30
|
+
for (let w = 0; w < 50; w++) {
|
|
31
|
+
const dispose = mountFn(root, w)
|
|
32
|
+
dispose()
|
|
33
|
+
}
|
|
34
|
+
await flush()
|
|
35
|
+
|
|
36
|
+
const runs: number[] = []
|
|
37
|
+
for (let r = 0; r < 5; r++) {
|
|
38
|
+
const t0 = performance.now()
|
|
39
|
+
const disposers: Array<() => void> = []
|
|
40
|
+
for (let i = 0; i < N; i++) disposers.push(mountFn(root, i))
|
|
41
|
+
for (const d of disposers) d()
|
|
42
|
+
runs.push(performance.now() - t0)
|
|
43
|
+
await flush()
|
|
44
|
+
}
|
|
45
|
+
cleanup()
|
|
46
|
+
|
|
47
|
+
const sorted = [...runs].sort((a, b) => a - b)
|
|
48
|
+
const median = sorted[2] as number
|
|
49
|
+
const min = sorted[0] as number
|
|
50
|
+
const max = sorted[4] as number
|
|
51
|
+
return { median, min, max, runs }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('Element + stack stress benchmark', () => {
|
|
55
|
+
it('500 bare Element mounts (mount + dispose, batched)', async () => {
|
|
56
|
+
const bench = await benchmark(500, (root, i) => mount(h(Element, null, `item-${i}`), root))
|
|
57
|
+
// oxlint-disable-next-line no-console
|
|
58
|
+
console.log(
|
|
59
|
+
`[stress] 500 bare Element: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
60
|
+
)
|
|
61
|
+
expect(bench.median).toBeLessThan(200)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('500 Element with css prop (exercises extendCss path)', async () => {
|
|
65
|
+
const bench = await benchmark(500, (root, i) =>
|
|
66
|
+
mount(
|
|
67
|
+
h(Element, { css: { color: 'red', padding: 8 } as unknown as Record<string, unknown> }, `item-${i}`),
|
|
68
|
+
root,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
// oxlint-disable-next-line no-console
|
|
72
|
+
console.log(
|
|
73
|
+
`[stress] 500 Element + css: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
74
|
+
)
|
|
75
|
+
expect(bench.median).toBeLessThan(500)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('depth-10 Element nesting × 50 mounts', async () => {
|
|
79
|
+
const buildDepth = (n: number, label: string): VNodeChild => {
|
|
80
|
+
if (n === 0) return label
|
|
81
|
+
return h(Element, null, buildDepth(n - 1, label))
|
|
82
|
+
}
|
|
83
|
+
const bench = await benchmark(50, (root, i) =>
|
|
84
|
+
mount(buildDepth(10, `leaf-${i}`) as unknown as Parameters<typeof mount>[0], root),
|
|
85
|
+
)
|
|
86
|
+
// oxlint-disable-next-line no-console
|
|
87
|
+
console.log(
|
|
88
|
+
`[stress] 50 depth-10: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
89
|
+
)
|
|
90
|
+
expect(bench.median).toBeLessThan(500)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Larger workload — clearer signal-to-noise. Mount + dispose 5000 elements.
|
|
94
|
+
it('5000 Element mounts — large workload, clearer wall-clock signal', async () => {
|
|
95
|
+
const bench = await benchmark(5000, (root, i) => mount(h(Element, null, `item-${i}`), root))
|
|
96
|
+
// oxlint-disable-next-line no-console
|
|
97
|
+
console.log(
|
|
98
|
+
`[stress] 5000 bare Element: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
99
|
+
)
|
|
100
|
+
expect(bench.median).toBeLessThan(2000)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// One-shot single-tree mount: reflects real-app cold-mount cost.
|
|
104
|
+
it('single one-shot mount of a 500-Element tree', async () => {
|
|
105
|
+
const bench = await benchmark(1, (root) => {
|
|
106
|
+
const tree = h(
|
|
107
|
+
'div',
|
|
108
|
+
null,
|
|
109
|
+
...Array.from({ length: 500 }, (_, i) => h(Element, null, `child-${i}`)),
|
|
110
|
+
)
|
|
111
|
+
return mount(tree, root)
|
|
112
|
+
})
|
|
113
|
+
// oxlint-disable-next-line no-console
|
|
114
|
+
console.log(
|
|
115
|
+
`[stress] 500-child tree mount: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
116
|
+
)
|
|
117
|
+
expect(bench.median).toBeLessThan(1000)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -7,6 +7,37 @@ import Wrapper from '../helpers/Wrapper/component'
|
|
|
7
7
|
|
|
8
8
|
const asVNode = (v: unknown) => v as VNode
|
|
9
9
|
|
|
10
|
+
// See Element.test.ts for context — Element's simple-element fast path moves
|
|
11
|
+
// layout props from `result.props.{tag, direction, …}` to
|
|
12
|
+
// `result.props.{as, $element.direction, …}`. This helper reads from
|
|
13
|
+
// whichever shape the result is in.
|
|
14
|
+
const getLayoutProps = (result: VNode): Record<string, unknown> => {
|
|
15
|
+
const p = result.props as Record<string, unknown>
|
|
16
|
+
if (p.$element && typeof p.$element === 'object') {
|
|
17
|
+
const el = p.$element as Record<string, unknown>
|
|
18
|
+
return {
|
|
19
|
+
tag: p.as,
|
|
20
|
+
direction: el.direction,
|
|
21
|
+
alignX: el.alignX,
|
|
22
|
+
alignY: el.alignY,
|
|
23
|
+
block: el.block,
|
|
24
|
+
equalCols: el.equalCols,
|
|
25
|
+
extendCss: el.extraStyles,
|
|
26
|
+
isInline: undefined,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
tag: p.tag,
|
|
31
|
+
direction: p.direction,
|
|
32
|
+
alignX: p.alignX,
|
|
33
|
+
alignY: p.alignY,
|
|
34
|
+
block: p.block,
|
|
35
|
+
equalCols: p.equalCols,
|
|
36
|
+
extendCss: p.extendCss,
|
|
37
|
+
isInline: p.isInline,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
10
41
|
const getContentSlots = (result: VNode): VNode[] => {
|
|
11
42
|
const children = result.props.children
|
|
12
43
|
if (!Array.isArray(children)) return []
|
|
@@ -20,17 +51,17 @@ describe('Element responsive props', () => {
|
|
|
20
51
|
describe('single values', () => {
|
|
21
52
|
it('renders with alignX as string', () => {
|
|
22
53
|
const result = asVNode(Element({ alignX: 'center', children: 'content' }))
|
|
23
|
-
expect(result.type).toBe(
|
|
54
|
+
expect(typeof result.type).toBe("function")
|
|
24
55
|
})
|
|
25
56
|
|
|
26
57
|
it('renders with alignY as string', () => {
|
|
27
58
|
const result = asVNode(Element({ alignY: 'top', children: 'content' }))
|
|
28
|
-
expect(result.type).toBe(
|
|
59
|
+
expect(typeof result.type).toBe("function")
|
|
29
60
|
})
|
|
30
61
|
|
|
31
62
|
it('renders with direction as string', () => {
|
|
32
63
|
const result = asVNode(Element({ direction: 'rows', children: 'content' }))
|
|
33
|
-
expect(result.type).toBe(
|
|
64
|
+
expect(typeof result.type).toBe("function")
|
|
34
65
|
})
|
|
35
66
|
|
|
36
67
|
it('renders with gap as number', () => {
|
|
@@ -42,13 +73,13 @@ describe('Element responsive props', () => {
|
|
|
42
73
|
children: 'content',
|
|
43
74
|
}),
|
|
44
75
|
)
|
|
45
|
-
expect(result.type).toBe(
|
|
76
|
+
expect(typeof result.type).toBe("function")
|
|
46
77
|
})
|
|
47
78
|
|
|
48
79
|
it('renders with block as boolean', () => {
|
|
49
80
|
const result = asVNode(Element({ block: true, children: 'content' }))
|
|
50
|
-
expect(result.type).toBe(
|
|
51
|
-
expect(result.
|
|
81
|
+
expect(typeof result.type).toBe("function")
|
|
82
|
+
expect(getLayoutProps(result).block).toBe(true)
|
|
52
83
|
})
|
|
53
84
|
|
|
54
85
|
it('renders with equalCols as boolean', () => {
|
|
@@ -60,7 +91,7 @@ describe('Element responsive props', () => {
|
|
|
60
91
|
children: 'content',
|
|
61
92
|
}),
|
|
62
93
|
)
|
|
63
|
-
expect(result.type).toBe(
|
|
94
|
+
expect(typeof result.type).toBe("function")
|
|
64
95
|
})
|
|
65
96
|
})
|
|
66
97
|
|
|
@@ -69,19 +100,19 @@ describe('Element responsive props', () => {
|
|
|
69
100
|
const result = asVNode(
|
|
70
101
|
Element({ alignX: ['left', 'center', 'right'] as any, children: 'content' }),
|
|
71
102
|
)
|
|
72
|
-
expect(result.type).toBe(
|
|
103
|
+
expect(typeof result.type).toBe("function")
|
|
73
104
|
})
|
|
74
105
|
|
|
75
106
|
it('renders with alignY as array', () => {
|
|
76
107
|
const result = asVNode(
|
|
77
108
|
Element({ alignY: ['top', 'center', 'bottom'] as any, children: 'content' }),
|
|
78
109
|
)
|
|
79
|
-
expect(result.type).toBe(
|
|
110
|
+
expect(typeof result.type).toBe("function")
|
|
80
111
|
})
|
|
81
112
|
|
|
82
113
|
it('renders with direction as array', () => {
|
|
83
114
|
const result = asVNode(Element({ direction: ['rows', 'inline'] as any, children: 'content' }))
|
|
84
|
-
expect(result.type).toBe(
|
|
115
|
+
expect(typeof result.type).toBe("function")
|
|
85
116
|
})
|
|
86
117
|
|
|
87
118
|
it('renders with gap as array', () => {
|
|
@@ -92,12 +123,12 @@ describe('Element responsive props', () => {
|
|
|
92
123
|
children: 'content',
|
|
93
124
|
}),
|
|
94
125
|
)
|
|
95
|
-
expect(result.type).toBe(
|
|
126
|
+
expect(typeof result.type).toBe("function")
|
|
96
127
|
})
|
|
97
128
|
|
|
98
129
|
it('renders with block as array', () => {
|
|
99
130
|
const result = asVNode(Element({ block: [false, true] as any, children: 'content' }))
|
|
100
|
-
expect(result.type).toBe(
|
|
131
|
+
expect(typeof result.type).toBe("function")
|
|
101
132
|
})
|
|
102
133
|
|
|
103
134
|
it('renders with equalCols as array', () => {
|
|
@@ -109,7 +140,7 @@ describe('Element responsive props', () => {
|
|
|
109
140
|
children: 'content',
|
|
110
141
|
}),
|
|
111
142
|
)
|
|
112
|
-
expect(result.type).toBe(
|
|
143
|
+
expect(typeof result.type).toBe("function")
|
|
113
144
|
})
|
|
114
145
|
})
|
|
115
146
|
|
|
@@ -121,14 +152,14 @@ describe('Element responsive props', () => {
|
|
|
121
152
|
children: 'content',
|
|
122
153
|
}),
|
|
123
154
|
)
|
|
124
|
-
expect(result.type).toBe(
|
|
155
|
+
expect(typeof result.type).toBe("function")
|
|
125
156
|
})
|
|
126
157
|
|
|
127
158
|
it('renders with alignY as breakpoint object', () => {
|
|
128
159
|
const result = asVNode(
|
|
129
160
|
Element({ alignY: { xs: 'top', lg: 'center' } as any, children: 'content' }),
|
|
130
161
|
)
|
|
131
|
-
expect(result.type).toBe(
|
|
162
|
+
expect(typeof result.type).toBe("function")
|
|
132
163
|
})
|
|
133
164
|
|
|
134
165
|
it('renders with direction as breakpoint object', () => {
|
|
@@ -138,7 +169,7 @@ describe('Element responsive props', () => {
|
|
|
138
169
|
children: 'content',
|
|
139
170
|
}),
|
|
140
171
|
)
|
|
141
|
-
expect(result.type).toBe(
|
|
172
|
+
expect(typeof result.type).toBe("function")
|
|
142
173
|
})
|
|
143
174
|
|
|
144
175
|
it('renders with gap as breakpoint object', () => {
|
|
@@ -149,14 +180,14 @@ describe('Element responsive props', () => {
|
|
|
149
180
|
children: 'content',
|
|
150
181
|
}),
|
|
151
182
|
)
|
|
152
|
-
expect(result.type).toBe(
|
|
183
|
+
expect(typeof result.type).toBe("function")
|
|
153
184
|
})
|
|
154
185
|
|
|
155
186
|
it('renders with block as breakpoint object', () => {
|
|
156
187
|
const result = asVNode(
|
|
157
188
|
Element({ block: { xs: false, md: true } as any, children: 'content' }),
|
|
158
189
|
)
|
|
159
|
-
expect(result.type).toBe(
|
|
190
|
+
expect(typeof result.type).toBe("function")
|
|
160
191
|
})
|
|
161
192
|
})
|
|
162
193
|
|
|
@@ -174,7 +205,7 @@ describe('Element responsive props', () => {
|
|
|
174
205
|
children: h('span', { 'data-testid': 'main' }, 'Main'),
|
|
175
206
|
}),
|
|
176
207
|
)
|
|
177
|
-
expect(result.type).toBe(
|
|
208
|
+
expect(typeof result.type).toBe("function")
|
|
178
209
|
const slots = getContentSlots(result)
|
|
179
210
|
expect(slots).toHaveLength(3)
|
|
180
211
|
})
|
|
@@ -190,7 +221,7 @@ describe('Element responsive props', () => {
|
|
|
190
221
|
children: h('span', null, 'Main'),
|
|
191
222
|
}),
|
|
192
223
|
)
|
|
193
|
-
expect(result.type).toBe(
|
|
224
|
+
expect(typeof result.type).toBe("function")
|
|
194
225
|
const slots = getContentSlots(result)
|
|
195
226
|
expect(slots).toHaveLength(3)
|
|
196
227
|
})
|
|
@@ -207,15 +238,15 @@ describe('Element responsive props', () => {
|
|
|
207
238
|
children: 'content',
|
|
208
239
|
}),
|
|
209
240
|
)
|
|
210
|
-
expect(result.type).toBe(
|
|
241
|
+
expect(typeof result.type).toBe("function")
|
|
211
242
|
})
|
|
212
243
|
})
|
|
213
244
|
|
|
214
245
|
describe('responsive css prop', () => {
|
|
215
246
|
it('renders with css as string', () => {
|
|
216
247
|
const result = asVNode(Element({ css: 'background: red;', children: 'content' }))
|
|
217
|
-
expect(result.type).toBe(
|
|
218
|
-
expect(result.
|
|
248
|
+
expect(typeof result.type).toBe("function")
|
|
249
|
+
expect(getLayoutProps(result).extendCss).toBe('background: red;')
|
|
219
250
|
})
|
|
220
251
|
|
|
221
252
|
it('renders with contentCss', () => {
|
|
@@ -226,7 +257,7 @@ describe('Element responsive props', () => {
|
|
|
226
257
|
children: 'content',
|
|
227
258
|
}),
|
|
228
259
|
)
|
|
229
|
-
expect(result.type).toBe(
|
|
260
|
+
expect(typeof result.type).toBe("function")
|
|
230
261
|
const slots = getContentSlots(result)
|
|
231
262
|
const contentSlot = slots.find((v) => v.props.contentType === 'content')
|
|
232
263
|
expect(contentSlot?.props.extendCss).toBe('color: blue;')
|
|
@@ -242,7 +273,7 @@ describe('Element responsive props', () => {
|
|
|
242
273
|
children: 'content',
|
|
243
274
|
}),
|
|
244
275
|
)
|
|
245
|
-
expect(result.type).toBe(
|
|
276
|
+
expect(typeof result.type).toBe("function")
|
|
246
277
|
const slots = getContentSlots(result)
|
|
247
278
|
const beforeSlot = slots.find((v) => v.props.contentType === 'before')
|
|
248
279
|
const afterSlot = slots.find((v) => v.props.contentType === 'after')
|
|
@@ -273,14 +304,14 @@ describe('Element responsive props', () => {
|
|
|
273
304
|
for (const value of alignXValues) {
|
|
274
305
|
it(`renders with alignX="${value}"`, () => {
|
|
275
306
|
const result = asVNode(Element({ alignX: value, children: 'content' }))
|
|
276
|
-
expect(result.type).toBe(
|
|
307
|
+
expect(typeof result.type).toBe("function")
|
|
277
308
|
})
|
|
278
309
|
}
|
|
279
310
|
|
|
280
311
|
for (const value of alignYValues) {
|
|
281
312
|
it(`renders with alignY="${value}"`, () => {
|
|
282
313
|
const result = asVNode(Element({ alignY: value, children: 'content' }))
|
|
283
|
-
expect(result.type).toBe(
|
|
314
|
+
expect(typeof result.type).toBe("function")
|
|
284
315
|
})
|
|
285
316
|
}
|
|
286
317
|
})
|
|
@@ -291,7 +322,7 @@ describe('Element responsive props', () => {
|
|
|
291
322
|
for (const value of directionValues) {
|
|
292
323
|
it(`renders with direction="${value}"`, () => {
|
|
293
324
|
const result = asVNode(Element({ direction: value, children: 'content' }))
|
|
294
|
-
expect(result.type).toBe(
|
|
325
|
+
expect(typeof result.type).toBe("function")
|
|
295
326
|
})
|
|
296
327
|
}
|
|
297
328
|
})
|
|
@@ -34,7 +34,13 @@ vi.mock('@pyreon/reactivity', () => {
|
|
|
34
34
|
return s
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// See sibling Overlay.test.ts mock: `@pyreon/core` imports
|
|
38
|
+
// `setSnapshotCapture` and calls it at module load to install the
|
|
39
|
+
// reactive-effect context-snapshot DI hook. The mock provides a no-op
|
|
40
|
+
// so the `@pyreon/core` import doesn't throw "No 'setSnapshotCapture'
|
|
41
|
+
// export is defined on the '@pyreon/reactivity' mock."
|
|
42
|
+
const setSnapshotCapture = () => {}
|
|
43
|
+
return { signal, setSnapshotCapture }
|
|
38
44
|
})
|
|
39
45
|
|
|
40
46
|
vi.mock('@pyreon/core', async (importOriginal) => {
|
package/src/env.d.ts
ADDED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* support `display: flex` consistently across browsers.
|
|
6
6
|
*/
|
|
7
7
|
import { splitProps } from '@pyreon/core'
|
|
8
|
+
import { getShouldBeEmpty } from '../../Element/utils'
|
|
8
9
|
import { IS_DEVELOPMENT } from '../../utils'
|
|
10
|
+
import { internElementBundle } from '../internElementBundle'
|
|
9
11
|
import Styled from './styled'
|
|
10
12
|
import type { Props } from './types'
|
|
11
13
|
import { isWebFixNeeded } from './utils'
|
|
@@ -41,46 +43,49 @@ const Component = (props: Partial<Props> & { ref?: unknown }) => {
|
|
|
41
43
|
|
|
42
44
|
const needsFix = !own.dangerouslySetInnerHTML && isWebFixNeeded(own.tag)
|
|
43
45
|
|
|
46
|
+
// Void HTML elements (hr, input, img, br, …) cannot have children. Even
|
|
47
|
+
// a falsy `{own.children}` slot becomes `[undefined]` in the vnode and
|
|
48
|
+
// trips runtime-dom's void-element warning. Element already skips passing
|
|
49
|
+
// children to Wrapper for void tags; this guard makes sure the empty
|
|
50
|
+
// slot is dropped here too instead of leaking into the JSX.
|
|
51
|
+
const isVoidTag = !own.dangerouslySetInnerHTML && getShouldBeEmpty(own.tag)
|
|
52
|
+
|
|
44
53
|
if (!needsFix) {
|
|
54
|
+
const bundle = internElementBundle({
|
|
55
|
+
block: own.block,
|
|
56
|
+
direction: own.direction,
|
|
57
|
+
alignX: own.alignX,
|
|
58
|
+
alignY: own.alignY,
|
|
59
|
+
equalCols: own.equalCols,
|
|
60
|
+
extraStyles: own.extendCss,
|
|
61
|
+
})
|
|
62
|
+
if (isVoidTag) {
|
|
63
|
+
return <Styled {...commonProps} $element={bundle} />
|
|
64
|
+
}
|
|
45
65
|
return (
|
|
46
|
-
<Styled
|
|
47
|
-
{...commonProps}
|
|
48
|
-
$element={{
|
|
49
|
-
block: own.block,
|
|
50
|
-
direction: own.direction,
|
|
51
|
-
alignX: own.alignX,
|
|
52
|
-
alignY: own.alignY,
|
|
53
|
-
equalCols: own.equalCols,
|
|
54
|
-
extraStyles: own.extendCss,
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
66
|
+
<Styled {...commonProps} $element={bundle}>
|
|
57
67
|
{own.children}
|
|
58
68
|
</Styled>
|
|
59
69
|
)
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
const asTag = own.isInline ? 'span' : 'div'
|
|
73
|
+
const parentBundle = internElementBundle({
|
|
74
|
+
parentFix: true as const,
|
|
75
|
+
block: own.block,
|
|
76
|
+
extraStyles: own.extendCss,
|
|
77
|
+
})
|
|
78
|
+
const childBundle = internElementBundle({
|
|
79
|
+
childFix: true as const,
|
|
80
|
+
direction: own.direction,
|
|
81
|
+
alignX: own.alignX,
|
|
82
|
+
alignY: own.alignY,
|
|
83
|
+
equalCols: own.equalCols,
|
|
84
|
+
})
|
|
63
85
|
|
|
64
86
|
return (
|
|
65
|
-
<Styled
|
|
66
|
-
{
|
|
67
|
-
$element={{
|
|
68
|
-
parentFix: true as const,
|
|
69
|
-
block: own.block,
|
|
70
|
-
extraStyles: own.extendCss,
|
|
71
|
-
}}
|
|
72
|
-
>
|
|
73
|
-
<Styled
|
|
74
|
-
as={asTag}
|
|
75
|
-
$childFix
|
|
76
|
-
$element={{
|
|
77
|
-
childFix: true as const,
|
|
78
|
-
direction: own.direction,
|
|
79
|
-
alignX: own.alignX,
|
|
80
|
-
alignY: own.alignY,
|
|
81
|
-
equalCols: own.equalCols,
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
87
|
+
<Styled {...commonProps} $element={parentBundle}>
|
|
88
|
+
<Styled as={asTag} $childFix $element={childBundle}>
|
|
84
89
|
{own.children}
|
|
85
90
|
</Styled>
|
|
86
91
|
</Styled>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-scope intern cache for `$element` bundles passed to Wrapper's styled
|
|
3
|
+
* component. Same primitive prop tuple → same object identity, so the styler's
|
|
4
|
+
* `elClassCache` (added 2026-Q2 alongside this) hits and skips the resolve
|
|
5
|
+
* pipeline. Analogous to `@pyreon/rocketstyle`'s dimension-prop memo (PR #344)
|
|
6
|
+
* but at the layer below — covers non-rocketstyle Element / Wrapper / Text usage
|
|
7
|
+
* AND the residual styled wrappers under any rocketstyle component.
|
|
8
|
+
*
|
|
9
|
+
* Cache key is a JSON-stringified shallow snapshot of the bundle. LRU-bound at
|
|
10
|
+
* 256 entries; oldest-first eviction. Bail (return the input as-is, no cache)
|
|
11
|
+
* when any value is a function (signal accessor) or a non-string object (CSS
|
|
12
|
+
* callback / CSSResult / nested object) — those cannot be safely round-tripped
|
|
13
|
+
* through JSON without losing identity guarantees.
|
|
14
|
+
*/
|
|
15
|
+
const _bundleCache = new Map<string, Record<string, unknown>>()
|
|
16
|
+
const BUNDLE_CAP = 256
|
|
17
|
+
|
|
18
|
+
export const internElementBundle = <T extends Record<string, unknown>>(bundle: T): T => {
|
|
19
|
+
for (const k in bundle) {
|
|
20
|
+
const v = bundle[k]
|
|
21
|
+
if (typeof v === 'function') return bundle
|
|
22
|
+
if (v != null && typeof v === 'object') return bundle
|
|
23
|
+
}
|
|
24
|
+
const key = JSON.stringify(bundle)
|
|
25
|
+
const existing = _bundleCache.get(key)
|
|
26
|
+
if (existing) {
|
|
27
|
+
_bundleCache.delete(key)
|
|
28
|
+
_bundleCache.set(key, existing)
|
|
29
|
+
return existing as T
|
|
30
|
+
}
|
|
31
|
+
if (_bundleCache.size >= BUNDLE_CAP) {
|
|
32
|
+
const oldest = _bundleCache.keys().next().value
|
|
33
|
+
if (oldest !== undefined) _bundleCache.delete(oldest)
|
|
34
|
+
}
|
|
35
|
+
_bundleCache.set(key, bundle)
|
|
36
|
+
return bundle
|
|
37
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -2,7 +2,4 @@
|
|
|
2
2
|
// literal-replaced so prod bundles tree-shake the dev branch to zero bytes.
|
|
3
3
|
// Typed through a narrowing interface so downstream packages don't need
|
|
4
4
|
// `vite/client` in their tsconfigs to type-check this file transitively.
|
|
5
|
-
|
|
6
|
-
readonly env?: { readonly DEV?: boolean }
|
|
7
|
-
}
|
|
8
|
-
export const IS_DEVELOPMENT: boolean = (import.meta as ViteMeta).env?.DEV === true
|
|
5
|
+
export const IS_DEVELOPMENT: boolean = process.env.NODE_ENV !== 'production'
|
package/lib/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/types.ts","../../src/Element/types.ts","../../src/Element/component.tsx","../../src/helpers/Iterator/types.ts","../../src/helpers/Iterator/component.tsx","../../src/List/component.tsx","../../src/Overlay/context.tsx","../../src/Overlay/positioning.ts","../../src/Overlay/useOverlay.tsx","../../src/Overlay/component.tsx","../../src/Portal/component.tsx","../../src/Text/component.tsx","../../src/Util/component.tsx"],"mappings":";;;;;;;KAWK,mBAAA,oBACS,CAAA,IAAK,CAAA,CAAE,CAAA,qCAAsC,CAAA,GAAI,CAAA,CAAE,CAAA;AAAA,KAG5D,EAAA,MAAQ,CAAA,iCAAkC,CAAA,GAAI,CAAA,CAAE,CAAA;AAAA,KAEhD,SAAA,SAAkB,EAAA,CAAG,IAAA,CAAK,CAAA,EAAG,OAAA,OAAc,CAAA,QAAS,CAAA,KAAM,CAAA;AAAA,KAE1D,MAAA,gCAAsC,CAAA,iCACvC,SAAA,CAAU,CAAA,EAAG,MAAA,CAAO,CAAA;AAAA,KAGZ,UAAA,gCAA0C,mBAAA,CAAoB,MAAA,CAAO,CAAA;AAAA,KAErE,QAAA,GAAW,WAAA,KAAgB,EAAA,EAAI,WAAA;AAAA,KAE/B,WAAA,IAAe,GAAA,SAAY,MAAA,CAAO,GAAA,KAAQ,UAAA,QAAkB,GAAA;AAAA,KAE5D,GAAA,GAAM,WAAA,GAAc,UAAA,QAAkB,MAAA,CAAO,GAAA;AAAA,KAE7C,OAAA,GAAU,UAAA,QAAkB,MAAA;AAAA,KAE5B,aAAA;AAAA,KAEA,aAAA;AAAA,KAEA,gBAAA;AAAA,KAEA,cAAA;AAAA,KACA,kBAAA;AAAA,KAIA,MAAA,GACR,aAAA,GACA,aAAA,KACA,OAAA,CAAQ,MAAA,CAAO,cAAA,EAAgB,aAAA;AAAA,KAEvB,MAAA,GACR,aAAA,GACA,aAAA,KACA,OAAA,CAAQ,MAAA,CAAO,cAAA,EAAgB,aAAA;AAAA,KAEvB,SAAA,GACR,gBAAA,GACA,gBAAA,KACA,OAAA,CAAQ,MAAA,CAAO,cAAA,EAAgB,gBAAA;AAAA,KAEvB,kBAAA,GACR,cAAA,GACA,cAAA,KACA,OAAA,CAAQ,MAAA,CAAO,cAAA,EAAgB,cAAA;AAAA,KAEvB,UAAA,GACR,kBAAA,GACA,kBAAA,KACA,OAAA,CAAQ,MAAA,CAAO,cAAA;AAAA,KAEP,SAAA,GAAY,GAAA,GAAM,GAAA,KAAQ,OAAA,CAAQ,MAAA,CAAO,cAAA,EAAgB,GAAA;AAAA,KAKzD,eAAA,WAA0B,MAAA,sBAA4B,WAAA,CAAY,CAAA,IAAK,YAAA;AAAA,UAElE,YAAA;EACf,WAAA;EACA,OAAA;EACA,iBAAA;AAAA;;;KCjEU,KAAA,GAAQ,OAAA;;;;EAIlB,GAAA,EAAK,QAAA;EDPiB;;;ECYtB,QAAA,EAAU,QAAA;EDXS;;;ECgBnB,QAAA,EAAU,OAAA;EDhBsD;;;;;;ECwBhE,OAAA,EAAS,OAAA;EDxBgD;;;;;AAAO;ECgChE,KAAA,EAAO,OAAA;ED7BF;;;ECkCL,aAAA,EAAe,OAAA;EDlCoC;;;ECuCnD,YAAA,EAAc,OAAA;EDvCH;;;;EC6CX,KAAA,EAAO,kBAAA;ED7C4C;;;AAAC;ECmDpD,SAAA,EAAW,kBAAA;EDjDC;;;;ECuDZ,gBAAA;EDvDwB;;;EC4DxB,GAAA,EAAK,UAAA;ED5DkB;;;ECiEvB,SAAA,EAAW,SAAA;EDjEa;;;ECsExB,gBAAA,EAAkB,SAAA;EDtEqC;;;EC2EvD,sBAAA,EAAwB,SAAA;EDzErB;;;EC8EH,qBAAA,EAAuB,SAAA;ED7EX;;;ECkFZ,MAAA,EAAQ,MAAA;EDlFG;;;ECuFX,aAAA,EAAe,MAAA;EDxF2C;;;EC6F1D,mBAAA,EAAqB,MAAA;ED5FN;;;ECiGf,kBAAA,EAAoB,MAAA;ED9FV;;;ECmGV,MAAA,EAAQ,MAAA;EDnGgE;;;ECwGxE,aAAA,EAAe,MAAA;EDxGM;;;EC6GrB,mBAAA,EAAqB,MAAA;ED7G2D;;AAElF;ECgHE,kBAAA,EAAoB,MAAA;;;;EAKpB,uBAAA;IAA2B,MAAA;EAAA;EDrHgC;AAE7D;;ECwHE,GAAA,EAAK,SAAA;EDxHgC;;;EC6HrC,UAAA,EAAY,SAAA;ED7HkD;;;ECkI9D,gBAAA,EAAkB,SAAA;EDlIkC;;;ECuIpD,eAAA,EAAiB,SAAA;AAAA,KAEjB,oBAAA;AAAA,KAEU,aAAA,WAAwB,MAAA,0BAAgC,WAAA,CAAY,KAAA,GAAQ,CAAA,IACtF,YAAA;;;cChII,SAAA,EAAW,aAAA;;;KCpCL,SAAA;AAAA,KACA,IAAA,GAAO,MAAA;AAAA,KACP,WAAA;AAAA,KACA,WAAA,GAAc,OAAA;EACxB,EAAA,EAAI,WAAA;EACJ,GAAA,EAAK,WAAA;EACL,MAAA,EAAQ,WAAA;EACR,SAAA,EAAW,WAAA;AAAA,KAEX,MAAA;AAAA,KAEU,WAAA,WAAsB,MAAA,2BAAiC,WAAA,CAAY,CAAA,IAAK,QAAA;AAAA,KAExE,aAAA;EACV,KAAA;EACA,KAAA;EACA,IAAA;EACA,GAAA;EACA,IAAA;EACA,QAAA;AAAA;AAAA,KAGU,aAAA,GACR,IAAA,KAEE,SAAA,EAAW,MAAA,kBAAwB,MAAA,SAAe,WAAA,IAAe,WAAA,EACjE,aAAA,EAAe,aAAA,KACZ,IAAA;AAAA,KAEG,OAAA,GAAQ,OAAA;EHpBC;;;EGwBnB,QAAA,EAAU,UAAA;EHxBsD;;AAAA;EG6BhE,IAAA,EAAM,KAAA,CAAM,WAAA,GAAc,WAAA,GAAc,SAAA;EH1BnC;;;EG+BL,SAAA,EAAW,WAAA;EH/BwC;;;;;EGsCnD,SAAA;EHtCkC;;;;;EG6ClC,aAAA,EAAe,WAAA;EH3CZ;;;EGgDH,SAAA,EAAW,aAAA;EHhDmC;;;EGqD9C,SAAA,GAAY,aAAA;EHrDiD;;;EG0D7D,OAAA,SACU,WAAA,KACJ,IAAA,EAAM,WAAA,GAAc,IAAA,CAAK,WAAA,gBAA2B,KAAA,aAAkB,WAAA;AAAA;;;cCjED,QAAA,WA4DnD,OAAA,KAAK,UAAA;;;;;;KCxD1B,SAAA;;;;ALPwD;;EKa3D,WAAA;ELVY;;;EKcZ,KAAA;ELd6D;;;EKkB7D,OAAA;AAAA;AAAA,KAGU,OAAA,GAAQ,UAAA,EAAY,OAAA,EAAe,SAAA,KAAc,OAAA,CAAQ,IAAA,CAAK,KAAA;AAAA,cAEpE,WAAA,EAAW,aAAA,CAAc,OAAA;;;UC1Bd,cAAA;EACf,OAAA;EACA,UAAA;EACA,YAAA;AAAA;AAAA,cAOI,WAAA,GAAa,KAAA,EAAO,cAAA;EAAmB,QAAA,EAAU,UAAA;AAAA,MAAU,aAAA,CAAE,KAAA;;;KCHvD,OAAA;AAAA,KACA,QAAA;AAAA,KACA,QAAA;;;KCOA,eAAA,GAAkB,OAAA;EAC5B,MAAA;EACA,MAAA;EACA,OAAA;EACA,IAAA;EACA,QAAA;EACA,KAAA,EAAO,OAAA;EACP,MAAA,EAAQ,QAAA;EACR,MAAA,EAAQ,QAAA;EACR,OAAA;EACA,OAAA;EACA,aAAA;EACA,eAAA,EAAiB,WAAA;EACjB,UAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,cA0GI,UAAA;EAAc,MAAA;EAAA,MAAA;EAAA,OAAA;EAAA,IAAA;EAAA,QAAA;EAAA,KAAA;EAAA,MAAA,EAAA,UAAA;EAAA,MAAA,EAAA,UAAA;EAAA,OAAA;EAAA,OAAA;EAAA,aAAA;EAAA,eAAA;EAAA,UAAA;EAAA,UAAA;EAAA,QAAA;EAAA,MAAA;EAAA;AAAA,IAkBjB,OAAA,CAAQ,eAAA;qBAkBiB,WAAA;qBAIQ,WAAA;UAAW,mBAAA,CAAA,MAAA;;;;;;;;;;;;;;;;KC3K1C,KAAA;AAAA,KACA,QAAA;AAAA,KACA,QAAA;AAAA,KAEA,eAAA,IACH,KAAA,EAAO,OAAA;EACL,MAAA;EACA,WAAA;EACA,WAAA;AAAA,OAEC,UAAA;AAAA,KAEA,eAAA,IACH,KAAA,EAAO,OAAA;EACL,MAAA;EACA,WAAA;EACA,WAAA;EACA,KAAA,EAAO,KAAA;EACP,MAAA,EAAQ,QAAA;EACR,MAAA,EAAQ,QAAA;AAAA,OAEP,UAAA;AAAA,KAEO,OAAA;EACV,QAAA,EAAU,eAAA,GAAkB,OAAA;EAC5B,OAAA,EAAS,eAAA,GAAkB,OAAA;EAC3B,WAAA,GAAc,WAAA;EACd,cAAA;EACA,cAAA;AAAA,IACE,eAAA;AAAA,cAEE,WAAA,EAAW,eAAA,CAAgB,OAAA;;;UCrChB,OAAA;EVAO;;;EUItB,WAAA,GAAc,WAAA;EVHK;;;EUOnB,QAAA,EAAU,UAAA;EVPsD;;;EUWhE,GAAA;AAAA;AAAA,cAGI,WAAA,EAAW,eAAA,CAAgB,OAAA;;;KCZrB,OAAA,GAAQ,OAAA;EXFN;;;EWMZ,KAAA,EAAO,UAAA;EXNsD;;;EWU7D,QAAA,EAAU,UAAA;EXXa;;;EWevB,SAAA;EXdmB;;;EWkBnB,GAAA,EAAK,YAAA;EXlB2D;;AAAA;EWsBhE,GAAA,EAAK,SAAA;AAAA,KAEL,oBAAA;AAAA,cAEI,WAAA,EAAW,eAAA,CAAgB,OAAA;EAC/B,MAAA;AAAA;;;UC5Be,OAAA;EZAO;;;EYItB,QAAA,EAAU,UAAA;EZHS;;;EYOnB,SAAA;EZPgE;;;EYWhE,KAAA,GAAQ,MAAA;AAAA;AAAA,cAGJ,WAAA,EAAW,eAAA,CAAgB,OAAA"}
|