diff --git a/.eslintrc b/.eslintrc index f17d658..1fa6c69 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,10 @@ "browser": false }, + "parserOptions": { + "sourceType": "module" + }, + "plugins": [ "flowtype" ], diff --git a/package.json b/package.json index b6d2605..5e53890 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ }, "pre-commit": [ "lint:flow", - "lint:staged" + "lint:staged", + "test" ], "lint-staged": { "*.js": [ @@ -102,14 +103,14 @@ "babel-plugin-syntax-flow": "6.18.0", "babel-plugin-transform-async-to-generator": "6.24.1", "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-polyfill": "^6.23.0", + "babel-polyfill": "6.23.0", "conventional-github-releaser": "1.1.11", - "coveralls": "^2.13.1", + "coveralls": "2.13.1", "cz-conventional-changelog": "2.0.0", "eslint-plugin-flowtype": "2.34.0", "flow-bin": "0.48.0", - "jest": "^20.0.4", - "lint-staged": "^3.4.0", + "jest": "20.0.4", + "lint-staged": "3.4.0", "pmm": "1.3.1", "pre-commit": "1.2.2", "prettier": "1.4.4", @@ -119,5 +120,13 @@ "commitizen": { "path": "./node_modules/cz-conventional-changelog" } + }, + "jest": { + "roots": [ + "./src" + ], + "collectCoverageFrom": [ + "src/**/*.js" + ] } } diff --git a/src/commands/build/webpack-build.js b/src/commands/build/webpack-build.js index be7a594..1090011 100644 --- a/src/commands/build/webpack-build.js +++ b/src/commands/build/webpack-build.js @@ -6,6 +6,8 @@ import webpack from "webpack"; import gzipSize from "gzip-size"; import webpackConfigBuilder from "./../../webpack/config-builder"; import { + print, + addTopSpace, builderBanner, builderRemovingDistMsg, builderRunningBuildMsg, @@ -32,26 +34,31 @@ export default async function runWebpackBuilder( try { fs.statSync(filename); } catch (error) { - fileDoesNotExistMsg(filename); + print(fileDoesNotExistMsg(filename), /* clear console */ true); return; } const config = webpackConfigBuilder(filename, flags, params); const compiler = webpack(config); - builderBanner(filename, flags, params); - builderRemovingDistMsg(params.dist.path); + print(builderBanner(filename, flags, params), /* clear console */ true); + print(addTopSpace(builderRemovingDistMsg(params.dist.path))); await removeDist(params.dist.path); - builderRunningBuildMsg(); + print(builderRunningBuildMsg()); return new Promise((resolve, reject) => { compiler.run((err, stats) => { const json = stats.toJson({}, true); if (err || stats.hasErrors()) { - builderErrorMsg(err || json.errors); + print( + builderErrorMsg(err || json.errors), + /* clear console */ true, + /* add sep */ true + ); + return reject(); } @@ -67,7 +74,13 @@ export default async function runWebpackBuilder( sizeGz: gzipSize.sync(content) / 1024 }; }); - builderSuccessMsg(params.dist.short, { buildDuration, assets }); + + print( + builderSuccessMsg(params.dist.short, { buildDuration, assets }), + /* clear console */ true, + /* add sep */ true + ); + resolve(compiler); }); }); diff --git a/src/commands/dev-server/index.js b/src/commands/dev-server/index.js index 6ee5f54..4714839 100644 --- a/src/commands/dev-server/index.js +++ b/src/commands/dev-server/index.js @@ -7,6 +7,8 @@ import createWebpackDevServer from "./webpack-dev-server"; import createNgrokTunnel from "./ngrok"; import createParams from "./../../utils/params"; import { + print, + addBottomSpace, devServerFileDoesNotExistMsg, devServerInvalidBuildMsg, fileDoesNotExistMsg, @@ -33,7 +35,7 @@ export function requestCreatingAnEntryPoint( return resolve(true); } - fileDoesNotExistMsg(filename); + print(fileDoesNotExistMsg(filename), /* clear console */ true); reject(); }); @@ -55,7 +57,10 @@ async function prepareEntryPoint(filename: string) { try { fs.statSync(filename); } catch (error) { - devServerFileDoesNotExistMsg(filename); + print( + addBottomSpace(devServerFileDoesNotExistMsg(filename)), + /* clear console */ true + ); const shouldCreateAnEntryPoint = await requestCreatingAnEntryPoint( filename @@ -72,7 +77,7 @@ function prepareForReact() { const needReactDom = !isModuleInstalled("react-dom"); if (needReact || needReactDom) { - devServerReactRequired(); + print(devServerReactRequired()); } needReact && installModule("react"); @@ -90,7 +95,7 @@ export default async function aikDevServer( await prepareEntryPoint(filename); - devServerInvalidBuildMsg(); + print(devServerInvalidBuildMsg(), /* clear console */ true); installAllModules(process.cwd()); if (flags.react) { diff --git a/src/commands/dev-server/webpack-dev-server.js b/src/commands/dev-server/webpack-dev-server.js index 41e5dfd..6b09eba 100644 --- a/src/commands/dev-server/webpack-dev-server.js +++ b/src/commands/dev-server/webpack-dev-server.js @@ -7,11 +7,13 @@ import WebpackDevServer from "webpack-dev-server"; import webpackConfigBuilder from "./../../webpack/config-builder"; import detectPort from "./../../utils/detect-port"; import testUtils from "./../../utils/test-utils"; +import { formatMessages } from "./../../utils/error-helpers"; import { - isLikelyASyntaxError, - formatMessage -} from "./../../utils/error-helpers"; -import { + print, + addTopSpace, + joinWithSpace, + joinWithSeparator, + separator, clearConsole, eslintExtraWarningMsg, devServerInvalidBuildMsg, @@ -41,18 +43,16 @@ export function onDone( clearConsole(true); if (!hasErrors && !hasWarnings) { - devServerCompiledSuccessfullyMsg(filename, flags, params, buildDuration); + print( + devServerCompiledSuccessfullyMsg(filename, flags, params, buildDuration) + ); testUtils(); return; } const json = stats.toJson({}, true); - const formattedWarnings = json.warnings.map( - message => "Warning in " + formatMessage(message) - ); - let formattedErrors = json.errors.map( - message => "Error in " + formatMessage(message) - ); + const formattedWarnings = formatMessages(json.warnings); + const formattedErrors = formatMessages(json.errors); if (hasErrors) { if ( @@ -63,25 +63,39 @@ export function onDone( return; } - devServerFailedToCompileMsg(); - - // If there are any syntax errors, show just them. - // This prevents a confusing ESLint parsing error - // preceding a much more useful Babel syntax error. - if (formattedErrors.some(isLikelyASyntaxError)) { - formattedErrors = formattedErrors.filter(isLikelyASyntaxError); - } + print( + devServerFailedToCompileMsg(), + /* clear console */ true, + /* add sep */ true + ); // If errors exist, ignore warnings. - formattedErrors.forEach(message => console.log("\n", message)); // eslint-disable-line - testUtils(); - return; + if (formattedErrors.length) { + print( + addTopSpace( + joinWithSeparator(`\n\n${separator()}\n\n`, formattedErrors) + ) + ); + testUtils(); + return; + } } - if (hasWarnings) { - devServerCompiledWithWarningsMsg(filename, flags, params, buildDuration); - formattedWarnings.forEach(message => console.log("\n", message)); // eslint-disable-line - eslintExtraWarningMsg(); + if (hasWarnings && formattedWarnings.length) { + print( + joinWithSeparator(`\n${separator()}\n`, [ + joinWithSpace([ + devServerCompiledWithWarningsMsg( + filename, + flags, + params, + buildDuration + ), + formattedWarnings.join(`\n\n${separator()}\n\n`) + ]), + eslintExtraWarningMsg() + ]) + ); } testUtils(); @@ -97,9 +111,10 @@ export function createWebpackCompiler( config: Object, invalidate: Function ) { - // eslint-disable-line const compiler = webpack(config); - compiler.plugin("invalid", devServerInvalidBuildMsg); + compiler.plugin("invalid", () => + print(devServerInvalidBuildMsg(), /* clear console */ true) + ); compiler.plugin( "done", onDone.bind(null, filename, flags, params, compiler, invalidate) @@ -138,12 +153,19 @@ export default async function createWebpackDevServer( try { resolveModule.sync(moduleName, { basedir: process.cwd() }); - devServerRestartMsg(moduleName); + print( + devServerRestartMsg(moduleName), + /* clear console */ true, + /* add sep */ true + ); server.close(); createWebpackDevServer(filename, flags, params); } catch (e) { - // eslint-disable-line - devServerModuleDoesntExists(moduleName, fileWithError); + print( + devServerModuleDoesntExists(moduleName, fileWithError), + /* clear console */ true, + /* add sep */ true + ); } }; diff --git a/src/utils/__test__/__snapshots__/error-helpers.test.js.snap b/src/utils/__test__/__snapshots__/error-helpers.test.js.snap new file mode 100644 index 0000000..2db8a54 --- /dev/null +++ b/src/utils/__test__/__snapshots__/error-helpers.test.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#formatMessages Format ESLint Warnings 1`] = ` +" 5:16 warning React.createClass is deprecated since React 15.5.0, use the npm module create-react-class instead react/no-deprecated +40:11 warning No duplicate props allowed react/jsx-no-duplicate-props + +Where: ./thinking-in-react/src/components/filtered-product-table/filtered-product-table.js + + +16:11 warning No duplicate props allowed react/jsx-no-duplicate-props + +Where: ./thinking-in-react/src/components/product-table/product-table.js" +`; + +exports[`#formatMessages Format Syntax Error 1`] = ` +"Error: + +| Syntax Error: Unexpected token (20:7) + +Where: ./thinking-in-react/src/components/product-table/product-table.js + +  18 |  return ( +  19 |  <div className=\\"productTable\\"> +> 20 |  <<table> +  |  ^ +  21 |  <thead> +  22 |  <tr> +  23 |  <th>Name</th>" +`; + +exports[`#formatMessages Format Webpack Warnings 1`] = `""`; + +exports[`#formatMessages Unknown messages 1`] = ` +Array [ + "some", + "custom", + "messages", +] +`; diff --git a/src/utils/__test__/__snapshots__/messages.test.js.snap b/src/utils/__test__/__snapshots__/messages.test.js.snap new file mode 100644 index 0000000..b369c44 --- /dev/null +++ b/src/utils/__test__/__snapshots__/messages.test.js.snap @@ -0,0 +1,191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Build Messages #builderBanner all flags and params 1`] = ` +" WAIT  Building... + +Entry point: ./src/index.js +Custom template: index.html +Base path: /subfolder/" +`; + +exports[`Build Messages #builderBanner basic 1`] = ` +" WAIT  Building... + +Entry point: ./src/index.js" +`; + +exports[`Build Messages #builderBanner no base 1`] = ` +" WAIT  Building... + +Entry point: ./src/index.js +Custom template: index.html" +`; + +exports[`Build Messages #builderBanner no template 1`] = ` +" WAIT  Building... + +Entry point: ./src/index.js +Base path: /subfolder/" +`; + +exports[`Build Messages #builderErrorMsg? Custom Error message 1`] = ` +" ERROR  Failed to create a production build. Reason: + +Some error message" +`; + +exports[`Build Messages #builderErrorMsg? Syntax Error 1`] = ` +" ERROR  Failed to create a production build. Reason: + +Error: + +| Syntax Error: Unexpected token (20:7) + +Where: ./thinking-in-react/src/components/product-table/product-table.js + +  18 |  return ( +  19 |  <div className=\\"productTable\\"> +> 20 |  <<table> +  |  ^ +  21 |  <thead> +  22 |  <tr> +  23 |  <th>Name</th>" +`; + +exports[`Build Messages #builderRemovingDistMsg 1`] = `"Removing folder: ./dist"`; + +exports[`Build Messages #builderRunningBuildMsg 1`] = `"Running webpack production build..."`; + +exports[`Build Messages #builderSuccessMsg 1`] = ` +" DONE  in 3892ms + +Aik has successfully generated a bundle in the \\"./dist\\" folder! +The bundle is optimized and ready to be deployed to production. + + ASSETS  + +index.2b2f089a.js:  22.90kb, 7.33kb gzip +index.1027be30.css: 0.67kb, 0.31kb gzip +index.html:  0.34kb, 0.24kb gzip" +`; + +exports[`Common Messages #eslintExtraWarningMsg 1`] = ` +"You may use special comments to disable some warnings. +Use // eslint-disable-next-line to ignore the next line. +Use /* eslint-disable */ to ignore all warnings in a file." +`; + +exports[`Common Messages #fileDoesNotExistMsg 1`] = ` +" ERROR  File doesn't exist. + +You are trying to use \\"./src/index.js\\" as an entry point, but this file doesn't exist. +Please, choose an existing file or create \\"./src/index.js\\" manualy." +`; + +exports[`Common Messages #foundPackageJson 1`] = ` +" WARNING  File \\"package.json\\" has been discovered. + +Since \\"node_modules\\" folder doesn't exist and in order to avoid possible artifacts caused by +accidentally updated versions of npm modules Aik will run \\"npm install\\" in current directory. + + WAIT  Installing npm modules..." +`; + +exports[`Common Messages #installingModuleMsg 1`] = `"Installing module \\"react\\" ..."`; + +exports[`Dev Server Messages #devServerBanner all flags enabled 1`] = ` +"Entry point: ./src/index.js +Custom template: index.html +Port changed:  3333  ->  3334  +Server: http://localhost:3334 +Ngrok: http://43kd92j3h.ngrok.com +React Hot Loader: enabled" +`; + +exports[`Dev Server Messages #devServerBanner no ngrok 1`] = ` +"Entry point: ./src/index.js +Custom template: index.html +Port changed:  3333  ->  3334  +Server: http://localhost:3334 +React Hot Loader: enabled" +`; + +exports[`Dev Server Messages #devServerBanner no old port 1`] = ` +"Entry point: ./src/index.js +Custom template: index.html +Server: http://localhost:3334 +Ngrok: http://43kd92j3h.ngrok.com +React Hot Loader: enabled" +`; + +exports[`Dev Server Messages #devServerBanner no react 1`] = ` +"Entry point: ./src/index.js +Custom template: index.html +Port changed:  3333  ->  3334  +Server: http://localhost:3334 +Ngrok: http://43kd92j3h.ngrok.com" +`; + +exports[`Dev Server Messages #devServerBanner no template 1`] = ` +"Entry point: ./src/index.js +Port changed:  3333  ->  3334  +Server: http://localhost:3334 +Ngrok: http://43kd92j3h.ngrok.com +React Hot Loader: enabled" +`; + +exports[`Dev Server Messages #devServerCompiledSuccessfullyMsg 1`] = ` +" DONE  Compiled successfully in 5800ms! + +Entry point: ./src/index.js +Custom template: index.html +Port changed:  3333  ->  3334  +Server: http://localhost:3334 +Ngrok: http://43kd92j3h.ngrok.com +React Hot Loader: enabled" +`; + +exports[`Dev Server Messages #devServerCompiledWithWarningsMsg 1`] = ` +" WARNING  Compiled with warnings in 5800ms. + +Entry point: ./src/index.js +Custom template: index.html +Port changed:  3333  ->  3334  +Server: http://localhost:3334 +Ngrok: http://43kd92j3h.ngrok.com +React Hot Loader: enabled + +---------" +`; + +exports[`Dev Server Messages #devServerFailedToCompileMsg 1`] = `" ERROR  Failed to compile."`; + +exports[`Dev Server Messages #devServerFileDoesNotExistMsg 1`] = `" WARNING  File \\"./src/index.js\\" doesn't exist."`; + +exports[`Dev Server Messages #devServerInvalidBuildMsg 1`] = `" WAIT  Compiling..."`; + +exports[`Dev Server Messages #devServerModuleDoesntExists 1`] = ` +" ERROR  Module 'react' doesn't exists. + +Error in ./src/index.js + +Webpack tried to resolve module  react  which doesn't exist. + +It's likely caused by a typo in the module name." +`; + +exports[`Dev Server Messages #devServerReactRequired 1`] = ` +" WARNING  \\"react\\" required. + +In order to make \\"react-hot-loader\\" work, \\"react\\" and \\"react-dom\\" are required. + + WAIT  Installing required modules..." +`; + +exports[`Dev Server Messages #devServerRestartMsg 1`] = ` +" WARNING  New npm module was added (react). + +Restarting of the \\"webpack-dev-server\\" is requried. + +Please be patient and wait until restart completes, otherwise some changes might not be tracked." +`; diff --git a/src/utils/__test__/error-helpers.test.js b/src/utils/__test__/error-helpers.test.js index 3a327af..9f5262a 100644 --- a/src/utils/__test__/error-helpers.test.js +++ b/src/utils/__test__/error-helpers.test.js @@ -1,3 +1,26 @@ -test("adds 1 + 2 to equal 3", () => { - expect(1 + 2).toBe(3); +import syntaxErrorMock from "./mock-data/syntax-error.json"; +import eslintWarningMock from "./mock-data/eslint-warning.json"; +import webpackWarningMock from "./mock-data/webpack-warning.json"; +import { formatMessages } from "../error-helpers"; + +describe("#formatMessages", () => { + test("Format Syntax Error", () => { + const [error] = formatMessages(syntaxErrorMock); + expect(error).toMatchSnapshot(); + }); + + test("Format ESLint Warnings", () => { + const warnings = formatMessages(eslintWarningMock).join("\n\n\n"); + expect(warnings).toMatchSnapshot(); + }); + + test("Format Webpack Warnings", () => { + const warnings = formatMessages(webpackWarningMock).join("\n\n\n"); + expect(warnings).toMatchSnapshot(); + }); + + test("Unknown messages", () => { + const messages = ["some", "custom", "messages"]; + expect(formatMessages(messages)).toMatchSnapshot(); + }); }); diff --git a/src/utils/__test__/messages.test.js b/src/utils/__test__/messages.test.js new file mode 100644 index 0000000..10d3622 --- /dev/null +++ b/src/utils/__test__/messages.test.js @@ -0,0 +1,253 @@ +import syntaxErrorMock from "./mock-data/syntax-error.json"; +import buildStatsMock from "./mock-data/build-stats.json"; + +import { + eslintExtraWarningMsg, + fileDoesNotExistMsg, + foundPackageJson, + installingModuleMsg, + devServerBanner, + devServerInvalidBuildMsg, + devServerCompiledSuccessfullyMsg, + devServerFailedToCompileMsg, + devServerCompiledWithWarningsMsg, + devServerFileDoesNotExistMsg, + devServerRestartMsg, + devServerModuleDoesntExists, + devServerReactRequired, + builderBanner, + builderRemovingDistMsg, + builderRunningBuildMsg, + builderErrorMsg, + builderSuccessMsg, + addTopSpace, + addBottomSpace, + joinWithSeparator +} from "../messages"; + +const print = msg => { + // console.log(msg.join("\n")); + return msg.join("\n"); +}; + +const filename = "./src/index.js"; + +describe("Helpers", () => { + test("#addTopSpace", () => { + expect(addTopSpace(["msg", "content"]).join("")).toBe( + ["", "msg", "content"].join("") + ); + }); + + test("#addBottomSpace", () => { + expect(addBottomSpace(["msg", "content"]).join("")).toBe( + ["msg", "content", ""].join("") + ); + }); + + describe("#joinWithSeparator", () => { + test("array of string", () => { + expect(joinWithSeparator("|", ["msg", "content", "here"]).join("")).toBe( + ["msg", "|", "content", "|", "here"].join("") + ); + }); + + test("array of string arrays", () => { + expect( + joinWithSeparator("|", [["msg", "content"], ["here"]]).join("") + ).toBe(["msg", "content", "|", "here"].join("")); + }); + }); +}); + +describe("Common Messages", () => { + test("#eslintExtraWarningMsg", () => { + expect(print(eslintExtraWarningMsg())).toMatchSnapshot(); + }); + + test("#fileDoesNotExistMsg", () => { + expect(print(fileDoesNotExistMsg(filename))).toMatchSnapshot(); + }); + + test("#foundPackageJson", () => { + expect(print(foundPackageJson())).toMatchSnapshot(); + }); + + test("#installingModuleMsg", () => { + expect(print(installingModuleMsg("react"))).toMatchSnapshot(); + }); +}); + +describe("Dev Server Messages", () => { + describe("#devServerBanner", () => { + let flags, params; + + beforeEach(() => { + flags = { + host: "localhost", + port: 3334, + oldPort: 3333, + react: true + }; + + params = { + template: { + short: "index.html" + }, + ngrok: "http://43kd92j3h.ngrok.com" + }; + }); + + test("all flags enabled", () => { + expect(print(devServerBanner(filename, flags, params))).toMatchSnapshot(); + }); + + test("no template", () => { + params.template = {}; + expect(print(devServerBanner(filename, flags, params))).toMatchSnapshot(); + }); + + test("no old port", () => { + flags.oldPort = false; + expect(print(devServerBanner(filename, flags, params))).toMatchSnapshot(); + }); + + test("no ngrok", () => { + params.ngrok = false; + expect(print(devServerBanner(filename, flags, params))).toMatchSnapshot(); + }); + + test("no react", () => { + flags.react = false; + expect(print(devServerBanner(filename, flags, params))).toMatchSnapshot(); + }); + }); + + test("#devServerCompiledSuccessfullyMsg", () => { + const flags = { + host: "localhost", + port: 3334, + oldPort: 3333, + react: true + }; + + const params = { + template: { + short: "index.html" + }, + ngrok: "http://43kd92j3h.ngrok.com" + }; + + expect( + print(devServerCompiledSuccessfullyMsg(filename, flags, params, 5800)) + ).toMatchSnapshot(); + }); + + test("#devServerCompiledWithWarningsMsg", () => { + const flags = { + host: "localhost", + port: 3334, + oldPort: 3333, + react: true + }; + + const params = { + template: { + short: "index.html" + }, + ngrok: "http://43kd92j3h.ngrok.com" + }; + + expect( + print(devServerCompiledWithWarningsMsg(filename, flags, params, 5800)) + ).toMatchSnapshot(); + }); + + test("#devServerInvalidBuildMsg", () => { + expect(print(devServerInvalidBuildMsg())).toMatchSnapshot(); + }); + + test("#devServerFailedToCompileMsg", () => { + expect(print(devServerFailedToCompileMsg())).toMatchSnapshot(); + }); + + test("#devServerFileDoesNotExistMsg", () => { + expect(print(devServerFileDoesNotExistMsg(filename))).toMatchSnapshot(); + }); + + test("#devServerRestartMsg", () => { + expect(print(devServerRestartMsg("react"))).toMatchSnapshot(); + }); + + test("#devServerModuleDoesntExists", () => { + expect( + print(devServerModuleDoesntExists("react", filename)) + ).toMatchSnapshot(); + }); + + test("#devServerReactRequired", () => { + expect(print(devServerReactRequired())).toMatchSnapshot(); + }); +}); + +describe("Build Messages", () => { + describe("#builderBanner", () => { + let flags, params; + + beforeEach(() => { + flags = { + base: "/subfolder/" + }; + + params = { + template: { + short: "index.html" + } + }; + }); + + test("all flags and params", () => { + expect(print(builderBanner(filename, flags, params))).toMatchSnapshot(); + }); + + test("no template", () => { + params.template = {}; + expect(print(builderBanner(filename, flags, params))).toMatchSnapshot(); + }); + + test("no base", () => { + flags.base = false; + expect(print(builderBanner(filename, flags, params))).toMatchSnapshot(); + }); + + test("basic", () => { + expect( + print(builderBanner(filename, {}, { template: {} })) + ).toMatchSnapshot(); + }); + }); + + test("#builderRemovingDistMsg", () => { + expect(print(builderRemovingDistMsg("./dist"))).toMatchSnapshot(); + }); + + test("#builderRunningBuildMsg", () => { + expect(print(builderRunningBuildMsg())).toMatchSnapshot(); + }); + + describe("#builderErrorMsg?", () => { + test("Custom Error message", () => { + expect(print(builderErrorMsg(["Some error message"]))).toMatchSnapshot(); + }); + + test("Syntax Error", () => { + expect(print(builderErrorMsg(syntaxErrorMock))).toMatchSnapshot(); + }); + }); + + test("#builderSuccessMsg", () => { + expect( + print(builderSuccessMsg("./dist", buildStatsMock)) + ).toMatchSnapshot(); + }); +}); diff --git a/src/utils/__test__/mock-data/build-stats.json b/src/utils/__test__/mock-data/build-stats.json new file mode 100644 index 0000000..db45bcb --- /dev/null +++ b/src/utils/__test__/mock-data/build-stats.json @@ -0,0 +1,20 @@ +{ + "buildDuration": 3892, + "assets": [ + { + "name": "index.2b2f089a.js", + "size": 22.8974609375, + "sizeGz": 7.330078125 + }, + { + "name": "index.1027be30.css", + "size": 0.6708984375, + "sizeGz": 0.3115234375 + }, + { + "name": "index.html", + "size": 0.3427734375, + "sizeGz": 0.23828125 + } + ] +} diff --git a/src/utils/__test__/mock-data/eslint-warning.json b/src/utils/__test__/mock-data/eslint-warning.json new file mode 100644 index 0000000..6070eed --- /dev/null +++ b/src/utils/__test__/mock-data/eslint-warning.json @@ -0,0 +1,4 @@ +[ + "./thinking-in-react/src/components/filtered-product-table/filtered-product-table.js\n\n\u001b[4m/Users/ssysoev/Development/aik-examples/thinking-in-react/src/components/filtered-product-table/filtered-product-table.js\u001b[24m\n \u001b[2m5:16\u001b[22m \u001b[33mwarning\u001b[39m React.createClass is deprecated since React 15.5.0, use the npm module create-react-class instead \u001b[2mreact/no-deprecated\u001b[22m\n \u001b[2m40:11\u001b[22m \u001b[33mwarning\u001b[39m No duplicate props allowed \u001b[2mreact/jsx-no-duplicate-props\u001b[22m\n\n\u001b[33m\u001b[1m✖ 2 problems (0 errors, 2 warnings)\n\u001b[22m\u001b[39m\n @ ./thinking-in-react/src/index.js 14:28-95\n @ ../aik/lib/webpack/assets/react-entry-point.js\n @ multi ../aik/~/webpack-dev-server/client?http://localhost:4444/ ../aik/~/webpack/hot/dev-server.js ../aik/lib/webpack/assets/react-entry-point.js", + "./thinking-in-react/src/components/product-table/product-table.js\n\n\u001b[4m/Users/ssysoev/Development/aik-examples/thinking-in-react/src/components/product-table/product-table.js\u001b[24m\n \u001b[2m16:11\u001b[22m \u001b[33mwarning\u001b[39m No duplicate props allowed \u001b[2mreact/jsx-no-duplicate-props\u001b[22m\n\n\u001b[33m\u001b[1m✖ 1 problem (0 errors, 1 warning)\n\u001b[22m\u001b[39m\n @ ./thinking-in-react/src/components/filtered-product-table/filtered-product-table.js 27:20-69\n @ ./thinking-in-react/src/index.js\n @ ../aik/lib/webpack/assets/react-entry-point.js\n @ multi ../aik/~/webpack-dev-server/client?http://localhost:4444/ ../aik/~/webpack/hot/dev-server.js ../aik/lib/webpack/assets/react-entry-point.js" +] diff --git a/src/utils/__test__/mock-data/syntax-error.json b/src/utils/__test__/mock-data/syntax-error.json new file mode 100644 index 0000000..47f7261 --- /dev/null +++ b/src/utils/__test__/mock-data/syntax-error.json @@ -0,0 +1,4 @@ +[ + "./thinking-in-react/src/components/product-table/product-table.js\n\n\u001b[4m/Users/ssysoev/Development/aik-examples/thinking-in-react/src/components/product-table/product-table.js\u001b[24m\n \u001b[2m20:8\u001b[22m \u001b[31merror\u001b[39m Parsing error: Unexpected token <\n\n\u001b[31m\u001b[1m✖ 1 problem (1 error, 0 warnings)\n\u001b[22m\u001b[39m\n @ ./thinking-in-react/src/components/filtered-product-table/filtered-product-table.js 27:20-69\n @ ./thinking-in-react/src/index.js\n @ ../aik/lib/webpack/assets/react-entry-point.js\n @ multi ../aik/~/webpack-dev-server/client?http://localhost:4444/ ../aik/~/webpack/hot/dev-server.js ../aik/lib/webpack/assets/react-entry-point.js", + "./thinking-in-react/src/components/product-table/product-table.js\nModule build failed: SyntaxError: Unexpected token (20:7)\n\n\u001b[0m \u001b[90m 18 | \u001b[39m \u001b[36mreturn\u001b[39m (\n \u001b[90m 19 | \u001b[39m \u001b[33m<\u001b[39m\u001b[33mdiv\u001b[39m className\u001b[33m=\u001b[39m\u001b[32m\"productTable\"\u001b[39m\u001b[33m>\u001b[39m\n\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 20 | \u001b[39m \u001b[33m<<\u001b[39m\u001b[33mtable\u001b[39m\u001b[33m>\u001b[39m\n \u001b[90m | \u001b[39m \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\n \u001b[90m 21 | \u001b[39m \u001b[33m<\u001b[39m\u001b[33mthead\u001b[39m\u001b[33m>\u001b[39m\n \u001b[90m 22 | \u001b[39m \u001b[33m<\u001b[39m\u001b[33mtr\u001b[39m\u001b[33m>\u001b[39m\n \u001b[90m 23 | \u001b[39m \u001b[33m<\u001b[39m\u001b[33mth\u001b[39m\u001b[33m>\u001b[39m\u001b[33mName\u001b[39m\u001b[33m<\u001b[39m\u001b[33m/\u001b[39m\u001b[33mth\u001b[39m\u001b[33m>\u001b[39m\u001b[0m\n\n @ ./thinking-in-react/src/components/filtered-product-table/filtered-product-table.js 27:20-69\n @ ./thinking-in-react/src/index.js\n @ ../aik/lib/webpack/assets/react-entry-point.js\n @ multi ../aik/~/webpack-dev-server/client?http://localhost:4444/ ../aik/~/webpack/hot/dev-server.js ../aik/lib/webpack/assets/react-entry-point.js" +] diff --git a/src/utils/__test__/mock-data/webpack-warning.json b/src/utils/__test__/mock-data/webpack-warning.json new file mode 100644 index 0000000..144581a --- /dev/null +++ b/src/utils/__test__/mock-data/webpack-warning.json @@ -0,0 +1,4 @@ +[ + "./~/jsondiffpatch/src/main.js\n56:20-50 Critical dependency: the request of a dependency is an expression", + "./~/jsondiffpatch/src/main.js\n61:19-47 Critical dependency: the request of a dependency is an expression" +] diff --git a/src/utils/error-helpers.js b/src/utils/error-helpers.js index 27a509c..4df0a9e 100644 --- a/src/utils/error-helpers.js +++ b/src/utils/error-helpers.js @@ -1,12 +1,15 @@ /* @flow */ +import chalk from "chalk"; + const SYNTAX_ERROR_LABEL = "SyntaxError:"; const SYNTAX_ERROR_LABEL_HUMAN_FRIENDLY = "Syntax Error:"; +const ESLINT_PARSE_ERROR = "Parsing error:"; /** * Checks whether error is syntax error. */ -export function isLikelyASyntaxError(message: string): boolean { +export function isSyntaxError(message: string): boolean { return ( message.indexOf(SYNTAX_ERROR_LABEL) !== -1 || message.indexOf(SYNTAX_ERROR_LABEL_HUMAN_FRIENDLY) !== -1 @@ -14,25 +17,109 @@ export function isLikelyASyntaxError(message: string): boolean { } /** - * Makes some common errors shorter. + * Checks whether error is eslint rules warning. */ -export function formatMessage(message: string): string { - return ( - message - // Babel syntax error - .replace( - "Module build failed: SyntaxError:", - SYNTAX_ERROR_LABEL_HUMAN_FRIENDLY - ) - // Webpack file not found error - .replace( - /Module not found: Error: Cannot resolve 'file' or 'directory'/, - "Module not found:" - ) - // Internal stacks are generally useless so we strip them - .replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s\)]*(\n|$)/gm, "") // at ... ...:x:y - // Webpack loader names obscure CSS filenames - .replace("./~/css-loader!./~/postcss-loader!", "") - .replace(/\s@ multi .+/, "") +export function isEslintWarning(message: string): boolean { + return !!message.match(/\d\sproblem/) && !!message.match("warning"); +} + +/** + * Checks whether error is eslint parse error. + */ +export function isEslintParseError(message: string): boolean { + return !!message.match(ESLINT_PARSE_ERROR); +} + +export function isDependencyAnExpression(message: string): boolean { + return !!message.match("the request of a dependency is an expression"); +} + +export function findMessagesToFormat(messages: string[]): string[] { + return messages.filter(message => { + if (isEslintParseError(message)) { + return false; + } + + if (isDependencyAnExpression(message)) { + return false; + } + + return true; + }); +} + +export function isFileSnippetLine(line: string): boolean { + return !!line.match(/\s+\|/); +} + +export function isEslintWarningRuleLine(line: string): boolean { + return !!line.match(/\d+:\d+(.+)warning/); +} + +export function getLinePadding(line: string): number { + return line.split("").findIndex(c => !!c.match(/\S/)); +} + +export function removeLinePadding(padding: number, line: string): string { + return line.substring(padding); +} + +/** + * Formats "Syntax Error" message. + */ +export function formatSyntaxError(message: string): string { + const messageByLine = message.split("\n"); + const padding = getLinePadding(messageByLine[0]); + const filePath = removeLinePadding(padding, messageByLine[0]); + const error = removeLinePadding( + padding, + messageByLine[1].replace( + "Module build failed: SyntaxError:", + SYNTAX_ERROR_LABEL_HUMAN_FRIENDLY + ) ); + const snippet = messageByLine + .filter(isFileSnippetLine) + .map(removeLinePadding.bind(null, padding)); + + return `${chalk.red("Error:")} + +${chalk.dim("|")} ${error} + +${chalk.yellow("Where:")} ${filePath} + +${snippet.join("\n")}`; +} + +/** + * Formats "ESLint warning" message. + */ +export function formatEslintWarning(message: string): string { + const messageByLine = message.split("\n"); + const padding = getLinePadding(messageByLine[0]); + const filePath = removeLinePadding(padding, messageByLine[0]); + const rule = messageByLine + .filter(isEslintWarningRuleLine) + .map(removeLinePadding.bind(null, padding + 2)); + + return `${rule.join("\n")} + +${chalk.yellow("Where:")} ${filePath}`; +} + +/** + * Beautifies error messages and warnings. + */ +export function formatMessages(messages: string[]): string[] { + return findMessagesToFormat(messages).map(message => { + if (isSyntaxError(message)) { + return formatSyntaxError(message); + } + + if (isEslintWarning(message)) { + return formatEslintWarning(message); + } + + return message; + }); } diff --git a/src/utils/messages.js b/src/utils/messages.js index b990f3d..283e0c6 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -1,7 +1,7 @@ /* @flow */ import chalk from "chalk"; -import { isLikelyASyntaxError, formatMessage } from "./error-helpers"; +import { formatMessages } from "./error-helpers"; /** * Moves current line to the most top of console. @@ -17,7 +17,12 @@ export function clearConsole(sep?: boolean) { /** * Actually prints message to the console */ -export function print(msg: string[]) { +export function print( + msg: string[], + clear?: boolean = false, + clearWithSep?: boolean = false +) { + if (clear) clearConsole(clearWithSep); return console.log(msg.join("\n")); // eslint-disable-line } @@ -28,6 +33,29 @@ export function print(msg: string[]) { * */ +export function addTopSpace(msg: string[]): string[] { + return [""].concat(msg); +} + +export function addBottomSpace(msg: string[]): string[] { + return [].concat(msg, ""); +} + +// TODO: string[] | string[][] +export function joinWithSeparator(sep: string, msg: any): string[] { + return msg.reduce((acc: string[], item: string | string[], index) => { + acc = acc.concat(item); + if (index < msg.length - 1) { + acc.push(sep); + } + return acc; + }, []); +} + +export function joinWithSpace(msg: any): string[] { + return joinWithSeparator("", msg); +} + export function doneBadge() { return chalk.bgGreen.black(" DONE "); } @@ -44,14 +72,18 @@ export function errorBadge() { return chalk.bgRed.black(" ERROR "); } +export function separator() { + return chalk.dim("---------"); +} + /** * * Common Messages * */ -export function eslintExtraWarningMsg() { - return print([ +export function eslintExtraWarningMsg(): string[] { + return [ "You may use special comments to disable some warnings.", "Use " + chalk.yellow("// eslint-disable-next-line") + @@ -59,36 +91,41 @@ export function eslintExtraWarningMsg() { "Use " + chalk.yellow("/* eslint-disable */") + " to ignore all warnings in a file." - ]); + ]; } -export function fileDoesNotExistMsg(filename: string) { - clearConsole(); - return print([ +export function fileDoesNotExistMsg(filename: string): string[] { + return [ errorBadge() + chalk.red(" File doesn't exist."), "", - `You are trying to use ${chalk.yellow('"' + filename + '"')} as entry point, but this file doesn't exist.`, - `Please, choose existing file or create ${chalk.yellow('"' + filename + '"')} manualy.` - ]); + `You are trying to use ${chalk.yellow( + '"' + filename + '"' + )} as an entry point, but this file doesn't exist.`, + `Please, choose an existing file or create ${chalk.yellow( + '"' + filename + '"' + )} manualy.` + ]; } -export function foundPackageJson() { - return print([ - "", +export function foundPackageJson(): string[] { + return [ warningBadge() + " " + chalk.yellow('File "package.json" has been discovered.'), "", - `Since ${chalk.yellow('"node_modules"')} folder doesn't exist and in order to avoid possible artifacts caused by`, - `accidentally updated versions of npm modules Aik will run ${chalk.yellow('"npm install"')} in current directory.`, + `Since ${chalk.yellow( + '"node_modules"' + )} folder doesn't exist and in order to avoid possible artifacts caused by`, + `accidentally updated versions of npm modules Aik will run ${chalk.yellow( + '"npm install"' + )} in current directory.`, "", - waitBadge() + " " + chalk.blue("Installing npm modules..."), - "" - ]); + waitBadge() + " " + chalk.blue("Installing npm modules...") + ]; } -export function installingModuleMsg(moduleName: string) { - return print([`Installing module "${chalk.yellow(moduleName)}" ...`]); +export function installingModuleMsg(moduleName: string): string[] { + return [`Installing module "${chalk.yellow(moduleName)}" ...`]; } /** @@ -102,24 +139,26 @@ export function devServerBanner( flags: CLIFlags, params: AikParams ): string[] { - const msg: string[] = ["", chalk.magenta("Entry point: ") + filename]; + const msg: string[] = [chalk.magenta("Entry point: ") + filename]; if (params.template.short) { msg.push(chalk.magenta("Custom template: ") + params.template.short); } - msg.push( - chalk.magenta("Server: ") + - chalk.cyan(`http://${flags.host}:${flags.port}`) - ); - if (flags.oldPort) { msg.push( chalk.magenta("Port changed: ") + - `${chalk.bgRed.black(" " + flags.oldPort + " ")} -> ${chalk.bgGreen.black(" " + flags.port + " ")}` + `${chalk.bgRed.black( + " " + flags.oldPort + " " + )} -> ${chalk.bgGreen.black(" " + flags.port + " ")}` ); } + msg.push( + chalk.magenta("Server: ") + + chalk.cyan(`http://${flags.host}:${flags.port}`) + ); + if (params.ngrok) { msg.push(chalk.magenta("Ngrok: ") + chalk.cyan(params.ngrok)); } @@ -131,9 +170,8 @@ export function devServerBanner( return msg; } -export function devServerInvalidBuildMsg() { - clearConsole(); - return print([waitBadge() + " " + chalk.blue("Compiling...")]); +export function devServerInvalidBuildMsg(): string[] { + return [waitBadge() + " " + chalk.blue("Compiling...")]; } export function devServerCompiledSuccessfullyMsg( @@ -141,20 +179,20 @@ export function devServerCompiledSuccessfullyMsg( flags: CLIFlags, params: AikParams, buildDuration: number -) { - // eslint-disable-line +): string[] { const msg = devServerBanner(filename, flags, params); + msg.unshift(""); msg.unshift( doneBadge() + " " + chalk.green(`Compiled successfully in ${buildDuration}ms!`) ); - return print(msg); + return msg; } -export function devServerFailedToCompileMsg() { - clearConsole(true); - return print([errorBadge() + " " + chalk.red("Failed to compile.")]); +export function devServerFailedToCompileMsg(): string[] { + // clearConsole(true); + return [errorBadge() + " " + chalk.red("Failed to compile.")]; } export function devServerCompiledWithWarningsMsg( @@ -162,64 +200,63 @@ export function devServerCompiledWithWarningsMsg( flags: CLIFlags, params: AikParams, buildDuration: number -) { - // eslint-disable-line +): string[] { const msg = devServerBanner(filename, flags, params); + msg.unshift(""); msg.unshift( warningBadge() + " " + chalk.yellow(`Compiled with warnings in ${buildDuration}ms.`) ); - msg.push("", chalk.dim("---------")); - return print(msg); + msg.push("", separator()); + return msg; } -export function devServerFileDoesNotExistMsg(filename: string) { - clearConsole(); - return print([ - warningBadge() + ` File "${chalk.yellow(filename)}" doesn\'t exist.`, - "" - ]); +export function devServerFileDoesNotExistMsg(filename: string): string[] { + return [warningBadge() + chalk.yellow(` File "${filename}" doesn\'t exist.`)]; } -export function devServerRestartMsg(module: string) { - clearConsole(true); - return print([ +export function devServerRestartMsg(module: string): string[] { + return [ warningBadge() + " " + chalk.yellow(`New npm module was added (${module}).`), "", - "Restarting webpack-dev-server is requried.", + `Restarting of the ${chalk.yellow('"webpack-dev-server"')} is requried.`, "", - "Please be patient and wait until restart completes, otherwise some changes might not be tracked.", - "" - ]); + "Please be patient and wait until restart completes, otherwise some changes might not be tracked." + ]; } -export function devServerModuleDoesntExists(module: string, filename: string) { - clearConsole(true); - return print([ +export function devServerModuleDoesntExists( + module: string, + filename: string +): string[] { + return [ errorBadge() + " " + chalk.red(`Module '${module}' doesn't exists.`), "", - `Error in ${filename}`, + `Error in ${chalk.yellow(filename)}`, "", - `Webpack tried to resolve module ${chalk.bgYellow.black(" " + module + " ")} which doesn't exist.`, + `Webpack tried to resolve module ${chalk.bgYellow.black( + " " + module + " " + )} which doesn't exist.`, "", - `It's likely caused by ${chalk.yellow("typo")} in the module name.`, - "" - ]); + `It's likely caused by a ${chalk.yellow("typo")} in the module name.` + ]; } -export function devServerReactRequired() { - return print([ - "", +export function devServerReactRequired(): string[] { + return [ warningBadge() + " " + chalk.yellow('"react" required.'), "", - 'In order to make "react-hot-loader" work, "react" and "react-dom" are required.', + `In order to make ${chalk.yellow( + '"react-hot-loader"' + )} work, ${chalk.yellow('"react"')} and ${chalk.yellow( + '"react-dom"' + )} are required.`, "", - waitBadge() + " " + chalk.blue("Installing required modules..."), - "" - ]); + waitBadge() + " " + chalk.blue("Installing required modules...") + ]; } /** @@ -232,9 +269,7 @@ export function builderBanner( filename: string, flags: CLIFlags, params: AikParams -) { - clearConsole(); - +): string[] { const msg = [ waitBadge() + " " + chalk.blue("Building..."), "", @@ -250,41 +285,32 @@ export function builderBanner( msg.push(chalk.magenta("Base path: ") + base); } - return print(msg); + return msg; } -export function builderRemovingDistMsg(distPath: string) { - return print(["", chalk.yellow("Removing folder: ") + distPath]); +export function builderRemovingDistMsg(distPath: string): string[] { + return [chalk.yellow("Removing folder: ") + distPath]; } -export function builderRunningBuildMsg() { - return print([chalk.yellow("Running webpack production build...")]); +export function builderRunningBuildMsg(): string[] { + return [chalk.yellow("Running webpack production build...")]; } -export function builderErrorMsg(err: { message: string } | string) { - clearConsole(true); - - let msg: string = typeof err.message === "string" - ? err.message - : err.toString(); +export function builderErrorMsg(err: string[]): string[] { + const msg = formatMessages(err); - if (isLikelyASyntaxError(msg)) { - msg = formatMessage(msg); - } - return print([ + return [ errorBadge() + " " + chalk.red("Failed to create a production build. Reason:"), - msg - ]); + "" + ].concat(msg); } export function builderSuccessMsg( distShortName: string, buildStats: BuildStats -) { - clearConsole(true); - +): string[] { const assets = buildStats.assets; const longestNameSize = assets.reduce( @@ -295,11 +321,13 @@ export function builderSuccessMsg( const padString = (placeholder: string, str: string) => (str + placeholder).substr(0, placeholder.length); - return print([ + return [ doneBadge() + ` in ${buildStats.buildDuration}ms`, "", chalk.green( - `Successfully generated a bundle in the ${chalk.cyan('"' + distShortName + '"')} folder!` + `Aik has successfully generated a bundle in the ${chalk.cyan( + '"' + distShortName + '"' + )} folder!` ), chalk.green( "The bundle is optimized and ready to be deployed to production." @@ -315,5 +343,5 @@ export function builderSuccessMsg( ].join(" "); }) .join("\n") - ]); + ]; } diff --git a/src/utils/npm.js b/src/utils/npm.js index 46afafc..dda0115 100644 --- a/src/utils/npm.js +++ b/src/utils/npm.js @@ -3,7 +3,12 @@ import { execSync, spawnSync } from "child_process"; import fs from "fs"; import path from "path"; import resolveModule from "resolve"; -import { installingModuleMsg, foundPackageJson } from "./messages"; +import { + print, + addBottomSpace, + installingModuleMsg, + foundPackageJson +} from "./messages"; export function isModuleInstalled(moduleName: string): boolean { try { @@ -16,7 +21,7 @@ export function isModuleInstalled(moduleName: string): boolean { export function installModule(moduleName: string) { execSync(`npm install ${moduleName} --silent`, { cwd: process.cwd() }); - installingModuleMsg(moduleName); + print(installingModuleMsg(moduleName)); } export function hasPackageJson(cwd: string) { @@ -40,7 +45,7 @@ export function hasNodeModules(cwd: string) { export function installAllModules(cwd: string) { if (!hasPackageJson(cwd)) return; if (hasNodeModules(cwd)) return; - foundPackageJson(); + print(addBottomSpace(foundPackageJson()), /* clear console */ true); spawnSync("npm", ["install", "--silent"], { cwd, stdio: "inherit" }); } diff --git a/src/webpack/plugins.js b/src/webpack/plugins.js index 64234d4..9987d56 100644 --- a/src/webpack/plugins.js +++ b/src/webpack/plugins.js @@ -15,11 +15,9 @@ export function htmlWebpackPlugin(template: string | false) { } export function npmInstallPlugin(options?: Object = {}) { - return new NpmInstallPlugin({ - dev: true, - peerDependencies: true, - ...options - }); + return new NpmInstallPlugin( + Object.assign({ dev: true, peerDependencies: true }, options) + ); } /**