debug-server-next
Version:
Dev server for hippy-core.
1,574 lines (1,304 loc) • 70.5 kB
JavaScript
'use strict';
const os = require('os');
const path = require('path');
const url = require('url');
const util = require('util');
const fs = require('graceful-fs');
const ipaddr = require('ipaddr.js');
const defaultGateway = require('default-gateway');
const express = require('express');
const { validate } = require('schema-utils');
const schema = require('./options.json');
if (!process.env.WEBPACK_SERVE) {
process.env.WEBPACK_SERVE = true;
}
class Server {
constructor(options = {}, compiler) {
// TODO: remove this after plugin support is published
if (options.hooks) {
util.deprecate(
() => {},
'Using \'compiler\' as the first argument is deprecated. Please use \'options\' as the first argument and \'compiler\' as the second argument.',
'DEP_WEBPACK_DEV_SERVER_CONSTRUCTOR',
)();
[options = {}, compiler] = [compiler, options];
}
validate(schema, options, 'webpack Dev Server');
this.options = options;
this.staticWatchers = [];
this.listeners = [];
// Keep track of websocket proxies for external websocket upgrade.
this.webSocketProxies = [];
this.sockets = [];
this.compiler = compiler;
this.currentHash = null;
}
static get DEFAULT_STATS() {
return {
all: false,
hash: true,
warnings: true,
errors: true,
errorDetails: false,
};
}
static isAbsoluteURL(URL) {
// Don't match Windows paths `c:\`
if (/^[a-zA-Z]:\\/.test(URL)) {
return false;
}
// Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
// Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(URL);
}
static findIp(gateway) {
const gatewayIp = ipaddr.parse(gateway);
// Look for the matching interface in all local interfaces.
for (const addresses of Object.values(os.networkInterfaces())) {
for (const { cidr } of addresses) {
const net = ipaddr.parseCIDR(cidr);
if (net[0] && net[0].kind() === gatewayIp.kind() && gatewayIp.match(net)) {
return net[0].toString();
}
}
}
}
static async internalIP(family) {
try {
const { gateway } = await defaultGateway[family]();
return Server.findIp(gateway);
} catch {
// ignore
}
}
static internalIPSync(family) {
try {
const { gateway } = defaultGateway[family].sync();
return Server.findIp(gateway);
} catch {
// ignore
}
}
static async getHostname(hostname) {
if (hostname === 'local-ip') {
return (await Server.internalIP('v4')) || (await Server.internalIP('v6')) || '0.0.0.0';
}
if (hostname === 'local-ipv4') {
return (await Server.internalIP('v4')) || '0.0.0.0';
}
if (hostname === 'local-ipv6') {
return (await Server.internalIP('v6')) || '::';
}
return hostname;
}
static async getFreePort(port) {
if (typeof port !== 'undefined' && port !== null && port !== 'auto') {
return port;
}
const pRetry = require('p-retry');
const portfinder = require('portfinder');
portfinder.basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT || 38988;
// Try to find unused port and listen on it for 3 times,
// if port is not specified in options.
const defaultPortRetry = parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10) || 3;
return pRetry(() => portfinder.getPortPromise(), {
retries: defaultPortRetry,
});
}
static findCacheDir() {
const cwd = process.cwd();
let dir = cwd;
for (;;) {
try {
if (fs.statSync(path.join(dir, 'package.json')).isFile()) break;
// eslint-disable-next-line no-empty
} catch (e) {}
const parent = path.dirname(dir);
if (dir === parent) {
// eslint-disable-next-line no-undefined
dir = undefined;
break;
}
dir = parent;
}
if (!dir) {
return path.resolve(cwd, '.cache/webpack-dev-server');
}
if (process.versions.pnp === '1') {
return path.resolve(dir, '.pnp/.cache/webpack-dev-server');
}
if (process.versions.pnp === '3') {
return path.resolve(dir, '.yarn/.cache/webpack-dev-server');
}
return path.resolve(dir, 'node_modules/.cache/webpack-dev-server');
}
addAdditionalEntries(compiler) {
const additionalEntries = [];
const isWebTarget = compiler.options.externalsPresets
? compiler.options.externalsPresets.web
: [
'web',
'webworker',
'electron-preload',
'electron-renderer',
'node-webkit',
// eslint-disable-next-line no-undefined
undefined,
null,
].includes(compiler.options.target);
// TODO maybe empty empty client
if (this.options.client && isWebTarget) {
let webSocketURL = '';
if (this.options.webSocketServer) {
const searchParams = new URLSearchParams();
/** @type {"ws:" | "wss:" | "http:" | "https:" | "auto:"} */
let protocol;
// We are proxying dev server and need to specify custom `hostname`
if (typeof this.options.client.webSocketURL.protocol !== 'undefined') {
protocol = this.options.client.webSocketURL.protocol;
} else {
protocol = this.options.server.type === 'http' ? 'ws:' : 'wss:';
}
searchParams.set('protocol', protocol);
if (typeof this.options.client.webSocketURL.username !== 'undefined') {
searchParams.set('username', this.options.client.webSocketURL.username);
}
if (typeof this.options.client.webSocketURL.password !== 'undefined') {
searchParams.set('password', this.options.client.webSocketURL.password);
}
/** @type {string} */
let hostname;
// SockJS is not supported server mode, so `hostname` and `port` can't specified, let's ignore them
// TODO show warning about this
const isSockJSType = this.options.webSocketServer.type === 'sockjs';
// We are proxying dev server and need to specify custom `hostname`
if (typeof this.options.client.webSocketURL.hostname !== 'undefined') {
hostname = this.options.client.webSocketURL.hostname;
} else if (typeof this.options.webSocketServer.options.host !== 'undefined' && !isSockJSType) {
// Web socket server works on custom `hostname`,
// only for `ws` because `sock-js` is not support custom `hostname`
hostname = this.options.webSocketServer.options.host;
} else if (typeof this.options.host !== 'undefined') {
// The `host` option is specified
hostname = this.options.host;
} else {
// The `port` option is not specified
hostname = '0.0.0.0';
}
searchParams.set('hostname', hostname);
/** @type {number | string} */
let port;
// We are proxying dev server and need to specify custom `port`
if (typeof this.options.client.webSocketURL.port !== 'undefined') {
port = this.options.client.webSocketURL.port;
} else if (typeof this.options.webSocketServer.options.port !== 'undefined' && !isSockJSType) {
// Web socket server works on custom `port`, only for `ws` because `sock-js` is not support custom `port`
port = this.options.webSocketServer.options.port;
} else if (typeof this.options.port === 'number') {
// The `port` option is specified
port = this.options.port;
} else if (typeof this.options.port === 'string' && this.options.port !== 'auto') {
// The `port` option is specified using `string`
port = Number(this.options.port);
} else {
// The `port` option is not specified or set to `auto`
port = '0';
}
searchParams.set('port', String(port));
/** @type {string} */
let pathname = '';
// We are proxying dev server and need to specify custom `pathname`
if (typeof this.options.client.webSocketURL.pathname !== 'undefined') {
pathname = this.options.client.webSocketURL.pathname;
} else if (
typeof this.options.webSocketServer.options.prefix !== 'undefined'
|| typeof this.options.webSocketServer.options.path !== 'undefined'
) {
// Web socket server works on custom `path`
pathname = this.options.webSocketServer.options.prefix || this.options.webSocketServer.options.path;
}
searchParams.set('pathname', pathname);
if (typeof this.options.client.logging !== 'undefined') {
searchParams.set('logging', this.options.client.logging);
}
if (typeof this.options.client.reconnect !== 'undefined') {
searchParams.set('reconnect', this.options.client.reconnect);
}
webSocketURL = searchParams.toString();
}
additionalEntries.push(`${require.resolve('../client/index.js')}?${webSocketURL}`);
}
if (this.options.hot) {
let hotEntry;
if (this.options.hot === 'only') {
hotEntry = require.resolve('../client/hot/only-dev-server');
} else if (this.options.hot) {
hotEntry = require.resolve('../client/hot/dev-server');
}
additionalEntries.push(hotEntry);
}
const webpack = compiler.webpack || require('webpack');
// use a hook to add entries if available
if (typeof webpack.EntryPlugin !== 'undefined') {
for (const additionalEntry of additionalEntries) {
new webpack.EntryPlugin(compiler.context, additionalEntry, {
// eslint-disable-next-line no-undefined
name: undefined,
}).apply(compiler);
}
} else {
// TODO remove after drop webpack v4 support
/**
* prependEntry Method for webpack 4
* @param {Entry} originalEntry
* @param {Entry} newAdditionalEntries
* @returns {Entry}
*/
const prependEntry = (originalEntry, newAdditionalEntries) => {
if (typeof originalEntry === 'function') {
return () => Promise.resolve(originalEntry()).then(entry => prependEntry(entry, newAdditionalEntries));
}
if (typeof originalEntry === 'object' && !Array.isArray(originalEntry)) {
/** @type {Object<string,string>} */
const clone = {};
Object.keys(originalEntry).forEach((key) => {
// entry[key] should be a string here
const entryDescription = originalEntry[key];
clone[key] = prependEntry(entryDescription, newAdditionalEntries);
});
return clone;
}
// in this case, entry is a string or an array.
// make sure that we do not add duplicates.
/** @type {Entry} */
const entriesClone = additionalEntries.slice(0);
[].concat(originalEntry).forEach((newEntry) => {
if (!entriesClone.includes(newEntry)) {
entriesClone.push(newEntry);
}
});
return entriesClone;
};
compiler.options.entry = prependEntry(compiler.options.entry || './src', additionalEntries);
compiler.hooks.entryOption.call(compiler.options.context, compiler.options.entry);
}
}
getCompilerOptions() {
if (typeof this.compiler.compilers !== 'undefined') {
if (this.compiler.compilers.length === 1) {
return this.compiler.compilers[0].options;
}
// Configuration with the `devServer` options
const compilerWithDevServer = this.compiler.compilers.find(config => config.options.devServer);
if (compilerWithDevServer) {
return compilerWithDevServer.options;
}
// Configuration with `web` preset
const isTarget = config => [
'web',
'webworker',
'electron-preload',
'electron-renderer',
'node-webkit',
// eslint-disable-next-line no-undefined
undefined,
null,
].includes(config.options.target);
const compilerWithWebPreset = this.compiler.compilers
.find(config => (config.options.externalsPresets && config.options.externalsPresets.web) || isTarget(config));
if (compilerWithWebPreset) {
return compilerWithWebPreset.options;
}
// Fallback
return this.compiler.compilers[0].options;
}
return this.compiler.options;
}
async normalizeOptions() {
const { options } = this;
if (!this.logger) {
this.logger = this.compiler.getInfrastructureLogger('webpack-dev-server');
}
const compilerOptions = this.getCompilerOptions();
// TODO remove `{}` after drop webpack v4 support
const compilerWatchOptions = compilerOptions.watchOptions || {};
const getWatchOptions = (watchOptions = {}) => {
const getPolling = () => {
if (typeof watchOptions.usePolling !== 'undefined') {
return watchOptions.usePolling;
}
if (typeof watchOptions.poll !== 'undefined') {
return Boolean(watchOptions.poll);
}
if (typeof compilerWatchOptions.poll !== 'undefined') {
return Boolean(compilerWatchOptions.poll);
}
return false;
};
const getInterval = () => {
if (typeof watchOptions.interval !== 'undefined') {
return watchOptions.interval;
}
if (typeof watchOptions.poll === 'number') {
return watchOptions.poll;
}
if (typeof compilerWatchOptions.poll === 'number') {
return compilerWatchOptions.poll;
}
};
const usePolling = getPolling();
const interval = getInterval();
const { poll, ...rest } = watchOptions;
return {
ignoreInitial: true,
persistent: true,
followSymlinks: false,
atomic: false,
alwaysStat: true,
ignorePermissionErrors: true,
// Respect options from compiler watchOptions
usePolling,
interval,
ignored: watchOptions.ignored,
// TODO: we respect these options for all watch options and allow
// developers to pass them to chokidar, but chokidar doesn't have
// these options maybe we need revisit that in future
...rest,
};
};
const getStaticItem = (optionsForStatic) => {
const getDefaultStaticOptions = () => ({
directory: path.join(process.cwd(), 'public'),
staticOptions: {},
publicPath: ['/'],
serveIndex: { icons: true },
watch: getWatchOptions(),
});
let item;
if (typeof optionsForStatic === 'undefined') {
item = getDefaultStaticOptions();
} else if (typeof optionsForStatic === 'string') {
item = {
...getDefaultStaticOptions(),
directory: optionsForStatic,
};
} else {
const def = getDefaultStaticOptions();
item = {
directory: typeof optionsForStatic.directory !== 'undefined' ? optionsForStatic.directory : def.directory,
// TODO: do merge in the next major release
staticOptions:
typeof optionsForStatic.staticOptions !== 'undefined' ? optionsForStatic.staticOptions : def.staticOptions,
publicPath: typeof optionsForStatic.publicPath !== 'undefined' ? optionsForStatic.publicPath : def.publicPath,
// TODO: do merge in the next major release
serveIndex:
// eslint-disable-next-line no-nested-ternary
typeof optionsForStatic.serveIndex !== 'undefined'
? typeof optionsForStatic.serveIndex === 'boolean' && optionsForStatic.serveIndex
? def.serveIndex
: optionsForStatic.serveIndex
: def.serveIndex,
watch:
// eslint-disable-next-line no-nested-ternary
typeof optionsForStatic.watch !== 'undefined'
? typeof optionsForStatic.watch === 'boolean'
? optionsForStatic.watch
? def.watch
: false
: getWatchOptions(optionsForStatic.watch)
: def.watch,
};
}
if (Server.isAbsoluteURL(item.directory)) {
throw new Error('Using a URL as static.directory is not supported');
}
// ensure that publicPath is an array
if (typeof item.publicPath === 'string') {
item.publicPath = [item.publicPath];
}
return item;
};
if (typeof options.allowedHosts === 'undefined') {
// AllowedHosts allows some default hosts picked from `options.host` or `webSocketURL.hostname` and `localhost`
options.allowedHosts = 'auto';
} else if (
typeof options.allowedHosts === 'string'
&& options.allowedHosts !== 'auto'
&& options.allowedHosts !== 'all'
) {
// We store allowedHosts as array when supplied as string
options.allowedHosts = [options.allowedHosts];
} else if (Array.isArray(options.allowedHosts) && options.allowedHosts.includes('all')) {
// CLI pass options as array, we should normalize them
options.allowedHosts = 'all';
}
if (typeof options.bonjour === 'undefined') {
options.bonjour = false;
} else if (typeof options.bonjour === 'boolean') {
options.bonjour = options.bonjour ? {} : false;
}
if (typeof options.client === 'undefined' || (typeof options.client === 'object' && options.client !== null)) {
if (!options.client) {
options.client = {};
}
if (typeof options.client.webSocketURL === 'undefined') {
options.client.webSocketURL = {};
} else if (typeof options.client.webSocketURL === 'string') {
const parsedURL = new URL(options.client.webSocketURL);
options.client.webSocketURL = {
protocol: parsedURL.protocol,
hostname: parsedURL.hostname,
port: parsedURL.port.length > 0 ? Number(parsedURL.port) : '',
pathname: parsedURL.pathname,
username: parsedURL.username,
password: parsedURL.password,
};
} else if (typeof options.client.webSocketURL.port === 'string') {
options.client.webSocketURL.port = Number(options.client.webSocketURL.port);
}
// Enable client overlay by default
if (typeof options.client.overlay === 'undefined') {
options.client.overlay = true;
} else if (typeof options.client.overlay !== 'boolean') {
options.client.overlay = {
errors: true,
warnings: true,
...options.client.overlay,
};
}
if (typeof options.client.reconnect === 'undefined') {
options.client.reconnect = 10;
} else if (options.client.reconnect === true) {
options.client.reconnect = Infinity;
} else if (options.client.reconnect === false) {
options.client.reconnect = 0;
}
// Respect infrastructureLogging.level
if (typeof options.client.logging === 'undefined') {
options.client.logging = compilerOptions.infrastructureLogging
? compilerOptions.infrastructureLogging.level
: 'info';
}
}
if (typeof options.compress === 'undefined') {
options.compress = true;
}
if (typeof options.devMiddleware === 'undefined') {
options.devMiddleware = {};
}
// No need to normalize `headers`
if (typeof options.historyApiFallback === 'undefined') {
options.historyApiFallback = false;
} else if (typeof options.historyApiFallback === 'boolean' && options.historyApiFallback) {
options.historyApiFallback = {};
}
// No need to normalize `host`
options.hot = typeof options.hot === 'boolean' || options.hot === 'only' ? options.hot : true;
const isHTTPs = Boolean(options.https);
const isSPDY = Boolean(options.http2);
if (isHTTPs || isSPDY) {
// TODO: remove in the next major release
util.deprecate(
() => {},
`'${isHTTPs ? 'https' : 'http2'}' option is deprecated. Please use the 'server' option.`,
`DEP_WEBPACK_DEV_SERVER_${isHTTPs ? 'HTTPS' : 'HTTP2'}`,
)();
}
options.server = {
type:
// eslint-disable-next-line no-nested-ternary
typeof options.server === 'string'
? options.server
: typeof (options.server || {}).type === 'string'
? options.server.type
: isSPDY
? 'spdy'
: isHTTPs
? 'https'
: 'http',
options: {
...options.https,
...(options.server || {}).options,
},
};
if (options.server.type === 'spdy' && typeof options.server.options.spdy === 'undefined') {
options.server.options.spdy = {
protocols: ['h2', 'http/1.1'],
};
}
if (options.server.type === 'https' || options.server.type === 'spdy') {
if (typeof options.server.options.requestCert === 'undefined') {
options.server.options.requestCert = false;
}
// TODO remove the `cacert` option in favor `ca` in the next major release
for (const property of ['cacert', 'ca', 'cert', 'crl', 'key', 'pfx']) {
if (typeof options.server.options[property] === 'undefined') {
// eslint-disable-next-line no-continue
continue;
}
const value = options.server.options[property];
const readFile = (item) => {
if (Buffer.isBuffer(item) || (typeof item === 'object' && item !== null && !Array.isArray(item))) {
return item;
}
if (item) {
let stats = null;
try {
stats = fs.lstatSync(fs.realpathSync(item)).isFile();
} catch (error) {
// Ignore error
}
// It is file
return stats ? fs.readFileSync(item) : item;
}
};
options.server.options[property] = Array.isArray(value) ? value.map(item => readFile(item)) : readFile(value);
}
let fakeCert;
if (!options.server.options.key || !options.server.options.cert) {
const certificateDir = Server.findCacheDir();
const certificatePath = path.join(certificateDir, 'server.pem');
let certificateExists;
try {
const certificate = await fs.promises.stat(certificatePath);
certificateExists = certificate.isFile();
} catch {
certificateExists = false;
}
if (certificateExists) {
const certificateTtl = 1000 * 60 * 60 * 24;
const certificateStat = await fs.promises.stat(certificatePath);
const now = new Date();
// cert is more than 30 days old, kill it with fire
if ((now - certificateStat.ctime) / certificateTtl > 30) {
const del = require('del');
this.logger.info('SSL certificate is more than 30 days old. Removing...');
await del([certificatePath], { force: true });
certificateExists = false;
}
}
if (!certificateExists) {
this.logger.info('Generating SSL certificate...');
const selfsigned = require('selfsigned');
const attributes = [{ name: 'commonName', value: 'localhost' }];
const pems = selfsigned.generate(attributes, {
algorithm: 'sha256',
days: 30,
keySize: 2048,
extensions: [
{
name: 'basicConstraints',
cA: true,
},
{
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true,
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
timeStamping: true,
},
{
name: 'subjectAltName',
altNames: [
{
// type 2 is DNS
type: 2,
value: 'localhost',
},
{
type: 2,
value: 'localhost.localdomain',
},
{
type: 2,
value: 'lvh.me',
},
{
type: 2,
value: '*.lvh.me',
},
{
type: 2,
value: '[::1]',
},
{
// type 7 is IP
type: 7,
ip: '127.0.0.1',
},
{
type: 7,
ip: 'fe80::1',
},
],
},
],
});
await fs.promises.mkdir(certificateDir, { recursive: true });
await fs.promises.writeFile(certificatePath, pems.private + pems.cert, {
encoding: 'utf8',
});
}
fakeCert = await fs.promises.readFile(certificatePath);
this.logger.info(`SSL certificate: ${certificatePath}`);
}
if (options.server.options.cacert) {
if (options.server.options.ca) {
this.logger.warn('Do not specify \'ca\' and \'cacert\' options together, the \'ca\' option will be used.');
} else {
options.server.options.ca = options.server.options.cacert;
}
delete options.server.options.cacert;
}
options.server.options.key = options.server.options.key || fakeCert;
options.server.options.cert = options.server.options.cert || fakeCert;
}
if (typeof options.ipc === 'boolean') {
const isWindows = process.platform === 'win32';
const pipePrefix = isWindows ? '\\\\.\\pipe\\' : os.tmpdir();
const pipeName = 'webpack-dev-server.sock';
options.ipc = path.join(pipePrefix, pipeName);
}
options.liveReload = typeof options.liveReload !== 'undefined' ? options.liveReload : true;
options.magicHtml = typeof options.magicHtml !== 'undefined' ? options.magicHtml : true;
// https://github.com/webpack/webpack-dev-server/issues/1990
const defaultOpenOptions = { wait: false };
const getOpenItemsFromObject = ({ target, ...rest }) => {
const normalizedOptions = { ...defaultOpenOptions, ...rest };
if (typeof normalizedOptions.app === 'string') {
normalizedOptions.app = {
name: normalizedOptions.app,
};
}
const normalizedTarget = typeof target === 'undefined' ? '<url>' : target;
if (Array.isArray(normalizedTarget)) {
return normalizedTarget.map(singleTarget => ({ target: singleTarget, options: normalizedOptions }));
}
return [{ target: normalizedTarget, options: normalizedOptions }];
};
if (typeof options.open === 'undefined') {
options.open = [];
} else if (typeof options.open === 'boolean') {
options.open = options.open ? [{ target: '<url>', options: defaultOpenOptions }] : [];
} else if (typeof options.open === 'string') {
options.open = [{ target: options.open, options: defaultOpenOptions }];
} else if (Array.isArray(options.open)) {
const result = [];
options.open.forEach((item) => {
if (typeof item === 'string') {
result.push({ target: item, options: defaultOpenOptions });
return;
}
result.push(...getOpenItemsFromObject(item));
});
options.open = result;
} else {
options.open = [...getOpenItemsFromObject(options.open)];
}
if (typeof options.port === 'string' && options.port !== 'auto') {
options.port = Number(options.port);
}
/**
* Assume a proxy configuration specified as:
* proxy: {
* 'context': { options }
* }
* OR
* proxy: {
* 'context': 'target'
* }
*/
if (typeof options.proxy !== 'undefined') {
// TODO remove in the next major release, only accept `Array`
if (!Array.isArray(options.proxy)) {
if (
Object.prototype.hasOwnProperty.call(options.proxy, 'target')
|| Object.prototype.hasOwnProperty.call(options.proxy, 'router')
) {
options.proxy = [options.proxy];
} else {
options.proxy = Object.keys(options.proxy).map((context) => {
let proxyOptions;
// For backwards compatibility reasons.
const correctedContext = context.replace(/^\*$/, '**').replace(/\/\*$/, '');
if (typeof options.proxy[context] === 'string') {
proxyOptions = {
context: correctedContext,
target: options.proxy[context],
};
} else {
proxyOptions = { ...options.proxy[context] };
proxyOptions.context = correctedContext;
}
return proxyOptions;
});
}
}
options.proxy = options.proxy.map((item) => {
const getLogLevelForProxy = (level) => {
if (level === 'none') {
return 'silent';
}
if (level === 'log') {
return 'info';
}
if (level === 'verbose') {
return 'debug';
}
return level;
};
if (typeof item.logLevel === 'undefined') {
item.logLevel = getLogLevelForProxy(compilerOptions.infrastructureLogging ? compilerOptions.infrastructureLogging.level : 'info');
}
if (typeof item.logProvider === 'undefined') {
item.logProvider = () => this.logger;
}
return item;
});
}
if (typeof options.setupExitSignals === 'undefined') {
options.setupExitSignals = true;
}
if (typeof options.static === 'undefined') {
options.static = [getStaticItem()];
} else if (typeof options.static === 'boolean') {
options.static = options.static ? [getStaticItem()] : false;
} else if (typeof options.static === 'string') {
options.static = [getStaticItem(options.static)];
} else if (Array.isArray(options.static)) {
options.static = options.static.map((item) => {
if (typeof item === 'string') {
return getStaticItem(item);
}
return getStaticItem(item);
});
} else {
options.static = [getStaticItem(options.static)];
}
if (typeof options.watchFiles === 'string') {
options.watchFiles = [{ paths: options.watchFiles, options: getWatchOptions() }];
} else if (
typeof options.watchFiles === 'object'
&& options.watchFiles !== null
&& !Array.isArray(options.watchFiles)
) {
options.watchFiles = [
{
paths: options.watchFiles.paths,
options: getWatchOptions(options.watchFiles.options || {}),
},
];
} else if (Array.isArray(options.watchFiles)) {
options.watchFiles = options.watchFiles.map((item) => {
if (typeof item === 'string') {
return { paths: item, options: getWatchOptions() };
}
return {
paths: item.paths,
options: getWatchOptions(item.options || {}),
};
});
} else {
options.watchFiles = [];
}
const defaultWebSocketServerType = 'ws';
const defaultWebSocketServerOptions = { path: '/ws' };
if (typeof options.webSocketServer === 'undefined') {
options.webSocketServer = {
type: defaultWebSocketServerType,
options: defaultWebSocketServerOptions,
};
} else if (typeof options.webSocketServer === 'boolean' && !options.webSocketServer) {
options.webSocketServer = false;
} else if (typeof options.webSocketServer === 'string' || typeof options.webSocketServer === 'function') {
options.webSocketServer = {
type: options.webSocketServer,
options: defaultWebSocketServerOptions,
};
} else {
options.webSocketServer = {
type: options.webSocketServer.type || defaultWebSocketServerType,
options: {
...defaultWebSocketServerOptions,
...options.webSocketServer.options,
},
};
if (typeof options.webSocketServer.options.port === 'string') {
options.webSocketServer.options.port = Number(options.webSocketServer.options.port);
}
}
}
getClientTransport() {
let ClientImplementation;
let clientImplementationFound = true;
const isKnownWebSocketServerImplementation = this.options.webSocketServer
&& typeof this.options.webSocketServer.type === 'string'
&& (this.options.webSocketServer.type === 'ws' || this.options.webSocketServer.type === 'sockjs');
let clientTransport;
if (this.options.client) {
if (typeof this.options.client.webSocketTransport !== 'undefined') {
clientTransport = this.options.client.webSocketTransport;
} else if (isKnownWebSocketServerImplementation) {
clientTransport = this.options.webSocketServer.type;
} else {
clientTransport = 'ws';
}
} else {
clientTransport = 'ws';
}
switch (typeof clientTransport) {
case 'string':
// could be 'sockjs', 'ws', or a path that should be required
if (clientTransport === 'sockjs') {
ClientImplementation = require.resolve('../client/clients/SockJSClient');
} else if (clientTransport === 'ws') {
ClientImplementation = require.resolve('../client/clients/WebSocketClient');
} else {
try {
// eslint-disable-next-line import/no-dynamic-require
ClientImplementation = require.resolve(clientTransport);
} catch (e) {
clientImplementationFound = false;
}
}
break;
default:
clientImplementationFound = false;
}
if (!clientImplementationFound) {
throw new Error(`${
!isKnownWebSocketServerImplementation
? 'When you use custom web socket implementation you must explicitly specify client.webSocketTransport. '
: ''
}client.webSocketTransport must be a string denoting a default implementation (e.g. 'sockjs', 'ws') or a full path to a JS file via require.resolve(...) which exports a class `);
}
return ClientImplementation;
}
getServerTransport() {
let implementation;
let implementationFound = true;
switch (typeof this.options.webSocketServer.type) {
case 'string':
// Could be 'sockjs', in the future 'ws', or a path that should be required
if (this.options.webSocketServer.type === 'sockjs') {
implementation = require('./servers/SockJSServer');
} else if (this.options.webSocketServer.type === 'ws') {
implementation = require('./servers/WebsocketServer');
} else {
try {
// eslint-disable-next-line import/no-dynamic-require
implementation = require(this.options.webSocketServer.type);
} catch (error) {
implementationFound = false;
}
}
break;
case 'function':
implementation = this.options.webSocketServer.type;
break;
default:
implementationFound = false;
}
if (!implementationFound) {
throw new Error('webSocketServer (webSocketServer.type) must be a string denoting a default implementation (e.g. \'ws\', \'sockjs\'), a full path to '
+ 'a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer.js) '
+ 'via require.resolve(...), or the class itself which extends BaseServer');
}
return implementation;
}
setupProgressPlugin() {
const { ProgressPlugin } = this.compiler.webpack || require('webpack');
new ProgressPlugin((percent, msg, addInfo, pluginName) => {
percent = Math.floor(percent * 100);
if (percent === 100) {
msg = 'Compilation completed';
}
if (addInfo) {
msg = `${msg} (${addInfo})`;
}
if (this.webSocketServer) {
this.sendMessage(this.webSocketServer.clients, 'progress-update', {
percent,
msg,
pluginName,
});
}
if (this.server) {
this.server.emit('progress-update', { percent, msg, pluginName });
}
}).apply(this.compiler);
}
async initialize() {
if (this.options.webSocketServer) {
const compilers = this.compiler.compilers || [this.compiler];
compilers.forEach((compiler) => {
this.addAdditionalEntries(compiler);
const webpack = compiler.webpack || require('webpack');
new webpack.ProvidePlugin({
__webpack_dev_server_client__: this.getClientTransport(),
}).apply(compiler);
// TODO remove after drop webpack v4 support
compiler.options.plugins = compiler.options.plugins || [];
if (this.options.hot) {
const HMRPluginExists = compiler.options.plugins
.find(p => p.constructor === webpack.HotModuleReplacementPlugin);
if (HMRPluginExists) {
this.logger.warn('"hot: true" automatically applies HMR plugin, you don\'t have to add it manually to your webpack configuration.');
} else {
// Apply the HMR plugin
const plugin = new webpack.HotModuleReplacementPlugin();
plugin.apply(compiler);
}
}
});
if (this.options.client && this.options.client.progress) {
this.setupProgressPlugin();
}
}
this.setupHooks();
this.setupApp();
this.setupHostHeaderCheck();
this.setupDevMiddleware();
// Should be after `webpack-dev-middleware`, otherwise other middlewares might rewrite response
this.setupBuiltInRoutes();
this.setupWatchFiles();
this.setupFeatures();
this.createServer();
if (this.options.setupExitSignals) {
const signals = ['SIGINT', 'SIGTERM'];
let needForceShutdown = false;
signals.forEach((signal) => {
const listener = () => {
if (needForceShutdown) {
// eslint-disable-next-line no-process-exit
process.exit();
}
this.logger.info('Gracefully shutting down. To force exit, press ^C again. Please wait...');
needForceShutdown = true;
this.stopCallback(() => {
if (typeof this.compiler.close === 'function') {
this.compiler.close(() => {
// eslint-disable-next-line no-process-exit
process.exit();
});
} else {
// eslint-disable-next-line no-process-exit
process.exit();
}
});
};
this.listeners.push({ name: signal, listener });
process.on(signal, listener);
});
}
// Proxy WebSocket without the initial http request
// https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
this.webSocketProxies.forEach((webSocketProxy) => {
this.server.on('upgrade', webSocketProxy.upgrade);
}, this);
}
setupApp() {
// Init express server
// eslint-disable-next-line new-cap
this.app = new express();
}
getStats(statsObj) {
const stats = Server.DEFAULT_STATS;
const compilerOptions = this.getCompilerOptions();
if (compilerOptions.stats && compilerOptions.stats.warningsFilter) {
stats.warningsFilter = compilerOptions.stats.warningsFilter;
}
return statsObj.toJson(stats);
}
setupHooks() {
this.compiler.hooks.invalid.tap('webpack-dev-server', () => {
if (this.webSocketServer) {
this.sendMessage(this.webSocketServer.clients, 'invalid');
}
});
this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {
if (this.webSocketServer) {
this.sendStats(this.webSocketServer.clients, this.getStats(stats));
}
this.stats = stats;
});
}
setupHostHeaderCheck() {
this.app.all('*', (req, res, next) => {
if (this.checkHeader(req.headers, 'host')) {
return next();
}
res.send('Invalid Host header');
});
}
setupDevMiddleware() {
const webpackDevMiddleware = require('webpack-dev-middleware');
// middleware for serving webpack bundle
this.middleware = webpackDevMiddleware(this.compiler, this.options.devMiddleware);
}
setupBuiltInRoutes() {
const { app, middleware } = this;
app.get('/__webpack_dev_server__/sockjs.bundle.js', (req, res) => {
res.setHeader('Content-Type', 'application/javascript');
const { createReadStream } = fs;
const clientPath = path.join(__dirname, '..', 'client');
createReadStream(path.join(clientPath, 'modules/sockjs-client/index.js')).pipe(res);
});
app.get('/webpack-dev-server/invalidate', (_req, res) => {
this.invalidate();
res.end();
});
app.get('/webpack-dev-server', (req, res) => {
middleware.waitUntilValid((stats) => {
res.setHeader('Content-Type', 'text/html');
res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>');
const statsForPrint = typeof stats.stats !== 'undefined' ? stats.toJson().children : [stats.toJson()];
res.write('<h1>Assets Report:</h1>');
statsForPrint.forEach((item, index) => {
res.write('<div>');
const name = item.name || (stats.stats ? `unnamed[${index}]` : 'unnamed');
res.write(`<h2>Compilation: ${name}</h2>`);
res.write('<ul>');
const publicPath = item.publicPath === 'auto' ? '' : item.publicPath;
for (const asset of item.assets) {
const assetName = asset.name;
const assetURL = `${publicPath}${assetName}`;
res.write(`<li>
<strong><a href="${assetURL}" target="_blank">${assetName}</a></strong>
</li>`);
}
res.write('</ul>');
res.write('</div>');
});
res.end('</body></html>');
});
});
}
setupCompressFeature() {
const compress = require('compression');
this.app.use(compress());
}
setupProxyFeature() {
const { createProxyMiddleware } = require('http-proxy-middleware');
const getProxyMiddleware = (proxyConfig) => {
// It is possible to use the `bypass` method without a `target` or `router`.
// However, the proxy middleware has no use in this case, and will fail to instantiate.
if (proxyConfig.target) {
const context = proxyConfig.context || proxyConfig.path;
return createProxyMiddleware(context, proxyConfig);
}
if (proxyConfig.router) {
return createProxyMiddleware(proxyConfig);
}
};
/**
* Assume a proxy configuration specified as:
* proxy: [
* {
* context: "value",
* ...options,
* },
* // or:
* function() {
* return {
* context: "context",
* ...options,
* };
* }
* ]
*/
this.options.proxy.forEach((proxyConfigOrCallback) => {
let proxyMiddleware;
let proxyConfig = typeof proxyConfigOrCallback === 'function' ? proxyConfigOrCallback() : proxyConfigOrCallback;
proxyMiddleware = getProxyMiddleware(proxyConfig);
if (proxyConfig.ws) {
this.webSocketProxies.push(proxyMiddleware);
}
const handle = async (req, res, next) => {
if (typeof proxyConfigOrCallback === 'function') {
const newProxyConfig = proxyConfigOrCallback(req, res, next);
if (newProxyConfig !== proxyConfig) {
proxyConfig = newProxyConfig;
proxyMiddleware = getProxyMiddleware(proxyConfig);
}
}
// - Check if we have a bypass function defined
// - In case the bypass function is defined we'll retrieve the
// bypassUrl from it otherwise bypassUrl would be null
// TODO remove in the next major in favor `context` and `router` options
const isByPassFuncDefined = typeof proxyConfig.bypass === 'function';
const bypassUrl = isByPassFuncDefined ? await proxyConfig.bypass(req, res, proxyConfig) : null;
if (typeof bypassUrl === 'boolean') {
// skip the proxy
req.url = null;
next();
} else if (typeof bypassUrl === 'string') {
// byPass to that url
req.url = bypassUrl;
next();
} else if (proxyMiddleware) {
return proxyMiddleware(req, res, next);
} else {
next();
}
};
this.app.use(handle);
// Also forward error requests to the proxy so it can handle them.
this.app.use((error, req, res, next) => handle(req, res, next));
});
}
setupHistoryApiFallbackFeature() {
const { historyApiFallback } = this.options;
if (typeof historyApiFallback.logger === 'undefined' && !historyApiFallback.verbose) {
historyApiFallback.logger = this.logger.log.bind(this.logger, '[connect-history-api-fallback]');
}
// Fall back to /index.html if nothing else matches.
this.app.use(require('connect-history-api-fallback')(historyApiFallback));
}
setupStaticFeature() {
this.options.static.forEach((staticOption) => {
staticOption.publicPath.forEach((publicPath) => {
this.app.use(publicPath, express.static(staticOption.directory, staticOption.staticOptions));
});
});
}
setupStaticServeIndexFeature() {
const serveIndex = require('serve-index');
this.options.static.forEach((staticOption) => {
staticOption.publicPath.forEach((publicPath) => {
if (staticOption.serveIndex) {
this.app.use(publicPath, (req, res, next) => {
// serve-index doesn't fallthrough non-get/head request to next middleware
if (req.method !== 'GET' && req.method !== 'HEAD') {
return next();
}
serveIndex(staticOption.directory, staticOption.serveIndex)(req, res, next);
});
}
});
});
}
setupStaticWatchFeature() {
this.options.static.forEach((staticOption) => {
if (staticOption.watch) {
this.watchFiles(staticOption.directory, staticOption.watch);
}
});
}
setupOnBeforeSetupMiddlewareFeature() {
this.options.onBeforeSetupMiddleware(this);
}
setupWatchFiles() {
const { watchFiles } = this.options;
if (watchFiles.length > 0) {
watchFiles.forEach((item) => {
this.watchFiles(item.paths, item.options);
});
}
}
setupMiddleware() {
this.app.use(this.middleware);
}
setupOnAfterSetupMiddlewareFeature() {
this.options.onAfterSetupMiddleware(this);
}
setupHeadersFeature() {
this.app.all('*', this.setHeaders.bind(this));
}
setupMagicHtmlFeature() {
this.app.get('*', this.serveMagicHtml.bind(this));
}
setupFeatures() {
const features = {
compress: () => {
if (this.options.compress) {
this.setupCompressFeature();
}
},
proxy: () => {
if (this.options.proxy) {
this.setupProxyFeature();
}
},
historyApiFallback: () => {
if (this.options.historyApiFallback) {
this.setupHistoryApiFallbackFeature();
}
},
static: () => {
this.setupStaticFeature();
},
staticServeIndex: () => {
this.setupStaticServeIndexFeature();
},
staticWatch: () => {
this.setupStaticWatchFeature();
},
onBeforeSetupMiddleware: () => {
if (typeof this.options.onBeforeSetupMiddleware === 'function') {
this.setupOnBeforeSetupMiddlewareFeature();
}
},
onAfterSetupMiddleware: () => {
if (typeof this.options.onAfterSetupMiddleware === 'function') {
this.setupOnAfterSetupMiddlewareFeature();
}
},
middleware: () => {
// include our middleware to ensure
// it is able to handle '/index.html' request after redirect
this.setupMiddleware();
},
headers: () => {
this.setupHeadersFeature();
},
magicHtml: () => {
this.setupMagicHtmlFeature();
},
};
const runnableFeatures = [];
// compress is placed last and uses unshift so that it will be the first middleware used
if (this.options.compress) {
runnableFeatures.push('compress');
}
if (this.options.onBeforeSetupMiddleware) {
runnableFeatures.push('onBeforeSetupMiddleware');
}
runnableFeatures.push('headers', 'middleware');
if (this.options.proxy) {
runnableFeatures.push('proxy', 'middleware');
}
if (this.options.static) {
runnableFeatures.push('static');
}
if (this.options.historyApiFallback) {
runnableFeatures.push('historyApiFallback', 'middleware');
if (this.options.static) {
runnableFeatures.push('static');
}
}
if (this.options.static) {
runnableFeatures.push('staticServeIndex', 'staticWatch');
}
if (this.options.magicHtml) {
runnableFeatures.push('magicHtml');
}
if (this.options.onAfterSetupMiddleware) {
runnableFeatures.push('onAfterSetupMiddleware');
}
runnableFeatures.forEach((feature) => {
features[feature]();
});
}
createServer() {
// eslint-disable-next-line import/no-dynamic-require
this.server = require(this.options.server.type).createServer(this.options.server.options, this.app);
this.server.on('connection',