@esmx/router-vue 3.0.0-rc.17 → 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 (55) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +563 -0
  3. package/README.zh-CN.md +563 -0
  4. package/dist/index.d.ts +6 -4
  5. package/dist/index.mjs +11 -4
  6. package/dist/index.test.d.ts +1 -0
  7. package/dist/index.test.mjs +206 -0
  8. package/dist/plugin.d.ts +61 -11
  9. package/dist/plugin.mjs +32 -16
  10. package/dist/plugin.test.d.ts +1 -0
  11. package/dist/plugin.test.mjs +436 -0
  12. package/dist/router-link.d.ts +202 -0
  13. package/dist/router-link.mjs +84 -0
  14. package/dist/router-link.test.d.ts +1 -0
  15. package/dist/router-link.test.mjs +456 -0
  16. package/dist/router-view.d.ts +31 -0
  17. package/dist/router-view.mjs +17 -0
  18. package/dist/router-view.test.d.ts +1 -0
  19. package/dist/router-view.test.mjs +459 -0
  20. package/dist/use.d.ts +198 -3
  21. package/dist/use.mjs +75 -9
  22. package/dist/use.test.d.ts +1 -0
  23. package/dist/use.test.mjs +461 -0
  24. package/dist/util.d.ts +7 -0
  25. package/dist/util.mjs +24 -0
  26. package/dist/util.test.d.ts +1 -0
  27. package/dist/util.test.mjs +319 -0
  28. package/dist/vue2.d.ts +13 -0
  29. package/dist/vue2.mjs +0 -0
  30. package/dist/vue3.d.ts +13 -0
  31. package/dist/vue3.mjs +0 -0
  32. package/package.json +31 -14
  33. package/src/index.test.ts +263 -0
  34. package/src/index.ts +16 -4
  35. package/src/plugin.test.ts +574 -0
  36. package/src/plugin.ts +92 -31
  37. package/src/router-link.test.ts +569 -0
  38. package/src/router-link.ts +148 -0
  39. package/src/router-view.test.ts +599 -0
  40. package/src/router-view.ts +62 -0
  41. package/src/use.test.ts +616 -0
  42. package/src/use.ts +307 -11
  43. package/src/util.test.ts +418 -0
  44. package/src/util.ts +32 -0
  45. package/src/vue2.ts +16 -0
  46. package/src/vue3.ts +15 -0
  47. package/dist/link.d.ts +0 -101
  48. package/dist/link.mjs +0 -103
  49. package/dist/symbols.d.ts +0 -3
  50. package/dist/symbols.mjs +0 -3
  51. package/dist/view.d.ts +0 -21
  52. package/dist/view.mjs +0 -75
  53. package/src/link.ts +0 -177
  54. package/src/symbols.ts +0 -8
  55. package/src/view.ts +0 -95
