kiru 1.4.0 → 1.5.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 (110) hide show
  1. package/dist/components/derive.d.ts +1 -1
  2. package/dist/components/derive.d.ts.map +1 -1
  3. package/dist/constants.d.ts +2 -1
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/constants.js +2 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/devtools.d.ts +1 -1
  8. package/dist/devtools.d.ts.map +1 -1
  9. package/dist/dom/nodes.d.ts +1 -1
  10. package/dist/dom/nodes.d.ts.map +1 -1
  11. package/dist/dom/nodes.js.map +1 -1
  12. package/dist/dom/props.js.map +1 -1
  13. package/dist/globalContext.d.ts +2 -2
  14. package/dist/globalContext.d.ts.map +1 -1
  15. package/dist/headlessRender.d.ts.map +1 -1
  16. package/dist/headlessRender.js +3 -0
  17. package/dist/headlessRender.js.map +1 -1
  18. package/dist/hooks/onCleanup.d.ts.map +1 -1
  19. package/dist/hooks/onCleanup.js +3 -1
  20. package/dist/hooks/onCleanup.js.map +1 -1
  21. package/dist/hooks/setup.d.ts.map +1 -1
  22. package/dist/hooks/setup.js +108 -63
  23. package/dist/hooks/setup.js.map +1 -1
  24. package/dist/hooks/utils.d.ts.map +1 -1
  25. package/dist/hooks/utils.js +2 -1
  26. package/dist/hooks/utils.js.map +1 -1
  27. package/dist/hydration.d.ts +1 -1
  28. package/dist/hydration.d.ts.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/profiling.d.ts +1 -1
  32. package/dist/profiling.d.ts.map +1 -1
  33. package/dist/reconciler.d.ts.map +1 -1
  34. package/dist/reconciler.js +38 -6
  35. package/dist/reconciler.js.map +1 -1
  36. package/dist/resource.d.ts +11 -5
  37. package/dist/resource.d.ts.map +1 -1
  38. package/dist/resource.js +83 -39
  39. package/dist/resource.js.map +1 -1
  40. package/dist/router/client/index.d.ts +1 -1
  41. package/dist/router/client/index.d.ts.map +1 -1
  42. package/dist/router/globals.d.ts +2 -2
  43. package/dist/router/globals.d.ts.map +1 -1
  44. package/dist/router/link.d.ts +1 -1
  45. package/dist/router/link.d.ts.map +1 -1
  46. package/dist/router/pageConfig.d.ts +1 -1
  47. package/dist/router/pageConfig.d.ts.map +1 -1
  48. package/dist/router/types.d.ts +3 -3
  49. package/dist/router/types.d.ts.map +1 -1
  50. package/dist/router/types.internal.d.ts +2 -2
  51. package/dist/router/types.internal.d.ts.map +1 -1
  52. package/dist/router/utils/index.d.ts +2 -2
  53. package/dist/router/utils/index.d.ts.map +1 -1
  54. package/dist/scheduler.d.ts.map +1 -1
  55. package/dist/scheduler.js +15 -2
  56. package/dist/scheduler.js.map +1 -1
  57. package/dist/signals/base.d.ts +1 -0
  58. package/dist/signals/base.d.ts.map +1 -1
  59. package/dist/signals/base.js +14 -2
  60. package/dist/signals/base.js.map +1 -1
  61. package/dist/signals/effect.d.ts.map +1 -1
  62. package/dist/signals/effect.js +8 -0
  63. package/dist/signals/effect.js.map +1 -1
  64. package/dist/signals/tracking.d.ts.map +1 -1
  65. package/dist/signals/tracking.js +13 -14
  66. package/dist/signals/tracking.js.map +1 -1
  67. package/dist/ssr/client.d.ts +1 -1
  68. package/dist/ssr/client.d.ts.map +1 -1
  69. package/dist/types.d.ts +31 -7
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.dom.d.ts +190 -2
  72. package/dist/types.dom.d.ts.map +1 -1
  73. package/dist/types.utils.d.ts +28 -21
  74. package/dist/types.utils.d.ts.map +1 -1
  75. package/dist/utils/vdom.d.ts.map +1 -1
  76. package/dist/utils/vdom.js +5 -2
  77. package/dist/utils/vdom.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/components/derive.ts +1 -1
  80. package/src/constants.ts +2 -0
  81. package/src/devtools.ts +1 -1
  82. package/src/dom/commit.ts +1 -1
  83. package/src/dom/nodes.ts +8 -3
  84. package/src/dom/props.ts +6 -6
  85. package/src/globalContext.ts +2 -2
  86. package/src/headlessRender.ts +4 -1
  87. package/src/hooks/onCleanup.ts +3 -1
  88. package/src/hooks/setup.ts +133 -80
  89. package/src/hooks/utils.ts +2 -1
  90. package/src/hydration.ts +1 -1
  91. package/src/index.ts +1 -1
  92. package/src/profiling.ts +1 -1
  93. package/src/reconciler.ts +51 -9
  94. package/src/resource.ts +112 -44
  95. package/src/router/client/index.ts +2 -2
  96. package/src/router/globals.ts +2 -2
  97. package/src/router/link.ts +1 -1
  98. package/src/router/pageConfig.ts +1 -1
  99. package/src/router/types.internal.ts +2 -2
  100. package/src/router/types.ts +3 -3
  101. package/src/router/utils/index.ts +1 -1
  102. package/src/scheduler.ts +20 -3
  103. package/src/signals/base.ts +20 -2
  104. package/src/signals/effect.ts +8 -0
  105. package/src/signals/tracking.ts +17 -15
  106. package/src/ssr/client.ts +1 -1
  107. package/src/types.dom.ts +270 -53
  108. package/src/types.ts +36 -32
  109. package/src/types.utils.ts +56 -22
  110. package/src/utils/vdom.ts +7 -1
