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
63 changes: 30 additions & 33 deletions src/build/virtual/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default function app(nitro: Nitro) {
const hasGlobalMiddleware = nitro.routing.globalMiddleware.length > 0;
const hasPlugins = nitro.options.plugins.length > 0;
const hasHooks = nitro.options.features?.runtimeHooks ?? hasPlugins;
const hasGetMiddleware = hasRouteRules || hasRoutedMiddleware;
const hasAsyncContext = !!nitro.options.experimental.asyncContext;

const routingImports = [
Expand All @@ -23,7 +22,9 @@ export default function app(nitro: Nitro) {
const code: string[] = [];

imports.push(
`import { H3Core } from "h3";`,
hasRouteRules || hasRoutedMiddleware
? `import { composeMiddleware, H3Core } from "h3";`
: `import { H3Core } from "h3";`,
`import errorHandler from "#nitro/virtual/error-handler";`
);

Expand Down Expand Up @@ -143,42 +144,38 @@ export default function app(nitro: Nitro) {
` h3App["~findRoute"] = (event) => findRoute(event.req.method, event.url.pathname);`
);
}
// Route rules, global and routed middleware are registered on `~middleware`
// in that order so h3 precomposes the chain once; the two path-dependent
// sources cache their composed chain on the memoized match result.
if (hasRouteRules) {
imports.push(`import { getRouteRules } from "#nitro/runtime/app";`);
code.push(
` h3App["~middleware"].push((event, next) => {`,
` const routeRules = getRouteRules(event.req.method, event.url.pathname);`,
` event.context.routeRules = routeRules?.routeRules;`,
` const middleware = routeRules?.routeRuleMiddleware;`,
` if (!middleware?.length) {`,
` return next();`,
` }`,
` return (routeRules["~composed"] ??= composeMiddleware(middleware))(event, next);`,
` });`
);
}
if (hasGlobalMiddleware) {
code.push(` h3App["~middleware"].push(...globalMiddleware);`);
}
if (hasGetMiddleware) {
if (hasRoutedMiddleware) {
imports.push(`import { memoizeRouteRulesMatcher } from "h3/rules";`);
code.push(
` h3App["~getMiddleware"] = (event, route) => {`,
` const pathname = event.url.pathname;`,
` const method = event.req.method;`,
` const middleware = [];`
` const matchRoutedMiddleware = memoizeRouteRulesMatcher(findRoutedMiddleware);`,
` h3App["~middleware"].push((event, next) => {`,
` const matched = matchRoutedMiddleware(event.req.method, event.url.pathname);`,
` if (matched.length === 0) {`,
` return next();`,
` }`,
` return (matched["~composed"] ??= composeMiddleware(matched.map((r) => r.data)))(event, next);`,
` });`
);
if (hasRouteRules) {
imports.push(`import { getRouteRules } from "#nitro/runtime/app";`);
code.push(
` const routeRules = getRouteRules(method, pathname);`,
` event.context.routeRules = routeRules?.routeRules;`,
` if (routeRules?.routeRuleMiddleware.length) {`,
` middleware.push(...routeRules.routeRuleMiddleware);`,
` }`
);
}
if (hasGlobalMiddleware) {
code.push(` middleware.push(...h3App["~middleware"]);`);
}
if (hasRoutedMiddleware) {
code.push(
` middleware.push(...findRoutedMiddleware(method, pathname).map((r) => r.data));`
);
}
if (hasRoutes) {
code.push(
` if (route?.data?.middleware?.length) {`,
` middleware.push(...route.data.middleware);`,
` }`
);
}
code.push(` return middleware;`, ` };`);
}
code.push(` return h3App;`, `}`);

Expand Down
66 changes: 66 additions & 0 deletions test/unit/virtual-app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import type { Nitro } from "nitro/types";

import app from "../../src/build/virtual/app.ts";

function createNitroStub(opts: {
routeRules?: boolean;
routedMiddleware?: boolean;
globalMiddleware?: boolean;
}): Nitro {
return {
options: {
plugins: [],
experimental: {},
},
routing: {
routes: { hasRoutes: () => true },
routeRules: { hasRoutes: () => !!opts.routeRules },
routedMiddleware: { hasRoutes: () => !!opts.routedMiddleware },
globalMiddleware: opts.globalMiddleware ? [{}] : [],
},
} as unknown as Nitro;
}

describe("virtual/app template", () => {
it("does not override `~getMiddleware`, so h3 can precompose the middleware chain", () => {
const template = app(
createNitroStub({ routeRules: true, routedMiddleware: true, globalMiddleware: true })
).template();
expect(template).not.toContain("~getMiddleware");
});

it("registers route rules, global and routed middleware in that order", () => {
const template = app(
createNitroStub({ routeRules: true, routedMiddleware: true, globalMiddleware: true })
).template();
const routeRules = template.indexOf("getRouteRules(event.req.method");
const global = template.indexOf("push(...globalMiddleware)");
const routed = template.indexOf("matchRoutedMiddleware(event.req.method");
expect(routeRules).toBeGreaterThan(-1);
expect(global).toBeGreaterThan(routeRules);
expect(routed).toBeGreaterThan(global);
});

it("keeps exposing matched route rules on `event.context.routeRules`", () => {
const template = app(createNitroStub({ routeRules: true })).template();
expect(template).toContain("event.context.routeRules = routeRules?.routeRules");
});

it("composes the per-path chains once and caches them on the memoized match", () => {
const template = app(createNitroStub({ routeRules: true, routedMiddleware: true })).template();
expect(template).toContain('import { composeMiddleware, H3Core } from "h3";');
expect(template).toContain('import { memoizeRouteRulesMatcher } from "h3/rules";');
expect(template).toContain("memoizeRouteRulesMatcher(findRoutedMiddleware)");
expect(template).toContain('routeRules["~composed"] ??= composeMiddleware(');
expect(template).toContain('matched["~composed"] ??= composeMiddleware(');
});

it("does not import the composition helpers when nothing is path-dependent", () => {
const template = app(createNitroStub({ globalMiddleware: true })).template();
expect(template).toContain('import { H3Core } from "h3";');
expect(template).not.toContain("composeMiddleware");
expect(template).not.toContain("h3/rules");
expect(template).toContain("push(...globalMiddleware)");
});
});
Loading