Skip to content

Commit 2b9255b

Browse files
committed
init
0 parents  commit 2b9255b

28 files changed

Lines changed: 3553 additions & 0 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
coverage
4+
docs

.prettierrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"arrowParens": "avoid",
3+
"semi": false,
4+
"singleQuote": true,
5+
"printWidth": 120,
6+
"jsxBracketSameLine": false,
7+
"tabWidth": 4
8+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 rasulomaroff
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.

eslint.config.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import eslint from '@eslint/js'
2+
import tseslint from 'typescript-eslint'
3+
import globals from 'globals'
4+
import prettier from 'eslint-plugin-prettier/recommended'
5+
6+
export default [
7+
eslint.configs.recommended,
8+
...tseslint.configs.recommendedTypeChecked,
9+
10+
{ ignores: ['**/dist/**/*', '**/coverage/**', '**/node_modules/**', '**/docs/**'] },
11+
12+
{
13+
rules: {
14+
'@typescript-eslint/explicit-function-return-type': 'error',
15+
'@typescript-eslint/consistent-type-definitions': 'off',
16+
'@typescript-eslint/consistent-type-assertions': 'off',
17+
// '@typescript-eslint/unbound-method': 'off',
18+
},
19+
},
20+
{
21+
languageOptions: {
22+
globals: globals.browser,
23+
parserOptions: {
24+
projectService: true,
25+
tsconfigRootDir: '.',
26+
},
27+
},
28+
},
29+
prettier,
30+
]

package.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "evit",
3+
"private": true,
4+
"workspaces": [
5+
"packages/*"
6+
],
7+
"scripts": {
8+
"spec": "vitest run",
9+
"doc": "typedoc",
10+
"lint:check": "eslint .",
11+
"type:check": "tsc --build ./tsconfig.typecheck.json --noEmit"
12+
},
13+
"description": "A lightweight, type-safe, and stateless event system for building reactive UIs. Designed to enable feature-level communication in large-scale applications using frameworks like React.",
14+
"keywords": [
15+
"event",
16+
"emitter",
17+
"react",
18+
"hook",
19+
"listener",
20+
"subscription"
21+
],
22+
"author": "rasulomaroff",
23+
"license": "MIT",
24+
"repository": {
25+
"type": "git",
26+
"url": "git+https://github.com/rasulomaroff/evit.git"
27+
},
28+
"bugs": {
29+
"url": "https://github.com/rasulomaroff/evit/issues"
30+
},
31+
"homepage": "https://rasulomaroff.github.io/evit/",
32+
"packageManager": "pnpm@10.12.1",
33+
"devDependencies": {
34+
"@eslint/js": "^9.29.0",
35+
"eslint": "^9.29.0",
36+
"eslint-config-prettier": "^10.1.5",
37+
"eslint-plugin-prettier": "^5.5.0",
38+
"globals": "^16.2.0",
39+
"happy-dom": "^18.0.1",
40+
"jiti": "^2.4.2",
41+
"prettier": "^3.6.0",
42+
"tsup": "^8.5.0",
43+
"typedoc": "^0.28.5",
44+
"typescript": "^5.8.3",
45+
"typescript-eslint": "^8.35.0",
46+
"vitest": "^3.2.4"
47+
}
48+
}

packages/core/package.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "@evit/core",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"description": "A lightweight, type-safe, stateless event system for building reactive UIs.",
6+
"exports": {
7+
".": {
8+
"types": "./dist/index.d.ts",
9+
"node": "./dist/index.cjs",
10+
"import": "./dist/index.js",
11+
"require": "./dist/index.cjs"
12+
}
13+
},
14+
"files": [
15+
"dist"
16+
],
17+
"scripts": {
18+
"spec": "vitest run --typecheck",
19+
"build": "tsup src/index.ts --format esm,cjs --dts --minify --clean --sourcemap"
20+
},
21+
"keywords": [
22+
"event",
23+
"emitter",
24+
"react",
25+
"hook",
26+
"listener",
27+
"subscription"
28+
],
29+
"author": "rasulomaroff",
30+
"license": "MIT",
31+
"repository": {
32+
"type": "git",
33+
"url": "git+https://github.com/rasulomaroff/evit.git"
34+
},
35+
"bugs": {
36+
"url": "https://github.com/rasulomaroff/evit/issues"
37+
},
38+
"homepage": "https://rasulomaroff.github.io/evit/",
39+
"publishConfig": {
40+
"access": "public"
41+
}
42+
}

packages/core/src/event.spec-d.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expectTypeOf, it } from 'vitest'
2+
import { defineEvent, withPayload } from './event'
3+
4+
describe('defineEvent function', () => {
5+
it('infers the tag type', () => {
6+
const event = defineEvent('tag')
7+
8+
expectTypeOf(event.tag).toEqualTypeOf<'tag'>()
9+
})
10+
11+
it('infers the payload type', () => {
12+
const event = defineEvent('tag', { test: 22 })
13+
14+
expectTypeOf(event.tag).toEqualTypeOf<'tag'>()
15+
16+
event.on(payload => {
17+
expectTypeOf(payload).toEqualTypeOf<{ test: number }>()
18+
})
19+
})
20+
21+
it('infers the payload type with the helper function', () => {
22+
const event = defineEvent('tag', withPayload<{ age: number }>())
23+
24+
expectTypeOf(event.tag).toEqualTypeOf<'tag'>()
25+
26+
event.on(payload => {
27+
expectTypeOf(payload).toEqualTypeOf<{ age: number }>()
28+
})
29+
})
30+
})
31+
32+
describe('withPayload function', () => {
33+
it('infers the type from the parameter and "returns" it', () => {
34+
const user = withPayload<{ name: string; age: number }>()
35+
36+
expectTypeOf(user).toEqualTypeOf<{ name: string; age: number }>()
37+
38+
const num = withPayload<number>()
39+
40+
expectTypeOf(num).toBeNumber()
41+
})
42+
})

