"use strict" var assign = require("object-assign") var loaderUtils = require("loader-utils") var objectHash = require("object-hash") var pkg = require("./package.json") var createCache = require("loader-fs-cache") var cache = createCache("eslint-loader") var engines = {} /** * Class representing an ESLintError. * @extends Error */ class ESLintError extends Error { /** * Create an ESLintError. * @param {string} messages - Formatted eslint errors. */ constructor(messages) { super() this.name = "ESLintError" this.message = messages this.stack = "" } /** * Returns a stringified representation of our error. This method is called * when an error is consumed by console methods * ex: console.error(new ESLintError(formattedMessage)) * @return {string} error - A stringified representation of the error. */ inspect() { return this.message } } /** * printLinterOutput * * @param {Object} eslint.executeOnText return value * @param {Object} config eslint configuration * @param {Object} webpack webpack instance * @return {void} */ function printLinterOutput(res, config, webpack) { // skip ignored file warning if ( !(res.warningCount === 1 && res.results[0].messages[0] && res.results[0].messages[0].message && res.results[0].messages[0].message.indexOf("ignore") > 1) ) { // quiet filter done now // eslint allow rules to be specified in the input between comments // so we can found warnings defined in the input itself if (res.warningCount && config.quiet) { res.warningCount = 0 res.results[0].warningCount = 0 res.results[0].messages = res.results[0].messages.filter(function( message ) { return message.severity !== 1 }) } // if enabled, use eslint auto-fixing where possible if (config.fix && res.results[0].output) { var eslint = require(config.eslintPath) eslint.CLIEngine.outputFixes(res) } if (res.errorCount || res.warningCount) { // add filename for each results so formatter can have relevant filename res.results.forEach(function(r) { r.filePath = webpack.resourcePath }) var messages = config.formatter(res.results) if (config.outputReport && config.outputReport.filePath) { var reportOutput // if a different formatter is passed in as an option use that if (config.outputReport.formatter) { reportOutput = config.outputReport.formatter(res.results) } else { reportOutput = messages } var filePath = loaderUtils.interpolateName(webpack, config.outputReport.filePath, { content: res.results.map(function(r) { return r.source }).join("\n"), } ) webpack.emitFile(filePath, reportOutput) } // default behavior: emit error only if we have errors var emitter = res.errorCount ? webpack.emitError : webpack.emitWarning // force emitError or emitWarning if user want this if (config.emitError) { emitter = webpack.emitError } else if (config.emitWarning) { emitter = webpack.emitWarning } if (emitter) { if (config.failOnError && res.errorCount) { throw new ESLintError( "Module failed because of a eslint error.\n" + messages ) } else if (config.failOnWarning && res.warningCount) { throw new ESLintError( "Module failed because of a eslint warning.\n" + messages ) } emitter(webpack.version === 2 ? new ESLintError(messages) : messages) } else { throw new Error( "Your module system doesn't support emitWarning. " + "Update available? \n" + messages ) } } } } /** * webpack loader * * @param {String|Buffer} input JavaScript string * @param {Object} map input source map * @return {void} */ module.exports = function(input, map) { var webpack = this var userOptions = assign( // user defaults this.options.eslint || {}, // loader query string loaderUtils.getOptions(this) ) var config = assign( // loader defaults { formatter: require("eslint/lib/formatters/stylish"), cacheIdentifier: JSON.stringify({ "eslint-loader": pkg.version, eslint: require(userOptions.eslintPath || "eslint").version, }), eslintPath: "eslint", }, userOptions ) var cacheDirectory = config.cache var cacheIdentifier = config.cacheIdentifier delete config.cacheDirectory delete config.cacheIdentifier // Create the engine only once per config var configHash = objectHash(config) if (!engines[configHash]) { var eslint = require(config.eslintPath) engines[configHash] = new eslint.CLIEngine(config) } this.cacheable() var resourcePath = webpack.resourcePath var cwd = process.cwd() // remove cwd from resource path in case webpack has been started from project // root, to allow having relative paths in .eslintignore if (resourcePath.indexOf(cwd) === 0) { resourcePath = resourcePath.substr(cwd.length + 1) } var engine = engines[configHash] // return early if cached if (config.cache) { var callback = this.async() return cache( { directory: cacheDirectory, identifier: cacheIdentifier, options: config, source: input, transform: function() { return lint(engine, input, resourcePath) }, }, function(err, res) { if (err) { return callback(err) } printLinterOutput(res || {}, config, webpack) return callback(null, input, map) } ) } printLinterOutput(lint(engine, input, resourcePath), config, this) this.callback(null, input, map) } function lint(engine, input, resourcePath) { return engine.executeOnText(input, resourcePath, true) }