@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
package/dist/use.mjs CHANGED
@@ -1,15 +1,81 @@
1
- import { getCurrentInstance, inject } from "vue";
2
- import { routerKey, routerViewLocationKey } from "./symbols.mjs";
3
- export function throwNoCurrentInstance(method) {
4
- if (!getCurrentInstance()) {
5
- throw new Error(
6
- `[router-vue]: Missing current instance. ${method}() must be called inside <script setup> or setup().`
7
- );
1
+ import {
2
+ computed,
3
+ getCurrentInstance,
4
+ inject,
5
+ onBeforeUnmount,
6
+ provide,
7
+ ref
8
+ } from "vue";
9
+ import { createSymbolProperty } from "./util.mjs";
10
+ const ROUTER_CONTEXT_KEY = Symbol("router-context");
11
+ const ROUTER_INJECT_KEY = Symbol("router-inject");
12
+ const ERROR_MESSAGES = {
13
+ SETUP_ONLY: (fnName) => `[@esmx/router-vue] ${fnName}() can only be called during setup()`,
14
+ CONTEXT_NOT_FOUND: "[@esmx/router-vue] Router context not found. Please ensure useProvideRouter() is called in a parent component."
15
+ };
16
+ const routerContextProperty = createSymbolProperty(ROUTER_CONTEXT_KEY);
17
+ function getCurrentProxy(functionName) {
18
+ const instance = getCurrentInstance();
19
+ if (!instance || !instance.proxy) {
20
+ throw new Error(ERROR_MESSAGES.SETUP_ONLY(functionName));
8
21
  }
22
+ return instance.proxy;
23
+ }
24
+ function findRouterContext(vm) {
25
+ if (!vm) {
26
+ vm = getCurrentProxy("findRouterContext");
27
+ }
28
+ let context = routerContextProperty.get(vm);
29
+ if (context) {
30
+ return context;
31
+ }
32
+ let current = vm.$parent;
33
+ while (current) {
34
+ context = routerContextProperty.get(current);
35
+ if (context) {
36
+ routerContextProperty.set(vm, context);
37
+ return context;
38
+ }
39
+ current = current.$parent;
40
+ }
41
+ throw new Error(ERROR_MESSAGES.CONTEXT_NOT_FOUND);
42
+ }
43
+ export function getRouter(instance) {
44
+ return findRouterContext(instance).router;
45
+ }
46
+ export function getRoute(instance) {
47
+ return findRouterContext(instance).route.value;
48
+ }
49
+ function useRouterContext(functionName) {
50
+ const injectedContext = inject(ROUTER_INJECT_KEY);
51
+ if (injectedContext) {
52
+ return injectedContext;
53
+ }
54
+ const proxy = getCurrentProxy(functionName);
55
+ return findRouterContext(proxy);
9
56
  }
10
57
  export function useRouter() {
11
- return inject(routerKey);
58
+ return useRouterContext("useRouter").router;
12
59
  }
13
60
  export function useRoute() {
14
- return inject(routerViewLocationKey);
61
+ return useRouterContext("useRoute").route.value;
62
+ }
63
+ export function useProvideRouter(router) {
64
+ const proxy = getCurrentProxy("useProvideRouter");
65
+ const context = {
66
+ router,
67
+ route: ref(router.route)
68
+ };
69
+ provide(ROUTER_INJECT_KEY, context);
70
+ routerContextProperty.set(proxy, context);
71
+ const unwatch = router.afterEach((to) => {
72
+ if (router.route === to) {
73
+ to.syncTo(context.route.value);
74
+ }
75
+ });
76
+ onBeforeUnmount(unwatch);
77
+ }
78
+ export function useLink(props) {
79
+ const router = useRouter();
80
+ return computed(() => router.resolveLink(props));
15
81
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,461 @@
1
+ import { Router, RouterMode } from "@esmx/router";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import {
4
+ createApp,
5
+ defineComponent,
6
+ getCurrentInstance,
7
+ h,
8
+ nextTick
9
+ } from "vue";
10
+ import {
11
+ getRoute,
12
+ getRouter,
13
+ useLink,
14
+ useProvideRouter,
15
+ useRoute,
16
+ useRouter
17
+ } from "./use.mjs";
18
+ describe("use.ts - Vue Router Integration", () => {
19
+ let router;
20
+ let testContainer;
21
+ beforeEach(async () => {
22
+ testContainer = document.createElement("div");
23
+ testContainer.id = "test-app";
24
+ document.body.appendChild(testContainer);
25
+ const routes = [
26
+ {
27
+ path: "/",
28
+ component: defineComponent({ template: "<div>Home</div>" }),
29
+ meta: { title: "Home" }
30
+ },
31
+ {
32
+ path: "/about",
33
+ component: defineComponent({ template: "<div>About</div>" }),
34
+ meta: { title: "About" }
35
+ }
36
+ ];
37
+ router = new Router({
38
+ root: "#test-app",
39
+ routes,
40
+ mode: RouterMode.memory,
41
+ base: new URL("http://localhost:3000/")
42
+ });
43
+ await router.replace("/");
44
+ });
45
+ afterEach(() => {
46
+ if (testContainer.parentNode) {
47
+ testContainer.parentNode.removeChild(testContainer);
48
+ }
49
+ if (router) {
50
+ router.destroy();
51
+ }
52
+ });
53
+ describe("Error Handling - Context Not Found", () => {
54
+ const contextNotFoundError = "[@esmx/router-vue] Router context not found. Please ensure useProvideRouter() is called in a parent component.";
55
+ const contextErrorTestCases = [
56
+ {
57
+ name: "getRouter called without router context",
58
+ test: () => {
59
+ const app = createApp({ template: "<div>Test</div>" });
60
+ const vm = app.mount(testContainer);
61
+ const result = () => getRouter(vm);
62
+ app.unmount();
63
+ return result;
64
+ }
65
+ },
66
+ {
67
+ name: "getRoute called without router context",
68
+ test: () => {
69
+ const app = createApp({ template: "<div>Test</div>" });
70
+ const vm = app.mount(testContainer);
71
+ const result = () => getRoute(vm);
72
+ app.unmount();
73
+ return result;
74
+ }
75
+ }
76
+ ];
77
+ contextErrorTestCases.forEach(({ name, test }) => {
78
+ it(`should throw error when ${name}`, () => {
79
+ expect(test()).toThrow(contextNotFoundError);
80
+ });
81
+ });
82
+ const compositionContextErrorTestCases = [
83
+ {
84
+ name: "useRouter called without router context",
85
+ setupFn: () => {
86
+ expect(() => useRouter()).toThrow(contextNotFoundError);
87
+ }
88
+ },
89
+ {
90
+ name: "useRoute called without router context",
91
+ setupFn: () => {
92
+ expect(() => useRoute()).toThrow(contextNotFoundError);
93
+ }
94
+ },
95
+ {
96
+ name: "useLink called without router context",
97
+ setupFn: () => {
98
+ expect(
99
+ () => useLink({
100
+ to: "/about",
101
+ type: "push",
102
+ exact: "include"
103
+ })
104
+ ).toThrow(contextNotFoundError);
105
+ }
106
+ }
107
+ ];
108
+ compositionContextErrorTestCases.forEach(({ name, setupFn }) => {
109
+ it(`should throw error when ${name}`, () => {
110
+ const TestComponent = defineComponent({
111
+ setup() {
112
+ setupFn();
113
+ return () => "<div>Test</div>";
114
+ }
115
+ });
116
+ const app = createApp(TestComponent);
117
+ app.mount(testContainer);
118
+ app.unmount();
119
+ });
120
+ });
121
+ });
122
+ describe("Error Handling - Setup Only", () => {
123
+ const setupOnlyTestCases = [
124
+ {
125
+ name: "useRouter called outside setup()",
126
+ fn: () => useRouter(),
127
+ expectedError: "[@esmx/router-vue] useRouter() can only be called during setup()"
128
+ },
129
+ {
130
+ name: "useRoute called outside setup()",
131
+ fn: () => useRoute(),
132
+ expectedError: "[@esmx/router-vue] useRoute() can only be called during setup()"
133
+ },
134
+ {
135
+ name: "useLink called outside setup()",
136
+ fn: () => useLink({
137
+ to: "/about",
138
+ type: "push",
139
+ exact: "include"
140
+ }),
141
+ expectedError: "[@esmx/router-vue] useRouter() can only be called during setup()"
142
+ },
143
+ {
144
+ name: "useProvideRouter called outside setup()",
145
+ fn: () => useProvideRouter(router),
146
+ expectedError: "[@esmx/router-vue] useProvideRouter() can only be called during setup()"
147
+ }
148
+ ];
149
+ setupOnlyTestCases.forEach(({ name, fn, expectedError }) => {
150
+ it(`should throw error when ${name}`, () => {
151
+ expect(fn).toThrow(expectedError);
152
+ });
153
+ });
154
+ });
155
+ describe("Basic Functionality", () => {
156
+ it("should provide router context and return router instance from getRouter", async () => {
157
+ let routerInstance = null;
158
+ const app = createApp({
159
+ setup() {
160
+ useProvideRouter(router);
161
+ return () => "<div>App</div>";
162
+ }
163
+ });
164
+ const vm = app.mount(testContainer);
165
+ routerInstance = getRouter(vm);
166
+ expect(routerInstance).toBe(router);
167
+ expect(routerInstance).toBeInstanceOf(Router);
168
+ app.unmount();
169
+ });
170
+ it("should provide router context and return current route from getRoute", async () => {
171
+ let currentRoute = null;
172
+ const app = createApp({
173
+ setup() {
174
+ useProvideRouter(router);
175
+ return () => "<div>App</div>";
176
+ }
177
+ });
178
+ const vm = app.mount(testContainer);
179
+ currentRoute = getRoute(vm);
180
+ expect(currentRoute).toBeTruthy();
181
+ expect(currentRoute.path).toBe("/");
182
+ expect(currentRoute.meta.title).toBe("Home");
183
+ app.unmount();
184
+ });
185
+ });
186
+ describe("Setup() Support - useRouter in setup()", () => {
187
+ it("should allow useRouter to work in setup() via provide/inject", async () => {
188
+ let routerInstance = null;
189
+ let childRoute = null;
190
+ const ChildComponent = defineComponent({
191
+ name: "ChildComponent",
192
+ setup() {
193
+ routerInstance = useRouter();
194
+ childRoute = useRoute();
195
+ expect(routerInstance).toBe(router);
196
+ expect(childRoute.path).toBe("/");
197
+ return () => h("div", "Child Component");
198
+ }
199
+ });
200
+ const ParentComponent = defineComponent({
201
+ name: "ParentComponent",
202
+ components: { ChildComponent },
203
+ setup() {
204
+ useProvideRouter(router);
205
+ return () => h(ChildComponent);
206
+ }
207
+ });
208
+ const app = createApp(ParentComponent);
209
+ app.mount(testContainer);
210
+ await nextTick();
211
+ expect(routerInstance).toBe(router);
212
+ expect(childRoute).toBeTruthy();
213
+ expect(childRoute.path).toBe("/");
214
+ app.unmount();
215
+ });
216
+ it("should work with nested components in setup()", async () => {
217
+ let deepChildRouter = null;
218
+ const DeepChildComponent = defineComponent({
219
+ name: "DeepChildComponent",
220
+ setup() {
221
+ deepChildRouter = useRouter();
222
+ expect(deepChildRouter).toBe(router);
223
+ return () => h("div", "Deep Child");
224
+ }
225
+ });
226
+ const MiddleComponent = defineComponent({
227
+ name: "MiddleComponent",
228
+ components: { DeepChildComponent },
229
+ setup() {
230
+ return () => h(DeepChildComponent);
231
+ }
232
+ });
233
+ const TopComponent = defineComponent({
234
+ name: "TopComponent",
235
+ components: { MiddleComponent },
236
+ setup() {
237
+ useProvideRouter(router);
238
+ return () => h(MiddleComponent);
239
+ }
240
+ });
241
+ const app = createApp(TopComponent);
242
+ app.mount(testContainer);
243
+ await nextTick();
244
+ expect(deepChildRouter).toBe(router);
245
+ app.unmount();
246
+ });
247
+ });
248
+ describe("Component Hierarchy Context Finding - Investigation", () => {
249
+ it("should investigate component hierarchy traversal with logging", async () => {
250
+ let childRouterResult = null;
251
+ let parentVmInstance = null;
252
+ let childVmInstance = null;
253
+ const ChildComponent = defineComponent({
254
+ name: "ChildComponent",
255
+ setup(_, { expose }) {
256
+ const instance = getCurrentInstance();
257
+ childVmInstance = (instance == null ? void 0 : instance.proxy) || null;
258
+ try {
259
+ childRouterResult = useRouter();
260
+ } catch (error) {
261
+ expect(error.message).toContain(
262
+ "Router context not found"
263
+ );
264
+ }
265
+ expose({ childVmInstance });
266
+ return () => "<div>Child Component</div>";
267
+ }
268
+ });
269
+ const ParentComponent = defineComponent({
270
+ name: "ParentComponent",
271
+ components: { ChildComponent },
272
+ setup(_, { expose }) {
273
+ const instance = getCurrentInstance();
274
+ parentVmInstance = (instance == null ? void 0 : instance.proxy) || null;
275
+ useProvideRouter(router);
276
+ expose({ parentVmInstance });
277
+ return () => h(ChildComponent);
278
+ }
279
+ });
280
+ const app = createApp(ParentComponent);
281
+ const mountedApp = app.mount(testContainer);
282
+ await nextTick();
283
+ if (childVmInstance && parentVmInstance) {
284
+ const parentHasContext = !!parentVmInstance[Symbol.for("router-context")] || Object.getOwnPropertySymbols(parentVmInstance).some(
285
+ (sym) => sym.toString().includes("router-context")
286
+ );
287
+ expect(parentHasContext).toBe(true);
288
+ }
289
+ if (childRouterResult) {
290
+ expect(childRouterResult).toBe(router);
291
+ }
292
+ app.unmount();
293
+ });
294
+ it("should investigate direct getRouter call with component instances", async () => {
295
+ let parentInstance = null;
296
+ let childInstance = null;
297
+ const ChildComponent = defineComponent({
298
+ name: "ChildComponent",
299
+ setup() {
300
+ const instance = getCurrentInstance();
301
+ childInstance = (instance == null ? void 0 : instance.proxy) || null;
302
+ return () => "<div>Child</div>";
303
+ }
304
+ });
305
+ const ParentComponent = defineComponent({
306
+ name: "ParentComponent",
307
+ components: { ChildComponent },
308
+ setup() {
309
+ const instance = getCurrentInstance();
310
+ parentInstance = (instance == null ? void 0 : instance.proxy) || null;
311
+ useProvideRouter(router);
312
+ return () => h(ChildComponent);
313
+ }
314
+ });
315
+ const app = createApp(ParentComponent);
316
+ app.mount(testContainer);
317
+ await nextTick();
318
+ if (childInstance && parentInstance) {
319
+ try {
320
+ const routerFromChild = getRouter(childInstance);
321
+ expect(routerFromChild).toBe(router);
322
+ } catch (error) {
323
+ expect(error.message).toContain(
324
+ "Router context not found"
325
+ );
326
+ }
327
+ }
328
+ app.unmount();
329
+ });
330
+ });
331
+ describe("Navigation", () => {
332
+ it("should handle router navigation correctly", async () => {
333
+ const app = createApp({
334
+ setup() {
335
+ useProvideRouter(router);
336
+ return () => "<div>App</div>";
337
+ }
338
+ });
339
+ app.mount(testContainer);
340
+ expect(router.route.path).toBe("/");
341
+ expect(router.route.meta.title).toBe("Home");
342
+ await router.push("/about");
343
+ expect(router.route.path).toBe("/about");
344
+ expect(router.route.meta.title).toBe("About");
345
+ app.unmount();
346
+ });
347
+ });
348
+ describe("Composition API Integration", () => {
349
+ it("should work with composition API functions in same component", async () => {
350
+ let compositionRouter = null;
351
+ let compositionRoute = null;
352
+ let linkResolver = null;
353
+ const TestComponent = defineComponent({
354
+ setup() {
355
+ useProvideRouter(router);
356
+ compositionRouter = useRouter();
357
+ compositionRoute = useRoute();
358
+ linkResolver = useLink({
359
+ to: "/about",
360
+ type: "push",
361
+ exact: "include"
362
+ });
363
+ return () => "<div>Test Component</div>";
364
+ }
365
+ });
366
+ const app = createApp(TestComponent);
367
+ app.mount(testContainer);
368
+ await nextTick();
369
+ expect(compositionRouter).toBe(router);
370
+ expect(compositionRoute).toBeTruthy();
371
+ expect(compositionRoute.path).toBe("/");
372
+ expect(compositionRoute.meta.title).toBe("Home");
373
+ expect(linkResolver).toBeTruthy();
374
+ expect(linkResolver.value).toBeTruthy();
375
+ const link = linkResolver.value;
376
+ expect(link).toHaveProperty("attributes");
377
+ expect(link).toHaveProperty("getEventHandlers");
378
+ expect(link).toHaveProperty("isActive");
379
+ app.unmount();
380
+ });
381
+ it("should handle route updates reactively", async () => {
382
+ let routeRef = null;
383
+ const TestComponent = defineComponent({
384
+ setup() {
385
+ useProvideRouter(router);
386
+ const route = useRoute();
387
+ routeRef = route;
388
+ return () => `<div>Current: ${route.path}</div>`;
389
+ }
390
+ });
391
+ const app = createApp(TestComponent);
392
+ app.mount(testContainer);
393
+ await nextTick();
394
+ expect(routeRef).toBeTruthy();
395
+ expect(routeRef.path).toBe("/");
396
+ await router.push("/about");
397
+ await nextTick();
398
+ expect(routeRef.path).toBe("/about");
399
+ app.unmount();
400
+ });
401
+ });
402
+ describe("Deep Component Hierarchy", () => {
403
+ it("should cover deep component hierarchy traversal (multi-level parent chain)", async () => {
404
+ let childRouterResult = null;
405
+ const ChildComponent = defineComponent({
406
+ name: "ChildComponent",
407
+ setup() {
408
+ return () => h("div", "Deep Child");
409
+ }
410
+ });
411
+ const ParentComponent = defineComponent({
412
+ name: "ParentComponent",
413
+ setup() {
414
+ return () => h(ChildComponent);
415
+ }
416
+ });
417
+ const GrandParentComponent = defineComponent({
418
+ name: "GrandParentComponent",
419
+ setup() {
420
+ useProvideRouter(router);
421
+ return () => h(ParentComponent);
422
+ }
423
+ });
424
+ const app = createApp(GrandParentComponent);
425
+ const mountedApp = app.mount(testContainer);
426
+ await nextTick();
427
+ const deepChildInstance = mountedApp;
428
+ const mockDeepChild = {
429
+ $parent: {
430
+ // This is the middle parent (no router context)
431
+ $parent: mountedApp
432
+ // This is the grandparent (has router context)
433
+ }
434
+ };
435
+ childRouterResult = getRouter(mockDeepChild);
436
+ expect(childRouterResult).toBe(router);
437
+ expect(childRouterResult).toBeInstanceOf(Router);
438
+ app.unmount();
439
+ });
440
+ it("should handle component hierarchy traversal with manual parent chain setup", () => {
441
+ const app = createApp({
442
+ setup() {
443
+ useProvideRouter(router);
444
+ return () => h("div", "Root");
445
+ }
446
+ });
447
+ const rootInstance = app.mount(testContainer);
448
+ const leafInstance = {
449
+ $parent: {
450
+ // Middle level - no router context
451
+ $parent: rootInstance
452
+ // Root level - has router context
453
+ }
454
+ };
455
+ const foundRouter = getRouter(leafInstance);
456
+ expect(foundRouter).toBe(router);
457
+ expect(foundRouter).toBeInstanceOf(Router);
458
+ app.unmount();
459
+ });
460
+ });
461
+ });
package/dist/util.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export declare const isVue3: boolean;
2
+ export declare function createSymbolProperty<T>(symbol: symbol): {
3
+ readonly set: (instance: any, value: T) => void;
4
+ readonly get: (instance: any) => T | undefined;
5
+ };
6
+ export declare function isESModule(obj: unknown): obj is Record<string | symbol, any>;
7
+ export declare function resolveComponent(component: unknown): unknown;
package/dist/util.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import { version } from "vue";
2
+ export const isVue3 = version.startsWith("3.");
3
+ export function createSymbolProperty(symbol) {
4
+ return {
5
+ set(instance, value) {
6
+ instance[symbol] = value;
7
+ },
8
+ get(instance) {
9
+ return symbol in instance ? instance[symbol] : void 0;
10
+ }
11
+ };
12
+ }
13
+ export function isESModule(obj) {
14
+ if (!obj || typeof obj !== "object") return false;
15
+ const module = obj;
16
+ return Boolean(module.__esModule) || module[Symbol.toStringTag] === "Module";
17
+ }
18
+ export function resolveComponent(component) {
19
+ if (!component) return null;
20
+ if (isESModule(component)) {
21
+ return component.default || component;
22
+ }
23
+ return component;
24
+ }
@@ -0,0 +1 @@
1
+ export {};