Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

servitsy

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

servitsy - npm Package Compare versions

Comparing version 0.1.2 to 0.1.3

lib/content-type.js

137

lib/constants.js

@@ -5,14 +5,4 @@ /**

@typedef {import('./types.js').PortsConfig} PortsConfig
@typedef {{
default: string;
file: string[];
extension: string[];
extensionMap: Record<string, string>;
suffix: string[];
}} ContentTypes
**/
const strarr = (s = '') => s.trim().split(/\s+/);
/** @type {readonly string[]} */

@@ -93,128 +83,1 @@ export const EXTENSIONS_DEFAULT = Object.freeze(['.html']);

});
export const DEFAULT_CHARSET = 'UTF-8';
/** @type {ContentTypes} */
export const TEXT_TYPES = {
default: 'text/plain',
extensionMap: {
atom: 'application/atom+xml',
cjs: 'text/javascript',
css: 'text/css',
csv: 'text/csv',
htm: 'text/html',
html: 'text/html',
ics: 'text/calendar',
js: 'text/javascript',
json: 'application/json',
json5: 'text/plain',
jsonc: 'text/plain',
jsonld: 'application/ld+json',
map: 'application/json',
md: 'text/markdown',
mdown: 'text/markdown',
mjs: 'text/javascript',
rss: 'application/rss+xml',
sql: 'application/sql',
svg: 'image/svg+xml',
text: 'text/plain',
txt: 'text/plain',
xhtml: 'application/xhtml+xml',
xml: 'application/xml',
},
// Loosely based on npm:textextensions
extension: strarr(`
ada adb ads as ascx asm asmx asp aspx atom
bas bat bbcolors bdsgroup bdsproj bib
c cbl cc cfc cfg cfm cfml cgi clj cls cmake cmd cnf cob coffee conf cpp cpt cpy crt cs cson csr ctl cxx
dart dfm diff dof dpk dproj dtd
eco ejs el emacs eml ent erb erl ex exs
for fpp frm ftn
go gpp gradle groovy groupproj grunit gtmpl
h haml hbs hh hpp hrl hs hta htc hxx
iced inc ini ino int itcl itk
jade java jhtm jhtml js jsp jspx jsx
latex less lhs liquid lisp log ls lsp lua
m mak markdown mdwn mdx metadata mht mhtml mjs mk mkd mkdn mkdown ml mli mm mxml
nfm nfo njk noon
ops pas pasm patch pbxproj pch pem pg php pir pl pm pmc pod pot properties props ps1 pt pug py
r rake rb rdoc resx rhtml rjs rlib rmd ron rs rst rtf rxml
s sass scala scm scss sh shtml sls spec sql sqlite ss sss st strings sty styl stylus sub sv svc svelte
t tcl tex textile tg tmpl toml tpl ts tsv tsx tt tt2 ttml txt
v vb vbs vh vhd vhdl vim vue
wxml wxss x-php xaml xht xs xsd xsl xslt
`),
file: strarr(`
.gitattributes .gitkeep .gitignore .gitmodules
.htaccess .htpasswd
.viminfo .vimrc
`),
suffix: strarr(`config file html ignore rc`),
};
/**
* @type {ContentTypes}
*/
export const BIN_TYPES = {
default: 'application/octet-stream',
extensionMap: {
'7z': 'application/x-7z-compressed',
aac: 'audio/aac',
apng: 'image/apng',
aif: 'audio/aiff',
aiff: 'audio/aiff',
avi: 'video/x-msvideo',
avif: 'image/avif',
bmp: 'image/bmp',
bz: 'application/x-bzip',
bz2: 'application/x-bzip2',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
epub: 'application/epub+zip',
flac: 'audio/flac',
gif: 'image/gif',
gzip: 'application/gzip',
gz: 'application/gzip',
ico: 'image/x-icon',
jpg: 'image/jpg',
jpeg: 'image/jpg',
jxl: 'image/jxl',
jxr: 'image/jxr',
mid: 'audio/midi',
midi: 'audio/midi',
mp3: 'audio/mpeg',
mp4: 'video/mp4',
mpeg: 'video/mpeg',
ods: 'application/vnd.oasis.opendocument.spreadsheet',
odt: 'application/vnd.oasis.opendocument.text',
oga: 'audio/ogg',
ogg: 'audio/ogg',
ogv: 'video/ogg',
opus: 'audio/opus',
otf: 'font/otf',
pdf: 'application/pdf',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
png: 'image/png',
rar: 'application/vnd.rar',
rtf: 'application/rtf',
tar: 'application/x-tar',
tif: 'image/tiff',
tiff: 'image/tiff',
ttf: 'font/ttf',
wav: 'audio/wav',
weba: 'audio/webm',
webm: 'video/webm',
webp: 'image/webp',
woff: 'font/woff',
woff2: 'font/woff2',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
yaml: 'application/yaml',
yml: 'application/yaml',
zip: 'application/zip',
},
extension: strarr('bin exe link msi so'),
file: [],
suffix: [],
};

