Why is it recommended to use concat then uglify when the latter can do both? - gruntjs

I keep seeing the recommendation for making JS files ready for production to be concat then uglify.
For example here, in on of Yeoman's grunt tasks.
By default the flow is: concat -> uglifyjs.
Considering UglifyJS can do both concatenation and minification, why would you ever need both at the same time?
Thanks.

Running a basic test to see if there is a performance difference between executing concat and then uglify vs. just uglify.
package.json
{
"name": "grunt-concat-vs-uglify",
"version": "0.0.1",
"description": "A basic test to see if we can ditch concat and use only uglify for JS files.",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-uglify": "^0.6.0",
"load-grunt-tasks": "^1.0.0",
"time-grunt": "^1.0.0"
}
}
Gruntfile.js
module.exports = function (grunt) {
// Display the elapsed execution time of grunt tasks
require('time-grunt')(grunt);
// Load all grunt-* packages from package.json
require('load-grunt-tasks')(grunt);
grunt.initConfig({
paths: {
src: {
js: 'src/**/*.js'
},
dest: {
js: 'dist/main.js',
jsMin: 'dist/main.min.js'
}
},
concat: {
js: {
options: {
separator: ';'
},
src: '<%= paths.src.js %>',
dest: '<%= paths.dest.js %>'
}
},
uglify: {
options: {
compress: true,
mangle: true,
sourceMap: true
},
target: {
src: '<%= paths.src.js %>',
dest: '<%= paths.dest.jsMin %>'
}
}
});
grunt.registerTask('default', 'concat vs. uglify', function (concat) {
// grunt default:true
if (concat) {
// Update the uglify dest to be the result of concat
var dest = grunt.config('concat.js.dest');
grunt.config('uglify.target.src', dest);
grunt.task.run('concat');
}
// grunt default
grunt.task.run('uglify');
});
};
In src, I've put a bunch of JS files, including the uncompressed source of jQuery, copied several times, spread around into subfolders. Much more than what a normal site/app usually has.
Turns out the time it takes to concat and compress all of these files is essentially the same in both scenarios.
Except when using the sourceMap: true option on concat as well (see below).
On my computer:
grunt default : 6.2s (just uglify)
grunt default:true : 6s (concat and uglify)
It's worth noting that the resulting main.min.js is the same in both cases.
Also, uglify automatically takes care of using the proper separator when combining the files.
The only case where it does matter is when adding sourceMap: true to the concat options.
This creates a main.js.map file next to main.js, and results in:
grunt default : 6.2s (just uglify)
grunt default:true : 13s (concat and uglify)
But if the production site loads only the min version, this option is useless.
I did found a major disadvantage with using concat before uglify.
When an error occurs in one of the JS files, the sourcemap will link to the concatenated main.js file and not the original file. Whereas when uglify does the whole work, it will link to the original file.
Update:
We can add 2 more options to uglify that will link the uglify sourcemap to concat sourcemap, thus handling the "disadvantage" I mentioned above.
uglify: {
options: {
compress: true,
mangle: true,
sourceMap: true,
sourceMapIncludeSources: true,
sourceMapIn: '<%= paths.dest.js %>.map',
},
target: {
src: '<%= paths.src.js %>',
dest: '<%= paths.dest.jsMin %>'
}
}
But it seems highly unnecessary.
Conclusion
I think it's safe to conclude that we can ditch concat for JS files if we're using uglify, and use it for other purposes, when needed.

In the example you mention, which I'm quoting below, the files are first concatenated with concat and then uglified/minified by uglify:
{
concat: {
'.tmp/concat/js/app.js': [
'app/js/app.js',
'app/js/controllers/thing-controller.js',
'app/js/models/thing-model.js',
'app/js/views/thing-view.js'
]
},
uglifyjs: {
'dist/js/app.js': ['.tmp/concat/js/app.js']
}
}
The same could be achieved with:
{
uglifyjs: {
'dist/js/app.js': [
'app/js/app.js',
'app/js/controllers/thing-controller.js',
'app/js/models/thing-model.js',
'app/js/views/thing-view.js'
]
}
}
Typically, the task clean would then run after tasks that write to a temporary folder (in this example concat) and delete whatever content is in that folder. Some people also like to run clean before tasks like compass, to delete things like randomly named image sprites (which are newly generated every time the task runs). This would keep wheels turning even for the most paranoid.
This is all a matter of preference and workflow, as is with when to run jshint. Some people like to run it before the compilation, others prefer to run it on compiled files.
Complex projects with an incredible amount of JavaScript files - or with a increasingly broad number of peers & contributors, might choose to concatenate files outside uglify just to keep things more readable and maintainable. I think this was the reasoning behind Yeoman's choice of transformation flow.
uglify can be notoriously slow depending of the project's configuration, so there might be some small gain in concatenating it with concat first - but that would have to be confirmed.
concat also supports separators, which uglify doesn't as far as README.md files are concerned.
concat: {
options: {
separator: ';',
}
}

Related

How do I generate sourcemaps for Uglified files using Grunt?

