Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add additional metadata to RoutingResult #673

Merged
merged 8 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion packages/open-next/src/adapters/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import path from "node:path";

import { debug } from "../logger";
import {
loadAppPathsManifest,
loadAppPathsManifestKeys,
loadBuildId,
loadConfig,
loadConfigHeaders,
loadHtmlPages,
loadMiddlewareManifest,
loadPrerenderManifest,
// loadPublicAssets,
loadRoutesManifest,
} from "./util.js";

Expand All @@ -31,3 +31,4 @@ export const AppPathsManifestKeys =
/* @__PURE__ */ loadAppPathsManifestKeys(NEXT_DIR);
export const MiddlewareManifest =
/* @__PURE__ */ loadMiddlewareManifest(NEXT_DIR);
export const AppPathsManifest = loadAppPathsManifest(NEXT_DIR);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need /* @__PURE__ */ here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not for all of them, but this is necessary for some part. Without it esbuild fail to remove some of the unused one and it can cause error at runtime

11 changes: 6 additions & 5 deletions packages/open-next/src/adapters/config/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function loadPrerenderManifest(nextDir: string) {
return JSON.parse(json) as PrerenderManifest;
}

export function loadAppPathsManifestKeys(nextDir: string) {
export function loadAppPathsManifest(nextDir: string) {
const appPathsManifestPath = path.join(
nextDir,
"server",
Expand All @@ -86,10 +86,11 @@ export function loadAppPathsManifestKeys(nextDir: string) {
const appPathsManifestJson = fs.existsSync(appPathsManifestPath)
? fs.readFileSync(appPathsManifestPath, "utf-8")
: "{}";
const appPathsManifest = JSON.parse(appPathsManifestJson) as Record<
string,
string
>;
return JSON.parse(appPathsManifestJson) as Record<string, string>;
}

export function loadAppPathsManifestKeys(nextDir: string) {
const appPathsManifest = loadAppPathsManifest(nextDir);
return Object.keys(appPathsManifest).map((key) => {
// Remove parallel route
let cleanedKey = key.replace(/\/@[^\/]+/g, "");
Expand Down
18 changes: 16 additions & 2 deletions packages/open-next/src/adapters/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import {
resolveQueue,
resolveTagCache,
} from "../core/resolve";
import routingHandler from "../core/routingHandler";
import routingHandler, {
INTERNAL_HEADER_INITIAL_PATH,
INTERNAL_HEADER_RESOLVED_ROUTE,
INTERNAL_HEADER_ROUTE_TYPE,
} from "../core/routingHandler";

globalThis.internalFetch = fetch;
globalThis.__openNextAls = new AsyncLocalStorage();
Expand Down Expand Up @@ -57,10 +61,19 @@ const defaultHandler = async (
);
return {
type: "middleware",
internalEvent: result.internalEvent,
internalEvent: {
...result.internalEvent,
headers: {
...result.internalEvent.headers,
[INTERNAL_HEADER_INITIAL_PATH]: internalEvent.rawPath,
[INTERNAL_HEADER_RESOLVED_ROUTE]: result.resolvedRoute ?? "",
[INTERNAL_HEADER_ROUTE_TYPE]: result.routeType ?? "",
},
},
isExternalRewrite: result.isExternalRewrite,
origin,
isISR: result.isISR,
initialPath: result.initialPath,
};
}
try {
Expand All @@ -79,6 +92,7 @@ const defaultHandler = async (
isExternalRewrite: false,
origin: false,
isISR: result.isISR,
initialPath: result.internalEvent.rawPath,
};
}
}
Expand Down
18 changes: 18 additions & 0 deletions packages/open-next/src/core/requestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IncomingMessage } from "http/index.js";
import type {
InternalEvent,
InternalResult,
RouteType,
RoutingResult,
StreamCreator,
} from "types/open-next";
Expand All @@ -14,6 +15,9 @@ import { debug, error, warn } from "../adapters/logger";
import { patchAsyncStorage } from "./patchAsyncStorage";
import { convertRes, createServerResponse } from "./routing/util";
import routingHandler, {
INTERNAL_HEADER_INITIAL_PATH,
INTERNAL_HEADER_RESOLVED_ROUTE,
INTERNAL_HEADER_ROUTE_TYPE,
MIDDLEWARE_HEADER_PREFIX,
MIDDLEWARE_HEADER_PREFIX_LEN,
} from "./routingHandler";
Expand All @@ -37,11 +41,23 @@ export async function openNextHandler(
}
debug("internalEvent", internalEvent);

// These 3 will get overwritten by the routing handler if not using an external middleware
const internalHeaders = {
initialPath:
internalEvent.headers[INTERNAL_HEADER_INITIAL_PATH] ??
conico974 marked this conversation as resolved.
Show resolved Hide resolved
internalEvent.rawPath,
resolvedRoute: internalEvent.headers[INTERNAL_HEADER_RESOLVED_ROUTE],
routeType: internalEvent.headers[INTERNAL_HEADER_ROUTE_TYPE] as
| RouteType
| undefined,
};

let routingResult: InternalResult | RoutingResult = {
internalEvent,
isExternalRewrite: false,
origin: false,
isISR: false,
...internalHeaders,
};

//#override withRouting
Expand Down Expand Up @@ -94,6 +110,8 @@ export async function openNextHandler(
isExternalRewrite: false,
isISR: false,
origin: false,
initialPath: internalEvent.rawPath,
resolvedRoute: "/500",
};
}
}
Expand Down
129 changes: 91 additions & 38 deletions packages/open-next/src/core/routingHandler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {
AppPathsManifest,
BuildId,
ConfigHeaders,
PrerenderManifest,
RoutesManifest,
} from "config/index";
import type { RouteDefinition } from "types/next-types";
import type {
InternalEvent,
InternalResult,
RouteType,
RoutingResult,
} from "types/open-next";

Expand All @@ -22,6 +25,10 @@ import {
import { handleMiddleware } from "./routing/middleware";

export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
export const INTERNAL_HEADER_PREFIX = "x-opennext-";
export const INTERNAL_HEADER_INITIAL_PATH = `${INTERNAL_HEADER_PREFIX}initial-path`;
export const INTERNAL_HEADER_RESOLVED_ROUTE = `${INTERNAL_HEADER_PREFIX}resolved-route`;
export const INTERNAL_HEADER_ROUTE_TYPE = `${INTERNAL_HEADER_PREFIX}route-type`;
export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
conico974 marked this conversation as resolved.
Show resolved Hide resolved

// Add the locale prefix to the regex so we correctly match the rawPath
Expand All @@ -39,23 +46,49 @@ const apiPrefix = RoutesManifest.basePath
? `${RoutesManifest.basePath}/api`
: "/api";

const staticRegexp = RoutesManifest.routes.static.map(
(route) =>
new RegExp(
route.regex
.replace("^/", optionalLocalePrefixRegex)
.replace("^/", optionalBasepathPrefixRegex),
),
);

const dynamicRegexp = RoutesManifest.routes.dynamic.map(
(route) =>
new RegExp(
route.regex
.replace("^/", optionalLocalePrefixRegex)
.replace("^/", optionalBasepathPrefixRegex),
),
);
export function routeMatcher(routeDefinitions: RouteDefinition[]) {
const regexp = routeDefinitions.map((route) => {
return {
page: route.page,
regexp: new RegExp(
route.regex
.replace("^/", optionalLocalePrefixRegex)
.replace("^/", optionalBasepathPrefixRegex),
),
};
});
conico974 marked this conversation as resolved.
Show resolved Hide resolved
const appPathsSet = new Set(
Object.keys(AppPathsManifest)
.filter((key) => key.endsWith("/page"))
// Remove the /page suffix
.map((key) => key.replace(/\/page$/, "")),
);
const routePathsSet = new Set(
Object.keys(AppPathsManifest)
.filter((key) => key.endsWith("/route"))
// Remove the /route suffix
.map((key) => key.replace(/\/route$/, "")),
);
return function matchRoute(path: string) {
const foundRoute = regexp.find((route) => route.regexp.test(path));
let routeType: RouteType | undefined = undefined;
if (foundRoute) {
routeType = appPathsSet.has(foundRoute.page)
? "app"
: routePathsSet.has(foundRoute.page)
? "route"
: "page";
return {
page: foundRoute.page,
routeType,
};
}
return false;
};
}

const staticRouteMatcher = routeMatcher(RoutesManifest.routes.static);
const dynamicRouteMatcher = routeMatcher(RoutesManifest.routes.dynamic);

// Geolocation headers starting from Nextjs 15
// See https://github.com/vercel/vercel/blob/7714b1c/packages/functions/src/headers.ts
Expand Down Expand Up @@ -95,6 +128,17 @@ export default async function routingHandler(
}
}

// First we remove internal headers
// We don't want to allow users to set these headers
for (const key of Object.keys(event.headers)) {
if (
key.startsWith(INTERNAL_HEADER_PREFIX) ||
key.startsWith(MIDDLEWARE_HEADER_PREFIX)
) {
delete event.headers[key];
}
}

const nextHeaders = getNextConfigHeaders(event, ConfigHeaders);

let internalEvent = fixDataPage(event, BuildId);
Expand Down Expand Up @@ -127,12 +171,8 @@ export default async function routingHandler(
internalEvent = beforeRewrites.internalEvent;
isExternalRewrite = beforeRewrites.isExternalRewrite;
}

const isStaticRoute =
!isExternalRewrite &&
staticRegexp.some((route) =>
route.test((internalEvent as InternalEvent).rawPath),
);
const foundStaticRoute = staticRouteMatcher(internalEvent.rawPath);
const isStaticRoute = !isExternalRewrite && Boolean(foundStaticRoute);

if (!isStaticRoute && !isExternalRewrite) {
conico974 marked this conversation as resolved.
Show resolved Hide resolved
// Second rewrite to be applied
Expand All @@ -151,12 +191,10 @@ export default async function routingHandler(
);
internalEvent = fallbackEvent;

const isDynamicRoute =
!isExternalRewrite &&
dynamicRegexp.some((route) =>
route.test((internalEvent as InternalEvent).rawPath),
);
if (!isDynamicRoute && !isStaticRoute && !isExternalRewrite) {
const foundDynamicRoute = dynamicRouteMatcher(internalEvent.rawPath);
const isDynamicRoute = !isExternalRewrite && Boolean(foundDynamicRoute);

if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) {
// Fallback rewrite to be applied
const fallbackRewrites = handleRewrites(
internalEvent,
Expand All @@ -181,15 +219,13 @@ export default async function routingHandler(
// If we still haven't found a route, we show the 404 page
// We need to ensure that rewrites are applied before showing the 404 page
if (
!isRouteFoundBeforeAllRewrites &&
!isApiRoute &&
!isNextImageRoute &&
// We need to check again once all rewrites have been applied
!staticRegexp.some((route) =>
route.test((internalEvent as InternalEvent).rawPath),
) &&
!dynamicRegexp.some((route) =>
route.test((internalEvent as InternalEvent).rawPath),
!(
isRouteFoundBeforeAllRewrites ||
isApiRoute ||
isNextImageRoute ||
// We need to check again once all rewrites have been applied
staticRouteMatcher(internalEvent.rawPath) ||
dynamicRouteMatcher(internalEvent.rawPath)
)
) {
internalEvent = {
Expand Down Expand Up @@ -229,10 +265,27 @@ export default async function routingHandler(
...nextHeaders,
});

let resolvedRoute: string | undefined;
let routeType: RouteType | undefined;

if (foundStaticRoute) {
resolvedRoute = foundStaticRoute.page;
routeType = foundStaticRoute.routeType;
} else if (foundDynamicRoute) {
resolvedRoute = foundDynamicRoute.page;
routeType = foundDynamicRoute.routeType;
} else if (isApiRoute) {
// For /api paths we assume that they're route types
routeType = "route";
}

return {
internalEvent,
isExternalRewrite,
origin: false,
isISR,
initialPath: event.rawPath,
resolvedRoute,
routeType,
};
}
3 changes: 3 additions & 0 deletions packages/open-next/src/plugins/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Plugin } from "esbuild";
import type { MiddlewareInfo } from "types/next-types.js";

import {
loadAppPathsManifest,
loadAppPathsManifestKeys,
loadBuildId,
loadConfig,
Expand Down Expand Up @@ -167,6 +168,7 @@ ${contents}
const PrerenderManifest = loadPrerenderManifest(nextDir);
const AppPathsManifestKeys = loadAppPathsManifestKeys(nextDir);
const MiddlewareManifest = loadMiddlewareManifest(nextDir);
const AppPathsManifest = loadAppPathsManifest(nextDir);

const contents = `
import path from "node:path";
Expand All @@ -188,6 +190,7 @@ ${contents}
export const PrerenderManifest = ${JSON.stringify(PrerenderManifest)};
export const AppPathsManifestKeys = ${JSON.stringify(AppPathsManifestKeys)};
export const MiddlewareManifest = ${JSON.stringify(MiddlewareManifest)};
export const AppPathsManifest = ${JSON.stringify(AppPathsManifest)};

process.env.NEXT_BUILD_ID = BuildId;
`;
Expand Down
11 changes: 11 additions & 0 deletions packages/open-next/src/types/open-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,22 @@ export type IncludedConverter =
| "sqs-revalidate"
| "dummy";

export type RouteType = "route" | "page" | "app";

export interface RoutingResult {
internalEvent: InternalEvent;
// If the request is an external rewrite, if used with an external middleware will be false on every server function
isExternalRewrite: boolean;
// Origin is only used in external middleware, will be false on every server function
origin: Origin | false;
// If the request is for an ISR route, will be false on every server function. Only used in external middleware
isISR: boolean;
// The initial rawPath of the request before applying rewrites, if used with an external middleware will be defined in x-opennext-initial-path header
initialPath: string;
// The resolved route after applying rewrites, if used with an external middleware will be defined in x-opennext-resolved-route header
resolvedRoute?: string;
// The type of the resolved route, if used with an external middleware will be defined in x-opennext-route-type header
routeType?: RouteType;
}

export interface MiddlewareResult
Expand Down
Loading