pages-template-worker.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { match } from "path-to-regexp";
  2. //note: this explicitly does not include the * character, as pages requires this
  3. const escapeRegex = /[.+?^${}()|[\]\\]/g;
  4. type HTTPMethod =
  5. | "HEAD"
  6. | "OPTIONS"
  7. | "GET"
  8. | "POST"
  9. | "PUT"
  10. | "PATCH"
  11. | "DELETE";
  12. /* TODO: Grab these from @cloudflare/workers-types instead */
  13. type Params<P extends string = string> = Record<P, string | string[]>;
  14. type EventContext<Env, P extends string, Data> = {
  15. request: Request;
  16. functionPath: string;
  17. waitUntil: (promise: Promise<unknown>) => void;
  18. passThroughOnException: () => void;
  19. next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
  20. env: Env & { ASSETS: { fetch: typeof fetch } };
  21. params: Params<P>;
  22. data: Data;
  23. };
  24. declare type PagesFunction<
  25. Env = unknown,
  26. P extends string = string,
  27. Data extends Record<string, unknown> = Record<string, unknown>
  28. > = (context: EventContext<Env, P, Data>) => Response | Promise<Response>;
  29. /* end @cloudflare/workers-types */
  30. type RouteHandler = {
  31. routePath: string;
  32. mountPath: string;
  33. method?: HTTPMethod;
  34. modules: PagesFunction[];
  35. middlewares: PagesFunction[];
  36. };
  37. // inject `routes` via ESBuild
  38. declare const routes: RouteHandler[];
  39. // define `__FALLBACK_SERVICE__` via ESBuild
  40. declare const __FALLBACK_SERVICE__: string;
  41. // expect an ASSETS fetcher binding pointing to the asset-server stage
  42. type FetchEnv = {
  43. [name: string]: { fetch: typeof fetch };
  44. ASSETS: { fetch: typeof fetch };
  45. };
  46. type WorkerContext = {
  47. waitUntil: (promise: Promise<unknown>) => void;
  48. passThroughOnException: () => void;
  49. };
  50. function* executeRequest(request: Request) {
  51. const requestPath = new URL(request.url).pathname;
  52. // First, iterate through the routes (backwards) and execute "middlewares" on partial route matches
  53. for (const route of [...routes].reverse()) {
  54. if (route.method && route.method !== request.method) {
  55. continue;
  56. }
  57. // replaces with "\\$&", this prepends a backslash to the matched string, e.g. "[" becomes "\["
  58. const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
  59. end: false,
  60. });
  61. const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
  62. end: false,
  63. });
  64. const matchResult = routeMatcher(requestPath);
  65. const mountMatchResult = mountMatcher(requestPath);
  66. if (matchResult && mountMatchResult) {
  67. for (const handler of route.middlewares.flat()) {
  68. yield {
  69. handler,
  70. params: matchResult.params as Params,
  71. path: mountMatchResult.path,
  72. };
  73. }
  74. }
  75. }
  76. // Then look for the first exact route match and execute its "modules"
  77. for (const route of routes) {
  78. if (route.method && route.method !== request.method) {
  79. continue;
  80. }
  81. const routeMatcher = match(route.routePath.replace(escapeRegex, "\\$&"), {
  82. end: true,
  83. });
  84. const mountMatcher = match(route.mountPath.replace(escapeRegex, "\\$&"), {
  85. end: false,
  86. });
  87. const matchResult = routeMatcher(requestPath);
  88. const mountMatchResult = mountMatcher(requestPath);
  89. if (matchResult && mountMatchResult && route.modules.length) {
  90. for (const handler of route.modules.flat()) {
  91. yield {
  92. handler,
  93. params: matchResult.params as Params,
  94. path: matchResult.path,
  95. };
  96. }
  97. break;
  98. }
  99. }
  100. }
  101. export default {
  102. async fetch(
  103. originalRequest: Request,
  104. env: FetchEnv,
  105. workerContext: WorkerContext
  106. ) {
  107. let request = originalRequest;
  108. const handlerIterator = executeRequest(request);
  109. let data = {}; // arbitrary data the user can set between functions
  110. let isFailOpen = false;
  111. const next = async (input?: RequestInfo, init?: RequestInit) => {
  112. if (input !== undefined) {
  113. let url = input;
  114. if (typeof input === "string") {
  115. url = new URL(input, request.url).toString();
  116. }
  117. request = new Request(url, init);
  118. }
  119. const result = handlerIterator.next();
  120. // Note we can't use `!result.done` because this doesn't narrow to the correct type
  121. if (result.done === false) {
  122. const { handler, params, path } = result.value;
  123. const context = {
  124. request: new Request(request.clone()),
  125. functionPath: path,
  126. next,
  127. params,
  128. get data() {
  129. return data;
  130. },
  131. set data(value) {
  132. if (typeof value !== "object" || value === null) {
  133. throw new Error("context.data must be an object");
  134. }
  135. // user has overriden context.data, so we need to merge it with the existing data
  136. data = value;
  137. },
  138. env,
  139. waitUntil: workerContext.waitUntil.bind(workerContext),
  140. passThroughOnException: () => {
  141. isFailOpen = true;
  142. },
  143. };
  144. const response = await handler(context);
  145. if (!(response instanceof Response)) {
  146. throw new Error("Your Pages function should return a Response");
  147. }
  148. return cloneResponse(response);
  149. } else if (__FALLBACK_SERVICE__) {
  150. // There are no more handlers so finish with the fallback service (`env.ASSETS.fetch` in Pages' case)
  151. const response = await env[__FALLBACK_SERVICE__].fetch(request);
  152. return cloneResponse(response);
  153. } else {
  154. // There was not fallback service so actually make the request to the origin.
  155. const response = await fetch(request);
  156. return cloneResponse(response);
  157. }
  158. };
  159. try {
  160. return await next();
  161. } catch (error) {
  162. if (isFailOpen) {
  163. const response = await env[__FALLBACK_SERVICE__].fetch(request);
  164. return cloneResponse(response);
  165. }
  166. throw error;
  167. }
  168. },
  169. };
  170. // This makes a Response mutable
  171. const cloneResponse = (response: Response) =>
  172. // https://fetch.spec.whatwg.org/#null-body-status
  173. new Response(
  174. [101, 204, 205, 304].includes(response.status) ? null : response.body,
  175. response
  176. );