import { match } from "path-to-regexp"; //note: this explicitly does not include the * character, as pages requires this const escapeRegex = /[.+?^${}()|[\]\\]/g; type HTTPMethod = | "HEAD" | "OPTIONS" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; /* TODO: Grab these from @cloudflare/workers-types instead */ type Params

= Record; type EventContext = { request: Request; functionPath: string; waitUntil: (promise: Promise) => void; passThroughOnException: () => void; next: (input?: Request | string, init?: RequestInit) => Promise; env: Env & { ASSETS: { fetch: typeof fetch } }; params: Params

; data: Data; }; declare type PagesFunction< Env = unknown, P extends string = string, Data extends Record = Record > = (context: EventContext) => Response | Promise; /* end @cloudflare/workers-types */ type RouteHandler = { routePath: string; mountPath: string; method?: HTTPMethod; modules: PagesFunction[]; middlewares: PagesFunction[]; }; // inject `routes` via ESBuild declare const routes: RouteHandler[]; // define `__FALLBACK_SERVICE__` via ESBuild declare const __FALLBACK_SERVICE__: string; // expect an ASSETS fetcher binding pointing to the asset-server stage type FetchEnv = { [name: string]: { fetch: typeof fetch }; ASSETS: { fetch: typeof fetch }; }; type WorkerContext = { waitUntil: (promise: Promise) => void; passThroughOnException: () => void; }; function* executeRequest(request: Request) { const requestPath = new URL(request.url).pathname; // First, iterate through the routes (backwards) and execute "middlewares" on partial route matches for (const route of [...routes].reverse()) { if (route.method && route.method !== request.method) { continue; } // replaces with "\\$&", this prepends a backslash to the matched string, e.g. "[" becomes "\[" const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), { end: false, }); const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), { end: false, }); const matchResult = routeMatcher(requestPath); const mountMatchResult = mountMatcher(requestPath); if (matchResult && mountMatchResult) { for (const handler of route.middlewares.flat()) { yield { handler, params: matchResult.params as Params, path: mountMatchResult.path, }; } } } // Then look for the first exact route match and execute its "modules" for (const route of routes) { if (route.method && route.method !== request.method) { continue; } const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), { end: true, }); const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), { end: false, }); const matchResult = routeMatcher(requestPath); const mountMatchResult = mountMatcher(requestPath); if (matchResult && mountMatchResult && route.modules.length) { for (const handler of route.modules.flat()) { yield { handler, params: matchResult.params as Params, path: matchResult.path, }; } break; } } } export default { async fetch( originalRequest: Request, env: FetchEnv, workerContext: WorkerContext ) { let request = originalRequest; const handlerIterator = executeRequest(request); let data = {}; // arbitrary data the user can set between functions let isFailOpen = false; const next = async (input?: RequestInfo, init?: RequestInit) => { if (input !== undefined) { let url = input; if (typeof input === "string") { url = new URL(input, request.url).toString(); } request = new Request(url, init); } const result = handlerIterator.next(); // Note we can't use `!result.done` because this doesn't narrow to the correct type if (result.done === false) { const { handler, params, path } = result.value; const context = { request: new Request(request.clone()), functionPath: path, next, params, get data() { return data; }, set data(value) { if (typeof value !== "object" || value === null) { throw new Error("context.data must be an object"); } // user has overriden context.data, so we need to merge it with the existing data data = value; }, env, waitUntil: workerContext.waitUntil.bind(workerContext), passThroughOnException: () => { isFailOpen = true; }, }; const response = await handler(context); if (!(response instanceof Response)) { throw new Error("Your Pages function should return a Response"); } return cloneResponse(response); } else if (__FALLBACK_SERVICE__) { // There are no more handlers so finish with the fallback service (`env.ASSETS.fetch` in Pages' case) const response = await env[__FALLBACK_SERVICE__].fetch(request); return cloneResponse(response); } else { // There was not fallback service so actually make the request to the origin. const response = await fetch(request); return cloneResponse(response); } }; try { return await next(); } catch (error) { if (isFailOpen) { const response = await env[__FALLBACK_SERVICE__].fetch(request); return cloneResponse(response); } throw error; } }, }; // This makes a Response mutable const cloneResponse = (response: Response) => // https://fetch.spec.whatwg.org/#null-body-status new Response( [101, 204, 205, 304].includes(response.status) ? null : response.body, response );