kiru 0.51.6 → 0.51.8

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 (48) hide show
  1. package/dist/constants.d.ts +2 -1
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +2 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/globalContext.d.ts +4 -0
  6. package/dist/globalContext.d.ts.map +1 -1
  7. package/dist/globalContext.js +2 -0
  8. package/dist/globalContext.js.map +1 -1
  9. package/dist/hmr.d.ts.map +1 -1
  10. package/dist/hmr.js +6 -22
  11. package/dist/hmr.js.map +1 -1
  12. package/dist/router/context.d.ts +13 -0
  13. package/dist/router/context.d.ts.map +1 -1
  14. package/dist/router/context.js.map +1 -1
  15. package/dist/router/fileRouterController.d.ts +4 -1
  16. package/dist/router/fileRouterController.d.ts.map +1 -1
  17. package/dist/router/fileRouterController.js +113 -93
  18. package/dist/router/fileRouterController.js.map +1 -1
  19. package/dist/router/link.js +1 -1
  20. package/dist/router/link.js.map +1 -1
  21. package/dist/router/pageConfig.js +1 -1
  22. package/dist/router/pageConfig.js.map +1 -1
  23. package/dist/router/server/index.js +4 -4
  24. package/dist/router/server/index.js.map +1 -1
  25. package/dist/router/types.internal.d.ts +17 -2
  26. package/dist/router/types.internal.d.ts.map +1 -1
  27. package/dist/router/utils/index.d.ts.map +1 -1
  28. package/dist/router/utils/index.js +17 -4
  29. package/dist/router/utils/index.js.map +1 -1
  30. package/dist/signals/watch.d.ts +0 -3
  31. package/dist/signals/watch.d.ts.map +1 -1
  32. package/dist/signals/watch.js +0 -12
  33. package/dist/signals/watch.js.map +1 -1
  34. package/dist/types.dom.d.ts +3 -2
  35. package/dist/types.dom.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/constants.ts +2 -0
  38. package/src/globalContext.ts +6 -0
  39. package/src/hmr.ts +7 -23
  40. package/src/router/context.ts +17 -1
  41. package/src/router/fileRouterController.ts +144 -128
  42. package/src/router/link.ts +1 -1
  43. package/src/router/pageConfig.ts +1 -1
  44. package/src/router/server/index.ts +4 -4
  45. package/src/router/types.internal.ts +21 -2
  46. package/src/router/utils/index.ts +21 -4
  47. package/src/signals/watch.ts +0 -13
  48. package/src/types.dom.ts +3 -1
package/src/constants.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  $MEMO,
9
9
  $ERROR_BOUNDARY,
10
10
  $SUSPENSE_THROW,
11
+ $DEV_FILE_LINK,
11
12
  CONSECUTIVE_DIRTY_LIMIT,
12
13
  PREFETCHED_DATA_EVENT,
13
14
  EVENT_PREFIX_REGEX,
@@ -31,6 +32,7 @@ const $HMR_ACCEPT = Symbol.for("kiru.hmrAccept")
31
32
  const $MEMO = Symbol.for("kiru.memo")
32
33
  const $ERROR_BOUNDARY = Symbol.for("kiru.errorBoundary")
33
34
  const $SUSPENSE_THROW = Symbol.for("kiru.suspenseThrow")
35
+ const $DEV_FILE_LINK = Symbol.for("kiru.devFileLink")
34
36
 
35
37
  const CONSECUTIVE_DIRTY_LIMIT = 50
36
38
  const PREFETCHED_DATA_EVENT = "kiru:prefetched"
@@ -1,6 +1,8 @@
1
1
  import { __DEV__ } from "./env.js"
2
2
  import { createHMRContext } from "./hmr.js"
3
3
  import { createProfilingContext } from "./profiling.js"
4
+ import { fileRouterInstance } from "./router/globals.js"
5
+ import type { FileRouterController } from "./router/fileRouterController"
4
6
  import type { AppContext } from "./appContext"
5
7
  import type { Store } from "./store"
6
8
  import type { SWRCache } from "./swr"
@@ -86,6 +88,9 @@ interface KiruGlobalContext {
86
88
  HMRContext?: ReturnType<typeof createHMRContext>
87
89
  profilingContext?: ReturnType<typeof createProfilingContext>
88
90
  SWRGlobalCache?: SWRCache
91
+ fileRouterInstance?: {
92
+ current: FileRouterController | null
93
+ }
89
94
  }
