@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.
- package/CHANGELOG.md +7 -0
- package/bun.lock +25 -0
- package/package.json +39 -0
- package/src/block.ts +102 -0
- package/src/bootstrap-app.ts +115 -0
- package/src/client-only.ts +17 -0
- package/src/constants.ts +27 -0
- package/src/context.ts +85 -0
- package/src/detect-child.ts +21 -0
- package/src/error-boundary.ts +51 -0
- package/src/exports/client.ts +1 -0
- package/src/exports/hmr.ts +1 -0
- package/src/exports/index.ts +21 -0
- package/src/exports/jsx-runtime.ts +4 -0
- package/src/exports/server.ts +1 -0
- package/src/fragment.ts +28 -0
- package/src/global.dev.d.ts +12 -0
- package/src/hmr/hmr-dev.ts +10 -0
- package/src/hmr/register-component.ts +109 -0
- package/src/hmr/registered-component.ts +59 -0
- package/src/hmr/registry.ts +78 -0
- package/src/hmr/types.ts +6 -0
- package/src/hmr/utils.ts +20 -0
- package/src/html-element.ts +95 -0
- package/src/jsx-types.ts +13 -0
- package/src/lazy.ts +44 -0
- package/src/lib-context-reader.ts +33 -0
- package/src/lib-scripts.ts +8 -0
- package/src/lifecycle.ts +44 -0
- package/src/list.ts +175 -0
- package/src/portal.ts +101 -0
- package/src/prop-types.ts +1165 -0
- package/src/ref.ts +11 -0
- package/src/render-to-dom.ts +325 -0
- package/src/render-to-string.ts +65 -0
- package/src/server-node.ts +98 -0
- package/src/show.ts +46 -0
- package/src/signals.ts +323 -0
- package/src/store.ts +68 -0
- package/src/text.ts +35 -0
- package/src/types.ts +69 -0
- package/src/utils.ts +118 -0
- package/tests/signals.test.ts +403 -0
- package/tsconfig.json +17 -0
- 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
|
+
}
|