UNPKG

@aniyajs/rotor

Version:

基于webpack5开发的一款专注于打包、运行的工具

428 lines (387 loc) 11.7 kB
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, };