UNPKG

@universal-middleware/sirv

Version:
328 lines (324 loc) 10.6 kB
import * as fs from 'node:fs'; import { resolve, normalize, join, sep } from 'node:path'; import { Readable } from 'node:stream'; import { lookup } from 'mrmime'; import { totalist } from 'totalist/sync'; // src/middleware.ts // ../core/dist/index.js var knownUserAgents = { deno: "Deno", bun: "Bun", workerd: "Cloudflare-Workers", node: "Node.js" }; var _getRuntimeKey = () => { const global = globalThis; const userAgentSupported = typeof navigator !== "undefined" && typeof navigator.userAgent === "string"; if (userAgentSupported) { for (const [runtimeKey2, userAgent] of Object.entries(knownUserAgents)) { if (checkUserAgentEquals(userAgent)) { return runtimeKey2; } } } if (typeof global?.EdgeRuntime === "string") { return "edge-light"; } if (global?.fastly !== void 0) { return "fastly"; } if (global?.process?.release?.name === "node") { return "node"; } return "other"; }; var runtimeKey; var getRuntimeKey = () => { if (runtimeKey === void 0) { runtimeKey = _getRuntimeKey(); } return runtimeKey; }; var checkUserAgentEquals = (platform) => { const userAgent = navigator.userAgent; return userAgent.startsWith(platform); }; function getRuntime(args) { const key = getRuntimeKey(); return { runtime: key, ...args }; } function getAdapter(key, args) { return { adapter: key, ...args }; } function getAdapterRuntime(adapter, adapterArgs, runtimeArgs, request) { const a = getAdapter(adapter, adapterArgs); const r = getRuntime(runtimeArgs); const s = getSrvxNodeRuntime(request); return { ...r, ...a, ...s }; } function getSrvxNodeRuntime(request) { const ret = {}; if (request?.runtime?.node?.req) ret.req = request?.runtime.node.req; if (request?.runtime?.node?.res) ret.res = request?.runtime.node.res; return ret; } var universalSymbol = /* @__PURE__ */ Symbol.for("universal"); var unboundSymbol = /* @__PURE__ */ Symbol.for("unbound"); var contextSymbol = /* @__PURE__ */ Symbol.for("unContext"); var urlSymbol = /* @__PURE__ */ Symbol.for("unUrl"); function isBodyInit(value) { return value === null || typeof value === "string" || value instanceof Blob || value instanceof ArrayBuffer || ArrayBuffer.isView(value) || value instanceof FormData || value instanceof URLSearchParams || value instanceof ReadableStream; } function mergeHeadersInto(first, ...sources) { for (const source of sources) { const headers = new Headers(source); for (const [key, value] of headers.entries()) { if (key === "set-cookie") { if (!first.getSetCookie().includes(value)) first.append(key, value); } else { if (first.get(key) !== value) first.set(key, value); } } } return first; } function nodeHeadersToWeb(nodeHeaders) { const headers = []; const keys = Object.keys(nodeHeaders); for (const key of keys) { headers.push([key, normalizeHttpHeader(nodeHeaders[key])]); } return new Headers(headers); } function normalizeHttpHeader(value) { if (Array.isArray(value)) { return value.join(", "); } return value || ""; } function url(request) { if (request[urlSymbol]) { return request[urlSymbol]; } if (Object.isFrozen(request) || Object.isSealed(request)) { return new URL(request.url); } request[urlSymbol] = new URL(request.url); return request[urlSymbol]; } function cloneRequest(request, fields) { if (!fields) { return request.clone(); } return new Request(fields?.url ?? request.url, { method: fields?.method ?? request.method, headers: fields?.headers ?? request.headers, body: fields?.body ?? request.body, mode: fields?.mode ?? request.mode, credentials: fields?.credentials ?? request.credentials, cache: fields?.cache ?? request.cache, redirect: fields?.redirect ?? request.redirect, referrer: fields?.referrer ?? request.referrer, integrity: fields?.integrity ?? request.integrity, keepalive: fields?.keepalive ?? request.keepalive, referrerPolicy: fields?.referrerPolicy ?? request.referrerPolicy, signal: fields?.signal ?? request.signal, // @ts-expect-error RequestInit: duplex option is required when sending a body duplex: "half" }); } function bindUniversal(universal, fn, wrapper) { const unboundFn = unboundSymbol in fn ? fn[unboundSymbol] : fn; const self = { [universalSymbol]: universal, [unboundSymbol]: unboundFn }; const boundFn = unboundFn.bind(self); Object.assign(boundFn, self); return wrapper ? wrapper(boundFn) : boundFn; } function attachUniversal(universal, subject) { return Object.assign(subject, { [universalSymbol]: universal }); } var noop = () => { }; function isMatch(uri, arr) { for (let i = 0; i < arr.length; i++) { if (arr[i].test(uri)) return true; } return false; } function toAssume(uri, extns) { let i = 0; let x; const len = uri.length - 1; let uri_ = uri; if (uri.charCodeAt(len) === 47) { uri_ = uri.substring(0, len); } const arr = []; const tmp = `${uri_}/index`; for (; i < extns.length; i++) { x = extns[i] ? `.${extns[i]}` : ""; if (uri_) arr.push(uri_ + x); arr.push(tmp + x); } return arr; } function viaCache(cache, uri, extns) { let i = 0; let data; const arr = toAssume(uri, extns); for (; i < arr.length; i++) { if (data = cache[arr[i]]) return data; } return void 0; } function viaLocal(dir, isEtag, uri, extns) { let i = 0; const arr = toAssume(uri, extns); let abs; let stats; let name; let headers; for (; i < arr.length; i++) { abs = normalize(join(dir, name = arr[i])); if (abs.startsWith(dir) && fs.existsSync(abs)) { stats = fs.statSync(abs); if (stats.isDirectory()) continue; headers = toHeaders(name, stats, isEtag); headers["Cache-Control"] = isEtag ? "no-cache" : "no-store"; return { abs, stats, headers }; } } return void 0; } function send(req, file, stats, headers) { let code = 200; const newHeaders = { ...headers }; const rangeHeader = req.headers.get("range"); if (rangeHeader) { code = 206; const [x, y] = rangeHeader.replace("bytes=", "").split("-"); let end = Number.parseInt(y, 10) || stats.size - 1; const start = Number.parseInt(x, 10) || 0; if (end >= stats.size) { end = stats.size - 1; } if (start >= stats.size) { return new Response(null, { status: 416, headers: { "Content-Range": `bytes */${stats.size}` } }); } newHeaders["Content-Range"] = `bytes ${start}-${end}/${stats.size}`; newHeaders["Content-Length"] = (end - start + 1).toString(); newHeaders["Accept-Ranges"] = "bytes"; } const webStream = Readable.toWeb(fs.createReadStream(file)); return new Response(webStream, { status: code, headers: newHeaders }); } var ENCODING = { ".br": "br", ".gz": "gzip" }; function toHeaders(name, stats, isEtag) { const enc = ENCODING[name.slice(-3)]; let ctype = lookup(name.slice(0, enc ? -3 : void 0)) || ""; if (ctype === "text/html") ctype += ";charset=utf-8"; const headers = { "Content-Length": stats.size.toString(), "Content-Type": ctype, "Last-Modified": stats.mtime.toUTCString() }; if (enc) headers["Content-Encoding"] = enc; if (isEtag) headers["ETag"] = `W/"${stats.size}-${stats.mtime.getTime()}"`; return headers; } function createUniversalMiddleware(isEtag, isSPA, ignores, lookup2, extensions, gzips, brots, setHeaders, isNotFound, fallback) { return (request) => { const extns = [""]; const url2 = url(request); let pathname = url2.pathname; const acceptEncoding = request.headers.get("accept-encoding") || ""; if (gzips && acceptEncoding.includes("gzip")) extns.unshift(...gzips); if (brots && /(br|brotli)/i.test(acceptEncoding)) extns.unshift(...brots); extns.push(...extensions); if (pathname.indexOf("%") !== -1) { try { pathname = decodeURI(pathname); } catch (_err) { } } const data = lookup2(pathname, extns) || isSPA && !isMatch(pathname, ignores) && lookup2(fallback, extns); if (!data) return isNotFound ? isNotFound(request) : void 0; if (isEtag && request.headers.get("if-none-match") === data.headers["ETag"]) { return new Response(null, { status: 304 }); } if (gzips || brots) { data.headers["Vary"] = "Accept-Encoding"; } const response = send(request, data.abs, data.stats, data.headers); setHeaders(response, pathname, data.stats); return response; }; } function serveStatic(dir, opts = {}) { dir = resolve(dir || "."); const isNotFound = opts.onNoMatch; const setHeaders = opts.setHeaders || noop; const extensions = opts.extensions || ["html", "htm"]; const gzips = opts.gzip && extensions.map((x) => `${x}.gz`).concat("gz"); const brots = opts.brotli && extensions.map((x) => `${x}.br`).concat("br"); const FILES = {}; let fallback = "/"; const isEtag = !!opts.etag; const isSPA = !!opts.single; if (typeof opts.single === "string") { const idx = opts.single.lastIndexOf("."); fallback += ~idx ? opts.single.substring(0, idx) : opts.single; } const ignores = []; if (opts.ignores !== false) { ignores.push(/[/]([A-Za-z\s\d~$._-]+\.\w+){1,}$/); if (opts.dotfiles) ignores.push(/\/\.\w/); else ignores.push(/\/\.well-known/); const optsIgnores = Array.isArray(opts.ignores) ? opts.ignores : opts.ignores ? [opts.ignores] : []; for (const x of optsIgnores) { ignores.push(new RegExp(x, "i")); } } let cc = opts.maxAge != null && `public,max-age=${opts.maxAge}`; if (cc && opts.immutable) cc += ",immutable"; else if (cc && opts.maxAge === 0) cc += ",must-revalidate"; if (!opts.dev) { totalist(dir, (name, abs, stats) => { if (/\.well-known[\\+/]/.test(name)) ; else if (!opts.dotfiles && /(^\.|[\\+|/+]\.)/.test(name)) return; const headers = toHeaders(name, stats, isEtag); if (cc) headers["Cache-Control"] = cc; FILES[`/${name.normalize().replace(/\\+/g, "/")}`] = { abs, stats, headers }; }); } const lookupFn = opts.dev ? (uri, extns) => viaLocal(dir + sep, isEtag, uri, extns) : (uri, extns) => viaCache(FILES, uri, extns); return createUniversalMiddleware( isEtag, isSPA, ignores, lookupFn, extensions, gzips, brots, setHeaders, isNotFound, fallback ); } export { attachUniversal, bindUniversal, cloneRequest, contextSymbol, getAdapterRuntime, isBodyInit, mergeHeadersInto, nodeHeadersToWeb, serveStatic, universalSymbol };