@pyreon/core 0.15.0 → 0.18.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.
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { makeReactiveProps, REACTIVE_PROP, _rp } from '../props'
2
+ import { makeReactiveProps, REACTIVE_PROP, _rp, _wrapSpread } from '../props'
3
3
 
4
4
  describe('makeReactiveProps', () => {
5
5
  it('returns raw object when no reactive props exist (fast path)', () => {
@@ -85,3 +85,73 @@ describe('_rp', () => {
85
85
  expect(branded()).toBe('hello')
86
86
  })
87
87
  })
88
+
89
+ describe('_wrapSpread', () => {
90
+ it('returns null/undefined unchanged (primitive guard)', () => {
91
+ expect(_wrapSpread(null)).toBe(null)
92
+ expect(_wrapSpread(undefined)).toBe(undefined)
93
+ })
94
+
95
+ it('returns source unchanged when no getter descriptors exist (fast path)', () => {
96
+ const source = { a: 1, b: 'x', c: true }
97
+ expect(_wrapSpread(source)).toBe(source)
98
+ })
99
+
100
+ it('returns source unchanged for empty objects', () => {
101
+ const source = {}
102
+ expect(_wrapSpread(source)).toBe(source)
103
+ })
104
+
105
+ it('wraps getter-shaped reactive props as _rp-branded thunks', () => {
106
+ let liveValue = 'a'
107
+ const source = {} as Record<string, unknown>
108
+ Object.defineProperty(source, 'x', {
109
+ get: () => liveValue,
110
+ enumerable: true,
111
+ configurable: true,
112
+ })
113
+
114
+ const result = _wrapSpread(source) as Record<string, unknown>
115
+ expect(result).not.toBe(source) // new object allocated
116
+
117
+ const wrappedX = result.x as () => unknown
118
+ expect(typeof wrappedX).toBe('function')
119
+ expect((wrappedX as unknown as Record<symbol, unknown>)[REACTIVE_PROP]).toBe(true)
120
+
121
+ // Lazy read — each call reads the current source[x] getter value
122
+ expect(wrappedX()).toBe('a')
123
+ liveValue = 'b'
124
+ expect(wrappedX()).toBe('b') // live re-read, not captured
125
+ })
126
+
127
+ it('preserves data properties as-is when mixed with getters', () => {
128
+ const source = { plain: 'data' } as Record<string, unknown>
129
+ Object.defineProperty(source, 'reactive', {
130
+ get: () => 'live',
131
+ enumerable: true,
132
+ configurable: true,
133
+ })
134
+
135
+ const result = _wrapSpread(source) as Record<string, unknown>
136
+ expect(result.plain).toBe('data') // copied through
137
+ expect(typeof result.reactive).toBe('function') // wrapped as thunk
138
+ })
139
+
140
+ it('preserves Reflect.ownKeys symbol-keyed properties', () => {
141
+ const sym = Symbol('marker')
142
+ const source = { regular: 'x' } as Record<string | symbol, unknown>
143
+ Object.defineProperty(source, 'reactive', {
144
+ get: () => 'live',
145
+ enumerable: true,
146
+ configurable: true,
147
+ })
148
+ source[sym] = 'symbol-value'
149
+
150
+ const result = _wrapSpread(source) as Record<string | symbol, unknown>
151
+ expect(result.regular).toBe('x')
152
+ // Note: symbol keys go through Reflect.ownKeys; the wrap path indexes
153
+ // via `key as string` for type narrowing but the runtime carries them
154
+ // forward as data properties.
155
+ expect(typeof result.reactive).toBe('function')
156
+ })
157
+ })
package/src/types.ts CHANGED
@@ -30,8 +30,49 @@ export type ComponentFn<P extends Props = Props> = (props: P) => VNodeChild
30
30
 
31
31
  // ─── Utility types ───────────────────────────────────────────────────────────
32
32
 
33
- /** Extract the props type from a component function, or pass through if already a props type. */
34
- export type ExtractProps<T> = T extends ComponentFn<infer P> ? P : T
33
+ /**
34
+ * Extract the props type from a component function, or pass through if already
35
+ * a props type. **Multi-overload aware** — matches up to 4 call signatures and
36
+ * produces the UNION of their first-argument types. A single-overload function
37
+ * still works (the union of 4 copies of the same props type dedupes back to
38
+ * the single shape).
39
+ *
40
+ * **Why this shape**. `T extends (props: infer P) => any ? P : never` only
41
+ * captures the LAST overload of a multi-overload function — TS's overload-
42
+ * resolution-against-conditional-types semantics. Multi-overload primitives
43
+ * (Iterator, List, Element, etc.) need the union of every overload's props
44
+ * to survive HOC wrapping (`rocketstyle()`, `attrs()`) without silently
45
+ * downgrading the public prop surface to the loosest overload. Mirrors
46
+ * vitus-labs PR #222.
47
+ *
48
+ * @example
49
+ * function Iterator<T extends SimpleValue>(p: { data: T[]; valueName?: string }): VNodeChild
50
+ * function Iterator<T extends ObjectValue>(p: { data: T[]; component: ComponentFn<T> }): VNodeChild
51
+ * type Props = ExtractProps<typeof Iterator>
52
+ * // → { data: SimpleValue[]; valueName?: string }
53
+ * // | { data: ObjectValue[]; component: ComponentFn<ObjectValue> }
54
+ */
55
+ export type ExtractProps<T> = T extends {
56
+ (props: infer P1, ...args: any): any
57
+ (props: infer P2, ...args: any): any
58
+ (props: infer P3, ...args: any): any
59
+ (props: infer P4, ...args: any): any
60
+ }
61
+ ? P1 | P2 | P3 | P4
62
+ : T extends {
63
+ (props: infer P1, ...args: any): any
64
+ (props: infer P2, ...args: any): any
65
+ (props: infer P3, ...args: any): any
66
+ }
67
+ ? P1 | P2 | P3
68
+ : T extends {
69
+ (props: infer P1, ...args: any): any
70
+ (props: infer P2, ...args: any): any
71
+ }
72
+ ? P1 | P2
73
+ : T extends ComponentFn<infer P>
74
+ ? P
75
+ : T
35
76
 
36
77
  /** A higher-order component that wraps a component, optionally transforming its props. */
37
78
  export type HigherOrderComponent<HOP extends Props, P extends Props | undefined = undefined> = (