kiru 0.50.0-preview.2 → 0.50.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 (76) hide show
  1. package/dist/appContext.d.ts.map +1 -1
  2. package/dist/appContext.js +11 -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/context.d.ts +13 -0
  18. package/dist/router/context.d.ts.map +1 -1
  19. package/dist/router/context.js.map +1 -1
  20. package/dist/router/fileRouter.d.ts +1 -28
  21. package/dist/router/fileRouter.d.ts.map +1 -1
  22. package/dist/router/fileRouter.js +2 -303
  23. package/dist/router/fileRouter.js.map +1 -1
  24. package/dist/router/fileRouterController.d.ts +28 -0
  25. package/dist/router/fileRouterController.d.ts.map +1 -0
  26. package/dist/router/fileRouterController.js +418 -0
  27. package/dist/router/fileRouterController.js.map +1 -0
  28. package/dist/router/globals.d.ts +1 -1
  29. package/dist/router/globals.d.ts.map +1 -1
  30. package/dist/router/index.d.ts +3 -3
  31. package/dist/router/index.d.ts.map +1 -1
  32. package/dist/router/index.js +2 -3
  33. package/dist/router/index.js.map +1 -1
  34. package/dist/router/link.d.ts +9 -0
  35. package/dist/router/link.d.ts.map +1 -1
  36. package/dist/router/link.js.map +1 -1
  37. package/dist/router/{config.d.ts → pageConfig.d.ts} +1 -1
  38. package/dist/router/pageConfig.d.ts.map +1 -0
  39. package/dist/router/{config.js → pageConfig.js} +1 -1
  40. package/dist/router/pageConfig.js.map +1 -0
  41. package/dist/router/types.d.ts +24 -0
  42. package/dist/router/types.d.ts.map +1 -1
  43. package/dist/scheduler.js +7 -7
  44. package/dist/scheduler.js.map +1 -1
  45. package/dist/signals/base.d.ts.map +1 -1
  46. package/dist/signals/base.js +6 -3
  47. package/dist/signals/base.js.map +1 -1
  48. package/dist/signals/watch.d.ts.map +1 -1
  49. package/dist/signals/watch.js +1 -2
  50. package/dist/signals/watch.js.map +1 -1
  51. package/dist/swr.js.map +1 -1
  52. package/dist/types.d.ts +1 -1
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/src/appContext.ts +13 -7
  56. package/src/context.ts +1 -1
  57. package/src/dom.ts +4 -4
  58. package/src/globalContext.ts +7 -20
  59. package/src/hmr.ts +2 -2
  60. package/src/index.ts +0 -9
  61. package/src/reconciler.ts +2 -2
  62. package/src/router/context.ts +13 -0
  63. package/src/router/fileRouter.ts +4 -442
  64. package/src/router/fileRouterController.ts +591 -0
  65. package/src/router/globals.ts +1 -1
  66. package/src/router/index.ts +3 -3
  67. package/src/router/link.ts +9 -0
  68. package/src/router/types.ts +24 -0
  69. package/src/scheduler.ts +8 -8
  70. package/src/signals/base.ts +6 -4
  71. package/src/signals/watch.ts +2 -5
  72. package/src/swr.ts +1 -1
  73. package/src/types.ts +1 -1
  74. package/dist/router/config.d.ts.map +0 -1
  75. package/dist/router/config.js.map +0 -1
  76. /package/src/router/{config.ts → pageConfig.ts} +0 -0
