Skip to content

Commit

Permalink
feat: initial slugs support
Browse files Browse the repository at this point in the history
  • Loading branch information
justinawrey committed Aug 29, 2022
1 parent 10c541a commit 4740001
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 15 deletions.
7 changes: 7 additions & 0 deletions _scripts/pages/[fallback].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type FsHandler } from "../../private/route.ts";

const handler: FsHandler = (_req, query) => {
return new Response(`Hello from: /${query.fallback}`);
};

export default handler;
3 changes: 3 additions & 0 deletions _scripts/pages/blog/[id].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default (_req: Request) => {
return new Response("Hello from: /blog/[id]");
};
11 changes: 9 additions & 2 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Route } from "./private/route.ts";
function handleRoutes(routes: Route[]): Handler {
// Split routes into ones that are exact (don't have slugs) and ones that aren't
const exactRoutes = routes.filter((route) => !route.hasSlugs);
const _slugRoutes = routes.filter((route) => route.hasSlugs);
const slugRoutes = Route.sort(routes.filter((route) => route.hasSlugs));

// Make a map out of the exact routes for easier lookup.
const exactRouteMap = new Map<string, Route>(
Expand All @@ -23,7 +23,14 @@ function handleRoutes(routes: Route[]): Handler {

// Exact route (no slugs) found, serve it
if (exactRoute) {
return exactRoute.handler(req, connInfo);
return exactRoute.handler(req, {}, connInfo);
}

for (const slugRoute of slugRoutes) {
const matches = slugRoute.matches(urlPath);
if (matches) {
return slugRoute.handler(req, matches, connInfo);
}
}

// Respond with a 404 Not Found if asking for a route
Expand Down
1 change: 1 addition & 0 deletions private/deps/std/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
isHttpError,
} from "https://deno.land/std@0.152.0/http/http_errors.ts";
export {
type ConnInfo,
type Handler,
serve,
} from "https://deno.land/std@0.152.0/http/server.ts";
89 changes: 76 additions & 13 deletions private/route.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { type Handler } from "./deps/std/http.ts";
import { type ConnInfo } from "./deps/std/http.ts";
import { resolve, toFileUrl } from "./deps/std/path.ts";
import { extname, relative } from "./deps/std/path.ts";

function removeExtension(path: string) {
export type Query = Readonly<Record<string, string>>;
export type FsHandler = (
request: Request,
query: Query,
connInfo: ConnInfo,
) => Response | Promise<Response>;

function removeExtension(path: string): string {
const ext = extname(path);
return path.slice(0, -ext.length);
}

function removeIndex(path: string) {
function removeIndex(path: string): string {
if (!path.endsWith("/index")) {
return path;
}

return path.slice(0, -6);
}

function isSlug(part: string): boolean {
return part.startsWith("[") && part.endsWith("]") && part.length > 2;
}

// parseRoute takes an absolute file path and transforms it into
// a valid route to which requests can be routed
function parseRoute(
Expand All @@ -32,14 +43,14 @@ function parseRoute(

export class Route {
constructor(
// The original file name for this route
// The original file name for this route, from fs.walk
public file: string,
// The parsed route with file extension and /index stripped away
public parsed: string,
// The Handler responsible for responding to requests
public handler: Handler,
// Slugs in the filename, with the '[' and ']' characters still included
public slugs: string[] = [],
// The absolute path of the route
public absPath: string,
// The absolute root dir from which the router was started
public absRootDir: string,
// The RouterHandler in charge of responding to http requests for this route
public handler: FsHandler,
) {}

static async create(
Expand All @@ -53,12 +64,64 @@ export class Route {

return new this(
filePath,
parseRoute(absRootDir, absPath),
(await import(absPath)).default as Handler,
absPath,
absRootDir,
(await import(absPath)).default as FsHandler,
);
}

get hasSlugs() {
// Sort an array of routes by length, longest to shortest
static sort(routes: Route[]): Route[] {
routes.sort((a, b) => b.length - a.length);
return routes;
}

// The parsed route, e.g. with file extension and trailing '/index' stripped away
get parsed(): string {
return parseRoute(this.absRootDir, this.absPath);
}

// A list of '/' delimited 'parts' of the route
get parts(): string[] {
return this.parsed.split("/").filter((part) => part !== "");
}

// The amount of '/' delimited 'parts' the route has
get length(): number {
return this.parts.length;
}

// Slugs from the filename, with the '[' and ']' characters stripped away
get slugs(): string[] {
return this.parts.filter((part) => isSlug(part)).map((slug) =>
slug.slice(1, -1)
);
}

// Whether or not this route has slugs in it
get hasSlugs(): boolean {
return this.slugs.length > 0;
}

get regEx(): RegExp {
return new RegExp(
"\\/" +
this.parts.map((part) => isSlug(part) ? "(\\w+)" : part).join("\\/"),
"g",
);
}

matches(urlPath: string): Record<string, string> | null {
const matches = this.regEx.exec(urlPath);
if (!matches) {
return null;
}

const matchObj: Record<string, string> = {};
for (const [index, match] of matches.slice(1).entries()) {
matchObj[this.slugs[index]] = match;
}

return matchObj;
}
}

0 comments on commit 4740001

Please sign in to comment.