dominatr-grunt v8.0.0
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.jsSetup for projects is simple with
load-grunt-config.module.exports = function ( grunt ) { // for environment configuration var env = grunt.option( "env" ) || "local"; grunt.initConfig( { env: grunt.file.readJSON( "env.json" )[ env ], envName: env, version: grunt.option( "gitver" ) || Date.now(), // for deployment cache-busting slackApiToken: grunt.option( "slacktoken" ) // if using slack notifications } ); 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
configPathpoints tonpm's installation of dominatr-grunt and lets the project rootgruntfolder override these tasks where necessary.package.jsonThe 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 filescriptssection."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
installscript ensures thatprotractortests will function properly by downloading or updating the proper drivers.env.jsondominatr-grunt supports any number of development/deployment environments, but depends on one
localconfiguration. 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=nameflag when running anygrunttask to swap to a different configuration. Here's an exampleenv.jsonfile 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 starttask by default uses thelocalenvironment but can be changed by including theenvflag in thestartscript of thepackage.jsonfile.
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.jsonThe 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.jsThis is the main
.jsfile 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 withrequire. At the end of the file, thengtemplatesoutput 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.jsfile or the file should be specified likerequire( "./thing/module.js" )./modules/_app/index.jsIn an angular application, this is where you would define your app module. The only requirement here is to
requireother scripts in your.jssubfolders.angular.module( "App", [] ) .service( "CoolSrvc", require( "./services/coolThing" ) ) .filter( "fullName", require( "./filters/fullName" ) );You don't need to include
.jsin the file paths. If you have complex angularconfigorrunblocks, you can require those like.run( require( "./run" ) )and place arun.jsfile next to the module index file./modules/_app/templates/index.htmlThis is your main index file with
<head>and<body>declarations. It is not included in thengtemplatestask, just copied during thebuildtask. All other.htmlfiles in moduletemplatesdirectories are watched and included in thengtemplatestask./modules/_app/styles/main.lessThis is the main
.lessfile for generating theproject.cssfile in thebuildstep. While all.lessfiles in any modulestylessubdirectory is watched for changes, only this file is used in thegrunt lesstask.To include other module styles into this file, you can use
@import "../../moduleName/styles/fileName". You can ignore the.lessextension in these imports. To import css files from third party modules, you can perform the following:// create shortcut to the node_modules folder @nodeModules: "../../../../node_modules"; // import inline @import (inline) "@{nodeModules}/ng-dialog/css/ngDialog.css"; @import (inline) "@{nodeModules}/toastr/build/toastr.min.css";/modules/mocksThe
mocksdirectory is special in that it is excluded in all environments exceptlocal. 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.jsfile is included by default when running locally and any support files should be included withrequirefrom that point.Other Script Files
All other subfolder script files should include a
module.exportsvalue so that it can be required by the moduleindex.jsor alternate declaration file. For most angular singletons, the format will look similar to the following:"use strict"; module.exports = [ "SomeSrvc", "$q", function ( SomeSrvc, $q ) { // stuff here } ];
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,
buildandtest.browserify:buildgenerates adist.jsfile in thebuilddirectorybrowserify:testincludes istanbul code coverage and generates the dist file in the.instrumentedfolder
Both tasks include mocks when in the
localenvironment. If you wish to change the root file used for mocks frommodules/mocks/index.js, you can provide a flag like--mocks=other.jsand it will look formodules/mocks/other.jsinstead.String replacement is also handled in the browserify tasks. The selected environment object from
env.jsonfile 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, andcoverage, 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
3000for development and9000for 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.htmlfile.filerev
Additional cache-busting power utilized in deployment. Includes a
filerev_replacetask to update build files of filenames changed usingfilerev.imagemin
Minification of images during deployment.
jshint
jsstandards control using jshint. This task requires a.jshintrcfile 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 teststackornpm 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
deployalias.The notification task requires the AWS access key and secret key to work, as well as a host url set in the
env.jsonfile. 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 asnotification.emailToandnotification.emailFrom.emailTocan be either a string or an array andemailFrommust 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 theterminaland 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.jsonfile. The target Slack channel should be set innotification.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_TOKENswitch. Documentation for creating an api token can be found here.ngtemplates
Compiles a
templates.jsfile in the build directory with all of themodules/*/templates/*.htmlfiles for caching in angular. When referencing these files in an angular app, the file path should be similar tomodules/<modulename>/templates/<filename>.html.postcss
Removes unnecessary vendor prefixes using Autoprefixer and includes minification when using a
prodenvironment.protractor_coverage
Testing task, split for
localandstackoptions. The latter is designated for Browserstack. See thegrunt testandgrunt teststackaliases below.robots
Custom task for writing a
robots.txtfile during a deployment. The content of the file is determined by the environment and ahostvalue in theenv.jsonfile.s3
Deployment task for sending files to an AWS s3 bucket.
svg_sprite
Generates svg sprites and their
.cssfiles based on images located in thesource/images/svg-spritedirectory. The output folder isbuild/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
.svgfiles during deployment.truecolors_less
Converts a truecolors file to a
.lessfile. More documentation can be found here.uglify
Deployment task to minify (and mangle) the crap out of the
dist.jsfile.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
.htmlfiles inmodules/*/templates - all
.jsfiles inmodules/** build/templates.js- all
.lessfiles inmodules/*/styles - svg files in `source/images/svg_sprite
- all
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:
pretestposttestpackage
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
/.inlinedThe /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.