jquery/build/tasks/build.js
Michał Gołębiowski-Owczarek 9ec09c3b4a
Build: Fix the regex parsing AMD var-modules (#4389)
The previous regex caused the final jQuery binary to have syntax errors for
var-modules with names starting with "return". For example, the following module
wouldn't work when the file is named `returnTrue.js`:

```js
define( function() {
	"use strict";
	return function returnTrue() {
		return true;
	};
} );
```

Closes gh-4389
2019-05-13 21:55:45 +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 );
}
} );
};