Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefix route config helper #12094

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/three-seals-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": minor
---

Add `prefix` route config helper to `@react-router/dev/routes`
36 changes: 33 additions & 3 deletions docs/start/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
route,
index,
layout,
prefix,
} from "@react-router/dev/routes";

export const routes: RouteConfig = [
Expand All @@ -48,7 +49,7 @@ export const routes: RouteConfig = [
route("register", "./auth/register.tsx"),
]),

route("concerts", [
...prefix("concerts", [
index("./concerts/home.tsx"),
route(":city", "./concerts/city.tsx"),
route("trending", "./concerts/trending.tsx"),
Expand Down Expand Up @@ -136,20 +137,21 @@ Every route in `routes.ts` is nested inside the special `app/root.tsx` module.

Using `layout`, layout routes create new nesting for their children, but they don't add any segments to the URL. It's like the root route but they can be added at any level.

```tsx filename=app/routes.ts lines=[9,15]
```tsx filename=app/routes.ts lines=[10,16]
import {
type RouteConfig,
route,
layout,
index,
prefix,
} from "@react-router/dev/routes";

export const routes: RouteConfig = [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
route("projects", [
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
Expand Down Expand Up @@ -187,6 +189,34 @@ export const routes: RouteConfig = [

Note that index routes can't have children.

## Route Prefixes

Using `prefix`, you can add a path prefix to a set of routes without needing to introduce a parent route file.

```tsx filename=app/routes.ts lines=[14]
import {
type RouteConfig,
route,
layout,
index,
prefix,
} from "@react-router/dev/routes";

export const routes: RouteConfig = [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
];
```

## Dynamic Segments

If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs.
Expand Down
161 changes: 159 additions & 2 deletions packages/react-router-dev/__tests__/route-config-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
route,
layout,
index,
prefix,
relative,
} from "../config/routes";

Expand All @@ -12,9 +13,9 @@ describe("route config", () => {
expect(
validateRouteConfig({
routeConfigFile: "routes.ts",
routeConfig: [
routeConfig: prefix("prefix", [
route("parent", "parent.tsx", [route("child", "child.tsx")]),
],
]),
}).valid
).toBe(true);
});
Expand Down Expand Up @@ -306,6 +307,157 @@ describe("route config", () => {
});
});

describe("prefix", () => {
it("adds a prefix to routes", () => {
expect(prefix("prefix", [route("route", "routes/route.tsx")]))
.toMatchInlineSnapshot(`
[
{
"children": undefined,
"file": "routes/route.tsx",
"path": "prefix/route",
},
]
`);
});

it("adds a prefix to routes with a blank path", () => {
expect(prefix("prefix", [route("", "routes/route.tsx")]))
.toMatchInlineSnapshot(`
[
{
"children": undefined,
"file": "routes/route.tsx",
"path": "prefix",
},
]
`);
});

it("adds a prefix with a trailing slash to routes", () => {
expect(prefix("prefix/", [route("route", "routes/route.tsx")]))
.toMatchInlineSnapshot(`
[
{
"children": undefined,
"file": "routes/route.tsx",
"path": "prefix/route",
},
]
`);
});

it("adds a prefix to routes with leading slash", () => {
expect(prefix("prefix", [route("/route", "routes/route.tsx")]))
.toMatchInlineSnapshot(`
[
{
"children": undefined,
"file": "routes/route.tsx",
"path": "prefix/route",
},
]
`);
});

it("adds a prefix with a trailing slash to routes with leading slash", () => {
expect(prefix("prefix/", [route("/route", "routes/route.tsx")]))
.toMatchInlineSnapshot(`
[
{
"children": undefined,
"file": "routes/route.tsx",
"path": "prefix/route",
},
]
`);
});

it("adds a prefix to index routes", () => {
expect(prefix("prefix", [index("routes/index.tsx")]))
.toMatchInlineSnapshot(`
[
{
"children": undefined,
"file": "routes/index.tsx",
"index": true,
"path": "prefix",
},
]
`);
});

it("adds a prefix to children of layout routes", () => {
expect(
prefix("prefix", [
layout("routes/layout.tsx", [route("route", "routes/route.tsx")]),
])
).toMatchInlineSnapshot(`
[
{
"children": [
{
"children": undefined,
"file": "routes/route.tsx",
"path": "prefix/route",
},
],
"file": "routes/layout.tsx",
},
]
`);
});

it("adds a prefix to children of nested layout routes", () => {
expect(
prefix("prefix", [
layout("routes/layout-1.tsx", [
route("layout-1-child", "routes/layout-1-child.tsx"),
layout("routes/layout-2.tsx", [
route("layout-2-child", "routes/layout-2-child.tsx"),
layout("routes/layout-3.tsx", [
route("layout-3-child", "routes/layout-3-child.tsx"),
]),
]),
]),
])
).toMatchInlineSnapshot(`
[
{
"children": [
{
"children": undefined,
"file": "routes/layout-1-child.tsx",
"path": "prefix/layout-1-child",
},
{
"children": [
{
"children": undefined,
"file": "routes/layout-2-child.tsx",
"path": "prefix/layout-2-child",
},
{
"children": [
{
"children": undefined,
"file": "routes/layout-3-child.tsx",
"path": "prefix/layout-3-child",
},
],
"file": "routes/layout-3.tsx",
},
],
"file": "routes/layout-2.tsx",
},
],
"file": "routes/layout-1.tsx",
},
]
`);
});
});

describe("relative", () => {
it("supports relative routes", () => {
let { route } = relative("/path/to/dirname");
Expand Down Expand Up @@ -368,6 +520,11 @@ describe("route config", () => {
}
`);
});

it("provides passthrough for non-relative APIs", () => {
let { prefix: relativePrefix } = relative("/path/to/dirname");
expect(relativePrefix).toBe(prefix);
});
});
});
});
67 changes: 46 additions & 21 deletions packages/react-router-dev/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,18 @@ type CreateRouteOptions = Pick<
* Helper function for creating a route config entry, for use within
* `routes.ts`.
*/
function createRoute(
function route(
path: string | null | undefined,
file: string,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function createRoute(
function route(
path: string | null | undefined,
file: string,
options: CreateRouteOptions,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function createRoute(
function route(
path: string | null | undefined,
file: string,
optionsOrChildren: CreateRouteOptions | RouteConfigEntry[] | undefined,
Expand Down Expand Up @@ -227,10 +227,7 @@ type CreateIndexOptions = Pick<
* Helper function for creating a route config entry for an index route, for use
* within `routes.ts`.
*/
function createIndex(
file: string,
options?: CreateIndexOptions
): RouteConfigEntry {
function index(file: string, options?: CreateIndexOptions): RouteConfigEntry {
return {
file,
index: true,
Expand All @@ -249,16 +246,13 @@ type CreateLayoutOptions = Pick<
* Helper function for creating a route config entry for a layout route, for use
* within `routes.ts`.
*/
function createLayout(
file: string,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function createLayout(
function layout(file: string, children?: RouteConfigEntry[]): RouteConfigEntry;
function layout(
file: string,
options: CreateLayoutOptions,
children?: RouteConfigEntry[]
): RouteConfigEntry;
function createLayout(
function layout(
file: string,
optionsOrChildren: CreateLayoutOptions | RouteConfigEntry[] | undefined,
children?: RouteConfigEntry[]
Expand All @@ -278,19 +272,39 @@ function createLayout(
};
}

export const route = createRoute;
export const index = createIndex;
export const layout = createLayout;
/**
* Helper function for adding a path prefix to a set of routes without needing
* to introduce a parent route file, for use within `routes.ts`.
*/
function prefix(
prefixPath: string,
routes: RouteConfigEntry[]
): RouteConfigEntry[] {
return routes.map((route) => {
if (route.index || typeof route.path === "string") {
return {
...route,
path: route.path ? joinRoutePaths(prefixPath, route.path) : prefixPath,
children: route.children,
};
} else if (route.children) {
return {
...route,
children: prefix(prefixPath, route.children),
};
}
return route;
});
}

const helpers = { route, index, layout, prefix };
export { route, index, layout, prefix };
/**
* Creates a set of route config helpers that resolve file paths relative to the
* given directory, for use within `routes.ts`. This is designed to support
* splitting route config into multiple files within different directories.
*/
export function relative(directory: string): {
route: typeof route;
index: typeof index;
layout: typeof layout;
} {
export function relative(directory: string): typeof helpers {
return {
/**
* Helper function for creating a route config entry, for use within
Expand Down Expand Up @@ -319,6 +333,10 @@ export function relative(directory: string): {
layout: (file, ...rest) => {
return layout(resolve(directory, file), ...(rest as any));
},

// Passthrough of helper functions that don't need relative scoping so that
// a complete API is still provided.
prefix,
};
}

Expand Down Expand Up @@ -371,3 +389,10 @@ function normalizeSlashes(file: string) {
function stripFileExtension(file: string) {
return file.replace(/\.[a-z0-9]+$/i, "");
}

function joinRoutePaths(path1: string, path2: string): string {
return [
path1.replace(/\/+$/, ""), // Remove trailing slashes
path2.replace(/^\/+/, ""), // Remove leading slashes
].join("/");
}
Loading
Loading