@pyreon/attrs 0.0.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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/lib/index.d.ts +157 -0
- package/lib/index.js +177 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Vit Bokisch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# @pyreon/attrs
|
|
2
|
+
|
|
3
|
+
Immutable, chainable default-props factory for Pyreon components.
|
|
4
|
+
|
|
5
|
+
Think of styled-components' `.attrs()` as a standalone, type-safe composition system. Define default props, swap base components, attach HOCs, and add metadata — all through an immutable chain where every call returns a new component.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Immutable chaining** — every method returns a new component, never mutates the original
|
|
10
|
+
- **Props merge order** — `priorityAttrs` > `attrs` > explicit props, with full control over precedence
|
|
11
|
+
- **Prop filtering** — strip internal props before they reach the DOM
|
|
12
|
+
- **HOC composition** — named HOCs via `.compose()` with selective removal
|
|
13
|
+
- **Static metadata** — attach and access custom data via `.statics()` / `.meta`
|
|
14
|
+
- **TypeScript inference** — generics accumulate through the chain
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add @pyreon/attrs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import attrs from '@pyreon/attrs'
|
|
26
|
+
import { Element } from '@pyreon/elements'
|
|
27
|
+
|
|
28
|
+
const Button = attrs({ name: 'Button', component: Element })
|
|
29
|
+
.attrs({ tag: 'button', alignX: 'center', alignY: 'center' })
|
|
30
|
+
|
|
31
|
+
// Renders Element with tag="button", alignX="center", alignY="center"
|
|
32
|
+
Button({ label: 'Click me' })
|
|
33
|
+
|
|
34
|
+
// Explicit props override attrs defaults
|
|
35
|
+
Button({ tag: 'a', label: 'Link button' })
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
### attrs(options)
|
|
41
|
+
|
|
42
|
+
Creates an attrs-enhanced component.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const Component = attrs({
|
|
46
|
+
name: 'ComponentName', // required — sets displayName
|
|
47
|
+
component: BaseComponent, // required — the Pyreon component to wrap
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### .attrs(props | callback, options?)
|
|
52
|
+
|
|
53
|
+
Add default props. Can be called multiple times — defaults stack left-to-right.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// Object form — static defaults
|
|
57
|
+
Button.attrs({ tag: 'button' })
|
|
58
|
+
|
|
59
|
+
// Callback form — computed defaults based on current props
|
|
60
|
+
Button.attrs((props) => ({
|
|
61
|
+
'aria-label': props.label,
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
// Priority — resolved before regular attrs, cannot be overridden by explicit props
|
|
65
|
+
Button.attrs({ tag: 'button' }, { priority: true })
|
|
66
|
+
|
|
67
|
+
// Filter — remove props before passing to the underlying component
|
|
68
|
+
Button.attrs({}, { filter: ['internalFlag', 'variant'] })
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Props merge order:**
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
priorityAttrs (highest) → attrs → explicit props (lowest for priority, highest for regular)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For regular attrs, explicit props win. For priority attrs, the priority value wins.
|
|
78
|
+
|
|
79
|
+
### .config(options)
|
|
80
|
+
|
|
81
|
+
Reconfigure the component. Returns a new component instance.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// Rename
|
|
85
|
+
Button.config({ name: 'PrimaryButton' })
|
|
86
|
+
|
|
87
|
+
// Swap the base component
|
|
88
|
+
Button.config({ component: AnotherComponent })
|
|
89
|
+
|
|
90
|
+
// Enable debug mode — adds data-attrs attribute in development
|
|
91
|
+
Button.config({ DEBUG: true })
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### .compose(hocs)
|
|
95
|
+
|
|
96
|
+
Attach named Higher-Order Components. Applied in declaration order.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const Enhanced = Button.compose({
|
|
100
|
+
withTheme: (Component) => (props) => Component({ ...props, themed: true }),
|
|
101
|
+
withTracking: trackingHoc,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Remove a specific HOC from the chain
|
|
105
|
+
const WithoutTracking = Enhanced.compose({ withTracking: null })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### .statics(metadata)
|
|
109
|
+
|
|
110
|
+
Attach metadata accessible via the `.meta` property.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const Button = attrs({ name: 'Button', component: Element })
|
|
114
|
+
.statics({ category: 'action', sizes: ['sm', 'md', 'lg'] })
|
|
115
|
+
|
|
116
|
+
Button.meta.category // => 'action'
|
|
117
|
+
Button.meta.sizes // => ['sm', 'md', 'lg']
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### isAttrsComponent(value)
|
|
121
|
+
|
|
122
|
+
Runtime type guard.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { isAttrsComponent } from '@pyreon/attrs'
|
|
126
|
+
|
|
127
|
+
isAttrsComponent(Button) // => true
|
|
128
|
+
isAttrsComponent('div') // => false
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### getDefaultAttrs()
|
|
132
|
+
|
|
133
|
+
Retrieve the computed default props for a component.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
Button.getDefaultAttrs() // => { tag: 'button', alignX: 'center', ... }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## TypeScript
|
|
140
|
+
|
|
141
|
+
Each `.attrs<P>()` call adds `P` to the component's prop types through `MergeTypes`:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const Base = attrs({ name: 'Base', component: Element })
|
|
145
|
+
|
|
146
|
+
const Typed = Base
|
|
147
|
+
.attrs<{ variant: 'primary' | 'secondary' }>({ variant: 'primary' })
|
|
148
|
+
.attrs<{ size?: 'sm' | 'md' | 'lg' }>({})
|
|
149
|
+
|
|
150
|
+
// Typed accepts: Element props + { variant, size? }
|
|
151
|
+
Typed({ variant: 'secondary', size: 'lg', label: 'Hello' })
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Access the accumulated types via type-only properties:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
type AllProps = typeof Typed.$$types
|
|
158
|
+
type OriginalProps = typeof Typed.$$originTypes
|
|
159
|
+
type ExtendedProps = typeof Typed.$$extendedTypes
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Peer Dependencies
|
|
163
|
+
|
|
164
|
+
| Package | Version |
|
|
165
|
+
| ------- | ------- |
|
|
166
|
+
| @pyreon/core | >= 0.0.1 |
|
|
167
|
+
| @pyreon/ui-core | >= 0.0.1 |
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { VNode } from "@pyreon/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types/attrs.d.ts
|
|
4
|
+
/** Callback form of `.attrs()` — receives current props and returns partial overrides. */
|
|
5
|
+
type AttrsCb<A> = (props: Partial<A>) => Partial<A>;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/types/utils.d.ts
|
|
8
|
+
type TObj = Record<string, unknown>;
|
|
9
|
+
type TFn = (...args: any) => any;
|
|
10
|
+
/**
|
|
11
|
+
* A Pyreon component function that accepts additional static properties.
|
|
12
|
+
* In Pyreon, components are plain functions: (props: P) => VNode | null.
|
|
13
|
+
*/
|
|
14
|
+
type ComponentFn<P = any> = ((props: P) => VNode | null) & Partial<Record<string, any>>;
|
|
15
|
+
/**
|
|
16
|
+
* An element type — either a Pyreon component function or an intrinsic tag string.
|
|
17
|
+
*/
|
|
18
|
+
type ElementType<T extends TObj | unknown = any> = ComponentFn<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Forces TypeScript to expand/flatten a type for better IDE display.
|
|
21
|
+
* Short-circuits for `any` — the mapped type would turn `any` into
|
|
22
|
+
* `{ [x: string]: any; [x: number]: any; [x: symbol]: any }`, losing
|
|
23
|
+
* its identity as `any` and breaking downstream type checks.
|
|
24
|
+
*/
|
|
25
|
+
type Id<T> = 0 extends 1 & T ? T : T extends infer U ? { [K in keyof U]: U[K] } : never;
|
|
26
|
+
/**
|
|
27
|
+
* Strips keys whose values are `never`, `null`, or `undefined`.
|
|
28
|
+
* Uses tuple wrapping `[T[P]] extends [never]` to avoid distribution
|
|
29
|
+
* over union types (a bare `T[P] extends never` would incorrectly
|
|
30
|
+
* match union members).
|
|
31
|
+
*
|
|
32
|
+
* Short-circuits for `any` — the `as` clause in mapped types loses
|
|
33
|
+
* index signatures, which would turn `any` into an empty type.
|
|
34
|
+
*/
|
|
35
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
36
|
+
type ExtractNullableKeys<T> = 0 extends 1 & T ? T : { [P in keyof T as IsAny<T[P]> extends true ? P : [T[P]] extends [never] ? never : [T[P]] extends [null | undefined] ? never : P]: T[P] };
|
|
37
|
+
/** Merges two types: keeps all keys from L that don't exist in R, then adds all of R. */
|
|
38
|
+
type SpreadTwo<L, R> = Id<Pick<L, Exclude<keyof L, keyof R>> & R>;
|
|
39
|
+
/** Recursively spreads a tuple of types left-to-right. */
|
|
40
|
+
type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ? SpreadTwo<L, Spread<R>> : unknown;
|
|
41
|
+
/** Recursively checks whether any element in the tuple is `any`. */
|
|
42
|
+
type _HasAny<A> = A extends [infer L, ...infer R] ? (0 extends 1 & L ? true : _HasAny<R>) : false;
|
|
43
|
+
type MergeTypes<A extends readonly [...any]> = _HasAny<A> extends true ? any : ExtractNullableKeys<Spread<A>>;
|
|
44
|
+
/** Extracts the props type from a Pyreon component function. */
|
|
45
|
+
type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends ComponentFn<infer TProps> ? TProps : TComponentOrTProps;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/types/config.d.ts
|
|
48
|
+
/** A component that has been enhanced by attrs — identified by the `IS_ATTRS` marker. */
|
|
49
|
+
type AttrsComponentType = ElementType & {
|
|
50
|
+
IS_ATTRS: true;
|
|
51
|
+
};
|
|
52
|
+
/** Parameters accepted by the `.config()` chaining method. */
|
|
53
|
+
type ConfigAttrs<C extends ElementType | unknown> = Partial<{
|
|
54
|
+
name: string;
|
|
55
|
+
component: C;
|
|
56
|
+
DEBUG: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/types/hoc.d.ts
|
|
60
|
+
type GenericHoc = (component: ElementType) => ElementType;
|
|
61
|
+
/**
|
|
62
|
+
* Parameters for `.compose()` — a record of named HOCs.
|
|
63
|
+
* Setting a key to `null`, `undefined`, or `false` removes a
|
|
64
|
+
* previously defined HOC from the chain.
|
|
65
|
+
*/
|
|
66
|
+
type ComposeParam = Record<string, GenericHoc | null | undefined | false>;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/types/AttrsComponent.d.ts
|
|
69
|
+
/**
|
|
70
|
+
* @param OA Origin component props params.
|
|
71
|
+
* @param EA Extended prop types
|
|
72
|
+
* @param S Defined statics
|
|
73
|
+
* @param HOC High-order components
|
|
74
|
+
* @param DFP Calculated final component props
|
|
75
|
+
*/
|
|
76
|
+
interface AttrsComponent<C extends ElementType = ElementType, OA extends TObj = TObj, EA extends TObj = TObj, S extends TObj = TObj, HOC extends TObj = TObj, DFP extends Record<string, any> = MergeTypes<[OA, EA]>> {
|
|
77
|
+
(props: DFP): VNode | null;
|
|
78
|
+
config: <NC extends ElementType | unknown = unknown>({
|
|
79
|
+
name,
|
|
80
|
+
component: NC,
|
|
81
|
+
DEBUG
|
|
82
|
+
}: ConfigAttrs<NC>) => NC extends ElementType ? AttrsComponent<NC, ExtractProps<NC>, EA, S, HOC> : AttrsComponent<C, OA, EA, S, HOC>;
|
|
83
|
+
attrs: <P extends TObj | unknown = unknown>(param: P extends TObj ? Partial<MergeTypes<[DFP, P]>> | AttrsCb<MergeTypes<[DFP, P]>> : Partial<DFP> | AttrsCb<DFP>, config?: Partial<{
|
|
84
|
+
priority: boolean;
|
|
85
|
+
filter: unknown extends P ? string[] : (keyof MergeTypes<[EA, P]>)[];
|
|
86
|
+
}>) => P extends TObj ? AttrsComponent<C, OA, MergeTypes<[EA, P]>, S, HOC> : AttrsComponent<C, OA, EA, S, HOC>;
|
|
87
|
+
compose: <P extends ComposeParam>(param: P) => P extends TObj ? AttrsComponent<C, OA, EA, S, MergeTypes<[HOC, P]>> : AttrsComponent<C, OA, EA, S, HOC>;
|
|
88
|
+
statics: <P extends TObj | unknown = unknown>(param: P) => P extends TObj ? AttrsComponent<C, OA, EA, MergeTypes<[S, P]>, HOC> : AttrsComponent<C, OA, EA, S, HOC>;
|
|
89
|
+
/** Access to all defined statics on the component. */
|
|
90
|
+
meta: S;
|
|
91
|
+
getDefaultAttrs: (props: TObj) => TObj;
|
|
92
|
+
readonly $$originTypes: OA;
|
|
93
|
+
readonly $$extendedTypes: EA;
|
|
94
|
+
readonly $$types: DFP;
|
|
95
|
+
IS_ATTRS: true;
|
|
96
|
+
displayName: string;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/types/configuration.d.ts
|
|
100
|
+
type OptionFunc = (...arg: unknown[]) => Record<string, unknown>;
|
|
101
|
+
type InitConfiguration<C> = {
|
|
102
|
+
name?: string | undefined;
|
|
103
|
+
component: C;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Internal configuration accumulated across the chaining API.
|
|
107
|
+
* Arrays hold the full chain — each `.attrs()` call appends to these.
|
|
108
|
+
*/
|
|
109
|
+
type Configuration<C = ElementType | unknown> = InitConfiguration<C> & {
|
|
110
|
+
DEBUG?: boolean | undefined; /** Chain of default-props callbacks (resolved in order, later wins). */
|
|
111
|
+
attrs: OptionFunc[]; /** Chain of priority-props callbacks (resolved before `attrs`, can be overridden by both). */
|
|
112
|
+
priorityAttrs: OptionFunc[]; /** Prop names to omit before passing to the underlying component. */
|
|
113
|
+
filterAttrs: string[]; /** Named HOCs — set to null/false to remove from chain. */
|
|
114
|
+
compose: Record<string, TFn | null | undefined | false>; /** Metadata accessible via `Component.meta`. */
|
|
115
|
+
statics: Record<string, any>;
|
|
116
|
+
} & Record<string, any>;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/types/InitAttrsComponent.d.ts
|
|
119
|
+
/**
|
|
120
|
+
* Type of the internal `attrsComponent` factory function.
|
|
121
|
+
* Takes a full Configuration and returns an AttrsComponent whose
|
|
122
|
+
* original props (OA) are extracted from the component type C,
|
|
123
|
+
* with all extension slots (EA, S, HOC) starting empty.
|
|
124
|
+
*/
|
|
125
|
+
type InitAttrsComponent<C extends ElementType = ElementType> = (params: Configuration<C>) => AttrsComponent<C, ExtractProps<C>, // OA — original component props
|
|
126
|
+
TObj, // EA — extended props (empty initially)
|
|
127
|
+
TObj, // S — statics (empty initially)
|
|
128
|
+
TObj>;
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/init.d.ts
|
|
131
|
+
/**
|
|
132
|
+
* Public entry point for creating an attrs-enhanced component.
|
|
133
|
+
*
|
|
134
|
+
* ```tsx
|
|
135
|
+
* const Button = attrs({ name: 'Button', component: Element })
|
|
136
|
+
* .attrs({ tag: 'button' })
|
|
137
|
+
* .attrs<{ primary?: boolean }>(({ primary }) => ({
|
|
138
|
+
* backgroundColor: primary ? 'blue' : 'gray',
|
|
139
|
+
* }))
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
type Attrs = <C extends ElementType>({
|
|
143
|
+
name,
|
|
144
|
+
component
|
|
145
|
+
}: {
|
|
146
|
+
name: string;
|
|
147
|
+
component: C;
|
|
148
|
+
}) => ReturnType<InitAttrsComponent<C>>;
|
|
149
|
+
declare const attrs: Attrs;
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/isAttrsComponent.d.ts
|
|
152
|
+
type IsAttrsComponent = <T>(component: T) => boolean;
|
|
153
|
+
/** Runtime type guard — checks if a component was created by `attrs()`. */
|
|
154
|
+
declare const isAttrsComponent: IsAttrsComponent;
|
|
155
|
+
//#endregion
|
|
156
|
+
export { type Attrs, type AttrsCb, type AttrsComponent, type AttrsComponentType, type ComponentFn, type ComposeParam, type ConfigAttrs, type ElementType, type GenericHoc, type IsAttrsComponent, type TObj, attrs, attrs as default, isAttrsComponent };
|
|
157
|
+
//# sourceMappingURL=index2.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { compose, hoistNonReactStatics, isEmpty, omit, pick } from "@pyreon/ui-core";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/attrs.ts
|
|
4
|
+
const removeUndefinedProps = ((props) => Object.keys(props).reduce((acc, key) => {
|
|
5
|
+
const currentValue = props[key];
|
|
6
|
+
if (currentValue !== void 0) acc[key] = currentValue;
|
|
7
|
+
return acc;
|
|
8
|
+
}, {}));
|
|
9
|
+
const calculateChainOptions = (options) => (args) => {
|
|
10
|
+
const result = {};
|
|
11
|
+
if (!options || isEmpty(options)) return result;
|
|
12
|
+
return options.reduce((acc, item) => Object.assign(acc, item(...args)), {});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/hoc/attrsHoc.ts
|
|
17
|
+
/**
|
|
18
|
+
* Creates the core HOC that computes default props from the `.attrs()` chain.
|
|
19
|
+
*
|
|
20
|
+
* This is always the outermost HOC in the compose chain, so it runs first.
|
|
21
|
+
* It resolves both priority and normal attrs callbacks, then merges them
|
|
22
|
+
* with the consumer's explicit props following this precedence:
|
|
23
|
+
*
|
|
24
|
+
* priorityAttrs < normalAttrs < explicit props (last wins)
|
|
25
|
+
*
|
|
26
|
+
* In Pyreon, components are plain functions — no forwardRef needed.
|
|
27
|
+
* The ref flows as a normal prop through the chain.
|
|
28
|
+
*/
|
|
29
|
+
const createAttrsHOC = ({ attrs, priorityAttrs }) => {
|
|
30
|
+
const calculateAttrs = calculateChainOptions(attrs);
|
|
31
|
+
const calculatePriorityAttrs = calculateChainOptions(priorityAttrs);
|
|
32
|
+
const attrsHoc = (WrappedComponent) => {
|
|
33
|
+
const HOCComponent = (props) => {
|
|
34
|
+
const filteredProps = removeUndefinedProps(props);
|
|
35
|
+
const prioritizedAttrs = calculatePriorityAttrs([filteredProps]);
|
|
36
|
+
const finalAttrs = calculateAttrs([{
|
|
37
|
+
...prioritizedAttrs,
|
|
38
|
+
...filteredProps
|
|
39
|
+
}]);
|
|
40
|
+
return WrappedComponent({
|
|
41
|
+
...prioritizedAttrs,
|
|
42
|
+
...finalAttrs,
|
|
43
|
+
...filteredProps
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
return HOCComponent;
|
|
47
|
+
};
|
|
48
|
+
return attrsHoc;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/utils/chaining.ts
|
|
53
|
+
const chainOptions = (opts, defaultOpts = []) => {
|
|
54
|
+
const result = [...defaultOpts];
|
|
55
|
+
if (typeof opts === "function") result.push(opts);
|
|
56
|
+
else if (typeof opts === "object") result.push(() => opts);
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/utils/compose.ts
|
|
62
|
+
const calculateHocsFuncs = (options = {}) => Object.values(options).filter((item) => typeof item === "function").reverse();
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/utils/statics.ts
|
|
66
|
+
const createStaticsEnhancers = ({ context, options }) => {
|
|
67
|
+
if (!isEmpty(options)) Object.assign(context, options);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/attrs.ts
|
|
72
|
+
const cloneAndEnhance = (defaultOpts, opts) => attrsComponent({
|
|
73
|
+
...defaultOpts,
|
|
74
|
+
...opts.name ? { name: opts.name } : void 0,
|
|
75
|
+
...opts.component ? { component: opts.component } : void 0,
|
|
76
|
+
attrs: chainOptions(opts.attrs, defaultOpts.attrs),
|
|
77
|
+
filterAttrs: [...defaultOpts.filterAttrs ?? [], ...opts.filterAttrs ?? []],
|
|
78
|
+
priorityAttrs: chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
|
|
79
|
+
statics: {
|
|
80
|
+
...defaultOpts.statics,
|
|
81
|
+
...opts.statics
|
|
82
|
+
},
|
|
83
|
+
compose: {
|
|
84
|
+
...defaultOpts.compose,
|
|
85
|
+
...opts.compose
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Core factory that builds an attrs-enhanced Pyreon component.
|
|
90
|
+
*
|
|
91
|
+
* Creates a plain ComponentFn that:
|
|
92
|
+
* 1. Wraps the original with attrsHoc (default props) + user HOCs from `.compose()`.
|
|
93
|
+
* 2. Filters out internal props listed in `filterAttrs`.
|
|
94
|
+
* 3. Attaches `data-attrs` attribute in development for debugging.
|
|
95
|
+
*
|
|
96
|
+
* Then adds chaining methods (`.attrs()`, `.config()`, `.compose()`, `.statics()`)
|
|
97
|
+
* as static properties — each calls `cloneAndEnhance` to produce a new component.
|
|
98
|
+
*
|
|
99
|
+
* In Pyreon, there is no forwardRef — ref flows as a normal prop.
|
|
100
|
+
* Components are plain functions that run once per mount.
|
|
101
|
+
*/
|
|
102
|
+
const attrsComponent = (options) => {
|
|
103
|
+
const componentName = options.name ?? options.component.displayName ?? options.component.name;
|
|
104
|
+
const RenderComponent = options.component;
|
|
105
|
+
const hocsFuncs = [createAttrsHOC(options), ...calculateHocsFuncs(options.compose)];
|
|
106
|
+
const EnhancedComponent = (props) => {
|
|
107
|
+
const filteredProps = options.filterAttrs && options.filterAttrs.length > 0 ? omit(props, options.filterAttrs) : props;
|
|
108
|
+
return RenderComponent(process.env.NODE_ENV !== "production" ? {
|
|
109
|
+
...filteredProps,
|
|
110
|
+
"data-attrs": componentName
|
|
111
|
+
} : filteredProps);
|
|
112
|
+
};
|
|
113
|
+
const AttrsComponent = compose(...hocsFuncs)(EnhancedComponent);
|
|
114
|
+
AttrsComponent.IS_ATTRS = true;
|
|
115
|
+
AttrsComponent.displayName = componentName;
|
|
116
|
+
AttrsComponent.meta = {};
|
|
117
|
+
hoistNonReactStatics(AttrsComponent, options.component);
|
|
118
|
+
createStaticsEnhancers({
|
|
119
|
+
context: AttrsComponent.meta,
|
|
120
|
+
options: options.statics
|
|
121
|
+
});
|
|
122
|
+
Object.assign(AttrsComponent, {
|
|
123
|
+
attrs: (attrs, { priority, filter } = {}) => {
|
|
124
|
+
const result = {};
|
|
125
|
+
if (filter) result.filterAttrs = filter;
|
|
126
|
+
if (priority) {
|
|
127
|
+
result.priorityAttrs = attrs;
|
|
128
|
+
return cloneAndEnhance(options, result);
|
|
129
|
+
}
|
|
130
|
+
result.attrs = attrs;
|
|
131
|
+
return cloneAndEnhance(options, result);
|
|
132
|
+
},
|
|
133
|
+
config: (opts = {}) => {
|
|
134
|
+
return cloneAndEnhance(options, pick(opts));
|
|
135
|
+
},
|
|
136
|
+
compose: (opts) => cloneAndEnhance(options, { compose: opts }),
|
|
137
|
+
statics: (opts) => cloneAndEnhance(options, { statics: opts }),
|
|
138
|
+
getDefaultAttrs: (props) => calculateChainOptions(options.attrs)([props])
|
|
139
|
+
});
|
|
140
|
+
return AttrsComponent;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/init.ts
|
|
145
|
+
const attrs = ({ name, component }) => {
|
|
146
|
+
if (process.env.NODE_ENV !== "production") {
|
|
147
|
+
const errors = {};
|
|
148
|
+
if (!component) errors.component = "Parameter `component` is missing in params!";
|
|
149
|
+
if (!name) errors.name = "Parameter `name` is missing in params!";
|
|
150
|
+
if (!isEmpty(errors)) throw Error(JSON.stringify(errors));
|
|
151
|
+
}
|
|
152
|
+
return attrsComponent({
|
|
153
|
+
name,
|
|
154
|
+
component,
|
|
155
|
+
attrs: [],
|
|
156
|
+
priorityAttrs: [],
|
|
157
|
+
filterAttrs: [],
|
|
158
|
+
compose: {},
|
|
159
|
+
statics: {}
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/isAttrsComponent.ts
|
|
165
|
+
/** Runtime type guard — checks if a component was created by `attrs()`. */
|
|
166
|
+
const isAttrsComponent = (component) => {
|
|
167
|
+
if (component && (typeof component === "object" || typeof component === "function") && component !== null && Object.hasOwn(component, "IS_ATTRS")) return true;
|
|
168
|
+
return false;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/index.ts
|
|
173
|
+
var src_default = attrs;
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
export { attrs, src_default as default, isAttrsComponent };
|
|
177
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pyreon/attrs",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Attrs HOC chaining for Pyreon components",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"import": "./lib/index.js",
|
|
11
|
+
"types": "./lib/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"main": "./lib/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"!lib/**/*.map",
|
|
18
|
+
"!lib/analysis",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">= 18"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"prepublish": "bun run build",
|
|
30
|
+
"build": "bun run vl_rolldown_build",
|
|
31
|
+
"build:watch": "bun run vl_rolldown_build-watch",
|
|
32
|
+
"lint": "biome check src/",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:coverage": "vitest run --coverage",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@pyreon/core": ">=0.3.0",
|
|
40
|
+
"@pyreon/ui-core": "^0.0.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@vitus-labs/tools-rolldown": "^1.15.0",
|
|
44
|
+
"@vitus-labs/tools-typescript": "^1.15.0"
|
|
45
|
+
}
|
|
46
|
+
}
|