@esmx/router-vue 3.0.0-rc.103

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +570 -0
  3. package/README.zh-CN.md +570 -0
  4. package/dist/index.d.ts +6 -0
  5. package/dist/index.mjs +13 -0
  6. package/dist/index.test.d.ts +1 -0
  7. package/dist/index.test.mjs +216 -0
  8. package/dist/plugin.d.ts +61 -0
  9. package/dist/plugin.mjs +41 -0
  10. package/dist/plugin.test.d.ts +1 -0
  11. package/dist/plugin.test.mjs +631 -0
  12. package/dist/router-link.d.ts +220 -0
  13. package/dist/router-link.mjs +119 -0
  14. package/dist/router-link.test.d.ts +1 -0
  15. package/dist/router-link.test.mjs +663 -0
  16. package/dist/router-view.d.ts +31 -0
  17. package/dist/router-view.mjs +15 -0
  18. package/dist/router-view.test.d.ts +1 -0
  19. package/dist/router-view.test.mjs +676 -0
  20. package/dist/run-with-context.test.d.ts +1 -0
  21. package/dist/run-with-context.test.mjs +57 -0
  22. package/dist/use.d.ts +260 -0
  23. package/dist/use.mjs +125 -0
  24. package/dist/use.test.d.ts +1 -0
  25. package/dist/use.test.mjs +381 -0
  26. package/dist/util.d.ts +20 -0
  27. package/dist/util.mjs +49 -0
  28. package/dist/util.test.d.ts +4 -0
  29. package/dist/util.test.mjs +604 -0
  30. package/dist/vue2.d.ts +15 -0
  31. package/dist/vue2.mjs +0 -0
  32. package/dist/vue3.d.ts +13 -0
  33. package/dist/vue3.mjs +0 -0
  34. package/package.json +85 -0
  35. package/src/index.test.ts +273 -0
  36. package/src/index.ts +15 -0
  37. package/src/plugin.test.ts +812 -0
  38. package/src/plugin.ts +107 -0
  39. package/src/router-link.test.ts +830 -0
  40. package/src/router-link.ts +172 -0
  41. package/src/router-view.test.ts +840 -0
  42. package/src/router-view.ts +59 -0
  43. package/src/run-with-context.test.ts +64 -0
  44. package/src/use.test.ts +484 -0
  45. package/src/use.ts +416 -0
  46. package/src/util.test.ts +760 -0
  47. package/src/util.ts +85 -0
  48. package/src/vue2.ts +18 -0
  49. package/src/vue3.ts +15 -0
