@pyreon/ui-core 0.11.0 → 0.11.2

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.
@@ -0,0 +1,537 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { get, merge, omit, pick, set, throttle } from "../utils"
3
+
4
+ // --------------------------------------------------------
5
+ // omit
6
+ // --------------------------------------------------------
7
+ describe("omit", () => {
8
+ it("should return object without specified keys", () => {
9
+ const obj = { a: 1, b: 2, c: 3 }
10
+ expect(omit(obj, ["b"])).toEqual({ a: 1, c: 3 })
11
+ })
12
+
13
+ it("should return shallow copy when no keys specified", () => {
14
+ const obj = { a: 1, b: 2 }
15
+ const result = omit(obj)
16
+ expect(result).toEqual({ a: 1, b: 2 })
17
+ expect(result).not.toBe(obj)
18
+ })
19
+
20
+ it("should return shallow copy when keys is empty array", () => {
21
+ const obj = { a: 1 }
22
+ expect(omit(obj, [])).toEqual({ a: 1 })
23
+ })
24
+
25
+ it("should return empty object for null", () => {
26
+ expect(omit(null)).toEqual({})
27
+ })
28
+
29
+ it("should return empty object for undefined", () => {
30
+ expect(omit(undefined)).toEqual({})
31
+ })
32
+
33
+ it("should handle multiple keys", () => {
34
+ const obj = { a: 1, b: 2, c: 3, d: 4 }
35
+ expect(omit(obj, ["a", "c"])).toEqual({ b: 2, d: 4 })
36
+ })
37
+
38
+ it("should ignore keys not present in object", () => {
39
+ const obj = { a: 1 }
40
+ expect(omit(obj, ["b", "c"])).toEqual({ a: 1 })
41
+ })
42
+
43
+ it("should only include own properties", () => {
44
+ const proto = { inherited: true }
45
+ const obj = Object.create(proto)
46
+ obj.own = 1
47
+ expect(omit(obj, [])).toEqual({ own: 1 })
48
+ })
49
+ })
50
+
51
+ // --------------------------------------------------------
52
+ // pick
53
+ // --------------------------------------------------------
54
+ describe("pick", () => {
55
+ it("should return object with only specified keys", () => {
56
+ const obj = { a: 1, b: 2, c: 3 }
57
+ expect(pick(obj, ["a", "c"])).toEqual({ a: 1, c: 3 })
58
+ })
59
+
60
+ it("should return shallow copy when no keys specified", () => {
61
+ const obj = { a: 1, b: 2 }
62
+ const result = pick(obj)
63
+ expect(result).toEqual({ a: 1, b: 2 })
64
+ expect(result).not.toBe(obj)
65
+ })
66
+
67
+ it("should return shallow copy when keys is empty array", () => {
68
+ const obj = { a: 1 }
69
+ expect(pick(obj, [])).toEqual({ a: 1 })
70
+ })
71
+
72
+ it("should return empty object for null", () => {
73
+ expect(pick(null)).toEqual({})
74
+ })
75
+
76
+ it("should return empty object for undefined", () => {
77
+ expect(pick(undefined)).toEqual({})
78
+ })
79
+
80
+ it("should ignore keys not present in object", () => {
81
+ const obj = { a: 1 }
82
+ expect(pick(obj, ["a", "b"])).toEqual({ a: 1 })
83
+ })
84
+
85
+ it("should only pick own properties", () => {
86
+ const proto = { inherited: true }
87
+ const obj = Object.create(proto)
88
+ obj.own = 1
89
+ expect(pick(obj, ["own", "inherited"])).toEqual({ own: 1 })
90
+ })
91
+ })
92
+
93
+ // --------------------------------------------------------
94
+ // get
95
+ // --------------------------------------------------------
96
+ describe("get", () => {
97
+ it("should get nested value by dot path", () => {
98
+ const obj = { a: { b: { c: 42 } } }
99
+ expect(get(obj, "a.b.c")).toBe(42)
100
+ })
101
+
102
+ it("should get value by array path", () => {
103
+ const obj = { a: { b: 10 } }
104
+ expect(get(obj, ["a", "b"])).toBe(10)
105
+ })
106
+
107
+ it("should get array element by bracket notation", () => {
108
+ const obj = { items: [10, 20, 30] }
109
+ expect(get(obj, "items[1]")).toBe(20)
110
+ })
111
+
112
+ it("should return defaultValue when path does not exist", () => {
113
+ const obj = { a: 1 }
114
+ expect(get(obj, "b.c", "default")).toBe("default")
115
+ })
116
+
117
+ it("should return defaultValue when intermediate is null", () => {
118
+ const obj = { a: null }
119
+ expect(get(obj, "a.b", "fallback")).toBe("fallback")
120
+ })
121
+
122
+ it("should return defaultValue when intermediate is undefined", () => {
123
+ const obj = { a: undefined }
124
+ expect(get(obj, "a.b", "fallback")).toBe("fallback")
125
+ })
126
+
127
+ it("should return undefined when path does not exist and no default", () => {
128
+ expect(get({}, "a.b.c")).toBeUndefined()
129
+ })
130
+
131
+ it("should return the root value for empty array path", () => {
132
+ const obj = { a: 1 }
133
+ expect(get(obj, [])).toEqual({ a: 1 })
134
+ })
135
+
136
+ it("should handle top-level key", () => {
137
+ expect(get({ x: 5 }, "x")).toBe(5)
138
+ })
139
+
140
+ it("should return actual value even if it is falsy", () => {
141
+ expect(get({ a: 0 }, "a", "default")).toBe(0)
142
+ expect(get({ a: false }, "a", "default")).toBe(false)
143
+ expect(get({ a: "" }, "a", "default")).toBe("")
144
+ expect(get({ a: null }, "a", "default")).toBeNull()
145
+ })
146
+
147
+ it("should use defaultValue only when result is undefined", () => {
148
+ expect(get({ a: undefined }, "a", "default")).toBe("default")
149
+ })
150
+
151
+ // UNSAFE_KEYS guard
152
+ it("should return defaultValue for __proto__ key", () => {
153
+ const obj = { a: 1 }
154
+ expect(get(obj, "__proto__", "safe")).toBe("safe")
155
+ })
156
+
157
+ it("should return defaultValue for prototype key", () => {
158
+ const obj = { a: 1 }
159
+ expect(get(obj, "prototype", "safe")).toBe("safe")
160
+ })
161
+
162
+ it("should return defaultValue for constructor key", () => {
163
+ const obj = { a: 1 }
164
+ expect(get(obj, "constructor", "safe")).toBe("safe")
165
+ })
166
+
167
+ it("should return defaultValue for __proto__ in nested path", () => {
168
+ const obj = { a: { b: 1 } }
169
+ expect(get(obj, "a.__proto__.c", "safe")).toBe("safe")
170
+ })
171
+
172
+ it("should return obj itself for empty string path (no keys parsed)", () => {
173
+ // parsePath('') returns [] — no iteration — result stays as obj
174
+ // obj is not undefined — returned as-is
175
+ expect(get({ a: 1 }, "")).toEqual({ a: 1 })
176
+ })
177
+ })
178
+
179
+ // --------------------------------------------------------
180
+ // set
181
+ // --------------------------------------------------------
182
+ describe("set", () => {
183
+ it("should set nested value by dot path", () => {
184
+ const obj: any = {}
185
+ set(obj, "a.b.c", 42)
186
+ expect(obj.a.b.c).toBe(42)
187
+ })
188
+
189
+ it("should set value by array path", () => {
190
+ const obj: any = {}
191
+ set(obj, ["x", "y"], 10)
192
+ expect(obj.x.y).toBe(10)
193
+ })
194
+
195
+ it("should create arrays for numeric keys", () => {
196
+ const obj: any = {}
197
+ set(obj, "items.0", "first")
198
+ expect(Array.isArray(obj.items)).toBe(true)
199
+ expect(obj.items[0]).toBe("first")
200
+ })
201
+
202
+ it("should overwrite existing values", () => {
203
+ const obj = { a: { b: 1 } }
204
+ set(obj, "a.b", 2)
205
+ expect(obj.a.b).toBe(2)
206
+ })
207
+
208
+ it("should return the mutated object", () => {
209
+ const obj = {}
210
+ const result = set(obj, "a", 1)
211
+ expect(result).toBe(obj)
212
+ })
213
+
214
+ it("should handle single key path", () => {
215
+ const obj: any = {}
216
+ set(obj, "key", "value")
217
+ expect(obj.key).toBe("value")
218
+ })
219
+
220
+ it("should not set anything for empty path", () => {
221
+ const obj = { a: 1 }
222
+ set(obj, "", "value")
223
+ expect(obj).toEqual({ a: 1 })
224
+ })
225
+
226
+ // Security: prototype pollution protection
227
+ it("should not pollute Object.prototype via __proto__", () => {
228
+ const obj = {}
229
+ set(obj, "__proto__.polluted", true)
230
+ expect(({} as any).polluted).toBeUndefined()
231
+ })
232
+
233
+ it("should not pollute via constructor.prototype", () => {
234
+ const obj = {}
235
+ set(obj, "constructor.prototype.polluted", true)
236
+ expect(({} as any).polluted).toBeUndefined()
237
+ })
238
+
239
+ it("should bail out when intermediate key is __proto__", () => {
240
+ const obj: any = { a: 1 }
241
+ const result = set(obj, "__proto__.polluted", true)
242
+ expect(result).toBe(obj)
243
+ expect(({} as any).polluted).toBeUndefined()
244
+ })
245
+
246
+ it("should bail out when next key in path is unsafe", () => {
247
+ const obj: any = {}
248
+ const result = set(obj, "a.__proto__", "bad")
249
+ expect(result).toBe(obj)
250
+ expect(obj.a).toBeUndefined()
251
+ })
252
+
253
+ it("should bail out when last key is unsafe", () => {
254
+ const obj: any = {}
255
+ set(obj, "prototype", "bad")
256
+ // prototype is in UNSAFE_KEYS, so the set should be blocked
257
+ expect(obj.prototype).toBeUndefined()
258
+ })
259
+
260
+ it("should handle bracket notation in paths", () => {
261
+ const obj: any = {}
262
+ set(obj, "items[0].name", "first")
263
+ expect(obj.items[0].name).toBe("first")
264
+ })
265
+
266
+ it("should not overwrite existing intermediate objects", () => {
267
+ const obj: any = { a: { existing: true } }
268
+ set(obj, "a.b", "new")
269
+ expect(obj.a.existing).toBe(true)
270
+ expect(obj.a.b).toBe("new")
271
+ })
272
+ })
273
+
274
+ // --------------------------------------------------------
275
+ // throttle
276
+ // --------------------------------------------------------
277
+ describe("throttle", () => {
278
+ beforeEach(() => {
279
+ vi.useFakeTimers()
280
+ })
281
+
282
+ afterEach(() => {
283
+ vi.useRealTimers()
284
+ })
285
+
286
+ it("should call function immediately on first invocation", () => {
287
+ const fn = vi.fn()
288
+ const throttled = throttle(fn, 100)
289
+ throttled("a")
290
+ expect(fn).toHaveBeenCalledWith("a")
291
+ expect(fn).toHaveBeenCalledTimes(1)
292
+ })
293
+
294
+ it("should not call again within wait period", () => {
295
+ const fn = vi.fn()
296
+ const throttled = throttle(fn, 100)
297
+ throttled()
298
+ throttled()
299
+ throttled()
300
+ expect(fn).toHaveBeenCalledTimes(1)
301
+ })
302
+
303
+ it("should call with latest args after wait period", () => {
304
+ const fn = vi.fn()
305
+ const throttled = throttle(fn, 100)
306
+
307
+ throttled("first")
308
+ throttled("second")
309
+ throttled("third")
310
+
311
+ expect(fn).toHaveBeenCalledTimes(1)
312
+ expect(fn).toHaveBeenCalledWith("first")
313
+
314
+ vi.advanceTimersByTime(100)
315
+
316
+ expect(fn).toHaveBeenCalledTimes(2)
317
+ expect(fn).toHaveBeenLastCalledWith("third")
318
+ })
319
+
320
+ it("should allow immediate call after wait period elapses", () => {
321
+ const fn = vi.fn()
322
+ const throttled = throttle(fn, 100)
323
+
324
+ throttled()
325
+ vi.advanceTimersByTime(100)
326
+ throttled()
327
+ expect(fn).toHaveBeenCalledTimes(2)
328
+ })
329
+
330
+ it("should cancel pending invocations", () => {
331
+ const fn = vi.fn()
332
+ const throttled = throttle(fn, 100)
333
+
334
+ throttled("first")
335
+ throttled("second")
336
+ throttled.cancel()
337
+
338
+ vi.advanceTimersByTime(200)
339
+ expect(fn).toHaveBeenCalledTimes(1)
340
+ })
341
+
342
+ it("should work with default wait of 0", () => {
343
+ const fn = vi.fn()
344
+ const throttled = throttle(fn)
345
+ throttled()
346
+ expect(fn).toHaveBeenCalledTimes(1)
347
+ })
348
+
349
+ it("should skip trailing call when trailing: false", () => {
350
+ const fn = vi.fn()
351
+ const throttled = throttle(fn, 100, { trailing: false })
352
+
353
+ throttled("first")
354
+ throttled("second")
355
+ throttled("third")
356
+
357
+ expect(fn).toHaveBeenCalledTimes(1)
358
+ expect(fn).toHaveBeenCalledWith("first")
359
+
360
+ vi.advanceTimersByTime(200)
361
+ expect(fn).toHaveBeenCalledTimes(1)
362
+ })
363
+
364
+ it("should skip leading call when leading: false", () => {
365
+ const fn = vi.fn()
366
+ const throttled = throttle(fn, 100, { leading: false })
367
+
368
+ throttled("first")
369
+ expect(fn).toHaveBeenCalledTimes(0)
370
+
371
+ vi.advanceTimersByTime(100)
372
+ expect(fn).toHaveBeenCalledTimes(1)
373
+ expect(fn).toHaveBeenCalledWith("first")
374
+ })
375
+
376
+ it("should support leading: false with trailing: true (default)", () => {
377
+ const fn = vi.fn()
378
+ const throttled = throttle(fn, 100, { leading: false })
379
+
380
+ throttled("a")
381
+ throttled("b")
382
+ expect(fn).toHaveBeenCalledTimes(0)
383
+
384
+ vi.advanceTimersByTime(100)
385
+ expect(fn).toHaveBeenCalledTimes(1)
386
+ expect(fn).toHaveBeenCalledWith("b")
387
+ })
388
+
389
+ it("should still fire leading call after cooldown with trailing: false", () => {
390
+ const fn = vi.fn()
391
+ const throttled = throttle(fn, 100, { trailing: false })
392
+
393
+ throttled("first")
394
+ expect(fn).toHaveBeenCalledTimes(1)
395
+
396
+ vi.advanceTimersByTime(100)
397
+ throttled("second")
398
+ expect(fn).toHaveBeenCalledTimes(2)
399
+ expect(fn).toHaveBeenLastCalledWith("second")
400
+ })
401
+
402
+ it("should not fire trailing call when cancelled before timer fires", () => {
403
+ const fn = vi.fn()
404
+ const throttled = throttle(fn, 100)
405
+
406
+ // Leading call fires immediately
407
+ throttled("first")
408
+ expect(fn).toHaveBeenCalledTimes(1)
409
+
410
+ // Call again within wait period — queues trailing
411
+ throttled("second")
412
+
413
+ // Cancel before trailing timer fires
414
+ throttled.cancel()
415
+
416
+ vi.advanceTimersByTime(200)
417
+ // Should not have fired the trailing call
418
+ expect(fn).toHaveBeenCalledTimes(1)
419
+ })
420
+
421
+ it("should handle leading: false with trailing: false (no calls fire)", () => {
422
+ const fn = vi.fn()
423
+ const throttled = throttle(fn, 100, { leading: false, trailing: false })
424
+
425
+ throttled("a")
426
+ expect(fn).toHaveBeenCalledTimes(0)
427
+
428
+ vi.advanceTimersByTime(200)
429
+ expect(fn).toHaveBeenCalledTimes(0)
430
+ })
431
+ })
432
+
433
+ // --------------------------------------------------------
434
+ // merge
435
+ // --------------------------------------------------------
436
+ describe("merge", () => {
437
+ it("should deep merge two objects", () => {
438
+ const target = { a: { b: 1, c: 2 } }
439
+ const source = { a: { c: 3, d: 4 } }
440
+ expect(merge({ ...target }, source)).toEqual({ a: { b: 1, c: 3, d: 4 } })
441
+ })
442
+
443
+ it("should replace arrays instead of merging them", () => {
444
+ const target = { items: [1, 2, 3] }
445
+ const source = { items: [4, 5] }
446
+ expect(merge({ ...target }, source)).toEqual({ items: [4, 5] })
447
+ })
448
+
449
+ it("should handle multiple sources", () => {
450
+ const result = merge({ a: 1 }, { b: 2 }, { c: 3 })
451
+ expect(result).toEqual({ a: 1, b: 2, c: 3 })
452
+ })
453
+
454
+ it("should overwrite primitive values", () => {
455
+ expect(merge({ a: 1 }, { a: 2 })).toEqual({ a: 2 })
456
+ })
457
+
458
+ it("should skip null sources", () => {
459
+ const target = { a: 1 }
460
+ expect(merge(target, null as any)).toEqual({ a: 1 })
461
+ })
462
+
463
+ it("should skip undefined sources", () => {
464
+ const target = { a: 1 }
465
+ expect(merge(target, undefined as any)).toEqual({ a: 1 })
466
+ })
467
+
468
+ it("should not merge non-plain objects deeply", () => {
469
+ const date = new Date()
470
+ const result = merge({} as any, { d: date })
471
+ expect(result.d).toBe(date)
472
+ })
473
+
474
+ it("should return the target object (mutates)", () => {
475
+ const target = { a: 1 }
476
+ const result = merge(target, { b: 2 })
477
+ expect(result).toBe(target)
478
+ })
479
+
480
+ it("should deeply merge nested objects", () => {
481
+ const target = { a: { b: { c: 1 } } }
482
+ const source = { a: { b: { d: 2 } } }
483
+ expect(merge({ ...target }, source)).toEqual({
484
+ a: { b: { c: 1, d: 2 } },
485
+ })
486
+ })
487
+
488
+ // Security: prototype pollution protection
489
+ it("should not pollute Object.prototype via __proto__", () => {
490
+ const malicious = JSON.parse('{"__proto__": {"polluted": true}}')
491
+ merge({}, malicious)
492
+ expect(({} as any).polluted).toBeUndefined()
493
+ })
494
+
495
+ it("should not pollute via constructor.prototype", () => {
496
+ const malicious = JSON.parse('{"constructor": {"prototype": {"polluted": true}}}')
497
+ merge({}, malicious)
498
+ expect(({} as any).polluted).toBeUndefined()
499
+ })
500
+
501
+ it("should not pollute via prototype key", () => {
502
+ const malicious = { prototype: { polluted: true } }
503
+ merge({}, malicious)
504
+ expect(({} as any).polluted).toBeUndefined()
505
+ })
506
+
507
+ it("should overwrite target plain object with source array", () => {
508
+ const target = { a: { b: 1 } }
509
+ const source = { a: [1, 2, 3] }
510
+ const result = merge({ ...target }, source as any)
511
+ expect(result.a).toEqual([1, 2, 3])
512
+ })
513
+
514
+ it("should overwrite target array with source plain object", () => {
515
+ const target = { a: [1, 2] } as any
516
+ const source = { a: { key: "value" } }
517
+ const result = merge({ ...target }, source)
518
+ expect(result.a).toEqual({ key: "value" })
519
+ })
520
+
521
+ it("should handle source with class instances (non-plain objects)", () => {
522
+ class MyClass {
523
+ x = 1
524
+ }
525
+ const instance = new MyClass()
526
+ const target = { a: { old: true } }
527
+ const result = merge({ ...target }, { a: instance } as any)
528
+ // class instances are not plain objects, so they replace (not deep merge)
529
+ expect(result.a).toBe(instance)
530
+ })
531
+
532
+ it("should handle empty sources array", () => {
533
+ const target = { a: 1 }
534
+ const result = merge(target)
535
+ expect(result).toEqual({ a: 1 })
536
+ })
537
+ })
package/src/compose.ts ADDED
@@ -0,0 +1,11 @@
1
+ type ArityOneFn = (arg: any) => any
2
+ type PickLastInTuple<T extends any[]> = T extends [...rest: infer _U, argn: infer L] ? L : any
3
+ type FirstFnParameterType<T extends any[]> = Parameters<PickLastInTuple<T>>[any]
4
+ type LastFnReturnType<T extends any[]> = ReturnType<T[0]>
5
+
6
+ const compose =
7
+ <T extends ArityOneFn[]>(...fns: T) =>
8
+ (p: FirstFnParameterType<T>): LastFnReturnType<T> =>
9
+ fns.reduceRight((acc: any, cur: any) => cur(acc), p)
10
+
11
+ export default compose
package/src/config.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { StyledFunction } from "@pyreon/styler"
2
+ import { css, keyframes, styled } from "@pyreon/styler"
3
+ import type { HTMLTags } from "./html"
4
+
5
+ /**
6
+ * Describes the shape of the CSS-in-JS engine.
7
+ * Pyreon uses @pyreon/styler directly — no connector abstraction needed.
8
+ * This type is kept for API compatibility with downstream packages.
9
+ */
10
+ export interface CSSEngineConnector {
11
+ css: typeof css
12
+ styled: typeof styled
13
+ keyframes: typeof keyframes
14
+ }
15
+
16
+ interface PlatformConfig {
17
+ component: string | HTMLTags
18
+ textComponent: string | HTMLTags
19
+ createMediaQueries?: (props: {
20
+ breakpoints: Record<string, number>
21
+ rootSize: number
22
+ css: CSSEngineConnector["css"]
23
+ }) => Record<string, (...args: any[]) => any>
24
+ }
25
+
26
+ type InitConfig = Partial<CSSEngineConnector & PlatformConfig>
27
+
28
+ /**
29
+ * Configuration singleton that bridges the UI system with the CSS engine.
30
+ * All packages reference config.css, config.styled, etc.
31
+ *
32
+ * In Pyreon, the engine is @pyreon/styler and is available immediately —
33
+ * no lazy initialization or connector pattern needed.
34
+ */
35
+ class Configuration {
36
+ css = css
37
+ styled: StyledFunction = styled
38
+ keyframes = keyframes
39
+ component: string | HTMLTags = "div"
40
+ textComponent: string | HTMLTags = "span"
41
+ createMediaQueries: PlatformConfig["createMediaQueries"] = undefined
42
+
43
+ init = (props: InitConfig) => {
44
+ if (props.css) this.css = props.css
45
+ if (props.styled) this.styled = props.styled
46
+ if (props.keyframes) this.keyframes = props.keyframes
47
+ if (props.component) this.component = props.component
48
+ if (props.textComponent) this.textComponent = props.textComponent
49
+ if (props.createMediaQueries) this.createMediaQueries = props.createMediaQueries
50
+ }
51
+ }
52
+
53
+ const config = new Configuration()
54
+ const { init } = config
55
+
56
+ export default config
57
+ export { init }
@@ -0,0 +1,40 @@
1
+ import type { VNodeChild } from "@pyreon/core"
2
+ import { createContext, provide } from "@pyreon/core"
3
+ import isEmpty from "./isEmpty"
4
+ import type { Breakpoints } from "./types"
5
+
6
+ /**
7
+ * Internal context shared across all @pyreon packages.
8
+ * Carries the theme object plus any extra provider props.
9
+ */
10
+ const context = createContext<any>({})
11
+
12
+ type Theme = Partial<
13
+ {
14
+ rootSize: number
15
+ breakpoints: Breakpoints
16
+ } & Record<string, any>
17
+ >
18
+
19
+ type ProviderType = Partial<
20
+ {
21
+ theme: Theme
22
+ children: VNodeChild
23
+ } & Record<string, any>
24
+ >
25
+
26
+ /**
27
+ * Provider that feeds the internal Pyreon context with the theme.
28
+ * When no theme is supplied, renders children directly.
29
+ */
30
+ function Provider({ theme, children, ...props }: ProviderType): VNodeChild {
31
+ if (isEmpty(theme) || !theme) return children ?? null
32
+
33
+ provide(context, { theme, ...props })
34
+
35
+ return children ?? null
36
+ }
37
+
38
+ export { context }
39
+
40
+ export default Provider
@@ -0,0 +1,59 @@
1
+ const KNOWN_STATICS: Record<string, true> = {
2
+ name: true,
3
+ length: true,
4
+ prototype: true,
5
+ caller: true,
6
+ callee: true,
7
+ arguments: true,
8
+ arity: true,
9
+ }
10
+
11
+ const COMPONENT_STATICS: Record<string, true> = {
12
+ displayName: true,
13
+ defaultProps: true,
14
+ }
15
+
16
+ /**
17
+ * Copies non-framework static properties from a source component to a target.
18
+ *
19
+ * Pyreon equivalent of hoistNonReactStatics — simplified since Pyreon
20
+ * components are plain functions without React-specific statics like
21
+ * contextType, propTypes, getDerivedStateFromProps, etc.
22
+ */
23
+ const hoistNonReactStatics = <T, S>(
24
+ target: T,
25
+ source: S,
26
+ excludeList?: Record<string, true>,
27
+ ): T => {
28
+ if (typeof source === "string") return target
29
+
30
+ const proto = Object.getPrototypeOf(source)
31
+ if (proto && proto !== Object.prototype) {
32
+ hoistNonReactStatics(target, proto, excludeList)
33
+ }
34
+
35
+ const keys: (string | symbol)[] = [
36
+ ...Object.getOwnPropertyNames(source),
37
+ ...Object.getOwnPropertySymbols(source),
38
+ ]
39
+
40
+ for (const key of keys) {
41
+ const k = key as string
42
+ if (KNOWN_STATICS[k] || excludeList?.[k] || COMPONENT_STATICS[k]) {
43
+ continue
44
+ }
45
+
46
+ const descriptor = Object.getOwnPropertyDescriptor(source, key)
47
+ if (descriptor) {
48
+ try {
49
+ Object.defineProperty(target, key, descriptor)
50
+ } catch {
51
+ // Silently skip non-configurable properties
52
+ }
53
+ }
54
+ }
55
+
56
+ return target
57
+ }
58
+
59
+ export default hoistNonReactStatics