Comparing version 0.4.2 to 0.4.3
@@ -5,3 +5,2 @@ import { CLI_OPTIONS, PORTS_CONFIG } from './constants.js'; | ||
/** | ||
@typedef {import('./types.d.ts').ErrorList} ErrorList | ||
@typedef {import('./types.d.ts').HttpHeaderRule} HttpHeaderRule | ||
@@ -120,8 +119,8 @@ @typedef {import('./types.d.ts').OptionSpec} OptionSpec | ||
/** @type {(args: CLIArgs, context?: { error: ErrorList }) => Partial<ServerOptions>} */ | ||
export function parseArgs(args, context) { | ||
/** @type {(args: CLIArgs, context: { onError(msg: string): void }) => Partial<ServerOptions>} */ | ||
export function parseArgs(args, { onError }) { | ||
const invalid = (optName = '', input = '') => { | ||
const value = | ||
typeof input === 'string' ? `'${input.replaceAll(`'`, `\'`)}'` : JSON.stringify(input); | ||
context?.error(`invalid ${optName} value: ${value}`); | ||
onError(`invalid ${optName} value: ${value}`); | ||
}; | ||
@@ -191,3 +190,3 @@ | ||
for (const name of unknownArgs(args)) { | ||
context?.error(`unknown option '${name}'`); | ||
onError(`unknown option '${name}'`); | ||
} | ||
@@ -194,0 +193,0 @@ |
@@ -0,1 +1,3 @@ | ||
import { createServer } from 'node:http'; | ||
import { createRequire } from 'node:module'; | ||
import { homedir, networkInterfaces } from 'node:os'; | ||
@@ -8,6 +10,7 @@ import { sep as dirSep } from 'node:path'; | ||
import { CLI_OPTIONS, HOSTS_LOCAL, HOSTS_WILDCARD } from './constants.js'; | ||
import { checkDirAccess, pkgFilePath, readPkgJson } from './fs-utils.js'; | ||
import { checkDirAccess, readPkgJson } from './fs-utils.js'; | ||
import { RequestHandler } from './handler.js'; | ||
import { color, logger, requestLogLine } from './logger.js'; | ||
import { serverOptions } from './options.js'; | ||
import { staticServer } from './server.js'; | ||
import { FileResolver } from './resolver.js'; | ||
import { clamp, errorList, getRuntime, isPrivateIPv4 } from './utils.js'; | ||
@@ -27,3 +30,3 @@ | ||
if (args.has('--version')) { | ||
const pkg = await readPkgJson(); | ||
const pkg = readPkgJson(); | ||
logger.write('info', pkg.version); | ||
@@ -38,15 +41,9 @@ process.exitCode = 0; | ||
const error = errorList(); | ||
const userOptions = parseArgs(args, { error }); | ||
const options = serverOptions({ root: '', ...userOptions }, { error }); | ||
const onError = errorList(); | ||
const userOptions = parseArgs(args, { onError }); | ||
const options = serverOptions({ root: '', ...userOptions }, { onError }); | ||
await checkDirAccess(options.root, { onError }); | ||
await checkDirAccess(options.root, { error }); | ||
// check access to assets needed by server pages, | ||
// to trigger a permission prompt before the server starts | ||
if (getRuntime() === 'deno') { | ||
await checkDirAccess(pkgFilePath('assets')); | ||
} | ||
if (error.list.length) { | ||
logger.error(...error.list); | ||
if (onError.list.length) { | ||
logger.error(...onError.list); | ||
logger.error(`Try 'servitsy --help' for more information.`); | ||
@@ -77,2 +74,5 @@ process.exitCode = 1; | ||
/** @type {FileResolver} */ | ||
#resolver; | ||
/** @param {ServerOptions} options */ | ||
@@ -86,12 +86,18 @@ constructor(options) { | ||
this.#server = staticServer(options, { | ||
logNetwork: (info) => { | ||
logger.write('request', requestLogLine(info)); | ||
}, | ||
const resolver = new FileResolver(options); | ||
const server = createServer(async (req, res) => { | ||
const handler = new RequestHandler({ req, res, resolver, options }); | ||
res.on('close', () => { | ||
logger.write('request', requestLogLine(handler.data())); | ||
}); | ||
await handler.process(); | ||
}); | ||
this.#server.on('error', (error) => this.#onServerError(error)); | ||
this.#server.on('listening', () => { | ||
server.on('error', (error) => this.#onServerError(error)); | ||
server.on('listening', () => { | ||
const info = this.headerInfo(); | ||
if (info) logger.write('header', info, { top: 1, bottom: 1 }); | ||
}); | ||
this.#resolver = resolver; | ||
this.#server = server; | ||
} | ||
@@ -98,0 +104,0 @@ |
/** @type {string[]} */ | ||
export const HOSTS_LOCAL = ['localhost', '127.0.0.1', '::1']; | ||
/** @type {{ v4: string; v6: string }} */ | ||
export const HOSTS_WILDCARD = { v4: '0.0.0.0', v6: '::' }; | ||
export const HOSTS_WILDCARD = { | ||
v4: '0.0.0.0', | ||
v6: '::', | ||
}; | ||
/** @type {import('./types.d.ts').PortsConfig} */ | ||
export const PORTS_CONFIG = { | ||
@@ -32,3 +33,6 @@ initial: 8080, | ||
/** @type {import('./types.d.ts').OptionSpecs} */ | ||
/** | ||
@typedef {'cors' | 'dirFile' | 'dirList' | 'exclude' | 'ext' | 'gzip' | 'header' | 'help' | 'host' | 'port' | 'version'} OptionName | ||
@type {Record<OptionName, import('./types.d.ts').OptionSpec>} | ||
*/ | ||
export const CLI_OPTIONS = { | ||
@@ -35,0 +39,0 @@ cors: { |
@@ -5,3 +5,2 @@ import { basename, extname } from 'node:path'; | ||
@typedef {import('node:fs/promises').FileHandle} FileHandle | ||
@typedef {{ | ||
@@ -8,0 +7,0 @@ default: string; |
@@ -1,14 +0,14 @@ | ||
import { access, constants, lstat, readdir, readFile, realpath, stat } from 'node:fs/promises'; | ||
import { isAbsolute, join, relative, sep as dirSep } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
import { access, constants, lstat, readdir, realpath, stat } from 'node:fs/promises'; | ||
import { createRequire } from 'node:module'; | ||
import { isAbsolute, join, sep as dirSep } from 'node:path'; | ||
import { trimSlash } from './utils.js'; | ||
/** | ||
@typedef {import('./types.d.ts').FSEntryBase} FSEntryBase | ||
@typedef {import('./types.d.ts').FSEntryKind} FSEntryKind | ||
@typedef {import('./types.d.ts').ErrorList} ErrorList | ||
@typedef {import('./types.d.ts').FSKind} FSKind | ||
@typedef {import('./types.d.ts').FSLocation} FSLocation | ||
*/ | ||
/** @type {(dirPath: string, context?: { error: ErrorList }) => Promise<boolean>} */ | ||
export async function checkDirAccess(dirPath, context) { | ||
/** @type {(dirPath: string, context: { onError(msg: string): void }) => Promise<boolean>} */ | ||
export async function checkDirAccess(dirPath, { onError }) { | ||
let msg = ''; | ||
@@ -33,7 +33,7 @@ try { | ||
} | ||
if (msg) context?.error(msg); | ||
if (msg) onError(msg); | ||
return false; | ||
} | ||
/** @type {(dirPath: string) => Promise<FSEntryBase[]>} */ | ||
/** @type {(dirPath: string) => Promise<FSLocation[]>} */ | ||
export async function getIndex(dirPath) { | ||
@@ -51,3 +51,3 @@ try { | ||
/** @type {(filePath: string) => Promise<FSEntryKind | null>} */ | ||
/** @type {(filePath: string) => Promise<FSKind>} */ | ||
export async function getKind(filePath) { | ||
@@ -80,3 +80,3 @@ try { | ||
/** @type {(filePath: string, kind?: FSEntryKind | null) => Promise<boolean>} */ | ||
/** @type {(filePath: string, kind?: FSKind) => Promise<boolean>} */ | ||
export async function isReadable(filePath, kind) { | ||
@@ -103,29 +103,13 @@ if (kind === undefined) { | ||
/** @type {(moduleUrl: URL | string) => string} */ | ||
export function moduleDirname(moduleUrl) { | ||
return fileURLToPath(new URL('.', moduleUrl)); | ||
/** @type {() => Record<string, any>} */ | ||
export function readPkgJson() { | ||
return createRequire(import.meta.url)('../package.json'); | ||
} | ||
/** @type {(localPath: string) => string} */ | ||
export function pkgFilePath(localPath) { | ||
return join(moduleDirname(import.meta.url), '..', localPath); | ||
} | ||
/** @type {(localPath: string) => Promise<string>} */ | ||
export async function readPkgFile(localPath) { | ||
return readFile(pkgFilePath(localPath), { encoding: 'utf8' }); | ||
} | ||
/** @type {() => Promise<Record<string, any>>} */ | ||
export async function readPkgJson() { | ||
const raw = await readPkgFile('package.json'); | ||
return JSON.parse(raw); | ||
} | ||
/** @type {(stats: import('node:fs').Dirent | import('node:fs').StatsBase<any>) => FSEntryKind | null} */ | ||
/** @type {(stats: {isSymbolicLink?(): boolean; isDirectory?(): boolean; isFile?(): boolean}) => FSKind} */ | ||
export function statsKind(stats) { | ||
if (stats.isSymbolicLink()) return 'link'; | ||
if (stats.isDirectory()) return 'dir'; | ||
else if (stats.isFile()) return 'file'; | ||
if (stats.isSymbolicLink?.()) return 'link'; | ||
if (stats.isDirectory?.()) return 'dir'; | ||
else if (stats.isFile?.()) return 'file'; | ||
return null; | ||
} |
@@ -9,3 +9,3 @@ import { release } from 'node:os'; | ||
/** | ||
@typedef {import('./types.d.ts').ReqResMeta} ReqResMeta | ||
@typedef {import('./types.d.ts').ResMetaData} ResMetaData | ||
@typedef {{ | ||
@@ -119,9 +119,10 @@ group: 'header' | 'info' | 'request' | 'error'; | ||
/** @type {(data: ReqResMeta) => string} */ | ||
export function requestLogLine({ startedAt, endedAt, status, method, urlPath, localPath, error }) { | ||
/** @type {(data: import('./types.d.ts').ResMetaData) => string} */ | ||
export function requestLogLine({ status, method, urlPath, localPath, timing, error }) { | ||
const { start, close } = timing; | ||
const { style: _, brackets } = color; | ||
const isSuccess = status >= 200 && status < 300; | ||
const timestamp = new Date(endedAt ?? startedAt).toTimeString().split(' ')[0]?.padStart(8); | ||
const duration = endedAt && startedAt ? endedAt - startedAt : -1; | ||
const timestamp = start ? new Date(start).toTimeString().split(' ')[0]?.padStart(8) : undefined; | ||
const duration = start && close ? Math.ceil(close - start) : undefined; | ||
@@ -139,3 +140,3 @@ let displayPath = _(urlPath, 'cyan'); | ||
const line = [ | ||
_(timestamp, 'dim'), | ||
timestamp && _(timestamp, 'dim'), | ||
_(`${status}`, isSuccess ? 'green' : 'red'), | ||
@@ -145,5 +146,5 @@ _('—', 'dim'), | ||
displayPath, | ||
duration >= 0 ? _(`(${duration}ms)`, 'dim') : undefined, | ||
duration && _(`(${duration}ms)`, 'dim'), | ||
] | ||
.filter(Boolean) | ||
.filter((s) => typeof s === 'string' && s !== '') | ||
.join(' '); | ||
@@ -150,0 +151,0 @@ |
@@ -6,3 +6,2 @@ import { isAbsolute, resolve } from 'node:path'; | ||
/** | ||
@typedef {import('./types.d.ts').ErrorList} ErrorList | ||
@typedef {import('./types.d.ts').HttpHeaderRule} HttpHeaderRule | ||
@@ -13,5 +12,5 @@ @typedef {import('./types.d.ts').ServerOptions} ServerOptions | ||
export class OptionsValidator { | ||
/** @param {ErrorList} [errorList] */ | ||
constructor(errorList) { | ||
this.errorList = errorList; | ||
/** @param {(msg: string) => void} [onError] */ | ||
constructor(onError) { | ||
this.onError = onError; | ||
} | ||
@@ -39,3 +38,3 @@ | ||
#error(msg = '') { | ||
this.errorList?.(msg); | ||
this.onError?.(msg); | ||
} | ||
@@ -177,7 +176,7 @@ | ||
@param {{ root: string } & Partial<ServerOptions>} options | ||
@param {{ error: ErrorList }} [context] | ||
@param {{ onError(msg: string): void }} [context] | ||
@returns {ServerOptions} | ||
*/ | ||
export function serverOptions(options, context) { | ||
const validator = new OptionsValidator(context?.error); | ||
const validator = new OptionsValidator(context?.onError); | ||
@@ -184,0 +183,0 @@ /** @type {Partial<ServerOptions>} */ |
import { basename, dirname } from 'node:path'; | ||
import { readPkgFile } from './fs-utils.js'; | ||
import { FAVICON_LIST, FAVICON_ERROR, ICONS, STYLES } from './page-assets.js'; | ||
import { clamp, escapeHtml, trimSlash } from './utils.js'; | ||
/** | ||
@typedef {import('./types.d.ts').DirIndexItem} DirIndexItem | ||
@typedef {import('./types.d.ts').FSLocation} FSLocation | ||
@typedef {import('./types.d.ts').ServerOptions} ServerOptions | ||
*/ | ||
/** @type {Map<string, string>} */ | ||
const assetCache = new Map(); | ||
/** @type {(localPath: string) => Promise<string>} */ | ||
export async function readAsset(localPath) { | ||
if (!assetCache.has(localPath)) { | ||
assetCache.set(localPath, await readPkgFile(localPath)); | ||
} | ||
return assetCache.get(localPath) ?? ''; | ||
} | ||
/** | ||
@typedef {{ base?: string; body: string; icon?: 'list' | 'error'; title?: string }} HtmlTemplateData | ||
@type {(data: HtmlTemplateData) => Promise<string>} | ||
@param {{ base?: string; body: string; icon?: 'list' | 'error'; title?: string }} data | ||
*/ | ||
async function htmlTemplate({ base, body, icon, title }) { | ||
const [css, svgSprite, svgIcon] = await Promise.all([ | ||
readAsset('assets/styles.css'), | ||
readAsset('assets/icons.svg'), | ||
icon === 'list' || icon === 'error' ? readAsset(`assets/favicon-${icon}.svg`) : undefined, | ||
]); | ||
const svgIcon = { list: FAVICON_LIST, error: FAVICON_ERROR }[String(icon)]; | ||
return `<!doctype html> | ||
@@ -41,6 +24,6 @@ <html lang="en"> | ||
${svgIcon ? `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${btoa(svgIcon)}">` : ''} | ||
<style>${css.toString()}</style> | ||
<style>${STYLES}</style> | ||
</head> | ||
<body> | ||
${svgSprite.toString()} | ||
${ICONS} | ||
${body} | ||
@@ -80,3 +63,3 @@ </body> | ||
/** | ||
@param {{ urlPath: string; filePath: string; items: DirIndexItem[] }} data | ||
@param {{ urlPath: string; filePath: string; items: FSLocation[] }} data | ||
@param {Pick<ServerOptions, 'root' | 'ext'>} options | ||
@@ -91,11 +74,8 @@ @returns {Promise<string>} | ||
const displayPath = decodeURIPathSegments(trimmedUrl ? `${rootName}/${trimmedUrl}` : rootName); | ||
const parentPath = dirname(filePath); | ||
const showParent = trimmedUrl !== ''; | ||
const sorted = [...items.filter((x) => isDirLike(x)), ...items.filter((x) => !isDirLike(x))]; | ||
if (trimmedUrl !== '') { | ||
sorted.unshift({ | ||
filePath: dirname(filePath), | ||
kind: 'dir', | ||
isParent: true, | ||
}); | ||
if (showParent) { | ||
sorted.unshift({ filePath: parentPath, kind: 'dir' }); | ||
} | ||
@@ -112,6 +92,6 @@ | ||
<h1> | ||
Index of <span class="bc">${htmlBreadcrumbs(displayPath)}</span> | ||
Index of <span class="bc">${renderBreadcrumbs(displayPath)}</span> | ||
</h1> | ||
<ul class="files" style="--max-col-count:${maxCols}"> | ||
${sorted.map((item) => dirListItem(item, options)).join('\n')} | ||
${sorted.map((item) => renderListItem(item, { ext: options.ext, parentPath })).join('\n')} | ||
</ul> | ||
@@ -123,39 +103,40 @@ `.trim(), | ||
/** | ||
@type {(item: DirIndexItem, options: Pick<ServerOptions, 'ext'>) => string} | ||
@type {(item: FSLocation, options: { ext: ServerOptions['ext']; parentPath: string }) => string} | ||
*/ | ||
function dirListItem(item, { ext }) { | ||
const { filePath, isParent = false } = item; | ||
const isSymlink = item.kind === 'link'; | ||
const displayKind = isDirLike(item) ? 'dir' : 'file'; | ||
function renderListItem(item, { ext, parentPath }) { | ||
const isDir = isDirLike(item); | ||
const isParent = isDir && item.filePath === parentPath; | ||
const iconId = `icon-${displayKind}${isSymlink ? '-link' : ''}`; | ||
const className = ['files-item', `files-item--${displayKind}`]; | ||
if (isParent) className.push('files-item--parent'); | ||
if (isSymlink) className.push('files-item--symlink'); | ||
let icon = isDir ? 'icon-dir' : 'icon-file'; | ||
if (item.kind === 'link') icon += '-link'; | ||
let name = basename(item.filePath); | ||
let suffix = ''; | ||
let label = ''; | ||
let href = encodeURIComponent(name); | ||
const label = isParent ? 'Parent directory' : ''; | ||
const name = isParent ? '..' : basename(filePath); | ||
const suffix = displayKind === 'dir' ? '/' : ''; | ||
// clean url: remove extension if possible | ||
let href = encodeURIComponent(name); | ||
if (displayKind === 'file') { | ||
const match = ext.find((e) => filePath.endsWith(e)); | ||
if (isParent) { | ||
name = '..'; | ||
href = '..'; | ||
label = 'Parent directory'; | ||
} | ||
if (isDir) { | ||
suffix = '/'; | ||
} else { | ||
// clean url: remove extension if possible | ||
const match = ext.find((e) => item.filePath.endsWith(e)); | ||
if (match) href = href.slice(0, href.length - match.length); | ||
} | ||
const parts = [ | ||
`<li class="${attr(className.join(' '))}">\n`, | ||
return [ | ||
`<li class="files-item">\n`, | ||
`<a class="files-link" href="${attr(href)}"${label && ` aria-label="${attr(label)}" title="${attr(label)}"`}>`, | ||
`<svg class="files-icon" width="20" height="20"><use xlink:href="#${attr(iconId)}"></use></svg>`, | ||
`<svg class="files-icon" width="20" height="20"><use xlink:href="#${attr(icon)}"></use></svg>`, | ||
`<span class="files-name filepath">${html(nl2sp(name))}${suffix && `<span>${html(suffix)}</span>`}</span>`, | ||
`</a>`, | ||
`\n</li>`, | ||
]; | ||
return parts.join(''); | ||
].join(''); | ||
} | ||
/** @type {(path: string) => string} */ | ||
function htmlBreadcrumbs(path) { | ||
function renderBreadcrumbs(path) { | ||
const slash = '<span class="bc-sep">/</span>'; | ||
@@ -176,3 +157,3 @@ return path | ||
/** @type {(item: DirIndexItem) => boolean} */ | ||
/** @type {(item: FSLocation) => boolean} */ | ||
function isDirLike(item) { | ||
@@ -179,0 +160,0 @@ return item.kind === 'dir' || (item.kind === 'link' && item.target?.kind === 'dir'); |
@@ -8,5 +8,3 @@ import { isAbsolute, join } from 'node:path'; | ||
/** | ||
@typedef {import('./types.d.ts').DirIndexItem} DirIndexItem | ||
@typedef {import('./types.d.ts').FSEntryBase} FSEntryBase | ||
@typedef {import('./types.d.ts').ResolveResult} ResolveResult | ||
@typedef {import('./types.d.ts').FSLocation} FSLocation | ||
@typedef {import('./types.d.ts').ServerOptions} ServerOptions | ||
@@ -45,13 +43,8 @@ */ | ||
/** @type {(url: string) => Promise<ResolveResult>} */ | ||
/** @param {string} url */ | ||
async find(url) { | ||
const { urlPath, filePath: targetPath } = resolveUrlPath(this.#root, url); | ||
/** @type {ResolveResult} */ | ||
const result = { | ||
urlPath, | ||
status: 404, | ||
filePath: null, | ||
kind: null, | ||
}; | ||
/** @type {{status: number; urlPath: string; file?: FSLocation}} */ | ||
const result = { status: 404, urlPath }; | ||
@@ -74,8 +67,5 @@ if (targetPath == null) { | ||
if (file.kind === 'file' || file.kind === 'dir') { | ||
Object.assign(result, file); | ||
} | ||
// Check permissions (directories are always a 404 if dirList is false) | ||
if (file.kind === 'file' || (file.kind === 'dir' && this.#dirList)) { | ||
const allowed = this.allowedPath(file.filePath); | ||
result.file = file; | ||
const allowed = | ||
file.kind === 'dir' && !this.#dirList ? false : this.allowedPath(file.filePath); | ||
const readable = allowed && (await isReadable(file.filePath, file.kind)); | ||
@@ -88,7 +78,7 @@ result.status = allowed ? (readable ? 200 : 403) : 404; | ||
/** @type {(dirPath: string) => Promise<DirIndexItem[]>} */ | ||
/** @type {(dirPath: string) => Promise<FSLocation[]>} */ | ||
async index(dirPath) { | ||
if (!this.#dirList) return []; | ||
/** @type {DirIndexItem[]} */ | ||
/** @type {FSLocation[]} */ | ||
const items = (await getIndex(dirPath)).filter( | ||
@@ -116,3 +106,3 @@ (item) => item.kind != null && this.allowedPath(item.filePath), | ||
/** | ||
@type {(filePath: string[]) => Promise<FSEntryBase | void>} | ||
@type {(filePath: string[]) => Promise<FSLocation | void>} | ||
*/ | ||
@@ -132,3 +122,3 @@ async locateAltFiles(filePaths) { | ||
using the config for extensions and index file lookup. | ||
@type {(filePath: string) => Promise<FSEntryBase>} | ||
@type {(filePath: string) => Promise<FSLocation>} | ||
*/ | ||
@@ -135,0 +125,0 @@ async locateFile(filePath) { |
@@ -1,16 +0,7 @@ | ||
export type DirIndexItem = FSEntryBase & { | ||
isParent?: boolean; | ||
target?: FSEntryBase; | ||
}; | ||
export type FSKind = 'dir' | 'file' | 'link' | null; | ||
export interface ErrorList { | ||
(msg: string): void; | ||
list: string[]; | ||
} | ||
export type FSEntryKind = 'dir' | 'file' | 'link'; | ||
export interface FSEntryBase { | ||
export interface FSLocation { | ||
filePath: string; | ||
kind: FSEntryKind | null; | ||
kind: FSKind; | ||
target?: { filePath: string; kind: FSKind }; | ||
} | ||
@@ -30,40 +21,10 @@ | ||
export type OptionSpecs = Record< | ||
| 'cors' | ||
| 'dirFile' | ||
| 'dirList' | ||
| 'exclude' | ||
| 'ext' | ||
| 'gzip' | ||
| 'header' | ||
| 'help' | ||
| 'host' | ||
| 'port' | ||
| 'version', | ||
OptionSpec | ||
>; | ||
export interface PortsConfig { | ||
initial: number; | ||
count: number; | ||
maxCount: number; | ||
} | ||
export interface ResolveResult { | ||
status: number; | ||
urlPath: string; | ||
filePath: string | null; | ||
kind: FSEntryKind | null; | ||
} | ||
export type ReqResMeta = { | ||
export interface ResMetaData { | ||
method: string; | ||
status: number; | ||
url: string; | ||
urlPath: string; | ||
localPath: string | null; | ||
startedAt: number; | ||
endedAt?: number; | ||
timing: { start: number; send?: number; close?: number }; | ||
error?: Error | string; | ||
}; | ||
} | ||
@@ -70,0 +31,0 @@ export interface HttpOptions { |
import { env, versions } from 'node:process'; | ||
/** | ||
@typedef {import('./types.d.ts').ErrorList} ErrorList | ||
*/ | ||
/** @type {(value: number, min: number, max: number) => number} */ | ||
@@ -21,3 +17,3 @@ export function clamp(value, min, max) { | ||
/** @type {() => ErrorList} */ | ||
/** @type {() => { (msg: string): void; list: string[] }} */ | ||
export function errorList() { | ||
@@ -24,0 +20,0 @@ /** @type {string[]} */ |
{ | ||
"name": "servitsy", | ||
"version": "0.4.2", | ||
"version": "0.4.3", | ||
"license": "MIT", | ||
@@ -33,3 +33,2 @@ "description": "Small, local HTTP server for static files", | ||
"files": [ | ||
"./assets", | ||
"./bin", | ||
@@ -41,2 +40,4 @@ "./lib", | ||
"scripts": { | ||
"prepack": "npm run build && npm run typecheck && npm test", | ||
"build": "node scripts/bundle.js", | ||
"format": "prettier --write '**/*.{js,css}' '**/*config*.json'", | ||
@@ -43,0 +44,0 @@ "test": "node --test --test-reporter=spec", |
@@ -24,4 +24,4 @@ # servitsy | ||
# Running with Deno | ||
deno run --allow-net --allow-read --allow-sys npm:servitsy | ||
# Running with Deno (will prompt for read access) | ||
deno run --allow-net --allow-sys npm:servitsy | ||
``` | ||
@@ -68,3 +68,3 @@ | ||
| ------------- | ------- | ------------ | --------------- | | ||
| [servitsy] | 0.4.1 | 0 | 124 kB | | ||
| [servitsy] | 0.4.3 | 0 | 116 kB | | ||
| [servor] | 4.0.2 | 0 | 144 kB | | ||
@@ -77,3 +77,3 @@ | [sirv-cli] | 3.0.0 | 12 | 396 kB | | ||
Otherwise, [servor], [sirv-cli] or [servitsy] might work for you. | ||
Otherwise [servitsy], [sirv-cli] or [servor] might work for you. | ||
@@ -80,0 +80,0 @@ _† 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)._ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
80711
11
19
2415