243 lines
6.9 KiB
TypeScript
243 lines
6.9 KiB
TypeScript
// 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 fileLookup = 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 =
|
|
fileLookup(pathname, extns) ||
|
|
(isSPA && !isMatch(pathname, ignores) && fileLookup(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);
|
|
};
|
|
}
|