kiru 1.4.1 → 1.5.1

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 (115) 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 +11 -28
  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 +90 -40
  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/fileRouterController.d.ts.map +1 -1
  43. package/dist/router/fileRouterController.js +1 -1
  44. package/dist/router/fileRouterController.js.map +1 -1
  45. package/dist/router/globals.d.ts +2 -2
  46. package/dist/router/globals.d.ts.map +1 -1
  47. package/dist/router/link.d.ts +1 -1
  48. package/dist/router/link.d.ts.map +1 -1
  49. package/dist/router/pageConfig.d.ts +1 -1
  50. package/dist/router/pageConfig.d.ts.map +1 -1
  51. package/dist/router/types.d.ts +3 -3
  52. package/dist/router/types.d.ts.map +1 -1
  53. package/dist/router/types.internal.d.ts +2 -2
  54. package/dist/router/types.internal.d.ts.map +1 -1
  55. package/dist/router/utils/index.d.ts +2 -2
  56. package/dist/router/utils/index.d.ts.map +1 -1
  57. package/dist/scheduler.d.ts.map +1 -1
  58. package/dist/scheduler.js +15 -2
  59. package/dist/scheduler.js.map +1 -1
  60. package/dist/signals/base.d.ts +1 -0
  61. package/dist/signals/base.d.ts.map +1 -1
  62. package/dist/signals/base.js +14 -2
  63. package/dist/signals/base.js.map +1 -1
  64. package/dist/signals/effect.d.ts.map +1 -1
  65. package/dist/signals/effect.js +8 -0
  66. package/dist/signals/effect.js.map +1 -1
  67. package/dist/signals/tracking.d.ts +1 -1
  68. package/dist/signals/tracking.d.ts.map +1 -1
  69. package/dist/signals/tracking.js +14 -15
  70. package/dist/signals/tracking.js.map +1 -1
  71. package/dist/ssr/client.d.ts +1 -1
  72. package/dist/ssr/client.d.ts.map +1 -1
  73. package/dist/types.d.ts +31 -7
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.dom.d.ts +190 -2
  76. package/dist/types.dom.d.ts.map +1 -1
  77. package/dist/types.utils.d.ts +28 -21
  78. package/dist/types.utils.d.ts.map +1 -1
  79. package/dist/utils/vdom.d.ts.map +1 -1
  80. package/dist/utils/vdom.js +5 -2
  81. package/dist/utils/vdom.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/components/derive.ts +1 -1
  84. package/src/constants.ts +2 -0
  85. package/src/devtools.ts +1 -1
  86. package/src/dom/commit.ts +1 -1
  87. package/src/dom/nodes.ts +8 -3
  88. package/src/dom/props.ts +6 -6
  89. package/src/globalContext.ts +2 -2
  90. package/src/headlessRender.ts +4 -1
  91. package/src/hooks/onCleanup.ts +3 -1
  92. package/src/hooks/setup.ts +17 -31
  93. package/src/hooks/utils.ts +2 -1
  94. package/src/hydration.ts +1 -1
  95. package/src/index.ts +1 -1
  96. package/src/profiling.ts +1 -1
  97. package/src/reconciler.ts +51 -9
  98. package/src/resource.ts +119 -45
  99. package/src/router/client/index.ts +2 -2
  100. package/src/router/fileRouterController.ts +5 -5
  101. package/src/router/globals.ts +2 -2
  102. package/src/router/link.ts +1 -1
  103. package/src/router/pageConfig.ts +1 -1
  104. package/src/router/types.internal.ts +2 -2
  105. package/src/router/types.ts +3 -3
  106. package/src/router/utils/index.ts +1 -1
  107. package/src/scheduler.ts +20 -3
  108. package/src/signals/base.ts +20 -2
  109. package/src/signals/effect.ts +8 -0
  110. package/src/signals/tracking.ts +18 -16
  111. package/src/ssr/client.ts +1 -1
  112. package/src/types.dom.ts +270 -53
  113. package/src/types.ts +36 -32
  114. package/src/types.utils.ts +56 -22
  115. 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,11 +1,9 @@
1
1
  import { signal, Signal } from "../signals/base.js"
2
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
 
11
9
  let currentAccessedPaths: Set<string[]> | null = null
