diff --git a/docs/router/api/file-based-routing.md b/docs/router/api/file-based-routing.md index 55a37a8c670..7de532e72fe 100644 --- a/docs/router/api/file-based-routing.md +++ b/docs/router/api/file-based-routing.md @@ -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. @@ -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. diff --git a/docs/router/framework/react/routing/file-naming-conventions.md b/docs/router/framework/react/routing/file-naming-conventions.md index 9ea52459783..2425e694a4e 100644 --- a/docs/router/framework/react/routing/file-naming-conventions.md +++ b/docs/router/framework/react/routing/file-naming-conventions.md @@ -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. diff --git a/packages/router-generator/src/config.ts b/packages/router-generator/src/config.ts index 0d1ae473391..e0f3da893c6 100644 --- a/packages/router-generator/src/config.ts +++ b/packages/router-generator/src/config.ts @@ -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 + +export type TokenMatcher = z.infer + export const baseConfigSchema = z.object({ target: z.enum(['react', 'solid', 'vue']).optional().default('react'), virtualRouteConfig: virtualRootRouteSchema.or(z.string()).optional(), @@ -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(), @@ -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) } @@ -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.`, ) @@ -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 +} diff --git a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts index 05ed695a960..eecd53b534b 100644 --- a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts +++ b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts @@ -6,6 +6,7 @@ import { removeExt, replaceBackslash, routePathToVariable, + unwrapBracketWrappedSegment, } from '../../utils' import { getRouteNodes as getRouteNodesVirtual } from '../virtual/getRouteNodes' import { loadConfigFile } from '../virtual/loadConfigFile' @@ -18,6 +19,16 @@ import type { import type { FsRouteType, GetRouteNodesResult, RouteNode } from '../../types' import type { Config } from '../../config' +/** + * Pre-compiled segment regexes for matching token patterns against route segments. + * These are created once (in Generator constructor) and passed through to avoid + * repeated regex compilation during route crawling. + */ +export interface TokenRegexBundle { + indexTokenSegmentRegex: RegExp + routeTokenSegmentRegex: RegExp +} + const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx|vue)/ const virtualConfigFileRegExp = /__virtual\.[mc]?[jt]s$/ @@ -37,6 +48,7 @@ export async function getRouteNodes( | 'indexToken' >, root: string, + tokenRegexes: TokenRegexBundle, ): Promise { const { routeFilePrefix, routeFileIgnorePrefix, routeFileIgnorePattern } = config @@ -104,6 +116,7 @@ export async function getRouteNodes( virtualRouteConfig: dummyRoot, }, root, + tokenRegexes, ) allPhysicalDirectories.push(...physicalDirectories) virtualRouteNodes.forEach((node) => { @@ -158,7 +171,7 @@ export async function getRouteNodes( throw new Error(errorMessage) } - const meta = getRouteMeta(routePath, originalRoutePath, config) + const meta = getRouteMeta(routePath, originalRoutePath, tokenRegexes) const variableName = meta.variableName let routeType: FsRouteType = meta.fsRouteType @@ -174,7 +187,7 @@ export async function getRouteNodes( routePath, originalRoutePath, routeType, - config, + tokenRegexes, ) ) { routeType = 'pathless_layout' @@ -206,6 +219,9 @@ export async function getRouteNodes( const lastOriginalSegmentForSuffix = originalSegments[originalSegments.length - 1] || '' + const { routeTokenSegmentRegex, indexTokenSegmentRegex } = + tokenRegexes + // List of special suffixes that can be escaped const specialSuffixes = [ 'component', @@ -213,55 +229,87 @@ export async function getRouteNodes( 'notFoundComponent', 'pendingComponent', 'loader', - config.routeToken, 'lazy', ] - // Only strip the suffix if it wasn't escaped (not wrapped in brackets) + const routePathSegments = routePath.split('/').filter(Boolean) + const lastRouteSegment = + routePathSegments[routePathSegments.length - 1] || '' + const suffixToStrip = specialSuffixes.find((suffix) => { const endsWithSuffix = routePath.endsWith(`/${suffix}`) - const isEscaped = lastOriginalSegmentForSuffix === `[${suffix}]` + // A suffix is escaped if wrapped in brackets in the original: [lazy] means literal "lazy" + const isEscaped = + lastOriginalSegmentForSuffix.startsWith('[') && + lastOriginalSegmentForSuffix.endsWith(']') && + unwrapBracketWrappedSegment(lastOriginalSegmentForSuffix) === + suffix return endsWithSuffix && !isEscaped }) - if (suffixToStrip) { - routePath = routePath.replace(new RegExp(`/${suffixToStrip}$`), '') + const routeTokenCandidate = unwrapBracketWrappedSegment( + lastOriginalSegmentForSuffix, + ) + const isRouteTokenEscaped = + lastOriginalSegmentForSuffix !== routeTokenCandidate && + routeTokenSegmentRegex.test(routeTokenCandidate) + + const shouldStripRouteToken = + routeTokenSegmentRegex.test(lastRouteSegment) && + !isRouteTokenEscaped + + if (suffixToStrip || shouldStripRouteToken) { + const stripSegment = suffixToStrip ?? lastRouteSegment + routePath = routePath.replace(new RegExp(`/${stripSegment}$`), '') originalRoutePath = originalRoutePath.replace( - new RegExp(`/${suffixToStrip}$`), + new RegExp(`/${stripSegment}$`), '', ) } // Check if the index token should be treated specially or as a literal path - // If it's escaped (wrapped in brackets in originalRoutePath), it should be literal + // Escaping stays literal-only: if the last original segment is bracket-wrapped, + // treat it as literal even if it matches the token regex. const lastOriginalSegment = originalRoutePath.split('/').filter(Boolean).pop() || '' + + const indexTokenCandidate = + unwrapBracketWrappedSegment(lastOriginalSegment) const isIndexEscaped = - lastOriginalSegment === `[${config.indexToken}]` + lastOriginalSegment !== indexTokenCandidate && + indexTokenSegmentRegex.test(indexTokenCandidate) if (!isIndexEscaped) { - if (routePath === config.indexToken) { - routePath = '/' - } + const updatedRouteSegments = routePath.split('/').filter(Boolean) + const updatedLastRouteSegment = + updatedRouteSegments[updatedRouteSegments.length - 1] || '' - if (originalRoutePath === config.indexToken) { - originalRoutePath = '/' - } - - // For layout routes, don't use '/' fallback - an empty path means - // "layout for the parent path" which is important for physical() mounts - // where route.tsx at root should have empty path, not '/' - const isLayoutRoute = routeType === 'layout' + if (indexTokenSegmentRegex.test(updatedLastRouteSegment)) { + if (routePathSegments.length === 1) { + routePath = '/' + } - routePath = - routePath.replace(new RegExp(`/${config.indexToken}$`), '/') || - (isLayoutRoute ? '' : '/') + if (lastOriginalSegment === updatedLastRouteSegment) { + originalRoutePath = '/' + } - originalRoutePath = - originalRoutePath.replace( - new RegExp(`/${config.indexToken}$`), - '/', - ) || (isLayoutRoute ? '' : '/') + // For layout routes, don't use '/' fallback - an empty path means + // "layout for the parent path" which is important for physical() mounts + // where route.tsx at root should have empty path, not '/' + const isLayoutRoute = routeType === 'layout' + + routePath = + routePath.replace( + new RegExp(`/${updatedLastRouteSegment}$`), + '/', + ) || (isLayoutRoute ? '' : '/') + + originalRoutePath = + originalRoutePath.replace( + new RegExp(`/${indexTokenCandidate}$`), + '/', + ) || (isLayoutRoute ? '' : '/') + } } routeNodes.push({ @@ -312,13 +360,13 @@ export async function getRouteNodes( * * @param routePath - The determined initial routePath (with brackets removed). * @param originalRoutePath - The original route path (may contain brackets for escaped content). - * @param config - The user configuration object. + * @param tokenRegexes - Pre-compiled token regexes for matching. * @returns An object containing the type of the route and the variable name derived from the route path. */ export function getRouteMeta( routePath: string, originalRoutePath: string, - config: Pick, + tokenRegexes: TokenRegexBundle, ): { // `__root` is can be more easily determined by filtering down to routePath === /${rootPathId} // `pathless` is needs to determined after `lazy` has been cleaned up from the routePath @@ -343,15 +391,27 @@ export function getRouteMeta( const lastOriginalSegment = originalSegments[originalSegments.length - 1] || '' - // Helper to check if a specific suffix is escaped + const { routeTokenSegmentRegex } = tokenRegexes + + // Helper to check if a specific suffix is escaped (literal-only) + // A suffix is escaped if the original segment is wrapped in brackets: [lazy] means literal "lazy" const isSuffixEscaped = (suffix: string): boolean => { - return lastOriginalSegment === `[${suffix}]` + return ( + lastOriginalSegment.startsWith('[') && + lastOriginalSegment.endsWith(']') && + unwrapBracketWrappedSegment(lastOriginalSegment) === suffix + ) } - if ( - routePath.endsWith(`/${config.routeToken}`) && - !isSuffixEscaped(config.routeToken) - ) { + const routeSegments = routePath.split('/').filter(Boolean) + const lastRouteSegment = routeSegments[routeSegments.length - 1] || '' + + const routeTokenCandidate = unwrapBracketWrappedSegment(lastOriginalSegment) + const isRouteTokenEscaped = + lastOriginalSegment !== routeTokenCandidate && + routeTokenSegmentRegex.test(routeTokenCandidate) + + if (routeTokenSegmentRegex.test(lastRouteSegment) && !isRouteTokenEscaped) { // layout routes, i.e `/foo/route.tsx` or `/foo/_layout/route.tsx` fsRouteType = 'layout' } else if (routePath.endsWith('/lazy') && !isSuffixEscaped('lazy')) { @@ -396,14 +456,14 @@ export function getRouteMeta( * @param normalizedRoutePath Normalized route path, i.e `/foo/_layout/route.tsx` and `/foo._layout.route.tsx` to `/foo/_layout/route` * @param originalRoutePath Original route path with brackets for escaped content * @param routeType The route type determined from file extension - * @param config The `router-generator` configuration object + * @param tokenRegexes Pre-compiled token regexes for matching * @returns Boolean indicating if the route is a pathless layout route */ function isValidPathlessLayoutRoute( normalizedRoutePath: string, originalRoutePath: string, routeType: FsRouteType, - config: Pick, + tokenRegexes: TokenRegexBundle, ): boolean { if (routeType === 'lazy') { return false @@ -428,12 +488,14 @@ function isValidPathlessLayoutRoute( return false } - // If segment === config.routeToken and secondToLastSegment is a string that starts with _, then exit as true + const { routeTokenSegmentRegex, indexTokenSegmentRegex } = tokenRegexes + + // If segment matches routeToken and secondToLastSegment is a string that starts with _, then exit as true // Since the route is actually a configuration route for a layout/pathless route // i.e. /foo/_layout/route.tsx === /foo/_layout.tsx // But if the underscore is escaped, it's not a pathless layout if ( - lastRouteSegment === config.routeToken && + routeTokenSegmentRegex.test(lastRouteSegment) && typeof secondToLastRouteSegment === 'string' && typeof secondToLastOriginalSegment === 'string' ) { @@ -451,8 +513,8 @@ function isValidPathlessLayoutRoute( } return ( - lastRouteSegment !== config.indexToken && - lastRouteSegment !== config.routeToken && + !indexTokenSegmentRegex.test(lastRouteSegment) && + !routeTokenSegmentRegex.test(lastRouteSegment) && lastRouteSegment.startsWith('_') ) } diff --git a/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts b/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts index 920e066ab1c..c7d2db03076 100644 --- a/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts +++ b/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts @@ -17,6 +17,7 @@ import type { } from '@tanstack/virtual-file-routes' import type { GetRouteNodesResult, RouteNode } from '../../types' import type { Config } from '../../config' +import type { TokenRegexBundle } from '../physical/getRouteNodes' function ensureLeadingUnderScore(id: string) { if (id.startsWith('_')) { @@ -49,6 +50,7 @@ export async function getRouteNodes( | 'routeToken' >, root: string, + tokenRegexes: TokenRegexBundle, ): Promise { const fullDir = resolve(tsrConfig.routesDirectory) if (tsrConfig.virtualRouteConfig === undefined) { @@ -68,6 +70,7 @@ export async function getRouteNodes( root, fullDir, virtualRouteConfig.children, + tokenRegexes, ) const allNodes = flattenTree({ children, @@ -134,7 +137,8 @@ export async function getRouteNodesRecursive( >, root: string, fullDir: string, - nodes?: Array, + nodes: Array | undefined, + tokenRegexes: TokenRegexBundle, parent?: RouteNode, ): Promise<{ children: Array; physicalDirectories: Array }> { if (nodes === undefined) { @@ -150,8 +154,12 @@ export async function getRouteNodesRecursive( routesDirectory: resolve(fullDir, node.directory), }, root, + tokenRegexes, + ) + allPhysicalDirectories.push( + resolve(fullDir, node.directory), + ...physicalDirectories, ) - allPhysicalDirectories.push(node.directory) routeNodes.forEach((subtreeNode) => { subtreeNode.variableName = routePathToVariable( `${node.pathPrefix}/${removeExt(subtreeNode.filePath)}`, @@ -229,6 +237,7 @@ export async function getRouteNodesRecursive( root, fullDir, node.children, + tokenRegexes, routeNode, ) routeNode.children = children @@ -275,6 +284,7 @@ export async function getRouteNodesRecursive( root, fullDir, node.children, + tokenRegexes, routeNode, ) routeNode.children = children diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts index 01f485581f8..e7009eb8369 100644 --- a/packages/router-generator/src/generator.ts +++ b/packages/router-generator/src/generator.ts @@ -20,6 +20,7 @@ import { createRouteNodesByFullPath, createRouteNodesById, createRouteNodesByTo, + createTokenRegex, determineNodePath, findParent, format, @@ -191,8 +192,26 @@ export class Generator { private static routeGroupPatternRegex = /\(.+\)/ private physicalDirectories: Array = [] - private indexTokenRegex: RegExp - private routeTokenRegex: RegExp + /** + * Token regexes are pre-compiled once here and reused throughout route processing. + * We need TWO types of regex for each token because they match against different inputs: + * + * 1. FILENAME regexes: Match token patterns within full file path strings. + * Example: For file "routes/dashboard.index.tsx", we want to detect ".index." + * Pattern: `[./](?:token)[.]` - matches token bounded by path separators/dots + * Used in: sorting route nodes by file path + * + * 2. SEGMENT regexes: Match token against a single logical route segment. + * Example: For segment "index" (extracted from path), match the whole segment + * Pattern: `^(?:token)$` - matches entire segment exactly + * Used in: route parsing, determining route types, escape detection + * + * We cannot reuse one for the other without false positives or missing matches. + */ + private indexTokenFilenameRegex: RegExp + private routeTokenFilenameRegex: RegExp + private indexTokenSegmentRegex: RegExp + private routeTokenSegmentRegex: RegExp private static componentPieceRegex = /[./](component|errorComponent|notFoundComponent|pendingComponent|loader|lazy)[.]/ @@ -207,8 +226,19 @@ export class Generator { this.routesDirectoryPath = this.getRoutesDirectoryPath() this.plugins.push(...(opts.config.plugins || [])) - this.indexTokenRegex = new RegExp(`[./]${this.config.indexToken}[.]`) - this.routeTokenRegex = new RegExp(`[./]${this.config.routeToken}[.]`) + // Create all token regexes once in constructor + this.indexTokenFilenameRegex = createTokenRegex(this.config.indexToken, { + type: 'filename', + }) + this.routeTokenFilenameRegex = createTokenRegex(this.config.routeToken, { + type: 'filename', + }) + this.indexTokenSegmentRegex = createTokenRegex(this.config.indexToken, { + type: 'segment', + }) + this.routeTokenSegmentRegex = createTokenRegex(this.config.routeToken, { + type: 'segment', + }) for (const plugin of this.plugins) { plugin.init?.({ generator: this }) @@ -330,9 +360,19 @@ export class Generator { let getRouteNodesResult: GetRouteNodesResult if (this.config.virtualRouteConfig) { - getRouteNodesResult = await virtualGetRouteNodes(this.config, this.root) + getRouteNodesResult = await virtualGetRouteNodes(this.config, this.root, { + indexTokenSegmentRegex: this.indexTokenSegmentRegex, + routeTokenSegmentRegex: this.routeTokenSegmentRegex, + }) } else { - getRouteNodesResult = await physicalGetRouteNodes(this.config, this.root) + getRouteNodesResult = await physicalGetRouteNodes( + this.config, + this.root, + { + indexTokenSegmentRegex: this.indexTokenSegmentRegex, + routeTokenSegmentRegex: this.routeTokenSegmentRegex, + }, + ) } const { @@ -354,9 +394,9 @@ export class Generator { const preRouteNodes = multiSortBy(beforeRouteNodes, [ (d) => (d.routePath === '/' ? -1 : 1), (d) => d.routePath?.split('/').length, - (d) => (d.filePath.match(this.indexTokenRegex) ? 1 : -1), + (d) => (d.filePath.match(this.indexTokenFilenameRegex) ? 1 : -1), (d) => (d.filePath.match(Generator.componentPieceRegex) ? 1 : -1), - (d) => (d.filePath.match(this.routeTokenRegex) ? -1 : 1), + (d) => (d.filePath.match(this.routeTokenFilenameRegex) ? -1 : 1), (d) => (d.routePath?.endsWith('/') ? -1 : 1), (d) => d.routePath, ]).filter((d) => { @@ -541,10 +581,20 @@ export class Generator { const { rootRouteNode, acc } = opts + // Use pre-compiled regex if config hasn't been overridden, otherwise create new one + const indexTokenSegmentRegex = + config.indexToken === this.config.indexToken + ? this.indexTokenSegmentRegex + : createTokenRegex(config.indexToken, { type: 'segment' }) + const sortedRouteNodes = multiSortBy(acc.routeNodes, [ (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1), (d) => d.routePath?.split('/').length, - (d) => (d.routePath?.endsWith(config.indexToken) ? -1 : 1), + (d) => { + const segments = d.routePath?.split('/').filter(Boolean) ?? [] + const last = segments[segments.length - 1] ?? '' + return indexTokenSegmentRegex.test(last) ? -1 : 1 + }, (d) => d, ]) diff --git a/packages/router-generator/src/utils.ts b/packages/router-generator/src/utils.ts index 1f00d379995..366a4e84f6f 100644 --- a/packages/router-generator/src/utils.ts +++ b/packages/router-generator/src/utils.ts @@ -3,7 +3,7 @@ import * as fsp from 'node:fs/promises' import path from 'node:path' import * as prettier from 'prettier' import { rootPathId } from './filesystem/physical/rootPathId' -import type { Config } from './config' +import type { Config, TokenMatcher } from './config' import type { ImportDeclaration, RouteNode } from './types' /** @@ -418,6 +418,78 @@ function escapeRegExp(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } +function sanitizeTokenFlags(flags?: string): string | undefined { + if (!flags) return flags + + // Prevent stateful behavior with RegExp.prototype.test/exec + // g = global, y = sticky + return flags.replace(/[gy]/g, '') +} + +export function createTokenRegex( + token: TokenMatcher, + opts: { + type: 'segment' | 'filename' + }, +): RegExp { + // Defensive check: if token is undefined/null, throw a clear error + // (runtime safety for config loading edge cases) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (token === undefined || token === null) { + throw new Error( + `createTokenRegex: token is ${token}. This usually means the config was not properly parsed with defaults.`, + ) + } + + try { + if (typeof token === 'string') { + return opts.type === 'segment' + ? new RegExp(`^${escapeRegExp(token)}$`) + : new RegExp(`[./]${escapeRegExp(token)}[.]`) + } + + if (token instanceof RegExp) { + const flags = sanitizeTokenFlags(token.flags) + return opts.type === 'segment' + ? new RegExp(`^(?:${token.source})$`, flags) + : new RegExp(`[./](?:${token.source})[.]`, flags) + } + + // Handle JSON regex object form: { regex: string, flags?: string } + if (typeof token === 'object' && 'regex' in token) { + const flags = sanitizeTokenFlags(token.flags) + return opts.type === 'segment' + ? new RegExp(`^(?:${token.regex})$`, flags) + : new RegExp(`[./](?:${token.regex})[.]`, flags) + } + + throw new Error( + `createTokenRegex: invalid token type. Expected string, RegExp, or { regex, flags } object, got: ${typeof token}`, + ) + } catch (e) { + if (e instanceof SyntaxError) { + const pattern = + typeof token === 'string' + ? token + : token instanceof RegExp + ? token.source + : token.regex + throw new Error( + `Invalid regex pattern in token config: "${pattern}". ${e.message}`, + ) + } + throw e + } +} + +export function isBracketWrappedSegment(segment: string): boolean { + return segment.startsWith('[') && segment.endsWith(']') +} + +export function unwrapBracketWrappedSegment(segment: string): string { + return isBracketWrappedSegment(segment) ? segment.slice(1, -1) : segment +} + export function removeLeadingUnderscores(s: string, routeToken: string) { if (!s) return s diff --git a/packages/router-generator/tests/generator.test.ts b/packages/router-generator/tests/generator.test.ts index 40c66e30ed4..1fd787cda1e 100644 --- a/packages/router-generator/tests/generator.test.ts +++ b/packages/router-generator/tests/generator.test.ts @@ -1,5 +1,6 @@ +import { existsSync } from 'node:fs' import fs from 'node:fs/promises' -import { dirname, join, relative } from 'node:path' +import path, { dirname, join, relative } from 'node:path' import { describe, expect, it } from 'vitest' import { @@ -9,7 +10,7 @@ import { rootRoute, route, } from '@tanstack/virtual-file-routes' -import { Generator, getConfig } from '../src' +import { Generator, getConfig, virtualGetRouteNodes } from '../src' import type { Config } from '../src' function makeFolderDir(folder: string) { @@ -47,12 +48,18 @@ function setupConfig( const { generatedRouteTree = `/routeTree.gen.ts`, ...rest } = inlineConfig const dir = makeFolderDir(folder) - const config = getConfig({ - disableLogging: true, - routesDirectory: dir + '/routes', - generatedRouteTree: dir + generatedRouteTree, - ...rest, - }) + const configFilePath = join(dir, 'tsr.config.json') + const configDirectory = existsSync(configFilePath) ? dir : undefined + + const config = getConfig( + { + disableLogging: true, + routesDirectory: dir + '/routes', + generatedRouteTree: dir + generatedRouteTree, + ...rest, + }, + configDirectory, + ) return config } @@ -98,24 +105,6 @@ function rewriteConfigByFolderName(folderName: string, config: Config) { config.virtualRouteConfig = virtualRouteConfig } break - case 'virtual-config-file-named-export': - config.virtualRouteConfig = './routes.ts' - break - case 'virtual-config-file-default-export': - config.virtualRouteConfig = './routes.ts' - break - case 'virtual-physical-empty-path-merge': - config.virtualRouteConfig = './routes.ts' - break - case 'virtual-physical-empty-path-conflict-root': - config.virtualRouteConfig = './routes.ts' - break - case 'virtual-physical-empty-path-conflict-virtual': - config.virtualRouteConfig = './routes.ts' - break - case 'virtual-physical-no-prefix': - config.virtualRouteConfig = './routes.ts' - break case 'virtual-with-escaped-underscore': { // Test case for escaped underscores in physical routes mounted via virtual config @@ -169,6 +158,13 @@ function rewriteConfigByFolderName(folderName: string, config: Config) { config.routeFileIgnorePattern = 'ignoredPattern' config.routeFilePrefix = 'r&' break + case 'regex-tokens-inline': + // Test inline config with RegExp tokens + // indexToken matches patterns like "index-page", "home-page" + // routeToken matches patterns like "main-layout", "protected-layout" + config.indexToken = /[a-z]+-page/ + config.routeToken = /[a-z]+-layout/ + break default: break } @@ -302,6 +298,22 @@ describe('generator works', async () => { }, ) + it('physical() mount returns absolute physicalDirectories', async () => { + const folderName = 'virtual-physical-no-prefix' + const dir = makeFolderDir(folderName) + const config = await setupConfig(folderName) + + const { physicalDirectories } = await virtualGetRouteNodes(config, dir, { + indexTokenSegmentRegex: /^(?:index)$/, + routeTokenSegmentRegex: /^(?:route)$/, + }) + + expect(physicalDirectories.length).toBeGreaterThan(0) + physicalDirectories.forEach((physicalDir) => { + expect(path.isAbsolute(physicalDir)).toBe(true) + }) + }) + it.each(folderNames)( 'should create directory for routeTree if it does not exist', async () => { diff --git a/packages/router-generator/tests/generator/regex-tokens-inline/routeTree.snapshot.ts b/packages/router-generator/tests/generator/regex-tokens-inline/routeTree.snapshot.ts new file mode 100644 index 00000000000..3909bbe9f48 --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-inline/routeTree.snapshot.ts @@ -0,0 +1,121 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DashboardMainLayoutRouteImport } from './routes/dashboard.main-layout' +import { Route as IndexPageRouteImport } from './routes/index-page' +import { Route as DashboardHomePageRouteImport } from './routes/dashboard.home-page' +import { Route as DashboardSettingsRouteImport } from './routes/dashboard.settings' + +const DashboardMainLayoutRoute = DashboardMainLayoutRouteImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => rootRouteImport, +} as any) +const IndexPageRoute = IndexPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardHomePageRoute = DashboardHomePageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => DashboardMainLayoutRoute, +} as any) +const DashboardSettingsRoute = DashboardSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => DashboardMainLayoutRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexPageRoute + '/dashboard': typeof DashboardMainLayoutRouteWithChildren + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/': typeof DashboardHomePageRoute +} +export interface FileRoutesByTo { + '/': typeof IndexPageRoute + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard': typeof DashboardHomePageRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexPageRoute + '/dashboard': typeof DashboardMainLayoutRouteWithChildren + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/': typeof DashboardHomePageRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/dashboard' | '/dashboard/settings' | '/dashboard/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/dashboard/settings' | '/dashboard' + id: '__root__' | '/' | '/dashboard' | '/dashboard/settings' | '/dashboard/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexPageRoute: typeof IndexPageRoute + DashboardMainLayoutRoute: typeof DashboardMainLayoutRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof DashboardMainLayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexPageRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard/': { + id: '/dashboard/' + path: '/' + fullPath: '/dashboard/' + preLoaderRoute: typeof DashboardHomePageRouteImport + parentRoute: typeof DashboardMainLayoutRoute + } + '/dashboard/settings': { + id: '/dashboard/settings' + path: '/settings' + fullPath: '/dashboard/settings' + preLoaderRoute: typeof DashboardSettingsRouteImport + parentRoute: typeof DashboardMainLayoutRoute + } + } +} + +interface DashboardMainLayoutRouteChildren { + DashboardSettingsRoute: typeof DashboardSettingsRoute + DashboardHomePageRoute: typeof DashboardHomePageRoute +} + +const DashboardMainLayoutRouteChildren: DashboardMainLayoutRouteChildren = { + DashboardSettingsRoute: DashboardSettingsRoute, + DashboardHomePageRoute: DashboardHomePageRoute, +} + +const DashboardMainLayoutRouteWithChildren = + DashboardMainLayoutRoute._addFileChildren(DashboardMainLayoutRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexPageRoute: IndexPageRoute, + DashboardMainLayoutRoute: DashboardMainLayoutRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/packages/router-generator/tests/generator/regex-tokens-inline/routes/__root.tsx b/packages/router-generator/tests/generator/regex-tokens-inline/routes/__root.tsx new file mode 100644 index 00000000000..742d24a4ea3 --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-inline/routes/__root.tsx @@ -0,0 +1,5 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: () => , +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.home-page.tsx b/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.home-page.tsx new file mode 100644 index 00000000000..71d13d851a7 --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.home-page.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard/')({ + component: () =>
Dashboard Home
, +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.main-layout.tsx b/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.main-layout.tsx new file mode 100644 index 00000000000..5d03a657c6d --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.main-layout.tsx @@ -0,0 +1,10 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard')({ + component: () => ( +
+

Dashboard Layout

+ +
+ ), +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.settings.tsx b/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.settings.tsx new file mode 100644 index 00000000000..c05ef65adeb --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-inline/routes/dashboard.settings.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard/settings')({ + component: () =>
Dashboard Settings
, +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-inline/routes/index-page.tsx b/packages/router-generator/tests/generator/regex-tokens-inline/routes/index-page.tsx new file mode 100644 index 00000000000..2f2e105077b --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-inline/routes/index-page.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: () =>
Home Page
, +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-json/routeTree.snapshot.ts b/packages/router-generator/tests/generator/regex-tokens-json/routeTree.snapshot.ts new file mode 100644 index 00000000000..3909bbe9f48 --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-json/routeTree.snapshot.ts @@ -0,0 +1,121 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DashboardMainLayoutRouteImport } from './routes/dashboard.main-layout' +import { Route as IndexPageRouteImport } from './routes/index-page' +import { Route as DashboardHomePageRouteImport } from './routes/dashboard.home-page' +import { Route as DashboardSettingsRouteImport } from './routes/dashboard.settings' + +const DashboardMainLayoutRoute = DashboardMainLayoutRouteImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => rootRouteImport, +} as any) +const IndexPageRoute = IndexPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardHomePageRoute = DashboardHomePageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => DashboardMainLayoutRoute, +} as any) +const DashboardSettingsRoute = DashboardSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => DashboardMainLayoutRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexPageRoute + '/dashboard': typeof DashboardMainLayoutRouteWithChildren + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/': typeof DashboardHomePageRoute +} +export interface FileRoutesByTo { + '/': typeof IndexPageRoute + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard': typeof DashboardHomePageRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexPageRoute + '/dashboard': typeof DashboardMainLayoutRouteWithChildren + '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/': typeof DashboardHomePageRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/dashboard' | '/dashboard/settings' | '/dashboard/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/dashboard/settings' | '/dashboard' + id: '__root__' | '/' | '/dashboard' | '/dashboard/settings' | '/dashboard/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexPageRoute: typeof IndexPageRoute + DashboardMainLayoutRoute: typeof DashboardMainLayoutRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof DashboardMainLayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexPageRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard/': { + id: '/dashboard/' + path: '/' + fullPath: '/dashboard/' + preLoaderRoute: typeof DashboardHomePageRouteImport + parentRoute: typeof DashboardMainLayoutRoute + } + '/dashboard/settings': { + id: '/dashboard/settings' + path: '/settings' + fullPath: '/dashboard/settings' + preLoaderRoute: typeof DashboardSettingsRouteImport + parentRoute: typeof DashboardMainLayoutRoute + } + } +} + +interface DashboardMainLayoutRouteChildren { + DashboardSettingsRoute: typeof DashboardSettingsRoute + DashboardHomePageRoute: typeof DashboardHomePageRoute +} + +const DashboardMainLayoutRouteChildren: DashboardMainLayoutRouteChildren = { + DashboardSettingsRoute: DashboardSettingsRoute, + DashboardHomePageRoute: DashboardHomePageRoute, +} + +const DashboardMainLayoutRouteWithChildren = + DashboardMainLayoutRoute._addFileChildren(DashboardMainLayoutRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexPageRoute: IndexPageRoute, + DashboardMainLayoutRoute: DashboardMainLayoutRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/packages/router-generator/tests/generator/regex-tokens-json/routes/__root.tsx b/packages/router-generator/tests/generator/regex-tokens-json/routes/__root.tsx new file mode 100644 index 00000000000..742d24a4ea3 --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-json/routes/__root.tsx @@ -0,0 +1,5 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: () => , +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.home-page.tsx b/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.home-page.tsx new file mode 100644 index 00000000000..71d13d851a7 --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.home-page.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard/')({ + component: () =>
Dashboard Home
, +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.main-layout.tsx b/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.main-layout.tsx new file mode 100644 index 00000000000..5d03a657c6d --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.main-layout.tsx @@ -0,0 +1,10 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard')({ + component: () => ( +
+

Dashboard Layout

+ +
+ ), +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.settings.tsx b/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.settings.tsx new file mode 100644 index 00000000000..c05ef65adeb --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-json/routes/dashboard.settings.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard/settings')({ + component: () =>
Dashboard Settings
, +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-json/routes/index-page.tsx b/packages/router-generator/tests/generator/regex-tokens-json/routes/index-page.tsx new file mode 100644 index 00000000000..2f2e105077b --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-json/routes/index-page.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: () =>
Home Page
, +}) diff --git a/packages/router-generator/tests/generator/regex-tokens-json/tsr.config.json b/packages/router-generator/tests/generator/regex-tokens-json/tsr.config.json new file mode 100644 index 00000000000..fea8c3bd4c4 --- /dev/null +++ b/packages/router-generator/tests/generator/regex-tokens-json/tsr.config.json @@ -0,0 +1,4 @@ +{ + "indexToken": { "regex": "[a-z]+-page" }, + "routeToken": { "regex": "[a-z]+-layout" } +} diff --git a/packages/router-generator/tests/generator/virtual-config-file-default-export/tsr.config.json b/packages/router-generator/tests/generator/virtual-config-file-default-export/tsr.config.json new file mode 100644 index 00000000000..4d587108e38 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-config-file-default-export/tsr.config.json @@ -0,0 +1,5 @@ +{ + "routesDirectory": "./routes", + "generatedRouteTree": "./routeTree.gen.ts", + "virtualRouteConfig": "./routes.ts" +} diff --git a/packages/router-generator/tests/generator/virtual-config-file-named-export/tsr.config.json b/packages/router-generator/tests/generator/virtual-config-file-named-export/tsr.config.json new file mode 100644 index 00000000000..4d587108e38 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-config-file-named-export/tsr.config.json @@ -0,0 +1,5 @@ +{ + "routesDirectory": "./routes", + "generatedRouteTree": "./routeTree.gen.ts", + "virtualRouteConfig": "./routes.ts" +} diff --git a/packages/router-generator/tests/generator/virtual-physical-layout-and-index/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-physical-layout-and-index/routeTree.snapshot.ts index 2191ecf8b33..bade75773cf 100644 --- a/packages/router-generator/tests/generator/virtual-physical-layout-and-index/routeTree.snapshot.ts +++ b/packages/router-generator/tests/generator/virtual-physical-layout-and-index/routeTree.snapshot.ts @@ -10,7 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as FeatureRouteRouteImport } from './routes/feature/route' -import { Route as IndexRouteImport } from './routes/index' +import { Route as indexRouteImport } from './routes/index' import { Route as FeatureIndexRouteImport } from './routes/feature/index' const FeatureRouteRoute = FeatureRouteRouteImport.update({ @@ -18,7 +18,7 @@ const FeatureRouteRoute = FeatureRouteRouteImport.update({ path: '/feature', getParentRoute: () => rootRouteImport, } as any) -const IndexRoute = IndexRouteImport.update({ +const indexRoute = indexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, @@ -30,17 +30,17 @@ const FeatureIndexRoute = FeatureIndexRouteImport.update({ } as any) export interface FileRoutesByFullPath { - '/': typeof IndexRoute + '/': typeof indexRoute '/feature': typeof FeatureRouteRouteWithChildren '/feature/': typeof FeatureIndexRoute } export interface FileRoutesByTo { - '/': typeof IndexRoute + '/': typeof indexRoute '/feature': typeof FeatureIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport - '/': typeof IndexRoute + '/': typeof indexRoute '/feature': typeof FeatureRouteRouteWithChildren '/feature/': typeof FeatureIndexRoute } @@ -53,7 +53,7 @@ export interface FileRouteTypes { fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute + indexRoute: typeof indexRoute FeatureRouteRoute: typeof FeatureRouteRouteWithChildren } @@ -70,7 +70,7 @@ declare module '@tanstack/react-router' { id: '/' path: '/' fullPath: '/' - preLoaderRoute: typeof IndexRouteImport + preLoaderRoute: typeof indexRouteImport parentRoute: typeof rootRouteImport } '/feature/': { @@ -96,7 +96,7 @@ const FeatureRouteRouteWithChildren = FeatureRouteRoute._addFileChildren( ) const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, + indexRoute: indexRoute, FeatureRouteRoute: FeatureRouteRouteWithChildren, } export const routeTree = rootRouteImport