🎯 Ejemplos recomendados
Balanced sample collections from various categories for you to explore
Herramienta de Construcción Grunt
Ejemplos del task runner de JavaScript Grunt incluyendo automatización, procesos de construcción y flujos de despliegue
💻 Tareas Básicas de Grunt javascript
🟢 simple
⭐⭐
Tareas esenciales de Grunt para desarrollo web moderno incluyendo procesamiento de archivos, observación y construcción
⏱️ 20 min
🏷️ grunt, automation, build
Prerequisites:
Node.js knowledge, JavaScript basics
// Grunt Basic Tasks Setup
// Modern Grunt configuration for web development workflows
// ===== package.json =====
{
"name": "my-grunt-project",
"version": "1.0.0",
"description": "Modern web application using Grunt for task automation",
"main": "index.js",
"scripts": {
"dev": "grunt dev",
"build": "grunt build",
"watch": "grunt watch",
"clean": "grunt clean",
"serve": "grunt serve",
"test": "grunt test",
"lint": "grunt lint",
"deploy": "grunt deploy"
},
"devDependencies": {
"grunt": "^1.6.1",
"grunt-cli": "^1.4.3",
"grunt-contrib-sass": "^2.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-contrib-clean": "^2.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-concat": "^2.1.0",
"grunt-contrib-uglify": "^5.2.2",
"grunt-contrib-cssmin": "^4.0.0",
"grunt-contrib-imagemin": "^4.0.0",
"grunt-contrib-htmlmin": "^3.1.0",
"grunt-contrib-compress": "^2.0.0",
"grunt-browser-sync": "^2.2.0",
"grunt-eslint": "^24.3.0",
"grunt-stylelint": "^0.18.0",
"grunt-postcss": "^0.9.0",
"grunt-autoprefixer": "^4.0.0",
"grunt-sass-lint": "^0.2.4",
"grunt-babel": "^8.0.0",
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/preset-react": "^7.18.0",
"grunt-filerev": "^4.0.0",
"grunt-usemin": "^3.1.1",
"grunt-replace": "^2.0.2",
"grunt-newer": "^1.3.0",
"grunt-notify": "^0.4.5",
"grunt-shell": "^4.0.0",
"grunt-run": "^0.8.1",
"grunt-string-replace": "^1.3.1",
"grunt-banner": "^0.6.7",
"grunt-text-replace": "^0.4.0",
"load-grunt-tasks": "^5.1.0",
"time-grunt": "^2.0.0",
"jit-grunt": "^0.10.0",
"dotenv": "^16.0.0"
}
}
// ===== Gruntfile.js =====
module.exports = function(grunt) {
'use strict';
// Load environment variables
require('dotenv').config();
// Measure build time
require('time-grunt')(grunt);
// Load grunt tasks automatically
require('jit-grunt')(grunt, {
'babel': 'grunt-babel',
'stylelint': 'grunt-stylelint',
'sasslint': 'grunt-sass-lint'
});
// Project configuration
grunt.initConfig({
// Project settings
pkg: grunt.file.readJSON('package.json'),
// Environment settings
env: process.env,
// Directory structure
dirs: {
src: 'src',
dist: 'dist',
tmp: '.tmp',
css: 'src/scss',
js: 'src/js',
images: 'src/images',
fonts: 'src/fonts',
vendor: 'src/vendor'
},
// Clean task
clean: {
dist: {
src: ['<%= dirs.dist %>/**/*', '!<%= dirs.dist %>/.gitkeep']
},
tmp: {
src: ['<%= dirs.tmp %>/**/*']
},
server: {
src: ['.sass-cache']
}
},
// Copy task
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= dirs.src %>',
dest: '<%= dirs.dist %>',
src: [
'*.{ico,png,txt}',
'.htaccess',
'images/{,*/}*.{webp,gif}',
'fonts/**/*',
'vendor/**/*'
]
}]
},
styles: {
expand: true,
dot: true,
cwd: '<%= dirs.src %>',
dest: '<%= dirs.dist %>',
src: 'css/**/*.css'
}
},
// Sass/SCSS compilation
sass: {
options: {
implementation: require('sass'),
sourceMap: true,
outputStyle: 'expanded'
},
dist: {
files: {
'<%= dirs.tmp %>/css/main.css': '<%= dirs.css %>/main.scss',
'<%= dirs.tmp %>/css/components.css': '<%= dirs.css %>/components.scss'
}
},
server: {
options: {
sourceMap: true
},
files: {
'<%= dirs.tmp %>/css/main.css': '<%= dirs.css %>/main.scss'
}
}
},
// PostCSS processing (autoprefixer)
postcss: {
options: {
processors: [
require('autoprefixer')({
overrideBrowserslist: ['last 2 versions', 'not dead']
})
]
},
dist: {
src: '<%= dirs.tmp %>/css/*.css'
}
},
// CSS minification
cssmin: {
options: {
level: 2,
compatibility: 'ie8'
},
dist: {
files: [{
expand: true,
cwd: '<%= dirs.tmp %>/css',
dest: '<%= dirs.dist %>/css',
src: ['*.css', '!*.min.css'],
ext: '.min.css'
}]
}
},
// JavaScript concatenation
concat: {
options: {
separator: ';'
},
dist: {
src: [
'<%= dirs.js %>/libs/jquery.js',
'<%= dirs.js %>/libs/bootstrap.js',
'<%= dirs.js %>/plugins/*.js',
'<%= dirs.js %>/main.js'
],
dest: '<%= dirs.tmp %>/js/scripts.js'
}
},
// Babel transpilation
babel: {
options: {
sourceMap: true,
presets: [
['@babel/preset-env', {
targets: {
browsers: ['last 2 versions', 'not dead']
}
}]
]
},
dist: {
files: [{
expand: true,
cwd: '<%= dirs.js %>',
src: ['**/*.js', '!libs/**/*.js', '!vendor/**/*.js'],
dest: '<%= dirs.tmp %>/js'
}]
}
},
// JavaScript minification
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
mangle: true,
compress: {
drop_console: true,
drop_debugger: true
}
},
dist: {
files: {
'<%= dirs.dist %>/js/scripts.min.js': ['<%= dirs.tmp %>/js/scripts.js']
}
}
},
// HTML minification
htmlmin: {
dist: {
options: {
collapseWhitespace: true,
conservativeCollapse: true,
collapseBooleanAttributes: true,
removeCommentsFromCDATA: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyCSS: true,
minifyJS: true
},
files: [{
expand: true,
cwd: '<%= dirs.src %>',
src: '**/*.html',
dest: '<%= dirs.dist %>'
}]
}
},
// Image optimization
imagemin: {
dist: {
options: {
optimizationLevel: 7,
progressive: true,
interlaced: true,
svgoPlugins: [{ removeViewBox: false }]
},
files: [{
expand: true,
cwd: '<%= dirs.images %>',
src: ['**/*.{png,jpg,jpeg,gif,svg}'],
dest: '<%= dirs.dist %>/images'
}]
}
},
// File versioning
filerev: {
options: {
encoding: 'utf8',
algorithm: 'md5',
length: 8
},
dist: {
src: [
'<%= dirs.dist %>/js/*.js',
'<%= dirs.dist %>/css/*.css',
'<%= dirs.dist %>/images/**/*.{png,jpg,jpeg,gif,webp,svg}',
'<%= dirs.dist %>/fonts/**/*.{eot,svg,ttf,woff,woff2}'
]
}
},
// Usemin for HTML asset replacement
useminPrepare: {
html: '<%= dirs.src %>/index.html',
options: {
dest: '<%= dirs.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin']
},
post: {}
}
}
}
},
usemin: {
html: ['<%= dirs.dist %>/**/*.html'],
css: ['<%= dirs.dist %>/css/**/*.css'],
options: {
assetsDirs: [
'<%= dirs.dist %>',
'<%= dirs.dist %>/images',
'<%= dirs.dist %>/css'
]
}
},
// ESLint for JavaScript
eslint: {
options: {
configFile: '.eslintrc.json',
failOnError: false,
quiet: true
},
target: ['<%= dirs.js %>/**/*.js', '!<%= dirs.js %>/vendor/**/*']
},
// Stylelint for CSS/SCSS
stylelint: {
all: {
src: ['<%= dirs.css %>/**/*.scss'],
options: {
configFile: '.stylelintrc.json',
failOnError: false,
quiet: true
}
}
},
// Sass linting
sasslint: {
allFiles: {
src: ['<%= dirs.css %>/**/*.scss'],
options: {
configFile: '.sass-lint.yml'
}
}
},
// Banner for files
banner: {
dist: {
options: {
banner: '/*! <%= pkg.name %> <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
files: {
src: ['<%= dirs.dist %>/css/*.css', '<%= dirs.dist %>/js/*.js']
}
}
},
// File replacement
replace: {
dist: {
options: {
patterns: [{
match: /VERSION/g,
replacement: '<%= pkg.version %>'
}, {
match: /BUILD_DATE/g,
replacement: '<%= grunt.template.today("yyyy-mm-dd") %>'
}]
},
files: [{
expand: true,
flatten: true,
src: ['<%= dirs.dist %>/**/*.html'],
dest: '<%= dirs.dist %>'
}]
}
},
// Browser sync for development
browserSync: {
dev: {
bsFiles: {
src: [
'<%= dirs.tmp %>/css/**/*.css',
'<%= dirs.tmp %>/js/**/*.js',
'<%= dirs.src %>/**/*.html'
]
},
options: {
watchTask: true,
server: {
baseDir: ['<%= dirs.tmp %>', '<%= dirs.src %>'],
routes: {
'/bower_components': 'bower_components'
}
},
port: 3000,
open: true,
notify: false
}
}
},
// Watch task
watch: {
options: {
livereload: true,
spawn: false
},
sass: {
files: ['<%= dirs.css %>/**/*.{scss,sass}'],
tasks: ['sass:server', 'postcss']
},
js: {
files: ['<%= dirs.js %>/**/*.js', '!<%= dirs.js %>/vendor/**/*'],
tasks: ['eslint', 'babel']
},
html: {
files: ['<%= dirs.src %>/**/*.html'],
tasks: ['copy:html']
},
images: {
files: ['<%= dirs.images %>/**/*.{png,jpg,jpeg,gif,webp,svg}'],
tasks: ['newer:imagemin']
}
},
// Shell commands
shell: {
testUnit: {
command: 'npm run test:unit'
},
testE2E: {
command: 'npm run test:e2e'
},
buildProduction: {
command: 'NODE_ENV=production npm run build'
}
},
// Notify for build completion
notify: {
buildComplete: {
options: {
title: 'Build Complete',
message: 'Build completed successfully!'
}
},
watchStart: {
options: {
title: 'Grunt Watch',
message: 'Watching files for changes...'
}
}
},
// ZIP creation
compress: {
dist: {
options: {
archive: 'dist-<%= pkg.version %>.zip',
mode: 'zip',
level: 9,
pretty: true
},
files: [{
src: ['**'],
cwd: '<%= dirs.dist %>',
expand: true
}]
}
}
});
// Development task
grunt.registerTask('dev', [
'clean:tmp',
'clean:server',
'sass:server',
'postcss',
'babel',
'browserSync',
'watch',
'notify:watchStart'
]);
// Build task
grunt.registerTask('build', [
'clean:dist',
'clean:tmp',
'sass:dist',
'postcss',
'babel',
'concat',
'copy',
'cssmin',
'uglify',
'htmlmin',
'imagemin',
'filerev',
'usemin',
'banner:dist',
'replace:dist',
'notify:buildComplete'
]);
// Test task
grunt.registerTask('test', [
'eslint',
'stylelint',
'sasslint',
'shell:testUnit'
]);
// Lint task
grunt.registerTask('lint', [
'eslint',
'stylelint',
'sasslint'
]);
// Default task
grunt.registerTask('default', [
'build'
]);
// Serve task
grunt.registerTask('serve', [
'dev'
]);
// Deploy task
grunt.registerTask('deploy', [
'test',
'build',
'compress:dist'
]);
// Custom task: Build stats
grunt.registerTask('build-stats', 'Display build statistics', function() {
var distDir = grunt.config('dirs.dist');
var fs = require('fs');
var path = require('path');
var stats = {
files: 0,
totalSize: 0,
extensions: {}
};
function processDirectory(dir) {
var files = fs.readdirSync(dir);
files.forEach(function(file) {
var filePath = path.join(dir, file);
var stat = fs.statSync(filePath);
if (stat.isDirectory()) {
processDirectory(filePath);
} else {
stats.files++;
stats.totalSize += stat.size;
var ext = path.extname(file).toLowerCase();
if (ext) {
stats.extensions[ext] = (stats.extensions[ext] || 0) + 1;
}
}
});
}
if (fs.existsSync(distDir)) {
processDirectory(distDir);
}
grunt.log.writeln('Build Statistics:');
grunt.log.writeln('----------------');
grunt.log.writeln('Total files: ' + stats.files);
grunt.log.writeln('Total size: ' + (stats.totalSize / 1024).toFixed(2) + ' KB');
grunt.log.writeln('Extensions:');
Object.keys(stats.extensions).forEach(function(ext) {
grunt.log.writeln(' ' + ext + ': ' + stats.extensions[ext]);
});
});
// Custom task: Environment check
grunt.registerTask('env-check', 'Check environment requirements', function() {
var nodeVersion = process.version;
var npmVersion = require('child_process')
.execSync('npm --version', { encoding: 'utf8' })
.trim();
grunt.log.writeln('Environment Check:');
grunt.log.writeln('-----------------');
grunt.log.writeln('Node.js: ' + nodeVersion);
grunt.log.writeln('npm: ' + npmVersion);
grunt.log.writeln('Environment: ' + (grunt.config('env.NODE_ENV') || 'development'));
// Check required versions
var requiredNode = '>=14.0.0';
if (require('semver').satisfies(nodeVersion, requiredNode)) {
grunt.log.ok('Node.js version satisfies requirement: ' + requiredNode);
} else {
grunt.log.error('Node.js version does not satisfy requirement: ' + requiredNode);
}
});
};
💻 Flujos de Trabajo Avanzados de Grunt javascript
🟡 intermediate
⭐⭐⭐⭐
Flujos de trabajo de Grunt avanzados incluyendo construcciones multi-objetivo, optimización y estrategias de despliegue
⏱️ 35 min
🏷️ grunt, workflows, optimization
Prerequisites:
Grunt basics, JavaScript optimization, Build processes
// Grunt Advanced Workflows
// Complex build processes, optimization, and deployment automation
module.exports = function(grunt) {
'use strict';
// Load all grunt tasks
require('load-grunt-tasks')(grunt);
// Load environment-specific configuration
var envConfig = grunt.file.exists('grunt-config.js')
? require('./grunt-config')(grunt)
: {};
// Merge configurations
grunt.util._.merge(grunt.config(), envConfig);
// Advanced configuration
grunt.initConfig({
// Multi-environment configuration
environments: {
development: {
dirs: {
src: 'src',
dist: 'dist-dev',
tmp: '.tmp-dev'
},
optimization: false,
sourcemaps: true,
minify: false,
watch: true,
livereload: true
},
staging: {
dirs: {
src: 'src',
dist: 'dist-staging',
tmp: '.tmp-staging'
},
optimization: true,
sourcemaps: true,
minify: true,
watch: false,
livereload: false
},
production: {
dirs: {
src: 'src',
dist: 'dist-prod',
tmp: '.tmp-prod'
},
optimization: true,
sourcemaps: false,
minify: true,
watch: false,
livereload: false
}
},
// Dynamic configuration based on environment
env: grunt.option('env') || 'development',
config: grunt.config('environments.<%= env %>'),
// Advanced clean task with patterns
clean: {
dist: {
files: [{
dot: true,
src: [
'<%= config.dirs.dist %>/**/*',
'!<%= config.dirs.dist %>/.gitkeep'
]
}]
},
temp: {
files: [{
dot: true,
src: [
'<%= config.dirs.tmp %>/**/*',
'.sass-cache/**/*',
'.tmp/**/*'
]
}]
},
legacy: {
files: [{
src: [
'dist/**/*',
'tmp/**/*',
'.cache/**/*'
]
}]
}
},
// Multi-target sass compilation
sass: {
options: {
implementation: require('sass'),
sourceMap: '<%= config.sourcemaps %>',
outputStyle: '<%= config.minify ? "compressed" : "expanded" %>'
},
main: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/scss',
src: ['**/*.scss', '!**/_*.scss'],
dest: '<%= config.dirs.tmp %>/css',
ext: '.css'
}]
},
critical: {
options: {
outputStyle: 'expanded'
},
files: {
'<%= config.dirs.tmp %>/css/critical.css':
'<%= config.dirs.src %>/scss/critical.scss'
}
},
themes: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/scss/themes',
src: ['**/*.scss'],
dest: '<%= config.dirs.tmp %>/css/themes',
ext: '.css'
}]
}
},
// Advanced PostCSS with multiple processors
postcss: {
options: {
processors: [
require('autoprefixer')({
overrideBrowserslist: ['last 2 versions', 'not dead']
}),
require('postcss-flexbugs-fixes')(),
require('postcss-preset-env')({
stage: 2,
features: {
'custom-properties': true,
'nesting': true,
'calc': true
}
})
].concat(
grunt.config('config.minify')
? [require('cssnano')({ preset: 'default' })]
: []
)
},
dist: {
files: [{
expand: true,
cwd: '<%= config.dirs.tmp %>/css',
src: ['**/*.css'],
dest: '<%= config.dirs.tmp %>/css'
}]
}
},
// Advanced Babel with environment-specific presets
babel: {
options: {
sourceMap: '<%= config.sourcemaps %>',
presets: [
['@babel/preset-env', {
targets: {
browsers: grunt.config('config.env') === 'production'
? ['> 0.5%', 'not dead']
: ['last 2 versions']
},
modules: false,
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread'
].concat(
grunt.config('config.env') === 'production'
? ['@babel/plugin-transform-runtime']
: []
)
},
dist: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/js',
src: ['**/*.js', '!**/*.min.js'],
dest: '<%= config.dirs.tmp %>/js'
}]
}
},
// Advanced concatenation with multiple targets
concat: {
options: {
separator: grunt.config('config.minify') ? ';' : ';
',
process: function(src, filepath) {
return '// Source: ' + filepath + '
' + src;
}
},
vendor: {
files: {
'<%= config.dirs.tmp %>/js/vendor.js': [
'<%= config.dirs.src %>/js/libs/jquery.js',
'<%= config.dirs.src %>/js/libs/bootstrap.js',
'<%= config.dirs.src %>/js/libs/modernizr.js'
]
}
},
app: {
files: {
'<%= config.dirs.tmp %>/js/app.js': [
'<%= config.dirs.tmp %>/js/app/**/*.js',
'!<%= config.dirs.tmp %>/js/app/vendor.js'
]
}
},
themes: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/js/themes',
src: ['**/*.js'],
dest: '<%= config.dirs.tmp %>/js/themes',
ext: '.bundle.js'
}]
}
},
// Advanced uglify with multiple configurations
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= pkg.version %> - ' +
grunt.template.today('yyyy-mm-dd') + ' */
',
sourceMap: '<%= config.sourcemaps %>',
sourceMapIncludeSources: true,
sourceMapRoot: '../../<%= config.dirs.src %>/js',
mangle: grunt.config('config.minify'),
compress: {
drop_console: grunt.config('config.minify'),
drop_debugger: grunt.config('config.minify'),
dead_code: true,
unused: true,
warnings: false
},
output: {
comments: grunt.config('config.minify') ? false : /^!/
}
},
vendor: {
files: [{
'<%= config.dirs.dist %>/js/vendor.min.js':
'<%= concat.vendor.files[0]["<%= config.dirs.tmp %>/js/vendor.js"] %>'
}]
},
app: {
files: [{
'<%= config.dirs.dist %>/js/app.min.js':
'<%= concat.app.files[0]["<%= config.dirs.tmp %>/js/app.js"] %>'
}]
},
themes: {
files: [{
expand: true,
cwd: '<%= config.dirs.tmp %>/js/themes',
src: ['**/*.bundle.js'],
dest: '<%= config.dirs.dist %>/js/themes',
ext: '.min.js'
}]
}
},
// Critical CSS extraction
critical: {
options: {
base: '<%= config.dirs.dist %>/',
css: [
'<%= config.dirs.dist %>/css/main.min.css'
],
width: 1200,
height: 900,
minify: grunt.config('config.minify'),
extract: true,
ignore: []
},
dist: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>',
src: ['**/*.html', '!**/_*.html'],
dest: '<%= config.dirs.dist %>'
}]
}
},
// Advanced image optimization with different strategies
imagemin: {
options: {
cache: false,
optimizationLevel: grunt.config('config.optimization') ? 7 : 3,
progressive: true,
interlaced: true
},
static: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/images',
src: ['**/*.{png,jpg,jpeg,gif,svg}'],
dest: '<%= config.dirs.dist %>/images'
}]
},
dynamic: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/images/uploads',
src: ['**/*.{png,jpg,jpeg,gif,svg,webp}'],
dest: '<%= config.dirs.dist %>/images/uploads'
}]
},
icons: {
options: {
optimizationLevel: 7,
svgoPlugins: [{
removeViewBox: false,
removeEmptyAttrs: false
}]
},
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/icons',
src: ['**/*.svg'],
dest: '<%= config.dirs.dist %>/icons'
}]
}
},
// Sprite generation
sprite: {
options: {
spritemap: '<%= config.dirs.src %>/images/sprite/**/*.png',
spritesmithPath: require('spritesmith'),
engine: 'pixelsmith',
algorithm: 'binary-tree',
padding: 2
},
dist: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>/scss',
src: ['*.scss'],
dest: '<%= config.dirs.tmp %>/scss',
rename: function(name) {
return '_' + name;
}
}],
dest: {
'<%= config.dirs.tmp %>/images': 'sprite.png'
}
}
},
// Advanced HTML processing
processhtml: {
options: {
includeBase: '<%= config.dirs.src %>/',
commentMarker: 'build',
strip: true,
data: {
version: '<%= pkg.version %>',
buildDate: grunt.template.today('yyyy-mm-dd'),
environment: grunt.config('config.env')
}
},
dist: {
files: [{
expand: true,
cwd: '<%= config.dirs.src %>',
src: ['**/*.html', '!**/_*.html'],
dest: '<%= config.dirs.dist %>'
}]
}
},
// Advanced file versioning
filerev: {
options: {
algorithm: 'sha256',
length: 12
},
dist: {
files: [{
expand: true,
cwd: '<%= config.dirs.dist %>',
src: [
'js/**/*.js',
'css/**/*.css',
'images/**/*.{png,jpg,jpeg,gif,webp,svg}',
'fonts/**/*.{eot,svg,ttf,woff,woff2}'
]
}]
}
},
// Asset rewriting
replace: {
assets: {
options: {
patterns: [{
match: /assets\/images\//g,
replacement: 'assets/images/'
}, {
match: /assets\/css\//g,
replacement: function(match) {
return grunt.filerev.replacements[match] || match;
}
}, {
match: /assets\/js\//g,
replacement: function(match) {
return grunt.filerev.replacements[match] || match;
}
}]
},
files: [{
expand: true,
cwd: '<%= config.dirs.dist %>',
src: ['**/*.html', '**/*.css', '**/*.js']
}]
}
},
// Cache busting with file hashes
cacheBust: {
options: {
baseDir: '<%= config.dirs.dist %>',
assets: ['js/**/*.js', 'css/**/*.css'],
createCopies: false
},
dist: {
files: [{
expand: true,
cwd: '<%= config.dirs.dist %>',
src: ['**/*.html']
}]
}
},
// Progressive enhancement for IE
legacy: {
ie8: {
options: {
dest: '<%= config.dirs.dist %>/legacy',
minify: false
},
files: {
'<%= config.dirs.dist %>/legacy/js/app.ie8.js':
'<%= config.dirs.src %>/js/ie/app.ie8.js'
}
}
},
// Advanced watch with livereload and reload on different events
watch: {
options: {
livereload: {
port: 35729,
hostname: 'localhost'
},
spawn: false,
interval: 1000
},
sass: {
files: ['<%= config.dirs.src %>/scss/**/*.{scss,sass}'],
tasks: ['sass', 'postcss'],
options: {
livereload: true
}
},
js: {
files: ['<%= config.dirs.src %>/js/**/*.js', '!**/*.min.js'],
tasks: ['babel', 'concat', 'uglify'],
options: {
livereload: true
}
},
images: {
files: ['<%= config.dirs.src %>/images/**/*.{png,jpg,jpeg,gif,svg,webp}'],
tasks: ['newer:imagemin'],
options: {
livereload: true
}
},
config: {
files: ['Gruntfile.js', 'grunt-config.js'],
tasks: ['config-reload'],
options: {
reload: true
}
}
},
// Code quality tasks
eslint: {
options: {
configFile: '.eslintrc.json',
fix: grunt.config('config.env') === 'development',
cache: true,
quiet: true
},
target: [
'<%= config.dirs.src %>/js/**/*.js',
'!<%= config.dirs.src %>/js/vendor/**/*',
'!<%= config.dirs.src %>/js/libs/**/*'
]
},
// Performance budgeting
filesize: {
options: {
gzip: true,
showFiles: true,
showSizes: true
},
js: {
files: '<%= config.dirs.dist %>/js/**/*.js'
},
css: {
files: '<%= config.dirs.dist %>/css/**/*.css'
}
},
// Bundle analysis
bundlesize: {
options: {
maxSize: '200kb',
gzip: true
},
main: {
src: '<%= config.dirs.dist %>/js/app.min.js'
}
},
// Custom tasks and workflows
concurrent: {
options: {
logConcurrentOutput: true
},
dev: ['watch', 'connect'],
build: ['sass', 'babel', 'imagemin'],
deploy: ['build', 'test']
},
// Server configuration
connect: {
options: {
port: 9000,
hostname: 'localhost',
base: '<%= config.dirs.tmp %>',
livereload: true,
open: true,
middleware: function(connect, options, middlewares) {
// Custom middleware for API proxy
var proxy = require('grunt-connect-proxy/lib/utils').proxyRequest;
middlewares.unshift(proxy);
return middlewares;
}
},
proxies: [{
context: '/api',
host: 'localhost',
port: 8080,
https: false,
changeOrigin: false,
xforward: false
}],
livereload: {
options: {
base: '<%= config.dirs.tmp %>',
open: {
target: 'http://localhost:9000'
}
}
}
}
});
// Environment-specific task registration
grunt.registerTask('build:dev', function() {
grunt.option('env', 'development');
grunt.task.run(['build']);
});
grunt.registerTask('build:staging', function() {
grunt.option('env', 'staging');
grunt.task.run(['build']);
});
grunt.registerTask('build:prod', function() {
grunt.option('env', 'production');
grunt.task.run(['build']);
});
// Main build task
grunt.registerTask('build', 'Build the application', function() {
var tasks = [
'clean',
'sass',
'postcss',
'sprite',
'babel',
'concat',
'copy',
'imagemin',
'eslint',
'processhtml',
'cacheBust'
];
if (grunt.config('config.minify')) {
tasks.push('cssmin', 'uglify');
}
tasks.push('replace:assets', 'filerev');
grunt.task.run(tasks);
});
// Development workflow
grunt.registerTask('dev', [
'clean:tmp',
'build:dev',
'connect',
'watch'
]);
// Testing workflow
grunt.registerTask('test', [
'eslint',
'bundlesize',
'filesize',
'qunit'
]);
// Deployment workflow
grunt.registerTask('deploy', [
'test',
'build:prod',
'compress'
]);
// Custom task: Performance audit
grunt.registerTask('performance-audit', 'Run performance audit on built assets', function() {
var fs = require('fs');
var path = require('path');
var distDir = grunt.config('config.dirs.dist');
var audit = {
assets: [],
totalSize: 0,
recommendations: []
};
function analyzeFile(filePath, type) {
var stats = fs.statSync(filePath);
var sizeKB = (stats.size / 1024).toFixed(2);
audit.assets.push({
type: type,
path: path.relative(distDir, filePath),
sizeKB: parseFloat(sizeKB),
sizeRaw: stats.size
});
audit.totalSize += stats.size;
// Check for optimization opportunities
if (sizeKB > 100 && type === 'js') {
audit.recommendations.push(
'Consider splitting ' + filePath + ' into smaller modules'
);
}
if (sizeKB > 50 && type === 'css') {
audit.recommendations.push(
'Consider critical CSS extraction for ' + filePath
);
}
}
// Analyze JavaScript files
grunt.file.expand(distDir + '/js/**/*.js').forEach(function(file) {
analyzeFile(file, 'js');
});
// Analyze CSS files
grunt.file.expand(distDir + '/css/**/*.css').forEach(function(file) {
analyzeFile(file, 'css');
});
// Output results
grunt.log.writeln('Performance Audit Results:');
grunt.log.writeln('========================');
grunt.log.writeln('Total assets: ' + audit.assets.length);
grunt.log.writeln('Total size: ' + (audit.totalSize / 1024).toFixed(2) + ' KB');
grunt.log.writeln('');
grunt.log.writeln('Asset Details:');
audit.assets.forEach(function(asset) {
grunt.log.writeln(
' ' + asset.type.toUpperCase() + ': ' +
asset.path + ' (' + asset.sizeKB + ' KB)'
);
});
if (audit.recommendations.length > 0) {
grunt.log.writeln('');
grunt.log.writeln('Recommendations:');
audit.recommendations.forEach(function(rec) {
grunt.log.writeln(' • ' + rec);
});
}
});
// Custom task: Dependency checker
grunt.registerTask('deps-check', 'Check for outdated dependencies', function() {
var shell = require('shelljs');
var pkg = require('./package.json');
grunt.log.writeln('Checking for outdated dependencies...');
var outdated = shell.exec('npm outdated --json', { silent: true }).stdout;
if (outdated) {
var outdatedDeps = JSON.parse(outdated);
var keys = Object.keys(outdatedDeps);
if (keys.length > 0) {
grunt.log.writeln('Outdated dependencies found:');
keys.forEach(function(dep) {
var info = outdatedDeps[dep];
grunt.log.writeln(
' ' + dep + ': current=' + info.current +
', wanted=' + info.wanted + ', latest=' + info.latest
);
});
} else {
grunt.log.writeln('All dependencies are up to date!');
}
}
});
// Default task
grunt.registerTask('default', ['build:dev']);
};
💻 Desarrollo de Plugins Grunt javascript
🟡 intermediate
⭐⭐⭐⭐
Creación de plugins personalizados de Grunt y extensión de funcionalidad de Grunt
⏱️ 40 min
🏷️ grunt, plugins, development
Prerequisites:
Grunt basics, JavaScript ES6+, Plugin architecture
// Grunt Plugin Development Guide
// Creating custom plugins and extending Grunt functionality
// ===== BASIC PLUGIN STRUCTURE =====
// 1. Simple File Processor Plugin
// grunt-plugin-file-processor.js
'use strict';
module.exports = function(grunt) {
grunt.registerMultiTask('fileProcessor', 'Process files with custom logic', function() {
var options = this.options({
encoding: 'utf8',
process: function(content) {
return content; // Default: no processing
},
extension: '.processed'
});
this.files.forEach(function(file) {
var dest = file.dest;
var contents = file.src.map(function(filepath) {
var content = grunt.file.read(filepath, { encoding: options.encoding });
return options.process(content, filepath);
}).join('\n');
grunt.file.write(dest, contents);
grunt.log.writeln('Processed file: ' + dest);
});
});
};
// 2. Multi-Task Plugin with Template Support
// grunt-plugin-template-processor.js
module.exports = function(grunt) {
grunt.registerMultiTask('templateProcessor', 'Process templates with data', function() {
var options = this.options({
data: {},
delimiters: ['{{', '}}'],
ext: '.html'
});
// Load template data
var templateData = {};
if (typeof options.data === 'string') {
templateData = grunt.file.readJSON(options.data);
} else {
templateData = options.data;
}
this.files.forEach(function(file) {
var src = file.src[0];
var dest = file.dest || src;
if (grunt.file.exists(src)) {
var content = grunt.file.read(src);
var processed = grunt.template.process(content, {
data: templateData,
delimiters: options.delimiters
});
grunt.file.write(dest, processed);
grunt.log.writeln('Processed template: ' + dest);
}
});
});
};
// 3. Async Plugin with Progress Reporting
// grunt-plugin-async-processor.js
module.exports = function(grunt) {
grunt.registerTask('asyncProcessor', 'Process files asynchronously', function() {
var done = this.async();
var options = this.options({
concurrency: 5,
timeout: 30000
});
var files = this.filesSrc;
var processed = 0;
var total = files.length;
// Progress reporting
var progressInterval = setInterval(function() {
grunt.log.writeln('Progress: ' + processed + '/' + total + ' files processed');
}, 1000);
// Process files with limited concurrency
var processFile = function(filepath, callback) {
var startTime = Date.now();
setTimeout(function() {
// Simulate file processing
grunt.file.read(filepath);
processed++;
var duration = Date.now() - startTime;
grunt.log.writeln('Processed: ' + filepath + ' (' + duration + 'ms)');
callback(null, filepath);
}, Math.random() * 2000);
};
// Queue management
var queue = [];
var active = 0;
function processQueue() {
while (active < options.concurrency && queue.length > 0) {
active++;
var filepath = queue.shift();
processFile(filepath, function() {
active--;
if (queue.length > 0) {
processQueue();
} else if (active === 0) {
clearInterval(progressInterval);
done();
}
});
}
}
// Add files to queue
files.forEach(function(filepath) {
queue.push(filepath);
});
// Start processing
processQueue();
// Timeout handling
setTimeout(function() {
if (processed < total) {
clearInterval(progressInterval);
grunt.log.error('Processing timed out');
done(false);
}
}, options.timeout);
});
};
// ===== ADVANCED PLUGIN EXAMPLES =====
// 4. CSS Sprite Generator Plugin
// grunt-plugin-css-sprite.js
module.exports = function(grunt) {
var.spritesmith = require('spritesmith');
var crypto = require('crypto');
grunt.registerMultiTask('cssSprite', 'Generate CSS sprites from images', function() {
var done = this.async();
var options = this.options({
algorithm: 'binary-tree',
padding: 2,
engine: 'pixelsmith',
cssFormat: 'css',
cssTemplate: null,
cssVarMap: function(sprite) {
return sprite.name;
}
});
this.files.forEach(function(file) {
var sprites = file.src.map(function(filepath) {
return {
src: filepath,
name: grunt.config('process').basename(filepath, '.png').replace('@2x', '')
};
});
spritesmith.create({
src: file.src,
dest: file.dest,
engine: options.engine,
algorithm: options.algorithm,
padding: options.padding
}, function(err, result) {
if (err) {
grunt.log.error(err);
return done(err);
}
// Write sprite image
grunt.file.write(file.dest, result.image);
// Generate CSS
var css = generateCSS(result.coordinates, result.properties, options);
var cssPath = file.dest.replace(/\.png$/, '.' + options.cssFormat);
grunt.file.write(cssPath, css);
grunt.log.writeln('Generated sprite: ' + file.dest);
grunt.log.writeln('Generated CSS: ' + cssPath);
done();
});
});
function generateCSS(coordinates, properties, options) {
var css = '';
var baseName = properties.name || 'sprite';
for (var name in coordinates) {
var sprite = coordinates[name];
css += '.sprite-' + name + ' {\n';
css += ' background-image: url("' + baseName + '.png");\n';
css += ' background-position: ' + (-sprite.x) + 'px ' + (-sprite.y) + 'px;\n';
css += ' width: ' + sprite.width + 'px;\n';
css += ' height: ' + sprite.height + 'px;\n';
css += '}\n\n';
}
return css;
}
});
};
// 5. Performance Optimizer Plugin
// grunt-plugin-perf-optimizer.js
module.exports = function(grunt) {
var UglifyJS = require('uglify-js');
var CleanCSS = require('clean-css');
var imagemin = require('imagemin');
var mozjpeg = require('imagemin-mozjpeg');
var pngquant = require('imagemin-pngquant');
grunt.registerMultiTask('perfOptimizer', 'Optimize files for performance', function() {
var options = this.options({
jsOptimization: true,
cssOptimization: true,
imageOptimization: true,
report: true,
threshold: 1024 * 1024 // 1MB
});
var stats = {
files: 0,
originalSize: 0,
optimizedSize: 0,
compressionRatio: 0
};
var done = this.async();
this.files.forEach(function(file) {
file.src.forEach(function(src) {
if (grunt.file.isFile(src)) {
var extension = grunt.config('process').extname(src).toLowerCase();
var originalContent = grunt.file.read(src);
var optimizedContent = originalContent;
var originalSize = originalContent.length;
stats.files++;
stats.originalSize += originalSize;
// JavaScript optimization
if (extension === '.js' && options.jsOptimization) {
var result = UglifyJS.minify(originalContent);
if (!result.error) {
optimizedContent = result.code;
grunt.log.writeln('Optimized JavaScript: ' + src);
}
}
// CSS optimization
else if (extension === '.css' && options.cssOptimization) {
var cleanCSS = new CleanCSS();
var result = cleanCSS.minify(originalContent);
optimizedContent = result.styles;
grunt.log.writeln('Optimized CSS: ' + src);
}
// Image optimization
else if (['.png', '.jpg', '.jpeg', '.gif', '.svg'].includes(extension) &&
options.imageOptimization) {
imagemin([src], {
plugins: [
imagemin.gifsicle(),
imagemin.jpegtran(),
imagemin.optipng(),
imagemin.svgo(),
pngquant(),
mozjpeg()
]
}).then(function(files) {
if (files && files[0]) {
grunt.file.write(file.dest || src, files[0].data);
var optimizedSize = files[0].data.length;
updateStats(originalSize, optimizedSize);
}
});
return;
}
// Write optimized content
grunt.file.write(file.dest || src, optimizedContent);
updateStats(originalSize, optimizedContent.length);
}
});
});
function updateStats(original, optimized) {
stats.optimizedSize += optimized;
stats.compressionRatio = ((stats.originalSize - stats.optimizedSize) / stats.originalSize * 100).toFixed(2);
}
// Generate report
if (options.report) {
setTimeout(function() {
grunt.log.writeln('\nPerformance Optimization Report');
grunt.log.writeln('================================');
grunt.log.writeln('Files processed: ' + stats.files);
grunt.log.writeln('Original size: ' + (stats.originalSize / 1024).toFixed(2) + ' KB');
grunt.log.writeln('Optimized size: ' + (stats.optimizedSize / 1024).toFixed(2) + ' KB');
grunt.log.writeln('Compression ratio: ' + stats.compressionRatio + '%');
if (stats.originalSize > options.threshold) {
grunt.log.warn('Total size exceeds threshold of ' + (options.threshold / 1024) + ' KB');
}
done();
}, 1000);
} else {
done();
}
});
};
// 6. i18n Bundle Generator Plugin
// grunt-plugin-i18n.js
module.exports = function(grunt) {
grunt.registerMultiTask('i18nBundle', 'Generate i18n translation bundles', function() {
var options = this.options({
languages: ['en', 'es', 'fr'],
defaultLanguage: 'en',
outputFormat: 'json',
namespace: 'translations'
});
var translations = {};
var defaultTranslations = {};
// Load translations from source files
this.files.forEach(function(file) {
file.src.forEach(function(src) {
if (grunt.file.exists(src)) {
var content = grunt.file.readJSON(src);
var lang = grunt.config('process').basename(src, '.json');
translations[lang] = content;
if (lang === options.defaultLanguage) {
defaultTranslations = content;
}
}
});
});
// Generate bundles
options.languages.forEach(function(lang) {
var bundle = mergeTranslations(defaultTranslations, translations[lang]);
var outputDir = file.dest || 'dist/i18n';
var outputFile = outputDir + '/' + lang + '.json';
grunt.file.write(outputFile, JSON.stringify(bundle, null, 2));
grunt.log.writeln('Generated i18n bundle: ' + outputFile);
});
function mergeTranslations(defaults, translations) {
if (!translations) {
return defaults;
}
var merged = {};
var keys = Object.keys(defaults);
keys.forEach(function(key) {
if (translations[key]) {
merged[key] = translations[key];
} else {
merged[key] = defaults[key];
}
});
return merged;
}
});
};
// ===== PLUGIN TESTING FRAMEWORK =====
// 7. Plugin Test Framework
// grunt-plugin-test.js
module.exports = function(grunt) {
var path = require('path');
grunt.registerTask('testPlugin', 'Test a Grunt plugin', function(pluginName, testFile) {
if (!pluginName) {
grunt.log.error('Plugin name is required');
return;
}
var testPath = testFile || 'test/' + pluginName + '_test.js';
if (!grunt.file.exists(testPath)) {
grunt.log.error('Test file not found: ' + testPath);
return;
}
// Load the plugin
var pluginPath = path.resolve('node_modules/grunt-' + pluginName);
try {
require(pluginPath);
} catch (e) {
// Try local plugin path
pluginPath = path.resolve('plugins/grunt-' + pluginName + '.js');
require(pluginPath);
}
// Run tests
try {
require(testPath)(grunt);
grunt.log.ok('Plugin tests passed: ' + pluginName);
} catch (e) {
grunt.log.error('Plugin tests failed: ' + pluginName);
grunt.log.error(e.message);
}
});
};
// Example test file for fileProcessor plugin
// test/fileProcessor_test.js
module.exports = function(grunt) {
var fs = require('fs');
var tempDir = 'test/temp';
// Setup test environment
grunt.task.run('setup:test');
// Test cases
var tests = [
{
name: 'Basic file processing',
input: 'test/fixtures/sample.txt',
expected: 'test/expected/sample.processed.txt',
options: {}
},
{
name: 'Custom processing function',
input: 'test/fixtures/sample.txt',
expected: 'test/expected/sample.uppercase.txt',
options: {
process: function(content) {
return content.toUpperCase();
}
}
}
];
// Run tests
tests.forEach(function(test) {
grunt.task.options('fileProcessor', test.options);
grunt.task.run('fileProcessor', {
src: [test.input],
dest: tempDir + '/processed.txt'
});
// Verify output
var actual = fs.readFileSync(tempDir + '/processed.txt', 'utf8');
var expected = fs.readFileSync(test.expected, 'utf8');
if (actual === expected) {
grunt.log.ok('✓ ' + test.name);
} else {
grunt.log.error('✗ ' + test.name);
grunt.log.error('Expected: ' + expected);
grunt.log.error('Actual: ' + actual);
}
});
// Cleanup
grunt.task.run('cleanup:test');
};
// ===== PLUGIN REGISTRATION =====
// Register all plugins
module.exports = function(grunt) {
// Load individual plugins
require('./grunt-plugin-file-processor')(grunt);
require('./grunt-plugin-template-processor')(grunt);
require('./grunt-plugin-async-processor')(grunt);
require('./grunt-plugin-css-sprite')(grunt);
require('./grunt-plugin-perf-optimizer')(grunt);
require('./grunt-plugin-i18n')(grunt);
require('./grunt-plugin-test')(grunt);
// Plugin configuration
grunt.initConfig({
// File processor configuration
fileProcessor: {
basic: {
files: {
'dist/processed.txt': ['src/input.txt']
}
},
advanced: {
options: {
process: function(content, filepath) {
return 'Processed from ' + filepath + ':\n' + content;
}
},
files: {
'dist/advanced.txt': ['src/**/*.txt']
}
}
},
// Template processor configuration
templateProcessor: {
development: {
options: {
data: 'config/dev.json'
},
files: {
'dist/index.html': 'src/index.html'
}
}
},
// CSS sprite configuration
cssSprite: {
options: {
algorithm: 'binary-tree',
padding: 2,
cssFormat: 'scss',
cssVarMap: function(sprite) {
return 'sprite-' + sprite.name;
}
},
all: {
src: ['src/images/sprites/**/*.png'],
dest: 'dist/images/sprite.png'
}
},
// Performance optimizer configuration
perfOptimizer: {
options: {
jsOptimization: true,
cssOptimization: true,
imageOptimization: true,
report: true
},
assets: {
files: {
'dist/': ['src/**/*.{js,css,png,jpg,jpeg,gif,svg}']
}
}
},
// i18n configuration
i18nBundle: {
options: {
languages: ['en', 'es', 'fr', 'de', 'ja'],
defaultLanguage: 'en'
},
translations: {
src: ['src/i18n/*.json'],
dest: 'dist/i18n'
}
}
});
// Register tasks
grunt.registerTask('dev', ['fileProcessor', 'templateProcessor:development']);
grunt.registerTask('optimize', ['perfOptimizer', 'cssSprite']);
grunt.registerTask('i18n', ['i18nBundle']);
grunt.registerTask('build', ['dev', 'optimize', 'i18n']);
grunt.registerTask('default', ['build']);
};
// ===== PLUGIN DISTRIBUTION =====
// package.json for plugin distribution
{
"name": "grunt-custom-plugins",
"version": "1.0.0",
"description": "Collection of custom Grunt plugins",
"main": "index.js",
"scripts": {
"test": "grunt test",
"lint": "grunt eslint",
"build": "grunt build"
},
"keywords": ["gruntplugin", "automation", "build", "optimization"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"grunt": "^1.6.1",
"spritesmith": "^4.0.0",
"uglify-js": "^3.17.0",
"clean-css": "^5.3.0",
"imagemin": "^8.0.0",
"imagemin-mozjpeg": "^10.0.0",
"imagemin-pngquant": "^9.0.0"
},
"peerDependencies": {
"grunt": ">=1.0.0"
},
"devDependencies": {
"grunt-contrib-clean": "^2.0.0",
"grunt-contrib-concat": "^2.1.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-eslint": "^24.3.0"
},
"engines": {
"node": ">=14.0.0"
}
}
// Plugin documentation template
/*
# Grunt Custom Plugins
A collection of custom Grunt plugins for advanced build automation.
## Installation
```bash
npm install grunt-custom-plugins --save-dev
```
## Available Plugins
### fileProcessor
Process files with custom logic.
```javascript
grunt.initConfig({
fileProcessor: {
options: {
process: function(content, filepath) {
// Custom processing logic
return processedContent;
}
},
files: {
'dist/processed.txt': ['src/input.txt']
}
}
});
```
### templateProcessor
Process templates with data.
```javascript
grunt.initConfig({
templateProcessor: {
options: {
data: 'config.json',
delimiters: ['{{', '}}']
},
files: {
'dist/index.html': 'src/index.html'
}
}
});
```
### cssSprite
Generate CSS sprites from images.
```javascript
grunt.initConfig({
cssSprite: {
options: {
algorithm: 'binary-tree',
cssFormat: 'scss'
},
files: {
'dist/sprite.png': ['src/sprites/*.png']
}
}
});
```
## Testing
```bash
npm test
```
## License
MIT
*/