@web/dev-server-core
Version:
139 lines (122 loc) • 4.43 kB
text/typescript
import Koa from 'koa';
import path from 'path';
import { FSWatcher } from 'chokidar';
import httpServer, { IncomingMessage, ServerResponse } from 'http';
import http2Server from 'http2';
import fs from 'fs';
import net, { Server, Socket, ListenOptions } from 'net';
import { DevServerCoreConfig } from './DevServerCoreConfig.js';
import { createMiddleware } from './createMiddleware.js';
import { Logger } from '../logger/Logger.js';
import { addPlugins } from './addPlugins.js';
/**
* A request handler that returns a 301 HTTP Redirect to the same location as the original
* request but using the https protocol
*/
function httpsRedirect(req: IncomingMessage, res: ServerResponse) {
const { host } = req.headers;
res.writeHead(301, { Location: `https://${host}${req.url}` });
res.end();
}
/**
* Creates a koa server with middlewares, but does not start it. Returns the koa app and
* http server instances.
*/
export function createServer(
logger: Logger,
cfg: DevServerCoreConfig,
fileWatcher: FSWatcher,
middlewareMode = false,
) {
const app = new Koa();
app.silent = true;
app.on('error', error => {
if (['EPIPE', 'ECONNRESET', 'ERR_STREAM_PREMATURE_CLOSE'].includes(error.code)) {
return;
}
console.error('Error while handling server request.');
console.error(error);
});
addPlugins(logger, cfg);
// special case the legacy plugin, if it is given make sure the resolve module imports plugin
// runs before the legacy plugin because it compiles away module syntax. ideally we have a
// generic API for this, but we need to design that a bit more first
const indexOfLegacy = cfg.plugins!.findIndex(p => p.name === 'legacy');
let indexOfResolve = cfg.plugins!.findIndex(p => p.name === 'resolve-module-imports');
if (indexOfLegacy !== -1 && indexOfResolve !== -1) {
const legacy = cfg.plugins!.splice(indexOfLegacy, 1)[0];
// recompute after splicing
indexOfResolve = cfg.plugins!.findIndex(p => p.name === 'resolve-module-imports');
cfg.plugins!.splice(indexOfResolve, 1, cfg.plugins![indexOfResolve], legacy);
}
const middleware = createMiddleware(cfg, logger, fileWatcher);
for (const m of middleware) {
app.use(m);
}
if (middlewareMode) {
return { app };
}
let server: Server;
if (cfg.http2) {
const dir = path.join(__dirname, '..');
const options = {
key: fs.readFileSync(
cfg.sslKey
? path.resolve(cfg.sslKey)
: path.join(dir, '..', '.self-signed-dev-server-ssl.key'),
),
cert: fs.readFileSync(
cfg.sslCert
? path.resolve(cfg.sslCert)
: path.join(dir, '..', '.self-signed-dev-server-ssl.cert'),
),
allowHTTP1: true,
maxSessionMemory: 20,
};
const httpsRedirectServer = httpServer.createServer(httpsRedirect);
server = http2Server.createSecureServer(options, app.callback());
let appServerPort: number;
let httpsRedirectServerPort: number;
/**
* A connection handler that checks if the connection is using TLS
*/
const httpRedirectProxy = (socket: Socket) => {
socket.once('data', buffer => {
// A TLS handshake record starts with byte 22.
const address = buffer[0] === 22 ? appServerPort : httpsRedirectServerPort;
const proxy = (net as any).createConnection(address, () => {
proxy.write(buffer);
socket.pipe(proxy).pipe(socket);
});
});
};
const wrapperServer = net.createServer(httpRedirectProxy);
wrapperServer.addListener('close', () => {
httpsRedirectServer.close();
server.close();
});
wrapperServer.addListener('listening', () => {
const info = server.address();
if (!info || typeof info === 'string') {
return;
}
const { address, port } = info;
appServerPort = port + 1;
httpsRedirectServerPort = port + 2;
server.listen({ address, port: appServerPort });
httpsRedirectServer.listen({ address, port: httpsRedirectServerPort });
});
const serverListen = wrapperServer.listen.bind(wrapperServer);
(wrapperServer as any).listen = (config: ListenOptions, callback: () => void) => {
server.addListener('listening', callback);
serverListen(config);
return server;
};
} else {
server = httpServer.createServer(app.callback());
}
return {
server,
app,
};
}