Skip to content

Commit 331875f

Browse files
authored
feat: natively mount h3 as sub-app (#1129)
1 parent 57bca28 commit 331875f

File tree

4 files changed

+92
-12
lines changed

4 files changed

+92
-12
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { H3, serve, redirect, withBase } from "h3";
1+
import { H3, serve, redirect } from "h3";
22

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

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

99
serve(app);

src/h3.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,32 @@ export const H3Core = /* @__PURE__ */ (() => {
104104
});
105105
}
106106

107-
mount(base: string, input: FetchHandler | { fetch: FetchHandler }): H3Type {
108-
const fetchHandler = "fetch" in input ? input.fetch : input;
109-
this.all(`${base}/**`, (event) => {
110-
const url = new URL(event.url);
111-
url.pathname = url.pathname.slice(base.length) || "/";
112-
return fetchHandler(new Request(url, event.req));
113-
});
107+
mount(
108+
base: string,
109+
input: FetchHandler | { fetch: FetchHandler } | H3Type,
110+
): H3Type {
111+
if ("handler" in input) {
112+
if (input._middleware.length > 0) {
113+
this._middleware.push((event, next) => {
114+
return event.url.pathname.startsWith(base)
115+
? callMiddleware(event, input._middleware, next)
116+
: next();
117+
});
118+
}
119+
for (const r of input._routes) {
120+
this._addRoute({
121+
...r,
122+
route: base + r.route,
123+
});
124+
}
125+
} else {
126+
const fetchHandler = "fetch" in input ? input.fetch : input;
127+
this.all(`${base}/**`, (event) => {
128+
const url = new URL(event.url);
129+
url.pathname = url.pathname.slice(base.length) || "/";
130+
return fetchHandler(new Request(url, event.req));
131+
});
132+
}
114133
return this as unknown as H3Type;
115134
}
116135

src/types/h3.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export declare class H3 {
6969
/**
7070
* @internal
7171
*/
72-
_routes?: H3Route[];
72+
_routes: H3Route[];
7373

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

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

128132
/**
129133
* Register a global middleware.

test/mount.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { H3 } from "../src/h3.ts";
2+
import { describeMatrix } from "./_setup.ts";
3+
4+
describeMatrix("mount", (t, { it, expect, describe }) => {
5+
describe("mount fetch", () => {
6+
it("works with fetch function passed", async () => {
7+
t.app.mount("/test", (req) => new Response(new URL(req.url).pathname));
8+
expect(await t.fetch("/test").then((r) => r.text())).toBe("/");
9+
expect(await t.fetch("/test/").then((r) => r.text())).toBe("/");
10+
expect(await t.fetch("/test/123").then((r) => r.text())).toBe("/123");
11+
});
12+
13+
it("works with compat object", async () => {
14+
t.app.mount("/test", {
15+
fetch: (req: Request) => new Response(new URL(req.url).pathname),
16+
});
17+
expect(await t.fetch("/test/123").then((r) => r.text())).toBe("/123");
18+
});
19+
});
20+
21+
describe("mount H3", () => {
22+
it("works with H3 handler", async () => {
23+
t.app.mount(
24+
"/test",
25+
new H3()
26+
.use((event) => {
27+
event.res.headers.set("x-test", "1");
28+
})
29+
.use((event) => {
30+
if (event.url.pathname === "/test/intercept") {
31+
return "intercepted";
32+
}
33+
})
34+
.get("/**:slug", (event) => ({
35+
url: event.url.pathname,
36+
slug: event.context.params?.slug,
37+
})),
38+
);
39+
40+
expect(t.app._routes).toHaveLength(1);
41+
expect(t.app._routes[0].route).toBe("/test/**:slug");
42+
43+
expect(t.app._middleware).toHaveLength(1);
44+
45+
const res = await t.fetch("/test/123");
46+
expect(res.headers.get("x-test")).toBe("1");
47+
expect(await res.json()).toMatchObject({
48+
url: "/test/123",
49+
slug: "123",
50+
});
51+
52+
const interceptRes = await t.fetch("/test/intercept");
53+
expect(interceptRes.headers.get("x-test")).toBe("1");
54+
expect(await interceptRes.text()).toBe("intercepted");
55+
});
56+
});
57+
});

0 commit comments

Comments
 (0)