501 lines
15 KiB
JavaScript
501 lines
15 KiB
JavaScript
|
/**
|
|||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|||
|
*
|
|||
|
* This source code is licensed under the MIT license found in the
|
|||
|
* LICENSE file in the root directory of this source tree.
|
|||
|
*/
|
|||
|
'use strict';
|
|||
|
|
|||
|
const address = require('address');
|
|||
|
const fs = require('fs');
|
|||
|
const path = require('path');
|
|||
|
const url = require('url');
|
|||
|
const chalk = require('chalk');
|
|||
|
const detect = require('detect-port-alt');
|
|||
|
const isRoot = require('is-root');
|
|||
|
const inquirer = require('inquirer');
|
|||
|
const clearConsole = require('./clearConsole');
|
|||
|
const formatWebpackMessages = require('./formatWebpackMessages');
|
|||
|
const getProcessForPort = require('./getProcessForPort');
|
|||
|
const typescriptFormatter = require('./typescriptFormatter');
|
|||
|
const forkTsCheckerWebpackPlugin = require('./ForkTsCheckerWebpackPlugin');
|
|||
|
|
|||
|
const isInteractive = process.stdout.isTTY;
|
|||
|
|
|||
|
function prepareUrls(protocol, host, port, pathname = '/') {
|
|||
|
const formatUrl = hostname =>
|
|||
|
url.format({
|
|||
|
protocol,
|
|||
|
hostname,
|
|||
|
port,
|
|||
|
pathname,
|
|||
|
});
|
|||
|
const prettyPrintUrl = hostname =>
|
|||
|
url.format({
|
|||
|
protocol,
|
|||
|
hostname,
|
|||
|
port: chalk.bold(port),
|
|||
|
pathname,
|
|||
|
});
|
|||
|
|
|||
|
const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
|
|||
|
let prettyHost, lanUrlForConfig, lanUrlForTerminal;
|
|||
|
if (isUnspecifiedHost) {
|
|||
|
prettyHost = 'localhost';
|
|||
|
try {
|
|||
|
// This can only return an IPv4 address
|
|||
|
lanUrlForConfig = address.ip();
|
|||
|
if (lanUrlForConfig) {
|
|||
|
// Check if the address is a private ip
|
|||
|
// https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
|
|||
|
if (
|
|||
|
/^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
|
|||
|
lanUrlForConfig
|
|||
|
)
|
|||
|
) {
|
|||
|
// Address is private, format it for later use
|
|||
|
lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
|
|||
|
} else {
|
|||
|
// Address is not private, so we will discard it
|
|||
|
lanUrlForConfig = undefined;
|
|||
|
}
|
|||
|
}
|
|||
|
} catch (_e) {
|
|||
|
// ignored
|
|||
|
}
|
|||
|
} else {
|
|||
|
prettyHost = host;
|
|||
|
}
|
|||
|
const localUrlForTerminal = prettyPrintUrl(prettyHost);
|
|||
|
const localUrlForBrowser = formatUrl(prettyHost);
|
|||
|
return {
|
|||
|
lanUrlForConfig,
|
|||
|
lanUrlForTerminal,
|
|||
|
localUrlForTerminal,
|
|||
|
localUrlForBrowser,
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
function printInstructions(appName, urls, useYarn) {
|
|||
|
console.log();
|
|||
|
console.log(`You can now view ${chalk.bold(appName)} in the browser.`);
|
|||
|
console.log();
|
|||
|
|
|||
|
if (urls.lanUrlForTerminal) {
|
|||
|
console.log(
|
|||
|
` ${chalk.bold('Local:')} ${urls.localUrlForTerminal}`
|
|||
|
);
|
|||
|
console.log(
|
|||
|
` ${chalk.bold('On Your Network:')} ${urls.lanUrlForTerminal}`
|
|||
|
);
|
|||
|
} else {
|
|||
|
console.log(` ${urls.localUrlForTerminal}`);
|
|||
|
}
|
|||
|
|
|||
|
console.log();
|
|||
|
console.log('Note that the development build is not optimized.');
|
|||
|
console.log(
|
|||
|
`To create a production build, use ` +
|
|||
|
`${chalk.cyan(`${useYarn ? 'yarn' : 'npm run'} build`)}.`
|
|||
|
);
|
|||
|
console.log();
|
|||
|
}
|
|||
|
|
|||
|
function createCompiler({
|
|||
|
appName,
|
|||
|
config,
|
|||
|
devSocket,
|
|||
|
urls,
|
|||
|
useYarn,
|
|||
|
useTypeScript,
|
|||
|
tscCompileOnError,
|
|||
|
webpack,
|
|||
|
}) {
|
|||
|
// "Compiler" is a low-level interface to webpack.
|
|||
|
// It lets us listen to some events and provide our own custom messages.
|
|||
|
let compiler;
|
|||
|
try {
|
|||
|
compiler = webpack(config);
|
|||
|
} catch (err) {
|
|||
|
console.log(chalk.red('Failed to compile.'));
|
|||
|
console.log();
|
|||
|
console.log(err.message || err);
|
|||
|
console.log();
|
|||
|
process.exit(1);
|
|||
|
}
|
|||
|
|
|||
|
// "invalid" event fires when you have changed a file, and webpack is
|
|||
|
// recompiling a bundle. WebpackDevServer takes care to pause serving the
|
|||
|
// bundle, so if you refresh, it'll wait instead of serving the old one.
|
|||
|
// "invalid" is short for "bundle invalidated", it doesn't imply any errors.
|
|||
|
compiler.hooks.invalid.tap('invalid', () => {
|
|||
|
if (isInteractive) {
|
|||
|
clearConsole();
|
|||
|
}
|
|||
|
console.log('Compiling...');
|
|||
|
});
|
|||
|
|
|||
|
let isFirstCompile = true;
|
|||
|
let tsMessagesPromise;
|
|||
|
let tsMessagesResolver;
|
|||
|
|
|||
|
if (useTypeScript) {
|
|||
|
compiler.hooks.beforeCompile.tap('beforeCompile', () => {
|
|||
|
tsMessagesPromise = new Promise(resolve => {
|
|||
|
tsMessagesResolver = msgs => resolve(msgs);
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
forkTsCheckerWebpackPlugin
|
|||
|
.getCompilerHooks(compiler)
|
|||
|
.receive.tap('afterTypeScriptCheck', (diagnostics, lints) => {
|
|||
|
const allMsgs = [...diagnostics, ...lints];
|
|||
|
const format = message =>
|
|||
|
`${message.file}\n${typescriptFormatter(message, true)}`;
|
|||
|
|
|||
|
tsMessagesResolver({
|
|||
|
errors: allMsgs.filter(msg => msg.severity === 'error').map(format),
|
|||
|
warnings: allMsgs
|
|||
|
.filter(msg => msg.severity === 'warning')
|
|||
|
.map(format),
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// "done" event fires when webpack has finished recompiling the bundle.
|
|||
|
// Whether or not you have warnings or errors, you will get this event.
|
|||
|
compiler.hooks.done.tap('done', async stats => {
|
|||
|
if (isInteractive) {
|
|||
|
clearConsole();
|
|||
|
}
|
|||
|
|
|||
|
// We have switched off the default webpack output in WebpackDevServer
|
|||
|
// options so we are going to "massage" the warnings and errors and present
|
|||
|
// them in a readable focused way.
|
|||
|
// We only construct the warnings and errors for speed:
|
|||
|
// https://github.com/facebook/create-react-app/issues/4492#issuecomment-421959548
|
|||
|
const statsData = stats.toJson({
|
|||
|
all: false,
|
|||
|
warnings: true,
|
|||
|
errors: true,
|
|||
|
});
|
|||
|
|
|||
|
if (useTypeScript && statsData.errors.length === 0) {
|
|||
|
const delayedMsg = setTimeout(() => {
|
|||
|
console.log(
|
|||
|
chalk.yellow(
|
|||
|
'Files successfully emitted, waiting for typecheck results...'
|
|||
|
)
|
|||
|
);
|
|||
|
}, 100);
|
|||
|
|
|||
|
const messages = await tsMessagesPromise;
|
|||
|
clearTimeout(delayedMsg);
|
|||
|
if (tscCompileOnError) {
|
|||
|
statsData.warnings.push(...messages.errors);
|
|||
|
} else {
|
|||
|
statsData.errors.push(...messages.errors);
|
|||
|
}
|
|||
|
statsData.warnings.push(...messages.warnings);
|
|||
|
|
|||
|
// Push errors and warnings into compilation result
|
|||
|
// to show them after page refresh triggered by user.
|
|||
|
if (tscCompileOnError) {
|
|||
|
stats.compilation.warnings.push(...messages.errors);
|
|||
|
} else {
|
|||
|
stats.compilation.errors.push(...messages.errors);
|
|||
|
}
|
|||
|
stats.compilation.warnings.push(...messages.warnings);
|
|||
|
|
|||
|
if (messages.errors.length > 0) {
|
|||
|
if (tscCompileOnError) {
|
|||
|
devSocket.warnings(messages.errors);
|
|||
|
} else {
|
|||
|
devSocket.errors(messages.errors);
|
|||
|
}
|
|||
|
} else if (messages.warnings.length > 0) {
|
|||
|
devSocket.warnings(messages.warnings);
|
|||
|
}
|
|||
|
|
|||
|
if (isInteractive) {
|
|||
|
clearConsole();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const messages = formatWebpackMessages(statsData);
|
|||
|
const isSuccessful = !messages.errors.length && !messages.warnings.length;
|
|||
|
if (isSuccessful) {
|
|||
|
console.log(chalk.green('Compiled successfully!'));
|
|||
|
}
|
|||
|
if (isSuccessful && (isInteractive || isFirstCompile)) {
|
|||
|
printInstructions(appName, urls, useYarn);
|
|||
|
}
|
|||
|
isFirstCompile = false;
|
|||
|
|
|||
|
// If errors exist, only show errors.
|
|||
|
if (messages.errors.length) {
|
|||
|
// Only keep the first error. Others are often indicative
|
|||
|
// of the same problem, but confuse the reader with noise.
|
|||
|
if (messages.errors.length > 1) {
|
|||
|
messages.errors.length = 1;
|
|||
|
}
|
|||
|
console.log(chalk.red('Failed to compile.\n'));
|
|||
|
console.log(messages.errors.join('\n\n'));
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Show warnings if no errors were found.
|
|||
|
if (messages.warnings.length) {
|
|||
|
console.log(chalk.yellow('Compiled with warnings.\n'));
|
|||
|
console.log(messages.warnings.join('\n\n'));
|
|||
|
|
|||
|
// Teach some ESLint tricks.
|
|||
|
console.log(
|
|||
|
'\nSearch for the ' +
|
|||
|
chalk.underline(chalk.yellow('keywords')) +
|
|||
|
' to learn more about each warning.'
|
|||
|
);
|
|||
|
console.log(
|
|||
|
'To ignore, add ' +
|
|||
|
chalk.cyan('// eslint-disable-next-line') +
|
|||
|
' to the line before.\n'
|
|||
|
);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// You can safely remove this after ejecting.
|
|||
|
// We only use this block for testing of Create React App itself:
|
|||
|
const isSmokeTest = process.argv.some(
|
|||
|
arg => arg.indexOf('--smoke-test') > -1
|
|||
|
);
|
|||
|
if (isSmokeTest) {
|
|||
|
compiler.hooks.failed.tap('smokeTest', async () => {
|
|||
|
await tsMessagesPromise;
|
|||
|
process.exit(1);
|
|||
|
});
|
|||
|
compiler.hooks.done.tap('smokeTest', async stats => {
|
|||
|
await tsMessagesPromise;
|
|||
|
if (stats.hasErrors() || stats.hasWarnings()) {
|
|||
|
process.exit(1);
|
|||
|
} else {
|
|||
|
process.exit(0);
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
return compiler;
|
|||
|
}
|
|||
|
|
|||
|
function resolveLoopback(proxy) {
|
|||
|
const o = url.parse(proxy);
|
|||
|
o.host = undefined;
|
|||
|
if (o.hostname !== 'localhost') {
|
|||
|
return proxy;
|
|||
|
}
|
|||
|
// Unfortunately, many languages (unlike node) do not yet support IPv6.
|
|||
|
// This means even though localhost resolves to ::1, the application
|
|||
|
// must fall back to IPv4 (on 127.0.0.1).
|
|||
|
// We can re-enable this in a few years.
|
|||
|
/*try {
|
|||
|
o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
|
|||
|
} catch (_ignored) {
|
|||
|
o.hostname = '127.0.0.1';
|
|||
|
}*/
|
|||
|
|
|||
|
try {
|
|||
|
// Check if we're on a network; if we are, chances are we can resolve
|
|||
|
// localhost. Otherwise, we can just be safe and assume localhost is
|
|||
|
// IPv4 for maximum compatibility.
|
|||
|
if (!address.ip()) {
|
|||
|
o.hostname = '127.0.0.1';
|
|||
|
}
|
|||
|
} catch (_ignored) {
|
|||
|
o.hostname = '127.0.0.1';
|
|||
|
}
|
|||
|
return url.format(o);
|
|||
|
}
|
|||
|
|
|||
|
// We need to provide a custom onError function for httpProxyMiddleware.
|
|||
|
// It allows us to log custom error messages on the console.
|
|||
|
function onProxyError(proxy) {
|
|||
|
return (err, req, res) => {
|
|||
|
const host = req.headers && req.headers.host;
|
|||
|
console.log(
|
|||
|
chalk.red('Proxy error:') +
|
|||
|
' Could not proxy request ' +
|
|||
|
chalk.cyan(req.url) +
|
|||
|
' from ' +
|
|||
|
chalk.cyan(host) +
|
|||
|
' to ' +
|
|||
|
chalk.cyan(proxy) +
|
|||
|
'.'
|
|||
|
);
|
|||
|
console.log(
|
|||
|
'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
|
|||
|
chalk.cyan(err.code) +
|
|||
|
').'
|
|||
|
);
|
|||
|
console.log();
|
|||
|
|
|||
|
// And immediately send the proper error response to the client.
|
|||
|
// Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
|
|||
|
if (res.writeHead && !res.headersSent) {
|
|||
|
res.writeHead(500);
|
|||
|
}
|
|||
|
res.end(
|
|||
|
'Proxy error: Could not proxy request ' +
|
|||
|
req.url +
|
|||
|
' from ' +
|
|||
|
host +
|
|||
|
' to ' +
|
|||
|
proxy +
|
|||
|
' (' +
|
|||
|
err.code +
|
|||
|
').'
|
|||
|
);
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
function prepareProxy(proxy, appPublicFolder, servedPathname) {
|
|||
|
// `proxy` lets you specify alternate servers for specific requests.
|
|||
|
if (!proxy) {
|
|||
|
return undefined;
|
|||
|
}
|
|||
|
if (typeof proxy !== 'string') {
|
|||
|
console.log(
|
|||
|
chalk.red('When specified, "proxy" in package.json must be a string.')
|
|||
|
);
|
|||
|
console.log(
|
|||
|
chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')
|
|||
|
);
|
|||
|
console.log(
|
|||
|
chalk.red('Either remove "proxy" from package.json, or make it a string.')
|
|||
|
);
|
|||
|
process.exit(1);
|
|||
|
}
|
|||
|
|
|||
|
// If proxy is specified, let it handle any request except for
|
|||
|
// files in the public folder and requests to the WebpackDevServer socket endpoint.
|
|||
|
// https://github.com/facebook/create-react-app/issues/6720
|
|||
|
const sockPath = process.env.WDS_SOCKET_PATH || '/sockjs-node';
|
|||
|
const isDefaultSockHost = !process.env.WDS_SOCKET_HOST;
|
|||
|
function mayProxy(pathname) {
|
|||
|
const maybePublicPath = path.resolve(
|
|||
|
appPublicFolder,
|
|||
|
pathname.replace(new RegExp('^' + servedPathname), '')
|
|||
|
);
|
|||
|
const isPublicFileRequest = fs.existsSync(maybePublicPath);
|
|||
|
// used by webpackHotDevClient
|
|||
|
const isWdsEndpointRequest =
|
|||
|
isDefaultSockHost && pathname.startsWith(sockPath);
|
|||
|
return !(isPublicFileRequest || isWdsEndpointRequest);
|
|||
|
}
|
|||
|
|
|||
|
if (!/^http(s)?:\/\//.test(proxy)) {
|
|||
|
console.log(
|
|||
|
chalk.red(
|
|||
|
'When "proxy" is specified in package.json it must start with either http:// or https://'
|
|||
|
)
|
|||
|
);
|
|||
|
process.exit(1);
|
|||
|
}
|
|||
|
|
|||
|
let target;
|
|||
|
if (process.platform === 'win32') {
|
|||
|
target = resolveLoopback(proxy);
|
|||
|
} else {
|
|||
|
target = proxy;
|
|||
|
}
|
|||
|
return [
|
|||
|
{
|
|||
|
target,
|
|||
|
logLevel: 'silent',
|
|||
|
// For single page apps, we generally want to fallback to /index.html.
|
|||
|
// However we also want to respect `proxy` for API calls.
|
|||
|
// So if `proxy` is specified as a string, we need to decide which fallback to use.
|
|||
|
// We use a heuristic: We want to proxy all the requests that are not meant
|
|||
|
// for static assets and as all the requests for static assets will be using
|
|||
|
// `GET` method, we can proxy all non-`GET` requests.
|
|||
|
// For `GET` requests, if request `accept`s text/html, we pick /index.html.
|
|||
|
// Modern browsers include text/html into `accept` header when navigating.
|
|||
|
// However API calls like `fetch()` won’t generally accept text/html.
|
|||
|
// If this heuristic doesn’t work well for you, use `src/setupProxy.js`.
|
|||
|
context: function(pathname, req) {
|
|||
|
return (
|
|||
|
req.method !== 'GET' ||
|
|||
|
(mayProxy(pathname) &&
|
|||
|
req.headers.accept &&
|
|||
|
req.headers.accept.indexOf('text/html') === -1)
|
|||
|
);
|
|||
|
},
|
|||
|
onProxyReq: proxyReq => {
|
|||
|
// Browsers may send Origin headers even with same-origin
|
|||
|
// requests. To prevent CORS issues, we have to change
|
|||
|
// the Origin to match the target URL.
|
|||
|
if (proxyReq.getHeader('origin')) {
|
|||
|
proxyReq.setHeader('origin', target);
|
|||
|
}
|
|||
|
},
|
|||
|
onError: onProxyError(target),
|
|||
|
secure: false,
|
|||
|
changeOrigin: true,
|
|||
|
ws: true,
|
|||
|
xfwd: true,
|
|||
|
},
|
|||
|
];
|
|||
|
}
|
|||
|
|
|||
|
function choosePort(host, defaultPort) {
|
|||
|
return detect(defaultPort, host).then(
|
|||
|
port =>
|
|||
|
new Promise(resolve => {
|
|||
|
if (port === defaultPort) {
|
|||
|
return resolve(port);
|
|||
|
}
|
|||
|
const message =
|
|||
|
process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
|
|||
|
? `Admin permissions are required to run a server on a port below 1024.`
|
|||
|
: `Something is already running on port ${defaultPort}.`;
|
|||
|
if (isInteractive) {
|
|||
|
clearConsole();
|
|||
|
const existingProcess = getProcessForPort(defaultPort);
|
|||
|
const question = {
|
|||
|
type: 'confirm',
|
|||
|
name: 'shouldChangePort',
|
|||
|
message:
|
|||
|
chalk.yellow(
|
|||
|
message +
|
|||
|
`${existingProcess ? ` Probably:\n ${existingProcess}` : ''}`
|
|||
|
) + '\n\nWould you like to run the app on another port instead?',
|
|||
|
default: true,
|
|||
|
};
|
|||
|
inquirer.prompt(question).then(answer => {
|
|||
|
if (answer.shouldChangePort) {
|
|||
|
resolve(port);
|
|||
|
} else {
|
|||
|
resolve(null);
|
|||
|
}
|
|||
|
});
|
|||
|
} else {
|
|||
|
console.log(chalk.red(message));
|
|||
|
resolve(null);
|
|||
|
}
|
|||
|
}),
|
|||
|
err => {
|
|||
|
throw new Error(
|
|||
|
chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) +
|
|||
|
'\n' +
|
|||
|
('Network error message: ' + err.message || err) +
|
|||
|
'\n'
|
|||
|
);
|
|||
|
}
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
module.exports = {
|
|||
|
choosePort,
|
|||
|
createCompiler,
|
|||
|
prepareProxy,
|
|||
|
prepareUrls,
|
|||
|
};
|