package/src/dom/props.ts CHANGED
@@ -733,8 +733,8 @@ function setStyleProp(
733
733
  // Avoid Set allocation for the common case where prevStyle is empty
734
734
  if (prevKeys.length === 0) {
735
735
  for (let i = 0; i < nextKeys.length; i++) {
736
- const k = nextKeys[i] as keyof StyleObject
737
- const rawNext = nextStyle[k]
736
+ const k = nextKeys[i]
737
+ const rawNext = nextStyle[k as keyof StyleObject]
738
738
  const nextVal = unwrap(rawNext)
739
739
  if (trackSignals && Signal.isSignal(rawNext)) {
740
740
  styleKeyToSignal.set(k, rawNext)
@@ -751,7 +751,7 @@ function setStyleProp(
751
751
  // Full merge path: iterate prevKeys for removals, nextKeys for additions/changes
752
752
  const nextStyleKeys = new Set(nextKeys)
753
753
  for (let i = 0; i < prevKeys.length; i++) {
754
- const k = prevKeys[i] as keyof StyleObject
754
+ const k = prevKeys[i]
755
755
  if (!nextStyleKeys.has(k)) {
756
756
  // Property was removed
757
757
  if ((k as string).startsWith("--")) {
@@ -763,9 +763,9 @@ function setStyleProp(
763
763
  }
764
764
 
765
765
  for (let i = 0; i < nextKeys.length; i++) {
766
- const k = nextKeys[i] as keyof StyleObject
767
- const rawNext = nextStyle[k]
768
- const prevVal = unwrap(prevStyle[k])
766
+ const k = nextKeys[i]
767
+ const rawNext = nextStyle[k as keyof StyleObject]
768
+ const prevVal = unwrap(prevStyle[k as keyof StyleObject])
769
769
  const nextVal = unwrap(rawNext)
770
770
  if (trackSignals && Signal.isSignal(rawNext)) {
771
771
  styleKeyToSignal.set(k, rawNext)
@@ -2,8 +2,8 @@ import { __DEV__ } from "./env.js"
2
2
  import { createHmrContext } from "./hmr.js"
3
3
  import { createProfilingContext } from "./profiling.js"
4
4
  import { fileRouterInstance } from "./router/globals.js"
5
- import type { FileRouterController } from "./router/fileRouterController"
6
- import type { AppHandle } from "./appHandle"
5
+ import type { FileRouterController } from "./router/fileRouterController.js"
6
+ import type { AppHandle } from "./appHandle.js"
7
7
 
8
8
  export { createKiruGlobalContext, type GlobalKiruEvent, type KiruGlobalContext }
9
9
 
@@ -12,7 +12,7 @@ import {
12
12
  import { Signal } from "./signals/base.js"
13
13
  import { $ERROR_BOUNDARY, voidElements, $STREAM_DATA } from "./constants.js"
14
14
  import { __DEV__ } from "./env.js"
15
- import type { ErrorBoundaryNode } from "./types.utils"
15
+ import type { ErrorBoundaryNode } from "./types.utils.js"
16
16
 
17
17
  export interface HeadlessRenderContext {
18
18
  write(chunk: string): void
@@ -37,6 +37,9 @@ export function headlessRender(
37
37
  if (el instanceof Array) {
38
38
  return el.forEach((c, i) => headlessRender(ctx, c, parent, i))
39
39
  }
40
+ if (typeof el === "function") {
41
+ return headlessRender(ctx, el(), parent, idx)
42
+ }
40
43
  if (Signal.isSignal(el)) {
41
44
  const value = el.peek()
42
45
  if (!isPrimitiveChild(value)) {
@@ -1,3 +1,5 @@
1
+ import { $INLINE_FN } from "../constants.js"
2
+ import { __DEV__ } from "../env.js"
1
3
  import { node } from "../globals.js"
2
4
  import {
3
5
  generateRandomID,
@@ -14,7 +16,7 @@ import {
14
16
  export function onCleanup(fn: () => void): void {
15
17
  if (!sideEffectsEnabled()) return
16
18
  const vNode = node.current!
17
- if (!vNode) {
19
+ if (!vNode || (__DEV__ && vNode.type === $INLINE_FN)) {
18
20
  throw new Error("Cannot queue onCleanup effect outside of a component")
19
21
  }
20
22
  registerVNodeCleanup(vNode, generateRandomID(10), fn)
@@ -1,18 +1,20 @@
1
1
  import { signal, Signal } from "../signals/base.js"
2
- import { createVNodeId } from "../utils/vdom.js"
2
+ import { createVNodeId, isVNodeDeleted } from "../utils/vdom.js"
3
+ import { $INLINE_FN } from "../constants.js"
3
4
  import { __DEV__ } from "../env.js"
4
5
  import { node, setups } from "../globals.js"
5
- import {
6
- tracking,
7
- type TrackingStackObservations,
8
- } from "../signals/tracking.js"
6
+ import { executeWithTracking } from "../signals/tracking.js"
9
7
  import { registerVNodeCleanup } from "../utils/index.js"
10
8
 
9
+ let currentAccessedPaths: Set<string[]> | null = null
10
+ const OWN_KEYS = `__KEYS__`
11
+
11
12
  export interface Setup<Props extends {}> {
12
13
  readonly derive: <T>(
13
14
  selector: (props: Props extends Kiru.FC<infer P> ? P : Props) => T
14
15
  ) => Signal<T>
15
16
  readonly id: Signal<string>
17
+ // Not reactive — for use in render functions only
16
18
  readonly props: Readonly<Props extends Kiru.FC<infer P> ? P : Props>
17
19
  }
18
20
 
@@ -25,7 +27,7 @@ export interface Setup<Props extends {}> {
25
27
  export function setup<Props extends {}>(): Setup<Props> {
26
28
  const vNode = node.current!
27
29
  if (__DEV__) {
28
- if (!vNode) {
30
+ if (!vNode || vNode.type === $INLINE_FN) {
29
31
  throw new Error("setup() must be called inside a Kiru component")
30
32
  }
31
33
  if (vNode.render) {
@@ -41,8 +43,10 @@ export function setup<Props extends {}>(): Setup<Props> {
41
43
  setups.set(vNode, setup)
42
44
  return setup
43
45
  }
46
+
44
47
  function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
45
48
  let id: Signal<string>
49
+ let propsProxy: InferredProps
46
50
 
47
51
  type InferredProps = Props extends Kiru.FC<infer R> ? R : Props
48
52
  const propSyncs = (vNode.propSyncs = []) as ((props: InferredProps) => void)[]
@@ -53,7 +57,7 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
53
57
  const currentProps = { current: { ...vNode.props } as InferredProps }
54
58
  const deriveCleanups: Array<() => void> = []
55
59
 
56
- type DeriveEntry = { run: () => void; accessedPaths: Set<string> }
60
+ type DeriveEntry = { run: () => void; paths: Set<string[]> }
57
61
  const deriveEntries: DeriveEntry[] = []
58
62
 
59
63
  propSyncs.push((p) => {
@@ -61,12 +65,8 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
61
65
  const skip = new Set<DeriveEntry>()
62
66
  for (const entry of deriveEntries) {
63
67
  if (
64
- entry.accessedPaths.size > 0 &&
65
- propsUnchangedAtPaths(
66
- old,
67
- p as Record<string, unknown>,
68
- entry.accessedPaths
69
- )
68
+ entry.paths.size > 0 &&
69
+ propsUnchangedAtPaths(old, p as Record<string, unknown>, entry.paths)
70
70
  ) {
71
71
  skip.add(entry)
72
72
  }
@@ -88,41 +88,27 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
88
88
  ) {
89
89
  const resultSig = signal(undefined!) as Signal<T>
90
90
  const unsubs = new Map<string, () => void>()
91
- const accessedPaths = new Set<string>()
91
+ const accessedPaths = new Set<string[]>()
92
92
 
93
93
  function sync() {
94
94
  accessedPaths.clear()
95
- const propsProxy = createPropsProxy(
96
- currentProps.current as Record<string, unknown>,
97
- accessedPaths
95
+ currentAccessedPaths = accessedPaths
96
+
97
+ const propsProxy = createProxy(
98
+ currentProps.current as Record<string, unknown>
98
99
  ) as InferredProps
99
- const observations: TrackingStackObservations = new Map()
100
- tracking.stack.push(observations)
101
- const value = selector(propsProxy)
102
- tracking.stack.pop()
103
- // Always assign and notify so the component re-renders when the derived value changes
104
- // (e.g. when parent passes a different signal ref like toggle switching count/double).
105
- resultSig.value = value
106
-
107
- for (const [sid, unsub] of unsubs) {
108
- if (!observations.has(sid)) {
109
- unsub()
110
- unsubs.delete(sid)
111
- }
112
- }
113
- for (const [sid, observedSig] of observations) {
114
- if (!unsubs.has(sid)) {
115
- try {
116
- unsubs.set(sid, observedSig.subscribe(sync))
117
- } catch {
118
- // Signal may be disposed after HMR; skip subscribing
119
- }
120
- }
121
- }
100
+
101
+ resultSig.value = executeWithTracking({
102
+ id: Signal.id(resultSig),
103
+ fn: () => selector(propsProxy),
104
+ onDepChanged: sync,
105
+ subs: unsubs,
106
+ })
107
+ currentAccessedPaths = null
122
108
  }
123
109
 
124
110
  sync()
125
- const entry: DeriveEntry = { run: sync, accessedPaths }
111
+ const entry: DeriveEntry = { run: sync, paths: accessedPaths }
126
112
  deriveEntries.push(entry)
127
113
  deriveCleanups.push(() => {
128
114
  unsubs.forEach((u) => u())
@@ -136,6 +122,16 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
136
122
  get id() {
137
123
  if (!id) {
138
124
  id = signal(createVNodeId(vNode))
125
+ if (isVNodeDeleted(vNode)) {
126
+ return id
127
+ }
128
+ if (node.current !== vNode) {
129
+ registerVNodeCleanup(
130
+ vNode,
131
+ Signal.id(id),
132
+ Signal.dispose.bind(null, id)
133
+ )
134
+ }
139
135
  prevIndex = vNode.index
140
136
  propSyncs.push(() => {
141
137
  if (prevIndex !== vNode.index) {
@@ -148,61 +144,118 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
148
144
  return id
149
145
  },
150
146
  get props() {
151
- return currentProps.current
147
+ return (propsProxy ??= new Proxy(
148
+ {},
149
+ {
150
+ get(_, key) {
151
+ if (typeof key === "symbol")
152
+ return Reflect.get(currentProps.current as any, key)
153
+ const v = (currentProps.current as any)[key]
154
+ if (v !== null && typeof v === "object" && !Signal.isSignal(v)) {
155
+ return createProxy(v)
156
+ }
157
+ return v
158
+ },
159
+ }
160
+ ) as InferredProps)
152
161
  },
153
162
  }
154
163
  return setupResult
155
164
  }
165
+
156
166
  function propsUnchangedAtPaths(
157
167
  oldProps: Record<string, unknown>,
158
168
  newProps: Record<string, unknown>,
159
- paths: Set<string>
169
+ accessedPaths: Set<string[]>
160
170
  ): boolean {
161
- for (const path of paths) {
162
- if (!Object.is(getAtPath(oldProps, path), getAtPath(newProps, path))) {
163
- return false
171
+ outer: for (const path of accessedPaths) {
172
+ let a: unknown = oldProps
173
+ let b: unknown = newProps
174
+
175
+ for (let i = 0; i < path.length; i++) {
176
+ const key = path[i]
177
+
178
+ // Sentinel: caller iterated keys of the object at this path —
179
+ // re-run if the key sets differ
180
+ if (key === OWN_KEYS) {
181
+ if (a === b) continue outer
182
+ if (
183
+ a == null ||
184
+ b == null ||
185
+ typeof a !== "object" ||
186
+ typeof b !== "object"
187
+ ) {
188
+ return false
189
+ }
190
+ const aKeys = Object.keys(a)
191
+ const bKeys = Object.keys(b)
192
+ if (aKeys.length !== bKeys.length) return false
193
+ for (const k of aKeys) {
194
+ if (!(k in (b as object))) return false
195
+ }
196
+ continue outer
197
+ }
198
+
199
+ if (a === b) continue outer
200
+ if (
201
+ a == null ||
202
+ b == null ||
203
+ typeof a !== "object" ||
204
+ typeof b !== "object"
205
+ ) {
206
+ if (!Object.is(a, b)) return false
207
+ continue outer
208
+ }
209
+ a = (a as Record<string, unknown>)[key]
210
+ b = (b as Record<string, unknown>)[key]
164
211
  }
212
+
213
+ if (!Object.is(a, b)) return false
165
214
  }
215
+
166
216
  return true
167
217
  }
168
218
 
169
- function getAtPath(obj: Record<string, unknown>, path: string): unknown {
170
- let cur: unknown = obj
171
- for (const key of path.split(".")) {
172
- if (cur == null || typeof cur !== "object") return undefined
173
- cur = (cur as Record<string, unknown>)[key]
174
- }
175
- return cur
176
- }
219
+ const proxyCache = new WeakMap<object, any>()
177
220
 
178
221
  /**
179
- * Proxy that records paths and wraps signals. We only add to accessedPaths when
180
- * we hit a signal (the leaf we subscribe to), so propSync skip only compares
181
- * signal refs. Container objects (e.g. "data") are new every render and would
182
- * always fail the skip.
222
+ * Proxy that records accessed paths and primitives into the current
223
+ * tracking context (currentAccessedPaths).
183
224
  */
184
- function createPropsProxy<P extends Record<string, unknown>>(
185
- props: P,
186
- accessedPaths: Set<string>,
187
- pathPrefix?: string
225
+ function createProxy<P extends Record<string, unknown>>(
226
+ source: P,
227
+ path: string[] = []
188
228
  ): P {
189
- return new Proxy(props, {
190
- get(holder, key: string) {
191
- const path = pathPrefix ? `${pathPrefix}.${key}` : key
192
- const v = holder[key]
193
- if (Signal.isSignal(v)) {
194
- accessedPaths.add(path) // only record path for signal leaves
229
+ let cached = proxyCache.get(source)
230
+
231
+ if (!cached) {
232
+ cached = new Proxy(source, {
233
+ get(holder, key: string | symbol) {
234
+ if (typeof key === "symbol") return Reflect.get(holder, key)
235
+
236
+ const keyPath = [...path, key as string]
237
+ const v = holder[key as string]
238
+
239
+ if (v !== null && typeof v === "object" && !Signal.isSignal(v)) {
240
+ return createProxy(v as Record<string, unknown>, keyPath)
241
+ }
242
+
243
+ currentAccessedPaths?.add(keyPath)
195
244
  return v
196
- }
197
- if (v !== null && typeof v === "object" && !Array.isArray(v)) {
198
- return createPropsProxy(
199
- v as Record<string, unknown>,
200
- accessedPaths,
201
- path
202
- ) as P[keyof P]
203
- }
204
- accessedPaths.add(path) // primitive leaf
205
- return v
206
- },
207
- }) as P
245
+ },
246
+ has(holder, key: string | symbol) {
247
+ if (typeof key === "symbol") return Reflect.has(holder, key)
248
+ currentAccessedPaths?.add([...path, key as string])
249
+ return key in holder
250
+ },
251
+ ownKeys(holder) {
252
+ currentAccessedPaths?.add([...path, OWN_KEYS])
253
+ return Reflect.ownKeys(holder)
254
+ },
255
+ })
256
+
257
+ proxyCache.set(source, cached)
258
+ }
259
+
260
+ return cached
208
261
  }
@@ -1,10 +1,11 @@
1
+ import { $INLINE_FN } from "../constants.js"
1
2
  import { node } from "../globals.js"
2
3
 
3
4
  export function getVNodeLifecycleHooks(): null | NonNullable<
4
5
  Kiru.VNode["hooks"]
5
6
  > {
6
7
  const vNode = node.current!
7
- if (!vNode) return null
8
+ if (!vNode || vNode.type === $INLINE_FN) return null
8
9
 
9
10
  return (vNode.hooks ??= {
10
11
  pre: [],
package/src/hydration.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MaybeDom, SomeDom } from "./types.utils"
1
+ import type { MaybeDom, SomeDom } from "./types.utils.js"
2
2
 
3
3
  const parents: SomeDom[] = []
4
4
  const childIdx: number[] = []
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createKiruGlobalContext } from "./globalContext.js"
2
2
  import { isBrowser } from "./env.js"
3
3
 
4
- export type * from "./types"
4
+ export type * from "./types.js"
5
5
  export * from "./signals/index.js"
6
6
  export * from "./action.js"
7
7
  export * from "./appHandle.js"
package/src/profiling.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AppHandle } from "./appHandle"
1
+ import type { AppHandle } from "./appHandle.js"
2
2
 
3
3
  const MAX_TICKS = 100
4
4
 
package/src/reconciler.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { $FRAGMENT, FLAG_PLACEMENT, FLAG_UPDATE } from "./constants.js"
1
+ import {
2
+ $FRAGMENT,
3
+ $INLINE_FN,
4
+ FLAG_PLACEMENT,
5
+ FLAG_UPDATE,
6
+ } from "./constants.js"
2
7
  import {
3
8
  getVNodeApp,
4
9
  isElement,
@@ -189,6 +194,10 @@ function updateSlot(
189
194
  }
190
195
  return updateFragment(parent, oldChild, child)
191
196
  }
197
+ if (typeof child === "function") {
198
+ if (key !== null) return null
199
+ return updateInlineFnChild(parent, oldChild, child)
200
+ }
192
201
  return null
193
202
  }
194
203
 
@@ -263,6 +272,23 @@ function updateFragment(
263
272
  return oldChild
264
273
  }
265
274
 
275
+ function updateInlineFnChild(
276
+ parent: VNode,
277
+ oldChild: VNode | null,
278
+ expr: Function
279
+ ) {
280
+ if (oldChild === null || oldChild.type !== $INLINE_FN) {
281
+ return createVNode(parent, $INLINE_FN, { expr })
282
+ }
283
+ if (__DEV__) {
284
+ dev_emitUpdateNode()
285
+ }
286
+ oldChild.props = { expr }
287
+ oldChild.flags |= FLAG_UPDATE
288
+ oldChild.sibling = null
289
+ return oldChild
290
+ }
291
+
266
292
  function createChild(parent: VNode, child: unknown): VNode | null {
267
293
  if (isValidTextChild(child)) {
268
294
  return createVNode(parent, "#text", { nodeValue: "" + child })
@@ -283,6 +309,10 @@ function createChild(parent: VNode, child: unknown): VNode | null {
283
309
  return createVNode(parent, $FRAGMENT, { children: child })
284
310
  }
285
311
 
312
+ if (typeof child === "function") {
313
+ return createVNode(parent, $INLINE_FN, { expr: child })
314
+ }
315
+
286
316
  return null
287
317
  }
288
318
 
@@ -316,14 +346,11 @@ function updateFromMap(
316
346
  const isSig = Signal.isSignal(child)
317
347
  if (isSig || isValidTextChild(child)) {
318
348
  const oldChild = existingChildren.get(index)
319
- if (oldChild) {
349
+ if (oldChild?.type === "#text") {
320
350
  if (oldChild.props.nodeValue === child) {
321
351
  return oldChild
322
352
  }
323
- if (
324
- oldChild.type === "#text" &&
325
- Signal.isSignal(oldChild.props.nodeValue)
326
- ) {
353
+ if (Signal.isSignal(oldChild.props.nodeValue)) {
327
354
  oldChild.cleanups?.["nodeValue"]?.()
328
355
  }
329
356
  }
@@ -356,11 +383,11 @@ function updateFromMap(
356
383
 
357
384
  if (Array.isArray(child)) {
358
385
  const props = { children: child }
359
- const oldChild = existingChildren.get(index)
360
386
  if (__DEV__) {
361
387
  markListChild(child)
362
388
  }
363
- if (oldChild) {
389
+ const oldChild = existingChildren.get(index)
390
+ if (oldChild?.type === $FRAGMENT) {
364
391
  if (__DEV__) {
365
392
  dev_emitUpdateNode()
366
393
  }
@@ -372,6 +399,21 @@ function updateFromMap(
372
399
  return createVNode(parent, $FRAGMENT, props, null, index)
373
400
  }
374
401
 
402
+ if (typeof child === "function") {
403
+ const props = { expr: child }
404
+ const oldChild = existingChildren.get(index)
405
+ if (oldChild?.type === $INLINE_FN) {
406
+ if (__DEV__) {
407
+ dev_emitUpdateNode()
408
+ }
409
+ oldChild.flags |= FLAG_UPDATE
410
+ oldChild.props = props
411
+ return oldChild
412
+ }
413
+
414
+ return createVNode(parent, $INLINE_FN, props, null, index)
415
+ }
416
+
375
417
  return null
376
418
  }
377
419
 
@@ -472,7 +514,7 @@ function getNearestParentFcTag(vNode: VNode) {
472
514
  function createVNode(
473
515
  parent: VNode,
474
516
  type: VNode["type"],
475
- props: VNode["props"],
517
+ props?: VNode["props"],
476
518
  key: VNode["key"] = null,
477
519
  index = 0
478
520
  ): VNode {