-
-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add lintDirtyModulesOnly option (#53)
* resolves #30 - add lint dirty files only flag * Separate logic to extract changed style files. - Add tests for lintDirtyFilesOnly flag * Added a test case for non style file change * Add unit test for getChangedFiles * Remove unused dependencies * Quiet Stylelint during tests * Refactore lint dirty modules into plugin in separate file * Add testdouble * Fix lint warnings
- Loading branch information
1 parent
96781a3
commit 7e60e9b
Showing
8 changed files
with
291 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
'use strict'; | ||
var minimatch = require('minimatch'); | ||
var reduce = require('lodash.reduce'); | ||
var assign = require('object-assign'); | ||
var runCompilation = require('./run-compilation'); | ||
|
||
/** | ||
* Binds callback with provided options and stores initial values. | ||
* | ||
* @param compiler - webpack compiler object | ||
* @param options - stylelint nodejs options | ||
* @param callback <function(options, compilitaion)> - callback to call on emit | ||
*/ | ||
function LintDirtyModulesPlugin (compiler, options) { | ||
this.startTime = Date.now(); | ||
this.prevTimestamps = {}; | ||
this.isFirstRun = true; | ||
this.compiler = compiler; | ||
this.options = options; | ||
compiler.plugin('emit', | ||
this.lint.bind(this) // bind(this) is here to prevent context overriding by webpack | ||
); | ||
} | ||
|
||
/** | ||
* Lints changed files provided by compilation object. | ||
* Fully executed only after initial run. | ||
* | ||
* @param options - stylelint options | ||
* @param compilation - webpack compilation object | ||
* @param callback - to be called when execution is done | ||
* @returns {*} | ||
*/ | ||
LintDirtyModulesPlugin.prototype.lint = function (compilation, callback) { | ||
if (this.isFirstRun) { | ||
this.isFirstRun = false; | ||
this.prevTimestamps = compilation.fileTimestamps; | ||
return callback(); | ||
} | ||
var dirtyOptions = assign({}, this.options); | ||
var glob = dirtyOptions.files.join('|'); | ||
var changedFiles = this.getChangedFiles(compilation.fileTimestamps, glob); | ||
this.prevTimestamps = compilation.fileTimestamps; | ||
if (changedFiles.length) { | ||
dirtyOptions.files = changedFiles; | ||
runCompilation.call(this, dirtyOptions, this.compiler, callback); | ||
} else { | ||
callback(); | ||
} | ||
}; | ||
|
||
/** | ||
* Returns an array of changed files comparing current timestamps | ||
* against cached timestamps from previous run. | ||
* | ||
* @param plugin - stylelint-webpack-plugin this scopr | ||
* @param fileTimestamps - an object with keys as filenames and values as their timestamps. | ||
* e.g. {'/filename.scss': 12444222000} | ||
* @param glob - glob pattern to match files | ||
*/ | ||
LintDirtyModulesPlugin.prototype.getChangedFiles = function (fileTimestamps, glob) { | ||
return reduce(fileTimestamps, function (changedStyleFiles, timestamp, filename) { | ||
// Check if file has been changed first ... | ||
if ((this.prevTimestamps[filename] || this.startTime) < (fileTimestamps[filename] || Infinity) && | ||
// ... then validate by the glob pattern. | ||
minimatch(filename, glob, {matchBase: true}) | ||
) { | ||
changedStyleFiles = changedStyleFiles.concat(filename); | ||
} | ||
return changedStyleFiles; | ||
}.bind(this), []); | ||
}; | ||
|
||
module.exports = LintDirtyModulesPlugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
'use strict'; | ||
|
||
var td = require('testdouble'); | ||
var formatter = require('stylelint').formatters.string; | ||
|
||
var runCompilation = td.replace('../../lib/run-compilation'); | ||
|
||
var LintDirtyModulesPlugin = require('../../lib/lint-dirty-modules-plugin'); | ||
|
||
var configFilePath = getPath('./.stylelintrc'); | ||
var glob = '/**/*.s?(c|a)ss'; | ||
|
||
describe('lint-dirty-modules-plugin', function () { | ||
context('lintDirtyModulesOnly flag is enabled', function () { | ||
var LintDirtyModulesPluginCloned; | ||
var compilerMock; | ||
var optionsMock; | ||
|
||
beforeEach(function () { | ||
LintDirtyModulesPluginCloned = function () { | ||
LintDirtyModulesPlugin.apply(this, arguments); | ||
}; | ||
LintDirtyModulesPluginCloned.prototype = Object.create(LintDirtyModulesPlugin.prototype); | ||
LintDirtyModulesPluginCloned.prototype.constructor = LintDirtyModulesPlugin; | ||
|
||
compilerMock = { | ||
callback: null, | ||
plugin: function plugin (event, callback) { | ||
this.callback = callback; | ||
} | ||
}; | ||
|
||
optionsMock = { | ||
configFile: configFilePath, | ||
lintDirtyModulesOnly: true, | ||
fomatter: formatter, | ||
files: [glob] | ||
}; | ||
}); | ||
|
||
it('lint is called on \'emit\'', function () { | ||
var lintStub = td.function(); | ||
var doneStub = td.function(); | ||
LintDirtyModulesPluginCloned.prototype.lint = lintStub; | ||
var plugin = new LintDirtyModulesPluginCloned(compilerMock, optionsMock); | ||
|
||
var compilationMock = { | ||
fileTimestamps: { | ||
'/udpated.scss': 5 | ||
} | ||
}; | ||
compilerMock.callback(compilationMock, doneStub); | ||
|
||
expect(plugin.isFirstRun).to.eql(true); | ||
td.verify(lintStub(compilationMock, doneStub)); | ||
}); | ||
|
||
context('LintDirtyModulesPlugin.prototype.lint()', function () { | ||
var getChangedFilesStub; | ||
var doneStub; | ||
var compilationMock; | ||
var fileTimestamps = { | ||
'/test/changed.scss': 5, | ||
'/test/newly-created.scss': 5 | ||
}; | ||
var pluginMock; | ||
beforeEach(function () { | ||
getChangedFilesStub = td.function(); | ||
doneStub = td.function(); | ||
compilationMock = { | ||
fileTimestamps: {} | ||
}; | ||
td.when(getChangedFilesStub({}, glob)).thenReturn([]); | ||
td.when(getChangedFilesStub(fileTimestamps, glob)).thenReturn(Object.keys(fileTimestamps)); | ||
pluginMock = { | ||
getChangedFiles: getChangedFilesStub, | ||
compiler: compilerMock, | ||
options: optionsMock, | ||
isFirstRun: true | ||
}; | ||
}); | ||
|
||
it('skips compilation on first run', function () { | ||
expect(pluginMock.isFirstRun).to.eql(true); | ||
LintDirtyModulesPluginCloned.prototype.lint.call(pluginMock, compilationMock, doneStub); | ||
td.verify(doneStub()); | ||
expect(pluginMock.isFirstRun).to.eql(false); | ||
td.verify(getChangedFilesStub, {times: 0, ignoreExtraArgs: true}); | ||
td.verify(runCompilation, {times: 0, ignoreExtraArgs: true}); | ||
}); | ||
|
||
it('runCompilation is not called if files are not changed', function () { | ||
pluginMock.isFirstRun = false; | ||
LintDirtyModulesPluginCloned.prototype.lint.call(pluginMock, compilationMock, doneStub); | ||
td.verify(doneStub()); | ||
td.verify(runCompilation, {times: 0, ignoreExtraArgs: true}); | ||
}); | ||
|
||
it('runCompilation is called if styles are changed', function () { | ||
pluginMock.isFirstRun = false; | ||
compilationMock = { | ||
fileTimestamps: fileTimestamps | ||
}; | ||
LintDirtyModulesPluginCloned.prototype.lint.call(pluginMock, compilationMock, doneStub); | ||
optionsMock.files = Object.keys(fileTimestamps); | ||
td.verify(runCompilation(optionsMock, compilerMock, doneStub)); | ||
}); | ||
}); | ||
|
||
context('LintDirtyModulesPlugin.prototype.getChangedFiles()', function () { | ||
var pluginMock; | ||
before(function () { | ||
pluginMock = { | ||
compiler: compilerMock, | ||
options: optionsMock, | ||
isFirstRun: true, | ||
startTime: 10, | ||
prevTimestamps: { | ||
'/test/changed.scss': 5, | ||
'/test/removed.scss': 5, | ||
'/test/changed.js': 5 | ||
} | ||
}; | ||
}); | ||
it('returns changed style files', function () { | ||
var fileTimestamps = { | ||
'/test/changed.scss': 20, | ||
'/test/changed.js': 20, | ||
'/test/newly-created.scss': 15 | ||
}; | ||
|
||
expect( | ||
LintDirtyModulesPluginCloned.prototype.getChangedFiles.call(pluginMock, fileTimestamps, glob)).to.eql([ | ||
'/test/changed.scss', | ||
'/test/newly-created.scss' | ||
] | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.