kiru 0.50.5 → 0.50.7

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 (59) hide show
  1. package/dist/components/suspense.d.ts +2 -12
  2. package/dist/components/suspense.d.ts.map +1 -1
  3. package/dist/components/suspense.js +4 -78
  4. package/dist/components/suspense.js.map +1 -1
  5. package/dist/dom.d.ts.map +1 -1
  6. package/dist/dom.js +22 -3
  7. package/dist/dom.js.map +1 -1
  8. package/dist/hooks/index.d.ts +1 -0
  9. package/dist/hooks/index.d.ts.map +1 -1
  10. package/dist/hooks/index.js +1 -0
  11. package/dist/hooks/index.js.map +1 -1
  12. package/dist/hooks/usePromise.d.ts +13 -0
  13. package/dist/hooks/usePromise.d.ts.map +1 -0
  14. package/dist/hooks/usePromise.js +79 -0
  15. package/dist/hooks/usePromise.js.map +1 -0
  16. package/dist/recursiveRender.d.ts.map +1 -1
  17. package/dist/recursiveRender.js +12 -3
  18. package/dist/recursiveRender.js.map +1 -1
  19. package/dist/router/context.d.ts +4 -0
  20. package/dist/router/context.d.ts.map +1 -1
  21. package/dist/router/context.js.map +1 -1
  22. package/dist/router/fileRouter.js +1 -1
  23. package/dist/router/fileRouter.js.map +1 -1
  24. package/dist/router/fileRouterController.d.ts +3 -4
  25. package/dist/router/fileRouterController.d.ts.map +1 -1
  26. package/dist/router/fileRouterController.js +33 -159
  27. package/dist/router/fileRouterController.js.map +1 -1
  28. package/dist/router/link.d.ts +9 -0
  29. package/dist/router/link.d.ts.map +1 -1
  30. package/dist/router/link.js +21 -3
  31. package/dist/router/link.js.map +1 -1
  32. package/dist/router/types.internal.d.ts +14 -0
  33. package/dist/router/types.internal.d.ts.map +1 -1
  34. package/dist/router/utils/index.d.ts +15 -0
  35. package/dist/router/utils/index.d.ts.map +1 -0
  36. package/dist/router/utils/index.js +148 -0
  37. package/dist/router/utils/index.js.map +1 -0
  38. package/dist/utils/dom.d.ts +2 -0
  39. package/dist/utils/dom.d.ts.map +1 -0
  40. package/dist/utils/dom.js +9 -0
  41. package/dist/utils/dom.js.map +1 -0
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/dist/utils/index.js +1 -0
  45. package/dist/utils/index.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/components/suspense.ts +6 -125
  48. package/src/dom.ts +26 -2
  49. package/src/hooks/index.ts +1 -0
  50. package/src/hooks/usePromise.ts +121 -0
  51. package/src/recursiveRender.ts +14 -2
  52. package/src/router/context.ts +8 -0
  53. package/src/router/fileRouter.ts +1 -1
  54. package/src/router/fileRouterController.ts +47 -216
  55. package/src/router/link.ts +39 -2
  56. package/src/router/types.internal.ts +16 -0
  57. package/src/router/utils/index.ts +206 -0
  58. package/src/utils/dom.ts +10 -0
  59. package/src/utils/index.ts +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.js","sourceRoot":"","sources":["../../src/utils/dom.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,SAAS;QAC1B,KAAK,KAAK,SAAS;QACnB,KAAK,KAAK,IAAI,CACf,CAAA;AACH,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export * from "./compare.js";
2
+ export * from "./dom.js";
2
3
  export * from "./format.js";
3
4
  export * from "./runtime.js";
4
5
  export * from "./vdom.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,iBAAiB,CAAA;AAE/B,eAAO,MAAM,IAAI,YAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,iBAAiB,CAAA;AAE/B,eAAO,MAAM,IAAI,YAA0B,CAAA"}
@@ -1,4 +1,5 @@
1
1
  export * from "./compare.js";
2
+ export * from "./dom.js";
2
3
  export * from "./format.js";
3
4
  export * from "./runtime.js";
4
5
  export * from "./vdom.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,iBAAiB,CAAA;AAE/B,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,iBAAiB,CAAA;AAE/B,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiru",
3
- "version": "0.50.5",
3
+ "version": "0.50.7",
4
4
  "description": "A batteries-included, easy-to-use rendering library with a tiny footprint",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -1,18 +1,10 @@
1
- import { requestUpdate } from "../scheduler.js"
2
- import { renderMode } from "../globals.js"
3
- import {
4
- cleanupHook,
5
- depsRequireChange,
6
- useHook,
7
- useId,
8
- } from "../hooks/index.js"
1
+ import { $SUSPENSE_THROW } from "../constants.js"
9
2
  import { __DEV__ } from "../env.js"
10
- import { getCurrentVNode } from "../utils/index.js"
11
- import { $SUSPENSE_THROW, PREFETCHED_DATA_EVENT } from "../constants.js"
12
- import { Signal, useSignal } from "../signals/base.js"
3
+ import { renderMode, node } from "../globals.js"
4
+ import { requestUpdate } from "../scheduler.js"
13
5
 
14
- export type { SuspenseProps, UsePromiseCallbackContext, UsePromiseState }
15
- export { Suspense, isSuspenseThrowValue, usePromise }
6
+ export { Suspense, isSuspenseThrowValue }
7
+ export type { SuspenseProps }
16
8
 
17
9
  type StatefulPromiseValues<T extends readonly Kiru.StatefulPromise<unknown>[]> =