@@ -29,7 +27,7 @@ export interface Setup<Props extends {}> {
29
27
  export function setup<Props extends {}>(): Setup<Props> {
30
28
  const vNode = node.current!
31
29
  if (__DEV__) {
32
- if (!vNode) {
30
+ if (!vNode || vNode.type === $INLINE_FN) {
33
31
  throw new Error("setup() must be called inside a Kiru component")
34
32
  }
35
33
  if (vNode.render) {
@@ -94,34 +92,19 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
94
92
 
95
93
  function sync() {
96
94
  accessedPaths.clear()
95
+ currentAccessedPaths = accessedPaths
96
+
97
97
  const propsProxy = createProxy(
98
98
  currentProps.current as Record<string, unknown>
99
99
  ) as InferredProps
100
- const observations: TrackingStackObservations = new Map()
101
- tracking.stack.push(observations)
102
- currentAccessedPaths = accessedPaths
103
- const value = selector(propsProxy)
100
+
101
+ resultSig.value = executeWithTracking({
102
+ id: Signal.id(resultSig),
103
+ fn: () => selector(propsProxy),
104
+ onDepChanged: sync,
105
+ subs: unsubs,
106
+ })
104
107
  currentAccessedPaths = null
105
- tracking.stack.pop()
106
- // Always assign and notify so the component re-renders when the derived value changes
107
- // (e.g. when parent passes a different signal ref like toggle switching count/double).
108
- resultSig.value = value
109
-
110
- for (const [sid, unsub] of unsubs) {
111
- if (!observations.has(sid)) {
112
- unsub()
113
- unsubs.delete(sid)
114
- }
115
- }
116
- for (const [sid, observedSig] of observations) {
117
- if (!unsubs.has(sid)) {
118
- try {
119
- unsubs.set(sid, observedSig.subscribe(sync))
120
- } catch {
121
- // Signal may be disposed after HMR; skip subscribing
122
- }
123
- }
124
- }
125
108
  }
126
109
 
127
110
  sync()
@@ -143,8 +126,11 @@ function createSetup<Props extends {}>(vNode: Kiru.VNode): Setup<Props> {
143
126
  return id
144
127
  }
145
128
  if (node.current !== vNode) {
146
- // @ts-expect-error
147
- registerVNodeCleanup(vNode, id.$id, Signal.dispose.bind(null, id))
129
+ registerVNodeCleanup(
130
+ vNode,
131
+ Signal.id(id),
132
+ Signal.dispose.bind(null, id)
133
+ )
148
134
  }
149
135
  prevIndex = vNode.index
150
136
  propSyncs.push(() => {
@@ -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 {
package/src/resource.ts CHANGED
@@ -1,23 +1,20 @@
1
1
  import { $HMR_ACCEPT, STREAMED_DATA_EVENT } from "./constants.js"
2
2
  import { hydrationMode, node, renderMode } from "./globals.js"
3
3
  import { Signal, signal } from "./signals/base.js"
4
+ import { executeWithTracking } from "./signals/tracking.js"
4
5
  import { createVNodeId, registerVNodeCleanup } from "./utils/vdom.js"
5
6
  import { generateRandomID } from "./utils/generateId.js"
6
7
  import { __DEV__, isBrowser } from "./env.js"
7
8
  import { GenericHMRAcceptor, performHmrAccept } from "./hmr.js"
8
9
 
9
- /**
10
- * Returns true if the value is a {@link Resource}
11
- */
12
- export function isResource(thing: unknown): thing is Resource<unknown> {
13
- return (
14
- Signal.isSignal(thing) &&
15
- "promise" in thing &&
16
- thing["promise"] instanceof Promise
17
- )
18
- }
10
+ export type ResourceSource = Record<string, Signal<unknown>> | Signal<unknown>
19
11
 
20
- const resourceMeta = new WeakMap<Kiru.VNode, { id: string; index: number }>()
12
+ type InnerOf<T> = T extends Kiru.Signal<infer V> ? V : never
13
+
14
+ type UnwrapResourceSource<T extends ResourceSource> =
15
+ T extends Kiru.Signal<unknown>
16
+ ? InnerOf<T>
17
+ : { [K in keyof T]: InnerOf<T[K]> }
21
18
 
22
19
  interface ResourceState<T> {
23
20
  error: Signal<Error | null>
@@ -32,14 +29,31 @@ export interface ResourceLoaderContext {
32
29
  signal: AbortSignal
33
30
  }
34
31
 
35
- export function resource<T, Source>(
36
- source: Kiru.Signal<Source>,
37
- callback: (source: Source, ctx: ResourceLoaderContext) => Promise<T>
32
+ const resourceMeta = new WeakMap<Kiru.VNode, { id: string; index: number }>()
33
+
34
+ export function resource<T>(
35
+ callback: (ctx: ResourceLoaderContext) => Promise<T>
36
+ ): Resource<T>
37
+ export function resource<T, Source extends ResourceSource>(
38
+ source: Source,
39
+ callback: (
40
+ source: UnwrapResourceSource<Source>,
41
+ ctx: ResourceLoaderContext
42
+ ) => Promise<T>
43
+ ): Resource<T>
44
+ export function resource<T, Source extends ResourceSource>(
45
+ callbackOrSource: Source | ((ctx: ResourceLoaderContext) => Promise<T>),
46
+ callback?: (
47
+ source: UnwrapResourceSource<Source>,
48
+ ctx: ResourceLoaderContext
49
+ ) => Promise<T>
38
50
  ): Resource<T> {
39
51
  const data = signal(void 0 as T)
40
52
  const error = signal<Error | null>(null)
41
53
  const isPending = signal(true)
42
54
 
55
+ let controller = new AbortController()
56
+
43
57
  let promiseId = ""
44
58
  const vNode = node.current
45
59
  if (!vNode) {
@@ -62,30 +76,53 @@ export function resource<T, Source>(
62
76
  promiseId = generateRandomID()
63
77
  }
64
78
 
65
- const unsub = source.subscribe((src) => {
66
- resource.promise = createPromise(src)
79
+ const updateResource = () => {
80
+ resource.promise = createPromise()
67
81
  resource.notify()
68
- })
82
+ }
69
83
 
70
- let controller = new AbortController()
84
+ let unsubFromSource: (() => void) | undefined
85
+ if (typeof callbackOrSource === "object") {
86
+ if (Signal.isSignal(callbackOrSource)) {
87
+ unsubFromSource = callbackOrSource.subscribe(updateResource)
88
+ } else {
89
+ const unsubs: (() => void)[] = []
90
+ for (const key in callbackOrSource) {
91
+ if (!Signal.isSignal(callbackOrSource[key])) continue
92
+ unsubs.push(callbackOrSource[key].subscribe(updateResource))
93
+ }
94
+ unsubFromSource = () => {
95
+ unsubs.forEach((unsub) => unsub())
96
+ }
97
+ }
98
+ }
99
+
100
+ const observedSignalUnsubs = new Map<string, () => void>()
71
101
  const dispose = () => {
72
102
  if (!controller.signal.aborted) controller.abort()
73
103
  Signal.dispose(data)
74
104
  Signal.dispose(isPending)
75
- unsub()
105
+ observedSignalUnsubs.forEach((unsub) => unsub())
106
+ unsubFromSource?.()
76
107
  }
77
108
 
78
109
  if (vNode) {
79
110
  registerVNodeCleanup(vNode, promiseId, dispose)
80
111
  }
81
112
 
113
+ let promise: Kiru.StatefulPromise<T>
82
114
  const resource: Resource<T> = Object.assign(data, {
83
115
  error,
84
116
  isPending,
85
- promise: undefined as unknown as Kiru.StatefulPromise<T>,
117
+ get promise() {
118
+ return (promise ??= createPromise())
119
+ },
120
+ set promise(newPromise) {
121
+ promise = newPromise
122
+ },
86
123
  refetch() {
87
124
  data.value = void 0 as T
88
- this.promise = createPromise(source.peek())
125
+ resource.promise = createPromise()
89
126
  },
90
127
  dispose,
91
128
  })
@@ -111,33 +148,38 @@ export function resource<T, Source>(
111
148
  }
112
149
  }
113
150
 
114
- if (__DEV__ && isBrowser && window.__kiru.HMRContext?.isReplacement()) {
115
- queueMicrotask(() => {
116
- resource.promise = createPromise(source.peek())
117
- })
118
- } else {
119
- resource.promise = createPromise(source.peek())
120
- }
121
-
122
- function createPromise(source: Source): Kiru.StatefulPromise<T> {
151
+ function createPromise(): Kiru.StatefulPromise<T> {
123
152
  controller.abort()
124
153
  const ctrl = (controller = new AbortController())
125
154
  isPending.value = true
126
- let newPromise: Promise<T>
127
- if (renderMode.current === "string") {
128
- // if we're rendering to a string, there's no need to fire the callback
129
- newPromise = Promise.resolve() as Promise<T>
130
- } else if (
131
- renderMode.current === "hydrate" &&
132
- hydrationMode.current === "dynamic"
133
- ) {
134
- // if we're hydrating and the hydration mode is not static,
135
- // we need to resolve the promise from cache/event
136
- newPromise = resolveDeferredPromise<T>(promiseId, ctrl.signal)
137
- } else {
138
- // stream / dom / (hydrate + static)
139
- newPromise = callback(source, { signal: ctrl.signal })
140
- }
155
+ const newPromise = executeWithTracking({
156
+ fn: () => {
157
+ let promise: Promise<T>
158
+ if (renderMode.current === "string") {
159
+ // if we're rendering to a string, there's no need to fire the callback
160
+ promise = Promise.resolve() as Promise<T>
161
+ } else if (
162
+ renderMode.current === "hydrate" &&
163
+ hydrationMode.current === "dynamic"
164
+ ) {
165
+ // if we're hydrating and the hydration mode is not static,
166
+ // we need to resolve the promise from cache/event
167
+ promise = resolveDeferredPromise<T>(promiseId, ctrl.signal)
168
+ } else {
169
+ // stream / dom / (hydrate + static)
170
+ if (typeof callbackOrSource === "function") {
171
+ promise = callbackOrSource({ signal: ctrl.signal })
172
+ } else {
173
+ const source = unwrapResourceSource(callbackOrSource)
174
+ promise = callback!(source, { signal: ctrl.signal })
175
+ }
176
+ }
177
+ return promise
178
+ },
179
+ id: Signal.id(data),
180
+ onDepChanged: updateResource,
181
+ subs: observedSignalUnsubs,
182
+ })
141
183
 
142
184
  const statefulPromise: Kiru.StatefulPromise<T> = Object.assign(newPromise, {
143
185
  id: promiseId,
@@ -162,6 +204,12 @@ export function resource<T, Source>(
162
204
  return statefulPromise
163
205
  }
164
206
 
207
+ if (__DEV__ && isBrowser && window.__kiru.HMRContext?.isReplacement()) {
208
+ queueMicrotask(() => (resource.promise = createPromise()))
209
+ } else {
210
+ resource.promise ??= createPromise()
211
+ }
212
+
165
213
  return resource
166
214
  }
167
215
 
@@ -205,3 +253,29 @@ function resolveDeferredPromise<T>(
205
253
  })
206
254
  })
207
255
  }
256
+
257
+ /**
258
+ * Returns true if the value is a {@link Resource}
259
+ */
260
+ export function isResource(thing: unknown): thing is Resource<unknown> {
261
+ return (
262
+ Signal.isSignal(thing) &&
263
+ "promise" in thing &&
264
+ thing["promise"] instanceof Promise
265
+ )
266
+ }
267
+
268
+ function unwrapResourceSource<T extends ResourceSource>(
269
+ source: T
270
+ ): UnwrapResourceSource<T> {
271
+ if (Signal.isSignal(source)) {
272
+ return source.peek() as UnwrapResourceSource<T>
273
+ }
274
+ const out: Record<string, unknown> = {}
275
+ for (const key in source) {
276
+ if (Signal.isSignal(source[key])) {
277
+ out[key] = source[key].peek()
278
+ }
279
+ }
280
+ return out as UnwrapResourceSource<T>
281
+ }
@@ -9,8 +9,8 @@ import {
9
9
  match404Route,
10
10
  parseQuery,
11
11
  } from "../utils/index.js"
12
- import type { FormattedViteImportMap, PageModule } from "../types.internal"
13
- import type { FileRouterConfig, FileRouterPreloadConfig } from "../types"
12
+ import type { FormattedViteImportMap, PageModule } from "../types.internal.js"
13
+ import type { FileRouterConfig, FileRouterPreloadConfig } from "../types.js"
14
14
  import { fileRouterInstance, fileRouterRoute, routerCache } from "../globals.js"
15
15
  import { FileRouterController } from "../fileRouterController.js"
16
16
  import { FileRouterDataLoadError } from "../errors.js"
@@ -2,7 +2,7 @@ import { signal, Signal } from "../signals/base.js"
2
2
  import { effect } from "../signals/effect.js"
3
3
  import { __DEV__ } from "../env.js"
4
4
  import { nextIdle } from "../scheduler.js"
5
- import { ReloadOptions, type FileRouterContextType } from "./context.js"
5
+ import { type FileRouterContextType } from "./context.js"
6
6
  import { FileRouterDataLoadError } from "./errors.js"
7
7
  import { fileRouterInstance, fileRouterRoute, routerCache } from "./globals.js"
8
8
  import type {
@@ -521,7 +521,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
521
521
  data: null,
522
522
  error: new FileRouterDataLoadError(error),
523
523
  loading: false,
524
- } satisfies PageProps<PageConfig<unknown>>)
524
+ }) satisfies PageProps<PageConfig<unknown>>
525
525
  )
526
526
  .then((state) => {
527
527
  if (context.signal.aborted) return
@@ -661,13 +661,13 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
661
661
  )
662
662
  }
663
663
  }
664
- private createContextValue() {
664
+ private createContextValue(): FileRouterContextType {
665
665
  const __this = this
666
666
  return {
667
667
  get baseUrl() {
668
668
  return __this.baseUrl
669
669
  },
670
- invalidate: async (...paths: string[]) => {
670
+ invalidate: async (...paths) => {
671
671
  if (this.invalidate(...paths)) {
672
672
  return this.loadRoute(void 0, void 0, true)
673
673
  }
@@ -677,7 +677,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
677
677
  },
678
678
  navigate: this.navigate.bind(this),
679
679
  prefetchRouteModules: this.prefetchRouteModules.bind(this),
680
- reload: async (options?: ReloadOptions) => {
680
+ reload: (options) => {
681
681
  if (options?.invalidate ?? true) {
682
682
  this.invalidate(this.state.pathname.peek())
683
683
  }
@@ -1,5 +1,5 @@
1
- import type { RouterCache } from "./cache"
2
- import type { FileRouterController } from "./fileRouterController"
1
+ import type { RouterCache } from "./cache.js"
2
+ import type { FileRouterController } from "./fileRouterController.js"
3
3
 
4
4
  export const fileRouterInstance = {
5
5
  current: null as FileRouterController | null,
@@ -1,4 +1,4 @@
1
- import type { ElementProps } from "../types"
1
+ import type { ElementProps } from "../types.js"
2
2
  import { createElement } from "../element.js"
3
3
  import { useFileRouter } from "./context.js"
4
4
 
@@ -1,6 +1,6 @@
1
1
  import { __DEV__, isBrowser } from "../env.js"
2
2
  import { fileRouterInstance } from "./globals.js"
3
- import type { PageConfig } from "./types"
3
+ import type { PageConfig } from "./types.js"
4
4
 
5
5
  export function definePageConfig<T>(config: PageConfig<T>): PageConfig<T> {
6
6
  if (__DEV__ && isBrowser) {
@@ -1,5 +1,5 @@
1
- import type { FileRouterContextType } from "./context"
2
- import type { PageConfig } from "./types"
1
+ import type { FileRouterContextType } from "./context.js"
2
+ import type { PageConfig } from "./types.js"
3
3
 
4
4
  export interface CurrentPage {
5
5
  component: Kiru.FC<any>
@@ -1,10 +1,10 @@
1
- import type { AsyncTaskState } from "../types.utils"
2
- import type { FileRouterDataLoadError } from "./errors"
1
+ import type { AsyncTaskState } from "../types.utils.js"
2
+ import type { FileRouterDataLoadError } from "./errors.js"
3
3
  import type {
4
4
  DefaultComponentModule,
5
5
  FormattedViteImportMap,
6
6
  PageModule,
7
- } from "./types.internal"
7
+ } from "./types.internal.js"
8
8
 
9
9
  export interface FileRouterPreloadConfig {
10
10
  pages: FormattedViteImportMap
@@ -4,7 +4,7 @@ import type {
4
4
  FormattedViteImportMap,
5
5
  RouteMatch,
6
6
  ViteImportMap,
7
- } from "../types.internal"
7
+ } from "../types.internal.js"
8
8
 
9
9
  export {
10
10
  formatViteImportMap,