diff options
Diffstat (limited to 'packages/integrations/partytown/src/sirv.ts')
-rw-r--r-- | packages/integrations/partytown/src/sirv.ts | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/packages/integrations/partytown/src/sirv.ts b/packages/integrations/partytown/src/sirv.ts new file mode 100644 index 000000000..860a715bf --- /dev/null +++ b/packages/integrations/partytown/src/sirv.ts @@ -0,0 +1,241 @@ +// TODO: The below has been modified from the original sirv package to support +// the feature of mounting the served files from a certain path (in this case, `/~partytown/`) +// It would be good to bring this into Astro for all integrations to take advantage of, +// and potentially also to respect your config automatically for things like `base` path. +// @ts-nocheck + +/** + * @license + * + * The MIT License (MIT) + * + * Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (https://lukeed.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import * as fs from 'fs'; +import { join, normalize, resolve } from 'path'; +// import { totalist } from 'totalist/sync'; +// import { parse } from '@polka/url'; +import { lookup } from 'mrmime'; +import { URL } from 'url'; + +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 (start >= stats.size || end >= 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; +} + +export default function (dir, opts = {}) { + dir = resolve(dir || '.'); + + let mountTo = opts.mount || ''; + 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, isEtag) : viaCache.bind(0, FILES); + + return function (req, res, next) { + let extns = ['']; + let pathname = new URL(req.url, 'https://example.dev').pathname; + // NEW + if (mountTo && pathname.startsWith(mountTo)) { + pathname = pathname.substring(mountTo.length); + } + // NEW END + 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 = decodeURIComponent(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); + }; +} |