@@ -0,0 +1,59 @@
1
+ import { defineComponent, h } from 'vue';
2
+ import { _useRouterViewDepth, useRoute } from './use';
3
+ import { resolveComponent } from './util';
4
+
5
+ /**
6
+ * RouterView component for rendering matched route components.
7
+ * Acts as a placeholder where route components are rendered based on the current route.
8
+ * Supports nested routing with proper depth tracking using Vue's provide/inject mechanism.
9
+ *
10
+ * @param props - Component properties (RouterView accepts no props)
11
+ * @param context - Vue setup context (not used)
12
+ * @param context.slots - Component slots (not used)
13
+ * @returns Vue render function that renders the matched route component at current depth
14
+ *
15
+ * @example
16
+ *
17
+ * ```vue
18
+ * <template>
19
+ * <div id="app">
20
+ * <!-- Navigation links -->
21
+ * <nav>
22
+ * <RouterLink to="/">Home</RouterLink>
23
+ * <RouterLink to="/about">About</RouterLink>
24
+ * <RouterLink to="/users">Users</RouterLink>
25
+ * </nav>
26
+ *
27
+ * <!-- Root level route components render here -->
28
+ * <RouterView />
29
+ * </div>
30
+ * </template>
31
+ * ```
32
+ */
33
+ export const RouterView = defineComponent({
34
+ name: 'RouterView',
35
+ setup() {
36
+ const route = useRoute();
37
+
38
+ // Get current RouterView depth and automatically provide depth + 1 for children
39
+ // This enables proper nested routing by tracking how deep we are in the component tree
40
+ const depth = _useRouterViewDepth(true);
41
+
42
+ return () => {
43
+ // Get the matched route configuration at current depth
44
+ // route.matched is an array of matched route configs from parent to child
45
+ const matchedRoute = route.matched[depth];
46
+
47
+ // Resolve the component, handling ES module format if necessary
48
+ const component = matchedRoute
49
+ ? resolveComponent(matchedRoute.component)
50
+ : null;
51
+
52
+ // Render the component with compilePath as key to force re-render when route config changes
53
+ // Using compilePath ensures component is recreated when navigating to different route configs
54
+ return component
55
+ ? h(component, { key: matchedRoute.compilePath })
56
+ : null;
57
+ };
58
+ }
59
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { describe, expect, it } from 'vitest';
5
+ import type { InjectionKey } from 'vue';
6
+ import {
7
+ createApp,
8
+ getCurrentInstance,
9
+ h,
10
+ inject,
11
+ nextTick,
12
+ provide
13
+ } from 'vue';
14
+
15
+ describe('app.runWithContext()', () => {
16
+ it('should exist on Vue app', () => {
17
+ const app = createApp({ render: () => h('div') });
18
+ expect(typeof app.runWithContext).toBe('function');
19
+ });
20
+
21
+ it('should enable inject to read provided value within context', () => {
22
+ const KEY: InjectionKey<number> = Symbol('ctx-key');
23
+ const app = createApp({ render: () => h('div') });
24
+ app.provide(KEY, 42);
25
+
26
+ const value = app.runWithContext(() => inject(KEY));
27
+ expect(value).toBe(42);
28
+ });
29
+ it('should not read value provided in root setup via runWithContext', async () => {
30
+ const KEY: InjectionKey<number> = Symbol('ctx-key-setup');
31
+ const app = createApp({
32
+ setup() {
33
+ provide(KEY, 7);
34
+ return () => h('div');
35
+ }
36
+ });
37
+ const container = document.createElement('div');
38
+ document.body.appendChild(container);
39
+ app.mount(container);
40
+ await nextTick();
41
+ const value = app.runWithContext(() => inject(KEY));
42
+ expect(value).toBeUndefined();
43
+ app.unmount();
44
+ container.remove();
45
+ });
46
+ it('should read app-level provide set inside setup via appContext', async () => {
47
+ const KEY: InjectionKey<number> = Symbol('ctx-key-setup-app');
48
+ const app = createApp({
49
+ setup() {
50
+ const appInst = getCurrentInstance()!.appContext.app;
51
+ appInst.provide(KEY, 9);
52
+ return () => h('div');
53
+ }
54
+ });
55
+ const container = document.createElement('div');
56
+ document.body.appendChild(container);
57
+ app.mount(container);
58
+ await nextTick();
59
+ const value = app.runWithContext(() => inject(KEY));
60
+ expect(value).toBe(9);
61
+ app.unmount();
62
+ container.remove();
63
+ });
64
+ });
@@ -0,0 +1,484 @@
1
+ import { type Route, Router, RouterMode } from '@esmx/router';
2
+ /**
3
+ * @vitest-environment happy-dom
4
+ */
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import {
7
+ createApp,
8
+ defineComponent,
9
+ getCurrentInstance,
10
+ h,
11
+ nextTick
12
+ } from 'vue';
13
+ import { RouterView } from './router-view';
14
+ import {
15
+ getRouterViewDepth,
16
+ useProvideRouter,
17
+ useRoute,
18
+ useRouter,
19
+ useRouterViewDepth
20
+ } from './use';
21
+
22
+ describe('Router Vue Integration', () => {
23
+ let app: ReturnType<typeof createApp>;
24
+ let router: Router;
25
+ let mountPoint: HTMLElement;
26
+
27
+ beforeEach(async () => {
28
+ // Create a real Router instance
29
+ router = new Router({
30
+ mode: RouterMode.memory,
31
+ routes: [
32
+ { path: '/initial', component: {} },
33
+ { path: '/new-route', component: {} },
34
+ { path: '/user/:id', component: {} },
35
+ { path: '/new-path', component: {} }
36
+ ],
37
+ base: new URL('http://localhost:8000/')
38
+ });
39
+
40
+ // Ensure navigation to initial route is complete
41
+ await router.replace('/initial');
42
+
43
+ // Create mount point
44
+ mountPoint = document.createElement('div');
45
+ mountPoint.id = 'app';
46
+ document.body.appendChild(mountPoint);
47
+ });
48
+
49
+ afterEach(() => {
50
+ if (app) {
51
+ app.unmount();
52
+ }
53
+ document.body.removeChild(mountPoint);
54
+
55
+ // Clean up router
56
+ router.destroy();
57
+ });
58
+
59
+ describe('Router and Route Access', () => {
60
+ it('should provide router and route access', async () => {
61
+ let routerResult: Router | undefined;
62
+ let routeResult: Route | undefined;
63
+
64
+ const TestApp = {
65
+ setup() {
66
+ useProvideRouter(router);
67
+ routerResult = useRouter();
68
+ routeResult = useRoute();
69
+ return () => h('div', 'Test App');
70
+ }
71
+ };
72
+
73
+ app = createApp(TestApp);
74
+ app.mount('#app');
75
+
76
+ // Check retrieved objects
77
+ expect(routerResult).toEqual(router);
78
+ expect(routeResult).toBeDefined();
79
+ expect(routeResult?.path).toBe('/initial');
80
+ });
81
+ });
82
+
83
+ describe('Route Reactivity', () => {
84
+ it('should update route properties when route changes', async () => {
85
+ let routeRef: Route | undefined;
86
+
87
+ const TestApp = {
88
+ setup() {
89
+ useProvideRouter(router);
90
+ routeRef = useRoute();
91
+ return () => h('div', routeRef?.path);
92
+ }
93
+ };
94
+
95
+ app = createApp(TestApp);
96
+ app.mount('#app');
97
+
98
+ // Initial state
99
+ expect(routeRef?.path).toBe('/initial');
100
+
101
+ // Save reference to check identity
102
+ const initialRouteRef = routeRef;
103
+
104
+ // Navigate to new route
105
+ await router.replace('/new-route');
106
+ await nextTick();
107
+
108
+ // Check that reference is preserved but properties are updated
109
+ expect(routeRef).toBe(initialRouteRef);
110
+ expect(routeRef?.path).toBe('/new-route');
111
+ });
112
+
113
+ it('should update route params when route changes', async () => {
114
+ let routeRef: Route | undefined;
115
+
116
+ const TestApp = {
117
+ setup() {
118
+ useProvideRouter(router);
119
+ routeRef = useRoute();
120
+ return () =>
121
+ h('div', [
122
+ h('span', routeRef?.path),
123
+ h('span', routeRef?.params?.id || 'no-id')
124
+ ]);
125
+ }
126
+ };
127
+
128
+ app = createApp(TestApp);
129
+ app.mount('#app');
130
+
131
+ // Navigate to route with params
132
+ await router.replace('/user/123');
133
+ await nextTick();
134
+
135
+ // Check if params are updated
136
+ expect(routeRef?.path).toBe('/user/123');
137
+ expect(routeRef?.params?.id).toBe('123');
138
+ });
139
+
140
+ it('should automatically update view when route changes', async () => {
141
+ // Track render count
142
+ const renderCount = { value: 0 };
143
+ let routeRef: Route | undefined;
144
+
145
+ const TestApp = {
146
+ setup() {
147
+ useProvideRouter(router);
148
+ routeRef = useRoute();
149
+ return () => {
150
+ renderCount.value++;
151
+ return h('div', routeRef?.path);
152
+ };
153
+ }
154
+ };
155
+
156
+ app = createApp(TestApp);
157
+ app.mount('#app');
158
+
159
+ // Initial render
160
+ const initialRenderCount = renderCount.value;
161
+ expect(routeRef?.path).toBe('/initial');
162
+
163
+ // Navigate to new route
164
+ await router.replace('/new-route');
165
+ await nextTick();
166
+
167
+ // Check if render count increased, confirming view update
168
+ expect(renderCount.value).toBeGreaterThan(initialRenderCount);
169
+ expect(routeRef?.path).toBe('/new-route');
170
+
171
+ // Navigate to another route
172
+ const previousRenderCount = renderCount.value;
173
+ await router.replace('/new-path');
174
+ await nextTick();
175
+
176
+ // Check if render count increased again
177
+ expect(renderCount.value).toBeGreaterThan(previousRenderCount);
178
+ expect(routeRef?.path).toBe('/new-path');
179
+ });
180
+ });
181
+
182
+ describe('Nested Components', () => {
183
+ it('should provide route context to child components', async () => {
184
+ let parentRoute: Route | undefined;
185
+ let childRoute: Route | undefined;
186
+
187
+ const ChildComponent = {
188
+ setup() {
189
+ childRoute = useRoute();
190
+ return () => h('div', 'Child: ' + childRoute?.path);
191
+ }
192
+ };
193
+
194
+ const ParentComponent = {
195
+ setup() {
196
+ parentRoute = useRoute();
197
+ return () =>
198
+ h('div', [
199
+ h('span', 'Parent: ' + parentRoute?.path),
200
+ h(ChildComponent)
201
+ ]);
202
+ }
203
+ };
204
+
205
+ const TestApp = {
206
+ setup() {
207
+ useProvideRouter(router);
208
+ return () => h(ParentComponent);
209
+ }
210
+ };
211
+
212
+ app = createApp(TestApp);
213
+ app.mount('#app');
214
+
215
+ expect(parentRoute).toBeDefined();
216
+ expect(childRoute).toBeDefined();
217
+ expect(parentRoute?.path).toBe('/initial');
218
+ expect(childRoute?.path).toBe('/initial');
219
+
220
+ // Navigate to new path
221
+ await router.replace('/new-path');
222
+ await nextTick();
223
+
224
+ // Both parent and child components should see updates
225
+ expect(parentRoute?.path).toBe('/new-path');
226
+ expect(childRoute?.path).toBe('/new-path');
227
+ });
228
+ });
229
+
230
+ describe('RouterView Depth', () => {
231
+ it('should get depth in single RouterView', async () => {
232
+ let observedDepth: number | undefined;
233
+
234
+ const LeafProbe = defineComponent({
235
+ setup() {
236
+ const p = getCurrentInstance()!.proxy as any;
237
+ observedDepth = getRouterViewDepth(p);
238
+ return () => h('div');
239
+ }
240
+ });
241
+
242
+ const Level1 = defineComponent({
243
+ setup() {
244
+ return () => h('div', [h(LeafProbe)]);
245
+ }
246
+ });
247
+
248
+ router = new Router({
249
+ mode: RouterMode.memory,
250
+ routes: [{ path: '/level1', component: Level1 }],
251
+ base: new URL('http://localhost:8000/')
252
+ });
253
+
254
+ await router.replace('/level1');
255
+
256
+ const TestApp = defineComponent({
257
+ setup() {
258
+ useProvideRouter(router);
259
+ return () => h('div', [h(RouterView)]);
260
+ }
261
+ });
262
+
263
+ app = createApp(TestApp);
264
+ app.mount('#app');
265
+ await nextTick();
266
+
267
+ expect(observedDepth).toBe(1);
268
+ });
269
+
270
+ it('should get depth in nested RouterView', async () => {
271
+ let observedDepth: number | undefined;
272
+
273
+ const LeafProbe = defineComponent({
274
+ setup() {
275
+ const p = getCurrentInstance()!.proxy as any;
276
+ observedDepth = getRouterViewDepth(p);
277
+ return () => h('div');
278
+ }
279
+ });
280
+
281
+ const Level1 = defineComponent({
282
+ setup() {
283
+ return () => h('div', [h(RouterView)]);
284
+ }
285
+ });
286
+
287
+ const Leaf = defineComponent({
288
+ setup() {
289
+ return () => h('div', [h(LeafProbe)]);
290
+ }
291
+ });
292
+
293
+ router = new Router({
294
+ mode: RouterMode.memory,
295
+ routes: [
296
+ {
297
+ path: '/level1',
298
+ component: Level1,
299
+ children: [{ path: 'leaf', component: Leaf }]
300
+ }
301
+ ],
302
+ base: new URL('http://localhost:8000/')
303
+ });
304
+
305
+ await router.replace('/level1/leaf');
306
+
307
+ const TestApp = defineComponent({
308
+ setup() {
309
+ useProvideRouter(router);
310
+ return () => h('div', [h(RouterView)]);
311
+ }
312
+ });
313
+
314
+ app = createApp(TestApp);
315
+ app.mount('#app');
316
+ await nextTick();
317
+
318
+ expect(observedDepth).toBe(2);
319
+ });
320
+
321
+ it('should get depth in double-nested RouterViews', async () => {
322
+ let observedDepth: number | undefined;
323
+
324
+ const LeafProbe = defineComponent({
325
+ setup() {
326
+ const p = getCurrentInstance()!.proxy as any;
327
+ observedDepth = getRouterViewDepth(p);
328
+ return () => h('div');
329
+ }
330
+ });
331
+
332
+ const Level1 = defineComponent({
333
+ setup() {
334
+ return () => h('div', [h(RouterView)]);
335
+ }
336
+ });
337
+
338
+ const Level2 = defineComponent({
339
+ setup() {
340
+ return () => h('div', [h(RouterView)]);
341
+ }
342
+ });
343
+
344
+ const Leaf = defineComponent({
345
+ setup() {
346
+ return () => h('div', [h(LeafProbe)]);
347
+ }
348
+ });
349
+
350
+ router = new Router({
351
+ mode: RouterMode.memory,
352
+ routes: [
353
+ {
354
+ path: '/level1',
355
+ component: Level1,
356
+ children: [
357
+ {
358
+ path: 'level2',
359
+ component: Level2,
360
+ children: [{ path: 'leaf', component: Leaf }]
361
+ }
362
+ ]
363
+ }
364
+ ],
365
+ base: new URL('http://localhost:8000/')
366
+ });
367
+
368
+ await router.replace('/level1/level2/leaf');
369
+
370
+ const TestApp = defineComponent({
371
+ setup() {
372
+ useProvideRouter(router);
373
+ return () => h('div', [h(RouterView)]);
374
+ }
375
+ });
376
+
377
+ app = createApp(TestApp);
378
+ app.mount('#app');
379
+ await nextTick();
380
+
381
+ expect(observedDepth).toBe(3);
382
+ });
383
+
384
+ it('should throw when no RouterView ancestor exists', async () => {
385
+ let callDepth: (() => void) | undefined;
386
+
387
+ const Probe = defineComponent({
388
+ setup() {
389
+ const p = getCurrentInstance()!.proxy as any;
390
+ callDepth = () => getRouterViewDepth(p);
391
+ return () => h('div');
392
+ }
393
+ });
394
+
395
+ const TestApp = defineComponent({
396
+ setup() {
397
+ useProvideRouter(router);
398
+ return () => h(Probe);
399
+ }
400
+ });
401
+
402
+ app = createApp(TestApp);
403
+ app.mount('#app');
404
+ await nextTick();
405
+
406
+ expect(() => callDepth!()).toThrow(
407
+ new Error(
408
+ '[@esmx/router-vue] RouterView depth not found. Please ensure a RouterView exists in ancestor components.'
409
+ )
410
+ );
411
+ });
412
+
413
+ it('should return 0 for useRouterViewDepth without RouterView', async () => {
414
+ let observed = -1;
415
+
416
+ const Probe = defineComponent({
417
+ setup() {
418
+ observed = useRouterViewDepth();
419
+ return () => h('div');
420
+ }
421
+ });
422
+
423
+ const TestApp = defineComponent({
424
+ setup() {
425
+ useProvideRouter(router);
426
+ return () => h(Probe);
427
+ }
428
+ });
429
+
430
+ app = createApp(TestApp);
431
+ app.mount('#app');
432
+ await nextTick();
433
+
434
+ expect(observed).toBe(0);
435
+ });
436
+
437
+ it('should reflect depth via useRouterViewDepth at each level', async () => {
438
+ let level1Depth = -1;
439
+ let level2Depth = -1;
440
+
441
+ const Level2 = defineComponent({
442
+ setup() {
443
+ level2Depth = useRouterViewDepth();
444
+ return () => h('div');
445
+ }
446
+ });
447
+
448
+ const Level1 = defineComponent({
449
+ setup() {
450
+ level1Depth = useRouterViewDepth();
451
+ return () => h('div', [h(RouterView)]);
452
+ }
453
+ });
454
+
455
+ router = new Router({
456
+ mode: RouterMode.memory,
457
+ routes: [
458
+ {
459
+ path: '/level1',
460
+ component: Level1,
461
+ children: [{ path: 'level2', component: Level2 }]
462
+ }
463
+ ],
464
+ base: new URL('http://localhost:8000/')
465
+ });
466
+
467
+ await router.replace('/level1/level2');
468
+
469
+ const TestApp = defineComponent({
470
+ setup() {
471
+ useProvideRouter(router);
472
+ return () => h('div', [h(RouterView)]);
473
+ }
474
+ });
475
+
476
+ app = createApp(TestApp);
477
+ app.mount('#app');
478
+ await nextTick();
479
+
480
+ expect(level1Depth).toBe(1);
481
+ expect(level2Depth).toBe(2);
482
+ });
483
+ });
484
+ });