UNPKG

13.4 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2015-present, Facebook, Inc.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7'use strict';
8
9const address = require('address');
10const fs = require('fs');
11const path = require('path');
12const url = require('url');
13const chalk = require('chalk');
14const detect = require('detect-port-alt');
15const isRoot = require('is-root');
16const inquirer = require('inquirer');
17const clearConsole = require('./clearConsole');
18const formatWebpackMessages = require('./formatWebpackMessages');
19const getProcessForPort = require('./getProcessForPort');
20
21const isInteractive = process.stdout.isTTY;
22let handleCompile;
23
24// You can safely remove this after ejecting.
25// We only use this block for testing of Create React App itself:
26const isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
27if (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
37function 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 // This can only return an IPv4 address
59 lanUrlForConfig = address.ip();
60 if (lanUrlForConfig) {
61 // Check if the address is a private ip
62 // https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
63 if (
64 /^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
65 lanUrlForConfig
66 )
67 ) {
68 // Address is private, format it for later use
69 lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
70 } else {
71 // Address is not private, so we will discard it
72 lanUrlForConfig = undefined;
73 }
74 }
75 } catch (_e) {
76 // ignored
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
91function 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
116function createCompiler(webpack, config, appName, urls, useYarn) {
117 // "Compiler" is a low-level interface to Webpack.
118 // It lets us listen to some events and provide our own custom messages.
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 // "invalid" event fires when you have changed a file, and Webpack is
131 // recompiling a bundle. WebpackDevServer takes care to pause serving the
132 // bundle, so if you refresh, it'll wait instead of serving the old one.
133 // "invalid" is short for "bundle invalidated", it doesn't imply any errors.
134 compiler.plugin('invalid', () => {
135 if (isInteractive) {
136 clearConsole();
137 }
138 console.log('Compiling...');
139 });
140
141 let isFirstCompile = true;
142
143 // "done" event fires when Webpack has finished recompiling the bundle.
144 // Whether or not you have warnings or errors, you will get this event.
145 compiler.plugin('done', stats => {
146 if (isInteractive) {
147 clearConsole();
148 }
149
150 // We have switched off the default Webpack output in WebpackDevServer
151 // options so we are going to "massage" the warnings and errors and present
152 // them in a readable focused way.
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 // If errors exist, only show errors.
164 if (messages.errors.length) {
165 // Only keep the first error. Others are often indicative
166 // of the same problem, but confuse the reader with noise.
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 // Show warnings if no errors were found.
176 if (messages.warnings.length) {
177 console.log(chalk.yellow('Compiled with warnings.\n'));
178 console.log(messages.warnings.join('\n\n'));
179
180 // Teach some ESLint tricks.
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
196function resolveLoopback(proxy) {
197 const o = url.parse(proxy);
198 o.host = undefined;
199 if (o.hostname !== 'localhost') {
200 return proxy;
201 }
202 // Unfortunately, many languages (unlike node) do not yet support IPv6.
203 // This means even though localhost resolves to ::1, the application
204 // must fall back to IPv4 (on 127.0.0.1).
205 // We can re-enable this in a few years.
206 /*try {
207 o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
208 } catch (_ignored) {
209 o.hostname = '127.0.0.1';
210 }*/
211
212 try {
213 // Check if we're on a network; if we are, chances are we can resolve
214 // localhost. Otherwise, we can just be safe and assume localhost is
215 // IPv4 for maximum compatibility.
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// We need to provide a custom onError function for httpProxyMiddleware.
226// It allows us to log custom error messages on the console.
227function 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 // And immediately send the proper error response to the client.
248 // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
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
266function prepareProxy(proxy, appPublicFolder) {
267 // `proxy` lets you specify alternate servers for specific requests.
268 // It can either be a string or an object conforming to the Webpack dev server proxy configuration
269 // https://webpack.github.io/docs/webpack-dev-server.html
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 // Otherwise, if proxy is specified, we will let it handle any request except for files in the public folder.
291 function mayProxy(pathname) {
292 const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1));
293 return !fs.existsSync(maybePublicPath);
294 }
295
296 // Support proxy as a string for those who are using the simple proxy option
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 // For single page apps, we generally want to fallback to /index.html.
318 // However we also want to respect `proxy` for API calls.
319 // So if `proxy` is specified as a string, we need to decide which fallback to use.
320 // We use a heuristic: if request `accept`s text/html, we pick /index.html.
321 // Modern browsers include text/html into `accept` header when navigating.
322 // However API calls like `fetch()` won’t generally accept text/html.
323 // If this heuristic doesn’t work well for you, use a custom `proxy` object.
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 // Browers may send Origin headers even with same-origin
333 // requests. To prevent CORS issues, we have to change
334 // the Origin to match the target URL.
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 // Otherwise, proxy is an object so create an array of proxies to pass to webpackDevServer
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 // Browers may send Origin headers even with same-origin
371 // requests. To prevent CORS issues, we have to change
372 // the Origin to match the target URL.
373 if (proxyReq.getHeader('origin')) {
374 proxyReq.setHeader('origin', target);
375 }
376 },
377 target,
378 onError: onProxyError(target),
379 });
380 });
381}
382
383function 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
430module.exports = {
431 choosePort,
432 createCompiler,
433 prepareProxy,
434 prepareUrls,
435};