Skip to content

Commit

Permalink
Support for http method exports in route modules.
Browse files Browse the repository at this point in the history
  • Loading branch information
jollytoad committed Mar 22, 2024
1 parent ee8a07e commit 238b983
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 27 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.0]

### Added

- Support for modules that export http method functions rather than a default
handler, when generating a routes module.
- `lazy` supports module transformation after loading (to allow dynamic loading
of http method modules).

### Fixed

- `generate_routes.ts` example to enable `jsr` imports.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@http/fns",
"version": "0.6.4",
"version": "0.7.0",
"description": "HTTP server functions (including routing)",
"tasks": {
"example": "deno run -A --watch",
Expand Down
52 changes: 52 additions & 0 deletions examples/generated_routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
assertEquals,
assertOk,
assertStatus,
STATUS_CODE,
} from "./_test_deps.ts";

Deno.test("generated routes.ts", async (t) => {
await using _server = (await import("./generated_routes.ts")).default;

await t.step("GET /", async () => {
const response = await fetch("/");

assertOk(response);
assertEquals(await response.text(), "This is the index page");
});

await t.step("GET /user/bob", async () => {
const response = await fetch("/user/bob");

assertOk(response);
assertEquals(await response.text(), "Hello bob");
});

await t.step("GET /methods", async () => {
const response = await fetch("/methods");

assertOk(response);
assertEquals(await response.text(), "GET method");
});

await t.step("PUT /methods", async () => {
const response = await fetch("/methods", { method: "PUT" });

assertOk(response);
assertEquals(await response.text(), "PUT method");
});

await t.step("POST /methods", async () => {
const response = await fetch("/methods", { method: "POST" });

assertOk(response);
assertEquals(await response.text(), "POST method");
});

await t.step("DELETE /methods (405)", async () => {
const response = await fetch("/methods", { method: "DELETE" });

assertStatus(response, STATUS_CODE.MethodNotAllowed);
await response.body?.cancel();
});
});
7 changes: 6 additions & 1 deletion examples/routes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// IMPORTANT: This file has been automatically generated, DO NOT edit by hand.

import { byMethod } from "@http/fns/by_method";
import { lazy } from "@http/fns/lazy";
import { byPattern } from "@http/fns/by_pattern";
import { cascade } from "@http/fns/cascade";
import { lazy } from "@http/fns/lazy";

export default cascade(
byPattern("/user/:name", lazy(() => import("./routes/user/[name].ts"))),
byPattern(
"/methods",
lazy(async () => byMethod(await import("./routes/methods.ts"))),
),
byPattern("/", lazy(() => import("./routes/index.ts"))),
);
13 changes: 13 additions & 0 deletions examples/routes/methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ok } from "@http/fns/response/ok";

export function GET(_req: Request) {
return ok(`GET method`);
}

export function PUT(_req: Request) {
return ok(`PUT method`);
}

