Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 225 additions & 59 deletions lib/Server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const path = require('path');
const fs = require('fs');
const url = require('url');
const ipaddr = require('ipaddr.js');
const internalIp = require('internal-ip');
Expand All @@ -11,9 +13,7 @@ const colors = require('./utils/colors');
const routes = require('./utils/routes');
const getSocketServerImplementation = require('./utils/getSocketServerImplementation');
const getCompilerConfigArray = require('./utils/getCompilerConfigArray');
const setupExitSignals = require('./utils/setupExitSignals');
const getStatsOption = require('./utils/getStatsOption');
const getColorsOption = require('./utils/getColorsOption');
const schema = require('./options.json');

if (!process.env.WEBPACK_SERVE) {
Expand All @@ -39,7 +39,12 @@ class Server {
// this value of web socket can be overwritten for tests
this.webSocketHeartbeatInterval = 30000;

normalizeOptions(this.compiler, this.options, this.logger);
normalizeOptions(
this.compiler,
this.options,
this.logger,
Server.findCacheDir()
);

this.applyDevServerPlugin();

Expand All @@ -64,7 +69,19 @@ class Server {
this.createServer();

killable(this.server);
setupExitSignals(this);

if (this.options.setupExitSignals) {
const signals = ['SIGINT', 'SIGTERM'];

signals.forEach((signal) => {
process.on(signal, () => {
this.close(() => {
// eslint-disable-next-line no-process-exit
process.exit();
});
});
});
}

// Proxy WebSocket without the initial http request
// https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade
Expand All @@ -74,6 +91,92 @@ class Server {
}, this);
}

static get DEFAULT_STATS() {
return {
all: false,
hash: true,
assets: true,
warnings: true,
errors: true,
errorDetails: false,
};
}

static getHostname(hostname) {
if (hostname === 'local-ip') {
return internalIp.v4.sync() || internalIp.v6.sync() || '0.0.0.0';
} else if (hostname === 'local-ipv4') {
return internalIp.v4.sync() || '0.0.0.0';
} else if (hostname === 'local-ipv6') {
return internalIp.v6.sync() || '::';
}

return hostname;
}

static getFreePort(port) {
const pRetry = require('p-retry');
const portfinder = require('portfinder');

if (port && port !== 'auto') {
return Promise.resolve(port);
}

function runPortFinder() {
return new Promise((resolve, reject) => {
// Default port
portfinder.basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT || 8080;
portfinder.getPort((error, foundPort) => {
if (error) {
return reject(error);
}

return resolve(foundPort);
});
});
}

// 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(runPortFinder, { 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');
} else if (process.versions.pnp === '1') {
return path.resolve(dir, '.pnp/.cache/webpack-dev-server');
} else if (process.versions.pnp === '3') {
return path.resolve(dir, '.yarn/.cache/webpack-dev-server');
}

return path.resolve(dir, 'node_modules/.cache/webpack-dev-server');
}

applyDevServerPlugin() {
const DevServerPlugin = require('./utils/DevServerPlugin');

Expand Down Expand Up @@ -539,7 +642,122 @@ class Server {
});
}

