New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

servitsy

Package Overview
Dependencies
Maintainers
0
Versions
17
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.3.2 to 0.3.3

141

lib/cli.js
import { homedir, networkInterfaces } from 'node:os';
import { sep as dirSep } from 'node:path';
import { default as process, argv, env, exit, stdin } from 'node:process';
import process, { argv, exit, platform, stdin } from 'node:process';
import { emitKeypressEvents } from 'node:readline';

@@ -12,3 +12,3 @@

import { staticServer } from './server.js';
import { color, clamp, isPrivateIPv4 } from './utils.js';
import { clamp, color, getRuntime, isPrivateIPv4 } from './utils.js';

@@ -22,2 +22,4 @@ /**

const runtime = getRuntime();
/**

@@ -65,2 +67,5 @@ * Run servitsy with configuration from command line arguments.

/** @type {import('node:os').NetworkInterfaceInfo | undefined} */
#localNetworkInfo;
/** @type {import('node:http').Server} */

@@ -75,2 +80,5 @@ #server;

this.#portIterator = new Set(options.ports).values();
this.#localNetworkInfo = Object.values(networkInterfaces())
.flat()
.find((c) => c?.family === 'IPv4' && isPrivateIPv4(c?.address));

@@ -82,5 +90,5 @@ this.#server = staticServer(options, {

});
this.#server.on('error', (error) => this.#handleServerError(error));
this.#server.on('error', (error) => this.#onServerError(error));
this.#server.on('listening', () => {
logger.write('header', this.#headerInfo(), { top: 1, bottom: 1 });
logger.write('header', this.headerInfo(), { top: 1, bottom: 1 });
});

@@ -90,11 +98,17 @@ }

start() {
this.#handleSignals();
this.#handleKeyboardInput();
this.#server.listen({
host: this.#options.host,
port: this.#portIterator.next().value,
});
this.handleSignals();
this.#server.listen(
{
host: this.#options.host,
port: this.#portIterator.next().value,
},
// Wait until the server started listening — and hopefully all Deno
// permission requests are done — before we can take over stdin inputs.
() => {
this.handleKeyboardInput();
},
);
}
#headerInfo() {
headerInfo() {
const { host, root } = this.#options;

@@ -106,2 +120,3 @@ const address = this.#server.address();

currentHost: address.address,
networkAddress: this.#localNetworkInfo?.address,
});

@@ -125,42 +140,6 @@ const data = [

/**
* @param {NodeJS.ErrnoException & {hostname?: string}} error
*/
#handleServerError(error) {
// Try restarting with the next port
if (error.syscall === 'listen' && error.code === 'EADDRINUSE') {
const { value: nextPort } = this.#portIterator.next();
const { ports } = this.#options;
this.#server.closeAllConnections();
this.#server.close(() => {
if (nextPort) {
this.#port = nextPort;
this.#server.listen({
host: this.#options.host,
port: this.#port,
});
} else {
logger.writeErrors({
error: `${ports.length > 1 ? 'ports' : 'port'} already in use: ${ports.join(', ')}`,
});
exit(1);
}
});
return;
}
// Handle other errors
if (error.syscall === 'getaddrinfo' && error.code === 'ENOTFOUND') {
logger.writeErrors({ error: `host not found: '${error.hostname}'` });
} else {
logger.writeErrorObj(error);
}
exit(1);
}
#handleKeyboardInput() {
handleKeyboardInput() {
if (!stdin.isTTY) return;
let helpShown = false;
emitKeypressEvents(stdin);
stdin.setRawMode(true);
stdin.on('keypress', (_str, key) => {

@@ -179,5 +158,6 @@ if (

});
stdin.setRawMode(true);
}
#handleSignals() {
handleSignals() {
process.on('SIGBREAK', this.shutdown);

@@ -201,2 +181,37 @@ process.on('SIGINT', this.shutdown);

};
/**
* @param {NodeJS.ErrnoException & {hostname?: string}} error
*/
#onServerError(error) {
// Try restarting with the next port
if (error.syscall === 'listen' && error.code === 'EADDRINUSE') {
const { value: nextPort } = this.#portIterator.next();
const { ports } = this.#options;
this.#server.closeAllConnections();
this.#server.close(() => {
if (nextPort) {
this.#port = nextPort;
this.#server.listen({
host: this.#options.host,
port: this.#port,
});
} else {
logger.writeErrors({
error: `${ports.length > 1 ? 'ports' : 'port'} already in use: ${ports.join(', ')}`,
});
exit(1);
}
});
return;
}
// Handle other errors
if (error.syscall === 'getaddrinfo' && error.code === 'ENOTFOUND') {
logger.writeErrors({ error: `host not found: '${error.hostname}'` });
} else {
logger.writeErrorObj(error);
}
exit(1);
}
}