42

lib/logger.js

@@ -1,6 +0,5 @@

import { relative } from 'node:path';
import { stderr, stdout } from 'node:process';
import { inspect } from 'node:util';
import { color, clamp, fwdSlash, withResolvers } from './utils.js';
import { color, clamp, fwdSlash, withResolvers, trimSlash } from './utils.js';

@@ -100,13 +99,5 @@ /**

* @param {import('./types.js').ReqResInfo} info
* @returns {string}
*/
export function requestLogLine({
filePath,
urlPath,
status,
startedAt,
endedAt,
root,
method,
error,
}) {
export function requestLogLine({ startedAt, endedAt, status, method, urlPath, localPath, error }) {
const { brackets, style } = color;

@@ -118,11 +109,7 @@

let displayPath = style(urlPath, 'cyan');
const fileUrlPath = filePath ? '/' + fwdSlash(relative(root, filePath)) : undefined;
if (isSuccess && fileUrlPath && fileUrlPath != '/') {
const hasSuffix = fileUrlPath.startsWith(urlPath) && !fileUrlPath.endsWith(urlPath);
const suffix = hasSuffix
? fileUrlPath.slice(fileUrlPath.lastIndexOf(urlPath) + urlPath.length)
: '';
if (suffix.length > 1) {
displayPath += brackets(suffix, 'dim,gray,dim');
if (isSuccess && localPath) {
const basePath = urlPath.length > 1 ? trimSlash(urlPath, { end: true }) : urlPath;
const suffix = pathSuffix(basePath, `/${fwdSlash(localPath)}`);
if (suffix) {
displayPath = style(basePath, 'cyan') + brackets(suffix, 'dim,gray,dim');
}

@@ -144,1 +131,14 @@ }

}
/**
* @param {string} basePath
* @param {string} fullPath
* @returns {string | undefined}
*/
function pathSuffix(basePath, fullPath) {
if (basePath === fullPath) {
return '';
} else if (fullPath.startsWith(basePath)) {
return fullPath.slice(basePath.length);
}
}

@@ -61,3 +61,3 @@ import { access, constants, lstat, readdir, realpath } from 'node:fs/promises';

* @param {import('node:fs').Dirent | import('node:fs').StatsBase<any>} stats
* @returns {import('./types.js').FSEntryKind}
* @returns {import('./types.js').FSEntryKind | null}
*/

@@ -64,0 +64,0 @@ function statsKind(stats) {

import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { basename, dirname, join } from 'node:path';
import { clamp, escapeHtml, fwdSlash, getDirname, trimSlash } from './utils.js';
import { clamp, escapeHtml, getDirname, trimSlash } from './utils.js';
/**
@typedef {import('./types.js').DirIndexItem} DirIndexItem
@typedef {import('./types.js').ResolvedFile} ResolvedFile
@typedef {import('./types.js').ServerOptions} ServerOptions

@@ -29,3 +30,3 @@ **/

} else {
const contents = await readFile(fullPath, { encoding: 'utf-8' });
const contents = await readFile(fullPath, { encoding: 'utf8' });
assetCache.set(fullPath, contents);

@@ -93,18 +94,19 @@ return contents;

/**
* @param {{ dirPath: string; urlPath: string, items: DirIndexItem[] }} data
* @param {{ urlPath: string; file: ResolvedFile; items: DirIndexItem[] }} data
* @param {Pick<ServerOptions, 'root' | 'ext'>} options
* @returns {Promise<string>}
*/
export function dirListPage({ dirPath, urlPath, items }, options) {
let baseUrl = '/';
let displayPath = basename(options.root);
export function dirListPage({ urlPath, file, items }, options) {
const rootName = basename(options.root);
const trimmedUrl = trimSlash(urlPath);
const baseUrl = trimmedUrl ? `/${trimmedUrl}/` : '/';
const displayPath = decodeURIPathSegments(trimmedUrl ? `${rootName}/${trimmedUrl}` : rootName);
const sorted = [...items.filter((x) => isDirLike(x)), ...items.filter((x) => !isDirLike(x))];
const trimmedUrlPath = trimSlash(urlPath);
if (trimmedUrlPath) {
baseUrl = `/${trimmedUrlPath}/`;
displayPath = decodeURIPathSegments(`${displayPath}/${trimmedUrlPath}`);
if (trimmedUrl !== '') {
sorted.unshift({
filePath: join(dirPath, '..'),
filePath: dirname(file.filePath),
localPath: file.localPath && dirname(file.localPath),
kind: 'dir',

@@ -111,0 +113,0 @@ isParent: true,

@@ -56,6 +56,5 @@ import { fwdSlash, trimSlash } from './utils.js';

const result = {
urlPath: urlPath ?? url,
status: 404,
kind: null,
filePath: null,
urlPath: urlPath ?? url,
file: null,
};

@@ -81,6 +80,13 @@

if (resource.kind === 'dir' || resource.kind === 'file') {
Object.assign(result, resource);
const localPath = this.localPath(resource.filePath);
result.file = {
filePath: resource.filePath,
localPath: this.localPath(resource.filePath),
kind: resource.kind,
};
const enabled = resource.kind === 'file' || (resource.kind === 'dir' && this.#dirList);
const allowed = this.allowedPath(resource.filePath);
if (enabled && allowed) {
if (enabled && this.allowedPath(localPath)) {
const readable = await this.#fsUtils.readable(resource.filePath, resource.kind);

@@ -107,34 +113,45 @@ result.status = readable ? 200 : 403;

return { kind: targetKind, filePath: targetPath };
} else if (targetKind === 'dir') {
const candidates = this.#dirFile.map((name) => this.#fsUtils.join(targetPath, name));
for (const file of candidates) {
const kind = await this.#fsUtils.kind(file);
if (kind === 'file' || kind === 'link') {
return { kind, filePath: file };
}
}
/** @type {string[]} */
let candidates = [];
if (targetKind === 'dir' && this.#dirFile.length) {
candidates = this.#dirFile.map((name) => this.#fsUtils.join(targetPath, name));
} else if (targetKind === null && this.#ext.length) {
candidates = this.#ext.map((ext) => targetPath + ext);
}
for (const filePath of candidates) {
const kind = await this.#fsUtils.kind(filePath);
if (kind === 'file' || kind === 'link') {
return { kind, filePath };
}
return { kind: targetKind, filePath: targetPath };
} else {
const candidates = this.#ext.map((ext) => targetPath + ext);
for (const file of candidates) {
const kind = await this.#fsUtils.kind(file);
if (kind === 'file') return { kind, filePath: file };
}
return { kind: null, filePath: targetPath };
}
return { kind: targetKind, filePath: targetPath };
}
/**
* @param {string} resourcePath
* @param {string | null} localPath
* @returns {boolean}
*/
allowedPath(resourcePath) {
if (!this.withinRoot(resourcePath)) {
return false;
allowedPath(localPath) {
if (typeof localPath === 'string') {
return this.#excludeMatcher.test(localPath) === false;
}
const subPath = this.#fsUtils.relative(this.#root, resourcePath);
return this.#excludeMatcher.test(subPath) === false;
return false;
}
/**
* @param {string} filePath
* @returns {string | null}
*/
localPath(filePath) {
if (this.withinRoot(filePath)) {
return filePath.slice(this.#root.length + 1);
}
return null;
}
/**
* @param {string} dirPath

@@ -146,18 +163,29 @@ * @returns {Promise<DirIndexItem[]>}

const entries = (await this.#fsUtils.index(dirPath)).filter((entry) =>
this.allowedPath(entry.filePath),
);
entries.sort((a, b) => a.filePath.localeCompare(b.filePath));
/** @type {DirIndexItem[]} */
const items = [];
for (const { kind, filePath } of await this.#fsUtils.index(dirPath)) {
const localPath = this.localPath(filePath);
if (kind != null && this.allowedPath(localPath)) {
items.push({ filePath, localPath, kind });
}
}
items.sort((a, b) => a.filePath.localeCompare(b.filePath));
return Promise.all(
entries.map(async (entry) => {
items.map(async (item) => {
// resolve symlinks
if (entry.kind === 'link') {
const filePath = await this.#fsUtils.realpath(entry.filePath);
const kind = filePath && (await this.#fsUtils.kind(filePath));
if (filePath && kind) {
return { ...entry, target: { filePath, kind } };
if (item.kind === 'link') {
const filePath = await this.#fsUtils.realpath(item.filePath);
const kind = filePath ? await this.#fsUtils.kind(filePath) : null;
if (filePath != null && kind != null) {
item.target = {
kind,
filePath,
localPath: this.localPath(filePath),
};
}
}
return entry;
return item;
}),

@@ -206,10 +234,9 @@ );

/**
* @param {string} resourcePath
* @param {string} filePath
* @returns {boolean}
*/
withinRoot(resourcePath) {
return (
!resourcePath.includes('..') &&
(resourcePath === this.#root || resourcePath.startsWith(this.#root + this.#fsUtils.dirSep))
);
withinRoot(filePath) {
if (filePath.includes('..')) return false;
const prefix = this.#root + this.#fsUtils.dirSep;
return filePath === this.#root || filePath.startsWith(prefix);
}

@@ -216,0 +243,0 @@ }

import { open } from 'node:fs/promises';
import { createServer } from 'node:http';
import { join } from 'node:path';
import { getContentType, typeForFilePath } from './content-type.js';
import { fsUtils } from './node-fs.js';
import { dirListPage, errorPage } from './pages.js';
import { FileResolver, PathMatcher } from './resolver.js';
import { contentType } from './utils.js';
/**
@typedef {import('./types.js').DirIndexItem} DirIndexItem
@typedef {import('./types.js').FSEntryKind} FSEntryKind
@typedef {import('./types.js').ReqResInfo} ReqResInfo
@typedef {import('./types.js').ResolvedFile} ResolvedFile
@typedef {import('./types.js').ResolveResult} ResolveResult

@@ -20,3 +21,3 @@ @typedef {import('./types.js').ServerOptions} ServerOptions

* @param {{ logNetwork?: (info: ReqResInfo) => void }} callbacks
* @returns {ReturnType<createServer>}
* @returns {import('node:http').Server}
*/

@@ -27,5 +28,6 @@ export function staticServer(options, { logNetwork }) {

return createServer(async (req, res) => {
/** @type {Pick<ReqResInfo, 'root' | 'method' | 'startedAt' | 'error'>} */
const info = {
root: options.root,
/**
* @type {Pick<ReqResInfo, 'method' | 'startedAt' | 'error'>}
*/
const logInfo = {
method: req.method ?? '',

@@ -35,33 +37,51 @@ startedAt: Date.now(),

const urlPath =
typeof req.url === 'string' ? new URL(req.url, 'http://localhost/').pathname : '/';
const result = await resolver.find(urlPath);
const { file, ...result } = await resolver.find(req.url ?? '');
if (logNetwork) {
res.on('close', () =>
res.on('close', () => {
logNetwork({
...result,
...info,
status: result.status,
urlPath: result.urlPath,
localPath: file?.localPath ?? null,
...logInfo,
endedAt: Date.now(),
}),
);
});
});
}
// found a file to serve
if (result.kind === 'file' && result.status === 200 && result.filePath) {
if (
result.status === 200 &&
file?.kind === 'file' &&
file.filePath != null &&
file.localPath != null
) {
let fileHandle;
try {
// check that we can actually open the file
// (especially on windows where it might be busy)
const fileHandle = await open(result.filePath);
const stream = fileHandle.createReadStream();
const headers = fileHeaders(result.filePath, options);
fileHandle = await open(file.filePath);
const headers = fileHeaders({
localPath: file.localPath,
contentType: await getContentType({
filePath: file.localPath,
fileHandle,
}),
cors: options.cors,
headers: options.headers,
});
res.writeHead(result.status, headers);
const stream = fileHandle.createReadStream({
autoClose: true,
start: 0,
});
stream.pipe(res);
} catch (/** @type {any} */ err) {
result.status = 500;
if (err?.syscall === 'open' && err.code === 'EBUSY') {
result.status = 403;
if (err?.syscall === 'open') {
if (err.code === 'EBUSY') result.status = 403;
fileHandle?.close();
}
if (err?.message) {
info.error = err;
logInfo.error = err;
}

@@ -73,19 +93,10 @@ await sendErrorPage(res, result, options);

// found a directory that we can show a listing for
else if (result.kind === 'dir' && result.status === 200 && result.filePath) {
const headers = fileHeaders(
join(result.filePath, 'index.html'),
// ignore user options for directory listings
{ cors: false, headers: [] },
);
const body = await dirListPage(
{
urlPath: result.urlPath,
dirPath: result.filePath,
items: await resolver.index(result.filePath),
},
options,
);
res.writeHead(result.status, headers);
res.write(body);
res.end();
else if (
result.status === 200 &&
file?.kind === 'dir' &&
file.filePath != null &&
file.localPath != null
) {
const items = await resolver.index(file.filePath);
await sendDirListPage(res, { ...result, file, items }, options);
}

@@ -102,15 +113,31 @@

* @param {import('node:http').ServerResponse} res
* @param {ResolveResult} result
* @param {{ status: number, urlPath: string; file: ResolvedFile; items: DirIndexItem[] }} data
* @param {ServerOptions} options
*/
async function sendDirListPage(res, { status, urlPath, file, items }, options) {
const headers = fileHeaders({
localPath: 'index.html',
// ignore user options for directory listings
cors: false,
headers: [],
});
const body = await dirListPage({ urlPath, file, items }, options);
res.writeHead(status, headers);
res.write(body);
res.end();
}
/**
* @param {import('node:http').ServerResponse} res
* @param {Pick<ResolveResult, 'status' | 'urlPath'>} result
* @param {ServerOptions} options
*/
async function sendErrorPage(res, result, options) {
const headers = fileHeaders(
join(options.root, 'error.html'),
const headers = fileHeaders({
localPath: 'error.html',
cors: options.cors,
// ignore custom headers for error pages
{ cors: options.cors, headers: [] },
);
const body = await errorPage({
status: result.status,
urlPath: result.urlPath,
headers: [],
});
const body = await errorPage(result);
res.writeHead(result.status, headers);

@@ -122,13 +149,12 @@ res.write(body);

/**
* @param {string} filePath
* @param {Pick<ServerOptions, 'headers' | 'cors'>} options
* @param {{ localPath: string; contentType?: string; cors: boolean; headers: ServerOptions['headers'] }} data
* @returns {Record<string, string>}
*/
export function fileHeaders(filePath, { cors, headers }) {
export function fileHeaders({ localPath, contentType, cors, headers }) {
/** @type {Record<string, string>} */
const result = {};
const setHeader = (key = '', value = '') => (result[key.toLowerCase()] = value);
setHeader('content-type', contentType(filePath));
const obj = {};
const add = (key = '', value = '') => (obj[key.trim().toLowerCase()] = value);
add('content-type', contentType || typeForFilePath(localPath).toString());
if (cors) {
setHeader('access-control-allow-origin', '*');
add('access-control-allow-origin', '*');
}

@@ -138,9 +164,9 @@ for (const rule of headers) {

const matcher = new PathMatcher(rule.include);
if (!matcher.test(filePath)) continue;
if (!matcher.test(localPath)) continue;
}
for (const [key, value] of Object.entries(rule.headers)) {
setHeader(key, value);
add(key, value);
}
}
return result;
return obj;
}

@@ -14,4 +14,4 @@ /**

@typedef {'dir' | 'file' | 'link' | null} FSEntryKind
@typedef {{ filePath: string; kind: FSEntryKind; symlink?: boolean }} FSEntryBase
@typedef {'dir' | 'file' | 'link'} FSEntryKind
@typedef {{ filePath: string; kind: FSEntryKind | null }} FSEntryBase

@@ -24,4 +24,4 @@ @typedef {{

info(filePath: string): Promise<FSEntryBase & {readable: boolean}>;
kind(filePath: string): Promise<FSEntryKind>;
readable(filePath: string, kind?: FSEntryKind): Promise<boolean>;
kind(filePath: string): Promise<FSEntryKind | null>;
readable(filePath: string, kind?: FSEntryKind | null): Promise<boolean>;
realpath(filePath: string): Promise<string | null>;

@@ -53,15 +53,22 @@ }} FSUtils

@typedef {{
kind: FSEntryKind;
filePath: string;
localPath: string | null;
}} ResolvedFile
@typedef {{
status: number;
urlPath: string;
filePath: string | null;
kind: FSEntryKind;
file: ResolvedFile | null;
}} ResolveResult
@typedef {FSEntryBase & { isParent?: boolean; target?: FSEntryBase }} DirIndexItem
@typedef {ResolvedFile & {isParent?: boolean; target?: ResolvedFile}} DirIndexItem
@typedef {ResolveResult & {
method: string;
root: string;
@typedef {{
startedAt: number;
endedAt?: number;
status: number;
method: string;
urlPath: string;
localPath: string | null;
error?: Error | string;

@@ -68,0 +75,0 @@ }} ReqResInfo

@@ -1,6 +0,4 @@

import { basename, extname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ColorUtils } from './color.js';
import { DEFAULT_CHARSET, BIN_TYPES, TEXT_TYPES } from './constants.js';

@@ -18,29 +16,2 @@ export const color = new ColorUtils();

/**
* @type {(filename: string, charset?: string | null) => string}
*/
export function contentType(filename, charset = DEFAULT_CHARSET) {
const charsetSuffix = charset ? `; charset=${charset}` : '';
const name = basename(filename).toLowerCase();
const ext = extname(filename).replace('.', '').toLowerCase();
if (ext) {
if (Object.hasOwn(TEXT_TYPES.extensionMap, ext)) {
return TEXT_TYPES.extensionMap[ext] + charsetSuffix;
} else if (Object.hasOwn(BIN_TYPES.extensionMap, ext)) {
return BIN_TYPES.extensionMap[ext];
} else if (TEXT_TYPES.extension.includes(ext)) {
return TEXT_TYPES.default + charsetSuffix;
} else if (BIN_TYPES.extension.includes(ext)) {
return BIN_TYPES.default;
}
} else {
if (TEXT_TYPES.file.includes(name) || TEXT_TYPES.suffix.find((x) => name.endsWith(x))) {
return TEXT_TYPES.default + charsetSuffix;
}
}
return BIN_TYPES.default;
}
/**
* @type {(input: string, context?: 'text' | 'attr') => string}

@@ -47,0 +18,0 @@ */

{
"name": "servitsy",
"version": "0.1.2",
"version": "0.1.3",
"keywords": [

@@ -38,3 +38,3 @@ "cli",

"test": "node --test --test-reporter=spec",
"typecheck": "tsc -p jsconfig.json"
"typecheck": "tsc -p jsconfig.json && tsc -p test/jsconfig.json"
},

@@ -41,0 +41,0 @@ "devDependencies": {

@@ -5,7 +5,7 @@ # servitsy

- Small: zero dependencies, 24 kilobytes gzipped.
- What: for your local testing needs.
- How: with decent defaults, and no cool features.
- **Small:** no dependencies, 25 kilobytes gzipped.
- **Static:** serves static files and directory listings.
- **Local:** designed for single-user local workflows, not for production.
## Quick start
## Usage

@@ -21,41 +21,18 @@ ```sh

- serve `index.html` files for folders, and `.html` files when the extension was omitted in the URL;
- show directory contents (for folders without an index file).
- list directory contents (for folders without an index file).
See `npx servitsy --help` — or [the Options section](#options) — if you want to configure this behavior.
You can configure this behavior [with options](#options). Here are a couple examples:
## When you shouldn’t use this package
```sh
# serve current folder on port 3000, with CORS headers
npx servitsy -p 3000 --cors
### ⛔️ In production
# serve 'dist' folder and disable directory listings
npx servitsy dist --dir-list false
```
There are safer and faster tools to serve a folder of static HTML to the public. Apache, Nginx, fastify-static, etc.
## Options
### 🤔 For web app development…
See `npx servitsy --help` for an overview of available options.
… if you want nice dev features like live-reload, transpilation, bundling, etc. — use something like [Vite](https://vitejs.dev/) instead.
### 🌈 If you love another
There are good established alternatives to this package. Here is a brief and subjective comparison of a few packages I like:
| Package | Size on disk† | Dependencies | Highlights |
| ----------------------- | ------------- | ------------ | -------------------------- |
| servitsy (v0.1.2) | 112 kB | 0 | Tiny |
| [servor] (v4.0.2) | 144 kB | 0 | Tiny, some cool features |
| [sirv-cli] (v2.0.2) | 392 kB | 12 | Small, good options |
| [serve] (v14.2.3) | 7.6 MB | 89 | Good defaults, easy to use |
| [http-server] (v14.1.1) | 8.9 MB | 45 | Good defaults, featureful |
The philosophy of `servitsy` is to have few opinions and bells and whistles (like `sirv-cli`), and to try to offer that in a zero-dependency package (like `servor`).
If size and dependency count is not a concern and you want something stable and battle-tested, I recommend `serve` and `http-server`.
† Size on disk is the uncompressed size of the package and its dependencies (as reported by `/usr/bin/du` on macOS with an APFS filesystem; exact size may depend on the OS and/or filesystem).
[http-server]: https://www.npmjs.com/package/serve
[serve]: https://www.npmjs.com/package/serve
[servor]: https://www.npmjs.com/package/servor
[sirv-cli]: https://www.npmjs.com/package/sirv-cli
## Options
### `cors`

@@ -184,1 +161,28 @@

Defaults to `8080+`.
## Alternatives
> __🚨 Reminder: `servitsy` is not designed for production.__ There are safer and faster tools to serve a folder of static HTML to the public. See Apache, Nginx, [@fastify/static], etc.
For local testing, here are a few established alternatives you may prefer, with their respective size:
| Package | Version | Dependencies | Size on disk† |
| ------------- | ------- | ------------ | ------------- |
| [servitsy] | 0.1.3 | 0 | 124 kB |
| [servor] | 4.0.2 | 0 | 144 kB |
| [sirv-cli] | 2.0.2 | 12 | 392 kB |
| [serve] | 14.2.3 | 89 | 7.6 MB |
| [http-server] | 14.1.1 | 45 | 8.9 MB |
If size and dependency count is not a concern and you want something stable and battle-tested, I recommend [serve] and [http-server].
Otherwise, [servor], [sirv-cli] or [servitsy] might work for you.
_† Size on disk is the uncompressed size of the package and its dependencies (as reported by `du` on macOS; exact size may depend on the OS and/or filesystem)._
[@fastify/static]: https://www.npmjs.com/package/@fastify/static
[http-server]: https://www.npmjs.com/package/http-server
[serve]: https://www.npmjs.com/package/serve
[servitsy]: https://www.npmjs.com/package/servitsy
[servor]: https://www.npmjs.com/package/servor
[sirv-cli]: https://www.npmjs.com/package/sirv-cli

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc