Eslint support

Eslint configuration and ignore file are now generated, with
simple writeback support.
This commit is contained in:
Michael Krotscheck
2016-03-29 08:33:23 -07:00
parent a06cb9f35c
commit efa9fb66b3
9 changed files with 358 additions and 55 deletions

View File

@@ -1,3 +1,2 @@
coverage
node_modules
node_modules

View File

@@ -1,5 +1,4 @@
extends: openstack
env:
node: true
jasmine: true

View File

@@ -1,4 +1,4 @@
(function () {
(function() {
'use strict';
var yeoman = require('yeoman-generator');
@@ -7,17 +7,18 @@
var gerrit = require('./lib/component/gerrit');
var editorconfig = require('./lib/component/editorconfig');
var license = require('./lib/component/license');
var eslint = require('./lib/component/eslint');
module.exports = yeoman.generators.Base.extend({
constructor: function () {
constructor: function() {
yeoman.generators.Base.apply(this, arguments);
// Add support for a `--non-interactive` flag
this.option('non-interactive');
},
initializing: function () {
initializing: function() {
// Set our own defaults.
this.config.defaults({
projectName: this.appname
@@ -27,41 +28,51 @@
gerrit.init(this); // Gerrit
editorconfig.init(this); // Editorconfig
license.init(this); // Licensing
eslint.init(this); // Linting
},
prompting: function () {
prompting: function() {
if (!this.options['non-interactive']) {
// Prompt components.
gerrit.prompt(this); // Gerrit
editorconfig.prompt(this); // Editorconfig
license.prompt(this); // Licensing
eslint.prompt(this); // Linting
}
},
configuring: function () {
configuring: function() {
// Configure components.
gerrit.configure(this); // Gerrit
editorconfig.configure(this); // Editorconfig
license.configure(this); // Licensing
eslint.configure(this); // Linting
},
writing: function () {
writing: function() {
var self = this;
var config = self.config.getAll();
var included = projectBuilder.getIncludedFiles();
var excluded = projectBuilder.getExcludedFiles();
// Write out all files included in the project builder.
included.forEach(function (fileRef) {
self.fs.copyTpl(
self.templatePath(fileRef.from),
self.destinationPath(fileRef.to),
config
);
included.forEach(function(fileRef) {
if (fileRef.hasOwnProperty('content')) {
var content = typeof fileRef.content === 'function'
? "" + fileRef.content()
: "" + fileRef.content;
self.fs.write(fileRef.to, content);
} else {
self.fs.copyTpl(
self.templatePath(fileRef.from),
self.destinationPath(fileRef.to),
config
);
}
});
// Delete all files explicitly excluded in the project builder.
excluded.forEach(function (path) {
excluded.forEach(function(path) {
self.fs.delete(self.destinationPath(path));
});
}

View File

@@ -0,0 +1,84 @@
(function() {
'use strict';
var projectBuilder = require('../project_builder');
var yaml = require('js-yaml');
var excludedPaths = [];
var ignoreFile = '.eslintignore';
var rcFile = '.eslintrc';
var eslintrc = {extends: 'openstack'};
/**
* No-op placeholder method, for handlers we don't need.
*
* @returns {void}
*/
function noop () {
// Do nothing.
}
/**
* Read the existing .eslintrc and .eslintignore files, and populate our initial configuration
* with them.
*
* @param {*} generator The currently active yeoman generator.
* @returns {void}
*/
function initializeEslint (generator) {
var fs = generator.fs;
// Read .eslintignore.
if (fs.exists(ignoreFile)) {
excludedPaths = fs.read(ignoreFile)
.split('\n')
.filter(function(item) {
// Remove empty lines.
return item.length > 0;
});
}
// Read .eslintrc
if (fs.exists(rcFile)) {
eslintrc = yaml.safeLoad(fs.read(rcFile));
}
}
/**
* Configure the project by adding required files.
*
* @returns {void}
*/
function configureEslint () {
if (buildEslintIgnore().length === 0) {
projectBuilder.removeFile('.eslintignore');
} else {
projectBuilder.writeFile('.eslintignore', buildEslintIgnore);
}
projectBuilder.writeFile('.eslintrc', buildEslintRc);
}
/**
* Generate the content of our .eslintignore file from the configured list of excluded paths.
*
* @returns {string} The content of the .eslintignore file.
*/
function buildEslintIgnore () {
return excludedPaths.sort().join('\n');
}
/**
* Generate the content of our .eslintrc file from the current configuration.
*
* @returns {string} The content of the .eslintrc file.
*/
function buildEslintRc () {
return yaml.safeDump(eslintrc);
}
module.exports = {
init: initializeEslint,
prompt: noop,
configure: configureEslint
};
})();

View File

@@ -25,6 +25,17 @@
includedFiles.push({from: sourcePath, to: destinationPath || sourcePath});
}
/**
* Write a file to the project.
*
* @param {String} destinationPath The destination for the file.
* @param {String|Function} content A string of content, or method that returns one.
* @returns {void}
*/
function writeFile (destinationPath, content) {
includedFiles.push({to: destinationPath, content: content});
}
/**
* Get a list of all files that are to be included.
*
@@ -55,6 +66,7 @@
module.exports = {
addFile: addFile,
writeFile: writeFile,
removeFile: removeFile,
getIncludedFiles: getIncludedFiles,
getExcludedFiles: getExcludedFiles,

View File

@@ -23,6 +23,7 @@
"yo": "^1.7.0"
},
"dependencies": {
"js-yaml": "^3.5.5",
"yeoman-generator": "^0.21.1"
},
"devDependencies": {

View File

@@ -1,4 +1,4 @@
(function () {
(function() {
'use strict';
var path = require('path');
var assert = require('yeoman-assert');
@@ -8,16 +8,16 @@
var modules = ['gerrit', 'license', 'editorconfig'];
var projectBuilder = require('../../generators/app/lib/project_builder');
describe('generator-openstack:app', function () {
describe('generator-openstack:app', function() {
beforeEach(function () {
beforeEach(function() {
projectBuilder.clear();
});
it('should call all module lifecycle prompts',
function (done) {
function(done) {
var spies = [];
modules.forEach(function (name) {
modules.forEach(function(name) {
var module = require('../../generators/app/lib/component/' + name);
spies.push(spyOn(module, 'init').and.callThrough());
spies.push(spyOn(module, 'prompt').and.callThrough());
@@ -25,8 +25,8 @@
});
helpers.run(generator)
.on('end', function () {
spies.forEach(function (spy) {
.on('end', function() {
spies.forEach(function(spy) {
expect(spy.calls.any()).toBeTruthy();
});
@@ -35,10 +35,10 @@
});
it('should call no module prompts with the --non-interactive flag',
function (done) {
function(done) {
var promptSpies = [];
var regularSpies = [];
modules.forEach(function (name) {
modules.forEach(function(name) {
var module = require('../../generators/app/lib/component/' + name);
regularSpies.push(spyOn(module, 'init').and.callThrough());
promptSpies.push(spyOn(module, 'prompt').and.callThrough());
@@ -47,11 +47,11 @@
helpers.run(generator)
.withOptions({'non-interactive': true})
.on('end', function () {
promptSpies.forEach(function (spy) {
.on('end', function() {
promptSpies.forEach(function(spy) {
expect(spy.calls.any()).toBeFalsy();
});
regularSpies.forEach(function (spy) {
regularSpies.forEach(function(spy) {
expect(spy.calls.any()).toBeTruthy();
});
@@ -59,24 +59,48 @@
});
});
it('should create all files created in the project builder',
function (done) {
helpers.run(generator)
.on('end', function () {
assert.file(['.gitreview']); // We'll just use a file we know about.
done();
});
});
describe('writing()', function() {
it('should create all files created in the project builder',
function(done) {
helpers.run(generator)
.on('end', function() {
assert.file(['.gitreview']); // We'll just use a file we know about.
done();
});
});
it('should delete all files flagged in the project builder',
function (done) {
projectBuilder.removeFile('test.json');
helpers.run(generator)
.on('end', function () {
assert.noFile(['test.json']);
done();
it('should write any files provided to the content builder',
function(done) {
projectBuilder.writeFile('test.json', function() {
return 'foo';
});
});
projectBuilder.writeFile('test_null.json', function() {
// do nothing.
});
projectBuilder.writeFile('test_empty.json', function() {
return '';
});
projectBuilder.writeFile('test_static.json', 'static_content');
projectBuilder.writeFile('test_undefined.json');
helpers.run(generator)
.on('end', function() {
assert.file(['test.json', 'test_static.json','test_empty.json', 'test_null.json',
'test_undefined.json']);
done();
});
});
it('should delete all files flagged in the project builder',
function(done) {
projectBuilder.removeFile('test.json');
helpers.run(generator)
.on('end', function() {
assert.noFile(['test.json']);
done();
});
});
});
});
})();

View File

@@ -0,0 +1,168 @@
(function() {
'use strict';
var libDir = '../../../../generators/app/lib';
var mockGenerator;
var mockEslintIgnore = ['node_modules', 'bower_components', 'dist'];
var eslint = require(libDir + '/component/eslint');
var projectBuilder = require(libDir + '/project_builder');
var mocks = require('../../../helpers/mocks');
var yaml = require('js-yaml');
describe('generator-openstack:lib/component/eslint', function() {
beforeEach(function() {
mockGenerator = mocks.buildGenerator();
mockGenerator.fs.write('.eslintignore', mockEslintIgnore.join('\n'));
projectBuilder.clear();
});
it('should define init, prompt, and configure',
function() {
expect(typeof eslint.init).toBe('function');
expect(typeof eslint.prompt).toBe('function');
expect(typeof eslint.configure).toBe('function');
});
describe('init()', function() {
it('should not interact with config',
function() {
var spy = spyOn(mockGenerator.config, 'defaults');
eslint.init(mockGenerator);
expect(spy.calls.any()).toBeFalsy();
});
});
describe('prompt()', function() {
it('should do nothing',
function() {
var spy = spyOn(mockGenerator, 'prompt');
eslint.prompt(mockGenerator);
expect(spy.calls.any()).toBeFalsy();
});
});
describe('configure()', function() {
it('should add .eslintrc and .eslintignore to the project files.',
function() {
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
expect(files.length).toBe(2);
expect(files[0].to).toBe('.eslintignore');
expect(files[1].to).toBe('.eslintrc');
});
});
describe('.eslintrc management', function() {
var mockEslintRc = {
extends: 'openstack',
plugins: ['angular']
};
it('should write a .eslintrc file as valid .yaml',
function() {
eslint.init(mockGenerator);
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
var eslintRcRef = files[1];
expect(eslintRcRef.to).toBe('.eslintrc');
expect(yaml.safeLoad(eslintRcRef.content()))
.toEqual({extends: 'openstack'});
});
it('should echo back existing .eslintrc',
function() {
var yamlContent = yaml.safeDump(mockEslintRc);
mockGenerator.fs.write('.eslintrc', yamlContent);
eslint.init(mockGenerator);
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
var eslintRcRef = files[1];
var eslintContent = yaml.safeLoad(eslintRcRef.content());
expect(mockEslintRc).toEqual(eslintContent);
});
it('should convert a json .eslintrc to yaml',
function() {
mockGenerator.fs.write('.eslintrc', JSON.stringify(mockEslintRc));
eslint.init(mockGenerator);
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
var eslintRcRef = files[1];
var eslintContent = yaml.safeLoad(eslintRcRef.content());
expect(mockEslintRc).toEqual(eslintContent);
});
});
describe('.eslintignore management', function() {
it('should echo back existing .eslintignore',
function() {
mockGenerator.fs.write('.eslintignore', mockEslintIgnore.join('\n'));
eslint.init(mockGenerator);
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
var ignoreRef = files[0];
var ignoreContent = ignoreRef.content().split('\n');
expect(ignoreContent.length).toBe(mockEslintIgnore.length);
ignoreContent.forEach(function(item) {
expect(mockEslintIgnore.indexOf(item)).not.toBe(-1);
});
});
it('should sort the ignored files.',
function() {
mockGenerator.fs.write('.eslintignore', mockEslintIgnore.join('\n'));
eslint.init(mockGenerator);
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
var ignoreRef = files[0];
var ignoreContent = ignoreRef.content().split('\n');
expect(ignoreContent[0]).toBe('bower_components');
expect(ignoreContent[1]).toBe('dist');
expect(ignoreContent[2]).toBe('node_modules');
});
it('should remove any whitespace from the existing .eslintignore',
function() {
mockGenerator.fs.write('.eslintignore', ['1_one', '', '2_two', ''].join('\n'));
eslint.init(mockGenerator);
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
var ignoreRef = files[0];
var ignoreContent = ignoreRef.content().split('\n');
expect(ignoreContent.length).toBe(2);
expect(ignoreContent[0]).toBe('1_one');
expect(ignoreContent[1]).toBe('2_two');
});
it('should delete the file if there\'s nothing to ignore', function() {
mockGenerator.fs.write('.eslintignore', '');
eslint.init(mockGenerator);
eslint.configure(mockGenerator);
var files = projectBuilder.getIncludedFiles();
expect(files.length).toBe(1);
expect(files[0].to).not.toBe('.eslintignore');
var rmFiles = projectBuilder.getExcludedFiles();
expect(rmFiles.length).toBe(1);
expect(rmFiles[0]).toBe('.eslintignore');
});
});
});
})();

View File

@@ -1,29 +1,34 @@
(function () {
(function() {
'use strict';
function buildMockGenerator (config, mockAnswers, mockOptions) {
var configDefaults = {};
var memFs = require('mem-fs');
var editor = require('mem-fs-editor');
var store = memFs.create();
config = config || {};
mockAnswers = mockAnswers || {};
return {
fs: editor.create(store),
appname: 'generator-openstack',
async: function () {
return function () {
async: function() {
return function() {
};
},
config: {
defaults: function (values) {
Object.keys(values).forEach(function (key) {
defaults: function(values) {
Object.keys(values).forEach(function(key) {
configDefaults[key] = values[key];
});
},
get: function (value) {
get: function(value) {
return config[value] || configDefaults[value];
},
set: function (key, value) {
set: function(key, value) {
if (typeof key === 'object') {
Object.keys(key).forEach(function (index) {
Object.keys(key).forEach(function(index) {
config[index] = key[index];
});
} else {
@@ -31,9 +36,9 @@
}
}
},
prompt: function (params, callback) {
prompt: function(params, callback) {
var answers = {};
params.forEach(function (param) {
params.forEach(function(param) {
if (param.when && !param.when(answers)) {
return;