Skip to content

Commit ed69782

Browse files
author
Viktor Pasynok
committed
added: light type tests
1 parent aa09aba commit ed69782

File tree

7 files changed

+1306
-262
lines changed

7 files changed

+1306
-262
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ prepublish:
99

1010
publish:
1111
npm i && make prepublish && npx clean-publish
12+
13+
test:
14+
npx vitest

package-lock.json

Lines changed: 1116 additions & 129 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@
66
"main": "dist/index.js",
77
"module": "dist/index.js",
88
"types": "dist/index.d.ts",
9-
"files": ["/README.md", "/package.json", "/dist"],
9+
"files": [
10+
"/README.md",
11+
"/package.json",
12+
"/dist"
13+
],
1014
"author": "Viktor Pasynok",
1115
"license": "MIT",
1216
"devDependencies": {
13-
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1417
"@size-limit/preset-small-lib": "11.1.6",
18+
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1519
"clean-publish": "5.1.0",
1620
"prettier": "3.3.3",
1721
"prettier-plugin-organize-imports": "4.1.0",
1822
"size-limit": "11.1.6",
1923
"tslib": "2.8.1",
2024
"tsup": "8.3.5",
21-
"typescript": "5.6.3"
25+
"typescript": "5.6.3",
26+
"vitest": "3.1.4"
2227
},
2328
"peerDependencies": {
2429
"effector": "23",

src/__tests__/payload.spec-d.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import { expectTypeOf } from 'vitest';
3+
import { type Payload } from '../init';
4+
5+
type EmptyObject = Record<string, never>;
6+
7+
declare const payload: Payload<{ name: string }>;
8+
9+
payload({
10+
fn: (data) => ({ label: data.name }),
11+
component: (props) => {
12+
expectTypeOf<{ label: string }>(props);
13+
return <div />;
14+
},
15+
});
16+
17+
payload({
18+
component: (props) => {
19+
expectTypeOf<EmptyObject>(props);
20+
return <div />;
21+
},
22+
});
23+
24+
payload({
25+
fn: () => {},
26+
component: (props) => {
27+
expectTypeOf<EmptyObject>(props);
28+
return <div />;
29+
},
30+
});
31+
32+
payload({
33+
fn: (data) => ({ label: data.name }),
34+
// @ts-expect-error expected { label: string } but got { wrong: number }
35+
component: (props: { wrong: number }) => <div />,
36+
});

src/index.tsx

Lines changed: 1 addition & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1 @@
1-
import { createEvent, createStore } from 'effector';
2-
import { useStoreMap } from 'effector-react';
3-
import { nanoid } from 'nanoid';
4-
import React, { type FunctionComponent, memo, useMemo } from 'react';
5-
6-
type EmptyObject = Record<string, never>;
7-
type Entries<T> = {
8-
[K in keyof T]: [K, T[K]];
9-
}[keyof T][];
10-
11-
const isNil = <T,>(x: T | undefined | null): x is undefined | null => x === null || x === undefined;
12-
13-
const insertAtPosition = <T,>(list: T[], position: number, element: T) => {
14-
const newList = [...list];
15-
16-
if (position <= 0) {
17-
newList.unshift(element);
18-
} else if (position >= list.length) {
19-
newList.push(element);
20-
} else {
21-
newList.splice(position, 0, element);
22-
}
23-
24-
return newList;
25-
};
26-
27-
type CreateSlotIdentifier = <T>() => (_: T) => T;
28-
29-
const createSlotIdentifier: CreateSlotIdentifier = () => (props) => props;
30-
31-
type SlotFunction<T> = (_: T) => T;
32-
33-
type Payload<T> = <R>(params: {
34-
component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => JSX.Element;
35-
fn?: (arg: T) => R;
36-
order?: number;
37-
}) => void;
38-
39-
const createSlots = <T extends Record<string, SlotFunction<any>>>(config: T) => {
40-
const entries = Object.entries(config) as Entries<typeof config>;
41-
const keys = entries.map(([k]) => k);
42-
43-
type SetApi = {
44-
[key in keyof T]: T[key] extends (_: any) => unknown ? Payload<Parameters<T[key]>[0]> : never;
45-
};
46-
47-
type State = {
48-
[key in keyof T]: (Parameters<SetApi[key]>[0] & { id: string })[];
49-
};
50-
51-
type Slots = {
52-
[key in keyof T]: FunctionComponent<Parameters<T[key]>[0]>;
53-
};
54-
55-
type ExtractData<P extends keyof T> = Parameters<T[P]>[0];
56-
57-
const $slots = createStore<State>(
58-
keys.reduce((acc, x) => {
59-
acc[x] = [];
60-
61-
return acc;
62-
}, {} as State),
63-
);
64-
65-
const insertApi = keys.reduce((acc, key) => {
66-
const evt = createEvent<Parameters<Payload<ExtractData<typeof key>>>[0]>();
67-
68-
$slots.on(evt, (state, payload) => {
69-
const item = { ...payload, id: nanoid(10) };
70-
const list = insertAtPosition(state[key], payload.order ?? state[key].length + 1, item);
71-
const sortedList = list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
72-
73-
return { ...state, [key]: sortedList };
74-
});
75-
76-
// @ts-expect-error its ok. avoid extra fn creation
77-
acc[key] = evt;
78-
79-
return acc;
80-
}, {} as SetApi);
81-
82-
// @ts-expect-error its ok
83-
const makeChildWithProps = (child) =>
84-
// @ts-expect-error its ok
85-
memo<any>((props) => {
86-
const childProps = useMemo(() => child.fn(props), [props]);
87-
88-
return (
89-
<React.Fragment key={child.id}>
90-
<child.component {...childProps} />
91-
</React.Fragment>
92-
);
93-
});
94-
95-
const slots = keys.reduce((acc, key) => {
96-
const component = memo<ExtractData<typeof key>>((props) => {
97-
const childrens = useStoreMap($slots, (x) => x[key]);
98-
99-
return childrens.map((child) => {
100-
if (isNil(child.fn)) {
101-
return (
102-
<React.Fragment key={child.id}>
103-
<child.component />
104-
</React.Fragment>
105-
);
106-
}
107-
108-
const ChildWithProps = makeChildWithProps(child);
109-
110-
return (
111-
<React.Fragment key={child.id}>
112-
<ChildWithProps {...props} />
113-
</React.Fragment>
114-
);
115-
});
116-
});
117-
118-
acc[key] = component;
119-
120-
return acc;
121-
}, {} as Slots);
122-
123-
const slotsApi = {
124-
insert: { into: insertApi },
125-
};
126-
127-
return { slotsApi, Slots: slots };
128-
};
129-
130-
export { createSlotIdentifier, createSlots, type EmptyObject };
1+
export { createSlotIdentifier, createSlots, type EmptyObject } from './init';

src/init.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { createEvent, createStore } from 'effector';
2+
import { useStoreMap } from 'effector-react';
3+
import { nanoid } from 'nanoid';
4+
import React, { type FunctionComponent, memo, useMemo } from 'react';
5+
6+
type EmptyObject = Record<string, never>;
7+
type Entries<T> = {
8+
[K in keyof T]: [K, T[K]];
9+
}[keyof T][];
10+
11+
const isNil = <T,>(x: T | undefined | null): x is undefined | null => x === null || x === undefined;
12+
13+
const insertAtPosition = <T,>(list: T[], position: number, element: T) => {
14+
const newList = [...list];
15+
16+
if (position <= 0) {
17+
newList.unshift(element);
18+
} else if (position >= list.length) {
19+
newList.push(element);
20+
} else {
21+
newList.splice(position, 0, element);
22+
}
23+
24+
return newList;
25+
};
26+
27+
type CreateSlotIdentifier = <T>() => (_: T) => T;
28+
29+
const createSlotIdentifier: CreateSlotIdentifier = () => (props) => props;
30+
31+
type SlotFunction<T> = (_: T) => T;
32+
33+
type Payload<T> = <R>(params: {
34+
component: (props: unknown extends R ? EmptyObject : R extends void ? EmptyObject : R) => JSX.Element;
35+
fn?: (arg: T) => R;
36+
order?: number;
37+
}) => void;
38+
39+
const createSlots = <T extends Record<string, SlotFunction<any>>>(config: T) => {
40+
const entries = Object.entries(config) as Entries<typeof config>;
41+
const keys = entries.map(([k]) => k);
42+
43+
type SetApi = {
44+
[key in keyof T]: T[key] extends (_: any) => unknown ? Payload<Parameters<T[key]>[0]> : never;
45+
};
46+
47+
type State = {
48+
[key in keyof T]: (Parameters<SetApi[key]>[0] & { id: string })[];
49+
};
50+
51+
type Slots = {
52+
[key in keyof T]: FunctionComponent<Parameters<T[key]>[0]>;
53+
};
54+
55+
type ExtractData<P extends keyof T> = Parameters<T[P]>[0];
56+
57+
const $slots = createStore<State>(
58+
keys.reduce((acc, x) => {
59+
acc[x] = [];
60+
61+
return acc;
62+
}, {} as State),
63+
);
64+
65+
const insertApi = keys.reduce((acc, key) => {
66+
const evt = createEvent<Parameters<Payload<ExtractData<typeof key>>>[0]>();
67+
68+
$slots.on(evt, (state, payload) => {
69+
const item = { ...payload, id: nanoid(10) };
70+
const list = insertAtPosition(state[key], payload.order ?? state[key].length + 1, item);
71+
const sortedList = list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
72+
73+
return { ...state, [key]: sortedList };
74+
});
75+
76+
// @ts-expect-error its ok. avoid extra fn creation
77+
acc[key] = evt;
78+
79+
return acc;
80+
}, {} as SetApi);
81+
82+
// @ts-expect-error its ok
83+
const makeChildWithProps = (child) =>
84+
// @ts-expect-error its ok
85+
memo<any>((props) => {
86+
const childProps = useMemo(() => child.fn(props), [props]);
87+
88+
return (
89+
<React.Fragment key={child.id}>
90+
<child.component {...childProps} />
91+
</React.Fragment>
92+
);
93+
});
94+
95+
const slots = keys.reduce((acc, key) => {
96+
const component = memo<ExtractData<typeof key>>((props) => {
97+
const childrens = useStoreMap($slots, (x) => x[key]);
98+
99+
return childrens.map((child) => {
100+
if (isNil(child.fn)) {
101+
return (
102+
<React.Fragment key={child.id}>
103+
<child.component />
104+
</React.Fragment>
105+
);
106+
}
107+
108+
const ChildWithProps = makeChildWithProps(child);
109+
110+
return (
111+
<React.Fragment key={child.id}>
112+
<ChildWithProps {...props} />
113+
</React.Fragment>
114+
);
115+
});
116+
});
117+
118+
acc[key] = component;
119+
120+
return acc;
121+
}, {} as Slots);
122+
123+
const slotsApi = {
124+
insert: { into: insertApi },
125+
};
126+
127+
return { slotsApi, Slots: slots };
128+
};
129+
130+
export { createSlotIdentifier, createSlots, type EmptyObject, type Payload };

vitest.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
typecheck: {
6+
enabled: true,
7+
ignoreSourceErrors: true,
8+
include: ['src/**/__tests__/**/*.spec-d.ts?(x)'],
9+
},
10+
watch: false,
11+
},
12+
});

0 commit comments

Comments
 (0)