@@ -0,0 +1,599 @@
1
+ import { type RouteConfig, Router, RouterMode } from '@esmx/router';
2
+ /**
3
+ * @vitest-environment happy-dom
4
+ */
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { createApp, defineComponent, h, inject, nextTick, provide } from 'vue';
7
+ import { RouterView } from './router-view';
8
+ import { useProvideRouter } from './use';
9
+
10
+ // Mock components for testing
11
+ const HomeComponent = defineComponent({
12
+ name: 'HomeComponent',
13
+ setup() {
14
+ return () => h('div', { class: 'home' }, 'Home Page');
15
+ }
16
+ });
17
+
18
+ const AboutComponent = defineComponent({
19
+ name: 'AboutComponent',
20
+ setup() {
21
+ return () => h('div', { class: 'about' }, 'About Page');
22
+ }
23
+ });
24
+
25
+ const UserComponent = defineComponent({
26
+ name: 'UserComponent',
27
+ setup() {
28
+ return () => h('div', { class: 'user' }, 'User Component');
29
+ }
30
+ });
31
+
32
+ // ES Module component for testing resolveComponent
33
+ const ESModuleComponent = {
34
+ __esModule: true,
35
+ default: defineComponent({
36
+ name: 'ESModuleComponent',
37
+ setup() {
38
+ return () =>
39
+ h('div', { class: 'es-module' }, 'ES Module Component');
40
+ }
41
+ })
42
+ };
43
+
44
+ describe('router-view.ts - RouterView Component', () => {
45
+ let router: Router;
46
+ let testContainer: HTMLElement;
47
+
48
+ beforeEach(async () => {
49
+ // Create test container
50
+ testContainer = document.createElement('div');
51
+ testContainer.id = 'test-app';
52
+ document.body.appendChild(testContainer);
53
+
54
+ // Create test routes
55
+ const routes: RouteConfig[] = [
56
+ {
57
+ path: '/',
58
+ component: HomeComponent,
59
+ meta: { title: 'Home' }
60
+ },
61
+ {
62
+ path: '/about',
63
+ component: AboutComponent,
64
+ meta: { title: 'About' }
65
+ },
66
+ {
67
+ path: '/users/:id',
68
+ component: UserComponent,
69
+ meta: { title: 'User' }
70
+ },
71
+ {
72
+ path: '/es-module',
73
+ component: ESModuleComponent,
74
+ meta: { title: 'ES Module' }
75
+ }
76
+ ];
77
+
78
+ // Create router instance
79
+ router = new Router({
80
+ root: '#test-app',
81
+ routes,
82
+ mode: RouterMode.memory,
83
+ base: new URL('http://localhost:3000/')
84
+ });
85
+
86
+ // Initialize router to root path and wait for it to be ready
87
+ await router.replace('/');
88
+ // Wait for route to be fully initialized
89
+ await new Promise((resolve) => setTimeout(resolve, 10));
90
+ });
91
+
92
+ afterEach(() => {
93
+ // Clean up test environment
94
+ if (testContainer.parentNode) {
95
+ testContainer.parentNode.removeChild(testContainer);
96
+ }
97
+
98
+ // Destroy router
99
+ if (router) {
100
+ router.destroy();
101
+ }
102
+ });
103
+
104
+ describe('Basic Functionality', () => {
105
+ it('should render matched route component at depth 0', async () => {
106
+ const TestApp = defineComponent({
107
+ setup() {
108
+ useProvideRouter(router);
109
+ return () => h('div', [h(RouterView)]);
110
+ }
111
+ });
112
+
113
+ const app = createApp(TestApp);
114
+ app.mount(testContainer);
115
+ await nextTick();
116
+
117
+ // Check if HomeComponent is rendered
118
+ expect(testContainer.textContent).toContain('Home Page');
119
+
120
+ app.unmount();
121
+ });
122
+
123
+ it('should render different components when route changes', async () => {
124
+ const TestApp = defineComponent({
125
+ setup() {
126
+ useProvideRouter(router);
127
+ return () => h('div', [h(RouterView)]);
128
+ }
129
+ });
130
+
131
+ const app = createApp(TestApp);
132
+ app.mount(testContainer);
133
+ await nextTick();
134
+
135
+ // Initially should show Home
136
+ expect(testContainer.textContent).toContain('Home Page');
137
+
138
+ // Navigate to About
139
+ await router.push('/about');
140
+ await nextTick();
141
+
142
+ expect(testContainer.textContent).toContain('About Page');
143
+
144
+ app.unmount();
145
+ });
146
+
147
+ it('should handle routes with parameters', async () => {
148
+ const TestApp = defineComponent({
149
+ setup() {
150
+ useProvideRouter(router);
151
+ return () => h('div', [h(RouterView)]);
152
+ }
153
+ });
154
+
155
+ const app = createApp(TestApp);
156
+ app.mount(testContainer);
157
+
158
+ // Navigate to user route with parameter
159
+ await router.push('/users/123');
160
+ await nextTick();
161
+
162
+ expect(testContainer.textContent).toContain('User Component');
163
+
164
+ app.unmount();
165
+ });
166
+ });
167
+
168
+ describe('Component Resolution', () => {
169
+ it('should resolve ES module components correctly', async () => {
170
+ const TestApp = defineComponent({
171
+ setup() {
172
+ useProvideRouter(router);
173
+ return () => h('div', [h(RouterView)]);
174
+ }
175
+ });
176
+
177
+ const app = createApp(TestApp);
178
+ app.mount(testContainer);
179
+
180
+ // Navigate to ES module route
181
+ await router.push('/es-module');
182
+ await nextTick();
183
+
184
+ expect(testContainer.textContent).toContain('ES Module Component');
185
+
186
+ app.unmount();
187
+ });
188
+
189
+ it('should handle function components', async () => {
190
+ const FunctionComponent = () => h('div', 'Function Component');
191
+
192
+ const routes: RouteConfig[] = [
193
+ {
194
+ path: '/function',
195
+ component: FunctionComponent,
196
+ meta: { title: 'Function' }
197
+ }
198
+ ];
199
+
200
+ const functionRouter = new Router({
201
+ root: '#test-app',
202
+ routes,
203
+ mode: RouterMode.memory,
204
+ base: new URL('http://localhost:3000/')
205
+ });
206
+
207
+ // Initialize the router and wait for it to be ready
208
+ await functionRouter.replace('/function');
209
+ await new Promise((resolve) => setTimeout(resolve, 10));
210
+
211
+ const TestApp = defineComponent({
212
+ setup() {
213
+ useProvideRouter(functionRouter);
214
+ return () => h('div', [h(RouterView)]);
215
+ }
216
+ });
217
+
218
+ const app = createApp(TestApp);
219
+ app.mount(testContainer);
220
+ await nextTick();
221
+
222
+ expect(testContainer.textContent).toContain('Function Component');
223
+
224
+ app.unmount();
225
+ functionRouter.destroy();
226
+ });
227
+ });
228
+
229
+ describe('Depth Tracking', () => {
230
+ it('should inject depth 0 by default', async () => {
231
+ let injectedDepth: number | undefined;
232
+
233
+ // Use the same symbol key that RouterView uses internally
234
+ const RouterViewDepthKey = Symbol('RouterViewDepth');
235
+
236
+ // Create a custom RouterView component that can capture the injected depth
237
+ const TestRouterView = defineComponent({
238
+ name: 'TestRouterView',
239
+ setup() {
240
+ injectedDepth = inject(RouterViewDepthKey, -1);
241
+ return () => h(RouterView);
242
+ }
243
+ });
244
+
245
+ const TestApp = defineComponent({
246
+ setup() {
247
+ useProvideRouter(router);
248
+ return () => h('div', [h(TestRouterView)]);
249
+ }
250
+ });
251
+
252
+ const app = createApp(TestApp);
253
+ app.mount(testContainer);
254
+ await nextTick();
255
+
256
+ // TestRouterView should inject the default depth 0 when no parent RouterView exists
257
+ expect(injectedDepth).toBe(-1); // Default value since no parent RouterView provides depth
258
+
259
+ app.unmount();
260
+ });
261
+
262
+ it('should provide correct depth in nested RouterViews', async () => {
263
+ let parentDepth: number | undefined;
264
+ let childDepth: number | undefined;
265
+
266
+ const RouterViewDepthKey = Symbol('RouterViewDepth');
267
+
268
+ const ParentTestComponent = defineComponent({
269
+ name: 'ParentTestComponent',
270
+ setup() {
271
+ parentDepth = inject(RouterViewDepthKey, -1);
272
+ provide(RouterViewDepthKey, 0); // Simulate parent RouterView
273
+ return () =>
274
+ h('div', [h('span', 'Parent'), h(ChildTestComponent)]);
275
+ }
276
+ });
277
+
278
+ const ChildTestComponent = defineComponent({
279
+ name: 'ChildTestComponent',
280
+ setup() {
281
+ childDepth = inject(RouterViewDepthKey, -1);
282
+ return () => h('div', 'Child');
283
+ }
284
+ });
285
+
286
+ const TestApp = defineComponent({
287
+ setup() {
288
+ useProvideRouter(router);
289
+ return () => h('div', [h(ParentTestComponent)]);
290
+ }
291
+ });
292
+
293
+ const app = createApp(TestApp);
294
+ app.mount(testContainer);
295
+ await nextTick();
296
+
297
+ // Parent should see default depth, child should see provided depth
298
+ expect(parentDepth).toBe(-1); // Default value since no RouterView above
299
+ expect(childDepth).toBe(0); // Value provided by parent
300
+
301
+ app.unmount();
302
+ });
303
+
304
+ it('should handle nested RouterViews with correct depth', async () => {
305
+ const Level1Component = defineComponent({
306
+ name: 'Level1Component',
307
+ setup() {
308
+ return () =>
309
+ h('div', [h('span', 'Level 1'), h(RouterView)]);
310
+ }
311
+ });
312
+
313
+ const Level2Component = defineComponent({
314
+ name: 'Level2Component',
315
+ setup() {
316
+ return () => h('div', 'Level 2');
317
+ }
318
+ });
319
+
320
+ const nestedRoutes: RouteConfig[] = [
321
+ {
322
+ path: '/level1',
323
+ component: Level1Component,
324
+ children: [
325
+ {
326
+ path: 'level2',
327
+ component: Level2Component
328
+ }
329
+ ]
330
+ }
331
+ ];
332
+
333
+ const nestedRouter = new Router({
334
+ root: '#test-app',
335
+ routes: nestedRoutes,
336
+ mode: RouterMode.memory,
337
+ base: new URL('http://localhost:3000/')
338
+ });
339
+
340
+ // Initialize the router and wait for it to be ready
341
+ await nestedRouter.replace('/level1/level2');
342
+ await new Promise((resolve) => setTimeout(resolve, 10));
343
+
344
+ const TestApp = defineComponent({
345
+ setup() {
346
+ useProvideRouter(nestedRouter);
347
+ return () => h('div', [h(RouterView)]);
348
+ }
349
+ });
350
+
351
+ const app = createApp(TestApp);
352
+ app.mount(testContainer);
353
+ await nextTick();
354
+
355
+ expect(testContainer.textContent).toContain('Level 1');
356
+ expect(testContainer.textContent).toContain('Level 2');
357
+
358
+ app.unmount();
359
+ nestedRouter.destroy();
360
+ });
361
+ });
362
+
363
+ describe('Edge Cases and Error Handling', () => {
364
+ it('should render null when no route matches at current depth', async () => {
365
+ const RouterViewDepthKey = Symbol('RouterViewDepth');
366
+
367
+ const DeepRouterView = defineComponent({
368
+ name: 'DeepRouterView',
369
+ setup() {
370
+ // Inject depth 0 from parent RouterView and provide depth 1
371
+ const currentDepth = inject(RouterViewDepthKey, 0);
372
+ provide(RouterViewDepthKey, currentDepth + 1);
373
+ return () => h(RouterView);
374
+ }
375
+ });
376
+
377
+ const TestApp = defineComponent({
378
+ setup() {
379
+ useProvideRouter(router);
380
+ return () =>
381
+ h('div', [
382
+ h('span', 'App'),
383
+ h(RouterView), // This renders Home component at depth 0
384
+ h(DeepRouterView) // This tries to render at depth 1, but no match
385
+ ]);
386
+ }
387
+ });
388
+
389
+ const app = createApp(TestApp);
390
+ app.mount(testContainer);
391
+ await nextTick();
392
+
393
+ // Should contain "App" and "Home Page" from the first RouterView
394
+ // but no additional content from the deep RouterView
395
+ expect(testContainer.textContent).toContain('App');
396
+ expect(testContainer.textContent).toContain('Home Page');
397
+
398
+ app.unmount();
399
+ });
400
+
401
+ it('should handle null components gracefully', async () => {
402
+ const routesWithNull: RouteConfig[] = [
403
+ {
404
+ path: '/null-component',
405
+ component: null as RouteConfig['component'],
406
+ meta: { title: 'Null Component' }
407
+ }
408
+ ];
409
+
410
+ const nullRouter = new Router({
411
+ root: '#test-app',
412
+ routes: routesWithNull,
413
+ mode: RouterMode.memory,
414
+ base: new URL('http://localhost:3000/')
415
+ });
416
+
417
+ // Initialize the router and wait for it to be ready
418
+ await nullRouter.replace('/null-component');
419
+ await new Promise((resolve) => setTimeout(resolve, 10));
420
+
421
+ const TestApp = defineComponent({
422
+ setup() {
423
+ useProvideRouter(nullRouter);
424
+ return () => h('div', [h('span', 'App'), h(RouterView)]);
425
+ }
426
+ });
427
+
428
+ const app = createApp(TestApp);
429
+ app.mount(testContainer);
430
+ await nextTick();
431
+
432
+ // Verify that only the "App" text is rendered and RouterView renders nothing
433
+ expect(testContainer.textContent?.trim()).toBe('App');
434
+ expect(testContainer.querySelector('div')?.children.length).toBe(1); // Only the span element
435
+ expect(testContainer.querySelector('span')?.textContent).toBe(
436
+ 'App'
437
+ );
438
+
439
+ app.unmount();
440
+ nullRouter.destroy();
441
+ });
442
+
443
+ it('should handle non-existent routes', async () => {
444
+ // Create a new router instance with a valid initial route
445
+ const nonExistentRouter = new Router({
446
+ root: '#test-app',
447
+ routes: [
448
+ {
449
+ path: '/',
450
+ component: null // Initial route with null component
451
+ }
452
+ ],
453
+ mode: RouterMode.memory,
454
+ base: new URL('http://localhost:3000/')
455
+ });
456
+
457
+ // Initialize router with root path
458
+ await nonExistentRouter.replace('/');
459
+ await new Promise((resolve) => setTimeout(resolve, 10));
460
+
461
+ const TestApp = defineComponent({
462
+ setup() {
463
+ useProvideRouter(nonExistentRouter);
464
+ return () => h('div', [h('span', 'App'), h(RouterView)]);
465
+ }
466
+ });
467
+
468
+ const app = createApp(TestApp);
469
+ app.mount(testContainer);
470
+
471
+ // Navigate to non-existent route
472
+ await nonExistentRouter.push('/non-existent');
473
+ await nextTick();
474
+
475
+ // Wait for any pending route changes
476
+ await new Promise((resolve) => setTimeout(resolve, 10));
477
+
478
+ // Verify that only the "App" text is rendered and RouterView renders nothing
479
+ expect(testContainer.textContent?.trim()).toBe('App');
480
+ expect(testContainer.querySelector('div')?.children.length).toBe(1); // Only the span element
481
+ expect(testContainer.querySelector('span')?.textContent).toBe(
482
+ 'App'
483
+ );
484
+
485
+ app.unmount();
486
+ nonExistentRouter.destroy();
487
+ });
488
+
489
+ it('should handle malformed ES modules', async () => {
490
+ const MalformedModule = {
491
+ __esModule: true,
492
+ default: null
493
+ };
494
+
495
+ const malformedRoutes: RouteConfig[] = [
496
+ {
497
+ path: '/malformed',
498
+ component: MalformedModule as RouteConfig['component'],
499
+ meta: { title: 'Malformed' }
500
+ }
501
+ ];
502
+
503
+ const malformedRouter = new Router({
504
+ root: '#test-app',
505
+ routes: malformedRoutes,
506
+ mode: RouterMode.memory,
507
+ base: new URL('http://localhost:3000/')
508
+ });
509
+
510
+ // Initialize the router and wait for it to be ready
511
+ await malformedRouter.replace('/malformed');
512
+ await new Promise((resolve) => setTimeout(resolve, 10));
513
+
514
+ const TestApp = defineComponent({
515
+ setup() {
516
+ useProvideRouter(malformedRouter);
517
+ return () => h('div', [h('span', 'App'), h(RouterView)]);
518
+ }
519
+ });
520
+
521
+ const app = createApp(TestApp);
522
+ app.mount(testContainer);
523
+ await nextTick();
524
+
525
+ expect(testContainer.textContent).toBe('App');
526
+
527
+ app.unmount();
528
+ malformedRouter.destroy();
529
+ });
530
+ });
531
+
532
+ describe('Component Properties', () => {
533
+ it('should have correct component name', () => {
534
+ expect(RouterView.name).toBe('RouterView');
535
+ });
536
+
537
+ it('should be a valid Vue component', () => {
538
+ expect(RouterView).toHaveProperty('setup');
539
+ expect(typeof RouterView.setup).toBe('function');
540
+ });
541
+
542
+ it('should not define props', () => {
543
+ expect(RouterView.props).toBeUndefined();
544
+ });
545
+ });
546
+
547
+ describe('Integration Tests', () => {
548
+ it('should re-render when route changes', async () => {
549
+ const TestApp = defineComponent({
550
+ setup() {
551
+ useProvideRouter(router);
552
+ return () => h('div', [h(RouterView)]);
553
+ }
554
+ });
555
+
556
+ const app = createApp(TestApp);
557
+ app.mount(testContainer);
558
+ await nextTick();
559
+
560
+ expect(testContainer.textContent).toContain('Home Page');
561
+
562
+ await router.push('/about');
563
+ await nextTick();
564
+ expect(testContainer.textContent).toContain('About Page');
565
+
566
+ await router.push('/users/123');
567
+ await nextTick();
568
+ expect(testContainer.textContent).toContain('User Component');
569
+
570
+ app.unmount();
571
+ });
572
+
573
+ it('should work with router navigation methods', async () => {
574
+ const TestApp = defineComponent({
575
+ setup() {
576
+ useProvideRouter(router);
577
+ return () => h('div', [h(RouterView)]);
578
+ }
579
+ });
580
+
581
+ const app = createApp(TestApp);
582
+ app.mount(testContainer);
583
+
584
+ await router.push('/about');
585
+ await nextTick();
586
+ expect(testContainer.textContent).toContain('About Page');
587
+
588
+ await router.replace('/users/456');
589
+ await nextTick();
590
+ expect(testContainer.textContent).toContain('User Component');
591
+
592
+ await router.back();
593
+ await nextTick();
594
+ expect(testContainer.textContent).toContain('Home Page');
595
+
596
+ app.unmount();
597
+ });
598
+ });
599
+ });
@@ -0,0 +1,62 @@
1
+ import { defineComponent, h, inject, provide } from 'vue';
2
+ import { useRoute } from './use';
3
+ import { resolveComponent } from './util';
4
+
5
+ const RouterViewDepthKey = Symbol('RouterViewDepth');
6
+
7
+ /**
8
+ * RouterView component for rendering matched route components.
9
+ * Acts as a placeholder where route components are rendered based on the current route.
10
+ * Supports nested routing with proper depth tracking using Vue's provide/inject mechanism.
11
+ *
12
+ * @param props - Component properties (RouterView accepts no props)
13
+ * @param context - Vue setup context (not used)
14
+ * @param context.slots - Component slots (not used)
15
+ * @returns Vue render function that renders the matched route component at current depth
16
+ *
17
+ * @example
18
+ *
19
+ * ```vue
20
+ * <template>
21
+ * <div id="app">
22
+ * <!-- Navigation links -->
23
+ * <nav>
24
+ * <RouterLink to="/">Home</RouterLink>
25
+ * <RouterLink to="/about">About</RouterLink>
26
+ * <RouterLink to="/users">Users</RouterLink>
27
+ * </nav>
28
+ *
29
+ * <!-- Root level route components render here -->
30
+ * <RouterView />
31
+ * </div>
32
+ * </template>
33
+ * ```
34
+ */
35
+ export const RouterView = defineComponent({
36
+ name: 'RouterView',
37
+ setup() {
38
+ const route = useRoute();
39
+
40
+ // Get current RouterView depth from parent RouterView (if any)
41
+ // This enables proper nested routing by tracking how deep we are in the component tree
42
+ const depth = inject(RouterViewDepthKey, 0);
43
+
44
+ // Provide depth + 1 to child RouterView components
45
+ // This ensures each nested RouterView renders the correct route component
46
+ provide(RouterViewDepthKey, depth + 1);
47
+
48
+ return () => {
49
+ // Get the matched route configuration at current depth
50
+ // route.matched is an array of matched route configs from parent to child
51
+ const matchedRoute = route.matched[depth];
52
+
53
+ // Resolve the component, handling ES module format if necessary
54
+ const component = matchedRoute
55
+ ? resolveComponent(matchedRoute.component)
56
+ : null;
57
+
58
+ // Render the component or null if no match at this depth
59
+ return component ? h(component) : null;
60
+ };
61
+ }
62
+ });