@potok-web-framework/core 0.1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/bun.lock +25 -0
  3. package/package.json +39 -0
  4. package/src/block.ts +102 -0
  5. package/src/bootstrap-app.ts +115 -0
  6. package/src/client-only.ts +17 -0
  7. package/src/constants.ts +27 -0
  8. package/src/context.ts +85 -0
  9. package/src/detect-child.ts +21 -0
  10. package/src/error-boundary.ts +51 -0
  11. package/src/exports/client.ts +1 -0
  12. package/src/exports/hmr.ts +1 -0
  13. package/src/exports/index.ts +21 -0
  14. package/src/exports/jsx-runtime.ts +4 -0
  15. package/src/exports/server.ts +1 -0
  16. package/src/fragment.ts +28 -0
  17. package/src/global.dev.d.ts +12 -0
  18. package/src/hmr/hmr-dev.ts +10 -0
  19. package/src/hmr/register-component.ts +109 -0
  20. package/src/hmr/registered-component.ts +59 -0
  21. package/src/hmr/registry.ts +78 -0
  22. package/src/hmr/types.ts +6 -0
  23. package/src/hmr/utils.ts +20 -0
  24. package/src/html-element.ts +95 -0
  25. package/src/jsx-types.ts +13 -0
  26. package/src/lazy.ts +44 -0
  27. package/src/lib-context-reader.ts +33 -0
  28. package/src/lib-scripts.ts +8 -0
  29. package/src/lifecycle.ts +44 -0
  30. package/src/list.ts +175 -0
  31. package/src/portal.ts +101 -0
  32. package/src/prop-types.ts +1165 -0
  33. package/src/ref.ts +11 -0
  34. package/src/render-to-dom.ts +325 -0
  35. package/src/render-to-string.ts +65 -0
  36. package/src/server-node.ts +98 -0
  37. package/src/show.ts +46 -0
  38. package/src/signals.ts +323 -0
  39. package/src/store.ts +68 -0
  40. package/src/text.ts +35 -0
  41. package/src/types.ts +69 -0
  42. package/src/utils.ts +118 -0
  43. package/tests/signals.test.ts +403 -0
  44. package/tsconfig.json +17 -0
  45. package/vite.config.ts +21 -0