packages/core/src/event.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { defineEvent, mergeSubscriptions, withPayload } from './event'
3+
4+
describe('defineEvent function', () => {
5+
it('should call subscribed handler with tag', () => {
6+
const handler = vi.fn()
7+
8+
const event = defineEvent('my.event')
9+
event.on(handler)
10+
11+
event()
12+
13+
expect(handler).toHaveBeenCalledWith(undefined, event.tag)
14+
expect(handler).toHaveBeenCalledTimes(1)
15+
expect(event.tag).toBe('my.event')
16+
})
17+
18+
it('should call subscribed handler with payload (using withPayload function) and tag', () => {
19+
const handler = vi.fn()
20+
21+
const event = defineEvent('my.event', withPayload<number>())
22+
event.on(handler)
23+
24+
event(42)
25+
26+
expect(handler).toHaveBeenCalledWith(42, event.tag)
27+
expect(handler).toHaveBeenCalledTimes(1)
28+
expect(event.tag).toBe('my.event')
29+
})
30+
31+
it('should unsubscribe correctly', () => {
32+
const handler = vi.fn()
33+
34+
const event = defineEvent('unsub.test', withPayload<string>())
35+
const unsubscribe = event.on(handler)
36+
37+
event('hello')
38+
unsubscribe()
39+
event('world')
40+
41+
expect(handler).toHaveBeenCalledTimes(1)
42+
expect(handler).toHaveBeenCalledWith('hello', event.tag)
43+
})
44+
})
45+
46+
describe('mergeSubscriptions function', () => {
47+
it('should unsubscribe all passed callbacks', () => {
48+
const unsub1 = vi.fn()
49+
const unsub2 = vi.fn()
50+
51+
const merged = mergeSubscriptions(unsub1, unsub2)
52+
merged()
53+
54+
expect(unsub1).toHaveBeenCalled()
55+
expect(unsub2).toHaveBeenCalled()
56+
})
57+
})

packages/core/src/event.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { Action, EventHandler } from './types'
2+
3+
/**
4+
* Defines a new typed event with a payload and tag.
5+
*
6+
* @template Tag - A unique identifier for the event.
7+
* @template P - The payload type.
8+
*
9+
* @param tag - The tag for the event.
10+
* @param _infer - Optionally used to infer payload type (can use `withPayload<T>()`).
11+
*
12+
* @returns The event that you can subscribe to and listen.
13+
*
14+
* @example
15+
* ```ts
16+
* const event = defineEvent('user.created')
17+
*
18+
* // or with a payload
19+
* const eventWithPayload = defineEvent('user.created', withPayload<{ id: number }>())
20+
* ```
21+
*/
22+
export function defineEvent<const Tag, P = void>(
23+
tag: Tag,
24+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25+
_infer?: P,
26+
): Action<P, Tag> {
27+
const handlers = new Set<EventHandler<P, Action<P, Tag>>>()
28+
29+
const ev = ((payload: P): void => handlers.forEach(fn => fn(payload, tag))) as Action<P, Tag>
30+
31+
// @ts-expect-error we only assign this property once here
32+
ev.tag = tag
33+
34+
ev.on = function on(fn: EventHandler<P, Action<P, Tag>>): () => void {
35+
handlers.add(fn)
36+
37+
return (): void => {
38+
handlers.delete(fn)
39+
}
40+
}
41+
42+
return ev
43+
}
44+
45+
/**
46+
* Utility function to help infer payload types when defining events.
47+
* Does nothing at runtime.
48+
*
49+
* @example
50+
* ```ts
51+
* const event = defineEvent('user.updated', withPayload<{ id: string }>())
52+
* ```
53+
*/
54+
export const withPayload = (() => {}) as <T>() => T
55+
56+
/**
57+
* A utility function to unsubscribe from multiple subscriptions at once.
58+
*
59+
* @param unsubscribers - An array of unsubscribe functions.
60+
*
61+
* @returns A single function that unsubscribes from all.
62+
*
63+
* @example
64+
* ```ts
65+
* const newUserEvent = defineEvent('user.created')
66+
* const deleteUserEvent = defineEvent('user.deleted')
67+
*
68+
* const unsubscribe = mergeSubscriptions(
69+
* newUserEvent.on(() => {}),
70+
* deleteUserEvent.on(() => {}),
71+
* )
72+
* ```
73+
*/
74+
export function mergeSubscriptions(...unsubscribers: Array<() => void>): () => void {
75+
return function unsubscribe(): void {
76+
unsubscribers.forEach(fn => fn())
77+
}
78+
}

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { defineEvent, withPayload, mergeSubscriptions } from './event'
2+
3+
export type { Action, EventHandler } from './types'

0 commit comments

Comments
 (0)