@vobs/router 0.1.0 → 0.3.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.
- package/dist/history.d.ts +1 -1
- package/dist/history.js +23 -2
- package/dist/resolve.js +12 -1
- package/dist/router.d.ts +4 -2
- package/dist/router.js +35 -6
- package/dist/types.d.ts +17 -3
- package/package.json +5 -5
package/dist/history.d.ts
CHANGED
|
@@ -21,5 +21,5 @@ export interface ScrollRestoration {
|
|
|
21
21
|
settle(action: 'none' | 'push' | 'replace', historyEntryKey: string | undefined): void;
|
|
22
22
|
destroy(): void;
|
|
23
23
|
}
|
|
24
|
-
export declare function createScrollRestoration(window: RouterWindow | undefined): ScrollRestoration;
|
|
24
|
+
export declare function createScrollRestoration(window: RouterWindow | undefined, maxEntries?: number): ScrollRestoration;
|
|
25
25
|
export {};
|
package/dist/history.js
CHANGED
|
@@ -85,12 +85,16 @@ export function resolveHistoryWindow(options) {
|
|
|
85
85
|
? (options.window ?? defaultRouterWindow())
|
|
86
86
|
: defaultRouterWindow();
|
|
87
87
|
}
|
|
88
|
-
export function createScrollRestoration(window) {
|
|
88
|
+
export function createScrollRestoration(window, maxEntries = 100) {
|
|
89
89
|
const positions = new Map();
|
|
90
|
+
const positionLimit = Number.isFinite(maxEntries) ? Math.max(0, Math.trunc(maxEntries)) : 100;
|
|
90
91
|
let currentKey;
|
|
91
|
-
|
|
92
|
+
const previousScrollRestoration = window?.history?.scrollRestoration;
|
|
93
|
+
let changedScrollRestoration = false;
|
|
94
|
+
if (previousScrollRestoration !== undefined && window !== undefined) {
|
|
92
95
|
try {
|
|
93
96
|
window.history.scrollRestoration = 'manual';
|
|
97
|
+
changedScrollRestoration = true;
|
|
94
98
|
}
|
|
95
99
|
catch {
|
|
96
100
|
// ignore read-only scrollRestoration (e.g. sandboxed iframes)
|
|
@@ -107,7 +111,16 @@ export function createScrollRestoration(window) {
|
|
|
107
111
|
saveCurrent() {
|
|
108
112
|
if (window === undefined || currentKey === undefined)
|
|
109
113
|
return;
|
|
114
|
+
if (positionLimit === 0)
|
|
115
|
+
return;
|
|
116
|
+
positions.delete(currentKey);
|
|
110
117
|
positions.set(currentKey, readPosition());
|
|
118
|
+
while (positions.size > positionLimit) {
|
|
119
|
+
const oldestKey = positions.keys().next().value;
|
|
120
|
+
if (oldestKey === undefined)
|
|
121
|
+
break;
|
|
122
|
+
positions.delete(oldestKey);
|
|
123
|
+
}
|
|
111
124
|
},
|
|
112
125
|
settle(action, historyEntryKey) {
|
|
113
126
|
if (window === undefined)
|
|
@@ -121,6 +134,14 @@ export function createScrollRestoration(window) {
|
|
|
121
134
|
destroy() {
|
|
122
135
|
positions.clear();
|
|
123
136
|
currentKey = undefined;
|
|
137
|
+
if (changedScrollRestoration && window !== undefined) {
|
|
138
|
+
try {
|
|
139
|
+
window.history.scrollRestoration = previousScrollRestoration;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// ignore read-only scrollRestoration (e.g. sandboxed iframes)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
124
145
|
},
|
|
125
146
|
};
|
|
126
147
|
}
|
package/dist/resolve.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveRouteMatch } from './match.js';
|
|
|
8
8
|
import { createRouteLocation } from './location.js';
|
|
9
9
|
export const defaultRedirectLimit = 8;
|
|
10
10
|
const routeComponentCache = new WeakMap();
|
|
11
|
+
const pendingLoaderSignals = new WeakMap();
|
|
11
12
|
export function resolveRoute(options) {
|
|
12
13
|
const match = resolveRouteMatch(options.routes, options.path, options.redirectLimit ?? defaultRedirectLimit, 0);
|
|
13
14
|
const component = match?.component ?? options.notFound;
|
|
@@ -44,7 +45,16 @@ export function resolveRouteInitialState(route, options = {}) {
|
|
|
44
45
|
const context = createRouteLoaderContext(route.route, options);
|
|
45
46
|
const cacheKey = readLoaderCacheKey(route.loaderCache, route.route, options);
|
|
46
47
|
if (cacheKey !== undefined && options.loaderCache?.has(cacheKey) === true) {
|
|
47
|
-
|
|
48
|
+
const cached = options.loaderCache.get(cacheKey);
|
|
49
|
+
if (!isPromiseLike(cached))
|
|
50
|
+
return cached;
|
|
51
|
+
// A pending loader may belong to a superseded navigation. Keep prefetch deduplication,
|
|
52
|
+
// but do not hand a cancelled navigation's request to a new consumer.
|
|
53
|
+
if (pendingLoaderSignals.get(cached) === undefined ||
|
|
54
|
+
!pendingLoaderSignals.get(cached)?.aborted) {
|
|
55
|
+
return cached;
|
|
56
|
+
}
|
|
57
|
+
options.loaderCache.delete(cacheKey);
|
|
48
58
|
}
|
|
49
59
|
const loaded = runWithInjectionScope(context.injections, () => route.loader(context));
|
|
50
60
|
return cacheLoaderResult(loaded, cacheKey, context.signal, options.loaderCache);
|
|
@@ -184,6 +194,7 @@ function cacheLoaderResult(loaded, cacheKey, signal, loaderCache) {
|
|
|
184
194
|
}
|
|
185
195
|
throw error;
|
|
186
196
|
});
|
|
197
|
+
pendingLoaderSignals.set(pending, signal);
|
|
187
198
|
writeLoaderCache(cacheKey, pending, signal, loaderCache);
|
|
188
199
|
return pending;
|
|
189
200
|
}
|
package/dist/router.d.ts
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
* Copyright (c) 2026 vobsjs
|
|
3
3
|
* @vobs/router
|
|
4
4
|
*/
|
|
5
|
-
import { type Router, type RouterOptions } from './types.js';
|
|
6
|
-
export declare function createRouter(options: RouterOptions
|
|
5
|
+
import { type RouteRecord, type ValidatedRouteRecords, type Router, type RouterOptions } from './types.js';
|
|
6
|
+
export declare function createRouter<const Routes extends readonly RouteRecord[]>(options: RouterOptions<Routes> & {
|
|
7
|
+
readonly routes: Routes & ValidatedRouteRecords<Routes>;
|
|
8
|
+
}): Router;
|
package/dist/router.js
CHANGED
|
@@ -44,7 +44,7 @@ export function createRouter(options) {
|
|
|
44
44
|
historyBinding = createHistoryBinding(options.history);
|
|
45
45
|
scrollRestoration =
|
|
46
46
|
options.scrollRestoration === true
|
|
47
|
-
? createScrollRestoration(resolveHistoryWindow(options.history) ?? defaultRouterWindow())
|
|
47
|
+
? createScrollRestoration(resolveHistoryWindow(options.history) ?? defaultRouterWindow(), options.scrollRestorationMaxEntries)
|
|
48
48
|
: undefined;
|
|
49
49
|
try {
|
|
50
50
|
await commitNavigation(options.initialPath ?? historyBinding?.readPath() ?? '/', historyBinding === undefined ? 'none' : 'replace', 0, options.hydrate === true ? 'hydrate' : 'mount', undefined);
|
|
@@ -339,18 +339,47 @@ export function createRouter(options) {
|
|
|
339
339
|
mounted = false;
|
|
340
340
|
container = undefined;
|
|
341
341
|
environment = undefined;
|
|
342
|
-
|
|
342
|
+
const errors = [];
|
|
343
|
+
try {
|
|
344
|
+
historyBinding?.destroy();
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
errors.push(error);
|
|
348
|
+
}
|
|
343
349
|
historyBinding = undefined;
|
|
344
350
|
const page = currentPage;
|
|
345
351
|
currentPage = undefined;
|
|
346
352
|
current.value = null;
|
|
347
353
|
pending.value = null;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
354
|
+
try {
|
|
355
|
+
page?.destroy();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
errors.push(error);
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
keepAliveCache.destroyAll();
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
errors.push(error);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
scrollRestoration?.destroy();
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
errors.push(error);
|
|
371
|
+
}
|
|
351
372
|
scrollRestoration = undefined;
|
|
352
|
-
|
|
373
|
+
try {
|
|
374
|
+
routerInjectionScope?.dispose();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
errors.push(error);
|
|
378
|
+
}
|
|
353
379
|
routerInjectionScope = undefined;
|
|
380
|
+
if (errors.length > 0) {
|
|
381
|
+
throw new AggregateError(errors, 'Router destroy failed');
|
|
382
|
+
}
|
|
354
383
|
}
|
|
355
384
|
return {
|
|
356
385
|
current,
|
package/dist/types.d.ts
CHANGED
|
@@ -59,6 +59,18 @@ export interface RouteLoaderCache {
|
|
|
59
59
|
clear(): void;
|
|
60
60
|
}
|
|
61
61
|
export type RouteRecord<Component extends RouteComponent = RouteComponent> = PageRouteRecord<Component> | NestedRouteRecord<Component> | RedirectRouteRecord;
|
|
62
|
+
type RouteRecordForValue<Route> = Route extends RouteRecord ? Route extends {
|
|
63
|
+
readonly children: infer Children extends readonly RouteRecord[];
|
|
64
|
+
} ? Omit<Route, 'children'> & {
|
|
65
|
+
readonly children: {
|
|
66
|
+
readonly [Key in keyof Children]: RouteRecordForValue<Children[Key]>;
|
|
67
|
+
};
|
|
68
|
+
} : Route extends {
|
|
69
|
+
readonly component: infer Component extends RouteComponent;
|
|
70
|
+
} ? Route & PageRouteRecord<Component> : Route : never;
|
|
71
|
+
export type ValidatedRouteRecords<Routes extends readonly RouteRecord[]> = {
|
|
72
|
+
readonly [Key in keyof Routes]: RouteRecordForValue<Routes[Key]>;
|
|
73
|
+
};
|
|
62
74
|
export type PageRouteRecord<Component extends RouteComponent = RouteComponent> = PageRouteRecordWithoutInitialState<Component> | PageRouteRecordWithInitialState<Component> | PageRouteRecordWithLoader<Component>;
|
|
63
75
|
export interface PageRouteRecordWithoutInitialState<Component extends RouteComponent = RouteComponent> {
|
|
64
76
|
readonly path: string;
|
|
@@ -102,7 +114,7 @@ export interface PageRouteRecordWithLoader<Component extends RouteComponent = Ro
|
|
|
102
114
|
export interface NestedRouteRecord<Component extends RouteComponent = RouteComponent> {
|
|
103
115
|
readonly path: string;
|
|
104
116
|
readonly component?: Component;
|
|
105
|
-
readonly children: readonly RouteRecord[];
|
|
117
|
+
readonly children: readonly RouteRecord<Component>[];
|
|
106
118
|
readonly layouts?: readonly RouteComponent[];
|
|
107
119
|
readonly error?: RouteComponent;
|
|
108
120
|
readonly meta?: RouteMeta;
|
|
@@ -159,8 +171,8 @@ export interface RouteNavigationScope {
|
|
|
159
171
|
readonly description: ReadonlySignal<string>;
|
|
160
172
|
readonly status: ReadonlySignal<string>;
|
|
161
173
|
}
|
|
162
|
-
export interface RouterOptions {
|
|
163
|
-
readonly routes:
|
|
174
|
+
export interface RouterOptions<Routes extends readonly RouteRecord[] = readonly RouteRecord[]> {
|
|
175
|
+
readonly routes: Routes;
|
|
164
176
|
readonly notFound?: RouteComponent | undefined;
|
|
165
177
|
readonly error?: RouteComponent | undefined;
|
|
166
178
|
readonly initialPath?: string;
|
|
@@ -168,6 +180,8 @@ export interface RouterOptions {
|
|
|
168
180
|
readonly hydrate?: boolean;
|
|
169
181
|
readonly navigation?: RouterNavigationOptions;
|
|
170
182
|
readonly scrollRestoration?: boolean;
|
|
183
|
+
/** Maximum number of saved history-entry scroll positions. Defaults to 100. */
|
|
184
|
+
readonly scrollRestorationMaxEntries?: number;
|
|
171
185
|
readonly keepAliveMax?: number;
|
|
172
186
|
}
|
|
173
187
|
export interface RouterNavigationOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vobs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Client and server route resolution for vobs pages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/vobsjs/vobs#readme",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vobs/reactivity": "0.
|
|
22
|
-
"@vobs/runtime-core": "0.
|
|
21
|
+
"@vobs/reactivity": "0.3.0",
|
|
22
|
+
"@vobs/runtime-core": "0.3.0"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist"
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"main": "./dist/index.js",
|
|
37
37
|
"sideEffects": false,
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@vobs/resource": "0.
|
|
39
|
+
"@vobs/resource": "0.3.0"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=
|
|
42
|
+
"node": ">=22.12.0"
|
|
43
43
|
}
|
|
44
44
|
}
|