Skip to content

Commit ab3b3b7

Browse files
committed
fix: remove universal-router
1 parent 1e60860 commit ab3b3b7

9 files changed

Lines changed: 501 additions & 215 deletions

File tree

packages/app/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,14 @@
4242
"minimatch": "^5.1.6",
4343
"mobx": "^6.15.0",
4444
"nanoid": "^3.3.11",
45-
"path-to-regexp": "^8.3.0",
4645
"react": "18.2.0",
4746
"react-dom": "18.2.0",
4847
"ts-invariant": "^0.10.3",
49-
"universal-router": "^9.2.1",
5048
"warning": "^4.0.3",
5149
"zod": "^3.25.76"
5250
},
5351
"devDependencies": {
5452
"@types/lodash": "^4.17.23",
55-
"@types/universal-router": "^8.0.0",
5653
"@types/warning": "^3.0.3",
5754
"@webiny/build-tools": "0.0.0",
5855
"rimraf": "^6.1.2",

packages/app/src/features/router/HistoryRouterGateway.test.ts

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, beforeEach, expect, vi } from "vitest";
22
import { createMemoryHistory } from "history";
33
import { HistoryRouterGateway } from "./HistoryRouterGateway.js";
4-
import { generateUrl } from "./generateUrl.js";
4+
import { RouteUrl } from "./RouteUrl.js";
55

66
const wait = () => new Promise(resolve => setTimeout(resolve, 10));
77

