UNPKG

@quasar/cli

Version:

Quasar Framework - the Global CLI

319 lines (262 loc) 7.68 kB
import { getArgv } from '../get-argv.js' const argv = getArgv({ port: { type: 'string', short: 'p', default: process.env.PORT || '4000' }, hostname: { type: 'string', short: 'H', default: process.env.HOSTNAME || '0.0.0.0' }, silent: { type: 'boolean', short: 's' }, cors: { type: 'boolean' }, open: { type: 'boolean', short: 'o' }, index: { type: 'string', short: 'i', default: 'index.html' }, sw: { type: 'boolean' }, history: { type: 'boolean' }, proxy: { type: 'string', short: 'P' }, https: { type: 'boolean' }, C: { type: 'string' }, K: { type: 'string' }, 'no-color': { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }) if (argv.help) { console.log(` Description Start a HTTP(S) server on a folder. Usage $ quasar serve [path] $ quasar serve . # serve current folder If you serve a SSR dist folder built with Quasar CLI then run "node index.js" instead. Options --port, -p Port to use (default: 4000) --hostname, -H Address to use (default: 0.0.0.0) --silent, -s Suppress log message --cors Enable CORS --open, -o Open browser window after starting --index, -i <path> Index url path (default: index.html) --history Use history mode; All requests fallback to /index.html, or whatever "--index" parameter specifies (default: false) --https Enable HTTPS --cert, -C [path] Path to SSL cert file (Optional) --key, -K [path] Path to SSL key file (Optional) --no-color Disable colored output --help, -h Displays this message --proxy, -P [path] Path to proxy definition file (Optional) Proxy file example: // https://hono.dev/docs/helpers/proxy // "proxy" param is hono/proxy export default ({ app, proxy }) => { app.get('/proxy/:path', (c) => { return proxy('http://some.api.com/' + c.req.param('path')) }) } `) argv.__warn?.() process.exit(0) } import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' import { join, isAbsolute } from 'node:path' import { cliPkg } from '../cli-pkg.js' import { log, fatal } from '../logger.js' const root = getAbsolutePath(argv._[0] || '.') const resolve = path => join(root, path) function getAbsolutePath(pathParam) { return isAbsolute(pathParam) ? pathParam : join(process.cwd(), pathParam) } const { green, gray, red } = await import('kolorist') const { Hono } = await import('hono') const app = new Hono() if (argv.cors) { const { cors } = await import('hono/cors') app.use( '/*', cors({ origin: '*', allowMethods: ['*'] // Allows all methods }) ) } if (!argv.silent) { app.use('*', async (c, next) => { await next() const ip = c.env?.incoming?.socket?.remoteAddress || c.req.header('x-forwarded-for') || 'unknown' if (c.res.status >= 200 && c.res.status < 300) { console.log( `${green(`[${c.req.method}]`)} ${c.req.path} ${green(c.res.status)} ${gray('[' + ip + ']')} ${new Date()}` ) } else { console.log( `${red(`[${c.req.method}]`)} ${c.req.path} ${red(`!! ${c.res.status}`)} ${gray('[' + ip + ']')} ${new Date()}` ) } }) } if (argv.proxy) { argv.proxy = getAbsolutePath(argv.proxy) if (!existsSync(argv.proxy)) { fatal(`Proxy definition file not found: ${argv.proxy}`) } // Import the config file const { default: proxyFn } = await import(argv.proxy) const { proxy } = await import('hono/proxy') proxyFn({ app, proxy }) } if (argv.history) { const indexFile = resolve(argv.index) if (!existsSync(indexFile)) { fatal(`Index file not found: ${indexFile}`) } const indexFileContent = await readFile(indexFile, 'utf8') app.use('*', async (c, next) => { if (c.req.method !== 'GET') { return await next() } const acceptHeader = c.req.header('Accept') if (!acceptHeader?.includes('text/html')) { return await next() } const requestedFile = resolve('.' + c.req.path) if (existsSync(requestedFile)) { return await next() } return c.html(indexFileContent) }) } import { serveStatic } from '@hono/node-server/serve-static' const indexFile = argv.index.startsWith('/') ? argv.index.slice(1) : argv.index app.use( '/*', serveStatic({ root, index: indexFile, precompressed: true }) ) const getListeningUrl = hostname => `http${argv.https ? 's' : ''}://${hostname}:${argv.port}` const { getIPs } = await import('../net.js') const getListeningBanner = () => { let { hostname } = argv if (hostname === '0.0.0.0') { const acc = getIPs().map(ip => ['', getListeningUrl(ip)]) if (acc.length !== 0) { acc[0][0] = 'URLs in use' return acc } hostname = 'localhost' } return [['URL in use', getListeningUrl(hostname)]] } const getHttpOptions = async () => { if (!argv.https) { const { createServer } = await import('node:http') return { createServer, serverOptions: { keepAlive: true, keepAliveTimeout: 5000 } } } let fakeCert, key, cert if (argv.key && argv.cert) { key = getAbsolutePath(argv.key) cert = getAbsolutePath(argv.cert) if (existsSync(key)) { key = await readFile(key) } else { fatal(`SSL key file not found: ${key}`) } if (existsSync(cert)) { cert = await readFile(cert) } else { fatal(`SSL cert file not found: ${cert}`) } } else { const { getCertificate } = await import('@quasar/ssl-certificate') fakeCert = await getCertificate({ log, fatal }) } const { createServer } = await import('node:http2') return { createServer, serverOptions: { keepAlive: true, keepAliveTimeout: 5000, key: key || fakeCert, cert: cert || fakeCert } } } import { serve } from '@hono/node-server' const server = serve({ fetch: app.fetch, port: Number.parseInt(argv.port, 10), hostname: argv.hostname, ...(await getHttpOptions()) }) // graceful shutdown process.on('SIGINT', () => { server.close() process.exit(0) }) process.on('SIGTERM', () => { server.close(err => { if (err) { console.error(err) process.exit(1) } process.exit(0) }) }) await new Promise((resolveListen, rejectListen) => { server.once('listening', resolveListen) server.once('error', rejectListen) }) const filler = ''.padEnd(20, ' ') const info = [ ['Quasar CLI', `v${cliPkg.version}`], ...getListeningBanner(), ['Web server root', root], argv.https ? ['HTTPS', 'enabled'] : '', ['Index file', argv.index], argv.history ? ['History mode', 'enabled'] : '', argv.cors ? ['CORS', 'enabled'] : '', argv.proxy ? ['Proxy file', argv.proxy] : '' ] .filter(Boolean) .map( msg => ' ' + (msg[0] === '' ? filler : msg[0].padEnd(20, '.')) + ' ' + green(msg[1]) ) console.log('\n' + info.join('\n') + '\n') if ( argv.open && process.stdout.isTTY && process.env.NODE_ENV !== 'test' && (await import('ci-info').then(({ isCI }) => !isCI)) ) { const url = getListeningUrl( argv.hostname === '0.0.0.0' ? 'localhost' : argv.hostname ) log('Opening default browser at ' + url + '\n') const { default: open } = await import('open') // oxlint-disable-next-line unicorn/prefer-top-level-await open(url).catch(() => { warn('Failed to open default browser') warn() }) }