@esmx/router 3.0.0-rc.12 → 3.0.0-rc.122

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/README.zh-CN.md +158 -0
  4. package/dist/error.d.ts +23 -0
  5. package/dist/error.mjs +64 -0
  6. package/dist/increment-id.d.ts +7 -0
  7. package/dist/increment-id.mjs +16 -0
  8. package/dist/index.d.ts +14 -3
  9. package/dist/index.mjs +13 -3
  10. package/dist/location.d.ts +22 -0
  11. package/dist/location.mjs +64 -0
  12. package/dist/matcher.d.ts +4 -0
  13. package/dist/matcher.mjs +49 -0
  14. package/dist/micro-app.d.ts +20 -0
  15. package/dist/micro-app.mjs +132 -0
  16. package/dist/navigation.d.ts +45 -0
  17. package/dist/navigation.mjs +153 -0
  18. package/dist/options.d.ts +4 -0
  19. package/dist/options.mjs +95 -0
  20. package/dist/route-task.d.ts +40 -0
  21. package/dist/route-task.mjs +77 -0
  22. package/dist/route-transition.d.ts +54 -0
  23. package/dist/route-transition.mjs +362 -0
  24. package/dist/route.d.ts +77 -0
  25. package/dist/route.mjs +223 -0
  26. package/dist/router-link.d.ts +10 -0
  27. package/dist/router-link.mjs +142 -0
  28. package/dist/router.d.ts +113 -102
  29. package/dist/router.mjs +321 -354
  30. package/dist/scroll.d.ts +33 -0
  31. package/dist/scroll.mjs +50 -0
  32. package/dist/types.d.ts +312 -0
  33. package/dist/types.mjs +18 -0
  34. package/dist/util.d.ts +32 -0
  35. package/dist/util.mjs +100 -0
  36. package/package.json +89 -62
  37. package/src/error.ts +84 -0
  38. package/src/increment-id.ts +12 -0
  39. package/src/index.ts +67 -3
  40. package/src/location.ts +124 -0
  41. package/src/matcher.ts +71 -0
  42. package/src/micro-app.ts +153 -0
  43. package/src/navigation.ts +202 -0
  44. package/src/options.ts +136 -0
  45. package/src/route-task.ts +102 -0
  46. package/src/route-transition.ts +480 -0
  47. package/src/route.ts +335 -0
  48. package/src/router-link.ts +241 -0
  49. package/src/router.ts +351 -467
  50. package/src/scroll.ts +106 -0
  51. package/src/types.ts +415 -0
  52. package/src/util.ts +184 -0
  53. package/dist/history/abstract.d.ts +0 -29
  54. package/dist/history/abstract.mjs +0 -107
  55. package/dist/history/base.d.ts +0 -79
  56. package/dist/history/base.mjs +0 -275
  57. package/dist/history/html.d.ts +0 -22
  58. package/dist/history/html.mjs +0 -181
  59. package/dist/history/index.d.ts +0 -7
  60. package/dist/history/index.mjs +0 -16
  61. package/dist/matcher/create-matcher.d.ts +0 -5
  62. package/dist/matcher/create-matcher.mjs +0 -218
  63. package/dist/matcher/create-matcher.spec.d.ts +0 -1
  64. package/dist/matcher/create-matcher.spec.mjs +0 -0
  65. package/dist/matcher/index.d.ts +0 -1
  66. package/dist/matcher/index.mjs +0 -1
  67. package/dist/task-pipe/index.d.ts +0 -1
  68. package/dist/task-pipe/index.mjs +0 -1
  69. package/dist/task-pipe/task.d.ts +0 -30
  70. package/dist/task-pipe/task.mjs +0 -66
  71. package/dist/utils/bom.d.ts +0 -5
  72. package/dist/utils/bom.mjs +0 -10
  73. package/dist/utils/encoding.d.ts +0 -48
  74. package/dist/utils/encoding.mjs +0 -44
  75. package/dist/utils/guards.d.ts +0 -9
  76. package/dist/utils/guards.mjs +0 -12
  77. package/dist/utils/index.d.ts +0 -7
  78. package/dist/utils/index.mjs +0 -27
  79. package/dist/utils/path.d.ts +0 -60
  80. package/dist/utils/path.mjs +0 -264
  81. package/dist/utils/path.spec.d.ts +0 -1
  82. package/dist/utils/path.spec.mjs +0 -30
  83. package/dist/utils/scroll.d.ts +0 -25
  84. package/dist/utils/scroll.mjs +0 -59
  85. package/dist/utils/utils.d.ts +0 -16
  86. package/dist/utils/utils.mjs +0 -11
  87. package/dist/utils/warn.d.ts +0 -2
  88. package/dist/utils/warn.mjs +0 -12
  89. package/src/history/abstract.ts +0 -149
  90. package/src/history/base.ts +0 -408
  91. package/src/history/html.ts +0 -231
  92. package/src/history/index.ts +0 -20
  93. package/src/matcher/create-matcher.spec.ts +0 -3
  94. package/src/matcher/create-matcher.ts +0 -293
  95. package/src/matcher/index.ts +0 -1
  96. package/src/task-pipe/index.ts +0 -1
  97. package/src/task-pipe/task.ts +0 -97
  98. package/src/utils/bom.ts +0 -14
  99. package/src/utils/encoding.ts +0 -153
  100. package/src/utils/guards.ts +0 -25
  101. package/src/utils/index.ts +0 -27
  102. package/src/utils/path.spec.ts +0 -44
  103. package/src/utils/path.ts +0 -397
  104. package/src/utils/scroll.ts +0 -120
  105. package/src/utils/utils.ts +0 -30
  106. package/src/utils/warn.ts +0 -13
