├── .babel.config.js ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _sandbox ├── .eslintrc ├── .gitignore ├── index.js ├── package.json ├── watch.js └── webpack.config.js ├── appveyor.yml ├── index.js ├── package.json ├── src ├── core │ ├── extractWebpackError.js │ ├── formatErrors.js │ └── transformErrors.js ├── formatters │ ├── defaultError.js │ ├── eslintError.js │ └── moduleNotFound.js ├── friendly-errors-plugin.js ├── output.js ├── transformers │ ├── babelSyntax.js │ ├── esLintError.js │ └── moduleNotFound.js └── utils │ ├── colors.js │ └── index.js ├── test ├── fixtures │ ├── .gitignore │ ├── babel-syntax-babel-6 │ │ ├── .babelrc │ │ ├── index.js │ │ └── webpack.config.js │ ├── babel-syntax-babel-7 │ │ ├── .babelrc │ │ ├── index.js │ │ └── webpack.config.js │ ├── eslint-warnings │ │ ├── .eslintrc │ │ ├── index.js │ │ ├── module.js │ │ └── webpack.config.js │ ├── eslint-webpack-plugin-warnings │ │ ├── .eslintrc │ │ ├── index.js │ │ ├── module.js │ │ └── webpack.config.js │ ├── mini-css-extract-babel-syntax │ │ ├── index.scss │ │ └── webpack.config.js │ ├── module-errors │ │ ├── index.js │ │ └── webpack.config.js │ ├── multi-compiler-module-errors │ │ ├── index.js │ │ ├── index2.js │ │ └── webpack.config.js │ ├── multi-compiler-success │ │ ├── index.js │ │ ├── index2.js │ │ └── webpack.config.js │ ├── multi-postcss-warnings │ │ ├── index.css │ │ ├── index2.css │ │ ├── postcss.config.js │ │ └── webpack.config.js │ ├── postcss-warnings │ │ ├── index.css │ │ ├── postcss.config.js │ │ └── webpack.config.js │ └── success │ │ ├── index.js │ │ └── webpack.config.js ├── integration.spec.js └── unit │ ├── formatErrors.spec.js │ ├── formatters │ ├── defaultError.spec.js │ └── moduleNotFound.spec.js │ ├── plugin │ └── friendlyErrors.spec.js │ ├── transformers │ ├── babelSyntax.spec.js │ └── moduleNotFound.spec.js │ └── utils │ └── utils.spec.js └── yarn.lock /.babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "only": ["test/"], 3 | "plugins": ["@babel/plugin-transform-async-to-generator"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8 4 | }, 5 | "plugins": ["node"], 6 | "extends": ["eslint:recommended", "plugin:node/recommended"], 7 | "rules": { 8 | "no-console": "off", 9 | "no-unused-vars": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 10 5 | - 12 6 | 7 | cache: 8 | yarn: true 9 | directories: 10 | - node_modules 11 | 12 | #after_success: npm run coverage 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Geoffroy Warin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Friendly-errors-webpack-plugin 2 | 3 | [![npm](https://img.shields.io/npm/v/@soda/friendly-errors-webpack-plugin.svg)](https://www.npmjs.com/package/@soda/friendly-errors-webpack-plugin) 4 | [![Build Status](https://travis-ci.org/sodatea/friendly-errors-webpack-plugin.svg?branch=master)](https://travis-ci.org/sodatea/friendly-errors-webpack-plugin) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/bbrt7hmp6lav3erh/branch/master?svg=true)](https://ci.appveyor.com/project/sodatea/friendly-errors-webpack-plugin/branch/master) 6 | 7 | Friendly-errors-webpack-plugin recognizes certain classes of webpack 8 | errors and cleans, aggregates and prioritizes them to provide a better 9 | Developer Experience. 10 | 11 | It is easy to add types of errors so if you would like to see more 12 | errors get handled, please open a [PR](https://help.github.com/articles/creating-a-pull-request/)! 13 | 14 | ## Getting started 15 | 16 | ### Installation 17 | 18 | ```bash 19 | npm install @soda/friendly-errors-webpack-plugin --save-dev 20 | ``` 21 | 22 | ### Basic usage 23 | 24 | Simply add `FriendlyErrorsWebpackPlugin` to the plugin section in your Webpack config. 25 | 26 | ```javascript 27 | var FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin'); 28 | 29 | var webpackConfig = { 30 | // ... 31 | plugins: [ 32 | new FriendlyErrorsWebpackPlugin(), 33 | ], 34 | // ... 35 | } 36 | ``` 37 | 38 | ### Turn off errors 39 | 40 | You need to turn off all error logging by setting your webpack config quiet option to true. 41 | 42 | ```javascript 43 | app.use(require('webpack-dev-middleware')(compiler, { 44 | // ... 45 | logLevel: 'silent', 46 | // ... 47 | })); 48 | ``` 49 | 50 | If you use the webpack-dev-server, there is a setting in webpack's ```devServer``` options: 51 | 52 | ```javascript 53 | // webpack config root 54 | { 55 | // ... 56 | devServer: { 57 | // ... 58 | quiet: true, 59 | // ... 60 | }, 61 | // ... 62 | } 63 | ``` 64 | 65 | If you use webpack-hot-middleware, that is done by setting the log option to `false`. You can do something sort of like this, depending upon your setup: 66 | 67 | ```javascript 68 | app.use(require('webpack-hot-middleware')(compiler, { 69 | log: false 70 | })); 71 | ``` 72 | 73 | _Thanks to [webpack-dashboard](https://github.com/FormidableLabs/webpack-dashboard) for this piece of info._ 74 | 75 | ## Demo 76 | 77 | ### Build success 78 | 79 | ![success](http://i.imgur.com/MkUEhYz.gif) 80 | 81 | ### eslint-loader errors 82 | 83 | ![lint](http://i.imgur.com/xYRkldr.gif) 84 | 85 | ### babel-loader syntax errors 86 | 87 | ![babel](http://i.imgur.com/W59z8WF.gif) 88 | 89 | ### Module not found 90 | 91 | ![babel](http://i.imgur.com/OivW4As.gif) 92 | 93 | ## Options 94 | 95 | You can pass options to the plugin: 96 | 97 | ```js 98 | new FriendlyErrorsPlugin({ 99 | compilationSuccessInfo: { 100 | messages: ['You application is running here http://localhost:3000'], 101 | notes: ['Some additional notes to be displayed upon successful compilation'] 102 | }, 103 | onErrors: function (severity, errors) { 104 | // You can listen to errors transformed and prioritized by the plugin 105 | // severity can be 'error' or 'warning' 106 | }, 107 | // should the console be cleared between each compilation? 108 | // default is true 109 | clearConsole: true, 110 | 111 | // add formatters and transformers (see below) 112 | additionalFormatters: [], 113 | additionalTransformers: [] 114 | }) 115 | ``` 116 | 117 | ## Adding desktop notifications 118 | 119 | The plugin has no native support for desktop notifications but it is easy 120 | to add them thanks to [node-notifier](https://www.npmjs.com/package/node-notifier) for instance. 121 | 122 | ```js 123 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); 124 | var notifier = require('node-notifier'); 125 | var ICON = path.join(__dirname, 'icon.png'); 126 | 127 | new FriendlyErrorsPlugin({ 128 | onErrors: (severity, errors) => { 129 | if (severity !== 'error') { 130 | return; 131 | } 132 | const error = errors[0]; 133 | notifier.notify({ 134 | title: "Webpack error", 135 | message: severity + ': ' + error.name, 136 | subtitle: error.file || '', 137 | icon: ICON 138 | }); 139 | } 140 | }) 141 | ``` 142 | 143 | ## API 144 | 145 | ### Transformers and formatters 146 | 147 | Webpack's errors processing, is done in four phases: 148 | 149 | 1. Extract relevant info from webpack errors. This is done by the plugin [here](https://github.com/sodatea/friendly-errors-webpack-plugin/blob/master/src/core/extractWebpackError.js) 150 | 2. Apply transformers to all errors to identify and annotate well know errors and give them a priority 151 | 3. Get only top priority error or top priority warnings if no errors are thrown 152 | 4. Apply formatters to all annotated errors 153 | 154 | You can add transformers and formatters. Please see [transformErrors](https://github.com/sodatea/friendly-errors-webpack-plugin/blob/master/src/core/transformErrors.js), 155 | and [formatErrors](https://github.com/sodatea/friendly-errors-webpack-plugin/blob/master/src/core/formatErrors.js) 156 | in the source code and take a look a the [default transformers](https://github.com/sodatea/friendly-errors-webpack-plugin/tree/master/src/transformers) 157 | and the [default formatters](https://github.com/sodatea/friendly-errors-webpack-plugin/tree/master/src/formatters). 158 | 159 | ## TODO 160 | 161 | - [x] Make it compatible with node 4 162 | 163 | -------------------------------------------------------------------------------- /_sandbox/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "commonjs": true, 7 | "es6": true, 8 | "jest": true, 9 | "node": true 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 6, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | // http://eslint.org/docs/rules/ 17 | "no-unused-expressions": "warn", 18 | "no-unused-labels": "warn", 19 | "no-unused-vars": ["warn", { "vars": "local", "args": "none" }] 20 | } 21 | } -------------------------------------------------------------------------------- /_sandbox/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /_sandbox/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const unsed = 'I am unused'; 3 | 4 | export default class MyComponent extends React.Component { 5 | 6 | render() { 7 | return
Hello
8 | } 9 | } -------------------------------------------------------------------------------- /_sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "_sandbox", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": {}, 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "node watch.js" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /_sandbox/watch.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const config = require("./webpack.config"); 3 | 4 | const compiler = webpack(config); 5 | 6 | compiler.watch({}, (stats) => { 7 | 8 | }); -------------------------------------------------------------------------------- /_sandbox/webpack.config.js: -------------------------------------------------------------------------------- 1 | const FriendlyErrorsWebpackPlugin = require('../index'); 2 | 3 | module.exports = { 4 | entry: __dirname + "/index.js", 5 | output: { 6 | path: __dirname + "/dist", 7 | filename: "bundle.js" 8 | }, 9 | plugins: [ 10 | new FriendlyErrorsWebpackPlugin() 11 | ], 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | loader: 'eslint-loader', 17 | enforce: 'pre', 18 | include: __dirname 19 | }, 20 | { 21 | test: /\.jsx?$/, 22 | loader: require.resolve('babel-loader-7'), 23 | query: { 24 | presets: ['react'], 25 | }, 26 | exclude: /node_modules/ 27 | } 28 | ] 29 | } 30 | }; -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "10" 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - yarn install 8 | 9 | test_script: 10 | - node --version 11 | - yarn --version 12 | - yarn test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const FriendlyErrorsWebpackPlugin = require('./src/friendly-errors-plugin'); 3 | 4 | module.exports = FriendlyErrorsWebpackPlugin; 5 | module.exports.default = FriendlyErrorsWebpackPlugin; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@soda/friendly-errors-webpack-plugin", 3 | "version": "1.8.1", 4 | "description": "Recognizes certain classes of webpack errors and cleans, aggregates and prioritizes them to provide a better Developer Experience", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=8.0.0" 8 | }, 9 | "scripts": { 10 | "test": "eslint --ignore-pattern \"test/**\" . && jest --testEnvironment node" 11 | }, 12 | "files": [ 13 | "src", 14 | "index.js" 15 | ], 16 | "keywords": [ 17 | "friendly", 18 | "errors", 19 | "webpack", 20 | "plugin" 21 | ], 22 | "author": "Geoffroy Warin", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/sodatea/friendly-errors-webpack-plugin.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/sodatea/friendly-errors-webpack-plugin/issues" 29 | }, 30 | "license": "MIT", 31 | "jest": { 32 | "testEnvironment": "node", 33 | "transformIgnorePatterns": [ 34 | "node_modules", 35 | "src", 36 | "index.js" 37 | ] 38 | }, 39 | "peerDependencies": { 40 | "webpack": "^4.0.0 || ^5.0.0" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.5.4", 44 | "@babel/plugin-transform-async-to-generator": "^7.5.0", 45 | "autoprefixer": "^9.6.0", 46 | "babel-core": "6.26.3", 47 | "babel-eslint": "^10.0.1", 48 | "babel-preset-react": "^6.23.0", 49 | "babel-loader-7": "npm:babel-loader@7.1.5", 50 | "babel-loader": "^8.0.6", 51 | "css-loader": "^2.1.1", 52 | "eslint": "^5.16.0", 53 | "eslint-7": "npm:eslint@^7.14.0", 54 | "eslint-loader": "^2.1.2", 55 | "eslint-plugin-node": "^9.0.1", 56 | "eslint-webpack-plugin": "^2.4.0", 57 | "expect": "^24.8.0", 58 | "jest": "^24.8.0", 59 | "memory-fs": "^0.4.1", 60 | "mini-css-extract-plugin": "^0.6.0", 61 | "node-sass": "^4.12.0", 62 | "postcss-loader": "^3.0.0", 63 | "sass": "^1.20.1", 64 | "sass-loader": "^7.1.0", 65 | "style-loader": "^0.23.1", 66 | "webpack": "^4.31.0" 67 | }, 68 | "dependencies": { 69 | "chalk": "^3.0.0", 70 | "error-stack-parser": "^2.0.6", 71 | "string-width": "^4.2.3", 72 | "strip-ansi": "^6.0.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/core/extractWebpackError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ErrorStackParser = require('error-stack-parser'); 4 | const RequestShortener = require("webpack/lib/RequestShortener"); 5 | 6 | // TODO: allow the location to be customized in options 7 | const requestShortener = new RequestShortener(process.cwd()); 8 | 9 | /* 10 | This logic is mostly duplicated from webpack/lib/Stats.js#toJson() 11 | See: https://github.com/webpack/webpack/blob/2f618e733aab4755deb42e9d8e859609005607c0/lib/Stats.js#L89 12 | */ 13 | 14 | function extractError (e) { 15 | return { 16 | message: e.message, 17 | file: getFile(e), 18 | origin: getOrigin(e), 19 | name: e.name, 20 | severity: 0, 21 | webpackError: e, 22 | originalStack: getOriginalErrorStack(e) 23 | }; 24 | } 25 | 26 | function getOriginalErrorStack(e) { 27 | while (e.error != null) { 28 | e = e.error; 29 | } 30 | if (e.stack) { 31 | return ErrorStackParser.parse(e); 32 | } 33 | return []; 34 | } 35 | 36 | function getFile (e) { 37 | if (e.file) { 38 | return e.file; 39 | } else if (e.module && e.module.readableIdentifier && typeof e.module.readableIdentifier === "function") { 40 | return e.module.readableIdentifier(requestShortener); 41 | } 42 | } 43 | 44 | function getOrigin (e) { 45 | let origin = ''; 46 | if (e.dependencies && e.origin) { 47 | origin += '\n @ ' + e.origin.readableIdentifier(requestShortener); 48 | e.dependencies.forEach(function (dep) { 49 | if (!dep.loc) return; 50 | if (typeof dep.loc === "string") return; 51 | if (!dep.loc.start) return; 52 | if (!dep.loc.end) return; 53 | origin += ' ' + dep.loc.start.line + ':' + dep.loc.start.column + '-' + 54 | (dep.loc.start.line !== dep.loc.end.line ? dep.loc.end.line + ':' : '') + dep.loc.end.column; 55 | }); 56 | var current = e.origin; 57 | while (current.issuer && typeof current.issuer.readableIdentifier === 'function') { 58 | current = current.issuer; 59 | origin += '\n @ ' + current.readableIdentifier(requestShortener); 60 | } 61 | } 62 | return origin; 63 | } 64 | 65 | module.exports = extractError; 66 | -------------------------------------------------------------------------------- /src/core/formatErrors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Applies formatters to all AnnotatedErrors. 5 | * 6 | * A formatter has the following signature: FormattedError => Array. 7 | * It takes a formatted error produced by a transformer and returns a list 8 | * of log statements to print. 9 | * 10 | */ 11 | function formatErrors(errors, formatters, errorType) { 12 | const format = (formatter) => formatter(errors, errorType) || []; 13 | const flatten = (accum, curr) => accum.concat(curr); 14 | 15 | return formatters.map(format).reduce(flatten, []) 16 | } 17 | 18 | module.exports = formatErrors; 19 | -------------------------------------------------------------------------------- /src/core/transformErrors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const extractError = require('./extractWebpackError'); 4 | 5 | /** 6 | * Applies all transformers to all errors and returns "annotated" 7 | * errors. 8 | * 9 | * Each transformer should have the following signature WebpackError => AnnotatedError 10 | * 11 | * A WebpackError has the following fields: 12 | * - message 13 | * - file 14 | * - origin 15 | * - name 16 | * - severity 17 | * - webpackError (original error) 18 | * 19 | * An AnnotatedError should be an extension (Object.assign) of the WebpackError 20 | * and add whatever information is convenient for formatting. 21 | * In particular, they should have a 'priority' field. 22 | * 23 | * The plugin will only display errors having maximum priority at the same time. 24 | * 25 | * If they don't have a 'type' field, the will be handled by the default formatter. 26 | */ 27 | function processErrors (errors, transformers) { 28 | const transform = (error, transformer) => transformer(error); 29 | const applyTransformations = (error) => transformers.reduce(transform, error); 30 | 31 | return errors.map(extractError).map(applyTransformations); 32 | } 33 | 34 | module.exports = processErrors; 35 | -------------------------------------------------------------------------------- /src/formatters/defaultError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const concat = require('../utils').concat; 4 | const formatTitle = require('../utils/colors').formatTitle; 5 | 6 | function displayError(severity, error) { 7 | const baseError = formatTitle(severity, severity); 8 | 9 | return concat( 10 | `${baseError} ${removeLoaders(error.file)}`, 11 | '', 12 | error.message, 13 | (error.origin ? error.origin : undefined), 14 | '', 15 | error.infos 16 | ); 17 | } 18 | 19 | function removeLoaders(file) { 20 | if (!file) { 21 | return ""; 22 | } 23 | const split = file.split('!'); 24 | const filePath = split[split.length - 1]; 25 | return `in ${filePath}`; 26 | } 27 | 28 | function isDefaultError(error) { 29 | return !error.type; 30 | } 31 | 32 | /** 33 | * Format errors without a type 34 | */ 35 | function format(errors, type) { 36 | return errors 37 | .filter(isDefaultError) 38 | .reduce((accum, error) => ( 39 | accum.concat(displayError(type, error)) 40 | ), []); 41 | } 42 | 43 | module.exports = format; 44 | -------------------------------------------------------------------------------- /src/formatters/eslintError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const concat = require('../utils').concat; 4 | const chalk = require('chalk'); 5 | 6 | const infos = [ 7 | 'You may use special comments to disable some warnings.', 8 | 'Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.', 9 | 'Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.' 10 | ]; 11 | 12 | function displayError(error) { 13 | return [error.message, ''] 14 | } 15 | 16 | function format(errors, type) { 17 | const lintErrors = errors.filter(e => e.type === 'lint-error'); 18 | if (lintErrors.length > 0) { 19 | const flatten = (accum, curr) => accum.concat(curr); 20 | return concat( 21 | lintErrors 22 | .map(error => displayError(error)) 23 | .reduce(flatten, []), 24 | infos 25 | ) 26 | } 27 | 28 | return []; 29 | } 30 | 31 | module.exports = format; 32 | -------------------------------------------------------------------------------- /src/formatters/moduleNotFound.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const concat = require('../utils').concat; 3 | 4 | function isRelative (module) { 5 | return module.startsWith('./') || module.startsWith('../'); 6 | } 7 | 8 | function formatFileList (files) { 9 | const length = files.length; 10 | if (!length) return ''; 11 | return ` in ${files[0]}${files[1] ? `, ${files[1]}` : ''}${length > 2 ? ` and ${length - 2} other${length === 3 ? '' : 's'}` : ''}`; 12 | } 13 | 14 | function formatGroup (group) { 15 | const files = group.errors.map(e => e.file).filter(Boolean); 16 | return `* ${group.module}${formatFileList(files)}`; 17 | } 18 | 19 | 20 | function forgetToInstall (missingDependencies) { 21 | const moduleNames = missingDependencies.map(missingDependency => missingDependency.module); 22 | 23 | if (missingDependencies.length === 1) { 24 | return `To install it, you can run: npm install --save ${moduleNames.join(' ')}`; 25 | } 26 | 27 | return `To install them, you can run: npm install --save ${moduleNames.join(' ')}`; 28 | } 29 | 30 | function dependenciesNotFound (dependencies) { 31 | if (dependencies.length === 0) return; 32 | 33 | return concat( 34 | dependencies.length === 1 ? 'This dependency was not found:' : 'These dependencies were not found:', 35 | '', 36 | dependencies.map(formatGroup), 37 | '', 38 | forgetToInstall(dependencies) 39 | ); 40 | } 41 | 42 | function relativeModulesNotFound (modules) { 43 | if (modules.length === 0) return; 44 | 45 | return concat( 46 | modules.length === 1 ? 'This relative module was not found:' : 'These relative modules were not found:', 47 | '', 48 | modules.map(formatGroup) 49 | ); 50 | } 51 | 52 | function groupModules (errors) { 53 | const missingModule = new Map(); 54 | 55 | errors.forEach((error) => { 56 | if (!missingModule.has(error.module)) { 57 | missingModule.set(error.module, []) 58 | } 59 | missingModule.get(error.module).push(error); 60 | }); 61 | 62 | return Array.from(missingModule.keys()).map(module => ({ 63 | module: module, 64 | relative: isRelative(module), 65 | errors: missingModule.get(module), 66 | })); 67 | } 68 | 69 | function formatErrors (errors) { 70 | if (errors.length === 0) { 71 | return []; 72 | } 73 | 74 | const groups = groupModules(errors); 75 | 76 | const dependencies = groups.filter(group => !group.relative); 77 | const relativeModules = groups.filter(group => group.relative); 78 | 79 | return concat( 80 | dependenciesNotFound(dependencies), 81 | dependencies.length && relativeModules.length ? ['', ''] : null, 82 | relativeModulesNotFound(relativeModules) 83 | ); 84 | } 85 | 86 | function format (errors) { 87 | return formatErrors(errors.filter((e) => ( 88 | e.type === 'module-not-found' 89 | ))); 90 | } 91 | 92 | module.exports = format; 93 | -------------------------------------------------------------------------------- /src/friendly-errors-plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const chalk = require('chalk'); 5 | const os = require('os'); 6 | const transformErrors = require('./core/transformErrors'); 7 | const formatErrors = require('./core/formatErrors'); 8 | const output = require('./output'); 9 | const utils = require('./utils'); 10 | 11 | const concat = utils.concat; 12 | const uniqueBy = utils.uniqueBy; 13 | 14 | const defaultTransformers = [ 15 | require('./transformers/babelSyntax'), 16 | require('./transformers/moduleNotFound'), 17 | require('./transformers/esLintError'), 18 | ]; 19 | 20 | const defaultFormatters = [ 21 | require('./formatters/moduleNotFound'), 22 | require('./formatters/eslintError'), 23 | require('./formatters/defaultError'), 24 | ]; 25 | 26 | class FriendlyErrorsWebpackPlugin { 27 | 28 | constructor(options) { 29 | options = options || {}; 30 | this.compilationSuccessInfo = options.compilationSuccessInfo || {}; 31 | this.onErrors = options.onErrors; 32 | this.shouldClearConsole = options.clearConsole == null ? true : Boolean(options.clearConsole); 33 | this.formatters = concat(defaultFormatters, options.additionalFormatters); 34 | this.transformers = concat(defaultTransformers, options.additionalTransformers); 35 | this.previousEndTimes = {}; 36 | } 37 | 38 | apply(compiler) { 39 | 40 | const doneFn = stats => { 41 | this.clearConsole(); 42 | 43 | const hasErrors = stats.hasErrors(); 44 | const hasWarnings = stats.hasWarnings(); 45 | 46 | if (!hasErrors && !hasWarnings) { 47 | this.displaySuccess(stats); 48 | return; 49 | } 50 | 51 | if (hasErrors) { 52 | this.displayErrors(extractErrorsFromStats(stats, 'errors'), 'error'); 53 | return; 54 | } 55 | 56 | if (hasWarnings) { 57 | this.displayErrors(extractErrorsFromStats(stats, 'warnings'), 'warning'); 58 | } 59 | }; 60 | 61 | const invalidFn = () => { 62 | this.clearConsole(); 63 | output.title('info', 'WAIT', 'Compiling...'); 64 | }; 65 | 66 | if (compiler.hooks) { 67 | const plugin = { name: 'FriendlyErrorsWebpackPlugin' }; 68 | 69 | compiler.hooks.done.tap(plugin, doneFn); 70 | compiler.hooks.invalid.tap(plugin, invalidFn); 71 | } else { 72 | compiler.plugin('done', doneFn); 73 | compiler.plugin('invalid', invalidFn); 74 | } 75 | } 76 | 77 | clearConsole() { 78 | if (this.shouldClearConsole) { 79 | output.clearConsole(); 80 | } 81 | } 82 | 83 | displaySuccess(stats) { 84 | const time = isMultiStats(stats) ? this.getMultiStatsCompileTime(stats) : this.getStatsCompileTime(stats); 85 | output.title('success', 'DONE', 'Compiled successfully in ' + time + 'ms'); 86 | 87 | if (this.compilationSuccessInfo.messages) { 88 | this.compilationSuccessInfo.messages.forEach(message => output.info(message)); 89 | } 90 | if (this.compilationSuccessInfo.notes) { 91 | output.log(); 92 | this.compilationSuccessInfo.notes.forEach(note => output.note(note)); 93 | } 94 | } 95 | 96 | displayErrors(errors, severity) { 97 | const processedErrors = transformErrors(errors, this.transformers); 98 | 99 | const topErrors = getMaxSeverityErrors(processedErrors); 100 | const nbErrors = topErrors.length; 101 | 102 | const subtitle = severity === 'error' ? 103 | `Failed to compile with ${nbErrors} ${severity}${nbErrors === 1 ? '' : 's'}` : 104 | `Compiled with ${nbErrors} ${severity}${nbErrors === 1 ? '' : 's'}`; 105 | output.title(severity, severity.toUpperCase(), subtitle); 106 | 107 | if (this.onErrors) { 108 | this.onErrors(severity, topErrors); 109 | } 110 | 111 | formatErrors(topErrors, this.formatters, severity) 112 | .forEach(chunk => output.log(chunk)); 113 | } 114 | 115 | getStatsCompileTime(stats, statsIndex) { 116 | // When we have multi compilations but only one of them is rebuilt, we need to skip the 117 | // unchanged compilers to report the true rebuild time. 118 | if (statsIndex !== undefined) { 119 | if (this.previousEndTimes[statsIndex] === stats.endTime) { 120 | return 0; 121 | } 122 | 123 | this.previousEndTimes[statsIndex] = stats.endTime; 124 | } 125 | 126 | return stats.endTime - stats.startTime; 127 | } 128 | 129 | getMultiStatsCompileTime(stats) { 130 | // Webpack multi compilations run in parallel so using the longest duration. 131 | // https://webpack.github.io/docs/configuration.html#multiple-configurations 132 | return stats.stats 133 | .reduce((time, stats, index) => Math.max(time, this.getStatsCompileTime(stats, index)), 0); 134 | } 135 | } 136 | 137 | function extractErrorsFromStats(stats, type) { 138 | if (isMultiStats(stats)) { 139 | const errors = stats.stats 140 | .reduce((errors, stats) => errors.concat(extractErrorsFromStats(stats, type)), []); 141 | // Dedupe to avoid showing the same error many times when multiple 142 | // compilers depend on the same module. 143 | return uniqueBy(errors, error => error.message); 144 | } 145 | 146 | const findErrorsRecursive = (compilation) => { 147 | const errors = compilation[type]; 148 | if (errors.length === 0 && compilation.children) { 149 | for (const child of compilation.children) { 150 | errors.push(...findErrorsRecursive(child)); 151 | } 152 | } 153 | 154 | return uniqueBy(errors, error => error.message); 155 | }; 156 | 157 | return findErrorsRecursive(stats.compilation); 158 | } 159 | 160 | function isMultiStats(stats) { 161 | return stats.stats; 162 | } 163 | 164 | function getMaxSeverityErrors(errors) { 165 | const maxSeverity = getMaxInt(errors, 'severity'); 166 | return errors.filter(e => e.severity === maxSeverity); 167 | } 168 | 169 | function getMaxInt(collection, propertyName) { 170 | return collection.reduce((res, curr) => { 171 | return curr[propertyName] > res ? curr[propertyName] : res; 172 | }, 0) 173 | } 174 | 175 | module.exports = FriendlyErrorsWebpackPlugin; 176 | -------------------------------------------------------------------------------- /src/output.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const colors = require('./utils/colors'); 4 | const chalk = require('chalk'); 5 | const stringWidth = require('string-width'); 6 | const readline = require('readline'); 7 | const stripAnsi = require('strip-ansi'); 8 | 9 | class Debugger { 10 | 11 | constructor () { 12 | this.enabled = true; 13 | this.capturing = false; 14 | this.capturedMessages = []; 15 | } 16 | 17 | enable () { 18 | this.enabled = true; 19 | } 20 | 21 | capture () { 22 | this.enabled = true; 23 | this.capturing = true; 24 | } 25 | 26 | endCapture () { 27 | this.enabled = false; 28 | this.capturing = false; 29 | this.capturedMessages = []; 30 | } 31 | 32 | log () { 33 | if (this.enabled) { 34 | this.captureConsole(Array.from(arguments), console.log); 35 | } 36 | } 37 | 38 | info (message) { 39 | if (this.enabled) { 40 | const titleFormatted = colors.formatTitle('info', 'I'); 41 | this.log(titleFormatted, message); 42 | } 43 | } 44 | 45 | note (message) { 46 | if (this.enabled) { 47 | const titleFormatted = colors.formatTitle('note', 'N'); 48 | this.log(titleFormatted, message); 49 | } 50 | } 51 | 52 | title (severity, title, subtitle) { 53 | if (this.enabled) { 54 | const date = new Date(); 55 | const dateString = chalk.grey(date.toLocaleTimeString()); 56 | const titleFormatted = colors.formatTitle(severity, title); 57 | const subTitleFormatted = colors.formatText(severity, subtitle); 58 | const message = `${titleFormatted} ${subTitleFormatted}` 59 | 60 | // In test environment we don't include timestamp 61 | if(process.env.NODE_ENV === 'test') { 62 | this.log(message); 63 | this.log(); 64 | return; 65 | } 66 | 67 | // Make timestamp appear at the end of the line 68 | let logSpace = process.stdout.columns - stringWidth(message) - stringWidth(dateString) 69 | if (logSpace <= 0) { 70 | logSpace = 10 71 | } 72 | 73 | this.log(`${message}${' '.repeat(logSpace)}${dateString}`); 74 | this.log(); 75 | } 76 | } 77 | 78 | clearConsole () { 79 | if (!process.env.CI && !this.capturing && this.enabled && process.stdout.isTTY) { 80 | // Fill screen with blank lines. Then move to 0 (beginning of visible part) and clear it 81 | const blank = '\n'.repeat(process.stdout.rows) 82 | console.log(blank) 83 | readline.cursorTo(process.stdout, 0, 0) 84 | readline.clearScreenDown(process.stdout) 85 | } 86 | } 87 | 88 | captureLogs (fun) { 89 | try { 90 | this.capture(); 91 | fun.call(); 92 | return this.capturedMessages; 93 | } finally { 94 | this.endCapture(); 95 | } 96 | } 97 | 98 | captureConsole (args, method) { 99 | if (this.capturing) { 100 | this.capturedMessages.push(stripAnsi(args.join(' ')).trim()); 101 | } else { 102 | method.apply(console, args); 103 | } 104 | } 105 | } 106 | 107 | function capitalizeFirstLetter (string) { 108 | return string.charAt(0).toUpperCase() + string.slice(1); 109 | } 110 | 111 | module.exports = new Debugger(); 112 | -------------------------------------------------------------------------------- /src/transformers/babelSyntax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This will be removed in next versions as it is not handled in the babel-loader 5 | * See: https://github.com/geowarin/friendly-errors-webpack-plugin/issues/2 6 | */ 7 | function cleanStackTrace(message) { 8 | return message 9 | .replace(/^\s*at\s.*:\d+:\d+[\s)]*\n/gm, ''); // at ... ...:x:y 10 | } 11 | 12 | function cleanMessage(message) { 13 | return message 14 | // match until the last semicolon followed by a space 15 | // this should match 16 | // linux => "(SyntaxError: )Unexpected token (5:11)" 17 | // windows => "(SyntaxError: C:/projects/index.js: )Unexpected token (5:11)" 18 | .replace(/^Module build failed.*:\s/, 'Syntax Error: ') 19 | // remove mini-css-extract-plugin loader tracing errors 20 | .replace(/^Syntax Error: ModuleBuildError:.*:\s/, '') 21 | // remove babel extra wording and path 22 | .replace(/^Syntax Error: SyntaxError: (([A-Z]:)?\/.*:\s)?/, 'Syntax Error: '); 23 | } 24 | 25 | function isBabelSyntaxError(e) { 26 | return e.name === 'ModuleBuildError' || e.name === 'ModuleBuildError' && 27 | e.message.indexOf('SyntaxError') >= 0; 28 | } 29 | 30 | function transform(error) { 31 | if (isBabelSyntaxError(error)) { 32 | return Object.assign({}, error, { 33 | message: cleanStackTrace(cleanMessage(error.message) + '\n'), 34 | severity: 1000, 35 | name: 'Syntax Error', 36 | }); 37 | } 38 | 39 | return error; 40 | } 41 | 42 | module.exports = transform; -------------------------------------------------------------------------------- /src/transformers/esLintError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isEslintError (e) { 4 | return e.originalStack 5 | .some(stackframe => stackframe.fileName && stackframe.fileName.indexOf('eslint-loader') > 0) || 6 | e.name === 'ESLintError'; 7 | } 8 | 9 | function transform(error) { 10 | if (isEslintError(error)) { 11 | return Object.assign({}, error, { 12 | name: 'Lint error', 13 | type: 'lint-error', 14 | }); 15 | } 16 | 17 | return error; 18 | } 19 | 20 | module.exports = transform; 21 | -------------------------------------------------------------------------------- /src/transformers/moduleNotFound.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const TYPE = 'module-not-found'; 4 | 5 | function isModuleNotFoundError (e) { 6 | const webpackError = e.webpackError || {}; 7 | return webpackError.dependencies 8 | && webpackError.dependencies.length > 0 9 | && e.name === 'ModuleNotFoundError' 10 | && e.message.indexOf('Module not found') === 0; 11 | } 12 | 13 | function transform(error) { 14 | const webpackError = error.webpackError; 15 | if (isModuleNotFoundError(error)) { 16 | const dependency = webpackError.dependencies[0]; 17 | const module = dependency.request || dependency.options.request; 18 | return Object.assign({}, error, { 19 | message: `Module not found ${module}`, 20 | type: TYPE, 21 | severity: 900, 22 | module, 23 | name: 'Module not found' 24 | }); 25 | } 26 | 27 | return error; 28 | } 29 | 30 | module.exports = transform; 31 | -------------------------------------------------------------------------------- /src/utils/colors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | 5 | function formatTitle(severity, message) { 6 | return chalk[bgColor(severity)].black('', message, ''); 7 | } 8 | 9 | function formatText(severity, message) { 10 | return chalk[textColor(severity)](message); 11 | } 12 | 13 | function bgColor(severity) { 14 | const color = textColor(severity); 15 | return 'bg'+ capitalizeFirstLetter(color) 16 | } 17 | 18 | function textColor(severity) { 19 | switch (severity.toLowerCase()) { 20 | case 'success': return 'green'; 21 | case 'info': return 'blue'; 22 | case 'note': return 'white'; 23 | case 'warning': return 'yellow'; 24 | case 'error': return 'red'; 25 | default: return 'red'; 26 | } 27 | } 28 | 29 | function capitalizeFirstLetter(string) { 30 | return string.charAt(0).toUpperCase() + string.slice(1); 31 | } 32 | 33 | module.exports = { 34 | bgColor: bgColor, 35 | textColor: textColor, 36 | formatTitle: formatTitle, 37 | formatText: formatText 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Concat and flattens non-null values. 5 | * Ex: concat(1, undefined, 2, [3, 4]) = [1, 2, 3, 4] 6 | */ 7 | function concat() { 8 | const args = Array.from(arguments).filter(e => e != null); 9 | const baseArray = Array.isArray(args[0]) ? args[0] : [args[0]]; 10 | return Array.prototype.concat.apply(baseArray, args.slice(1)); 11 | } 12 | 13 | /** 14 | * Dedupes array based on criterion returned from iteratee function. 15 | * Ex: uniqueBy( 16 | * [{ id: 1 }, { id: 1 }, { id: 2 }], 17 | * val => val.id 18 | * ) = [{ id: 1 }, { id: 2 }] 19 | */ 20 | function uniqueBy(arr, fun) { 21 | const seen = {}; 22 | return arr.filter(el => { 23 | const e = fun(el); 24 | return !(e in seen) && (seen[e] = 1); 25 | }) 26 | } 27 | 28 | module.exports = { 29 | concat: concat, 30 | uniqueBy: uniqueBy 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | **/dist -------------------------------------------------------------------------------- /test/fixtures/babel-syntax-babel-6/.babelrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/babel-syntax-babel-6/index.js: -------------------------------------------------------------------------------- 1 | 2 | class MyComponent extends React.Component { 3 | 4 | render() { 5 | return
6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/babel-syntax-babel-6/webpack.config.js: -------------------------------------------------------------------------------- 1 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: __dirname + "/index.js", 6 | output: { 7 | path: __dirname + "/dist", 8 | filename: "bundle.js" 9 | }, 10 | plugins: [ 11 | new FriendlyErrorsWebpackPlugin() 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | exclude: /node_modules/, 18 | loader: require.resolve('babel-loader-7'), 19 | } 20 | ] 21 | }, 22 | }; -------------------------------------------------------------------------------- /test/fixtures/babel-syntax-babel-7/.babelrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/fixtures/babel-syntax-babel-7/index.js: -------------------------------------------------------------------------------- 1 | 2 | class MyComponent extends React.Component { 3 | 4 | render() { 5 | return
6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/babel-syntax-babel-7/webpack.config.js: -------------------------------------------------------------------------------- 1 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: __dirname + "/index.js", 6 | output: { 7 | path: __dirname + "/dist", 8 | filename: "bundle.js" 9 | }, 10 | plugins: [ 11 | new FriendlyErrorsWebpackPlugin() 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | exclude: /node_modules/, 18 | loader: require.resolve('babel-loader'), 19 | } 20 | ] 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /test/fixtures/eslint-warnings/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "commonjs": true, 7 | "es6": true, 8 | "jest": true, 9 | "node": true 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 6, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | // http://eslint.org/docs/rules/ 17 | "no-unused-expressions": "warn", 18 | "no-unused-labels": "warn", 19 | "no-unused-vars": ["warn", { "vars": "local", "args": "none" }] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/eslint-warnings/index.js: -------------------------------------------------------------------------------- 1 | require('./module'); 2 | 3 | const unused = 'I am unused'; 4 | const unused2 = 'I am unused too'; 5 | -------------------------------------------------------------------------------- /test/fixtures/eslint-warnings/module.js: -------------------------------------------------------------------------------- 1 | const unused = 'I am unused'; 2 | -------------------------------------------------------------------------------- /test/fixtures/eslint-warnings/webpack.config.js: -------------------------------------------------------------------------------- 1 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: __dirname + "/index.js", 6 | output: { 7 | path: __dirname + "/dist", 8 | filename: "bundle.js" 9 | }, 10 | plugins: [ 11 | new FriendlyErrorsWebpackPlugin(), 12 | 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | enforce: 'pre', 18 | test: /\.js$/, 19 | loader: 'eslint-loader', 20 | include: __dirname, 21 | }, 22 | { 23 | test: /\.jsx?$/, 24 | exclude: /node_modules/, 25 | loader: require.resolve('babel-loader-7'), 26 | } 27 | ] 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/eslint-webpack-plugin-warnings/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "commonjs": true, 7 | "es6": true, 8 | "jest": true, 9 | "node": true 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 6, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | // http://eslint.org/docs/rules/ 17 | "no-unused-expressions": "warn", 18 | "no-unused-labels": "warn", 19 | "no-unused-vars": ["warn", { "vars": "local", "args": "none" }] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/eslint-webpack-plugin-warnings/index.js: -------------------------------------------------------------------------------- 1 | require('./module'); 2 | 3 | const unused = 'I am unused'; 4 | const unused2 = 'I am unused too'; 5 | -------------------------------------------------------------------------------- /test/fixtures/eslint-webpack-plugin-warnings/module.js: -------------------------------------------------------------------------------- 1 | const unused = 'I am unused'; 2 | -------------------------------------------------------------------------------- /test/fixtures/eslint-webpack-plugin-warnings/webpack.config.js: -------------------------------------------------------------------------------- 1 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 2 | const ESLintPlugin = require('eslint-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: __dirname + "/index.js", 7 | output: { 8 | path: __dirname + "/dist", 9 | filename: "bundle.js" 10 | }, 11 | plugins: [ 12 | new FriendlyErrorsWebpackPlugin(), 13 | new ESLintPlugin({ 14 | eslintPath: require.resolve('eslint-7') 15 | }) 16 | ], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.jsx?$/, 21 | exclude: /node_modules/, 22 | loader: 'babel-loader', 23 | } 24 | ] 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/mini-css-extract-babel-syntax/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | color: red; 3 | 4 | .container { 5 | margin-top: 100px 6 | 7 | .test { 8 | vertical-align: middle; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /test/fixtures/mini-css-extract-babel-syntax/webpack.config.js: -------------------------------------------------------------------------------- 1 | // mini-css-extract-plugin test 2 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | 5 | module.exports = { 6 | entry: __dirname + "/index.scss", 7 | output: { 8 | path: __dirname + "/dist", 9 | }, 10 | plugins: [ 11 | new FriendlyErrorsWebpackPlugin(), 12 | new MiniCssExtractPlugin({ 13 | filename: '[name].css', 14 | chunkFilename: '[id].css', 15 | }), 16 | ], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(s*)css$/, 21 | exclude: /node_modules/, 22 | use: [ 23 | MiniCssExtractPlugin.loader, 24 | 'style-loader', 25 | 'css-loader', 26 | { 27 | loader: 'sass-loader', 28 | options: { 29 | implementation: require('sass'), 30 | } 31 | }, 32 | ], 33 | } 34 | ] 35 | }, 36 | }; -------------------------------------------------------------------------------- /test/fixtures/module-errors/index.js: -------------------------------------------------------------------------------- 1 | require('not-found'); 2 | require('./non-existing'); 3 | require('../non-existing'); 4 | -------------------------------------------------------------------------------- /test/fixtures/module-errors/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 3 | 4 | module.exports = { 5 | entry: __dirname + "/index.js", 6 | output: { 7 | path: __dirname + "/dist", 8 | filename: "bundle.js" 9 | }, 10 | plugins: [ 11 | new FriendlyErrorsWebpackPlugin() 12 | ] 13 | }; -------------------------------------------------------------------------------- /test/fixtures/multi-compiler-module-errors/index.js: -------------------------------------------------------------------------------- 1 | require('./non-existing'); 2 | -------------------------------------------------------------------------------- /test/fixtures/multi-compiler-module-errors/index2.js: -------------------------------------------------------------------------------- 1 | require('not-found'); 2 | -------------------------------------------------------------------------------- /test/fixtures/multi-compiler-module-errors/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | entry: __dirname + "/index.js", 4 | output: { 5 | path: __dirname + "/dist", 6 | filename: "bundle.js" 7 | } 8 | }, 9 | { 10 | entry: __dirname + "/index2.js", 11 | output: { 12 | path: __dirname + "/dist", 13 | filename: "bundle2.js" 14 | } 15 | } 16 | ]; 17 | -------------------------------------------------------------------------------- /test/fixtures/multi-compiler-success/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'I am an entry point'; 2 | -------------------------------------------------------------------------------- /test/fixtures/multi-compiler-success/index2.js: -------------------------------------------------------------------------------- 1 | module.exports = 'I am another entry point'; 2 | -------------------------------------------------------------------------------- /test/fixtures/multi-compiler-success/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | mode: 'development', 4 | entry: __dirname + "/index.js", 5 | output: { 6 | path: __dirname + "/dist", 7 | filename: "bundle.js" 8 | } 9 | }, 10 | { 11 | mode: 'development', 12 | entry: __dirname + "/index2.js", 13 | output: { 14 | path: __dirname + "/dist", 15 | filename: "bundle2.js" 16 | } 17 | } 18 | ]; 19 | -------------------------------------------------------------------------------- /test/fixtures/multi-postcss-warnings/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: grid; 3 | grid-gap: 1px; 4 | } -------------------------------------------------------------------------------- /test/fixtures/multi-postcss-warnings/index2.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: grid; 3 | grid-auto-flow: row; 4 | } -------------------------------------------------------------------------------- /test/fixtures/multi-postcss-warnings/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: { 4 | grid: true, 5 | } 6 | } 7 | }; -------------------------------------------------------------------------------- /test/fixtures/multi-postcss-warnings/webpack.config.js: -------------------------------------------------------------------------------- 1 | // postcss-loader warnings test (multi-compiler version) 2 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 3 | 4 | const COMMON_CONFIG = { 5 | mode: 'production', 6 | output: { 7 | path: __dirname + '/dist', 8 | }, 9 | plugins: [ 10 | new FriendlyErrorsWebpackPlugin(), 11 | ], 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.css$/, 16 | exclude: /node_modules/, 17 | use: [ 18 | { 19 | loader: 'css-loader', 20 | options: { 21 | importLoaders: 1, 22 | }, 23 | }, 24 | { 25 | loader: 'postcss-loader', 26 | options: { 27 | config: { 28 | path: __dirname + '/postcss.config.js' 29 | } 30 | }, 31 | }, 32 | ], 33 | } 34 | ] 35 | }, 36 | }; 37 | 38 | module.exports = [ 39 | Object.assign({}, { entry: __dirname + '/index.css' }, COMMON_CONFIG), 40 | Object.assign({}, { entry: __dirname + '/index2.css' }, COMMON_CONFIG), 41 | ]; -------------------------------------------------------------------------------- /test/fixtures/postcss-warnings/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: grid; 3 | grid-gap: 1px; 4 | } -------------------------------------------------------------------------------- /test/fixtures/postcss-warnings/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: { 4 | grid: true, 5 | } 6 | } 7 | }; -------------------------------------------------------------------------------- /test/fixtures/postcss-warnings/webpack.config.js: -------------------------------------------------------------------------------- 1 | // postcss-loader warnings test 2 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: __dirname + '/index.css', 7 | output: { 8 | path: __dirname + '/dist', 9 | }, 10 | plugins: [ 11 | new FriendlyErrorsWebpackPlugin(), 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/, 17 | exclude: /node_modules/, 18 | use: [ 19 | { 20 | loader: 'css-loader', 21 | options: { 22 | importLoaders: 1, 23 | }, 24 | }, 25 | { 26 | loader: 'postcss-loader', 27 | options: { 28 | config: { 29 | path: __dirname + '/postcss.config.js' 30 | } 31 | }, 32 | }, 33 | ], 34 | } 35 | ] 36 | }, 37 | }; -------------------------------------------------------------------------------- /test/fixtures/success/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'I am an entry point'; 2 | -------------------------------------------------------------------------------- /test/fixtures/success/webpack.config.js: -------------------------------------------------------------------------------- 1 | const FriendlyErrorsWebpackPlugin = require('../../../index'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: __dirname + "/index.js", 6 | output: { 7 | path: __dirname + "/dist", 8 | filename: "bundle.js" 9 | }, 10 | plugins: [ 11 | new FriendlyErrorsWebpackPlugin() 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /test/integration.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const output = require('../src/output'); 4 | const webpack = require('webpack'); 5 | const FriendlyErrorsWebpackPlugin = require('../src/friendly-errors-plugin'); 6 | const MemoryFileSystem = require('memory-fs'); 7 | const path = require('path'); 8 | 9 | const webpackPromise = function (config, globalPlugins) { 10 | const compiler = webpack(config); 11 | compiler.outputFileSystem = new MemoryFileSystem(); 12 | if (Array.isArray(globalPlugins)) { 13 | globalPlugins.forEach(p => p.apply(compiler)); 14 | } 15 | 16 | return new Promise((resolve, reject) => { 17 | compiler.run(err => { 18 | if (err) { 19 | reject(err) 20 | } 21 | resolve() 22 | }); 23 | }); 24 | }; 25 | 26 | async function executeAndGetLogs(fixture, globalPlugins) { 27 | try { 28 | output.capture(); 29 | await webpackPromise(require(fixture), globalPlugins); 30 | return output.capturedMessages; 31 | } finally { 32 | output.endCapture() 33 | } 34 | } 35 | 36 | it('integration : success', async() => { 37 | 38 | const logs = await executeAndGetLogs('./fixtures/success/webpack.config') 39 | 40 | expect(logs.join('\n')).toMatch(/DONE {2}Compiled successfully in (.\d*)ms/); 41 | }); 42 | 43 | it('integration : module-errors', async() => { 44 | 45 | const logs = await executeAndGetLogs('./fixtures/module-errors/webpack.config.js'); 46 | 47 | expect(logs).toEqual([ 48 | 'ERROR Failed to compile with 3 errors', 49 | '', 50 | 'This dependency was not found:', 51 | '', 52 | '* not-found in ./test/fixtures/module-errors/index.js', 53 | '', 54 | 'To install it, you can run: npm install --save not-found', 55 | '', 56 | '', 57 | 'These relative modules were not found:', 58 | '', 59 | '* ../non-existing in ./test/fixtures/module-errors/index.js', 60 | '* ./non-existing in ./test/fixtures/module-errors/index.js', 61 | ]); 62 | }); 63 | 64 | function filename(filePath) { 65 | return path.join(__dirname, path.normalize(filePath)) 66 | } 67 | 68 | it('integration : should display eslint warnings', async() => { 69 | 70 | const logs = await executeAndGetLogs('./fixtures/eslint-warnings/webpack.config.js'); 71 | 72 | expect(logs.join('\n')).toEqual( 73 | `WARNING Compiled with 2 warnings 74 | 75 | Module Warning (from ./node_modules/eslint-loader/index.js): 76 | 77 | ${filename('fixtures/eslint-warnings/index.js')} 78 | 3:7 warning 'unused' is assigned a value but never used no-unused-vars 79 | 4:7 warning 'unused2' is assigned a value but never used no-unused-vars 80 | 81 | ✖ 2 problems (0 errors, 2 warnings) 82 | 83 | Module Warning (from ./node_modules/eslint-loader/index.js): 84 | 85 | ${filename('fixtures/eslint-warnings/module.js')} 86 | 1:7 warning 'unused' is assigned a value but never used no-unused-vars 87 | 88 | ✖ 1 problem (0 errors, 1 warning) 89 | 90 | You may use special comments to disable some warnings. 91 | Use // eslint-disable-next-line to ignore the next line. 92 | Use /* eslint-disable */ to ignore all warnings in a file.` 93 | ) 94 | }); 95 | 96 | it('integration : should display eslint-webpack-plugin warnings', async() => { 97 | 98 | const logs = await executeAndGetLogs('./fixtures/eslint-webpack-plugin-warnings/webpack.config.js'); 99 | 100 | expect(logs.join('\n')).toEqual( 101 | `WARNING Compiled with 1 warning 102 | 103 | ${filename('fixtures/eslint-webpack-plugin-warnings/index.js')} 104 | 3:7 warning 'unused' is assigned a value but never used no-unused-vars 105 | 4:7 warning 'unused2' is assigned a value but never used no-unused-vars 106 | 107 | ${filename('fixtures/eslint-webpack-plugin-warnings/module.js')} 108 | 1:7 warning 'unused' is assigned a value but never used no-unused-vars 109 | 110 | ✖ 3 problems (0 errors, 3 warnings) 111 | 112 | You may use special comments to disable some warnings. 113 | Use // eslint-disable-next-line to ignore the next line. 114 | Use /* eslint-disable */ to ignore all warnings in a file.` 115 | ) 116 | }); 117 | 118 | it('integration : babel syntax error with babel-loader 7 (babel 6)', async() => { 119 | 120 | const logs = await executeAndGetLogs('./fixtures/babel-syntax-babel-6/webpack.config'); 121 | 122 | expect(logs).toEqual([ 123 | 'ERROR Failed to compile with 1 error', 124 | '', 125 | 'error in ./test/fixtures/babel-syntax-babel-6/index.js', 126 | '', 127 | `Syntax Error: Unexpected token (5:11) 128 | 129 | 3 |${' '} 130 | 4 | render() { 131 | > 5 | return
132 | | ^ 133 | 6 | } 134 | 7 | }`, 135 | '' 136 | ]); 137 | }); 138 | it('integration : babel syntax error with babel-loader 8 (babel 7)', async() => { 139 | 140 | const logs = await executeAndGetLogs('./fixtures/babel-syntax-babel-7/webpack.config'); 141 | 142 | expect(logs).toEqual([ 143 | 'ERROR Failed to compile with 1 error', 144 | '', 145 | 'error in ./test/fixtures/babel-syntax-babel-7/index.js', 146 | '', 147 | `Syntax Error: Unexpected token (5:11) 148 | 149 | 3 |${' '} 150 | 4 | render() { 151 | > 5 | return
152 | | ^ 153 | 6 | } 154 | 7 | }`, 155 | '' 156 | ]); 157 | }); 158 | 159 | it('integration : mini CSS extract plugin babel error', async() => { 160 | 161 | const logs = await executeAndGetLogs('./fixtures/mini-css-extract-babel-syntax/webpack.config'); 162 | const clean_logs = logs.toString().replace(/\"/g, ""); //<- double quotes issue with slash 163 | expect(clean_logs).toEqual( 164 | `ERROR Failed to compile with 1 error,,error in ./test/fixtures/mini-css-extract-babel-syntax/index.scss,,.test { 165 | ^ 166 | Expected digit. 167 | ╷ 168 | 7 │ .test { 169 | │ ^ 170 | ╵ 171 | stdin 7:4 root stylesheet 172 | in ${filename('fixtures/mini-css-extract-babel-syntax/index.scss')} (line 7, column 4),` 173 | ); 174 | }); 175 | 176 | it('integration : webpack multi compiler : success', async() => { 177 | 178 | // We apply the plugin directly to the compiler when targeting multi-compiler 179 | let globalPlugins = [new FriendlyErrorsWebpackPlugin()]; 180 | const logs = await executeAndGetLogs('./fixtures/multi-compiler-success/webpack.config', globalPlugins); 181 | 182 | expect(logs.join('\n')).toMatch(/DONE {2}Compiled successfully in (.\d*)ms/) 183 | }); 184 | 185 | it('integration : webpack multi compiler : module-errors', async() => { 186 | 187 | // We apply the plugin directly to the compiler when targeting multi-compiler 188 | let globalPlugins = [new FriendlyErrorsWebpackPlugin()]; 189 | const logs = await executeAndGetLogs('./fixtures/multi-compiler-module-errors/webpack.config', globalPlugins); 190 | 191 | expect(logs).toEqual([ 192 | 'ERROR Failed to compile with 2 errors', 193 | '', 194 | 'This dependency was not found:', 195 | '', 196 | '* not-found in ./test/fixtures/multi-compiler-module-errors/index2.js', 197 | '', 198 | 'To install it, you can run: npm install --save not-found', 199 | '', 200 | '', 201 | 'This relative module was not found:', 202 | '', 203 | '* ./non-existing in ./test/fixtures/multi-compiler-module-errors/index.js', 204 | ]); 205 | }); 206 | 207 | it('integration : postcss-loader : warnings', async() => { 208 | 209 | const logs = await executeAndGetLogs('./fixtures/postcss-warnings/webpack.config'); 210 | expect(logs).toEqual([ 211 | 'WARNING Compiled with 1 warning', 212 | '', 213 | 'warning in ./test/fixtures/postcss-warnings/index.css', 214 | '', 215 | `Module Warning (from ./node_modules/postcss-loader/src/index.js): 216 | Warning 217 | 218 | (3:2) grid-gap only works if grid-template(-areas) is being used`, 219 | '' 220 | ]); 221 | }); 222 | 223 | it('integration : postcss-loader : warnings (multi-compiler version)', async() => { 224 | 225 | const logs = await executeAndGetLogs('./fixtures/multi-postcss-warnings/webpack.config'); 226 | expect(logs).toEqual([ 227 | 'WARNING Compiled with 1 warning', 228 | '', 229 | 'warning in ./test/fixtures/multi-postcss-warnings/index.css', 230 | '', 231 | `Module Warning (from ./node_modules/postcss-loader/src/index.js): 232 | Warning 233 | 234 | (3:2) grid-gap only works if grid-template(-areas) is being used`, 235 | '', 236 | 'WARNING Compiled with 1 warning', 237 | '', 238 | 'warning in ./test/fixtures/multi-postcss-warnings/index2.css', 239 | '', 240 | `Module Warning (from ./node_modules/postcss-loader/src/index.js): 241 | Warning 242 | 243 | (3:2) grid-auto-flow works only if grid-template-rows and grid-template-columns are present in the same rule`, 244 | '' 245 | ]); 246 | }); 247 | -------------------------------------------------------------------------------- /test/unit/formatErrors.spec.js: -------------------------------------------------------------------------------- 1 | const formatErrors = require('../../src/core/formatErrors'); 2 | 3 | const simple = (errors) => errors 4 | .filter(error => !error.type).map(e => e.message); 5 | 6 | const allCaps = (errors) => errors 7 | .filter(error => error.type == 'other').map(e => e.message.toUpperCase()); 8 | 9 | const notFound = (errors) => errors 10 | .filter(error => error.type === 'not-found').map(() => 'Not found'); 11 | 12 | const formatters = [allCaps]; 13 | 14 | it('formats the error based on the matching formatters', () => { 15 | const errors = [ 16 | { message: 'Error 1', type: undefined }, 17 | { message: 'Error 2', type: 'other' }, 18 | { message: 'Error 3', type: 'not-found' }, 19 | ]; 20 | 21 | expect(formatErrors(errors, [simple, allCaps, notFound], 'Error')).toEqual([ 22 | 'Error 1', 23 | 'ERROR 2', 24 | 'Not found', 25 | ]); 26 | }); 27 | -------------------------------------------------------------------------------- /test/unit/formatters/defaultError.spec.js: -------------------------------------------------------------------------------- 1 | const defaultError = require('../../../src/formatters/defaultError'); 2 | const stripAnsi = require('strip-ansi'); 3 | 4 | const noColor = (arr) => arr.map(stripAnsi); 5 | const error = { message: 'Error message', file: './src/index.js' }; 6 | 7 | it('Formats errors with no type', () => { 8 | expect(noColor(defaultError([error], 'Warning'))).toEqual([ 9 | ' Warning in ./src/index.js', 10 | '', 11 | 'Error message', 12 | '', 13 | ]); 14 | }); 15 | 16 | it('Does not format other errors', () => { 17 | const otherError = { type: 'other-error' }; 18 | expect(noColor(defaultError([otherError], 'Error'))).toEqual([]); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/formatters/moduleNotFound.spec.js: -------------------------------------------------------------------------------- 1 | const moduleNotFound = require('../../../src/formatters/moduleNotFound'); 2 | 3 | it('Formats module-not-found errors', () => { 4 | const error = { type: 'module-not-found', module: 'redux' }; 5 | expect(moduleNotFound([error])).toEqual([ 6 | 'This dependency was not found:', 7 | '', 8 | '* redux', 9 | '', 10 | 'To install it, you can run: npm install --save redux' 11 | ]); 12 | }); 13 | 14 | it('Groups all module-not-found into one', () => { 15 | const reduxError = { type: 'module-not-found', module: 'redux' }; 16 | const reactError = { type: 'module-not-found', module: 'react' }; 17 | expect(moduleNotFound([reduxError, reactError])).toEqual([ 18 | 'These dependencies were not found:', 19 | '', 20 | '* redux', 21 | '* react', 22 | '', 23 | 'To install them, you can run: npm install --save redux react' 24 | ]); 25 | }); 26 | 27 | it('Groups same module in module-not-found with 2 files', () => { 28 | const reduxError = { type: 'module-not-found', module: 'redux' }; 29 | const reactError1 = { type: 'module-not-found', module: 'react', file: './src/file1.js' }; 30 | const reactError2 = { type: 'module-not-found', module: 'react', file: '../src/file2.js' }; 31 | expect(moduleNotFound([reduxError, reactError1, reactError2])).toEqual([ 32 | 'These dependencies were not found:', 33 | '', 34 | '* redux', 35 | '* react in ./src/file1.js, ../src/file2.js', 36 | '', 37 | 'To install them, you can run: npm install --save redux react' 38 | ]); 39 | }); 40 | 41 | it('Groups same module in module-not-found with 3 files', () => { 42 | const reduxError = { type: 'module-not-found', module: 'redux' }; 43 | const reactError1 = { type: 'module-not-found', module: 'react', file: './src/file1.js' }; 44 | const reactError2 = { type: 'module-not-found', module: 'react', file: './src/file2.js' }; 45 | const reactError3 = { type: 'module-not-found', module: 'react', file: './src/file3.js' }; 46 | expect(moduleNotFound([reduxError, reactError1, reactError2, reactError3])).toEqual([ 47 | 'These dependencies were not found:', 48 | '', 49 | '* redux', 50 | '* react in ./src/file1.js, ./src/file2.js and 1 other', 51 | '', 52 | 'To install them, you can run: npm install --save redux react' 53 | ]); 54 | }); 55 | 56 | it('Groups same module in module-not-found with 4 files', () => { 57 | const reduxError = { type: 'module-not-found', module: 'redux' }; 58 | const reactError1 = { type: 'module-not-found', module: 'react', file: './src/file1.js' }; 59 | const reactError2 = { type: 'module-not-found', module: 'react', file: './src/file2.js' }; 60 | const reactError3 = { type: 'module-not-found', module: 'react', file: './src/file3.js' }; 61 | const reactError4 = { type: 'module-not-found', module: 'react', file: './src/file4.js' }; 62 | expect(moduleNotFound([reduxError, reactError1, reactError2, reactError3, reactError4])).toEqual([ 63 | 'These dependencies were not found:', 64 | '', 65 | '* redux', 66 | '* react in ./src/file1.js, ./src/file2.js and 2 others', 67 | '', 68 | 'To install them, you can run: npm install --save redux react' 69 | ]); 70 | }); 71 | 72 | it('Does not format other errors', () => { 73 | const otherError = { type: 'other-error', module: 'foo' }; 74 | expect(moduleNotFound([otherError])).toEqual([]); 75 | }); 76 | -------------------------------------------------------------------------------- /test/unit/plugin/friendlyErrors.spec.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const Stats = require('webpack/lib/Stats') 3 | const Module = require('webpack/lib/Module'); 4 | EventEmitter.prototype.plugin = EventEmitter.prototype.on; 5 | 6 | const output = require("../../../src/output"); 7 | const FriendlyErrorsPlugin = require("../../../index"); 8 | 9 | var notifierPlugin; 10 | var mockCompiler; 11 | 12 | beforeEach(() => { 13 | notifierPlugin = new FriendlyErrorsPlugin(); 14 | mockCompiler = new EventEmitter(); 15 | notifierPlugin.apply(mockCompiler); 16 | }); 17 | 18 | it('friendlyErrors : capture invalid message', () => { 19 | 20 | const logs = output.captureLogs(() => { 21 | mockCompiler.emit('invalid'); 22 | }); 23 | 24 | expect(logs).toEqual([ 25 | 'WAIT Compiling...', 26 | '' 27 | ]); 28 | }); 29 | 30 | it('friendlyErrors : capture compilation without errors', () => { 31 | 32 | const stats = successfulCompilationStats(); 33 | const logs = output.captureLogs(() => { 34 | mockCompiler.emit('done', stats); 35 | }); 36 | 37 | expect(logs).toEqual([ 38 | 'DONE Compiled successfully in 100ms', 39 | '' 40 | ]); 41 | }); 42 | 43 | it('friendlyErrors : default clearConsole option', () => { 44 | const plugin = new FriendlyErrorsPlugin(); 45 | expect(plugin.shouldClearConsole).toBeTruthy() 46 | }); 47 | 48 | it('friendlyErrors : clearConsole option', () => { 49 | const plugin = new FriendlyErrorsPlugin({ clearConsole: false }); 50 | expect(plugin.shouldClearConsole).toBeFalsy() 51 | }); 52 | 53 | function successfulCompilationStats(opts) { 54 | const options = Object.assign({ startTime: 0, endTime: 100 }, opts); 55 | 56 | const compilation = { 57 | errors: [], 58 | warnings: [], 59 | children: [] 60 | }; 61 | const stats = new Stats(compilation); 62 | stats.startTime = options.startTime; 63 | stats.endTime = options.endTime; 64 | return stats; 65 | } 66 | -------------------------------------------------------------------------------- /test/unit/transformers/babelSyntax.spec.js: -------------------------------------------------------------------------------- 1 | const babelSyntax = require('../../../src/transformers/babelSyntax'); 2 | 3 | it('Sets severity to 1000', () => { 4 | const error = { name: 'ModuleBuildError', message: 'SyntaxError' }; 5 | expect(babelSyntax(error).severity).toEqual(1000); 6 | }); 7 | 8 | it('Does not set type (it should be handle by the default formatter)', () => { 9 | const error = { name: 'ModuleBuildError', message: 'SyntaxError' }; 10 | expect(babelSyntax(error).type).toEqual(undefined); 11 | }); 12 | 13 | it('Ignores other errors', () => { 14 | const error = { name: 'OtherError' }; 15 | expect(babelSyntax(error)).toEqual(error); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/transformers/moduleNotFound.spec.js: -------------------------------------------------------------------------------- 1 | const moduleNotFound = require('../../../src/transformers/moduleNotFound'); 2 | 3 | const error = { 4 | name: 'ModuleNotFoundError', 5 | message: 'Module not found : redux', 6 | webpackError: { 7 | dependencies: [{ request: 'redux' } ], 8 | }, 9 | }; 10 | 11 | it('Sets severity to 900', () => { 12 | expect(moduleNotFound(error).severity).toEqual(900); 13 | }); 14 | 15 | it('Sets module name', () => { 16 | expect(moduleNotFound(error).module).toEqual('redux'); 17 | }); 18 | 19 | it('Sets the appropiate message', () => { 20 | const message = 'Module not found redux'; 21 | expect(moduleNotFound(error).message).toEqual(message); 22 | }); 23 | 24 | it('Sets the appropiate type', () => { 25 | expect(moduleNotFound({ 26 | name: 'ModuleNotFoundError', 27 | message: 'Module not found', 28 | webpackError: error.webpackError, 29 | }).type).toEqual('module-not-found'); 30 | }); 31 | 32 | it('Ignores other errors', () => { 33 | const error = { name: 'OtherError' }; 34 | expect(moduleNotFound(error)).toEqual(error); 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/utils/utils.spec.js: -------------------------------------------------------------------------------- 1 | const utils = require('../../../src/utils'); 2 | 3 | const concat = utils.concat; 4 | const uniqueBy = utils.uniqueBy; 5 | 6 | it('concat should concat removing undefined and null values', () => { 7 | const result = concat(1, undefined, '', null); 8 | expect(result).toEqual( 9 | [1, ''] 10 | ); 11 | }); 12 | 13 | it('concat should handle arrays', () => { 14 | const result = concat(1, [2, 3], null); 15 | expect(result).toEqual( 16 | [1, 2, 3] 17 | ); 18 | }); 19 | 20 | it('uniqueBy should dedupe based on criterion returned from iteratee function', () => { 21 | const result = uniqueBy([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 3 }], function(val) { 22 | return val.id; 23 | }); 24 | expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); 25 | }); 26 | --------------------------------------------------------------------------------