Skip to content

Instantly share code, notes, and snippets.

@thecotne
Last active May 24, 2024 19:34
Show Gist options
  • Save thecotne/aef9a4e05045e1c8ab008c4e5052e793 to your computer and use it in GitHub Desktop.
Save thecotne/aef9a4e05045e1c8ab008c4e5052e793 to your computer and use it in GitHub Desktop.

Hono Serve

simple web server with ssl and spa support

install dependencies

pnpm add hono @hono/node-server vite @vitejs/plugin-basic-ssl mime-db

help

Usage:
  $ pnpm tsx ./node-hono.ts <directory> [options]
    <directory>: Directory to serve πŸ“

Options:
  -s, --ssl               Serve over HTTPS πŸ”’
  -S, --spa               Rewrite all 404s to index.html ✏️
  -a, --api               Proxy /api to REACT_APP_SERVICE_URL πŸ”€

Examples:
  $ pnpm tsx ./node-hono.ts dist/public -aSs
  $ pnpm tsx --watch ./node-hono.ts dist/public --ssl --spa --api
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { getCertificate } from '@vitejs/plugin-basic-ssl'
import { Hono } from 'hono'
import assert from 'node:assert'
import fs from 'node:fs/promises'
import { createServer } from 'node:https'
import path from 'node:path'
import { loadEnv } from 'vite'
const config = await getConfig()
console.info('Starting server... 🌎')
const app = new Hono()
if (config.serviceUrl) {
app.all(`${config.serviceUrl.pathname.replace(/\/$/, '')}/*`, async (c) => {
const url = new URL(c.req.raw.url)
url.protocol = config.serviceUrl!.protocol
url.host = config.serviceUrl!.host
url.port = config.serviceUrl!.port
const headers = new Headers(c.req.raw.headers)
headers.set('host', config.serviceUrl!.host)
const res = await fetch(url, {
method: c.req.raw.method,
headers,
body: c.req.raw.body,
// @ts-expect-error - TS doesn't know about `duplex`
// https://github.com/nodejs/node/issues/46221#issuecomment-1383246036
duplex: 'half',
})
{
const headers = new Headers(res.headers)
// https://github.com/honojs/node-server/issues/121#issuecomment-1915749023
headers.delete('content-encoding')
headers.delete('content-length')
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers,
})
}
})
}
app.use(
serveStatic({
root: config.directory,
})
)
if (config.spa) {
app.use(
serveStatic({
root: config.directory,
rewriteRequestPath: () => '/',
})
)
}
const server = serve({
fetch: app.fetch,
createServer: config.cert ? createServer : undefined,
serverOptions: config.cert ? { cert: config.cert, key: config.cert } : undefined,
port: 4000,
})
server.on('listening', () => {
const { port } = server.address() as {
port: number
}
console.info(`Serving ${config.directory} at http${config.cert != null ? 's' : ''}://localhost:${port} 🌎`)
const emojis: string[] = []
if (config.cert != null) emojis.push('πŸ”’')
if (config.spa) emojis.push('✏️ ')
if (config.serviceUrl != null) emojis.push('πŸ”€ ')
if (emojis.length > 0) console.info(emojis.join(' '))
})
async function getConfig() {
if (process.argv.length < 3) {
const script = path.relative(path.dirname(new URL(import.meta.url).pathname), process.argv[1])
console.info('Usage:')
console.info(` $ pnpm tsx ./${script} <directory> [options]`)
console.info(' <directory>: Directory to serve πŸ“')
console.info('\nOptions:')
console.info(' -s, --ssl Serve over HTTPS πŸ”’')
console.info(' -S, --spa Rewrite all 404s to index.html ✏️')
console.info(' -a, --api Proxy /api to REACT_APP_SERVICE_URL πŸ”€')
console.info('\nExamples:')
console.info(` $ pnpm tsx ./${script} dist/public -aSs`)
console.info(` $ pnpm tsx --watch ./${script} dist/public --ssl --spa --api`)
process.exit(1)
}
const directory = process.argv[2]
assert(await fs.stat(directory).then((stat) => stat.isDirectory()), 'Not a directory')
const invalidOptions = process.argv
.slice(3)
.filter((arg) => !/^(--(api|spa|ssl)|-(a|s|S)+)$/.test(arg))
.join(', ')
if (invalidOptions) {
console.error(`Invalid options: ${invalidOptions}`)
process.exit(1)
}
const ssl = process.argv.some((arg) => arg === '--ssl' || /^(--ssl|-\w*s\w*)$/.test(arg))
const spa = process.argv.some((arg) => arg === '--spa' || /^(--spa|-\w*S\w*)$/.test(arg))
const api = process.argv.some((arg) => arg === '--api' || /^(--api|-\w*a\w*)$/.test(arg))
const cert = ssl ? await getCertificate('node_modules/.vite') : undefined
const env = api ? loadEnv('production', process.cwd(), 'REACT_APP_') : undefined
const serviceUrl = env ? new URL(env.REACT_APP_SERVICE_URL) : undefined
return {
directory,
cert,
spa,
serviceUrl,
}
}
#!/usr/bin/env -S pnpm tsx --env-file=.env
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { getCertificate } from '@vitejs/plugin-basic-ssl'
import { Hono } from 'hono'
import assert from 'node:assert'
import fs from 'node:fs/promises'
import { createServer } from 'node:https'
import path from 'node:path'
const config = await getConfig()
console.info('Starting server... 🌎')
const app = new Hono()
if (config.serviceUrl) {
app.all(`${config.serviceUrl.pathname.replace(/\/$/, '')}/*`, async (c) => {
const url = new URL(c.req.raw.url)
url.protocol = config.serviceUrl!.protocol
url.host = config.serviceUrl!.host
url.port = config.serviceUrl!.port
const headers = new Headers(c.req.raw.headers)
headers.set('host', config.serviceUrl!.host)
const res = await fetch(url, {
method: c.req.raw.method,
headers,
body: c.req.raw.body,
// @ts-expect-error - TS doesn't know about `duplex`
// https://github.com/nodejs/node/issues/46221#issuecomment-1383246036
duplex: 'half',
})
{
const headers = new Headers(res.headers)
// https://github.com/honojs/node-server/issues/121#issuecomment-1915749023
headers.delete('content-encoding')
headers.delete('content-length')
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers,
})
}
})
}
app.use(
serveStatic({
root: config.directory,
})
)
if (config.spa) {
app.use(
serveStatic({
root: config.directory,
rewriteRequestPath: () => '/',
})
)
}
const server = serve({
fetch: app.fetch,
createServer: config.cert ? createServer : undefined,
serverOptions: config.cert ? { cert: config.cert, key: config.cert } : undefined,
port: config.port ?? 4000,
})
server.on('listening', () => {
const { port } = server.address() as {
port: number
}
console.info(`Serving ${config.directory} at http${config.cert != null ? 's' : ''}://localhost:${port} 🌎`)
const emojis: string[] = []
if (config.cert != null) emojis.push('πŸ”’')
if (config.spa) emojis.push('✏️ ')
if (config.serviceUrl != null) emojis.push('πŸ”€ ')
if (emojis.length > 0) console.info(emojis.join(' '))
})
async function getConfig() {
if (process.argv.length < 3 || process.argv.includes('--help')) {
const script = path.relative(path.dirname(new URL(import.meta.url).pathname), process.argv[1])
console.info('Usage:')
console.info(` $ ./${script} <directory> [options]`)
console.info(' <directory>: Directory to serve πŸ“')
console.info('\nOptions:')
console.info(' -p, --port <port> Port to listen on (default: 4000) πŸšͺ')
console.info(' -a, --api Proxy /api to REACT_APP_SERVICE_URL πŸ”€')
console.info(' -S, --spa Rewrite all 404s to index.html ✏️')
console.info(' -s, --ssl Serve over HTTPS πŸ”’')
console.info(' --help Display this message πŸ†˜')
console.info('\nExamples:')
console.info(` $ ./${script} dist/public -aSs`)
console.info(` $ ./${script} dist/public -aSsp 3000`)
console.info(` $ ./${script} dist/public --ssl --spa --api --port 3000`)
console.info(` $ pnpm tsx --env-file=.env --watch ./${script} dist/public --ssl --spa --api --port 3000`)
process.exit(1)
}
const directory = process.argv[2]
try {
const stat = await fs.stat(directory)
assert(stat.isDirectory(), 'Not a directory')
} catch {
console.error(`Invalid directory: ${directory}`)
process.exit(1)
}
let api = false
let spa = false
let ssl = false
let port: number = 4000
const invalidOptions: string[] = []
for (let i = 3; i < process.argv.length; i++) {
if (!/^(--(api|spa|ssl|port)|-\w*[asSp]+\w*)$/.test(process.argv[i])) invalidOptions.push(process.argv[i])
else {
if (/^(--api|-\w*a\w*)$/.test(process.argv[i])) api = true
if (/^(--spa|-\w*S\w*)$/.test(process.argv[i])) spa = true
if (/^(--ssl|-\w*s\w*)$/.test(process.argv[i])) ssl = true
if (/^(--port|-\w*p)$/.test(process.argv[i])) {
port = Number(process.argv[i + 1])
i++
}
}
}
if (invalidOptions.length > 0) {
console.error(`Invalid options: ${invalidOptions.join(', ')}`)
process.exit(1)
}
if (!Number.isInteger(port)) {
console.error(`Invalid port: ${port}`)
process.exit(1)
}
const cert = ssl ? await getCertificate('node_modules/.vite') : undefined
const serviceUrl = api && process.env.REACT_APP_SERVICE_URL ? new URL(process.env.REACT_APP_SERVICE_URL) : undefined
return {
directory,
port,
cert,
spa,
serviceUrl,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment