diff --git a/src/index.ts b/src/index.ts index 8b28222..ed03aaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,12 @@ type RouteInfo = { path: string file: string name: string - parent?: string // first pass parent is undefined - isIndex: boolean + parentId?: string // first pass parent is undefined + index?: boolean + caseSensitive?: boolean +} +interface RouteManifest { + [key: string]: RouteInfo } type DefineRouteOptions = { @@ -49,7 +53,7 @@ export default function flatRoutes( baseDir: string, defineRoutes: DefineRoutesFunction, options: FlatRoutesOptions = {}, -) { +): RouteManifest { const routeMap = new Map() const parentMap = new Map() const visitor = options?.visitFiles || visitFiles @@ -59,8 +63,8 @@ export default function flatRoutes( path: '', file: 'root.tsx', name: 'root', - parent: '', - isIndex: false, + parentId: '', + index: false, }) var routes = defineRoutes(route => { visitor(`app/${baseDir}`, routeFile => { @@ -117,7 +121,7 @@ function getRoutes( if (parentRoute && parentRoute.children) { const routeOptions: DefineRouteOptions = { caseSensitive: false, - index: parentRoute!.routeInfo.isIndex, + index: parentRoute!.routeInfo.index, } const routeChildren: DefineRouteChildren = () => { for (let child of parentRoute!.children) { @@ -125,7 +129,7 @@ function getRoutes( const path = child.path.substring( parentRoute!.routeInfo.path.length + 1, ) - route(path, child.file, { index: child.isIndex }) + route(path, child.file, { index: child.index }) } } route( @@ -180,7 +184,7 @@ export function getRouteInfo( file: path.join(baseDir, routeFile), name, //parent: parent will be calculated after all routes are defined, - isIndex: + index: routeSegments.at(-1) === 'index' || routeSegments.at(-1) === '_index', } } @@ -190,6 +194,10 @@ function appendPathSegment(url: string, segment: string) { if (segment.startsWith('_')) { // handle pathless route (not included in url) return url + } else if (segment.endsWith('_')) { + // handle parent override + segment = segment.substring(0, segment.length - 1) + url += '/' + segment } else if (['index', '_index'].some(name => segment === name)) { // handle index route if (!url.endsWith('/')) { @@ -211,5 +219,6 @@ export type { DefineRouteFunction, DefineRouteOptions, DefineRouteChildren, + RouteManifest, RouteInfo, } diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index 84a2501..f4094be 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -1,5 +1,87 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`define routes should allow routes to specify different parent routes 1`] = ` +Object { + "routes/parent": Object { + "caseSensitive": undefined, + "file": "routes/parent.tsx", + "id": "routes/parent", + "index": undefined, + "parentId": "root", + "path": "parent", + }, + "routes/parent.some.nested": Object { + "caseSensitive": undefined, + "file": "routes/parent.some.nested.tsx", + "id": "routes/parent.some.nested", + "index": undefined, + "parentId": "routes/parent", + "path": "some/nested", + }, + "routes/parent.some_.nested.route": Object { + "caseSensitive": undefined, + "file": "routes/parent.some_.nested.route.tsx", + "id": "routes/parent.some_.nested.route", + "index": undefined, + "parentId": "routes/parent", + "path": "some/nested/route", + }, +} +`; + +exports[`define routes should correctly nest routes 1`] = ` +Object { + "routes/app.$organizationSlug": Object { + "caseSensitive": undefined, + "file": "routes/app.$organizationSlug.tsx", + "id": "routes/app.$organizationSlug", + "index": undefined, + "parentId": "root", + "path": "app/:organizationSlug", + }, + "routes/app.$organizationSlug.edit": Object { + "caseSensitive": undefined, + "file": "routes/app.$organizationSlug.edit.tsx", + "id": "routes/app.$organizationSlug.edit", + "index": undefined, + "parentId": "routes/app.$organizationSlug", + "path": "edit", + }, + "routes/app.$organizationSlug.projects": Object { + "caseSensitive": undefined, + "file": "routes/app.$organizationSlug.projects.tsx", + "id": "routes/app.$organizationSlug.projects", + "index": undefined, + "parentId": "routes/app.$organizationSlug", + "path": "projects", + }, + "routes/app.$organizationSlug.projects.$projectId": Object { + "caseSensitive": undefined, + "file": "routes/app.$organizationSlug.projects.$projectId.tsx", + "id": "routes/app.$organizationSlug.projects.$projectId", + "index": undefined, + "parentId": "routes/app.$organizationSlug.projects", + "path": ":projectId", + }, + "routes/app.$organizationSlug.projects.$projectId.edit": Object { + "caseSensitive": undefined, + "file": "routes/app.$organizationSlug.projects.$projectId.edit.tsx", + "id": "routes/app.$organizationSlug.projects.$projectId.edit", + "index": undefined, + "parentId": "routes/app.$organizationSlug.projects.$projectId", + "path": "edit", + }, + "routes/app.$organizationSlug.projects.new": Object { + "caseSensitive": undefined, + "file": "routes/app.$organizationSlug.projects.new.tsx", + "id": "routes/app.$organizationSlug.projects.new", + "index": undefined, + "parentId": "routes/app.$organizationSlug.projects", + "path": "new", + }, +} +`; + exports[`define routes should define routes for complex structure 1`] = ` Object { "routes/_auth": Object { @@ -120,7 +202,7 @@ Object { "id": "routes/app_.projects.$id.roadmap", "index": undefined, "parentId": "root", - "path": "app_/projects/:id/roadmap", + "path": "app/projects/:id/roadmap", }, "routes/app_.projects.$id.roadmap[.pdf]": Object { "caseSensitive": undefined, @@ -128,7 +210,7 @@ Object { "id": "routes/app_.projects.$id.roadmap[.pdf]", "index": undefined, "parentId": "root", - "path": "app_/projects/:id/roadmap.pdf", + "path": "app/projects/:id/roadmap.pdf", }, } `; diff --git a/test/index.test.ts b/test/index.test.ts index bff0eb9..fca1501 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,5 @@ import { defineRoutes } from '@remix-run/dev/config/routes' +import type { RouteManifest } from '../src/index' import flatRoutes from '../src/index' describe('define routes', () => { @@ -69,6 +70,32 @@ describe('define routes', () => { expect(routes).toMatchSnapshot() }) + it('should correctly nest routes', () => { + const routeList = [ + 'app.$organizationSlug.tsx', + 'app.$organizationSlug.edit.tsx', + 'app.$organizationSlug.projects.tsx', + 'app.$organizationSlug.projects.$projectId.tsx', + 'app.$organizationSlug.projects.$projectId.edit.tsx', + 'app.$organizationSlug.projects.new.tsx', + ] + const routes = flatRoutes('routes', defineRoutes, { + visitFiles: visitFilesFromArray(routeList), + }) + expect(routes).toMatchSnapshot() + }) + it('should allow routes to specify different parent routes', () => { + const routeList = [ + 'parent.tsx', + 'parent.some.nested.tsx', + 'parent.some_.nested.route.tsx', + ] + const routes = flatRoutes('routes', defineRoutes, { + visitFiles: visitFilesFromArray(routeList), + }) + expect(routes).toMatchSnapshot() + }) + it('should ignore non-route files in flat-folders', () => { const flatFolders = [ '$lang.$ref/_layout.tsx', @@ -107,3 +134,53 @@ function visitFilesFromArray(files: string[]) { }) } } + +type RouteMapInfo = { + id: string + path: string + file: string + index?: boolean + children: string[] +} +function dumpRoutes(routes: RouteManifest) { + const routeMap = new Map() + const rootRoute: RouteMapInfo = { + id: 'root', + path: '', + file: 'root.tsx', + index: false, + children: [], + } + routeMap.set('root', rootRoute) + Object.entries(routes).forEach(([name, route]) => { + if (!route.parentId) return + const parent = routeMap.get(route.parentId) + if (parent) { + parent.children.push(name) + } + routeMap.set(name, { + id: name, + path: route.path, + file: route.file, + index: route.index, + children: [], + }) + }) + const dump = (route: RouteMapInfo, indent: string) => { + const getPath = (path?: string) => (path ? `path="${path}" ` : '') + const getIndex = (index?: boolean) => (index ? 'index ' : '') + output += `${indent}\n` + if (route.children.length) { + route.children.forEach((childId: string) => { + dump(routeMap.get(childId)!, indent + ' ') + }) + output += `${indent}\n` + } + } + let output = '\n' + dump(routeMap.get('root')!, ' ') + output += '\n' + console.log(output) +}