kiru 0.50.0 → 0.50.2

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 (61) hide show
  1. package/dist/appContext.d.ts.map +1 -1
  2. package/dist/appContext.js +16 -6
  3. package/dist/appContext.js.map +1 -1
  4. package/dist/context.js.map +1 -1
  5. package/dist/dom.js +4 -4
  6. package/dist/dom.js.map +1 -1
  7. package/dist/globalContext.d.ts +3 -3
  8. package/dist/globalContext.d.ts.map +1 -1
  9. package/dist/globalContext.js +3 -15
  10. package/dist/globalContext.js.map +1 -1
  11. package/dist/hmr.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +0 -8
  14. package/dist/index.js.map +1 -1
  15. package/dist/reconciler.js +2 -2
  16. package/dist/reconciler.js.map +1 -1
  17. package/dist/router/fileRouter.d.ts +1 -28
  18. package/dist/router/fileRouter.d.ts.map +1 -1
  19. package/dist/router/fileRouter.js +2 -302
  20. package/dist/router/fileRouter.js.map +1 -1
  21. package/dist/router/fileRouterController.d.ts +28 -0
  22. package/dist/router/fileRouterController.d.ts.map +1 -0
  23. package/dist/router/fileRouterController.js +418 -0
  24. package/dist/router/fileRouterController.js.map +1 -0
  25. package/dist/router/globals.d.ts +1 -1
  26. package/dist/router/globals.d.ts.map +1 -1
  27. package/dist/router/index.d.ts +3 -3
  28. package/dist/router/index.d.ts.map +1 -1
  29. package/dist/router/index.js +2 -3
  30. package/dist/router/index.js.map +1 -1
  31. package/dist/router/{config.d.ts → pageConfig.d.ts} +1 -1
  32. package/dist/router/pageConfig.d.ts.map +1 -0
  33. package/dist/router/{config.js → pageConfig.js} +1 -1
  34. package/dist/router/pageConfig.js.map +1 -0
  35. package/dist/scheduler.js +7 -7
  36. package/dist/scheduler.js.map +1 -1
  37. package/dist/signals/watch.d.ts.map +1 -1
  38. package/dist/signals/watch.js +1 -2
  39. package/dist/signals/watch.js.map +1 -1
  40. package/dist/swr.js.map +1 -1
  41. package/dist/types.d.ts +1 -1
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/appContext.ts +18 -7
  45. package/src/context.ts +1 -1
  46. package/src/dom.ts +4 -4
  47. package/src/globalContext.ts +7 -20
  48. package/src/hmr.ts +2 -2
  49. package/src/index.ts +0 -9
  50. package/src/reconciler.ts +2 -2
  51. package/src/router/fileRouter.ts +4 -442
  52. package/src/router/fileRouterController.ts +591 -0
  53. package/src/router/globals.ts +1 -1
  54. package/src/router/index.ts +3 -3
  55. package/src/scheduler.ts +8 -8
  56. package/src/signals/watch.ts +2 -5
  57. package/src/swr.ts +1 -1
  58. package/src/types.ts +1 -1
  59. package/dist/router/config.d.ts.map +0 -1
  60. package/dist/router/config.js.map +0 -1
  61. /package/src/router/{config.ts → pageConfig.ts} +0 -0
@@ -1,349 +1,9 @@
1
- import { Signal, computed, flushSync } from "../index.js"
2
1
  import { __DEV__ } from "../env.js"
3
2
  import { createElement } from "../element.js"
4
3
  import { useState, useEffect } from "../hooks/index.js"
