jquery/build/tasks/build.js
Michał Gołębiowski-Owczarek 438b1a3e8a
Build: ESLint: forbid unused function parameters
This commit requires all function parameters to be used, not just the last one.
In cases where that's not possible as we need to match an external API, there's
an escape hatch of prefixing an unused argument with `_`.

This change makes it easier to catch unused AMD dependencies and unused
parameters in internal functions the API of which we may change at will, among
other things.

Unused AMD dependencies have been removed as part of this commit.

Closes gh-4381
2019-05-13 22:25:11 +02:00

383 lines
11 KiB
JavaScript

/**
* Special concat/build task to handle various jQuery build requirements
* Concats AMD modules, removes their definitions,
* and includes/excludes specified modules
*/
module.exports = function( grunt ) {
"use strict";
var fs = require( "fs" ),
requirejs = require( "requirejs" ),
Insight = require( "insight" ),
pkg = require( "../../package.json" ),
srcFolder = __dirname + "/../../src/",
rdefineEnd = /\}\s*?\);[^}\w]*$/,
read = function( fileName ) {
return grunt.file.read( srcFolder + fileName );
},
// Catch `// @CODE` and subsequent comment lines event if they don't start
// in the first column.
wrapper = read( "wrapper.js" ).split( /[\x20\t]*\/\/ @CODE\n(?:[\x20\t]*\/\/[^\n]+\n)*/ ),
config = {
baseUrl: "src",
name: "jquery",
// Allow strict mode
useStrict: true,
// We have multiple minify steps
optimize: "none",
// Include dependencies loaded with require
findNestedDependencies: true,
// Avoid inserting define() placeholder
skipModuleInsertion: true,
// Avoid breaking semicolons inserted by r.js
skipSemiColonInsertion: true,
wrap: {
start: wrapper[ 0 ].replace( /\/\*\s*eslint(?: |-).*\s*\*\/\n/, "" ),
end: wrapper[ 1 ]
},
rawText: {},
onBuildWrite: convert
};
/**
* Strip all definitions generated by requirejs
* Convert "var" modules to var declarations
* "var module" means the module only contains a return
* statement that should be converted to a var declaration
* This is indicated by including the file in any "var" folder
* @param {String} name
* @param {String} path
* @param {String} contents The contents to be written (including their AMD wrappers)
*/
function convert( name, path, contents ) {
var amdName;
// Convert var modules
if ( /.\/var\//.test( path.replace( process.cwd(), "" ) ) ) {
contents = contents
.replace(
/define\(\s*(["'])[\w\W]*?\1[\w\W]*?return/,
"var " +
( /var\/([\w-]+)/.exec( name )[ 1 ] ) +
" ="
)
.replace( rdefineEnd, "" );
// Sizzle treatment
} else if ( /\/sizzle$/.test( name ) ) {
contents = "var Sizzle =\n" + contents
// Remove EXPOSE lines from Sizzle
.replace( /\/\/\s*EXPOSE[\w\W]*\/\/\s*EXPOSE/, "return Sizzle;" );
} else {
contents = contents
.replace( /\s*return\s+[^\}]+(\}\s*?\);[^\w\}]*)$/, "$1" )
// Multiple exports
.replace( /\s*exports\.\w+\s*=\s*\w+;/g, "" );
// Remove define wrappers, closure ends, and empty declarations
contents = contents
.replace( /define\([^{]*?{\s*(?:("|')use strict\1(?:;|))?/, "" )
.replace( rdefineEnd, "" );
// Remove anything wrapped with
// /* ExcludeStart */ /* ExcludeEnd */
// or a single line directly after a // BuildExclude comment
contents = contents
.replace( /\/\*\s*ExcludeStart\s*\*\/[\w\W]*?\/\*\s*ExcludeEnd\s*\*\//ig, "" )
.replace( /\/\/\s*BuildExclude\n\r?[\w\W]*?\n\r?/ig, "" );
// Remove empty definitions
contents = contents
.replace( /define\(\[[^\]]*\]\)[\W\n]+$/, "" );
}
// AMD Name
if ( ( amdName = grunt.option( "amd" ) ) != null && /^exports\/amd$/.test( name ) ) {
if ( amdName ) {
grunt.log.writeln( "Naming jQuery with AMD name: " + amdName );
} else {
grunt.log.writeln( "AMD name now anonymous" );
}
// Remove the comma for anonymous defines
contents = contents
.replace( /(\s*)"jquery"(\,\s*)/, amdName ? "$1\"" + amdName + "\"$2" : "" );
}
return contents;
}
grunt.registerMultiTask(
"build",
"Concatenate source, remove sub AMD definitions, " +
"(include/exclude modules with +/- flags), embed date/version",
function() {
var flag, index,
done = this.async(),
flags = this.flags,
optIn = flags[ "*" ],
name = grunt.option( "filename" ),
minimum = this.data.minimum,
removeWith = this.data.removeWith,
excluded = [],
included = [],
version = grunt.config( "pkg.version" ),
/**
* Recursively calls the excluder to remove on all modules in the list
* @param {Array} list
* @param {String} [prepend] Prepend this to the module name.
* Indicates we're walking a directory
*/
excludeList = function( list, prepend ) {
if ( list ) {
prepend = prepend ? prepend + "/" : "";
list.forEach( function( module ) {
// Exclude var modules as well
if ( module === "var" ) {
excludeList(
fs.readdirSync( srcFolder + prepend + module ), prepend + module
);
return;
}
if ( prepend ) {
// Skip if this is not a js file and we're walking files in a dir
if ( !( module = /([\w-\/]+)\.js$/.exec( module ) ) ) {
return;
}
// Prepend folder name if passed
// Remove .js extension
module = prepend + module[ 1 ];
}
// Avoid infinite recursion
if ( excluded.indexOf( module ) === -1 ) {
excluder( "-" + module );
}
} );
}
},
/**
* Adds the specified module to the excluded or included list, depending on the flag
* @param {String} flag A module path relative to
* the src directory starting with + or - to indicate
* whether it should included or excluded
*/
excluder = function( flag ) {
var additional,
m = /^(\+|\-|)([\w\/-]+)$/.exec( flag ),
exclude = m[ 1 ] === "-",
module = m[ 2 ];
if ( exclude ) {
// Can't exclude certain modules
if ( minimum.indexOf( module ) === -1 ) {
// Add to excluded
if ( excluded.indexOf( module ) === -1 ) {
grunt.log.writeln( flag );
excluded.push( module );
// Exclude all files in the folder of the same name
// These are the removable dependencies
// It's fine if the directory is not there
try {
excludeList( fs.readdirSync( srcFolder + module ), module );
} catch ( e ) {
grunt.verbose.writeln( e );
}
}
additional = removeWith[ module ];
// Check removeWith list
if ( additional ) {
excludeList( additional.remove || additional );
if ( additional.include ) {
included = included.concat( additional.include );
grunt.log.writeln( "+" + additional.include );
}
}
} else {
grunt.log.error( "Module \"" + module + "\" is a minimum requirement." );
if ( module === "selector" ) {
grunt.log.error(
"If you meant to replace Sizzle, use -sizzle instead."
);
}
}
} else {
grunt.log.writeln( flag );
included.push( module );
}
};
// Filename can be passed to the command line using
// command line options
// e.g. grunt build --filename=jquery-custom.js
name = name ? ( "dist/" + name ) : this.data.dest;
// append commit id to version
if ( process.env.COMMIT ) {
version += " " + process.env.COMMIT;
}
// figure out which files to exclude based on these rules in this order:
// dependency explicit exclude
// > explicit exclude
// > explicit include
// > dependency implicit exclude
// > implicit exclude
// examples:
// * none (implicit exclude)
// *:* all (implicit include)
// *:*:-css all except css and dependents (explicit > implicit)
// *:*:-css:+effects same (excludes effects because explicit include is
// trumped by explicit exclude of dependency)
// *:+effects none except effects and its dependencies
// (explicit include trumps implicit exclude of dependency)
delete flags[ "*" ];
for ( flag in flags ) {
excluder( flag );
}
// Handle Sizzle exclusion
// Replace with selector-native
if ( ( index = excluded.indexOf( "sizzle" ) ) > -1 ) {
config.rawText.selector = "define(['./selector-native']);";
excluded.splice( index, 1 );
}
// Replace exports/global with a noop noConflict
if ( ( index = excluded.indexOf( "exports/global" ) ) > -1 ) {
config.rawText[ "exports/global" ] = "define(['../core']," +
"function( jQuery ) {\njQuery.noConflict = function() {};\n});";
excluded.splice( index, 1 );
}
grunt.verbose.writeflags( excluded, "Excluded" );
grunt.verbose.writeflags( included, "Included" );
// append excluded modules to version
if ( excluded.length ) {
version += " -" + excluded.join( ",-" );
// set pkg.version to version with excludes, so minified file picks it up
grunt.config.set( "pkg.version", version );
grunt.verbose.writeln( "Version changed to " + version );
// Have to use shallow or core will get excluded since it is a dependency
config.excludeShallow = excluded;
}
config.include = included;
/**
* Handle Final output from the optimizer
* @param {String} compiled
*/
config.out = function( compiled ) {
compiled = compiled
// Embed Version
.replace( /@VERSION/g, version )
// Embed Date
// yyyy-mm-ddThh:mmZ
.replace( /@DATE/g, ( new Date() ).toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );
// Write concatenated source to file
grunt.file.write( name, compiled );
};
// Turn off opt-in if necessary
if ( !optIn ) {
// Overwrite the default inclusions with the explicit ones provided
config.rawText.jquery = "define([" +
( included.length ? included.join( "," ) : "" ) +
"]);";
}
// Trace dependencies and concatenate files
requirejs.optimize( config, function( response ) {
grunt.verbose.writeln( response );
grunt.log.ok( "File '" + name + "' created." );
done();
}, function( err ) {
done( err );
} );
} );
// Special "alias" task to make custom build creation less grawlix-y
// Translation example
//
// grunt custom:+ajax,-dimensions,-effects,-offset
//
// Becomes:
//
// grunt build:*:*:+ajax:-dimensions:-effects:-offset
grunt.registerTask( "custom", function() {
var args = this.args,
modules = args.length ? args[ 0 ].replace( /,/g, ":" ) : "",
done = this.async(),
insight = new Insight( {
trackingCode: "UA-1076265-4",
pkg: pkg
} );
function exec( trackingAllowed ) {
var tracks = args.length ? args[ 0 ].split( "," ) : [];
var defaultPath = [ "build", "custom" ];
tracks = tracks.map( function( track ) {
return track.replace( /\//g, "+" );
} );
if ( trackingAllowed ) {
// Track individuals
tracks.forEach( function( module ) {
var path = defaultPath.concat( [ "individual" ], module );
insight.track.apply( insight, path );
} );
// Track full command
insight.track.apply( insight, defaultPath.concat( [ "full" ], tracks ) );
}
grunt.task.run( [ "build:*:*" + ( modules ? ":" + modules : "" ), "uglify", "dist" ] );
done();
}
grunt.log.writeln( "Creating custom build...\n" );
// Ask for permission the first time
if ( insight.optOut === undefined ) {
insight.askPermission( null, function( _error, result ) {
exec( result );
} );
} else {
exec( !insight.optOut );
}
} );
};