Tests: Add custom attribute getter tests to the selector module
[jquery.git] / build / tasks / build.js
blobd05f7daf0ca4586c427e190c88331f7f00345660
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 import fs from  "node:fs/promises";
8 import path from  "node:path";
9 import util from  "node:util";
10 import { exec as nodeExec } from "node:child_process";
11 import * as rollup from  "rollup";
12 import excludedFromSlim from  "./lib/slim-exclude.js";
13 import rollupFileOverrides from  "./lib/rollupFileOverridesPlugin.js";
14 import isCleanWorkingDir from  "./lib/isCleanWorkingDir.js";
15 import processForDist from  "./dist.js";
16 import minify from  "./minify.js";
17 import getTimestamp from  "./lib/getTimestamp.js";
18 import { compareSize } from "./lib/compareSize.js";
20 const exec = util.promisify( nodeExec );
21 const pkg = JSON.parse( await fs.readFile( "./package.json", "utf8" ) );
23 const minimum = [ "core" ];
25 // Exclude specified modules if the module matching the key is removed
26 const removeWith = {
27         ajax: [ "manipulation/_evalUrl", "deprecated/ajax-event-alias" ],
28         callbacks: [ "deferred" ],
29         css: [ "effects", "dimensions", "offset" ],
30         "css/showHide": [ "effects" ],
31         deferred: {
32                 remove: [ "ajax", "effects", "queue", "core/ready" ],
33                 include: [ "core/ready-no-deferred" ]
34         },
35         event: [ "deprecated/ajax-event-alias", "deprecated/event" ],
36         selector: [ "css/hiddenVisibleSelectors", "effects/animatedSelector" ]
39 async function read( filename ) {
40         return fs.readFile( path.join( "./src", filename ), "utf8" );
43 // Remove the src folder and file extension
44 // and ensure unix-style path separators
45 function moduleName( filename ) {
46         return filename
47                 .replace( new RegExp( `.*\\${ path.sep }src\\${ path.sep }` ), "" )
48                 .replace( /\.js$/, "" )
49                 .split( path.sep )
50                 .join( path.posix.sep );
53 async function readdirRecursive( dir, all = [] ) {
54         let files;
55         try {
56                 files = await fs.readdir( path.join( "./src", dir ), {
57                         withFileTypes: true
58                 } );
59         } catch ( e ) {
60                 return all;
61         }
62         for ( const file of files ) {
63                 const filepath = path.join( dir, file.name );
65                 if ( file.isDirectory() ) {
66                         all.push( ...( await readdirRecursive( filepath ) ) );
67                 } else {
68                         all.push( moduleName( filepath ) );
69                 }
70         }
71         return all;
74 async function getOutputRollupOptions( {
75         esm = false,
76         factory = false
77 } = {} ) {
78         const wrapperFileName = `wrapper${
79                 factory ? "-factory" : ""
80         }${
81                 esm ? "-esm" : ""
82         }.js`;
84         const wrapperSource = await read( wrapperFileName );
86         // Catch `// @CODE` and subsequent comment lines event if they don't start
87         // in the first column.
88         const wrapper = wrapperSource.split(
89                 /[\x20\t]*\/\/ @CODE\n(?:[\x20\t]*\/\/[^\n]+\n)*/
90         );
92         return {
94                 // The ESM format is not actually used as we strip it during the
95                 // build, inserting our own wrappers; it's just that it doesn't
96                 // generate any extra wrappers so there's nothing for us to remove.
97                 format: "esm",
99                 intro: wrapper[ 0 ].replace( /\n*$/, "" ),
100                 outro: wrapper[ 1 ].replace( /^\n*/, "" )
101         };
104 function unique( array ) {
105         return [ ...new Set( array ) ];
108 async function checkExclude( exclude, include ) {
109         const included = [ ...include ];
110         const excluded = [ ...exclude ];
112         for ( const module of exclude ) {
113                 if ( minimum.indexOf( module ) !== -1 ) {
114                         throw new Error( `Module \"${ module }\" is a minimum requirement.` );
115                 }
117                 // Exclude all files in the dir of the same name
118                 // These are the removable dependencies
119                 // It's fine if the directory is not there
120                 // `selector` is a special case as we don't just remove
121                 // the module, but we replace it with `selector-native`
122                 // which re-uses parts of the `src/selector` dir.
123                 if ( module !== "selector" ) {
124                         const files = await readdirRecursive( module );
125                         excluded.push( ...files );
126                 }
128                 // Check removeWith list
129                 const additional = removeWith[ module ];
130                 if ( additional ) {
131                         const [ additionalExcluded, additionalIncluded ] = await checkExclude(
132                                 additional.remove || additional,
133                                 additional.include || []
134                         );
135                         excluded.push( ...additionalExcluded );
136                         included.push( ...additionalIncluded );
137                 }
138         }
140         return [ unique( excluded ), unique( included ) ];
143 async function getLastModifiedDate() {
144         const { stdout } = await exec( "git log -1 --format=\"%at\"" );
145         return new Date( parseInt( stdout, 10 ) * 1000 );
148 async function writeCompiled( { code, dir, filename, version } ) {
150         // Use the last modified date so builds are reproducible
151         const date = process.env.RELEASE_DATE ?
152                 new Date( process.env.RELEASE_DATE ) :
153                 await getLastModifiedDate();
155         const compiledContents = code
157                 // Embed Version
158                 .replace( /@VERSION/g, version )
160                 // Embed Date
161                 // yyyy-mm-ddThh:mmZ
162                 .replace( /@DATE/g, date.toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );
164         await fs.writeFile( path.join( dir, filename ), compiledContents );
165         console.log( `[${ getTimestamp() }] ${ filename } v${ version } created.` );
168 // Build jQuery ECMAScript modules
169 export async function build( {
170         amd,
171         dir = "dist",
172         exclude = [],
173         filename = "jquery.js",
174         include = [],
175         esm = false,
176         factory = false,
177         slim = false,
178         version,
179         watch = false
180 } = {} ) {
181         const pureSlim = slim && !exclude.length && !include.length;
183         const fileOverrides = new Map();
185         function setOverride( filePath, source ) {
187                 // We want normalized paths in overrides as they will be matched
188                 // against normalized paths in the file overrides Rollup plugin.
189                 fileOverrides.set( path.resolve( filePath ), source );
190         }
192         // Add the short commit hash to the version string
193         // when the version is not for a release.
194         if ( !version ) {
195                 const { stdout } = await exec( "git rev-parse --short HEAD" );
196                 const isClean = await isCleanWorkingDir();
198                 // "+[slim.]SHA" is semantically correct
199                 // Add ".dirty" as well if the working dir is not clean
200                 version = `${ pkg.version }+${ slim ? "slim." : "" }${ stdout.trim() }${
201                         isClean ? "" : ".dirty"
202                 }`;
203         } else if ( slim ) {
204                 version += "+slim";
205         }
207         await fs.mkdir( dir, { recursive: true } );
209         // Exclude slim modules when slim is true
210         const [ excluded, included ] = await checkExclude(
211                 slim ? exclude.concat( excludedFromSlim ) : exclude,
212                 include
213         );
215         // Replace exports/global with a noop noConflict
216         if ( excluded.includes( "exports/global" ) ) {
217                 const index = excluded.indexOf( "exports/global" );
218                 setOverride(
219                         "./src/exports/global.js",
220                         "import { jQuery } from \"../core.js\";\n\n" +
221                                 "jQuery.noConflict = function() {};"
222                 );
223                 excluded.splice( index, 1 );
224         }
226         // Set a desired AMD name.
227         if ( amd != null ) {
228                 if ( amd ) {
229                         console.log( "Naming jQuery with AMD name: " + amd );
230                 } else {
231                         console.log( "AMD name now anonymous" );
232                 }
234                 // Replace the AMD name in the AMD export
235                 // No name means an anonymous define
236                 const amdExportContents = await read( "exports/amd.js" );
237                 setOverride(
238                         "./src/exports/amd.js",
239                         amdExportContents.replace(
241                                 // Remove the comma for anonymous defines
242                                 /(\s*)"jquery"(,\s*)/,
243                                 amd ? `$1\"${ amd }\"$2` : " "
244                         )
245                 );
246         }
248         // Append excluded modules to version.
249         // Skip adding exclusions for slim builds.
250         // Don't worry about semver syntax for these.
251         if ( !pureSlim && excluded.length ) {
252                 version += " -" + excluded.join( ",-" );
253         }
255         // Append extra included modules to version.
256         if ( !pureSlim && included.length ) {
257                 version += " +" + included.join( ",+" );
258         }
260         const inputOptions = {
261                 input: "./src/jquery.js"
262         };
264         const includedImports = included
265                 .map( ( module ) => `import "./${ module }.js";` )
266                 .join( "\n" );
268         const jQueryFileContents = await read( "jquery.js" );
269         if ( include.length ) {
271                 // If include is specified, only add those modules.
272                 setOverride( inputOptions.input, includedImports );
273         } else {
275                 // Remove the jQuery export from the entry file, we'll use our own
276                 // custom wrapper.
277                 setOverride(
278                         inputOptions.input,
279                         jQueryFileContents.replace( /\n*export \{ jQuery, jQuery as \$ };\n*/, "\n" ) +
280                                 includedImports
281                 );
282         }
284         // Replace excluded modules with empty sources.
285         for ( const module of excluded ) {
286                 setOverride(
287                         `./src/${ module }.js`,
289                         // The `selector` module is not removed, but replaced
290                         // with `selector-native`.
291                         module === "selector" ? await read( "selector-native.js" ) : ""
292                 );
293         }
295         const outputOptions = await getOutputRollupOptions( { esm, factory } );
297         if ( watch ) {
298                 const watcher = rollup.watch( {
299                         ...inputOptions,
300                         output: [ outputOptions ],
301                         plugins: [ rollupFileOverrides( fileOverrides ) ],
302                         watch: {
303                                 include: "./src/**",
304                                 skipWrite: true
305                         }
306                 } );
308                 watcher.on( "event", async( event ) => {
309                         switch ( event.code ) {
310                                 case "ERROR":
311                                         console.error( event.error );
312                                         break;
313                                 case "BUNDLE_END":
314                                         const {
315                                                 output: [ { code } ]
316                                         } = await event.result.generate( outputOptions );
318                                         await writeCompiled( {
319                                                 code,
320                                                 dir,
321                                                 filename,
322                                                 version
323                                         } );
325                                         // Don't minify factory files; they are not meant
326                                         // for the browser anyway.
327                                         if ( !factory ) {
328                                                 await minify( { dir, filename, esm } );
329                                         }
330                                         break;
331                         }
332                 } );
334                 return watcher;
335         } else {
336                 const bundle = await rollup.rollup( {
337                         ...inputOptions,
338                         plugins: [ rollupFileOverrides( fileOverrides ) ]
339                 } );
341                 const {
342                         output: [ { code } ]
343                 } = await bundle.generate( outputOptions );
345                 await writeCompiled( { code, dir, filename, version } );
347                 // Don't minify factory files; they are not meant
348                 // for the browser anyway.
349                 if ( !factory ) {
350                         await minify( { dir, filename, esm } );
351                 } else {
353                         // We normally process for dist during minification to save
354                         // file reads. However, some files are not minified and then
355                         // we need to do it separately.
356                         const contents = await fs.readFile(
357                                 path.join( dir, filename ),
358                                 "utf8"
359                         );
360                         processForDist( contents, filename );
361                 }
362         }
365 export async function buildDefaultFiles( {
366         version = process.env.VERSION,
367         watch
368 } = {} ) {
369         await Promise.all( [
370                 build( { version, watch } ),
371                 build( { filename: "jquery.slim.js", slim: true, version, watch } ),
372                 build( {
373                         dir: "dist-module",
374                         filename: "jquery.module.js",
375                         esm: true,
376                         version,
377                         watch
378                 } ),
379                 build( {
380                         dir: "dist-module",
381                         filename: "jquery.slim.module.js",
382                         esm: true,
383                         slim: true,
384                         version,
385                         watch
386                 } ),
388                 build( {
389                         filename: "jquery.factory.js",
390                         factory: true,
391                         version,
392                         watch
393                 } ),
394                 build( {
395                         filename: "jquery.factory.slim.js",
396                         slim: true,
397                         factory: true,
398                         version,
399                         watch
400                 } ),
401                 build( {
402                         dir: "dist-module",
403                         filename: "jquery.factory.module.js",
404                         esm: true,
405                         factory: true,
406                         version,
407                         watch
408                 } ),
409                 build( {
410                         dir: "dist-module",
411                         filename: "jquery.factory.slim.module.js",
412                         esm: true,
413                         slim: true,
414                         factory: true,
415                         version,
416                         watch
417                 } )
418         ] );
420         if ( watch ) {
421                 console.log( "Watching files..." );
422         } else {
423                 return compareSize( {
424                         files: [
425                                 "dist/jquery.min.js",
426                                 "dist/jquery.slim.min.js",
427                                 "dist-module/jquery.module.min.js",
428                                 "dist-module/jquery.slim.module.min.js"
429                         ]
430                 } );
431         }