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,91 @@
1
+ import { ctx, node, nodeToCtxMap, renderMode } from "./globals.js"
2
+ import { createAppContext } from "./appContext.js"
3
+ import { Fragment } from "./element.js"
4
+ import {
5
+ isVNode,
6
+ encodeHtmlEntities,
7
+ propsToElementAttributes,
8
+ isExoticType,
9
+ } from "./utils.js"
10
+ import { Signal } from "./signals/base.js"
11
+ import { $HYDRATION_BOUNDARY, voidElements } from "./constants.js"
12
+ import { assertValidElementProps } from "./props.js"
13
+ import { HYDRATION_BOUNDARY_MARKER } from "./ssr/hydrationBoundary.js"
14
+ import { __DEV__ } from "./env.js"
15
+
16
+ export function renderToString<T extends Record<string, unknown>>(
17
+ appFunc: (props: T) => JSX.Element,
18
+ appProps = {} as T
19
+ ) {
20
+ const prev = renderMode.current
21
+ renderMode.current = "string"
22
+ const prevCtx = ctx.current
23
+ const c = (ctx.current = createAppContext(appFunc, appProps, {
24
+ rootType: Fragment,
25
+ }))
26
+ const res = renderToString_internal(c.rootNode, null, 0)
27
+ renderMode.current = prev
28
+ ctx.current = prevCtx
29
+ return res
30
+ }
31
+
32
+ function renderToString_internal(
33
+ el: unknown,
34
+ parent: Kaioken.VNode | null,
35
+ idx: number
36
+ ): string {
37
+ if (el === null) return ""
38
+ if (el === undefined) return ""
39
+ if (typeof el === "boolean") return ""
40
+ if (typeof el === "string") return encodeHtmlEntities(el)
41
+ if (typeof el === "number" || typeof el === "bigint") return el.toString()
42
+ if (el instanceof Array) {
43
+ return el.map((c, i) => renderToString_internal(c, parent, i)).join("")
44
+ }
45
+ if (Signal.isSignal(el)) return String(el.peek())
46
+ if (!isVNode(el)) return String(el)
47
+ el.parent = parent
48
+ el.depth = (parent?.depth ?? -1) + 1
49
+ el.index = idx
50
+ const props = el.props ?? {}
51
+ const type = el.type
52
+ if (type === "#text") return encodeHtmlEntities(props.nodeValue ?? "")
53
+
54
+ const children = props.children
55
+ if (isExoticType(type)) {
56
+ if (type === $HYDRATION_BOUNDARY) {
57
+ return `<!--${HYDRATION_BOUNDARY_MARKER}-->${renderToString_internal(
58
+ children,
59
+ el,
60
+ idx
61
+ )}<!--/${HYDRATION_BOUNDARY_MARKER}-->`
62
+ }
63
+
64
+ return renderToString_internal(children, el, idx)
65
+ }
66
+
67
+ if (typeof type !== "string") {
68
+ nodeToCtxMap.set(el, ctx.current)
69
+ node.current = el
70
+ const res = type(props)
71
+ node.current = null
72
+ return renderToString_internal(res, el, idx)
73
+ }
74
+
75
+ if (__DEV__) {
76
+ assertValidElementProps(el)
77
+ }
78
+ const attrs = propsToElementAttributes(props)
79
+ const inner =
80
+ "innerHTML" in props
81
+ ? Signal.isSignal(props.innerHTML)
82
+ ? props.innerHTML.peek()
83
+ : props.innerHTML
84
+ : Array.isArray(children)
85
+ ? children.map((c, i) => renderToString_internal(c, el, i)).join("")
86
+ : renderToString_internal(children, el, 0)
87
+
88
+ return `<${type}${attrs.length ? ` ${attrs}` : ""}>${
89
+ voidElements.has(type) ? "" : `${inner}</${type}>`
90
+ }`
91
+ }
@@ -0,0 +1,2 @@
1
+ export { Router, useRouter, navigate, Link, type LinkProps } from "./router.js"
2
+ export { Route } from "./route.js"
@@ -0,0 +1,51 @@
1
+ import { isVNode } from "../utils.js"
2
+
3
+ interface RouteProps {
4
+ /**
5
+ * The path to match.
6
+ * @example
7
+ * ```tsx
8
+ * <Router>
9
+ * <Route path="/" element={<h1>Home</h1>} />
10
+ * <Route path="/:id" element={<UserProfile />} />
11
+ * </Router>
12
+ * //
13
+ * const UserProfile = () => {
14
+ * const router = useRouter()
15
+ * const { id } = router.params
16
+ * return <h1>{id}</h1>
17
+ * }
18
+ * ```
19
+ */
20
+ path: string
21
+ /**
22
+ * Allow url with additional segments being matched. Useful with nested routers.
23
+ * @example
24
+ * ```tsx
25
+ * <Route path="/profile" fallthrough element={<UserProfile />} />
26
+ * //
27
+ * const UserProfile = () => {
28
+ * return (
29
+ * <Router>
30
+ * <Route path="/" element={<UserDetails />} />
31
+ * <Route path="/update" element={<UserUpdateForm />} />
32
+ * </Router>
33
+ * )
34
+ * }
35
+ * ```
36
+ */
37
+ fallthrough?: boolean
38
+ /**
39
+ * The element to render.
40
+ */
41
+ element: JSX.Element
42
+ }
43
+ export function Route({ element }: RouteProps) {
44
+ return element
45
+ }
46
+
47
+ export function isRoute(
48
+ thing: unknown
49
+ ): thing is Kaioken.VNode & { props: RouteProps } {
50
+ return isVNode(thing) && thing.type === Route
51
+ }
@@ -0,0 +1,275 @@
1
+ import { createElement } from "../element.js"
2
+ import {
3
+ useState,
4
+ useMemo,
5
+ useContext,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useAppContext,
9
+ } from "../hooks/index.js"
10
+ import { __DEV__ } from "../env.js"
11
+ import {
12
+ parsePathParams,
13
+ parseSearchParams,
14
+ routeMatchesPath,
15
+ } from "./routerUtils.js"
16
+ import { createContext } from "../context.js"
17
+ import { isRoute, Route } from "./route.js"
18
+ import { getVNodeAppContext, noop } from "../utils.js"
19
+ import { node } from "../globals.js"
20
+ import type { ElementProps } from "../types"
21
+
22
+ export interface LinkProps extends Omit<ElementProps<"a">, "href"> {
23
+ /**
24
+ * The relative path to navigate to. If `inherit` is true,
25
+ * the path will be relative to the parent <Route> component.
26
+ */
27
+ to: string
28
+ /**
29
+ * Event handler called when the link is clicked.
30
+ * If you call `e.preventDefault()`, the navigation will not happen.
31
+ */
32
+ onclick?: (e: Event) => void
33
+ /**
34
+ * Specifies whether to replace the current history entry
35
+ * instead of adding a new one.
36
+ */
37
+ replace?: boolean
38
+ /**
39
+ * If true, the path used for `to` will be relative to the parent <Route> component.
40
+ * @default false
41
+ */
42
+ inherit?: boolean
43
+ }
44
+ export function Link({ to, onclick, replace, inherit, ...props }: LinkProps) {
45
+ const router = useContext(RouterContext, false)
46
+
47
+ const href = useMemo(() => {
48
+ if (!inherit || router.isDefault) return to
49
+ const parentPath = Object.entries(router.params).reduce(
50
+ (acc, [k, v]) => acc.replace(`:${k}`, v),
51
+ router.routePath
52
+ )
53
+ if (to === "/") return parentPath
54
+ return (parentPath + to).replaceAll(/\/+/g, "/")
55
+ }, [router.params, to, inherit])
56
+
57
+ return createElement("a", {
58
+ ...props,
59
+ href,
60
+ onclick: (e: Event) => {
61
+ onclick?.(e)
62
+ if (e.defaultPrevented) return
63
+ e.preventDefault()
64
+ navigate(href, { replace })
65
+ },
66
+ })
67
+ }
68
+
69
+ type RouterCtx = {
70
+ viewTransition: Kaioken.RefObject<ViewTransition>
71
+ queueSyncNav: (callback: () => void) => void
72
+ params: Record<string, string>
73
+ query: Record<string, string>
74
+ routePath: string
75
+ basePath?: string
76
+ isDefault: boolean
77
+ }
78
+ const RouterContext = createContext<RouterCtx>({
79
+ viewTransition: { current: null },
80
+ queueSyncNav: noop,
81
+ params: {},
82
+ query: {},
83
+ routePath: "/",
84
+ isDefault: true,
85
+ })
86
+ RouterContext.displayName = "Router"
87
+
88
+ function setQuery(query: Record<string, string>) {
89
+ const url = new URL(window.location.href)
90
+ Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v))
91
+ window.history.pushState({}, "", url.toString())
92
+ window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
93
+ }
94
+
95
+ /**
96
+ * Gets state and methods provided by a parent <Router>.
97
+ *
98
+ * @see https://kaioken.dev/docs/api/routing
99
+ */
100
+ export function useRouter() {
101
+ const { viewTransition, params, query } = useContext(RouterContext)
102
+ return { viewTransition, params, query, setQuery }
103
+ }
104
+
105
+ export function navigate(to: string, options?: { replace?: boolean }) {
106
+ const doNav = () => {
107
+ window.history[options?.replace ? "replaceState" : "pushState"]({}, "", to)
108
+ window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
109
+ }
110
+ // not called during render, just do the navigation
111
+ if (!node.current) return doNav(), null
112
+
113
+ const routerCtx = useContext(RouterContext, false)
114
+ if (routerCtx.isDefault) {
115
+ /**
116
+ * called from a non-router-decendant - postpone
117
+ * until next tick to avoid race conditions
118
+ */
119
+ const ctx = getVNodeAppContext(node.current)
120
+ return ctx.scheduler?.nextIdle(doNav), null
121
+ }
122
+ /**
123
+ * set the value of our router's syncNavCallback,
124
+ * causing it to be executed synchronously
125
+ * during the router's useLayoutEffect.
126
+ * consecutive calls to navigate will overwrite
127
+ * the previous value.
128
+ */
129
+ return routerCtx.queueSyncNav(doNav), null
130
+ }
131
+
132
+ export interface RouterProps {
133
+ /**
134
+ * Base path for all routes in this router. Use this
135
+ * to add a prefix to all routes
136
+ */
137
+ basePath?: string
138
+ /**
139
+ * Enable ViewTransition API for navigations
140
+ */
141
+ transition?: boolean
142
+ /**
143
+ * Children to render - the only supported children are <Route> components
144
+ */
145
+ children?: JSX.Children
146
+ }
147
+ const initLoc = () => ({
148
+ pathname: window.location.pathname,
149
+ search: window.location.search,
150
+ })
151
+
152
+ /**
153
+ * Main router component.
154
+ *
155
+ * @see https://kaioken.dev/docs/api/routing
156
+ */
157
+ export function Router(props: RouterProps) {
158
+ const appCtx = useAppContext()
159
+ const viewTransition = useRef<ViewTransition | null>(null)
160
+ const syncNavCallback = useRef<(() => void) | null>(null)
161
+ const parentRouterContext = useContext(RouterContext, false)
162
+ const dynamicParentPath = parentRouterContext.isDefault
163
+ ? null
164
+ : parentRouterContext.routePath
165
+ const dynamicParentPathSegments = useMemo(
166
+ () => dynamicParentPath?.split("/").filter(Boolean) || [],
167
+ [dynamicParentPath]
168
+ )
169
+
170
+ const [loc, setLoc] = useState(initLoc)
171
+ const query = useMemo(() => parseSearchParams(loc.search), [loc.search])
172
+ const realPathSegments = useMemo(
173
+ () => loc.pathname.split("/").filter(Boolean),
174
+ [loc.pathname]
175
+ )
176
+
177
+ useLayoutEffect(() => {
178
+ const handler = () => {
179
+ if (!document.startViewTransition || !props.transition) {
180
+ return setLoc({
181
+ pathname: window.location.pathname,
182
+ search: window.location.search,
183
+ })
184
+ }
185
+
186
+ viewTransition.current = document.startViewTransition(() => {
187
+ setLoc({
188
+ pathname: window.location.pathname,
189
+ search: window.location.search,
190
+ })
191
+ appCtx.flushSync()
192
+ })
193
+ viewTransition.current.finished.then(() => {
194
+ viewTransition.current = null
195
+ })
196
+ }
197
+ window.addEventListener("popstate", handler)
198
+ return () => window.removeEventListener("popstate", handler)
199
+ }, [])
200
+
201
+ useLayoutEffect(() => {
202
+ if (syncNavCallback.current) {
203
+ syncNavCallback.current()
204
+ syncNavCallback.current = null
205
+ }
206
+ })
207
+
208
+ type RouteComponent = Kaioken.VNode & {
209
+ props: Kaioken.InferProps<typeof Route>
210
+ }
211
+ let fallbackRoute: RouteComponent | undefined
212
+ let route: RouteComponent | undefined
213
+ const _children = (
214
+ Array.isArray(props.children) ? props.children : [props.children]
215
+ ).flat()
216
+
217
+ for (const child of _children) {
218
+ if (!isRoute(child)) continue
219
+
220
+ if (child.props.path === "*") {
221
+ if (__DEV__) {
222
+ if (fallbackRoute) {
223
+ console.warn(
224
+ "[kaioken]: More than one fallback route defined. Only the last one will be used."
225
+ )
226
+ }
227
+ }
228
+ fallbackRoute = child
229
+ continue
230
+ }
231
+ const dynamicChildPathSegments = ((props.basePath || "") + child.props.path)
232
+ .split("/")
233
+ .filter(Boolean)
234
+ if (
235
+ routeMatchesPath(
236
+ dynamicParentPathSegments.concat(dynamicChildPathSegments),
237
+ realPathSegments,
238
+ child.props.fallthrough
239
+ )
240
+ ) {
241
+ route = child
242
+ break
243
+ }
244
+ }
245
+
246
+ let parsedParams = {}
247
+ if (route) {
248
+ const dynamicChildPathSegments = ((props.basePath || "") + route.props.path)
249
+ .split("/")
250
+ .filter(Boolean)
251
+ parsedParams = parsePathParams(
252
+ dynamicParentPathSegments.concat(dynamicChildPathSegments),
253
+ realPathSegments
254
+ )
255
+ }
256
+ const params = { ...parentRouterContext.params, ...parsedParams }
257
+
258
+ return RouterContext.Provider({
259
+ value: {
260
+ params,
261
+ query,
262
+ routePath:
263
+ (dynamicParentPath || "") +
264
+ (props.basePath || "") +
265
+ (route?.props.path || ""),
266
+ basePath: props.basePath,
267
+ isDefault: false,
268
+ queueSyncNav: (callback: () => void) => {
269
+ syncNavCallback.current = callback
270
+ },
271
+ viewTransition: viewTransition,
272
+ },
273
+ children: route ?? fallbackRoute ?? null,
274
+ })
275
+ }
@@ -0,0 +1,49 @@
1
+ export { routeMatchesPath, parsePathParams, parseSearchParams }
2
+
3
+ function routeMatchesPath(
4
+ dynamicPathSegments: string[],
5
+ realPathSegments: string[],
6
+ fallthrough?: boolean
7
+ ) {
8
+ if (!fallthrough && dynamicPathSegments.length < realPathSegments.length) {
9
+ return false
10
+ }
11
+
12
+ for (let i = 0; i < dynamicPathSegments.length; i++) {
13
+ const segment = dynamicPathSegments[i]
14
+ if (segment.startsWith(":")) {
15
+ continue
16
+ } else if (segment !== realPathSegments[i]) {
17
+ return false
18
+ }
19
+ }
20
+
21
+ return true
22
+ }
23
+
24
+ function parsePathParams(
25
+ dynamicPathSegments: string[],
26
+ realPathSegments: string[]
27
+ ) {
28
+ const params: Record<string, string> = {}
29
+ for (let i = 0; i < dynamicPathSegments.length; i++) {
30
+ const segment = dynamicPathSegments[i]
31
+ if (segment.startsWith(":")) {
32
+ params[segment.slice(1)] = realPathSegments[i]
33
+ }
34
+ }
35
+ return params
36
+ }
37
+
38
+ function parseSearchParams(search: string) {
39
+ const parsed: Record<string, string> = {}
40
+ const str = search.split("?")[1]
41
+ if (!str || str === "") return parsed
42
+
43
+ const parts = str.split("&")
44
+ for (let i = 0; i < parts.length; i++) {
45
+ const [key, val] = parts[i].split("=")
46
+ parsed[key] = val
47
+ }
48
+ return parsed
49
+ }