#!/usr/bin/env node // source/main.ts import { cwd as getPwd, exit, env as env2, stdout } from "node:process"; import path from "node:path"; import chalk4 from "chalk"; import boxen from "boxen"; import clipboard from "clipboardy"; // package.json var package_default = { name: "serve", version: "14.2.0", description: "Static file serving and directory listing", keywords: [ "vercel", "serve", "micro", "http-server" ], repository: "vercel/serve", license: "MIT", type: "module", bin: { serve: "./build/main.js" }, files: [ "build/" ], engines: { node: ">= 14" }, scripts: { develop: "tsx watch ./source/main.ts", start: "node ./build/main.js", compile: "tsup ./source/main.ts", "test:tsc": "tsc --project tsconfig.json", "test:unit": "vitest run --config config/vitest.ts", "test:watch": "vitest watch --config config/vitest.ts", test: "pnpm test:tsc && pnpm test:unit", "lint:code": "eslint --max-warnings 0 source/**/*.ts", "lint:style": "prettier --check --ignore-path .gitignore .", lint: "pnpm lint:code && pnpm lint:style", format: "prettier --write --ignore-path .gitignore .", prepare: "husky install config/husky && pnpm compile" }, dependencies: { "@zeit/schemas": "2.29.0", ajv: "8.11.0", arg: "5.0.2", boxen: "7.0.0", chalk: "5.0.1", "chalk-template": "0.4.0", clipboardy: "3.0.0", compression: "1.7.4", "is-port-reachable": "4.0.0", "serve-handler": "6.1.5", "update-check": "1.5.4" }, devDependencies: { "@types/compression": "1.7.2", "@types/serve-handler": "6.1.1", "@vercel/style-guide": "3.0.0", c8: "7.12.0", eslint: "8.19.0", got: "12.1.0", husky: "8.0.1", "lint-staged": "13.0.3", prettier: "2.7.1", tsup: "6.1.3", tsx: "3.7.1", typescript: "4.6.4", vitest: "0.18.0" }, tsup: { target: "esnext", format: [ "esm" ], outDir: "./build/" }, prettier: "@vercel/style-guide/prettier", eslintConfig: { extends: [ "./node_modules/@vercel/style-guide/eslint/node.js", "./node_modules/@vercel/style-guide/eslint/typescript.js" ], parserOptions: { project: "tsconfig.json" } }, "lint-staged": { "*": [ "prettier --ignore-unknown --write" ], "source/**/*.ts": [ "eslint --max-warnings 0 --fix", "vitest related --run" ], tests: [ "vitest --run" ] } }; // source/utilities/promise.ts import { promisify } from "node:util"; var resolve = async (promiseLike) => { try { const data = await promiseLike; return [void 0, data]; } catch (error2) { return [error2, void 0]; } }; // source/utilities/server.ts import http2 from "node:http"; import https from "node:https"; import { readFile } from "node:fs/promises"; import handler from "serve-handler"; import compression from "compression"; import isPortReachable from "is-port-reachable"; import chalk2 from "chalk"; // source/utilities/http.ts import { networkInterfaces as getNetworkInterfaces } from "node:os"; var networkInterfaces = getNetworkInterfaces(); var registerCloseListener = (fn) => { let run = false; const wrapper = () => { if (!run) { run = true; fn(); } }; process.on("SIGINT", wrapper); process.on("SIGTERM", wrapper); process.on("exit", wrapper); }; var getNetworkAddress = () => { for (const interfaceDetails of Object.values(networkInterfaces)) { if (!interfaceDetails) continue; for (const details of interfaceDetails) { const { address, family, internal } = details; if (family === "IPv4" && !internal) return address; } } }; // source/utilities/logger.ts import chalk from "chalk"; var http = (...message) => console.info(chalk.bgBlue.bold(" HTTP "), ...message); var info = (...message) => console.info(chalk.bgMagenta.bold(" INFO "), ...message); var warn = (...message) => console.error(chalk.bgYellow.bold(" WARN "), ...message); var error = (...message) => console.error(chalk.bgRed.bold(" ERROR "), ...message); var log = console.log; var logger = { http, info, warn, error, log }; // source/utilities/server.ts var compress = promisify(compression()); var startServer = async (endpoint, config2, args2, previous) => { const serverHandler = (request, response) => { const run = async () => { const requestTime = new Date(); const formattedTime = `${requestTime.toLocaleDateString()} ${requestTime.toLocaleTimeString()}`; const ipAddress = request.socket.remoteAddress?.replace("::ffff:", "") ?? "unknown"; const requestUrl = `${request.method ?? "GET"} ${request.url ?? "/"}`; if (!args2["--no-request-logging"]) logger.http(chalk2.dim(formattedTime), chalk2.yellow(ipAddress), chalk2.cyan(requestUrl)); if (args2["--cors"]) { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Private-Network", "true"); } if (!args2["--no-compression"]) await compress(request, response); await handler(request, response, config2); const responseTime = Date.now() - requestTime.getTime(); if (!args2["--no-request-logging"]) logger.http(chalk2.dim(formattedTime), chalk2.yellow(ipAddress), chalk2[response.statusCode < 400 ? "green" : "red"](`Returned ${response.statusCode} in ${responseTime} ms`)); }; run().catch((error2) => { throw error2; }); }; const sslCert = args2["--ssl-cert"]; const sslKey = args2["--ssl-key"]; const sslPass = args2["--ssl-pass"]; const isPFXFormat = sslCert && /[.](?pfx|p12)$/.exec(sslCert) !== null; const useSsl = sslCert && (sslKey || sslPass || isPFXFormat); let serverConfig = {}; if (useSsl && sslCert && sslKey) { serverConfig = { key: await readFile(sslKey), cert: await readFile(sslCert), passphrase: sslPass ? await readFile(sslPass, "utf8") : "" }; } else if (useSsl && sslCert && isPFXFormat) { serverConfig = { pfx: await readFile(sslCert), passphrase: sslPass ? await readFile(sslPass, "utf8") : "" }; } const server = useSsl ? https.createServer(serverConfig, serverHandler) : http2.createServer(serverHandler); const getServerDetails = () => { registerCloseListener(() => server.close()); const details = server.address(); let local; let network; if (typeof details === "string") { local = details; } else if (typeof details === "object" && details.port) { let address; if (details.address === "::") address = "localhost"; else if (details.family === "IPv6") address = `[${details.address}]`; else address = details.address; const ip = getNetworkAddress(); const protocol = useSsl ? "https" : "http"; local = `${protocol}://${address}:${details.port}`; network = ip ? `${protocol}://${ip}:${details.port}` : void 0; } return { local, network, previous }; }; server.on("error", (error2) => { throw new Error(`Failed to serve: ${error2.stack?.toString() ?? error2.message}`); }); if (typeof endpoint.port === "number" && !isNaN(endpoint.port) && endpoint.port !== 0) { const port = endpoint.port; const isClosed = await isPortReachable(port, { host: endpoint.host ?? "localhost" }); if (isClosed) return startServer({ port: 0 }, config2, args2, port); } return new Promise((resolve2, _reject) => { if (typeof endpoint.port !== "undefined" && typeof endpoint.host === "undefined") server.listen(endpoint.port, () => resolve2(getServerDetails())); else if (typeof endpoint.port === "undefined" && typeof endpoint.host !== "undefined") server.listen(endpoint.host, () => resolve2(getServerDetails())); else if (typeof endpoint.port !== "undefined" && typeof endpoint.host !== "undefined") server.listen(endpoint.port, endpoint.host, () => resolve2(getServerDetails())); }); }; // source/utilities/cli.ts import { parse as parseUrl } from "node:url"; import { env } from "node:process"; import chalk3 from "chalk"; import chalkTemplate from "chalk-template"; import parseArgv from "arg"; import checkForUpdate from "update-check"; var helpText = chalkTemplate` {bold.cyan serve} - Static file serving and directory listing {bold USAGE} {bold $} {cyan serve} --help {bold $} {cyan serve} --version {bold $} {cyan serve} folder_name {bold $} {cyan serve} [-l {underline listen_uri} [-l ...]] [{underline directory}] By default, {cyan serve} will listen on {bold 0.0.0.0:3000} and serve the current working directory on that address. Specifying a single {bold --listen} argument will overwrite the default, not supplement it. {bold OPTIONS} --help Shows this help message -v, --version Displays the current version of serve -l, --listen {underline listen_uri} Specify a URI endpoint on which to listen (see below) - more than one may be specified to listen in multiple places -p Specify custom port -s, --single Rewrite all not-found requests to \`index.html\` -d, --debug Show debugging information -c, --config Specify custom path to \`serve.json\` -L, --no-request-logging Do not log any request information to the console. -C, --cors Enable CORS, sets \`Access-Control-Allow-Origin\` to \`*\` -n, --no-clipboard Do not copy the local address to the clipboard -u, --no-compression Do not compress files --no-etag Send \`Last-Modified\` header instead of \`ETag\` -S, --symlinks Resolve symlinks instead of showing 404 errors --ssl-cert Optional path to an SSL/TLS certificate to serve with HTTPS {grey Supported formats: PEM (default) and PKCS12 (PFX)} --ssl-key Optional path to the SSL/TLS certificate\'s private key {grey Applicable only for PEM certificates} --ssl-pass Optional path to the SSL/TLS certificate\'s passphrase --no-port-switching Do not open a port other than the one specified when it\'s taken. {bold ENDPOINTS} Listen endpoints (specified by the {bold --listen} or {bold -l} options above) instruct {cyan serve} to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes. For TCP ports on hostname "localhost": {bold $} {cyan serve} -l {underline 1234} For TCP (traditional host/port) endpoints: {bold $} {cyan serve} -l tcp://{underline hostname}:{underline 1234} For UNIX domain socket endpoints: {bold $} {cyan serve} -l unix:{underline /path/to/socket.sock} For Windows named pipe endpoints: {bold $} {cyan serve} -l pipe:\\\\.\\pipe\\{underline PipeName} `; var getHelpText = () => helpText; var parseEndpoint = (uriOrPort) => { if (!isNaN(Number(uriOrPort))) return { port: Number(uriOrPort) }; const endpoint = uriOrPort; const url = parseUrl(endpoint); switch (url.protocol) { case "pipe:": { const pipe = endpoint.replace(/^pipe:/, ""); if (!pipe.startsWith("\\\\.\\")) throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`); return { host: pipe }; } case "unix:": if (!url.pathname) throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`); return { host: url.pathname }; case "tcp:": url.port = url.port ?? "3000"; url.hostname = url.hostname ?? "localhost"; return { port: Number(url.port), host: url.hostname }; default: throw new Error(`Unknown --listen endpoint scheme (protocol): ${url.protocol ?? "undefined"}`); } }; var options = { "--help": Boolean, "--version": Boolean, "--listen": [parseEndpoint], "--single": Boolean, "--debug": Boolean, "--config": String, "--no-clipboard": Boolean, "--no-compression": Boolean, "--no-etag": Boolean, "--symlinks": Boolean, "--cors": Boolean, "--no-port-switching": Boolean, "--ssl-cert": String, "--ssl-key": String, "--ssl-pass": String, "--no-request-logging": Boolean, "-h": "--help", "-v": "--version", "-l": "--listen", "-s": "--single", "-d": "--debug", "-c": "--config", "-n": "--no-clipboard", "-u": "--no-compression", "-S": "--symlinks", "-C": "--cors", "-L": "--no-request-logging", "-p": "--listen" }; var parseArguments = () => parseArgv(options); var checkForUpdates = async (manifest) => { if (env.NO_UPDATE_CHECK) return; const [error2, update] = await resolve(checkForUpdate(manifest)); if (error2) throw error2; if (!update) return; logger.log(chalk3.bgRed.white(" UPDATE "), `The latest version of \`serve\` is ${update.latest}`); }; // source/utilities/config.ts import { resolve as resolvePath, relative as resolveRelativePath } from "node:path"; import { readFile as readFile2 } from "node:fs/promises"; import Ajv from "ajv"; import schema from "@zeit/schemas/deployment/config-static.js"; var loadConfiguration = async (presentDirectory2, directoryToServe2, args2) => { const files = ["serve.json", "now.json", "package.json"]; if (args2["--config"]) files.unshift(args2["--config"]); const config2 = {}; for (const file of files) { const location = resolvePath(directoryToServe2, file); const [error2, rawContents] = await resolve(readFile2(location, "utf8")); if (error2) { if (error2.code === "ENOENT" && file !== args2["--config"]) continue; else throw new Error(`Could not read configuration from file ${location}: ${error2.message}`); } let parsedJson; try { parsedJson = JSON.parse(rawContents); if (typeof parsedJson !== "object") throw new Error("configuration is not an object"); } catch (parserError) { throw new Error(`Could not parse ${location} as JSON: ${parserError.message}`); } if (file === "now.json") { parsedJson = parsedJson; parsedJson = parsedJson.now.static; } else if (file === "package.json") { parsedJson = parsedJson; parsedJson = parsedJson.static; } if (!parsedJson) continue; Object.assign(config2, parsedJson); if (file === "now.json" || file === "package.json") logger.warn("The config files `now.json` and `package.json` are deprecated. Please use `serve.json`."); break; } if (directoryToServe2) { const staticDirectory = config2.public; config2.public = resolveRelativePath(presentDirectory2, staticDirectory ? resolvePath(directoryToServe2, staticDirectory) : directoryToServe2); } if (Object.keys(config2).length !== 0) { const ajv = new Ajv({ allowUnionTypes: true }); const validate = ajv.compile(schema); if (!validate(config2) && validate.errors) { const defaultMessage = "The configuration you provided is invalid:"; const error2 = validate.errors[0]; throw new Error(`${defaultMessage} ${error2.message ?? ""} ${JSON.stringify(error2.params)}`); } } config2.etag = !args2["--no-etag"]; config2.symlinks = args2["--symlinks"] || config2.symlinks; return config2; }; // source/main.ts var [parseError, args] = await resolve(parseArguments()); if (parseError || !args) { logger.error(parseError.message); exit(1); } var [updateError] = await resolve(checkForUpdates(package_default)); if (updateError) { const suffix = args["--debug"] ? ":" : " (use `--debug` to see full error)"; logger.warn(`Checking for updates failed${suffix}`); if (args["--debug"]) logger.error(updateError.message); } if (args["--version"]) { logger.log(package_default.version); exit(0); } if (args["--help"]) { logger.log(getHelpText()); exit(0); } if (!args["--listen"]) args["--listen"] = [{ port: parseInt(env2.PORT ?? "3000", 10) }]; if (args._.length > 1) { logger.error("Please provide one path argument at maximum"); exit(1); } var presentDirectory = getPwd(); var directoryToServe = args._[0] ? path.resolve(args._[0]) : presentDirectory; var [configError, config] = await resolve(loadConfiguration(presentDirectory, directoryToServe, args)); if (configError || !config) { logger.error(configError.message); exit(1); } if (args["--single"]) { const { rewrites } = config; const existingRewrites = Array.isArray(rewrites) ? rewrites : []; config.rewrites = [ { source: "**", destination: "/index.html" }, ...existingRewrites ]; } for (const endpoint of args["--listen"]) { const { local, network, previous } = await startServer(endpoint, config, args); const copyAddress = !args["--no-clipboard"]; if (!stdout.isTTY || env2.NODE_ENV === "production") { const suffix = local ? ` at ${local}` : ""; logger.info(`Accepting connections${suffix}`); continue; } let message = chalk4.green("Serving!"); if (local) { const prefix = network ? "- " : ""; const space = network ? " " : " "; message += ` ${chalk4.bold(`${prefix}Local:`)}${space}${local}`; } if (network) message += ` ${chalk4.bold("- Network:")} ${network}`; if (previous) message += chalk4.red(` This port was picked because ${chalk4.underline(previous.toString())} is in use.`); if (copyAddress && local) { try { await clipboard.write(local); message += ` ${chalk4.grey("Copied local address to clipboard!")}`; } catch (error2) { logger.error(`Cannot copy server address to clipboard: ${error2.message}.`); } } logger.log(boxen(message, { padding: 1, borderColor: "green", margin: 1 })); } registerCloseListener(() => { logger.log(); logger.info("Gracefully shutting down. Please wait..."); process.on("SIGINT", () => { logger.log(); logger.warn("Force-closing all open sockets..."); exit(0); }); });