@pyreon/attrs 0.24.5 → 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 +5 -7
- package/src/__tests__/attrs.test.ts +0 -531
- package/src/__tests__/attrsHoc.test.ts +0 -179
- package/src/__tests__/utils.test.ts +0 -241
- package/src/attrs.ts +0 -131
- package/src/env.d.ts +0 -6
- package/src/hoc/attrsHoc.ts +0 -74
- package/src/hoc/index.ts +0 -3
- package/src/index.ts +0 -26
- package/src/init.ts +0 -65
- package/src/isAttrsComponent.ts +0 -16
- package/src/types/AttrsComponent.ts +0 -83
- package/src/types/InitAttrsComponent.ts +0 -19
- package/src/types/attrs.ts +0 -2
- package/src/types/config.ts +0 -13
- package/src/types/configuration.ts +0 -40
- package/src/types/hoc.ts +0 -10
- package/src/types/utils.ts +0 -137
- package/src/utils/attrs.ts +0 -46
- package/src/utils/chaining.ts +0 -21
- package/src/utils/collection.ts +0 -14
- package/src/utils/compose.ts +0 -14
- package/src/utils/statics.ts +0 -16
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { h } from '@pyreon/core'
|
|
2
|
-
import createAttrsHOC from '../hoc/attrsHoc'
|
|
3
|
-
|
|
4
|
-
const Receiver = (props: any) => ({
|
|
5
|
-
type: 'div',
|
|
6
|
-
props: { ...props, 'data-testid': 'receiver' },
|
|
7
|
-
children: props.label ?? '',
|
|
8
|
-
key: null,
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
// --------------------------------------------------------
|
|
12
|
-
// attrsHoc - props merging
|
|
13
|
-
// --------------------------------------------------------
|
|
14
|
-
describe('attrsHoc - props merging', () => {
|
|
15
|
-
it('should pass through props unchanged when no attrs defined', () => {
|
|
16
|
-
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
17
|
-
const Enhanced = hoc(Receiver)
|
|
18
|
-
|
|
19
|
-
const result = Enhanced({ label: 'hello', 'data-custom': 'yes' }) as any
|
|
20
|
-
expect(result.children).toBe('hello')
|
|
21
|
-
expect(result.props['data-custom']).toBe('yes')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('should apply attrs as default props', () => {
|
|
25
|
-
const hoc = createAttrsHOC({
|
|
26
|
-
attrs: [(_props: any) => ({ label: 'default' })],
|
|
27
|
-
priorityAttrs: [],
|
|
28
|
-
})
|
|
29
|
-
const Enhanced = hoc(Receiver)
|
|
30
|
-
|
|
31
|
-
const result = Enhanced({}) as any
|
|
32
|
-
expect(result.children).toBe('default')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('should let explicit props override attrs', () => {
|
|
36
|
-
const hoc = createAttrsHOC({
|
|
37
|
-
attrs: [() => ({ label: 'from-attrs' })],
|
|
38
|
-
priorityAttrs: [],
|
|
39
|
-
})
|
|
40
|
-
const Enhanced = hoc(Receiver)
|
|
41
|
-
|
|
42
|
-
const result = Enhanced({ label: 'explicit' }) as any
|
|
43
|
-
expect(result.children).toBe('explicit')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('should apply priorityAttrs with lowest precedence', () => {
|
|
47
|
-
const hoc = createAttrsHOC({
|
|
48
|
-
attrs: [(_props: any) => ({ label: 'from-attrs' })],
|
|
49
|
-
priorityAttrs: [(_props: any) => ({ label: 'from-priority' })],
|
|
50
|
-
})
|
|
51
|
-
const Enhanced = hoc(Receiver)
|
|
52
|
-
|
|
53
|
-
const result = Enhanced({}) as any
|
|
54
|
-
expect(result.children).toBe('from-attrs')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('should merge results from multiple attrs functions', () => {
|
|
58
|
-
const hoc = createAttrsHOC({
|
|
59
|
-
attrs: [() => ({ 'data-first': 'a' }), () => ({ 'data-second': 'b' })],
|
|
60
|
-
priorityAttrs: [],
|
|
61
|
-
})
|
|
62
|
-
const Enhanced = hoc(Receiver)
|
|
63
|
-
|
|
64
|
-
const result = Enhanced({}) as any
|
|
65
|
-
expect(result.props['data-first']).toBe('a')
|
|
66
|
-
expect(result.props['data-second']).toBe('b')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('should remove undefined props so they dont override defaults', () => {
|
|
70
|
-
const hoc = createAttrsHOC({
|
|
71
|
-
attrs: [() => ({ label: 'default-label' })],
|
|
72
|
-
priorityAttrs: [],
|
|
73
|
-
})
|
|
74
|
-
const Enhanced = hoc(Receiver)
|
|
75
|
-
|
|
76
|
-
const result = Enhanced({ label: undefined }) as any
|
|
77
|
-
expect(result.children).toBe('default-label')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('should allow null to override defaults', () => {
|
|
81
|
-
const hoc = createAttrsHOC({
|
|
82
|
-
attrs: [() => ({ label: 'default-label' })],
|
|
83
|
-
priorityAttrs: [],
|
|
84
|
-
})
|
|
85
|
-
const Enhanced = hoc(Receiver)
|
|
86
|
-
|
|
87
|
-
const result = Enhanced({ label: null }) as any
|
|
88
|
-
expect(result.children).toBe('')
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// --------------------------------------------------------
|
|
93
|
-
// attrsHoc - attrs callback receives props
|
|
94
|
-
// --------------------------------------------------------
|
|
95
|
-
describe('attrsHoc - attrs callback receives props', () => {
|
|
96
|
-
it('should pass filtered props to attrs callback', () => {
|
|
97
|
-
const attrsFn = vi.fn(() => ({}))
|
|
98
|
-
const hoc = createAttrsHOC({
|
|
99
|
-
attrs: [attrsFn],
|
|
100
|
-
priorityAttrs: [],
|
|
101
|
-
})
|
|
102
|
-
const Enhanced = hoc(Receiver)
|
|
103
|
-
|
|
104
|
-
Enhanced({ variant: 'primary', size: 'lg' })
|
|
105
|
-
expect(attrsFn).toHaveBeenCalledWith(
|
|
106
|
-
expect.objectContaining({ variant: 'primary', size: 'lg' }),
|
|
107
|
-
)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('should pass priority attrs merged with props to attrs callback', () => {
|
|
111
|
-
const attrsFn = vi.fn(() => ({}))
|
|
112
|
-
const hoc = createAttrsHOC({
|
|
113
|
-
attrs: [attrsFn],
|
|
114
|
-
priorityAttrs: [() => ({ fromPriority: true })],
|
|
115
|
-
})
|
|
116
|
-
const Enhanced = hoc(Receiver)
|
|
117
|
-
|
|
118
|
-
Enhanced({ variant: 'primary' })
|
|
119
|
-
expect(attrsFn).toHaveBeenCalledWith(
|
|
120
|
-
expect.objectContaining({ variant: 'primary', fromPriority: true }),
|
|
121
|
-
)
|
|
122
|
-
})
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// --------------------------------------------------------
|
|
126
|
-
// attrsHoc - ref passthrough
|
|
127
|
-
// --------------------------------------------------------
|
|
128
|
-
describe('attrsHoc - ref passthrough', () => {
|
|
129
|
-
it('should pass ref as a normal prop to wrapped component', () => {
|
|
130
|
-
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
131
|
-
const Enhanced = hoc(Receiver)
|
|
132
|
-
|
|
133
|
-
const refObj = { current: null }
|
|
134
|
-
const result = Enhanced({ ref: refObj }) as any
|
|
135
|
-
expect(result.props.ref).toBe(refObj)
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// ─── attrsHoc — real h() round-trip (parallel to the Receiver mock) ──
|
|
140
|
-
//
|
|
141
|
-
// The Receiver above is a mock that returns a `{ type, props,
|
|
142
|
-
// children, key }` literal. The tests assert against that shape.
|
|
143
|
-
// This block re-runs the core HOC contracts against a Receiver that
|
|
144
|
-
// builds its return value via real `h()` from `@pyreon/core` —
|
|
145
|
-
// catches divergence between the mock shape and the actual VNode
|
|
146
|
-
// shape h() produces.
|
|
147
|
-
|
|
148
|
-
describe('attrsHoc — real h() round-trip', () => {
|
|
149
|
-
// Receiver that returns a real VNode via h() instead of a literal.
|
|
150
|
-
const ReceiverH = (props: any) =>
|
|
151
|
-
h('div', { ...props, 'data-testid': 'receiver' }, props.label ?? '')
|
|
152
|
-
|
|
153
|
-
it('passes through props unchanged when no attrs defined', () => {
|
|
154
|
-
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
155
|
-
const Enhanced = hoc(ReceiverH as any)
|
|
156
|
-
const result = Enhanced({ label: 'hello', 'data-custom': 'yes' }) as any
|
|
157
|
-
expect(result.props.label).toBe('hello')
|
|
158
|
-
expect(result.props['data-custom']).toBe('yes')
|
|
159
|
-
expect(result.type).toBe('div')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('applies attrs as default props through real h()', () => {
|
|
163
|
-
const hoc = createAttrsHOC({
|
|
164
|
-
attrs: [(_props: any) => ({ label: 'default' })],
|
|
165
|
-
priorityAttrs: [],
|
|
166
|
-
})
|
|
167
|
-
const Enhanced = hoc(ReceiverH as any)
|
|
168
|
-
const result = Enhanced({}) as any
|
|
169
|
-
expect(result.props.label).toBe('default')
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('passes ref through real h() output unchanged', () => {
|
|
173
|
-
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
174
|
-
const Enhanced = hoc(ReceiverH as any)
|
|
175
|
-
const refObj = { current: null }
|
|
176
|
-
const result = Enhanced({ ref: refObj }) as any
|
|
177
|
-
expect(result.props.ref).toBe(refObj)
|
|
178
|
-
})
|
|
179
|
-
})
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { calculateChainOptions, removeUndefinedProps } from '../utils/attrs'
|
|
2
|
-
import { chainOptions } from '../utils/chaining'
|
|
3
|
-
import { removeNullableValues } from '../utils/collection'
|
|
4
|
-
import { calculateHocsFuncs } from '../utils/compose'
|
|
5
|
-
import { createStaticsEnhancers } from '../utils/statics'
|
|
6
|
-
|
|
7
|
-
// --------------------------------------------------------
|
|
8
|
-
// removeUndefinedProps
|
|
9
|
-
// --------------------------------------------------------
|
|
10
|
-
describe('removeUndefinedProps', () => {
|
|
11
|
-
it('should remove properties with undefined values', () => {
|
|
12
|
-
const result = removeUndefinedProps({
|
|
13
|
-
a: 1,
|
|
14
|
-
b: undefined,
|
|
15
|
-
c: 'hello',
|
|
16
|
-
})
|
|
17
|
-
expect(result).toEqual({ a: 1, c: 'hello' })
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('should keep null values', () => {
|
|
21
|
-
const result = removeUndefinedProps({ a: null, b: undefined })
|
|
22
|
-
expect(result).toEqual({ a: null })
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('should keep false values', () => {
|
|
26
|
-
const result = removeUndefinedProps({ a: false, b: undefined })
|
|
27
|
-
expect(result).toEqual({ a: false })
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should keep zero values', () => {
|
|
31
|
-
const result = removeUndefinedProps({ a: 0, b: undefined })
|
|
32
|
-
expect(result).toEqual({ a: 0 })
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('should keep empty string values', () => {
|
|
36
|
-
const result = removeUndefinedProps({ a: '', b: undefined })
|
|
37
|
-
expect(result).toEqual({ a: '' })
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('should return empty object when all values are undefined', () => {
|
|
41
|
-
const result = removeUndefinedProps({ a: undefined, b: undefined })
|
|
42
|
-
expect(result).toEqual({})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('should return all props when none are undefined', () => {
|
|
46
|
-
const input = { a: 1, b: 'test', c: true }
|
|
47
|
-
const result = removeUndefinedProps(input)
|
|
48
|
-
expect(result).toEqual(input)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('should handle empty object', () => {
|
|
52
|
-
const result = removeUndefinedProps({})
|
|
53
|
-
expect(result).toEqual({})
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
// --------------------------------------------------------
|
|
58
|
-
// removeNullableValues
|
|
59
|
-
// --------------------------------------------------------
|
|
60
|
-
describe('removeNullableValues', () => {
|
|
61
|
-
it('should remove null values', () => {
|
|
62
|
-
const result = removeNullableValues({ a: 1, b: null })
|
|
63
|
-
expect(result).toEqual({ a: 1 })
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should remove undefined values', () => {
|
|
67
|
-
const result = removeNullableValues({ a: 1, b: undefined })
|
|
68
|
-
expect(result).toEqual({ a: 1 })
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should remove false values', () => {
|
|
72
|
-
const result = removeNullableValues({ a: 1, b: false })
|
|
73
|
-
expect(result).toEqual({ a: 1 })
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('should keep zero values', () => {
|
|
77
|
-
const result = removeNullableValues({ a: 0, b: null })
|
|
78
|
-
expect(result).toEqual({ a: 0 })
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('should keep empty string values', () => {
|
|
82
|
-
const result = removeNullableValues({ a: '', b: null })
|
|
83
|
-
expect(result).toEqual({ a: '' })
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should keep truthy values', () => {
|
|
87
|
-
const result = removeNullableValues({ a: 1, b: 'test', c: true })
|
|
88
|
-
expect(result).toEqual({ a: 1, b: 'test', c: true })
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('should handle empty object', () => {
|
|
92
|
-
const result = removeNullableValues({})
|
|
93
|
-
expect(result).toEqual({})
|
|
94
|
-
})
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
// --------------------------------------------------------
|
|
98
|
-
// calculateChainOptions
|
|
99
|
-
// --------------------------------------------------------
|
|
100
|
-
describe('calculateChainOptions', () => {
|
|
101
|
-
it('should return empty object when no options provided', () => {
|
|
102
|
-
const calculate = calculateChainOptions(undefined)
|
|
103
|
-
const result = calculate([{}])
|
|
104
|
-
expect(result).toEqual({})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should return empty object for empty options array', () => {
|
|
108
|
-
const calculate = calculateChainOptions([])
|
|
109
|
-
const result = calculate([{}])
|
|
110
|
-
expect(result).toEqual({})
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('should execute a single option function', () => {
|
|
114
|
-
const fn = (props: any) => ({
|
|
115
|
-
color: props.variant === 'primary' ? 'blue' : 'gray',
|
|
116
|
-
})
|
|
117
|
-
const calculate = calculateChainOptions([fn])
|
|
118
|
-
const result = calculate([{ variant: 'primary' }])
|
|
119
|
-
expect(result).toEqual({ color: 'blue' })
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('should merge results from multiple option functions', () => {
|
|
123
|
-
const fn1 = (_: any) => ({ color: 'blue' })
|
|
124
|
-
const fn2 = (_: any) => ({ size: 'large' })
|
|
125
|
-
const calculate = calculateChainOptions([fn1, fn2])
|
|
126
|
-
const result = calculate([{}])
|
|
127
|
-
expect(result).toEqual({ color: 'blue', size: 'large' })
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('should let later functions override earlier ones', () => {
|
|
131
|
-
const fn1 = (_: any) => ({ color: 'blue' })
|
|
132
|
-
const fn2 = (_: any) => ({ color: 'red' })
|
|
133
|
-
const calculate = calculateChainOptions([fn1, fn2])
|
|
134
|
-
const result = calculate([{}])
|
|
135
|
-
expect(result).toEqual({ color: 'red' })
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('should pass arguments to each option function', () => {
|
|
139
|
-
const fn = vi.fn((_: any) => ({}))
|
|
140
|
-
const calculate = calculateChainOptions([fn])
|
|
141
|
-
const props = { variant: 'primary' }
|
|
142
|
-
calculate([props])
|
|
143
|
-
expect(fn).toHaveBeenCalledWith(props)
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// --------------------------------------------------------
|
|
148
|
-
// chainOptions
|
|
149
|
-
// --------------------------------------------------------
|
|
150
|
-
describe('chainOptions', () => {
|
|
151
|
-
it('should return default options when opts is undefined', () => {
|
|
152
|
-
const defaults = [() => ({})]
|
|
153
|
-
const result = chainOptions(undefined, defaults)
|
|
154
|
-
expect(result).toEqual(defaults)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('should append function to defaults', () => {
|
|
158
|
-
const fn1 = () => ({ a: 1 })
|
|
159
|
-
const fn2 = () => ({ b: 2 })
|
|
160
|
-
const result = chainOptions(fn2, [fn1])
|
|
161
|
-
expect(result).toHaveLength(2)
|
|
162
|
-
expect(result[0]).toBe(fn1)
|
|
163
|
-
expect(result[1]).toBe(fn2)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('should wrap object in a function and append', () => {
|
|
167
|
-
const obj = { color: 'blue' }
|
|
168
|
-
const result = chainOptions(obj, [])
|
|
169
|
-
expect(result).toHaveLength(1)
|
|
170
|
-
expect(result[0]?.()).toEqual(obj)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('should return empty array when no defaults and undefined opts', () => {
|
|
174
|
-
const result = chainOptions(undefined, [])
|
|
175
|
-
expect(result).toEqual([])
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('should not mutate the defaults array', () => {
|
|
179
|
-
const defaults = [() => ({})]
|
|
180
|
-
const fn = () => ({ a: 1 })
|
|
181
|
-
const result = chainOptions(fn, defaults)
|
|
182
|
-
expect(defaults).toHaveLength(1)
|
|
183
|
-
expect(result).toHaveLength(2)
|
|
184
|
-
})
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
// --------------------------------------------------------
|
|
188
|
-
// createStaticsEnhancers
|
|
189
|
-
// --------------------------------------------------------
|
|
190
|
-
describe('createStaticsEnhancers', () => {
|
|
191
|
-
it('should assign options to context', () => {
|
|
192
|
-
const context: Record<string, any> = {}
|
|
193
|
-
createStaticsEnhancers({
|
|
194
|
-
context,
|
|
195
|
-
options: { theme: 'dark', variant: 'primary' },
|
|
196
|
-
})
|
|
197
|
-
expect(context).toEqual({ theme: 'dark', variant: 'primary' })
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('should not modify context when options is empty', () => {
|
|
201
|
-
const context: Record<string, any> = { existing: true }
|
|
202
|
-
createStaticsEnhancers({ context, options: {} })
|
|
203
|
-
expect(context).toEqual({ existing: true })
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('should merge with existing context properties', () => {
|
|
207
|
-
const context: Record<string, any> = { existing: true }
|
|
208
|
-
createStaticsEnhancers({ context, options: { newProp: 'value' } })
|
|
209
|
-
expect(context).toEqual({ existing: true, newProp: 'value' })
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
// --------------------------------------------------------
|
|
214
|
-
// calculateHocsFuncs
|
|
215
|
-
// --------------------------------------------------------
|
|
216
|
-
describe('calculateHocsFuncs', () => {
|
|
217
|
-
it('should return empty array for empty options', () => {
|
|
218
|
-
const result = calculateHocsFuncs({})
|
|
219
|
-
expect(result).toEqual([])
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('should filter out non-function values', () => {
|
|
223
|
-
const fn = (x: any) => x
|
|
224
|
-
const result = calculateHocsFuncs({ a: fn, b: 'string', c: 123 })
|
|
225
|
-
expect(result).toHaveLength(1)
|
|
226
|
-
expect(result[0]).toBe(fn)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('should reverse the order of functions', () => {
|
|
230
|
-
const fn1 = (x: any) => x
|
|
231
|
-
const fn2 = (x: any) => x
|
|
232
|
-
const result = calculateHocsFuncs({ a: fn1, b: fn2 })
|
|
233
|
-
expect(result[0]).toBe(fn2)
|
|
234
|
-
expect(result[1]).toBe(fn1)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('should return empty array for undefined input', () => {
|
|
238
|
-
const result = calculateHocsFuncs(undefined as any)
|
|
239
|
-
expect(result).toEqual([])
|
|
240
|
-
})
|
|
241
|
-
})
|
package/src/attrs.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { compose, hoistNonReactStatics, omit, pick } from '@pyreon/ui-core'
|
|
2
|
-
import { attrsHoc } from './hoc'
|
|
3
|
-
import type { AttrsComponent as AttrsComponentType } from './types/AttrsComponent'
|
|
4
|
-
import type { Configuration, ExtendedConfiguration } from './types/configuration'
|
|
5
|
-
import type { InitAttrsComponent } from './types/InitAttrsComponent'
|
|
6
|
-
import { calculateChainOptions } from './utils/attrs'
|
|
7
|
-
import { chainOptions } from './utils/chaining'
|
|
8
|
-
import { calculateHocsFuncs } from './utils/compose'
|
|
9
|
-
import { createStaticsEnhancers } from './utils/statics'
|
|
10
|
-
|
|
11
|
-
// Dev-mode gate. `import.meta.env.DEV` is literal-replaced by Vite at build
|
|
12
|
-
// time and tree-shakes to zero bytes in prod. The previous
|
|
13
|
-
// `process.env.NODE_ENV !== 'production'` form was dead code in real Vite
|
|
14
|
-
// browser bundles (Vite does not polyfill `process`).
|
|
15
|
-
const __DEV__ = process.env.NODE_ENV !== 'production'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Clones the current configuration and merges new options, then creates a
|
|
19
|
-
* fresh component. This makes the chaining API immutable — each `.attrs()`
|
|
20
|
-
* / `.config()` / `.statics()` call returns a brand-new component with an
|
|
21
|
-
* updated configuration rather than mutating the existing one.
|
|
22
|
-
*/
|
|
23
|
-
type CloneAndEnhance = (
|
|
24
|
-
defaultOpts: Configuration,
|
|
25
|
-
opts: Partial<ExtendedConfiguration>,
|
|
26
|
-
) => ReturnType<typeof attrsComponent>
|
|
27
|
-
|
|
28
|
-
const cloneAndEnhance: CloneAndEnhance = (defaultOpts, opts) =>
|
|
29
|
-
attrsComponent({
|
|
30
|
-
...defaultOpts,
|
|
31
|
-
...(opts.name ? { name: opts.name } : undefined),
|
|
32
|
-
...(opts.component ? { component: opts.component } : undefined),
|
|
33
|
-
attrs: chainOptions(opts.attrs, defaultOpts.attrs),
|
|
34
|
-
filterAttrs: [...(defaultOpts.filterAttrs ?? []), ...(opts.filterAttrs ?? [])],
|
|
35
|
-
priorityAttrs: chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
|
|
36
|
-
statics: { ...defaultOpts.statics, ...opts.statics },
|
|
37
|
-
compose: { ...defaultOpts.compose, ...opts.compose },
|
|
38
|
-
} as Parameters<typeof attrsComponent>[0])
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Core factory that builds an attrs-enhanced Pyreon component.
|
|
42
|
-
*
|
|
43
|
-
* Creates a plain ComponentFn that:
|
|
44
|
-
* 1. Wraps the original with attrsHoc (default props) + user HOCs from `.compose()`.
|
|
45
|
-
* 2. Filters out internal props listed in `filterAttrs`.
|
|
46
|
-
* 3. Attaches `data-attrs` attribute in development for debugging.
|
|
47
|
-
*
|
|
48
|
-
* Then adds chaining methods (`.attrs()`, `.config()`, `.compose()`, `.statics()`)
|
|
49
|
-
* as static properties — each calls `cloneAndEnhance` to produce a new component.
|
|
50
|
-
*
|
|
51
|
-
* In Pyreon, there is no forwardRef — ref flows as a normal prop.
|
|
52
|
-
* Components are plain functions that run once per mount.
|
|
53
|
-
*/
|
|
54
|
-
const attrsComponent: InitAttrsComponent = (options) => {
|
|
55
|
-
const componentName = options.name ?? options.component.displayName ?? options.component.name
|
|
56
|
-
|
|
57
|
-
const RenderComponent = options.component
|
|
58
|
-
|
|
59
|
-
// Build the HOC chain: attrsHoc is always first (resolves default props),
|
|
60
|
-
// followed by user-composed HOCs in reverse order (outermost wraps first).
|
|
61
|
-
const hocsFuncs = [attrsHoc(options), ...calculateHocsFuncs(options.compose)]
|
|
62
|
-
|
|
63
|
-
// The inner component receives already-computed props from the HOC chain.
|
|
64
|
-
// It handles prop filtering and final rendering.
|
|
65
|
-
const EnhancedComponent = (props: Record<string, any>) => {
|
|
66
|
-
const needsFiltering = options.filterAttrs && options.filterAttrs.length > 0
|
|
67
|
-
|
|
68
|
-
const filteredProps = needsFiltering ? omit(props, options.filterAttrs) : props
|
|
69
|
-
|
|
70
|
-
const finalProps = __DEV__
|
|
71
|
-
? { ...filteredProps, 'data-attrs': componentName }
|
|
72
|
-
: filteredProps
|
|
73
|
-
|
|
74
|
-
return RenderComponent(finalProps)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Apply the full HOC chain: compose(attrsHoc, ...userHocs)(EnhancedComponent)
|
|
78
|
-
const AttrsComponent: AttrsComponentType = compose(...hocsFuncs)(EnhancedComponent)
|
|
79
|
-
|
|
80
|
-
AttrsComponent.IS_ATTRS = true
|
|
81
|
-
AttrsComponent.displayName = componentName
|
|
82
|
-
AttrsComponent.meta = {}
|
|
83
|
-
|
|
84
|
-
// Copy static properties from the original component.
|
|
85
|
-
hoistNonReactStatics(AttrsComponent, options.component)
|
|
86
|
-
|
|
87
|
-
// Populate `component.meta` with user-defined statics from `.statics()`.
|
|
88
|
-
createStaticsEnhancers({
|
|
89
|
-
context: AttrsComponent.meta,
|
|
90
|
-
options: options.statics,
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// ─── Chaining Methods ──────────────────────────────────
|
|
94
|
-
// Each method creates a new component via cloneAndEnhance.
|
|
95
|
-
// The original component is never mutated.
|
|
96
|
-
Object.assign(AttrsComponent, {
|
|
97
|
-
attrs: (attrs: any, { priority, filter }: any = {}) => {
|
|
98
|
-
const result: Record<string, any> = {}
|
|
99
|
-
|
|
100
|
-
if (filter) {
|
|
101
|
-
result.filterAttrs = filter
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (priority) {
|
|
105
|
-
result.priorityAttrs = attrs as ExtendedConfiguration['priorityAttrs']
|
|
106
|
-
|
|
107
|
-
return cloneAndEnhance(options, result)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
result.attrs = attrs as ExtendedConfiguration['attrs']
|
|
111
|
-
|
|
112
|
-
return cloneAndEnhance(options, result)
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
config: (opts: any = {}) => {
|
|
116
|
-
const result = pick(opts)
|
|
117
|
-
|
|
118
|
-
return cloneAndEnhance(options, result)
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
compose: (opts: any) => cloneAndEnhance(options, { compose: opts }),
|
|
122
|
-
|
|
123
|
-
statics: (opts: any) => cloneAndEnhance(options, { statics: opts }),
|
|
124
|
-
|
|
125
|
-
getDefaultAttrs: (props: any) => calculateChainOptions(options.attrs)([props]),
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
return AttrsComponent
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export default attrsComponent
|
package/src/env.d.ts
DELETED
package/src/hoc/attrsHoc.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { Configuration } from '../types/configuration'
|
|
2
|
-
import type { ComponentFn } from '../types/utils'
|
|
3
|
-
import { calculateChainOptions, removeUndefinedProps } from '../utils/attrs'
|
|
4
|
-
|
|
5
|
-
export type AttrsStyleHOC = ({
|
|
6
|
-
attrs,
|
|
7
|
-
priorityAttrs,
|
|
8
|
-
}: Pick<Configuration, 'attrs' | 'priorityAttrs'>) => (
|
|
9
|
-
WrappedComponent: ComponentFn<any>,
|
|
10
|
-
) => ComponentFn<any>
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Creates the core HOC that computes default props from the `.attrs()` chain.
|
|
14
|
-
*
|
|
15
|
-
* This is always the outermost HOC in the compose chain, so it runs first.
|
|
16
|
-
* It resolves both priority and normal attrs callbacks, then merges them
|
|
17
|
-
* with the consumer's explicit props following this precedence:
|
|
18
|
-
*
|
|
19
|
-
* priorityAttrs < normalAttrs < explicit props (last wins)
|
|
20
|
-
*
|
|
21
|
-
* In Pyreon, components are plain functions — no forwardRef needed.
|
|
22
|
-
* The ref flows as a normal prop through the chain.
|
|
23
|
-
*/
|
|
24
|
-
const createAttrsHOC: AttrsStyleHOC = ({ attrs, priorityAttrs }) => {
|
|
25
|
-
// Pre-build the chain reducers once (not per render).
|
|
26
|
-
const calculateAttrs = calculateChainOptions(attrs)
|
|
27
|
-
const calculatePriorityAttrs = calculateChainOptions(priorityAttrs)
|
|
28
|
-
// Most components never call .attrs() — short-circuit the merge work below
|
|
29
|
-
// so the no-chain mount path skips 2 reducer invocations + 3 object spreads.
|
|
30
|
-
// Mirrors vitus-labs's attrsHoc fast-path; React-side memoization
|
|
31
|
-
// (useMemo / useStableValue) is omitted because Pyreon components run once
|
|
32
|
-
// per mount, not on every render.
|
|
33
|
-
const hasAttrs = (attrs?.length ?? 0) > 0
|
|
34
|
-
const hasPriorityAttrs = (priorityAttrs?.length ?? 0) > 0
|
|
35
|
-
const hasAnyChain = hasAttrs || hasPriorityAttrs
|
|
36
|
-
|
|
37
|
-
const attrsHoc = (WrappedComponent: ComponentFn<any>) => {
|
|
38
|
-
const HOCComponent: ComponentFn<any> = (props) => {
|
|
39
|
-
// Strip undefined values so they don't shadow defaults from attrs callbacks.
|
|
40
|
-
const filteredProps = removeUndefinedProps(props)
|
|
41
|
-
|
|
42
|
-
// Fast path: no attrs configured — skip reducers + spreads entirely.
|
|
43
|
-
if (!hasAnyChain) return WrappedComponent(filteredProps)
|
|
44
|
-
|
|
45
|
-
// 1. Resolve priority attrs (lowest precedence defaults).
|
|
46
|
-
const prioritizedAttrs = hasPriorityAttrs
|
|
47
|
-
? calculatePriorityAttrs([filteredProps])
|
|
48
|
-
: null
|
|
49
|
-
// 2. Resolve normal attrs — these see priority + explicit props as input.
|
|
50
|
-
const finalAttrs = hasAttrs
|
|
51
|
-
? calculateAttrs([
|
|
52
|
-
prioritizedAttrs
|
|
53
|
-
? { ...prioritizedAttrs, ...filteredProps }
|
|
54
|
-
: filteredProps,
|
|
55
|
-
])
|
|
56
|
-
: null
|
|
57
|
-
|
|
58
|
-
// 3. Merge: priority < normal attrs < explicit props (last wins).
|
|
59
|
-
const finalProps = {
|
|
60
|
-
...prioritizedAttrs,
|
|
61
|
-
...finalAttrs,
|
|
62
|
-
...filteredProps,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return WrappedComponent(finalProps)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return HOCComponent
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return attrsHoc
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export default createAttrsHOC
|
package/src/hoc/index.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Attrs } from './init'
|
|
2
|
-
import attrs from './init'
|
|
3
|
-
import type { IsAttrsComponent } from './isAttrsComponent'
|
|
4
|
-
import isAttrsComponent from './isAttrsComponent'
|
|
5
|
-
import type { AttrsComponent } from './types/AttrsComponent'
|
|
6
|
-
import type { AttrsCb } from './types/attrs'
|
|
7
|
-
import type { AttrsComponentType, ConfigAttrs } from './types/config'
|
|
8
|
-
import type { ComposeParam, GenericHoc } from './types/hoc'
|
|
9
|
-
import type { ComponentFn, ElementType, TObj } from './types/utils'
|
|
10
|
-
|
|
11
|
-
export type {
|
|
12
|
-
Attrs,
|
|
13
|
-
AttrsCb,
|
|
14
|
-
AttrsComponent,
|
|
15
|
-
AttrsComponentType,
|
|
16
|
-
ComponentFn,
|
|
17
|
-
ComposeParam,
|
|
18
|
-
ConfigAttrs,
|
|
19
|
-
ElementType,
|
|
20
|
-
GenericHoc,
|
|
21
|
-
IsAttrsComponent,
|
|
22
|
-
TObj,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export { attrs, isAttrsComponent }
|
|
26
|
-
export default attrs
|