UNPKG

14.8 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3'use strict';
4
5/* eslint global-require: off, import/order: off, no-console: off, import/no-extraneous-dependencies: off */
6require('../lib/polyfills');
7
8const debug = require('debug')('webpack-dev-server');
9const fs = require('fs');
10const net = require('net');
11const path = require('path');
12const importLocal = require('import-local');
13const open = require('opn');
14const portfinder = require('portfinder');
15const addDevServerEntrypoints = require('../lib/util/addDevServerEntrypoints');
16const createDomain = require('../lib/util/createDomain'); // eslint-disable-line
17const createLog = require('../lib/createLog');
18
19// Prefer the local installation of webpack-dev-server
20if (importLocal(__filename)) {
21 debug('Using local install of webpack-dev-server');
22 return;
23}
24
25const Server = require('../lib/Server');
26const webpack = require('webpack'); // eslint-disable-line
27
28try {
29 require.resolve('webpack-cli');
30} catch (e) {
31 console.error('The CLI moved into a separate package: webpack-cli.');
32 console.error("Please install 'webpack-cli' in addition to webpack itself to use the CLI.");
33 console.error('-> When using npm: npm install webpack-cli -D');
34 console.error('-> When using yarn: yarn add webpack-cli -D');
35 process.exitCode = 1;
36}
37
38function versionInfo() {
39 return `webpack-dev-server ${require('../package.json').version}\n` +
40 `webpack ${require('webpack/package.json').version}`;
41}
42
43function colorInfo(useColor, msg) {
44 if (useColor) {
45 // Make text blue and bold, so it *pops*
46 return `\u001b[1m\u001b[34m${msg}\u001b[39m\u001b[22m`;
47 }
48 return msg;
49}
50
51function colorError(useColor, msg) {
52 if (useColor) {
53 // Make text red and bold, so it *pops*
54 return `\u001b[1m\u001b[31m${msg}\u001b[39m\u001b[22m`;
55 }
56 return msg;
57}
58
59// eslint-disable-next-line
60const defaultTo = (value, def) => value == null ? def : value;
61
62const yargs = require('yargs')
63 .usage(`${versionInfo()}\nUsage: https://webpack.js.org/configuration/dev-server/`);
64
65require('webpack-cli/bin/config-yargs')(yargs);
66
67// It is important that this is done after the webpack yargs config,
68// so it overrides webpack's version info.
69yargs
70 .version(versionInfo());
71
72const ADVANCED_GROUP = 'Advanced options:';
73const DISPLAY_GROUP = 'Stats options:';
74const SSL_GROUP = 'SSL options:';
75const CONNECTION_GROUP = 'Connection options:';
76const RESPONSE_GROUP = 'Response options:';
77const BASIC_GROUP = 'Basic options:';
78
79// Taken out of yargs because we must know if
80// it wasn't given by the user, in which case
81// we should use portfinder.
82const DEFAULT_PORT = 8080;
83
84yargs.options({
85 bonjour: {
86 type: 'boolean',
87 describe: 'Broadcasts the server via ZeroConf networking on start'
88 },
89 lazy: {
90 type: 'boolean',
91 describe: 'Lazy'
92 },
93 inline: {
94 type: 'boolean',
95 default: true,
96 describe: 'Inline mode (set to false to disable including client scripts like livereload)'
97 },
98 progress: {
99 type: 'boolean',
100 describe: 'Print compilation progress in percentage',
101 group: BASIC_GROUP
102 },
103 'hot-only': {
104 type: 'boolean',
105 describe: 'Do not refresh page if HMR fails',
106 group: ADVANCED_GROUP
107 },
108 stdin: {
109 type: 'boolean',
110 describe: 'close when stdin ends'
111 },
112 open: {
113 type: 'string',
114 describe: 'Open the default browser, or optionally specify a browser name'
115 },
116 useLocalIp: {
117 type: 'boolean',
118 describe: 'Open default browser with local IP'
119 },
120 'open-page': {
121 type: 'string',
122 describe: 'Open default browser with the specified page',
123 requiresArg: true
124 },
125 color: {
126 type: 'boolean',
127 alias: 'colors',
128 default: function supportsColor() {
129 return require('supports-color');
130 },
131 group: DISPLAY_GROUP,
132 describe: 'Enables/Disables colors on the console'
133 },
134 info: {
135 type: 'boolean',
136 group: DISPLAY_GROUP,
137 default: true,
138 describe: 'Info'
139 },
140 quiet: {
141 type: 'boolean',
142 group: DISPLAY_GROUP,
143 describe: 'Quiet'
144 },
145 'client-log-level': {
146 type: 'string',
147 group: DISPLAY_GROUP,
148 default: 'info',
149 describe: 'Log level in the browser (info, warning, error or none)'
150 },
151 https: {
152 type: 'boolean',
153 group: SSL_GROUP,
154 describe: 'HTTPS'
155 },
156 key: {
157 type: 'string',
158 describe: 'Path to a SSL key.',
159 group: SSL_GROUP
160 },
161 cert: {
162 type: 'string',
163 describe: 'Path to a SSL certificate.',
164 group: SSL_GROUP
165 },
166 cacert: {
167 type: 'string',
168 describe: 'Path to a SSL CA certificate.',
169 group: SSL_GROUP
170 },
171 pfx: {
172 type: 'string',
173 describe: 'Path to a SSL pfx file.',
174 group: SSL_GROUP
175 },
176 'pfx-passphrase': {
177 type: 'string',
178 describe: 'Passphrase for pfx file.',
179 group: SSL_GROUP
180 },
181 'content-base': {
182 type: 'string',
183 describe: 'A directory or URL to serve HTML content from.',
184 group: RESPONSE_GROUP
185 },
186 'watch-content-base': {
187 type: 'boolean',
188 describe: 'Enable live-reloading of the content-base.',
189 group: RESPONSE_GROUP
190 },
191 'history-api-fallback': {
192 type: 'boolean',
193 describe: 'Fallback to /index.html for Single Page Applications.',
194 group: RESPONSE_GROUP
195 },
196 compress: {
197 type: 'boolean',
198 describe: 'Enable gzip compression',
199 group: RESPONSE_GROUP
200 },
201 port: {
202 describe: 'The port',
203 group: CONNECTION_GROUP
204 },
205 'disable-host-check': {
206 type: 'boolean',
207 describe: 'Will not check the host',
208 group: CONNECTION_GROUP
209 },
210 socket: {
211 type: 'String',
212 describe: 'Socket to listen',
213 group: CONNECTION_GROUP
214 },
215 public: {
216 type: 'string',
217 describe: 'The public hostname/ip address of the server',
218 group: CONNECTION_GROUP
219 },
220 host: {
221 type: 'string',
222 default: 'localhost',
223 describe: 'The hostname/ip address the server will bind to',
224 group: CONNECTION_GROUP
225 },
226 'allowed-hosts': {
227 type: 'string',
228 describe: 'A comma-delimited string of hosts that are allowed to access the dev server',
229 group: CONNECTION_GROUP
230 }
231});
232
233const argv = yargs.argv;
234const wpOpt = require('webpack-cli/bin/convert-argv')(yargs, argv, {
235 outputFilename: '/bundle.js'
236});
237
238function processOptions(webpackOptions) {
239 // process Promise
240 if (typeof webpackOptions.then === 'function') {
241 webpackOptions.then(processOptions).catch((err) => {
242 console.error(err.stack || err);
243 process.exit(); // eslint-disable-line
244 });
245 return;
246 }
247
248 const firstWpOpt = Array.isArray(webpackOptions) ? webpackOptions[0] : webpackOptions;
249
250 const options = webpackOptions.devServer || firstWpOpt.devServer || {};
251
252 if (argv.bonjour) { options.bonjour = true; }
253
254 if (argv.host !== 'localhost' || !options.host) { options.host = argv.host; }
255
256 if (argv['allowed-hosts']) { options.allowedHosts = argv['allowed-hosts'].split(','); }
257
258 if (argv.public) { options.public = argv.public; }
259
260 if (argv.socket) { options.socket = argv.socket; }
261
262 if (argv.progress) { options.progress = argv.progress; }
263
264 if (!options.publicPath) {
265 // eslint-disable-next-line
266 options.publicPath = firstWpOpt.output && firstWpOpt.output.publicPath || '';
267 if (!/^(https?:)?\/\//.test(options.publicPath) && options.publicPath[0] !== '/') {
268 options.publicPath = `/${options.publicPath}`;
269 }
270 }
271
272 if (!options.filename) { options.filename = firstWpOpt.output && firstWpOpt.output.filename; }
273
274 if (!options.watchOptions) { options.watchOptions = firstWpOpt.watchOptions; }
275
276 if (argv.stdin) {
277 process.stdin.on('end', () => {
278 process.exit(0); // eslint-disable-line no-process-exit
279 });
280 process.stdin.resume();
281 }
282
283 if (!options.hot) { options.hot = argv.hot; }
284
285 if (!options.hotOnly) { options.hotOnly = argv['hot-only']; }
286
287 if (!options.clientLogLevel) { options.clientLogLevel = argv['client-log-level']; }
288
289 // eslint-disable-next-line
290 if (options.contentBase === undefined) {
291 if (argv['content-base']) {
292 options.contentBase = argv['content-base'];
293 if (Array.isArray(options.contentBase)) {
294 options.contentBase = options.contentBase.map(val => path.resolve(val));
295 } else if (/^[0-9]$/.test(options.contentBase)) { options.contentBase = +options.contentBase; } else if (!/^(https?:)?\/\//.test(options.contentBase)) { options.contentBase = path.resolve(options.contentBase); }
296 // It is possible to disable the contentBase by using `--no-content-base`, which results in arg["content-base"] = false
297 } else if (argv['content-base'] === false) {
298 options.contentBase = false;
299 }
300 }
301
302 if (argv['watch-content-base']) { options.watchContentBase = true; }
303
304 if (!options.stats) {
305 options.stats = {
306 cached: false,
307 cachedAssets: false
308 };
309 }
310
311 if (typeof options.stats === 'object' && typeof options.stats.colors === 'undefined') {
312 options.stats = Object.assign({}, options.stats, { colors: argv.color });
313 }
314
315 if (argv.lazy) { options.lazy = true; }
316
317 if (!argv.info) { options.noInfo = true; }
318
319 if (argv.quiet) { options.quiet = true; }
320
321 if (argv.https) { options.https = true; }
322
323 if (argv.cert) { options.cert = fs.readFileSync(path.resolve(argv.cert)); }
324
325 if (argv.key) { options.key = fs.readFileSync(path.resolve(argv.key)); }
326
327 if (argv.cacert) { options.ca = fs.readFileSync(path.resolve(argv.cacert)); }
328
329 if (argv.pfx) { options.pfx = fs.readFileSync(path.resolve(argv.pfx)); }
330
331 if (argv['pfx-passphrase']) { options.pfxPassphrase = argv['pfx-passphrase']; }
332
333 if (argv.inline === false) { options.inline = false; }
334
335 if (argv['history-api-fallback']) { options.historyApiFallback = true; }
336
337 if (argv.compress) { options.compress = true; }
338
339 if (argv['disable-host-check']) { options.disableHostCheck = true; }
340
341 if (argv['open-page']) {
342 options.open = true;
343 options.openPage = argv['open-page'];
344 }
345
346 if (typeof argv.open !== 'undefined') {
347 options.open = argv.open !== '' ? argv.open : true;
348 }
349
350 if (options.open && !options.openPage) { options.openPage = ''; }
351
352 if (argv.useLocalIp) { options.useLocalIp = true; }
353
354 // Kind of weird, but ensures prior behavior isn't broken in cases
355 // that wouldn't throw errors. E.g. both argv.port and options.port
356 // were specified, but since argv.port is 8080, options.port will be
357 // tried first instead.
358 options.port = argv.port === DEFAULT_PORT ? defaultTo(options.port, argv.port) : defaultTo(argv.port, options.port);
359
360 if (options.port != null) {
361 startDevServer(webpackOptions, options);
362 return;
363 }
364
365 portfinder.basePort = DEFAULT_PORT;
366 portfinder.getPort((err, port) => {
367 if (err) throw err;
368 options.port = port;
369 startDevServer(webpackOptions, options);
370 });
371}
372
373function startDevServer(webpackOptions, options) {
374 const log = createLog(options);
375 addDevServerEntrypoints(webpackOptions, options);
376
377 let compiler;
378 try {
379 compiler = webpack(webpackOptions);
380 } catch (e) {
381 if (e instanceof webpack.WebpackOptionsValidationError) {
382 log.error(colorError(options.stats.colors, e.message));
383 process.exit(1); // eslint-disable-line
384 }
385 throw e;
386 }
387
388 if (options.progress) {
389 new webpack.ProgressPlugin({
390 profile: argv.profile
391 }).apply(compiler);
392 }
393
394 const suffix = (options.inline !== false || options.lazy === true ? '/' : '/webpack-dev-server/');
395
396 let server;
397 try {
398 server = new Server(compiler, options, log);
399 } catch (e) {
400 const OptionsValidationError = require('../lib/OptionsValidationError');
401 if (e instanceof OptionsValidationError) {
402 log.error(colorError(options.stats.colors, e.message));
403 process.exit(1); // eslint-disable-line
404 }
405 throw e;
406 }
407
408 ['SIGINT', 'SIGTERM'].forEach((sig) => {
409 process.on(sig, () => {
410 server.close(() => {
411 process.exit(); // eslint-disable-line no-process-exit
412 });
413 });
414 });
415
416 if (options.socket) {
417 server.listeningApp.on('error', (e) => {
418 if (e.code === 'EADDRINUSE') {
419 const clientSocket = new net.Socket();
420 clientSocket.on('error', (clientError) => {
421 if (clientError.code === 'ECONNREFUSED') {
422 // No other server listening on this socket so it can be safely removed
423 fs.unlinkSync(options.socket);
424 server.listen(options.socket, options.host, (err) => {
425 if (err) throw err;
426 });
427 }
428 });
429 clientSocket.connect({ path: options.socket }, () => {
430 throw new Error('This socket is already used');
431 });
432 }
433 });
434 server.listen(options.socket, options.host, (err) => {
435 if (err) throw err;
436 // chmod 666 (rw rw rw)
437 const READ_WRITE = 438;
438 fs.chmod(options.socket, READ_WRITE, (fsError) => {
439 if (fsError) throw fsError;
440
441 const uri = createDomain(options, server.listeningApp) + suffix;
442 reportReadiness(uri, options, log);
443 });
444 });
445 } else {
446 server.listen(options.port, options.host, (err) => {
447 if (err) throw err;
448 if (options.bonjour) broadcastZeroconf(options);
449
450 const uri = createDomain(options, server.listeningApp) + suffix;
451 reportReadiness(uri, options, log);
452 });
453 }
454}
455
456function reportReadiness(uri, options, log) {
457 const useColor = argv.color;
458 const contentBase = Array.isArray(options.contentBase) ? options.contentBase.join(', ') : options.contentBase;
459
460 if (options.socket) {
461 log.info(`Listening to socket at ${colorInfo(useColor, options.socket)}`);
462 } else {
463 log.info(`Project is running at ${colorInfo(useColor, uri)}`);
464 }
465
466 log.info(`webpack output is served from ${colorInfo(useColor, options.publicPath)}`);
467
468 if (contentBase) { log.info(`Content not from webpack is served from ${colorInfo(useColor, contentBase)}`); }
469
470 if (options.historyApiFallback) { log.info(`404s will fallback to ${colorInfo(useColor, options.historyApiFallback.index || '/index.html')}`); }
471
472 if (options.bonjour) { log.info('Broadcasting "http" with subtype of "webpack" via ZeroConf DNS (Bonjour)'); }
473
474 if (options.open) {
475 let openOptions = {};
476 let openMessage = 'Unable to open browser';
477
478 if (typeof options.open === 'string') {
479 openOptions = { app: options.open };
480 openMessage += `: ${options.open}`;
481 }
482
483 open(uri + (options.openPage || ''), openOptions).catch(() => {
484 log.warn(`${openMessage}. If you are running in a headless environment, please do not use the open flag.`);
485 });
486 }
487}
488
489function broadcastZeroconf(options) {
490 const bonjour = require('bonjour')();
491 bonjour.publish({
492 name: 'Webpack Dev Server',
493 port: options.port,
494 type: 'http',
495 subtypes: ['webpack']
496 });
497 process.on('exit', () => {
498 bonjour.unpublishAll(() => {
499 bonjour.destroy();
500 });
501 });
502}
503
504processOptions(wpOpt);