WebpackDevServerUtils.js 15 KB

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