From 960b42068579dddc2c216720a7f16d8d189e8225 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 13 Jan 2021 17:49:57 -0500 Subject: [PATCH] feat: support using vite as a middleware --- docs/guide/api-javascript.md | 28 ++++++++ .../vite/src/node/plugins/clientInjections.ts | 11 ++- packages/vite/src/node/server/index.ts | 71 +++++++++++-------- .../vite/src/node/server/middlewares/proxy.ts | 30 ++++---- packages/vite/src/node/server/ws.ts | 35 +++++---- 5 files changed, 120 insertions(+), 55 deletions(-) diff --git a/docs/guide/api-javascript.md b/docs/guide/api-javascript.md index 08efcef52b96fc..966cbdc4545046 100644 --- a/docs/guide/api-javascript.md +++ b/docs/guide/api-javascript.md @@ -28,6 +28,34 @@ const { createServer } = require('vite') })() ``` +### Using the Vite Server as a Middleware + +Vite can be used as a middleware in an existing raw Node.js http server or frameworks that are comaptible with the `(req, res, next) => {}` style middlewares. For example with `express`: + +```js +const vite = require('vite') +const express = require('express') + +;(async () => { + const app = express() + + // create vite dev server in middelware mode + // so vite creates the hmr websocket server on its own. + // the ws server will be listening at port 24678 by default, and can be + // configured via server.hmr.port + const viteServer = await vite.createServer({ + server: { + middlewareMode: true + } + }) + + // use vite's connect instance as middleware + app.use(viteServer.app) + + app.listen(3000) +})() +``` + ## `InlineConfig` The `InlineConfig` interface extends `UserConfig` with additional properties: diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 7d4e65df4d7654..d4913fe770d14c 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -21,7 +21,16 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { const protocol = options.protocol || null const timeout = options.timeout || 30000 const overlay = options.overlay !== false - let port = String(options.port || config.server.port!) + let port + if (config.server.middlewareMode) { + port = String( + typeof config.server.hmr === 'object' + ? config.server.hmr.port + : 24678 + ) + } else { + port = String(options.port || config.server.port!) + } if (options.path) { port = `${port}/${options.path}` } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 5d834ce1876736..9ea3e922e2388c 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -40,7 +40,6 @@ import { EsbuildTransformResult } from '../plugins/esbuild' import { TransformOptions as EsbuildTransformOptions } from 'esbuild' -import { createLogger } from '../logger' import { DepOptimizationMetadata, optimizeDeps } from '../optimizer' export interface ServerOptions { @@ -102,6 +101,10 @@ export interface ServerOptions { * If enabled, vite will exit if specified port is already in use */ strictPort?: boolean + /** + * Create Vite dev server to be used as a middleware in an existing server + */ + middlewareMode?: boolean } /** @@ -139,8 +142,9 @@ export interface ViteDevServer { app: Connect.Server /** * native Node http server instance + * will be null in middleware mode */ - httpServer: http.Server + httpServer: http.Server | null /** * chokidar watcher instance * https://github.com/paulmillr/chokidar#api @@ -193,12 +197,13 @@ export async function createServer( ): Promise { const config = await resolveConfig(inlineConfig, 'serve', 'development') const root = config.root - const logger = createLogger(config.logLevel) const serverConfig = config.server || {} const app = connect() as Connect.Server - const httpServer = await resolveHttpServer(serverConfig, app) - const ws = createWebSocketServer(httpServer, logger) + const httpServer = serverConfig.middlewareMode + ? null + : await resolveHttpServer(serverConfig, app) + const ws = createWebSocketServer(httpServer, config) const watchOptions = serverConfig.watch || {} const watcher = chokidar.watch(root, { @@ -339,30 +344,32 @@ export async function createServer( // error handler app.use(errorMiddleware(server)) - // overwrite listen to run optimizer before server start - const listen = httpServer.listen.bind(httpServer) - httpServer.listen = (async (port: number, ...args: any[]) => { - await container.buildStart({}) - - if (config.optimizeCacheDir) { - // run optimizer - await optimizeDeps(config) - // after optimization, read updated optimization metadata - const dataPath = path.resolve(config.optimizeCacheDir, 'metadata.json') - if (fs.existsSync(dataPath)) { - server.optimizeDepsMetadata = JSON.parse( - fs.readFileSync(dataPath, 'utf-8') - ) + if (httpServer) { + // overwrite listen to run optimizer before server start + const listen = httpServer.listen.bind(httpServer) + httpServer.listen = (async (port: number, ...args: any[]) => { + await container.buildStart({}) + + if (config.optimizeCacheDir) { + // run optimizer + await optimizeDeps(config) + // after optimization, read updated optimization metadata + const dataPath = path.resolve(config.optimizeCacheDir, 'metadata.json') + if (fs.existsSync(dataPath)) { + server.optimizeDepsMetadata = JSON.parse( + fs.readFileSync(dataPath, 'utf-8') + ) + } } - } - return listen(port, ...args) - }) as any + return listen(port, ...args) + }) as any - httpServer.once('listening', () => { - // update actual port since this may be different from initial value - serverConfig.port = (httpServer.address() as AddressInfo).port - }) + httpServer.once('listening', () => { + // update actual port since this may be different from initial value + serverConfig.port = (httpServer.address() as AddressInfo).port + }) + } return server } @@ -396,11 +403,15 @@ async function startServer( server: ViteDevServer, inlinePort?: number ): Promise { + const httpServer = server.httpServer + if (!httpServer) { + throw new Error('Cannot call server.listen in middleware mode.') + } + const options = server.config.server || {} let port = inlinePort || options.port || 3000 let hostname = options.host || 'localhost' const protocol = options.https ? 'https' : 'http' - const httpServer = server.httpServer const info = server.config.logger.info return new Promise((resolve, reject) => { @@ -485,7 +496,11 @@ async function startServer( }) } -function createSeverCloseFn(server: http.Server) { +function createSeverCloseFn(server: http.Server | null) { + if (!server) { + return () => {} + } + const openSockets = new Set() server.on('connection', (socket) => { diff --git a/packages/vite/src/node/server/middlewares/proxy.ts b/packages/vite/src/node/server/middlewares/proxy.ts index 59750b35f84eed..d414e24d462efd 100644 --- a/packages/vite/src/node/server/middlewares/proxy.ts +++ b/packages/vite/src/node/server/middlewares/proxy.ts @@ -57,23 +57,25 @@ export function proxyMiddleware({ proxies[context] = [proxy, { ...opts }] }) - httpServer.on('upgrade', (req, socket, head) => { - const url = req.url! - for (const context in proxies) { - if (url.startsWith(context)) { - const [proxy, opts] = proxies[context] - if ( - (opts.ws || opts.target?.toString().startsWith('ws:')) && - req.headers['sec-websocket-protocol'] !== HMR_HEADER - ) { - if (opts.rewrite) { - req.url = opts.rewrite(url) + if (httpServer) { + httpServer.on('upgrade', (req, socket, head) => { + const url = req.url! + for (const context in proxies) { + if (url.startsWith(context)) { + const [proxy, opts] = proxies[context] + if ( + (opts.ws || opts.target?.toString().startsWith('ws:')) && + req.headers['sec-websocket-protocol'] !== HMR_HEADER + ) { + if (opts.rewrite) { + req.url = opts.rewrite(url) + } + proxy.ws(req, socket, head) } - proxy.ws(req, socket, head) } } - } - }) + }) + } return (req, res, next) => { const url = req.url! diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index e5df69a4b66123..739771f750a135 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -2,7 +2,7 @@ import chalk from 'chalk' import { Server } from 'http' import WebSocket from 'ws' import { ErrorPayload, HMRPayload } from 'types/hmrPayload' -import { Logger } from '../logger' +import { ResolvedConfig } from '..' export const HMR_HEADER = 'vite-hmr' @@ -12,18 +12,29 @@ export interface WebSocketServer { } export function createWebSocketServer( - server: Server, - logger: Logger + server: Server | null, + config: ResolvedConfig ): WebSocketServer { - const wss = new WebSocket.Server({ noServer: true }) + const wss = new WebSocket.Server( + server + ? { noServer: true } + : { + port: + typeof config.server.hmr === 'object' + ? config.server.hmr.port + : 24678 + } + ) - server.on('upgrade', (req, socket, head) => { - if (req.headers['sec-websocket-protocol'] === HMR_HEADER) { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit('connection', ws, req) - }) - } - }) + if (server) { + server.on('upgrade', (req, socket, head) => { + if (req.headers['sec-websocket-protocol'] === HMR_HEADER) { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, req) + }) + } + }) + } wss.on('connection', (socket) => { socket.send(JSON.stringify({ type: 'connected' })) @@ -35,7 +46,7 @@ export function createWebSocketServer( wss.on('error', (e: Error & { code: string }) => { if (e.code !== 'EADDRINUSE') { - logger.error( + config.logger.error( chalk.red(`WebSocket server error:\n${e.stack || e.message}`) ) }