kiru 0.51.0-preview.1 → 0.51.0

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 (62) hide show
  1. package/dist/constants.d.ts.map +1 -1
  2. package/dist/constants.js +1 -0
  3. package/dist/constants.js.map +1 -1
  4. package/dist/router/cache.d.ts +71 -0
  5. package/dist/router/cache.d.ts.map +1 -0
  6. package/dist/router/cache.js +325 -0
  7. package/dist/router/cache.js.map +1 -0
  8. package/dist/router/client/index.d.ts.map +1 -1
  9. package/dist/router/client/index.js +62 -1
  10. package/dist/router/client/index.js.map +1 -1
  11. package/dist/router/context.d.ts +8 -0
  12. package/dist/router/context.d.ts.map +1 -1
  13. package/dist/router/context.js.map +1 -1
  14. package/dist/router/fileRouterController.d.ts +1 -0
  15. package/dist/router/fileRouterController.d.ts.map +1 -1
  16. package/dist/router/fileRouterController.js +124 -36
  17. package/dist/router/fileRouterController.js.map +1 -1
  18. package/dist/router/globals.d.ts +4 -0
  19. package/dist/router/globals.d.ts.map +1 -1
  20. package/dist/router/globals.js +3 -0
  21. package/dist/router/globals.js.map +1 -1
  22. package/dist/router/head.d.ts +4 -36
  23. package/dist/router/head.d.ts.map +1 -1
  24. package/dist/router/head.js +33 -53
  25. package/dist/router/head.js.map +1 -1
  26. package/dist/router/index.d.ts +57 -3
  27. package/dist/router/index.d.ts.map +1 -1
  28. package/dist/router/index.js +67 -3
  29. package/dist/router/index.js.map +1 -1
  30. package/dist/router/server/index.d.ts +1 -3
  31. package/dist/router/server/index.d.ts.map +1 -1
  32. package/dist/router/server/index.js +23 -20
  33. package/dist/router/server/index.js.map +1 -1
  34. package/dist/router/types.d.ts +31 -3
  35. package/dist/router/types.d.ts.map +1 -1
  36. package/dist/router/types.internal.d.ts +9 -8
  37. package/dist/router/types.internal.d.ts.map +1 -1
  38. package/dist/router/utils/index.d.ts +1 -6
  39. package/dist/router/utils/index.d.ts.map +1 -1
  40. package/dist/router/utils/index.js +1 -1
  41. package/dist/router/utils/index.js.map +1 -1
  42. package/dist/types.dom.d.ts +2 -1
  43. package/dist/types.dom.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/constants.ts +1 -0
  46. package/src/router/cache.ts +385 -0
  47. package/src/router/client/index.ts +81 -1
  48. package/src/router/context.ts +8 -0
  49. package/src/router/fileRouterController.ts +140 -47
  50. package/src/router/globals.ts +5 -0
  51. package/src/router/head.ts +44 -54
  52. package/src/router/index.ts +70 -3
  53. package/src/router/server/index.ts +33 -24
  54. package/src/router/types.internal.ts +10 -8
  55. package/src/router/types.ts +43 -13
  56. package/src/router/utils/index.ts +1 -1
  57. package/src/types.dom.ts +5 -1
  58. package/dist/router/dev/index.d.ts +0 -2
  59. package/dist/router/dev/index.d.ts.map +0 -1
  60. package/dist/router/dev/index.js +0 -46
  61. package/dist/router/dev/index.js.map +0 -1
  62. package/src/router/dev/index.ts +0 -63
package/src/constants.ts CHANGED
@@ -47,6 +47,7 @@ const EVENT_PREFIX_REGEX = /^on:?/
47
47
 
