@kimesh/router-runtime 0.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1298 @@
1
+ import { KeepAlive, Suspense, Transition, computed, createApp, defineComponent, h, inject, isRef, nextTick, onErrorCaptured, reactive, ref, toRef } from "vue";
2
+ import { RouterLink, RouterView, createRouter, createWebHashHistory, createWebHistory, onBeforeRouteLeave, onBeforeRouteUpdate, useLink, useRoute, useRoute as useRoute$1, useRouter, useRouter as useRouter$1 } from "vue-router";
3
+ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
4
+ import { createHooks } from "hookable";
5
+
6
+ //#region src/guards/loader-guard.ts
7
+ /**
8
+ * Safely parse input with schema, returning input on failure
9
+ */
10
+ function safeParse(schema, input) {
11
+ try {
12
+ return schema.parse(input);
13
+ } catch {
14
+ return input;
15
+ }
16
+ }
17
+ /**
18
+ * Validate search params using the provided schema
19
+ */
20
+ function validateSearchParams(query, schema) {
21
+ if (!schema) return query;
22
+ if (schema.safeParse) {
23
+ const result = schema.safeParse(query);
24
+ if (result.success && result.data !== void 0) return result.data;
25
+ return safeParse(schema, {});
26
+ }
27
+ const parsed = safeParse(schema, query);
28
+ if (parsed !== query) return parsed;
29
+ const defaults = safeParse(schema, {});
30
+ return defaults !== query ? defaults : query;
31
+ }
32
+ /**
33
+ * Collect loaders from matched route records
34
+ */
35
+ function collectLoaders(to) {
36
+ const loaders = [];
37
+ for (const record of to.matched) {
38
+ const routeDef = record.meta?.__kimesh;
39
+ if (routeDef?.loader) loaders.push({
40
+ loader: routeDef.loader,
41
+ routeDef
42
+ });
43
+ }
44
+ return loaders;
45
+ }
46
+ /**
47
+ * Check if error is an AbortError
48
+ */
49
+ function isAbortError(error) {
50
+ return error instanceof Error && error.name === "AbortError";
51
+ }
52
+ /**
53
+ * Create the loader navigation guard
54
+ */
55
+ function createLoaderGuard(context, options = {}) {
56
+ const { onError } = options;
57
+ return async function loaderGuard(to, _from, abortController) {
58
+ const loaders = collectLoaders(to);
59
+ if (loaders.length === 0) return;
60
+ await Promise.all(loaders.map(async ({ loader, routeDef }) => {
61
+ const search = validateSearchParams(to.query, routeDef.validateSearch);
62
+ const loaderContext = {
63
+ params: to.params,
64
+ search,
65
+ context,
66
+ signal: abortController.signal
67
+ };
68
+ try {
69
+ await loader(loaderContext);
70
+ } catch (error) {
71
+ if (isAbortError(error)) throw error;
72
+ console.error("[kimesh] Loader error:", error);
73
+ onError?.(error, to);
74
+ to.meta.__kimeshError = error;
75
+ }
76
+ }));
77
+ };
78
+ }
79
+ /**
80
+ * Install loader guard on router
81
+ */
82
+ function installLoaderGuard(router, context, options) {
83
+ let abortController = null;
84
+ const removeBeforeEach = router.beforeEach(() => {
85
+ abortController?.abort();
86
+ abortController = new AbortController();
87
+ });
88
+ const removeBeforeResolve = router.beforeResolve(async (to, from) => {
89
+ if (!abortController) abortController = new AbortController();
90
+ const guard = createLoaderGuard(context, { onError: options?.onError });
91
+ try {
92
+ await guard(to, from, abortController);
93
+ } catch (error) {
94
+ if (error.name === "AbortError") return false;
95
+ throw error;
96
+ }
97
+ });
98
+ const removeAfterEach = router.afterEach(() => {
99
+ abortController = null;
100
+ });
101
+ return () => {
102
+ removeBeforeEach();
103
+ removeBeforeResolve();
104
+ removeAfterEach();
105
+ };
106
+ }
107
+
108
+ //#endregion
109
+ //#region src/runtime-plugin.ts
110
+ const KIMESH_PLUGIN_INDICATOR = "__kimesh_plugin";
111
+ /**
112
+ * Define a Kimesh runtime plugin
113
+ *
114
+ * @example Function-style plugin
115
+ * ```ts
116
+ * export default defineKimeshRuntimePlugin((app) => {
117
+ * const analytics = new AnalyticsService()
118
+ * app.router.afterEach((to) => analytics.trackPageView(to.path))
119
+ * return { provide: { analytics } }
120
+ * })
121
+ * ```
122
+ *
123
+ * @example Object-style plugin with hooks
124
+ * ```ts
125
+ * export default defineKimeshRuntimePlugin({
126
+ * name: 'my-plugin',
127
+ * enforce: 'pre',
128
+ * hooks: {
129
+ * 'app:mounted': () => console.log('App mounted!')
130
+ * },
131
+ * setup(app) {
132
+ * return { provide: { myService: new MyService() } }
133
+ * }
134
+ * })
135
+ * ```
136
+ */
137
+ function defineKimeshRuntimePlugin(plugin) {
138
+ if (typeof plugin === "function") {
139
+ plugin[KIMESH_PLUGIN_INDICATOR] = true;
140
+ return plugin;
141
+ }
142
+ const { name, enforce, order, dependsOn, parallel, hooks, setup } = plugin;
143
+ const pluginFn = setup ?? (() => {});
144
+ pluginFn[KIMESH_PLUGIN_INDICATOR] = true;
145
+ pluginFn._name = name;
146
+ pluginFn.meta = {
147
+ name,
148
+ enforce,
149
+ order,
150
+ dependsOn,
151
+ parallel
152
+ };
153
+ pluginFn.hooks = hooks;
154
+ return pluginFn;
155
+ }
156
+ /**
157
+ * Check if a value is a Kimesh runtime plugin
158
+ */
159
+ function isKimeshRuntimePlugin(value) {
160
+ return typeof value === "function" && value[KIMESH_PLUGIN_INDICATOR] === true;
161
+ }
162
+ /**
163
+ * Get plugin metadata
164
+ */
165
+ function getPluginMeta(plugin) {
166
+ return plugin.meta ?? {};
167
+ }
168
+ /**
169
+ * Get plugin name
170
+ */
171
+ function getPluginName(plugin) {
172
+ return plugin._name ?? plugin.meta?.name ?? "anonymous";
173
+ }
174
+ /**
175
+ * Get plugin hooks
176
+ */
177
+ function getPluginHooks(plugin) {
178
+ return plugin.hooks;
179
+ }
180
+
181
+ //#endregion
182
+ //#region src/plugin-executor.ts
183
+ const ORDER_MAP = {
184
+ pre: -20,
185
+ default: 0,
186
+ post: 20
187
+ };
188
+ /**
189
+ * Apply a single plugin to the app context
190
+ */
191
+ async function applyPlugin(app, plugin) {
192
+ const pluginName = getPluginName(plugin);
193
+ try {
194
+ const hooks = getPluginHooks(plugin);
195
+ if (hooks) app.hooks.addHooks(hooks);
196
+ const result = await app.runWithContext(() => plugin(app));
197
+ if (result?.provide) for (const [key, value] of Object.entries(result.provide)) app.provide(key, value);
198
+ } catch (error) {
199
+ console.error(`[Kimesh] Plugin "${pluginName}" failed:`, error);
200
+ await app.hooks.callHook("app:error", error);
201
+ throw error;
202
+ }
203
+ }
204
+ /**
205
+ * Sort plugins by their execution order
206
+ */
207
+ function sortPluginsByOrder(plugins) {
208
+ return [...plugins].sort((a, b) => {
209
+ const aMeta = getPluginMeta(a);
210
+ const bMeta = getPluginMeta(b);
211
+ return (aMeta.order ?? ORDER_MAP[aMeta.enforce ?? "default"]) - (bMeta.order ?? ORDER_MAP[bMeta.enforce ?? "default"]);
212
+ });
213
+ }
214
+ /**
215
+ * Mark a plugin as resolved if it has a name
216
+ */
217
+ function markResolved(resolved, plugin) {
218
+ const name = getPluginName(plugin);
219
+ if (name !== "anonymous") resolved.add(name);
220
+ }
221
+ /**
222
+ * Remove resolved dependencies from a pending plugin entry
223
+ */
224
+ function updatePendingDeps(entry, resolved) {
225
+ for (const dep of entry.deps) if (resolved.has(dep)) entry.deps.delete(dep);
226
+ }
227
+ /**
228
+ * Apply all plugins to the app context with dependency resolution
229
+ */
230
+ async function applyPlugins(app, plugins) {
231
+ const sortedPlugins = sortPluginsByOrder(plugins);
232
+ const resolved = /* @__PURE__ */ new Set();
233
+ const pending = [];
234
+ async function tryResolvePending() {
235
+ let madeProgress = true;
236
+ while (madeProgress) {
237
+ madeProgress = false;
238
+ const stillPending = [];
239
+ for (const entry of pending) {
240
+ updatePendingDeps(entry, resolved);
241
+ if (entry.deps.size === 0) {
242
+ await applyPlugin(app, entry.plugin);
243
+ markResolved(resolved, entry.plugin);
244
+ madeProgress = true;
245
+ } else stillPending.push(entry);
246
+ }
247
+ pending.length = 0;
248
+ pending.push(...stillPending);
249
+ }
250
+ }
251
+ for (const plugin of sortedPlugins) {
252
+ const unresolvedDeps = getPluginMeta(plugin).dependsOn?.filter((dep) => !resolved.has(dep)) ?? [];
253
+ if (unresolvedDeps.length > 0) pending.push({
254
+ plugin,
255
+ deps: new Set(unresolvedDeps)
256
+ });
257
+ else {
258
+ await applyPlugin(app, plugin);
259
+ markResolved(resolved, plugin);
260
+ await tryResolvePending();
261
+ }
262
+ }
263
+ for (const { plugin, deps } of pending) console.warn(`[Kimesh] Plugin "${getPluginName(plugin)}" has unresolved dependencies: ${[...deps].join(", ")}`);
264
+ }
265
+ /**
266
+ * Apply plugins with parallel support.
267
+ * Plugins with parallel: true run concurrently within their group.
268
+ */
269
+ async function applyPluginsWithParallel(app, plugins) {
270
+ const sortedPlugins = sortPluginsByOrder(plugins);
271
+ const groups = [];
272
+ for (const plugin of sortedPlugins) {
273
+ const isParallel = getPluginMeta(plugin).parallel ?? false;
274
+ const lastGroup = groups[groups.length - 1];
275
+ if (lastGroup?.parallel === isParallel) lastGroup.plugins.push(plugin);
276
+ else groups.push({
277
+ parallel: isParallel,
278
+ plugins: [plugin]
279
+ });
280
+ }
281
+ for (const group of groups) if (group.parallel) await Promise.all(group.plugins.map((p) => applyPlugin(app, p)));
282
+ else for (const plugin of group.plugins) await applyPlugin(app, plugin);
283
+ }
284
+
285
+ //#endregion
286
+ //#region src/types.ts
287
+ /**
288
+ * Injection key for Kimesh context
289
+ */
290
+ const KIMESH_CONTEXT_KEY = Symbol("kimesh-context");
291
+
292
+ //#endregion
293
+ //#region src/create-app.ts
294
+ const KIMESH_APP_CONTEXT_KEY = Symbol("kimesh-app-context");
295
+ const DEFAULT_SCROLL_BEHAVIOR = (to, _from, savedPosition) => {
296
+ if (savedPosition) return savedPosition;
297
+ if (to.hash) return {
298
+ el: to.hash,
299
+ behavior: "smooth"
300
+ };
301
+ return { top: 0 };
302
+ };
303
+ /**
304
+ * Create a Kimesh application with router, query client, and runtime plugins
305
+ */
306
+ async function createKimeshApp(options) {
307
+ const { rootComponent, routes, base = "/", history = "web", scrollBehavior = DEFAULT_SCROLL_BEHAVIOR, queryClientConfig, context: userContext, plugins = [], runtimeConfig = {}, layersConfig = {} } = options;
308
+ const queryClient = new QueryClient(queryClientConfig ?? { defaultOptions: { queries: {
309
+ staleTime: 1e3 * 60 * 5,
310
+ gcTime: 1e3 * 60 * 30
311
+ } } });
312
+ const fullContext = {
313
+ queryClient,
314
+ ...typeof userContext === "function" ? userContext() : userContext ?? {}
315
+ };
316
+ const router = createRouter({
317
+ history: history === "hash" ? createWebHashHistory(base) : createWebHistory(base),
318
+ routes,
319
+ scrollBehavior
320
+ });
321
+ const removeLoaderGuard = installLoaderGuard(router, fullContext, { onError: (error, to) => console.error(`[Kimesh] Loader error for ${to.path}:`, error) });
322
+ const vueApp = createApp({ render: () => h(rootComponent) });
323
+ const hooks = createHooks();
324
+ const appContext = {
325
+ vueApp,
326
+ router,
327
+ queryClient,
328
+ hooks,
329
+ $config: runtimeConfig,
330
+ $layersConfig: layersConfig,
331
+ isHydrating: false,
332
+ _state: reactive({}),
333
+ provide(name, value) {
334
+ const $name = `$${name}`;
335
+ Object.defineProperty(vueApp.config.globalProperties, $name, {
336
+ get: () => value,
337
+ configurable: true
338
+ });
339
+ vueApp.provide($name, value);
340
+ },
341
+ runWithContext(fn) {
342
+ return vueApp.runWithContext(fn);
343
+ }
344
+ };
345
+ if (plugins.length > 0) await applyPlugins(appContext, plugins);
346
+ await hooks.callHook("app:created", vueApp);
347
+ vueApp.provide(KIMESH_CONTEXT_KEY, fullContext);
348
+ vueApp.provide(KIMESH_APP_CONTEXT_KEY, appContext);
349
+ vueApp.use(router);
350
+ vueApp.use(VueQueryPlugin, { queryClient });
351
+ router.beforeEach(() => hooks.callHook("page:start"));
352
+ router.afterEach(() => hooks.callHook("page:finish"));
353
+ const kimeshApp = {
354
+ app: vueApp,
355
+ router,
356
+ queryClient,
357
+ context: appContext,
358
+ async mount(container) {
359
+ await hooks.callHook("app:beforeMount", vueApp);
360
+ vueApp.mount(container);
361
+ await hooks.callHook("app:mounted", vueApp);
362
+ return kimeshApp;
363
+ },
364
+ unmount() {
365
+ removeLoaderGuard();
366
+ vueApp.unmount();
367
+ }
368
+ };
369
+ return kimeshApp;
370
+ }
371
+
372
+ //#endregion
373
+ //#region src/create-file-route.ts
374
+ /**
375
+ * Create a file-based route definition
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * // In routes/about.vue
380
+ * export const Route = createFileRoute('/about')({
381
+ * meta: { title: 'About Us' },
382
+ * loader: async () => {
383
+ * return { data: await fetchAboutData() }
384
+ * },
385
+ * head: {
386
+ * title: 'About Us',
387
+ * meta: [{ name: 'description', content: 'Learn about us' }]
388
+ * }
389
+ * })
390
+ * ```
391
+ */
392
+ function createFileRoute(path) {
393
+ return (options = {}) => ({
394
+ path,
395
+ meta: options.meta,
396
+ loader: options.loader,
397
+ validateSearch: options.validateSearch,
398
+ beforeLoad: options.beforeLoad,
399
+ head: options.head,
400
+ transition: options.transition,
401
+ viewTransition: options.viewTransition,
402
+ keepalive: options.keepalive
403
+ });
404
+ }
405
+ /**
406
+ * Define route for use in Vue SFC
407
+ * Alternative syntax for createFileRoute
408
+ */
409
+ function defineRoute(options) {
410
+ return options;
411
+ }
412
+
413
+ //#endregion
414
+ //#region src/context.ts
415
+ /**
416
+ * Define app context with type inference
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * // src/app.context.ts
421
+ * import { defineContext } from '@kimesh/router-runtime'
422
+ *
423
+ * export default defineContext(() => ({
424
+ * auth: useAuthStore(),
425
+ * i18n: createI18n(),
426
+ * }))
427
+ * ```
428
+ */
429
+ function defineContext(factory) {
430
+ return factory;
431
+ }
432
+ /**
433
+ * Use Kimesh context in components
434
+ *
435
+ * @example
436
+ * ```vue
437
+ * <script setup>
438
+ * const ctx = useKimeshContext()
439
+ * ctx.auth.logout()
440
+ * <\/script>
441
+ * ```
442
+ */
443
+ function useKimeshContext() {
444
+ const context = inject(KIMESH_CONTEXT_KEY);
445
+ if (!context) throw new Error("[Kimesh] Context not found. Make sure you are using this inside a Kimesh app.");
446
+ return context;
447
+ }
448
+
449
+ //#endregion
450
+ //#region src/plugins/core.ts
451
+ /**
452
+ * @kimesh/router-runtime - Core Plugins
453
+ *
454
+ * Built-in runtime plugins for Kimesh.
455
+ */
456
+ /**
457
+ * Router plugin that wires Vue Router guards to Kimesh hook system
458
+ *
459
+ * This plugin runs with `enforce: 'pre'` to ensure router hooks
460
+ * are available to all other plugins.
461
+ */
462
+ const routerPlugin = defineKimeshRuntimePlugin({
463
+ name: "kimesh:router",
464
+ enforce: "pre",
465
+ setup({ router, hooks }) {
466
+ const callHook = hooks.callHook.bind(hooks);
467
+ router.beforeEach(async (to, from) => {
468
+ const ctx = {
469
+ to,
470
+ from
471
+ };
472
+ try {
473
+ await callHook("navigate:before", ctx);
474
+ } catch (error) {
475
+ await callHook("navigate:error", {
476
+ error,
477
+ to,
478
+ from
479
+ });
480
+ throw error;
481
+ }
482
+ });
483
+ router.afterEach((to, from, failure) => {
484
+ callHook("navigate:after", {
485
+ to,
486
+ from,
487
+ failure: failure ?? void 0
488
+ });
489
+ });
490
+ router.onError((error, to, from) => {
491
+ callHook("navigate:error", {
492
+ error,
493
+ to,
494
+ from
495
+ });
496
+ });
497
+ }
498
+ });
499
+ /**
500
+ * Query plugin that sets up prefetching on navigation
501
+ *
502
+ * Note: QueryClient is already initialized in createKimeshApp,
503
+ * this plugin adds navigation-based prefetching.
504
+ */
505
+ const queryPlugin = defineKimeshRuntimePlugin({
506
+ name: "kimesh:query",
507
+ enforce: "pre",
508
+ dependsOn: ["kimesh:router"],
509
+ setup() {}
510
+ });
511
+ /**
512
+ * Get the default core plugins for a Kimesh app
513
+ */
514
+ function getCorePlugins() {
515
+ return [routerPlugin, queryPlugin];
516
+ }
517
+
518
+ //#endregion
519
+ //#region src/components/utils.ts
520
+ /**
521
+ * Wrap content in Vue KeepAlive component
522
+ *
523
+ * @param config - KeepAlive configuration or boolean
524
+ * @param content - Content to wrap (VNode)
525
+ * @returns Wrapped VNode
526
+ */
527
+ function wrapInKeepAlive(config, content) {
528
+ if (!config) return content;
529
+ return h(KeepAlive, config === true ? {} : config, { default: () => content });
530
+ }
531
+ /**
532
+ * Merge multiple transition props into one
533
+ * Later props override earlier ones
534
+ *
535
+ * @param propsArray - Array of transition props to merge
536
+ * @returns Merged transition props
537
+ */
538
+ function mergeTransitionProps(propsArray) {
539
+ const merged = {};
540
+ for (const props of propsArray) {
541
+ if (!props || props === true) continue;
542
+ Object.assign(merged, props);
543
+ }
544
+ return merged;
545
+ }
546
+ /**
547
+ * Check if route is changing based on path and matched components
548
+ * Used to determine if View Transitions API should be triggered
549
+ *
550
+ * @param to - Target route
551
+ * @param from - Current route
552
+ * @returns True if route is actually changing
553
+ */
554
+ function isChangingPage(to, from) {
555
+ if (to === from) return false;
556
+ if (to.path === from.path) return false;
557
+ return !to.matched.every((comp, index) => comp.components?.default === from.matched[index]?.components?.default);
558
+ }
559
+ /**
560
+ * Generate unique route key for transition identification
561
+ * Based on Nuxt's generateRouteKey implementation
562
+ *
563
+ * @param routeProps - RouterView slot props
564
+ * @param override - Optional key override
565
+ * @returns Unique route key
566
+ */
567
+ const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g;
568
+ const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g;
569
+ const ROUTE_KEY_NORMAL_RE = /:\w+/g;
570
+ function generateRouteKey(routeProps, override) {
571
+ const matchedRoute = routeProps.route.matched.find((m) => m.components?.default === routeProps.Component?.type);
572
+ const source = override ?? matchedRoute?.meta.key ?? (matchedRoute && interpolatePath(routeProps.route, matchedRoute));
573
+ return typeof source === "function" ? source(routeProps.route) : source;
574
+ }
575
+ function interpolatePath(route, match) {
576
+ return match.path.replace(ROUTE_KEY_PARENTHESES_RE, "$1").replace(ROUTE_KEY_SYMBOLS_RE, "$1").replace(ROUTE_KEY_NORMAL_RE, (r) => route.params[r.slice(1)]?.toString() || "");
577
+ }
578
+
579
+ //#endregion
580
+ //#region src/plugins/view-transitions.ts
581
+ /**
582
+ * View Transitions API Plugin
583
+ * Enables native browser View Transitions for smooth page navigation
584
+ *
585
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
586
+ *
587
+ * @example
588
+ * ```ts
589
+ * import { viewTransitionsPlugin } from '@kimesh/router-runtime/plugins/view-transitions'
590
+ *
591
+ * createKimeshApp({
592
+ * plugins: [viewTransitionsPlugin]
593
+ * })
594
+ * ```
595
+ */
596
+ const viewTransitionsPlugin = defineKimeshRuntimePlugin({
597
+ name: "kimesh:view-transitions",
598
+ enforce: "pre",
599
+ setup(app) {
600
+ if (typeof document === "undefined" || !document.startViewTransition) {
601
+ if (process.env.NODE_ENV === "development") console.warn("[Kimesh View Transitions] Browser does not support View Transitions API");
602
+ return;
603
+ }
604
+ let transition;
605
+ let hasUAVisualTransition = false;
606
+ let finishTransition;
607
+ let abortTransition;
608
+ const resetTransitionState = () => {
609
+ transition = void 0;
610
+ hasUAVisualTransition = false;
611
+ abortTransition = void 0;
612
+ finishTransition = void 0;
613
+ };
614
+ if (typeof window !== "undefined") window.addEventListener("popstate", (event) => {
615
+ hasUAVisualTransition = event.hasUAVisualTransition || false;
616
+ if (hasUAVisualTransition) transition?.skipTransition();
617
+ });
618
+ app.router.beforeResolve(async (to, from) => {
619
+ const viewTransitionMode = to.meta.__kimesh?.viewTransition;
620
+ const prefersNoTransition = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches && viewTransitionMode !== "always";
621
+ if (viewTransitionMode === false || prefersNoTransition || hasUAVisualTransition || !isChangingPage(to, from)) return;
622
+ const promise = new Promise((resolve, reject) => {
623
+ finishTransition = resolve;
624
+ abortTransition = reject;
625
+ });
626
+ let changeRoute;
627
+ const ready = new Promise((resolve) => changeRoute = resolve);
628
+ transition = document.startViewTransition(() => {
629
+ changeRoute();
630
+ return promise;
631
+ });
632
+ transition.finished.then(resetTransitionState).catch(resetTransitionState);
633
+ await app.hooks.callHook("page:view-transition:start", transition);
634
+ return ready;
635
+ });
636
+ app.hooks.hook("app:error", () => {
637
+ abortTransition?.();
638
+ resetTransitionState();
639
+ });
640
+ app.hooks.hook("page:finish", () => {
641
+ finishTransition?.();
642
+ });
643
+ }
644
+ });
645
+
646
+ //#endregion
647
+ //#region src/components/KmOutlet.ts
648
+ /**
649
+ * Check if View Transitions API is supported
650
+ */
651
+ const supportsViewTransitions = () => typeof document !== "undefined" && "startViewTransition" in document;
652
+ /**
653
+ * KmOutlet - Router view wrapper component
654
+ * Renders the matched route component with optional transitions and keepalive
655
+ *
656
+ * Transition options for async pages (useSuspenseQuery):
657
+ *
658
+ * 1. viewTransition: true (Recommended)
659
+ * - Uses View Transitions API (Chrome 111+, Edge 111+, Opera 97+)
660
+ * - Works perfectly with async pages
661
+ * - Falls back to instant transition on unsupported browsers
662
+ *
663
+ * 2. transition: { name: 'fade' } (without mode: 'out-in')
664
+ * - Uses Vue Transition with default mode (cross-fade)
665
+ * - Both pages exist during transition (old fades out, new fades in)
666
+ * - Requires CSS: .page-leave-active { position: absolute }
667
+ *
668
+ * 3. transition: { name: 'fade', mode: 'out-in' }
669
+ * - ⚠️ NOT recommended for async pages - may cause blank screen
670
+ */
671
+ const KmOutlet = defineComponent({
672
+ name: "KmOutlet",
673
+ props: {
674
+ name: {
675
+ type: String,
676
+ default: "default"
677
+ },
678
+ transition: {
679
+ type: [Boolean, Object],
680
+ default: void 0
681
+ },
682
+ viewTransition: {
683
+ type: Boolean,
684
+ default: false
685
+ },
686
+ keepalive: {
687
+ type: [Boolean, Object],
688
+ default: void 0
689
+ },
690
+ pageKey: {
691
+ type: [Function, String],
692
+ default: null
693
+ }
694
+ },
695
+ setup(props, { slots }) {
696
+ const kimeshApp = inject(KIMESH_APP_CONTEXT_KEY, null);
697
+ const router = useRouter$1();
698
+ let cachedVNode;
699
+ const isPending = ref(false);
700
+ let viewTransitionAbort = null;
701
+ if (props.viewTransition && supportsViewTransitions()) router.beforeResolve(async () => {
702
+ if (viewTransitionAbort) return;
703
+ let resolveTransition;
704
+ const transitionReady = new Promise((resolve) => {
705
+ resolveTransition = resolve;
706
+ });
707
+ const transition = document.startViewTransition(async () => {
708
+ await transitionReady;
709
+ });
710
+ viewTransitionAbort = () => {
711
+ transition.skipTransition();
712
+ viewTransitionAbort = null;
713
+ };
714
+ await nextTick();
715
+ resolveTransition();
716
+ try {
717
+ await transition.finished;
718
+ } finally {
719
+ viewTransitionAbort = null;
720
+ }
721
+ });
722
+ return () => {
723
+ return h(RouterView, { name: props.name }, { default: (routeProps) => {
724
+ if (slots.default) return slots.default(routeProps);
725
+ if (!routeProps.Component) return cachedVNode;
726
+ const routeKey = generateRouteKey(routeProps, props.pageKey) || routeProps.route.path;
727
+ const routeTransition = routeProps.route.meta.__kimesh?.transition;
728
+ const hasVueTransition = !props.viewTransition && !!(props.transition ?? routeTransition ?? false);
729
+ const transitionProps = hasVueTransition ? mergeTransitionProps([
730
+ props.transition,
731
+ routeTransition,
732
+ { onAfterLeave: () => {
733
+ if (kimeshApp) delete kimeshApp._runningTransition;
734
+ kimeshApp?.hooks.callHook("page:transition:finish");
735
+ } }
736
+ ]) : void 0;
737
+ const keepaliveConfig = props.keepalive ?? routeProps.route.meta.__kimesh?.keepalive;
738
+ const componentNode = h(routeProps.Component, { key: routeKey });
739
+ const wrappedNode = wrapInKeepAlive(keepaliveConfig, h(Suspense, {
740
+ suspensible: true,
741
+ onPending: () => {
742
+ isPending.value = true;
743
+ if (hasVueTransition && kimeshApp) kimeshApp._runningTransition = true;
744
+ kimeshApp?.hooks.callHook("page:start");
745
+ },
746
+ onResolve: async () => {
747
+ await nextTick();
748
+ isPending.value = false;
749
+ if (kimeshApp) delete kimeshApp._runningTransition;
750
+ await kimeshApp?.hooks.callHook("page:finish");
751
+ }
752
+ }, { default: () => componentNode }));
753
+ if (props.viewTransition) {
754
+ const pageVNode$1 = h("div", {
755
+ key: routeKey,
756
+ class: "km-outlet-page",
757
+ style: { viewTransitionName: "km-page" }
758
+ }, [wrappedNode]);
759
+ cachedVNode = pageVNode$1;
760
+ return pageVNode$1;
761
+ }
762
+ if (!hasVueTransition || !transitionProps) {
763
+ cachedVNode = wrappedNode;
764
+ return wrappedNode;
765
+ }
766
+ const pageVNode = h("div", {
767
+ key: routeKey,
768
+ class: "km-outlet-page"
769
+ }, [wrappedNode]);
770
+ cachedVNode = pageVNode;
771
+ return h(Transition, {
772
+ ...transitionProps,
773
+ appear: transitionProps.appear ?? false
774
+ }, { default: () => pageVNode });
775
+ } });
776
+ };
777
+ }
778
+ });
779
+
780
+ //#endregion
781
+ //#region src/components/KmLink.ts
782
+ /**
783
+ * Resolve path by replacing param placeholders with actual values
784
+ */
785
+ function resolvePath(path, params) {
786
+ if (!params) return path;
787
+ let resolved = path;
788
+ for (const [key, value] of Object.entries(params)) resolved = resolved.replace(`:${key}`, String(value));
789
+ return resolved;
790
+ }
791
+ /**
792
+ * KmLink - Type-safe router link component
793
+ *
794
+ * @example
795
+ * ```vue
796
+ * <!-- Static route -->
797
+ * <KmLink to="/posts">Posts</KmLink>
798
+ *
799
+ * <!-- Dynamic route with params -->
800
+ * <KmLink to="/posts/:postId" :params="{ postId: '123' }">
801
+ * Post 123
802
+ * </KmLink>
803
+ * ```
804
+ */
805
+ const KmLink = defineComponent({
806
+ name: "KmLink",
807
+ props: {
808
+ to: {
809
+ type: String,
810
+ required: true
811
+ },
812
+ params: {
813
+ type: Object,
814
+ default: void 0
815
+ },
816
+ replace: {
817
+ type: Boolean,
818
+ default: false
819
+ },
820
+ activeClass: {
821
+ type: String,
822
+ default: "km-link-active"
823
+ },
824
+ exactActiveClass: {
825
+ type: String,
826
+ default: "km-link-exact-active"
827
+ },
828
+ prefetch: {
829
+ type: Boolean,
830
+ default: true
831
+ },
832
+ tag: {
833
+ type: String,
834
+ default: "a"
835
+ }
836
+ },
837
+ setup(props, { slots, attrs }) {
838
+ const route = useRoute$1();
839
+ const resolvedPath = computed(() => resolvePath(props.to, props.params));
840
+ const isActive = computed(() => route.path.startsWith(resolvedPath.value));
841
+ const isExactActive = computed(() => route.path === resolvedPath.value);
842
+ const isCustomTag = computed(() => props.tag !== "a");
843
+ return () => {
844
+ const renderSlotContent = slots.default ? { default: (linkProps) => {
845
+ if (!isCustomTag.value) return slots.default?.(linkProps);
846
+ return h(props.tag, {
847
+ onClick: linkProps.navigate,
848
+ class: {
849
+ [props.activeClass]: isActive.value,
850
+ [props.exactActiveClass]: isExactActive.value
851
+ }
852
+ }, slots.default?.(linkProps));
853
+ } } : void 0;
854
+ return h(RouterLink, {
855
+ to: resolvedPath.value,
856
+ replace: props.replace,
857
+ activeClass: props.activeClass,
858
+ exactActiveClass: props.exactActiveClass,
859
+ custom: isCustomTag.value,
860
+ ...attrs
861
+ }, renderSlotContent);
862
+ };
863
+ }
864
+ });
865
+
866
+ //#endregion
867
+ //#region src/components/KmDeferred.ts
868
+ /**
869
+ * KmDeferred - Suspense wrapper with error handling
870
+ *
871
+ * Wraps Vue's <Suspense> with error boundary functionality.
872
+ * Use with useSuspenseQuery for progressive data loading.
873
+ *
874
+ * @example
875
+ * ```vue
876
+ * <KmDeferred :timeout="200">
877
+ * <AsyncDataComponent />
878
+ * <template #fallback>
879
+ * <Skeleton />
880
+ * </template>
881
+ * <template #error="{ error, retry }">
882
+ * <ErrorCard :error="error" @retry="retry" />
883
+ * </template>
884
+ * </KmDeferred>
885
+ * ```
886
+ */
887
+ const KmDeferred = defineComponent({
888
+ name: "KmDeferred",
889
+ props: { timeout: {
890
+ type: Number,
891
+ default: 0
892
+ } },
893
+ setup(props, { slots, expose }) {
894
+ const error = ref(null);
895
+ const key = ref(0);
896
+ onErrorCaptured((err) => {
897
+ error.value = err instanceof Error ? err : new Error(String(err));
898
+ return false;
899
+ });
900
+ function retry() {
901
+ error.value = null;
902
+ key.value++;
903
+ }
904
+ expose({
905
+ retry,
906
+ error
907
+ });
908
+ return () => {
909
+ if (error.value) {
910
+ if (slots.error) return h("div", { class: "km-deferred-error-wrapper" }, slots.error({
911
+ error: error.value,
912
+ retry
913
+ }));
914
+ return h("div", { class: "km-deferred-error" }, [h("p", error.value.message), h("button", { onClick: retry }, "Retry")]);
915
+ }
916
+ const fallbackContent = slots.fallback ? slots.fallback() : h("div", { class: "km-deferred-loading" }, "Loading...");
917
+ return h(Suspense, {
918
+ key: key.value,
919
+ timeout: props.timeout
920
+ }, {
921
+ default: () => slots.default?.(),
922
+ fallback: () => fallbackContent
923
+ });
924
+ };
925
+ }
926
+ });
927
+
928
+ //#endregion
929
+ //#region src/composables/use-search.ts
930
+ /**
931
+ * Convert an object to a query string record, filtering out null/undefined values
932
+ */
933
+ function toQueryRecord(obj) {
934
+ const query = {};
935
+ for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) query[key] = String(value);
936
+ return query;
937
+ }
938
+ /**
939
+ * Parse with schema, falling back to defaults or empty object
940
+ */
941
+ function parseWithDefaults(schema, input) {
942
+ try {
943
+ return schema.parse(input);
944
+ } catch {
945
+ return {};
946
+ }
947
+ }
948
+ /**
949
+ * Access and update validated search params
950
+ *
951
+ * @example
952
+ * ```vue
953
+ * <script setup>
954
+ * import { useSearch } from '@kimesh/router-runtime'
955
+ * import { z } from 'zod'
956
+ *
957
+ * const searchSchema = z.object({
958
+ * page: z.coerce.number().default(1),
959
+ * sort: z.enum(['asc', 'desc']).default('desc')
960
+ * })
961
+ *
962
+ * const { search, setSearch } = useSearch(searchSchema)
963
+ * <\/script>
964
+ * ```
965
+ */
966
+ function useSearch(schema) {
967
+ const route = useRoute$1();
968
+ const router = useRouter$1();
969
+ const defaults = parseWithDefaults(schema, {});
970
+ const search = computed(() => parseWithDefaults(schema, route.query));
971
+ function setSearch(key, value) {
972
+ router.replace({ query: {
973
+ ...route.query,
974
+ [key]: String(value)
975
+ } });
976
+ }
977
+ function setAllSearch(params) {
978
+ const merged = {
979
+ ...search.value,
980
+ ...params
981
+ };
982
+ router.replace({ query: toQueryRecord(merged) });
983
+ }
984
+ function resetSearch() {
985
+ router.replace({ query: toQueryRecord(defaults) });
986
+ }
987
+ return {
988
+ search,
989
+ setSearch,
990
+ setAllSearch,
991
+ resetSearch
992
+ };
993
+ }
994
+
995
+ //#endregion
996
+ //#region src/composables/use-params.ts
997
+ /**
998
+ * Get typed route params (non-reactive snapshot)
999
+ *
1000
+ * @example
1001
+ * ```ts
1002
+ * // In /posts/:postId page
1003
+ * const params = useParams<'/posts/:postId'>()
1004
+ * // params is typed as { postId: string }
1005
+ * console.log(params.postId)
1006
+ * ```
1007
+ */
1008
+ function useParams() {
1009
+ return useRoute$1().params;
1010
+ }
1011
+ /**
1012
+ * Get reactive typed route params
1013
+ *
1014
+ * @example
1015
+ * ```ts
1016
+ * // In /posts/:postId page
1017
+ * const params = useReactiveParams<'/posts/:postId'>()
1018
+ * // params.value is typed as { postId: string }
1019
+ * watchEffect(() => {
1020
+ * console.log(params.value.postId)
1021
+ * })
1022
+ * ```
1023
+ */
1024
+ function useReactiveParams() {
1025
+ const route = useRoute$1();
1026
+ return computed(() => route.params);
1027
+ }
1028
+
1029
+ //#endregion
1030
+ //#region src/composables/use-navigate.ts
1031
+ /**
1032
+ * Type-safe navigation composable
1033
+ *
1034
+ * @example
1035
+ * ```ts
1036
+ * const { navigate } = useNavigate()
1037
+ *
1038
+ * // Navigate to static route
1039
+ * navigate({ to: '/posts' })
1040
+ *
1041
+ * // Navigate to dynamic route with params
1042
+ * navigate({ to: '/posts/:postId', params: { postId: '123' } })
1043
+ *
1044
+ * // Replace instead of push
1045
+ * navigate({ to: '/posts', replace: true })
1046
+ * ```
1047
+ */
1048
+ function useNavigate() {
1049
+ const router = useRouter$1();
1050
+ /**
1051
+ * Navigate to a route with type-safe params
1052
+ */
1053
+ function navigate(options) {
1054
+ let path = options.to;
1055
+ if (options.params) for (const [key, value] of Object.entries(options.params)) path = path.replace(`:${key}`, String(value));
1056
+ if (options.replace) return router.replace(path);
1057
+ return router.push(path);
1058
+ }
1059
+ /**
1060
+ * Navigate back in history
1061
+ */
1062
+ function back() {
1063
+ return router.back();
1064
+ }
1065
+ /**
1066
+ * Navigate forward in history
1067
+ */
1068
+ function forward() {
1069
+ return router.forward();
1070
+ }
1071
+ /**
1072
+ * Navigate to a specific position in history
1073
+ */
1074
+ function go(delta) {
1075
+ return router.go(delta);
1076
+ }
1077
+ return {
1078
+ navigate,
1079
+ back,
1080
+ forward,
1081
+ go,
1082
+ router
1083
+ };
1084
+ }
1085
+
1086
+ //#endregion
1087
+ //#region src/composables/use-runtime-config.ts
1088
+ let _config;
1089
+ /**
1090
+ * Access runtime configuration.
1091
+ *
1092
+ * Returns the runtime config that was injected at build time via Vite's `define`.
1093
+ * Values can be overridden using `KIMESH_*` environment variables during build.
1094
+ *
1095
+ * @returns Frozen runtime config object
1096
+ *
1097
+ * @example
1098
+ * ```vue
1099
+ * <script setup>
1100
+ * const config = useRuntimeConfig()
1101
+ * console.log(config.apiBase)
1102
+ * console.log(config.debug)
1103
+ * <\/script>
1104
+ * ```
1105
+ *
1106
+ * @example
1107
+ * ```ts
1108
+ * // In a composable
1109
+ * export function useApi() {
1110
+ * const config = useRuntimeConfig()
1111
+ *
1112
+ * async function fetch<T>(endpoint: string): Promise<T> {
1113
+ * const url = `${config.apiBase}${endpoint}`
1114
+ * const response = await globalThis.fetch(url)
1115
+ * return response.json()
1116
+ * }
1117
+ *
1118
+ * return { fetch }
1119
+ * }
1120
+ * ```
1121
+ */
1122
+ function useRuntimeConfig() {
1123
+ if (!_config) try {
1124
+ _config = Object.freeze({ ...__KIMESH_CONFIG__ ?? {} });
1125
+ } catch {
1126
+ _config = Object.freeze({});
1127
+ }
1128
+ return _config;
1129
+ }
1130
+
1131
+ //#endregion
1132
+ //#region src/composables/use-kimesh-app.ts
1133
+ /**
1134
+ * @kimesh/router-runtime - useKimeshApp Composable
1135
+ *
1136
+ * Provides access to the Kimesh app context from within components.
1137
+ */
1138
+ /**
1139
+ * Get the Kimesh app context
1140
+ *
1141
+ * @example
1142
+ * ```ts
1143
+ * const { router, queryClient, $config } = useKimeshApp()
1144
+ * console.log($config.apiUrl)
1145
+ * ```
1146
+ *
1147
+ * @throws Error if called outside a Kimesh app
1148
+ */
1149
+ function useKimeshApp() {
1150
+ const context = inject(KIMESH_APP_CONTEXT_KEY);
1151
+ if (!context) throw new Error("[Kimesh] useKimeshApp() must be called within a Kimesh app. Make sure you have created the app with createKimeshApp().");
1152
+ return context;
1153
+ }
1154
+ /**
1155
+ * Try to get the Kimesh app context without throwing
1156
+ */
1157
+ function tryUseKimeshApp() {
1158
+ return inject(KIMESH_APP_CONTEXT_KEY);
1159
+ }
1160
+
1161
+ //#endregion
1162
+ //#region src/composables/useState.ts
1163
+ /**
1164
+ * @kimesh/router-runtime - useState Composable
1165
+ *
1166
+ * Centralized reactive state management for Kimesh applications.
1167
+ * CSR-optimized alternative to Nuxt's useState.
1168
+ */
1169
+ const STATE_KEY_PREFIX = "$s_";
1170
+ /**
1171
+ * Get all state keys (without prefix).
1172
+ */
1173
+ function getKmStateKeys() {
1174
+ const app = tryUseKimeshApp();
1175
+ if (!app?._state) return [];
1176
+ return Object.keys(app._state).filter((k) => k.startsWith(STATE_KEY_PREFIX)).map((k) => k.slice(3));
1177
+ }
1178
+ /**
1179
+ * Check if a state exists.
1180
+ */
1181
+ function hasKmState(key) {
1182
+ const app = tryUseKimeshApp();
1183
+ if (!app?._state) return false;
1184
+ const prefixedKey = STATE_KEY_PREFIX + key;
1185
+ return prefixedKey in app._state && app._state[prefixedKey] !== void 0;
1186
+ }
1187
+ /**
1188
+ * Clear Kimesh state by key(s) or predicate.
1189
+ * Pass undefined to clear all state, a string for single key,
1190
+ * an array for multiple keys, or a predicate function.
1191
+ */
1192
+ function clearKmState(keys) {
1193
+ const app = tryUseKimeshApp();
1194
+ if (!app?._state) return;
1195
+ const allStateKeys = getKmStateKeys();
1196
+ const keysToDelete = keys === void 0 ? allStateKeys : typeof keys === "function" ? allStateKeys.filter(keys) : Array.isArray(keys) ? keys : [keys];
1197
+ for (const key of keysToDelete) {
1198
+ const prefixedKey = STATE_KEY_PREFIX + key;
1199
+ if (prefixedKey in app._state) delete app._state[prefixedKey];
1200
+ }
1201
+ }
1202
+ /**
1203
+ * Get or create reactive state by key.
1204
+ *
1205
+ * @throws TypeError if key is not a non-empty string or init is not a function
1206
+ * @throws Error if _state is not initialized
1207
+ */
1208
+ function useState(key, init) {
1209
+ if (!key || typeof key !== "string") throw new TypeError("[Kimesh] [useState] key must be a non-empty string");
1210
+ if (init !== void 0 && typeof init !== "function") throw new TypeError("[Kimesh] [useState] init must be a function");
1211
+ const app = useKimeshApp();
1212
+ if (!app._state) throw new Error("[Kimesh] [useState] _state not initialized. Make sure you are using Kimesh v1.0+ with state support.");
1213
+ const prefixedKey = STATE_KEY_PREFIX + key;
1214
+ const state = toRef(app._state, prefixedKey);
1215
+ if (state.value === void 0 && init) {
1216
+ const initialValue = init();
1217
+ if (isRef(initialValue)) {
1218
+ app._state[prefixedKey] = initialValue;
1219
+ return initialValue;
1220
+ }
1221
+ state.value = initialValue;
1222
+ }
1223
+ return state;
1224
+ }
1225
+
1226
+ //#endregion
1227
+ //#region src/composables/use-navigation-middleware.ts
1228
+ /**
1229
+ * Register navigation middleware using the hook system
1230
+ *
1231
+ * @example
1232
+ * ```ts
1233
+ * // In a plugin or component setup
1234
+ * const { remove } = useNavigationMiddleware({
1235
+ * before: ({ to }) => {
1236
+ * if (!isAuthenticated() && to.meta.requiresAuth) {
1237
+ * return false // Cancel navigation
1238
+ * }
1239
+ * },
1240
+ * after: ({ to }) => {
1241
+ * analytics.trackPageView(to.path)
1242
+ * },
1243
+ * error: ({ error }) => {
1244
+ * console.error('Navigation failed:', error)
1245
+ * },
1246
+ * })
1247
+ *
1248
+ * // Cleanup when done
1249
+ * onUnmounted(() => remove())
1250
+ * ```
1251
+ *
1252
+ * @returns Object with remove function to cleanup hooks
1253
+ */
1254
+ function useNavigationMiddleware(options) {
1255
+ const { hooks } = useKimeshApp();
1256
+ const cleanups = [];
1257
+ if (options.before) cleanups.push(hooks.hook("navigate:before", options.before));
1258
+ if (options.after) cleanups.push(hooks.hook("navigate:after", options.after));
1259
+ if (options.error) cleanups.push(hooks.hook("navigate:error", options.error));
1260
+ return { remove() {
1261
+ for (const cleanup of cleanups) cleanup();
1262
+ } };
1263
+ }
1264
+ /**
1265
+ * Create a navigation guard that runs before navigation
1266
+ *
1267
+ * @example
1268
+ * ```ts
1269
+ * // Auth guard plugin
1270
+ * export default defineKimeshRuntimePlugin({
1271
+ * name: 'auth-guard',
1272
+ * setup(app) {
1273
+ * app.runWithContext(() => {
1274
+ * useNavigationGuard(({ to }) => {
1275
+ * if (to.meta.requiresAuth && !isAuthenticated()) {
1276
+ * return false
1277
+ * }
1278
+ * })
1279
+ * })
1280
+ * }
1281
+ * })
1282
+ * ```
1283
+ */
1284
+ function useNavigationGuard(guard) {
1285
+ const { hooks } = useKimeshApp();
1286
+ return hooks.hook("navigate:before", guard);
1287
+ }
1288
+ /**
1289
+ * Register a callback for after navigation
1290
+ */
1291
+ function useAfterNavigation(callback) {
1292
+ const { hooks } = useKimeshApp();
1293
+ return hooks.hook("navigate:after", callback);
1294
+ }
1295
+
1296
+ //#endregion
1297
+ export { KIMESH_APP_CONTEXT_KEY, KIMESH_CONTEXT_KEY, KIMESH_PLUGIN_INDICATOR, KmDeferred, KmLink, KmOutlet, STATE_KEY_PREFIX, applyPlugin, applyPlugins, applyPluginsWithParallel, clearKmState, createFileRoute, createKimeshApp, createLoaderGuard, defineContext, defineKimeshRuntimePlugin, defineRoute, getCorePlugins, getKmStateKeys, getPluginHooks, getPluginMeta, getPluginName, hasKmState, installLoaderGuard, isKimeshRuntimePlugin, onBeforeRouteLeave, onBeforeRouteUpdate, queryPlugin, routerPlugin, tryUseKimeshApp, useAfterNavigation, useKimeshApp, useKimeshContext, useLink, useNavigate, useNavigationGuard, useNavigationMiddleware, useParams, useReactiveParams, useRoute, useRouter, useRuntimeConfig, useSearch, useState };
1298
+ //# sourceMappingURL=index.mjs.map