Skip to content
Open
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
31 changes: 26 additions & 5 deletions src/build/virtual/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ import * as srvxNode from "srvx/node"
import * as h3 from "h3";${traceH3 ? `\nimport { wrapHandlerWithTracing } from "h3/tracing";` : ""}

${routeRulesModule}
${
traceH3
? `const wrapMiddlewareWithTracing = (middleware) => {
const { tracingChannel } = globalThis.process?.getBuiltinModule?.("diagnostics_channel") ?? {};
if (!tracingChannel || middleware.__traced__) return middleware;
const channel = tracingChannel("h3.request");
const wrapped = (...args) => channel.tracePromise(async () => middleware(...args), { event: args[0], type: "middleware" });
wrapped.__traced__ = true;
return wrapped;
};`
: ""
}
const multiHandler = (...handlers) => {
const final = handlers.pop()
const middleware = handlers.filter(Boolean).map(h => h3.toMiddleware(h));
Expand All @@ -48,7 +60,7 @@ ${allHandlers

export const findRoute = ${nitro.routing.routes.compileToString({ serialize: (h) => serializeHandler(h, { tracing: traceH3 }) })}

export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: serializeHandler, matchAll: true })};
export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: (h) => serializeHandlerFn(h, { tracing: traceH3, type: "middleware" }), matchAll: true })};

