/* jshint node:true */ module.exports = function(grunt) { var path = require('path'), fs = require( 'fs' ), SOURCE_DIR = 'src/', BUILD_DIR = 'build/', autoprefixer = require('autoprefixer'), mediaConfig = {}, mediaBuilds = ['audiovideo', 'grid', 'models', 'views']; // Load tasks. require('matchdep').filterDev(['grunt-*', '!grunt-legacy-util']).forEach( grunt.loadNpmTasks ); // Load legacy utils grunt.util = require('grunt-legacy-util'); mediaBuilds.forEach( function ( build ) { var path = SOURCE_DIR + 'wp-includes/js/media'; mediaConfig[ build ] = { files : {} }; mediaConfig[ build ].files[ path + '-' + build + '.js' ] = [ path + '/' + build + '.manifest.js' ]; } ); // Project configuration. grunt.initConfig({ postcss: { options: { processors: [ autoprefixer({ browsers: [ 'Android >= 2.1', 'Chrome >= 21', 'Edge >= 12', 'Explorer >= 7', 'Firefox >= 17', 'Opera >= 12.1', 'Safari >= 6.0' ], cascade: false }) ] }, core: { expand: true, cwd: SOURCE_DIR, dest: SOURCE_DIR, src: [ 'wp-admin/css/*.css', 'wp-includes/css/*.css' ] }, colors: { expand: true, cwd: BUILD_DIR, dest: BUILD_DIR, src: [ 'wp-admin/css/colors/*/colors.css' ] } }, clean: { all: [BUILD_DIR], dynamic: { dot: true, expand: true, cwd: BUILD_DIR, src: [] }, tinymce: ['<%= concat.tinymce.dest %>'], qunit: ['tests/qunit/compiled.html'] }, copy: { files: { files: [ { dot: true, expand: true, cwd: SOURCE_DIR, src: [ '**', '!wp-includes/js/media/**', '!**/.{svn,git}/**', // Ignore version control directories. // Ignore unminified versions of external libs we don't ship: '!wp-includes/js/backbone.js', '!wp-includes/js/underscore.js', '!wp-includes/js/jquery/jquery.masonry.js', '!wp-includes/js/jquery/ui/*.js', '!wp-includes/js/tinymce/tinymce.js', '!wp-includes/version.php' // Exclude version.php ], dest: BUILD_DIR }, { src: 'wp-config-sample.php', dest: BUILD_DIR } ] }, 'wp-admin-css-compat-rtl': { options: { processContent: function( src ) { return src.replace( /\.css/g, '-rtl.css' ); } }, src: SOURCE_DIR + 'wp-admin/css/wp-admin.css', dest: BUILD_DIR + 'wp-admin/css/wp-admin-rtl.css' }, 'wp-admin-css-compat-min': { options: { processContent: function( src ) { return src.replace( /\.css/g, '.min.css' ); } }, files: [ { src: SOURCE_DIR + 'wp-admin/css/wp-admin.css', dest: BUILD_DIR + 'wp-admin/css/wp-admin.min.css' }, { src: BUILD_DIR + 'wp-admin/css/wp-admin-rtl.css', dest: BUILD_DIR + 'wp-admin/css/wp-admin-rtl.min.css' } ] }, version: { options: { processContent: function( src ) { return src.replace( /^\$wp_version = '(.+?)';/m, function( str, version ) { version = version.replace( /-src$/, '' ); // If the version includes an SVN commit (-12345), it's not a released alpha/beta. Append a timestamp. version = version.replace( /-[\d]{5}$/, '-' + grunt.template.today( 'yyyymmdd.HHMMss' ) ); /* jshint quotmark: true */ return "$wp_version = '" + version + "';"; }); } }, src: SOURCE_DIR + 'wp-includes/version.php', dest: BUILD_DIR + 'wp-includes/version.php' }, dynamic: { dot: true, expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, src: [] }, qunit: { src: 'tests/qunit/index.html', dest: 'tests/qunit/compiled.html', options: { processContent: function( src ) { return src.replace( /(\".+?\/)src(\/.+?)(?:.min)?(.js\")/g , function( match, $1, $2, $3 ) { // Don't add `.min` to files that don't have it. return $1 + 'build' + $2 + ( /jquery$/.test( $2 ) ? '' : '.min' ) + $3; } ); } } } }, browserify: mediaConfig, sass: { colors: { expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '.css', src: ['wp-admin/css/colors/*/colors.scss'], options: { outputStyle: 'expanded' } } }, cssmin: { options: { compatibility: 'ie7' }, core: { expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '.min.css', src: [ 'wp-admin/css/*.css', '!wp-admin/css/wp-admin*.css', 'wp-includes/css/*.css', 'wp-includes/js/mediaelement/wp-mediaelement.css' ] }, rtl: { expand: true, cwd: BUILD_DIR, dest: BUILD_DIR, ext: '.min.css', src: [ 'wp-admin/css/*-rtl.css', '!wp-admin/css/wp-admin*.css', 'wp-includes/css/*-rtl.css' ] }, colors: { expand: true, cwd: BUILD_DIR, dest: BUILD_DIR, ext: '.min.css', src: [ 'wp-admin/css/colors/*/*.css' ] } }, rtlcss: { options: { // rtlcss options opts: { clean: false, processUrls: { atrule: true, decl: false }, stringMap: [ { name: 'import-rtl-stylesheet', priority: 10, exclusive: true, search: [ '.css' ], replace: [ '-rtl.css' ], options: { scope: 'url', ignoreCase: false } } ] }, saveUnmodified: false, plugins: [ { name: 'swap-dashicons-left-right-arrows', priority: 10, directives: { control: {}, value: [] }, processors: [ { expr: /content/im, action: function( prop, value ) { if ( value === '"\\f141"' ) { // dashicons-arrow-left value = '"\\f139"'; } else if ( value === '"\\f340"' ) { // dashicons-arrow-left-alt value = '"\\f344"'; } else if ( value === '"\\f341"' ) { // dashicons-arrow-left-alt2 value = '"\\f345"'; } else if ( value === '"\\f139"' ) { // dashicons-arrow-right value = '"\\f141"'; } else if ( value === '"\\f344"' ) { // dashicons-arrow-right-alt value = '"\\f340"'; } else if ( value === '"\\f345"' ) { // dashicons-arrow-right-alt2 value = '"\\f341"'; } return { prop: prop, value: value }; } } ] } ] }, core: { expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '-rtl.css', src: [ 'wp-admin/css/*.css', 'wp-includes/css/*.css', // Exceptions '!wp-includes/css/dashicons.css', '!wp-includes/css/wp-embed-template.css', '!wp-includes/css/wp-embed-template-ie.css' ] }, colors: { expand: true, cwd: BUILD_DIR, dest: BUILD_DIR, ext: '-rtl.css', src: [ 'wp-admin/css/colors/*/colors.css' ] }, dynamic: { expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '-rtl.css', src: [] } }, jshint: { options: grunt.file.readJSON('.jshintrc'), grunt: { src: ['Gruntfile.js'] }, tests: { src: [ 'tests/qunit/**/*.js', '!tests/qunit/vendor/*', '!tests/qunit/editor/**' ], options: grunt.file.readJSON('tests/qunit/.jshintrc') }, themes: { expand: true, cwd: SOURCE_DIR + 'wp-content/themes', src: [ 'twenty*/**/*.js', '!twenty{eleven,twelve,thirteen}/**', // Third party scripts '!twenty{fourteen,fifteen,sixteen}/js/html5.js', '!twentyseventeen/assets/js/html5.js', '!twentyseventeen/assets/js/jquery.scrollTo.js' ] }, media: { options: { browserify: true }, src: [ SOURCE_DIR + 'wp-includes/js/media/**/*.js' ] }, core: { expand: true, cwd: SOURCE_DIR, src: [ 'wp-admin/js/*.js', 'wp-includes/js/*.js', // Built scripts. '!wp-includes/js/media-*', // WordPress scripts inside directories 'wp-includes/js/jquery/jquery.table-hotkeys.js', 'wp-includes/js/mediaelement/wp-mediaelement.js', 'wp-includes/js/mediaelement/wp-playlist.js', 'wp-includes/js/plupload/handlers.js', 'wp-includes/js/plupload/wp-plupload.js', 'wp-includes/js/tinymce/plugins/wordpress/plugin.js', 'wp-includes/js/tinymce/plugins/wp*/plugin.js', // Third party scripts '!wp-admin/js/farbtastic.js', '!wp-includes/js/backbone*.js', '!wp-includes/js/swfobject.js', '!wp-includes/js/underscore*.js', '!wp-includes/js/colorpicker.js', '!wp-includes/js/hoverIntent.js', '!wp-includes/js/json2.js', '!wp-includes/js/tw-sack.js', '!wp-includes/js/twemoji.js', '!**/*.min.js' ], // Remove once other JSHint errors are resolved options: { curly: false, eqeqeq: false }, // Limit JSHint's run to a single specified file: // // grunt jshint:core --file=filename.js // // Optionally, include the file path: // // grunt jshint:core --file=path/to/filename.js // filter: function( filepath ) { var index, file = grunt.option( 'file' ); // Don't filter when no target file is specified if ( ! file ) { return true; } // Normalize filepath for Windows filepath = filepath.replace( /\\/g, '/' ); index = filepath.lastIndexOf( '/' + file ); // Match only the filename passed from cli if ( filepath === file || ( -1 !== index && index === filepath.length - ( file.length + 1 ) ) ) { return true; } return false; } }, plugins: { expand: true, cwd: SOURCE_DIR + 'wp-content/plugins', src: [ '**/*.js', '!**/*.min.js' ], // Limit JSHint's run to a single specified plugin directory: // // grunt jshint:plugins --dir=foldername // filter: function( dirpath ) { var index, dir = grunt.option( 'dir' ); // Don't filter when no target folder is specified if ( ! dir ) { return true; } dirpath = dirpath.replace( /\\/g, '/' ); index = dirpath.lastIndexOf( '/' + dir ); // Match only the folder name passed from cli if ( -1 !== index ) { return true; } return false; } } }, qunit: { files: [ 'tests/qunit/**/*.html', '!tests/qunit/editor/**' ] }, phpunit: { 'default': { cmd: 'phpunit', args: ['-c', 'phpunit.xml.dist'] }, ajax: { cmd: 'phpunit', args: ['-c', 'phpunit.xml.dist', '--group', 'ajax'] }, multisite: { cmd: 'phpunit', args: ['-c', 'tests/phpunit/multisite.xml'] }, 'external-http': { cmd: 'phpunit', args: ['-c', 'phpunit.xml.dist', '--group', 'external-http'] } }, uglify: { options: { ASCIIOnly: true }, core: { expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '.min.js', src: [ 'wp-admin/js/*.js', 'wp-includes/js/*.js', 'wp-includes/js/mediaelement/wp-mediaelement.js', 'wp-includes/js/mediaelement/wp-playlist.js', 'wp-includes/js/plupload/handlers.js', 'wp-includes/js/plupload/wp-plupload.js', 'wp-includes/js/tinymce/plugins/wordpress/plugin.js', 'wp-includes/js/tinymce/plugins/wp*/plugin.js', // Exceptions '!wp-admin/js/bookmarklet.*', // Minified and updated in /src with the precommit task. See uglify:bookmarklet. '!wp-admin/js/custom-header.js', // Why? We should minify this. '!wp-admin/js/farbtastic.js', '!wp-admin/js/iris.min.js', '!wp-includes/js/backbone.*', '!wp-includes/js/masonry.min.js', '!wp-includes/js/swfobject.js', '!wp-includes/js/underscore.*', '!wp-includes/js/zxcvbn.min.js', '!wp-includes/js/wp-embed.js' // We have extra options for this, see uglify:embed ] }, embed: { options: { compress: { conditionals: false } }, expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '.min.js', src: ['wp-includes/js/wp-embed.js'] }, media: { expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '.min.js', src: [ 'wp-includes/js/media-audiovideo.js', 'wp-includes/js/media-grid.js', 'wp-includes/js/media-models.js', 'wp-includes/js/media-views.js' ] }, jqueryui: { options: { // Preserve comments that start with a bang. preserveComments: /^!/ }, expand: true, cwd: SOURCE_DIR, dest: BUILD_DIR, ext: '.min.js', src: ['wp-includes/js/jquery/ui/*.js'] }, bookmarklet: { options: { compress: { negate_iife: false } }, src: SOURCE_DIR + 'wp-admin/js/bookmarklet.js', dest: SOURCE_DIR + 'wp-admin/js/bookmarklet.min.js' }, masonry: { options: { // Preserve comments that start with a bang. preserveComments: /^!/ }, src: SOURCE_DIR + 'wp-includes/js/jquery/jquery.masonry.js', dest: SOURCE_DIR + 'wp-includes/js/jquery/jquery.masonry.min.js' } }, concat: { tinymce: { options: { separator: '\n', process: function( src, filepath ) { return '// Source: ' + filepath.replace( BUILD_DIR, '' ) + '\n' + src; } }, src: [ BUILD_DIR + 'wp-includes/js/tinymce/tinymce.min.js', BUILD_DIR + 'wp-includes/js/tinymce/themes/modern/theme.min.js', BUILD_DIR + 'wp-includes/js/tinymce/plugins/*/plugin.min.js' ], dest: BUILD_DIR + 'wp-includes/js/tinymce/wp-tinymce.js' }, emoji: { options: { separator: '\n', process: function( src, filepath ) { return '// Source: ' + filepath.replace( BUILD_DIR, '' ) + '\n' + src; } }, src: [ BUILD_DIR + 'wp-includes/js/twemoji.min.js', BUILD_DIR + 'wp-includes/js/wp-emoji.min.js' ], dest: BUILD_DIR + 'wp-includes/js/wp-emoji-release.min.js' } }, compress: { tinymce: { options: { mode: 'gzip', level: 9 }, src: '<%= concat.tinymce.dest %>', dest: BUILD_DIR + 'wp-includes/js/tinymce/wp-tinymce.js.gz' } }, jsvalidate:{ options: { globals: {}, esprimaOptions:{}, verbose: false }, build: { files: { src: [ BUILD_DIR + 'wp-{admin,includes}/**/*.js', BUILD_DIR + 'wp-content/themes/twenty*/**/*.js' ] } } }, imagemin: { core: { expand: true, cwd: SOURCE_DIR, src: [ 'wp-{admin,includes}/images/**/*.{png,jpg,gif,jpeg}', 'wp-includes/js/tinymce/skins/wordpress/images/*.{png,jpg,gif,jpeg}' ], dest: SOURCE_DIR } }, includes: { emoji: { src: BUILD_DIR + 'wp-includes/formatting.php', dest: '.' }, embed: { src: BUILD_DIR + 'wp-includes/embed.php', dest: '.' } }, _watch: { all: { files: [ SOURCE_DIR + '**', '!' + SOURCE_DIR + 'wp-includes/js/media/**', // Ignore version control directories. '!' + SOURCE_DIR + '**/.{svn,git}/**' ], tasks: ['clean:dynamic', 'copy:dynamic'], options: { dot: true, spawn: false, interval: 2000 } }, config: { files: 'Gruntfile.js' }, colors: { files: [SOURCE_DIR + 'wp-admin/css/colors/**'], tasks: ['sass:colors'] }, rtl: { files: [ SOURCE_DIR + 'wp-admin/css/*.css', SOURCE_DIR + 'wp-includes/css/*.css' ], tasks: ['rtlcss:dynamic'], options: { spawn: false, interval: 2000 } }, test: { files: [ 'tests/qunit/**', '!tests/qunit/editor/**' ], tasks: ['qunit'] } } }); // Register tasks. // RTL task. grunt.registerTask('rtl', ['rtlcss:core', 'rtlcss:colors']); // Color schemes task. grunt.registerTask('colors', ['sass:colors', 'postcss:colors']); // JSHint task. grunt.registerTask( 'jshint:corejs', [ 'jshint:grunt', 'jshint:tests', 'jshint:themes', 'jshint:core', 'jshint:media' ] ); grunt.renameTask( 'watch', '_watch' ); grunt.registerTask( 'watch', function() { if ( ! this.args.length || this.args.indexOf( 'browserify' ) > -1 ) { grunt.config( 'browserify.options', { browserifyOptions: { debug: true }, watch: true } ); grunt.task.run( 'browserify' ); } grunt.task.run( '_' + this.nameArgs ); } ); grunt.registerTask( 'precommit:image', [ 'imagemin:core' ] ); grunt.registerTask( 'precommit:js', [ 'browserify', 'jshint:corejs', 'uglify:bookmarklet', 'uglify:masonry', 'qunit:compiled' ] ); grunt.registerTask( 'precommit:css', [ 'postcss:core' ] ); grunt.registerTask( 'precommit:php', [ 'phpunit' ] ); grunt.registerTask( 'precommit', 'Runs test and build tasks in preparation for a commit', function() { var done = this.async(); var map = { svn: 'svn status --ignore-externals', git: 'git status --short' }; find( [ __dirname + '/.svn', __dirname + '/.git', path.dirname( __dirname ) + '/.svn' ] ); function find( set ) { var dir; if ( set.length ) { fs.stat( dir = set.shift(), function( error ) { error ? find( set ) : run( path.basename( dir ).substr( 1 ) ); } ); } else { grunt.fatal( 'This WordPress install is not under version control.' ); } } function run( type ) { var command = map[ type ].split( ' ' ); grunt.util.spawn( { cmd: command.shift(), args: command }, function( error, result, code ) { var taskList = []; if ( code !== 0 ) { grunt.fatal( 'The `' + map[ type ] + '` command returned a non-zero exit code.', code ); } // Callback for finding modified paths. function testPath( path ) { var regex = new RegExp( ' ' + path + '$', 'm' ); return regex.test( result.stdout ); } // Callback for finding modified files by extension. function testExtension( extension ) { var regex = new RegExp( '\.' + extension + '$', 'm' ); return regex.test( result.stdout ); } if ( [ 'package.json', 'Gruntfile.js' ].some( testPath ) ) { grunt.log.writeln( 'Configuration files modified. Running `prerelease`.' ); taskList.push( 'prerelease' ); } else { if ( [ 'png', 'jpg', 'gif', 'jpeg' ].some( testExtension ) ) { grunt.log.writeln( 'Image files modified. Minifying.' ); taskList.push( 'precommit:image' ); } [ 'js', 'css', 'php' ].forEach( function( extension ) { if ( testExtension( extension ) ) { grunt.log.writeln( extension.toUpperCase() + ' files modified. ' + extension.toUpperCase() + ' tests will be run.' ); taskList.push( 'precommit:' + extension ); } } ); } grunt.task.run( taskList ); done(); } ); } } ); grunt.registerTask( 'copy:all', [ 'copy:files', 'copy:wp-admin-css-compat-rtl', 'copy:wp-admin-css-compat-min', 'copy:version' ] ); grunt.registerTask( 'build', [ 'clean:all', 'copy:all', 'cssmin:core', 'colors', 'rtl', 'cssmin:rtl', 'cssmin:colors', 'uglify:core', 'uglify:embed', 'uglify:jqueryui', 'concat:tinymce', 'compress:tinymce', 'clean:tinymce', 'concat:emoji', 'includes:emoji', 'includes:embed', 'jsvalidate:build' ] ); grunt.registerTask( 'prerelease', [ 'precommit:php', 'precommit:js', 'precommit:css', 'precommit:image' ] ); // Testing tasks. grunt.registerMultiTask('phpunit', 'Runs PHPUnit tests, including the ajax, external-http, and multisite tests.', function() { grunt.util.spawn({ cmd: this.data.cmd, args: this.data.args, opts: {stdio: 'inherit'} }, this.async()); }); grunt.registerTask('qunit:compiled', 'Runs QUnit tests on compiled as well as uncompiled scripts.', ['build', 'copy:qunit', 'qunit']); grunt.registerTask('test', 'Runs all QUnit and PHPUnit tasks.', ['qunit:compiled', 'phpunit']); // Travis CI tasks. grunt.registerTask('travis:js', 'Runs Javascript Travis CI tasks.', [ 'jshint:corejs', 'qunit:compiled' ]); grunt.registerTask('travis:phpunit', 'Runs PHPUnit Travis CI tasks.', 'phpunit'); // Patch task. grunt.renameTask('patch_wordpress', 'patch'); // Default task. grunt.registerTask('default', ['build']); /* * Automatically updates the `:dynamic` configurations * so that only the changed files are updated. */ grunt.event.on('watch', function( action, filepath, target ) { var src; if ( [ 'all', 'rtl', 'browserify' ].indexOf( target ) === -1 ) { return; } src = [ path.relative( SOURCE_DIR, filepath ) ]; if ( action === 'deleted' ) { grunt.config( [ 'clean', 'dynamic', 'src' ], src ); } else { grunt.config( [ 'copy', 'dynamic', 'src' ], src ); if ( target === 'rtl' ) { grunt.config( [ 'rtlcss', 'dynamic', 'src' ], src ); } } }); };