Skip to content
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
54 changes: 54 additions & 0 deletions docs/router/api/file-based-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,34 @@ src/routes/posts.route.tsx -> /posts
src/routes/posts/route.tsx -> /posts
```

#### Using regex patterns for `routeToken`

You can use a regular expression pattern instead of a literal string to match multiple layout route naming conventions. This is useful when you want more flexibility in your file naming.

**In `tsr.config.json`** (JSON config), use an object with `regex` and optional `flags` properties:

```json
{
"routeToken": { "regex": "[a-z]+-layout", "flags": "i" }
}
```

**In code** (inline config), you can use a native `RegExp`:

```ts
{
routeToken: /[a-z]+-layout/i
}
```

With the regex pattern `[a-z]+-layout`, filenames like `dashboard.main-layout.tsx`, `posts.protected-layout.tsx`, or `admin.settings-layout.tsx` would all be recognized as layout routes.

> [!NOTE]
> The regex is matched against the **entire** final segment of the route path. For example, with `routeToken: { "regex": "[a-z]+-layout" }`:
>
> - `dashboard.main-layout.tsx` matches (`main-layout` is the full segment)
> - `dashboard.my-layout-extra.tsx` does NOT match (the segment is `my-layout-extra`, not just `my-layout`)

### `indexToken`

As mentioned in the Routing Concepts guide, an index route is a route that is matched when the URL path is exactly the same as the parent route. The `indexToken` is used to identify the index route file in the route directory.
Expand All @@ -114,6 +142,32 @@ src/routes/posts.index.tsx -> /posts/
src/routes/posts/index.tsx -> /posts/
```

#### Using regex patterns for `indexToken`

Similar to `routeToken`, you can use a regular expression pattern for `indexToken` to match multiple index route naming conventions.

**In `tsr.config.json`** (JSON config):

```json
{
"indexToken": { "regex": "[a-z]+-page" }
}
```

**In code** (inline config):

```ts
{
indexToken: /[a-z]+-page/
}
```

With the regex pattern `[a-z]+-page`, filenames like `home-page.tsx`, `posts.list-page.tsx`, or `dashboard.overview-page.tsx` would all be recognized as index routes.

#### Escaping regex tokens

When using regex tokens, you can still escape a segment to prevent it from being treated as a token by wrapping it in square brackets. For example, if your `indexToken` is `{ "regex": "[a-z]+-page" }` and you want a literal route segment called `home-page`, name your file `[home-page].tsx`.

### `quoteStyle`

When your generated route tree is generated and when you first create a new route, those files will be formatted with the quote style you specify here.
Expand Down
24 changes: 12 additions & 12 deletions docs/router/framework/react/routing/file-naming-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ title: File Naming Conventions

File-based routing requires that you follow a few simple file naming conventions to ensure that your routes are generated correctly. The concepts these conventions enable are covered in detail in the [Route Trees & Nesting](./route-trees.md) guide.

