2014-05-03 17:34:41 +04:00
// # Task automation for Ghost
//
// Run various tasks when developing for and working with Ghost.
//
// **Usage instructions:** can be found in the [Custom Tasks](#custom%20tasks) section or by running `grunt --help`.
//
// **Debug tip:** If you have any problems with any Grunt tasks, try running them with the `--verbose` command
2016-09-14 13:22:16 +03:00
2017-11-01 16:44:54 +03:00
require ( './core/server/overrides' ) ;
2019-02-11 15:26:06 +03:00
const config = require ( './core/server/config' ) ;
2019-06-19 12:30:28 +03:00
const urlService = require ( './core/frontend/services/url' ) ;
2019-02-11 15:26:06 +03:00
const _ = require ( 'lodash' ) ;
const chalk = require ( 'chalk' ) ;
const fs = require ( 'fs-extra' ) ;
const KnexMigrator = require ( 'knex-migrator' ) ;
const knexMigrator = new KnexMigrator ( {
knexMigratorFilePath : config . get ( 'paths:appRoot' )
} ) ;
const path = require ( 'path' ) ;
const escapeChar = process . platform . match ( /^win/ ) ? '^' : '\\' ;
const cwd = process . cwd ( ) . replace ( /( |\(|\))/g , escapeChar + '$1' ) ;
const buildDirectory = path . resolve ( cwd , '.build' ) ;
const distDirectory = path . resolve ( cwd , '.dist' ) ;
let hasBuiltClient = false ;
const logBuildingClient = function ( grunt ) {
if ( ! hasBuiltClient ) {
grunt . log . writeln ( 'Building admin client... (can take ~1min)' ) ;
setTimeout ( logBuildingClient , 5000 , grunt ) ;
}
} ;
// ## Grunt configuration
const configureGrunt = function ( grunt ) {
// #### Load all grunt tasks
//
// Find all of the task which start with `grunt-` and load them, rather than explicitly declaring them all
require ( 'matchdep' ) . filterDev ( [ 'grunt-*' , '!grunt-cli' ] ) . forEach ( grunt . loadNpmTasks ) ;
var cfg = {
// #### Common paths used by tasks
paths : {
build : buildDirectory ,
releaseBuild : path . join ( buildDirectory , 'release' ) ,
dist : distDirectory ,
releaseDist : path . join ( distDirectory , 'release' )
} ,
// Standard build type, for when we have nightlies again.
buildType : 'Build' ,
// Load package.json so that we can create correctly versioned releases.
pkg : grunt . file . readJSON ( 'package.json' ) ,
clientFiles : [
'server/web/admin/views/default.html' ,
'built/assets/ghost.js' ,
'built/assets/ghost.css' ,
'built/assets/vendor.js' ,
'built/assets/vendor.css'
] ,
// ### grunt-contrib-watch
// Watch files and livereload in the browser during development.
// See the [grunt dev](#live%20reload) task for how this is used.
watch : grunt . option ( 'no-server-watch' ) ? { files : [ ] } : {
livereload : {
files : [
'content/themes/casper/assets/css/*.css' ,
'content/themes/casper/assets/js/*.js'
] ,
options : {
2019-04-01 13:59:48 +03:00
livereload : true ,
interval : 500
2013-08-17 22:39:02 +04:00
}
} ,
express : {
2019-02-11 15:26:06 +03:00
files : [
'core/ghost-server.js' ,
'core/server/**/*.js' ,
2019-07-18 17:31:04 +03:00
'core/frontend/**/*.js' ,
2019-02-11 15:26:06 +03:00
'config.*.json' ,
'!config.testing.json'
] ,
tasks : [ 'express:dev' ] ,
2013-08-17 22:39:02 +04:00
options : {
2019-02-11 15:26:06 +03:00
spawn : false ,
2019-04-01 13:59:48 +03:00
livereload : true ,
interval : 500
2013-08-17 22:39:02 +04:00
}
2019-02-11 15:26:06 +03:00
}
} ,
// ### grunt-express-server
// Start a Ghost express server for use in development and testing
express : {
options : {
script : 'index.js' ,
output : 'Ghost is running'
2013-08-17 22:39:02 +04:00
} ,
2019-02-11 15:26:06 +03:00
dev : {
options : { }
2015-02-19 02:47:55 +03:00
} ,
2019-02-11 15:26:06 +03:00
test : {
2013-06-25 15:43:15 +04:00
options : {
2019-02-11 15:26:06 +03:00
node _env : 'testing'
2015-04-18 15:20:03 +03:00
}
2019-02-11 15:26:06 +03:00
}
} ,
// ### grunt-mocha-cli
mochacli : {
options : {
ui : 'bdd' ,
reporter : grunt . option ( 'reporter' ) || 'spec' ,
timeout : '30000' ,
save : grunt . option ( 'reporter-output' ) ,
require : [ 'core/server/overrides' ] ,
exit : true
2015-04-18 15:20:03 +03:00
} ,
2019-02-11 15:26:06 +03:00
unit : {
src : [
'core/test/unit/**/*_spec.js'
]
2015-02-14 21:43:18 +03:00
} ,
2016-08-07 12:53:28 +03:00
2019-02-11 15:26:06 +03:00
acceptance : {
src : [
'core/test/acceptance/**/*_spec.js'
]
2017-04-05 22:46:22 +03:00
} ,
2019-02-11 15:26:06 +03:00
regression : {
src : [
'core/test/regression/**/*_spec.js'
]
2013-09-18 22:56:39 +04:00
} ,
2019-02-11 15:26:06 +03:00
// #### Run single test (src is set dynamically, see grunt task 'test')
single : { }
} ,
bgShell : {
client : {
cmd : function ( ) {
logBuildingClient ( grunt ) ;
return 'grunt subgrunt:watch' ;
} ,
bg : grunt . option ( 'client' ) ? false : true ,
stdout : function ( chunk ) {
// hide certain output to prevent confusion when running alongside server
var filter = grunt . option ( 'client' ) ? false : [
/> ghost-admin/ ,
/^Livereload/ ,
/^Serving on/
] . some ( function ( regexp ) {
return regexp . test ( chunk ) ;
} ) ;
if ( ! filter ) {
grunt . log . write ( chunk ) ;
2014-05-27 18:37:44 +04:00
}
2015-12-10 17:46:58 +03:00
2019-04-04 11:47:54 +03:00
if ( chunk . indexOf ( 'Slowest Nodes' ) !== - 1 ) {
2019-02-11 15:26:06 +03:00
hasBuiltClient = true ;
2015-12-10 17:46:58 +03:00
}
2019-02-11 15:26:06 +03:00
} ,
stderr : function ( chunk ) {
hasBuiltClient = true ;
grunt . log . error ( chunk ) ;
2015-12-10 17:46:58 +03:00
}
2019-02-11 15:26:06 +03:00
}
} ,
2016-05-31 16:40:20 +03:00
2019-02-11 15:26:06 +03:00
// ### grunt-shell
// Command line tools where it's easier to run a command directly than configure a grunt plugin
shell : {
lint : {
command : 'yarn lint'
} ,
master : {
command : function ( ) {
var upstream = grunt . option ( 'upstream' ) || process . env . GHOST _UPSTREAM || 'upstream' ;
grunt . log . writeln ( 'Pulling down the latest master from ' + upstream ) ;
2019-02-11 19:25:25 +03:00
return `
2019-03-06 11:03:01 +03:00
git submodule sync
git submodule update
if ! git diff -- exit - code -- quiet -- ignore - submodules = untracked ; then
2019-02-11 19:25:25 +03:00
echo "Working directory is not clean, do you have uncommited changes? Please commit, stash or discard changes to continue."
exit 1
fi
git checkout master
git pull $ { upstream } master
yarn
git submodule foreach "
git checkout master && git pull $ { upstream } master
"
` ;
2017-04-10 12:30:21 +03:00
}
2019-02-11 15:26:06 +03:00
}
} ,
// ### grunt-contrib-clean
// Clean up files as part of other tasks
clean : {
built : {
src : [
'core/built/**'
]
2017-04-10 12:30:21 +03:00
} ,
2019-02-11 15:26:06 +03:00
release : {
src : [ '<%= paths.releaseBuild %>/**' ]
} ,
test : {
src : [ 'content/data/ghost-test.db' ]
} ,
tmp : {
src : [ '.tmp/**' ]
} ,
dependencies : {
src : [ 'node_modules/**' , 'core/client/bower_components/**' , 'core/client/node_modules/**' ]
}
} ,
2017-04-10 12:30:21 +03:00
2019-02-11 15:26:06 +03:00
// ### grunt-contrib-compress
// Zip up files for builds / releases
compress : {
release : {
2016-08-07 12:53:28 +03:00
options : {
2019-02-11 15:26:06 +03:00
archive : '<%= paths.releaseDist %>/Ghost-<%= pkg.version %>.zip'
2016-08-07 12:53:28 +03:00
} ,
2019-02-11 15:26:06 +03:00
expand : true ,
cwd : '<%= paths.releaseBuild %>/' ,
src : [ '**' ]
}
} ,
2017-03-14 16:51:32 +03:00
2019-02-11 15:26:06 +03:00
// ### grunt-update-submodules
// Grunt task to update git submodules
update _submodules : {
pinned : {
options : {
params : '--init'
}
}
} ,
2016-08-07 12:53:28 +03:00
2019-02-11 15:26:06 +03:00
uglify : {
prod : {
options : {
sourceMap : false
2016-08-07 12:53:28 +03:00
} ,
2019-02-11 15:26:06 +03:00
files : {
'core/server/public/ghost-sdk.min.js' : 'core/server/public/ghost-sdk.js'
}
}
} ,
2016-08-07 12:53:28 +03:00
2019-02-11 15:26:06 +03:00
cssnano : {
prod : {
options : {
sourcemap : false
2016-08-07 12:53:28 +03:00
} ,
2019-02-11 15:26:06 +03:00
files : {
'core/server/public/ghost.min.css' : 'core/server/public/ghost.css'
2016-05-31 16:40:20 +03:00
}
2019-02-11 15:26:06 +03:00
}
} ,
// ### grunt-subgrunt
// Run grunt tasks in submodule Gruntfiles
subgrunt : {
options : {
npmInstall : false ,
npmPath : 'yarn'
2017-04-13 10:26:48 +03:00
} ,
2019-02-11 15:26:06 +03:00
init : {
options : {
npmInstall : true
} ,
projects : {
2019-05-13 11:11:29 +03:00
'core/client' : 'init'
2017-04-13 10:26:48 +03:00
}
2019-02-11 15:26:06 +03:00
} ,
2013-05-13 23:18:20 +04:00
2019-02-11 15:26:06 +03:00
dev : {
'core/client' : 'shell:ember:dev'
} ,
2013-06-25 15:43:15 +04:00
2019-02-11 15:26:06 +03:00
prod : {
2019-05-13 11:11:29 +03:00
'core/client' : 'shell:ember:prod'
2019-02-11 15:26:06 +03:00
} ,
2013-08-01 11:12:59 +04:00
2019-02-11 15:26:06 +03:00
watch : {
projects : {
2019-05-13 11:11:29 +03:00
'core/client' : [ 'shell:ember:watch' , '--live-reload-base-url="' + urlService . utils . getSubdir ( ) + '/ghost/"' ]
2015-05-28 18:16:09 +03:00
}
2019-02-11 15:26:06 +03:00
}
} ,
// ### grunt-contrib-symlink
// Create symlink for git hooks
symlink : {
githooks : {
// Enable overwrite to delete symlinks before recreating them
overwrite : false ,
// Enable force to overwrite symlinks outside the current working directory
force : false ,
// Expand to all files in /hooks
expand : true ,
cwd : '.github/hooks' ,
src : [ '*' ] ,
dest : '.git/hooks'
}
}
} ;
2015-05-28 18:16:09 +03:00
2019-02-11 15:26:06 +03:00
// Load the configuration
grunt . initConfig ( cfg ) ;
// # Custom Tasks
// Ghost has a number of useful tasks that we use every day in development. Tasks marked as *Utility* are used
// by grunt to perform current actions, but isn't useful to developers.
//
// Skip ahead to the section on:
//
// * [Building assets](#building%20assets):
// `grunt init`, `grunt` & `grunt prod` or live reload with `grunt dev`
// * [Testing](#testing):
2019-04-08 17:12:48 +03:00
// `grunt validate`, the `grunt test-*` sub-tasks.
2019-02-11 15:26:06 +03:00
// ### Help
// Run `grunt help` on the commandline to get a print out of the available tasks and details of
// what each one does along with any available options. This is an alias for `grunt --help`
grunt . registerTask ( 'help' ,
'Outputs help information if you type `grunt help` instead of `grunt --help`' ,
function ( ) {
grunt . log . writeln ( 'Type `grunt --help` to get the details of available grunt tasks.' ) ;
2015-05-28 18:16:09 +03:00
} ) ;
2019-02-11 15:26:06 +03:00
// ## Testing
// Ghost has an extensive set of test suites. The following section documents the various types of tests
// and how to run them.
//
// TLDR; run `grunt validate`
// #### Set Test Env *(Utility Task)*
// Set the NODE_ENV to 'testing' unless the environment is already set to TRAVIS.
// This ensures that the tests get run under the correct environment, using the correct database, and
// that they work as expected. Trying to run tests with no ENV set will throw an error to do with `client`.
grunt . registerTask ( 'setTestEnv' ,
'Use "testing" Ghost config; unless we are running on travis (then show queries for debugging)' ,
function ( ) {
process . env . NODE _ENV = process . env . TRAVIS ? process . env . NODE _ENV : 'testing' ;
cfg . express . test . options . node _env = process . env . NODE _ENV ;
} ) ;
2016-05-25 09:02:23 +03:00
2019-02-11 15:26:06 +03:00
// ### Test
// **Testing utility**
//
// `grunt test:unit/apps_spec.js` will run just the tests inside the apps_spec.js file
//
// It works for any path relative to the core/test folder. It will also run all the tests in a single directory
// You can also run a test with grunt test:core/test/unit/... to get bash autocompletion
//
// `grunt test:regression/api` - runs the api regression tests
grunt . registerTask ( 'test' , 'Run a particular spec file from the core/test directory e.g. `grunt test:unit/apps_spec.js`' , function ( test ) {
if ( ! test ) {
grunt . fail . fatal ( 'No test provided. `grunt test` expects a filename. e.g.: `grunt test:unit/apps_spec.js`. Did you mean `npm test` or `grunt validate`?' ) ;
}
2016-05-25 09:02:23 +03:00
2019-02-11 15:26:06 +03:00
if ( ! test . match ( /core\/test/ ) && ! test . match ( /core\/server/ ) ) {
test = 'core/test/' + test ;
}
2014-12-24 16:51:16 +03:00
2019-02-11 15:26:06 +03:00
// CASE: execute folder
if ( ! test . match ( /.js/ ) ) {
test += '/**' ;
} else if ( ! fs . existsSync ( test ) ) {
grunt . fail . fatal ( 'This file does not exist!' ) ;
}
2016-07-20 13:39:40 +03:00
2019-02-11 15:26:06 +03:00
cfg . mochacli . single . src = [ test ] ;
grunt . initConfig ( cfg ) ;
grunt . task . run ( 'test-setup' , 'mochacli:single' ) ;
} ) ;
// #### Stub out ghost files *(Utility Task)*
// Creates stub files in the built directory and the views directory
// so that the test environments do not need to build out the client files
grunt . registerTask ( 'stubClientFiles' , function ( ) {
_ . each ( cfg . clientFiles , function ( file ) {
var filePath = path . resolve ( cwd + '/core/' + file ) ;
fs . ensureFileSync ( filePath ) ;
2017-03-09 22:38:20 +03:00
} ) ;
2019-02-11 15:26:06 +03:00
} ) ;
/ * *
* Ensures the target database get ' s automatically created .
* /
grunt . registerTask ( 'knex-migrator' , function ( ) {
return knexMigrator . init ( { noScripts : true } ) ;
} ) ;
// ### Validate
// **Main testing task**
//
// `grunt validate` will either run all tests or run linting
2019-03-11 18:43:47 +03:00
// `grunt validate` is called by `yarn test` and is used by Travis.
grunt . registerTask ( 'validate' , 'Run tests' , function ( ) {
2019-02-11 15:26:06 +03:00
grunt . task . run ( [ 'test-acceptance' , 'test-unit' ] ) ;
} ) ;
grunt . registerTask ( 'test-all' , 'Run all server tests' ,
[ 'test-acceptance' , 'test-unit' , 'test-regression' ] ) ;
// ### Lint
//
// `grunt lint` will run the linter
grunt . registerTask ( 'lint' , 'Run the code style checks for server & tests' ,
[ 'shell:lint' ]
) ;
// ### test-setup *(utility)(
// `grunt test-setup` will run all the setup tasks required for running tests
grunt . registerTask ( 'test-setup' , 'Setup ready to run tests' ,
[ 'knex-migrator' , 'clean:test' , 'setTestEnv' , 'stubClientFiles' ]
) ;
// ### Unit Tests *(sub task)*
// `grunt test-unit` will run just the unit tests
//
// If you need to run an individual unit test file, you can use the `grunt test:<file_path>` task:
//
// `grunt test:unit/config_spec.js`
//
// This also works for folders (although it isn't recursive), E.g.
//
// `grunt test:unit/helpers`
//
// Unit tests are run with [mocha](http://mochajs.org/) using
// [should](https://github.com/visionmedia/should.js) to describe the tests in a highly readable style.
// Unit tests do **not** touch the database.
grunt . registerTask ( 'test-unit' , 'Run unit tests (mocha)' ,
[ 'test-setup' , 'mochacli:unit' ]
) ;
grunt . registerTask ( 'test-regression' , 'Run regression tests.' ,
[ 'test-setup' , 'mochacli:regression' ]
) ;
grunt . registerTask ( 'test-acceptance' , 'Run acceptance tests' ,
[ 'test-setup' , 'mochacli:acceptance' ]
) ;
// #### Master Warning *(Utility Task)*
// Warns git users not ot use the `master` branch in production.
// `master` is an unstable branch and shouldn't be used in production as you run the risk of ending up with a
// database in an unrecoverable state. Instead there is a branch called `stable` which is the equivalent of the
// release zip for git users.
grunt . registerTask ( 'master-warn' ,
'Outputs a warning to runners of grunt prod, that master shouldn\'t be used for live blogs' ,
function ( ) {
grunt . log . writeln ( chalk . red (
'Use the ' + chalk . bold ( 'stable' ) + ' branch for live blogs. '
+ chalk . bold . underline ( 'Never' ) + ' master!'
) ) ;
grunt . log . writeln ( '>' , 'Always two there are, no more, no less. A master and a ' + chalk . bold ( 'stable' ) + '.' ) ;
2017-03-14 16:51:32 +03:00
} ) ;
2014-07-01 16:35:50 +04:00
2019-02-11 15:26:06 +03:00
// ## Building assets
//
// Ghost's GitHub repository contains the un-built source code for Ghost. If you're looking for the already
// built release zips, you can get these from the [release page](https://github.com/TryGhost/Ghost/releases) on
// GitHub or from https://ghost.org/download. These zip files are created using the [grunt release](#release)
// task.
//
// If you want to work on Ghost core, or you want to use the source files from GitHub, then you have to build
// the Ghost assets in order to make them work.
//
// There are a number of grunt tasks available to help with this. Firstly after fetching an updated version of
// the Ghost codebase, after running `yarn install`, you will need to run [grunt init](#init%20assets).
//
// For production blogs you will need to run [grunt prod](#production%20assets).
//
// For updating assets during development, the tasks [grunt](#default%20asset%20build) and
// [grunt dev](#live%20reload) are available.
// ### Init assets
// `grunt init` - will run an initial asset build for you
//
// Grunt init runs `yarn install && bower install` inside `core/client` as well as the standard asset build
// tasks which occur when you run just `grunt`. This fetches the latest client-side dependencies.
//
// This task is very important, and should always be run when fetching down an updated code base just after
// running `yarn install`.
//
// `bower` does have some quirks, such as not running as root. If you have problems please try running
// `grunt init --verbose` to see if there are any errors.
grunt . registerTask ( 'init' , 'Prepare the project for development' ,
[ 'update_submodules:pinned' , 'subgrunt:init' , 'clean:tmp' , 'default' ] ) ;
// ### Build assets
// `grunt build` - will build client assets (without updating the submodule)
//
// This task is identical to `grunt init`, except it does not build client dependencies
grunt . registerTask ( 'build' , 'Build client app' ,
[ 'subgrunt:init' , 'clean:tmp' , 'default' ] ) ;
// ### Default asset build
// `grunt` - default grunt task
//
// Build assets and dev version of the admin app.
grunt . registerTask ( 'default' , 'Build JS & templates for development' ,
[ 'subgrunt:dev' ] ) ;
// ### Production assets
// `grunt prod` - will build the minified assets used in production.
//
// It is otherwise the same as running `grunt`, but is only used when running Ghost in the `production` env.
grunt . registerTask ( 'prod' , 'Build JS & templates for production' ,
[ 'subgrunt:prod' , 'uglify:prod' , 'cssnano:prod' , 'master-warn' ] ) ;
// ### Live reload
// `grunt dev` - build assets on the fly whilst developing
//
// If you want Ghost to live reload for you whilst you're developing, you can do this by running `grunt dev`.
// This works hand-in-hand with the [livereload](http://livereload.com/) chrome extension.
//
// `grunt dev` manages starting an express server and restarting the server whenever core files change (which
// require a server restart for the changes to take effect) and also manage reloading the browser whenever
// frontend code changes.
//
// Note that the current implementation of watch only works with casper, not other themes.
grunt . registerTask ( 'dev' , 'Dev Mode; watch files and restart server on changes' , function ( ) {
if ( grunt . option ( 'client' ) ) {
grunt . task . run ( [ 'clean:built' , 'bgShell:client' ] ) ;
} else if ( grunt . option ( 'server' ) ) {
grunt . task . run ( [ 'express:dev' , 'watch' ] ) ;
} else {
grunt . task . run ( [ 'clean:built' , 'bgShell:client' , 'express:dev' , 'watch' ] ) ;
}
} ) ;
// ### grunt master
// This command helps you to bring your working directory back to current master.
// It will also update your dependencies to master and shows you if your database is healthy.
// It won't build the client!
//
// `grunt master` [`upstream` is the default upstream to pull from]
// `grunt master --upstream=parent`
grunt . registerTask ( 'master' , 'Update your current working folder to latest master.' ,
[ 'shell:master' , 'subgrunt:init' ]
) ;
// ### Release
// Run `grunt release` to create a Ghost release zip file.
// Uses the files specified by `.npmignore` to know what should and should not be included.
// Runs the asset generation tasks for production and duplicates default-prod.html to default.html
// so it can be run in either production or development, and packages all the files up into a zip.
grunt . registerTask ( 'release' ,
'Release task - creates a final built zip\n' +
' - Do our standard build steps \n' +
2019-03-15 13:28:22 +03:00
' - Run all tests(acceptance + regression + unit) \n' +
2019-02-11 15:26:06 +03:00
' - Copy files to release-folder/#/#{version} directory\n' +
' - Clean out unnecessary files (travis, .git*, etc)\n' +
' - Zip files in release-folder to dist-folder/#{version} directory' ,
function ( ) {
grunt . config . set ( 'copy.release' , {
expand : true ,
// #### Build File Patterns
// A list of files and patterns to include when creating a release zip.
// This is read from the `.npmignore` file and all patterns are inverted as the `.npmignore`
// file defines what to ignore, whereas we want to define what to include.
src : fs . readFileSync ( '.npmignore' , 'utf8' ) . split ( '\n' ) . filter ( Boolean ) . map ( function ( pattern ) {
return pattern [ 0 ] === '!' ? pattern . substr ( 1 ) : '!' + pattern ;
} ) ,
dest : '<%= paths.releaseBuild %>/'
2014-04-16 13:45:49 +04:00
} ) ;
2019-02-11 15:26:06 +03:00
grunt . config . set ( 'copy.admin_html' , {
files : [ {
cwd : '.' ,
src : 'core/server/web/admin/views/default-prod.html' ,
dest : 'core/server/web/admin/views/default.html'
} ]
} ) ;
2019-03-15 13:28:22 +03:00
if ( ! grunt . option ( 'skip-tests' ) ) {
grunt . task . run ( [ 'update_submodules:pinned' , 'subgrunt:init' , 'test-all' , 'clean:built' , 'clean:tmp' , 'prod' , 'clean:release' , 'copy:admin_html' , 'copy:release' , 'compress:release' ] ) ;
} else {
grunt . log . writeln ( chalk . red (
chalk . bold ( 'Skipping tests...' )
) ) ;
grunt . task . run ( [ 'update_submodules:pinned' , 'subgrunt:init' , 'clean:built' , 'clean:tmp' , 'prod' , 'clean:release' , 'copy:admin_html' , 'copy:release' , 'compress:release' ] ) ;
}
2019-02-11 15:26:06 +03:00
}
) ;
} ;
2013-06-25 15:43:15 +04:00
2014-06-02 18:55:49 +04:00
module . exports = configureGrunt ;