@esmx/router 3.0.0-rc.18 → 3.0.0-rc.20

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 (158) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +70 -0
  3. package/README.zh-CN.md +70 -0
  4. package/dist/error.d.ts +23 -0
  5. package/dist/error.mjs +61 -0
  6. package/dist/increment-id.d.ts +7 -0
  7. package/dist/increment-id.mjs +11 -0
  8. package/dist/index.d.ts +5 -3
  9. package/dist/index.mjs +14 -3
  10. package/dist/index.test.mjs +8 -0
  11. package/dist/location.d.ts +15 -0
  12. package/dist/location.mjs +53 -0
  13. package/dist/location.test.d.ts +8 -0
  14. package/dist/location.test.mjs +370 -0
  15. package/dist/matcher.d.ts +3 -0
  16. package/dist/matcher.mjs +44 -0
  17. package/dist/matcher.test.mjs +1492 -0
  18. package/dist/micro-app.d.ts +18 -0
  19. package/dist/micro-app.dom.test.d.ts +1 -0
  20. package/dist/micro-app.dom.test.mjs +532 -0
  21. package/dist/micro-app.mjs +80 -0
  22. package/dist/navigation.d.ts +43 -0
  23. package/dist/navigation.mjs +143 -0
  24. package/dist/navigation.test.d.ts +1 -0
  25. package/dist/navigation.test.mjs +681 -0
  26. package/dist/options.d.ts +4 -0
  27. package/dist/options.mjs +88 -0
  28. package/dist/route-task.d.ts +40 -0
  29. package/dist/route-task.mjs +75 -0
  30. package/dist/route-task.test.d.ts +1 -0
  31. package/dist/route-task.test.mjs +673 -0
  32. package/dist/route-transition.d.ts +53 -0
  33. package/dist/route-transition.mjs +307 -0
  34. package/dist/route-transition.test.d.ts +1 -0
  35. package/dist/route-transition.test.mjs +146 -0
  36. package/dist/route.d.ts +72 -0
  37. package/dist/route.mjs +194 -0
  38. package/dist/route.test.d.ts +1 -0
  39. package/dist/route.test.mjs +1664 -0
  40. package/dist/router-back.test.d.ts +1 -0
  41. package/dist/router-back.test.mjs +361 -0
  42. package/dist/router-forward.test.d.ts +1 -0
  43. package/dist/router-forward.test.mjs +376 -0
  44. package/dist/router-go.test.d.ts +1 -0
  45. package/dist/router-go.test.mjs +73 -0
  46. package/dist/router-guards-cleanup.test.d.ts +1 -0
  47. package/dist/router-guards-cleanup.test.mjs +437 -0
  48. package/dist/router-link.d.ts +10 -0
  49. package/dist/router-link.mjs +126 -0
  50. package/dist/router-push.test.d.ts +1 -0
  51. package/dist/router-push.test.mjs +115 -0
  52. package/dist/router-replace.test.d.ts +1 -0
  53. package/dist/router-replace.test.mjs +114 -0
  54. package/dist/router-resolve.test.d.ts +1 -0
  55. package/dist/router-resolve.test.mjs +393 -0
  56. package/dist/router-restart-app.dom.test.d.ts +1 -0
  57. package/dist/router-restart-app.dom.test.mjs +616 -0
  58. package/dist/router-window-navigation.test.d.ts +1 -0
  59. package/dist/router-window-navigation.test.mjs +359 -0
  60. package/dist/router.d.ts +109 -102
  61. package/dist/router.mjs +260 -361
  62. package/dist/types.d.ts +246 -0
  63. package/dist/types.mjs +18 -0
  64. package/dist/util.d.ts +26 -0
  65. package/dist/util.mjs +53 -0
  66. package/dist/util.test.d.ts +1 -0
  67. package/dist/util.test.mjs +1020 -0
  68. package/package.json +10 -13
  69. package/src/error.ts +84 -0
  70. package/src/increment-id.ts +12 -0
  71. package/src/index.test.ts +9 -0
  72. package/src/index.ts +54 -3
  73. package/src/location.test.ts +406 -0
  74. package/src/location.ts +96 -0
  75. package/src/matcher.test.ts +1685 -0
  76. package/src/matcher.ts +59 -0
  77. package/src/micro-app.dom.test.ts +708 -0
  78. package/src/micro-app.ts +101 -0
  79. package/src/navigation.test.ts +858 -0
  80. package/src/navigation.ts +195 -0
  81. package/src/options.ts +131 -0
  82. package/src/route-task.test.ts +901 -0
  83. package/src/route-task.ts +105 -0
  84. package/src/route-transition.test.ts +178 -0
  85. package/src/route-transition.ts +425 -0
  86. package/src/route.test.ts +2014 -0
  87. package/src/route.ts +308 -0
  88. package/src/router-back.test.ts +487 -0
  89. package/src/router-forward.test.ts +506 -0
  90. package/src/router-go.test.ts +91 -0
  91. package/src/router-guards-cleanup.test.ts +595 -0
  92. package/src/router-link.ts +235 -0
  93. package/src/router-push.test.ts +140 -0
  94. package/src/router-replace.test.ts +139 -0
  95. package/src/router-resolve.test.ts +475 -0
  96. package/src/router-restart-app.dom.test.ts +783 -0
  97. package/src/router-window-navigation.test.ts +457 -0
  98. package/src/router.ts +289 -470
  99. package/src/types.ts +341 -0
  100. package/src/util.test.ts +1262 -0
  101. package/src/util.ts +116 -0
  102. package/dist/history/abstract.d.ts +0 -29
  103. package/dist/history/abstract.mjs +0 -107
  104. package/dist/history/base.d.ts +0 -79
  105. package/dist/history/base.mjs +0 -275
  106. package/dist/history/html.d.ts +0 -30
  107. package/dist/history/html.mjs +0 -183
  108. package/dist/history/index.d.ts +0 -7
  109. package/dist/history/index.mjs +0 -16
  110. package/dist/matcher/create-matcher.d.ts +0 -5
  111. package/dist/matcher/create-matcher.mjs +0 -218
  112. package/dist/matcher/create-matcher.spec.mjs +0 -0
  113. package/dist/matcher/index.d.ts +0 -1
  114. package/dist/matcher/index.mjs +0 -1
  115. package/dist/task-pipe/index.d.ts +0 -1
  116. package/dist/task-pipe/index.mjs +0 -1
  117. package/dist/task-pipe/task.d.ts +0 -30
  118. package/dist/task-pipe/task.mjs +0 -66
  119. package/dist/types/index.d.ts +0 -694
  120. package/dist/types/index.mjs +0 -6
  121. package/dist/utils/bom.d.ts +0 -5
  122. package/dist/utils/bom.mjs +0 -10
  123. package/dist/utils/encoding.d.ts +0 -48
  124. package/dist/utils/encoding.mjs +0 -44
  125. package/dist/utils/guards.d.ts +0 -9
  126. package/dist/utils/guards.mjs +0 -12
  127. package/dist/utils/index.d.ts +0 -7
  128. package/dist/utils/index.mjs +0 -27
  129. package/dist/utils/path.d.ts +0 -60
  130. package/dist/utils/path.mjs +0 -282
  131. package/dist/utils/path.spec.mjs +0 -27
  132. package/dist/utils/scroll.d.ts +0 -25
  133. package/dist/utils/scroll.mjs +0 -59
  134. package/dist/utils/utils.d.ts +0 -16
  135. package/dist/utils/utils.mjs +0 -11
  136. package/dist/utils/warn.d.ts +0 -2
  137. package/dist/utils/warn.mjs +0 -12
  138. package/src/history/abstract.ts +0 -149
  139. package/src/history/base.ts +0 -408
  140. package/src/history/html.ts +0 -228
  141. package/src/history/index.ts +0 -20
  142. package/src/matcher/create-matcher.spec.ts +0 -3
  143. package/src/matcher/create-matcher.ts +0 -292
  144. package/src/matcher/index.ts +0 -1
  145. package/src/task-pipe/index.ts +0 -1
  146. package/src/task-pipe/task.ts +0 -97
  147. package/src/types/index.ts +0 -858
  148. package/src/utils/bom.ts +0 -14
  149. package/src/utils/encoding.ts +0 -153
  150. package/src/utils/guards.ts +0 -25
  151. package/src/utils/index.ts +0 -27
  152. package/src/utils/path.spec.ts +0 -32
  153. package/src/utils/path.ts +0 -418
  154. package/src/utils/scroll.ts +0 -120
  155. package/src/utils/utils.ts +0 -30
  156. package/src/utils/warn.ts +0 -13
  157. /package/dist/{matcher/create-matcher.spec.d.ts → index.test.d.ts} +0 -0
  158. /package/dist/{utils/path.spec.d.ts → matcher.test.d.ts} +0 -0
