UNPKG

@farmfe/core

Version:

Farm is a extremely fast web build tool written in Rust. Farm can start a project in milliseconds and perform HMR within 10ms, making it much faster than similar tools like webpack and vite.

289 lines 10.9 kB
import http from 'node:http'; import http2 from 'node:http2'; import * as httpsServer from 'node:https'; import Koa from 'koa'; import compression from 'koa-compress'; import path from 'node:path'; import { promisify } from 'node:util'; import { __FARM_GLOBAL__ } from '../config/_global.js'; import { DEFAULT_HMR_OPTIONS, normalizePublicDir } from '../config/index.js'; import { getValidPublicPath, normalizePublicPath } from '../config/normalize-config/normalize-output.js'; import { resolveHostname, resolveServerUrls } from '../utils/http.js'; import { Logger, bootstrap, clearScreen, normalizeBasePath, printServerUrls } from '../utils/index.js'; import { logError } from './error.js'; import { HmrEngine } from './hmr-engine.js'; import { hmrPing } from './middlewares/hmrPing.js'; import { cors, headers, lazyCompilation, proxy, resources, staticMiddleware } from './middlewares/index.js'; import { openBrowser } from './open.js'; import WsServer from './ws.js'; export class Server { constructor({ compiler = null, logger }) { this.restart_promise = null; this.compiler = compiler; this.logger = logger ?? new Logger(); this.initializeKoaServer(); if (!compiler) return; this.publicDir = normalizePublicDir(compiler?.config.config.root); this.publicPath = normalizePublicPath(compiler.config.config.output.targetEnv, compiler.config.config.output.publicPath, logger, false) || '/'; } getCompiler() { return this.compiler; } app() { return this._app; } async listen() { if (!this.server) { this.logger.error('HTTP server is not created yet'); return; } const { port, open, protocol, hostname } = this.config; const start = Date.now(); // compile the project and start the dev server await this.compile(); // watch extra files after compile this.watcher?.watchExtraFiles?.(); bootstrap(Date.now() - start, this.compiler.config); await this.startServer(this.config); !__FARM_GLOBAL__.__FARM_RESTART_DEV_SERVER__ && (await this.displayServerUrls()); if (open) { let publicPath = getValidPublicPath(this.publicPath) || '/'; const serverUrl = `${protocol}://${hostname.name}:${port}${publicPath}`; openBrowser(serverUrl); } } async compile() { try { await this.compiler.compile(); } catch (err) { throw new Error(logError(err)); } if (this.config.writeToDisk) { this.compiler.writeResourcesToDisk(); } else { this.compiler.callWriteResourcesHook(); } } async startServer(serverOptions) { const { port, hostname } = serverOptions; const listen = promisify(this.server.listen).bind(this.server); try { await listen(port, hostname.host); } catch (error) { this.handleServerError(error, port, hostname.host); } } handleServerError(error, port, host) { const errorMap = { EACCES: `Permission denied to use port ${port} `, EADDRNOTAVAIL: `The IP address host: ${host} is not available on this machine.` }; const errorMessage = errorMap[error.code] || `An error occurred: ${error.stack} `; this.logger.error(errorMessage); } async close() { if (!this.server) { this.logger.error('HTTP server is not created yet'); } // the server is already closed if (!this.server.listening) { return; } const promises = []; if (this.ws) { promises.push(this.ws.close()); } if (this.server) { promises.push(new Promise((resolve) => this.server.close(resolve))); } await Promise.all(promises); } async restart(promise) { if (!this.restart_promise) { this.restart_promise = promise(); } return this.restart_promise; } initializeKoaServer() { this._app = new Koa(); } async createServer(options) { const { https, host } = options; const protocol = https ? 'https' : 'http'; const hostname = await resolveHostname(host); const publicPath = getValidPublicPath(this.compiler?.config.config.output?.publicPath ?? options?.output.publicPath); // TODO refactor previewServer If it's preview server, then you can't use create server. we need to create a new one because hmr is false when you preview. const hmrPath = normalizeBasePath(path.join(publicPath, options.hmr.path ?? DEFAULT_HMR_OPTIONS.path)); this.config = { ...options, port: Number(process.env.FARM_DEV_SERVER_PORT || options.port), hmr: { ...options.hmr, path: hmrPath }, protocol, hostname }; const isProxy = Object.keys(options.proxy).length; if (https) { if (isProxy) { this.server = httpsServer.createServer(https, this._app.callback()); } else { this.server = http2.createSecureServer({ maxSessionMemory: 1000, ...https, allowHTTP1: true }, this._app.callback()); } } else { this.server = http.createServer(this._app.callback()); } } createWebSocket() { if (!this.server) { throw new Error('Websocket requires a server.'); } this.ws = new WsServer(this.server, this.config, this.hmrEngine); } invalidateVite() { // Note: path should be Farm's id, which is a relative path in dev mode, // but in vite, it's a url path like /xxx/xxx.js this.ws.on('vite:invalidate', ({ path, message }) => { // find hmr boundary starting from the parent of the file this.logger.info(`HMR invalidate: ${path}. ${message ?? ''} `); const parentFiles = this.compiler.getParentFiles(path); this.hmrEngine.hmrUpdate(parentFiles, true); }); } async createPreviewServer(options) { await this.createServer(options); this.applyPreviewServerMiddlewares(this.config.middlewares); await this.startServer(this.config); await this.displayServerUrls(true); } async createDevServer(options) { if (!this.compiler) { throw new Error('DevServer requires a compiler for development mode.'); } await this.createServer(options); this.hmrEngine = new HmrEngine(this.compiler, this, this.logger); this.createWebSocket(); this.invalidateVite(); this.applyServerMiddlewares(options.middlewares); } static async resolvePortConflict(normalizedDevConfig, logger) { let devPort = normalizedDevConfig.port; let hmrPort = normalizedDevConfig.hmr.port; const { strictPort, host } = normalizedDevConfig; const httpServer = http.createServer(); const isPortAvailable = (portToCheck) => { return new Promise((resolve, reject) => { const onError = async (error) => { if (error.code === 'EADDRINUSE') { clearScreen(); if (strictPort) { httpServer.removeListener('error', onError); reject(new Error(`Port ${devPort} is already in use`)); } else { logger.warn(`Port ${devPort} is in use, trying another one...`); httpServer.removeListener('error', onError); resolve(false); } } else { logger.error(`Error in httpServer: ${error} `); reject(true); } }; httpServer.on('error', onError); httpServer.on('listening', () => { httpServer.close(); resolve(true); }); httpServer.listen(portToCheck, host); }); }; let isPortAvailableResult = await isPortAvailable(devPort); while (isPortAvailableResult === false) { if (typeof normalizedDevConfig.hmr === 'object') { normalizedDevConfig.hmr.port = ++hmrPort; } normalizedDevConfig.port = ++devPort; isPortAvailableResult = await isPortAvailable(devPort); } } /** * Add listening files for root manually * * > listening file with root must as file. * * @param root * @param deps */ addWatchFile(root, deps) { this.getCompiler().addExtraWatchFile(root, deps); } applyMiddlewares(internalMiddlewares) { internalMiddlewares.forEach((middleware) => { const middlewareImpl = middleware(this); if (middlewareImpl) { if (Array.isArray(middlewareImpl)) { middlewareImpl.forEach((m) => { this._app.use(m); }); } else { this._app.use(middlewareImpl); } } }); } setCompiler(compiler) { this.compiler = compiler; } applyPreviewServerMiddlewares(middlewares) { const internalMiddlewares = [ ...(middlewares || []), compression, proxy, staticMiddleware ]; this.applyMiddlewares(internalMiddlewares); } applyServerMiddlewares(middlewares) { const internalMiddlewares = [ ...(middlewares || []), hmrPing, headers, lazyCompilation, cors, resources, proxy ]; this.applyMiddlewares(internalMiddlewares); } async displayServerUrls(showPreviewFlag = false) { let publicPath = getValidPublicPath(this.compiler ? this.compiler.config.config.output?.publicPath : this.config.output.publicPath); this.resolvedUrls = await resolveServerUrls(this.server, this.config, publicPath); if (this.resolvedUrls) { printServerUrls(this.resolvedUrls, this.logger, showPreviewFlag); } else { throw new Error('cannot print server URLs with Server Error.'); } } } //# sourceMappingURL=index.js.map