kiru 0.44.4
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 +7 -0
- package/README.md +5 -0
- package/package.json +81 -0
- package/src/appContext.ts +186 -0
- package/src/cloneVNode.ts +14 -0
- package/src/constants.ts +146 -0
- package/src/context.ts +56 -0
- package/src/dom.ts +712 -0
- package/src/element.ts +54 -0
- package/src/env.ts +6 -0
- package/src/error.ts +85 -0
- package/src/flags.ts +15 -0
- package/src/form/index.ts +662 -0
- package/src/form/types.ts +261 -0
- package/src/form/utils.ts +19 -0
- package/src/generateId.ts +19 -0
- package/src/globalContext.ts +161 -0
- package/src/globals.ts +21 -0
- package/src/hmr.ts +178 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAsync.ts +136 -0
- package/src/hooks/useCallback.ts +31 -0
- package/src/hooks/useContext.ts +79 -0
- package/src/hooks/useEffect.ts +44 -0
- package/src/hooks/useEffectEvent.ts +24 -0
- package/src/hooks/useId.ts +42 -0
- package/src/hooks/useLayoutEffect.ts +47 -0
- package/src/hooks/useMemo.ts +33 -0
- package/src/hooks/useReducer.ts +50 -0
- package/src/hooks/useRef.ts +40 -0
- package/src/hooks/useState.ts +62 -0
- package/src/hooks/useSyncExternalStore.ts +59 -0
- package/src/hooks/useViewTransition.ts +26 -0
- package/src/hooks/utils.ts +259 -0
- package/src/hydration.ts +67 -0
- package/src/index.ts +61 -0
- package/src/jsx.ts +11 -0
- package/src/lazy.ts +238 -0
- package/src/memo.ts +48 -0
- package/src/portal.ts +43 -0
- package/src/profiling.ts +105 -0
- package/src/props.ts +36 -0
- package/src/reconciler.ts +531 -0
- package/src/renderToString.ts +91 -0
- package/src/router/index.ts +2 -0
- package/src/router/route.ts +51 -0
- package/src/router/router.ts +275 -0
- package/src/router/routerUtils.ts +49 -0
- package/src/scheduler.ts +522 -0
- package/src/signals/base.ts +237 -0
- package/src/signals/computed.ts +139 -0
- package/src/signals/effect.ts +60 -0
- package/src/signals/globals.ts +11 -0
- package/src/signals/index.ts +12 -0
- package/src/signals/jsx.ts +45 -0
- package/src/signals/types.ts +10 -0
- package/src/signals/utils.ts +12 -0
- package/src/signals/watch.ts +151 -0
- package/src/ssr/client.ts +29 -0
- package/src/ssr/hydrationBoundary.ts +63 -0
- package/src/ssr/index.ts +1 -0
- package/src/ssr/server.ts +124 -0
- package/src/store.ts +241 -0
- package/src/swr.ts +360 -0
- package/src/transition.ts +80 -0
- package/src/types.dom.ts +1250 -0
- package/src/types.ts +209 -0
- package/src/types.utils.ts +39 -0
- package/src/utils.ts +581 -0
- package/src/warning.ts +9 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { KaiokenError } from "../error.js"
|
|
2
|
+
import { __DEV__ } from "../env.js"
|
|
3
|
+
import { ctx, hookIndex, node, nodeToCtxMap } from "../globals.js"
|
|
4
|
+
import { getVNodeAppContext, noop } from "../utils.js"
|
|
5
|
+
export { sideEffectsEnabled } from "../utils.js"
|
|
6
|
+
export {
|
|
7
|
+
cleanupHook,
|
|
8
|
+
depsRequireChange,
|
|
9
|
+
useHook,
|
|
10
|
+
useVNode,
|
|
11
|
+
useAppContext,
|
|
12
|
+
useHookDebugGroup,
|
|
13
|
+
useRequestUpdate,
|
|
14
|
+
HookDebugGroupAction,
|
|
15
|
+
type HookState,
|
|
16
|
+
type HookCallback,
|
|
17
|
+
type HookCallbackContext as HookCallbackState,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type HookState<T> = Kaioken.Hook<T>
|
|
21
|
+
|
|
22
|
+
enum HookDebugGroupAction {
|
|
23
|
+
Start = "start",
|
|
24
|
+
End = "end",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* **dev only - this is a no-op in production.**
|
|
29
|
+
*
|
|
30
|
+
* Used to create 'groups' of hooks in the devtools.
|
|
31
|
+
* Useful for debugging and profiling.
|
|
32
|
+
*/
|
|
33
|
+
const useHookDebugGroup = (name: string, action: HookDebugGroupAction) => {
|
|
34
|
+
if (__DEV__) {
|
|
35
|
+
return useHook(
|
|
36
|
+
"devtools:useHookDebugGroup",
|
|
37
|
+
{ displayName: name, action },
|
|
38
|
+
noop
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Used obtain an 'requestUpdate' function for the current component.
|
|
45
|
+
*/
|
|
46
|
+
const useRequestUpdate = () => {
|
|
47
|
+
const n = node.current
|
|
48
|
+
if (!n) error_hookMustBeCalledTopLevel("useRequestUpdate")
|
|
49
|
+
const ctx = getVNodeAppContext(n)
|
|
50
|
+
return () => ctx.requestUpdate(n)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Used to obtain the 'AppContext' for the current component.
|
|
55
|
+
*/
|
|
56
|
+
const useAppContext = () => {
|
|
57
|
+
if (!node.current) error_hookMustBeCalledTopLevel("useAppContext")
|
|
58
|
+
const ctx = nodeToCtxMap.get(node.current)
|
|
59
|
+
if (!ctx)
|
|
60
|
+
error_hookMustBeCalledTopLevel(
|
|
61
|
+
"[kaioken]: unable to find node's AppContext"
|
|
62
|
+
)
|
|
63
|
+
return ctx
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Used to obtain the 'VNode' for the current component.
|
|
68
|
+
*/
|
|
69
|
+
const useVNode = () => {
|
|
70
|
+
const n = node.current
|
|
71
|
+
if (!n) error_hookMustBeCalledTopLevel("useVNode")
|
|
72
|
+
return n
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type HookCallbackContext<T> = {
|
|
76
|
+
/**
|
|
77
|
+
* The current state of the hook
|
|
78
|
+
*/
|
|
79
|
+
hook: HookState<T>
|
|
80
|
+
/**
|
|
81
|
+
* Indicates if this is the first time the hook has been initialized
|
|
82
|
+
*/
|
|
83
|
+
isInit: boolean
|
|
84
|
+
/**
|
|
85
|
+
* Dev mode only - indicates if the hook is being run as a result of a HMR update.
|
|
86
|
+
* This is the time to clean up the previous version of the hook if necessary, ie. initial arguments changed.
|
|
87
|
+
*/
|
|
88
|
+
isHMR?: boolean
|
|
89
|
+
/**
|
|
90
|
+
* Queues the current component to be re-rendered
|
|
91
|
+
*/
|
|
92
|
+
update: () => void
|
|
93
|
+
/**
|
|
94
|
+
* Queues an effect to be run, either immediately or on the next render
|
|
95
|
+
*/
|
|
96
|
+
queueEffect: (callback: Function, opts?: { immediate?: boolean }) => void
|
|
97
|
+
/**
|
|
98
|
+
* The VNode associated with the current component
|
|
99
|
+
*/
|
|
100
|
+
vNode: Kaioken.VNode
|
|
101
|
+
/**
|
|
102
|
+
* The index of the current hook.
|
|
103
|
+
* You can count on this being stable across renders,
|
|
104
|
+
* and unique across separate hooks in the same component.
|
|
105
|
+
*/
|
|
106
|
+
index: number
|
|
107
|
+
}
|
|
108
|
+
type HookCallback<T> = (state: HookCallbackContext<T>) => any
|
|
109
|
+
|
|
110
|
+
let currentHookName: string | null = null
|
|
111
|
+
const nestedHookWarnings = new Set<string>()
|
|
112
|
+
|
|
113
|
+
function useHook<
|
|
114
|
+
T extends () => Record<string, unknown>,
|
|
115
|
+
U extends HookCallback<ReturnType<T>>
|
|
116
|
+
>(hookName: string, hookInitializer: T, callback: U): ReturnType<U>
|
|
117
|
+
|
|
118
|
+
function useHook<T extends Record<string, unknown>, U extends HookCallback<T>>(
|
|
119
|
+
hookName: string,
|
|
120
|
+
hookData: T,
|
|
121
|
+
callback: U
|
|
122
|
+
): ReturnType<U>
|
|
123
|
+
|
|
124
|
+
function useHook<
|
|
125
|
+
T,
|
|
126
|
+
U extends T extends () => Record<string, unknown>
|
|
127
|
+
? HookCallback<ReturnType<T>>
|
|
128
|
+
: HookCallback<T>
|
|
129
|
+
>(
|
|
130
|
+
hookName: string,
|
|
131
|
+
hookDataOrInitializer: HookState<T> | (() => HookState<T>),
|
|
132
|
+
callback: U
|
|
133
|
+
): ReturnType<U> {
|
|
134
|
+
const vNode = node.current
|
|
135
|
+
if (!vNode) error_hookMustBeCalledTopLevel(hookName)
|
|
136
|
+
|
|
137
|
+
if (__DEV__) {
|
|
138
|
+
if (
|
|
139
|
+
currentHookName !== null &&
|
|
140
|
+
!nestedHookWarnings.has(hookName + currentHookName)
|
|
141
|
+
) {
|
|
142
|
+
nestedHookWarnings.add(hookName + currentHookName)
|
|
143
|
+
throw new KaiokenError({
|
|
144
|
+
message: `Nested primitive "useHook" calls are not supported. "${hookName}" was called inside "${currentHookName}". Strange will most certainly happen.`,
|
|
145
|
+
vNode,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const queueEffect = (callback: Function, opts?: { immediate?: boolean }) => {
|
|
151
|
+
if (opts?.immediate) {
|
|
152
|
+
;(vNode.immediateEffects ??= []).push(callback)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
;(vNode.effects ??= []).push(callback)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const appCtx = ctx.current
|
|
159
|
+
const index = hookIndex.current++
|
|
160
|
+
|
|
161
|
+
let oldHook = (
|
|
162
|
+
vNode.prev ? vNode.prev.hooks?.at(index) : vNode.hooks?.at(index)
|
|
163
|
+
) as HookState<T> | undefined
|
|
164
|
+
|
|
165
|
+
if (__DEV__) {
|
|
166
|
+
currentHookName = hookName
|
|
167
|
+
|
|
168
|
+
vNode.hooks ??= []
|
|
169
|
+
vNode.hookSig ??= []
|
|
170
|
+
|
|
171
|
+
if (!vNode.hookSig[index]) {
|
|
172
|
+
vNode.hookSig[index] = hookName
|
|
173
|
+
} else {
|
|
174
|
+
if (vNode.hookSig[index] !== hookName) {
|
|
175
|
+
console.warn(
|
|
176
|
+
`[kaioken]: hooks must be called in the same order. Hook "${hookName}" was called in place of "${vNode.hookSig[index]}". Strange things may happen.`
|
|
177
|
+
)
|
|
178
|
+
vNode.hooks.length = index
|
|
179
|
+
vNode.hookSig.length = index
|
|
180
|
+
oldHook = undefined
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let hook: HookState<T>
|
|
185
|
+
if (!oldHook) {
|
|
186
|
+
hook =
|
|
187
|
+
typeof hookDataOrInitializer === "function"
|
|
188
|
+
? hookDataOrInitializer()
|
|
189
|
+
: { ...hookDataOrInitializer }
|
|
190
|
+
hook.name = hookName
|
|
191
|
+
} else {
|
|
192
|
+
hook = oldHook
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
vNode.hooks[index] = hook
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const res = (callback as HookCallback<T>)({
|
|
199
|
+
hook,
|
|
200
|
+
isInit: !oldHook,
|
|
201
|
+
isHMR: vNode.hmrUpdated,
|
|
202
|
+
update: () => appCtx.requestUpdate(vNode),
|
|
203
|
+
queueEffect,
|
|
204
|
+
vNode,
|
|
205
|
+
index,
|
|
206
|
+
})
|
|
207
|
+
return res
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw error
|
|
210
|
+
} finally {
|
|
211
|
+
currentHookName = null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const hook: HookState<T> =
|
|
217
|
+
oldHook ??
|
|
218
|
+
(typeof hookDataOrInitializer === "function"
|
|
219
|
+
? hookDataOrInitializer()
|
|
220
|
+
: { ...hookDataOrInitializer })
|
|
221
|
+
|
|
222
|
+
vNode.hooks ??= []
|
|
223
|
+
vNode.hooks[index] = hook
|
|
224
|
+
|
|
225
|
+
const res = (callback as HookCallback<T>)({
|
|
226
|
+
hook,
|
|
227
|
+
isInit: !oldHook,
|
|
228
|
+
update: () => appCtx.requestUpdate(vNode),
|
|
229
|
+
queueEffect,
|
|
230
|
+
vNode,
|
|
231
|
+
index,
|
|
232
|
+
})
|
|
233
|
+
return res
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function error_hookMustBeCalledTopLevel(hookName: string): never {
|
|
240
|
+
throw new KaiokenError(
|
|
241
|
+
`Hook "${hookName}" must be used at the top level of a component or inside another composite hook.`
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function cleanupHook(hook: { cleanup?: () => void }) {
|
|
246
|
+
if (hook.cleanup) {
|
|
247
|
+
hook.cleanup()
|
|
248
|
+
hook.cleanup = undefined
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function depsRequireChange(a?: unknown[], b?: unknown[]) {
|
|
253
|
+
return (
|
|
254
|
+
a === undefined ||
|
|
255
|
+
b === undefined ||
|
|
256
|
+
a.length !== b.length ||
|
|
257
|
+
(a.length > 0 && b.some((dep, i) => !Object.is(dep, a[i])))
|
|
258
|
+
)
|
|
259
|
+
}
|
package/src/hydration.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MaybeDom, SomeDom } from "./types.utils"
|
|
2
|
+
|
|
3
|
+
export const hydrationStack = {
|
|
4
|
+
parentStack: [] as Array<SomeDom>,
|
|
5
|
+
childIdxStack: [] as Array<number>,
|
|
6
|
+
eventDeferrals: new Map<Element, Array<() => void>>(),
|
|
7
|
+
parent: function () {
|
|
8
|
+
return this.parentStack[this.parentStack.length - 1]
|
|
9
|
+
},
|
|
10
|
+
clear: function () {
|
|
11
|
+
this.parentStack.length = 0
|
|
12
|
+
this.childIdxStack.length = 0
|
|
13
|
+
},
|
|
14
|
+
pop: function () {
|
|
15
|
+
this.parentStack.pop()
|
|
16
|
+
this.childIdxStack.pop()
|
|
17
|
+
},
|
|
18
|
+
push: function (el: SomeDom) {
|
|
19
|
+
this.parentStack.push(el)
|
|
20
|
+
this.childIdxStack.push(0)
|
|
21
|
+
},
|
|
22
|
+
currentChild: function () {
|
|
23
|
+
return this.parentStack[this.parentStack.length - 1].childNodes[
|
|
24
|
+
this.childIdxStack[this.childIdxStack.length - 1]
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
nextChild: function () {
|
|
28
|
+
return this.parentStack[this.parentStack.length - 1].childNodes[
|
|
29
|
+
this.childIdxStack[this.childIdxStack.length - 1]++
|
|
30
|
+
] as MaybeDom
|
|
31
|
+
},
|
|
32
|
+
bumpChildIndex: function () {
|
|
33
|
+
this.childIdxStack[this.childIdxStack.length - 1]++
|
|
34
|
+
},
|
|
35
|
+
captureEvents: function (element: Element) {
|
|
36
|
+
toggleEvtListeners(element, true)
|
|
37
|
+
this.eventDeferrals.set(element, [])
|
|
38
|
+
},
|
|
39
|
+
resetEvents: function (element: Element) {
|
|
40
|
+
this.eventDeferrals.delete(element)
|
|
41
|
+
},
|
|
42
|
+
releaseEvents: function (element: Element) {
|
|
43
|
+
toggleEvtListeners(element, false)
|
|
44
|
+
const events = this.eventDeferrals.get(element)
|
|
45
|
+
while (events?.length) events.shift()!()
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const captureEvent = (e: Event) => {
|
|
50
|
+
const t = e.target
|
|
51
|
+
if (!e.isTrusted || !t) return
|
|
52
|
+
hydrationStack.eventDeferrals
|
|
53
|
+
.get(t as Element)
|
|
54
|
+
?.push(() => t.dispatchEvent(e))
|
|
55
|
+
}
|
|
56
|
+
const toggleEvtListeners = (element: Element, value: boolean) => {
|
|
57
|
+
for (const key in element) {
|
|
58
|
+
if (key.startsWith("on")) {
|
|
59
|
+
const eventType = key.substring(2)
|
|
60
|
+
element[value ? "addEventListener" : "removeEventListener"](
|
|
61
|
+
eventType,
|
|
62
|
+
captureEvent,
|
|
63
|
+
{ passive: true }
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppContext,
|
|
3
|
+
type AppContext,
|
|
4
|
+
type AppContextOptions,
|
|
5
|
+
} from "./appContext.js"
|
|
6
|
+
import { ctx } from "./globals.js"
|
|
7
|
+
import { createKaiokenGlobalContext } from "./globalContext.js"
|
|
8
|
+
import { __DEV__ } from "./env.js"
|
|
9
|
+
import { KaiokenError } from "./error.js"
|
|
10
|
+
|
|
11
|
+
export type * from "./types"
|
|
12
|
+
export * from "./appContext.js"
|
|
13
|
+
export * from "./context.js"
|
|
14
|
+
export * from "./cloneVNode.js"
|
|
15
|
+
export * from "./element.js"
|
|
16
|
+
export * from "./hooks/index.js"
|
|
17
|
+
export * from "./lazy.js"
|
|
18
|
+
export { memo } from "./memo.js"
|
|
19
|
+
export * from "./portal.js"
|
|
20
|
+
export * from "./renderToString.js"
|
|
21
|
+
export * from "./signals/index.js"
|
|
22
|
+
export * from "./store.js"
|
|
23
|
+
export * from "./transition.js"
|
|
24
|
+
|
|
25
|
+
if ("window" in globalThis) {
|
|
26
|
+
globalThis.window.__kaioken ??= createKaiokenGlobalContext()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function mount<T extends Record<string, unknown>>(
|
|
30
|
+
appFunc: (props: T) => JSX.Element,
|
|
31
|
+
options: AppContextOptions,
|
|
32
|
+
appProps?: T
|
|
33
|
+
): Promise<AppContext<T>>
|
|
34
|
+
|
|
35
|
+
export function mount<T extends Record<string, unknown>>(
|
|
36
|
+
appFunc: (props: T) => JSX.Element,
|
|
37
|
+
root: HTMLElement,
|
|
38
|
+
appProps?: T
|
|
39
|
+
): Promise<AppContext<T>>
|
|
40
|
+
|
|
41
|
+
export function mount<T extends Record<string, unknown>>(
|
|
42
|
+
appFunc: (props: T) => JSX.Element,
|
|
43
|
+
optionsOrRoot: HTMLElement | AppContextOptions,
|
|
44
|
+
appProps = {} as T
|
|
45
|
+
): Promise<AppContext<T>> {
|
|
46
|
+
let root: HTMLElement, opts: AppContextOptions | undefined
|
|
47
|
+
if (optionsOrRoot instanceof HTMLElement) {
|
|
48
|
+
root = optionsOrRoot
|
|
49
|
+
opts = { root }
|
|
50
|
+
} else {
|
|
51
|
+
opts = optionsOrRoot
|
|
52
|
+
root = optionsOrRoot.root!
|
|
53
|
+
if (__DEV__) {
|
|
54
|
+
if (!(root instanceof HTMLElement)) {
|
|
55
|
+
throw new KaiokenError("Root node must be an HTMLElement")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ctx.current = createAppContext<T>(appFunc, appProps, opts)
|
|
60
|
+
return ctx.current.mount()
|
|
61
|
+
}
|
package/src/jsx.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createElement, Fragment } from "./element.js"
|
|
2
|
+
|
|
3
|
+
export { jsx, jsx as jsxs, jsx as jsxDEV, Fragment }
|
|
4
|
+
|
|
5
|
+
function jsx(
|
|
6
|
+
type: Kaioken.VNode["type"],
|
|
7
|
+
{ children, ...props } = {} as { children?: Kaioken.VNode[] }
|
|
8
|
+
) {
|
|
9
|
+
if (!children) return createElement(type, props)
|
|
10
|
+
return createElement(type, props, children)
|
|
11
|
+
}
|
package/src/lazy.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createElement } from "./element.js"
|
|
2
|
+
import { __DEV__ } from "./env.js"
|
|
3
|
+
import { KaiokenError } from "./error.js"
|
|
4
|
+
import { node, renderMode } from "./globals.js"
|
|
5
|
+
import { useContext } from "./hooks/useContext.js"
|
|
6
|
+
import { useRef } from "./hooks/useRef.js"
|
|
7
|
+
import { useAppContext, useRequestUpdate } from "./hooks/utils.js"
|
|
8
|
+
import { hydrationStack } from "./hydration.js"
|
|
9
|
+
import {
|
|
10
|
+
HYDRATION_BOUNDARY_MARKER,
|
|
11
|
+
HydrationBoundaryContext,
|
|
12
|
+
} from "./ssr/hydrationBoundary.js"
|
|
13
|
+
import type { SomeDom } from "./types.utils"
|
|
14
|
+
import { noop } from "./utils.js"
|
|
15
|
+
|
|
16
|
+
type FCModule = { default: Kaioken.FC<any> }
|
|
17
|
+
type LazyImportValue = Kaioken.FC<any> | FCModule
|
|
18
|
+
type InferLazyImportProps<T extends LazyImportValue> = T extends FCModule
|
|
19
|
+
? Kaioken.InferProps<T["default"]>
|
|
20
|
+
: Kaioken.InferProps<T>
|
|
21
|
+
|
|
22
|
+
type LazyState = {
|
|
23
|
+
fn: string
|
|
24
|
+
promise: Promise<LazyImportValue>
|
|
25
|
+
result: Kaioken.FC | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type LazyComponentProps<T extends LazyImportValue> = InferLazyImportProps<T> & {
|
|
29
|
+
fallback?: JSX.Element
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const lazyCache: Map<string, LazyState> =
|
|
33
|
+
"window" in globalThis
|
|
34
|
+
? // @ts-ignore - we're shamefully polluting the global scope here and hiding it 🥲
|
|
35
|
+
(window.__KAIOKEN_LAZY_CACHE ??= new Map<string, LazyState>())
|
|
36
|
+
: new Map<string, LazyState>()
|
|
37
|
+
|
|
38
|
+
function consumeHydrationBoundaryChildren(parentNode: Kaioken.VNode): {
|
|
39
|
+
parent: HTMLElement
|
|
40
|
+
childNodes: Node[]
|
|
41
|
+
startIndex: number
|
|
42
|
+
} {
|
|
43
|
+
const boundaryStart = hydrationStack.currentChild()
|
|
44
|
+
if (
|
|
45
|
+
boundaryStart?.nodeType !== Node.COMMENT_NODE ||
|
|
46
|
+
boundaryStart.nodeValue !== HYDRATION_BOUNDARY_MARKER
|
|
47
|
+
) {
|
|
48
|
+
throw new KaiokenError({
|
|
49
|
+
message:
|
|
50
|
+
"Invalid HydrationBoundary node. This is likely a bug in Kaioken.",
|
|
51
|
+
fatal: true,
|
|
52
|
+
vNode: parentNode,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
const parent = boundaryStart.parentElement!
|
|
56
|
+
const childNodes: Node[] = []
|
|
57
|
+
const isBoundaryEnd = (n: Node) => {
|
|
58
|
+
return (
|
|
59
|
+
n.nodeType === Node.COMMENT_NODE &&
|
|
60
|
+
n.nodeValue === "/" + HYDRATION_BOUNDARY_MARKER
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
let n = boundaryStart.nextSibling
|
|
64
|
+
boundaryStart.remove()
|
|
65
|
+
const startIndex =
|
|
66
|
+
hydrationStack.childIdxStack[hydrationStack.childIdxStack.length - 1]
|
|
67
|
+
while (n && !isBoundaryEnd(n)) {
|
|
68
|
+
childNodes.push(n)
|
|
69
|
+
hydrationStack.bumpChildIndex()
|
|
70
|
+
n = n.nextSibling
|
|
71
|
+
}
|
|
72
|
+
const boundaryEnd = hydrationStack.currentChild()
|
|
73
|
+
if (!isBoundaryEnd(boundaryEnd)) {
|
|
74
|
+
throw new KaiokenError({
|
|
75
|
+
message:
|
|
76
|
+
"Invalid HydrationBoundary node. This is likely a bug in Kaioken.",
|
|
77
|
+
fatal: true,
|
|
78
|
+
vNode: parentNode,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
boundaryEnd.remove()
|
|
82
|
+
return { parent, childNodes, startIndex }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function lazy<T extends LazyImportValue>(
|
|
86
|
+
componentPromiseFn: () => Promise<T>
|
|
87
|
+
): Kaioken.FC<LazyComponentProps<T>> {
|
|
88
|
+
function LazyComponent(props: LazyComponentProps<T>) {
|
|
89
|
+
const { fallback = null, ...rest } = props
|
|
90
|
+
const appCtx = useAppContext()
|
|
91
|
+
const hydrationCtx = useContext(HydrationBoundaryContext, false)
|
|
92
|
+
const needsHydration = useRef(
|
|
93
|
+
hydrationCtx && renderMode.current === "hydrate"
|
|
94
|
+
)
|
|
95
|
+
const abortHydration = useRef(noop)
|
|
96
|
+
const requestUpdate = useRequestUpdate()
|
|
97
|
+
if (renderMode.current === "string" || renderMode.current === "stream") {
|
|
98
|
+
return fallback
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fn = componentPromiseFn.toString()
|
|
102
|
+
const withoutQuery = removeQueryString(fn)
|
|
103
|
+
const cachedState = lazyCache.get(withoutQuery)
|
|
104
|
+
if (!cachedState || cachedState.fn !== fn) {
|
|
105
|
+
const promise = componentPromiseFn()
|
|
106
|
+
const state: LazyState = {
|
|
107
|
+
fn,
|
|
108
|
+
promise,
|
|
109
|
+
result: null,
|
|
110
|
+
}
|
|
111
|
+
lazyCache.set(withoutQuery, state)
|
|
112
|
+
|
|
113
|
+
const ready = promise.then((componentOrModule) => {
|
|
114
|
+
state.result =
|
|
115
|
+
typeof componentOrModule === "function"
|
|
116
|
+
? componentOrModule
|
|
117
|
+
: componentOrModule.default
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (!needsHydration.current) {
|
|
121
|
+
ready.then(() => requestUpdate())
|
|
122
|
+
return fallback
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const thisNode = node.current!
|
|
126
|
+
|
|
127
|
+
abortHydration.current = () => {
|
|
128
|
+
for (const child of childNodes) {
|
|
129
|
+
if (child instanceof Element) {
|
|
130
|
+
hydrationStack.resetEvents(child)
|
|
131
|
+
}
|
|
132
|
+
child.parentNode?.removeChild(child)
|
|
133
|
+
}
|
|
134
|
+
needsHydration.current = false
|
|
135
|
+
delete thisNode.lastChildDom
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (__DEV__) {
|
|
139
|
+
window.__kaioken?.HMRContext?.onHmr(() => {
|
|
140
|
+
if (needsHydration.current) {
|
|
141
|
+
abortHydration.current()
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { parent, childNodes, startIndex } =
|
|
147
|
+
consumeHydrationBoundaryChildren(thisNode)
|
|
148
|
+
|
|
149
|
+
thisNode.lastChildDom = childNodes[childNodes.length - 1] as SomeDom
|
|
150
|
+
|
|
151
|
+
for (const child of childNodes) {
|
|
152
|
+
if (child instanceof Element) {
|
|
153
|
+
hydrationStack.captureEvents(child)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const hydrate = () => {
|
|
157
|
+
if (needsHydration.current === false) return
|
|
158
|
+
|
|
159
|
+
appCtx.scheduler?.nextIdle(() => {
|
|
160
|
+
delete thisNode.lastChildDom
|
|
161
|
+
needsHydration.current = false
|
|
162
|
+
hydrationStack.push(parent)
|
|
163
|
+
hydrationStack.childIdxStack[
|
|
164
|
+
hydrationStack.childIdxStack.length - 1
|
|
165
|
+
] = startIndex
|
|
166
|
+
const prev = renderMode.current
|
|
167
|
+
/**
|
|
168
|
+
* must call requestUpdate before setting renderMode
|
|
169
|
+
* to hydrate, otherwise the update will be postponed
|
|
170
|
+
* and flushSync will have no effect
|
|
171
|
+
*/
|
|
172
|
+
requestUpdate()
|
|
173
|
+
renderMode.current = "hydrate"
|
|
174
|
+
appCtx.flushSync()
|
|
175
|
+
renderMode.current = prev
|
|
176
|
+
for (const child of childNodes) {
|
|
177
|
+
if (child instanceof Element) {
|
|
178
|
+
hydrationStack.releaseEvents(child)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* once the promise resolves, we need to act according
|
|
186
|
+
* to the HydrationBoundaryContext 'mode'.
|
|
187
|
+
*
|
|
188
|
+
* - with 'eager', we just hydrate the children immediately
|
|
189
|
+
* - with 'lazy', we'll wait for user interaction before hydrating
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
if (hydrationCtx.mode === "eager") {
|
|
193
|
+
ready.then(hydrate)
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
const interactionEvents = hydrationCtx.events
|
|
197
|
+
const onInteraction = (e: Event) => {
|
|
198
|
+
const tgt = e.target
|
|
199
|
+
if (
|
|
200
|
+
tgt instanceof Element &&
|
|
201
|
+
childNodes.some((child) => child.contains(tgt))
|
|
202
|
+
) {
|
|
203
|
+
interactionEvents.forEach((evtName) => {
|
|
204
|
+
window.removeEventListener(evtName, onInteraction)
|
|
205
|
+
})
|
|
206
|
+
ready.then(hydrate)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
interactionEvents.forEach((evtName) => {
|
|
210
|
+
window.addEventListener(evtName, onInteraction)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (cachedState.result === null) {
|
|
217
|
+
cachedState.promise.then(requestUpdate)
|
|
218
|
+
return fallback
|
|
219
|
+
}
|
|
220
|
+
if (needsHydration.current) {
|
|
221
|
+
abortHydration.current()
|
|
222
|
+
}
|
|
223
|
+
return createElement(cachedState.result, rest)
|
|
224
|
+
}
|
|
225
|
+
LazyComponent.displayName = "Kaioken.lazy"
|
|
226
|
+
return LazyComponent
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* removes the query string from a function - prevents
|
|
231
|
+
* vite-modified imports (eg. () => import("./Counter.tsx?t=123456"))
|
|
232
|
+
* from causing issues
|
|
233
|
+
*/
|
|
234
|
+
const removeQueryString = (fnStr: string): string =>
|
|
235
|
+
fnStr.replace(
|
|
236
|
+
/import\((["'])([^?"']+)\?[^)"']*\1\)/g,
|
|
237
|
+
(_, quote, path) => `import(${quote}${path}${quote})`
|
|
238
|
+
)
|
package/src/memo.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { $MEMO } from "./constants.js"
|
|
2
|
+
import { createElement } from "./element.js"
|
|
3
|
+
import { __DEV__ } from "./env.js"
|
|
4
|
+
|
|
5
|
+
function _arePropsEqual<T extends Record<string, unknown>>(
|
|
6
|
+
prevProps: T,
|
|
7
|
+
nextProps: T
|
|
8
|
+
) {
|
|
9
|
+
const keys = new Set([...Object.keys(prevProps), ...Object.keys(nextProps)])
|
|
10
|
+
for (const key of keys) {
|
|
11
|
+
if (prevProps[key] !== nextProps[key]) {
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type MemoFn = Function & {
|
|
19
|
+
[$MEMO]: {
|
|
20
|
+
arePropsEqual: (
|
|
21
|
+
prevProps: Record<string, unknown>,
|
|
22
|
+
nextProps: Record<string, unknown>
|
|
23
|
+
) => boolean
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function memo<T extends Record<string, unknown> = {}>(
|
|
28
|
+
fn: Kaioken.FC<T>,
|
|
29
|
+
arePropsEqual: (prevProps: T, nextProps: T) => boolean = _arePropsEqual
|
|
30
|
+
): (props: T) => JSX.Element {
|
|
31
|
+
return Object.assign(
|
|
32
|
+
function Memo(props: T) {
|
|
33
|
+
return createElement(fn, props)
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
[$MEMO]: { arePropsEqual },
|
|
37
|
+
displayName: "Kaioken.memo",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isMemoFn(fn: any): fn is MemoFn {
|
|
43
|
+
return (
|
|
44
|
+
typeof fn === "function" &&
|
|
45
|
+
fn[$MEMO] &&
|
|
46
|
+
typeof fn[$MEMO].arePropsEqual === "function"
|
|
47
|
+
)
|
|
48
|
+
}
|