@@ -0,0 +1,591 @@
1
+ import { Signal, computed, flushSync } from "../index.js"
2
+ import { __DEV__ } from "../env.js"
3
+ import { createElement } from "../element.js"
4
+ import { type FileRouterContextType } from "./context.js"
5
+ import { FileRouterDataLoadError } from "./errors.js"
6
+ import { fileRouterInstance } from "./globals.js"
7
+ import type {
8
+ ErrorPageProps,
9
+ FileRouterConfig,
10
+ PageConfig,
11
+ PageProps,
12
+ RouteQuery,
13
+ RouterState,
14
+ } from "./types.js"
15
+ import type {
16
+ DefaultComponentModule,
17
+ PageModule,
18
+ ViteImportMap,
19
+ } from "./types.internal.js"
20
+
21
+ interface FormattedViteImportMap {
22
+ [key: string]: {
23
+ load: () => Promise<DefaultComponentModule>
24
+ specificity: number
25
+ segments: string[]
26
+ filePath?: string
27
+ }
28
+ }
29
+
30
+ export class FileRouterController {
31
+ private enableTransitions: boolean
32
+ private pages: FormattedViteImportMap
33
+ private layouts: FormattedViteImportMap
34
+ private abortController: AbortController
35
+ private currentPage: Signal<{
36
+ component: Kiru.FC<any>
37
+ config?: PageConfig
38
+ route: string
39
+ } | null>
40
+ private currentPageProps: Signal<PageProps<PageConfig>>
41
+ private currentLayouts: Signal<Kiru.FC[]>
42
+ private state: Signal<RouterState>
43
+ private contextValue: Signal<FileRouterContextType>
44
+ private cleanups: (() => void)[] = []
45
+ private filePathToPageRoute?: Map<
46
+ string,
47
+ { route: string; config: PageConfig }
48
+ >
49
+ private pageRouteToConfig?: Map<string, PageConfig>
50
+ private currentRoute: string | null
51
+
52
+ constructor(config: FileRouterConfig) {
53
+ fileRouterInstance.current = this
54
+ this.pages = {}
55
+ this.layouts = {}
56
+ this.abortController = new AbortController()
57
+ this.currentPage = new Signal(null)
58
+ this.currentPageProps = new Signal({})
59
+ this.currentLayouts = new Signal([])
60
+ this.state = new Signal<RouterState>({
61
+ path: window.location.pathname,
62
+ params: {},
63
+ query: {},
64
+ signal: this.abortController.signal,
65
+ })
66
+ this.contextValue = computed<FileRouterContextType>(() => ({
67
+ state: this.state.value,
68
+ navigate: this.navigate.bind(this),
69
+ setQuery: this.setQuery.bind(this),
70
+ reload: (options?: { transition?: boolean }) =>
71
+ this.loadRoute(void 0, void 0, options?.transition),
72
+ }))
73
+ if (__DEV__) {
74
+ this.filePathToPageRoute = new Map()
75
+ this.pageRouteToConfig = new Map()
76
+ }
77
+ this.currentRoute = null
78
+
79
+ const { pages, layouts, dir = "/pages", baseUrl = "/", transition } = config
80
+ this.enableTransitions = !!transition
81
+ const [normalizedDir, normalizedBaseUrl] = [
82
+ normalizePrefixPath(dir),
83
+ normalizePrefixPath(baseUrl),
84
+ ]
85
+ this.pages = formatViteImportMap(
86
+ pages as ViteImportMap,
87
+ normalizedDir,
88
+ normalizedBaseUrl
89
+ )
90
+ if (__DEV__) {
91
+ validateRoutes(this.pages)
92
+ }
93
+ this.layouts = formatViteImportMap(
94
+ layouts as ViteImportMap,
95
+ normalizedDir,
96
+ normalizedBaseUrl
97
+ )
98
+
99
+ this.loadRoute()
100
+
101
+ const handlePopState = () => this.loadRoute()
102
+ window.addEventListener("popstate", handlePopState)
103
+ this.cleanups.push(() =>
104
+ window.removeEventListener("popstate", handlePopState)
105
+ )
106
+ }
107
+
108
+ public onPageConfigDefined<T extends PageConfig>(fp: string, config: T) {
109
+ const existing = this.filePathToPageRoute?.get(fp)
110
+ if (existing === undefined) {
111
+ const route = this.currentRoute
112
+ if (!route) return
113
+ this.filePathToPageRoute?.set(fp, { route, config })
114
+ return
115
+ }
116
+ const curPage = this.currentPage.value
117
+ if (curPage?.route === existing.route && config.loader) {
118
+ const p = this.currentPageProps.value
119
+ let transition = this.enableTransitions
120
+ if (config.loader.transition !== undefined) {
121
+ transition = config.loader.transition
122
+ }
123
+ const props = {
124
+ ...p,
125
+ loading: true,
126
+ data: null,
127
+ error: null,
128
+ }
129
+ handleStateTransition(this.state.value.signal, transition, () => {
130
+ this.currentPageProps.value = props
131
+ })
132
+
133
+ this.loadRouteData(config.loader, props, this.state.value, transition)
134
+ }
135
+
136
+ this.pageRouteToConfig?.set(existing.route, config)
137
+ }
138
+
139
+ public getContextValue() {
140
+ return this.contextValue.value
141
+ }
142
+
143
+ public getChildren() {
144
+ const page = this.currentPage.value,
145
+ props = this.currentPageProps.value,
146
+ layouts = this.currentLayouts.value
147
+
148
+ if (page) {
149
+ // Wrap component with layouts (outermost first)
150
+ return layouts.reduceRight(
151
+ (children, Layout) => createElement(Layout, { children }),
152
+ createElement(page.component, props)
153
+ )
154
+ }
155
+
156
+ return null
157
+ }
158
+
159
+ public dispose() {
160
+ this.cleanups.forEach((cleanup) => cleanup())
161
+ }
162
+
163
+ private matchRoute(pathSegments: string[]) {
164
+ const matches: Array<{
165
+ route: string
166
+ pageEntry: FormattedViteImportMap[string]
167
+ params: Record<string, string>
168
+ routeSegments: string[]
169
+ }> = []
170
+
171
+ // Find all matching routes
172
+ outer: for (const [route, pageEntry] of Object.entries(this.pages)) {
173
+ const routeSegments = pageEntry.segments
174
+ const pathMatchingSegments = routeSegments.filter(
175
+ (seg) => !seg.startsWith("(") && !seg.endsWith(")")
176
+ )
177
+
178
+ const params: Record<string, string> = {}
179
+ let hasCatchall = false
180
+
181
+ // Check if route matches
182
+ for (
183
+ let i = 0;
184
+ i < pathMatchingSegments.length && i < pathSegments.length;
185
+ i++
186
+ ) {
187
+ const routeSeg = pathMatchingSegments[i]
188
+
189
+ if (routeSeg.startsWith(":")) {
190
+ const key = routeSeg.slice(1)
191
+
192
+ if (routeSeg.endsWith("*")) {
193
+ // Catchall route - matches remaining segments
194
+ hasCatchall = true
195
+ const catchallKey = key.slice(0, -1) // Remove the *
196
+ params[catchallKey] = pathSegments.slice(i).join("/")
197
+ break
198
+ } else {
199
+ // Regular dynamic segment
200
+ if (i >= pathSegments.length) {
201
+ continue outer
202
+ }
203
+ params[key] = pathSegments[i]
204
+ }
205
+ } else {
206
+ // Static segment
207
+ if (routeSeg !== pathSegments[i]) {
208
+ continue outer
209
+ }
210
+ }
211
+ }
212
+
213
+ // For non-catchall routes, ensure exact length match
214
+ if (!hasCatchall && pathMatchingSegments.length !== pathSegments.length) {
215
+ continue
216
+ }
217
+
218
+ matches.push({
219
+ route,
220
+ pageEntry,
221
+ params,
222
+ routeSegments,
223
+ })
224
+ }
225
+
226
+ // Sort by specificity (highest first) and return the best match
227
+ if (matches.length === 0) {
228
+ return null
229
+ }
230
+
231
+ matches.sort((a, b) => b.pageEntry.specificity - a.pageEntry.specificity)
232
+ const bestMatch = matches[0]
233
+
234
+ return bestMatch
235
+ }
236
+
237
+ private async loadRoute(
238
+ path: string = window.location.pathname,
239
+ props: PageProps<PageConfig> = {},
240
+ enableTransition = this.enableTransitions
241
+ ): Promise<void> {
242
+ this.abortController?.abort()
243
+ const signal = (this.abortController = new AbortController()).signal
244
+
245
+ try {
246
+ const pathSegments = path.split("/").filter(Boolean)
247
+ const routeMatch = this.matchRoute(pathSegments)
248
+
249
+ if (!routeMatch) {
250
+ const _404 = this.matchRoute(["404"])
251
+ if (!_404) {
252
+ if (__DEV__) {
253
+ console.error(
254
+ `[kiru/router]: No 404 route defined (path: ${path}).
255
+ See https://kirujs.dev/docs/api/file-router#404 for more information.`
256
+ )
257
+ }
258
+ return
259
+ }
260
+ const errorProps = {
261
+ source: { path },
262
+ } satisfies ErrorPageProps
263
+
264
+ return this.navigate("/404", { replace: true, props: errorProps })
265
+ }
266
+
267
+ const { route, pageEntry, params, routeSegments } = routeMatch
268
+
269
+ this.currentRoute = route
270
+ const pagePromise = pageEntry.load()
271
+
272
+ const layoutPromises = ["/", ...routeSegments].reduce((acc, _, i) => {
273
+ const layoutPath = "/" + routeSegments.slice(0, i).join("/")
274
+ const layout = this.layouts[layoutPath]
275
+
276
+ if (!layout) {
277
+ return acc
278
+ }
279
+
280
+ return [...acc, layout.load()]
281
+ }, [] as Promise<DefaultComponentModule>[])
282
+
283
+ const query = parseQuery(window.location.search)
284
+ const [page, ...layouts] = await Promise.all([
285
+ pagePromise,
286
+ ...layoutPromises,
287
+ ])
288
+
289
+ this.currentRoute = null
290
+ if (signal.aborted) return
291
+
292
+ if (typeof page.default !== "function") {
293
+ throw new Error(
294
+ "[kiru/router]: Route component must be a default exported function"
295
+ )
296
+ }
297
+
298
+ const routerState: RouterState = {
299
+ path,
300
+ params,
301
+ query,
302
+ signal,
303
+ }
304
+
305
+ let config = (page as unknown as PageModule).config
306
+ if (__DEV__) {
307
+ if (this.pageRouteToConfig?.has(route)) {
308
+ config = this.pageRouteToConfig.get(route)
309
+ }
310
+ }
311
+
312
+ if (config?.loader) {
313
+ props = { ...props, loading: true, data: null, error: null }
314
+ this.loadRouteData(config.loader, props, routerState, enableTransition)
315
+ }
316
+
317
+ this.state.value = routerState
318
+ handleStateTransition(signal, enableTransition, () => {
319
+ this.currentPage.value = {
320
+ component: page.default,
321
+ config,
322
+ route: "/" + routeSegments.join("/"),
323
+ }
324
+ this.currentPageProps.value = props
325
+ this.currentLayouts.value = layouts
326
+ .filter((m) => typeof m.default === "function")
327
+ .map((m) => m.default)
328
+ })
329
+ } catch (error) {
330
+ console.error("[kiru/router]: Failed to load route component:", error)
331
+ this.currentPage.value = null
332
+ }
333
+ }
334
+
335
+ private async loadRouteData(
336
+ loader: NonNullable<PageConfig["loader"]>,
337
+ props: PageProps<PageConfig>,
338
+ routerState: RouterState,
339
+ enableTransition = this.enableTransitions
340
+ ) {
341
+ loader
342
+ .load(routerState)
343
+ .then(
344
+ (data) => ({ data, error: null }),
345
+ (error) => ({
346
+ data: null,
347
+ error: new FileRouterDataLoadError(error),
348
+ })
349
+ )
350
+ .then(({ data, error }) => {
351
+ if (routerState.signal.aborted) return
352
+
353
+ let transition = enableTransition
354
+ if (loader.transition !== undefined) {
355
+ transition = loader.transition
356
+ }
357
+
358
+ handleStateTransition(routerState.signal, transition, () => {
359
+ this.currentPageProps.value = {
360
+ ...props,
361
+ loading: false,
362
+ data,
363
+ error,
364
+ }
365
+ })
366
+ })
367
+ }
368
+
369
+ private async navigate(
370
+ path: string,
371
+ options?: {
372
+ replace?: boolean
373
+ transition?: boolean
374
+ props?: Record<string, unknown>
375
+ }
376
+ ) {
377
+ const f = options?.replace ? "replaceState" : "pushState"
378
+ window.history[f]({}, "", path)
379
+ window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
380
+ return this.loadRoute(path, options?.props, options?.transition)
381
+ }
382
+
383
+ private setQuery(query: RouteQuery) {
384
+ const queryString = buildQueryString(query)
385
+ const newUrl = `${this.state.value.path}${
386
+ queryString ? `?${queryString}` : ""
387
+ }`
388
+ window.history.pushState(null, "", newUrl)
389
+ this.state.value = { ...this.state.value, query }
390
+ }
391
+ }
392
+
393
+ function parseQuery(
394
+ search: string
395
+ ): Record<string, string | string[] | undefined> {
396
+ const params = new URLSearchParams(search)
397
+ const query: Record<string, string | string[] | undefined> = {}
398
+
399
+ for (const [key, value] of params.entries()) {
400
+ if (query[key]) {
401
+ // Convert to array if multiple values
402
+ if (Array.isArray(query[key])) {
403
+ ;(query[key] as string[]).push(value)
404
+ } else {
405
+ query[key] = [query[key] as string, value]
406
+ }
407
+ } else {
408
+ query[key] = value
409
+ }
410
+ }
411
+
412
+ return query
413
+ }
414
+
415
+ function buildQueryString(
416
+ query: Record<string, string | string[] | undefined>
417
+ ): string {
418
+ const params = new URLSearchParams()
419
+
420
+ for (const [key, value] of Object.entries(query)) {
421
+ if (value !== undefined) {
422
+ if (Array.isArray(value)) {
423
+ value.forEach((v) => params.append(key, v))
424
+ } else {
425
+ params.set(key, value)
426
+ }
427
+ }
428
+ }
429
+
430
+ return params.toString()
431
+ }
432
+
433
+ function formatViteImportMap(
434
+ map: ViteImportMap,
435
+ dir: string,
436
+ baseUrl: string
437
+ ): FormattedViteImportMap {
438
+ return Object.keys(map).reduce<FormattedViteImportMap>((acc, key) => {
439
+ const dirIndex = key.indexOf(dir)
440
+ if (dirIndex === -1) {
441
+ return acc
442
+ }
443
+
444
+ let specificity = 0
445
+ let k = key.slice(dirIndex + dir.length)
446
+ while (k.startsWith("/")) {
447
+ k = k.slice(1)
448
+ }
449
+ const segments: string[] = []
450
+ const parts = k.split("/").slice(0, -1)
451
+
452
+ for (let i = 0; i < parts.length; i++) {
453
+ const part = parts[i]
454
+ if (part.startsWith("[...") && part.endsWith("]")) {
455
+ if (i !== parts.length - 1) {
456
+ throw new Error(
457
+ `[kiru/router]: Catchall must be the folder name. Got "${key}"`
458
+ )
459
+ }
460
+ segments.push(`:${part.slice(4, -1)}*`)
461
+ specificity += 1
462
+ break
463
+ }
464
+ if (part.startsWith("[") && part.endsWith("]")) {
465
+ segments.push(`:${part.slice(1, -1)}`)
466
+ specificity += 10
467
+ continue
468
+ }
469
+ specificity += 100
470
+ segments.push(part)
471
+ }
472
+
473
+ const value: FormattedViteImportMap[string] = {
474
+ load: map[key],
475
+ specificity,
476
+ segments,
477
+ }
478
+
479
+ if (__DEV__) {
480
+ value.filePath = key
481
+ }
482
+
483
+ return {
484
+ ...acc,
485
+ [baseUrl + segments.join("/")]: value,
486
+ }
487
+ }, {})
488
+ }
489
+
490
+ function normalizePrefixPath(path: string) {
491
+ while (path.startsWith(".")) {
492
+ path = path.slice(1)
493
+ }
494
+ path = `/${path}/`
495
+ while (path.startsWith("//")) {
496
+ path = path.slice(1)
497
+ }
498
+ while (path.endsWith("//")) {
499
+ path = path.slice(0, -1)
500
+ }
501
+ return path
502
+ }
503
+
504
+ function handleStateTransition(
505
+ signal: AbortSignal,
506
+ enableTransition: boolean,
507
+ callback: () => void
508
+ ) {
509
+ if (!enableTransition || typeof document.startViewTransition !== "function") {
510
+ return callback()
511
+ }
512
+ const vt = document.startViewTransition(() => {
513
+ callback()
514
+ flushSync()
515
+ })
516
+
517
+ signal.addEventListener("abort", () => vt.skipTransition())
518
+ }
519
+
520
+ function validateRoutes(pageMap: FormattedViteImportMap) {
521
+ type Entry = FormattedViteImportMap[string]
522
+ const routeConflicts: [Entry, Entry][] = []
523
+ const routes = Object.keys(pageMap)
524
+ for (let i = 0; i < routes.length; i++) {
525
+ for (let j = i + 1; j < routes.length; j++) {
526
+ const route1 = routes[i]
527
+ const route2 = routes[j]
528
+
529
+ if (routesConflict(route1, route2)) {
530
+ routeConflicts.push([pageMap[route1], pageMap[route2]])
531
+ }
532
+ }
533
+ }
534
+
535
+ if (routeConflicts.length > 0) {
536
+ let warning = "[kiru/router]: Route conflicts detected:\n"
537
+ warning += routeConflicts.map(([route1, route2]) => {
538
+ return ` - "${route1.filePath}" conflicts with "${route2.filePath}"\n`
539
+ })
540
+ warning += "Routes are ordered by specificity (higher specificity wins)"
541
+ console.warn(warning)
542
+ }
543
+ }
544
+
545
+ function routesConflict(route1: string, route2: string): boolean {
546
+ const segments1 = route1.split("/").filter(Boolean)
547
+ const segments2 = route2.split("/").filter(Boolean)
548
+
549
+ // Filter out route groups for comparison
550
+ const pathSegments1 = segments1.filter(
551
+ (seg) => !seg.startsWith("(") && !seg.endsWith(")")
552
+ )
553
+ const pathSegments2 = segments2.filter(
554
+ (seg) => !seg.startsWith("(") && !seg.endsWith(")")
555
+ )
556
+
557
+ // Routes conflict if they have the same path structure
558
+ if (pathSegments1.length !== pathSegments2.length) {
559
+ return false
560
+ }
561
+
562
+ for (let i = 0; i < pathSegments1.length; i++) {
563
+ const seg1 = pathSegments1[i]
564
+ const seg2 = pathSegments2[i]
565
+
566
+ // If both are static segments, they must match exactly
567
+ if (!seg1.startsWith(":") && !seg2.startsWith(":")) {
568
+ if (seg1 !== seg2) {
569
+ return false
570
+ }
571
+ }
572
+ // If one is static and one is dynamic, they conflict
573
+ else if (
574
+ (seg1.startsWith(":") && !seg2.startsWith(":")) ||
575
+ (!seg1.startsWith(":") && seg2.startsWith(":"))
576
+ ) {
577
+ return false
578
+ }
579
+ // If both are dynamic, they conflict
580
+ else if (seg1.startsWith(":") && seg2.startsWith(":")) {
581
+ // Both are dynamic, check if they're the same type
582
+ const isCatchall1 = seg1.endsWith("*")
583
+ const isCatchall2 = seg2.endsWith("*")
584
+ if (isCatchall1 !== isCatchall2) {
585
+ return false
586
+ }
587
+ }
588
+ }
589
+
590
+ return true
591
+ }
@@ -1,4 +1,4 @@
1
- import type { FileRouterController } from "./fileRouter"
1
+ import type { FileRouterController } from "./fileRouterController"
2
2
 
