2 * Special build task to handle various jQuery build requirements.
3 * Compiles JS modules into one bundle, sets the custom AMD name,
4 * and includes/excludes specified modules
9 module.exports = function( grunt ) {
10 const fs = require( "fs" );
11 const path = require( "path" );
12 const rollup = require( "rollup" );
13 const slimBuildFlags = require( "./lib/slim-build-flags" );
14 const rollupFileOverrides = require( "./lib/rollup-plugin-file-overrides" );
15 const srcFolder = path.resolve( `${ __dirname }/../../src` );
16 const read = function( fileName ) {
17 return grunt.file.read( `${ srcFolder }/${ fileName }` );
20 const inputFileName = "jquery.js";
21 const inputRollupOptions = {
22 input: `${ srcFolder }/${ inputFileName }`
25 function getOutputRollupOptions( {
28 const wrapperFileName = `wrapper${ esm ? "-esm" : "" }.js`;
30 // Catch `// @CODE` and subsequent comment lines event if they don't start
31 // in the first column.
32 const wrapper = read( wrapperFileName )
33 .split( /[\x20\t]*\/\/ @CODE\n(?:[\x20\t]*\/\/[^\n]+\n)*/ );
37 // The ESM format is not actually used as we strip it during the
38 // build, inserting our own wrappers; it's just that it doesn't
39 // generate any extra wrappers so there's nothing for us to remove.
42 intro: `${ wrapper[ 0 ].replace( /\n*$/, "" ) }`,
43 outro: wrapper[ 1 ].replace( /^\n*/, "" )
47 const fileOverrides = new Map();
49 function getOverride( filePath ) {
50 return fileOverrides.get( path.resolve( filePath ) );
53 function setOverride( filePath, source ) {
55 // We want normalized paths in overrides as they will be matched
56 // against normalized paths in the file overrides Rollup plugin.
57 fileOverrides.set( path.resolve( filePath ), source );
60 grunt.registerMultiTask(
62 "Build jQuery ECMAScript modules, " +
63 "(include/exclude modules with +/- flags), embed date/version",
65 const done = this.async();
68 const flags = this.flags;
69 const optIn = flags[ "*" ];
70 let name = grunt.option( "filename" );
71 const esm = !!grunt.option( "esm" );
72 const distFolder = grunt.option( "dist-folder" );
73 const minimum = this.data.minimum;
74 const removeWith = this.data.removeWith;
77 let version = grunt.config( "pkg.version" );
79 // We'll skip printing the whole big exclusions for a bare `build:*:*:slim` which
80 // usually comes from `custom:slim`.
81 const isPureSlim = !!( flags.slim && flags[ "*" ] &&
82 Object.keys( flags ).length === 2 );
88 for ( const flag of slimBuildFlags ) {
95 * Recursively calls the excluder to remove on all modules in the list
97 * @param {String} [prepend] Prepend this to the module name.
98 * Indicates we're walking a directory
100 const excludeList = ( list, prepend ) => {
102 prepend = prepend ? `${ prepend }/` : "";
103 list.forEach( function( module ) {
105 // Exclude var modules as well
106 if ( module === "var" ) {
108 fs.readdirSync( `${ srcFolder }/${ prepend }${ module }` ),
115 // Skip if this is not a js file and we're walking files in a dir
116 if ( !( module = /([\w-\/]+)\.js$/.exec( module ) ) ) {
120 // Prepend folder name if passed
121 // Remove .js extension
122 module = prepend + module[ 1 ];
125 // Avoid infinite recursion
126 if ( excluded.indexOf( module ) === -1 ) {
127 excluder( "-" + module );
134 * Adds the specified module to the excluded or included list, depending on the flag
135 * @param {String} flag A module path relative to
136 * the src directory starting with + or - to indicate
137 * whether it should be included or excluded
139 const excluder = flag => {
141 const m = /^(\+|-|)([\w\/-]+)$/.exec( flag );
142 const exclude = m[ 1 ] === "-";
143 const module = m[ 2 ];
147 // Can't exclude certain modules
148 if ( minimum.indexOf( module ) === -1 ) {
151 if ( excluded.indexOf( module ) === -1 ) {
152 grunt.log.writeln( flag );
153 excluded.push( module );
155 // Exclude all files in the folder of the same name
156 // These are the removable dependencies
157 // It's fine if the directory is not there
160 // `selector` is a special case as we don't just remove
161 // the module, but we replace it with `selector-native`
162 // which re-uses parts of the `src/selector` folder.
163 if ( module !== "selector" ) {
165 fs.readdirSync( `${ srcFolder }/${ module }` ),
170 grunt.verbose.writeln( e );
174 additional = removeWith[ module ];
176 // Check removeWith list
178 excludeList( additional.remove || additional );
179 if ( additional.include ) {
180 included.push( ...additional.include );
181 grunt.log.writeln( "+" + additional.include );
185 grunt.log.error( "Module \"" + module + "\" is a minimum requirement." );
188 grunt.log.writeln( flag );
189 included.push( module );
193 // Filename can be passed to the command line using
194 // command line options
195 // e.g. grunt build --filename=jquery-custom.js
196 name = name ? `${ distFolder }/${ name }` : this.data.dest;
198 // append commit id to version
199 if ( process.env.COMMIT ) {
200 version += " " + process.env.COMMIT;
203 // figure out which files to exclude based on these rules in this order:
204 // dependency explicit exclude
205 // > explicit exclude
206 // > explicit include
207 // > dependency implicit exclude
208 // > implicit exclude
210 // * none (implicit exclude)
211 // *:* all (implicit include)
212 // *:*:-css all except css and dependents (explicit > implicit)
213 // *:*:-css:+effects same (excludes effects because explicit include is
214 // trumped by explicit exclude of dependency)
215 // *:+effects none except effects and its dependencies
216 // (explicit include trumps implicit exclude of dependency)
217 for ( const flag in flags ) {
221 // Remove the jQuery export from the entry file, we'll use our own
223 setOverride( inputRollupOptions.input,
224 read( inputFileName ).replace( /\n*export default jQuery;\n*/, "\n" ) );
226 // Replace exports/global with a noop noConflict
227 if ( excluded.includes( "exports/global" ) ) {
228 const index = excluded.indexOf( "exports/global" );
229 setOverride( `${ srcFolder }/exports/global.js`,
230 "import jQuery from \"../core.js\";\n\n" +
231 "jQuery.noConflict = function() {};" );
232 excluded.splice( index, 1 );
235 // Set a desired AMD name.
236 let amdName = grunt.option( "amd" );
237 if ( amdName != null ) {
239 grunt.log.writeln( "Naming jQuery with AMD name: " + amdName );
241 grunt.log.writeln( "AMD name now anonymous" );
244 // Remove the comma for anonymous defines
245 setOverride( `${ srcFolder }/exports/amd.js`,
246 read( "exports/amd.js" )
247 .replace( /(\s*)"jquery"(,\s*)/,
248 amdName ? "$1\"" + amdName + "\"$2" : "" ) );
251 grunt.verbose.writeflags( excluded, "Excluded" );
252 grunt.verbose.writeflags( included, "Included" );
254 // Indicate a Slim build without listing all the exclusions
259 // Append excluded modules to version.
260 } else if ( excluded.length ) {
261 version += " -" + excluded.join( ",-" );
264 if ( excluded.length ) {
266 // Set pkg.version to version with excludes or with the "slim" marker,
267 // so minified file picks it up but skip the commit hash the same way
268 // it's done for the full build.
269 const commitlessVersion = version.replace( " " + process.env.COMMIT, "" );
270 grunt.config.set( "pkg.version", commitlessVersion );
271 grunt.verbose.writeln( "Version changed to " + commitlessVersion );
273 // Replace excluded modules with empty sources.
274 for ( const module of excluded ) {
276 `${ srcFolder }/${ module }.js`,
278 // The `selector` module is not removed, but replaced
279 // with `selector-native`.
280 module === "selector" ? read( "selector-native.js" ) : ""
285 // Turn off opt-in if necessary
288 // Remove the default inclusions, they will be overwritten with the explicitly
290 setOverride( inputRollupOptions.input, "" );
294 // Import the explicitly included modules.
295 if ( included.length ) {
296 setOverride( inputRollupOptions.input,
297 getOverride( inputRollupOptions.input ) + included
298 .map( module => `import "./${module}.js";` )
302 const bundle = await rollup.rollup( {
303 ...inputRollupOptions,
304 plugins: [ rollupFileOverrides( fileOverrides ) ]
307 const outputRollupOptions =
308 getOutputRollupOptions( { esm } );
310 const { output: [ { code } ] } = await bundle.generate( outputRollupOptions );
312 const compiledContents = code
315 .replace( /@VERSION/g, version )
321 ( new Date() ).toISOString()
322 .replace( /:\d+\.\d+Z$/, "Z" )
325 grunt.file.write( name, compiledContents );
326 grunt.log.ok( `File '${ name }' created.` );
333 // Special "alias" task to make custom build creation less grawlix-y
334 // Translation example
336 // grunt custom:+ajax,-dimensions,-effects,-offset
340 // grunt build:*:*:+ajax:-dimensions:-effects:-offset
342 // There's also a special "slim" alias that resolves to the jQuery Slim build
346 grunt.registerTask( "custom", function() {
347 const args = this.args;
348 const modules = args.length ?
349 args[ 0 ].split( "," ).join( ":" ) :
352 grunt.log.writeln( "Creating custom build...\n" );
353 grunt.task.run( [ "build:*:*" + ( modules ? ":" + modules : "" ), "minify", "dist" ] );