@@ -265,15 +280,9 @@

/**
* @param {{ configuredHost: string; currentHost: string }} address
* @param {{ configuredHost: string; currentHost: string; networkAddress?: string }} address
* @returns {{ local: string; network?: string }}
*/
function displayHosts({ configuredHost, currentHost }) {
function displayHosts({ configuredHost, currentHost, networkAddress }) {
const isLocalhost = (value = '') => HOSTS_LOCAL.includes(value);
const isWildcard = (value = '') => Object.values(HOSTS_WILDCARD).includes(value);
const isWebcontainers = () => env['SHELL']?.endsWith('/jsh');
const isWildcard = (value = '') => HOSTS_WILDCARD.v4 === value || HOSTS_WILDCARD.v6 === value;
const networkAddress = () => {
const configs = Object.values(networkInterfaces()).flat();
return configs.find((c) => c?.family === 'IPv4' && isPrivateIPv4(c?.address))?.address;
};
if (!isWildcard(configuredHost) && !isLocalhost(configuredHost)) {

@@ -285,3 +294,3 @@ return { local: configuredHost };

local: isWildcard(currentHost) || isLocalhost(currentHost) ? 'localhost' : currentHost,
network: isWildcard(configuredHost) && !isWebcontainers() ? networkAddress() : undefined,
network: isWildcard(configuredHost) && runtime !== 'webcontainer' ? networkAddress : undefined,
};

@@ -291,2 +300,3 @@ }

/**
* Replace the home dir with '~' in path
* @param {string} root

@@ -296,7 +306,14 @@ * @returns {string}

function displayRoot(root) {
const prefix = homedir() + dirSep;
if (root.startsWith(prefix)) {
return root.replace(prefix, '~' + dirSep);
if (
// skip: not a common windows convention
platform !== 'win32' &&
// skip: requires --allow-sys=homedir in Deno
runtime !== 'deno'
) {
const prefix = homedir() + dirSep;
if (root.startsWith(prefix)) {
return root.replace(prefix, '~' + dirSep);
}
}
return root;
}

@@ -1,4 +0,3 @@

import { accessSync, statSync, constants as fsConstants } from 'node:fs';
import { accessSync, constants as fsConstants, statSync } from 'node:fs';
import { resolve } from 'node:path';
import { cwd } from 'node:process';

@@ -26,16 +25,2 @@ import { CLIArgs } from './args.js';

/**
* @param {CLIArgs} args
* @param {ValidationContext} context
* @returns
*/
export function validateArgPresence(args, { warn }) {
const knownArgs = Object.values(CLI_OPTIONS).flatMap((spec) => spec.args);
for (const name of args.keys()) {
if (!knownArgs.includes(name)) {
warn(`unknown option '${name}'`);
}
}
}
/**
* @param {Partial<ListenOptions & ServerOptions> & {args?: CLIArgs}} options

@@ -88,2 +73,16 @@ * @returns {{errors: ErrorMessage[]; options: ListenOptions & ServerOptions}}

/**
* @param {CLIArgs} args
* @param {ValidationContext} context
* @returns
*/
export function validateArgPresence(args, { warn }) {
const knownArgs = Object.values(CLI_OPTIONS).flatMap((spec) => spec.args);
for (const name of args.keys()) {
if (!knownArgs.includes(name)) {
warn(`unknown option '${name}'`);
}
}
}
/**
* @param {string | boolean | undefined} input

@@ -372,3 +371,3 @@ * @param {ValidationContext & { optName: string; defaultValue: boolean; emptyValue: boolean }} context

const root = resolve(cwd(), typeof input === 'string' ? input : '');
const root = resolve(input ?? '');

@@ -375,0 +374,0 @@ try {

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

import { ColorUtils } from './color.js';
import { release } from 'node:os';
import { env, platform, versions } from 'node:process';
import { inspect } from 'node:util';
export const color = new ColorUtils();
/**

@@ -13,2 +13,36 @@ * @type {(value: number, min: number, max: number) => number}

export class ColorUtils {
/** @param {boolean} [colorEnabled] */
constructor(colorEnabled) {
this.enabled = typeof colorEnabled === 'boolean' ? colorEnabled : true;
}
/** @type {(text: string, format?: string) => string} */
style = (text, format = '') => {
if (!this.enabled) return text;
return styleText(format.trim().split(/\s+/g), text);
};
/** @type {(text: string, format?: string, chars?: [string, string]) => string} */
brackets = (text, format = 'dim,,dim', chars = ['[', ']']) => {
return this.sequence([chars[0], text, chars[1]], format);
};
/** @type {(parts: string[], format?: string) => string} */
sequence = (parts, format = '') => {
if (!format || !this.enabled) {
return parts.join('');
}
const formats = format.split(',');
return parts
.map((part, index) => (formats[index] ? this.style(part, formats[index]) : part))
.join('');
};
/** @type {(input: string) => string} */
strip = stripStyle;
}
export const color = new ColorUtils(supportsColor());
/**

@@ -54,2 +88,19 @@ * @type {(input: string, context?: 'text' | 'attr') => string}

/**
* @type {(key: string) => string}
*/
export function getEnv(key) {
return env[key] ?? '';
}
/**
* @returns {'bun' | 'deno' | 'node' | 'webcontainer'}
*/
export function getRuntime() {
if (versions.bun && globalThis.Bun) return 'bun';
if (versions.deno && globalThis.Deno) return 'deno';
if (versions.webcontainer && getEnv('SHELL').endsWith('/jsh')) return 'webcontainer';
return 'node';
}
/**
* @param {string} name

@@ -101,2 +152,62 @@ * @returns {string}

/**
* @param {string} input
* @returns {string}
*/
export function stripStyle(input) {
if (typeof input === 'string' && input.includes('\x1b[')) {
return input.replace(/\x1b\[\d+m/g, '');
}
return input;
}
/**
* Basic implementation of 'node:util' styleText to support Node 18 + Deno.
* @param {string | string[]} format
* @param {string} text
* @returns {string}
*/
export function styleText(format, text) {
let before = '';
let after = '';
for (const style of Array.isArray(format) ? format : [format]) {
const codes = inspect.colors[style.trim()];
if (!codes) continue;
before = `${before}\x1b[${codes[0]}m`;
after = `\x1b[${codes[1]}m${after}`;
}
return `${before}${text}${after}`;
}
/**
* @returns {boolean}
*/
function supportsColor() {
if (typeof globalThis.Deno?.noColor === 'boolean') {
return !globalThis.Deno.noColor;
}
if (getEnv('NO_COLOR')) {
const forceColor = getEnv('FORCE_COLOR');
return forceColor === 'true' || /^\d$/.test(forceColor);
}
// Logic borrowed from supports-color.
// Windows 10 build 10586 is the first release that supports 256 colors.
if (platform === 'win32') {
const [major, _, build] = release().split('.');
return Number(major) >= 10 && Number(build) >= 10_586;
}
// Should work in *nix terminals.
const term = getEnv('TERM');
const colorterm = getEnv('COLORTERM');
return (
colorterm === 'truecolor' ||
term === 'xterm-256color' ||
term === 'xterm-16color' ||
term === 'xterm-color'
);
}
/**
* @type {(input: string, options?: { start?: boolean; end?: boolean }) => string}

@@ -103,0 +214,0 @@ */

{
"name": "servitsy",
"version": "0.3.2",
"version": "0.3.3",
"license": "MIT",

@@ -5,0 +5,0 @@ "description": "Small, local HTTP server for static files",

@@ -18,6 +18,14 @@ # servitsy

> [!NOTE]
> servitsy is a command-line tool, published as a npm package. It requires Node.js version 18 (or higher).
> servitsy is a command-line tool, published as a npm package. It requires [Node.js] version 18 or higher, or a compatible runtime like [Deno] or [Bun].
Calling servitsy without any option will:
```sh
# Running with Bun
bunx servitsy
# Running with Deno
deno run --allow-net --allow-read --allow-sys npm:servitsy
```
Calling servitsy without options will:
- serve the current directory at `http://localhost:8080` (listening on hostname `0.0.0.0`);

@@ -33,6 +41,6 @@ - try the next port numbers if `8080` is not available;

```sh
# serve current folder on port 3000, with CORS headers
# Serve current folder on port 3000, with CORS headers
npx servitsy -p 3000 --cors
# serve 'dist' folder and disable directory listings
# Serve 'dist' folder and disable directory listings
npx servitsy dist --dir-list false

@@ -55,3 +63,3 @@ ```

> [!WARNING]
> **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.
> **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.

@@ -62,3 +70,3 @@ For local testing, here are a few established alternatives you may prefer, with their respective size:

| ------------- | ------- | ------------ | --------------- |
| [servitsy] | 0.3.1 | 0 | 124 kB |
| [servitsy] | 0.3.3 | 0 | 128 kB |
| [servor] | 4.0.2 | 0 | 144 kB |

@@ -75,3 +83,5 @@ | [sirv-cli] | 3.0.0 | 12 | 396 kB |

[@fastify/static]: https://www.npmjs.com/package/@fastify/static
[Bun]: https://bun.sh/
[Deno]: https://deno.com/
[Node.js]: https://nodejs.org/
[http-server]: https://www.npmjs.com/package/http-server

@@ -78,0 +88,0 @@ [serve]: https://www.npmjs.com/package/serve

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