@module-federation/bridge-vue3 2.3.1 → 2.3.3

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.
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "git+https://github.com/module-federation/core.git",
8
8
  "directory": "packages/bridge/vue3-bridge"
9
9
  },
10
- "version": "2.3.1",
10
+ "version": "2.3.3",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -31,9 +31,9 @@
31
31
  "vue-router": "=4"
32
32
  },
33
33
  "dependencies": {
34
- "@module-federation/bridge-shared": "2.3.1",
35
- "@module-federation/runtime": "2.3.1",
36
- "@module-federation/sdk": "2.3.1"
34
+ "@module-federation/bridge-shared": "2.3.3",
35
+ "@module-federation/runtime": "2.3.3",
36
+ "@module-federation/sdk": "2.3.3"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^18.3.11",
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { createBridgeComponent } from './provider';
2
+ export type { ProviderFnParams } from './provider';
2
3
  export { createRemoteComponent, createRemoteAppComponent } from './create';
3
4
  export type { RenderFnParams } from '@module-federation/bridge-shared';
package/src/provider.ts CHANGED
@@ -16,9 +16,13 @@ type AddOptionsFnParams = {
16
16
 
17
17
  export type ProviderFnParams = {
18
18
  rootComponent: Vue.Component;
19
- appOptions: (
20
- params: AddOptionsFnParams,
21
- ) => { router?: VueRouter.Router } | void;
19
+ appOptions: (params: AddOptionsFnParams) => {
20
+ router?: VueRouter.Router;
21
+ /** Called with the bridge's internal router after creation but before navigation.
22
+ * Use this to register global guards (beforeEach, afterEach, etc.) that would
23
+ * otherwise be lost when the bridge recreates the router. */
24
+ afterRouterCreate?: (router: VueRouter.Router) => void;
25
+ } | void;
22
26
  };
23
27
 
24
28
  export function createBridgeComponent(bridgeInfo: ProviderFnParams) {
@@ -59,7 +63,7 @@ export function createBridgeComponent(bridgeInfo: ProviderFnParams) {
59
63
  ...extraProps,
60
64
  });
61
65
  if (bridgeOptions?.router) {
62
- const { history, routes } = processRoutes({
66
+ const { history, routes, patchRouter } = processRoutes({
63
67
  router: bridgeOptions.router,
64
68
  basename: info.basename,
65
69
  memoryRoute: info.memoryRoute,
@@ -72,6 +76,14 @@ export function createBridgeComponent(bridgeInfo: ProviderFnParams) {
72
76
  routes,
73
77
  });
74
78
 
79
+ if (patchRouter) {
80
+ patchRouter(router);
81
+ }
82
+
83
+ if (bridgeOptions.afterRouterCreate) {
84
+ bridgeOptions.afterRouterCreate(router);
85
+ }
86
+
75
87
  LoggerInstance.debug(`createBridgeComponent render router info>>>`, {
76
88
  moduleName,
77
89
  router,
package/src/routeUtils.ts CHANGED
@@ -10,6 +10,7 @@ export interface RouteProcessingOptions {
10
10
  export interface RouteProcessingResult {
11
11
  history: VueRouter.RouterHistory;
12
12
  routes: VueRouter.RouteRecordNormalized[];
13
+ patchRouter?: (router: VueRouter.Router) => void;
13
14
  }
14
15
 
15
16
  /**
@@ -27,8 +28,13 @@ function addBasenameToNestedRoutes(
27
28
  * Join two path segments, collapse multiple slashes, and optionally
28
29
  * preserve a trailing slash that was present in the original value.
29
30
  * A bare '/' root is never considered an intentional trailing slash.
31
+ *
32
+ * Relative paths (not starting with '/') are left untouched — Vue Router
33
+ * resolves them against the parent route, which already carries the basename.
30
34
  */
31
35
  const prefixPath = (original: string): string => {
36
+ if (!original.startsWith('/')) return original;
37
+
32
38
  const hasTrailingSlash = original.length > 1 && original.endsWith('/');
33
39
  const normalized =
34
40
  `${basename}/${original}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
@@ -68,6 +74,59 @@ function addBasenameToNestedRoutes(
68
74
  });
69
75
  }
70
76
 
77
+ /**
78
+ * Create a patch function that rewrites path-based navigations to include
79
+ * the basename prefix. This is needed because createWebHashHistory() does
80
+ * not accept a basename argument, so router.push('/foo') would bypass the
81
+ * prefixed route definitions.
82
+ *
83
+ * By patching push/replace/resolve *before* Vue Router resolves the
84
+ * location we also avoid the "No match found" console warning.
85
+ */
86
+ function createHashBasenamePatch(
87
+ basename: string,
88
+ ): (router: VueRouter.Router) => void {
89
+ const normalized = basename.replace(/\/+$/, '');
90
+
91
+ /**
92
+ * Only absolute paths (starting with '/') that don't already carry the
93
+ * basename prefix need rewriting. Relative segments ('settings'),
94
+ * query-only ('?tab=1'), and hash-only ('#anchor') strings are resolved
95
+ * by Vue Router against the current route and must pass through untouched.
96
+ */
97
+ const needsPrefix = (path: string): boolean =>
98
+ path.startsWith('/') &&
99
+ path !== normalized &&
100
+ !path.startsWith(normalized + '/');
101
+
102
+ const prefix = (path: string): string =>
103
+ `${normalized}${path}`.replace(/\/+/g, '/');
104
+
105
+ const rewrite = (
106
+ to: VueRouter.RouteLocationRaw,
107
+ ): VueRouter.RouteLocationRaw => {
108
+ if (typeof to === 'string') {
109
+ return needsPrefix(to) ? prefix(to) : to;
110
+ }
111
+ if ('path' in to && typeof to.path === 'string' && needsPrefix(to.path)) {
112
+ return { ...to, path: prefix(to.path) };
113
+ }
114
+ return to;
115
+ };
116
+
117
+ return (router) => {
118
+ const originalPush = router.push.bind(router);
119
+ const originalReplace = router.replace.bind(router);
120
+ const originalResolve = router.resolve.bind(router);
121
+
122
+ router.push = (to: VueRouter.RouteLocationRaw) => originalPush(rewrite(to));
123
+ router.replace = (to: VueRouter.RouteLocationRaw) =>
124
+ originalReplace(rewrite(to));
125
+ router.resolve = ((to: VueRouter.RouteLocationRaw, ...rest: any[]) =>
126
+ originalResolve(rewrite(to), ...rest)) as typeof router.resolve;
127
+ };
128
+ }
129
+
71
130
  /**
72
131
  * Route processing solution based on path analysis
73
132
  *
@@ -89,11 +148,15 @@ export function processRoutes(
89
148
  );
90
149
 
91
150
  // Use Map/Set for O(1) lookup performance
92
- const flatRoutesMap = new Map<string, VueRouter.RouteRecordNormalized>();
151
+ // Store arrays because multiple routes can resolve to the same path
152
+ // (e.g. a parent and a default child with path: '')
153
+ const flatRoutesMap = new Map<string, VueRouter.RouteRecordNormalized[]>();
93
154
  const processedRoutes = new Set<VueRouter.RouteRecordNormalized>();
94
155
 
95
156
  flatRoutes.forEach((route) => {
96
- flatRoutesMap.set(route.path, route);
157
+ const existing = flatRoutesMap.get(route.path) || [];
158
+ existing.push(route);
159
+ flatRoutesMap.set(route.path, existing);
97
160
  });
98
161
 
99
162
  /**
@@ -115,9 +178,20 @@ export function processRoutes(
115
178
  for (let j = 0; j < route.children.length; j++) {
116
179
  const child = route.children[j];
117
180
  const fullPath = normalizePath(prefix, child.path);
118
- const childRoute = flatRoutesMap.get(fullPath);
181
+ const candidates = flatRoutesMap.get(fullPath) || [];
182
+ // Find a matching route that:
183
+ // 1. Hasn't been processed yet
184
+ // 2. Isn't the current parent route (avoids circular references when
185
+ // a child with path: '' resolves to the same absolute path)
186
+ // 3. Matches by name when the child definition specifies one
187
+ const childRoute = candidates.find(
188
+ (r) =>
189
+ !processedRoutes.has(r) &&
190
+ r !== route &&
191
+ (child.name == null || r.name === child.name),
192
+ );
119
193
 
120
- if (childRoute && !processedRoutes.has(childRoute)) {
194
+ if (childRoute) {
121
195
  // Create a new optimized route object with relative path for nested routes
122
196
  const relativeChildRoute: VueRouter.RouteRecordNormalized = {
123
197
  ...childRoute,
@@ -146,21 +220,19 @@ export function processRoutes(
146
220
  }
147
221
 
148
222
  let history: VueRouter.RouterHistory;
223
+ let patchRouter: ((router: VueRouter.Router) => void) | undefined;
224
+
149
225
  if (memoryRoute) {
150
- // Memory route mode
151
226
  history = VueRouter.createMemoryHistory(basename);
152
227
  } else if (hashRoute) {
153
- // Hash route mode
154
228
  history = VueRouter.createWebHashHistory();
155
- // Recursively process nested routes and add basename prefix to all paths
156
- if (basename) routes = addBasenameToNestedRoutes(routes, basename);
229
+ if (basename) {
230
+ routes = addBasenameToNestedRoutes(routes, basename);
231
+ patchRouter = createHashBasenamePatch(basename);
232
+ }
157
233
  } else {
158
- // Default Web History mode
159
234
  history = VueRouter.createWebHistory(basename);
160
235
  }
161
236
 
162
- return {
163
- history,
164
- routes,
165
- };
237
+ return { history, routes, patchRouter };
166
238
  }