48
48
  const voidElements = new Set([
49
49
  "kiru-head-outlet",
50
+ "kiru-body-outlet",
50
51
  "area",
51
52
  "base",
52
53
  "br",
@@ -0,0 +1,385 @@
1
+ import type { PageDataLoaderCacheConfig, RouteParams } from "./types.js"
2
+
3
+ export interface CacheEntry<T = unknown> {
4
+ data: T
5
+ timestamp: number
6
+ ttl: number
7
+ }
8
+
9
+ export interface CacheKey {
10
+ path: string
11
+ params: RouteParams
12
+ query: Record<string, string | string[] | undefined>
13
+ }
14
+
15
+ /**
16
+ * Abstract base class for router cache implementations
17
+ */
18
+ abstract class BaseCacheStore {
19
+ protected abstract getItem(key: string): CacheEntry | null
20
+ protected abstract setItem(key: string, entry: CacheEntry): void
21
+ public abstract removeItem(key: string): void
22
+ public abstract getAllKeys(): string[]
23
+ public abstract clear(): void
24
+
25
+ /**
26
+ * Generate a cache key from route information
27
+ */
28
+ protected generateKey(key: CacheKey): string {
29
+ const { path, params, query } = key
30
+ const sortedParams = Object.keys(params)
31
+ .sort()
32
+ .map((k) => `${k}=${params[k]}`)
33
+ .join("&")
34
+
35
+ const sortedQuery = Object.keys(query)
36
+ .sort()
37
+ .map((k) => {
38
+ const value = query[k]
39
+ if (Array.isArray(value)) {
40
+ return `${k}=${value.sort().join(",")}`
41
+ }
42
+ return `${k}=${value}`
43
+ })
44
+ .join("&")
45
+
46
+ return `kiru-cache:${path}?${sortedParams}&${sortedQuery}`
47
+ }
48
+
49
+ /**
50
+ * Get cached data if it exists and hasn't expired
51
+ */
52
+ get<T>(key: CacheKey): null | { value: T } {
53
+ const cacheKey = this.generateKey(key)
54
+ const entry = this.getItem(cacheKey)
55
+
56
+ if (!entry) {
57
+ return null
58
+ }
59
+
60
+ const now = Date.now()
61
+ if (now - entry.timestamp > entry.ttl) {
62
+ this.removeItem(cacheKey)
63
+ return null
64
+ }
65
+
66
+ return { value: entry.data as T }
67
+ }
68
+
69
+ /**
70
+ * Set cached data with TTL
71
+ */
72
+ set<T>(key: CacheKey, data: T, ttl: number): void {
73
+ const cacheKey = this.generateKey(key)
74
+ const entry: CacheEntry<T> = {
75
+ data,
76
+ timestamp: Date.now(),
77
+ ttl,
78
+ }
79
+ this.setItem(cacheKey, entry)
80
+ }
81
+
82
+ /**
83
+ * Get cache size for debugging
84
+ */
85
+ size(): number {
86
+ return this.getAllKeys().length
87
+ }
88
+ }
89
+
90
+ /**
91
+ * In-memory cache implementation
92
+ */
93
+ class MemoryCacheStore extends BaseCacheStore {
94
+ private cache = new Map<string, CacheEntry>()
95
+
96
+ protected getItem(key: string): CacheEntry | null {
97
+ return this.cache.get(key) || null
98
+ }
99
+
100
+ protected setItem(key: string, entry: CacheEntry): void {
101
+ this.cache.set(key, entry)
102
+ }
103
+
104
+ public removeItem(key: string): void {
105
+ this.cache.delete(key)
106
+ }
107
+
108
+ public getAllKeys(): string[] {
109
+ return Array.from(this.cache.keys())
110
+ }
111
+
112
+ public clear(): void {
113
+ this.cache.clear()
114
+ }
115
+ }
116
+
117
+ /**
118
+ * (local|session)Storage cache implementation
119
+ */
120
+ class StorageCacheStore extends BaseCacheStore {
121
+ private keyPrefix = "kiru-cache:"
122
+ constructor(private storage: Storage) {
123
+ super()
124
+ }
125
+
126
+ protected getItem(key: string): CacheEntry | null {
127
+ try {
128
+ const item = this.storage.getItem(key)
129
+ return item ? JSON.parse(item) : null
130
+ } catch {
131
+ // Handle JSON parse errors or storage unavailable
132
+ return null
133
+ }
134
+ }
135
+
136
+ protected setItem(key: string, entry: CacheEntry): void {
137
+ try {
138
+ this.storage.setItem(key, JSON.stringify(entry))
139
+ } catch {
140
+ // Handle storage quota exceeded or unavailable
141
+ // Silently fail - cache is not critical
142
+ }
143
+ }
144
+
145
+ public removeItem(key: string): void {
146
+ try {
147
+ this.storage.removeItem(key)
148
+ } catch {
149
+ // Silently handle errors
150
+ }
151
+ }
152
+
153
+ public getAllKeys(): string[] {
154
+ try {
155
+ const keys: string[] = []
156
+ for (let i = 0; i < localStorage.length; i++) {
157
+ const key = this.storage.key(i)
158
+ if (key && key.startsWith(this.keyPrefix)) {
159
+ keys.push(key)
160
+ }
161
+ }
162
+ return keys
163
+ } catch {
164
+ return []
165
+ }
166
+ }
167
+
168
+ public clear(): void {
169
+ try {
170
+ const keysToRemove = this.getAllKeys()
171
+ keysToRemove.forEach((key) => this.storage.removeItem(key))
172
+ } catch {
173
+ // Silently handle errors
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Main router cache that manages different storage types
180
+ */
181
+ export class RouterCache {
182
+ private stores = new Map<string, BaseCacheStore>()
183
+
184
+ constructor() {
185
+ // Initialize default stores
186
+ this.stores.set("memory", new MemoryCacheStore())
187
+ this.stores.set("localStorage", new StorageCacheStore(localStorage))
188
+ this.stores.set("sessionStorage", new StorageCacheStore(sessionStorage))
189
+ }
190
+
191
+ /**
192
+ * Get the appropriate cache store for the given config
193
+ */
194
+ private getStore(config: PageDataLoaderCacheConfig): BaseCacheStore {
195
+ const store = this.stores.get(config.type)
196
+ if (!store) {
197
+ // Fallback to memory cache if type is not supported
198
+ return this.stores.get("memory")!
199
+ }
200
+ return store
201
+ }
202
+
203
+ /**
204
+ * Get cached data if it exists and hasn't expired
205
+ */
206
+ get<T>(
207
+ key: CacheKey,
208
+ config: PageDataLoaderCacheConfig
209
+ ): null | { value: T } {
210
+ const store = this.getStore(config)
211
+ return store.get<T>(key)
212
+ }
213
+
214
+ /**
215
+ * Set cached data with TTL
216
+ */
217
+ set<T>(key: CacheKey, data: T, config: PageDataLoaderCacheConfig): void {
218
+ const store = this.getStore(config)
219
+ store.set(key, data, config.ttl)
220
+ }
221
+
222
+ /**
223
+ * Invalidate cache entries by path patterns across all storage types
224
+ * Supports both exact paths ("/users/1") and folder patterns ("/users/[id]")
225
+ */
226
+ invalidate(...paths: string[]): void {
227
+ // Invalidate across all storage types
228
+ for (const store of this.stores.values()) {
229
+ this.invalidateInStore(store, paths)
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Invalidate cache entries in a specific store
235
+ */
236
+ private invalidateInStore(store: BaseCacheStore, paths: string[]): void {
237
+ const keysToDelete: string[] = []
238
+
239
+ for (const path of paths) {
240
+ if (this.isPattern(path)) {
241
+ // Folder path pattern - check all cache keys
242
+ for (const cacheKey of store.getAllKeys()) {
243
+ const keyPath = this.extractPathFromCacheKey(cacheKey)
244
+ if (keyPath && this.pathMatchesPattern(keyPath, [path])) {
245
+ keysToDelete.push(cacheKey)
246
+ }
247
+ }
248
+ } else {
249
+ // Exact path - find all cache keys that match this path
250
+ for (const cacheKey of store.getAllKeys()) {
251
+ const keyPath = this.extractPathFromCacheKey(cacheKey)
252
+ if (keyPath === path) {
253
+ keysToDelete.push(cacheKey)
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ // Delete all matched keys
260
+ for (const key of keysToDelete) {
261
+ store.removeItem(key)
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Extract the path part from a cache key
267
+ */
268
+ private extractPathFromCacheKey(cacheKey: string): string | null {
269
+ // Remove the prefix and extract path part
270
+ const withoutPrefix = cacheKey.replace(/^kiru-cache:/, "")
271
+ const pathPart = withoutPrefix.split("?")[0]
272
+ return pathPart || null
273
+ }
274
+
275
+ /**
276
+ * Check if a pattern string contains dynamic segments
277
+ */
278
+ private isPattern(path: string): boolean {
279
+ return path.includes("[") && path.includes("]")
280
+ }
281
+
282
+ /**
283
+ * Check if a given path matches any of the invalidation patterns
284
+ */
285
+ pathMatchesPattern(currentPath: string, patterns: string[]): boolean {
286
+ for (const pattern of patterns) {
287
+ if (this.isPattern(pattern)) {
288
+ // Convert pattern to segments and match
289
+ if (this.matchPathPattern(currentPath, pattern)) {
290
+ return true
291
+ }
292
+ } else {
293
+ // Exact path match
294
+ if (currentPath === pattern) {
295
+ return true
296
+ }
297
+ }
298
+ }
299
+ return false
300
+ }
301
+
302
+ /**
303
+ * Match a path against a pattern using segment-by-segment comparison
304
+ * This is more reliable than regex for our use case
305
+ */
306
+ private matchPathPattern(path: string, pattern: string): boolean {
307
+ const pathSegments = path.split("/").filter(Boolean)
308
+ const patternSegments = pattern.split("/").filter(Boolean)
309
+
310
+ // Handle catchall patterns
311
+ const hasCatchall = patternSegments.some(
312
+ (seg) => seg.startsWith("[...") && seg.endsWith("]")
313
+ )
314
+
315
+ if (hasCatchall) {
316
+ // Find the catchall position
317
+ const catchallIndex = patternSegments.findIndex(
318
+ (seg) => seg.startsWith("[...") && seg.endsWith("]")
319
+ )
320
+
321
+ // Check segments before catchall
322
+ for (let i = 0; i < catchallIndex; i++) {
323
+ if (!this.segmentMatches(pathSegments[i], patternSegments[i])) {
324
+ return false
325
+ }
326
+ }
327
+
328
+ // Catchall matches remaining segments
329
+ return pathSegments.length >= catchallIndex
330
+ } else {
331
+ // Regular pattern - must have same number of segments
332
+ if (pathSegments.length !== patternSegments.length) {
333
+ return false
334
+ }
335
+
336
+ // Check each segment
337
+ for (let i = 0; i < pathSegments.length; i++) {
338
+ if (!this.segmentMatches(pathSegments[i], patternSegments[i])) {
339
+ return false
340
+ }
341
+ }
342
+
343
+ return true
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Check if a path segment matches a pattern segment
349
+ */
350
+ private segmentMatches(pathSegment: string, patternSegment: string): boolean {
351
+ // Dynamic segment [param] matches any value
352
+ if (
353
+ patternSegment.startsWith("[") &&
354
+ patternSegment.endsWith("]") &&
355
+ !patternSegment.startsWith("[...")
356
+ ) {
357
+ return pathSegment !== undefined
358
+ }
359
+
360
+ // Catchall segment [...param] matches any remaining segments
361
+ if (patternSegment.startsWith("[...") && patternSegment.endsWith("]")) {
362
+ return true
363
+ }
364
+
365
+ // Static segment must match exactly
366
+ return pathSegment === patternSegment
367
+ }
368
+
369
+ /**
370
+ * Clear all cached data across all storage types
371
+ */
372
+ clear(): void {
373
+ for (const store of this.stores.values()) {
374
+ store.clear()
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Get cache size for debugging (memory cache only)
380
+ */
381
+ size(): number {
382
+ const memoryStore = this.stores.get("memory")
383
+ return memoryStore ? memoryStore.size() : 0
384
+ }
385
+ }
@@ -11,10 +11,11 @@ import {
11
11
  } from "../utils/index.js"
12
12
  import type { FormattedViteImportMap, PageModule } from "../types.internal"
13
13
  import type { FileRouterConfig, FileRouterPreloadConfig } from "../types"
14
- import { fileRouterInstance, fileRouterRoute } from "../globals.js"
14
+ import { fileRouterInstance, fileRouterRoute, routerCache } from "../globals.js"
15
15
  import { FileRouterController } from "../fileRouterController.js"
16
16
  import { FileRouterDataLoadError } from "../errors.js"
17
17
  import { __DEV__ } from "../../env.js"
18
+ import { RouterCache } from "../cache.js"
18
19
 
19
20
  interface InitClientOptions {
20
21
  dir: string
@@ -24,7 +25,9 @@ interface InitClientOptions {
24
25
  }
25
26
 
26
27
  export async function initClient(options: InitClientOptions) {
28
+ routerCache.current = new RouterCache()
27
29
  const { dir, baseUrl, pages, layouts } = options
30
+
28
31
  const config: FileRouterConfig = {
29
32
  dir,
30
33
  baseUrl,
@@ -34,6 +37,9 @@ export async function initClient(options: InitClientOptions) {
34
37
  transition: true,
35
38
  }
36
39
  hydrate(createElement(FileRouter, { config }), document.body)
40
+ if (__DEV__) {
41
+ onLoadedDev()
42
+ }
37
43
  }
38
44
 
39
45
  async function preparePreloadConfig(
@@ -41,6 +47,7 @@ async function preparePreloadConfig(
41
47
  isStatic404 = false
42
48
  ): Promise<FileRouterPreloadConfig> {
43
49
  let pageProps = {}
50
+ let cacheData: null | { value: unknown } = null
44
51
  let url = new URL(window.location.pathname, "http://localhost")
45
52
  const pathSegments = url.pathname.split("/").filter(Boolean)
46
53
  let routeMatch = matchRoute(options.pages, pathSegments)
@@ -82,6 +89,18 @@ async function preparePreloadConfig(
82
89
  : { data: staticProps.data, error: null, loading: false }
83
90
  } else if (typeof page.config?.loader?.load === "function") {
84
91
  pageProps = { loading: true, data: null, error: null }
92
+
93
+ const loader = page.config.loader
94
+ // Check cache first if caching is enabled
95
+ if (loader.mode !== "static" && loader.cache) {
96
+ const cacheKey = {
97
+ path: window.location.pathname,
98
+ params: routeMatch.params,
99
+ query: parseQuery(url.search),
100
+ }
101
+
102
+ cacheData = routerCache.current!.get(cacheKey, loader.cache)
103
+ }
85
104
  }
86
105
 
87
106
  return {
@@ -93,5 +112,66 @@ async function preparePreloadConfig(
93
112
  params: routeMatch.params,
94
113
  query: parseQuery(url.search),
95
114
  route: routeMatch.route,
115
+ cacheData,
96
116
  }
97
117
  }
118
+
119
+ function onLoadedDev() {
120
+ if (!__DEV__) {
121
+ throw new Error(
122
+ "onLoadedDev should not have been included in production build."
123
+ )
124
+ }
125
+ removeInjectedStyles()
126
+ }
127
+
128
+ function removeInjectedStyles() {
129
+ let sleep = 2
130
+
131
+ function runClean() {
132
+ if (clean()) return
133
+
134
+ if (sleep < 1000) {
135
+ sleep *= 2
136
+ }
137
+ setTimeout(runClean, sleep)
138
+ }
139
+
140
+ setTimeout(runClean, sleep)
141
+ }
142
+
143
+ function clean() {
144
+ let isCleaned = true
145
+ const VITE_ID = "data-vite-dev-id"
146
+ const injectedByVite = [
147
+ ...document.querySelectorAll(`style[${VITE_ID}]`),
148
+ ].map((style) => style.getAttribute(VITE_ID))
149
+
150
+ const suffix = "?temp"
151
+ const injectedByKiru = [
152
+ ...document.querySelectorAll(
153
+ `link[rel="stylesheet"][type="text/css"][href$="${suffix}"]`
154
+ ),
155
+ ]
156
+
157
+ injectedByKiru.forEach((linkKiru) => {
158
+ const href = linkKiru.getAttribute("href")!
159
+ let filePathAbsoluteUserRootDir = href.slice(0, -suffix.length)
160
+ const prefix = "/@fs/"
161
+ if (filePathAbsoluteUserRootDir.startsWith(prefix))
162
+ filePathAbsoluteUserRootDir = filePathAbsoluteUserRootDir.slice(
163
+ prefix.length
164
+ )
165
+
166
+ if (
167
+ injectedByVite.some((filePathAbsoluteFilesystem) =>
168
+ filePathAbsoluteFilesystem!.endsWith(filePathAbsoluteUserRootDir)
169
+ )
170
+ ) {
171
+ linkKiru.remove()
172
+ } else {
173
+ isCleaned = false
174
+ }
175
+ })
176
+ return isCleaned
177
+ }
@@ -4,6 +4,14 @@ import { useContext } from "../hooks/index.js"
4
4
  import type { RouteQuery, RouterState } from "./types.js"
5
5
 
6
6
  export interface FileRouterContextType {
7
+ /**
8
+ * Invalidate cached loader data for the given paths
9
+ * @example
10
+ * invalidate("/users", "/posts", "/users/1")
11
+ * // or invalidate based on folder path
12
+ * invalidate("/users/[id]") // (invalidates /users/1, /users/2, etc.)
13
+ */
14
+ invalidate(...paths: string[]): void
7
15
  /**
8
16
  * The current router state
9
17
  */