90
95
 
91
96
  function createKiruGlobalContext(): KiruGlobalContext {
@@ -136,6 +141,7 @@ function createKiruGlobalContext(): KiruGlobalContext {
136
141
  globalContext.HMRContext = createHMRContext()
137
142
  globalContext.profilingContext = createProfilingContext()
138
143
  globalContext.stores = createReactiveMap()
144
+ globalContext.fileRouterInstance = fileRouterInstance
139
145
  }
140
146
 
141
147
  return globalContext
package/src/hmr.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $HMR_ACCEPT } from "./constants.js"
1
+ import { $HMR_ACCEPT, $DEV_FILE_LINK } from "./constants.js"
2
2
  import { __DEV__ } from "./env.js"
3
3
  import { traverseApply } from "./utils/index.js"
4
4
  import { requestUpdate } from "./scheduler.js"
@@ -56,7 +56,6 @@ export function createHMRContext() {
56
56
  let isModuleReplacementExecution = false
57
57
  const isReplacement = () => isModuleReplacementExecution
58
58
  let isWaitingForNextWatchCall = false
59
- let tmpUnnamedWatchers: WatchEffect[] = []
60
59
 
61
60
  const onHmrCallbacks: Array<() => void> = []
62
61
  const onHmr = (callback: () => void) => {
@@ -74,7 +73,11 @@ export function createHMRContext() {
74
73
  moduleMap.set(filePath, mod)
75
74
  } else {
76
75
  while (onHmrCallbacks.length) onHmrCallbacks.shift()!()
76
+ for (const prevWatcher of mod.unnamedWatchers.splice(0)) {
77
+ prevWatcher.stop()
78
+ }
77
79
  }
80
+
78
81
  currentModuleMemory = mod!
79
82
  currentModuleFilePath = filePath
80
83
  }
@@ -90,7 +93,7 @@ export function createHMRContext() {
90
93
  const oldEntry = currentModuleMemory.hotVars.get(name)
91
94
 
92
95
  // @ts-ignore - this is how we tell devtools what file the hotvar is from
93
- newEntry.value.__devtoolsFileLink = newEntry.link
96
+ newEntry.value[$DEV_FILE_LINK] = newEntry.link
94
97
 
95
98
  if (typeof newEntry.value === "function") {
96
99
  if (oldEntry?.value) {
@@ -134,25 +137,6 @@ export function createHMRContext() {
134
137
  dirtiedApps.forEach((ctx) => ctx.rootNode && requestUpdate(ctx.rootNode))
135
138
  isModuleReplacementExecution = false
136
139
 
137
- if (tmpUnnamedWatchers.length) {
138
- let i = 0
139
- for (; i < tmpUnnamedWatchers.length; i++) {
140
- const newWatcher = tmpUnnamedWatchers[i]
141
- const oldWatcher = currentModuleMemory.unnamedWatchers[i]
142
- if (oldWatcher) {
143
- newWatcher[$HMR_ACCEPT]!.inject(oldWatcher[$HMR_ACCEPT]!.provide())
144
- oldWatcher[$HMR_ACCEPT]!.destroy()
145
- }
146
- currentModuleMemory.unnamedWatchers[i] = newWatcher
147
- }
148
- for (; i < currentModuleMemory.unnamedWatchers.length; i++) {
149
- const oldWatcher = currentModuleMemory.unnamedWatchers[i]
150
- oldWatcher[$HMR_ACCEPT]!.destroy()
151
- }
152
- currentModuleMemory.unnamedWatchers.length = tmpUnnamedWatchers.length
153
- tmpUnnamedWatchers.length = 0
154
- }
155
-
156
140
  currentModuleMemory = null
157
141
  currentModuleFilePath = null
158
142
  }
@@ -165,7 +149,7 @@ export function createHMRContext() {
165
149
  return isWaitingForNextWatchCall
166
150
  },
167
151
  pushWatch(watch: WatchEffect) {
168
- tmpUnnamedWatchers.push(watch)
152
+ currentModuleMemory!.unnamedWatchers.push(watch)
169
153
  isWaitingForNextWatchCall = false
170
154
  },
171
155
  }
@@ -3,6 +3,19 @@ import { __DEV__ } from "../env.js"
3
3
  import { useContext } from "../hooks/index.js"
4
4
  import type { RouteQuery, RouterState } from "./types.js"
5
5
 
6
+ export interface ReloadOptions {
7
+ /**
8
+ * Trigger a view transition (overrides transition from config)
9
+ * @default false
10
+ */
11
+ transition?: boolean
12
+ /**
13
+ * Invalidate the cache for the current route
14
+ * @default true
15
+ */
16
+ invalidate?: boolean
17
+ }
18
+
6
19
  export interface FileRouterContextType {
7
20
  /**
8
21
  * Invalidate cached loader data for the given paths
@@ -41,7 +54,10 @@ export interface FileRouterContextType {
41
54
  /**
42
55
  * Reload the current route, optionally triggering a view transition
43
56
  */
44
- reload: (options?: { transition?: boolean }) => Promise<void>
57
+ reload: (options?: {
58
+ transition?: boolean
59
+ invalidate?: boolean
60
+ }) => Promise<void>
45
61
 
46
62
  /**
47
63
  * Set the current query parameters
@@ -1,7 +1,7 @@
1
1
  import { Signal } from "../signals/base.js"
2
2
  import { flushSync, nextIdle } from "../scheduler.js"
3
3
  import { __DEV__ } from "../env.js"
4
- import { type FileRouterContextType } from "./context.js"
4
+ import { ReloadOptions, type FileRouterContextType } from "./context.js"
5
5
  import { FileRouterDataLoadError } from "./errors.js"
6
6
  import { fileRouterInstance, fileRouterRoute, routerCache } from "./globals.js"
7
7
  import type {
@@ -13,6 +13,8 @@ import type {
13
13
  RouterState,
14
14
  } from "./types.js"
15
15
  import type {
16
+ CurrentPage,
17
+ DevtoolsInterface,
16
18
  FormattedViteImportMap,
17
19
  PageModule,
18
20
  ViteImportMap,
@@ -27,6 +29,7 @@ import {
27
29
  wrapWithLayouts,
28
30
  } from "./utils/index.js"
29
31
  import { RouterCache, type CacheKey } from "./cache.js"
32
+ import { watch } from "../signals/watch.js"
30
33
 
31
34
  interface PageConfigWithLoader<T = unknown> extends PageConfig {
32
35
  loader: PageDataLoaderConfig<T>
@@ -38,11 +41,7 @@ export class FileRouterController {
38
41
  private pages: FormattedViteImportMap<PageModule>
39
42
  private layouts: FormattedViteImportMap
40
43
  private abortController: AbortController
41
- private currentPage: Signal<{
42
- component: Kiru.FC<any>
43
- config?: PageConfig
44
- route: string
45
- } | null>
44
+ private currentPage: Signal<CurrentPage | null>
46
45
  private currentPageProps: Signal<Record<string, unknown>>
47
46
  private currentLayouts: Signal<Kiru.FC[]>
48
47
  private state: RouterState
@@ -52,6 +51,11 @@ export class FileRouterController {
52
51
  { route: string; config: PageConfig }
53
52
  >
54
53
  private pageRouteToConfig?: Map<string, PageConfig>
54
+ public dev_onPageConfigDefined?: <T extends PageConfig<any>>(
55
+ fp: string,
56
+ config: T
57
+ ) => void
58
+ public devtools?: DevtoolsInterface
55
59
 
56
60
  constructor() {
57
61
  routerCache.current ??= new RouterCache()
@@ -71,7 +75,9 @@ export class FileRouterController {
71
75
  }
72
76
  const __this = this
73
77
  this.contextValue = {
74
- invalidate: this.invalidate.bind(this),
78
+ invalidate: (...paths: string[]) => {
79
+ this.invalidate(...paths)
80
+ },
75
81
  get state() {
76
82
  return {
77
83
  ...__this.state,
@@ -87,38 +93,113 @@ export class FileRouterController {
87
93
  },
88
94
  navigate: this.navigate.bind(this),
89
95
  prefetchRouteModules: this.prefetchRouteModules.bind(this),
90
- reload: (options?: { transition?: boolean }) =>
91
- this.loadRoute(void 0, void 0, options?.transition),
96
+ reload: (options?: ReloadOptions) => {
97
+ if (
98
+ (options?.invalidate ?? true) &&
99
+ this.invalidate(this.state.pathname)
100
+ ) {
101
+ return Promise.resolve() // invalidate triggered a reload
102
+ }
103
+
104
+ return this.loadRoute(void 0, void 0, options?.transition)
105
+ },
92
106
  setQuery: this.setQuery.bind(this),
93
107
  }
94
108
  if (__DEV__) {
95
109
  this.filePathToPageRoute = new Map()
96
110
  this.pageRouteToConfig = new Map()
111
+ this.dev_onPageConfigDefined = (fp, config) => {
112
+ const existing = this.filePathToPageRoute?.get(fp)
113
+ if (existing === undefined) {
114
+ const route = fileRouterRoute.current
115
+ if (!route) return
116
+ this.filePathToPageRoute?.set(fp, { route, config })
117
+ return
118
+ }
119
+ const curPage = this.currentPage.value
120
+ const loader = config.loader
121
+ if (curPage?.route === existing.route && loader) {
122
+ const p = this.currentPageProps.value
123
+ const transition =
124
+ (loader.mode !== "static" && loader.transition) ??
125
+ this.enableTransitions
126
+
127
+ // Check cache first if caching is enabled
128
+ let cachedData = null
129
+ if (loader.mode !== "static" && loader.cache) {
130
+ const cacheKey: CacheKey = {
131
+ path: this.state.pathname,
132
+ params: this.state.params,
133
+ query: this.state.query,
134
+ }
135
+ cachedData = routerCache.current!.get(cacheKey, loader.cache)
136
+ }
137
+
138
+ if (cachedData !== null) {
139
+ // Use cached data immediately - no loading state needed
140
+ const props = {
141
+ ...p,
142
+ data: cachedData.value,
143
+ error: null,
144
+ loading: false,
145
+ }
146
+ handleStateTransition(this.state.signal, transition, () => {
147
+ this.currentPageProps.value = props
148
+ })
149
+ } else {
150
+ // No cached data - show loading state and load data
151
+ const props = {
152
+ ...p,
153
+ loading: true,
154
+ data: null,
155
+ error: null,
156
+ }
157
+ handleStateTransition(this.state.signal, transition, () => {
158
+ this.currentPageProps.value = props
159
+ })
160
+
161
+ this.loadRouteData(
162
+ config as PageConfigWithLoader,
163
+ props,
164
+ this.state,
165
+ transition
166
+ )
167
+ }
168
+ }
169
+
170
+ this.pageRouteToConfig?.set(existing.route, config)
171
+ }
172
+ this.devtools = {
173
+ getPages: () => this.pages,
174
+ invalidate: this.invalidate.bind(this),
175
+ navigate: this.navigate.bind(this),
176
+ reload: () => {
177
+ if (this.invalidate(this.state.pathname)) {
178
+ return Promise.resolve() // invalidate triggered a reload
179
+ }
180
+ return this.loadRoute()
181
+ },
182
+ subscribe: (callback) => {
183
+ const watcher = watch(
184
+ [this.currentPage, this.currentPageProps],
185
+ callback
186
+ )
187
+ return () => watcher.stop()
188
+ },
189
+ }
97
190
  }
98
191
 
99
- const handlePopState = () => this.loadRoute()
100
192
  window.addEventListener("popstate", (e) => {
101
- const state = e.state
102
- if (!isCustomNavigationState(state)) {
103
- this.loadRoute()
104
- return
105
- }
193
+ e.preventDefault()
194
+ const { pathname: prevPath, hash: prevHash } = this.state
195
+ const { pathname: nextPath, hash: nextHash } = window.location
106
196
 
107
197
  this.loadRoute().then(() => {
108
- nextIdle(() => {
109
- if (state.prevHash !== state.nextHash) {
110
- window.location.hash = ""
111
- window.location.hash = state.nextHash
112
- }
113
- if (!state.nextHash) {
114
- window.scrollTo(0, 0)
115
- }
116
- })
198
+ if (prevHash !== nextHash || prevPath !== nextPath) {
199
+ this.queueScrollManagement(nextHash)
200
+ }
117
201
  })
118
202
  })
119
- this.cleanups.push(() =>
120
- window.removeEventListener("popstate", handlePopState)
121
- )
122
203
  }
123
204
 
124
205
  public init(config: FileRouterConfig) {
@@ -166,14 +247,17 @@ export class FileRouterController {
166
247
  this.layouts = layouts
167
248
  if (__DEV__) {
168
249
  if (page.config) {
169
- this.onPageConfigDefined(route, page.config)
250
+ this.dev_onPageConfigDefined!(route, page.config)
170
251
  }
171
252
  }
172
253
  if (__DEV__) {
173
254
  validateRoutes(this.pages)
174
255
  }
175
256
  const loader = page.config?.loader
176
- if (loader && loader.mode !== "static" && pageProps.loading === true) {
257
+ if (
258
+ loader &&
259
+ ((loader.mode !== "static" && pageProps.loading === true) || __DEV__)
260
+ ) {
177
261
  if (cacheData === null) {
178
262
  this.loadRouteData(
179
263
  page.config as PageConfigWithLoader,
@@ -188,6 +272,7 @@ export class FileRouterController {
188
272
  error: null,
189
273
  loading: false,
190
274
  }
275
+ // @ts-ignore
191
276
  const transition = loader.transition ?? this.enableTransitions
192
277
  handleStateTransition(this.state.signal, transition, () => {
193
278
  this.currentPageProps.value = props
@@ -195,6 +280,7 @@ export class FileRouterController {
195
280
  })
196
281
  }
197
282
  }
283
+ // window.history.scrollRestoration = "manual"
198
284
  } else {
199
285
  this.pages = formatViteImportMap(
200
286
  pages as ViteImportMap,
@@ -210,72 +296,11 @@ export class FileRouterController {
210
296
  if (__DEV__) {
211
297
  validateRoutes(this.pages)
212
298
  }
299
+ //window.history.scrollRestoration = "manual"
213
300
  this.loadRoute()
214
301
  }
215
302
  }
216
303
 
217
- public onPageConfigDefined<T extends PageConfig<any>>(fp: string, config: T) {
218
- const existing = this.filePathToPageRoute?.get(fp)
219
- if (existing === undefined) {
220
- const route = fileRouterRoute.current
221
- if (!route) return
222
- this.filePathToPageRoute?.set(fp, { route, config })
223
- return
224
- }
225
- const curPage = this.currentPage.value
226
- const loader = config.loader
227
- if (curPage?.route === existing.route && loader) {
228
- const p = this.currentPageProps.value
229
- const transition =
230
- (loader.mode !== "static" && loader.transition) ??
231
- this.enableTransitions
232
-
233
- // Check cache first if caching is enabled
234
- let cachedData = null
235
- if (loader.mode !== "static" && loader.cache) {
236
- const cacheKey: CacheKey = {
237
- path: this.state.pathname,
238
- params: this.state.params,
239
- query: this.state.query,
240
- }
241
- cachedData = routerCache.current!.get(cacheKey, loader.cache)
242
- }
243
-
244
- if (cachedData !== null) {
245
- // Use cached data immediately - no loading state needed
246
- const props = {
247
- ...p,
248
- data: cachedData.value,
249
- error: null,
250
- loading: false,
251
- }
252
- handleStateTransition(this.state.signal, transition, () => {
253
- this.currentPageProps.value = props
254
- })
255
- } else {
256
- // No cached data - show loading state and load data
257
- const props = {
258
- ...p,
259
- loading: true,
260
- data: null,
261
- error: null,
262
- }
263
- handleStateTransition(this.state.signal, transition, () => {
264
- this.currentPageProps.value = props
265
- })
266
-
267
- this.loadRouteData(
268
- config as PageConfigWithLoader,
269
- props,
270
- this.state,
271
- transition
272
- )
273
- }
274
- }
275
-
276
- this.pageRouteToConfig?.set(existing.route, config)
277
- }
278
-
279
304
  public getChildren() {
280
305
  const page = this.currentPage.value
281
306
  if (!page) return null
@@ -502,8 +527,10 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
502
527
 
503
528
  if (shouldRefresh) {
504
529
  // Refresh the current page to get fresh data
505
- this.loadRoute(currentPath, {}, this.enableTransitions)
530
+ this.loadRoute()
531
+ return true
506
532
  }
533
+ return false
507
534
  }
508
535
 
509
536
  private async navigate(
@@ -513,27 +540,36 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
513
540
  transition?: boolean
514
541
  }
515
542
  ) {
516
- const { pathname: prevPath, hash: prevHash } = window.location
517
-
518
543
  const url = new URL(path, "http://localhost")
519
- const { pathname: nextPath, hash: nextHash } = url
544
+ const { hash: prevHash, pathname: prevPath } = this.state
545
+ const { hash: nextHash, pathname: nextPath } = url
546
+
520
547
  if (options?.replace) {
521
548
  window.history.replaceState({}, "", path)
522
549
  } else {
523
550
  window.history.pushState({}, "", path)
524
551
  }
525
- window.dispatchEvent(
526
- new PopStateEvent("popstate", {
527
- state: {
528
- ["kiru-router-event"]: true,
529
- prevPath,
530
- nextPath,
531
- prevHash,
532
- nextHash,
533
- transition: options?.transition ?? this.enableTransitions,
534
- } satisfies CustomNavigationState,
535
- })
536
- )
552
+
553
+ this.loadRoute(
554
+ void 0,
555
+ void 0,
556
+ options?.transition ?? this.enableTransitions
557
+ ).then(() => {
558
+ if (prevHash !== nextHash || prevPath !== nextPath) {
559
+ this.queueScrollManagement(nextHash)
560
+ }
561
+ })
562
+ }
563
+
564
+ private queueScrollManagement(nextHash: string) {
565
+ nextIdle(() => {
566
+ let nextEl: HTMLElement | null = null
567
+ if (nextHash && (nextEl = document.getElementById(nextHash.slice(1)))) {
568
+ nextEl.scrollIntoView()
569
+ } else {
570
+ window.scrollTo(0, 0)
571
+ }
572
+ })
537
573
  }
538
574
 
539
575
  private async prefetchRouteModules(path: string) {
@@ -623,7 +659,7 @@ function validateRoutes(pageMap: FormattedViteImportMap) {
623
659
  let warning = "[kiru/router]: Route conflicts detected:\n"
624
660
  warning += routeConflicts
625
661
  .map(([route1, route2]) => {
626
- return ` - "${route1.absolutePath}" conflicts with "${route2.absolutePath}"\n`
662
+ return ` - "${route1.filePath}" conflicts with "${route2.filePath}"\n`
627
663
  })
628
664
  .join("")
629
665
  warning += "Routes are ordered by specificity (higher specificity wins)"
@@ -678,23 +714,3 @@ function routesConflict(route1: string, route2: string): boolean {
678
714
 
679
715
  return true
680
716
  }
681
-
682
- interface CustomNavigationState {
683
- ["kiru-router-event"]: true
684
- prevHash: string
685
- nextHash: string
686
- prevPath: string
687
- nextPath: string
688
- transition: boolean
689
- }
690
-
691
- function isCustomNavigationState(
692
- state: unknown
693
- ): state is CustomNavigationState {
694
- return (
695
- typeof state === "object" &&
696
- state !== null &&
697
- "kiru-router-event" in state &&
698
- state["kiru-router-event"] === true
699
- )
700
- }
@@ -65,7 +65,7 @@ export const Link: Kiru.FC<LinkProps> = ({
65
65
  e.preventDefault()
66
66
  navigate(to, { replace, transition })
67
67
  },
68
- [to, navigate, onclick, replace]
68
+ [onclick, navigate, to, replace, transition]
69
69
  )
70
70
 
71
71
  return createElement("a", {
@@ -7,7 +7,7 @@ export function definePageConfig<T>(config: PageConfig<T>): PageConfig<T> {
7
7
  const filePath = window.__kiru?.HMRContext?.getCurrentFilePath()
8
8
  const fileRouter = fileRouterInstance.current
9
9
  if (filePath && fileRouter) {
10
- fileRouter.onPageConfigDefined(filePath, config)
10
+ fileRouter.dev_onPageConfigDefined!(filePath, config)
11
11
  }
12
12
  }
13
13
 
@@ -67,7 +67,7 @@ export async function render(
67
67
  const layoutEntries = matchLayouts(ctx.layouts, routeSegments)
68
68
 
69
69
  ;[pageEntry, ...layoutEntries].forEach((e) => {
70
- ctx.registerModule(e.absolutePath!)
70
+ ctx.registerModule(e.filePath)
71
71
  })
72
72
 
73
73
  const [page, ...layouts] = await Promise.all([
@@ -133,7 +133,7 @@ export async function render(
133
133
  !documentShell.includes("<kiru-body-outlet>")
134
134
  ) {
135
135
  throw new Error(
136
- "[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#ssg"
136
+ "[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#general-usage"
137
137
  )
138
138
  }
139
139
 
@@ -198,7 +198,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
198
198
 
199
199
  const hasDynamic = urlSegments.some((s) => s.startsWith(":"))
200
200
  if (!hasDynamic) {
201
- results[basePath === "" ? "/" : basePath] = entry.absolutePath
201
+ results[basePath === "" ? "/" : basePath] = entry.filePath
202
202
  continue
203
203
  }
204
204
  try {
@@ -214,7 +214,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
214
214
  const value = params[key]
215
215
  p = p.replace(`:${key}*`, value).replace(`:${key}`, value)
216
216
  }
217
- results[p] = entry.absolutePath
217
+ results[p] = entry.filePath
218
218
  }
219
219
  } catch {}
220
220
  }
@@ -1,5 +1,12 @@
1
+ import type { FileRouterContextType } from "./context"
1
2
  import type { PageConfig } from "./types"
2
3
 
4
+ export interface CurrentPage {
5
+ component: Kiru.FC<any>
6
+ config?: PageConfig
7
+ route: string
8
+ }
9
+
3
10
  export interface DefaultComponentModule {
4
11
  default: Kiru.FC
5
12
  }
@@ -18,10 +25,12 @@ export interface ViteImportMap {
18
25
  }
19
26
 
20
27
  export interface FormattedViteImportMapEntry<T = DefaultComponentModule> {
28
+ filePath: string
21
29
  load: () => Promise<T>
22
- specificity: number
30
+ params: string[]
31
+ route: string
23
32
  segments: string[]
24
- absolutePath: string
33
+ specificity: number
25
34
  }
26
35
 
27
36
  export interface FormattedViteImportMap<T = DefaultComponentModule> {
@@ -34,3 +43,13 @@ export interface RouteMatch {
34
43
  params: Record<string, string>
35
44
  routeSegments: string[]
36
45
  }
46
+
47
+ export interface DevtoolsInterface {
48
+ getPages: () => FormattedViteImportMap<PageModule>
49
+ invalidate: FileRouterContextType["invalidate"]
50
+ navigate: FileRouterContextType["navigate"]
51
+ reload: FileRouterContextType["reload"]
52
+ subscribe: (
53
+ cb: (page: CurrentPage | null, props: Record<string, unknown>) => void
54
+ ) => () => void
55
+ }
@@ -39,6 +39,7 @@ function formatViteImportMap(
39
39
  }
40
40
  const segments: string[] = []
41
41
  const parts = k.split("/").slice(0, -1)
42
+ const params = new Set<string>()
42
43
 
43
44
  for (let i = 0; i < parts.length; i++) {
44
45
  const part = parts[i]
@@ -48,12 +49,26 @@ function formatViteImportMap(
48
49
  `[kiru/router]: Catchall must be the folder name. Got "${key}"`
49
50
  )
50
51
  }
51
- segments.push(`:${part.slice(4, -1)}*`)
52
+ const param = part.slice(4, -1)
53
+ if (params.has(param)) {
54
+ throw new Error(
55
+ `[kiru/router]: Duplicate parameter "${param}" in "${key}"`
56
+ )
57
+ }
58
+ params.add(param)
59
+ segments.push(`:${param}*`)
52
60
  specificity += 1
53
61
  break
54
62
  }
55
63
  if (part.startsWith("[") && part.endsWith("]")) {
56
- segments.push(`:${part.slice(1, -1)}`)
64
+ const param = part.slice(1, -1)
65
+ if (params.has(param)) {
66
+ throw new Error(
67
+ `[kiru/router]: Duplicate parameter "${param}" in "${key}"`
68
+ )
69
+ }
70
+ params.add(param)
71
+ segments.push(`:${param}`)
57
72
  specificity += 10
58
73
  continue
59
74
  }
@@ -62,10 +77,12 @@ function formatViteImportMap(
62
77
  }
63
78
 
64
79
  const value: FormattedViteImportMap[string] = {
80
+ filePath: key,
65
81
  load: map[key],
66
- specificity,
82
+ params: Array.from(params),
83
+ route: "/" + parts.join("/"),
67
84
  segments,
68
- absolutePath: key,
85
+ specificity,
69
86
  }
70
87
 
71
88
  return {