Spaces:
Sleeping
Sleeping
| import './shims.js'; | |
| import * as fs from 'node:fs'; | |
| import fs__default, { readdirSync, statSync, createReadStream } from 'node:fs'; | |
| import path, { resolve, join, sep, normalize } from 'node:path'; | |
| import process from 'node:process'; | |
| import * as qs from 'node:querystring'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { Readable } from 'node:stream'; | |
| import { Server } from './server/index.js'; | |
| import { manifest, prerendered, base } from './server/manifest.js'; | |
| import { env } from './env.js'; | |
| function totalist(dir, callback, pre='') { | |
| dir = resolve('.', dir); | |
| let arr = readdirSync(dir); | |
| let i=0, abs, stats; | |
| for (; i < arr.length; i++) { | |
| abs = join(dir, arr[i]); | |
| stats = statSync(abs); | |
| stats.isDirectory() | |
| ? totalist(abs, callback, join(pre, arr[i])) | |
| : callback(join(pre, arr[i]), abs, stats); | |
| } | |
| } | |
| /** | |
| * @typedef ParsedURL | |
| * @type {import('.').ParsedURL} | |
| */ | |
| /** | |
| * @typedef Request | |
| * @property {string} url | |
| * @property {ParsedURL} _parsedUrl | |
| */ | |
| /** | |
| * @param {Request} req | |
| * @returns {ParsedURL|void} | |
| */ | |
| function parse(req) { | |
| let raw = req.url; | |
| if (raw == null) return; | |
| let prev = req._parsedUrl; | |
| if (prev && prev.raw === raw) return prev; | |
| let pathname=raw, search='', query; | |
| if (raw.length > 1) { | |
| let idx = raw.indexOf('?', 1); | |
| if (idx !== -1) { | |
| search = raw.substring(idx); | |
| pathname = raw.substring(0, idx); | |
| if (search.length > 1) { | |
| query = qs.parse(search.substring(1)); | |
| } | |
| } | |
| } | |
| return req._parsedUrl = { pathname, search, query, raw }; | |
| } | |
| const mimes = { | |
| "3g2": "video/3gpp2", | |
| "3gp": "video/3gpp", | |
| "3gpp": "video/3gpp", | |
| "3mf": "model/3mf", | |
| "aac": "audio/aac", | |
| "ac": "application/pkix-attr-cert", | |
| "adp": "audio/adpcm", | |
| "adts": "audio/aac", | |
| "ai": "application/postscript", | |
| "aml": "application/automationml-aml+xml", | |
| "amlx": "application/automationml-amlx+zip", | |
| "amr": "audio/amr", | |
| "apng": "image/apng", | |
| "appcache": "text/cache-manifest", | |
| "appinstaller": "application/appinstaller", | |
| "appx": "application/appx", | |
| "appxbundle": "application/appxbundle", | |
| "asc": "application/pgp-keys", | |
| "atom": "application/atom+xml", | |
| "atomcat": "application/atomcat+xml", | |
| "atomdeleted": "application/atomdeleted+xml", | |
| "atomsvc": "application/atomsvc+xml", | |
| "au": "audio/basic", | |
| "avci": "image/avci", | |
| "avcs": "image/avcs", | |
| "avif": "image/avif", | |
| "aw": "application/applixware", | |
| "bdoc": "application/bdoc", | |
| "bin": "application/octet-stream", | |
| "bmp": "image/bmp", | |
| "bpk": "application/octet-stream", | |
| "btf": "image/prs.btif", | |
| "btif": "image/prs.btif", | |
| "buffer": "application/octet-stream", | |
| "ccxml": "application/ccxml+xml", | |
| "cdfx": "application/cdfx+xml", | |
| "cdmia": "application/cdmi-capability", | |
| "cdmic": "application/cdmi-container", | |
| "cdmid": "application/cdmi-domain", | |
| "cdmio": "application/cdmi-object", | |
| "cdmiq": "application/cdmi-queue", | |
| "cer": "application/pkix-cert", | |
| "cgm": "image/cgm", | |
| "cjs": "application/node", | |
| "class": "application/java-vm", | |
| "coffee": "text/coffeescript", | |
| "conf": "text/plain", | |
| "cpl": "application/cpl+xml", | |
| "cpt": "application/mac-compactpro", | |
| "crl": "application/pkix-crl", | |
| "css": "text/css", | |
| "csv": "text/csv", | |
| "cu": "application/cu-seeme", | |
| "cwl": "application/cwl", | |
| "cww": "application/prs.cww", | |
| "davmount": "application/davmount+xml", | |
| "dbk": "application/docbook+xml", | |
| "deb": "application/octet-stream", | |
| "def": "text/plain", | |
| "deploy": "application/octet-stream", | |
| "dib": "image/bmp", | |
| "disposition-notification": "message/disposition-notification", | |
| "dist": "application/octet-stream", | |
| "distz": "application/octet-stream", | |
| "dll": "application/octet-stream", | |
| "dmg": "application/octet-stream", | |
| "dms": "application/octet-stream", | |
| "doc": "application/msword", | |
| "dot": "application/msword", | |
| "dpx": "image/dpx", | |
| "drle": "image/dicom-rle", | |
| "dsc": "text/prs.lines.tag", | |
| "dssc": "application/dssc+der", | |
| "dtd": "application/xml-dtd", | |
| "dump": "application/octet-stream", | |
| "dwd": "application/atsc-dwd+xml", | |
| "ear": "application/java-archive", | |
| "ecma": "application/ecmascript", | |
| "elc": "application/octet-stream", | |
| "emf": "image/emf", | |
| "eml": "message/rfc822", | |
| "emma": "application/emma+xml", | |
| "emotionml": "application/emotionml+xml", | |
| "eps": "application/postscript", | |
| "epub": "application/epub+zip", | |
| "exe": "application/octet-stream", | |
| "exi": "application/exi", | |
| "exp": "application/express", | |
| "exr": "image/aces", | |
| "ez": "application/andrew-inset", | |
| "fdf": "application/fdf", | |
| "fdt": "application/fdt+xml", | |
| "fits": "image/fits", | |
| "g3": "image/g3fax", | |
| "gbr": "application/rpki-ghostbusters", | |
| "geojson": "application/geo+json", | |
| "gif": "image/gif", | |
| "glb": "model/gltf-binary", | |
| "gltf": "model/gltf+json", | |
| "gml": "application/gml+xml", | |
| "gpx": "application/gpx+xml", | |
| "gram": "application/srgs", | |
| "grxml": "application/srgs+xml", | |
| "gxf": "application/gxf", | |
| "gz": "application/gzip", | |
| "h261": "video/h261", | |
| "h263": "video/h263", | |
| "h264": "video/h264", | |
| "heic": "image/heic", | |
| "heics": "image/heic-sequence", | |
| "heif": "image/heif", | |
| "heifs": "image/heif-sequence", | |
| "hej2": "image/hej2k", | |
| "held": "application/atsc-held+xml", | |
| "hjson": "application/hjson", | |
| "hlp": "application/winhlp", | |
| "hqx": "application/mac-binhex40", | |
| "hsj2": "image/hsj2", | |
| "htm": "text/html", | |
| "html": "text/html", | |
| "ics": "text/calendar", | |
| "ief": "image/ief", | |
| "ifb": "text/calendar", | |
| "iges": "model/iges", | |
| "igs": "model/iges", | |
| "img": "application/octet-stream", | |
| "in": "text/plain", | |
| "ini": "text/plain", | |
| "ink": "application/inkml+xml", | |
| "inkml": "application/inkml+xml", | |
| "ipfix": "application/ipfix", | |
| "iso": "application/octet-stream", | |
| "its": "application/its+xml", | |
| "jade": "text/jade", | |
| "jar": "application/java-archive", | |
| "jhc": "image/jphc", | |
| "jls": "image/jls", | |
| "jp2": "image/jp2", | |
| "jpe": "image/jpeg", | |
| "jpeg": "image/jpeg", | |
| "jpf": "image/jpx", | |
| "jpg": "image/jpeg", | |
| "jpg2": "image/jp2", | |
| "jpgm": "image/jpm", | |
| "jpgv": "video/jpeg", | |
| "jph": "image/jph", | |
| "jpm": "image/jpm", | |
| "jpx": "image/jpx", | |
| "js": "text/javascript", | |
| "json": "application/json", | |
| "json5": "application/json5", | |
| "jsonld": "application/ld+json", | |
| "jsonml": "application/jsonml+json", | |
| "jsx": "text/jsx", | |
| "jt": "model/jt", | |
| "jxr": "image/jxr", | |
| "jxra": "image/jxra", | |
| "jxrs": "image/jxrs", | |
| "jxs": "image/jxs", | |
| "jxsc": "image/jxsc", | |
| "jxsi": "image/jxsi", | |
| "jxss": "image/jxss", | |
| "kar": "audio/midi", | |
| "ktx": "image/ktx", | |
| "ktx2": "image/ktx2", | |
| "less": "text/less", | |
| "lgr": "application/lgr+xml", | |
| "list": "text/plain", | |
| "litcoffee": "text/coffeescript", | |
| "log": "text/plain", | |
| "lostxml": "application/lost+xml", | |
| "lrf": "application/octet-stream", | |
| "m1v": "video/mpeg", | |
| "m21": "application/mp21", | |
| "m2a": "audio/mpeg", | |
| "m2v": "video/mpeg", | |
| "m3a": "audio/mpeg", | |
| "m4a": "audio/mp4", | |
| "m4p": "application/mp4", | |
| "m4s": "video/iso.segment", | |
| "ma": "application/mathematica", | |
| "mads": "application/mads+xml", | |
| "maei": "application/mmt-aei+xml", | |
| "man": "text/troff", | |
| "manifest": "text/cache-manifest", | |
| "map": "application/json", | |
| "mar": "application/octet-stream", | |
| "markdown": "text/markdown", | |
| "mathml": "application/mathml+xml", | |
| "mb": "application/mathematica", | |
| "mbox": "application/mbox", | |
| "md": "text/markdown", | |
| "mdx": "text/mdx", | |
| "me": "text/troff", | |
| "mesh": "model/mesh", | |
| "meta4": "application/metalink4+xml", | |
| "metalink": "application/metalink+xml", | |
| "mets": "application/mets+xml", | |
| "mft": "application/rpki-manifest", | |
| "mid": "audio/midi", | |
| "midi": "audio/midi", | |
| "mime": "message/rfc822", | |
| "mj2": "video/mj2", | |
| "mjp2": "video/mj2", | |
| "mjs": "text/javascript", | |
| "mml": "text/mathml", | |
| "mods": "application/mods+xml", | |
| "mov": "video/quicktime", | |
| "mp2": "audio/mpeg", | |
| "mp21": "application/mp21", | |
| "mp2a": "audio/mpeg", | |
| "mp3": "audio/mpeg", | |
| "mp4": "video/mp4", | |
| "mp4a": "audio/mp4", | |
| "mp4s": "application/mp4", | |
| "mp4v": "video/mp4", | |
| "mpd": "application/dash+xml", | |
| "mpe": "video/mpeg", | |
| "mpeg": "video/mpeg", | |
| "mpf": "application/media-policy-dataset+xml", | |
| "mpg": "video/mpeg", | |
| "mpg4": "video/mp4", | |
| "mpga": "audio/mpeg", | |
| "mpp": "application/dash-patch+xml", | |
| "mrc": "application/marc", | |
| "mrcx": "application/marcxml+xml", | |
| "ms": "text/troff", | |
| "mscml": "application/mediaservercontrol+xml", | |
| "msh": "model/mesh", | |
| "msi": "application/octet-stream", | |
| "msix": "application/msix", | |
| "msixbundle": "application/msixbundle", | |
| "msm": "application/octet-stream", | |
| "msp": "application/octet-stream", | |
| "mtl": "model/mtl", | |
| "musd": "application/mmt-usd+xml", | |
| "mxf": "application/mxf", | |
| "mxmf": "audio/mobile-xmf", | |
| "mxml": "application/xv+xml", | |
| "n3": "text/n3", | |
| "nb": "application/mathematica", | |
| "nq": "application/n-quads", | |
| "nt": "application/n-triples", | |
| "obj": "model/obj", | |
| "oda": "application/oda", | |
| "oga": "audio/ogg", | |
| "ogg": "audio/ogg", | |
| "ogv": "video/ogg", | |
| "ogx": "application/ogg", | |
| "omdoc": "application/omdoc+xml", | |
| "onepkg": "application/onenote", | |
| "onetmp": "application/onenote", | |
| "onetoc": "application/onenote", | |
| "onetoc2": "application/onenote", | |
| "opf": "application/oebps-package+xml", | |
| "opus": "audio/ogg", | |
| "otf": "font/otf", | |
| "owl": "application/rdf+xml", | |
| "oxps": "application/oxps", | |
| "p10": "application/pkcs10", | |
| "p7c": "application/pkcs7-mime", | |
| "p7m": "application/pkcs7-mime", | |
| "p7s": "application/pkcs7-signature", | |
| "p8": "application/pkcs8", | |
| "pdf": "application/pdf", | |
| "pfr": "application/font-tdpfr", | |
| "pgp": "application/pgp-encrypted", | |
| "pkg": "application/octet-stream", | |
| "pki": "application/pkixcmp", | |
| "pkipath": "application/pkix-pkipath", | |
| "pls": "application/pls+xml", | |
| "png": "image/png", | |
| "prc": "model/prc", | |
| "prf": "application/pics-rules", | |
| "provx": "application/provenance+xml", | |
| "ps": "application/postscript", | |
| "pskcxml": "application/pskc+xml", | |
| "pti": "image/prs.pti", | |
| "qt": "video/quicktime", | |
| "raml": "application/raml+yaml", | |
| "rapd": "application/route-apd+xml", | |
| "rdf": "application/rdf+xml", | |
| "relo": "application/p2p-overlay+xml", | |
| "rif": "application/reginfo+xml", | |
| "rl": "application/resource-lists+xml", | |
| "rld": "application/resource-lists-diff+xml", | |
| "rmi": "audio/midi", | |
| "rnc": "application/relax-ng-compact-syntax", | |
| "rng": "application/xml", | |
| "roa": "application/rpki-roa", | |
| "roff": "text/troff", | |
| "rq": "application/sparql-query", | |
| "rs": "application/rls-services+xml", | |
| "rsat": "application/atsc-rsat+xml", | |
| "rsd": "application/rsd+xml", | |
| "rsheet": "application/urc-ressheet+xml", | |
| "rss": "application/rss+xml", | |
| "rtf": "text/rtf", | |
| "rtx": "text/richtext", | |
| "rusd": "application/route-usd+xml", | |
| "s3m": "audio/s3m", | |
| "sbml": "application/sbml+xml", | |
| "scq": "application/scvp-cv-request", | |
| "scs": "application/scvp-cv-response", | |
| "sdp": "application/sdp", | |
| "senmlx": "application/senml+xml", | |
| "sensmlx": "application/sensml+xml", | |
| "ser": "application/java-serialized-object", | |
| "setpay": "application/set-payment-initiation", | |
| "setreg": "application/set-registration-initiation", | |
| "sgi": "image/sgi", | |
| "sgm": "text/sgml", | |
| "sgml": "text/sgml", | |
| "shex": "text/shex", | |
| "shf": "application/shf+xml", | |
| "shtml": "text/html", | |
| "sieve": "application/sieve", | |
| "sig": "application/pgp-signature", | |
| "sil": "audio/silk", | |
| "silo": "model/mesh", | |
| "siv": "application/sieve", | |
| "slim": "text/slim", | |
| "slm": "text/slim", | |
| "sls": "application/route-s-tsid+xml", | |
| "smi": "application/smil+xml", | |
| "smil": "application/smil+xml", | |
| "snd": "audio/basic", | |
| "so": "application/octet-stream", | |
| "spdx": "text/spdx", | |
| "spp": "application/scvp-vp-response", | |
| "spq": "application/scvp-vp-request", | |
| "spx": "audio/ogg", | |
| "sql": "application/sql", | |
| "sru": "application/sru+xml", | |
| "srx": "application/sparql-results+xml", | |
| "ssdl": "application/ssdl+xml", | |
| "ssml": "application/ssml+xml", | |
| "stk": "application/hyperstudio", | |
| "stl": "model/stl", | |
| "stpx": "model/step+xml", | |
| "stpxz": "model/step-xml+zip", | |
| "stpz": "model/step+zip", | |
| "styl": "text/stylus", | |
| "stylus": "text/stylus", | |
| "svg": "image/svg+xml", | |
| "svgz": "image/svg+xml", | |
| "swidtag": "application/swid+xml", | |
| "t": "text/troff", | |
| "t38": "image/t38", | |
| "td": "application/urc-targetdesc+xml", | |
| "tei": "application/tei+xml", | |
| "teicorpus": "application/tei+xml", | |
| "text": "text/plain", | |
| "tfi": "application/thraud+xml", | |
| "tfx": "image/tiff-fx", | |
| "tif": "image/tiff", | |
| "tiff": "image/tiff", | |
| "toml": "application/toml", | |
| "tr": "text/troff", | |
| "trig": "application/trig", | |
| "ts": "video/mp2t", | |
| "tsd": "application/timestamped-data", | |
| "tsv": "text/tab-separated-values", | |
| "ttc": "font/collection", | |
| "ttf": "font/ttf", | |
| "ttl": "text/turtle", | |
| "ttml": "application/ttml+xml", | |
| "txt": "text/plain", | |
| "u3d": "model/u3d", | |
| "u8dsn": "message/global-delivery-status", | |
| "u8hdr": "message/global-headers", | |
| "u8mdn": "message/global-disposition-notification", | |
| "u8msg": "message/global", | |
| "ubj": "application/ubjson", | |
| "uri": "text/uri-list", | |
| "uris": "text/uri-list", | |
| "urls": "text/uri-list", | |
| "vcard": "text/vcard", | |
| "vrml": "model/vrml", | |
| "vtt": "text/vtt", | |
| "vxml": "application/voicexml+xml", | |
| "war": "application/java-archive", | |
| "wasm": "application/wasm", | |
| "wav": "audio/wav", | |
| "weba": "audio/webm", | |
| "webm": "video/webm", | |
| "webmanifest": "application/manifest+json", | |
| "webp": "image/webp", | |
| "wgsl": "text/wgsl", | |
| "wgt": "application/widget", | |
| "wif": "application/watcherinfo+xml", | |
| "wmf": "image/wmf", | |
| "woff": "font/woff", | |
| "woff2": "font/woff2", | |
| "wrl": "model/vrml", | |
| "wsdl": "application/wsdl+xml", | |
| "wspolicy": "application/wspolicy+xml", | |
| "x3d": "model/x3d+xml", | |
| "x3db": "model/x3d+fastinfoset", | |
| "x3dbz": "model/x3d+binary", | |
| "x3dv": "model/x3d-vrml", | |
| "x3dvz": "model/x3d+vrml", | |
| "x3dz": "model/x3d+xml", | |
| "xaml": "application/xaml+xml", | |
| "xav": "application/xcap-att+xml", | |
| "xca": "application/xcap-caps+xml", | |
| "xcs": "application/calendar+xml", | |
| "xdf": "application/xcap-diff+xml", | |
| "xdssc": "application/dssc+xml", | |
| "xel": "application/xcap-el+xml", | |
| "xenc": "application/xenc+xml", | |
| "xer": "application/patch-ops-error+xml", | |
| "xfdf": "application/xfdf", | |
| "xht": "application/xhtml+xml", | |
| "xhtml": "application/xhtml+xml", | |
| "xhvml": "application/xv+xml", | |
| "xlf": "application/xliff+xml", | |
| "xm": "audio/xm", | |
| "xml": "text/xml", | |
| "xns": "application/xcap-ns+xml", | |
| "xop": "application/xop+xml", | |
| "xpl": "application/xproc+xml", | |
| "xsd": "application/xml", | |
| "xsf": "application/prs.xsf+xml", | |
| "xsl": "application/xml", | |
| "xslt": "application/xml", | |
| "xspf": "application/xspf+xml", | |
| "xvm": "application/xv+xml", | |
| "xvml": "application/xv+xml", | |
| "yaml": "text/yaml", | |
| "yang": "application/yang", | |
| "yin": "application/yin+xml", | |
| "yml": "text/yaml", | |
| "zip": "application/zip" | |
| }; | |
| function lookup(extn) { | |
| let tmp = ('' + extn).trim().toLowerCase(); | |
| let idx = tmp.lastIndexOf('.'); | |
| return mimes[!~idx ? tmp : tmp.substring(++idx)]; | |
| } | |
| const noop = () => {}; | |
| function isMatch(uri, arr) { | |
| for (let i=0; i < arr.length; i++) { | |
| if (arr[i].test(uri)) return true; | |
| } | |
| } | |
| function toAssume(uri, extns) { | |
| let i=0, x, len=uri.length - 1; | |
| if (uri.charCodeAt(len) === 47) { | |
| uri = uri.substring(0, len); | |
| } | |
| let arr=[], 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, data, arr=toAssume(uri, extns); | |
| for (; i < arr.length; i++) { | |
| if (data = cache[arr[i]]) return data; | |
| } | |
| } | |
| function viaLocal(dir, isEtag, uri, extns) { | |
| let i=0, arr=toAssume(uri, extns); | |
| let abs, stats, name, 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 }; | |
| } | |
| } | |
| } | |
| function is404(req, res) { | |
| return (res.statusCode=404,res.end()); | |
| } | |
| function send(req, res, file, stats, headers) { | |
| let code=200, tmp, opts={}; | |
| headers = { ...headers }; | |
| for (let key in headers) { | |
| tmp = res.getHeader(key); | |
| if (tmp) headers[key] = tmp; | |
| } | |
| if (tmp = res.getHeader('content-type')) { | |
| headers['Content-Type'] = tmp; | |
| } | |
| if (req.headers.range) { | |
| code = 206; | |
| let [x, y] = req.headers.range.replace('bytes=', '').split('-'); | |
| let end = opts.end = parseInt(y, 10) || stats.size - 1; | |
| let start = opts.start = parseInt(x, 10) || 0; | |
| if (end >= stats.size) { | |
| end = stats.size - 1; | |
| } | |
| if (start >= stats.size) { | |
| res.setHeader('Content-Range', `bytes */${stats.size}`); | |
| res.statusCode = 416; | |
| return res.end(); | |
| } | |
| headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`; | |
| headers['Content-Length'] = (end - start + 1); | |
| headers['Accept-Ranges'] = 'bytes'; | |
| } | |
| res.writeHead(code, headers); | |
| fs.createReadStream(file, opts).pipe(res); | |
| } | |
| const ENCODING = { | |
| '.br': 'br', | |
| '.gz': 'gzip', | |
| }; | |
| function toHeaders(name, stats, isEtag) { | |
| let enc = ENCODING[name.slice(-3)]; | |
| let ctype = lookup(name.slice(0, enc && -3)) || ''; | |
| if (ctype === 'text/html') ctype += ';charset=utf-8'; | |
| let headers = { | |
| 'Content-Length': stats.size, | |
| '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 sirv (dir, opts={}) { | |
| dir = resolve(dir || '.'); | |
| let isNotFound = opts.onNoMatch || is404; | |
| let setHeaders = opts.setHeaders || noop; | |
| let extensions = opts.extensions || ['html', 'htm']; | |
| let gzips = opts.gzip && extensions.map(x => `${x}.gz`).concat('gz'); | |
| let brots = opts.brotli && extensions.map(x => `${x}.br`).concat('br'); | |
| const FILES = {}; | |
| let fallback = '/'; | |
| let isEtag = !!opts.etag; | |
| let isSPA = !!opts.single; | |
| if (typeof opts.single === 'string') { | |
| let idx = opts.single.lastIndexOf('.'); | |
| fallback += !!~idx ? opts.single.substring(0, idx) : opts.single; | |
| } | |
| let ignores = []; | |
| if (opts.ignores !== false) { | |
| ignores.push(/[/]([A-Za-z\s\d~$._-]+\.\w+){1,}$/); // any extn | |
| if (opts.dotfiles) ignores.push(/\/\.\w/); | |
| else ignores.push(/\/\.well-known/); | |
| [].concat(opts.ignores || []).forEach(x => { | |
| 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)) ; // keep | |
| else if (!opts.dotfiles && /(^\.|[\\+|\/+]\.)/.test(name)) return; | |
| let headers = toHeaders(name, stats, isEtag); | |
| if (cc) headers['Cache-Control'] = cc; | |
| FILES['/' + name.normalize().replace(/\\+/g, '/')] = { abs, stats, headers }; | |
| }); | |
| } | |
| let lookup = opts.dev ? viaLocal.bind(0, dir + sep, isEtag) : viaCache.bind(0, FILES); | |
| return function (req, res, next) { | |
| let extns = ['']; | |
| let pathname = parse(req).pathname; | |
| let val = req.headers['accept-encoding'] || ''; | |
| if (gzips && val.includes('gzip')) extns.unshift(...gzips); | |
| if (brots && /(br|brotli)/i.test(val)) extns.unshift(...brots); | |
| extns.push(...extensions); // [...br, ...gz, orig, ...exts] | |
| if (pathname.indexOf('%') !== -1) { | |
| try { pathname = decodeURI(pathname); } | |
| catch (err) { /* malform uri */ } | |
| } | |
| let data = lookup(pathname, extns) || isSPA && !isMatch(pathname, ignores) && lookup(fallback, extns); | |
| if (!data) return next ? next() : isNotFound(req, res); | |
| if (isEtag && req.headers['if-none-match'] === data.headers['ETag']) { | |
| res.writeHead(304); | |
| return res.end(); | |
| } | |
| if (gzips || brots) { | |
| res.setHeader('Vary', 'Accept-Encoding'); | |
| } | |
| setHeaders(res, pathname, data.stats); | |
| send(req, res, data.abs, data.stats, data.headers); | |
| }; | |
| } | |
| var defaultParseOptions = { | |
| decodeValues: true, | |
| map: false, | |
| silent: false, | |
| split: "auto", // auto = split strings but not arrays | |
| }; | |
| function isForbiddenKey(key) { | |
| return typeof key !== "string" || key in {}; | |
| } | |
| function createNullObj() { | |
| return Object.create(null); | |
| } | |
| function isNonEmptyString(str) { | |
| return typeof str === "string" && !!str.trim(); | |
| } | |
| function parseString(setCookieValue, options) { | |
| var parts = setCookieValue.split(";").filter(isNonEmptyString); | |
| var nameValuePairStr = parts.shift(); | |
| var parsed = parseNameValuePair(nameValuePairStr); | |
| var name = parsed.name; | |
| var value = parsed.value; | |
| options = options | |
| ? Object.assign({}, defaultParseOptions, options) | |
| : defaultParseOptions; | |
| if (isForbiddenKey(name)) { | |
| return null; | |
| } | |
| try { | |
| value = options.decodeValues ? decodeURIComponent(value) : value; // decode cookie value | |
| } catch (e) { | |
| console.error( | |
| "set-cookie-parser: failed to decode cookie value. Set options.decodeValues=false to disable decoding.", | |
| e | |
| ); | |
| } | |
| var cookie = createNullObj(); | |
| cookie.name = name; | |
| cookie.value = value; | |
| parts.forEach(function (part) { | |
| var sides = part.split("="); | |
| var key = sides.shift().trimLeft().toLowerCase(); | |
| if (isForbiddenKey(key)) { | |
| return; | |
| } | |
| var value = sides.join("="); | |
| if (key === "expires") { | |
| cookie.expires = new Date(value); | |
| } else if (key === "max-age") { | |
| var n = parseInt(value, 10); | |
| if (!Number.isNaN(n)) cookie.maxAge = n; | |
| } else if (key === "secure") { | |
| cookie.secure = true; | |
| } else if (key === "httponly") { | |
| cookie.httpOnly = true; | |
| } else if (key === "samesite") { | |
| cookie.sameSite = value; | |
| } else if (key === "partitioned") { | |
| cookie.partitioned = true; | |
| } else if (key) { | |
| cookie[key] = value; | |
| } | |
| }); | |
| return cookie; | |
| } | |
| function parseNameValuePair(nameValuePairStr) { | |
| // Parses name-value-pair according to rfc6265bis draft | |
| var name = ""; | |
| var value = ""; | |
| var nameValueArr = nameValuePairStr.split("="); | |
| if (nameValueArr.length > 1) { | |
| name = nameValueArr.shift(); | |
| value = nameValueArr.join("="); // everything after the first =, joined by a "=" if there was more than one part | |
| } else { | |
| value = nameValuePairStr; | |
| } | |
| return { name: name, value: value }; | |
| } | |
| function parseSetCookie(input, options) { | |
| options = options | |
| ? Object.assign({}, defaultParseOptions, options) | |
| : defaultParseOptions; | |
| if (!input) { | |
| if (!options.map) { | |
| return []; | |
| } else { | |
| return createNullObj(); | |
| } | |
| } | |
| if (input.headers) { | |
| if (typeof input.headers.getSetCookie === "function") { | |
| // for fetch responses - they combine headers of the same type in the headers array, | |
| // but getSetCookie returns an uncombined array | |
| input = input.headers.getSetCookie(); | |
| } else if (input.headers["set-cookie"]) { | |
| // fast-path for node.js (which automatically normalizes header names to lower-case) | |
| input = input.headers["set-cookie"]; | |
| } else { | |
| // slow-path for other environments - see #25 | |
| var sch = | |
| input.headers[ | |
| Object.keys(input.headers).find(function (key) { | |
| return key.toLowerCase() === "set-cookie"; | |
| }) | |
| ]; | |
| // warn if called on a request-like object with a cookie header rather than a set-cookie header - see #34, 36 | |
| if (!sch && input.headers.cookie && !options.silent) { | |
| console.warn( | |
| "Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning." | |
| ); | |
| } | |
| input = sch; | |
| } | |
| } | |
| var split = options.split; | |
| var isArray = Array.isArray(input); | |
| if (split === "auto") { | |
| split = !isArray; | |
| } | |
| if (!isArray) { | |
| input = [input]; | |
| } | |
| input = input.filter(isNonEmptyString); | |
| if (split) { | |
| input = input.map(splitCookiesString).flat(); | |
| } | |
| if (!options.map) { | |
| return input | |
| .map(function (str) { | |
| return parseString(str, options); | |
| }) | |
| .filter(Boolean); | |
| } else { | |
| var cookies = createNullObj(); | |
| return input.reduce(function (cookies, str) { | |
| var cookie = parseString(str, options); | |
| if (cookie && !isForbiddenKey(cookie.name)) { | |
| cookies[cookie.name] = cookie; | |
| } | |
| return cookies; | |
| }, cookies); | |
| } | |
| } | |
| /* | |
| Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas | |
| that are within a single set-cookie field-value, such as in the Expires portion. | |
| This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 | |
| Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 | |
| React Native's fetch does this for *every* header, including set-cookie. | |
| Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 | |
| Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation | |
| */ | |
| function splitCookiesString(cookiesString) { | |
| if (Array.isArray(cookiesString)) { | |
| return cookiesString; | |
| } | |
| if (typeof cookiesString !== "string") { | |
| return []; | |
| } | |
| var cookiesStrings = []; | |
| var pos = 0; | |
| var start; | |
| var ch; | |
| var lastComma; | |
| var nextStart; | |
| var cookiesSeparatorFound; | |
| function skipWhitespace() { | |
| while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { | |
| pos += 1; | |
| } | |
| return pos < cookiesString.length; | |
| } | |
| function notSpecialChar() { | |
| ch = cookiesString.charAt(pos); | |
| return ch !== "=" && ch !== ";" && ch !== ","; | |
| } | |
| while (pos < cookiesString.length) { | |
| start = pos; | |
| cookiesSeparatorFound = false; | |
| while (skipWhitespace()) { | |
| ch = cookiesString.charAt(pos); | |
| if (ch === ",") { | |
| // ',' is a cookie separator if we have later first '=', not ';' or ',' | |
| lastComma = pos; | |
| pos += 1; | |
| skipWhitespace(); | |
| nextStart = pos; | |
| while (pos < cookiesString.length && notSpecialChar()) { | |
| pos += 1; | |
| } | |
| // currently special character | |
| if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { | |
| // we found cookies separator | |
| cookiesSeparatorFound = true; | |
| // pos is inside the next cookie, so back up and return it. | |
| pos = nextStart; | |
| cookiesStrings.push(cookiesString.substring(start, lastComma)); | |
| start = pos; | |
| } else { | |
| // in param ',' or param separator ';', | |
| // we continue from that comma | |
| pos = lastComma + 1; | |
| } | |
| } else { | |
| pos += 1; | |
| } | |
| } | |
| if (!cookiesSeparatorFound || pos >= cookiesString.length) { | |
| cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); | |
| } | |
| } | |
| return cookiesStrings; | |
| } | |
| // named export for CJS | |
| parseSetCookie.parseSetCookie = parseSetCookie; | |
| // for backwards compatibility | |
| parseSetCookie.parse = parseSetCookie; | |
| parseSetCookie.parseString = parseString; | |
| parseSetCookie.splitCookiesString = splitCookiesString; | |
| /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ | |
| /** | |
| * An error that was thrown from within the SvelteKit runtime that is not fatal and doesn't result in a 500, such as a 404. | |
| * `SvelteKitError` goes through `handleError`. | |
| * @extends Error | |
| */ | |
| class SvelteKitError extends Error { | |
| /** | |
| * @param {number} status | |
| * @param {string} text | |
| * @param {string} message | |
| */ | |
| constructor(status, text, message) { | |
| super(message); | |
| this.status = status; | |
| this.text = text; | |
| } | |
| } | |
| /** | |
| * @param {import('http').IncomingMessage} req | |
| * @param {number} [body_size_limit] | |
| */ | |
| function get_raw_body(req, body_size_limit) { | |
| const h = req.headers; | |
| if (!h['content-type']) { | |
| return null; | |
| } | |
| const content_length = Number(h['content-length']); | |
| // check if no request body | |
| if ( | |
| (req.httpVersionMajor === 1 && isNaN(content_length) && h['transfer-encoding'] == null) || | |
| content_length === 0 | |
| ) { | |
| return null; | |
| } | |
| if (req.destroyed) { | |
| const readable = new ReadableStream(); | |
| void readable.cancel(); | |
| return readable; | |
| } | |
| let size = 0; | |
| let cancelled = false; | |
| return new ReadableStream({ | |
| start(controller) { | |
| if (body_size_limit !== undefined && content_length > body_size_limit) { | |
| let message = `Content-length of ${content_length} exceeds limit of ${body_size_limit} bytes.`; | |
| if (body_size_limit === 0) { | |
| // https://github.com/sveltejs/kit/pull/11589 | |
| // TODO this exists to aid migration — remove in a future version | |
| message += ' To disable body size limits, specify Infinity rather than 0.'; | |
| } | |
| const error = new SvelteKitError(413, 'Payload Too Large', message); | |
| controller.error(error); | |
| return; | |
| } | |
| req.on('error', (error) => { | |
| cancelled = true; | |
| controller.error(error); | |
| }); | |
| req.on('end', () => { | |
| if (cancelled) return; | |
| controller.close(); | |
| }); | |
| req.on('data', (chunk) => { | |
| if (cancelled) return; | |
| size += chunk.length; | |
| if (size > content_length) { | |
| cancelled = true; | |
| const constraint = content_length ? 'content-length' : 'BODY_SIZE_LIMIT'; | |
| const message = `request body size exceeded ${constraint} of ${content_length}`; | |
| const error = new SvelteKitError(413, 'Payload Too Large', message); | |
| controller.error(error); | |
| return; | |
| } | |
| controller.enqueue(chunk); | |
| if (controller.desiredSize === null || controller.desiredSize <= 0) { | |
| req.pause(); | |
| } | |
| }); | |
| }, | |
| pull() { | |
| req.resume(); | |
| }, | |
| cancel(reason) { | |
| cancelled = true; | |
| req.destroy(reason); | |
| } | |
| }); | |
| } | |
| /** | |
| * @param {{ | |
| * request: import('http').IncomingMessage; | |
| * base: string; | |
| * bodySizeLimit?: number; | |
| * }} options | |
| * @returns {Promise<Request>} | |
| */ | |
| // TODO 3.0 make the signature synchronous? | |
| // eslint-disable-next-line @typescript-eslint/require-await | |
| async function getRequest({ request, base, bodySizeLimit }) { | |
| let headers = /** @type {Record<string, string>} */ (request.headers); | |
| if (request.httpVersionMajor >= 2) { | |
| // the Request constructor rejects headers with ':' in the name | |
| headers = Object.assign({}, headers); | |
| // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.5 | |
| if (headers[':authority']) { | |
| headers.host = headers[':authority']; | |
| } | |
| delete headers[':authority']; | |
| delete headers[':method']; | |
| delete headers[':path']; | |
| delete headers[':scheme']; | |
| } | |
| // TODO: Whenever Node >=22 is minimum supported version, we can use `request.readableAborted` | |
| // @see https://github.com/nodejs/node/blob/5cf3c3e24c7257a0c6192ed8ef71efec8ddac22b/lib/internal/streams/readable.js#L1443-L1453 | |
| const controller = new AbortController(); | |
| let errored = false; | |
| let end_emitted = false; | |
| request.once('error', () => (errored = true)); | |
| request.once('end', () => (end_emitted = true)); | |
| request.once('close', () => { | |
| if ((errored || request.destroyed) && !end_emitted) { | |
| controller.abort(); | |
| } | |
| }); | |
| return new Request(base + request.url, { | |
| // @ts-expect-error | |
| duplex: 'half', | |
| method: request.method, | |
| headers: Object.entries(headers), | |
| signal: controller.signal, | |
| body: | |
| request.method === 'GET' || request.method === 'HEAD' | |
| ? undefined | |
| : get_raw_body(request, bodySizeLimit) | |
| }); | |
| } | |
| /** | |
| * @param {import('http').ServerResponse} res | |
| * @param {Response} response | |
| * @returns {Promise<void>} | |
| */ | |
| // TODO 3.0 make the signature synchronous? | |
| // eslint-disable-next-line @typescript-eslint/require-await | |
| async function setResponse(res, response) { | |
| for (const [key, value] of response.headers) { | |
| try { | |
| res.setHeader( | |
| key, | |
| key === 'set-cookie' | |
| ? splitCookiesString( | |
| // This is absurd but necessary, TODO: investigate why | |
| /** @type {string}*/ (response.headers.get(key)) | |
| ) | |
| : value | |
| ); | |
| } catch (error) { | |
| res.getHeaderNames().forEach((name) => res.removeHeader(name)); | |
| res.writeHead(500).end(String(error)); | |
| return; | |
| } | |
| } | |
| res.writeHead(response.status); | |
| if (!response.body) { | |
| res.end(); | |
| return; | |
| } | |
| if (response.body.locked) { | |
| res.end( | |
| 'Fatal error: Response body is locked. ' + | |
| "This can happen when the response was already read (for example through 'response.json()' or 'response.text()')." | |
| ); | |
| return; | |
| } | |
| const reader = response.body.getReader(); | |
| if (res.destroyed) { | |
| void reader.cancel(); | |
| return; | |
| } | |
| const cancel = (/** @type {Error|undefined} */ error) => { | |
| res.off('close', cancel); | |
| res.off('error', cancel); | |
| // If the reader has already been interrupted with an error earlier, | |
| // then it will appear here, it is useless, but it needs to be catch. | |
| reader.cancel(error).catch(() => {}); | |
| if (error) res.destroy(error); | |
| }; | |
| res.on('close', cancel); | |
| res.on('error', cancel); | |
| void next(); | |
| async function next() { | |
| try { | |
| for (;;) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| if (!res.write(value)) { | |
| res.once('drain', next); | |
| return; | |
| } | |
| } | |
| res.end(); | |
| } catch (error) { | |
| cancel(error instanceof Error ? error : new Error(String(error))); | |
| } | |
| } | |
| } | |
| /** | |
| * Converts a file on disk to a readable stream | |
| * @param {string} file | |
| * @returns {ReadableStream} | |
| * @since 2.4.0 | |
| */ | |
| function createReadableStream(file) { | |
| return /** @type {ReadableStream} */ (Readable.toWeb(createReadStream(file))); | |
| } | |
| /** | |
| * Parses the given value into number of bytes. | |
| * | |
| * @param {string} value - Size in bytes. Can also be specified with a unit suffix kilobytes (K), megabytes (M), or gigabytes (G). | |
| * @returns {number} | |
| */ | |
| function parse_as_bytes(value) { | |
| const multiplier = | |
| { | |
| K: 1024, | |
| M: 1024 * 1024, | |
| G: 1024 * 1024 * 1024 | |
| }[value[value.length - 1]?.toUpperCase()] ?? 1; | |
| return Number(multiplier != 1 ? value.substring(0, value.length - 1) : value) * multiplier; | |
| } | |
| /** | |
| * Parses and validates an origin URL. | |
| * | |
| * @param {string | undefined} value - Origin URL with http:// or https:// protocol | |
| * @returns {string | undefined} The validated origin, or undefined if value is undefined | |
| * @throws {Error} If value is provided but invalid | |
| */ | |
| function parse_origin(value) { | |
| if (value === undefined) { | |
| return undefined; | |
| } | |
| const trimmed = value.trim(); | |
| let url; | |
| try { | |
| url = new URL(trimmed); | |
| } catch (error) { | |
| throw new Error( | |
| `Invalid ORIGIN: '${trimmed}'. ` + | |
| `ORIGIN must be a valid URL with http:// or https:// protocol. ` + | |
| `For example: 'http://localhost:3000' or 'https://my.site'`, | |
| { cause: error } | |
| ); | |
| } | |
| if (url.protocol !== 'http:' && url.protocol !== 'https:') { | |
| throw new Error( | |
| `Invalid ORIGIN: '${trimmed}'. ` + | |
| `Only http:// and https:// protocols are supported. ` + | |
| `Received protocol: ${url.protocol}` | |
| ); | |
| } | |
| return url.origin; | |
| } | |
| /* global "" */ | |
| /* global true */ | |
| const server = new Server(manifest); | |
| // parse_origin validates ORIGIN and throws descriptive errors for invalid values | |
| const origin = parse_origin(env('ORIGIN', undefined)); | |
| const xff_depth = parseInt(env('XFF_DEPTH', '1')); | |
| const address_header = env('ADDRESS_HEADER', '').toLowerCase(); | |
| const protocol_header = env('PROTOCOL_HEADER', '').toLowerCase(); | |
| const host_header = env('HOST_HEADER', '').toLowerCase(); | |
| const port_header = env('PORT_HEADER', '').toLowerCase(); | |
| const body_size_limit = parse_as_bytes(env('BODY_SIZE_LIMIT', '512K')); | |
| if (isNaN(body_size_limit)) { | |
| throw new Error( | |
| `Invalid BODY_SIZE_LIMIT: '${env('BODY_SIZE_LIMIT')}'. Please provide a numeric value.` | |
| ); | |
| } | |
| const dir = path.dirname(fileURLToPath(import.meta.url)); | |
| const asset_dir = `${dir}/client${base}`; | |
| await server.init({ | |
| env: /** @type {Record<string, string>} */ (process.env), | |
| read: (file) => createReadableStream(`${asset_dir}/${file}`) | |
| }); | |
| /** | |
| * @param {string} path | |
| * @param {boolean} client | |
| */ | |
| function serve(path, client = false) { | |
| return fs__default.existsSync(path) | |
| ? sirv(path, { | |
| etag: true, | |
| gzip: true, | |
| brotli: true, | |
| setHeaders: client | |
| ? (res, pathname) => { | |
| // only apply to build directory, not e.g. version.json | |
| if ( | |
| pathname.startsWith(`/${manifest.appPath}/immutable/`) && | |
| res.statusCode === 200 | |
| ) { | |
| res.setHeader('cache-control', 'public,max-age=31536000,immutable'); | |
| } | |
| } | |
| : undefined | |
| }) | |
| : undefined; | |
| } | |
| // required because the static file server ignores trailing slashes | |
| /** @returns {import('polka').Middleware} */ | |
| function serve_prerendered() { | |
| const handler = serve(path.join(dir, 'prerendered')); | |
| return (req, res, next) => { | |
| let { pathname, search, query } = parse(req); | |
| try { | |
| pathname = decodeURIComponent(pathname); | |
| } catch { | |
| // ignore invalid URI | |
| } | |
| if (prerendered.has(pathname)) { | |
| return handler?.(req, res, next); | |
| } | |
| // remove or add trailing slash as appropriate | |
| let location = pathname.at(-1) === '/' ? pathname.slice(0, -1) : pathname + '/'; | |
| if (prerendered.has(location)) { | |
| if (query) location += search; | |
| res.writeHead(308, { location }).end(); | |
| } else { | |
| void next(); | |
| } | |
| }; | |
| } | |
| /** @type {import('polka').Middleware} */ | |
| const ssr = async (req, res) => { | |
| /** @type {Request} */ | |
| let request; | |
| try { | |
| request = await getRequest({ | |
| base: origin || get_origin(req.headers), | |
| request: req, | |
| bodySizeLimit: body_size_limit | |
| }); | |
| } catch { | |
| res.statusCode = 400; | |
| res.end('Bad Request'); | |
| return; | |
| } | |
| await setResponse( | |
| res, | |
| await server.respond(request, { | |
| platform: { req }, | |
| getClientAddress: () => { | |
| if (address_header) { | |
| if (!(address_header in req.headers)) { | |
| throw new Error( | |
| `Address header was specified with ${ | |
| "" + 'ADDRESS_HEADER' | |
| }=${address_header} but is absent from request` | |
| ); | |
| } | |
| const value = /** @type {string} */ (req.headers[address_header]) || ''; | |
| if (address_header === 'x-forwarded-for') { | |
| const addresses = value.split(','); | |
| if (xff_depth < 1) { | |
| throw new Error(`${"" + 'XFF_DEPTH'} must be a positive integer`); | |
| } | |
| if (xff_depth > addresses.length) { | |
| throw new Error( | |
| `${"" + 'XFF_DEPTH'} is ${xff_depth}, but only found ${ | |
| addresses.length | |
| } addresses` | |
| ); | |
| } | |
| return addresses[addresses.length - xff_depth].trim(); | |
| } | |
| return value; | |
| } | |
| return ( | |
| req.connection?.remoteAddress || | |
| // @ts-expect-error | |
| req.connection?.socket?.remoteAddress || | |
| req.socket?.remoteAddress || | |
| // @ts-expect-error | |
| req.info?.remoteAddress | |
| ); | |
| } | |
| }) | |
| ); | |
| }; | |
| /** @param {import('polka').Middleware[]} handlers */ | |
| function sequence(handlers) { | |
| /** @type {import('polka').Middleware} */ | |
| return (req, res, next) => { | |
| /** | |
| * @param {number} i | |
| * @returns {ReturnType<import('polka').Middleware>} | |
| */ | |
| function handle(i) { | |
| if (i < handlers.length) { | |
| return handlers[i](req, res, () => handle(i + 1)); | |
| } else { | |
| return next(); | |
| } | |
| } | |
| return handle(0); | |
| }; | |
| } | |
| /** | |
| * @param {string} name | |
| * @param {string | string[] | undefined} value | |
| * @returns {string | undefined} | |
| */ | |
| function normalise_header(name, value) { | |
| if (!name) return undefined; | |
| if (Array.isArray(value)) { | |
| if (value.length === 0) return undefined; | |
| if (value.length === 1) return value[0]; | |
| throw new Error( | |
| `Multiple values provided for ${name} header where only one expected: ${value}` | |
| ); | |
| } | |
| return value; | |
| } | |
| /** | |
| * @param {import('http').IncomingHttpHeaders} headers | |
| * @returns {string} | |
| */ | |
| function get_origin(headers) { | |
| const protocol = decodeURIComponent( | |
| normalise_header(protocol_header, headers[protocol_header]) || 'https' | |
| ); | |
| // this helps us avoid host injections through the protocol header | |
| if (protocol.includes(':')) { | |
| throw new Error( | |
| `The ${protocol_header} header specified ${protocol} which is an invalid because it includes \`:\`. It should only contain the protocol scheme (e.g. \`https\`)` | |
| ); | |
| } | |
| const host = | |
| normalise_header(host_header, headers[host_header]) || | |
| normalise_header('host', headers['host']); | |
| if (!host) { | |
| const header_names = host_header ? `${host_header} or host headers` : 'host header'; | |
| throw new Error( | |
| `Could not determine host. The request must have a value provided by the ${header_names}` | |
| ); | |
| } | |
| const port = normalise_header(port_header, headers[port_header]); | |
| if (port && isNaN(+port)) { | |
| throw new Error( | |
| `The ${port_header} header specified ${port} which is an invalid port because it is not a number. The value should only contain the port number (e.g. 443)` | |
| ); | |
| } | |
| return port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`; | |
| } | |
| const handler = sequence( | |
| /** @type {(import('sirv').RequestHandler | import('polka').Middleware)[]} */ | |
| ([serve(path.join(dir, 'client'), true), serve_prerendered(), ssr].filter(Boolean)) | |
| ); | |
| export { handler }; | |