@esmx/router 3.0.0-rc.17 → 3.0.0-rc.19
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/LICENSE +1 -1
- package/README.md +70 -0
- package/README.zh-CN.md +70 -0
- package/dist/error.d.ts +23 -0
- package/dist/error.mjs +61 -0
- package/dist/increment-id.d.ts +7 -0
- package/dist/increment-id.mjs +11 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.mjs +14 -3
- package/dist/index.test.mjs +8 -0
- package/dist/location.d.ts +15 -0
- package/dist/location.mjs +53 -0
- package/dist/location.test.d.ts +8 -0
- package/dist/location.test.mjs +370 -0
- package/dist/matcher.d.ts +3 -0
- package/dist/matcher.mjs +44 -0
- package/dist/matcher.test.mjs +1492 -0
- package/dist/micro-app.d.ts +18 -0
- package/dist/micro-app.dom.test.d.ts +1 -0
- package/dist/micro-app.dom.test.mjs +532 -0
- package/dist/micro-app.mjs +80 -0
- package/dist/navigation.d.ts +43 -0
- package/dist/navigation.mjs +143 -0
- package/dist/navigation.test.d.ts +1 -0
- package/dist/navigation.test.mjs +681 -0
- package/dist/options.d.ts +4 -0
- package/dist/options.mjs +88 -0
- package/dist/route-task.d.ts +40 -0
- package/dist/route-task.mjs +75 -0
- package/dist/route-task.test.d.ts +1 -0
- package/dist/route-task.test.mjs +673 -0
- package/dist/route-transition.d.ts +53 -0
- package/dist/route-transition.mjs +307 -0
- package/dist/route-transition.test.d.ts +1 -0
- package/dist/route-transition.test.mjs +146 -0
- package/dist/route.d.ts +72 -0
- package/dist/route.mjs +194 -0
- package/dist/route.test.d.ts +1 -0
- package/dist/route.test.mjs +1664 -0
- package/dist/router-back.test.d.ts +1 -0
- package/dist/router-back.test.mjs +361 -0
- package/dist/router-forward.test.d.ts +1 -0
- package/dist/router-forward.test.mjs +376 -0
- package/dist/router-go.test.d.ts +1 -0
- package/dist/router-go.test.mjs +73 -0
- package/dist/router-guards-cleanup.test.d.ts +1 -0
- package/dist/router-guards-cleanup.test.mjs +437 -0
- package/dist/router-link.d.ts +10 -0
- package/dist/router-link.mjs +126 -0
- package/dist/router-push.test.d.ts +1 -0
- package/dist/router-push.test.mjs +115 -0
- package/dist/router-replace.test.d.ts +1 -0
- package/dist/router-replace.test.mjs +114 -0
- package/dist/router-resolve.test.d.ts +1 -0
- package/dist/router-resolve.test.mjs +393 -0
- package/dist/router-restart-app.dom.test.d.ts +1 -0
- package/dist/router-restart-app.dom.test.mjs +616 -0
- package/dist/router-window-navigation.test.d.ts +1 -0
- package/dist/router-window-navigation.test.mjs +359 -0
- package/dist/router.d.ts +109 -102
- package/dist/router.mjs +260 -361
- package/dist/types.d.ts +246 -0
- package/dist/types.mjs +18 -0
- package/dist/util.d.ts +26 -0
- package/dist/util.mjs +53 -0
- package/dist/util.test.d.ts +1 -0
- package/dist/util.test.mjs +1020 -0
- package/package.json +10 -13
- package/src/error.ts +84 -0
- package/src/increment-id.ts +12 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +54 -3
- package/src/location.test.ts +406 -0
- package/src/location.ts +96 -0
- package/src/matcher.test.ts +1685 -0
- package/src/matcher.ts +59 -0
- package/src/micro-app.dom.test.ts +708 -0
- package/src/micro-app.ts +101 -0
- package/src/navigation.test.ts +858 -0
- package/src/navigation.ts +195 -0
- package/src/options.ts +131 -0
- package/src/route-task.test.ts +901 -0
- package/src/route-task.ts +105 -0
- package/src/route-transition.test.ts +178 -0
- package/src/route-transition.ts +425 -0
- package/src/route.test.ts +2014 -0
- package/src/route.ts +308 -0
- package/src/router-back.test.ts +487 -0
- package/src/router-forward.test.ts +506 -0
- package/src/router-go.test.ts +91 -0
- package/src/router-guards-cleanup.test.ts +595 -0
- package/src/router-link.ts +235 -0
- package/src/router-push.test.ts +140 -0
- package/src/router-replace.test.ts +139 -0
- package/src/router-resolve.test.ts +475 -0
- package/src/router-restart-app.dom.test.ts +783 -0
- package/src/router-window-navigation.test.ts +457 -0
- package/src/router.ts +289 -470
- package/src/types.ts +341 -0
- package/src/util.test.ts +1262 -0
- package/src/util.ts +116 -0
- package/dist/history/abstract.d.ts +0 -29
- package/dist/history/abstract.mjs +0 -107
- package/dist/history/base.d.ts +0 -79
- package/dist/history/base.mjs +0 -275
- package/dist/history/html.d.ts +0 -22
- package/dist/history/html.mjs +0 -183
- package/dist/history/index.d.ts +0 -7
- package/dist/history/index.mjs +0 -16
- package/dist/matcher/create-matcher.d.ts +0 -5
- package/dist/matcher/create-matcher.mjs +0 -218
- package/dist/matcher/create-matcher.spec.mjs +0 -0
- package/dist/matcher/index.d.ts +0 -1
- package/dist/matcher/index.mjs +0 -1
- package/dist/task-pipe/index.d.ts +0 -1
- package/dist/task-pipe/index.mjs +0 -1
- package/dist/task-pipe/task.d.ts +0 -30
- package/dist/task-pipe/task.mjs +0 -66
- package/dist/utils/bom.d.ts +0 -5
- package/dist/utils/bom.mjs +0 -10
- package/dist/utils/encoding.d.ts +0 -48
- package/dist/utils/encoding.mjs +0 -44
- package/dist/utils/guards.d.ts +0 -9
- package/dist/utils/guards.mjs +0 -12
- package/dist/utils/index.d.ts +0 -7
- package/dist/utils/index.mjs +0 -27
- package/dist/utils/path.d.ts +0 -60
- package/dist/utils/path.mjs +0 -281
- package/dist/utils/path.spec.mjs +0 -27
- package/dist/utils/scroll.d.ts +0 -25
- package/dist/utils/scroll.mjs +0 -59
- package/dist/utils/utils.d.ts +0 -16
- package/dist/utils/utils.mjs +0 -11
- package/dist/utils/warn.d.ts +0 -2
- package/dist/utils/warn.mjs +0 -12
- package/src/history/abstract.ts +0 -149
- package/src/history/base.ts +0 -408
- package/src/history/html.ts +0 -228
- package/src/history/index.ts +0 -20
- package/src/matcher/create-matcher.spec.ts +0 -3
- package/src/matcher/create-matcher.ts +0 -293
- package/src/matcher/index.ts +0 -1
- package/src/task-pipe/index.ts +0 -1
- package/src/task-pipe/task.ts +0 -97
- package/src/utils/bom.ts +0 -14
- package/src/utils/encoding.ts +0 -153
- package/src/utils/guards.ts +0 -25
- package/src/utils/index.ts +0 -27
- package/src/utils/path.spec.ts +0 -32
- package/src/utils/path.ts +0 -417
- package/src/utils/scroll.ts +0 -120
- package/src/utils/utils.ts +0 -30
- package/src/utils/warn.ts +0 -13
- /package/dist/{matcher/create-matcher.spec.d.ts → index.test.d.ts} +0 -0
- /package/dist/{utils/path.spec.d.ts → matcher.test.d.ts} +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RouteNavigationAbortedError,
|
|
3
|
+
RouteSelfRedirectionError,
|
|
4
|
+
RouteTaskCancelledError,
|
|
5
|
+
RouteTaskExecutionError
|
|
6
|
+
} from './error';
|
|
7
|
+
import { Route } from './route';
|
|
8
|
+
import type { Router } from './router';
|
|
9
|
+
import type { RouteConfirmHook, RouteConfirmHookResult } from './types';
|
|
10
|
+
import { isUrlEqual, isValidConfirmHookResult } from './util';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Controls the execution and cancellation of a route task.
|
|
14
|
+
*/
|
|
15
|
+
export class RouteTaskController {
|
|
16
|
+
private _aborted = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Aborts the current task.
|
|
20
|
+
*/
|
|
21
|
+
abort(): void {
|
|
22
|
+
this._aborted = true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
shouldCancel(name: string): boolean {
|
|
26
|
+
if (this._aborted) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RouteTaskOptions {
|
|
34
|
+
to: Route;
|
|
35
|
+
from: Route | null;
|
|
36
|
+
tasks: RouteTask[];
|
|
37
|
+
router: Router;
|
|
38
|
+
/**
|
|
39
|
+
* Optional route task controller.
|
|
40
|
+
* Used to control the execution and cancellation of the task.
|
|
41
|
+
*/
|
|
42
|
+
controller?: RouteTaskController;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function createRouteTask(opts: RouteTaskOptions): Promise<Route> {
|
|
46
|
+
const { to, from, tasks, controller, router } = opts;
|
|
47
|
+
|
|
48
|
+
for (const task of tasks) {
|
|
49
|
+
if (controller?.shouldCancel(task.name)) {
|
|
50
|
+
throw new RouteTaskCancelledError(task.name, to, from);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let result: RouteConfirmHookResult | null = null;
|
|
54
|
+
try {
|
|
55
|
+
result = await task.task(to, from, router);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new RouteTaskExecutionError(task.name, to, from, e);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (controller?.shouldCancel(task.name)) {
|
|
61
|
+
throw new RouteTaskCancelledError(task.name, to, from);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isValidConfirmHookResult(result)) continue;
|
|
65
|
+
if (typeof result === 'function') {
|
|
66
|
+
to.handle = result;
|
|
67
|
+
return to;
|
|
68
|
+
}
|
|
69
|
+
if (result === false) {
|
|
70
|
+
throw new RouteNavigationAbortedError(task.name, to, from);
|
|
71
|
+
}
|
|
72
|
+
const nextTo = new Route({
|
|
73
|
+
options: router.parsedOptions,
|
|
74
|
+
toType: to.type,
|
|
75
|
+
toInput: result,
|
|
76
|
+
from: to.url
|
|
77
|
+
});
|
|
78
|
+
if (isUrlEqual(nextTo.url, to.url)) {
|
|
79
|
+
throw new RouteSelfRedirectionError(to.fullPath, to, from);
|
|
80
|
+
}
|
|
81
|
+
return createRouteTask({
|
|
82
|
+
...opts,
|
|
83
|
+
to: nextTo,
|
|
84
|
+
from: to,
|
|
85
|
+
controller
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return to;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export enum RouteTaskType {
|
|
92
|
+
fallback = 'fallback',
|
|
93
|
+
override = 'override',
|
|
94
|
+
asyncComponent = 'asyncComponent',
|
|
95
|
+
beforeEach = 'beforeEach',
|
|
96
|
+
beforeEnter = 'beforeEnter',
|
|
97
|
+
beforeUpdate = 'beforeUpdate',
|
|
98
|
+
beforeLeave = 'beforeLeave',
|
|
99
|
+
confirm = 'confirm'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface RouteTask {
|
|
103
|
+
name: string;
|
|
104
|
+
task: RouteConfirmHook;
|
|
105
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, test } from 'vitest';
|
|
2
|
+
import type { Route } from './route';
|
|
3
|
+
import { Router } from './router';
|
|
4
|
+
import { RouteType, RouterMode } from './types';
|
|
5
|
+
|
|
6
|
+
describe('Route Transition Tests', () => {
|
|
7
|
+
let router: Router;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
router = new Router({
|
|
11
|
+
mode: RouterMode.memory,
|
|
12
|
+
base: new URL('http://localhost:3000/'),
|
|
13
|
+
routes: [
|
|
14
|
+
{ path: '/', component: () => 'Home' },
|
|
15
|
+
{ path: '/user/:id', component: () => 'User' },
|
|
16
|
+
{ path: '/about', component: () => 'About' },
|
|
17
|
+
{
|
|
18
|
+
path: '/async',
|
|
19
|
+
asyncComponent: () => Promise.resolve('Async')
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await router.replace('/');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
router.destroy();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Basic transitions', () => {
|
|
32
|
+
test('should successfully transition to new route', async () => {
|
|
33
|
+
const route = await router.push('/user/123');
|
|
34
|
+
|
|
35
|
+
expect(route.path).toBe('/user/123');
|
|
36
|
+
expect(route.params.id).toBe('123');
|
|
37
|
+
expect(route.handle).not.toBe(null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should handle async component loading', async () => {
|
|
41
|
+
const route = await router.push('/async');
|
|
42
|
+
|
|
43
|
+
expect(route.path).toBe('/async');
|
|
44
|
+
expect(route.handle).not.toBe(null);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Error handling', () => {
|
|
49
|
+
test('should handle non-existent routes', async () => {
|
|
50
|
+
const route = await router.push('/non-existent');
|
|
51
|
+
expect(route.matched).toHaveLength(0);
|
|
52
|
+
expect(route.config).toBe(null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should handle component loading errors', async () => {
|
|
56
|
+
const errorRouter = new Router({
|
|
57
|
+
mode: RouterMode.memory,
|
|
58
|
+
base: new URL('http://localhost:3000/'),
|
|
59
|
+
routes: [
|
|
60
|
+
{ path: '/', component: () => 'Home' },
|
|
61
|
+
{
|
|
62
|
+
path: '/error',
|
|
63
|
+
asyncComponent: () =>
|
|
64
|
+
Promise.reject(new Error('Loading failed'))
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await errorRouter.replace('/');
|
|
70
|
+
await expect(errorRouter.push('/error')).rejects.toThrow();
|
|
71
|
+
|
|
72
|
+
errorRouter.destroy();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Concurrent navigation', () => {
|
|
77
|
+
test('should handle concurrent navigation attempts', async () => {
|
|
78
|
+
const promises = Array.from({ length: 5 }, (_, i) =>
|
|
79
|
+
router.push(`/user/${i + 1}`).catch((err) => err)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const results = await Promise.all(promises);
|
|
83
|
+
|
|
84
|
+
const successResults = results.filter((r) => !(r instanceof Error));
|
|
85
|
+
const abortedResults = results.filter((r) => r instanceof Error);
|
|
86
|
+
|
|
87
|
+
expect(successResults).toHaveLength(1);
|
|
88
|
+
expect(abortedResults).toHaveLength(4);
|
|
89
|
+
|
|
90
|
+
const successResult = successResults[0] as Route;
|
|
91
|
+
expect(router.route.path).toBe(successResult.path);
|
|
92
|
+
expect(router.route.params.id).toBe(successResult.params.id);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Route guards', () => {
|
|
97
|
+
let guardRouter: Router;
|
|
98
|
+
let guardLog: string[];
|
|
99
|
+
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
guardLog = [];
|
|
102
|
+
|
|
103
|
+
guardRouter = new Router({
|
|
104
|
+
mode: RouterMode.memory,
|
|
105
|
+
base: new URL('http://localhost:3000/'),
|
|
106
|
+
routes: [
|
|
107
|
+
{ path: '/', component: () => 'Home' },
|
|
108
|
+
{
|
|
109
|
+
path: '/protected',
|
|
110
|
+
component: () => 'Protected',
|
|
111
|
+
beforeEnter: () => {
|
|
112
|
+
guardLog.push('beforeEnter-protected');
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: '/allowed',
|
|
118
|
+
component: () => 'Allowed',
|
|
119
|
+
beforeEnter: () => {
|
|
120
|
+
guardLog.push('beforeEnter-allowed');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await guardRouter.replace('/');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
guardRouter.destroy();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should block navigation when guard returns false', async () => {
|
|
134
|
+
await expect(guardRouter.push('/protected')).rejects.toThrow();
|
|
135
|
+
expect(guardLog).toContain('beforeEnter-protected');
|
|
136
|
+
expect(guardRouter.route.path).toBe('/');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should allow navigation when guard returns true', async () => {
|
|
140
|
+
const route = await guardRouter.push('/allowed');
|
|
141
|
+
|
|
142
|
+
expect(guardLog).toContain('beforeEnter-allowed');
|
|
143
|
+
expect(route.path).toBe('/allowed');
|
|
144
|
+
expect(guardRouter.route.path).toBe('/allowed');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Navigation types', () => {
|
|
149
|
+
test('should correctly set route type for push navigation', async () => {
|
|
150
|
+
const route = await router.push('/user/123');
|
|
151
|
+
expect(route.type).toBe(RouteType.push);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('should correctly set route type for replace navigation', async () => {
|
|
155
|
+
const route = await router.replace('/user/123');
|
|
156
|
+
expect(route.type).toBe(RouteType.replace);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Route parameters and query', () => {
|
|
161
|
+
test('should correctly extract route parameters', async () => {
|
|
162
|
+
const route = await router.push('/user/456');
|
|
163
|
+
|
|
164
|
+
expect(route.params.id).toBe('456');
|
|
165
|
+
expect(route.path).toBe('/user/456');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should handle query parameters', async () => {
|
|
169
|
+
const route = await router.push(
|
|
170
|
+
'/user/123?tab=profile&active=true'
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(route.params.id).toBe('123');
|
|
174
|
+
expect(route.query.tab).toBe('profile');
|
|
175
|
+
expect(route.query.active).toBe('true');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { Route } from './route';
|
|
2
|
+
import {
|
|
3
|
+
type RouteTask,
|
|
4
|
+
RouteTaskController,
|
|
5
|
+
createRouteTask
|
|
6
|
+
} from './route-task';
|
|
7
|
+
import type { Router } from './router';
|
|
8
|
+
import { RouteType } from './types';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
RouteConfirmHook,
|
|
12
|
+
RouteConfirmHookResult,
|
|
13
|
+
RouteHandleHook,
|
|
14
|
+
RouteLocationInput,
|
|
15
|
+
RouteNotifyHook
|
|
16
|
+
} from './types';
|
|
17
|
+
import {
|
|
18
|
+
isRouteMatched,
|
|
19
|
+
isUrlEqual,
|
|
20
|
+
isValidConfirmHookResult,
|
|
21
|
+
removeFromArray
|
|
22
|
+
} from './util';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Route transition hooks responsible for handling different stages of the navigation process.
|
|
26
|
+
* Each hook is responsible for a specific aspect of route transition.
|
|
27
|
+
*/
|
|
28
|
+
export const ROUTE_TRANSITION_HOOKS = {
|
|
29
|
+
async fallback(
|
|
30
|
+
to: Route,
|
|
31
|
+
from: Route | null,
|
|
32
|
+
router: Router
|
|
33
|
+
): Promise<RouteConfirmHookResult> {
|
|
34
|
+
if (to.matched.length === 0) {
|
|
35
|
+
return router.parsedOptions.fallback;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async override(
|
|
40
|
+
to: Route,
|
|
41
|
+
from: Route | null,
|
|
42
|
+
router: Router
|
|
43
|
+
): Promise<RouteConfirmHookResult> {
|
|
44
|
+
const result = await to.config?.override?.(to, from, router);
|
|
45
|
+
if (isValidConfirmHookResult(result)) {
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async asyncComponent(
|
|
51
|
+
to: Route,
|
|
52
|
+
from: Route | null,
|
|
53
|
+
router: Router
|
|
54
|
+
): Promise<RouteConfirmHookResult> {
|
|
55
|
+
await Promise.all(
|
|
56
|
+
to.matched.map(async (matched) => {
|
|
57
|
+
const { asyncComponent, component } = matched;
|
|
58
|
+
if (!component && typeof asyncComponent === 'function') {
|
|
59
|
+
try {
|
|
60
|
+
const result = await asyncComponent();
|
|
61
|
+
matched.component = result;
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Async component '${matched.compilePath}' is not a valid component.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async beforeLeave(
|
|
73
|
+
to: Route,
|
|
74
|
+
from: Route | null,
|
|
75
|
+
router: Router
|
|
76
|
+
): Promise<RouteConfirmHookResult> {
|
|
77
|
+
if (!from?.matched.length) return;
|
|
78
|
+
|
|
79
|
+
// Find routes that need to be left (routes in 'from' but not in 'to').
|
|
80
|
+
const leavingRoutes = from.matched.filter(
|
|
81
|
+
(fromRoute) => !to.matched.some((toRoute) => toRoute === fromRoute)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Execute beforeLeave guards in order from child to parent.
|
|
85
|
+
for (let i = leavingRoutes.length - 1; i >= 0; i--) {
|
|
86
|
+
const route = leavingRoutes[i];
|
|
87
|
+
if (route.beforeLeave) {
|
|
88
|
+
const result = await route.beforeLeave(to, from, router);
|
|
89
|
+
if (isValidConfirmHookResult(result)) {
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async beforeEnter(
|
|
97
|
+
to: Route,
|
|
98
|
+
from: Route | null,
|
|
99
|
+
router: Router
|
|
100
|
+
): Promise<RouteConfirmHookResult> {
|
|
101
|
+
if (!to.matched.length) return;
|
|
102
|
+
|
|
103
|
+
// Find routes that need to be entered (routes in 'to' but not in 'from').
|
|
104
|
+
const enteringRoutes = to.matched.filter(
|
|
105
|
+
(toRoute) =>
|
|
106
|
+
!from?.matched.some((fromRoute) => fromRoute === toRoute)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Execute beforeEnter guards in order from parent to child.
|
|
110
|
+
for (const route of enteringRoutes) {
|
|
111
|
+
if (route.beforeEnter) {
|
|
112
|
+
const result = await route.beforeEnter(to, from, router);
|
|
113
|
+
if (isValidConfirmHookResult(result)) {
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async beforeUpdate(
|
|
121
|
+
to: Route,
|
|
122
|
+
from: Route | null,
|
|
123
|
+
router: Router
|
|
124
|
+
): Promise<RouteConfirmHookResult> {
|
|
125
|
+
// beforeUpdate is only executed when parameters change within the exact same route combination.
|
|
126
|
+
// Quick check: if the final route configs are different, it's definitely not the same combination.
|
|
127
|
+
if (!isRouteMatched(to, from, 'route')) return;
|
|
128
|
+
|
|
129
|
+
// Detailed check: the 'matched' arrays of both routes must be identical.
|
|
130
|
+
if (!from || to.matched.length !== from.matched.length) return;
|
|
131
|
+
const isSameRouteSet = to.matched.every(
|
|
132
|
+
(toRoute, index) => toRoute === from.matched[index]
|
|
133
|
+
);
|
|
134
|
+
if (!isSameRouteSet) return;
|
|
135
|
+
|
|
136
|
+
// Only execute beforeUpdate when path parameters or query parameters change.
|
|
137
|
+
if (!isRouteMatched(to, from, 'exact')) {
|
|
138
|
+
// Execute beforeUpdate guards in order from parent to child.
|
|
139
|
+
for (const route of to.matched) {
|
|
140
|
+
if (route.beforeUpdate) {
|
|
141
|
+
const result = await route.beforeUpdate(to, from, router);
|
|
142
|
+
if (isValidConfirmHookResult(result)) {
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async beforeEach(
|
|
151
|
+
to: Route,
|
|
152
|
+
from: Route | null,
|
|
153
|
+
router: Router
|
|
154
|
+
): Promise<RouteConfirmHookResult> {
|
|
155
|
+
// Access the transition instance from the router to get guards
|
|
156
|
+
const transition = router.transition;
|
|
157
|
+
for (const guard of transition.guards.beforeEach) {
|
|
158
|
+
const result = await guard(to, from, router);
|
|
159
|
+
if (isValidConfirmHookResult(result)) {
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async confirm(
|
|
166
|
+
to: Route,
|
|
167
|
+
from: Route | null,
|
|
168
|
+
router: Router
|
|
169
|
+
): Promise<RouteConfirmHookResult> {
|
|
170
|
+
if (to.confirm) {
|
|
171
|
+
const result = await to.confirm(to, from, router);
|
|
172
|
+
if (isValidConfirmHookResult(result)) {
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
switch (to.type) {
|
|
178
|
+
case RouteType.push:
|
|
179
|
+
return ROUTE_TYPE_HANDLERS.push;
|
|
180
|
+
case RouteType.replace:
|
|
181
|
+
return ROUTE_TYPE_HANDLERS.replace;
|
|
182
|
+
case RouteType.restartApp:
|
|
183
|
+
return ROUTE_TYPE_HANDLERS.restartApp;
|
|
184
|
+
case RouteType.pushWindow:
|
|
185
|
+
return ROUTE_TYPE_HANDLERS.pushWindow;
|
|
186
|
+
case RouteType.replaceWindow:
|
|
187
|
+
return ROUTE_TYPE_HANDLERS.replaceWindow;
|
|
188
|
+
case RouteType.pushLayer:
|
|
189
|
+
return ROUTE_TYPE_HANDLERS.pushLayer;
|
|
190
|
+
default:
|
|
191
|
+
return ROUTE_TYPE_HANDLERS.default;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} satisfies Record<string, RouteConfirmHook>;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Route type handlers configuration.
|
|
198
|
+
* Maps each route type to its corresponding navigation handler function.
|
|
199
|
+
* These handlers perform the actual navigation operations like updating browser state,
|
|
200
|
+
* managing micro-app updates, and handling different navigation patterns.
|
|
201
|
+
*/
|
|
202
|
+
export const ROUTE_TYPE_HANDLERS = {
|
|
203
|
+
push(to, from, router) {
|
|
204
|
+
router.transition.route = to;
|
|
205
|
+
router.microApp._update(router);
|
|
206
|
+
if (!isUrlEqual(to.url, from?.url)) {
|
|
207
|
+
const newState = router.navigation.push(to.state, to.url);
|
|
208
|
+
to.applyNavigationState(newState);
|
|
209
|
+
} else {
|
|
210
|
+
const newState = router.navigation.replace(to.state, to.url);
|
|
211
|
+
to.applyNavigationState(newState);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
replace(to, from, router) {
|
|
215
|
+
router.transition.route = to;
|
|
216
|
+
router.microApp._update(router);
|
|
217
|
+
const newState = router.navigation.replace(to.state, to.url);
|
|
218
|
+
to.applyNavigationState(newState);
|
|
219
|
+
},
|
|
220
|
+
restartApp(to, from, router) {
|
|
221
|
+
router.transition.route = to;
|
|
222
|
+
router.microApp._update(router, true);
|
|
223
|
+
const newState = router.navigation.replace(to.state, to.url);
|
|
224
|
+
to.applyNavigationState(newState);
|
|
225
|
+
},
|
|
226
|
+
pushWindow(to, from, router) {
|
|
227
|
+
return router.parsedOptions.fallback(to, from, router);
|
|
228
|
+
},
|
|
229
|
+
replaceWindow(to, from, router) {
|
|
230
|
+
return router.parsedOptions.fallback(to, from, router);
|
|
231
|
+
},
|
|
232
|
+
async pushLayer(to, from, router) {
|
|
233
|
+
const { promise } = await router.createLayer(to);
|
|
234
|
+
return promise;
|
|
235
|
+
},
|
|
236
|
+
default(to, from, router) {
|
|
237
|
+
router.transition.route = to;
|
|
238
|
+
router.microApp._update(router);
|
|
239
|
+
if (!isUrlEqual(to.url, from?.url)) {
|
|
240
|
+
const newState = router.navigation.replace(to.state, to.url);
|
|
241
|
+
to.applyNavigationState(newState);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} satisfies Record<string, RouteHandleHook>;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Route transition pipeline configuration.
|
|
248
|
+
* Defines the sequence of hooks and guards that should be executed for each route type.
|
|
249
|
+
* The order matters: hooks are executed sequentially from first to last.
|
|
250
|
+
*
|
|
251
|
+
* Pipeline stages:
|
|
252
|
+
* - fallback: Handle unmatched routes
|
|
253
|
+
* - override: Allow route override logic
|
|
254
|
+
* - beforeLeave: Execute before leaving current route
|
|
255
|
+
* - beforeEach: Global navigation guard
|
|
256
|
+
* - beforeUpdate: Execute before updating route (same component)
|
|
257
|
+
* - beforeEnter: Execute before entering new route
|
|
258
|
+
* - asyncComponent: Load async components
|
|
259
|
+
* - confirm: Final confirmation and navigation execution
|
|
260
|
+
*/
|
|
261
|
+
const ROUTE_TRANSITION_PIPELINE = {
|
|
262
|
+
[RouteType.push]: [
|
|
263
|
+
ROUTE_TRANSITION_HOOKS.fallback,
|
|
264
|
+
ROUTE_TRANSITION_HOOKS.override,
|
|
265
|
+
ROUTE_TRANSITION_HOOKS.beforeLeave,
|
|
266
|
+
ROUTE_TRANSITION_HOOKS.beforeEach,
|
|
267
|
+
ROUTE_TRANSITION_HOOKS.beforeUpdate,
|
|
268
|
+
ROUTE_TRANSITION_HOOKS.beforeEnter,
|
|
269
|
+
ROUTE_TRANSITION_HOOKS.asyncComponent,
|
|
270
|
+
ROUTE_TRANSITION_HOOKS.confirm
|
|
271
|
+
],
|
|
272
|
+
|
|
273
|
+
[RouteType.replace]: [
|
|
274
|
+
ROUTE_TRANSITION_HOOKS.fallback,
|
|
275
|
+
ROUTE_TRANSITION_HOOKS.override,
|
|
276
|
+
ROUTE_TRANSITION_HOOKS.beforeLeave,
|
|
277
|
+
ROUTE_TRANSITION_HOOKS.beforeEach,
|
|
278
|
+
ROUTE_TRANSITION_HOOKS.beforeUpdate,
|
|
279
|
+
ROUTE_TRANSITION_HOOKS.beforeEnter,
|
|
280
|
+
ROUTE_TRANSITION_HOOKS.asyncComponent,
|
|
281
|
+
ROUTE_TRANSITION_HOOKS.confirm
|
|
282
|
+
],
|
|
283
|
+
[RouteType.pushWindow]: [
|
|
284
|
+
ROUTE_TRANSITION_HOOKS.fallback,
|
|
285
|
+
ROUTE_TRANSITION_HOOKS.override,
|
|
286
|
+
// ROUTE_TRANSITION_HOOKS.beforeLeave
|
|
287
|
+
ROUTE_TRANSITION_HOOKS.beforeEach,
|
|
288
|
+
// ROUTE_TRANSITION_HOOKS.beforeUpdate
|
|
289
|
+
// ROUTE_TRANSITION_HOOKS.beforeEnter
|
|
290
|
+
// ROUTE_TRANSITION_HOOKS.asyncComponent
|
|
291
|
+
ROUTE_TRANSITION_HOOKS.confirm
|
|
292
|
+
],
|
|
293
|
+
|
|
294
|
+
[RouteType.replaceWindow]: [
|
|
295
|
+
ROUTE_TRANSITION_HOOKS.fallback,
|
|
296
|
+
ROUTE_TRANSITION_HOOKS.override,
|
|
297
|
+
ROUTE_TRANSITION_HOOKS.beforeLeave,
|
|
298
|
+
ROUTE_TRANSITION_HOOKS.beforeEach,
|
|
299
|
+
// ROUTE_TRANSITION_HOOKS.beforeUpdate
|
|
300
|
+
// ROUTE_TRANSITION_HOOKS.beforeEnter
|
|
301
|
+
// ROUTE_TRANSITION_HOOKS.asyncComponent
|
|
302
|
+
ROUTE_TRANSITION_HOOKS.confirm
|
|
303
|
+
],
|
|
304
|
+
[RouteType.pushLayer]: [
|
|
305
|
+
ROUTE_TRANSITION_HOOKS.fallback,
|
|
306
|
+
ROUTE_TRANSITION_HOOKS.override,
|
|
307
|
+
// ROUTE_TRANSITION_HOOKS.beforeLeave
|
|
308
|
+
ROUTE_TRANSITION_HOOKS.beforeEach,
|
|
309
|
+
// ROUTE_TRANSITION_HOOKS.beforeUpdate
|
|
310
|
+
// ROUTE_TRANSITION_HOOKS.beforeEnter
|
|
311
|
+
// ROUTE_TRANSITION_HOOKS.asyncComponent
|
|
312
|
+
ROUTE_TRANSITION_HOOKS.confirm
|
|
313
|
+
],
|
|
314
|
+
[RouteType.restartApp]: [
|
|
315
|
+
ROUTE_TRANSITION_HOOKS.fallback,
|
|
316
|
+
// ROUTE_TRANSITION_HOOKS.override,
|
|
317
|
+
ROUTE_TRANSITION_HOOKS.beforeLeave,
|
|
318
|
+
ROUTE_TRANSITION_HOOKS.beforeEach,
|
|
319
|
+
ROUTE_TRANSITION_HOOKS.beforeUpdate,
|
|
320
|
+
ROUTE_TRANSITION_HOOKS.beforeEnter,
|
|
321
|
+
ROUTE_TRANSITION_HOOKS.asyncComponent,
|
|
322
|
+
ROUTE_TRANSITION_HOOKS.confirm
|
|
323
|
+
],
|
|
324
|
+
|
|
325
|
+
[RouteType.unknown]: [
|
|
326
|
+
ROUTE_TRANSITION_HOOKS.fallback,
|
|
327
|
+
// ROUTE_TRANSITION_HOOKS.override,
|
|
328
|
+
ROUTE_TRANSITION_HOOKS.beforeLeave,
|
|
329
|
+
ROUTE_TRANSITION_HOOKS.beforeEach,
|
|
330
|
+
ROUTE_TRANSITION_HOOKS.beforeUpdate,
|
|
331
|
+
ROUTE_TRANSITION_HOOKS.beforeEnter,
|
|
332
|
+
ROUTE_TRANSITION_HOOKS.asyncComponent,
|
|
333
|
+
ROUTE_TRANSITION_HOOKS.confirm
|
|
334
|
+
]
|
|
335
|
+
} satisfies Record<string, RouteConfirmHook[]>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Route Transition Manager
|
|
339
|
+
* Responsible for managing all route transition logic, including guard execution,
|
|
340
|
+
* task processing, and status updates.
|
|
341
|
+
*/
|
|
342
|
+
export class RouteTransition {
|
|
343
|
+
private readonly router: Router;
|
|
344
|
+
|
|
345
|
+
public route: Route | null = null;
|
|
346
|
+
|
|
347
|
+
// Task controller for the current transition.
|
|
348
|
+
private _controller: RouteTaskController | null = null;
|
|
349
|
+
|
|
350
|
+
// Guard arrays, responsible for storing navigation guards.
|
|
351
|
+
public readonly guards = {
|
|
352
|
+
beforeEach: [] as RouteConfirmHook[],
|
|
353
|
+
afterEach: [] as RouteNotifyHook[]
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
constructor(router: Router) {
|
|
357
|
+
this.router = router;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public beforeEach(guard: RouteConfirmHook): () => void {
|
|
361
|
+
this.guards.beforeEach.push(guard);
|
|
362
|
+
return () => {
|
|
363
|
+
removeFromArray(this.guards.beforeEach, guard);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public afterEach(guard: RouteNotifyHook): () => void {
|
|
368
|
+
this.guards.afterEach.push(guard);
|
|
369
|
+
return () => {
|
|
370
|
+
removeFromArray(this.guards.afterEach, guard);
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
public destroy(): void {
|
|
375
|
+
this._controller?.abort();
|
|
376
|
+
this._controller = null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
public async to(
|
|
380
|
+
toType: RouteType,
|
|
381
|
+
toInput: RouteLocationInput
|
|
382
|
+
): Promise<Route> {
|
|
383
|
+
const from = this.route;
|
|
384
|
+
const to = await this._runTask(
|
|
385
|
+
new Route({
|
|
386
|
+
options: this.router.parsedOptions,
|
|
387
|
+
toType,
|
|
388
|
+
toInput,
|
|
389
|
+
from: from?.url ?? null
|
|
390
|
+
}),
|
|
391
|
+
from
|
|
392
|
+
);
|
|
393
|
+
if (typeof to.handle === 'function') {
|
|
394
|
+
to.handleResult = await to.handle(to, from, this.router);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (to.handle) {
|
|
398
|
+
for (const guard of this.guards.afterEach) {
|
|
399
|
+
guard(to, from, this.router);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return to;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private async _runTask(to: Route, from: Route | null): Promise<Route> {
|
|
407
|
+
this._controller?.abort();
|
|
408
|
+
this._controller = new RouteTaskController();
|
|
409
|
+
const taskFunctions: RouteConfirmHook[] =
|
|
410
|
+
ROUTE_TRANSITION_PIPELINE[to.type] ||
|
|
411
|
+
ROUTE_TRANSITION_PIPELINE[RouteType.unknown];
|
|
412
|
+
const tasks = taskFunctions.map<RouteTask>((taskFn) => ({
|
|
413
|
+
name: taskFn.name,
|
|
414
|
+
task: taskFn
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
return createRouteTask({
|
|
418
|
+
to,
|
|
419
|
+
from,
|
|
420
|
+
tasks,
|
|
421
|
+
controller: this._controller,
|
|
422
|
+
router: this.router
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|