Skip to content

Commit

Permalink
feat(createHandlerContext): support rendering page components (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
uki00a authored Sep 23, 2023
1 parent 53dda73 commit ed2c97f
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 15 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ You can test async route components by combining `createRouteContext()` and
`render()`:

```ts
import { cleanup, render, setup } from "$fresh-testing-library/components.ts";
import {
cleanup,
getByText,
render,
setup,
} from "$fresh-testing-library/components.ts";
import { createRouteContext } from "$fresh-testing-library/server.ts";
import { assertExists } from "$std/assert/assert_exists.ts";
import { afterEach, beforeAll, describe, it } from "$std/testing/bdd.ts";
Expand All @@ -150,7 +155,8 @@ describe("routes/users/[id].tsx", () => {
const req = new Request("http://localhost:8000/users/2");
const ctx = createRouteContext<void>(req, { manifest });
const screen = render(await UserDetail(req, ctx));
assertExists(screen.getByText("Hello bar!"));
const group = screen.getByRole("group");
assertExists(getByText(group, "bar"));
});
});
```
Expand Down
11 changes: 9 additions & 2 deletions async-route-components.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createRouteContext } from "$fresh-testing-library/server.ts";
import { cleanup, render, setup } from "$fresh-testing-library/components.ts";
import {
cleanup,
getByText,
render,
setup,
} from "$fresh-testing-library/components.ts";
import { assertExists } from "$std/assert/assert_exists.ts";
import { afterEach, beforeAll, describe, it } from "$std/testing/bdd.ts";
import { default as UserDetail } from "./demo/routes/users/[id].tsx";
Expand All @@ -13,6 +18,8 @@ describe("testing async route components", () => {
const req = new Request("http://localhost:8000/users/2");
const ctx = createRouteContext<void>(req, { manifest });
const screen = render(await UserDetail(req, ctx));
assertExists(screen.getByText("Hello bar!"));
const list = screen.getByRole("group");
assertExists(getByText(list, "2"));
assertExists(getByText(list, "bar"));
});
});
9 changes: 6 additions & 3 deletions demo/routes/users/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ export default defineRoute((_, ctx) => {
}

return (
<div>
Hello {maybeUser}!
</div>
<dl role="group">
<dt>ID</dt>
<dd>{ctx.params.id}</dd>
<dt>Name</dt>
<dd>{maybeUser}</dd>
</dl>
);
});
1 change: 1 addition & 0 deletions deps/cheerio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as cheerio } from "https://esm.sh/[email protected]?pin=v132";
26 changes: 24 additions & 2 deletions internal/fresh/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { assertType } from "$std/testing/types.ts";
import {
determineRoute,
determineRouteDestinationKind,
findMatchingRouteFromManifest,
freshPathToURLPattern,
} from "./mod.ts";

Expand Down Expand Up @@ -57,7 +58,7 @@ describe("$fresh-testing-library/_util", () => {

it("should return a `path-to-regexp` pattern based on `manifest`", async () => {
const request = new Request("http://localhost:9876/api/users/1234");
const { default: manifest } = await import("../../demo/fresh.gen.ts");
const manifest = await loadManifest();
assertEquals(determineRoute(request, manifest), "/api/users/:id");
});
});
Expand All @@ -71,9 +72,30 @@ describe("$fresh-testing-library/_util", () => {
]
) {
it(`should return "${expected}" for "${given}"`, async () => {
const { default: manifest } = await import("../../demo/fresh.gen.ts");
const manifest = await loadManifest();
assertEquals(determineRouteDestinationKind(given, manifest), expected);
});
}
});

describe("findMatchingRouteFromManifest", () => {
it(`should return the maching route`, async () => {
const request = new Request("http://localhost:9876/users/9876");
const manifest = await loadManifest();
const route = findMatchingRouteFromManifest(request, manifest);
assertEquals(route, manifest.routes["./routes/users/[id].tsx"]);
});

it(`should return \`null\` when no route matches`, async () => {
const request = new Request("http://localhost:9876/users/9876/foo");
const manifest = await loadManifest();
const route = findMatchingRouteFromManifest(request, manifest);
assertEquals(route, null);
});
});
});

async function loadManifest() {
const { default: manifest } = await import("../../demo/fresh.gen.ts");
return manifest;
}
75 changes: 72 additions & 3 deletions internal/fresh/mod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { extname } from "node:path";
import type { Manifest, RouteConfig, RouteContext } from "$fresh/server.ts";
import { h } from "preact";
import { render } from "preact-render-to-string";
import type {
Manifest,
MiddlewareHandler,
RouteConfig,
RouteContext,
} from "$fresh/server.ts";

const routeExtnames = [".tsx", ".jsx", ".mts", ".ts", ".js", ".mjs"];
const kFreshPathPrefix = "./routes" as const;
Expand Down Expand Up @@ -42,6 +49,21 @@ function freshPathToPathToRegexPattern(path: FreshPath): string {
: removeSuffix(pattern, "/");
}