3
3
  export const fileRouterInstance = {
4
4
  current: null as FileRouterController | null,
@@ -1,6 +1,6 @@
1
- export * from "./config.js"
1
+ export { useFileRouter, type FileRouterContextType } from "./context.js"
2
2
  export * from "./errors.js"
3
3
  export { FileRouter, type FileRouterProps } from "./fileRouter.js"
4
- export { useFileRouter } from "./context.js"
5
4
  export * from "./link.js"
6
- export * from "./types.js"
5
+ export * from "./pageConfig.js"
6
+ export type * from "./types.js"
@@ -4,8 +4,17 @@ import { useCallback } from "../hooks/index.js"
4
4
  import { useFileRouter } from "./context.js"
5
5
 
6
6
  export interface LinkProps extends ElementProps<"a"> {
7
+ /**
8
+ * The path to navigate to
9
+ */
7
10
  to: string
11
+ /**
12
+ * Whether to replace the current history entry
13
+ */
8
14
  replace?: boolean
15
+ /**
16
+ * Whether to trigger a view transition
17
+ */
9
18
  transition?: boolean
10
19
  }
11
20
 
@@ -46,20 +46,44 @@ export interface RouteQuery {
46
46
  }
47
47
 
48
48
  export interface RouterState {
49
+ /**
50
+ * The current path
51
+ */
49
52
  path: string
53
+ /**
54
+ * The current route params
55
+ * @example
56
+ * "/foo/[id]/page.tsx" -> { id: "123" }
57
+ */
50
58
  params: RouteParams
59
+ /**
60
+ * The current route query
61
+ */
51
62
  query: RouteQuery
63
+ /**
64
+ * The abort signal for the current route, aborted and
65
+ * renewed each time the route changes or reloads
66
+ */
52
67
  signal: AbortSignal
53
68
  }
54
69
 
55
70
  type PageDataLoaderContext = RouterState & {}
56
71
 
57
72
  export interface PageDataLoaderConfig<T = unknown> {
73
+ /**
74
+ * The function to load the page data
75
+ */
58
76
  load: (context: PageDataLoaderContext) => Promise<T>
77
+ /**
78
+ * Enable transitions when swapping between "load", "error" and "data" states
79
+ */
59
80
  transition?: boolean
60
81
  }
61
82
 
62
83
  export interface PageConfig {
84
+ /**
85
+ * The loader configuration for this page
86
+ */
63
87
  loader?: PageDataLoaderConfig
64
88
  // title?: string
65
89
  // description?: string