🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Outil de Construction Grunt
Exemples du task runner JavaScript Grunt incluant l'automatisation, les processus de construction et les workflows de déploiement
💻 Tâches de Base Grunt javascript
🟢 simple
⭐⭐
Tâches essentielles de Grunt pour le développement web moderne incluant le traitement de fichiers, la surveillance et la construction
⏱️ 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);
}
});
};
💻 Workflows Avancés Grunt javascript
🟡 intermediate
⭐⭐⭐⭐
Workflows de Grunt avancés incluant les constructions multi-cibles, l'optimisation et les stratégies de déploiement
⏱️ 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']);
};
💻 Développement de Plugins Grunt javascript
🟡 intermediate
⭐⭐⭐⭐
Création de plugins personnalisés Grunt et extension de la fonctionnalité 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
*/