export function POST(_req: Request) {
return ok(`POST method`);
}
1 change: 1 addition & 0 deletions examples/scripts/generate_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function generateRoutes() {
moduleOutUrl: import.meta.resolve("../routes.ts"),
pathMapper: "@http/fns/fresh/path_mapper",
httpFns: "@http/fns/",
jsr: true,
routeDiscovery: "static",
moduleImports: "dynamic",
verbose: true,
Expand Down
2 changes: 1 addition & 1 deletion lib/discover_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface DiscoverRoutesOptions {
*/
pathMapper?: PathMapper;
/**
* Function to mapper each file entry to zero, one or many routes.
* Function to map each file entry to zero, one or many routes.
* The default mapping only maps typescript files.
*/
routeMapper?: RouteMapper;
Expand Down
11 changes: 10 additions & 1 deletion lib/dynamic_route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,14 @@ async function buildHandler(opts: DynamicRouteOptions) {
}

function asLazyRoute({ pattern, module }: DiscoveredRoute) {
return byPattern(pattern, lazy(module));
return byPattern(pattern, lazy(module, transformMethodExports));
}

async function transformMethodExports(loaded: unknown): Promise<unknown> {
if (loaded && typeof loaded === "object" && !("default" in loaded)) {
// assume module of individually exported http method functions
const { byMethod } = await import("./by_method.ts");
return byMethod(loaded);
}
return loaded;
}
61 changes: 39 additions & 22 deletions lib/generate_routes_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,16 @@ export async function generateRoutesModule({
const outPath = dirname(outUrl.pathname);
const ext = jsr ? "" : ".ts";

const httpFnImports = new Map<string, string>();
const head: string[] = [];
const body: string[] = [];
let i = 1;

head.push(
"// IMPORTANT: This file has been automatically generated, DO NOT edit by hand.\n\n",
);

switch (routeDiscovery) {
case "startup":
case "request":
{
const dynamic_ts = `${httpFnsUrl}dynamic_route${ext}`;

head.push(`import { dynamicRoute } from "${dynamic_ts}";\n`);
httpFnImports.set("dynamicRoute", "dynamic_route");

let modulePath = relative(
outPath,
Expand Down Expand Up @@ -148,17 +143,8 @@ export async function generateRoutesModule({
case "static":
default: {
const isLazy = moduleImports !== "static";
const pattern_ts = `${httpFnsUrl}by_pattern${ext}`;
const cascade_ts = `${httpFnsUrl}cascade${ext}`;

head.push(`import { byPattern } from "${pattern_ts}";\n`);
head.push(`import { cascade } from "${cascade_ts}";\n`);

if (isLazy) {
const lazy_ts = `${httpFnsUrl}lazy${ext}`;

head.push(`import { lazy } from "${lazy_ts}";\n`);
}
httpFnImports.set("cascade", "cascade");

body.push("export default cascade(\n");

Expand All @@ -183,22 +169,53 @@ export async function generateRoutesModule({
if (modulePath[0] !== ".") {
modulePath = "./" + modulePath;
}

const m = await import(String(module));

const hasDefault = !!m.default;

const patternJson = JSON.stringify(asSerializablePattern(pattern));

httpFnImports.set("byPattern", "by_pattern");

if (isLazy) {
body.push(
` byPattern(${patternJson}, lazy(() => import("${modulePath}"))),\n`,
);
httpFnImports.set("lazy", "lazy");
if (hasDefault) {
body.push(
` byPattern(${patternJson}, lazy(() => import("${modulePath}"))),\n`,
);
} else {
httpFnImports.set("byMethod", "by_method");
body.push(
` byPattern(${patternJson}, lazy(async () => byMethod(await import("${modulePath}")))),\n`,
);
}
} else {
head.push(`import route_${i} from "${modulePath}";\n`);
body.push(` byPattern(${patternJson}, route_${i}),\n`);
if (hasDefault) {
head.push(`import route_${i} from "${modulePath}";\n`);
body.push(` byPattern(${patternJson}, route_${i}),\n`);
} else {
httpFnImports.set("byMethod", "by_method");
head.push(`import * as route_${i} from "${modulePath}";\n`);
body.push(` byPattern(${patternJson}, byMethod(route_${i})),\n`);
}
}

i++;
}

body.push(`);\n`);
}
}

for (const [fn, module] of httpFnImports.entries()) {
head.unshift(`import { ${fn} } from "${httpFnsUrl}${module}${ext}";\n`);
}

head.unshift(
"// IMPORTANT: This file has been automatically generated, DO NOT edit by hand.\n\n",
);

head.push(`\n`);

const content = head.concat(body).join("");
Expand Down
9 changes: 8 additions & 1 deletion lib/lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { Awaitable } from "./types.ts";
*
* @param handlerLoader function to load the handler fn, or a module or
* module specifier that exports the handler as the default export.
* @param transformer an optional function that can transform the loaded
* module before returning it.
*/
export function lazy<
A extends unknown[],
Expand All @@ -14,6 +16,7 @@ export function lazy<
| (() => Awaitable<H | { default: H }>)
| string
| URL,
transformer?: (handlerOrModule: unknown) => Awaitable<unknown>,
): (req: Request, ...args: A) => Promise<Response | null> {
let handlerPromise: Promise<H | null> | undefined = undefined;
let handler: H | null | undefined = undefined;
Expand All @@ -36,14 +39,18 @@ export function lazy<
};

async function init() {
const loaded = typeof handlerLoader === "string"
let loaded = typeof handlerLoader === "string"
? await import(handlerLoader)
: handlerLoader instanceof URL
? await import(handlerLoader.href)
: typeof handlerLoader === "function"
? await handlerLoader()
: undefined;

if (transformer) {
loaded = await transformer(loaded);
}

if (typeof loaded === "function") {
return loaded;
} else if (typeof loaded?.default === "function") {
Expand Down

0 comments on commit 238b983

Please sign in to comment.