|
#!/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, |
|
} |
|
} |