Skip to content

Commit

Permalink
fix: correctly clone hoisted groups (#874)
Browse files Browse the repository at this point in the history
* fix: correctly clone hoisted groups

* fix trailing regex
  • Loading branch information
marklawlor authored Oct 23, 2023
1 parent abb3681 commit ed285f7
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,15 @@ describe(loadStaticParamsAsync, () => {
children: [
{
children: [],
contextKey: "./(app)/(index,about)/blog/[post].tsx",
contextKey: "./(app)/(index)/blog/[post].tsx",
dynamic: [{ deep: false, name: "post" }],
route: "(index,about)/blog/[post]",
route: "(index)/blog/[post]",
},
{
children: [],
contextKey: "./(app)/(about)/blog/[post].tsx",
dynamic: [{ deep: false, name: "post" }],
route: "(about)/blog/[post]",
},
],
contextKey: "./(app)/_layout.tsx",
Expand All @@ -319,21 +325,39 @@ describe(loadStaticParamsAsync, () => {
children: [
{
children: [],
contextKey: "./(app)/(index,about)/blog/[post].tsx",
contextKey: "./(app)/(index)/blog/[post].tsx",
dynamic: [{ deep: false, name: "post" }],
route: "(index)/blog/[post]",
},
{
children: [],
contextKey: "./(app)/(index)/blog/123.tsx",
dynamic: null,
route: "(index)/blog/123",
},
{
children: [],
contextKey: "./(app)/(index)/blog/abc.tsx",
dynamic: null,
route: "(index)/blog/abc",
},
{
children: [],
contextKey: "./(app)/(about)/blog/[post].tsx",
dynamic: [{ deep: false, name: "post" }],
route: "(index,about)/blog/[post]",
route: "(about)/blog/[post]",
},
{
children: [],
contextKey: "./(app)/(index,about)/blog/123.tsx",
contextKey: "./(app)/(about)/blog/123.tsx",
dynamic: null,
route: "(index,about)/blog/123",
route: "(about)/blog/123",
},
{
children: [],
contextKey: "./(app)/(index,about)/blog/abc.tsx",
contextKey: "./(app)/(about)/blog/abc.tsx",
dynamic: null,
route: "(index,about)/blog/abc",
route: "(about)/blog/abc",
},
],
contextKey: "./(app)/_layout.tsx",
Expand Down
11 changes: 11 additions & 0 deletions packages/expo-router/src/__tests__/matchers.test.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ describe(matchGroupName, () => {
expect(matchGroupName("[...foobar]")).toEqual(undefined);
expect(matchGroupName("[foobar]")).toEqual(undefined);
expect(matchGroupName("(foobar)")).toEqual("foobar");
expect(matchGroupName("(foo,bar)")).toEqual("foo,bar");
expect(matchGroupName("((foobar))")).toEqual("(foobar)");
expect(matchGroupName("(...foobar)")).toEqual("...foobar");
expect(matchGroupName("foobar")).toEqual(undefined);
expect(matchGroupName("leading/foobar")).toEqual(undefined);
expect(matchGroupName("leading/(foobar)")).toEqual("foobar");
expect(matchGroupName("leading/((foobar))")).toEqual("(foobar)");
expect(matchGroupName("leading/(...foobar)")).toEqual("...foobar");
expect(matchGroupName("leading/(foo,bar)")).toEqual("foo,bar");
expect(matchGroupName("leading/foobar/trailing")).toEqual(undefined);
expect(matchGroupName("leading/(foobar)/trailing")).toEqual("foobar");
expect(matchGroupName("leading/((foobar))/trailing")).toEqual("(foobar)");
expect(matchGroupName("leading/(...foobar)/trailing")).toEqual("...foobar");
expect(matchGroupName("leading/(foo,bar)/trailing)")).toEqual("foo,bar");
});
});
describe(matchDynamicName, () => {
Expand Down
33 changes: 33 additions & 0 deletions packages/expo-router/src/__tests__/navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,39 @@ it("does not loop infinitely when pushing a screen with empty options to an inva
expect(screen).toHavePathname("/main/welcome");
});

it("can navigate to hoisted groups", () => {
/** https://github.com/expo/router/issues/805 */

renderRouter({
index: () => <></>,
_layout: () => <Slot />,
"example/(a,b)/_layout": () => <Slot />,
"example/(a,b)/route": () => <Text testID="route" />,
});

expect(screen).toHavePathname("/");
act(() => router.push("/example/(a)/route"));

expect(screen).toHavePathname("/example/route");
expect(screen.getByTestId("route")).toBeTruthy();
});

it("can navigate to nested groups", () => {
renderRouter({
index: () => <></>,
_layout: () => <Slot />,
"example/(a,b)/_layout": () => <Slot />,
"example/(a,b)/folder/(c,d)/_layout": () => <Slot />,
"example/(a,b)/folder/(c,d)/route": () => <Text testID="route" />,
});

expect(screen).toHavePathname("/");
act(() => router.push("/example/(a)/folder/(d)/route"));

expect(screen).toHavePathname("/example/folder/route");
expect(screen.getByTestId("route")).toBeTruthy();
});

it("can push & replace with nested Slots", async () => {
renderRouter({
_layout: () => <Slot />,
Expand Down
83 changes: 46 additions & 37 deletions packages/expo-router/src/getRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,6 @@ function applyDefaultInitialRouteName(node: RouteNode): RouteNode {
};
}

function cloneGroupRoute(
node: RouteNode,
{ name: nextName }: { name: string }
): RouteNode {
const groupName = `(${nextName})`;
const parts = node.contextKey.split("/");
parts[parts.length - 2] = groupName;

return {
...node,
route: groupName,
contextKey: parts.join("/"),
};
}

function folderNodeToRouteNode({
name,
children,
Expand Down Expand Up @@ -212,25 +197,8 @@ function fileNodeToRouteNode(tree: TreeNode): RouteNode[] | null {

const dynamic = generateDynamic(name);

const groupName = matchGroupName(name);
const multiGroup = groupName?.includes(",");

const clones = multiGroup
? groupName!.split(",").map((v) => ({ name: v.trim() }))
: null;

// Assert duplicates:
if (clones) {
const names = new Set<string>();
for (const clone of clones) {
if (names.has(clone.name)) {
throw new Error(
`Array syntax cannot contain duplicate group name "${clone.name}" in "${node.contextKey}".`
);
}
names.add(clone.name);
}
}
const clones = extrapolateGroupRoutes(name, node.contextKey);
clones.delete(name);

const output = {
loadRoute: node.loadRoute,
Expand All @@ -240,9 +208,13 @@ function fileNodeToRouteNode(tree: TreeNode): RouteNode[] | null {
dynamic,
};

if (Array.isArray(clones)) {
return clones.map((clone) =>
applyDefaultInitialRouteName(cloneGroupRoute({ ...output }, clone))
if (clones.size) {
return [...clones].map((clone) =>
applyDefaultInitialRouteName({
...output,
contextKey: node.contextKey.replace(output.route, clone),
route: clone,
})
);
}

Expand All @@ -257,6 +229,43 @@ function fileNodeToRouteNode(tree: TreeNode): RouteNode[] | null {
];
}

function extrapolateGroupRoutes(
route: string,
contextKey: string,
routes: Set<string> = new Set()
): Set<string> {
const match = matchGroupName(route);

if (!match) {
routes.add(route);
return routes;
}

const groups = match?.split(",");
const groupsSet = new Set(groups);

if (groupsSet.size !== groups.length) {
throw new Error(
`Array syntax cannot contain duplicate group name "${groups}" in "${contextKey}".`
);
}

if (groups.length === 1) {
routes.add(route);
return routes;
}

for (const group of groups) {
extrapolateGroupRoutes(
route.replace(match, group.trim()),
contextKey,
routes
);
}

return routes;
}

function treeNodeToRouteNode(tree: TreeNode): RouteNode[] | null {
if (tree.node) {
return fileNodeToRouteNode(tree);
Expand Down
2 changes: 1 addition & 1 deletion packages/expo-router/src/matchers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function matchDeepDynamicRouteName(name: string): string | undefined {

/** Match `(page)` -> `page` */
export function matchGroupName(name: string): string | undefined {
return name.match(/^\(([^/]+?)\)$/)?.[1];
return name.match(/^(?:[^\\(\\)])*?\(([^\\/]+)\).*?$/)?.[1];
}

export function getNameFromFilePath(name: string): string {
Expand Down

0 comments on commit ed285f7

Please sign in to comment.
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy