1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | 'use strict';
|
8 |
|
9 | const address = require('address');
|
10 | const fs = require('fs');
|
11 | const path = require('path');
|
12 | const url = require('url');
|
13 | const chalk = require('chalk');
|
14 | const detect = require('detect-port-alt');
|
15 | const isRoot = require('is-root');
|
16 | const inquirer = require('inquirer');
|
17 | const clearConsole = require('./clearConsole');
|
18 | const formatWebpackMessages = require('./formatWebpackMessages');
|
19 | const getProcessForPort = require('./getProcessForPort');
|
20 |
|
21 | const isInteractive = process.stdout.isTTY;
|
22 | let handleCompile;
|
23 |
|
24 |
|
25 |
|
26 | const isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
|
27 | if (isSmokeTest) {
|
28 | handleCompile = (err, stats) => {
|
29 | if (err || stats.hasErrors() || stats.hasWarnings()) {
|
30 | process.exit(1);
|
31 | } else {
|
32 | process.exit(0);
|
33 | }
|
34 | };
|
35 | }
|
36 |
|
37 | function prepareUrls(protocol, host, port) {
|
38 | const formatUrl = hostname =>
|
39 | url.format({
|
40 | protocol,
|
41 | hostname,
|
42 | port,
|
43 | pathname: '/',
|
44 | });
|
45 | const prettyPrintUrl = hostname =>
|
46 | url.format({
|
47 | protocol,
|
48 | hostname,
|
49 | port: chalk.bold(port),
|
50 | pathname: '/',
|
51 | });
|
52 |
|
53 | const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
|
54 | let prettyHost, lanUrlForConfig, lanUrlForTerminal;
|
55 | if (isUnspecifiedHost) {
|
56 | prettyHost = 'localhost';
|
57 | try {
|
58 |
|
59 | lanUrlForConfig = address.ip();
|
60 | if (lanUrlForConfig) {
|
61 |
|
62 |
|
63 | if (
|
64 | /^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
|
65 | lanUrlForConfig
|
66 | )
|
67 | ) {
|
68 |
|
69 | lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
|
70 | } else {
|
71 |
|
72 | lanUrlForConfig = undefined;
|
73 | }
|
74 | }
|
75 | } catch (_e) {
|
76 |
|
77 | }
|
78 | } else {
|
79 | prettyHost = host;
|
80 | }
|
81 | const localUrlForTerminal = prettyPrintUrl(prettyHost);
|
82 | const localUrlForBrowser = formatUrl(prettyHost);
|
83 | return {
|
84 | lanUrlForConfig,
|
85 | lanUrlForTerminal,
|
86 | localUrlForTerminal,
|
87 | localUrlForBrowser,
|
88 | };
|
89 | }
|
90 |
|
91 | function printInstructions(appName, urls, useYarn) {
|
92 | console.log();
|
93 | console.log(`You can now view ${chalk.bold(appName)} in the browser.`);
|
94 | console.log();
|
95 |
|
96 | if (urls.lanUrlForTerminal) {
|
97 | console.log(
|
98 | ` ${chalk.bold('Local:')} ${urls.localUrlForTerminal}`
|
99 | );
|
100 | console.log(
|
101 | ` ${chalk.bold('On Your Network:')} ${urls.lanUrlForTerminal}`
|
102 | );
|
103 | } else {
|
104 | console.log(` ${urls.localUrlForTerminal}`);
|
105 | }
|
106 |
|
107 | console.log();
|
108 | console.log('Note that the development build is not optimized.');
|
109 | console.log(
|
110 | `To create a production build, use ` +
|
111 | `${chalk.cyan(`${useYarn ? 'yarn' : 'npm run'} build`)}.`
|
112 | );
|
113 | console.log();
|
114 | }
|
115 |
|
116 | function createCompiler(webpack, config, appName, urls, useYarn) {
|
117 |
|
118 |
|
119 | let compiler;
|
120 | try {
|
121 | compiler = webpack(config, handleCompile);
|
122 | } catch (err) {
|
123 | console.log(chalk.red('Failed to compile.'));
|
124 | console.log();
|
125 | console.log(err.message || err);
|
126 | console.log();
|
127 | process.exit(1);
|
128 | }
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 | compiler.plugin('invalid', () => {
|
135 | if (isInteractive) {
|
136 | clearConsole();
|
137 | }
|
138 | console.log('Compiling...');
|
139 | });
|
140 |
|
141 | let isFirstCompile = true;
|
142 |
|
143 |
|
144 |
|
145 | compiler.plugin('done', stats => {
|
146 | if (isInteractive) {
|
147 | clearConsole();
|
148 | }
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | const messages = formatWebpackMessages(stats.toJson({}, true));
|
154 | const isSuccessful = !messages.errors.length && !messages.warnings.length;
|
155 | if (isSuccessful) {
|
156 | console.log(chalk.green('Compiled successfully!'));
|
157 | }
|
158 | if (isSuccessful && (isInteractive || isFirstCompile)) {
|
159 | printInstructions(appName, urls, useYarn);
|
160 | }
|
161 | isFirstCompile = false;
|
162 |
|
163 |
|
164 | if (messages.errors.length) {
|
165 |
|
166 |
|
167 | if (messages.errors.length > 1) {
|
168 | messages.errors.length = 1;
|
169 | }
|
170 | console.log(chalk.red('Failed to compile.\n'));
|
171 | console.log(messages.errors.join('\n\n'));
|
172 | return;
|
173 | }
|
174 |
|
175 |
|
176 | if (messages.warnings.length) {
|
177 | console.log(chalk.yellow('Compiled with warnings.\n'));
|
178 | console.log(messages.warnings.join('\n\n'));
|
179 |
|
180 |
|
181 | console.log(
|
182 | '\nSearch for the ' +
|
183 | chalk.underline(chalk.yellow('keywords')) +
|
184 | ' to learn more about each warning.'
|
185 | );
|
186 | console.log(
|
187 | 'To ignore, add ' +
|
188 | chalk.cyan('// eslint-disable-next-line') +
|
189 | ' to the line before.\n'
|
190 | );
|
191 | }
|
192 | });
|
193 | return compiler;
|
194 | }
|
195 |
|
196 | function resolveLoopback(proxy) {
|
197 | const o = url.parse(proxy);
|
198 | o.host = undefined;
|
199 | if (o.hostname !== 'localhost') {
|
200 | return proxy;
|
201 | }
|
202 |
|
203 |
|
204 |
|
205 |
|
206 | |
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 | try {
|
213 |
|
214 |
|
215 |
|
216 | if (!address.ip()) {
|
217 | o.hostname = '127.0.0.1';
|
218 | }
|
219 | } catch (_ignored) {
|
220 | o.hostname = '127.0.0.1';
|
221 | }
|
222 | return url.format(o);
|
223 | }
|
224 |
|
225 |
|
226 |
|
227 | function onProxyError(proxy) {
|
228 | return (err, req, res) => {
|
229 | const host = req.headers && req.headers.host;
|
230 | console.log(
|
231 | chalk.red('Proxy error:') +
|
232 | ' Could not proxy request ' +
|
233 | chalk.cyan(req.url) +
|
234 | ' from ' +
|
235 | chalk.cyan(host) +
|
236 | ' to ' +
|
237 | chalk.cyan(proxy) +
|
238 | '.'
|
239 | );
|
240 | console.log(
|
241 | 'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
|
242 | chalk.cyan(err.code) +
|
243 | ').'
|
244 | );
|
245 | console.log();
|
246 |
|
247 |
|
248 |
|
249 | if (res.writeHead && !res.headersSent) {
|
250 | res.writeHead(500);
|
251 | }
|
252 | res.end(
|
253 | 'Proxy error: Could not proxy request ' +
|
254 | req.url +
|
255 | ' from ' +
|
256 | host +
|
257 | ' to ' +
|
258 | proxy +
|
259 | ' (' +
|
260 | err.code +
|
261 | ').'
|
262 | );
|
263 | };
|
264 | }
|
265 |
|
266 | function prepareProxy(proxy, appPublicFolder) {
|
267 |
|
268 |
|
269 |
|
270 | if (!proxy) {
|
271 | return undefined;
|
272 | }
|
273 | if (typeof proxy !== 'object' && typeof proxy !== 'string') {
|
274 | console.log(
|
275 | chalk.red(
|
276 | 'When specified, "proxy" in package.json must be a string or an object.'
|
277 | )
|
278 | );
|
279 | console.log(
|
280 | chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')
|
281 | );
|
282 | console.log(
|
283 | chalk.red(
|
284 | 'Either remove "proxy" from package.json, or make it an object.'
|
285 | )
|
286 | );
|
287 | process.exit(1);
|
288 | }
|
289 |
|
290 |
|
291 | function mayProxy(pathname) {
|
292 | const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1));
|
293 | return !fs.existsSync(maybePublicPath);
|
294 | }
|
295 |
|
296 |
|
297 | if (typeof proxy === 'string') {
|
298 | if (!/^http(s)?:\/\//.test(proxy)) {
|
299 | console.log(
|
300 | chalk.red(
|
301 | 'When "proxy" is specified in package.json it must start with either http:// or https://'
|
302 | )
|
303 | );
|
304 | process.exit(1);
|
305 | }
|
306 |
|
307 | let target;
|
308 | if (process.platform === 'win32') {
|
309 | target = resolveLoopback(proxy);
|
310 | } else {
|
311 | target = proxy;
|
312 | }
|
313 | return [
|
314 | {
|
315 | target,
|
316 | logLevel: 'silent',
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | context: function(pathname, req) {
|
325 | return (
|
326 | mayProxy(pathname) &&
|
327 | req.headers.accept &&
|
328 | req.headers.accept.indexOf('text/html') === -1
|
329 | );
|
330 | },
|
331 | onProxyReq: proxyReq => {
|
332 |
|
333 |
|
334 |
|
335 | if (proxyReq.getHeader('origin')) {
|
336 | proxyReq.setHeader('origin', target);
|
337 | }
|
338 | },
|
339 | onError: onProxyError(target),
|
340 | secure: false,
|
341 | changeOrigin: true,
|
342 | ws: true,
|
343 | xfwd: true,
|
344 | },
|
345 | ];
|
346 | }
|
347 |
|
348 |
|
349 | return Object.keys(proxy).map(function(context) {
|
350 | if (!proxy[context].hasOwnProperty('target')) {
|
351 | console.log(
|
352 | chalk.red(
|
353 | 'When `proxy` in package.json is as an object, each `context` object must have a ' +
|
354 | '`target` property specified as a url string'
|
355 | )
|
356 | );
|
357 | process.exit(1);
|
358 | }
|
359 | let target;
|
360 | if (process.platform === 'win32') {
|
361 | target = resolveLoopback(proxy[context].target);
|
362 | } else {
|
363 | target = proxy[context].target;
|
364 | }
|
365 | return Object.assign({}, proxy[context], {
|
366 | context: function(pathname) {
|
367 | return mayProxy(pathname) && pathname.match(context);
|
368 | },
|
369 | onProxyReq: proxyReq => {
|
370 |
|
371 |
|
372 |
|
373 | if (proxyReq.getHeader('origin')) {
|
374 | proxyReq.setHeader('origin', target);
|
375 | }
|
376 | },
|
377 | target,
|
378 | onError: onProxyError(target),
|
379 | });
|
380 | });
|
381 | }
|
382 |
|
383 | function choosePort(host, defaultPort) {
|
384 | return detect(defaultPort, host).then(
|
385 | port =>
|
386 | new Promise(resolve => {
|
387 | if (port === defaultPort) {
|
388 | return resolve(port);
|
389 | }
|
390 | const message =
|
391 | process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
|
392 | ? `Admin permissions are required to run a server on a port below 1024.`
|
393 | : `Something is already running on port ${defaultPort}.`;
|
394 | if (isInteractive) {
|
395 | clearConsole();
|
396 | const existingProcess = getProcessForPort(defaultPort);
|
397 | const question = {
|
398 | type: 'confirm',
|
399 | name: 'shouldChangePort',
|
400 | message:
|
401 | chalk.yellow(
|
402 | message +
|
403 | `${existingProcess ? ` Probably:\n ${existingProcess}` : ''}`
|
404 | ) + '\n\nWould you like to run the app on another port instead?',
|
405 | default: true,
|
406 | };
|
407 | inquirer.prompt(question).then(answer => {
|
408 | if (answer.shouldChangePort) {
|
409 | resolve(port);
|
410 | } else {
|
411 | resolve(null);
|
412 | }
|
413 | });
|
414 | } else {
|
415 | console.log(chalk.red(message));
|
416 | resolve(null);
|
417 | }
|
418 | }),
|
419 | err => {
|
420 | throw new Error(
|
421 | chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) +
|
422 | '\n' +
|
423 | ('Network error message: ' + err.message || err) +
|
424 | '\n'
|
425 | );
|
426 | }
|
427 | );
|
428 | }
|
429 |
|
430 | module.exports = {
|
431 | choosePort,
|
432 | createCompiler,
|
433 | prepareProxy,
|
434 | prepareUrls,
|
435 | };
|