@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
JavaScript
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