package/src/signals.ts ADDED
@@ -0,0 +1,323 @@
1
+ // Третья версия с улучшением производительности
2
+
3
+ import { HMR_DEV } from './hmr/hmr-dev'
4
+ import { isWindowDefined } from './hmr/utils'
5
+
6
+ import { klona } from 'klona'
7
+ import deepEqual from 'fast-deep-equal'
8
+ import { CachedInstanceState } from './hmr/registered-component'
9
+
10
+ export type SignalValue = Record<string, unknown> | Array<unknown>
11
+ export type EffectCallback = () => void
12
+ export type BatchCallback<Value> = () => Value
13
+ export type UntrackCallback<Value> = () => Value
14
+ export type DisposerCallback = () => void
15
+ export type SubscriptionKey = string | number | symbol
16
+
17
+ export type Signal<Value extends SignalValue> = {
18
+ value: Value
19
+ proxy: Value
20
+ subscribers: Set<Effect>
21
+ subscribe(effect: Effect, key: SubscriptionKey): void
22
+ }
23
+
24
+ export type Effect = {
25
+ callback: EffectCallback
26
+ subscriptions: Map<Signal<SignalValue>, Map<SubscriptionKey, number>>
27
+ version: number
28
+ ignoreVersion: boolean
29
+ dispose(): void
30
+ runCallback(): void
31
+ }
32
+
33
+ export type Disposer = {
34
+ effects: Set<Effect>
35
+ dispose(): void
36
+ }
37
+
38
+ let currentEffect: Effect | null = null
39
+ let currentBatch: Set<Effect> | null = null
40
+ let currentDisposer: Disposer | null = null
41
+
42
+ const VALUE_SIGNAL_SYMBOL = Symbol('VALUE_SIGNAL')
43
+ const ANY_KEY_SYMBOL = Symbol('ANY_KEY')
44
+
45
+ export function getSignal<Value extends SignalValue>(
46
+ value: Value,
47
+ ): Signal<Value> | undefined {
48
+ return value[VALUE_SIGNAL_SYMBOL as keyof Value] as Signal<Value> | undefined
49
+ }
50
+
51
+ export function isSignal<Value extends SignalValue>(value: Value): boolean {
52
+ return getSignal(value) !== undefined
53
+ }
54
+
55
+ function canBeSignal(value: unknown): value is SignalValue {
56
+ return (
57
+ typeof value === 'object' &&
58
+ value !== null &&
59
+ (Array.isArray(value) || Object.getPrototypeOf(value).isPrototypeOf(Object))
60
+ )
61
+ }
62
+
63
+ export function createSignal<Value extends SignalValue>(value: Value): Value {
64
+ if (process.env.NODE_ENV === 'development') {
65
+ if (isWindowDefined()) {
66
+ const currentInstance = HMR_DEV.currentInstance
67
+ if (currentInstance) {
68
+ if (currentInstance.stateIndex < currentInstance.states.length) {
69
+ let state = currentInstance.states[currentInstance.stateIndex] as
70
+ | CachedInstanceState
71
+ | undefined
72
+ while (!deepEqual(state?.initial, value)) {
73
+ currentInstance.states.splice(currentInstance.stateIndex, 1)
74
+ state = currentInstance.states[currentInstance.stateIndex]
75
+ if (!state) {
76
+ break
77
+ }
78
+ }
79
+ if (state) {
80
+ const existingSignal = getSignal(state.current as SignalValue)
81
+ if (existingSignal) {
82
+ currentInstance.stateIndex++
83
+
84
+ return existingSignal.proxy as Value
85
+ }
86
+ } else {
87
+ currentInstance.states.push({
88
+ current: value,
89
+ initial: klona(value),
90
+ })
91
+ currentInstance.stateIndex++
92
+ }
93
+ } else {
94
+ currentInstance.states.push({
95
+ current: value,
96
+ initial: klona(value),
97
+ })
98
+ currentInstance.stateIndex++
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ const existingSignal = getSignal(value)
105
+
106
+ if (existingSignal) {
107
+ return existingSignal.proxy
108
+ }
109
+
110
+ const signal: Signal<Value> = {
111
+ value,
112
+ proxy: new Proxy(value, {
113
+ get(target, key, reciever) {
114
+ if (key === VALUE_SIGNAL_SYMBOL) {
115
+ return signal
116
+ }
117
+
118
+ const value = Reflect.get(target, key, reciever)
119
+
120
+ if (currentEffect) {
121
+ signal.subscribe(currentEffect, key)
122
+ }
123
+
124
+ if (canBeSignal(value)) {
125
+ return createSignal(value)
126
+ }
127
+
128
+ if (typeof value === 'function') {
129
+ return value.bind(target)
130
+ }
131
+
132
+ return value
133
+ },
134
+ set(target, key, value, reciever) {
135
+ const isSet = Reflect.set(target, key, value, reciever)
136
+
137
+ if (isSet) {
138
+ signal.subscribers.forEach((effect) => {
139
+ const subscriptions = effect.subscriptions.get(signal)
140
+
141
+ if (
142
+ effect.ignoreVersion ||
143
+ subscriptions?.get(ANY_KEY_SYMBOL) === effect.version ||
144
+ subscriptions?.get(key) === effect.version
145
+ ) {
146
+ effect.runCallback()
147
+ }
148
+ })
149
+ }
150
+
151
+ return isSet
152
+ },
153
+ }),
154
+ subscribers: new Set(),
155
+ subscribe(effect, key) {
156
+ let subscribedKeys = effect.subscriptions.get(signal)
157
+
158
+ if (!subscribedKeys) {
159
+ subscribedKeys = new Map()
160
+ effect.subscriptions.set(signal, subscribedKeys)
161
+ signal.subscribers.add(effect)
162
+ }
163
+
164
+ if (subscribedKeys.get(key) !== effect.version) {
165
+ subscribedKeys.set(key, effect.version)
166
+ }
167
+ },
168
+ }
169
+
170
+ Object.defineProperty(value, VALUE_SIGNAL_SYMBOL, {
171
+ value: signal,
172
+ enumerable: false,
173
+ writable: false,
174
+ configurable: false,
175
+ })
176
+
177
+ return signal.proxy
178
+ }
179
+
180
+ export function createEffect(
181
+ callback: EffectCallback,
182
+ disableVersionning = false,
183
+ ): Effect {
184
+ let isDisposed = false
185
+
186
+ const effect: Effect = {
187
+ callback,
188
+ subscriptions: new Map(),
189
+ version: 0,
190
+ ignoreVersion: disableVersionning,
191
+ dispose() {
192
+ isDisposed = true
193
+ },
194
+ runCallback() {
195
+ if (isDisposed) {
196
+ effect.subscriptions.forEach((_, signal) => {
197
+ signal.subscribers.delete(effect)
198
+ })
199
+ effect.subscriptions.clear()
200
+
201
+ return
202
+ }
203
+
204
+ if (currentBatch) {
205
+ currentBatch.add(effect)
206
+
207
+ return
208
+ }
209
+
210
+ currentEffect = effect
211
+ effect.version++
212
+ callback()
213
+ currentEffect = null
214
+ },
215
+ }
216
+
217
+ if (currentDisposer) {
218
+ currentDisposer.effects.add(effect)
219
+ }
220
+
221
+ effect.runCallback()
222
+
223
+ return effect
224
+ }
225
+
226
+ export function batch<Value>(callback: BatchCallback<Value>): Value {
227
+ if (currentBatch) {
228
+ return callback()
229
+ }
230
+
231
+ currentBatch = new Set()
232
+ const result = callback()
233
+
234
+ const executedBatch = currentBatch
235
+ currentBatch = null
236
+
237
+ executedBatch.forEach((effect) => {
238
+ effect.runCallback()
239
+ })
240
+
241
+ return result
242
+ }
243
+
244
+ export function untrack<Value>(callback: UntrackCallback<Value>): Value {
245
+ const effect = currentEffect
246
+ currentEffect = null
247
+ const value = callback()
248
+ currentEffect = effect
249
+
250
+ return value
251
+ }
252
+
253
+ function _deepTrack(value: SignalValue, depth?: number, currentDepth = 0) {
254
+ if (depth !== undefined && depth <= 0) {
255
+ console.warn(`depth=${depth} не может быть меньше или равен 0`)
256
+ return
257
+ }
258
+
259
+ if (!currentEffect) {
260
+ console.warn('deepTrack можно использовать только внутри эффекта')
261
+ return
262
+ }
263
+
264
+ const effect = currentEffect
265
+
266
+ untrack(() => {
267
+ const signal = getSignal(value)
268
+
269
+ if (!signal) {
270
+ console.warn('deepTrack можно использовать только с сигналами')
271
+ return
272
+ }
273
+
274
+ signal.subscribe(effect, ANY_KEY_SYMBOL)
275
+
276
+ currentDepth++
277
+
278
+ if (depth !== undefined && currentDepth >= depth) {
279
+ return
280
+ }
281
+
282
+ if (Array.isArray(value)) {
283
+ for (let i = 0; i < value.length; i++) {
284
+ const item = value[i]
285
+
286
+ if (canBeSignal(item)) {
287
+ currentEffect = effect
288
+ _deepTrack(item, depth, currentDepth)
289
+ }
290
+ }
291
+ } else {
292
+ for (const key in value) {
293
+ const item = value[key]
294
+
295
+ if (canBeSignal(item)) {
296
+ currentEffect = effect
297
+ _deepTrack(item, depth, currentDepth)
298
+ }
299
+ }
300
+ }
301
+ })
302
+ }
303
+
304
+ export function deepTrack(value: SignalValue, depth?: number) {
305
+ _deepTrack(value, depth)
306
+ }
307
+
308
+ export function createDisposer(callback: DisposerCallback): Disposer {
309
+ let disposer: Disposer = {
310
+ effects: new Set(),
311
+ dispose() {
312
+ disposer.effects.forEach((effect) => {
313
+ effect.dispose()
314
+ })
315
+ },
316
+ }
317
+
318
+ currentDisposer = disposer
319
+ callback()
320
+ currentDisposer = null
321
+
322
+ return disposer
323
+ }
package/src/store.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { createContext } from './context'
2
+ import { batch, createSignal } from './signals'
3
+ import { PotokElement, WithChildren } from './types'
4
+
5
+ type State = Record<string, any>
6
+ type StateOptions<P extends StoreProps> = {
7
+ props: P
8
+ }
9
+ type Actions = Record<string, (...args: any[]) => any>
10
+ type ActionsOptions<P extends StoreProps, S extends State> = {
11
+ props: P
12
+ state: S
13
+ }
14
+ type StoreProps = Record<string, any>
15
+ type StoreInitiator<
16
+ P extends StoreProps,
17
+ S extends State,
18
+ A extends Actions,
19
+ > = {
20
+ state: (options: StateOptions<P>) => S
21
+ actions: (options: ActionsOptions<P, S>) => A
22
+ }
23
+
24
+ export type Store<P extends StoreProps, S extends State, A extends Actions> = {
25
+ provider(props: WithChildren<P>): PotokElement
26
+ reader(props: {
27
+ children: (options: { state: S; actions: A }) => PotokElement
28
+ }): PotokElement
29
+ }
30
+
31
+ export function createStore<P extends StoreProps>() {
32
+ return function <S extends State, A extends Actions>(
33
+ initiator: StoreInitiator<P, S, A>,
34
+ ): Store<P, S, A> {
35
+ const context = createContext<{ state: S; actions: A }>()
36
+
37
+ return {
38
+ provider(props) {
39
+ const proxiedState = createSignal(initiator.state({ props }))
40
+ const initializedActions = initiator.actions({
41
+ props,
42
+ state: proxiedState,
43
+ })
44
+ const batchedActions = Object.entries(
45
+ initializedActions,
46
+ ).reduce<Actions>((acc, [key, value]) => {
47
+ acc[key] = (...args: any[]) => {
48
+ return batch(() => {
49
+ return value.apply(initializedActions, args)
50
+ })
51
+ }
52
+
53
+ return acc
54
+ }, {}) as A
55
+
56
+ return context.provider({
57
+ value: { state: proxiedState, actions: batchedActions },
58
+ children: props.children,
59
+ })
60
+ },
61
+ reader(props) {
62
+ return context.reader({
63
+ children: (value) => props.children(value),
64
+ })
65
+ },
66
+ }
67
+ }
68
+ }
package/src/text.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { LibBlock } from './block'
2
+ import { createEffect } from './signals'
3
+ import type { LibContext, LibTextNode, PotokElement } from './types'
4
+
5
+ export type LibTextProps = {
6
+ text: unknown
7
+ }
8
+
9
+ class LibText extends LibBlock {
10
+ constructor(readonly props: LibTextProps, readonly context: LibContext) {
11
+ super()
12
+
13
+ const textNode: LibTextNode = {
14
+ type: 'text',
15
+ props,
16
+ context,
17
+ }
18
+
19
+ this.node = textNode
20
+
21
+ this.addEffect(
22
+ createEffect(() => {
23
+ context.updateTextNode(textNode)
24
+ }, true)
25
+ )
26
+
27
+ this.insertNode()
28
+ }
29
+ }
30
+
31
+ export function text(props: LibTextProps): PotokElement {
32
+ return function (context: LibContext) {
33
+ return new LibText(props, context)
34
+ }
35
+ }
package/src/types.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { LibBlock } from './block'
2
+ import type { LibHTMLElementProps } from './html-element'
3
+ import type {
4
+ LibHTMLElementEventMap,
5
+ LibHTMLElementTagNameMap,
6
+ } from './prop-types'
7
+ import type { LibTextProps } from './text'
8
+
9
+ export type LibProps = Record<string, any>
10
+
11
+ export type LibTextNode = {
12
+ type: 'text'
13
+ context: LibContext
14
+ props: LibTextProps
15
+ }
16
+
17
+ export type LibHtmlElementNode<
18
+ Tag extends keyof LibHTMLElementTagNameMap = keyof LibHTMLElementTagNameMap,
19
+ > = {
20
+ type: 'html-element'
21
+ context: LibContext
22
+ props: LibHTMLElementProps<Tag>
23
+ }
24
+
25
+ export type LibNode = LibTextNode | LibHtmlElementNode
26
+
27
+ export type LibContext = {
28
+ parentBlock: LibBlock | null
29
+ index: number
30
+ insertNode(
31
+ parent: LibHtmlElementNode | null,
32
+ node: LibNode,
33
+ after: LibNode | null,
34
+ ): void
35
+ updateTextNode(node: LibTextNode): void
36
+ updateHtmlElementNodeProp(
37
+ node: LibHtmlElementNode,
38
+ key: string,
39
+ value: unknown,
40
+ ): void
41
+ removeNode(node: LibNode): void
42
+ portals: Record<string, WithChildren<{}>['children']>
43
+ contexts: Record<symbol, Record<string, unknown>>
44
+ listeners: Record<
45
+ keyof LibHTMLElementEventMap,
46
+ Map<HTMLElement, LibHTMLElementEventMap[keyof LibHTMLElementEventMap]>
47
+ >
48
+ isServer: boolean
49
+ promises: Promise<unknown>[]
50
+ isHydrating: boolean
51
+ }
52
+
53
+ export type PotokElement = (context: LibContext) => LibBlock
54
+
55
+ export type MaybeArray<T> = T | T[]
56
+
57
+ export type Child = PotokElement | Child[] | string | number | null | undefined
58
+
59
+ export type Children = MaybeArray<Child> | undefined
60
+
61
+ export type WithChildren<Props extends Record<string, unknown>> = Props & {
62
+ children?: Children
63
+ }
64
+
65
+ export type ComponentProps = Record<string, unknown>
66
+
67
+ export type Component<Props extends ComponentProps = ComponentProps> = (
68
+ props: Props,
69
+ ) => PotokElement
package/src/utils.ts ADDED
@@ -0,0 +1,118 @@
1
+ import type {
2
+ LibHTMLElementEventMap,
3
+ LibHTMLElementTagNameMap,
4
+ } from './prop-types'
5
+ import { text } from './text'
6
+ import type {
7
+ Child,
8
+ Children,
9
+ LibContext,
10
+ MaybeArray,
11
+ PotokElement,
12
+ } from './types'
13
+
14
+ export function mergeContext(
15
+ rootContext: LibContext,
16
+ childContext: Pick<LibContext, 'parentBlock' | 'index'> &
17
+ Partial<Pick<LibContext, 'contexts'>>
18
+ ): LibContext {
19
+ return {
20
+ parentBlock: childContext.parentBlock,
21
+ get index() {
22
+ return childContext.index
23
+ },
24
+ portals: rootContext.portals,
25
+ contexts: {
26
+ ...rootContext.contexts,
27
+ ...childContext.contexts,
28
+ },
29
+ listeners: rootContext.listeners,
30
+ isServer: rootContext.isServer,
31
+ promises: rootContext.promises,
32
+ get isHydrating() {
33
+ return rootContext.isHydrating
34
+ },
35
+ insertNode: rootContext.insertNode,
36
+ updateTextNode: rootContext.updateTextNode,
37
+ updateHtmlElementNodeProp: rootContext.updateHtmlElementNodeProp,
38
+ removeNode: rootContext.removeNode,
39
+ }
40
+ }
41
+
42
+ export function objectKeys<Obj extends Record<string, unknown>>(
43
+ obj: Obj
44
+ ): (keyof Obj)[] {
45
+ return Object.keys(obj)
46
+ }
47
+
48
+ export function extractListenersFromProps(
49
+ props: LibHTMLElementTagNameMap[keyof LibHTMLElementTagNameMap]
50
+ ): LibHTMLElementEventMap {
51
+ return Object.entries(props).reduce<LibHTMLElementEventMap>(
52
+ (acc, [key, value]) => {
53
+ if (key.startsWith('on')) {
54
+ acc[key as keyof LibHTMLElementEventMap] = value
55
+ }
56
+
57
+ return acc
58
+ },
59
+ {}
60
+ )
61
+ }
62
+
63
+ export function map<T extends string | number | symbol, K>(
64
+ field: T,
65
+ mapObject: Record<T, K>
66
+ ): K
67
+ export function map<T extends string | number | symbol, K>(
68
+ field: T | undefined | null,
69
+ mapObject: Partial<Record<T, K>>,
70
+ defaultValue: K
71
+ ): K
72
+ export function map<T extends boolean, K>(
73
+ field: T,
74
+ mapObject: Record<'true' | 'false', K>
75
+ ): K
76
+ export function map<T extends string | number | symbol | 'true' | 'false', K>(
77
+ field: T | undefined,
78
+ mapObject: Record<T, K>,
79
+ defaultValue?: K
80
+ ): K {
81
+ if (field === undefined) return defaultValue as K
82
+
83
+ return mapObject[field] ?? (defaultValue as K)
84
+ }
85
+
86
+ export function normalizeArray<T>(array: MaybeArray<T>): T[] {
87
+ if (Array.isArray(array)) {
88
+ return array
89
+ }
90
+
91
+ return [array]
92
+ }
93
+
94
+ export function normalizeChild(child: NonNullable<Child>): PotokElement[] {
95
+ if (typeof child === 'string' || typeof child === 'number') {
96
+ return [
97
+ text({
98
+ text: String(child),
99
+ }),
100
+ ]
101
+ }
102
+
103
+ if (Array.isArray(child)) {
104
+ return child.flatMap(normalizeChildren)
105
+ }
106
+
107
+ return [child]
108
+ }
109
+
110
+ export function normalizeChildren(children: Children): PotokElement[] {
111
+ if (!children) {
112
+ return []
113
+ }
114
+
115
+ return normalizeArray(children)
116
+ .filter((child) => child !== null && child !== undefined)
117
+ .flatMap(normalizeChild)
118
+ }