export function findMatchingRouteFromManifest(
request: Request,
manifest: Manifest,
): Manifest["routes"][string] | null {
const maybeRouteAndPattern = findMatchingRouteAndPathPatternFromManifest(
request,
manifest,
);
if (maybeRouteAndPattern == null) {
return null;
}
const [route] = maybeRouteAndPattern;
return route;
}

export function determineRoute(
request: Request,
manifest?: Manifest,
Expand All @@ -50,6 +72,24 @@ export function determineRoute(
return "/";
}

const maybeRouteAndPathPattern = findMatchingRouteAndPathPatternFromManifest(
request,
manifest,
);
if (maybeRouteAndPathPattern == null) {
return "/";
}

const [, pattern] = maybeRouteAndPathPattern;
return pattern;
}

type MatchingRouteAndPathPattern = [Manifest["routes"][string], string];

function findMatchingRouteAndPathPatternFromManifest(
request: Request,
manifest: Manifest,
): MatchingRouteAndPathPattern | null {
const url = new URL(request.url);
for (const freshPath of extractFreshPaths(manifest)) {
const module = manifest.routes[freshPath];
Expand All @@ -62,11 +102,40 @@ export function determineRoute(
const pattern = new URLPattern({ pathname: pathToRegexpPattern });
const match = pattern.exec(url.href);
if (match) {
return pathToRegexpPattern;
return [module, pathToRegexpPattern];
}
}
return null;
}
type MiddlewareModule = {
handler: MiddlewareHandler | Array<MiddlewareHandler>;
};
type RouteModule = Exclude<Manifest["routes"][string], MiddlewareModule>;
export function isRouteModule(
module: Manifest["routes"][string],
): module is RouteModule {
return (module as RouteModule)
.default != null;
}

return "/";
export async function renderRouteComponent(
routeComponent: Required<RouteModule>["default"],
request: Request,
ctx: RouteContext,
): Promise<Response> {
const result = await routeComponent(
request,
ctx,
);
if (result instanceof Response) {
return result;
}
const html = render(h("div", {}, result));
return new Response(html, {
headers: {
"Content-Type": "text/html; charset=UTF-8",
},
});
}

function removeExtname(path: string, knownExtnames: Array<string>): string {
Expand Down
17 changes: 17 additions & 0 deletions server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { assert } from "$std/assert/assert.ts";
import { assertEquals } from "$std/assert/assert_equals.ts";
import { describe, it } from "$std/testing/bdd.ts";

import { cheerio } from "./deps/cheerio.ts";

const defaultDummyLocalPort = 8020;
const defaultDummyRemotePort = 49152;

Expand Down Expand Up @@ -153,6 +155,21 @@ describe("$fresh-testing-library/server", () => {
assertEquals(ctx.params, {});
}
});

it("can render a page component from `Request` based on `manifest` option", async () => {
const manifest = await loadManifest();
const req = new Request("http://localhost:8003/users/1");
const ctx = createHandlerContext(req, { manifest });
const res = await ctx.render();
assertEquals(res.status, 200);
assertEquals(res.headers.get("Content-Type"), "text/html; charset=UTF-8");

const $ = cheerio.load(await res.text());
const $dd = $("dd");
assertEquals($dd.length, 2);
assertEquals($dd.eq(0).text(), "1");
assertEquals($dd.eq(1).text(), "foo");
});
});

describe("createRouteContext", () => {
Expand Down
35 changes: 33 additions & 2 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
determineRoute,
determineRouteDestinationKind,
extractParams,
findMatchingRouteFromManifest,
isRouteModule,
renderRouteComponent,
} from "./internal/fresh/mod.ts";

interface CreateHandlerContextOptions<
Expand Down Expand Up @@ -87,19 +90,47 @@ export function createHandlerContext<
const {
params,
state = {} as TState,
response = createDefaultResponse(),
response,
responseNotFound = createNotFoundResponse(),
localAddr = createDefaultLocalAddr(url),
remoteAddr = createDefaultRemoteAddr(url),
manifest,
} = options ?? {};

function createRender() {
if (response instanceof Response) {
return () => response;
}

if (manifest) {
return async () => {
const route = await findMatchingRouteFromManifest(request, manifest);
if (route && isRouteModule(route)) {
const routeComponent = route.default;
if (routeComponent == null) {
return createDefaultResponse();
}

return renderRouteComponent(
routeComponent,
request,
createRouteContext(request, { manifest }),
);
}

return createDefaultResponse();
};
}

return () => createDefaultResponse();
}

return {
params: params ?? (manifest ? extractParams(request, manifest) : {}),
state,
localAddr,
remoteAddr,
render: response instanceof Response ? () => response : response,
render: createRender(),
renderNotFound: responseNotFound instanceof Response
? () => responseNotFound
: responseNotFound,
Expand Down
3 changes: 2 additions & 1 deletion tools/check_imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ function isAllowedSpecifier(specifier: string): boolean {
}

// Bare specifiers
return specifier.startsWith("$fresh/");
return specifier.startsWith("$fresh/") ||
specifier === "preact-render-to-string" || specifier === "preact";
}

function isRelative(specifier: string): boolean {
Expand Down

0 comments on commit ed2c97f

Please sign in to comment.