@@ -0,0 +1,437 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { Router } from "./router.mjs";
3
+ import { RouterMode } from "./types.mjs";
4
+ import { removeFromArray } from "./util.mjs";
5
+ describe("Router Guards Cleanup Tests", () => {
6
+ let router;
7
+ beforeEach(async () => {
8
+ router = new Router({
9
+ mode: RouterMode.memory,
10
+ base: new URL("http://localhost:3000/"),
11
+ routes: [
12
+ {
13
+ path: "/",
14
+ component: () => "Home"
15
+ },
16
+ {
17
+ path: "/test1",
18
+ component: () => "Test1"
19
+ },
20
+ {
21
+ path: "/test2",
22
+ component: () => "Test2"
23
+ },
24
+ {
25
+ path: "/test3",
26
+ component: () => "Test3"
27
+ }
28
+ ]
29
+ });
30
+ await router.replace("/");
31
+ });
32
+ afterEach(() => {
33
+ router.destroy();
34
+ });
35
+ describe("\u{1F525} Guard Cleanup Effect Verification", () => {
36
+ describe("beforeEach cleanup effects", () => {
37
+ test("guard should not execute after cleanup", async () => {
38
+ const spy = vi.fn();
39
+ const unregister = router.beforeEach(spy);
40
+ await router.push("/test1");
41
+ expect(spy).toHaveBeenCalledTimes(1);
42
+ unregister();
43
+ await router.push("/test2");
44
+ expect(spy).toHaveBeenCalledTimes(1);
45
+ });
46
+ test("guard should not execute after cleanup for multiple navigations", async () => {
47
+ const spy = vi.fn();
48
+ const unregister = router.beforeEach(spy);
49
+ await router.push("/test1");
50
+ expect(spy).toHaveBeenCalledTimes(1);
51
+ unregister();
52
+ await router.push("/test2");
53
+ await router.push("/test3");
54
+ await router.push("/");
55
+ expect(spy).toHaveBeenCalledTimes(1);
56
+ });
57
+ test("async guard should not execute after cleanup", async () => {
58
+ const spy = vi.fn();
59
+ const asyncGuard = async (to, from) => {
60
+ await new Promise((resolve) => setTimeout(resolve, 10));
61
+ spy(to, from);
62
+ };
63
+ const unregister = router.beforeEach(asyncGuard);
64
+ await router.push("/test1");
65
+ expect(spy).toHaveBeenCalledTimes(1);
66
+ unregister();
67
+ await router.push("/test2");
68
+ expect(spy).toHaveBeenCalledTimes(1);
69
+ });
70
+ });
71
+ describe("afterEach cleanup effects", () => {
72
+ test("guard should not execute after cleanup", async () => {
73
+ const spy = vi.fn();
74
+ const unregister = router.afterEach(spy);
75
+ await router.push("/test1");
76
+ expect(spy).toHaveBeenCalledTimes(1);
77
+ unregister();
78
+ await router.push("/test2");
79
+ expect(spy).toHaveBeenCalledTimes(1);
80
+ });
81
+ test("guard should not execute after cleanup for multiple navigations", async () => {
82
+ const spy = vi.fn();
83
+ const unregister = router.afterEach(spy);
84
+ await router.push("/test1");
85
+ expect(spy).toHaveBeenCalledTimes(1);
86
+ unregister();
87
+ await router.push("/test2");
88
+ await router.push("/test3");
89
+ await router.push("/");
90
+ expect(spy).toHaveBeenCalledTimes(1);
91
+ });
92
+ });
93
+ });
94
+ describe("\u26A1 Multiple Guards Independent Cleanup", () => {
95
+ describe("beforeEach multiple guards cleanup", () => {
96
+ test("cleaning single guard should not affect other guards", async () => {
97
+ const spy1 = vi.fn();
98
+ const spy2 = vi.fn();
99
+ const spy3 = vi.fn();
100
+ const unregister1 = router.beforeEach(spy1);
101
+ const unregister2 = router.beforeEach(spy2);
102
+ const unregister3 = router.beforeEach(spy3);
103
+ await router.push("/test1");
104
+ expect(spy1).toHaveBeenCalledTimes(1);
105
+ expect(spy2).toHaveBeenCalledTimes(1);
106
+ expect(spy3).toHaveBeenCalledTimes(1);
107
+ unregister2();
108
+ await router.push("/test2");
109
+ expect(spy1).toHaveBeenCalledTimes(2);
110
+ expect(spy2).toHaveBeenCalledTimes(1);
111
+ expect(spy3).toHaveBeenCalledTimes(2);
112
+ unregister1();
113
+ unregister3();
114
+ });
115
+ test("cleaning multiple guards should work correctly", async () => {
116
+ const spy1 = vi.fn();
117
+ const spy2 = vi.fn();
118
+ const spy3 = vi.fn();
119
+ const spy4 = vi.fn();
120
+ const unregister1 = router.beforeEach(spy1);
121
+ const unregister2 = router.beforeEach(spy2);
122
+ const unregister3 = router.beforeEach(spy3);
123
+ const unregister4 = router.beforeEach(spy4);
124
+ await router.push("/test1");
125
+ expect(spy1).toHaveBeenCalledTimes(1);
126
+ expect(spy2).toHaveBeenCalledTimes(1);
127
+ expect(spy3).toHaveBeenCalledTimes(1);
128
+ expect(spy4).toHaveBeenCalledTimes(1);
129
+ unregister1();
130
+ unregister3();
131
+ spy1.mockClear();
132
+ spy2.mockClear();
133
+ spy3.mockClear();
134
+ spy4.mockClear();
135
+ await router.push("/test2");
136
+ expect(spy1).not.toHaveBeenCalled();
137
+ expect(spy2).toHaveBeenCalledTimes(1);
138
+ expect(spy3).not.toHaveBeenCalled();
139
+ expect(spy4).toHaveBeenCalledTimes(1);
140
+ unregister2();
141
+ unregister4();
142
+ });
143
+ test("guard execution order should remain correct after cleanup", async () => {
144
+ const executionOrder = [];
145
+ const guard1 = () => {
146
+ executionOrder.push("guard1");
147
+ };
148
+ const guard2 = () => {
149
+ executionOrder.push("guard2");
150
+ };
151
+ const guard3 = () => {
152
+ executionOrder.push("guard3");
153
+ };
154
+ router.beforeEach(guard1);
155
+ const unregister2 = router.beforeEach(guard2);
156
+ router.beforeEach(guard3);
157
+ await router.push("/test1");
158
+ expect(executionOrder).toEqual(["guard1", "guard2", "guard3"]);
159
+ executionOrder.length = 0;
160
+ unregister2();
161
+ await router.push("/test2");
162
+ expect(executionOrder).toEqual(["guard1", "guard3"]);
163
+ });
164
+ });
165
+ describe("afterEach multiple guards cleanup", () => {
166
+ test("cleaning single afterEach guard should not affect other guards", async () => {
167
+ const spy1 = vi.fn();
168
+ const spy2 = vi.fn();
169
+ const spy3 = vi.fn();
170
+ const unregister1 = router.afterEach(spy1);
171
+ const unregister2 = router.afterEach(spy2);
172
+ const unregister3 = router.afterEach(spy3);
173
+ await router.push("/test1");
174
+ expect(spy1).toHaveBeenCalledTimes(1);
175
+ expect(spy2).toHaveBeenCalledTimes(1);
176
+ expect(spy3).toHaveBeenCalledTimes(1);
177
+ unregister2();
178
+ await router.push("/test2");
179
+ expect(spy1).toHaveBeenCalledTimes(2);
180
+ expect(spy2).toHaveBeenCalledTimes(1);
181
+ expect(spy3).toHaveBeenCalledTimes(2);
182
+ unregister1();
183
+ unregister3();
184
+ });
185
+ test("afterEach guard execution order should remain correct after cleanup", async () => {
186
+ const executionOrder = [];
187
+ const guard1 = () => {
188
+ executionOrder.push("after1");
189
+ };
190
+ const guard2 = () => {
191
+ executionOrder.push("after2");
192
+ };
193
+ const guard3 = () => {
194
+ executionOrder.push("after3");
195
+ };
196
+ router.afterEach(guard1);
197
+ const unregister2 = router.afterEach(guard2);
198
+ router.afterEach(guard3);
199
+ await router.push("/test1");
200
+ expect(executionOrder).toEqual(["after1", "after2", "after3"]);
201
+ executionOrder.length = 0;
202
+ unregister2();
203
+ await router.push("/test2");
204
+ expect(executionOrder).toEqual(["after1", "after3"]);
205
+ });
206
+ });
207
+ describe("mixed guards cleanup", () => {
208
+ test("beforeEach and afterEach guards should cleanup independently", async () => {
209
+ const beforeSpy = vi.fn();
210
+ const afterSpy = vi.fn();
211
+ const unregisterBefore = router.beforeEach(beforeSpy);
212
+ const unregisterAfter = router.afterEach(afterSpy);
213
+ await router.push("/test1");
214
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
215
+ expect(afterSpy).toHaveBeenCalledTimes(1);
216
+ unregisterBefore();
217
+ await router.push("/test2");
218
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
219
+ expect(afterSpy).toHaveBeenCalledTimes(2);
220
+ unregisterAfter();
221
+ await router.push("/test3");
222
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
223
+ expect(afterSpy).toHaveBeenCalledTimes(2);
224
+ });
225
+ });
226
+ });
227
+ describe("\u{1F6E1}\uFE0F Edge Cases Testing", () => {
228
+ describe("repeated cleanup safety", () => {
229
+ test("repeated cleanup function calls should be safe", () => {
230
+ const spy = vi.fn();
231
+ const unregister = router.beforeEach(spy);
232
+ expect(() => {
233
+ unregister();
234
+ unregister();
235
+ unregister();
236
+ }).not.toThrow();
237
+ });
238
+ test("guard should still not execute after repeated cleanup", async () => {
239
+ const spy = vi.fn();
240
+ const unregister = router.beforeEach(spy);
241
+ await router.push("/test1");
242
+ expect(spy).toHaveBeenCalledTimes(1);
243
+ unregister();
244
+ unregister();
245
+ unregister();
246
+ await router.push("/test2");
247
+ expect(spy).toHaveBeenCalledTimes(1);
248
+ });
249
+ });
250
+ describe("cleanup and re-registration", () => {
251
+ test("can re-register same guard after cleanup", async () => {
252
+ const spy = vi.fn();
253
+ const unregister1 = router.beforeEach(spy);
254
+ await router.push("/test1");
255
+ expect(spy).toHaveBeenCalledTimes(1);
256
+ unregister1();
257
+ const unregister2 = router.beforeEach(spy);
258
+ await router.push("/test2");
259
+ expect(spy).toHaveBeenCalledTimes(2);
260
+ unregister2();
261
+ });
262
+ test("re-registration after cleanup should work normally", async () => {
263
+ const spy = vi.fn();
264
+ let unregister = router.beforeEach(spy);
265
+ await router.push("/test1");
266
+ expect(spy).toHaveBeenCalledTimes(1);
267
+ unregister();
268
+ unregister = router.beforeEach(spy);
269
+ await router.push("/test2");
270
+ expect(spy).toHaveBeenCalledTimes(2);
271
+ unregister();
272
+ unregister = router.beforeEach(spy);
273
+ await router.push("/test3");
274
+ expect(spy).toHaveBeenCalledTimes(3);
275
+ unregister();
276
+ });
277
+ });
278
+ describe("same guard multiple registrations", () => {
279
+ test("same guard function registered multiple times should handle correctly", async () => {
280
+ const spy = vi.fn();
281
+ const unregister1 = router.beforeEach(spy);
282
+ const unregister2 = router.beforeEach(spy);
283
+ await router.push("/test1");
284
+ expect(spy).toHaveBeenCalledTimes(2);
285
+ unregister1();
286
+ await router.push("/test2");
287
+ expect(spy).toHaveBeenCalledTimes(3);
288
+ unregister2();
289
+ await router.push("/test3");
290
+ expect(spy).toHaveBeenCalledTimes(3);
291
+ });
292
+ test("complex scenario with same guard multiple registrations and cleanups", async () => {
293
+ const spy = vi.fn();
294
+ const unregister1 = router.beforeEach(spy);
295
+ const unregister2 = router.beforeEach(spy);
296
+ const unregister3 = router.beforeEach(spy);
297
+ await router.push("/test1");
298
+ expect(spy).toHaveBeenCalledTimes(3);
299
+ unregister2();
300
+ await router.push("/test2");
301
+ expect(spy).toHaveBeenCalledTimes(5);
302
+ unregister1();
303
+ unregister3();
304
+ await router.push("/test3");
305
+ expect(spy).toHaveBeenCalledTimes(5);
306
+ });
307
+ });
308
+ describe("empty array and non-existent element handling", () => {
309
+ test("removing element from empty array should be safe", () => {
310
+ const emptyArray = [];
311
+ const element = vi.fn();
312
+ expect(() => {
313
+ removeFromArray(emptyArray, element);
314
+ }).not.toThrow();
315
+ expect(emptyArray).toEqual([]);
316
+ });
317
+ test("removing non-existent element should be safe", () => {
318
+ const array = [vi.fn(), vi.fn(), vi.fn()];
319
+ const nonExistentElement = vi.fn();
320
+ const originalLength = array.length;
321
+ expect(() => {
322
+ removeFromArray(array, nonExistentElement);
323
+ }).not.toThrow();
324
+ expect(array).toHaveLength(originalLength);
325
+ });
326
+ });
327
+ describe("special value handling", () => {
328
+ test("removeFromArray should handle NaN values correctly", () => {
329
+ const arr = [1, Number.NaN, 3, Number.NaN, 5];
330
+ removeFromArray(arr, Number.NaN);
331
+ expect(arr).toEqual([1, 3, Number.NaN, 5]);
332
+ });
333
+ test("removeFromArray should handle function references correctly", () => {
334
+ const func1 = () => "func1";
335
+ const func2 = () => "func2";
336
+ const func3 = () => "func3";
337
+ const arr = [func1, func2, func3];
338
+ removeFromArray(arr, func2);
339
+ expect(arr).toEqual([func1, func3]);
340
+ });
341
+ test("removeFromArray should handle object references correctly", () => {
342
+ const obj1 = { id: 1 };
343
+ const obj2 = { id: 2 };
344
+ const obj3 = { id: 3 };
345
+ const arr = [obj1, obj2, obj3];
346
+ removeFromArray(arr, obj2);
347
+ expect(arr).toEqual([obj1, obj3]);
348
+ });
349
+ });
350
+ describe("memory leak protection", () => {
351
+ test("large number of guard registrations and cleanups should not cause memory leaks", () => {
352
+ const unregisters = [];
353
+ for (let i = 0; i < 100; i++) {
354
+ const guard = vi.fn();
355
+ const unregister = router.beforeEach(guard);
356
+ unregisters.push(unregister);
357
+ }
358
+ expect(router.transition.guards.beforeEach).toHaveLength(100);
359
+ unregisters.forEach((unregister) => unregister());
360
+ expect(router.transition.guards.beforeEach).toHaveLength(0);
361
+ });
362
+ test("mixed registration and cleanup should not cause memory leaks", () => {
363
+ const guards = [];
364
+ const unregisters = [];
365
+ for (let i = 0; i < 50; i++) {
366
+ const guard = vi.fn();
367
+ guards.push(guard);
368
+ const unregister = router.beforeEach(guard);
369
+ unregisters.push(unregister);
370
+ if (i % 5 === 0 && i > 0) {
371
+ unregisters[i - 5]();
372
+ }
373
+ }
374
+ unregisters.forEach((unregister) => unregister());
375
+ expect(router.transition.guards.beforeEach).toHaveLength(0);
376
+ expect(router.transition.guards.afterEach).toHaveLength(0);
377
+ });
378
+ });
379
+ describe("concurrent cleanup safety", () => {
380
+ test("cleaning guard during navigation should be safe", async () => {
381
+ let unregister = null;
382
+ let guardExecuted = false;
383
+ const guard = async () => {
384
+ guardExecuted = true;
385
+ unregister == null ? void 0 : unregister();
386
+ await new Promise((resolve) => setTimeout(resolve, 10));
387
+ };
388
+ unregister = router.beforeEach(guard);
389
+ await expect(router.push("/test1")).resolves.toBeDefined();
390
+ expect(guardExecuted).toBe(true);
391
+ guardExecuted = false;
392
+ await router.push("/test2");
393
+ expect(guardExecuted).toBe(false);
394
+ });
395
+ test("cleaning multiple guards simultaneously should be safe", async () => {
396
+ const guards = Array.from({ length: 10 }, () => vi.fn());
397
+ const unregisters = guards.map(
398
+ (guard) => router.beforeEach(guard)
399
+ );
400
+ await router.push("/test1");
401
+ guards.forEach(
402
+ (guard) => expect(guard).toHaveBeenCalledTimes(1)
403
+ );
404
+ unregisters.forEach((unregister) => unregister());
405
+ await router.push("/test2");
406
+ guards.forEach(
407
+ (guard) => expect(guard).toHaveBeenCalledTimes(1)
408
+ );
409
+ });
410
+ });
411
+ });
412
+ describe("\u{1F527} Router destroy cleanup verification", () => {
413
+ test("Router destroy should cleanup all guards", async () => {
414
+ const beforeSpy = vi.fn();
415
+ const afterSpy = vi.fn();
416
+ router.beforeEach(beforeSpy);
417
+ router.afterEach(afterSpy);
418
+ await router.push("/test1");
419
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
420
+ expect(afterSpy).toHaveBeenCalledTimes(1);
421
+ router.destroy();
422
+ const newRouter = new Router({
423
+ mode: RouterMode.memory,
424
+ base: new URL("http://localhost:3000/"),
425
+ routes: [
426
+ { path: "/", component: () => "Home" },
427
+ { path: "/test", component: () => "Test" }
428
+ ]
429
+ });
430
+ await newRouter.replace("/");
431
+ await newRouter.push("/test");
432
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
433
+ expect(afterSpy).toHaveBeenCalledTimes(1);
434
+ newRouter.destroy();
435
+ });
436
+ });
437
+ });
@@ -0,0 +1,10 @@
1
+ import type { Router } from './router';
2
+ import type { RouterLinkProps, RouterLinkResolved } from './types';
3
+ /**
4
+ * Framework-agnostic link resolver
5
+ *
6
+ * @param router Router instance
7
+ * @param props Link configuration
8
+ * @returns Resolution result
9
+ */
10
+ export declare function createLinkResolver(router: Router, props: RouterLinkProps): RouterLinkResolved;
@@ -0,0 +1,126 @@
1
+ const CSS_CLASSES = {
2
+ BASE: "router-link",
3
+ ACTIVE: "router-link-active",
4
+ EXACT_ACTIVE: "router-link-exact-active"
5
+ };
6
+ function normalizeNavigationType(props) {
7
+ if (props.replace) {
8
+ console.warn(
9
+ '[RouterLink] The `replace` property is deprecated and will be removed in a future version.\nPlease use `type="replace"` instead.\nBefore: <RouterLink replace={true} />\nAfter: <RouterLink type="replace" />'
10
+ );
11
+ return "replace";
12
+ }
13
+ return props.type || "push";
14
+ }
15
+ function getEventTypeList(eventType) {
16
+ const events = Array.isArray(eventType) ? eventType : [eventType];
17
+ const validEvents = events.filter((type) => typeof type === "string").map((type) => type.trim()).filter(Boolean);
18
+ return validEvents.length ? validEvents : ["click"];
19
+ }
20
+ function guardEvent(e) {
21
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
22
+ if (e.defaultPrevented) return false;
23
+ if (e.button !== void 0 && e.button !== 0) return false;
24
+ if (e.preventDefault) e.preventDefault();
25
+ return true;
26
+ }
27
+ async function executeNavigation(router, props, linkType) {
28
+ const { to, layerOptions } = props;
29
+ switch (linkType) {
30
+ case "push":
31
+ await router.push(to);
32
+ break;
33
+ case "replace":
34
+ await router.replace(to);
35
+ break;
36
+ case "pushWindow":
37
+ await router.pushWindow(to);
38
+ break;
39
+ case "replaceWindow":
40
+ await router.replaceWindow(to);
41
+ break;
42
+ case "pushLayer":
43
+ await router.pushLayer(
44
+ layerOptions ? typeof to === "string" ? { path: to, layer: layerOptions } : { ...to, layer: layerOptions } : to
45
+ );
46
+ break;
47
+ default:
48
+ await router.push(to);
49
+ }
50
+ }
51
+ function createNavigateFunction(router, props, navigationType) {
52
+ return async (e) => {
53
+ if (e && !guardEvent(e)) {
54
+ return;
55
+ }
56
+ await executeNavigation(router, props, navigationType);
57
+ };
58
+ }
59
+ function computeAttributes(href, navigationType, isExternal, isActive, isExactActive, activeClass) {
60
+ const isNewWindow = navigationType === "pushWindow";
61
+ const classes = [CSS_CLASSES.BASE];
62
+ if (isActive) {
63
+ classes.push(activeClass || CSS_CLASSES.ACTIVE);
64
+ }
65
+ if (isExactActive) {
66
+ classes.push(CSS_CLASSES.EXACT_ACTIVE);
67
+ }
68
+ const attributes = {
69
+ href,
70
+ class: classes.join(" ")
71
+ };
72
+ if (isNewWindow) {
73
+ attributes.target = "_blank";
74
+ }
75
+ const relParts = [];
76
+ if (isNewWindow) {
77
+ relParts.push("noopener", "noreferrer");
78
+ }
79
+ if (isExternal) {
80
+ relParts.push("external", "nofollow");
81
+ }
82
+ if (relParts.length > 0) {
83
+ attributes.rel = relParts.join(" ");
84
+ }
85
+ return attributes;
86
+ }
87
+ function createEventHandlersGenerator(navigate, eventTypes) {
88
+ return (nameTransform) => {
89
+ const handlers = {};
90
+ eventTypes.forEach((eventType) => {
91
+ const eventName = nameTransform ? nameTransform(eventType) : eventType.toLowerCase();
92
+ handlers[eventName] = navigate;
93
+ });
94
+ return handlers;
95
+ };
96
+ }
97
+ export function createLinkResolver(router, props) {
98
+ const route = router.resolve(props.to);
99
+ const type = normalizeNavigationType(props);
100
+ const href = route.url.href;
101
+ const isActive = router.isRouteMatched(route, props.exact);
102
+ const isExactActive = router.isRouteMatched(route, "exact");
103
+ const isExternal = route.url.origin !== router.route.url.origin;
104
+ const navigate = createNavigateFunction(router, props, type);
105
+ const attributes = computeAttributes(
106
+ href,
107
+ type,
108
+ isExternal,
109
+ isActive,
110
+ isExactActive,
111
+ props.activeClass
112
+ );
113
+ const eventTypes = getEventTypeList(props.event || "click");
114
+ const getEventHandlers = createEventHandlersGenerator(navigate, eventTypes);
115
+ return {
116
+ route,
117
+ type,
118
+ isActive,
119
+ isExactActive,
120
+ isExternal,
121
+ tag: props.tag || "a",
122
+ attributes,
123
+ navigate,
124
+ getEventHandlers
125
+ };
126
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import { Router } from "./router.mjs";
3
+ import { RouteType, RouterMode } from "./types.mjs";
4
+ describe("Router Push Tests", () => {
5
+ let router;
6
+ beforeEach(async () => {
7
+ router = new Router({
8
+ mode: RouterMode.memory,
9
+ base: new URL("http://localhost:3000/"),
10
+ routes: [
11
+ { path: "/", component: () => "Home" },
12
+ { path: "/user/:id", component: () => "User" },
13
+ { path: "/about", component: () => "About" },
14
+ {
15
+ path: "/async",
16
+ asyncComponent: () => new Promise(
17
+ (resolve) => setTimeout(() => resolve("AsyncComponent"), 10)
18
+ )
19
+ }
20
+ ]
21
+ });
22
+ await router.replace("/");
23
+ });
24
+ afterEach(() => {
25
+ router.destroy();
26
+ });
27
+ describe("Basic push navigation", () => {
28
+ test("should successfully push to new route", async () => {
29
+ const route = await router.push("/user/123");
30
+ expect(route.path).toBe("/user/123");
31
+ expect(route.params.id).toBe("123");
32
+ expect(route.type).toBe(RouteType.push);
33
+ expect(route.handle).not.toBe(null);
34
+ expect(router.route.path).toBe("/user/123");
35
+ });
36
+ test("should handle query parameters in push", async () => {
37
+ const route = await router.push(
38
+ "/user/123?tab=profile&active=true"
39
+ );
40
+ expect(route.params.id).toBe("123");
41
+ expect(route.query.tab).toBe("profile");
42
+ expect(route.query.active).toBe("true");
43
+ });
44
+ test("should handle async component loading", async () => {
45
+ const route = await router.push("/async");
46
+ expect(route.path).toBe("/async");
47
+ expect(route.handle).not.toBe(null);
48
+ });
49
+ });
50
+ describe("Error handling", () => {
51
+ test("should throw error for async component loading failure", async () => {
52
+ const errorRouter = new Router({
53
+ mode: RouterMode.memory,
54
+ base: new URL("http://localhost:3000/"),
55
+ routes: [
56
+ { path: "/", component: () => "Home" },
57
+ {
58
+ path: "/error",
59
+ asyncComponent: () => Promise.reject(new Error("Loading failed"))
60
+ }
61
+ ]
62
+ });
63
+ await errorRouter.replace("/");
64
+ await expect(errorRouter.push("/error")).rejects.toThrow();
65
+ errorRouter.destroy();
66
+ });
67
+ test("should throw error when guard prevents navigation", async () => {
68
+ const guardRouter = new Router({
69
+ mode: RouterMode.memory,
70
+ base: new URL("http://localhost:3000/"),
71
+ routes: [
72
+ { path: "/", component: () => "Home" },
73
+ {
74
+ path: "/protected",
75
+ component: () => "Protected",
76
+ beforeEnter: () => false
77
+ }
78
+ ]
79
+ });
80
+ await guardRouter.replace("/");
81
+ await expect(guardRouter.push("/protected")).rejects.toThrow();
82
+ guardRouter.destroy();
83
+ });
84
+ });
85
+ describe("Concurrent navigation", () => {
86
+ test("should handle concurrent push operations", async () => {
87
+ const promises = [
88
+ router.push("/user/1").catch((err) => err),
89
+ router.push("/user/2").catch((err) => err)
90
+ ];
91
+ const [result1, result2] = await Promise.all(promises);
92
+ const successResults = [result1, result2].filter(
93
+ (r) => !(r instanceof Error)
94
+ );
95
+ const errorResults = [result1, result2].filter(
96
+ (r) => r instanceof Error
97
+ );
98
+ expect(successResults).toHaveLength(1);
99
+ expect(errorResults).toHaveLength(1);
100
+ });
101
+ });
102
+ describe("Edge cases", () => {
103
+ test("should handle push to current route", async () => {
104
+ await router.push("/about");
105
+ const route = await router.push("/about");
106
+ expect(route.path).toBe("/about");
107
+ expect(route.handle).not.toBe(null);
108
+ });
109
+ test("should handle empty parameter", async () => {
110
+ const route = await router.push("");
111
+ expect(route).toBeDefined();
112
+ expect(typeof route.path).toBe("string");
113
+ });
114
+ });
115
+ });
@@ -0,0 +1 @@
1
+ export {};