dominatr-grunt
Build all the things!
Grunt Tasks for Angular Sites
At Vokal, we build a lot of Angular sites. dominatr-grunt encapsulates our best practices in a set of Grunt tasks that can be easily installed and updated with npm.
At a high level, the tasks include:
- Linting
- Testing
- Generating code coverage
- Building a static site
- Deployment to S3/CloudFront
- Sending notifications of successful deployments
For a more complete explanation of tasks see Grunt Tasks or look in the /grunt
folder of this repo.
Getting Started
This plugin requires Grunt >=0.4.0
and a lengthy list of other dependencies. To get started, and paste the peerDependencies
from the dominatr-grunt package.json
file here to your local devDependencies
. Then run npm install dominatr-grunt --save-dev
If you haven't used Grunt before, be sure to check out the Getting Started guide, as it explains how to create a Gruntfile as well as install and use Grunt plugins.
Use
This build process is designed for JavaScript web projects and deployment through Amazon Web Services. While not every task may be in use on a given project, these tasks were found to be useful for most of them. Unique needs for projects can arise and load-grunt-config
provides easy configuration changes where necessary.
There is no guarantee that this will work on less than Node v4
or npm v2.14
, so please take that into consideration as necessary.
Configuration
dominatr-grunt contains the standard build process configuration for Vokal web projects. It uses load-grunt-config to keep our build process in sync across multiple projects. The project structure is important for dominatr-grunt to work effectively and will be described in more detail below.
Configuration of this plugin relies heavily on 3 files:
-
Gruntfile.js
Setup for projects is simple with load-grunt-config
.
module.exports = function ( grunt )
{
var env = grunt.option( "env" ) || "local";
grunt.initConfig( {
env: grunt.file.readJSON( "env.json" )[ env ],
envName: env,
version: grunt.option( "gitver" ) || Date.now(),
slackApiToken: grunt.option( "slacktoken" )
} );
var path = require( "path" );
require( "load-grunt-config" )( grunt, {
configPath: path.join( process.cwd(), "node_modules", "dominatr-grunt", "grunt" ),
overridePath: path.join( process.cwd(), "grunt" ),
mergeFunction: function ( obj, ext )
{
return require( "config-extend" )( obj, ext );
}
} );
};
The configPath
points to npm
's installation of dominatr-grunt and lets the project root grunt
folder override these tasks where necessary.
-
package.json
The dependency on dominatr-grunt should be locked with a minor version, like 6.0.x
. Also, a handful of shortcuts are provided that can be included in your package file scripts
section.
"scripts": {
"install": "node ./node_modules/protractor/bin/webdriver-manager update",
"server": "grunt connect:local:keepalive",
"start": "grunt build connect:local watch",
"test": "grunt test",
"teststack": "grunt teststack"
}
The oddball install
script ensures that protractor
tests will function properly by downloading or updating the proper drivers.
-
env.json
dominatr-grunt supports any number of development/deployment environments, but depends on one local
configuration. Environments should be in this root file and include any configurable settings.
{
"local": {
"apiroot": "https://api-dev.yourappname.com",
"libraryKey": "third-party-library-key"
}
}
Working with different environments is rather simple, just provide a --env=name
flag when running any grunt
task to swap to a different configuration. Here's an example env.json
file with deployment environment settings:
{
"local": {
"apiroot": "http://localhost:4000",
"libraryKey": "third-party-library-key"
},
"dev": {
"apiroot": "https://api-dev.yourapp.com",
"libraryKey": "third-party-library-key",
"host": "https://dev.yourapp.com",
"aws.s3Bucket": "yourapp-dev",
"aws.distributionId": "ABCDEFGHIJKLMN",
"notification.emailTo": "yourEmail@email.com",
"notification.emailFrom": "noreply@yourapp.com",
"notification.slackChannel": "channelname"
},
"staging": {
"apiroot": "https://api-staging.yourapp.com",
"libraryKey": "third-party-staging-key",
"host": "https://staging.yourapp.com",
"aws.s3Bucket": "yourapp-staging",
"aws.distributionId": "OPQRSTUVWXYZAB",
"notification.emailTo": [ "yourTeam@email.com", "yourEmail@email.com" ],
"notification.emailFrom": "noreply@yourapp.com",
"notification.slackChannel": "channelname"
},
"prod": {
"apiroot": "https://api.yourapp.com",
"libraryKey": "third-party-production-key",
"host": "https://www.yourapp.com",
"aws.s3Bucket": "yourapp-prod",
"aws.distributionId": "JKLMNOPQRSTUVW",
"notification.emailTo": [ "yourTeam@email.com", "yourEmail@email.com" ],
"notification.emailFrom": "noreply@yourapp.com",
"notification.slackChannel": "channelname"
}
}
The npm start
task by default uses the local
environment but can be changed by including the env
flag in the start
script of the package.json
file.
Grunt Flags in NPM
When running grunt
tasks, you can pass additional arguments similar to -flag=value
. This is used in deployment scripts to change the desired outcome environment by adding -env=staging
or -env=prod
. Flags for individual grunt tasks are mentioned with their specific task below.
As of npm
2.0.0, you can pass script arguments through the run-script
step to the scripts block in your package.json
file. To do this, flags need to be located after a --
delimiter like so: npm start -- -env=staging
. This would start the local server using the staging
block in env.json
and can be useful for debugging without ng-mocks
(which is included by default with the local
env).
Additional documentation for NPM's run-script
can be found here
Project Structure
Our build process is primarily designed to work with angular, but can work with any module based development.
Here are the basic required root files for dominatr-grunt to function.
.jshintrc
env.json
Gruntfile.js
package.json
The source folder is as follows.
source/
|-- fonts
|-- images
|-- modules
|-- index.js
|-- _app
|-- index.js
|-- styles
|-- main.less
|-- *.less
|-- templates
|-- index.html
|-- *.html
|-- <other subfolders>
|-- *.js
|-- <other module folders>
There are a few important files to take note of in our /source
directory.
-
/modules/index.js
This is the main .js
file and should be used to require any dependencies. Anything that needs to be set globally should be attached here.
All module folders (except mocks
, see below) should be included with require
. At the end of the file, the ngtemplates
output must also be required.
"use strict";
global.angular = require( "angular" );
require( "angular-route" );
require( "angular-touch" );
require( "./_app" );
require( "./<other module folders>" );
require( "../../build/templates.js" );
Module directories should include an index.js
file or the file should be specified like require( "./thing/module.js" )
.
-
/modules/_app/index.js
In an angular application, this is where you would define your app module. The only requirement here is to require
other scripts in your .js
subfolders.
angular.module( "App", [] )
.service( "CoolSrvc", require( "./services/coolThing" ) )
.filter( "fullName", require( "./filters/fullName" ) );
You don't need to include .js
in the file paths. If you have complex angular config
or run
blocks, you can require those like .run( require( "./run" ) )
and place a run.js
file next to the module index file.
-
/modules/_app/templates/index.html
This is your main index file with <head>
and <body>
declarations. It is not included in the ngtemplates
task, just copied during the build
task. All other .html
files in module templates
directories are watched and included in the ngtemplates
task.
-
/modules/_app/styles/main.less
This is the main .less
file for generating the project.css
file in the build
step. While all .less
files in any module styles
subdirectory is watched for changes, only this file is used in the grunt less
task.
To include other module styles into this file, you can use @import "../../moduleName/styles/fileName"
. You can ignore the .less
extension in these imports. To import css files from third party modules, you can perform the following:
@nodeModules: "../../../../node_modules";
@import (inline) "@{nodeModules}/ng-dialog/css/ngDialog.css";
@import (inline) "@{nodeModules}/toastr/build/toastr.min.css";
-
/modules/mocks
The mocks
directory is special in that it is excluded in all environments except local
. It is also excluded from code coverage reports since it is code not being included in a deployment.
While this is not the appropriate place to discuss how to use mocks, know that a /modules/mocks/index.js
file is included by default when running locally and any support files should be included with require
from that point.
-
Other Script Files
All other subfolder script files should include a module.exports
value so that it can be required by the module index.js
or alternate declaration file. For most angular singletons, the format will look similar to the following:
"use strict";
module.exports = [ "SomeSrvc", "$q",
function ( SomeSrvc, $q )
{
}
];
Grunt Tasks
A list of all available grunt
tasks can be found by running grunt -h
or grunt --help
. The default grunt
task runs the grunt build
alias. More documentation on each plugin can be found on their respective github or npm pages.
This list aims to be a reference and may not cover every detail of our implementation. Please consult our task configuration files if more detail is needed.
-
browserify
There are two browserify tasks available with dominatr-grunt, build
and test
.
browserify:build
generates a dist.js
file in the build
directorybrowserify:test
includes istanbul code coverage and generates the dist file in the .instrumented
folder
Both tasks include mocks when in the local
environment. If you wish to change the root file used for mocks from modules/mocks/index.js
, you can provide a flag like --mocks=other.js
and it will look for modules/mocks/other.js
instead.
String replacement is also handled in the browserify tasks. The selected environment object from env.json
file is read in and key-value replaced in files. To prevent conflict with angular, keys should be wrapped like << keyName >>
. Objects are reduced to strings separated as dot notation, so both "obj.someKey"
and "obj": { "someKey": ... }
replace << obj.someKey >>
.
-
clean
Three subtasks, build
, test
, and coverage
, to delete their respective directories.
-
cloudfront
Deployment task to create an 'invalidation' after files have been uploaded to s3
-
connect
Local file server, using port 3000
for development and 9000
for testing. Middleware is setup to serve the index file when there is no file extension specified.
-
copy
Move static files from multiple directories to the build folder, particularly the main index.html
file.
-
filerev
Additional cache-busting power utilized in deployment. Includes a filerev_replace
task to update build files of filenames changed using filerev
.
-
imagemin
Minification of images during deployment.
-
jshint
js
standards control using jshint. This task requires a .jshintrc
file to be located in the project root. Configuration options for this file can be found here.
-
less
CSS pre-processor, more information can be found on their website.
-
localstack
Method of communicating with Browserstack for testing. Only utilized when running grunt teststack
or npm run teststack
.
-
notification
A custom task to send an email using AWS SES after a deployment completes. This is included at the end of the deploy
alias.
The notification task requires the AWS access key and secret key to work, as well as a host url set in the env.json
file. This should point to the deployed url so it can be linked in the email correctly. Email addresses should be included in the environments file as notification.emailTo
and notification.emailFrom
. emailTo
can be either a string or an array and emailFrom
must be SES Verified.
Because this task is at the end of a deploy
, it can fail without preventing deployment. This will show as a failure in the terminal
and CI services but the files have been uploaded to AWS. IMPORTANT: The notification task will fail when AWS SES is in sandbox mode and any of the recipient emails are not verified.
-
notification_slack
A task to send a message to a Slack channel after a deployment completes.
The task requires a target Slack channel and a host url set in the env.json
file. The target Slack channel should be set in notification.slackChannel
. For reference, public channels need to be prefixed with a #
(#general
) while private groups do not (secret-group
). For more details, consult Slack's channel documentation.
Also, this requires a Slack API token passed to grunt via a --slacktoken=$SLACK_TOKEN
switch. Documentation for creating an api token can be found here.
-
ngtemplates
Compiles a templates.js
file in the build directory with all of the modules/*/templates/*.html
files for caching in angular. When referencing these files in an angular app, the file path should be similar to modules/<modulename>/templates/<filename>.html
.
-
postcss
Removes unnecessary vendor prefixes using Autoprefixer and includes minification when using a prod
environment.
-
protractor_coverage
Testing task, split for local
and stack
options. The latter is designated for Browserstack. See the grunt test
and grunt teststack
aliases below.
-
robots
Custom task for writing a robots.txt
file during a deployment. The content of the file is determined by the environment and a host
value in the env.json
file.
-
s3
Deployment task for sending files to an AWS s3 bucket.
-
svg_sprite
Generates svg sprites and their .css
files based on images located in the source/images/svg-sprite
directory. The output folder is build/svg-sprite
.
-
svg_inline
Injects SVG content referenced by SVG <use>
directly into the HTML document. This allows for the good parts of SVG <use>
like ability to apply CSS to SVG content, without the problematic lack of support in every version of IE. This is inline (haha) with what GitHub is doing for SVGs.
-
svgmin
Minification for .svg
files during deployment.
-
truecolors_less
Converts a truecolors file to a .less
file. More documentation can be found here.
-
uglify
Deployment task to minify (and mangle) the crap out of the dist.js
file.
-
watch
A multitude of targets to keep development moving without having to manually run builds. Uses livereload by default on any file served through connect:local
. Has actions for changes to:
- all
.html
files in modules/*/templates
- all
.js
files in modules/**
build/templates.js
- all
.less
files in modules/*/styles
- svg files in `source/images/svg_sprite
Grunt Aliases
The following group tasks are available as grunt <taskname>
for direct use:
build
Runs associated tasks to generate a working build folder, including at the least clean:build
, less
, copy
, and browserify:build
. It is included when running npm start
.
test
Runs protractor testing locally with protractor_coverage
and reporting. Generates a temporary .instrumented
folder to hold test files and deletes it on completion. This also runs jsint
. The configuration file for testing is located at /tests/config/protractor-config.js
.
teststack
Only different from test
in that it uses Browserstack instead of a local browser. Configuration for this is in /tests/config/protractor-config-browserstack.js
. An authorization key for Browserstack must be in the environment variable BROWSERSTACK_KEY
for this to work.
deploy
Clean build and packaging for output, an --env
flag should be specified when running this task and requires including AWS credentials if using s3 and cloudfront. The full command would look similar to grunt deploy --env=staging --aws-access-key-id=<aws-access-key> --aws-secret-access-key=<lengthy-aws-access-token>
Three additional aliases are created for internal shortcuts but should not be used except for debugging situations:
Overriding Tasks
To override an existing grunt task or subtask, create a grunt
folder in your project root directory and a filename matching the task name per load-grunt-config
. For example, if you need to override the copy:index
subtasks, you'd create /grunt/copy.js
with something like the following:
"use strict";
module.exports = {
index: {
src: "source/path/to/index.html",
dest: "build/index.html"
}
};
Replacements are done at the subtask level, so the file above would not destroy the copy:build
task in the process. For more information on writing task files, view the load-grunt-config
documentation.
Other Bits
While not required, we suggest adding the following lines to your .gitignore
file.
/build
/coverage
/.instrumented
/.inlined
The /coverage
and /.instrumented
directories are used during testing and erased with each run. Files in these folders are not intended to be committed. The /.inlined
directory holds HTML build artifacts after they are SVG inlined but before ngtemplates runs. The /build
folder contents changes with the current environment settings and is not fit for version control.