| Feature | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`__root.tsx`** | The root route file must be named `__root.tsx` and must be placed in the root of the configured `routesDirectory`. |
| **`.` Separator** | Routes can use the `.` character to denote a nested route. For example, `blog.post` will be generated as a child of `blog`. |
| **`$` Token** | Route segments with the `$` token are parameterized and will extract the value from the URL pathname as a route `param`. |
| **`_` Prefix** | Route segments with the `_` prefix are considered to be pathless layout routes and will not be used when matching its child routes against the URL pathname. |
| **`_` Suffix** | Route segments with the `_` suffix exclude the route from being nested under any parent routes. |
| **`-` Prefix** | Files and folders with the `-` prefix are excluded from the route tree. They will not be added to the `routeTree.gen.ts` file and can be used to colocate logic in route folders. |
| **`(folder)` folder name pattern** | A folder that matches this pattern is treated as a **route group**, preventing the folder from being included in the route's URL path. |
| **`[x]` Escaping** | Square brackets escape special characters in filenames that would otherwise have routing meaning. For example, `script[.]js.tsx` becomes `/script.js` and `api[.]v1.tsx` becomes `/api.v1`. |
| **`index` Token** | Route segments ending with the `index` token (before any file extensions) will match the parent route when the URL pathname matches the parent route exactly. This can be configured via the `indexToken` configuration option, see [options](../../../api/file-based-routing.md#indextoken). |
| **`.route.tsx` File Type** | When using directories to organise routes, the `route` suffix can be used to create a route file at the directory's path. For example, `blog.post.route.tsx` or `blog/post/route.tsx` can be used as the route file for the `/blog/post` route. This can be configured via the `routeToken` configuration option, see [options](../../../api/file-based-routing.md#routetoken). |
| Feature | Description |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`__root.tsx`** | The root route file must be named `__root.tsx` and must be placed in the root of the configured `routesDirectory`. |
| **`.` Separator** | Routes can use the `.` character to denote a nested route. For example, `blog.post` will be generated as a child of `blog`. |
| **`$` Token** | Route segments with the `$` token are parameterized and will extract the value from the URL pathname as a route `param`. |
| **`_` Prefix** | Route segments with the `_` prefix are considered to be pathless layout routes and will not be used when matching its child routes against the URL pathname. |
| **`_` Suffix** | Route segments with the `_` suffix exclude the route from being nested under any parent routes. |
| **`-` Prefix** | Files and folders with the `-` prefix are excluded from the route tree. They will not be added to the `routeTree.gen.ts` file and can be used to colocate logic in route folders. |
| **`(folder)` folder name pattern** | A folder that matches this pattern is treated as a **route group**, preventing the folder from being included in the route's URL path. |
| **`[x]` Escaping** | Square brackets escape special characters in filenames that would otherwise have routing meaning. For example, `script[.]js.tsx` becomes `/script.js` and `api[.]v1.tsx` becomes `/api.v1`. |
| **`index` Token** | Route segments ending with the `index` token (before any file extensions) will match the parent route when the URL pathname matches the parent route exactly. This can be configured via the `indexToken` configuration option (supports both strings and regex patterns), see [options](../../../api/file-based-routing.md#indextoken). |
| **`.route.tsx` File Type** | When using directories to organise routes, the `route` suffix can be used to create a route file at the directory's path. For example, `blog.post.route.tsx` or `blog/post/route.tsx` can be used as the route file for the `/blog/post` route. This can be configured via the `routeToken` configuration option (supports both strings and regex patterns), see [options](../../../api/file-based-routing.md#routetoken). |

> **💡 Remember:** The file-naming conventions for your project could be affected by what [options](../../../api/file-based-routing.md) are configured.

Expand Down
64 changes: 58 additions & 6 deletions packages/router-generator/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { z } from 'zod'
import { virtualRootRouteSchema } from './filesystem/virtual/config'
import type { GeneratorPlugin } from './plugin/types'

const tokenJsonRegexSchema = z.object({
regex: z.string(),
flags: z.string().optional(),
})

const tokenMatcherSchema = z.union([
z.string(),
z.instanceof(RegExp),
tokenJsonRegexSchema,
])

export type TokenMatcherJson = string | z.infer<typeof tokenJsonRegexSchema>

export type TokenMatcher = z.infer<typeof tokenMatcherSchema>

export const baseConfigSchema = z.object({
target: z.enum(['react', 'solid', 'vue']).optional().default('react'),
virtualRouteConfig: virtualRootRouteSchema.or(z.string()).optional(),
Expand All @@ -22,8 +37,8 @@ export const baseConfigSchema = z.object({
'// @ts-nocheck',
'// noinspection JSUnusedGlobalSymbols',
]),
indexToken: z.string().optional().default('index'),
routeToken: z.string().optional().default('route'),
indexToken: tokenMatcherSchema.optional().default('index'),
routeToken: tokenMatcherSchema.optional().default('route'),
pathParamsAllowedCharacters: z
.array(z.enum([';', ':', '@', '&', '=', '+', '$', ',']))
.optional(),
Expand Down Expand Up @@ -84,10 +99,16 @@ export function getConfig(
let config: Config

if (exists) {
config = configSchema.parse({
...JSON.parse(readFileSync(configFilePathJson, 'utf-8')),
// Parse file config (allows JSON regex-object form)
const fileConfigRaw = JSON.parse(readFileSync(configFilePathJson, 'utf-8'))

// Merge raw configs (inline overrides file), then parse once to apply defaults
// This ensures file config values aren't overwritten by inline defaults
const merged = {
...fileConfigRaw,
...inlineConfig,
})
}
config = configSchema.parse(merged)
} else {
config = configSchema.parse(inlineConfig)
}
Expand Down Expand Up @@ -160,7 +181,9 @@ ERROR: The "experimental.enableCodeSplitting" flag has been made stable and is n
throw new Error(message)
}

if (config.indexToken === config.routeToken) {
// Check that indexToken and routeToken are not identical
// Works for strings, RegExp, and JSON regex objects
if (areTokensEqual(config.indexToken, config.routeToken)) {
throw new Error(
`The "indexToken" and "routeToken" options must be different.`,
)
Expand All @@ -177,3 +200,32 @@ ERROR: The "experimental.enableCodeSplitting" flag has been made stable and is n

return config
}

/**
* Compares two token matchers for equality.
* Handles strings, RegExp instances, and JSON regex objects.
*/
function areTokensEqual(a: TokenMatcher, b: TokenMatcher): boolean {
// Both strings
if (typeof a === 'string' && typeof b === 'string') {
return a === b
}

// Both RegExp instances
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags
}

// Both JSON regex objects
if (
typeof a === 'object' &&
'regex' in a &&
typeof b === 'object' &&
'regex' in b
) {
return a.regex === b.regex && (a.flags ?? '') === (b.flags ?? '')
}

// Mixed types - not equal
return false
}
Loading
Loading