Tightening project with grunt build

Grunt plugins for tightening a project - linting, complexity, todos, etc.

Related: Tightening Nodejs Project, Tightening Jshint.

I use grunt to build my JavaScript projects. There are plugins to do everything, and if something is missing, I can easily write one myself.

As the project matures, there is often a switch from fast feature prototyping and finding what a client would pay for, to increasing the quality (tightening) the project once the contract is signed. There are several grunt plugins that can help tighten the project.

Before showing code examples, note that we have moved all source wild cards into a single config object

1
2
3
4
var appFiles = {
js: ['src/**/*.js', '!src/**/*-spec.js'],
testjs: ['Gruntfile.js', 'src/**/*-spec.js']
};

Strict dependency versioning

We enforce strict dependency versions using grunt-nice-package. It removes wild card symbols from dependencies listed in package.json file. This ensures that every build from clean slate is repeatable. You can configure the task in several ways, but most projects can get by with defaults. Just load the task and add it to the default pipeline

1
2
grunt.loadNpmTasks('grunt-nice-package');
grunt.registerTask('default', ['nice-package', ...]);

Filenames

We enforce consistent file naming scheme using grunt-filenames, for example all lowercase without any other characters.

1
2
3
4
5
6
filenames: {
options: {
valid: /^[a-z]+\.js$/
},
src: ['tasks/*.js']
}

There are two built-in schemes you can use instead of specifying regular expression, dashes and camelCase.

Source code linting

We use four linting modules: grunt-contrib-jshint, grunt-eslint, grunt-jscs and grunt-jsonlint.

Jshint was our first linting tool, but we found eslint to be highly configurable, because we could write our own rules easily. For example, we use camel_case to glue client to backend naming scheme. Finally, we use JavaScript style checker jscs to complement jshint and find the remaining stylistic issues.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
jshint: {
all: ['<%= appFiles.js %>'],
options: {
jshintrc: 'path/to/.jshintrc'
}
},
jscs: {
all: '<%= appFiles.js %>',
options: {
config: 'path/to/.eslintrc'
}
},
eslint: {
all: '<%= appFiles.js %>',
options: {
config: 'path/to/eslintconfig.json',
rulesdir: ['extra/rule1/path', 'extra/rule2/path']
}
},
jsonlint: {
all: {
src: ['*.json', 'src/**/*.json']
}
}

As the project matured, we turned on more rules, and we measure how many jshint rules we specified using grunt-jshint-solid plugin. It looks at our .jshintrc file and tells us what percentage of settings we have specified. Starting project can get by with only basic rules checked, while the production code needs to follow specific rules, thus we shoot for 100% of rules set. This is not an argument for tabs or spaces, just picking and checking the consistent white space either way.

Code complexity

Simple code is easy to read, debug and modify. We measure different static complexity metrics using grunt-complexity plugin. For now, we only print the results, but do not break the build if any file is above the threshold (too complex). Later we plan to switch high complexity from being just a warning to an error.

1
2
3
4
5
6
7
8
9
10
11
12
complexity: {
all: {
src: '<%= appFiles.js %>',
options: {
breakOnErrors: false, // keep building
errorsOnly: false, // show warnings and errors
cyclomatic: 5,
halstead: 22,
maintainability: 100
}
}
}

Hard-coded values

Our source follows strict style, thus we can find some bad smells using regular expressions. There is a plugin grunt-regex-check that can check source files against a regular expression. Unfortunately the plugin always breaks on a positive match. I created a fork that can either warn or break. For example, let us warn the user when there are URLs in the source code in the form /endpoint/v* (the URLs should be injected via config objects instead).

1
2
3
4
5
6
7
8
9
10
'regex-check': {
'no hardcoded api urls': {
options: {
breakOnError: true,
excluded: '*-spec.js',
pattern: /endpoint\/v/
},
src: ['<%= appFiles.js %>']
}
}

We allow hard-coded URLs in the test (spec) files.

We can easily ban more regular expressions by adding targets to the task. For example, let us forbid naming our AngularJs modules starting with ng-

1
2
3
4
5
6
7
8
9
10
11
'regex-check': {
'no hardcoded api urls': { ... },
'no ng- modules'
options: {
breakOnError: true,
excluded: '*-spec.js',
pattern: /angular\.module\('ng/
},
src: ['<%= appFiles.js %>']
}
}

The regular expression works well because we enforce the same code style (no space between module and opening parenthesis, single quotes) using linters.

Technical debt

As we write code, we leave lots of TODOs sprinkled around. This technical debt stays there for long time because it is hidden. We keep reminding ourselves there are things to refactor by finding and showing the todos using grunt-todos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
todos: {
all: {
src: ['<%= appFiles.js %>', '<%= appFiles.testjs %>'],
options: {
verbose: false,
reporter: {
fileTasks: function (file, tasks) {
if (!tasks.length) {
return '';
}
var result = '';
result += file + '\n';
tasks.forEach(function (task) {
result += task.lineNumber + ': ' + task.line + '\n';
});
result += '\n';
return result;
}
}
}
}
}

We find todos in the source and test code, writing filename and line number for each.

Conclusion

Once we have implemented certain features and signed a contract, we must make sure it continues to work. This means we need to turn on the screws tighter. Luckily, our build tools has multiple plugins that allows us to measure, warn and even stop the build if something suspicious finds its way into the source code. At present, we are looking into ways to adjust the limits using aged module, see Aged to perfection blog post.