@universal-middleware/sirv
Version:
Universal static file serving middleware
328 lines (324 loc) • 10.6 kB
JavaScript
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 };