Build: replace CRLF with LF during minify
[jquery.git] / build / tasks / build.js
blob79498d012a3af2d4aa2fc43ee4f71de86d349442
1 /**
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
5  */
7 "use strict";
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 }` );
18         };
20         const inputFileName = "jquery.js";
21         const inputRollupOptions = {
22                 input: `${ srcFolder }/${ inputFileName }`
23         };
25         function getOutputRollupOptions( {
26                 esm = false
27         } = {} ) {
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)*/ );
35                 return {
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.
40                         format: "esm",
42                         intro: `${ wrapper[ 0 ].replace( /\n*$/, "" ) }`,
43                         outro: wrapper[ 1 ].replace( /^\n*/, "" )
44                 };
45         }
47         const fileOverrides = new Map();
49         function getOverride( filePath ) {
50                 return fileOverrides.get( path.resolve( filePath ) );
51         }
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 );
58         }
60         grunt.registerMultiTask(
61                 "build",
62                 "Build jQuery ECMAScript modules, " +
63                         "(include/exclude modules with +/- flags), embed date/version",
64         async function() {
65                 const done = this.async();
67                 try {
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;
75                         const excluded = [];
76                         const included = [];
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 );
84                         delete flags[ "*" ];
86                         if ( flags.slim ) {
87                                 delete flags.slim;
88                                 for ( const flag of slimBuildFlags ) {
89                                         flags[ flag ] = true;
90                                 }
91                         }
94                         /**
95                          * Recursively calls the excluder to remove on all modules in the list
96                          * @param {Array} list
97                          * @param {String} [prepend] Prepend this to the module name.
98                          *  Indicates we're walking a directory
99                          */
100                         const excludeList = ( list, prepend ) => {
101                                 if ( list ) {
102                                         prepend = prepend ? `${ prepend }/` : "";
103                                         list.forEach( function( module ) {
105                                                 // Exclude var modules as well
106                                                 if ( module === "var" ) {
107                                                         excludeList(
108                                                                 fs.readdirSync( `${ srcFolder }/${ prepend }${ module }` ),
109                                                                 prepend + module
110                                                         );
111                                                         return;
112                                                 }
113                                                 if ( prepend ) {
115                                                         // Skip if this is not a js file and we're walking files in a dir
116                                                         if ( !( module = /([\w-\/]+)\.js$/.exec( module ) ) ) {
117                                                                 return;
118                                                         }
120                                                         // Prepend folder name if passed
121                                                         // Remove .js extension
122                                                         module = prepend + module[ 1 ];
123                                                 }
125                                                 // Avoid infinite recursion
126                                                 if ( excluded.indexOf( module ) === -1 ) {
127                                                         excluder( "-" + module );
128                                                 }
129                                         } );
130                                 }
131                         };
133                         /**
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
138                          */
139                         const excluder = flag => {
140                                 let additional;
141                                 const m = /^(\+|-|)([\w\/-]+)$/.exec( flag );
142                                 const exclude = m[ 1 ] === "-";
143                                 const module = m[ 2 ];
145                                 if ( exclude ) {
147                                         // Can't exclude certain modules
148                                         if ( minimum.indexOf( module ) === -1 ) {
150                                                 // Add to excluded
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
158                                                         try {
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" ) {
164                                                                         excludeList(
165                                                                                 fs.readdirSync( `${ srcFolder }/${ module }` ),
166                                                                                 module
167                                                                         );
168                                                                 }
169                                                         } catch ( e ) {
170                                                                 grunt.verbose.writeln( e );
171                                                         }
172                                                 }
174                                                 additional = removeWith[ module ];
176                                                 // Check removeWith list
177                                                 if ( additional ) {
178                                                         excludeList( additional.remove || additional );
179                                                         if ( additional.include ) {
180                                                                 included.push( ...additional.include );
181                                                                 grunt.log.writeln( "+" + additional.include );
182                                                         }
183                                                 }
184                                         } else {
185                                                 grunt.log.error( "Module \"" + module + "\" is a minimum requirement." );
186                                         }
187                                 } else {
188                                         grunt.log.writeln( flag );
189                                         included.push( module );
190                                 }
191                         };
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;
201                         }
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
209                         // examples:
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 ) {
218                                 excluder( flag );
219                         }
221                         // Remove the jQuery export from the entry file, we'll use our own
222                         // custom wrapper.
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 );
233                         }
235                         // Set a desired AMD name.
236                         let amdName = grunt.option( "amd" );
237                         if ( amdName != null ) {
238                                 if ( amdName ) {
239                                         grunt.log.writeln( "Naming jQuery with AMD name: " + amdName );
240                                 } else {
241                                         grunt.log.writeln( "AMD name now anonymous" );
242                                 }
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" : "" ) );
249                         }
251                         grunt.verbose.writeflags( excluded, "Excluded" );
252                         grunt.verbose.writeflags( included, "Included" );
254                         // Indicate a Slim build without listing all the exclusions
255                         // to save space.
256                         if ( isPureSlim ) {
257                                 version += " slim";
259                         // Append excluded modules to version.
260                         } else if ( excluded.length ) {
261                                 version += " -" + excluded.join( ",-" );
262                         }
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 ) {
275                                         setOverride(
276                                                 `${ srcFolder }/${ module }.js`,
278                                                 // The `selector` module is not removed, but replaced
279                                                 // with `selector-native`.
280                                                 module === "selector" ? read( "selector-native.js" ) : ""
281                                         );
282                                 }
283                         }
285                         // Turn off opt-in if necessary
286                         if ( !optIn ) {
288                                 // Remove the default inclusions, they will be overwritten with the explicitly
289                                 // included ones.
290                                 setOverride( inputRollupOptions.input, "" );
292                         }
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";` )
299                                                 .join( "\n" ) );
300                         }
302                         const bundle = await rollup.rollup( {
303                                 ...inputRollupOptions,
304                                 plugins: [ rollupFileOverrides( fileOverrides ) ]
305                         } );
307                         const outputRollupOptions =
308                                 getOutputRollupOptions( { esm } );
310                         const { output: [ { code } ] } = await bundle.generate( outputRollupOptions );
312                         const compiledContents = code
314                                 // Embed Version
315                                 .replace( /@VERSION/g, version )
317                                 // Embed Date
318                                 // yyyy-mm-ddThh:mmZ
319                                 .replace(
320                                         /@DATE/g,
321                                         ( new Date() ).toISOString()
322                                                 .replace( /:\d+\.\d+Z$/, "Z" )
323                                 );
325                         grunt.file.write( name, compiledContents );
326                         grunt.log.ok( `File '${ name }' created.` );
327                         done();
328                 } catch ( err ) {
329                         done( err );
330                 }
331         } );
333         // Special "alias" task to make custom build creation less grawlix-y
334         // Translation example
335         //
336         //   grunt custom:+ajax,-dimensions,-effects,-offset
337         //
338         // Becomes:
339         //
340         //   grunt build:*:*:+ajax:-dimensions:-effects:-offset
341         //
342         // There's also a special "slim" alias that resolves to the jQuery Slim build
343         // configuration:
344         //
345         //   grunt custom:slim
346         grunt.registerTask( "custom", function() {
347                 const args = this.args;
348                 const modules = args.length ?
349                         args[ 0 ].split( "," ).join( ":" ) :
350                         "";
352                 grunt.log.writeln( "Creating custom build...\n" );
353                 grunt.task.run( [ "build:*:*" + ( modules ? ":" + modules : "" ), "minify", "dist" ] );
354         } );