kiru 0.51.7 → 0.51.9

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 (49) 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/dom.d.ts.map +1 -1
  6. package/dist/dom.js +3 -9
  7. package/dist/dom.js.map +1 -1
  8. package/dist/globalContext.d.ts +4 -0
  9. package/dist/globalContext.d.ts.map +1 -1
  10. package/dist/globalContext.js +2 -0
  11. package/dist/globalContext.js.map +1 -1
  12. package/dist/hmr.d.ts.map +1 -1
  13. package/dist/hmr.js +6 -22
  14. package/dist/hmr.js.map +1 -1
  15. package/dist/router/context.d.ts +13 -0
  16. package/dist/router/context.d.ts.map +1 -1
  17. package/dist/router/context.js.map +1 -1
  18. package/dist/router/fileRouterController.d.ts +3 -1
  19. package/dist/router/fileRouterController.d.ts.map +1 -1
  20. package/dist/router/fileRouterController.js +95 -64
  21. package/dist/router/fileRouterController.js.map +1 -1
  22. package/dist/router/pageConfig.js +1 -1
  23. package/dist/router/pageConfig.js.map +1 -1
  24. package/dist/router/server/index.js +3 -3
  25. package/dist/router/server/index.js.map +1 -1
  26. package/dist/router/types.internal.d.ts +17 -2
  27. package/dist/router/types.internal.d.ts.map +1 -1
  28. package/dist/router/utils/index.d.ts.map +1 -1
  29. package/dist/router/utils/index.js +17 -4
  30. package/dist/router/utils/index.js.map +1 -1
  31. package/dist/signals/watch.d.ts +0 -3
  32. package/dist/signals/watch.d.ts.map +1 -1
  33. package/dist/signals/watch.js +0 -12
  34. package/dist/signals/watch.js.map +1 -1
  35. package/dist/types.dom.d.ts +1 -1
  36. package/dist/types.dom.d.ts.map +1 -1
  37. package/package.json +2 -2
  38. package/src/constants.ts +2 -0
  39. package/src/dom.ts +2 -11
  40. package/src/globalContext.ts +6 -0
  41. package/src/hmr.ts +7 -23
  42. package/src/router/context.ts +17 -1
  43. package/src/router/fileRouterController.ts +121 -81
  44. package/src/router/pageConfig.ts +1 -1
  45. package/src/router/server/index.ts +3 -3
  46. package/src/router/types.internal.ts +21 -2
  47. package/src/router/utils/index.ts +21 -4
  48. package/src/signals/watch.ts +0 -13
  49. package/src/types.dom.ts +1 -1
