Skip to content

Commit

Permalink
fix: Deno 2.0 compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
fvsch committed Oct 13, 2024
1 parent 93fb30a commit 26f96fb
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 275 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ npx servitsy [directory] [options]
```

> [!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`);
- try the next port numbers if `8080` is not available;
Expand All @@ -29,10 +37,10 @@ Calling servitsy without any option will:
You can configure servitsy's behavior [with options](https://github.com/fvsch/servitsy/blob/main/doc/options.md). For example:

```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
```

Expand All @@ -50,7 +58,7 @@ This package is licensed under [the MIT license](./LICENSE).
## Alternatives

> [!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.
For local testing, here are a few established alternatives you may prefer, with their respective size:

Expand All @@ -68,7 +76,9 @@ Otherwise, [servor], [sirv-cli] or [servitsy] might work for you.

_† Installed size 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
[Bun]: https://bun.sh/
[Deno]: https://deno.com/
[Node.js]: https://nodejs.org/
[http-server]: https://www.npmjs.com/package/http-server
[serve]: https://www.npmjs.com/package/serve
[servitsy]: https://www.npmjs.com/package/servitsy
Expand Down
9 changes: 9 additions & 0 deletions globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare var Bun: Bun | undefined;
declare var Deno: Deno | undefined;

interface Bun {}

interface Deno {
noColor: boolean;
permissions: any;
}
2 changes: 1 addition & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"include": ["./bin", "./lib"],
"include": ["./bin", "./lib", "./globals.d.ts"],
"compilerOptions": {
"module": "NodeNext",
"target": "ES2022",
Expand Down
141 changes: 79 additions & 62 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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';

import { CLIArgs } from './args.js';
Expand All @@ -9,7 +9,7 @@ import { readPkgJson } from './fs-utils.js';
import { logger, requestLogLine } from './logger.js';
import { serverOptions } from './options.js';
import { staticServer } from './server.js';
import { color, clamp, isPrivateIPv4 } from './utils.js';
import { clamp, color, getRuntime, isPrivateIPv4 } from './utils.js';

/**
@typedef {import('./types.js').OptionName} OptionName
Expand All @@ -18,6 +18,8 @@ import { color, clamp, isPrivateIPv4 } from './utils.js';
@typedef {import('./types.js').ServerOptions} ServerOptions
**/

const runtime = getRuntime();

/**
* Run servitsy with configuration from command line arguments.
*/
Expand Down Expand Up @@ -60,6 +62,9 @@ export class CLIServer {
/** @type {IterableIterator<number>} */
#portIterator;

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

/** @type {import('node:http').Server} */
#server;

Expand All @@ -69,34 +74,44 @@ export class CLIServer {
constructor(options) {
this.#options = options;
this.#portIterator = new Set(options.ports).values();
this.#localNetworkInfo = Object.values(networkInterfaces())
.flat()
.find((c) => c?.family === 'IPv4' && isPrivateIPv4(c?.address));

this.#server = staticServer(options, {
logNetwork: (info) => {
logger.write('request', requestLogLine(info));
},
});
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 });
});
}

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;
const address = this.#server.address();
if (address !== null && typeof address === 'object') {
const { local, network } = displayHosts({
configuredHost: host,
currentHost: address.address,
networkAddress: this.#localNetworkInfo?.address,
});
const data = [
['serving', displayRoot(root)],
Expand All @@ -115,46 +130,10 @@ export class CLIServer {
}
}

/**
* @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) => {
if (
// control+c
Expand All @@ -168,9 +147,10 @@ export class CLIServer {
logger.write('info', 'Hit Control+C or Escape to stop the server.');
}
});
stdin.setRawMode(true);
}

#handleSignals() {
handleSignals() {
process.on('SIGBREAK', this.shutdown);
process.on('SIGINT', this.shutdown);
process.on('SIGTERM', this.shutdown);
Expand All @@ -189,6 +169,41 @@ export class CLIServer {

exit();
};

/**
* @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);
}
}

export function helpPage() {
Expand Down Expand Up @@ -252,37 +267,39 @@ export function helpPage() {
}

/**
* @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 networkAddress = () => {
const configs = Object.values(networkInterfaces()).flat();
return configs.find((c) => c?.family === 'IPv4' && isPrivateIPv4(c?.address))?.address;
};
const isWildcard = (value = '') => HOSTS_WILDCARD.v4 === value || HOSTS_WILDCARD.v6 === value;

if (!isWildcard(configuredHost) && !isLocalhost(configuredHost)) {
return { local: configuredHost };
}

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

/**
* Replace the home dir with '~' in path
* @param {string} root
* @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;
}
Loading

0 comments on commit 26f96fb

Please sign in to comment.