@@ -89,11 +89,11 @@ describe("Router Gateway", () => {
8989

9090
it("should generate route URLs", async () => {
9191
const urls = [
92-
generateUrl("/"),
93-
generateUrl("/login", {}),
94-
generateUrl("/login", { redirect: "/", reason: "login" }),
95-
generateUrl("/dynamic-route/:name", { name: "cars" }),
96-
generateUrl("/dynamic-route", { folderId: "696556831e485d00027a1a0b#0001" })
92+
RouteUrl.fromPattern("/"),
93+
RouteUrl.fromPattern("/login", {}),
94+
RouteUrl.fromPattern("/login", { redirect: "/", reason: "login" }),
95+
RouteUrl.fromPattern("/dynamic-route/:name", { name: "cars" }),
96+
RouteUrl.fromPattern("/dynamic-route", { folderId: "696556831e485d00027a1a0b#0001" })
9797
];
9898
expect(urls).toEqual([
9999
"/",
@@ -103,4 +103,98 @@ describe("Router Gateway", () => {
103103
"/dynamic-route?folderId=696556831e485d00027a1a0b%230001"
104104
]);
105105
});
106+
107+
it("should handle baseUrl for route matching and URL generation", async () => {
108+
const spyFileManager = vi.fn();
109+
const spyHome = vi.fn();
110+
111+
// Create history with a tenant prefix
112+
const history = createMemoryHistory();
113+
history.replace("/tenant123/__unknown__");
114+
115+
const gateway = new HistoryRouterGateway(history, "/tenant123");
116+
gateway.setRoutes([
117+
{ name: "home", path: "/", onMatch: spyHome },
118+
{ name: "fileManager", path: "/file-manager", onMatch: spyFileManager }
119+
]);
120+
121+
// Navigate to /tenant123/file-manager
122+
history.push("/tenant123/file-manager");
123+
await wait();
124+
125+
// Should match the /file-manager route
126+
expect(spyFileManager).toHaveBeenCalledTimes(1);
127+
expect(spyFileManager).toHaveBeenCalledWith({
128+
name: "fileManager",
129+
path: "/file-manager",
130+
pathname: "/tenant123/file-manager",
131+
params: {}
132+
});
133+
134+
// Test URL generation with baseUrl
135+
const urlWithBase = RouteUrl.fromPattern("/file-manager", {}, "/tenant123");
136+
expect(urlWithBase).toBe("/tenant123/file-manager");
137+
138+
const urlWithBaseAndParams = RouteUrl.fromPattern(
139+
"/file-manager",
140+
{ folder: "abc" },
141+
"/tenant123"
142+
);
143+
expect(urlWithBaseAndParams).toBe("/tenant123/file-manager?folder=abc");
144+
});
145+
146+
it("should properly sort routes with wildcards always at the bottom", async () => {
147+
const spyWildcard = vi.fn();
148+
const spySpecific = vi.fn();
149+
const spyHome = vi.fn();
150+
151+
const history = createMemoryHistory();
152+
// Start at a non-matching path to avoid initial route resolution
153+
history.replace("/initial");
154+
155+
const gateway = new HistoryRouterGateway(history, "");
156+
157+
// Add wildcard first, then home route
158+
// Note: setRoutes will trigger route resolution for current path (/initial)
159+
gateway.setRoutes([
160+
{ name: "wildcard", path: "*", onMatch: spyWildcard },
161+
{ name: "home", path: "/", onMatch: spyHome }
162+
]);
163+
164+
// Wildcard should have matched /initial
165+
expect(spyWildcard).toHaveBeenCalledTimes(1);
166+
vi.clearAllMocks();
167+
168+
// Now add a specific route after the wildcard - this should re-sort and keep wildcard at bottom
169+
// Note: setRoutes will trigger route resolution again for /initial, matching wildcard again
170+
gateway.setRoutes([{ name: "specific", path: "/specific-route", onMatch: spySpecific }]);
171+
172+
// Wildcard should match /initial again after re-sorting
173+
expect(spyWildcard).toHaveBeenCalledTimes(1);
174+
vi.clearAllMocks();
175+
176+
// Navigate to the specific route
177+
history.push("/specific-route");
178+
await wait();
179+
180+
// Should match the specific route, not the wildcard
181+
expect(spySpecific).toHaveBeenCalledTimes(1);
182+
expect(spyWildcard).toHaveBeenCalledTimes(0);
183+
expect(spyHome).toHaveBeenCalledTimes(0);
184+
185+
// Navigate to home
186+
history.push("/");
187+
await wait();
188+
189+
// Should match home route
190+
expect(spyHome).toHaveBeenCalledTimes(1);
191+
expect(spyWildcard).toHaveBeenCalledTimes(0);
192+
193+
// Navigate to unknown route
194+
history.push("/unknown");
195+
await wait();
196+
197+
// Should match wildcard
198+
expect(spyWildcard).toHaveBeenCalledTimes(1);
199+
});
106200
});

packages/app/src/features/router/HistoryRouterGateway.ts

Lines changed: 11 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,19 @@
11
import type { z } from "zod";
22
import type { History } from "history";
3-
import UniversalRouter, {
4-
RouterContext,
5-
RouteParams as UniversalRouteParams
6-
} from "universal-router";
7-
import type { MatchedRoute, RouteDefinition, OnRouteExit } from "./abstractions.js";
3+
import type { RouteDefinition, OnRouteExit } from "./abstractions.js";
84
import { RouterGateway } from "./abstractions.js";
9-
import { generateUrl } from "./generateUrl.js";
10-
11-
type RouteResolveResult = [MatchedRoute, RouteDefinition["onMatch"]];
12-
13-
interface RouteDefinitionWithAction extends RouteDefinition {
14-
action(context: RouterContext, params: UniversalRouteParams): RouteResolveResult;
15-
}
5+
import { RouteUrl } from "./RouteUrl.js";
6+
import { Router } from "./Router.js";
167

178
export class HistoryRouterGateway implements RouterGateway.Interface {
189
private readonly history: History;
19-
private readonly routes: RouteDefinitionWithAction[] = [];
20-
private readonly router: UniversalRouter<RouteResolveResult>;
10+
private readonly router: Router;
2111
private stopListening: () => void;
2212
private unblock: (() => void) | undefined;
23-
private currentRoute: MatchedRoute | undefined;
2413

2514
constructor(history: History, baseUrl: string) {
2615
this.history = history;
27-
this.router = new UniversalRouter<RouteResolveResult>(this.routes, {
28-
baseUrl,
29-
resolveRoute: (context, params) => {
30-
if (!context.path) {
31-
return context.next();
32-
}
33-
34-
if (!context.route.action) {
35-
return undefined;
36-
}
37-
38-
return context.route.action(context, params);
39-
},
40-
errorHandler: () => undefined
41-
});
16+
this.router = new Router(baseUrl);
4217

4318
this.stopListening = history.listen(async ({ location }) => {
4419
const queryParams = Object.fromEntries(new URLSearchParams(location.search).entries());
@@ -51,27 +26,18 @@ export class HistoryRouterGateway implements RouterGateway.Interface {
5126
}
5227

5328
goToRoute(name: string, params: z.ZodTypeAny): void {
54-
const route = this.routes.find(r => r.name === name);
29+
const route = this.router.findRoute(name);
5530
if (!route) {
5631
console.warn(`Route "${name}" not found.`);
5732
return;
5833
}
5934

60-
this.history.push(generateUrl(route.path, params));
35+
const baseUrl = this.router.getBaseUrl();
36+
this.history.push(RouteUrl.fromPattern(route.path, params, baseUrl));
6137
}
6238

6339
setRoutes(routes: RouteDefinition[]) {
64-
routes.forEach(route => {
65-
const index = this.routes.findIndex(r => r.name === route.name);
66-
67-
if (index > -1) {
68-
this.routes[index] = this.routeWithAction(route);
69-
} else {
70-
this.routes.push(this.routeWithAction(route));
71-
}
72-
});
73-
74-
this.sortRoutes(this.routes);
40+
this.router.setRoutes(routes);
7541

7642
const queryParams = Object.fromEntries(
7743
new URLSearchParams(this.history.location.search).entries()
@@ -89,67 +55,17 @@ export class HistoryRouterGateway implements RouterGateway.Interface {
8955
this.history.push(url);
9056
}
9157

92-
private routeWithAction(route: RouteDefinition): RouteDefinitionWithAction {
93-
return {
94-
...route,
95-
path: route.path === "*" ? "(.*)" : route.path,
96-
action: (context, params) => {
97-
const matchedRoute = {
98-
name: route.name,
99-
path: route.path,
100-
pathname: context.pathname,
101-
params: { ...params, ...context.queryParams }
102-
};
103-
104-
const onMatch = async (matchedRoute: MatchedRoute) => {
105-
route.onMatch(matchedRoute);
106-
};
107-
108-
return [matchedRoute, onMatch];
109-
}
110-
};
111-
}
112-
11358
private async resolvePathname(pathname: string, queryParams?: Record<string, unknown>) {
114-
const result = await this.router.resolve({ pathname, queryParams });
59+
const result = this.router.resolve(pathname, queryParams);
11560
if (!result) {
11661
return;
11762
}
11863

119-
const [matchedRoute, onMatch] = result;
120-
121-
this.currentRoute = matchedRoute;
64+
const { matchedRoute, onMatch } = result;
12265

12366
onMatch(matchedRoute);
12467
}
12568

126-
private sortRoutes(routes: RouteDefinition[]) {
127-
const INDEX_PATH = "/";
128-
const MATCH_ALL = "(.*)";
129-
130-
routes.sort((a, b) => {
131-
const pathA = a.path || MATCH_ALL;
132-
const pathB = b.path || MATCH_ALL;
133-
134-
// This will sort paths at the very bottom of the list
135-
if (pathA === INDEX_PATH && pathB === MATCH_ALL) {
136-
return -1;
137-
}
138-
139-
// This will push * and / to the bottom of the list
140-
if (pathA === MATCH_ALL || pathA === INDEX_PATH) {
141-
return 1;
142-
}
143-
144-
// This will push * and / to the bottom of the list
145-
if ([MATCH_ALL, INDEX_PATH].includes(pathB)) {
146-
return -1;
147-
}
148-
149-
return 0;
150-
});
151-
}
152-
15369
private guardRouteExit(onRouteExit: OnRouteExit): void {
15470
if (this.unblock) {
15571
// Remove existing blocker before installing a new one.

0 commit comments

Comments
 (0)