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
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
27 ajax: [ "manipulation/_evalUrl", "deprecated/ajax-event-alias" ],
28 callbacks: [ "deferred" ],
29 css: [ "effects", "dimensions", "offset" ],
30 "css/showHide": [ "effects" ],
32 remove: [ "ajax", "effects", "queue", "core/ready" ],
33 include: [ "core/ready-no-deferred" ]
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 ) {
47 .replace( new RegExp( `.*\\${ path.sep }src\\${ path.sep }` ), "" )
48 .replace( /\.js$/, "" )
50 .join( path.posix.sep );
53 async function readdirRecursive( dir, all = [] ) {
56 files = await fs.readdir( path.join( "./src", dir ), {
62 for ( const file of files ) {
63 const filepath = path.join( dir, file.name );
65 if ( file.isDirectory() ) {
66 all.push( ...( await readdirRecursive( filepath ) ) );
68 all.push( moduleName( filepath ) );
74 async function getOutputRollupOptions( {
78 const wrapperFileName = `wrapper${
79 factory ? "-factory" : ""
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)*/
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.
99 intro: wrapper[ 0 ].replace( /\n*$/, "" ),
100 outro: wrapper[ 1 ].replace( /^\n*/, "" )
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.` );
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 );
128 // Check removeWith list
129 const additional = removeWith[ module ];
131 const [ additionalExcluded, additionalIncluded ] = await checkExclude(
132 additional.remove || additional,
133 additional.include || []
135 excluded.push( ...additionalExcluded );
136 included.push( ...additionalIncluded );
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
158 .replace( /@VERSION/g, version )
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( {
173 filename = "jquery.js",
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 );
192 // Add the short commit hash to the version string
193 // when the version is not for a release.
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"
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,
215 // Replace exports/global with a noop noConflict
216 if ( excluded.includes( "exports/global" ) ) {
217 const index = excluded.indexOf( "exports/global" );
219 "./src/exports/global.js",
220 "import { jQuery } from \"../core.js\";\n\n" +
221 "jQuery.noConflict = function() {};"
223 excluded.splice( index, 1 );
226 // Set a desired AMD name.
229 console.log( "Naming jQuery with AMD name: " + amd );
231 console.log( "AMD name now anonymous" );
234 // Replace the AMD name in the AMD export
235 // No name means an anonymous define
236 const amdExportContents = await read( "exports/amd.js" );
238 "./src/exports/amd.js",
239 amdExportContents.replace(
241 // Remove the comma for anonymous defines
242 /(\s*)"jquery"(,\s*)/,
243 amd ? `$1\"${ amd }\"$2` : " "
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( ",-" );
255 // Append extra included modules to version.
256 if ( !pureSlim && included.length ) {
257 version += " +" + included.join( ",+" );
260 const inputOptions = {
261 input: "./src/jquery.js"
264 const includedImports = included
265 .map( ( module ) => `import "./${ module }.js";` )
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 );
275 // Remove the jQuery export from the entry file, we'll use our own
279 jQueryFileContents.replace( /\n*export \{ jQuery, jQuery as \$ };\n*/, "\n" ) +
284 // Replace excluded modules with empty sources.
285 for ( const module of excluded ) {
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" ) : ""
295 const outputOptions = await getOutputRollupOptions( { esm, factory } );
298 const watcher = rollup.watch( {
300 output: [ outputOptions ],
301 plugins: [ rollupFileOverrides( fileOverrides ) ],
308 watcher.on( "event", async( event ) => {
309 switch ( event.code ) {
311 console.error( event.error );
316 } = await event.result.generate( outputOptions );
318 await writeCompiled( {
325 // Don't minify factory files; they are not meant
326 // for the browser anyway.
328 await minify( { dir, filename, esm } );
336 const bundle = await rollup.rollup( {
338 plugins: [ rollupFileOverrides( fileOverrides ) ]
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.
350 await minify( { dir, filename, esm } );
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 ),
360 processForDist( contents, filename );
365 export async function buildDefaultFiles( {
366 version = process.env.VERSION,
370 build( { version, watch } ),
371 build( { filename: "jquery.slim.js", slim: true, version, watch } ),
374 filename: "jquery.module.js",
381 filename: "jquery.slim.module.js",
389 filename: "jquery.factory.js",
395 filename: "jquery.factory.slim.js",
403 filename: "jquery.factory.module.js",
411 filename: "jquery.factory.slim.module.js",
421 console.log( "Watching files..." );
423 return compareSize( {
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"