diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index b5b92af..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,45 +0,0 @@
-module.exports = {
- env: {
- browser: true,
- es2021: true,
- },
- extends: 'standard-with-typescript',
- overrides: [
- {
- env: {
- node: true,
- },
- files: ['.eslintrc.{js,cjs}'],
- parserOptions: {
- sourceType: 'script',
- },
- },
- {
- files: [
- '**/*.ts',
- '**/*.ts'
- ],
- parserOptions: {
- project: './tsconfig.spec.json',
- },
- },
- ],
- parserOptions: {
- ecmaVersion: 'latest',
- sourceType: 'module',
- project: './tsconfig.json',
- },
-
- rules: {
- strict: 1,
- '@typescript-eslint/semi': ['off'],
- '@typescript-eslint/no-var-requires': ['off'],
- '@typescript-eslint/no-floating-promises':['off'],
- '@typescript-eslint/strict-boolean-expressions': ['off'],
- '@typescript-eslint/explicit-function-return-type': ['off'],
- },
- ignorePatterns: [
- "*.config.js",
- ".eslintrc.js"
- ]
-};
diff --git a/.release-it.json b/.release-it.json
index d972e09..9b7e430 100644
--- a/.release-it.json
+++ b/.release-it.json
@@ -1,7 +1,12 @@
{
"hooks": {
- "before:init": ["npm run lint", "npm run test"],
+ "before:init": [
+ "npm run check:types",
+ "npm run lint",
+ "npm run test",
+ "npm run build-binary"
+ ],
"after:bump": "npm run prerelease",
"after:git:release": "echo After git push, before github release"
}
-}
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 216c889..1e17ef8 100644
--- a/README.md
+++ b/README.md
@@ -1,63 +1,158 @@
-## General introduction
-> A command line tool to serve local directory with http protocol
-
-## Installation
-
-```text
-# Install globally:
-npm i instantly_http -g
-```
-
-## Options
-
-```bash
-instant_http --help
-Usage: instant_http [global options]
-
-Options:
- -V, --version output the version number
- -p --port [port] To point which port to use as the server address. (default: "9090")
- -d --dir [dir] Dir to serve (default: "/home/frank/code/InstantHttp")
- -pt --proxyTarget [proxyTarget] Where the delegated communication targets to
- -pp --proxyPattern [proxyPattern] URL matcher to be used to identify which url to proxy
- -m --mode [mode] Which mode to use (default: "NORMAL")
- -i --indexFile [indexFile] Index File location(relative to --dir) (default: "index.html")
- -q --quiet [quiet] Set it to false to see more debug outputs (default: false)
- -h, --help display help for command
-```
-
-## Usages
-
-### MJS/TS
-```javascript
-import {run} from 'instantly_http';
-```
-
-### CJS
-```javascript
-const {run} = require('instantly_http');
-```
-
-### As a binary
-```bash
-./instantHttp --port=8080 --proxyTarget=http://google.com --proxyPattern=/proxy
-```
-
-## Build for portable binary
-After checkout then install this repository, you can then try below commands to get an executable binary.
-
-```
-npm run build-binary
-```
-
-> [pkg](https://www.npmjs.com/package/pkg) is used as the package utility, please check pkg's document in order to build runnable binaries as you want.
-
-
-## Test
-
-```bash
-npm run test
-```
-
-## Breaking Changes
-- **vNext**: Removed `--open` option and chrome launcher functionality. Server URL is now displayed prominently in terminal for easy clicking.
+# InstantHttp
+
+> Static file server & reverse proxy with optional HTTPS — zero config, single binary.
+
+[](https://www.npmjs.com/package/instantly_http)
+[](https://github.com/pillsilly/InstantHttp/blob/master/LICENSE)
+
+Serves a local directory over HTTP, proxies requests to a backend, or does both in static-first hybrid mode. Ships as an npm package, a CLI, or a standalone binary.
+
+## Why
+
+Frontend development means serving built artifacts against a backend you don't control — a staging API, a production backend, or a colleague's dev server. Writing a throwaway Express script each time, wiring up CORS, compression, and a proxy middleware is boilerplate that adds nothing to your actual work.
+
+InstantHttp collapses that into a single command. No config files. No scaffolding. Point it at a directory, optionally give it a backend to proxy to, and you're done.
+
+Two scenarios where this matters:
+
+**Frontend-to-backend pairing.** You have a React, Vue, or Svelte build output and need to test it against a specific backend. `--proxyStaticFileWise` serves your static files first and proxies API calls to the backend. Switch backends by changing one flag — no code changes, no restart dance.
+
+**Static apps that need HTTP.** Some HTML+CSS+JS prototypes, spec pages, or tool UIs only work over HTTP (service workers, `fetch` to local resources, ES modules that need a real origin). `instant_http --dir ./demo` is zero-code — faster than pulling a full dependency tree.
+
+The binary build goes a step further: a single self-contained executable you can drop onto a CI runner or share with a teammate who doesn't have Node.
+
+## Installation
+
+```bash
+npm i -g instantly_http
+```
+
+Or run without installing:
+
+```bash
+npx instantly_http --port 8080
+```
+
+## Quick start
+
+```bash
+# Serve current directory
+instant_http
+
+# Serve another directory on a different port
+instant_http --dir ./public --port 3000
+
+# SPA mode — all unmatched routes fall back to index.html
+instant_http --mode SPA --dir ./dist
+
+# Static files first, proxy everything else to backend
+instant_http --proxyStaticFileWise --proxyTarget http://localhost:3001
+
+# HTTPS with default bundled cert/key
+instant_http --https
+```
+
+## CLI reference
+
+| Option | Default | Description |
+|---|---|---|
+| `-p, --port` | `9090` | Server port |
+| `-d, --dir` | `cwd` | Directory to serve |
+| `-m, --mode` | `NORMAL` | Server mode: `NORMAL`, `SPA`, or `PROXY_STATIC_FILE_WISE` |
+| `-i, --indexFile` | `index.html` | Index file (relative to `--dir`) for SPA fallback |
+| `-t, --proxyTarget` | — | Backend URL to proxy requests to |
+| `-P, --proxyPattern` | — | URL pattern to match for proxying (supports `*` wildcard) |
+| `--proxyStaticFileWise` | `false` | Serve static files first, proxy everything else |
+| `--https` | `false` | Enable HTTPS |
+| `--httpsKey` | — | Path to HTTPS private key (defaults to bundled `server.key`) |
+| `--httpsCert` | — | Path to HTTPS certificate (defaults to bundled `server.cert`) |
+| `-q, --quiet` | `true` | Suppress request logs (`false` for debug output) |
+| `-V, --version` | — | Print version |
+| `-h, --help` | — | Print help |
+
+## Modes
+
+### NORMAL (default)
+
+Serves static files from `--dir`. When a request matches a directory, renders a clickable directory listing. Missing files return a 404.
+
+### SPA
+
+Single Page Application mode. Serves static files AND falls back to `--indexFile` for any route that doesn't match a file on disk. Use this for React, Vue, or Angular apps with client-side routing.
+
+### PROXY_STATIC_FILE_WISE
+
+Hybrid mode. Serves static files from `--dir` first. Any request that doesn't match a static file gets proxied to `--proxyTarget`. Supports WebSocket upgrade for HMR/dev servers. `/` maps to `--indexFile`.
+
+> **Note:** `--proxyStaticFileWise` reuses `--proxyTarget` and is mutually exclusive with `--proxyPattern`.
+
+## Proxy
+
+Two proxy strategies are available:
+
+**Pattern-based** (`--proxyPattern` + `--proxyTarget`) — only requests matching the pattern are proxied. Everything else is served as static files.
+
+```bash
+# Proxy /api/* requests to backend, serve everything else from ./public
+instant_http --dir ./public --proxyTarget http://localhost:3001 --proxyPattern /api/*
+```
+
+**Static-first** (`--proxyStaticFileWise`) — try the file system first, then proxy the rest.
+
+```bash
+# Dev mode: serve Vite build, proxy everything else (HMR, API) to dev server
+instant_http --proxyStaticFileWise --proxyTarget http://localhost:5173
+```
+
+Proxy headers (`x-forwarded-for`, `x-forwarded-host`, `x-forwarded-proto`, `via`) are stripped from upstream requests to avoid proxy detection. The `referer` and `origin` headers are rewritten to match the target.
+
+## HTTPS
+
+Enable with `--https`. Uses bundled `server.key` and `server.cert` for development — **not for production**. Provide your own cert with `--httpsKey` and `--httpsCert`:
+
+```bash
+instant_http --https --httpsKey ./privkey.pem --httpsCert ./fullchain.pem
+```
+
+## Programmatic API
+
+```js
+// ESM
+import { run } from 'instantly_http';
+
+// CommonJS
+const { run } = require('instantly_http');
+
+const server = run({
+ port: '8080',
+ dir: './public',
+ mode: 'SPA',
+ indexFile: 'index.html',
+ https: true,
+ // httpsKey: './key.pem',
+ // httpsCert: './cert.pem',
+});
+
+// server is an http.Server or https.Server instance
+```
+
+The `run()` function returns a Node.js `http.Server` (or `https.Server`) instance. Call `server.close()` to shut down.
+
+## Build standalone binary
+
+```bash
+git clone https://github.com/pillsilly/InstantHttp
+cd InstantHttp
+npm install
+npm run build-binary
+```
+
+Outputs a self-contained `instant_http` binary in `./executable/` — no Node.js runtime needed. Uses [pkg](https://www.npmjs.com/package/pkg) under the hood.
+
+## Test
+
+```bash
+npm test
+```
+
+Coverage targets: 100% branches, functions, lines, and statements.
diff --git a/babel.config.js b/babel.config.js
deleted file mode 100644
index 8165fe4..0000000
--- a/babel.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- presets: [
- ['@babel/preset-env', { targets: { node: 'current' } }],
- '@babel/preset-typescript',
- ],
-};
diff --git a/package.json b/package.json
index 01bd101..8efbe59 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "instantly_http",
- "version": "1.2.2",
+ "version": "1.3.0",
"description": "Tool to instantly create your own http server for development-use",
"bin": "./dist/bin.js",
"main": "./dist/index.js",
@@ -8,8 +8,8 @@
"testpack": "rm ./tmp/* -rf && npm pack --pack-destination=./tmp",
"test": "npx jest --debug",
"lint": "tsc --noEmit && eslint",
- "build:dts": "tsc -p tsconfig.json --emitDeclarationOnly --outDir dist",
- "compile:prod": "rm -rf dist/* && npx tsup && npm run build:dts && ls -lha dist",
+ "check:types": "tsc -p tsconfig.check-types.json --noEmit",
+ "compile:prod": "rm -rf dist/* && npx tsup && ls -lha dist",
"prerelease": "npm run compile:prod",
"release": "npx release-it",
"audit": "npm audit --omit=dev",
@@ -31,7 +31,7 @@
"src/bin.ts"
],
"clean": true,
- "dts": false,
+ "dts": true,
"format": [
"cjs",
"esm"
@@ -57,8 +57,6 @@
],
"homepage": "https://github.com/pillsilly/InstantHttp",
"devDependencies": {
- "@babel/preset-env": "7.29.2",
- "@babel/preset-typescript": "7.28.5",
"@eslint/eslintrc": "3.3.5",
"@eslint/js": "9.39.2",
"@types/compression": "1.8.1",
@@ -80,7 +78,8 @@
"release-it": "20.0.1",
"supertest": "7.2.2",
"tsup": "8.5.1",
- "typescript": "6.0.3"
+ "typescript": "6.0.3",
+ "ws": "8.20.1"
},
"overrides": {
"basic-ftp": "5.3.0",
diff --git a/src/bin.ts b/src/bin.ts
index 96e2293..4386bac 100644
--- a/src/bin.ts
+++ b/src/bin.ts
@@ -1,12 +1,12 @@
#!/usr/bin/env node
-import { MODE, run } from './run';
-import { Command } from 'commander';
+import {MODE, run} from './run'
+import {Command} from 'commander'
-import pkgJson from '../package.json';
+import pkgJson from '../package.json'
-export function getOptions (): any {
- const program = new Command();
+export function getOptions(): any {
+ const program = new Command()
program
.name('instant_http ')
.version(pkgJson.version)
@@ -27,6 +27,10 @@ export function getOptions (): any {
'-P, --proxyPattern [proxyPattern]',
'URL matcher to be used to identify which url to proxy'
)
+ .option('--proxyStaticFileWise', 'Serve static files first and proxy everything else')
+ .option('--https', 'Enable HTTPS listener')
+ .option('--httpsKey [httpsKey]', 'HTTPS private key file')
+ .option('--httpsCert [httpsCert]', 'HTTPS certificate file')
.option('-m, --mode [mode]', 'Which mode to use', MODE.NORMAL)
.option(
'-i, --indexFile [indexFile]',
@@ -37,13 +41,26 @@ export function getOptions (): any {
'-q, --quiet [quiet]',
'Set it to false to see more debug outputs',
false
- );
+ )
// Parse only args from node onwards, skip jest-specific args
- program.parse(process.argv, { from: 'user' });
- const opts = program.opts();
- console.info(opts);
- return opts;
+ program.parse(process.argv, {from: 'user'})
+ const opts = program.opts()
+ console.info(opts)
+ return opts
+}
+
+export function main() {
+ try {
+ run(getOptions())
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error)
+ console.error(message)
+ process.exit(1)
+ }
}
-run(getOptions());
+/* c8 ignore next 3 */
+if (require.main === module) {
+ main()
+}
diff --git a/src/run-https.ts b/src/run-https.ts
index e98f038..b303701 100644
--- a/src/run-https.ts
+++ b/src/run-https.ts
@@ -18,6 +18,6 @@ app.get('/:filename', cors(), function (req: Request, res: Response, _next: Next
});
https.createServer({
- key: fs.readFileSync('server.key'),
- cert: fs.readFileSync('server.cert')
+ key: fs.readFileSync(path.resolve(__dirname, '..', 'server.key')),
+ cert: fs.readFileSync(path.resolve(__dirname, '..', 'server.cert'))
}, app).listen(9078);
diff --git a/src/run.ts b/src/run.ts
index b1a3a09..12a73f6 100644
--- a/src/run.ts
+++ b/src/run.ts
@@ -1,18 +1,39 @@
import path from 'path'
-import express, {Request} from 'express';
+import http from 'http'
+import https from 'https'
+import {Socket} from 'net'
+import express, {Request} from 'express'
import cors from 'cors'
import fs from 'fs'
-
import compression from 'compression'
-import { version } from '../package.json'
+import {createProxyMiddleware} from 'http-proxy-middleware'
+
+import {version} from '../package.json'
export const MODE = {
NORMAL: 'NORMAL',
- SPA: 'SPA'
-}
+ SPA: 'SPA',
+ PROXY_STATIC_FILE_WISE: 'PROXY_STATIC_FILE_WISE'
+} as const
+
+type Mode = typeof MODE[keyof typeof MODE]
interface CliArg {
+ port?: string
+ dir?: string
+ mode?: Mode | string
+ indexFile?: string
+ quiet?: boolean
+ proxyTarget?: string
+ proxyPattern?: string
+ proxyStaticFileWise?: boolean
+ https?: boolean
+ httpsKey?: string
+ httpsCert?: string
+}
+
+interface ResolvedCliArg {
port: string
dir: string
mode: string
@@ -20,39 +41,104 @@ interface CliArg {
quiet: boolean
proxyTarget: string
proxyPattern: string
+ proxyStaticFileWise: boolean
+ https: boolean
+ httpsKey: string
+ httpsCert: string
}
-const defaultArguments: CliArg = {
+const defaultArguments: ResolvedCliArg = {
port: '9090',
dir: process.cwd(),
mode: MODE.NORMAL,
indexFile: 'index.html',
quiet: true,
proxyTarget: '',
- proxyPattern: ''
-};
-export function run (parameters: CliArg) {
- parameters = Object.assign({ ...defaultArguments }, parameters)
- const { port, dir, proxyTarget, proxyPattern, mode, indexFile, quiet } =
- parameters
+ proxyPattern: '',
+ proxyStaticFileWise: false,
+ https: false,
+ httpsKey: '',
+ httpsCert: ''
+}
+
+const PROXY_FINGERPRINT_HEADERS = ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'via'] as const
+let uncaughtExceptionRegistered = false
+
+export function run(parameters: CliArg) {
+ const resolved: ResolvedCliArg = Object.assign({ ...defaultArguments }, parameters)
+ const port = parsePort(resolved.port)
+
+ validateArguments(resolved)
+
console.log(`Version: ${version}`)
+
const app = express()
- if (proxyTarget && proxyPattern) {
- const { createProxyMiddleware } = require('http-proxy-middleware')
+ const proxyMode = resolved.proxyStaticFileWise ? MODE.PROXY_STATIC_FILE_WISE : resolved.mode
+
+ let server: http.Server | https.Server
+
+ if (proxyMode === MODE.PROXY_STATIC_FILE_WISE) {
+ server = createProxyStaticFirstServer(app, resolved, port)
+ } else {
+ server = createLegacyServer(app, resolved, port)
+ }
+
+ if (!uncaughtExceptionRegistered) {
+ process.on('uncaughtException', function (err: any) {
+ if (err.code === 'EACCES') {
+ console.log(
+ 'EACCES error(lack of permission), use "run as Administrator" when you try to start the program'
+ )
+ } else {
+ console.log('Caught exception: ', err)
+ }
+ })
+ uncaughtExceptionRegistered = true
+ }
+
+ return server
+}
+
+function parsePort(port: string | number | undefined): number {
+ const parsed = Number(port ?? defaultArguments.port)
+ if (!Number.isFinite(parsed) || parsed < 0) {
+ throw new Error(`Invalid port: ${String(port)}`)
+ }
+
+ return parsed
+}
+
+function validateArguments(parameters: ResolvedCliArg) {
+ if (parameters.proxyStaticFileWise && !parameters.proxyTarget) {
+ throw new Error('Invalid argument: --proxyStaticFileWise requires --proxyTarget')
+ }
+
+ if (parameters.proxyStaticFileWise && parameters.proxyPattern) {
+ throw new Error('Invalid argument: --proxyStaticFileWise cannot be used with --proxyPattern')
+ }
+}
+
+function createLegacyServer(app: express.Express, parameters: ResolvedCliArg, port: number) {
+ const dir = path.resolve(parameters.dir)
+
+ app.use(cors())
+ app.use(compression())
+
+ if (parameters.proxyTarget && parameters.proxyPattern) {
const proxy = createProxyMiddleware({
- target: proxyTarget,
+ target: parameters.proxyTarget,
changeOrigin: true,
secure: false
})
- // Convert glob pattern to regex for matching
+
let patternRegex: RegExp
- if (proxyPattern.includes('*')) {
- const regexPattern = '^' + proxyPattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'
+ if (parameters.proxyPattern.includes('*')) {
+ const regexPattern = '^' + parameters.proxyPattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'
patternRegex = new RegExp(regexPattern)
} else {
- patternRegex = new RegExp('^' + proxyPattern.replace(/\./g, '\\.') + '.*$')
+ patternRegex = new RegExp('^' + parameters.proxyPattern.replace(/\./g, '\\.') + '.*$')
}
- // Use a custom middleware to match paths
+
app.use((req, res, next) => {
if (patternRegex.test(req.url)) {
proxy(req, res, next)
@@ -61,32 +147,29 @@ export function run (parameters: CliArg) {
}
})
}
+
const router = express.Router()
router.use(function (req, _res, next) {
- if (!quiet) console.log(`Incoming request: ${req.originalUrl}`)
+ if (!parameters.quiet) console.log(`Incoming request: ${req.originalUrl}`)
next()
})
- console.log(`Serving dir [${dir}]`)
+ console.log(`Serving dir [${parameters.dir}]`)
console.log('')
console.log(' Server running at:')
- console.log(` \x1b[1;34mhttp://127.0.0.1:${port}/\x1b[0m`)
+ console.log(` \x1b[1;34m${parameters.https ? 'https' : 'http'}://127.0.0.1:${port}/\x1b[0m`)
console.log('')
- if (!fs.existsSync(path.resolve(dir))) {
- throw Error(`Dir [${dir}] does not exit`)
+ if (!fs.existsSync(dir)) {
+ throw Error(`Dir [${parameters.dir}] does not exit`)
}
- if (mode === MODE.SPA) {
- app.use(cors())
- app.use(compression())
+ if (parameters.mode === MODE.SPA) {
app.use(router)
app.use(express.static(dir))
- app.use(handleSPA({ dir, indexFile }))
+ app.use(handleSPA({dir, indexFile: parameters.indexFile}))
} else {
- app.use(cors())
- app.use(compression())
app.use(router)
app.use(express.static(dir))
app.use(function (req: any, res: any) {
@@ -94,8 +177,8 @@ export function run (parameters: CliArg) {
fs.readdir(
requestPath,
- { withFileTypes: true },
- (_a: any, list: Array<{ name: any }>) => {
+ {withFileTypes: true},
+ (_a: any, list: Array<{name: any}>) => {
if (!list) {
res.status(404).send(`resource not found: ${requestPath}`)
return
@@ -104,7 +187,7 @@ export function run (parameters: CliArg) {
const html =
title +
list
- .map((f: { name: any }) => f.name)
+ .map((f: {name: any}) => f.name)
.map((name: any) => `${name}`)
.join('
')
@@ -115,31 +198,134 @@ export function run (parameters: CliArg) {
})
}
- const server = app.listen(port)
+ return createHttpOrHttpsServer(app, parameters, port)
+}
- process.on('uncaughtException', function (err: any) {
- if (err.code === 'EACCES') {
- console.log(
- 'EACCES error(lack of permission), use "run as Administrator" when you try to start the program'
- )
- } else {
- console.log('Caught exception: ', err)
+function createProxyStaticFirstServer(app: express.Express, parameters: ResolvedCliArg, port: number) {
+ const dir = path.resolve(parameters.dir)
+ const proxyTarget = new URL(parameters.proxyTarget)
+
+ if (!fs.existsSync(dir)) {
+ throw Error(`Dir [${parameters.dir}] does not exit`)
+ }
+
+ const proxy = createProxyMiddleware({
+ target: parameters.proxyTarget,
+ changeOrigin: true,
+ secure: false,
+ ws: true,
+ xfwd: false,
+ autoRewrite: true,
+ protocolRewrite: parameters.https ? 'https' : 'http',
+ on: {
+ proxyReq(proxyReq, req) {
+ normalizeProxyRequest(proxyReq, req, proxyTarget.origin, proxyTarget.host, false)
+ },
+ proxyReqWs(proxyReq, req) {
+ normalizeProxyRequest(proxyReq, req, proxyTarget.origin, proxyTarget.host, true)
+ }
+ }
+ })
+
+ app.use(
+ express.static(dir, {
+ index: parameters.indexFile,
+ fallthrough: true
+ })
+ )
+
+ app.use((req, res, next) => proxy(req, res, next))
+
+ const server = createHttpOrHttpsServer(app, parameters, port)
+ server.on('upgrade', (req, socket, head) => {
+ if (socket instanceof Socket) {
+ proxy.upgrade(req, socket, head)
}
})
+ console.log(`Serving dir [${parameters.dir}]`)
+ console.log('')
+ console.log(' Server running at:')
+ console.log(` \x1b[1;34m${parameters.https ? 'https' : 'http'}://127.0.0.1:${port}/\x1b[0m`)
+ console.log('')
+
+ return server
+}
+
+function normalizeProxyRequest(proxyReq: any, req: any, targetOrigin: string, targetHost: string, forceOrigin: boolean) {
+ proxyReq.setHeader('host', targetHost)
+
+ if (forceOrigin || req.method !== 'GET' && req.method !== 'HEAD') {
+ proxyReq.setHeader('origin', targetOrigin)
+ }
+
+ proxyReq.setHeader('referer', rewriteReferer(req.headers.referer, targetOrigin) || `${targetOrigin}/`)
+
+ for (const header of PROXY_FINGERPRINT_HEADERS) {
+ proxyReq.removeHeader(header)
+ }
+}
+
+function rewriteReferer(value: string | undefined, targetOrigin: string): string | undefined {
+ if (!value) return undefined
+
+ try {
+ const url = new URL(value)
+ const target = new URL(targetOrigin)
+ url.protocol = target.protocol
+ url.host = target.host
+ return url.toString()
+ } catch {
+ return value
+ }
+}
+
+function createHttpOrHttpsServer(app: express.Express, parameters: ResolvedCliArg, port: number) {
+ if (!parameters.https) {
+ const server = http.createServer(app)
+ server.listen(port)
+ return server
+ }
+
+ const keyPath = resolveHttpsPath(parameters.httpsKey, 'server.key')
+ const certPath = resolveHttpsPath(parameters.httpsCert, 'server.cert')
+
+ const server = https.createServer(
+ {
+ key: fs.readFileSync(keyPath),
+ cert: fs.readFileSync(certPath)
+ },
+ app
+ )
+
+ server.listen(port)
return server
}
-function handleSPA ({ dir, indexFile }: { dir: string, indexFile: string }) {
+function resolveHttpsPath(explicitPath: string, fileName: string): string {
+ const candidates = [
+ explicitPath,
+ path.resolve(process.cwd(), fileName),
+ path.resolve(__dirname, '..', fileName)
+ ].filter(Boolean)
+
+ for (const candidate of candidates) {
+ if (fs.existsSync(candidate)) {
+ return candidate
+ }
+ }
+
+ throw new Error(`Unable to find ${fileName}. Checked: ${candidates.join(', ')}`)
+}
+
+function handleSPA({dir, indexFile}: {dir: string, indexFile: string}) {
return (req: Request, res: any) => {
const requestPath = path.resolve(`${dir}${req.url}`)
- fs.readdir(requestPath, { withFileTypes: true }, () => {
- res.writeHead(200, { 'Content-Type': 'text/html' })
+ fs.readdir(requestPath, {withFileTypes: true}, () => {
+ res.writeHead(200, {'Content-Type': 'text/html'})
res.write(fs.readFileSync(`${dir}/${indexFile}`))
res.end()
})
}
}
-
-// export {run, MODE};
diff --git a/test.mjs b/test.mjs
deleted file mode 100644
index 99e9497..0000000
--- a/test.mjs
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env zx
-
-await $`cat package.json | grep name`
-
-let branch = await $`git branch --show-current`
-await $`dep deploy --branch=${branch}`
-
-await Promise.all([
- $`sleep 1; echo 1`,
- $`sleep 2; echo 2`,
- $`sleep 3; echo 3`,
-])
-
-let name = 'foo bar'
-await $`mkdir /tmp/${name}`
diff --git a/test/bin.spec.ts b/test/bin.spec.ts
index 3c38835..68a4fe4 100644
--- a/test/bin.spec.ts
+++ b/test/bin.spec.ts
@@ -1,16 +1,21 @@
-import { run } from '../src/run';
-import { getOptions } from '../src/bin';
-jest.mock('../src/run');
+import {getOptions} from '../src/bin'
+
+describe('Test bin.js', function () {
+ const originalArgv = process.argv
-describe.only('Test bin.js', function () {
beforeEach(function () {
- // @ts-ignore
- run.mockImplementation(() => {});
- });
- afterAll(function () {});
+ process.argv = ['node', 'instant_http']
+ })
+
+ afterEach(function () {
+ process.argv = originalArgv
+ jest.restoreAllMocks()
+ jest.dontMock('../src/run')
+ jest.resetModules()
+ })
it('should give default options', async function () {
- const res = getOptions();
+ const res = getOptions()
expect(res).toEqual(
expect.objectContaining({
dir: expect.stringContaining('InstantHttp'),
@@ -19,6 +24,95 @@ describe.only('Test bin.js', function () {
port: '9090',
quiet: false
})
- );
- });
-});
+ )
+ })
+
+ it('should parse the new proxy flags', async function () {
+ process.argv = [
+ 'node',
+ 'instant_http',
+ '--proxyStaticFileWise',
+ '--https',
+ '--httpsKey',
+ '/tmp/key.pem',
+ '--httpsCert',
+ '/tmp/cert.pem',
+ '--proxyTarget',
+ 'http://127.0.0.1:1234'
+ ]
+
+ const res = getOptions()
+ expect(res).toEqual(
+ expect.objectContaining({
+ proxyStaticFileWise: true,
+ https: true,
+ httpsKey: '/tmp/key.pem',
+ httpsCert: '/tmp/cert.pem',
+ proxyTarget: 'http://127.0.0.1:1234'
+ })
+ )
+ })
+
+ it('should call run with parsed options from main', function () {
+ const runMock = jest.fn()
+ const main = loadMainWithRunMock(runMock)
+
+ main()
+
+ expect(runMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ dir: expect.stringContaining('InstantHttp'),
+ indexFile: 'index.html',
+ mode: 'NORMAL',
+ port: '9090',
+ quiet: false
+ })
+ )
+ })
+
+ it('should exit with an error message when main fails', function () {
+ const runMock = jest.fn(() => {
+ throw new Error('boom')
+ })
+ const main = loadMainWithRunMock(runMock)
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => undefined) as never)
+
+ main()
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith('boom')
+ expect(exitSpy).toHaveBeenCalledWith(1)
+ })
+
+ it('should stringify non-Error failures from main', function () {
+ const runMock = jest.fn(() => {
+ throw 'boom'
+ })
+ const main = loadMainWithRunMock(runMock)
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => undefined) as never)
+
+ main()
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith('boom')
+ expect(exitSpy).toHaveBeenCalledWith(1)
+ })
+})
+
+function loadMainWithRunMock(runMock: jest.Mock) {
+ let main: typeof import('../src/bin').main
+
+ jest.isolateModules(() => {
+ jest.doMock('../src/run', () => {
+ const actual = jest.requireActual('../src/run')
+ return {
+ ...actual,
+ run: runMock
+ }
+ })
+
+ ;({main} = require('../src/bin'))
+ })
+
+ return main!
+}
diff --git a/test/test-import.ts b/test/test-import.ts
new file mode 100644
index 0000000..09d4d95
--- /dev/null
+++ b/test/test-import.ts
@@ -0,0 +1 @@
+import { run, MODE } from '../dist/index'
\ No newline at end of file
diff --git a/test/test.spec.ts b/test/test.spec.ts
index d7a7bd9..8b0c04e 100644
--- a/test/test.spec.ts
+++ b/test/test.spec.ts
@@ -1,157 +1,547 @@
-import { run } from '../src/run';
+import fs from 'fs'
+import http from 'http'
+import https from 'https'
+import os from 'os'
+import path from 'path'
+import {once} from 'events'
+import {AddressInfo} from 'net'
-const request = require('supertest');
+import {WebSocketServer} from 'ws'
-describe('Test run.js', function () {
- let app: any;
+import {run} from '../src/run'
- beforeEach(function () {});
+const request = require('supertest')
+const WebSocket = require('ws')
- afterEach(function () {
- if (app) {
- app.close();
- }
- jest.clearAllMocks();
- });
+jest.setTimeout(20000)
- afterAll(function () {});
+const activeServers: Array = []
- let portCounter = 9100;
-
- function getNextPort() {
- return String(portCounter++);
- }
+afterEach(async function () {
+ await Promise.all(activeServers.splice(0).map(closeServer))
+ jest.restoreAllMocks()
+})
+describe('run', function () {
it('should start server without passing arguments', async function () {
- app = run({ port: getNextPort() } as any);
- await request(app).get('/').expect('Content-Type', /html/).expect(200);
- });
+ const dir = makeTempDir()
+ const server = run({port: '0', dir} as any)
+ activeServers.push(server)
+
+ const response = await request(server).get('/').expect(200)
+
+ expect(response.text).toContain('Current Dir:')
+ })
it('should throw error when directory does not exist', function () {
expect(() => {
- run({ dir: '/non_existent_directory_12345', port: getNextPort() } as any);
- }).toThrow('Dir [/non_existent_directory_12345] does not exit');
- });
+ run({dir: '/non_existent_directory_12345', port: '0'} as any)
+ }).toThrow('Dir [/non_existent_directory_12345] does not exit')
+ })
it('should log incoming requests when quiet is false', async function () {
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
- app = run({ port: getNextPort(), quiet: false } as any);
- await request(app).get('/');
- expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Incoming request:'));
- consoleLogSpy.mockRestore();
- });
-
- describe('Default mode', function () {
- it('should list files when given request path is a real directory', async function () {
- app = run({ port: getNextPort() } as any);
- const response = await request(app).get('/').expect(200);
-
- expect(response.text.startsWith('Current Di')).toBeTruthy();
- });
-
- it('should return 404 when requested path does not exist', async function () {
- app = run({ port: getNextPort() } as any);
- // Request a file that doesn't exist (not a directory)
- const response = await request(app).get('/non_existent_file_12345.txt').expect(404);
- expect(response.text).toContain('resource not found');
- });
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
+ const dir = makeTempDir()
+ const server = run({port: '0', dir, quiet: false} as any)
+ activeServers.push(server)
- it('should forward request /api/abc to proxyTarget http://localhost:9091', async function () {
- const params = {
- port: getNextPort(),
- proxyPattern: '/api/*',
- proxyTarget: 'http://localhost:9099/'
- };
- app = run(params as any);
+ await request(server).get('/')
+
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Incoming request:'))
+ })
+
+ it.each(['abc', '-1'])('should reject invalid port %s', function (port) {
+ expect(() => {
+ run({port, dir: makeTempDir()} as any)
+ }).toThrow(`Invalid port: ${port}`)
+ })
+
+ it('should use the default port when port is omitted', function () {
+ expect(() => {
+ run({port: undefined, dir: '/non_existent_directory_12345'} as any)
+ }).toThrow('Dir [/non_existent_directory_12345] does not exit')
+ })
+
+ it('should log uncaught exceptions', function () {
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
+ const dir = makeTempDir()
+ const server = run({port: '0', dir} as any)
+ activeServers.push(server)
- const res = await request(app).get('/api/abc');
+ const handler = process
+ .listeners('uncaughtException')
+ .find(listener => String(listener).includes('EACCES error(lack of permission)'))
- expect(res.text).toContain('Error occurred while trying to proxy');
- });
+ expect(handler).toBeDefined()
- it('should forward request /api/abc to proxyTarget when proxyPattern has no wildcard', async function () {
- const params = {
- port: getNextPort(),
+ ;(handler as (err: Error & {code?: string}) => void)(Object.assign(new Error('denied'), {code: 'EACCES'}))
+ ;(handler as (err: Error & {code?: string}) => void)(Object.assign(new Error('boom'), {code: 'OTHER'}))
+
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ 'EACCES error(lack of permission), use "run as Administrator" when you try to start the program'
+ )
+ expect(consoleLogSpy).toHaveBeenCalledWith('Caught exception: ', expect.any(Error))
+ })
+
+ describe('legacy proxy mode', function () {
+ it('should forward request /api/abc to proxyTarget http://localhost:9091', async function () {
+ const upstream = await startHttpUpstream((req, res) => {
+ res.statusCode = 200
+ res.setHeader('content-type', 'text/plain')
+ res.end(`upstream:${req.url}`)
+ })
+
+ const server = run({
+ port: '0',
proxyPattern: '/api',
- proxyTarget: 'http://localhost:9099/'
- };
- app = run(params as any);
+ proxyTarget: upstream.url
+ } as any)
+ activeServers.push(server)
- const res = await request(app).get('/api/abc');
+ const response = await request(server).get('/api/abc').expect(200)
- expect(res.text).toContain('Error occurred while trying to proxy');
- });
+ expect(response.text).toBe('upstream:/api/abc')
+ })
it('should not forward non-matching request to proxy', async function () {
- const params = {
- port: getNextPort(),
+ const upstream = await startHttpUpstream((req, res) => {
+ res.statusCode = 200
+ res.end(`upstream:${req.url}`)
+ })
+
+ const dir = makeTempDir()
+ const server = run({
+ port: '0',
+ dir,
proxyPattern: '/api/*',
- proxyTarget: 'http://localhost:9099/'
- };
- app = run(params as any);
-
- await request(app).get('/').expect(200);
- });
- });
-
- describe('SPA Mode', function () {
- it('should redirect to index.html when given url can not match any resource', async function () {
- app = run({ mode: 'SPA', indexFile: 'test/resource/test.index.file', port: getNextPort() } as any);
-
- const res = await request(app).get('/api/abc').expect(200);
-
- expect(res.text.trim()).toEqual('test index file.');
- });
- });
-
- describe('Uncaught exception handler', function () {
- let consoleLogSpy: jest.SpyInstance;
- let uncaughtHandler: any = null;
-
- beforeEach(function () {
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
- // Capture the handler without causing infinite recursion
- const originalOn = process.on;
- (process as any).on = function (event: string, handler: any) {
- if (event === 'uncaughtException') {
- uncaughtHandler = handler;
- }
- return originalOn.call(process, event, handler);
- };
- });
-
- afterEach(function () {
- consoleLogSpy.mockRestore();
- if (uncaughtHandler) {
- process.removeListener('uncaughtException', uncaughtHandler);
- uncaughtHandler = null;
+ proxyTarget: upstream.url
+ } as any)
+ activeServers.push(server)
+
+ const response = await request(server).get('/').expect(200)
+
+ expect(response.text).toContain('Current Dir:')
+ })
+
+ it('should return 404 for missing paths', async function () {
+ const dir = makeTempDir()
+ const server = run({
+ port: '0',
+ dir
+ } as any)
+ activeServers.push(server)
+
+ const response = await request(server).get('/missing-route').expect(404)
+
+ expect(response.text).toContain(`resource not found: ${path.resolve(`${dir}/missing-route`)}`)
+ })
+ })
+
+ describe('proxyStaticFileWise mode', function () {
+ it('should reject missing static directories', function () {
+ expect(() => {
+ run({
+ port: '0',
+ dir: '/non_existent_directory_12345',
+ proxyStaticFileWise: true,
+ proxyTarget: 'http://127.0.0.1:1'
+ } as any)
+ }).toThrow('Dir [/non_existent_directory_12345] does not exit')
+ })
+
+ it('should serve static files locally before proxying', async function () {
+ const staticDir = makeStaticFixture({
+ 'login.html': 'local login',
+ 'asset.txt': 'local asset'
+ })
+ const upstream = await startHttpUpstream((req, res) => {
+ res.statusCode = 200
+ res.setHeader('content-type', 'text/plain')
+ res.end(`upstream:${req.url}`)
+ })
+
+ const server = run({
+ port: '0',
+ dir: staticDir,
+ indexFile: 'login.html',
+ proxyStaticFileWise: true,
+ proxyTarget: upstream.url
+ } as any)
+ activeServers.push(server)
+
+ const root = await request(server).get('/').expect(200)
+ const login = await request(server).get('/login.html').expect(200)
+ const asset = await request(server).get('/asset.txt').expect(200)
+
+ expect(root.text).toContain('local login')
+ expect(login.text).toContain('local login')
+ expect(asset.text).toBe('local asset')
+ })
+
+ it('should reject conflicting proxy flags', function () {
+ expect(() => {
+ run({
+ port: '0',
+ dir: makeTempDir(),
+ proxyStaticFileWise: true,
+ proxyPattern: '/api',
+ proxyTarget: 'http://127.0.0.1:1'
+ } as any)
+ }).toThrow('Invalid argument: --proxyStaticFileWise cannot be used with --proxyPattern')
+ })
+
+ it('should reject proxyStaticFileWise without proxyTarget', function () {
+ expect(() => {
+ run({
+ port: '0',
+ dir: makeTempDir(),
+ proxyStaticFileWise: true
+ } as any)
+ }).toThrow('Invalid argument: --proxyStaticFileWise requires --proxyTarget')
+ })
+
+ it('should normalize upstream headers and preserve POST bodies', async function () {
+ const upstreamCalls: Array<{
+ url: string
+ method: string
+ headers: http.IncomingHttpHeaders
+ body: string
+ }> = []
+
+ const upstream = await startHttpUpstream((req, res) => {
+ let body = ''
+ req.setEncoding('utf8')
+ req.on('data', chunk => {
+ body += chunk
+ })
+ req.on('end', () => {
+ upstreamCalls.push({
+ url: req.url ?? '',
+ method: req.method ?? '',
+ headers: req.headers,
+ body
+ })
+ res.statusCode = 200
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ok: true}))
+ })
+ })
+
+ const staticDir = makeStaticFixture({'login.html': 'local login'})
+ const server = run({
+ port: '0',
+ dir: staticDir,
+ indexFile: 'login.html',
+ proxyStaticFileWise: true,
+ proxyTarget: upstream.url
+ } as any)
+ activeServers.push(server)
+ const proxyPort = await getServerPort(server)
+
+ await request(server)
+ .post('/LoginRequest')
+ .set('Origin', `http://127.0.0.1:${proxyPort}`)
+ .set('Referer', `http://127.0.0.1:${proxyPort}/login.html`)
+ .set('Via', '1.1 proxy')
+ .set('X-Forwarded-For', '10.0.0.1')
+ .set('X-Forwarded-Host', '127.0.0.1')
+ .set('X-Forwarded-Proto', 'http')
+ .set('Content-Type', 'application/json')
+ .send('{"login":"Nemuadmin","token":"nemuuser"}')
+ .expect(200)
+
+ expect(upstreamCalls).toHaveLength(1)
+ expect(upstreamCalls[0].url).toBe('/LoginRequest')
+ expect(upstreamCalls[0].method).toBe('POST')
+ expect(upstreamCalls[0].headers.host).toBe(upstream.hostHeader)
+ expect(upstreamCalls[0].headers.origin).toBe(upstream.origin)
+ expect(upstreamCalls[0].headers.referer).toBe(`${upstream.origin}/login.html`)
+ expect(upstreamCalls[0].headers['x-forwarded-for']).toBeUndefined()
+ expect(upstreamCalls[0].headers.via).toBeUndefined()
+ expect(upstreamCalls[0].body).toBe('{"login":"Nemuadmin","token":"nemuuser"}')
+
+ await request(server)
+ .get('/InvalidRefererRequest')
+ .set('Referer', 'not a url')
+ .expect(200)
+
+ expect(upstreamCalls).toHaveLength(2)
+ expect(upstreamCalls[1].headers.referer).toBe('not a url')
+ })
+
+ it('should rewrite upstream redirects to the proxy origin', async function () {
+ let upstreamUrl = ''
+ const upstream = await startHttpUpstream((req, res) => {
+ res.statusCode = 302
+ res.setHeader('location', `${upstreamUrl}/redirected`)
+ res.end('')
+ })
+ upstreamUrl = upstream.url
+
+ const staticDir = makeStaticFixture({'login.html': 'local login'})
+ const server = run({
+ port: '0',
+ dir: staticDir,
+ indexFile: 'login.html',
+ proxyStaticFileWise: true,
+ proxyTarget: upstream.url
+ } as any)
+ activeServers.push(server)
+ const proxyPort = await getServerPort(server)
+
+ const response = await request(server).get('/VersionRequest').expect(302)
+
+ expect(response.headers.location).toBe(`http://127.0.0.1:${proxyPort}/redirected`)
+ })
+
+ it('should forward websocket upgrades', async function () {
+ const wsState = {
+ messages: [] as string[],
+ upgradeUrl: ''
+ }
+
+ const upstream = await startWebSocketUpstream(wsState)
+ const staticDir = makeStaticFixture({'login.html': 'local login'})
+ const server = run({
+ port: '0',
+ dir: staticDir,
+ indexFile: 'login.html',
+ proxyStaticFileWise: true,
+ proxyTarget: upstream.url
+ } as any)
+ activeServers.push(server)
+ const proxyPort = await getServerPort(server)
+
+ await new Promise((resolve, reject) => {
+ const socket = new WebSocket(`ws://127.0.0.1:${proxyPort}/websocket`)
+ socket.on('open', () => {
+ socket.send('ping')
+ })
+ socket.on('message', message => {
+ expect(String(message)).toBe('echo:ping')
+ socket.close()
+ resolve()
+ })
+ socket.on('error', reject)
+ })
+
+ expect(wsState.upgradeUrl).toBe('/websocket')
+ expect(wsState.messages).toContain('ping')
+ })
+
+ it('should fail when HTTPS certificate files are unavailable', function () {
+ const dir = makeTempDir()
+ jest.spyOn(fs, 'existsSync').mockImplementation(candidate => candidate === dir)
+
+ expect(() => {
+ run({
+ port: '0',
+ dir,
+ proxyStaticFileWise: true,
+ proxyTarget: 'http://127.0.0.1:1234',
+ https: true
+ } as any)
+ }).toThrow('Unable to find server.key')
+ })
+ })
+
+ describe('HTTPS listener', function () {
+ it('should serve the legacy server over HTTPS when enabled', async function () {
+ const staticDir = makeStaticFixture({
+ 'index.html': 'legacy https'
+ })
+ const server = run({
+ port: '0',
+ dir: staticDir,
+ https: true,
+ httpsKey: path.resolve(__dirname, '..', 'server.key'),
+ httpsCert: path.resolve(__dirname, '..', 'server.cert')
+ } as any)
+ activeServers.push(server)
+ const proxyPort = await getServerPort(server)
+
+ const body = await httpsGet(proxyPort, '/')
+
+ expect(body).toContain('legacy https')
+ })
+
+ it('should serve the proxy over HTTPS when enabled', async function () {
+ const staticDir = makeStaticFixture({
+ 'login.html': 'local login'
+ })
+ const upstream = await startHttpUpstream((req, res) => {
+ res.statusCode = 200
+ res.end('upstream')
+ })
+ const server = run({
+ port: '0',
+ dir: staticDir,
+ indexFile: 'login.html',
+ proxyStaticFileWise: true,
+ proxyTarget: upstream.url,
+ https: true,
+ httpsKey: path.resolve(__dirname, '..', 'server.key'),
+ httpsCert: path.resolve(__dirname, '..', 'server.cert')
+ } as any)
+ activeServers.push(server)
+ const proxyPort = await getServerPort(server)
+
+ const body = await httpsGet(proxyPort, '/')
+
+ expect(body).toContain('local login')
+ })
+ })
+
+ describe('SPA mode', function () {
+ it('should fall back to index.html for missing routes', async function () {
+ const staticDir = makeStaticFixture({
+ 'index.html': 'spa index',
+ 'assets/app.js': 'console.log("spa")'
+ })
+ const server = run({
+ port: '0',
+ dir: staticDir,
+ mode: 'SPA',
+ indexFile: 'index.html'
+ } as any)
+ activeServers.push(server)
+
+ const response = await request(server).get('/missing-route').expect(200)
+
+ expect(response.text).toContain('spa index')
+ })
+ })
+})
+
+function makeTempDir(): string {
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'instant-http-'))
+}
+
+function makeStaticFixture(files: Record): string {
+ const dir = makeTempDir()
+ for (const [filePath, content] of Object.entries(files)) {
+ const fullPath = path.join(dir, filePath)
+ fs.mkdirSync(path.dirname(fullPath), {recursive: true})
+ fs.writeFileSync(fullPath, content)
+ }
+ return dir
+}
+
+async function closeServer(server: http.Server | https.Server) {
+ if (!server.listening) return
+
+ await new Promise((resolve, reject) => {
+ server.close(error => {
+ if (error) {
+ reject(error)
+ return
+ }
+
+ resolve()
+ })
+ })
+}
+
+async function getServerPort(server: http.Server | https.Server): Promise {
+ if (!server.listening) {
+ await once(server, 'listening')
+ }
+
+ const address = server.address()
+ if (!address || typeof address === 'string') {
+ throw new Error('server did not start listening')
+ }
+
+ return address.port
+}
+
+async function startHttpUpstream(
+ handler: http.RequestListener
+): Promise<{server: http.Server, url: string, origin: string, hostHeader: string}> {
+ const server = http.createServer(handler)
+ await listen(server)
+ activeServers.push(server)
+ const port = getAddressPort(server)
+
+ return {
+ server,
+ url: `http://127.0.0.1:${port}`,
+ origin: `http://127.0.0.1:${port}`,
+ hostHeader: `127.0.0.1:${port}`
+ }
+}
+
+async function startWebSocketUpstream(state: {messages: string[], upgradeUrl: string}) {
+ const server = http.createServer()
+ const wss = new WebSocketServer({noServer: true})
+
+ server.on('upgrade', (req, socket, head) => {
+ state.upgradeUrl = req.url ?? ''
+ wss.handleUpgrade(req, socket, head, ws => {
+ wss.emit('connection', ws, req)
+ })
+ })
+
+ wss.on('connection', ws => {
+ ws.on('message', message => {
+ const text = String(message)
+ state.messages.push(text)
+ ws.send(`echo:${text}`)
+ })
+ })
+
+ await listen(server)
+ activeServers.push(server)
+ const port = getAddressPort(server)
+
+ return {
+ server,
+ url: `http://127.0.0.1:${port}`
+ }
+}
+
+async function listen(server: http.Server): Promise {
+ if (server.listening) return
+
+ await new Promise((resolve, reject) => {
+ server.once('error', reject)
+ server.listen(0, '127.0.0.1', () => {
+ server.off('error', reject)
+ resolve()
+ })
+ })
+}
+
+function getAddressPort(server: http.Server): number {
+ const address = server.address()
+ if (!address || typeof address === 'string') {
+ throw new Error('server did not start listening')
+ }
+
+ return (address as AddressInfo).port
+}
+
+async function httpsGet(port: number, pathname: string): Promise {
+ return await new Promise((resolve, reject) => {
+ const req = https.request(
+ {
+ host: '127.0.0.1',
+ port,
+ path: pathname,
+ method: 'GET',
+ rejectUnauthorized: false
+ },
+ res => {
+ let body = ''
+ res.setEncoding('utf8')
+ res.on('data', chunk => {
+ body += chunk
+ })
+ res.on('end', () => resolve(body))
}
- // Restore original process.on
- delete (process as any).on;
- });
-
- it('should log EACCES error message for permission errors', function () {
- app = run({ port: getNextPort(), quiet: true } as any);
- expect(uncaughtHandler).not.toBeNull();
-
- uncaughtHandler({ code: 'EACCES' });
-
- expect(consoleLogSpy).toHaveBeenCalledWith(
- 'EACCES error(lack of permission), use "run as Administrator" when you try to start the program'
- );
- });
-
- it('should log general error message for other exceptions', function () {
- app = run({ port: getNextPort(), quiet: true } as any);
- expect(uncaughtHandler).not.toBeNull();
-
- const testError = new Error('Test error');
- uncaughtHandler(testError);
-
- expect(consoleLogSpy).toHaveBeenCalledWith(
- 'Caught exception: ',
- testError
- );
- });
- });
-});
+ )
+
+ req.on('error', reject)
+ req.end()
+ })
+}
diff --git a/tsconfig.check-types.json b/tsconfig.check-types.json
new file mode 100644
index 0000000..020b5d6
--- /dev/null
+++ b/tsconfig.check-types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["test/test-import.ts"],
+ "compilerOptions": {
+ "noEmit": true,
+ "declaration": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 090ab59..1d7cf6a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -94,7 +94,7 @@
"noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
-
+ "ignoreDeprecations": "6.0",
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */