import os from 'os' import fs from 'fs' import path from 'path' import chalk from 'chalk' import { createLogger, createServer as createViteServer, ServerOptions, } from 'vite' import express from 'express' import { hasOwn } from '@vue/shared' import { parseManifestJson } from '@dcloudio/uni-cli-shared' import { CliOptions } from '.' import { addConfigFile, cleanOptions } from './utils' export async function createServer(options: CliOptions & ServerOptions) { const server = await createViteServer( addConfigFile({ root: process.env.VITE_ROOT_DIR, logLevel: options.logLevel, clearScreen: options.clearScreen, server: cleanOptions(options) as ServerOptions, }) ) await server.listen() } export async function createSSRServer(options: CliOptions & ServerOptions) { const app = express() /** * @type {import('vite').ViteDevServer} */ const vite = await createViteServer( addConfigFile({ root: process.env.VITE_ROOT_DIR, logLevel: options.logLevel, clearScreen: options.clearScreen, server: { middlewareMode: true, watch: { // During tests we edit the files too fast and sometimes chokidar // misses change events, so enforce polling for consistency usePolling: true, interval: 100, }, }, }) ) // use vite's connect instance as middleware app.use(vite.middlewares) app.use('*', async (req, res) => { try { const { h5 } = parseManifestJson(process.env.UNI_INPUT_DIR) const base = (h5 && h5.router && h5.router.base) || '' const url = req.originalUrl.replace(base, '') const template = await vite.transformIndexHtml( url, fs.readFileSync( path.resolve(process.env.VITE_ROOT_DIR!, 'index.html'), 'utf-8' ) ) const render = ( await vite.ssrLoadModule( path.resolve(process.env.UNI_INPUT_DIR, 'entry-server.js') ) ).render const [appHtml, preloadLinks, appContext, title] = await render(url) const icon = template.includes('rel="icon"') ? '' : '\n' const html = template .replace(/(.*?)<\/title>/, `${icon}<title>${title}`) .replace(``, preloadLinks) .replace(``, appHtml) .replace(``, appContext) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e: any) { vite && vite.ssrFixStacktrace(e) res.status(500).end(e.stack) } }) const logger = createLogger(options.logLevel) const serverOptions = vite.config.server || {} const protocol = ( hasOwn(options, 'https') ? options.https : serverOptions.https ) ? 'https' : 'http' let port = options.port || serverOptions.port || 3000 let hostname: string | undefined if (options.host === 'localhost') { // Use a secure default hostname = '127.0.0.1' } else if (options.host === undefined || options.host === true) { // probably passed --host in the CLI, without arguments hostname = undefined // undefined typically means 0.0.0.0 or :: (listen on all IPs) } else { hostname = options.host as string } return new Promise((resolve, reject) => { const onSuccess = () => { const interfaces = os.networkInterfaces() const locals: string[] = [] const networks: string[] = [] Object.keys(interfaces).forEach((key) => (interfaces[key] || []) .filter((details) => details.family === 'IPv4') .forEach((detail) => { if (detail.address.includes('127.0.0.1')) { locals.push(detail.address) } else { networks.push(detail.address) } }) ) locals.forEach((host) => { const url = `${protocol}://${host}:${chalk.bold(port)}${ vite.config.base }` logger.info(` - Local: ${chalk.cyan(url)}`) }) const networksLen = networks.length - 1 networks.forEach((host, index) => { const url = `${protocol}://${host}:${chalk.bold(port)}${ vite.config.base }` logger.info( ` ${index === networksLen ? '-' : '>'} Network: ${chalk.cyan(url)}` ) }) resolve(server) } const onError = (e: Error & { code?: string }) => { if (e.code === 'EADDRINUSE') { if (options.strictPort) { server.off('error', onError) reject(new Error(`Port ${port} is already in use`)) } else { logger.info(`Port ${port} is in use, trying another one...`) app.listen(++port, hostname!, onSuccess).on('error', onError) } } else { server.off('error', onError) reject(e) } } const server = app.listen(port, hostname!, onSuccess).on('error', onError) }) }