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.
Files changed (70) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +5 -0
  3. package/package.json +81 -0
  4. package/src/appContext.ts +186 -0
  5. package/src/cloneVNode.ts +14 -0
  6. package/src/constants.ts +146 -0
  7. package/src/context.ts +56 -0
  8. package/src/dom.ts +712 -0
  9. package/src/element.ts +54 -0
  10. package/src/env.ts +6 -0
  11. package/src/error.ts +85 -0
  12. package/src/flags.ts +15 -0
  13. package/src/form/index.ts +662 -0
  14. package/src/form/types.ts +261 -0
  15. package/src/form/utils.ts +19 -0
  16. package/src/generateId.ts +19 -0
  17. package/src/globalContext.ts +161 -0
  18. package/src/globals.ts +21 -0
  19. package/src/hmr.ts +178 -0
  20. package/src/hooks/index.ts +14 -0
  21. package/src/hooks/useAsync.ts +136 -0
  22. package/src/hooks/useCallback.ts +31 -0
  23. package/src/hooks/useContext.ts +79 -0
  24. package/src/hooks/useEffect.ts +44 -0
  25. package/src/hooks/useEffectEvent.ts +24 -0
  26. package/src/hooks/useId.ts +42 -0
  27. package/src/hooks/useLayoutEffect.ts +47 -0
  28. package/src/hooks/useMemo.ts +33 -0
  29. package/src/hooks/useReducer.ts +50 -0
  30. package/src/hooks/useRef.ts +40 -0
  31. package/src/hooks/useState.ts +62 -0
  32. package/src/hooks/useSyncExternalStore.ts +59 -0
  33. package/src/hooks/useViewTransition.ts +26 -0
  34. package/src/hooks/utils.ts +259 -0
  35. package/src/hydration.ts +67 -0
  36. package/src/index.ts +61 -0
  37. package/src/jsx.ts +11 -0
  38. package/src/lazy.ts +238 -0
  39. package/src/memo.ts +48 -0
  40. package/src/portal.ts +43 -0
  41. package/src/profiling.ts +105 -0
  42. package/src/props.ts +36 -0
  43. package/src/reconciler.ts +531 -0
  44. package/src/renderToString.ts +91 -0
  45. package/src/router/index.ts +2 -0
  46. package/src/router/route.ts +51 -0
  47. package/src/router/router.ts +275 -0
  48. package/src/router/routerUtils.ts +49 -0
  49. package/src/scheduler.ts +522 -0
  50. package/src/signals/base.ts +237 -0
  51. package/src/signals/computed.ts +139 -0
  52. package/src/signals/effect.ts +60 -0
  53. package/src/signals/globals.ts +11 -0
  54. package/src/signals/index.ts +12 -0
  55. package/src/signals/jsx.ts +45 -0
  56. package/src/signals/types.ts +10 -0
  57. package/src/signals/utils.ts +12 -0
  58. package/src/signals/watch.ts +151 -0
  59. package/src/ssr/client.ts +29 -0
  60. package/src/ssr/hydrationBoundary.ts +63 -0
  61. package/src/ssr/index.ts +1 -0
  62. package/src/ssr/server.ts +124 -0
  63. package/src/store.ts +241 -0
  64. package/src/swr.ts +360 -0
  65. package/src/transition.ts +80 -0
  66. package/src/types.dom.ts +1250 -0
  67. package/src/types.ts +209 -0
  68. package/src/types.utils.ts +39 -0
  69. package/src/utils.ts +581 -0
  70. 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
+ }
@@ -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
+ }