Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/nested-router.mjs → examples/nested-app.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { H3, serve, redirect, withBase } from "h3";
import { H3, serve, redirect } from "h3";

const nestedApp = new H3().get("/test", () => "/test (sub app)");

const app = new H3()
.get("/", (event) => redirect(event, "/api/test"))
.all("/api/**", withBase("/api", nestedApp.handler));
.mount("/api", nestedApp);

serve(app);
33 changes: 26 additions & 7 deletions src/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,32 @@
});
}

mount(base: string, input: FetchHandler | { fetch: FetchHandler }): H3Type {
const fetchHandler = "fetch" in input ? input.fetch : input;
this.all(`${base}/**`, (event) => {
const url = new URL(event.url);
url.pathname = url.pathname.slice(base.length) || "/";
return fetchHandler(new Request(url, event.req));
});
mount(
base: string,
input: FetchHandler | { fetch: FetchHandler } | H3Type,
): H3Type {
if ("handler" in input) {
if (input._middleware.length > 0) {
this._middleware.push((event, next) => {
return event.url.pathname.startsWith(base)
? callMiddleware(event, input._middleware, next)
: next();

Check warning on line 116 in src/h3.ts

View check run for this annotation

Codecov / codecov/patch

src/h3.ts#L116

Added line #L116 was not covered by tests
});
}
for (const r of input._routes) {
this._addRoute({
...r,
route: base + r.route,
});
}
} else {
const fetchHandler = "fetch" in input ? input.fetch : input;
this.all(`${base}/**`, (event) => {
const url = new URL(event.url);
url.pathname = url.pathname.slice(base.length) || "/";
return fetchHandler(new Request(url, event.req));
});
}
return this as unknown as H3Type;
}

Expand Down
10 changes: 7 additions & 3 deletions src/types/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export declare class H3 {
/**
* @internal
*/
_routes?: H3Route[];
_routes: H3Route[];

/**
* H3 instance config.
Expand Down Expand Up @@ -121,9 +121,13 @@ export declare class H3 {
handler(event: H3Event): unknown | Promise<unknown>;

/**
* Mount a `.fetch` compatible server (like Hono or Elysia) to the H3 app.
* Mount an H3 app or a `.fetch` compatible server (like Hono or Elysia) with a base prefix.
*
* When mounting a sub-app, all routes will be added with base prefix and global middleware will be added as one prefixed middleware.
*
* **Note:** Sub-app options and global hooks are not inherited by the mounted app please consider setting them in the main app directly.
*/
mount(base: string, input: FetchHandler | { fetch: FetchHandler }): this;
mount(base: string, input: FetchHandler | { fetch: FetchHandler } | H3): this;

/**
* Register a global middleware.
Expand Down
57 changes: 57 additions & 0 deletions test/mount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { H3 } from "../src/h3.ts";
import { describeMatrix } from "./_setup.ts";

describeMatrix("mount", (t, { it, expect, describe }) => {
describe("mount fetch", () => {
it("works with fetch function passed", async () => {
t.app.mount("/test", (req) => new Response(new URL(req.url).pathname));
expect(await t.fetch("/test").then((r) => r.text())).toBe("/");
expect(await t.fetch("/test/").then((r) => r.text())).toBe("/");
expect(await t.fetch("/test/123").then((r) => r.text())).toBe("/123");
});

it("works with compat object", async () => {
t.app.mount("/test", {
fetch: (req: Request) => new Response(new URL(req.url).pathname),
});
expect(await t.fetch("/test/123").then((r) => r.text())).toBe("/123");
});
});

describe("mount H3", () => {
it("works with H3 handler", async () => {
t.app.mount(
"/test",
new H3()
.use((event) => {
event.res.headers.set("x-test", "1");
})
.use((event) => {
if (event.url.pathname === "/test/intercept") {
return "intercepted";
}
})
.get("/**:slug", (event) => ({
url: event.url.pathname,
slug: event.context.params?.slug,
})),
);

expect(t.app._routes).toHaveLength(1);
expect(t.app._routes[0].route).toBe("/test/**:slug");

expect(t.app._middleware).toHaveLength(1);

const res = await t.fetch("/test/123");
expect(res.headers.get("x-test")).toBe("1");
expect(await res.json()).toMatchObject({
url: "/test/123",
slug: "123",
});

const interceptRes = await t.fetch("/test/intercept");
expect(interceptRes.headers.get("x-test")).toBe("1");
expect(await interceptRes.text()).toBe("intercepted");
});
});
});