From d7da3d6d372f3a6a5f1981697803582d6f4c4801 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sun, 19 May 2024 18:48:23 +0800 Subject: [PATCH 1/9] parallel routes --- packages/start/config/fs-router.js | 34 +++++---- packages/start/src/router/FileRoutes.ts | 16 +++- packages/start/src/router/routes.ts | 97 +++++++++++++++++++------ 3 files changed, 104 insertions(+), 43 deletions(-) diff --git a/packages/start/config/fs-router.js b/packages/start/config/fs-router.js index efefd87a4..952df092f 100644 --- a/packages/start/config/fs-router.js +++ b/packages/start/config/fs-router.js @@ -19,6 +19,7 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter { return routePath?.length > 0 ? `/${routePath}` : "/"; } + /** @param src {string} */ toRoute(src) { let path = this.toPath(src); @@ -37,22 +38,23 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter { const [_, exports] = analyzeModule(src); const hasDefault = exports.find(e => e.n === "default"); const hasRouteConfig = exports.find(e => e.n === "route"); - if (hasDefault) { - return { - $component: { - src: src, - pick: ["default", "$css"] - }, - $$route: hasRouteConfig - ? { - src: src, - pick: ["route"] - } - : undefined, - path, - filePath: src - }; - } + + return { + $component: hasDefault + ? { + src: src, + pick: ["default", "$css"] + } + : undefined, + $$route: hasRouteConfig + ? { + src: src, + pick: ["route"] + } + : undefined, + path, + filePath: src + }; } } diff --git a/packages/start/src/router/FileRoutes.ts b/packages/start/src/router/FileRoutes.ts index 8dde669dd..c09cf7952 100644 --- a/packages/start/src/router/FileRoutes.ts +++ b/packages/start/src/router/FileRoutes.ts @@ -1,12 +1,11 @@ import { getRequestEvent, isServer } from "solid-js/web"; import lazyRoute from "./lazyRoute"; -import type { Route } from "vinxi/fs-router"; import type { PageEvent } from "../server/types"; -import { pageRoutes as routeConfigs } from "./routes"; +import { Route, pageRoutes as routeConfigs } from "./routes"; export function createRoutes() { - function createRoute(route: Route) { + function createRoute(route: Route): any { return { ...route, ...(route.$$route ? route.$$route.require().route : undefined), @@ -21,7 +20,16 @@ export function createRoutes() { : import.meta.env.MANIFEST["client"], import.meta.env.MANIFEST["ssr"] ), - children: route.children ? route.children.map(createRoute) : undefined + children: route.children ? route.children.map(createRoute) : undefined, + ...(route.slots && { + slots: Object.entries(route.slots).reduce( + (acc, [slot, route]) => { + acc[slot] = createRoute(route); + return acc; + }, + {} as Record + ) + }) }; } const routes = routeConfigs.map(createRoute); diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index 0a907dcc9..fcf802ba4 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -1,11 +1,13 @@ import { createRouter } from "radix3"; import fileRoutes from "vinxi/routes"; -interface Route { +export interface Route { path: string; id: string; children?: Route[]; + slots?: Record; $component?: any; + $$route?: any; $GET?: any; $POST?: any; $PUT?: any; @@ -27,21 +29,64 @@ export const pageRoutes = defineRoutes( ); function defineRoutes(fileRoutes: Route[]) { + console.log({ fileRoutes }); + function processRoute(routes: Route[], route: Route, id: string, full: string) { + const [siblingPath, slotPath] = id.split("/@", 2); + + if (slotPath) { + const siblingRoute = routes.find(o => o.id === siblingPath); + console.log({ siblingPath, siblingRoute, slotPath }); + if (!siblingRoute) return routes; + + const [slotName] = slotPath.split("/", 1); + if (!slotName) return routes; + + const slots = (siblingRoute.slots ??= {}); + + if (!slots[slotName]) { + slots[slotName] = { + ...route, + id: "", + path: "" + }; + } else { + processRoute( + (slots[slotName]!.children ??= []), + route, + id.slice(siblingRoute.id.length + 2 + slotName.length), + full + ); + } + + return routes; + } + const parentRoute = Object.values(routes).find(o => { return id.startsWith(o.id + "/"); }); - if (!parentRoute) { - routes.push({ ...route, id, path: id.replace(/\/\([^)/]+\)/g, "").replace(/\([^)/]+\)/g, "") }); + if (parentRoute) { + processRoute( + parentRoute.children || (parentRoute.children = []), + route, + id.slice(parentRoute.id.length), + full + ); + return routes; } - processRoute( - parentRoute.children || (parentRoute.children = []), - route, - id.slice(parentRoute.id.length), - full - ); + + routes.push({ + ...route, + id, + path: id + // strip out escape group for escaping nested routes - e.g. foo(bar) -> foo + .replace(/\/\([^)/]+\)/g, "") + .replace(/\([^)/]+\)/g, "") + // replace . with / for flat routes - e.g. foo.bar -> foo/bar + .replace(/\./g, "/") + }); return routes; } @@ -70,18 +115,24 @@ function containsHTTP(route: Route) { } const router = createRouter({ - routes: (fileRoutes as unknown as Route[]).reduce((memo, route) => { - if (!containsHTTP(route)) return memo; - let path = route.path.replace(/\/\([^)/]+\)/g, "").replace(/\([^)/]+\)/g, "").replace(/\*([^/]*)/g, (_, m) => `**:${m}`); - if (/:[^/]*\?/g.test(path)) { - throw new Error(`Optional parameters are not supported in API routes: ${path}`); - } - if (memo[path]) { - throw new Error( - `Duplicate API routes for "${path}" found at "${memo[path]!.route.path}" and "${route.path}"` - ); - } - memo[path] = { route }; - return memo; - }, {} as Record) + routes: (fileRoutes as unknown as Route[]).reduce( + (memo, route) => { + if (!containsHTTP(route)) return memo; + let path = route.path + .replace(/\/\([^)/]+\)/g, "") + .replace(/\([^)/]+\)/g, "") + .replace(/\*([^/]*)/g, (_, m) => `**:${m}`); + if (/:[^/]*\?/g.test(path)) { + throw new Error(`Optional parameters are not supported in API routes: ${path}`); + } + if (memo[path]) { + throw new Error( + `Duplicate API routes for "${path}" found at "${memo[path]!.route.path}" and "${route.path}"` + ); + } + memo[path] = { route }; + return memo; + }, + {} as Record + ) }); From bb5864d59d11288c5723d67041b68794d04c37f3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 20 May 2024 13:17:01 +0800 Subject: [PATCH 2/9] nested parallel routes --- packages/start/src/router/routes.ts | 63 +++++++++++++---------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index fcf802ba4..4b22ea892 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -32,47 +32,42 @@ function defineRoutes(fileRoutes: Route[]) { console.log({ fileRoutes }); function processRoute(routes: Route[], route: Route, id: string, full: string) { - const [siblingPath, slotPath] = id.split("/@", 2); + const parentRoute = Object.values(routes).find(o => { + return id.startsWith(o.id + "/"); + }); - if (slotPath) { - const siblingRoute = routes.find(o => o.id === siblingPath); - console.log({ siblingPath, siblingRoute, slotPath }); - if (!siblingRoute) return routes; + if (parentRoute) { + const slicedId = id.slice(parentRoute.id.length); - const [slotName] = slotPath.split("/", 1); - if (!slotName) return routes; + if (slicedId.startsWith("/@")) { + let slotRoute: any = parentRoute; + let nextId: any = slicedId; - const slots = (siblingRoute.slots ??= {}); + // recursion is hard so while it is + while (nextId.startsWith("/@")) { + const slotName = /\/@([^/]+)/g.exec(nextId)![1]!; - if (!slots[slotName]) { - slots[slotName] = { - ...route, - id: "", - path: "" - }; - } else { - processRoute( - (slots[slotName]!.children ??= []), - route, - id.slice(siblingRoute.id.length + 2 + slotName.length), - full - ); - } + const slots = (slotRoute.slots ??= {}); - return routes; - } + nextId = nextId.slice(slotName.length + 2); - const parentRoute = Object.values(routes).find(o => { - return id.startsWith(o.id + "/"); - }); + if (!slots[slotName]) + slots[slotName] = { + ...route, + path: "", + id: "" + }; - if (parentRoute) { - processRoute( - parentRoute.children || (parentRoute.children = []), - route, - id.slice(parentRoute.id.length), - full - ); + slotRoute = slots[slotName]!; + } + + // route is a slot layout and doesn't need to be processed further as it was inserted in the while loop + if (nextId === "") return routes; + + processRoute((slotRoute.children ??= []), route, nextId, full); + } else { + processRoute((parentRoute.children ??= []), route, slicedId, full); + } return routes; } From 7e8e5c2ec111ea45c5e1061d3287057aea775275 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 20 May 2024 21:05:14 +0800 Subject: [PATCH 3/9] all workey --- packages/start/src/router/FileRoutes.ts | 16 +++++++++------- packages/start/src/router/routes.ts | 16 +++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/start/src/router/FileRoutes.ts b/packages/start/src/router/FileRoutes.ts index c09cf7952..781f98574 100644 --- a/packages/start/src/router/FileRoutes.ts +++ b/packages/start/src/router/FileRoutes.ts @@ -13,13 +13,15 @@ export function createRoutes() { ...(route.$$route ? route.$$route.require().route.info : {}), filesystem: true }, - component: lazyRoute( - route.$component, - import.meta.env.START_ISLANDS - ? import.meta.env.MANIFEST["ssr"] - : import.meta.env.MANIFEST["client"], - import.meta.env.MANIFEST["ssr"] - ), + component: route.$component + ? lazyRoute( + route.$component, + import.meta.env.START_ISLANDS + ? import.meta.env.MANIFEST["ssr"] + : import.meta.env.MANIFEST["client"], + import.meta.env.MANIFEST["ssr"] + ) + : undefined, children: route.children ? route.children.map(createRoute) : undefined, ...(route.slots && { slots: Object.entries(route.slots).reduce( diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index 4b22ea892..72722c98f 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -29,8 +29,6 @@ export const pageRoutes = defineRoutes( ); function defineRoutes(fileRoutes: Route[]) { - console.log({ fileRoutes }); - function processRoute(routes: Route[], route: Route, id: string, full: string) { const parentRoute = Object.values(routes).find(o => { return id.startsWith(o.id + "/"); @@ -51,14 +49,14 @@ function defineRoutes(fileRoutes: Route[]) { nextId = nextId.slice(slotName.length + 2); - if (!slots[slotName]) - slots[slotName] = { - ...route, - path: "", - id: "" - }; + if (nextId === "") { + slots[slotName] = { ...route }; + delete slots[slotName].path; - slotRoute = slots[slotName]!; + return routes; + } else { + slotRoute = slots[slotName] ??= {}; + } } // route is a slot layout and doesn't need to be processed further as it was inserted in the while loop From 9760a44210225ebbc969541542a1449b5a7ad8ce Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 11 Jun 2024 15:13:49 +0800 Subject: [PATCH 4/9] cleanup --- packages/start/src/router/FileRoutes.ts | 2 +- packages/start/src/router/routes.ts | 66 ++++++++++++------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/start/src/router/FileRoutes.ts b/packages/start/src/router/FileRoutes.ts index 850ec3d7c..d03049181 100644 --- a/packages/start/src/router/FileRoutes.ts +++ b/packages/start/src/router/FileRoutes.ts @@ -5,7 +5,7 @@ import type { PageEvent } from "../server/types"; import { Route, pageRoutes as routeConfigs } from "./routes"; export function createRoutes() { - function createRoute(route: Route): any { + function createRoute(route: Route) { return { ...route, ...(route.$$route ? route.$$route.require().route : undefined), diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index bc322629b..a986087de 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -33,50 +33,50 @@ function defineRoutes(fileRoutes: Route[]) { return id.startsWith(o.id + "/"); }); - if (parentRoute) { - const slicedId = id.slice(parentRoute.id.length); + if (!parentRoute) { + routes.push({ + ...route, + id, + path: id + // strip out escape group for escaping nested routes - e.g. foo(bar) -> foo + .replace(/\/\([^)/]+\)/g, "") + .replace(/\([^)/]+\)/g, "") + }); - if (slicedId.startsWith("/@")) { - let slotRoute = parentRoute; - let nextId = slicedId; + return routes; + } - // recursion is hard so while it is - while (nextId.startsWith("/@")) { - const slotName = /\/@([^/]+)/g.exec(nextId)![1]!; + const slicedId = id.slice(parentRoute.id.length); - const slots = (slotRoute.slots ??= {}); + if (slicedId.startsWith("/@")) { + let slotRoute = parentRoute; + let nextId = slicedId; - nextId = nextId.slice(slotName.length + 2); + // recursion is hard so while it is + while (nextId.startsWith("/@")) { + const slotName = /\/@([^/]+)/g.exec(nextId)![1]!; - if (nextId === "") { - slots[slotName] = { ...route }; - delete slots[slotName].path; + const slots = (slotRoute.slots ??= {}); - return routes; - } else { - slotRoute = slots[slotName] ??= {}; - } - } + nextId = nextId.slice(slotName.length + 2); - // route is a slot layout and doesn't need to be processed further as it was inserted in the while loop - if (nextId === "") return routes; + if (nextId === "") { + slots[slotName] = { ...route }; + delete slots[slotName].path; - processRoute((slotRoute.children ??= []), route, nextId, full); - } else { - processRoute((parentRoute.children ??= []), route, slicedId, full); + return routes; + } else { + slotRoute = slots[slotName] ??= {}; + } } - return routes; - } + // route is a slot layout and doesn't need to be processed further as it was inserted in the while loop + if (nextId === "") return routes; - routes.push({ - ...route, - id, - path: id - // strip out escape group for escaping nested routes - e.g. foo(bar) -> foo - .replace(/\/\([^)/]+\)/g, "") - .replace(/\([^)/]+\)/g, "") - }); + processRoute((slotRoute.children ??= []), route, nextId, full); + } else { + processRoute((parentRoute.children ??= []), route, slicedId, full); + } return routes; } From 303bccb0ca27a28b9a12e0148367c4677e95ef3c Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 11 Jun 2024 15:40:09 +0800 Subject: [PATCH 5/9] less ts errors + comments --- packages/start/src/router/FileRoutes.ts | 2 +- packages/start/src/router/routes.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/start/src/router/FileRoutes.ts b/packages/start/src/router/FileRoutes.ts index d03049181..850ec3d7c 100644 --- a/packages/start/src/router/FileRoutes.ts +++ b/packages/start/src/router/FileRoutes.ts @@ -5,7 +5,7 @@ import type { PageEvent } from "../server/types"; import { Route, pageRoutes as routeConfigs } from "./routes"; export function createRoutes() { - function createRoute(route: Route) { + function createRoute(route: Route): any { return { ...route, ...(route.$$route ? route.$$route.require().route : undefined), diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index a986087de..f7c85c68e 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -33,6 +33,12 @@ function defineRoutes(fileRoutes: Route[]) { return id.startsWith(o.id + "/"); }); + const path = id + // strip out escape group for escaping nested routes - e.g. foo(bar) -> foo + .replace(/\/\([^)/]+\)/g, "") + .replace(/\([^)/]+\)/g, ""); + + // Route is a leaf segment if (!parentRoute) { routes.push({ ...route, @@ -48,11 +54,11 @@ function defineRoutes(fileRoutes: Route[]) { const slicedId = id.slice(parentRoute.id.length); + // Route belongs to a slot if (slicedId.startsWith("/@")) { let slotRoute = parentRoute; let nextId = slicedId; - // recursion is hard so while it is while (nextId.startsWith("/@")) { const slotName = /\/@([^/]+)/g.exec(nextId)![1]!; @@ -62,12 +68,11 @@ function defineRoutes(fileRoutes: Route[]) { if (nextId === "") { slots[slotName] = { ...route }; - delete slots[slotName].path; return routes; - } else { - slotRoute = slots[slotName] ??= {}; } + + slotRoute = slots[slotName] ??= { id, path }; } // route is a slot layout and doesn't need to be processed further as it was inserted in the while loop From 14443fce387b6b9ea2f74444007c3ca895295ee8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 11 Jun 2024 17:39:20 +0800 Subject: [PATCH 6/9] fix --- packages/start/src/router/routes.ts | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index f7c85c68e..d9012745e 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -33,11 +33,6 @@ function defineRoutes(fileRoutes: Route[]) { return id.startsWith(o.id + "/"); }); - const path = id - // strip out escape group for escaping nested routes - e.g. foo(bar) -> foo - .replace(/\/\([^)/]+\)/g, "") - .replace(/\([^)/]+\)/g, ""); - // Route is a leaf segment if (!parentRoute) { routes.push({ @@ -52,35 +47,40 @@ function defineRoutes(fileRoutes: Route[]) { return routes; } - const slicedId = id.slice(parentRoute.id.length); + const idWithoutParent = id.slice(parentRoute.id.length); // Route belongs to a slot - if (slicedId.startsWith("/@")) { + if (idWithoutParent.startsWith("/@")) { let slotRoute = parentRoute; - let nextId = slicedId; + let idWithoutSlot = idWithoutParent; - while (nextId.startsWith("/@")) { - const slotName = /\/@([^/]+)/g.exec(nextId)![1]!; + // Drill down through nested slots + // Recursing would nest via 'children' but we want to nest via 'slots', + // so this is handled as a special case + while (idWithoutSlot.startsWith("/@")) { + const slotName = /\/@([^/]+)/g.exec(idWithoutSlot)![1]!; const slots = (slotRoute.slots ??= {}); - nextId = nextId.slice(slotName.length + 2); + idWithoutSlot = idWithoutSlot.slice(slotName.length + 2); - if (nextId === "") { - slots[slotName] = { ...route }; + // Route is a slot definition + if (idWithoutSlot === "") { + const slot = { ...route }; + delete (slot as any).path; + slots[slotName] = slot; return routes; } - slotRoute = slots[slotName] ??= { id, path }; + slotRoute = slots[slotName] ??= {} as any; } - // route is a slot layout and doesn't need to be processed further as it was inserted in the while loop - if (nextId === "") return routes; - - processRoute((slotRoute.children ??= []), route, nextId, full); - } else { - processRoute((parentRoute.children ??= []), route, slicedId, full); + processRoute((slotRoute.children ??= []), route, idWithoutSlot, full); + } + // Route just has a parent + else { + processRoute((parentRoute.children ??= []), route, idWithoutParent, full); } return routes; From 953c4ed358b39fdf7b0bf78915e171c58df4ccb2 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 11 Jun 2024 17:41:36 +0800 Subject: [PATCH 7/9] slot children comment --- packages/start/src/router/routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/start/src/router/routes.ts b/packages/start/src/router/routes.ts index d9012745e..66d346930 100644 --- a/packages/start/src/router/routes.ts +++ b/packages/start/src/router/routes.ts @@ -54,7 +54,7 @@ function defineRoutes(fileRoutes: Route[]) { let slotRoute = parentRoute; let idWithoutSlot = idWithoutParent; - // Drill down through nested slots + // Drill down through directly nested slots // Recursing would nest via 'children' but we want to nest via 'slots', // so this is handled as a special case while (idWithoutSlot.startsWith("/@")) { @@ -76,6 +76,7 @@ function defineRoutes(fileRoutes: Route[]) { slotRoute = slots[slotName] ??= {} as any; } + // We only resume with children once all the directly nested slots are traversed processRoute((slotRoute.children ??= []), route, idWithoutSlot, full); } // Route just has a parent From 22175ce15f80b0408883cf4267cfa5f0b97ba944 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 11 Jun 2024 17:50:23 +0800 Subject: [PATCH 8/9] fix bad merge --- packages/start/config/fs-router.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/start/config/fs-router.js b/packages/start/config/fs-router.js index 3186f9671..994c49a1c 100644 --- a/packages/start/config/fs-router.js +++ b/packages/start/config/fs-router.js @@ -19,7 +19,6 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter { return routePath?.length > 0 ? `/${routePath}` : "/"; } - /** @param src {string} */ toRoute(src) { let path = this.toPath(src); From 07e366d229476e8ae7e0fa424cd23ccb4140e0fb Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 11 Jun 2024 18:05:44 +0800 Subject: [PATCH 9/9] Create ninety-dogs-cheat.md --- .changeset/ninety-dogs-cheat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ninety-dogs-cheat.md diff --git a/.changeset/ninety-dogs-cheat.md b/.changeset/ninety-dogs-cheat.md new file mode 100644 index 000000000..27a0a41bd --- /dev/null +++ b/.changeset/ninety-dogs-cheat.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": minor +--- + +Parallel routes