@apployees-nx/webserver
Version:
A create-react-app inspired plugin for Nx, with SSR and PWA capabilities.
427 lines (392 loc) • 14.6 kB
text/typescript
/**
* 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.
*/
;
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("react-dev-utils/clearConsole");
const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
const getProcessForPort = require("react-dev-utils/getProcessForPort");
const typescriptFormatter = require("react-dev-utils/typescriptFormatter");
const forkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpackPlugin");
const isInteractive = process.stdout.isTTY;
export function prepareUrls(protocol, host, port) {
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,
};
}
export 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();
}
export 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(`(${appName}) 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(`${appName}-invalid`, () => {
if (isInteractive) {
clearConsole();
}
console.log(`(${appName}) Compiling...`);
});
let isFirstCompile = true;
let tsMessagesPromise;
let tsMessagesResolver;
if (useTypeScript) {
compiler.hooks.beforeCompile.tap(`${appName}-beforeCompile`, () => {
tsMessagesPromise = new Promise((resolve) => {
tsMessagesResolver = (msgs) => resolve(msgs);
});
});
forkTsCheckerWebpackPlugin
.getCompilerHooks(compiler)
.receive.tap(`${appName}-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(`${appName}-done`, async (stats) => {
// 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) {
console.log(chalk.yellow(`(${appName}) Files successfully emitted, waiting for typecheck results...`));
const messages = await tsMessagesPromise;
console.log(chalk.yellow(`(${appName}) typecheck results completed.`));
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);
}
}
const messages = formatWebpackMessages(statsData);
const isSuccessful = !messages.errors.length && !messages.warnings.length;
if (isSuccessful) {
console.log(chalk.green(`(${appName}) Compiled successfully!`));
}
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(`(${appName}) 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(`(${appName}) 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(`${appName}-smokeTest`, async () => {
await tsMessagesPromise;
process.exit(1);
});
compiler.hooks.done.tap(`${appName}-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 + ").",
);
};
}
export function prepareProxy(proxy, appPublicFolder) {
// `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
function mayProxy(pathname) {
const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1));
const isPublicFileRequest = fs.existsSync(maybePublicPath);
const isWdsEndpointRequest = pathname.startsWith("/sockjs-node"); // used by webpackHotDevClient
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,
},
];
}
export 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",
);
},
);
}