openBrowser(uri) {
const isAbsoluteUrl = require('is-absolute-url');
const open = require('open');

// https://github.com/webpack/webpack-dev-server/issues/1990
const defaultOpenOptions = { wait: false };
const openTasks = [];

const getOpenTask = (item) => {
if (typeof item === 'boolean') {
return [{ target: uri, options: defaultOpenOptions }];
}

if (typeof item === 'string') {
return [{ target: item, options: defaultOpenOptions }];
}

let targets;

if (item.target) {
targets = Array.isArray(item.target) ? item.target : [item.target];
} else {
targets = [uri];
}

return targets.map((target) => {
const openOptions = defaultOpenOptions;

if (item.app) {
if (typeof item.app === 'string') {
openOptions.app = { name: item.app };
} else {
openOptions.app = item.app;
}
}

return { target, options: openOptions };
});
};

if (Array.isArray(this.options.open)) {
this.options.open.forEach((item) => {
openTasks.push(...getOpenTask(item));
});
} else {
openTasks.push(...getOpenTask(this.options.open));
}

Promise.all(
openTasks.map((openTask) => {
let openTarget;

if (openTask.target) {
if (typeof openTask.target === 'boolean') {
openTarget = uri;
} else {
openTarget = isAbsoluteUrl(openTask.target)
? openTask.target
: new URL(openTask.target, uri).toString();
}
} else {
openTarget = uri;
}

return open(openTarget, openTask.options).catch(() => {
this.logger.warn(
`Unable to open "${openTarget}" page${
// eslint-disable-next-line no-nested-ternary
openTask.options.app
? ` in "${openTask.options.app.name}" app${
openTask.options.app.arguments
? ` with "${openTask.options.app.arguments.join(
' '
)}" arguments`
: ''
}`
: ''
}. If you are running in a headless environment, please do not use the "open" option or related flags like "--open", "--open-target", and "--open-app".`
);
});
})
);
}

runBonjour() {
const bonjour = require('bonjour')();
const os = require('os');

bonjour.publish({
name: `Webpack Dev Server ${os.hostname()}:${this.options.port}`,
port: this.options.port,
type: this.options.https ? 'https' : 'http',
subtypes: ['webpack'],
...this.options.bonjour,
});

process.on('exit', () => {
bonjour.unpublishAll(() => {
bonjour.destroy();
});
});
}

logStatus() {
const getColorsOption = (configArray) => {
const statsOption = getStatsOption(configArray);

let colorsEnabled = false;

if (typeof statsOption === 'object' && statsOption.colors) {
colorsEnabled = statsOption.colors;
}

return colorsEnabled;
};

const useColor = getColorsOption(getCompilerConfigArray(this.compiler));
const protocol = this.options.https ? 'https' : 'http';
const { address, port } = this.server.address();
Expand Down Expand Up @@ -661,11 +879,9 @@ class Server {
}

if (this.options.open) {
const runOpen = require('./utils/runOpen');

const openTarget = prettyPrintUrl(this.options.host || 'localhost');

runOpen(openTarget, this.options.open, this.logger);
this.openBrowser(openTarget);
}
}

Expand Down Expand Up @@ -702,14 +918,7 @@ class Server {
this.options.host = hostname;
}

if (this.options.host === 'local-ip') {
this.options.host =
internalIp.v4.sync() || internalIp.v6.sync() || '0.0.0.0';
} else if (this.options.host === 'local-ipv4') {
this.options.host = internalIp.v4.sync() || '0.0.0.0';
} else if (this.options.host === 'local-ipv6') {
this.options.host = internalIp.v6.sync() || '::';
}
this.options.host = Server.getHostname(this.options.host);

return Server.getFreePort(this.options.port)
.then((foundPort) => {
Expand All @@ -724,9 +933,7 @@ class Server {
}

if (this.options.bonjour) {
const runBonjour = require('./utils/runBonjour');

runBonjour(this.options);
this.runBonjour();
}

this.logStatus();
Expand Down Expand Up @@ -767,47 +974,6 @@ class Server {
});
}

static get DEFAULT_STATS() {
return {
all: false,
hash: true,
assets: true,
warnings: true,
errors: true,
errorDetails: false,
};
}

static getFreePort(port) {
const pRetry = require('p-retry');
const portfinder = require('portfinder');

if (port && port !== 'auto') {
return Promise.resolve(port);
}

function runPortFinder() {
return new Promise((resolve, reject) => {
// default port
portfinder.basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT || 8080;
portfinder.getPort((error, foundPort) => {
if (error) {
return reject(error);
}

return resolve(foundPort);
});
});
}

// 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(runPortFinder, { retries: defaultPortRetry });
}

getStats(statsObj) {
const stats = Server.DEFAULT_STATS;

Expand Down
Loading