5
- import { RouterContext, type FileRouterContextType } from "./context.js"
6
- import { FileRouterDataLoadError } from "./errors.js"
7
- import { fileRouterInstance } from "./globals.js"
8
- import type {
9
- ErrorPageProps,
10
- FileRouterConfig,
11
- PageConfig,
12
- PageProps,
13
- RouteQuery,
14
- RouterState,
15
- } from "./types.js"
16
- import type {
17
- DefaultComponentModule,
18
- PageModule,
19
- ViteImportMap,
20
- } from "./types.internal.js"
21
-
22
- export class FileRouterController {
23
- private enableTransitions: boolean
24
- private pages: ViteImportMap
25
- private layouts: ViteImportMap
26
- private abortController: AbortController
27
- private currentPage: Signal<{
28
- component: Kiru.FC<any>
29
- config?: PageConfig
30
- route: string
31
- } | null>
32
- private currentPageProps: Signal<PageProps<PageConfig>>
33
- private currentLayouts: Signal<Kiru.FC[]>
34
- private loading: Signal<boolean>
35
- private state: Signal<RouterState>
36
- private contextValue: Signal<FileRouterContextType>
37
- private cleanups: (() => void)[] = []
38
- private filePathToPageRoute: Map<
39
- string,
40
- {
41
- route: string
42
- config: PageConfig
43
- }
44
- >
45
- private pageRouteToConfig: Map<string, PageConfig>
46
- private currentRoute: string | null
47
-
48
- constructor(props: FileRouterProps) {
49
- fileRouterInstance.current = this
50
- this.pages = {}
51
- this.layouts = {}
52
- this.abortController = new AbortController()
53
- this.currentPage = new Signal(null)
54
- this.currentPageProps = new Signal({})
55
- this.currentLayouts = new Signal([])
56
- this.loading = new Signal(true)
57
- this.state = new Signal<RouterState>({
58
- path: window.location.pathname,
59
- params: {},
60
- query: {},
61
- signal: this.abortController.signal,
62
- })
63
- this.contextValue = computed<FileRouterContextType>(() => ({
64
- state: this.state.value,
65
- navigate: this.navigate.bind(this),
66
- setQuery: this.setQuery.bind(this),
67
- reload: (options?: { transition?: boolean }) =>
68
- this.loadRoute(void 0, void 0, options?.transition),
69
- }))
70
- this.filePathToPageRoute = new Map()
71
- this.pageRouteToConfig = new Map()
72
- this.currentRoute = null
73
-
74
- const {
75
- pages,
76
- layouts,
77
- dir = "/pages",
78
- baseUrl = "/",
79
- transition,
80
- } = props.config
81
- this.enableTransitions = !!transition
82
- const [normalizedDir, normalizedBaseUrl] = [
83
- normalizePrefixPath(dir),
84
- normalizePrefixPath(baseUrl),
85
- ]
86
- this.pages = formatViteImportMap(
87
- pages as ViteImportMap,
88
- normalizedDir,
89
- normalizedBaseUrl
90
- )
91
- this.layouts = formatViteImportMap(
92
- layouts as ViteImportMap,
93
- normalizedDir,
94
- normalizedBaseUrl
95
- )
96
-
97
- this.loadRoute()
98
-
99
- const handlePopState = () => this.loadRoute()
100
- window.addEventListener("popstate", handlePopState)
101
- this.cleanups.push(() =>
102
- window.removeEventListener("popstate", handlePopState)
103
- )
104
- }
105
-
106
- public onPageConfigDefined<T extends PageConfig>(fp: string, config: T) {
107
- const existing = this.filePathToPageRoute.get(fp)
108
- if (existing === undefined) {
109
- const route = this.currentRoute
110
- if (!route) return
111
- this.filePathToPageRoute.set(fp, { route, config })
112
- return
113
- }
114
- const curPage = this.currentPage.value
115
- if (curPage?.route === existing.route && config.loader) {
116
- const p = this.currentPageProps.value
117
- let transition = this.enableTransitions
118
- if (config.loader.transition !== undefined) {
119
- transition = config.loader.transition
120
- }
121
- const props = {
122
- ...p,
123
- loading: true,
124
- data: null,
125
- error: null,
126
- }
127
- handleStateTransition(this.state.value.signal, transition, () => {
128
- this.currentPageProps.value = props
129
- })
130
-
131
- this.loadRouteData(config.loader, props, this.state.value, transition)
132
- }
133
-
134
- this.pageRouteToConfig.set(existing.route, config)
135
- }
136
-
137
- public getContextValue() {
138
- return this.contextValue.value
139
- }
140
-
141
- public getChildren() {
142
- const page = this.currentPage.value,
143
- props = this.currentPageProps.value,
144
- layouts = this.currentLayouts.value
145
-
146
- if (page) {
147
- // Wrap component with layouts (outermost first)
148
- return layouts.reduceRight(
149
- (children, Layout) => createElement(Layout, { children }),
150
- createElement(page.component, props)
151
- )
152
- }
153
-
154
- return null
155
- }
156
-
157
- public dispose() {
158
- this.cleanups.forEach((cleanup) => cleanup())
159
- }
160
-
161
- private matchRoute(pathSegments: string[]) {
162
- outer: for (const [route, pageModuleLoader] of Object.entries(this.pages)) {
163
- const routeSegments = route.split("/").filter(Boolean)
164
-
165
- const pathMatchingSegments = routeSegments.filter(
166
- (seg) => !seg.startsWith("(") && !seg.endsWith(")")
167
- )
168
-
169
- if (pathMatchingSegments.length !== pathSegments.length) {
170
- continue
171
- }
172
- const params: Record<string, string> = {}
173
-
174
- for (let i = 0; i < pathMatchingSegments.length; i++) {
175
- const routeSeg = pathMatchingSegments[i]
176
- if (routeSeg.startsWith(":")) {
177
- const key = routeSeg.slice(1)
178
- params[key] = pathSegments[i]
179
- continue
180
- }
181
- if (routeSeg !== pathSegments[i]) {
182
- continue outer
183
- }
184
- }
185
-
186
- return { route, pageModuleLoader, params, routeSegments }
187
- }
188
-
189
- return null
190
- }
191
-
192
- private async loadRoute(
193
- path: string = window.location.pathname,
194
- props: PageProps<PageConfig> = {},
195
- enableTransition = this.enableTransitions
196
- ): Promise<void> {
197
- this.loading.value = true
198
- this.abortController?.abort()
199
-
200
- const query = parseQuery(window.location.search)
201
- const controller = (this.abortController = new AbortController())
202
- const signal = controller.signal
203
-
204
- try {
205
- const pathSegments = path.split("/").filter(Boolean)
206
- const routeMatch = this.matchRoute(pathSegments)
207
-
208
- if (!routeMatch) {
209
- const _404 = this.matchRoute(["404"])
210
- if (!_404) {
211
- if (__DEV__) {
212
- console.error(
213
- `No 404 route defined (path: ${path}). See https://kirujs.dev/404 for more information.`
214
- )
215
- }
216
- return
217
- }
218
- const errorProps = {
219
- source: { path },
220
- } satisfies ErrorPageProps
221
-
222
- return this.navigate("/404", { replace: true, props: errorProps })
223
- }
224
-
225
- const { route, pageModuleLoader, params, routeSegments } = routeMatch
226
-
227
- this.currentRoute = route
228
- const pagePromise = pageModuleLoader()
229
-
230
- const layoutPromises = ["/", ...routeSegments].reduce((acc, _, i) => {
231
- const layoutPath = "/" + routeSegments.slice(0, i).join("/")
232
- const layoutLoad = this.layouts[layoutPath]
233
-
234
- if (!layoutLoad) {
235
- return acc
236
- }
237
-
238
- return [...acc, layoutLoad()]
239
- }, [] as Promise<DefaultComponentModule>[])
240
-
241
- const [page, ...layouts] = await Promise.all([
242
- pagePromise,
243
- ...layoutPromises,
244
- ])
245
-
246
- this.currentRoute = null
247
- if (controller.signal.aborted) return
248
-
249
- if (typeof page.default !== "function") {
250
- throw new Error("Route component must be a default exported function")
251
- }
252
-
253
- const routerState: RouterState = {
254
- path,
255
- params,
256
- query,
257
- signal,
258
- }
259
-
260
- let config = (page as unknown as PageModule).config
261
- if (this.pageRouteToConfig.has(route)) {
262
- config = this.pageRouteToConfig.get(route)
263
- }
264
-
265
- if (config?.loader) {
266
- props = { ...props, loading: true, data: null, error: null }
267
- this.loadRouteData(config.loader, props, routerState, enableTransition)
268
- }
269
-
270
- handleStateTransition(signal, enableTransition, () => {
271
- this.currentPage.value = {
272
- component: page.default,
273
- config,
274
- route: "/" + routeSegments.join("/"),
275
- }
276
- this.state.value = routerState
277
- this.currentPageProps.value = props
278
- this.currentLayouts.value = layouts
279
- .filter((m) => typeof m.default === "function")
280
- .map((m) => m.default)
281
- })
282
- } catch (error) {
283
- console.error("Failed to load route component:", error)
284
- this.currentPage.value = null
285
- } finally {
286
- this.loading.value = false
287
- }
288
- }
289
-
290
- private async loadRouteData(
291
- loader: NonNullable<PageConfig["loader"]>,
292
- props: PageProps<PageConfig>,
293
- routerState: RouterState,
294
- enableTransition = this.enableTransitions
295
- ) {
296
- loader
297
- .load(routerState)
298
- .then(
299
- (data) => ({ data, error: null }),
300
- (error) => ({
301
- data: null,
302
- error: new FileRouterDataLoadError(error),
303
- })
304
- )
305
- .then(({ data, error }) => {
306
- if (routerState.signal.aborted) return
307
-
308
- let transition = enableTransition
309
- if (loader.transition !== undefined) {
310
- transition = loader.transition
311
- }
312
-
313
- handleStateTransition(routerState.signal, transition, () => {
314
- this.currentPageProps.value = {
315
- ...props,
316
- loading: false,
317
- data,
318
- error,
319
- }
320
- })
321
- })
322
- }
323
-
324
- private async navigate(
325
- path: string,
326
- options?: {
327
- replace?: boolean
328
- transition?: boolean
329
- props?: Record<string, unknown>
330
- }
331
- ) {
332
- const f = options?.replace ? "replaceState" : "pushState"
333
- window.history[f]({}, "", path)
334
- window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
335
- return this.loadRoute(path, options?.props, options?.transition)
336
- }
337
-
338
- private setQuery(query: RouteQuery) {
339
- const queryString = buildQueryString(query)
340
- const newUrl = `${this.state.value.path}${
341
- queryString ? `?${queryString}` : ""
342
- }`
343
- window.history.pushState(null, "", newUrl)
344
- this.state.value = { ...this.state.value, query }
345
- }
346
- }
4
+ import { RouterContext } from "./context.js"
5
+ import { FileRouterController } from "./fileRouterController.js"
6
+ import type { FileRouterConfig } from "./types.js"
347
7
 