18
10
  {
@@ -54,7 +46,7 @@ function Suspense<
54
46
  for (const p of promiseArray) {
55
47
  if (p.state === "rejected") throw p.error
56
48
  if (p.state === "pending") {
57
- const n = getCurrentVNode()!
49
+ const n = node.current!
58
50
  Promise.allSettled(promiseArray).then(() => requestUpdate(n))
59
51
  return fallback
60
52
  }
@@ -78,114 +70,3 @@ interface SuspenseThrowValue {
78
70
  function isSuspenseThrowValue(value: unknown): value is SuspenseThrowValue {
79
71
  return typeof value === "object" && !!value && $SUSPENSE_THROW in value
80
72
  }
81
-
82
- interface PromiseResolveEventDetail<T> {
83
- id: string
84
- data?: T
85
- error?: string
86
- }
87
-
88
- function resolveHydrationPromise<T>(
89
- id: string,
90
- signal: AbortSignal
91
- ): Promise<T> {
92
- return new Promise<T>((resolve, reject) => {
93
- const prefetchCache: Map<string, { data?: T; error?: string }> = // @ts-ignore
94
- (window[PREFETCHED_DATA_EVENT] ??= new Map())
95
-
96
- const existing = prefetchCache.get(id)
97
- if (existing) {
98
- const { data, error } = existing
99
- prefetchCache.delete(id)
100
- if (error) return reject(error)
101
- return resolve(data!)
102
- }
103
-
104
- const onDataEvent = (event: Event) => {
105
- const { detail } = event as CustomEvent<PromiseResolveEventDetail<T>>
106
- if (detail.id === id) {
107
- prefetchCache.delete(id)
108
- window.removeEventListener(PREFETCHED_DATA_EVENT, onDataEvent)
109
- const { data, error } = detail
110
- if (error) return reject(error)
111
- resolve(data!)
112
- }
113
- }
114
-
115
- window.addEventListener(PREFETCHED_DATA_EVENT, onDataEvent)
116
- signal.addEventListener("abort", () => {
117
- window.removeEventListener(PREFETCHED_DATA_EVENT, onDataEvent)
118
- reject()
119
- })
120
- })
121
- }
122
-
123
- const nodeToPromiseIndex = new WeakMap<Kiru.VNode, number>()
124
-
125
- interface UsePromiseCallbackContext {
126
- signal: AbortSignal
127
- }
128
-
129
- interface UsePromiseState<T> {
130
- data: Kiru.StatefulPromise<T>
131
- refresh: () => void
132
- pending: Signal<boolean>
133
- }
134
-
135
- function usePromise<T>(
136
- callback: (ctx: UsePromiseCallbackContext) => Promise<T>,
137
- deps: unknown[]
138
- ): UsePromiseState<T> {
139
- const id = useId()
140
- const pending = useSignal(true)
141
-
142
- return useHook(
143
- "usePromise",
144
- {} as {
145
- promise: Kiru.StatefulPromise<T>
146
- abortController?: AbortController
147
- deps?: unknown[]
148
- },
149
- ({ hook, isInit, vNode }) => {
150
- if (isInit || depsRequireChange(deps, hook.deps)) {
151
- pending.value = true
152
- hook.deps = deps
153
- cleanupHook(hook)
154
-
155
- const controller = (hook.abortController = new AbortController())
156
- hook.cleanup = () => controller.abort()
157
-
158
- const index = nodeToPromiseIndex.get(vNode) ?? 0
159
- nodeToPromiseIndex.set(vNode, index + 1)
160
-
161
- const promiseId = `${id}:data:${index}`
162
- const state: Kiru.PromiseState<T> = { id: promiseId, state: "pending" }
163
- const promise =
164
- renderMode.current === "hydrate"
165
- ? resolveHydrationPromise<T>(promiseId, controller.signal)
166
- : callback({ signal: controller.signal })
167
-
168
- const p = (hook.promise = Object.assign(promise, state))
169
- p.then((value) => {
170
- p.state = "fulfilled"
171
- p.value = value
172
- })
173
- .catch((error) => {
174
- p.state = "rejected"
175
- p.error = error instanceof Error ? error : new Error(error)
176
- })
177
- .finally(() => {
178
- pending.value = false
179
- })
180
- }
181
- return {
182
- data: hook.promise,
183
- refresh: () => {
184
- hook.deps = undefined
185
- requestUpdate(vNode)
186
- },
187
- pending,
188
- }
189
- }
190
- )
191
- }
package/src/dom.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  propFilters,
5
5
  propToHtmlAttr,
6
6
  getVNodeAppContext,
7
+ isPrimitiveChild,
7
8
  } from "./utils/index.js"
8
9
  import {
9
10
  booleanAttributes,
@@ -339,13 +340,36 @@ function subTextNode(vNode: VNode, textNode: Text, signal: Signal<string>) {
339
340
  })
340
341
  }
341
342
 
343
+ function tryHydrateNullableSignalChild(vNode: VNode): MaybeDom {
344
+ if (vNode.type !== "#text" || !Signal.isSignal(vNode.props.nodeValue)) {
345
+ return
346
+ }
347
+ const value = unwrap(vNode.props.nodeValue)
348
+ if (!isPrimitiveChild(value)) {
349
+ if (__DEV__) {
350
+ console.error(
351
+ `[kiru]: Hydration mismatch - expected primitive child but received ${value}`
352
+ )
353
+ }
354
+ return
355
+ }
356
+ const dom = createTextNode(vNode)
357
+ const parent = hydrationStack.parent()
358
+ if (parent) {
359
+ parent.appendChild(dom)
360
+ }
361
+ return dom
362
+ }
363
+
342
364
  function hydrateDom(vNode: VNode) {
343
- const dom = hydrationStack.nextChild()
344
- if (!dom)
365
+ const dom = hydrationStack.nextChild() ?? tryHydrateNullableSignalChild(vNode)
366
+
367
+ if (!dom) {
345
368
  throw new KiruError({
346
369
  message: `Hydration mismatch - no node found`,
347
370
  vNode,
348
371
  })
372
+ }
349
373
  let nodeName = dom.nodeName
350
374
  if (!svgTags.has(nodeName)) {
351
375
  nodeName = nodeName.toLowerCase()
@@ -6,6 +6,7 @@ export * from "./useEffectEvent.js"
6
6
  export * from "./useId.js"
7
7
  export * from "./useLayoutEffect.js"
8
8
  export * from "./useMemo.js"
9
+ export * from "./usePromise.js"
9
10
  export * from "./useReducer.js"
10
11
  export * from "./useRef.js"
11
12
  export * from "./useState.js"
@@ -0,0 +1,121 @@
1
+ import { PREFETCHED_DATA_EVENT } from "../constants.js"
2
+ import { __DEV__ } from "../env.js"
3
+ import { renderMode } from "../globals.js"
4
+ import { requestUpdate } from "../scheduler.js"
5
+ import { Signal, useSignal } from "../signals/base.js"
6
+ import { cleanupHook, depsRequireChange, useHook } from "./utils.js"
7
+ import { useId } from "./useId.js"
8
+
9
+ export { usePromise }
10
+ export type { UsePromiseCallbackContext, UsePromiseState }
11
+
12
+ const nodeToPromiseIndex = new WeakMap<Kiru.VNode, number>()
13
+
14
+ interface UsePromiseCallbackContext {
15
+ signal: AbortSignal
16
+ }
17
+
18
+ interface UsePromiseState<T> {
19
+ data: Kiru.StatefulPromise<T>
20
+ refresh: () => void
21
+ pending: Signal<boolean>
22
+ }
23
+
24
+ function usePromise<T>(
25
+ callback: (ctx: UsePromiseCallbackContext) => Promise<T>,
26
+ deps: unknown[]
27
+ ): UsePromiseState<T> {
28
+ const id = useId()
29
+ const pending = useSignal(true)
30
+
31
+ return useHook(
32
+ "usePromise",
33
+ {} as {
34
+ promise: Kiru.StatefulPromise<T>
35
+ abortController?: AbortController
36
+ deps?: unknown[]
37
+ },
38
+ ({ hook, isInit, vNode }) => {
39
+ if (isInit || depsRequireChange(deps, hook.deps)) {
40
+ pending.value = true
41
+ hook.deps = deps
42
+ cleanupHook(hook)
43
+
44
+ const controller = (hook.abortController = new AbortController())
45
+ hook.cleanup = () => controller.abort()
46
+
47
+ const index = nodeToPromiseIndex.get(vNode) ?? 0
48
+ nodeToPromiseIndex.set(vNode, index + 1)
49
+
50
+ const promiseId = `${id}:data:${index}`
51
+ const state: Kiru.PromiseState<T> = { id: promiseId, state: "pending" }
52
+ const promise =
53
+ renderMode.current === "hydrate"
54
+ ? resolvePrefetchedPromise<T>(promiseId, controller.signal)
55
+ : callback({ signal: controller.signal })
56
+
57
+ const p = (hook.promise = Object.assign(promise, state))
58
+ p.then((value) => {
59
+ p.state = "fulfilled"
60
+ p.value = value
61
+ })
62
+ .catch((error) => {
63
+ p.state = "rejected"
64
+ p.error = error instanceof Error ? error : new Error(error)
65
+ })
66
+ .finally(() => {
67
+ pending.value = false
68
+ })
69
+ }
70
+ return {
71
+ data: hook.promise,
72
+ refresh: () => {
73
+ hook.deps = undefined
74
+ requestUpdate(vNode)
75
+ },
76
+ pending,
77
+ }
78
+ }
79
+ )
80
+ }
81
+
82
+ interface PrefetchedPromiseEventDetail<T> {
83
+ id: string
84
+ data?: T
85
+ error?: string
86
+ }
87
+
88
+ function resolvePrefetchedPromise<T>(
89
+ id: string,
90
+ signal: AbortSignal
91
+ ): Promise<T> {
92
+ return new Promise<T>((resolve, reject) => {
93
+ const prefetchCache: Map<string, { data?: T; error?: string }> = // @ts-ignore
94
+ (window[PREFETCHED_DATA_EVENT] ??= new Map())
95
+
96
+ const existing = prefetchCache.get(id)
97
+ if (existing) {
98
+ const { data, error } = existing
99
+ prefetchCache.delete(id)
100
+ if (error) return reject(error)
101
+ return resolve(data!)
102
+ }
103
+
104
+ const onDataEvent = (event: Event) => {
105
+ const { detail } = event as CustomEvent<PrefetchedPromiseEventDetail<T>>
106
+ if (detail.id === id) {
107
+ prefetchCache.delete(id)
108
+ window.removeEventListener(PREFETCHED_DATA_EVENT, onDataEvent)
109
+ const { data, error } = detail
110
+ if (error) return reject(error)
111
+ resolve(data!)
112
+ }
113
+ }
114
+
115
+ window.addEventListener(PREFETCHED_DATA_EVENT, onDataEvent)
116
+ signal.addEventListener("abort", () => {
117
+ window.removeEventListener(PREFETCHED_DATA_EVENT, onDataEvent)
118
+ reject()
119
+ })
120
+ })
121
+ }
@@ -5,6 +5,7 @@ import {
5
5
  propsToElementAttributes,
6
6
  isExoticType,
7
7
  assertValidElementProps,
8
+ isPrimitiveChild,
8
9
  } from "./utils/index.js"
9
10
  import { Signal } from "./signals/base.js"
10
11
  import { $ERROR_BOUNDARY, voidElements, $SUSPENSE_THROW } from "./constants.js"
@@ -36,10 +37,21 @@ export function recursiveRender(
36
37
  return el.forEach((c, i) => recursiveRender(ctx, c, parent, i))
37
38
  }
38
39
  if (Signal.isSignal(el)) {
39
- return ctx.write(String(el.peek()))
40
+ const value = el.peek()
41
+ if (__DEV__) {
42
+ if (!isPrimitiveChild(value)) {
43
+ if (__DEV__) {
44
+ console.error(
45
+ `[kiru]: expected primitive child but received ${value}`
46
+ )
47
+ }
48
+ return
49
+ }
50
+ }
51
+ return ctx.write(encodeHtmlEntities(String(value)))
40
52
  }
41
53
  if (!isVNode(el)) {
42
- return ctx.write(String(el))
54
+ return
43
55
  }
44
56
  el.parent = parent
45
57
  el.depth = (parent?.depth ?? -1) + 1
@@ -8,6 +8,7 @@ export interface FileRouterContextType {
8
8
  * The current router state
9
9
  */
10
10
  state: RouterState
11
+
11
12
  /**
12
13
  * Navigate to a new route, optionally replacing the current route
13
14
  * in the history stack or triggering a view transition
@@ -16,10 +17,17 @@ export interface FileRouterContextType {
16
17
  path: string,
17
18
  options?: { replace?: boolean; transition?: boolean }
18
19
  ) => Promise<void>
20
+
21
+ /**
22
+ * Prefetch a route module and its dependencies to be loaded in the background
23
+ */
24
+ prefetchRouteModules: (path: string) => void
25
+
19
26
  /**
20
27
  * Reload the current route, optionally triggering a view transition
21
28
  */
22
29
  reload: (options?: { transition?: boolean }) => Promise<void>
30
+
23
31
  /**
24
32
  * Set the current query parameters
25
33
  */
@@ -30,7 +30,7 @@ export function FileRouter(props: FileRouterProps): JSX.Element {
30
30
 
31
31
  return createElement(
32
32
  RouterContext.Provider,
33
- { value: controller.getContextValue() },
33
+ { value: controller.contextValue },
34
34
  controller.getChildren()
35
35
  )
36
36
  }