feat: docker compose maybe
This commit is contained in:
		
							
								
								
									
										194
									
								
								node_modules/sirv/build.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								node_modules/sirv/build.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const { join, normalize, resolve } = require('path');
 | 
			
		||||
const { totalist } = require('totalist/sync');
 | 
			
		||||
const { parse } = require('@polka/url');
 | 
			
		||||
const { lookup } = require('mrmime');
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = function (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, 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);
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										194
									
								
								node_modules/sirv/build.mjs
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								node_modules/sirv/build.mjs
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,194 @@
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function (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, 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);
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								node_modules/sirv/package.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								node_modules/sirv/package.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "sirv",
 | 
			
		||||
  "version": "2.0.3",
 | 
			
		||||
  "description": "The optimized & lightweight middleware for serving requests to static assets",
 | 
			
		||||
  "repository": "lukeed/sirv",
 | 
			
		||||
  "module": "build.mjs",
 | 
			
		||||
  "types": "sirv.d.ts",
 | 
			
		||||
  "main": "build.js",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "files": [
 | 
			
		||||
    "build.*",
 | 
			
		||||
    "sirv.d.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": {
 | 
			
		||||
    "name": "Luke Edwards",
 | 
			
		||||
    "email": "luke@lukeed.com",
 | 
			
		||||
    "url": "https://lukeed.com"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">= 10"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@polka/url": "^1.0.0-next.20",
 | 
			
		||||
    "mrmime": "^1.0.0",
 | 
			
		||||
    "totalist": "^3.0.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										238
									
								
								node_modules/sirv/readme.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								node_modules/sirv/readme.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
			
		||||
# sirv 
 | 
			
		||||
 | 
			
		||||
> The optimized and lightweight middleware for serving requests to static assets
 | 
			
		||||
 | 
			
		||||
You may use `sirv` as a *very* fast and lightweight alternative to [`serve-static`](https://www.npmjs.com/package/serve-static).
 | 
			
		||||
 | 
			
		||||
The massive performance advantage over `serve-static` is explained by **not** relying on the file system for existence checks on every request. These are expensive interactions and must be avoided whenever possible! Instead, when not in "dev" mode, `sirv` performs all its file-system operations upfront and then relies on its cache for future operations.
 | 
			
		||||
 | 
			
		||||
This middleware will work out of the box for [Polka](https://github.com/lukeed/polka), Express, and other Express-like frameworks. It will also work with the native `http`, `https` and `http2` modules. It requires _very_ little effort to modify/wrap it for servers that don't accept the `(req, res, next)` signature.
 | 
			
		||||
 | 
			
		||||
:bulb: For a feature-complete CLI application, check out the sibling [`sirv-cli`](https://github.com/lukeed/sirv/tree/master/packages/sirv-cli) package as an alternative to [`zeit/serve`](https://github.com/zeit/serve)~!
 | 
			
		||||
 | 
			
		||||
## Install
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ npm install --save sirv
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
const sirv = require('sirv');
 | 
			
		||||
const polka = require('polka');
 | 
			
		||||
const compress = require('compression')();
 | 
			
		||||
 | 
			
		||||
// Init `sirv` handler
 | 
			
		||||
const assets = sirv('public', {
 | 
			
		||||
  maxAge: 31536000, // 1Y
 | 
			
		||||
  immutable: true
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
polka()
 | 
			
		||||
  .use(compress, assets)
 | 
			
		||||
  .use('/api', require('./api'))
 | 
			
		||||
  .listen(3000, err => {
 | 
			
		||||
    if (err) throw err;
 | 
			
		||||
    console.log('> Ready on localhost:3000~!');
 | 
			
		||||
  });
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## API
 | 
			
		||||
 | 
			
		||||
### sirv(dir, opts={})
 | 
			
		||||
 | 
			
		||||
Returns: `Function`
 | 
			
		||||
 | 
			
		||||
The returned function is a middleware in the standard Express-like signature: `(req, res, next)`, where `req` is the [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage), `res` is the [`http.ServerResponse`](https://nodejs.org/dist/latest-v9.x/docs/api/http.html#http_class_http_serverresponse), and `next` (in this case) is the function to call if no file was found for the given path.
 | 
			
		||||
 | 
			
		||||
When defined, a `next()` callback is always called _instead of_ the [`opts.onNoMatch`](#optsonnomatch) callback. However, unlike `onNoMatch`, your `next()` is given no arguments.
 | 
			
		||||
 | 
			
		||||
#### dir
 | 
			
		||||
Type: `String`<br>
 | 
			
		||||
Default: `.`
 | 
			
		||||
 | 
			
		||||
The directory from which to read and serve assets. It is resolved to an absolute path — you must provide an absolute path yourself if `process.cwd()` is not the correct assumption.
 | 
			
		||||
 | 
			
		||||
#### opts.dev
 | 
			
		||||
Type: `Boolean`<br>
 | 
			
		||||
Default: `false`
 | 
			
		||||
 | 
			
		||||
Enable "dev" mode, which disables/skips caching. Instead, `sirv` will traverse the file system ***on every request***.
 | 
			
		||||
 | 
			
		||||
Additionally, `dev` mode will ignore `maxAge` and `immutable` as these options generate a production-oriented `Cache-Control` header value.
 | 
			
		||||
 | 
			
		||||
> **Important:** Do not use `dev` mode in production!
 | 
			
		||||
 | 
			
		||||
#### opts.etag
 | 
			
		||||
Type: `Boolean`<br>
 | 
			
		||||
Default: `false`
 | 
			
		||||
 | 
			
		||||
Generate and attach an `ETag` header to responses.
 | 
			
		||||
 | 
			
		||||
> **Note:** If an incoming request's [`If-None-Match` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) matches the `ETag` value, a `304` response is given.
 | 
			
		||||
 | 
			
		||||
#### opts.dotfiles
 | 
			
		||||
Type: `Boolean`<br>
 | 
			
		||||
Default: `false`
 | 
			
		||||
 | 
			
		||||
Allow requests to dotfiles (files or directories beginning with a `.`).
 | 
			
		||||
 | 
			
		||||
> **Note:** Requests to [`/.well-known/*`](https://tools.ietf.org/html/rfc8615) are always allowed.
 | 
			
		||||
 | 
			
		||||
#### opts.extensions
 | 
			
		||||
Type: `Array<String>`<br>
 | 
			
		||||
Default: `['html', 'htm']`
 | 
			
		||||
 | 
			
		||||
The file extension fallbacks to check for if a pathame is not initially found. For example, if a `/login` request cannot find a `login` filename, it will then look for `login.html` and `login.htm` before giving up~!
 | 
			
		||||
 | 
			
		||||
> **Important:** Actually, `sirv` will **also** look for `login/index.html` and `login/index.htm` before giving up.
 | 
			
		||||
 | 
			
		||||
#### opts.gzip
 | 
			
		||||
Type: `Boolean`<br>
 | 
			
		||||
Default: `false`
 | 
			
		||||
 | 
			
		||||
Determine if `sirv` look for **precompiled** `*.gz` files.<br>
 | 
			
		||||
Must be enabled _and_ the incoming request's [`Accept Encoding`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) must include "gzip" in order for `sirv` to search for the gzip'd alternative.
 | 
			
		||||
 | 
			
		||||
> **Note:** The `.gz` assumption also applies to the `opts.extensions` list.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
// NOTE: PSEUDO CODE
 | 
			
		||||
// Showing lookup logic
 | 
			
		||||
 | 
			
		||||
// Request: [Accept-Encoding: gzip] "/foobar.jpg"
 | 
			
		||||
lookup([
 | 
			
		||||
  '/foobar.jpg.gz', '/foobar.jpg',
 | 
			
		||||
  '/foobar.jpg.html.gz', '/foobar.jpg/index.html.gz',
 | 
			
		||||
  '/foobar.jpg.htm.gz', '/foobar.jpg/index.htm.gz',
 | 
			
		||||
  '/foobar.jpg.html', '/foobar.jpg/index.html',
 | 
			
		||||
  '/foobar.jpg.htm', '/foobar.jpg/index.htm',
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// Request: [Accept-Encoding: gzip] "/"
 | 
			
		||||
lookup([
 | 
			
		||||
  '/index.html.gz',
 | 
			
		||||
  '/index.htm.gz',
 | 
			
		||||
  '/index.html',
 | 
			
		||||
  '/index.htm',
 | 
			
		||||
]);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#### opts.brotli
 | 
			
		||||
Type: `Boolean`<br>
 | 
			
		||||
Default: `false`
 | 
			
		||||
 | 
			
		||||
Determine if `sirv` look for **precompiled** `*.br` files.<br>
 | 
			
		||||
Must be enabled _and_ the incoming request's [`Accept Encoding`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) must include either "br" or "brotli" in order for `sirv` to search for the brotli-compressed alternative.
 | 
			
		||||
 | 
			
		||||
> **Note:** The `.br` assumption also applies to the `opts.extensions` list.
 | 
			
		||||
 | 
			
		||||
When both `opts.broli` and `opts.gzip` are enabled — and all conditions are equal — then the brotli variant always takes priority.
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
// NOTE: PSEUDO CODE
 | 
			
		||||
// Showing lookup logic
 | 
			
		||||
 | 
			
		||||
// Request: [Accept-Encoding: br] "/foobar.jpg"
 | 
			
		||||
lookup([
 | 
			
		||||
  '/foobar.jpg.br', '/foobar.jpg',
 | 
			
		||||
  '/foobar.jpg.html.br', '/foobar.jpg/index.html.br',
 | 
			
		||||
  '/foobar.jpg.htm.br', '/foobar.jpg/index.htm.br',
 | 
			
		||||
  '/foobar.jpg.html', '/foobar.jpg/index.html',
 | 
			
		||||
  '/foobar.jpg.htm', '/foobar.jpg/index.htm',
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// Request: [Accept-Encoding: br,gz] "/"
 | 
			
		||||
lookup([
 | 
			
		||||
  '/index.html.br'
 | 
			
		||||
  '/index.htm.br'
 | 
			
		||||
  '/index.html.gz'
 | 
			
		||||
  '/index.htm.gz'
 | 
			
		||||
  '/index.html'
 | 
			
		||||
  '/index.htm'
 | 
			
		||||
]);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### opts.maxAge
 | 
			
		||||
Type: `Number`<br>
 | 
			
		||||
Default: `undefined`
 | 
			
		||||
 | 
			
		||||
Enables the `Cache-Control` header on responses and sets the `max-age` value (in seconds).<br>
 | 
			
		||||
For example, `maxAge: 31536000` is equivalent to one year.
 | 
			
		||||
 | 
			
		||||
#### opts.immutable
 | 
			
		||||
Type: `Boolean`<br>
 | 
			
		||||
Default: `false`
 | 
			
		||||
 | 
			
		||||
Appends the [`immutable` directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Revalidation_and_reloading) on your `Cache-Control` header, used for uniquely-named assets that will not change!
 | 
			
		||||
 | 
			
		||||
> **Important:** Will only work if `opts.maxAge` has a value defined!
 | 
			
		||||
 | 
			
		||||
#### opts.single
 | 
			
		||||
Type: `Boolean` or `String`<br>
 | 
			
		||||
Default: `false`
 | 
			
		||||
 | 
			
		||||
Treat the directory as a single-page application.
 | 
			
		||||
 | 
			
		||||
When `true`, the directory's index page (default `index.html`) will be sent if the request asset does not exist.<br>
 | 
			
		||||
You may pass a `string` value to use a file _instead of_ `index.html` as your fallback.
 | 
			
		||||
 | 
			
		||||
For example, if "/about" is requested but no variants of that file exist, then the response for "/" is sent instead:
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
// Note: This is psuedo code to illustrate what's happening
 | 
			
		||||
 | 
			
		||||
// Request: "/about"
 | 
			
		||||
let file = find(['/about', '/about.html', '/about.htm', '/about/index.html', '/about.htm']);
 | 
			
		||||
if (file) {
 | 
			
		||||
  send(file);
 | 
			
		||||
} else if (opts.single === true) {
 | 
			
		||||
  file = find(['/', '/index.html', '/index.htm']);
 | 
			
		||||
  send(file);
 | 
			
		||||
} else if (typeof opts.single === 'string') {
 | 
			
		||||
  file = find([opts.single]);
 | 
			
		||||
  send(file);
 | 
			
		||||
} else {
 | 
			
		||||
  // next() or 404
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### opts.ignores
 | 
			
		||||
Type: `false` or `Array<String | RegExp>`
 | 
			
		||||
 | 
			
		||||
Specify paths/patterns that should ignore the fallback behavior that `opts.single` provides.
 | 
			
		||||
 | 
			
		||||
By default, any asset-like path (URLs that end with an extension) will be ignored. This means that, for example, if `/foobar.jpg` is not found, a `404` response is sent instead of the `index.html` fallback.
 | 
			
		||||
 | 
			
		||||
Additionally, any `/.well-known/*` pathname ignores the fallback – as do all other dotfile requests when `opts.dotfiles` is enabled.
 | 
			
		||||
 | 
			
		||||
Any string value(s) will be passed through `new RegExp(value, 'i')` directly.
 | 
			
		||||
 | 
			
		||||
Finally, you may set `ignores: false` to disable ***all*** ignores, including the defaults. Put differently, this will fallback ***all*** unknown pathnames to your `index.html` (or custom `opts.single` value).
 | 
			
		||||
 | 
			
		||||
> **Important:** Only has an effect if `opts.single` is enabled.
 | 
			
		||||
 | 
			
		||||
#### opts.onNoMatch
 | 
			
		||||
Type: `Function`
 | 
			
		||||
 | 
			
		||||
A custom function to run if a file cannot be found for a given request. <br>By default, `sirv` will send a basic `(404) Not found` response.
 | 
			
		||||
 | 
			
		||||
The function receives the current `req <IncomingMessage>, res <ServerResponse>` pair for as its two arguments.
 | 
			
		||||
 | 
			
		||||
> **Note:** This won't run if a `next` callback has been provided to the middleware; see [`sirv`](#sirvdir-opts) description.
 | 
			
		||||
 | 
			
		||||
#### opts.setHeaders
 | 
			
		||||
Type: `Function`
 | 
			
		||||
 | 
			
		||||
A custom function to append or change any headers on the outgoing response. There is no default.
 | 
			
		||||
 | 
			
		||||
Its signature is `(res, pathname, stats)`, where `res` is the `ServerResponse`, `pathname` is incoming request path (stripped of queries), and `stats` is the file's result from [`fs.statSync`](https://nodejs.org/api/fs.html#fs_fs_statsync_path).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
MIT © [Luke Edwards](https://lukeed.com)
 | 
			
		||||
							
								
								
									
										25
									
								
								node_modules/sirv/sirv.d.ts
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								node_modules/sirv/sirv.d.ts
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
declare module 'sirv' {
 | 
			
		||||
	import type { Stats } from 'fs';
 | 
			
		||||
	import type { IncomingMessage, ServerResponse } from 'http';
 | 
			
		||||
 | 
			
		||||
	type Arrayable<T> = T | T[];
 | 
			
		||||
	export type NextHandler = () => void | Promise<void>;
 | 
			
		||||
	export type RequestHandler = (req: IncomingMessage, res: ServerResponse, next?: NextHandler) => void;
 | 
			
		||||
 | 
			
		||||
	export interface Options {
 | 
			
		||||
		dev?: boolean;
 | 
			
		||||
		etag?: boolean;
 | 
			
		||||
		maxAge?: number;
 | 
			
		||||
		immutable?: boolean;
 | 
			
		||||
		single?: string | boolean;
 | 
			
		||||
		ignores?: false | Arrayable<string | RegExp>;
 | 
			
		||||
		extensions?: string[];
 | 
			
		||||
		dotfiles?: boolean;
 | 
			
		||||
		brotli?: boolean;
 | 
			
		||||
		gzip?: boolean;
 | 
			
		||||
		onNoMatch?: (req: IncomingMessage, res: ServerResponse) => void;
 | 
			
		||||
		setHeaders?: (res: ServerResponse, pathname: string, stats: Stats) => void;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export default function(dir?: string, opts?: Options): RequestHandler;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user