I have a Grunt project that uses both Browserify and Uglify. Here are the core bits of it:
browserify: {
myapp: {
options: {
transform: ['babelify'],
browserifyOptions: {
debug: true
},
},
src: 'src/index.js',
dest: 'build/myapp.js'
}
},
uglify: {
options: {
sourceMap: true,
banner: bannerContent
},
target: {
src: 'build/myapp.js',
dest: 'build/myapp.min.js'
}
},
It seems to generate a myapp.min.js.map file but it no longer has the raw sources in the source-map that existed prior to the Browserification.
Here's what the resultant source-map file contains:
{
"version":3,
"sources":[
"myapp.js"
],
"names":[
...
...
...
],
"mappings":".........",
"file":"myapp.min.js"
}
I've tried using the uglifyify transform for Browserify but that does not seem to generate as small files as the Uglify task.
I've also bumped all my dependencies to the latest but I haven't been able to resolve this issue.
grunt-contrib-uglify has a sourceMapIn option that allows you to specify the location of an input source map file from an earlier compilation - which in your scenario is the browserify task.
However, whilst setting browserifyOptions: { debug: true } in your browserify task does generate an inline source map in the resultant .js file (i.e. in build/myapp.js), the crux of the problem is twofold:
We don't have an external source map file that we can configure the sourceMapIn option of the subsequent grunt-contrib-uglify task to utilize.
grunt-browserify doesn't provide a feature to create an external .map file, it only creates them inline (see here)
To address the aforementioned issue consider utilizing grunt-extract-sourcemap to extract the inline source map from build/myapp.js (i.e. from the output file generated by your browserify task) after it has been produced.
Gruntfile.js
The following gist shows how your Gruntfile.js should be configured:
module.exports = function (grunt) {
grunt.initConfig({
browserify: {
myapp: {
options: {
transform: ['babelify'],
browserifyOptions: {
debug: true
},
},
src: 'src/index.js',
dest: 'build/myapp.js'
}
},
extract_sourcemap: {
myapp: {
files: {
'build': ['build/myapp.js']
}
}
},
uglify: {
options: {
sourceMap: true,
sourceMapIn: 'build/myapp.js.map'
},
target: {
src: 'build/myapp.js',
dest: 'build/myapp.min.js'
}
}
});
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-extract-sourcemap');
grunt.loadNpmTasks('grunt-contrib-uglify');
// Note the order of the tasks in your task list is important.
grunt.registerTask('default', ['browserify', 'extract_sourcemap', 'uglify']);
};
Explanation
First the browserify task is invoked which outputs a new file (i.e. build/myapp.js) containing your bundled JavaScript and an "inlined" source map info. If you were to inspect the content of build/myapp.js at this stage it includes something like the following at the end:
//# sourceMappingURL=data:application/json;charset=utf-8;base64, ...
Next the extract_sourcemap task is invoked. This essentially extracts the "inlined" source map info from build/myapp.js and writes it to a new file named myapp.js.map which is saved in your build directory.
The original "inlined" source map info in build/myapp.js is replaced with a link to the newly generated source map file, i.e. myapp.js.map. If you inspect the content of build/myapp.js you'll now notice the following at the end of the file instead:
//# sourceMappingURL=myapp.js.map
Lastly the uglify task is invoked. Notice how its sourceMapIn option is configured to read build/myapp.js.map, i.e the source map file we generated at step 2.
This task creates your desired build/myapp.min.js file containing; your minified JS, and a link to a newly generated source map file build/myapp.min.js.map.
Note The final resultant file (i.e. build/myapp.min.js) now correctly maps back to the original src/index.js file and any file(s) that index.js itself may have import'ed or require()'d

Specifying Grunt output to a dynamic parallel folder

The Context
I'm new to Grunt and am trying to learn a bit by modifying the meanjs boilerplate to support stylus, I would like to keep my precompiled css assets organized in modular buckets, as recommended by the current meanjs defaults.
The Question
I have the following file structure:
- app
- config
- public
- modules
- foo
- assets
- stylesheets
...
- css
...
...
How can I use Grunt to take Stylus .styl files in the public/modules/*/assets/stylesheets directory, and have them compile to the public/modules/*/css directory?
Naive Attempt:
Below is an example attempt, which didn't get very far.
stylus: {
compile: {
files: [{
dest: '../../css',
src: 'public/modules/*/assets/stylesheets/*.styl',
ext: '.css',
expand: true
}]
}
}
This results in: File ../../css/public/modules/foo/assets/stylesheets/baz.css created.
If I leave "dest" empty, it does properly compile but the output is in the assets/stylesheets folder (as expected). I'm sure there is a clean way to do this, but I don't know yet.
setting the src, dest, cwd, as well as using the hidden rename options of grunt should get stylus files in your desired format.
example:
stylus: {
compile: {
options: {
compress: true
},
files: [{
cwd: 'public/modules',
dest: 'public/modules',
src: ['*/assets/stylesheets/*.styl'],
expand: true,
rename: function(dest, src) {
var path = require('path');
var module = src.split(path.sep).slice(0,1)[0];
return path.join(dest, module + '/css/' + module + '.css');
}
}]
}
},
grunt tricks - customize file output rename

