I am building a project right now that will be a webapp (browser runnable) and a Phonegap app (iOS & Android). Although my project, theoretically, could use the same dist
folder that my Yeoman generated, Grunt tasks build
production ready code by running grunt build
. I would like to run something like grunt build_web
, grunt build_ios
, and grunt build_android
, to build out production code for each platform individually. Or grunt build:web
, grunt build:ios
, grunt build:android
. This way, I could customize some of the loaded scripts, images, etc each with their own build directives.
So, should I go through my Gruntfile copying and pasting all of by (tried this, didn't work)dist
and build
directives responsibly? Or, is there a best practice for this?
Yeoman folks, is this possible?
Here is my current Gruntfile.js in case it would be useful to see.
'use strict';
var LIVERELOAD_PORT = 35729;
var lrSnippet = require('connect-livereload')({port: LIVERELOAD_PORT});
var mountFolder = function (connect, dir) {
return connect.static(require('path').resolve(dir));
};
// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'
module.exports = function (grunt) {
// show elapsed time at the end
require('time-grunt')(grunt);
// load all grunt tasks
require('load-grunt-tasks')(grunt);
// configurable paths
var yeomanConfig = {
app: 'app',
dist: '../www'
};
grunt.initConfig({
yeoman: yeomanConfig,
watch: {
coffee: {
files: ['<%= yeoman.app %>/scripts/{,*/}*.coffee'],
tasks: ['coffee:dist']
},
coffeeTest: {
files: ['test/spec/{,*/}*.coffee'],
tasks: ['coffee:test']
},
compass: {
files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
tasks: ['compass:server', 'autoprefixer']
},
styles: {
files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
tasks: ['copy:styles', 'autoprefixer']
},
livereload: {
options: {
livereload: LIVERELOAD_PORT
},
files: [
'<%= yeoman.app %>/*.html',
'.tmp/styles/{,*/}*.css',
'{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js',
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
}
},
connect: {
options: {
port: 9000,
// change this to '0.0.0.0' to access the server from outside
hostname: 'localhost'
},
livereload: {
options: {
middleware: function (connect) {
return [
lrSnippet,
mountFolder(connect, '.tmp'),
mountFolder(connect, yeomanConfig.app)
];
}
}
},
test: {
options: {
middleware: function (connect) {
return [
mountFolder(connect, '.tmp'),
mountFolder(connect, 'test'),
mountFolder(connect, yeomanConfig.app)
];
}
}
},
dist: {
options: {
middleware: function (connect) {
return [
mountFolder(connect, yeomanConfig.dist)
];
}
}
}
},
open: {
server: {
path: 'http://localhost:<%= connect.options.port %>'
}
},
clean: {
options: {
force: true
},
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/*',
'!<%= yeoman.dist %>/.git*'
]
}]
},
server: '.tmp'
},
jshint: {
options: {
jshintrc: '.jshintrc'
},
all: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js',
'!<%= yeoman.app %>/scripts/vendor/*',
'test/spec/{,*/}*.js'
]
},
mocha: {
all: {
options: {
run: true,
urls: ['http://localhost:<%= connect.options.port %>/index.html']
}
}
},
coffee: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/scripts',
src: '{,*/}*.coffee',
dest: '.tmp/scripts',
ext: '.js'
}]
},
test: {
files: [{
expand: true,
cwd: 'test/spec',
src: '{,*/}*.coffee',
dest: '.tmp/spec',
ext: '.js'
}]
}
},
compass: {
options: {
sassDir: '<%= yeoman.app %>/styles',
cssDir: '.tmp/styles',
generatedImagesDir: '.tmp/images/generated',
imagesDir: '<%= yeoman.app %>/images',
javascriptsDir: '<%= yeoman.app %>/scripts',
fontsDir: '<%= yeoman.app %>/styles/fonts',
importPath: '<%= yeoman.app %>/bower_components',
httpImagesPath: '/images',
httpGeneratedImagesPath: '/images/generated',
httpFontsPath: '/styles/fonts',
relativeAssets: false
},
dist: {
options: {
generatedImagesDir: '<%= yeoman.dist %>/images/generated'
}
},
server: {
options: {
debugInfo: true
}
}
},
autoprefixer: {
options: {
browsers: ['last 1 version']
},
dist: {
files: [{
expand: true,
cwd: '.tmp/styles/',
src: '{,*/}*.css',
dest: '.tmp/styles/'
}]
}
},
// not used since Uglify task does concat,
// but still available if needed
/*concat: {
dist: {}
},*/
requirejs: {
dist: {
// Options: https://github.com/jrburke/r.js/blob/master/build/example.build.js
options: {
// `name` and `out` is set by grunt-usemin
baseUrl: yeomanConfig.app + '/scripts',
optimize: 'none',
// TODO: Figure out how to make sourcemaps work with grunt-usemin
// https://github.com/yeoman/grunt-usemin/issues/30
//generateSourceMaps: true,
// required to support SourceMaps
// http://requirejs.org/docs/errors.html#sourcemapcomments
preserveLicenseComments: false,
useStrict: true,
wrap: true
//uglify2: {} // https://github.com/mishoo/UglifyJS2
}
}
},
rev: {
dist: {
files: {
src: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp}',
'<%= yeoman.dist %>/styles/fonts/{,*/}*.*'
]
}
}
},
useminPrepare: {
options: {
dest: '<%= yeoman.dist %>'
},
html: '<%= yeoman.app %>/index.html'
},
usemin: {
options: {
dirs: ['<%= yeoman.dist %>']
},
html: ['<%= yeoman.dist %>/{,*/}*.html'],
css: ['<%= yeoman.dist %>/styles/{,*/}*.css']
},
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.{png,jpg,jpeg}',
dest: '<%= yeoman.dist %>/images'
}]
}
},
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.svg',
dest: '<%= yeoman.dist %>/images'
}]
}
},
cssmin: {
// This task is pre-configured if you do not wish to use Usemin
// blocks for your CSS. By default, the Usemin block from your
// `index.html` will take care of minification, e.g.
//
// <!-- build:css({.tmp,app}) styles/main.css -->
//
// dist: {
// files: {
// '<%= yeoman.dist %>/styles/main.css': [
// '.tmp/styles/{,*/}*.css',
// '<%= yeoman.app %>/styles/{,*/}*.css'
// ]
// }
// }
},
htmlmin: {
dist: {
options: {
/*removeCommentsFromCDATA: true,
// https://github.com/yeoman/grunt-usemin/issues/44
//collapseWhitespace: true,
collapseBooleanAttributes: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeOptionalTags: true*/
},
files: [{
expand: true,
cwd: '<%= yeoman.app %>',
src: '*.html',
dest: '<%= yeoman.dist %>'
}]
}
},
// Put files not handled in other tasks here
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= yeoman.app %>',
dest: '<%= yeoman.dist %>',
src: [
'*.{ico,png,txt}',
'.htaccess',
'images/{,*/}*.{webp,gif}',
'styles/fonts/{,*/}*.*'
]
}]
},
styles: {
expand: true,
dot: true,
cwd: '<%= yeoman.app %>/styles',
dest: '.tmp/styles/',
src: '{,*/}*.css'
}
},
modernizr: {
devFile: '<%= yeoman.app %>/bower_components/modernizr/modernizr.js',
outputFile: '<%= yeoman.dist %>/bower_components/modernizr/modernizr.js',
files: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'!<%= yeoman.dist %>/scripts/vendor/*'
],
uglify: true
},
concurrent: {
server: [
'compass',
'coffee:dist',
'copy:styles'
],
test: [
'coffee',
'copy:styles'
],
dist: [
'coffee',
'compass',
'copy:styles',
'imagemin',
'svgmin',
'htmlmin'
]
},
bower: {
options: {
exclude: ['modernizr']
},
all: {
rjsConfig: '<%= yeoman.app %>/scripts/main.js'
}
}
});
grunt.registerTask('server', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'open', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'open',
'watch'
]);
});
grunt.registerTask('test', [
'clean:server',
'concurrent:test',
'autoprefixer',
'connect:test',
'mocha'
]);
grunt.registerTask('build', [
'clean:dist',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'requirejs',
'concat',
'cssmin',
'uglify',
'modernizr',
'copy:dist',
'rev',
'usemin'
]);
grunt.registerTask('default', [
'jshint',
'test',
'build'
]);
};
There's really no best practice out there beside what's recommended for PG 3. For multiple clients, you may want to modify your gruntjs. This is my project structure, where assets folder contain data for different clients:
And here is my gruntjs file. It use ImageMagik to create all the necessary launcher icons. Build and deploy for over the air installation. You get the idea...
/***
* Package script for MobileStore
***/
'use strict';
// require package.json:
/*
{
"name": "MobileStore",
"version": "1.0.1",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-jshint": "~0.6.0",
"grunt-contrib-clean": "~0.4.1",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-exec": "~0.4.2",
"grunt-string-replace": "~0.2.4",
"grunt-image-resize": "~0.2.0"
}
}
*/
module.exports = function(grunt) {
var isMacOS = grunt.file.isDir('/etc');
// load all grunt tasks
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
// configurable paths
var myConfig = {
dest: 'out/',
platforms: ['ios', 'android'],
chains: [11,52,75,119,129,224,229,235],
chainConfig: {},
baseDir: __dirname + '/'
};
var chainid = grunt.option('chainid');
if (chainid) {
myConfig.chains = [chainid];
}
// set up grunt
var gruntConfig = {
clean: {
build: ['out', 'beta-assets']
},
copy: {},
exec: {},
"string-replace": {},
image_resize: {}
};
// first thing to do is clean
var packageConfig = ['clean'];
// duplicate source to the chain
// copy from platforms/(android/ios) to out/chainid/(android/ios)
myConfig.chains.forEach(function(chain) {
myConfig.platforms.forEach(function(platform) {
var key = 'setup_' + platform + chain;
// any platform is fine, we're just preping chainConfig
if (platform == 'android')
{
myConfig.chainConfig[chain] = require('./YourCompany.Mobile/assets/' + chain + '/build.json');
}
gruntConfig.copy[key] = {
files: [
{
expand: true,
cwd: 'YourCompany.Mobile/platforms/' + platform + '/',
src: ['./**'],
dest: 'out/' + chain + '/' + platform + '/'
}
]
};
packageConfig.push('copy:' + key);
});
});
// override the device resource (icon, splash, etc..)
// copy from assets/chainid/device/(android/ios) to out/chainid/(android/ios)
myConfig.chains.forEach(function(chain) {
myConfig.platforms.forEach(function(platform) {
var key = 'device_resource_' + platform + chain;
var srcArt = 'YourCompany.Mobile/assets/' + chain + '/device/AndroidArtwork.png';
var dest = 'out/' + chain + '/' + platform + '/MobileStore/';
if (platform == 'android')
{
dest += 'res/';
}
else if (platform == 'ios') {
dest += 'MobileStore/';
}
gruntConfig.copy[key] = {
files: [
{
expand: true,
cwd: 'YourCompany.Mobile/assets/' + chain + '/device/' + platform + '/',
src: ['./**'],
dest: dest
}
]
};
packageConfig.push('copy:' + key);
// do image manipulations
// Use AnroidArtwork.png
if (platform == 'android') {
gruntConfig.image_resize[key + '_36'] = {
options: {
width: 36,
height: 36,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'drawable-ldpi/icon.png'
}]
};
packageConfig.push('image_resize:' + key + '_36');
gruntConfig.image_resize[key + '_48'] = {
options: {
width: 48,
height: 48,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'drawable-mdpi/icon.png'
}]
};
packageConfig.push('image_resize:' + key + '_48');
gruntConfig.image_resize[key + '_72'] = {
options: {
width: 72,
height: 72,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'drawable-hdpi/icon.png'
}]
};
packageConfig.push('image_resize:' + key + '_72');
gruntConfig.image_resize[key + '_96'] = {
options: {
width: 96,
height: 96,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'drawable-xhdpi/icon.png'
}]
};
packageConfig.push('image_resize:' + key + '_96');
gruntConfig.image_resize[key + '_default'] = {
options: {
width: 96,
height: 96,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'drawable/icon.png'
}]
};
packageConfig.push('image_resize:' + key + '_default');
}
else if (platform == 'ios') {
gruntConfig.image_resize[key + '_57'] = {
options: {
width: 57,
height: 57,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/Icon.png'
}]
};
packageConfig.push('image_resize:' + key + '_57');
gruntConfig.image_resize[key + '_114'] = {
options: {
width: 114,
height: 114,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/[email protected]'
}]
};
packageConfig.push('image_resize:' + key + '_114');
gruntConfig.image_resize[key + '_72'] = {
options: {
width: 72,
height: 72,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/Icon-72.png'
}]
};
packageConfig.push('image_resize:' + key + '_72');
gruntConfig.image_resize[key + '_144'] = {
options: {
width: 144,
height: 144,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/[email protected]'
}]
};
packageConfig.push('image_resize:' + key + '_144');
gruntConfig.image_resize[key + '_29'] = {
options: {
width: 29,
height: 29,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/Icon-Small.png'
}]
};
packageConfig.push('image_resize:' + key + '_29');
gruntConfig.image_resize[key + '_58'] = {
options: {
width: 58,
height: 58,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/[email protected]'
}]
};
packageConfig.push('image_resize:' + key + '_58');
gruntConfig.image_resize[key + '_50'] = {
options: {
width: 50,
height: 50,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/Icon-Small-50.png'
}]
};
packageConfig.push('image_resize:' + key + '_50');
gruntConfig.image_resize[key + '_100'] = {
options: {
width: 100,
height: 100,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/icons/[email protected]'
}]
};
packageConfig.push('image_resize:' + key + '_100');
gruntConfig.image_resize[key + '_1024'] = {
options: {
width: 1024,
height: 1024,
overwrite: true,
upscale: true
},
files: [{
src: srcArt,
dest: dest + 'Resources/iTunesArtwork'
}]
};
packageConfig.push('image_resize:' + key + '_1024');
// setup images for ota deploy
gruntConfig.image_resize[key + '_512x'] = {
options: {
width: 512,
height: 512,
overwrite: true,
upscale: true
},
files: [{
src: srcArt,
dest: dest + '../iTunesArtwork.png'
}]
};
packageConfig.push('image_resize:' + key + '_512x');
gruntConfig.image_resize[key + '_57x'] = {
options: {
width: 57,
height: 57,
overwrite: true
},
files: [{
src: srcArt,
dest: dest + '../Icon.png'
}]
};
packageConfig.push('image_resize:' + key + '_57x');
}
});
});
// copy assets to www folder
// copy from www to out/chainid/...
myConfig.chains.forEach(function(chain) {
myConfig.platforms.forEach(function(platform) {
var key = 'content_' + platform + chain;
var dest = 'out/' + chain + '/' + platform + '/MobileStore/';
if (platform == 'android')
{
dest += 'assets/www/';
}
else if (platform == 'ios') {
dest += 'www/';
}
gruntConfig.copy[key] = {
files: [
{
expand: true,
cwd: 'YourCompany.Mobile/www/',
src: ['./**'],
dest: dest
}
]
};
packageConfig.push('copy:' + key);
});
});
// copy assets override
// copy from assets/chainid/www to out/chainid/...
myConfig.chains.forEach(function(chain) {
myConfig.platforms.forEach(function(platform) {
var key = 'content_override_' + platform + chain;
var dest = 'out/' + chain + '/' + platform + '/MobileStore/';
if (platform == 'android')
{
dest += 'assets/www/';
}
else if (platform == 'ios') {
dest += 'www/';
}
gruntConfig.copy[key] = {
files: [
{
expand: true,
cwd: 'YourCompany.Mobile/assets/' + chain + '/www/',
src: ['./**'],
dest: dest
}
]
};
packageConfig.push('copy:' + key);
});
});
// copy phonegap merges
// copy from merges to out/chainid/...
myConfig.chains.forEach(function(chain) {
myConfig.platforms.forEach(function(platform) {
var key = 'phonegap_override_' + platform + chain;
var dest = 'out/' + chain + '/' + platform + '/MobileStore/';
if (platform == 'android')
{
dest += 'assets/www/';
}
else if (platform == 'ios') {
dest += 'www/';
}
gruntConfig.copy[key] = {
files: [
{
expand: true,
cwd: 'YourCompany.Mobile/merges/' + platform,
src: ['./**'],
dest: dest
}
]
};
packageConfig.push('copy:' + key);
});
});
// doing beta assets deployment
myConfig.chains.forEach(function (chain) {
var key = 'content_beta_assets_' + chain;
var src = 'out/' + chain + '/ios/MobileStore/www/';
gruntConfig.copy[key] = {
files: [
{
expand: true,
cwd: src,
src: ['./**'],
dest: 'beta-assets/' + chain + '/www/'
}
]
};
packageConfig.push('copy:' + key);
// override cordova file
gruntConfig.copy[key + '_pg'] = {
files: [
{
expand: true,
cwd: 'YourCompany.Mobile/www/',
src: ['./cordova.js'],
dest: 'beta-assets/' + chain + '/www/'
}
]
};
packageConfig.push('copy:' + key + '_pg');
});
// doing android(ant) build
myConfig.chains.forEach(function (chain) {
var key = 'android_build_' + chain;
var dest = 'out/' + chain + '/android/MobileStore/';
var destFolder = './out/' + chain + '/';
// use previous chainConfig to perform text replace
gruntConfig["string-replace"][key + '_prep'] = {
files: [
{
expand: true,
cwd: 'out/' + chain + '/',
src: ['./**/**.java', './**/**.xml', './**/**.plist', './**/**.m'],
dest: 'out/' + chain + '/'
}
],
options: {
replacements: [
{
pattern: /(net.yourcompany.MobileStore)+/ig,
replacement: myConfig.chainConfig[chain].id
},
{
pattern: /(AppleBundleSeedID)+/ig,
replacement: myConfig.chainConfig[chain].AppleBundleSeedID
},
{ pattern: '<string name="app_name">MobileStore</string>',
replacement: '<string name="app_name">' + myConfig.chainConfig[chain].ApplicationName + '</string>'
}
]
}
};
packageConfig.push('string-replace:' + key + '_prep');
// do build
gruntConfig.exec[key] = {
cmd: 'ant clean && ant debug',
cwd: dest,
env: process.env
};
packageConfig.push('exec:' + key);
});
// doing xcode build
myConfig.chains.forEach(function(chain) {
var key = 'ios_build_' + chain;
var chainConfig = myConfig.chainConfig[chain];
var dest = myConfig.baseDir + 'out/' + chain + '/ios/MobileStore/';
var buildDir = dest + 'Build/Products/Release-iphoneos/';
var commandStart = 'export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/opt/local/bin:/opt/local/sbin:/sbin:/usr/local/bin:/Users/Shared/ImageMagick-6.8.6:$PATH" &&'
var command = 'chmod 777 dobuild && ./dobuild "' + chainConfig.AppleProductName + '" ' + chain + ' ' + process.env.BUILD_NUMBER + ' "' + chainConfig.AppleBundleSeedID + '.' + chainConfig.id + '"';
grunt.log.debug('ios build:' + command);
if (isMacOS) {
gruntConfig.exec[key] = {
cmd: command,
cwd: dest,
exitCode: 0
};
packageConfig.push('exec:' + key);
}
// prep apk for deployment
gruntConfig.copy['android_build_' + chain + '_apk'] = {
options: {
processContent: false
},
files: [
{
expand: true,
flatten: true,
src: ['out/' + chain + '/android/MobileStore/bin/MobileStore-debug.apk'],
dest: 'beta-assets/' + chain + '/',
filter: 'isFile'
}
]
};
packageConfig.push('copy:' + 'android_build_' + chain + '_apk');
// prep ipa for ios deploy
gruntConfig.copy['ios_build_' + chain + '_ipa'] = {
options: {
processContent: false
},
files: [
{
expand: true,
flatten: true,
src: ['out/' + chain + '/ios/MobileStore/Build/**.ipa'],
dest: 'beta-assets/' + chain + '/',
filter: 'isFile'
}
]
};
packageConfig.push('copy:' + 'ios_build_' + chain + '_ipa');
// drop plist
gruntConfig.copy['ios_build_' + chain + '_plist'] = {
options: {
processContent: false
},
files: [
{
expand: true,
flatten: true,
src: ['out/' + chain + '/ios/MobileStore/Build/**.plist'],
dest: 'beta-assets/' + chain + '/',
filter: 'isFile'
}
]
};
packageConfig.push('copy:' + 'ios_build_' + chain + '_plist');
// drop html
gruntConfig.copy['ios_build_' + chain + '_html'] = {
options: {
processContent: false
},
files: [
{
expand: true,
flatten: true,
src: ['out/' + chain + '/ios/MobileStore/Build/install.html'],
dest: 'beta-assets/' + chain + '/',
filter: 'isFile'
}
]
};
packageConfig.push('copy:' + 'ios_build_' + chain + '_html');
// drop png
gruntConfig.copy['ios_build_' + chain + '_png'] = {
options: {
processContent: false
},
files: [
{
expand: true,
flatten: true,
src: ['out/' + chain + '/ios/MobileStore/*.png'],
dest: 'beta-assets/' + chain + '/',
filter: 'isFile'
}
]
};
packageConfig.push('copy:' + 'ios_build_' + chain + '_png');
});
grunt.initConfig(gruntConfig);
grunt.registerTask('package', packageConfig);
// Default task.
grunt.registerTask('default', 'package');
};
Basically, it drop all the native and merges into out/clientorchainid folder to do the build for each client. Then it copy the build result into beta-assets/clientorchainid folder to prep for remote deployment.
Build is a list of other tasks. You could do something like this (coffescript):
grunt.registerTask "build", [
"clean:dist",
"jade:html",
"clientTemplates",
"useminPrepare",
"concurrent:dist",
"copy:prerequire",
"requirejs",
"cssmin",
"concat",
"uglify",
"copy:dist",
"rev",
"usemin"
]
That way you can deifne custom tasks and custom builds, etc.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With