@esmx/router-vue 3.0.0-rc.16 → 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.
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 +55 -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 +30 -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 +86 -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 +61 -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
package/src/plugin.ts CHANGED
@@ -1,36 +1,91 @@
1
- import type { Route, RouterInstance } from '@esmx/router';
2
- import { type App, type ShallowReactive, shallowReactive, unref } from 'vue';
1
+ import { RouterLink } from './router-link';
2
+ import { RouterView } from './router-view';
3
+ import { getRoute, getRouter } from './use';
4
+ import { isVue3 } from './util';
3
5
 
4
- import { RouterLink } from './link';
5
- import { routerKey, routerViewLocationKey } from './symbols';
6
- import { RouterView } from './view';
7
-
8
- declare module '@vue/runtime-core' {
9
- interface ComponentCustomProperties {
10
- // $route: Route;
11
- $route: ShallowReactive<Route>;
12
- $router: RouterInstance;
13
- }
14
-
15
- interface GlobalComponents {
16
- // RouterView:
17
- // RouterLink:
18
- }
6
+ interface VueApp {
7
+ config?: {
8
+ globalProperties: Record<string, unknown>;
9
+ };
10
+ prototype?: Record<string, unknown>;
11
+ component(name: string, component: unknown): void;
19
12
  }
20
13
 
21
- export function RouterVuePlugin(router: RouterInstance) {
22
- return function install(app: App) {
23
- const route = shallowReactive(router.route);
24
- router.route = route;
14
+ /**
15
+ * Vue plugin for @esmx/router integration.
16
+ * Registers RouterLink and RouterView components globally.
17
+ * Compatible with both Vue 2.7+ and Vue 3.
18
+ *
19
+ * @example Vue 3 installation
20
+ * ```typescript
21
+ * import { createApp } from 'vue';
22
+ * import { Router } from '@esmx/router';
23
+ * import { RouterPlugin, useProvideRouter } from '@esmx/router-vue';
24
+ *
25
+ * const routes = [
26
+ * { path: '/', component: Home },
27
+ * { path: '/about', component: About }
28
+ * ];
29
+ *
30
+ * const router = new Router({ routes });
31
+ * const app = createApp({
32
+ * setup() {
33
+ * useProvideRouter(router);
34
+ * }
35
+ * });
36
+ *
37
+ * app.use(RouterPlugin);
38
+ * app.mount('#app');
39
+ * ```
40
+ *
41
+ * @example Vue 2 installation
42
+ * ```typescript
43
+ * import Vue from 'vue';
44
+ * import { Router } from '@esmx/router';
45
+ * import { RouterPlugin, useProvideRouter } from '@esmx/router-vue';
46
+ *
47
+ * const routes = [
48
+ * { path: '/', component: Home },
49
+ * { path: '/about', component: About }
50
+ * ];
51
+ *
52
+ * const router = new Router({ routes });
53
+ * Vue.use(RouterPlugin);
54
+ *
55
+ * new Vue({
56
+ * setup() {
57
+ * useProvideRouter(router);
58
+ * }
59
+ * }).$mount('#app');
60
+ * ```
61
+ */
62
+ export const RouterPlugin = {
63
+ /**
64
+ * Install the router plugin.
65
+ * @param app Vue application instance (Vue 3) or Vue constructor (Vue 2)
66
+ */
67
+ install(app: unknown): void {
68
+ const vueApp = app as VueApp;
69
+ const target = vueApp.config?.globalProperties || vueApp.prototype;
25
70
 
26
- app.config.globalProperties.$router = router;
27
- app.config.globalProperties.$route = router.route;
71
+ if (!target) {
72
+ throw new Error('[@esmx/router-vue] Invalid Vue app instance');
73
+ }
74
+ Object.defineProperties(target, {
75
+ $router: {
76
+ get() {
77
+ return getRouter(isVue3 ? null : this);
78
+ }
79
+ },
80
+ $route: {
81
+ get() {
82
+ return getRoute(isVue3 ? null : this);
83
+ }
84
+ }
85
+ });
28
86
 
29
- app.provide(routerKey, unref(router));
30
- app.provide(routerViewLocationKey, unref(router.route));
31
-
32
- // 注册组件
33
- app.component('router-view', RouterView);
34
- app.component('router-link', RouterLink);
35
- };
36
- }
87
+ // Register global components
88
+ vueApp.component('RouterLink', RouterLink);
89
+ vueApp.component('RouterView', RouterView);
90
+ }
91
+ };
@@ -0,0 +1,569 @@
1
+ import type { RouteConfig } from '@esmx/router';
2
+ import { Router, RouterMode } from '@esmx/router';
3
+ /**
4
+ * @vitest-environment happy-dom
5
+ */
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import { createApp, defineComponent, h, nextTick } from 'vue';
8
+ import { RouterLink } from './router-link';
9
+ import { useProvideRouter } from './use';
10
+
11
+ describe('router-link.ts - RouterLink Component', () => {
12
+ let router: Router;
13
+ let app: ReturnType<typeof createApp>;
14
+ let container: HTMLElement;
15
+
16
+ beforeEach(async () => {
17
+ // Create DOM container
18
+ container = document.createElement('div');
19
+ container.id = 'test-app';
20
+ document.body.appendChild(container);
21
+
22
+ // Create test routes
23
+ const routes: RouteConfig[] = [
24
+ {
25
+ path: '/',
26
+ component: defineComponent({
27
+ name: 'Home',
28
+ template: '<div>Home Page</div>'
29
+ }),
30
+ meta: { title: 'Home' }
31
+ },
32
+ {
33
+ path: '/about',
34
+ component: defineComponent({
35
+ name: 'About',
36
+ template: '<div>About Page</div>'
37
+ }),
38
+ meta: { title: 'About' }
39
+ },
40
+ {
41
+ path: '/contact',
42
+ component: defineComponent({
43
+ name: 'Contact',
44
+ template: '<div>Contact Page</div>'
45
+ }),
46
+ meta: { title: 'Contact' }
47
+ }
48
+ ];
49
+
50
+ // Create router instance
51
+ router = new Router({
52
+ root: '#test-app',
53
+ routes,
54
+ mode: RouterMode.memory,
55
+ base: new URL('http://localhost:3000/')
56
+ });
57
+
58
+ // Initialize router and wait for it to be ready
59
+ await router.replace('/');
60
+ await nextTick();
61
+ });
62
+
63
+ afterEach(async () => {
64
+ // Clean up
65
+ if (app) {
66
+ app.unmount();
67
+ }
68
+ if (router) {
69
+ router.destroy();
70
+ }
71
+ if (container.parentNode) {
72
+ container.parentNode.removeChild(container);
73
+ }
74
+ // Wait for cleanup
75
+ await nextTick();
76
+ });
77
+
78
+ describe('Component Definition', () => {
79
+ it('should have correct component name', () => {
80
+ expect(RouterLink.name).toBe('RouterLink');
81
+ });
82
+
83
+ it('should have properly configured props', () => {
84
+ const props = RouterLink.props;
85
+
86
+ // Verify required props
87
+ expect(props.to).toBeDefined();
88
+ expect(props.to.required).toBe(true);
89
+
90
+ // Verify default values
91
+ expect(props.type).toBeDefined();
92
+ expect(props.type.default).toBe('push');
93
+
94
+ expect(props.exact).toBeDefined();
95
+ expect(props.exact.default).toBe('include');
96
+
97
+ expect(props.tag).toBeDefined();
98
+ expect(props.tag.default).toBe('a');
99
+
100
+ expect(props.event).toBeDefined();
101
+ expect(props.event.default).toBe('click');
102
+
103
+ expect(props.replace).toBeDefined();
104
+ expect(props.replace.default).toBe(false);
105
+ });
106
+
107
+ it('should have setup function defined', () => {
108
+ expect(RouterLink.setup).toBeDefined();
109
+ expect(typeof RouterLink.setup).toBe('function');
110
+ });
111
+ });
112
+
113
+ describe('Component Rendering', () => {
114
+ it('should render basic router link', async () => {
115
+ const TestApp = defineComponent({
116
+ setup() {
117
+ useProvideRouter(router);
118
+ return () =>
119
+ h(RouterLink, { to: '/about' }, () => 'About Link');
120
+ }
121
+ });
122
+
123
+ app = createApp(TestApp);
124
+ app.mount(container);
125
+ await nextTick();
126
+
127
+ const linkElement = container.querySelector('a');
128
+ expect(linkElement).toBeTruthy();
129
+ expect(linkElement?.textContent).toBe('About Link');
130
+ });
131
+
132
+ it('should render with custom tag', async () => {
133
+ const TestApp = defineComponent({
134
+ setup() {
135
+ useProvideRouter(router);
136
+ return () =>
137
+ h(
138
+ RouterLink,
139
+ {
140
+ to: '/contact',
141
+ tag: 'button'
142
+ },
143
+ () => 'Contact Button'
144
+ );
145
+ }
146
+ });
147
+
148
+ app = createApp(TestApp);
149
+ app.mount(container);
150
+ await nextTick();
151
+
152
+ const buttonElement = container.querySelector('button');
153
+ expect(buttonElement).toBeTruthy();
154
+ expect(buttonElement?.textContent).toBe('Contact Button');
155
+ });
156
+
157
+ it('should render with active class when route matches', async () => {
158
+ // Navigate to /about first and wait for completion
159
+ await router.push('/about');
160
+ await nextTick();
161
+
162
+ const TestApp = defineComponent({
163
+ setup() {
164
+ useProvideRouter(router);
165
+ return () =>
166
+ h(
167
+ RouterLink,
168
+ {
169
+ to: '/about',
170
+ activeClass: 'active-link'
171
+ },
172
+ () => 'Current Page'
173
+ );
174
+ }
175
+ });
176
+
177
+ app = createApp(TestApp);
178
+ app.mount(container);
179
+ await nextTick();
180
+
181
+ const linkElement = container.querySelector('a');
182
+ expect(linkElement).toBeTruthy();
183
+ expect(linkElement?.classList.contains('active-link')).toBe(true);
184
+ });
185
+
186
+ it('should handle different navigation types', async () => {
187
+ const TestApp = defineComponent({
188
+ setup() {
189
+ useProvideRouter(router);
190
+ return () =>
191
+ h('div', [
192
+ h(
193
+ RouterLink,
194
+ {
195
+ to: '/about',
196
+ type: 'push'
197
+ },
198
+ () => 'Push Link'
199
+ ),
200
+ h(
201
+ RouterLink,
202
+ {
203
+ to: '/contact',
204
+ type: 'replace'
205
+ },
206
+ () => 'Replace Link'
207
+ )
208
+ ]);
209
+ }
210
+ });
211
+
212
+ app = createApp(TestApp);
213
+ app.mount(container);
214
+ await nextTick();
215
+
216
+ const links = container.querySelectorAll('a');
217
+ expect(links).toHaveLength(2);
218
+ expect(links[0]?.textContent).toBe('Push Link');
219
+ expect(links[1]?.textContent).toBe('Replace Link');
220
+ });
221
+ });
222
+
223
+ describe('Navigation Functionality', () => {
224
+ it('should navigate when clicked', async () => {
225
+ const TestApp = defineComponent({
226
+ setup() {
227
+ useProvideRouter(router);
228
+ return () =>
229
+ h(
230
+ RouterLink,
231
+ { to: '/about' },
232
+ () => 'Navigate to About'
233
+ );
234
+ }
235
+ });
236
+
237
+ app = createApp(TestApp);
238
+ app.mount(container);
239
+ await nextTick();
240
+
241
+ const linkElement = container.querySelector('a');
242
+ expect(linkElement).toBeTruthy();
243
+
244
+ // Simulate click and wait for navigation
245
+ const clickPromise = new Promise<void>((resolve) => {
246
+ router.afterEach(() => resolve());
247
+ });
248
+
249
+ linkElement?.click();
250
+ await clickPromise;
251
+ await nextTick();
252
+
253
+ // Check if navigation occurred
254
+ expect(router.route.path).toBe('/about');
255
+ });
256
+
257
+ it('should handle custom events', async () => {
258
+ const TestApp = defineComponent({
259
+ setup() {
260
+ useProvideRouter(router);
261
+ return () =>
262
+ h(
263
+ RouterLink,
264
+ {
265
+ to: '/contact',
266
+ event: 'mouseenter'
267
+ },
268
+ () => 'Hover to Navigate'
269
+ );
270
+ }
271
+ });
272
+
273
+ app = createApp(TestApp);
274
+ app.mount(container);
275
+ await nextTick();
276
+
277
+ const linkElement = container.querySelector('a');
278
+ expect(linkElement).toBeTruthy();
279
+
280
+ // Simulate mouseenter event and wait for navigation
281
+ const navigationPromise = new Promise<void>((resolve) => {
282
+ router.afterEach(() => resolve());
283
+ });
284
+
285
+ const event = new MouseEvent('mouseenter', { bubbles: true });
286
+ linkElement?.dispatchEvent(event);
287
+ await navigationPromise;
288
+ await nextTick();
289
+
290
+ // Check if navigation occurred
291
+ expect(router.route.path).toBe('/contact');
292
+ });
293
+
294
+ it('should handle object-based route navigation', async () => {
295
+ const TestApp = defineComponent({
296
+ setup() {
297
+ useProvideRouter(router);
298
+ return () =>
299
+ h(
300
+ RouterLink,
301
+ {
302
+ to: { path: '/about', query: { tab: 'info' } }
303
+ },
304
+ () => 'About with Query'
305
+ );
306
+ }
307
+ });
308
+
309
+ app = createApp(TestApp);
310
+ app.mount(container);
311
+ await nextTick();
312
+
313
+ const linkElement = container.querySelector('a');
314
+ expect(linkElement).toBeTruthy();
315
+
316
+ // Simulate click and wait for navigation
317
+ const navigationPromise = new Promise<void>((resolve) => {
318
+ router.afterEach(() => resolve());
319
+ });
320
+
321
+ linkElement?.click();
322
+ await navigationPromise;
323
+ await nextTick();
324
+
325
+ // Check if navigation occurred with query
326
+ expect(router.route.path).toBe('/about');
327
+ expect(router.route.query.tab).toBe('info');
328
+ });
329
+ });
330
+
331
+ describe('Props Validation', () => {
332
+ it('should accept string as to prop', async () => {
333
+ const TestApp = defineComponent({
334
+ setup() {
335
+ useProvideRouter(router);
336
+ return () =>
337
+ h(RouterLink, { to: '/about' }, () => 'String Route');
338
+ }
339
+ });
340
+
341
+ expect(() => {
342
+ app = createApp(TestApp);
343
+ app.mount(container);
344
+ }).not.toThrow();
345
+ });
346
+
347
+ it('should accept object as to prop', async () => {
348
+ const TestApp = defineComponent({
349
+ setup() {
350
+ useProvideRouter(router);
351
+ return () =>
352
+ h(
353
+ RouterLink,
354
+ {
355
+ to: { path: '/contact' }
356
+ },
357
+ () => 'Object Route'
358
+ );
359
+ }
360
+ });
361
+
362
+ expect(() => {
363
+ app = createApp(TestApp);
364
+ app.mount(container);
365
+ }).not.toThrow();
366
+ });
367
+
368
+ it('should handle array of events', async () => {
369
+ const TestApp = defineComponent({
370
+ setup() {
371
+ useProvideRouter(router);
372
+ return () =>
373
+ h(
374
+ RouterLink,
375
+ {
376
+ to: '/about',
377
+ event: ['click', 'keydown']
378
+ },
379
+ () => 'Multi Event Link'
380
+ );
381
+ }
382
+ });
383
+
384
+ app = createApp(TestApp);
385
+ app.mount(container);
386
+ await nextTick();
387
+
388
+ const linkElement = container.querySelector('a');
389
+ expect(linkElement).toBeTruthy();
390
+
391
+ // Test click event
392
+ const clickPromise = new Promise<void>((resolve) => {
393
+ router.afterEach(() => resolve());
394
+ });
395
+
396
+ linkElement?.click();
397
+ await clickPromise;
398
+ await nextTick();
399
+ expect(router.route.path).toBe('/about');
400
+
401
+ // Reset route and test keydown event
402
+ await router.push('/');
403
+ await nextTick();
404
+
405
+ const keydownPromise = new Promise<void>((resolve) => {
406
+ router.afterEach(() => resolve());
407
+ });
408
+
409
+ const keyEvent = new KeyboardEvent('keydown', { key: 'Enter' });
410
+ linkElement?.dispatchEvent(keyEvent);
411
+ await keydownPromise;
412
+ await nextTick();
413
+ expect(router.route.path).toBe('/about');
414
+ });
415
+ });
416
+
417
+ describe('Error Handling', () => {
418
+ it('should throw error when router context is missing', () => {
419
+ const TestApp = defineComponent({
420
+ setup() {
421
+ // No useProvideRouter call - missing router context
422
+ return () =>
423
+ h(RouterLink, { to: '/about' }, () => 'No Router');
424
+ }
425
+ });
426
+
427
+ expect(() => {
428
+ app = createApp(TestApp);
429
+ app.mount(container);
430
+ }).toThrow();
431
+ });
432
+ });
433
+
434
+ describe('Slot Rendering', () => {
435
+ it('should render default slot content', async () => {
436
+ const TestApp = defineComponent({
437
+ setup() {
438
+ useProvideRouter(router);
439
+ return () =>
440
+ h(
441
+ RouterLink,
442
+ { to: '/about' },
443
+ {
444
+ default: () =>
445
+ h(
446
+ 'span',
447
+ { class: 'link-text' },
448
+ 'Custom Content'
449
+ )
450
+ }
451
+ );
452
+ }
453
+ });
454
+
455
+ app = createApp(TestApp);
456
+ app.mount(container);
457
+ await nextTick();
458
+
459
+ const spanElement = container.querySelector('span.link-text');
460
+ expect(spanElement).toBeTruthy();
461
+ expect(spanElement?.textContent).toBe('Custom Content');
462
+ });
463
+
464
+ it('should render complex slot content', async () => {
465
+ const TestApp = defineComponent({
466
+ setup() {
467
+ useProvideRouter(router);
468
+ return () =>
469
+ h(
470
+ RouterLink,
471
+ { to: '/contact' },
472
+ {
473
+ default: () => [
474
+ h('i', { class: 'icon' }, '→'),
475
+ h('span', 'Contact Us')
476
+ ]
477
+ }
478
+ );
479
+ }
480
+ });
481
+
482
+ app = createApp(TestApp);
483
+ app.mount(container);
484
+ await nextTick();
485
+
486
+ const iconElement = container.querySelector('i.icon');
487
+ const spanElement = container.querySelector('span');
488
+ expect(iconElement).toBeTruthy();
489
+ expect(spanElement).toBeTruthy();
490
+ expect(iconElement?.textContent).toBe('→');
491
+ expect(spanElement?.textContent).toBe('Contact Us');
492
+ });
493
+ });
494
+
495
+ describe('Active State Management', () => {
496
+ it('should apply active class with exact matching', async () => {
497
+ // Navigate to exact route and wait for completion
498
+ await router.push('/about');
499
+ await nextTick();
500
+
501
+ const TestApp = defineComponent({
502
+ setup() {
503
+ useProvideRouter(router);
504
+ return () =>
505
+ h('div', [
506
+ h(
507
+ RouterLink,
508
+ {
509
+ to: '/about',
510
+ exact: 'exact',
511
+ activeClass: 'exact-active'
512
+ },
513
+ () => 'Exact Match'
514
+ ),
515
+ h(
516
+ RouterLink,
517
+ {
518
+ to: '/about/sub',
519
+ exact: 'exact',
520
+ activeClass: 'exact-active'
521
+ },
522
+ () => 'Not Exact'
523
+ )
524
+ ]);
525
+ }
526
+ });
527
+
528
+ app = createApp(TestApp);
529
+ app.mount(container);
530
+ await nextTick();
531
+
532
+ const links = container.querySelectorAll('a');
533
+ expect(links[0]?.classList.contains('exact-active')).toBe(true);
534
+ expect(links[1]?.classList.contains('exact-active')).toBe(false);
535
+ });
536
+
537
+ it('should apply active class with include matching', async () => {
538
+ // Navigate to a route and wait for completion
539
+ await router.push('/about');
540
+ await nextTick();
541
+
542
+ const TestApp = defineComponent({
543
+ setup() {
544
+ useProvideRouter(router);
545
+ return () =>
546
+ h(
547
+ RouterLink,
548
+ {
549
+ to: '/about',
550
+ exact: 'include',
551
+ activeClass: 'include-active'
552
+ },
553
+ () => 'Include Match'
554
+ );
555
+ }
556
+ });
557
+
558
+ app = createApp(TestApp);
559
+ app.mount(container);
560
+ await nextTick();
561
+
562
+ const linkElement = container.querySelector('a');
563
+ // Should be active because current route '/about' matches exactly
564
+ expect(linkElement?.classList.contains('include-active')).toBe(
565
+ true
566
+ );
567
+ });
568
+ });
569
+ });