diff --git a/index.js b/index.js index e47758a..ace93da 100644 --- a/index.js +++ b/index.js @@ -1,93 +1,40 @@ var eslint = require("eslint") var assign = require("object-assign") var loaderUtils = require("loader-utils") -var crypto = require("crypto") -var fs = require("fs") -var findCacheDir = require("find-cache-dir") var objectHash = require("object-hash") -var os = require("os") +var pkg = require("./package.json") +var createCache = require("loader-fs-cache") +var cache = createCache("eslint-loader") var engines = {} -var rules = {} -var cache = null -var cachePath = null /** - * linter + * printLinterOutput * - * @param {String|Buffer} input JavaScript string + * @param {Object} eslint.executeOnText return value * @param {Object} config eslint configuration * @param {Object} webpack webpack instance * @return {void} */ -function lint(input, config, webpack) { - 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) - } - - // get engine - var configHash = objectHash(config) - var engine = engines[configHash] - var rulesHash = rules[configHash] - - var res - // If cache is enable and the data are the same as in the cache, just - // use them - if (config.cache) { - // just get rules hash once per engine for performance reasons - if (!rulesHash) { - rulesHash = objectHash(engine.getConfigForFile(resourcePath)) - rules[configHash] = rulesHash - } - var inputMD5 = crypto.createHash("md5").update(input).digest("hex") - if ( - cache[resourcePath] && - cache[resourcePath].hash === inputMD5 && - cache[resourcePath].rules === rulesHash - ) { - res = cache[resourcePath].res - } - } - - // Re-lint the text if the cache off or miss - if (!res) { - res = engine.executeOnText(input, resourcePath, true) - - // Save new results in the cache - if (config.cache) { - cache[resourcePath] = { - hash: inputMD5, - rules: rulesHash, - res: res, - } - fs.writeFileSync(cachePath, JSON.stringify(cache)) - } - } - - // executeOnText ensure we will have res.results[0] only - +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 - )) { + 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 - }) + res.results[0].messages = res.results[0].messages.filter(function( + message + ) { + return message.severity !== 1 + }) } // if enabled, use eslint auto-fixing where possible @@ -128,19 +75,21 @@ function lint(input, config, webpack) { if (emitter) { emitter(messages) if (config.failOnError && res.errorCount) { - throw new Error("Module failed because of a eslint error.\n" - + messages) + throw new Error( + "Module failed because of a eslint error.\n" + messages + ) } else if (config.failOnWarning && res.warningCount) { - throw new Error("Module failed because of a eslint warning.\n" - + messages) + throw new Error( + "Module failed because of a eslint warning.\n" + messages + ) } } else { throw new Error( "Your module system doesn't support emitWarning. " + - "Update available? \n" + - messages + "Update available? \n" + + messages ) } } @@ -155,17 +104,27 @@ function lint(input, config, webpack) { * @return {void} */ module.exports = function(input, map) { + var webpack = this var config = assign( // loader defaults { formatter: require("eslint/lib/formatters/stylish"), + cacheIdentifier: JSON.stringify({ + "eslint-loader": pkg.version, + eslint: eslint.version, + }), }, // user defaults this.options.eslint || {}, // loader query string loaderUtils.getOptions(this) ) - this.cacheable() + + 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) @@ -173,27 +132,44 @@ module.exports = function(input, map) { engines[configHash] = new eslint.CLIEngine(config) } - // Read the cached information only once and if enable - if (cache === null) { - if (config.cache) { - var thunk = findCacheDir({ - name: "eslint-loader", - thunk: true, - create: true, - }) - cachePath = thunk("data.json") || os.tmpdir() + "/data.json" - try { - cache = require(cachePath) - } - catch (e) { - cache = {} - } - } - else { - cache = false - } + 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) } - lint(input, config, this) + 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) +} diff --git a/package.json b/package.json index dad56f6..12cab08 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ }, "dependencies": { "find-cache-dir": "^0.1.1", + "loader-fs-cache": "^1.0.0", "loader-utils": "^1.0.2", "object-assign": "^4.0.1", - "object-hash": "^1.1.4" + "object-hash": "^1.1.4", + "rimraf": "^2.6.1" }, "devDependencies": { "ava": "^0.17.0", diff --git a/test/cache.js b/test/cache.js index 241f011..271d9af 100644 --- a/test/cache.js +++ b/test/cache.js @@ -1,48 +1,287 @@ var test = require("ava") -var webpack = require("webpack") -var conf = require("./utils/conf") var fs = require("fs") +var path = require("path") +var assign = require("object-assign") +var rimraf = require("rimraf") +var webpack = require("webpack") -var cacheFilePath = "./node_modules/.cache/eslint-loader/data.json" - -test.cb("eslint-loader can cache results", function(t) { - t.plan(2) - webpack(conf( - { - entry: "./test/fixtures/cache.js", - }, - { - cache: true, - } - ), - function(err) { - if (err) { - throw err - } - - fs.readFile(cacheFilePath, "utf8", function(err, contents) { - if (err) { - t.fail("expected cache file to have been created") - } - else { - t.pass("cache file has been created") - - var contentsJson = JSON.parse(contents) - t.deepEqual( - Object.keys(contentsJson["test/fixtures/cache.js"]), - ["hash", "rules", "res"], - "cache values have been set for the linted file" - ) - } +var defaultCacheDir = path.join( + __dirname, + "../node_modules/.cache/eslint-loader" +) +var cacheDir = path.join(__dirname, "output/cache/cachefiles") +var outputDir = path.join(__dirname, "output/cache") +var eslintLoader = path.join(__dirname, "../index") + +var globalConfig = { + entry: path.join(__dirname, "fixtures/cache.js"), + module: { + loaders: [ + { + test: /\.js$/, + loader: eslintLoader, + exclude: /node_modules/, + }, + ], + }, +} + +// Create a separate directory for each test so that the tests +// can run in parallel + +test.cb.beforeEach((t) => { + createTestDirectory(outputDir, t.title, (err, directory) => { + if (err) return t.end(err) + t.context.directory = directory + t.end() + }) +}) +test.cb.beforeEach((t) => { + createTestDirectory(cacheDir, t.title, (err, directory) => { + if (err) return t.end(err) + t.context.cache = directory + t.end() + }) +}) +test.cb.beforeEach((t) => rimraf(defaultCacheDir, t.end)) +test.cb.afterEach((t) => rimraf(t.context.directory, t.end)) +test.cb.afterEach((t) => rimraf(t.context.cache, t.end)) + +test.cb("should output files to cache directory", (t) => { + var config = assign({}, globalConfig, { + output: { + path: t.context.directory, + }, + module: { + loaders: [ + { + test: /\.js$/, + loader: eslintLoader, + exclude: /node_modules/, + query: { + cache: t.context.cache, + }, + }, + ], + }, + }) + + webpack(config, (err) => { + t.is(err, null) + + fs.readdir(t.context.cache, (err, files) => { + // console.log("CACHE SETTING:", t.context.cache) + t.is(err, null) + t.true(files.length > 0) + t.end() + }) + }) +}) + +test.cb.serial("should output json.gz files to standard cache dir by default", +(t) => { + var config = assign({}, globalConfig, { + output: { + path: t.context.directory, + }, + module: { + loaders: [ + { + test: /\.jsx?/, + loader: eslintLoader, + exclude: /node_modules/, + query: { + cache: true, + }, + }, + ], + }, + }) + + webpack(config, (err) => { + t.is(err, null) + + fs.readdir(defaultCacheDir, (err, files) => { + // console.log("CACHE SETTING:", t.context.cache) + files = files.filter((file) => /\b[0-9a-f]{5,40}\.json\.gz\b/.test(file)) + t.is(err, null) + t.true(files.length > 0) + t.end() + }) + }) +}) + +test.cb.serial( +"should output files to standard cache dir if set to true in query", +(t) => { + var config = assign({}, globalConfig, { + output: { + path: t.context.directory, + }, + module: { + loaders: [ + { + test: /\.jsx?/, + loader: `${eslintLoader}?cache=true`, + exclude: /node_modules/, + }, + ], + }, + }) + + webpack(config, (err) => { + t.is(err, null) + fs.readdir(defaultCacheDir, (err, files) => { + // console.log("CACHE SETTING:", t.context.cache) + files = files.filter((file) => /\b[0-9a-f]{5,40}\.json\.gz\b/.test(file)) + + t.is(err, null) + t.true(files.length > 0) t.end() + }) + }) +}) +test.cb.serial("should read from cache directory if cached file exists", +(t) => { + var config = assign({}, globalConfig, { + output: { + path: t.context.directory, + }, + module: { + loaders: [ + { + test: /\.jsx?/, + loader: eslintLoader, + exclude: /node_modules/, + query: { + cache: t.context.cache, + }, + }, + ], + }, + }) + + // @TODO Find a way to know if the file as correctly read without relying on + // Istanbul for coverage. + webpack(config, (err) => { + t.is(err, null) + + webpack(config, (err) => { + t.is(err, null) + fs.readdir(t.context.cache, (err, files) => { + t.is(err, null) + t.true(files.length > 0) + t.end() + }) }) + }) +}) + +test.cb.serial("should have one file per module", (t) => { + var config = assign({}, globalConfig, { + output: { + path: t.context.directory, + }, + module: { + loaders: [ + { + test: /\.jsx?/, + loader: eslintLoader, + exclude: /node_modules/, + query: { + cache: t.context.cache, + }, + }, + ], + }, }) + + webpack(config, (err) => { + t.is(err, null) + + fs.readdir(t.context.cache, (err, files) => { + // console.log("CACHE SETTING:", t.context.cache) + t.is(err, null) + t.true(files.length === 3) + t.end() + }) + }) +}) + +test.cb.serial("should generate a new file if the identifier changes", (t) => { + var configs = [ + assign({}, globalConfig, { + output: { + path: t.context.directory, + }, + module: { + loaders: [ + { + test: /\.jsx?/, + loader: eslintLoader, + exclude: /node_modules/, + query: { + cache: t.context.cache, + cacheIdentifier: "a", + }, + }, + ], + }, + }), + assign({}, globalConfig, { + output: { + path: t.context.directory, + }, + module: { + loaders: [ + { + test: /\.jsx?/, + loader: eslintLoader, + exclude: /node_modules/, + query: { + cache: t.context.cache, + cacheIdentifier: "b", + }, + }, + ], + }, + }), + ] + let counter = configs.length + + configs.forEach((config) => { + webpack(config, (err) => { + t.is(err, null) + counter -= 1 + + if (!counter) { + fs.readdir(t.context.cache, (err, files) => { + if (err) { + // console.log(err) + } + t.is(err, null) + t.true(files.length === 6) + t.end() + }) + } + }) + }) + }) -// delete the cache file once tests have completed -test.after.always("teardown", function() { - fs.unlinkSync(cacheFilePath) -}) \ No newline at end of file +var mkdirp = require("mkdirp") +function createTestDirectory(baseDirectory, testTitle, cb) { + const directory = path.join(baseDirectory, escapeDirectory(testTitle)) + + rimraf(directory, (err) => { + if (err) return cb(err) + mkdirp(directory, (mkdirErr) => cb(mkdirErr, directory)) + }) +} + +function escapeDirectory(directory) { + return directory.replace(/[\/?<>\\:*|"\s]/g, "_") +} \ No newline at end of file diff --git a/test/fixtures/cache.js b/test/fixtures/cache.js index 48bffcc..ee47263 100644 --- a/test/fixtures/cache.js +++ b/test/fixtures/cache.js @@ -1,5 +1,7 @@ "use strict" +require("./require") + function cacheIt() { return "cache" } diff --git a/test/fixtures/fixable.js b/test/fixtures/fixable.js index ec2abfb..7845f5e 100644 --- a/test/fixtures/fixable.js +++ b/test/fixtures/fixable.js @@ -1,5 +1,5 @@ function foo() { -return true; + return true } - foo(); +foo() diff --git a/test/fixtures/good-semi.js b/test/fixtures/good-semi.js index 12612ab..49a7bb9 100644 --- a/test/fixtures/good-semi.js +++ b/test/fixtures/good-semi.js @@ -1,7 +1,7 @@ -"use strict"; +"use strict" function test() { - return "value"; + return "value" } -test(); +test() diff --git a/test/fixtures/require.js b/test/fixtures/require.js new file mode 100644 index 0000000..7785c53 --- /dev/null +++ b/test/fixtures/require.js @@ -0,0 +1,3 @@ +"use strict" + +require("./good") \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e497785..dac4dcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2509,11 +2509,17 @@ load-json-file@^1.0.0, load-json-file@^1.1.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" +loader-fs-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.0.tgz#bbfc18ff4d986dcd39b41d0570fd752f08366340" + dependencies: + find-cache-dir "^0.1.1" + loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" -loader-utils@^0.2.16, loader-utils@^0.2.7: +loader-utils@^0.2.16: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" dependencies: @@ -2522,6 +2528,14 @@ loader-utils@^0.2.16, loader-utils@^0.2.7: json5 "^0.5.0" object-assign "^4.0.1" +loader-utils@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.0.2.tgz#a9f923c865a974623391a8602d031137fad74830" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + lodash.debounce@^4.0.3: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -3404,6 +3418,12 @@ rimraf@2, rimraf@^2.2.8, rimraf@~2.5.1, rimraf@~2.5.4: dependencies: glob "^7.0.5" +rimraf@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + ripemd160@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e"