diff --git a/.changeset/big-spoons-grab.md b/.changeset/big-spoons-grab.md new file mode 100644 index 00000000000..ab0ddcd25df --- /dev/null +++ b/.changeset/big-spoons-grab.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": minor +--- + +allow defining multiple routes for the same route module file diff --git a/contributors.yml b/contributors.yml index 54b97030a01..18c8c6b362d 100644 --- a/contributors.yml +++ b/contributors.yml @@ -257,6 +257,7 @@ - lpsinger - lswest - lucasdibz +- lucasferreira - luispagarcia - luisrivas - luistak diff --git a/packages/remix-dev/__tests__/defineRoutes-test.ts b/packages/remix-dev/__tests__/defineRoutes-test.ts index d4766d004f5..dbd73350dfa 100644 --- a/packages/remix-dev/__tests__/defineRoutes-test.ts +++ b/packages/remix-dev/__tests__/defineRoutes-test.ts @@ -87,4 +87,80 @@ describe("defineRoutes", () => { } `); }); + + it("allows multiple routes with the same route module", () => { + let routes = defineRoutes((route) => { + route("/user/:id", "routes/index.tsx", { id: "user-by-id" }); + route("/user", "routes/index.tsx", { id: "user" }); + route("/other", "routes/other-route.tsx"); + }); + + expect(routes).toMatchInlineSnapshot(` + Object { + "routes/other-route": Object { + "caseSensitive": undefined, + "file": "routes/other-route.tsx", + "id": "routes/other-route", + "index": undefined, + "parentId": undefined, + "path": "/other", + }, + "user": Object { + "caseSensitive": undefined, + "file": "routes/index.tsx", + "id": "user", + "index": undefined, + "parentId": undefined, + "path": "/user", + }, + "user-by-id": Object { + "caseSensitive": undefined, + "file": "routes/index.tsx", + "id": "user-by-id", + "index": undefined, + "parentId": undefined, + "path": "/user/:id", + }, + } + `); + }); + + it("throws an error on route id collisions", () => { + // Two conflicting custom id's + let defineNonUniqueRoutes = () => { + defineRoutes((route) => { + route("/user/:id", "routes/user.tsx", { id: "user" }); + route("/user", "routes/user.tsx", { id: "user" }); + route("/other", "routes/other-route.tsx"); + }); + }; + + expect(defineNonUniqueRoutes).toThrowErrorMatchingInlineSnapshot( + `"Unable to define routes with duplicate route id: \\"user\\""` + ); + + // Custom id conflicting with a later-defined auto-generated id + defineNonUniqueRoutes = () => { + defineRoutes((route) => { + route("/user/:id", "routes/user.tsx", { id: "routes/user" }); + route("/user", "routes/user.tsx"); + }); + }; + + expect(defineNonUniqueRoutes).toThrowErrorMatchingInlineSnapshot( + `"Unable to define routes with duplicate route id: \\"routes/user\\""` + ); + + // Custom id conflicting with an earlier-defined auto-generated id + defineNonUniqueRoutes = () => { + defineRoutes((route) => { + route("/user", "routes/user.tsx"); + route("/user/:id", "routes/user.tsx", { id: "routes/user" }); + }); + }; + + expect(defineNonUniqueRoutes).toThrowErrorMatchingInlineSnapshot( + `"Unable to define routes with duplicate route id: \\"routes/user\\""` + ); + }); }); diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index af02d83233c..60be5b6da73 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -56,10 +56,13 @@ export async function createAssetsManifest( config.appDirectory, config.entryClientFile ); - let routesByFile: Map = Object.keys(config.routes).reduce( + let routesByFile: Map = Object.keys(config.routes).reduce( (map, key) => { let route = config.routes[key]; - map.set(route.file, route); + map.set( + route.file, + map.has(route.file) ? [...map.get(route.file), route] : [route] + ); return map; }, new Map() @@ -83,22 +86,27 @@ export async function createAssetsManifest( /(^browser-route-module:|\?browser$)/g, "" ); - let route = routesByFile.get(entryPointFile); - invariant(route, `Cannot get route for entry point ${output.entryPoint}`); - let sourceExports = await getRouteModuleExports(config, route.id); - routes[route.id] = { - id: route.id, - parentId: route.parentId, - path: route.path, - index: route.index, - caseSensitive: route.caseSensitive, - module: resolveUrl(key), - imports: resolveImports(output.imports), - hasAction: sourceExports.includes("action"), - hasLoader: sourceExports.includes("loader"), - hasCatchBoundary: sourceExports.includes("CatchBoundary"), - hasErrorBoundary: sourceExports.includes("ErrorBoundary"), - }; + let groupedRoute = routesByFile.get(entryPointFile); + invariant( + groupedRoute, + `Cannot get route(s) for entry point ${output.entryPoint}` + ); + for (let route of groupedRoute) { + let sourceExports = await getRouteModuleExports(config, route.id); + routes[route.id] = { + id: route.id, + parentId: route.parentId, + path: route.path, + index: route.index, + caseSensitive: route.caseSensitive, + module: resolveUrl(key), + imports: resolveImports(output.imports), + hasAction: sourceExports.includes("action"), + hasLoader: sourceExports.includes("loader"), + hasCatchBoundary: sourceExports.includes("CatchBoundary"), + hasErrorBoundary: sourceExports.includes("ErrorBoundary"), + }; + } } } diff --git a/packages/remix-dev/config/routes.ts b/packages/remix-dev/config/routes.ts index 9d97158d03f..7f1e5696ef3 100644 --- a/packages/remix-dev/config/routes.ts +++ b/packages/remix-dev/config/routes.ts @@ -54,6 +54,12 @@ export interface DefineRouteOptions { * Should be `true` if this is an index route that does not allow child routes. */ index?: boolean; + + /** + * An optional unique id string for this route. Use this if you need to aggregate + * two or more routes with the same route file. + */ + id?: string; } interface DefineRouteChildren { @@ -141,7 +147,7 @@ export function defineRoutes( path: path ? path : undefined, index: options.index ? true : undefined, caseSensitive: options.caseSensitive ? true : undefined, - id: createRouteId(file), + id: options.id || createRouteId(file), parentId: parentRoutes.length > 0 ? parentRoutes[parentRoutes.length - 1].id @@ -149,6 +155,12 @@ export function defineRoutes( file, }; + if (route.id in routes) { + throw new Error( + `Unable to define routes with duplicate route id: "${route.id}"` + ); + } + routes[route.id] = route; if (children) {