Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parallel routes #1537

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/ninety-dogs-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": minor
---

Parallel routes
16 changes: 12 additions & 4 deletions packages/start/src/router/FileRoutes.ts
Original file line number Diff line number Diff line change
@@ -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),
Expand All @@ -23,7 +22,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>(route.slots).reduce(
(acc, [slot, route]) => {
acc[slot] = createRoute(route);
return acc;
},
{} as Record<string, any>
)
})
};
}
const routes = routeConfigs.map(createRoute);
Expand Down
96 changes: 71 additions & 25 deletions packages/start/src/router/routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { createRouter } from "radix3";
import fileRoutes from "vinxi/routes";

interface Route {
export interface Route {
path: string;
id: string;
children?: Route[];
slots?: Record<string, Route>;
page?: boolean;
$component?: any;
$$route?: any;
$GET?: any;
$POST?: any;
$PUT?: any;
Expand All @@ -23,26 +25,64 @@ declare module "vinxi/routes" {
}
}

export const pageRoutes = defineRoutes(
(fileRoutes as unknown as Route[]).filter(o => o.page)
);
export const pageRoutes = defineRoutes((fileRoutes as unknown as Route[]).filter(o => o.page));

function defineRoutes(fileRoutes: Route[]) {
function processRoute(routes: Route[], route: Route, id: string, full: string) {
const parentRoute = Object.values(routes).find(o => {
return id.startsWith(o.id + "/");
});

// Route is a leaf segment
if (!parentRoute) {
routes.push({ ...route, id, path: id.replace(/\/\([^)/]+\)/g, "").replace(/\([^)/]+\)/g, "") });
routes.push({
...route,
id,
path: id
// strip out escape group for escaping nested routes - e.g. foo(bar) -> foo
.replace(/\/\([^)/]+\)/g, "")
.replace(/\([^)/]+\)/g, "")
});

return routes;
}
processRoute(
parentRoute.children || (parentRoute.children = []),
route,
id.slice(parentRoute.id.length),
full
);

const idWithoutParent = id.slice(parentRoute.id.length);

// Route belongs to a slot
if (idWithoutParent.startsWith("/@")) {
let slotRoute = parentRoute;
let idWithoutSlot = idWithoutParent;

// 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("/@")) {
const slotName = /\/@([^/]+)/g.exec(idWithoutSlot)![1]!;

const slots = (slotRoute.slots ??= {});

idWithoutSlot = idWithoutSlot.slice(slotName.length + 2);

// Route is a slot definition
if (idWithoutSlot === "") {
const slot = { ...route };
delete (slot as any).path;
slots[slotName] = slot;

return routes;
}

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
else {
processRoute((parentRoute.children ??= []), route, idWithoutParent, full);
}

return routes;
}
Expand Down Expand Up @@ -71,18 +111,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<string, { route: Route }>)
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<string, { route: Route }>
)
});