export const globalMiddleware = [
${nitro.routing.globalMiddleware.map((h) => (h.lazy ? h._importHash : `h3.toEventHandler(${h._importHash})`)).join(",")}
Expand All @@ -72,26 +84,35 @@ function serializeHandler(
): string {
const meta = Array.isArray(h) ? h[0] : h;
const handler = Array.isArray(h)
? `multiHandler(${h.map((handler) => serializeHandlerFn(handler)).join(",")})`
: serializeHandlerFn(h);
? `multiHandler(${h.map((handler) => serializeHandlerFn(handler, { ...opts, type: "route" })).join(",")})`
: serializeHandlerFn(h, { ...opts, type: "route" });

return `{${[
`route:${JSON.stringify(meta.route)}`,
meta.method && `method:${JSON.stringify(meta.method)}`,
meta.meta && `meta:${JSON.stringify(meta.meta)}`,
`handler:${opts.tracing ? `wrapHandlerWithTracing(${handler})` : handler}`,
`handler:${handler}`,
]
.filter(Boolean)
.join(",")}}`;
}

function serializeHandlerFn(h: NitroEventHandler & { _importHash: string }): string {
function serializeHandlerFn(
h: NitroEventHandler & { _importHash: string },
opts: { tracing?: boolean; type?: "route" | "middleware" } = {}
): string {
let code = h._importHash;
if (!h.lazy) {
if (h.format === "node") {
code = `srvxNode.toFetchHandler(${code})`;
}
code = `h3.toEventHandler(${code})`;
}
if (opts.tracing) {
code =
opts.type === "middleware"
? `wrapMiddlewareWithTracing(${code})`
: `wrapHandlerWithTracing(${code})`;
}
return code;
}
127 changes: 127 additions & 0 deletions test/unit/routed-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { afterAll, describe, expect, it } from "vitest";
import { createNitro, build, prepare } from "nitro/builder";
import { join } from "pathe";
import { fileURLToPath } from "node:url";
import { rm, mkdir } from "node:fs/promises";

const tmpDir = fileURLToPath(new URL("./.tmp/routed-middleware", import.meta.url));

describe("route-scoped middleware", () => {
afterAll(async () => {
await rm(tmpDir, { recursive: true, force: true });
});

it("executes route-scoped middleware without throwing fn is not a function", async () => {
const outDir = join(tmpDir, "output");
await rm(outDir, { recursive: true, force: true });
await mkdir(outDir, { recursive: true });

const nitro = await createNitro({
rootDir: tmpDir,
preset: "standard",
output: { dir: outDir },
virtual: {
"#auth-middleware": () =>
`export default (event) => { event.res.headers.set("x-auth-middleware", "true"); }`,
"#hello-handler": () => `export default () => ({ message: "hello" })`,
"#other-handler": () => `export default () => ({ message: "other" })`,
},
handlers: [
{
route: "/api/**",
handler: "#auth-middleware",
middleware: true,
},
{
route: "/api/hello",
handler: "#hello-handler",
},
{
route: "/other",
handler: "#other-handler",
},
],
});

await prepare(nitro);
await build(nitro);

const entry = join(outDir, "server/index.mjs");
const { fetch } = await import(entry).then((m) => m.default);

// Request matching routed middleware
const res = await fetch(new Request("http://localhost/api/hello"));
expect(res.status).toBe(200);
expect(res.headers.get("x-auth-middleware")).toBe("true");
expect(await res.json()).toEqual({ message: "hello" });

// Request not matching routed middleware
const resOther = await fetch(new Request("http://localhost/other"));
expect(resOther.status).toBe(200);
expect(resOther.headers.get("x-auth-middleware")).toBeNull();
expect(await resOther.json()).toEqual({ message: "other" });
});

it("executes middleware in route-rules -> global -> routed middleware order", async () => {
const outDir = join(tmpDir, "output-ordering");
await rm(outDir, { recursive: true, force: true });
await mkdir(outDir, { recursive: true });

const nitro = await createNitro({
rootDir: tmpDir,
preset: "standard",
output: { dir: outDir },
routeRules: {
"/api/**": {
headers: { "x-route-rule": "applied" },
},
},
virtual: {
"#global-middleware": () =>
`export default (event) => {
const current = event.res.headers.get("x-execution-order") || "";
event.res.headers.set("x-execution-order", current ? current + ", global" : "global");
}`,
"#routed-middleware": () =>
`export default (event) => {
const current = event.res.headers.get("x-execution-order") || "";
event.res.headers.set("x-execution-order", current + ", routed");
}`,
"#order-handler": () => `export default (event) => ({
status: "ok",
order: event.res.headers.get("x-execution-order"),
hasContextRouteRules: !!event.context?.routeRules,
})`,
},
handlers: [
{
handler: "#global-middleware",
middleware: true,
},
{
route: "/api/**",
handler: "#routed-middleware",
middleware: true,
},
{
route: "/api/order",
handler: "#order-handler",
},
],
});

await prepare(nitro);
await build(nitro);

const entry = join(outDir, "server/index.mjs");
const { fetch } = await import(entry).then((m) => m.default);

const res = await fetch(new Request("http://localhost/api/order"));
expect(res.status).toBe(200);
expect(res.headers.get("x-execution-order")).toBe("global, routed");
const data = await res.json();
expect(data.status).toBe("ok");
expect(data.order).toBe("global, routed");
expect(data.hasContextRouteRules).toBe(true);
});
});
65 changes: 65 additions & 0 deletions test/unit/virtual-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,69 @@ describe("virtual/routing template", () => {
expect(template).toContain(`import { wrapHandlerWithTracing } from "h3/tracing"`);
expect(template).toContain("wrapHandlerWithTracing(h3.toEventHandler(_abc123))");
});

it("serializes routed middleware as handler functions instead of route descriptor objects", () => {
const middlewareHandler: NitroEventHandler & { _importHash: string } = {
route: "/api/**",
handler: "/path/to/middleware.ts",
middleware: true,
_importHash: "_mid123",
};
const nitroStub = {
options: {
tracingChannel: undefined,
baseURL: "/",
routeRules: {},
},
routing: {
routes: {
routes: [],
compileToString: () => `{}`,
},
routedMiddleware: {
routes: [{ route: "/api/**", method: "", data: middlewareHandler }],
compileToString: ({ serialize }: { serialize: (h: unknown) => string }) =>
`{"/api/**":${serialize(middlewareHandler)}}`,
},
globalMiddleware: [],
},
} as unknown as Nitro;

const template = routing(nitroStub).template();
expect(template).toContain("export const findRoutedMiddleware =");
expect(template).toContain("h3.toEventHandler(_mid123)");
expect(template).not.toContain(`route:"/api/**"`);
});

it("wraps routed middleware with wrapMiddlewareWithTracing when tracingChannel.h3 is true", () => {
const middlewareHandler: NitroEventHandler & { _importHash: string } = {
route: "/api/**",
handler: "/path/to/middleware.ts",
middleware: true,
_importHash: "_mid123",
};
const nitroStub = {
options: {
tracingChannel: { h3: true },
baseURL: "/",
routeRules: {},
},
routing: {
routes: {
routes: [],
compileToString: () => `{}`,
},
routedMiddleware: {
routes: [{ route: "/api/**", method: "", data: middlewareHandler }],
compileToString: ({ serialize }: { serialize: (h: unknown) => string }) =>
`{"/api/**":${serialize(middlewareHandler)}}`,
},
globalMiddleware: [],
},
} as unknown as Nitro;

const template = routing(nitroStub).template();
expect(template).toContain("export const findRoutedMiddleware =");
expect(template).toContain("wrapMiddlewareWithTracing(h3.toEventHandler(_mid123))");
});
});