UNPKG

15.3 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 prompts = require('prompts');
17const clearConsole = require('./clearConsole');
18const formatWebpackMessages = require('./formatWebpackMessages');
19const getProcessForPort = require('./getProcessForPort');
20const typescriptFormatter = require('./typescriptFormatter');
21const forkTsCheckerWebpackPlugin = require('./ForkTsCheckerWebpackPlugin');
22
23const isInteractive = process.stdout.isTTY;
24
25function prepareUrls(protocol, host, port, pathname = '/') {
26 const formatUrl = hostname =>
27 url.format({
28 protocol,
29 hostname,
30 port,
31 pathname,
32 });
33 const prettyPrintUrl = hostname =>
34 url.format({
35 protocol,
36 hostname,
37 port: chalk.bold(port),
38 pathname,
39 });
40
41 const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
42 let prettyHost, lanUrlForConfig, lanUrlForTerminal;
43 if (isUnspecifiedHost) {
44 prettyHost = 'localhost';
45 try {
46 // This can only return an IPv4 address
47 lanUrlForConfig = address.ip();
48 if (lanUrlForConfig) {
49 // Check if the address is a private ip
50 // https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
51 if (
52 /^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
53 lanUrlForConfig
54 )
55 ) {
56 // Address is private, format it for later use
57 lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
58 } else {
59 // Address is not private, so we will discard it
60 lanUrlForConfig = undefined;
61 }
62 }
63 } catch (_e) {
64 // ignored
65 }
66 } else {
67 prettyHost = host;
68 }
69 const localUrlForTerminal = prettyPrintUrl(prettyHost);
70 const localUrlForBrowser = formatUrl(prettyHost);
71 return {
72 lanUrlForConfig,
73 lanUrlForTerminal,
74 localUrlForTerminal,
75 localUrlForBrowser,
76 };
77}
78
79function printInstructions(appName, urls, useYarn) {
80 console.log();
81 console.log(`You can now view ${chalk.bold(appName)} in the browser.`);
82 console.log();
83
84 if (urls.lanUrlForTerminal) {
85 console.log(
86 ` ${chalk.bold('Local:')} ${urls.localUrlForTerminal}`
87 );
88 console.log(
89 ` ${chalk.bold('On Your Network:')} ${urls.lanUrlForTerminal}`
90 );
91 } else {
92 console.log(` ${urls.localUrlForTerminal}`);
93 }
94
95 console.log();
96 console.log('Note that the development build is not optimized.');
97 console.log(
98 `To create a production build, use ` +
99 `${chalk.cyan(`${useYarn ? 'yarn' : 'npm run'} build`)}.`
100 );
101 console.log();
102}
103
104function createCompiler({
105 appName,
106 config,
107 devSocket,
108 urls,
109 useYarn,
110 useTypeScript,
111 tscCompileOnError,
112 webpack,
113}) {
114 // "Compiler" is a low-level interface to webpack.
115 // It lets us listen to some events and provide our own custom messages.
116 let compiler;
117 try {
118 compiler = webpack(config);
119 } catch (err) {
120 console.log(chalk.red('Failed to compile.'));
121 console.log();
122 console.log(err.message || err);
123 console.log();
124 process.exit(1);
125 }
126
127 // "invalid" event fires when you have changed a file, and webpack is
128 // recompiling a bundle. WebpackDevServer takes care to pause serving the
129 // bundle, so if you refresh, it'll wait instead of serving the old one.
130 // "invalid" is short for "bundle invalidated", it doesn't imply any errors.
131 compiler.hooks.invalid.tap('invalid', () => {
132 if (isInteractive) {
133 clearConsole();
134 }
135 console.log('Compiling...');
136 });
137
138 let isFirstCompile = true;
139 let tsMessagesPromise;
140 let tsMessagesResolver;
141
142 if (useTypeScript) {
143 compiler.hooks.beforeCompile.tap('beforeCompile', () => {
144 tsMessagesPromise = new Promise(resolve => {
145 tsMessagesResolver = msgs => resolve(msgs);
146 });
147 });
148
149 forkTsCheckerWebpackPlugin
150 .getCompilerHooks(compiler)
151 .receive.tap('afterTypeScriptCheck', (diagnostics, lints) => {
152 const allMsgs = [...diagnostics, ...lints];
153 const format = message =>
154 `${message.file}\n${typescriptFormatter(message, true)}`;
155
156 tsMessagesResolver({
157 errors: allMsgs.filter(msg => msg.severity === 'error').map(format),
158 warnings: allMsgs
159 .filter(msg => msg.severity === 'warning')
160 .map(format),
161 });
162 });
163 }
164
165 // "done" event fires when webpack has finished recompiling the bundle.
166 // Whether or not you have warnings or errors, you will get this event.
167 compiler.hooks.done.tap('done', async stats => {
168 if (isInteractive) {
169 clearConsole();
170 }
171
172 // We have switched off the default webpack output in WebpackDevServer
173 // options so we are going to "massage" the warnings and errors and present
174 // them in a readable focused way.
175 // We only construct the warnings and errors for speed:
176 // https://github.com/facebook/create-react-app/issues/4492#issuecomment-421959548
177 const statsData = stats.toJson({
178 all: false,
179 warnings: true,
180 errors: true,
181 });
182
183 if (useTypeScript && statsData.errors.length === 0) {
184 const delayedMsg = setTimeout(() => {
185 console.log(
186 chalk.yellow(
187 'Files successfully emitted, waiting for typecheck results...'
188 )
189 );
190 }, 100);
191
192 const messages = await tsMessagesPromise;
193 clearTimeout(delayedMsg);
194 if (tscCompileOnError) {
195 statsData.warnings.push(...messages.errors);
196 } else {
197 statsData.errors.push(...messages.errors);
198 }
199 statsData.warnings.push(...messages.warnings);
200
201 // Push errors and warnings into compilation result
202 // to show them after page refresh triggered by user.
203 if (tscCompileOnError) {
204 stats.compilation.warnings.push(...messages.errors);
205 } else {
206 stats.compilation.errors.push(...messages.errors);
207 }
208 stats.compilation.warnings.push(...messages.warnings);
209
210 if (messages.errors.length > 0) {
211 if (tscCompileOnError) {
212 devSocket.warnings(messages.errors);
213 } else {
214 devSocket.errors(messages.errors);
215 }
216 } else if (messages.warnings.length > 0) {
217 devSocket.warnings(messages.warnings);
218 }
219
220 if (isInteractive) {
221 clearConsole();
222 }
223 }
224
225 const messages = formatWebpackMessages(statsData);
226 const isSuccessful = !messages.errors.length && !messages.warnings.length;
227 if (isSuccessful) {
228 console.log(chalk.green('Compiled successfully!'));
229 }
230 if (isSuccessful && (isInteractive || isFirstCompile)) {
231 printInstructions(appName, urls, useYarn);
232 }
233 isFirstCompile = false;
234
235 // If errors exist, only show errors.
236 if (messages.errors.length) {
237 // Only keep the first error. Others are often indicative
238 // of the same problem, but confuse the reader with noise.
239 if (messages.errors.length > 1) {
240 messages.errors.length = 1;
241 }
242 console.log(chalk.red('Failed to compile.\n'));
243 console.log(messages.errors.join('\n\n'));
244 return;
245 }
246
247 // Show warnings if no errors were found.
248 if (messages.warnings.length) {
249 console.log(chalk.yellow('Compiled with warnings.\n'));
250 console.log(messages.warnings.join('\n\n'));
251
252 // Teach some ESLint tricks.
253 console.log(
254 '\nSearch for the ' +
255 chalk.underline(chalk.yellow('keywords')) +
256 ' to learn more about each warning.'
257 );
258 console.log(
259 'To ignore, add ' +
260 chalk.cyan('// eslint-disable-next-line') +
261 ' to the line before.\n'
262 );
263 }
264 });
265
266 // You can safely remove this after ejecting.
267 // We only use this block for testing of Create React App itself:
268 const isSmokeTest = process.argv.some(
269 arg => arg.indexOf('--smoke-test') > -1
270 );
271 if (isSmokeTest) {
272 compiler.hooks.failed.tap('smokeTest', async () => {
273 await tsMessagesPromise;
274 process.exit(1);
275 });
276 compiler.hooks.done.tap('smokeTest', async stats => {
277 await tsMessagesPromise;
278 if (stats.hasErrors() || stats.hasWarnings()) {
279 process.exit(1);
280 } else {
281 process.exit(0);
282 }
283 });
284 }
285
286 return compiler;
287}
288
289function resolveLoopback(proxy) {
290 const o = url.parse(proxy);
291 o.host = undefined;
292 if (o.hostname !== 'localhost') {
293 return proxy;
294 }
295 // Unfortunately, many languages (unlike node) do not yet support IPv6.
296 // This means even though localhost resolves to ::1, the application
297 // must fall back to IPv4 (on 127.0.0.1).
298 // We can re-enable this in a few years.
299 /*try {
300 o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
301 } catch (_ignored) {
302 o.hostname = '127.0.0.1';
303 }*/
304
305 try {
306 // Check if we're on a network; if we are, chances are we can resolve
307 // localhost. Otherwise, we can just be safe and assume localhost is
308 // IPv4 for maximum compatibility.
309 if (!address.ip()) {
310 o.hostname = '127.0.0.1';
311 }
312 } catch (_ignored) {
313 o.hostname = '127.0.0.1';
314 }
315 return url.format(o);
316}
317
318// We need to provide a custom onError function for httpProxyMiddleware.
319// It allows us to log custom error messages on the console.
320function onProxyError(proxy) {
321 return (err, req, res) => {
322 const host = req.headers && req.headers.host;
323 console.log(
324 chalk.red('Proxy error:') +
325 ' Could not proxy request ' +
326 chalk.cyan(req.url) +
327 ' from ' +
328 chalk.cyan(host) +
329 ' to ' +
330 chalk.cyan(proxy) +
331 '.'
332 );
333 console.log(
334 'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
335 chalk.cyan(err.code) +
336 ').'
337 );
338 console.log();
339
340 // And immediately send the proper error response to the client.
341 // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
342 if (res.writeHead && !res.headersSent) {
343 res.writeHead(500);
344 }
345 res.end(
346 'Proxy error: Could not proxy request ' +
347 req.url +
348 ' from ' +
349 host +
350 ' to ' +
351 proxy +
352 ' (' +
353 err.code +
354 ').'
355 );
356 };
357}
358
359function prepareProxy(proxy, appPublicFolder, servedPathname) {
360 // `proxy` lets you specify alternate servers for specific requests.
361 if (!proxy) {
362 return undefined;
363 }
364 if (typeof proxy !== 'string') {
365 console.log(
366 chalk.red('When specified, "proxy" in package.json must be a string.')
367 );
368 console.log(
369 chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')
370 );
371 console.log(
372 chalk.red('Either remove "proxy" from package.json, or make it a string.')
373 );
374 process.exit(1);
375 }
376
377 // If proxy is specified, let it handle any request except for
378 // files in the public folder and requests to the WebpackDevServer socket endpoint.
379 // https://github.com/facebook/create-react-app/issues/6720
380 const sockPath = process.env.WDS_SOCKET_PATH || '/sockjs-node';
381 const isDefaultSockHost = !process.env.WDS_SOCKET_HOST;
382 function mayProxy(pathname) {
383 const maybePublicPath = path.resolve(
384 appPublicFolder,
385 pathname.replace(new RegExp('^' + servedPathname), '')
386 );
387 const isPublicFileRequest = fs.existsSync(maybePublicPath);
388 // used by webpackHotDevClient
389 const isWdsEndpointRequest =
390 isDefaultSockHost && pathname.startsWith(sockPath);
391 return !(isPublicFileRequest || isWdsEndpointRequest);
392 }
393
394 if (!/^http(s)?:\/\//.test(proxy)) {
395 console.log(
396 chalk.red(
397 'When "proxy" is specified in package.json it must start with either http:// or https://'
398 )
399 );
400 process.exit(1);
401 }
402
403 let target;
404 if (process.platform === 'win32') {
405 target = resolveLoopback(proxy);
406 } else {
407 target = proxy;
408 }
409 return [
410 {
411 target,
412 logLevel: 'silent',
413 // For single page apps, we generally want to fallback to /index.html.
414 // However we also want to respect `proxy` for API calls.
415 // So if `proxy` is specified as a string, we need to decide which fallback to use.
416 // We use a heuristic: We want to proxy all the requests that are not meant
417 // for static assets and as all the requests for static assets will be using
418 // `GET` method, we can proxy all non-`GET` requests.
419 // For `GET` requests, if request `accept`s text/html, we pick /index.html.
420 // Modern browsers include text/html into `accept` header when navigating.
421 // However API calls like `fetch()` won’t generally accept text/html.
422 // If this heuristic doesn’t work well for you, use `src/setupProxy.js`.
423 context: function (pathname, req) {
424 return (
425 req.method !== 'GET' ||
426 (mayProxy(pathname) &&
427 req.headers.accept &&
428 req.headers.accept.indexOf('text/html') === -1)
429 );
430 },
431 onProxyReq: proxyReq => {
432 // Browsers may send Origin headers even with same-origin
433 // requests. To prevent CORS issues, we have to change
434 // the Origin to match the target URL.
435 if (proxyReq.getHeader('origin')) {
436 proxyReq.setHeader('origin', target);
437 }
438 },
439 onError: onProxyError(target),
440 secure: false,
441 changeOrigin: true,
442 ws: true,
443 xfwd: true,
444 },
445 ];
446}
447
448function choosePort(host, defaultPort) {
449 return detect(defaultPort, host).then(
450 port =>
451 new Promise(resolve => {
452 if (port === defaultPort) {
453 return resolve(port);
454 }
455 const message =
456 process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
457 ? `Admin permissions are required to run a server on a port below 1024.`
458 : `Something is already running on port ${defaultPort}.`;
459 if (isInteractive) {
460 clearConsole();
461 const existingProcess = getProcessForPort(defaultPort);
462 const question = {
463 type: 'confirm',
464 name: 'shouldChangePort',
465 message:
466 chalk.yellow(
467 message +
468 `${existingProcess ? ` Probably:\n ${existingProcess}` : ''}`
469 ) + '\n\nWould you like to run the app on another port instead?',
470 initial: true,
471 };
472 prompts(question).then(answer => {
473 if (answer.shouldChangePort) {
474 resolve(port);
475 } else {
476 resolve(null);
477 }
478 });
479 } else {
480 console.log(chalk.red(message));
481 resolve(null);
482 }
483 }),
484 err => {
485 throw new Error(
486 chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) +
487 '\n' +
488 ('Network error message: ' + err.message || err) +
489 '\n'
490 );
491 }
492 );
493}
494
495module.exports = {
496 choosePort,
497 createCompiler,
498 prepareProxy,
499 prepareUrls,
500};