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