@@ -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,13 +93,100 @@ 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
192
  window.addEventListener("popstate", (e) => {
@@ -102,7 +195,7 @@ export class FileRouterController {
102
195
  const { pathname: nextPath, hash: nextHash } = window.location
103
196
 
104
197
  this.loadRoute().then(() => {
105
- if (prevHash !== nextHash || prevPath !== nextPath) {
198
+ if (nextHash !== prevHash || nextPath !== prevPath) {
106
199
  this.queueScrollManagement(nextHash)
107
200
  }
108
201
  })
@@ -154,14 +247,17 @@ export class FileRouterController {
154
247
  this.layouts = layouts
155
248
  if (__DEV__) {
156
249
  if (page.config) {
157
- this.onPageConfigDefined(route, page.config)
250
+ this.dev_onPageConfigDefined!(route, page.config)
158
251
  }
159
252
  }
160
253
  if (__DEV__) {
161
254
  validateRoutes(this.pages)
162
255
  }
163
256
  const loader = page.config?.loader
164
- if (loader && loader.mode !== "static" && pageProps.loading === true) {
257
+ if (
258
+ loader &&
259
+ ((loader.mode !== "static" && pageProps.loading === true) || __DEV__)
260
+ ) {
165
261
  if (cacheData === null) {
166
262
  this.loadRouteData(
167
263
  page.config as PageConfigWithLoader,
@@ -176,6 +272,7 @@ export class FileRouterController {
176
272
  error: null,
177
273
  loading: false,
178
274
  }
275
+ // @ts-ignore
179
276
  const transition = loader.transition ?? this.enableTransitions
180
277
  handleStateTransition(this.state.signal, transition, () => {
181
278
  this.currentPageProps.value = props
@@ -204,68 +301,6 @@ export class FileRouterController {
204
301
  }
205
302
  }
206
303
 
207
- public onPageConfigDefined<T extends PageConfig<any>>(fp: string, config: T) {
208
- const existing = this.filePathToPageRoute?.get(fp)
209
- if (existing === undefined) {
210
- const route = fileRouterRoute.current
211
- if (!route) return
212
- this.filePathToPageRoute?.set(fp, { route, config })
213
- return
214
- }
215
- const curPage = this.currentPage.value
216
- const loader = config.loader
217
- if (curPage?.route === existing.route && loader) {
218
- const p = this.currentPageProps.value
219
- const transition =
220
- (loader.mode !== "static" && loader.transition) ??
221
- this.enableTransitions
222
-
223
- // Check cache first if caching is enabled
224
- let cachedData = null
225
- if (loader.mode !== "static" && loader.cache) {
226
- const cacheKey: CacheKey = {
227
- path: this.state.pathname,
228
- params: this.state.params,
229
- query: this.state.query,
230
- }
231
- cachedData = routerCache.current!.get(cacheKey, loader.cache)
232
- }
233
-
234
- if (cachedData !== null) {
235
- // Use cached data immediately - no loading state needed
236
- const props = {
237
- ...p,
238
- data: cachedData.value,
239
- error: null,
240
- loading: false,
241
- }
242
- handleStateTransition(this.state.signal, transition, () => {
243
- this.currentPageProps.value = props
244
- })
245
- } else {
246
- // No cached data - show loading state and load data
247
- const props = {
248
- ...p,
249
- loading: true,
250
- data: null,
251
- error: null,
252
- }
253
- handleStateTransition(this.state.signal, transition, () => {
254
- this.currentPageProps.value = props
255
- })
256
-
257
- this.loadRouteData(
258
- config as PageConfigWithLoader,
259
- props,
260
- this.state,
261
- transition
262
- )
263
- }
264
- }
265
-
266
- this.pageRouteToConfig?.set(existing.route, config)
267
- }
268
-
269
304
  public getChildren() {
270
305
  const page = this.currentPage.value
271
306
  if (!page) return null
@@ -492,8 +527,10 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
492
527
 
493
528
  if (shouldRefresh) {
494
529
  // Refresh the current page to get fresh data
495
- this.loadRoute(currentPath, {}, this.enableTransitions)
530
+ this.loadRoute()
531
+ return true
496
532
  }
533
+ return false
497
534
  }
498
535
 
499
536
  private async navigate(
@@ -503,22 +540,25 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
503
540
  transition?: boolean
504
541
  }
505
542
  ) {
506
- const url = new URL(path, "http://localhost")
507
- const { hash: prevHash, pathname: prevPath } = this.state
508
- const { hash: nextHash, pathname: nextPath } = url
509
-
510
543
  if (options?.replace) {
511
544
  window.history.replaceState({}, "", path)
512
545
  } else {
513
546
  window.history.pushState({}, "", path)
514
547
  }
515
548
 
549
+ const url = new URL(path, "http://localhost")
550
+ const { hash: nextHash, pathname: nextPath } = url
551
+ const { hash: prevHash, pathname: prevPath } = this.state
552
+
516
553
  this.loadRoute(
517
554
  void 0,
518
555
  void 0,
519
556
  options?.transition ?? this.enableTransitions
520
557
  ).then(() => {
521
- if (prevHash !== nextHash || prevPath !== nextPath) {
558
+ if (nextHash !== prevHash) {
559
+ window.dispatchEvent(new HashChangeEvent("hashchange"))
560
+ }
561
+ if (nextHash !== prevHash || nextPath !== prevPath) {
522
562
  this.queueScrollManagement(nextHash)
523
563
  }
524
564
  })
@@ -622,7 +662,7 @@ function validateRoutes(pageMap: FormattedViteImportMap) {
622
662
  let warning = "[kiru/router]: Route conflicts detected:\n"
623
663
  warning += routeConflicts
624
664
  .map(([route1, route2]) => {
625
- return ` - "${route1.absolutePath}" conflicts with "${route2.absolutePath}"\n`
665
+ return ` - "${route1.filePath}" conflicts with "${route2.filePath}"\n`
626
666
  })
627
667
  .join("")
628
668
  warning += "Routes are ordered by specificity (higher specificity wins)"
@@ -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([
@@ -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 {
@@ -1,6 +1,4 @@
1
1
  import { __DEV__ } from "../env.js"
2
- import type { HMRAccept } from "../hmr.js"
3
- import { $HMR_ACCEPT } from "../constants.js"
4
2
  import { useHook } from "../hooks/utils.js"
5
3
  import { effectQueue } from "./globals.js"
6
4
  import { executeWithTracking } from "./effect.js"
@@ -17,7 +15,6 @@ export class WatchEffect<const Deps extends readonly Signal<unknown>[] = []> {
17
15
  protected unsubs: Map<string, Function>
18
16
  protected cleanup: (() => void) | null
19
17
  protected isRunning?: boolean
20
- protected [$HMR_ACCEPT]?: HMRAccept<WatchEffect<Deps>>
21
18
 
22
19
  constructor(
23
20
  getter: (...values: SignalValues<Deps>) => WatchCallbackReturn,
@@ -30,16 +27,6 @@ export class WatchEffect<const Deps extends readonly Signal<unknown>[] = []> {
30
27
  this.isRunning = false
31
28
  this.cleanup = null
32
29
  if (__DEV__) {
33
- this[$HMR_ACCEPT] = {
34
- provide: () => this,
35
- inject: (prev) => {
36
- if (prev.isRunning) return
37
- this.stop()
38
- },
39
- destroy: () => {
40
- this.stop()
41
- },
42
- }
43
30
  if ("window" in globalThis) {
44
31
  const signals = window.__kiru.HMRContext!.signals
45
32
  if (signals.isWaitingForNextWatchCall()) {
package/src/types.dom.ts CHANGED
@@ -297,7 +297,7 @@ type IFrameSandbox = string | boolean
297
297
  // | "allow-top-navigation-by-user-activation"
298
298
 
299
299
  type InputAccept = "audio/*" | "video/*" | "image/*" | MimeType
300
- type AutoComplete = "on" | "off"
300
+ type AutoComplete = string
301
301
  type FormMethod = "get" | "post" | "dialog"
302
302
 
303
303
  type Direction = "ltr" | "rtl" | "auto"