Grunt Compass for multiple SASS files for different sites

I have lots of different partials and sass files to generate 11 individual website specific style sheets so if I make a changes in a partial that is being used in all 11 style sheets then I have to wait for grunt to compile all these before I can refresh my browser and see the change, one workaround I have is to use the specify option and change the site ID depending on which site I am working on -
compass: {
dev: {
options: {
sassDir: "assets/sass",
specify: "assets/sass/site_##.scss",
cssDir: "assets/styles",
outputStyle: "expanded",
noLineComments: false,
sourcemap: true
}
}
},
watch: {
css: {
files: 'assets/sass/**/*',
tasks: 'compass',
},
},
Is there a way I could make this dynamic in the watch task, i.e. using an ID appended to the body or something?
My partials -
_reset
_grid
_layout
_variables
_mixins
_brand1
_brand2
_brand3
_summer
_winter
_site_1_specific
_site_2_specific
_site_3_specific
_site_4_specific
_site_5_specific
_site_6_specific
_site_7_specific
_site_7_specific
_site_9_specific
_site_10_specific
_site_11_specific
I then have 11 SCSS files importing a combination of the above partials to make the final style sheets.
You can use grunt-newer, that helps you to execute the compass task only in the file that is changed:
https://github.com/tschaub/grunt-newer
npm install grunt-newer --save-dev
grunt.loadNpmTasks('grunt-newer');
Then, you have to change your watch task:
watch: {
css: {
files: '<%= tui.sass %>/**/*',
tasks: ['newer:compass']
},
},
Hope it helps.
Regards.

Grunt-watch livereload does not work with compiled files (stylus, jade, etc.)

My livereload is "working" in that it live reloads the page when a file is changed, but only if I modify the file directly. If the file is generated through the grunt stylus or jade compiler, nothing happens.
When I look at grunt with verbose turned on, the Live reloading root.css... line appears only if I save root.css directly. If root.css is modified through the stylus compiler, the line does not appear. It's as if watch doesn't detect the file has been changed if it's changed through a compiler. This same issue occurs with jade as well.
Here's my stylus task (trimmed):
stylus: {
options: {
compress: false,
use: [
require('autoprefixer-stylus')
]
},
src: [
'app/styl/**/*.styl'
],
dest: 'build/css/root.css'
}
And here's my watch task (trimmed):
livereload: {
options: {
livereload: 1337,
},
files: 'build/**/*'
},
stylus: {
files: [
'app/styl/**/*.styl',
],
tasks: ['stylus:dev']
},
I really hope I'm just doing something stupid. I can't find any problems similar to this one.
EDIT:
Just in case this helps, I discovered that by changing my grunt task from running ['clean','jade:dev', 'stylus:dev', 'connect:dev', 'watch'] to only running ['connect:dev', 'watch'], livereload works as intended once, then never again. (Modifying the css directly still works though.)
EDIT 2:
I was able to fix this by adding livereload to each specific task in watch, like so:
livereload: {
options: {
livereload: 1337,
},
files: 'build/**/*'
},
stylus: {
files: [
'app/styl/**/*.styl',
],
tasks: ['stylus:dev'],
options: {
livereload: 1337
}
},
As to why this works, I have no idea. If anyone can shed some light on this, it'd be much appreciated. Though to be honest, I don't know why I didn't try this earlier.

Grunt recursive copy

I'm setting up a Grunt script that needs to copy and reorganise directories of images from A to B. Simple enough.
Directory structure:
components
componentA
js
img
imgfolderA
imgfolderB
css
componentB
js
img
imgfolderA
Each img directory could contain other directories and directories within those directories to help organise the images.
I want to use Grunt to take all those images and put them under one directory (assets/img):
assets
img
dirA
imgfolderA
imgfolderB
dirB
imgfolderA
Any ideas on how could I do this in grunt without specifying each component directory (it needs to be fully automated)?
know it's a bit late but this should do the job, use 'grunt-contrib-copy' like so
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
copy: {
production: {
files: [{
expand: true,
cwd: "componentA/img/imgfolderA/",
src: ["*.*", "**/*.*"],
dest: "./assets/img/dirA/",
},
{
expand: true,
cwd: "componentB/img/imgfolderB/",
src: ["*.*", "**/*.*"],
dest: "./assets/img/dirB/",
},
]
}
}
});
// Production Build Tools
grunt.loadNpmTasks('grunt-contrib-copy');
// Default Production Build task(s).
grunt.registerTask('default', ['copy']);
};
ps magic is in the files objects, there not very well documented, but the documentation is here, after one or two reads it makes sense honest!
grunt-contrib-copy setup: https://github.com/gruntjs/grunt-contrib-copy (the readme at the bottom)
files object setup: http://gruntjs.com/configuring-tasks#globbing-patterns
task setup: http://gruntjs.com/configuring-tasks
This is rather simple using grunt.file.expand.
Just pass matching glob patterns (e.g. **/img/**), and then recurse on the returned matching file values to copy.

Resources