@@ -0,0 +1,480 @@
1
+ import { Route } from './route';
2
+ import {
3
+ createRouteTask,
4
+ type RouteTask,
5
+ RouteTaskController
6
+ } from './route-task';
7
+ import type { Router } from './router';
8
+ import {
9
+ getSavedScrollPosition,
10
+ saveScrollPosition,
11
+ scrollToPosition
12
+ } from './scroll';
13
+ import type {
14
+ RouteConfirmHook,
15
+ RouteConfirmHookResult,
16
+ RouteHandleHook,
17
+ RouteLocationInput,
18
+ RouteNotifyHook
19
+ } from './types';
20
+ import { RouteType } from './types';
21
+ import {
22
+ isBrowser,
23
+ isRouteMatched,
24
+ isUrlEqual,
25
+ isValidConfirmHookResult,
26
+ removeFromArray
27
+ } from './util';
28
+
29
+ /**
30
+ * Route transition hooks responsible for handling different stages of the navigation process.
31
+ * Each hook is responsible for a specific aspect of route transition.
32
+ */
33
+ export const ROUTE_TRANSITION_HOOKS = {
34
+ async fallback(
35
+ to: Route,
36
+ from: Route | null,
37
+ router: Router
38
+ ): Promise<RouteConfirmHookResult> {
39
+ if (to.matched.length === 0) {
40
+ return router.parsedOptions.fallback;
41
+ }
42
+ },
43
+
44
+ async override(
45
+ to: Route,
46
+ from: Route | null,
47
+ router: Router
48
+ ): Promise<RouteConfirmHookResult> {
49
+ const result = await to.config?.override?.(to, from, router);
50
+ if (isValidConfirmHookResult(result)) {
51
+ return result;
52
+ }
53
+ },
54
+
55
+ async asyncComponent(
56
+ to: Route,
57
+ from: Route | null,
58
+ router: Router
59
+ ): Promise<RouteConfirmHookResult> {
60
+ await Promise.all(
61
+ to.matched.map(async (matched) => {
62
+ const { asyncComponent, component } = matched;
63
+ if (!component && typeof asyncComponent === 'function') {
64
+ try {
65
+ const result = await asyncComponent();
66
+ matched.component = result;
67
+ } catch (e) {
68
+ const error =
69
+ e instanceof Error ? e : new Error(String(e));
70
+ throw new Error(
71
+ `Async component '${matched.compilePath}' is not a valid component. Original error: ${error.message}`
72
+ );
73
+ }
74
+ }
75
+ })
76
+ );
77
+ },
78
+
79
+ async beforeLeave(
80
+ to: Route,
81
+ from: Route | null,
82
+ router: Router
83
+ ): Promise<RouteConfirmHookResult> {
84
+ if (!from?.matched.length) return;
85
+
86
+ // Find routes that need to be left (routes in 'from' but not in 'to').
87
+ const leavingRoutes = from.matched.filter(
88
+ (fromRoute) => !to.matched.some((toRoute) => toRoute === fromRoute)
89
+ );
90
+
91
+ // Execute beforeLeave guards in order from child to parent.
92
+ for (let i = leavingRoutes.length - 1; i >= 0; i--) {
93
+ const route = leavingRoutes[i];
94
+ if (route.beforeLeave) {
95
+ const result = await route.beforeLeave(to, from, router);
96
+ if (isValidConfirmHookResult(result)) {
97
+ return result;
98
+ }
99
+ }
100
+ }
101
+ },
102
+
103
+ async beforeEnter(
104
+ to: Route,
105
+ from: Route | null,
106
+ router: Router
107
+ ): Promise<RouteConfirmHookResult> {
108
+ if (!to.matched.length) return;
109
+
110
+ // Find routes that need to be entered (routes in 'to' but not in 'from').
111
+ const enteringRoutes = to.matched.filter(
112
+ (toRoute) =>
113
+ !from?.matched.some((fromRoute) => fromRoute === toRoute)
114
+ );
115
+
116
+ // Execute beforeEnter guards in order from parent to child.
117
+ for (const route of enteringRoutes) {
118
+ if (route.beforeEnter) {
119
+ const result = await route.beforeEnter(to, from, router);
120
+ if (isValidConfirmHookResult(result)) {
121
+ return result;
122
+ }
123
+ }
124
+ }
125
+ },
126
+
127
+ async beforeUpdate(
128
+ to: Route,
129
+ from: Route | null,
130
+ router: Router
131
+ ): Promise<RouteConfirmHookResult> {
132
+ // beforeUpdate is only executed when parameters change within the exact same route combination.
133
+ // Quick check: if the final route configs are different, it's definitely not the same combination.
134
+ if (!isRouteMatched(to, from, 'route')) return;
135
+
136
+ // Detailed check: the 'matched' arrays of both routes must be identical.
137
+ if (!from || to.matched.length !== from.matched.length) return;
138
+ const isSameRouteSet = to.matched.every(
139
+ (toRoute, index) => toRoute === from.matched[index]
140
+ );
141
+ if (!isSameRouteSet) return;
142
+
143
+ // Only execute beforeUpdate when path parameters or query parameters change.
144
+ if (!isRouteMatched(to, from, 'exact')) {
145
+ // Execute beforeUpdate guards in order from parent to child.
146
+ for (const route of to.matched) {
147
+ if (route.beforeUpdate) {
148
+ const result = await route.beforeUpdate(to, from, router);
149
+ if (isValidConfirmHookResult(result)) {
150
+ return result;
151
+ }
152
+ }
153
+ }
154
+ }
155
+ },
156
+
157
+ async beforeEach(
158
+ to: Route,
159
+ from: Route | null,
160
+ router: Router
161
+ ): Promise<RouteConfirmHookResult> {
162
+ // Access the transition instance from the router to get guards
163
+ const transition = router.transition;
164
+ for (const guard of transition.guards.beforeEach) {
165
+ const result = await guard(to, from, router);
166
+ if (isValidConfirmHookResult(result)) {
167
+ return result;
168
+ }
169
+ }
170
+ },
171
+
172
+ async confirm(
173
+ to: Route,
174
+ from: Route | null,
175
+ router: Router
176
+ ): Promise<RouteConfirmHookResult> {
177
+ if (to.confirm) {
178
+ const result = await to.confirm(to, from, router);
179
+ if (isValidConfirmHookResult(result)) {
180
+ return result;
181
+ }
182
+ }
183
+
184
+ if (isBrowser && 'scrollRestoration' in window.history)
185
+ window.history.scrollRestoration = 'manual';
186
+ // handle scroll position
187
+ if (from && isBrowser && !router.isLayer)
188
+ switch (to.type) {
189
+ case RouteType.push:
190
+ case RouteType.replace: {
191
+ if (!to.keepScrollPosition) {
192
+ saveScrollPosition(from.url.href);
193
+ scrollToPosition({ left: 0, top: 0 });
194
+ } else {
195
+ to.applyNavigationState({
196
+ __keepScrollPosition: to.keepScrollPosition
197
+ });
198
+ }
199
+ break;
200
+ }
201
+ case RouteType.go:
202
+ case RouteType.forward:
203
+ case RouteType.back:
204
+ // for popstate
205
+ case RouteType.unknown: {
206
+ saveScrollPosition(from.url.href);
207
+ setTimeout(async () => {
208
+ const state = window.history.state;
209
+ if (state?.__keepScrollPosition) {
210
+ return;
211
+ }
212
+ const savedPosition = getSavedScrollPosition(
213
+ to.url.href,
214
+ { left: 0, top: 0 }
215
+ );
216
+ if (!savedPosition) return;
217
+ await router.parsedOptions.nextTick();
218
+ scrollToPosition(savedPosition);
219
+ });
220
+ break;
221
+ }
222
+ }
223
+
224
+ switch (to.type) {
225
+ case RouteType.push:
226
+ return ROUTE_TYPE_HANDLERS.push;
227
+ case RouteType.replace:
228
+ return ROUTE_TYPE_HANDLERS.replace;
229
+ case RouteType.restartApp:
230
+ return ROUTE_TYPE_HANDLERS.restartApp;
231
+ case RouteType.pushWindow:
232
+ return ROUTE_TYPE_HANDLERS.pushWindow;
233
+ case RouteType.replaceWindow:
234
+ return ROUTE_TYPE_HANDLERS.replaceWindow;
235
+ case RouteType.pushLayer:
236
+ return ROUTE_TYPE_HANDLERS.pushLayer;
237
+ default:
238
+ return ROUTE_TYPE_HANDLERS.default;
239
+ }
240
+ }
241
+ } satisfies Record<string, RouteConfirmHook>;
242
+
243
+ /**
244
+ * Route type handlers configuration.
245
+ * Maps each route type to its corresponding navigation handler function.
246
+ * These handlers perform the actual navigation operations like updating browser state,
247
+ * managing micro-app updates, and handling different navigation patterns.
248
+ */
249
+ export const ROUTE_TYPE_HANDLERS = {
250
+ push(to, from, router) {
251
+ router.transition.route = to;
252
+ router.microApp._update(router);
253
+ if (!isUrlEqual(to.url, from?.url)) {
254
+ const newState = router.navigation.push(to.state, to.url);
255
+ to.applyNavigationState(newState);
256
+ } else {
257
+ const newState = router.navigation.replace(to.state, to.url);
258
+ to.applyNavigationState(newState);
259
+ }
260
+ },
261
+ replace(to, from, router) {
262
+ router.transition.route = to;
263
+ router.microApp._update(router);
264
+ const newState = router.navigation.replace(to.state, to.url);
265
+ to.applyNavigationState(newState);
266
+ },
267
+ restartApp(to, from, router) {
268
+ router.transition.route = to;
269
+ router.microApp._update(router, true);
270
+ const newState = router.navigation.replace(to.state, to.url);
271
+ to.applyNavigationState(newState);
272
+ },
273
+ pushWindow(to, from, router) {
274
+ return router.parsedOptions.fallback(to, from, router);
275
+ },
276
+ replaceWindow(to, from, router) {
277
+ return router.parsedOptions.fallback(to, from, router);
278
+ },
279
+ async pushLayer(to, from, router) {
280
+ const { promise } = await router.createLayer(to);
281
+ return promise;
282
+ },
283
+ default(to, from, router) {
284
+ router.transition.route = to;
285
+ router.microApp._update(router);
286
+ if (!isUrlEqual(to.url, from?.url)) {
287
+ const newState = router.navigation.replace(to.state, to.url);
288
+ to.applyNavigationState(newState);
289
+ }
290
+ }
291
+ } satisfies Record<string, RouteHandleHook>;
292
+
293
+ /**
294
+ * Route transition pipeline configuration.
295
+ * Defines the sequence of hooks and guards that should be executed for each route type.
296
+ * The order matters: hooks are executed sequentially from first to last.
297
+ *
298
+ * Pipeline stages:
299
+ * - fallback: Handle unmatched routes
300
+ * - override: Allow route override logic
301
+ * - beforeLeave: Execute before leaving current route
302
+ * - beforeEach: Global navigation guard
303
+ * - beforeUpdate: Execute before updating route (same component)
304
+ * - beforeEnter: Execute before entering new route
305
+ * - asyncComponent: Load async components
306
+ * - confirm: Final confirmation and navigation execution
307
+ */
308
+ const ROUTE_TRANSITION_PIPELINE = {
309
+ [RouteType.push]: [
310
+ ROUTE_TRANSITION_HOOKS.fallback,
311
+ ROUTE_TRANSITION_HOOKS.override,
312
+ ROUTE_TRANSITION_HOOKS.beforeLeave,
313
+ ROUTE_TRANSITION_HOOKS.beforeEach,
314
+ ROUTE_TRANSITION_HOOKS.beforeUpdate,
315
+ ROUTE_TRANSITION_HOOKS.beforeEnter,
316
+ ROUTE_TRANSITION_HOOKS.asyncComponent,
317
+ ROUTE_TRANSITION_HOOKS.confirm
318
+ ],
319
+
320
+ [RouteType.replace]: [
321
+ ROUTE_TRANSITION_HOOKS.fallback,
322
+ ROUTE_TRANSITION_HOOKS.override,
323
+ ROUTE_TRANSITION_HOOKS.beforeLeave,
324
+ ROUTE_TRANSITION_HOOKS.beforeEach,
325
+ ROUTE_TRANSITION_HOOKS.beforeUpdate,
326
+ ROUTE_TRANSITION_HOOKS.beforeEnter,
327
+ ROUTE_TRANSITION_HOOKS.asyncComponent,
328
+ ROUTE_TRANSITION_HOOKS.confirm
329
+ ],
330
+ [RouteType.pushWindow]: [
331
+ ROUTE_TRANSITION_HOOKS.fallback,
332
+ ROUTE_TRANSITION_HOOKS.override,
333
+ // ROUTE_TRANSITION_HOOKS.beforeLeave
334
+ ROUTE_TRANSITION_HOOKS.beforeEach,
335
+ // ROUTE_TRANSITION_HOOKS.beforeUpdate
336
+ // ROUTE_TRANSITION_HOOKS.beforeEnter
337
+ // ROUTE_TRANSITION_HOOKS.asyncComponent
338
+ ROUTE_TRANSITION_HOOKS.confirm
339
+ ],
340
+
341
+ [RouteType.replaceWindow]: [
342
+ ROUTE_TRANSITION_HOOKS.fallback,
343
+ ROUTE_TRANSITION_HOOKS.override,
344
+ ROUTE_TRANSITION_HOOKS.beforeLeave,
345
+ ROUTE_TRANSITION_HOOKS.beforeEach,
346
+ // ROUTE_TRANSITION_HOOKS.beforeUpdate
347
+ // ROUTE_TRANSITION_HOOKS.beforeEnter
348
+ // ROUTE_TRANSITION_HOOKS.asyncComponent
349
+ ROUTE_TRANSITION_HOOKS.confirm
350
+ ],
351
+ [RouteType.pushLayer]: [
352
+ ROUTE_TRANSITION_HOOKS.fallback,
353
+ ROUTE_TRANSITION_HOOKS.override,
354
+ // ROUTE_TRANSITION_HOOKS.beforeLeave
355
+ ROUTE_TRANSITION_HOOKS.beforeEach,
356
+ // ROUTE_TRANSITION_HOOKS.beforeUpdate
357
+ // ROUTE_TRANSITION_HOOKS.beforeEnter
358
+ // ROUTE_TRANSITION_HOOKS.asyncComponent
359
+ ROUTE_TRANSITION_HOOKS.confirm
360
+ ],
361
+ [RouteType.restartApp]: [
362
+ ROUTE_TRANSITION_HOOKS.fallback,
363
+ // ROUTE_TRANSITION_HOOKS.override,
364
+ ROUTE_TRANSITION_HOOKS.beforeLeave,
365
+ ROUTE_TRANSITION_HOOKS.beforeEach,
366
+ ROUTE_TRANSITION_HOOKS.beforeUpdate,
367
+ ROUTE_TRANSITION_HOOKS.beforeEnter,
368
+ ROUTE_TRANSITION_HOOKS.asyncComponent,
369
+ ROUTE_TRANSITION_HOOKS.confirm
370
+ ],
371
+
372
+ [RouteType.unknown]: [
373
+ ROUTE_TRANSITION_HOOKS.fallback,
374
+ // ROUTE_TRANSITION_HOOKS.override,
375
+ ROUTE_TRANSITION_HOOKS.beforeLeave,
376
+ ROUTE_TRANSITION_HOOKS.beforeEach,
377
+ ROUTE_TRANSITION_HOOKS.beforeUpdate,
378
+ ROUTE_TRANSITION_HOOKS.beforeEnter,
379
+ ROUTE_TRANSITION_HOOKS.asyncComponent,
380
+ ROUTE_TRANSITION_HOOKS.confirm
381
+ ]
382
+ } satisfies Record<string, RouteConfirmHook[]>;
383
+
384
+ /**
385
+ * Route Transition Manager
386
+ * Responsible for managing all route transition logic, including guard execution,
387
+ * task processing, and status updates.
388
+ */
389
+ export class RouteTransition {
390
+ private readonly router: Router;
391
+
392
+ public route: Route | null = null;
393
+
394
+ // Task controller for the current transition.
395
+ private _controller: RouteTaskController | null = null;
396
+
397
+ public readonly guards = {
398
+ beforeEach: [] as RouteConfirmHook[],
399
+ afterEach: [] as RouteNotifyHook[]
400
+ };
401
+
402
+ private destroyed = false;
403
+
404
+ constructor(router: Router) {
405
+ this.router = router;
406
+ }
407
+
408
+ public beforeEach(guard: RouteConfirmHook): () => void {
409
+ this.guards.beforeEach.push(guard);
410
+ return () => {
411
+ removeFromArray(this.guards.beforeEach, guard);
412
+ };
413
+ }
414
+
415
+ public afterEach(guard: RouteNotifyHook): () => void {
416
+ this.guards.afterEach.push(guard);
417
+ return () => {
418
+ removeFromArray(this.guards.afterEach, guard);
419
+ };
420
+ }
421
+
422
+ public destroy(): void {
423
+ this._controller?.abort();
424
+ this._controller = null;
425
+ this.guards.afterEach.length = 0;
426
+ this.guards.beforeEach.length = 0;
427
+ this.destroyed = true;
428
+ }
429
+
430
+ public async to(
431
+ toType: RouteType,
432
+ toInput: RouteLocationInput
433
+ ): Promise<Route> {
434
+ if (this.destroyed) {
435
+ throw new Error('RouteTransition has been destroyed');
436
+ }
437
+
438
+ const from = this.route;
439
+ const to = await this._runTask(
440
+ new Route({
441
+ options: this.router.parsedOptions,
442
+ toType,
443
+ toInput,
444
+ from: from?.url ?? null
445
+ }),
446
+ from
447
+ );
448
+ if (typeof to.handle === 'function') {
449
+ to.handleResult = await to.handle(to, from, this.router);
450
+ }
451
+
452
+ if (to.handle) {
453
+ for (const guard of this.guards.afterEach) {
454
+ guard(to, from, this.router);
455
+ }
456
+ }
457
+
458
+ return to;
459
+ }
460
+
461
+ private async _runTask(to: Route, from: Route | null): Promise<Route> {
462
+ this._controller?.abort();
463
+ this._controller = new RouteTaskController();
464
+ const taskFunctions: RouteConfirmHook[] =
465
+ ROUTE_TRANSITION_PIPELINE[to.type] ||
466
+ ROUTE_TRANSITION_PIPELINE[RouteType.unknown];
467
+ const tasks = taskFunctions.map<RouteTask>((taskFn) => ({
468
+ name: taskFn.name,
469
+ task: taskFn
470
+ }));
471
+
472
+ return createRouteTask({
473
+ to,
474
+ from,
475
+ tasks,
476
+ controller: this._controller,
477
+ router: this.router
478
+ });
479
+ }
480
+ }