348
8
  export interface FileRouterProps {
349
9
  /**
@@ -365,7 +25,7 @@ export interface FileRouterProps {
365
25
  }
366
26
 
367
27
  export function FileRouter(props: FileRouterProps): JSX.Element {
368
- const [controller] = useState(() => new FileRouterController(props))
28
+ const [controller] = useState(() => new FileRouterController(props.config))
369
29
  useEffect(() => () => controller.dispose(), [controller])
370
30
 
371
31
  return createElement(
@@ -374,101 +34,3 @@ export function FileRouter(props: FileRouterProps): JSX.Element {
374
34
  controller.getChildren()
375
35
  )
376
36
  }
377
-
378
- // Utility functions
379
-
380
- function parseQuery(
381
- search: string
382
- ): Record<string, string | string[] | undefined> {
383
- const params = new URLSearchParams(search)
384
- const query: Record<string, string | string[] | undefined> = {}
385
-
386
- for (const [key, value] of params.entries()) {
387
- if (query[key]) {
388
- // Convert to array if multiple values
389
- if (Array.isArray(query[key])) {
390
- ;(query[key] as string[]).push(value)
391
- } else {
392
- query[key] = [query[key] as string, value]
393
- }
394
- } else {
395
- query[key] = value
396
- }
397
- }
398
-
399
- return query
400
- }
401
-
402
- function buildQueryString(
403
- query: Record<string, string | string[] | undefined>
404
- ): string {
405
- const params = new URLSearchParams()
406
-
407
- for (const [key, value] of Object.entries(query)) {
408
- if (value !== undefined) {
409
- if (Array.isArray(value)) {
410
- value.forEach((v) => params.append(key, v))
411
- } else {
412
- params.set(key, value)
413
- }
414
- }
415
- }
416
-
417
- return params.toString()
418
- }
419
-
420
- function formatViteImportMap(
421
- map: ViteImportMap,
422
- dir: string,
423
- baseUrl: string
424
- ): ViteImportMap {
425
- return Object.keys(map).reduce((acc, key) => {
426
- let k = key
427
- const dirIndex = k.indexOf(dir)
428
- if (dirIndex === -1) {
429
- return acc
430
- }
431
-
432
- k = k.slice(dirIndex + dir.length)
433
- while (k.startsWith("/")) {
434
- k = k.slice(1)
435
- }
436
- k = k.split("/").slice(0, -1).join("/") // remove filename
437
- k = k.replace(/\[([^\]]+)\]/g, ":$1") // replace [param] with :param
438
-
439
- return {
440
- ...acc,
441
- [baseUrl + k]: map[key],
442
- }
443
- }, {})
444
- }
445
-
446
- function normalizePrefixPath(path: string) {
447
- while (path.startsWith(".")) {
448
- path = path.slice(1)
449
- }
450
- path = `/${path}/`
451
- while (path.startsWith("//")) {
452
- path = path.slice(1)
453
- }
454
- while (path.endsWith("//")) {
455
- path = path.slice(0, -1)
456
- }
457
- return path
458
- }
459
-
460
- function handleStateTransition(
461
- signal: AbortSignal,
462
- enableTransition: boolean,
463
- callback: () => void
464
- ) {
465
- if (!enableTransition || typeof document.startViewTransition !== "function") {
466
- return callback()
467
- }
468
- const vt = document.startViewTransition(() => {
469
- callback()
470
- flushSync()
471
- })
472
-
473
- signal.addEventListener("abort", () => vt.skipTransition())
474
- }