@aniyajs/rotor
Version:
基于webpack5开发的一款专注于打包、运行的工具
428 lines (387 loc) • 11.7 kB
JavaScript
const detect = require("detect-port-alt");
const isRoot = require("is-root");
const prompts = require("prompts");
const chalk = require("chalk");
const url = require("url");
const address = require("address");
const { clearConsole } = require("./common");
const forkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const isInteractive = process.stdout.isTTY;
/**
* Details of the current application
*
* @param {*} appName
* @param {*} appVersion
* @param {*} urls
* @param {*} useYarn
* @param {*} ms
*/
function printInstructions(appName, appVersion, urls, useYarn) {
console.log();
console.log();
console.log(
`App ${chalk.blue(appName)} ${chalk.gray(
"v" + appVersion,
)} is start, now accessible in browser.`,
);
console.log();
console.log(
` ${chalk.gray("--")} Local: ${chalk.cyan(
urls.localUrlForTerminal,
)}`,
);
console.log(
` ${chalk.gray("--")} On your network: ${chalk.cyan(
urls.lanUrlForTerminal,
)}`,
);
console.log();
console.log("Please note that the development version is not optimized.");
console.log(
`To create a development version, use ${chalk.cyan(
(useYarn ? "yarn" : "npm run") + " build",
)}.`,
);
console.log();
console.log();
}
/**
* 选择可使用的端口
*
* @param {*} host
* @param {*} defaultPort
* @returns
*/
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()
? "在低于1024的端口上运行服务器需要管理员权限"
: `${defaultPort}端口已被占用`;
if (isInteractive) {
const questions = {
type: "confirm",
name: "shouldChangePort",
message:
chalk.yellow(message) +
"\n\n你想要在另一个端口上运行应用程序吗?",
initial: true,
};
prompts(questions).then((answer) => {
if (answer.shouldChangePort) {
resolve(_port);
} else {
resolve(null);
}
});
} else {
console.log(chalk.red(message));
resolve(null);
}
}),
(err) => {
throw new Error(
chalk.red(`在${chalk.bold(host)}上找不到开放的端口。`) +
"\n" +
("网络错误信息:" + err.message || err) +
"\n",
);
},
);
}
/**
* 准备好的url配置
*
* @param {string} protocol
* @param {string} host
* @param {number} port
* @param {string} [pathname="/"]
* @return {object}
*/
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 {
// Returns an IPv4 address.
lanUrlForConfig = address.ip();
if (lanUrlForConfig) {
// Check whether the address is a private ip address.
if (
/^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
lanUrlForConfig,
)
) {
// The address is private and formatted for later use.
lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
} else {
// The address is not private, Discard.
lanUrlForTerminal = undefined;
}
}
} catch (_error) {
// ignore
}
} else {
prettyHost = host;
}
const localUrlForTerminal = prettyPrintUrl(prettyHost);
const localUrlForBrowser = formatUrl(prettyHost);
return {
lanUrlForConfig,
lanUrlForTerminal,
localUrlForTerminal,
localUrlForBrowser,
};
}
/**
* 创建一个自定义消息的webpack编译器
*
* @param {*} {
* webpack,
* latestConfig,
* useTypeScript,
* appName,
* appVersion,
* urls,
* useYarn,
* }
* @return {*}
*/
function createCompiler({
webpack,
latestConfig,
useTypeScript,
appName,
appVersion,
urls,
useYarn,
}) {
let compiler;
try {
compiler = webpack(latestConfig);
} catch (error) {
console.log(chalk.red("Failed to compile."));
console.log();
console.log(error.message || error);
console.log();
process.exit(1);
}
// 观察中的 compilation 无效时执行
// 比如更改了 package.json 文件
compiler.hooks.invalid.tap("invalid", () => {
if (isInteractive) {
clearConsole();
}
console.log("Compiling...");
});
let isFirstCompile = true;
let tsMessagesPromise;
if (useTypeScript) {
forkTsCheckerWebpackPlugin
.getCompilerHooks(compiler)
.waiting.tap("awaitingTypeScriptCheck", () => {
console.log(chalk.yellow("文件发送成功,正在等待类型检查结果…"));
});
}
compiler.hooks.done.tap("done", (stats) => {
if (isInteractive) {
clearConsole();
}
// because has ForkTsCheckerWebpackPlugin, so
// we don't need a custom error message here
// if your app is js ?
const statsData = stats.toJson({
all: false,
warnings: true,
errors: true,
});
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, appVersion, urls, useYarn, statsData.time);
}
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;
}
const friendlySyntaxErrorLabel = "Syntax error:";
function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
// Cleans up webpack error messages.
function formatMessage(message) {
let lines = [];
if (typeof message === "string") {
lines = message.split("\n");
} else if ("message" in message) {
lines = message.message.split("\n");
} else if (Array.isArray(message)) {
message.forEach((message) => {
if ("message" in message) {
lines = message.message.split("\n");
}
});
}
// Strip webpack-added headers off errors/warnings
// https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
lines = lines.filter((line) => !/Module [A-z ]+\(from/.test(line));
// Transform parsing error into syntax error
// TODO: move this to our ESLint formatter?
lines = lines.map((line) => {
const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(
line,
);
if (!parsingError) {
return line;
}
const [, errorLine, errorColumn, errorMessage] = parsingError;
return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`;
});
message = lines.join("\n");
// Smoosh syntax errors (commonly found in CSS)
message = message.replace(
/SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
`${friendlySyntaxErrorLabel} $3 ($1:$2)\n`,
);
// Clean up export errors
message = message.replace(
/^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
"Attempted import error: '$1' is not exported from '$2'.",
);
message = message.replace(
/^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
"Attempted import error: '$2' does not contain a default export (imported as '$1').",
);
message = message.replace(
/^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
"Attempted import error: '$1' is not exported from '$3' (imported as '$2').",
);
lines = message.split("\n");
// Remove leading newline
if (lines.length > 2 && lines[1].trim() === "") {
lines.splice(1, 1);
}
// Clean up file name
lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, "$1");
// Cleans up verbose "module not found" messages for files and packages.
if (lines[1] && lines[1].indexOf("Module not found: ") === 0) {
lines = [
lines[0],
lines[1]
.replace("Error: ", "")
.replace("Module not found: Cannot find file:", "Cannot find file:"),
];
}
// Add helpful message for users trying to use Sass for the first time
if (lines[1] && lines[1].match(/Cannot find module.+sass/)) {
lines[1] = "To import Sass files, you first need to install sass.\n";
lines[1] +=
"Run `npm install sass` or `yarn add sass` inside your workspace.";
}
message = lines.join("\n");
// Internal stacks are generally useless so we strip them... with the
// exception of stacks containing `webpack:` because they're normally
// from user code generated by webpack. For more information see
// https://github.com/facebook/create-react-app/pull/1050
message = message.replace(
/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm,
"",
); // at ... ...:x:y
message = message.replace(/^\s*at\s<anonymous>(\n|$)/gm, ""); // at <anonymous>
lines = message.split("\n");
// Remove duplicated newlines
lines = lines.filter(
(line, index, arr) =>
index === 0 ||
line.trim() !== "" ||
line.trim() !== arr[index - 1].trim(),
);
// Reassemble the message
message = lines.join("\n");
return message.trim();
}
function formatWebpackMessages(json) {
const formattedErrors = json.errors.map(formatMessage);
const formattedWarnings = json.warnings.map(formatMessage);
const result = { errors: formattedErrors, warnings: formattedWarnings };
if (result.errors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
result.errors = result.errors.filter(isLikelyASyntaxError);
}
return result;
}
module.exports = {
choosePort,
prepareUrls,
createCompiler,
};