UNPKG

azion

Version:

Azion Packages for Edge Computing.

340 lines (339 loc) 14 kB
import { parse as x } from "cookie"; import { a as p, c as m, b as w, d, m as v, e as L, i as f, p as y } from "./index.mjs"; function R(a, { url: e, cookies: s, headers: t }) { switch (a.type) { case "host": return e.hostname === a.value; case "header": return a.value !== void 0 ? !!t.get(a.key)?.match(a.value) : t.has(a.key); case "cookie": { const r = s[a.key]; return a.value !== void 0 ? !!r?.match(a.value) : r !== void 0; } case "query": return a.value !== void 0 ? !!e.searchParams.get(a.key)?.match(a.value) : e.searchParams.has(a.key); } } async function g(a, e = "none", s = !1) { return await a.run(e) === "error" || !s && a.status && a.status >= 400 ? g(a, "error", !0) : { path: a.path, status: a.status, headers: a.headers, searchParams: a.searchParams, body: a.body }; } function k(a) { switch (a) { // `none` applied headers/redirects/middleware/`beforeFiles` rewrites. It checked non-dynamic routes and static assets. case "none": return "filesystem"; // `filesystem` applied `afterFiles` rewrites. It checked those rewritten routes. case "filesystem": return "rewrite"; // `rewrite` applied dynamic params to requests. It checked dynamic routes. case "rewrite": return "resource"; // `resource` applied `fallback` rewrites. It checked the final routes. case "resource": return "miss"; default: return "miss"; } } async function P(a, { request: e, assetsFetcher: s, ctx: t }, { path: r, searchParams: i }) { let n; const c = new URL(e.url); p(c.searchParams, i); const h = new Request(c, e); try { switch (a?.type) { case "function": case "middleware": { n = await a.entrypoint.default(h, t); break; } case "override": { n = w(await s.fetch(m(h, a.path ?? r))), a.headers && d(n.headers, a.headers); break; } case "static": { n = await s.fetch(m(h, r)); break; } // run custom node server case "node": { n = await globalThis.runNodeCustomServer(h); break; } default: n = new Response("Not Found", { status: 404 }); } } catch (o) { return console.error(o), new Response("Internal Server Error", { status: 500 }); } return w(n); } function q(a, e) { const s = "^//?(?:", t = ")/(.*)"; return !a.startsWith(s) || !a.endsWith(t) ? !1 : a.slice(s.length, -t.length).split("|").every((i) => i in e); } class $ { /** * Creates a new instance of a request matcher. * * The matcher is used to match a request to a route and run the route's middleware. * @param {object} routes The processed Vercel build output config routes. * @param {object} output Vercel build output. * @param {object} reqCtx Request context object; request object, assets fetcher, and execution context. * @param {Array} buildMetadata Information about the build to be used in the routing. * @returns {void} The matched set of path, status, headers, and search params. */ constructor(e, s, t, r) { this.routes = e, this.output = s, this.reqCtx = t, this.url = new URL(t.request.url), this.cookies = x(t.request.headers.get("cookie") || ""), this.path = this.url.pathname || "/", this.headers = { normal: new Headers(), important: new Headers() }, this.searchParams = new URLSearchParams(), p(this.searchParams, this.url.searchParams), this.checkPhaseCounter = 0, this.middlewareInvoked = [], this.locales = new Set(r.i18n.locales), this.hasIndexFunctions = r.hasIndexFunctions, this.defaultLocale = r.i18n.defaultLocale; } /** * Checks if a Vercel source route from the build output config matches the request. * @param {object} route Build output config source route. * @param {string} checkStatus Whether to check the status code of the route. * @returns {void} The source path match result if the route matches, otherwise `undefined`. */ checkRouteMatch(e, s) { const t = v(e.src, this.path, e.caseSensitive); if (!t.match || e.methods && !e.methods.map((i) => i.toUpperCase()).includes(this.reqCtx.request.method.toUpperCase())) return; const r = { url: this.url, cookies: this.cookies, headers: this.reqCtx.request.headers }; if (!e.has?.find((i) => !R(i, r)) && !e.missing?.find((i) => R(i, r)) && !(s && e.status !== this.status)) return t; } /** * Processes the response from running a middleware function. * * Handles rewriting the URL and applying redirects, response headers, and overriden request headers. * @param {object} resp Middleware response object. */ processMiddlewareResp(e) { const s = "x-middleware-override-headers", t = e.headers.get(s); if (t) { const h = new Set(t.split(",").map((o) => o.trim())); for (const o of h.keys()) { const u = `x-middleware-request-${o}`, l = e.headers.get(u); this.reqCtx.request.headers.get(o) !== l && (l ? this.reqCtx.request.headers.set(o, l) : this.reqCtx.request.headers.delete(o)), e.headers.delete(u); } e.headers.delete(s); } const r = "x-middleware-rewrite", i = e.headers.get(r); if (i) { const h = new URL(i, this.url); this.path = h.pathname, p(this.searchParams, h.searchParams), e.headers.delete(r); } const n = "x-middleware-next"; e.headers.get(n) ? e.headers.delete(n) : !i && !e.headers.has("location") && (this.body = e.body, this.status = e.status), d(this.headers.normal, e.headers), this.headers.middlewareLocation = e.headers.get("location"); } /** * Runs the middleware function for a route if it exists. * @param {string} path Path to the route's middleware function. * @returns {boolean} Whether the middleware function was run successfully. */ async runRouteMiddleware(e) { if (!e) return !0; const s = e && this.output[e]; if (!s || s.type !== "middleware") return this.status = 500, !1; const t = await P(s, this.reqCtx, { path: this.path, searchParams: this.searchParams, headers: this.headers, status: this.status }); return this.middlewareInvoked.push(e), t.status === 500 ? (this.status = t.status, !1) : (this.processMiddlewareResp(t), !0); } /** * Resets the response status and headers if the route should override them. * @param {object} route Build output config source route. */ applyRouteOverrides(e) { e.override && (this.status = void 0, this.headers.normal = new Headers(), this.headers.important = new Headers()); } /** * Applies the route's headers for the response object. * @param {object} route Build output config source route. * @param {object[]} srcMatch Matches from the PCRE matcher. * @param {string[]}captureGroupKeys Named capture group keys from the PCRE matcher. */ applyRouteHeaders(e, s, t) { e.headers && (d(this.headers.normal, e.headers, { match: s, captureGroupKeys: t }), e.important && d(this.headers.important, e.headers, { match: s, captureGroupKeys: t })); } /** * Applies the route's status code for the response object. * @param {object} route Build output config source route. */ applyRouteStatus(e) { e.status && (this.status = e.status); } /** * Applies the route's destination for the matching the path to the Vercel build output. * @param {object} route Build output config source route. * @param {object} srcMatch Matches from the PCRE matcher. * @param {string[]} captureGroupKeys Named capture group keys from the PCRE matcher. * @returns {string }The previous path for the route before applying the destination. */ applyRouteDest(e, s, t) { if (this.locales.has(this.path.replace("/", "")) && this.hasIndexFunctions.length > 0 && (this.path = `${this.path}/`), !e.dest || this.defaultLocale === e.dest?.replace("/", "")) return this.path; const r = this.path; this.path = L(e.dest, s, t), /\/index\.rsc$/i.test(this.path) && !/\/(?:index)?$/i.test(r) && (this.path = r); const i = /\.rsc$/i.test(this.path), n = this.path in this.output; i && !n && (this.path = this.path.replace(/\.rsc/i, "")); const c = new URL(this.path, this.url); return p(this.searchParams, c.searchParams), f(this.path) || (this.path = c.pathname), r; } /** * Applies the route's redirects for locales and internationalization. * @param {void} route Build output config source route. */ applyLocaleRedirects(e) { if (!e.locale?.redirect || (this.locales || (this.locales = {}), !/^\^(.)*$/.test(e.src) && e.src !== this.path) || this.headers.normal.has("location")) return; const { locale: { redirect: t, cookie: r } } = e, i = r && this.cookies[r], n = y(i ?? ""), c = y(this.reqCtx.request.headers.get("accept-language") ?? ""), u = [...n, ...c].map((l) => t[l]).filter(Boolean)[0]; u && !this.path.startsWith(u) && (this.headers.normal.set("location", u), this.status = 307); } /** * Modifies the source route's `src` regex to be friendly with previously found locale's in the * `miss` phase. * * Sometimes, there is a source route with `src: '/{locale}'`, which rewrites all paths containing * the locale to `/`. This is problematic for matching, and should only do this if the path is * exactly the locale, i.e. `^/{locale}$`. * @param {object} route Build output config source route. * @param {object} phase Current phase of the routing process. * @returns {object} The route with the locale friendly regex. */ getLocaleFriendlyRoute(e, s) { return !this.locales || s !== "miss" ? e : /^\//.test(e.src) && e.src.slice(1) in this.locales ? { ...e, src: `^${e.src}$` } : q(e.src, this.locales) ? { ...e, src: e.src.replace(/\/\(\.\*\)$/, "(?:/(.*))?$") } : e; } /** * Checks a route to see if it matches the current request. * @param {object} phase Current phase of the routing process. * @param {object} rawRoute Build output config source route. * @returns {Promise}The status from checking the route. */ async checkRoute(e, s) { const t = this.getLocaleFriendlyRoute(s, e), r = this.checkRouteMatch(t, e === "error"); if (!r?.match || t.middlewarePath && this.middlewareInvoked.includes(t.middlewarePath)) return "skip"; const { match: i, captureGroupKeys: n } = r; if (this.applyRouteOverrides(t), this.applyLocaleRedirects(t), !await this.runRouteMiddleware(t.middlewarePath)) return "error"; if (this.body !== void 0) return "done"; this.applyRouteHeaders(t, i, n), this.applyRouteStatus(t); const h = this.applyRouteDest(t, i, n); if (t.check && !f(this.path)) if (h === this.path) { if (e !== "miss") return await this.checkPhase(k(e)); this.status = 404; } else if (e === "miss") { if (!(this.path in this.output)) return await this.checkPhase("filesystem"); this.status === 404 && (this.status = void 0); } else return await this.checkPhase("none"); return t.continue ? "next" : "done"; } /** * Checks a phase from the routing process to see if any route matches the current request. * @param {object} phase Current phase for routing. * @returns {Promise} The status from checking the phase. */ async checkPhase(e) { if (this.checkPhaseCounter++ >= 50) return console.error(`Routing encountered an infinite loop while checking ${this.url.pathname}`), this.status = 500, "error"; this.middlewareInvoked = []; let s = !0; for (const i of this.routes[e]) { const n = await this.checkRoute(e, i); if (n === "error") return "error"; if (n === "done") { s = !1; break; } } if (e === "hit" || f(this.path) || this.headers.normal.has("location")) return "done"; if (e === "none") for (const i of this.locales) { const n = new RegExp(`/${i}(/.*)`), h = this.path.match(n)?.[1]; if (h && h in this.output) { this.path = h; break; } } let t = this.path in this.output; if (e === "rewrite" && !t && this.path.endsWith("/")) { const i = this.path.replace(/\/$/, ""); t = i in this.output, t && (this.path = i); } if (e === "miss" && !t) { const i = !this.status || this.status < 400; this.status = i ? 404 : this.status; } let r = "miss"; return t || e === "miss" || e === "error" ? r = "hit" : s && (r = k(e)), await this.checkPhase(r); } /** * Runs the matcher for a phase. * @param {string} phase The phase to start matching routes from. * @returns {Promise} The status from checking for matches. */ async run(e = "none") { this.checkPhaseCounter = 0; const s = await this.checkPhase(e); return f(this.path) && this.headers.normal.set("location", this.path), this.headers.normal.has("location") && (!this.status || this.status < 300 || this.status >= 400) && (this.status = 307), s; } } async function I(a, { path: e = "/404", status: s, headers: t, searchParams: r, body: i }, n) { const c = t.normal.get("location"); if (c) { if (c !== t.middlewareLocation) { const u = [...r.keys()].length ? `?${r.toString()}` : ""; t.normal.set("location", `${c ?? "/"}${u}`); } return new Response(null, { status: s, headers: t.normal }); } let h = i !== void 0 ? ( // If we have a response body from matching, use it instead. new Response(i, { status: s }) ) : await P(n[e], a, { path: e, searchParams: r }); const o = t.normal; return d(o, h.headers), d(o, t.important), h = new Response(h.body, { ...h, status: s || h.status, headers: o }), h; } async function b(a, e, s, t) { const r = new $(e.routes, s, a, t), i = await g(r); return I(a, i, s); } export { I as generateResponse, b as handleRequest };