aboutsummaryrefslogtreecommitdiffstats
path: root/build/tasks
diff options
context:
space:
mode:
authorTimmy Willison <timmywil@users.noreply.github.com>2023-09-20 18:18:42 -0400
committerGitHub <noreply@github.com>2023-09-20 18:18:42 -0400
commitec8802bafefaebd99e4bfc0956fb409d9054d871 (patch)
tree19fb82ea61bae624b35bd31bee8ba1e6bd080fa0 /build/tasks
parent6fe88690a336a883ea310da76762e5a456dc487a (diff)
downloadjquery-ec8802bafefaebd99e4bfc0956fb409d9054d871.tar.gz
jquery-ec8802bafefaebd99e4bfc0956fb409d9054d871.zip
Build: migrate most grunt tasks off of grunt (3.x)
Close gh-5330 - lint - npmcopy - build, minify, and process for distribution. - new custom build command using yargs - compare size of minified/gzip built files - pretest scripts, including qunit-fixture, babel transpilation, and npmcopy - node smoke tests - promises aplus tests - new watch task using nodemon, which runs `npm run build:all` on `src` changes. Also: - upgraded husky and added the new lint command - updated lint config to use new "flat" config format. See https://eslint.org/docs/latest/use/configure/configuration-files-new - Temporarily disabled one lint rule until flat config is supported by eslint-plugin-import. See https://github.com/import-js/eslint-plugin-import/issues/2556 - committed package-lock.json - updated all test scripts to use the new build - added an express test server that uses middleware-mockserver this can be used to run tests without karma - build-all-variants is now build:all - run pretest script in jenkins --------- Co-authored-by: Michał Gołębiowski-Owczarek <m.goleb@gmail.com>
Diffstat (limited to 'build/tasks')
-rw-r--r--build/tasks/build.js531
-rw-r--r--build/tasks/compare_size.mjs174
-rw-r--r--build/tasks/dist.js94
-rw-r--r--build/tasks/lib/getTimestamp.js9
-rw-r--r--build/tasks/lib/isCleanWorkingDir.js9
-rw-r--r--build/tasks/lib/slim-exclude.js (renamed from build/tasks/lib/slim-build-flags.js)4
-rw-r--r--build/tasks/lib/spawn_test.js16
-rw-r--r--build/tasks/lib/verifyNodeVersion.js12
-rw-r--r--build/tasks/minify.js83
-rw-r--r--build/tasks/node_smoke_tests.js80
-rw-r--r--build/tasks/npmcopy.js42
-rw-r--r--build/tasks/promises_aplus_tests.js45
-rw-r--r--build/tasks/qunit-fixture.js17
-rw-r--r--build/tasks/qunit_fixture.js22
-rw-r--r--build/tasks/sourcemap.js17
-rw-r--r--build/tasks/testswarm.js62
16 files changed, 693 insertions, 524 deletions
diff --git a/build/tasks/build.js b/build/tasks/build.js
index a411ca872..50f6675c3 100644
--- a/build/tasks/build.js
+++ b/build/tasks/build.js
@@ -1,50 +1,85 @@
/**
- * Special concat/build task to handle various jQuery build requirements
- * Concats AMD modules, removes their definitions,
+ * Special build task to handle various jQuery build requirements.
+ * Compiles JS modules into one bundle, sets the custom AMD name,
* and includes/excludes specified modules
*/
"use strict";
-module.exports = function( grunt ) {
- var fs = require( "fs" ),
- requirejs = require( "requirejs" ),
- slimBuildFlags = require( "./lib/slim-build-flags" ),
- 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,
+const fs = require( "fs" );
+const path = require( "path" );
+const util = require( "util" );
+const exec = util.promisify( require( "child_process" ).exec );
+const requirejs = require( "requirejs" );
+const excludedFromSlim = require( "./lib/slim-exclude" );
+const pkg = require( "../../package.json" );
+const isCleanWorkingDir = require( "./lib/isCleanWorkingDir" );
+const minify = require( "./minify" );
+const getTimestamp = require( "./lib/getTimestamp" );
+const verifyNodeVersion = require( "./lib/verifyNodeVersion" );
+const srcFolder = path.resolve( __dirname, "../../src" );
+
+const rdefineEnd = /\}\s*?\);[^}\w]*$/;
+
+const minimum = [ "core" ];
+
+// Exclude specified modules if the module matching the key is removed
+const removeWith = {
+ ajax: [ "manipulation/_evalUrl", "deprecated/ajax-event-alias" ],
+ callbacks: [ "deferred" ],
+ css: [ "effects", "dimensions", "offset" ],
+ "css/showHide": [ "effects" ],
+ deferred: {
+ remove: [ "ajax", "effects", "queue", "core/ready" ],
+ include: [ "core/ready-no-deferred" ]
+ },
+ event: [ "deprecated/ajax-event-alias", "deprecated/event" ],
+ selector: [ "css/hiddenVisibleSelectors", "effects/animatedSelector" ]
+};
- // We have multiple minify steps
- optimize: "none",
+async function read( filename ) {
+ return fs.promises.readFile( path.join( srcFolder, filename ), "utf8" );
+}
+
+// Remove the src folder and file extension
+// and ensure unix-style path separators
+function moduleName( filename ) {
+ return filename
+ .replace( `${srcFolder}${path.sep}`, "" )
+ .replace( /\.js$/, "" )
+ .split( path.sep )
+ .join( path.posix.sep );
+}
+
+async function readdirRecursive( dir, all = [] ) {
+ let files;
+ try {
+ files = await fs.promises.readdir( path.join( srcFolder, dir ), {
+ withFileTypes: true
+ } );
+ } catch ( e ) {
+ return all;
+ }
+ for ( const file of files ) {
+ const filepath = path.join( dir, file.name );
- // Include dependencies loaded with require
- findNestedDependencies: true,
+ if ( file.isDirectory() ) {
+ all.push( ...( await readdirRecursive( filepath ) ) );
+ } else {
+ all.push( moduleName( filepath ) );
+ }
+ }
+ return all;
+}
- // Avoid inserting define() placeholder
- skipModuleInsertion: true,
+async function getRequireConfig( { amd } = {} ) {
+ const wrapperSource = await read( "wrapper.js" );
- // Avoid breaking semicolons inserted by r.js
- skipSemiColonInsertion: true,
- wrap: {
- start: wrapper[ 0 ].replace( /\/\*\s*eslint(?: |-).*\s*\*\/\n/, "" ),
- end: wrapper[ 1 ]
- },
- rawText: {},
- onBuildWrite: convert
- };
+ // Catch `// @CODE` and subsequent comment lines event if they don't start
+ // in the first column.
+ const wrapper = wrapperSource.split(
+ /[\x20\t]*\/\/ @CODE\n(?:[\x20\t]*\/\/[^\n]+\n)*/
+ );
/**
* Strip all definitions generated by requirejs
@@ -57,7 +92,6 @@ module.exports = function( grunt ) {
* @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(), "" ) ) ) {
@@ -96,273 +130,214 @@ module.exports = function( grunt ) {
}
// AMD Name
- if ( ( amdName = grunt.option( "amd" ) ) != null && /^exports\/amd$/.test( name ) ) {
- if ( amdName ) {
- grunt.log.writeln( "Naming jQuery with AMD name: " + amdName );
+ if ( amd != null && /^exports\/amd$/.test( name ) ) {
+ if ( amd ) {
+ console.log( "Naming jQuery with AMD name: " + amd );
} else {
- grunt.log.writeln( "AMD name now anonymous" );
+ console.log( "AMD name now anonymous" );
}
// Remove the comma for anonymous defines
contents = contents
- .replace( /(\s*)"jquery"(\,\s*)/, amdName ? "$1\"" + amdName + "\"$2" : "" );
+ .replace( /(\s*)"jquery"(\,\s*)/, amd ? "$1\"" + amd + "\"$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 be included or excluded
- */
- excluder = function( flag ) {
- var additional,
- m = /^(\+|-|)([\w\/-]+)$/.exec( flag ),
- exclude = m[ 1 ] === "-",
- module = m[ 2 ];
-
- // Recognize the legacy `sizzle` alias
- if ( module === "sizzle" ) {
- module = "selector";
- }
-
- 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 {
-
- // `selector` is a special case as we don't just remove
- // the module, but we replace it with `selector-native`
- // which re-uses parts of the `src/selector` folder.
- if ( module !== "selector" ) {
- 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." );
- }
- } 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;
- }
+ return {
+ 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,
- // 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 );
+ // Avoid breaking semicolons inserted by r.js
+ skipSemiColonInsertion: true,
+ wrap: {
+ start: wrapper[ 0 ].replace( /\/\*\s*eslint(?: |-).*\s*\*\/\n/, "" ),
+ end: wrapper[ 1 ]
+ },
+ rawText: {},
+ onBuildWrite: convert
+ };
+}
+
+function unique( array ) {
+ return [ ...new Set( array ) ];
+}
+
+async function checkExclude( exclude, include ) {
+ const included = [ ...include ];
+ const excluded = [ ...exclude ];
+
+ for ( const module of exclude ) {
+ if ( minimum.indexOf( module ) !== -1 ) {
+ throw new Error( `Module \"${module}\" is a minimum requirement.` );
}
- // Handle full selector module exclusion.
- // Replace with selector-native.
- if ( excluded.indexOf( "selector" ) > -1 ) {
- config.rawText.selector = "define([ \"./selector-native\" ]);";
+ // Exclude all files in the dir of the same name
+ // These are the removable dependencies
+ // It's fine if the directory is not there
+ // `selector` is a special case as we don't just remove
+ // the module, but we replace it with `selector-native`
+ // which re-uses parts of the `src/selector` folder.
+ // "sizzle" is legacy for selector
+ if ( module !== "selector" && module !== "sizzle" ) {
+ const files = await readdirRecursive( module );
+ excluded.push( ...files );
}
- // Replace exports/global with a noop noConflict
- if ( ( index = excluded.indexOf( "exports/global" ) ) > -1 ) {
- config.rawText[ "exports/global" ] = "define( [\n\t\"../core\"\n], " +
- "function( jQuery ) {\n\tjQuery.noConflict = function() {};\n} );";
- excluded.splice( index, 1 );
+ // Check removeWith list
+ const additional = removeWith[ module ];
+ if ( additional ) {
+ const [ additionalExcluded, additionalIncluded ] = await checkExclude(
+ additional.remove || additional,
+ additional.include || []
+ );
+ excluded.push( ...additionalExcluded );
+ included.push( ...additionalIncluded );
}
+ }
- grunt.verbose.writeflags( excluded, "Excluded" );
- grunt.verbose.writeflags( included, "Included" );
+ return [ unique( excluded ), unique( included ) ];
+}
+
+async function build( {
+ amd,
+ dir = "dist",
+ exclude = [],
+ filename = "jquery.js",
+ include = [],
+ slim = false,
+ version
+} = {} ) {
+
+ // Add the short commit hash to the version string
+ // when the version is not for a release.
+ if ( !version ) {
+ const { stdout } = await exec( "git rev-parse --short HEAD" );
+ const isClean = await isCleanWorkingDir();
+
+ // Add "+SHA" if the version is not set.
+ // Add ".dirty" as well if the working dir is not clean.
+ version = `${pkg.version}+${stdout.trim()}${isClean ? "" : ".dirty"}`;
+ }
- // append excluded modules to version
+ await fs.promises.mkdir( dir, { recursive: true } );
+
+ // Exclude slim modules when slim is true
+ const [ excluded, included ] = await checkExclude(
+ slim ? exclude.concat( excludedFromSlim ) : exclude,
+ include
+ );
+ const config = await getRequireConfig( { amd } );
+
+ // Replace exports/global with a noop noConflict
+ if ( excluded.includes( "exports/global" ) ) {
+ const index = excluded.indexOf( "exports/global" );
+ config.rawText[ "exports/global" ] = "define( [\n\t\"../core\"\n], " +
+ "function( jQuery ) {\n\tjQuery.noConflict = function() {};\n} );";
+ excluded.splice( index, 1 );
+ }
+
+ // "sizzle" is legacy for selector
+ if ( excluded.indexOf( "selector" ) > -1 || excluded.indexOf( "sizzle" ) > -1 ) {
+ config.rawText.selector = "define( [ \"./selector-native\" ] );";
+ }
+
+ if ( excluded.length ) {
+
+ // Append excluded modules to version.
+ // Skip adding exclusions for slim builds.
+ // Don't worry about semver syntax for these.
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;
+ }
- // Have to use shallow or core will get excluded since it is a dependency
- config.excludeShallow = excluded;
- }
+ if ( included.length ) {
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( [\n" +
- ( included.length ?
- included.map( module => "\t\"./" + module + "\"" ).join( ",\n" ) :
- "" ) +
- "\n] );";
+ // Append extra included modules to version.
+ if ( included.length ) {
+ version += " +" + 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 );
+ // Overwrite the default inclusions with the explicit ones provided
+ config.rawText.jquery = "define( [\n" +
+ included.map( module => "\t\"./" + module + "\"" ).join( ",\n" ) +
+ "\n] );";
+ }
+
+ /**
+ * Handle Final output from the optimizer
+ * @param {String} compiled
+ */
+ config.out = async function( compiled ) {
+ const compiledContents = 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
+ await fs.promises.writeFile(
+ path.join( dir, filename ),
+ compiledContents
+ );
+ };
+
+ await new Promise( ( resolve, reject ) => {
+ requirejs.optimize( config, () => {
+
+ // Wait a beat. For some reason, the write can
+ // take a moment after this to complete in Node 10.
+ setTimeout( resolve, 100 );
+ }, ( error ) => {
+ console.error( error );
+ reject( error );
} );
} );
- // 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
- //
- // There's also a special "slim" alias that resolves to the jQuery Slim build
- // configuration:
- //
- // grunt custom:slim
- grunt.registerTask( "custom", function() {
- var args = this.args,
- modules = args.length ?
- args[ 0 ]
- .split( "," )
-
- // Replace "slim" with respective exclusions meant for
- // the official slim build
- .reduce( ( acc, elem ) => acc.concat(
- elem === "slim" ?
- slimBuildFlags :
- [ elem ]
- ), [] )
-
- .join( ":" ) :
- "";
-
- grunt.log.writeln( "Creating custom build...\n" );
- grunt.task.run( [
- "build:*:*" + ( modules ? ":" + modules : "" ),
- "uglify",
- "remove_map_comment",
- "dist"
- ] );
+ console.log( `[${getTimestamp()}] ${filename} v${version} created.` );
+
+ await minify( { filename, dir } );
+}
+
+async function buildDefaultFiles( { version } = {} ) {
+ await Promise.all( [
+ build( { version } ),
+ build( { filename: "jquery.slim.js", slim: true, version } )
+ ] );
+
+ // Earlier Node.js versions do not support the ESM format.
+ if ( !verifyNodeVersion() ) {
+ return;
+ }
+
+ const { compareSize } = await import( "./compare_size.mjs" );
+ return compareSize( {
+ files: [
+ "dist/jquery.min.js",
+ "dist/jquery.slim.min.js"
+ ]
} );
-};
+}
+
+module.exports = { build, buildDefaultFiles };
diff --git a/build/tasks/compare_size.mjs b/build/tasks/compare_size.mjs
new file mode 100644
index 000000000..f1af39e71
--- /dev/null
+++ b/build/tasks/compare_size.mjs
@@ -0,0 +1,174 @@
+import chalk from "chalk";
+import fs from "node:fs";
+import { promisify } from "node:util";
+import zlib from "node:zlib";
+import { exec as nodeExec } from "node:child_process";
+import isCleanWorkingDir from "./lib/isCleanWorkingDir.js";
+
+const VERSION = 1;
+
+const gzip = promisify( zlib.gzip );
+const exec = promisify( nodeExec );
+
+async function getBranchName() {
+ const { stdout } = await exec( "git rev-parse --abbrev-ref HEAD" );
+ return stdout.trim();
+}
+
+async function getCommitHash() {
+ const { stdout } = await exec( "git rev-parse HEAD" );
+ return stdout.trim();
+}
+
+function getBranchHeader( branch, commit ) {
+ let branchHeader = branch.trim();
+ if ( commit ) {
+ branchHeader = chalk.bold( branchHeader ) + chalk.gray( ` @${commit}` );
+ } else {
+ branchHeader = chalk.italic( branchHeader );
+ }
+ return branchHeader;
+}
+
+async function getCache( loc ) {
+ let cache;
+ try {
+ const contents = await fs.promises.readFile( loc, "utf8" );
+ cache = JSON.parse( contents );
+ } catch ( err ) {
+ return {};
+ }
+
+ const lastRun = cache[ " last run" ];
+ if ( !lastRun || !lastRun.meta || lastRun.meta.version !== VERSION ) {
+ console.log( "Compare cache version mismatch. Rewriting..." );
+ return {};
+ }
+ return cache;
+}
+
+function cacheResults( results ) {
+ const files = Object.create( null );
+ results.forEach( function( result ) {
+ files[ result.filename ] = {
+ raw: result.raw,
+ gz: result.gz
+ };
+ } );
+ return files;
+}
+
+function saveCache( loc, cache ) {
+ return fs.promises.writeFile( loc, JSON.stringify( cache ) );
+}
+
+function compareSizes( existing, current, padLength ) {
+ if ( typeof current !== "number" ) {
+ return chalk.grey( `${existing}`.padStart( padLength ) );
+ }
+ const delta = current - existing;
+ if ( delta > 0 ) {
+ return chalk.red( `+${delta}`.padStart( padLength ) );
+ }
+ return chalk.green( `${delta}`.padStart( padLength ) );
+}
+
+export async function compareSize( { cache = ".sizecache.json", files } = {} ) {
+ if ( !files || !files.length ) {
+ throw new Error( "No files specified" );
+ }
+
+ const branch = await getBranchName();
+ const commit = await getCommitHash();
+ const sizeCache = await getCache( cache );
+
+ let rawPadLength = 0;
+ let gzPadLength = 0;
+ const results = await Promise.all(
+ files.map( async function( filename ) {
+
+ let contents = await fs.promises.readFile( filename, "utf8" );
+
+ // Remove the short SHA and .dirty from comparisons.
+ // The short SHA so commits can be compared against each other
+ // and .dirty to compare with the existing branch during development.
+ const sha = /jQuery v\d+.\d+.\d+(?:-\w+)?\+(?:slim.)?([^ \.]+(?:\.dirty)?)/.exec( contents )[ 1 ];
+ contents = contents.replace( new RegExp( sha, "g" ), "" );
+
+ const size = Buffer.byteLength( contents, "utf8" );
+ const gzippedSize = ( await gzip( contents ) ).length;
+
+ // Add one to give space for the `+` or `-` in the comparison
+ rawPadLength = Math.max( rawPadLength, size.toString().length + 1 );
+ gzPadLength = Math.max( gzPadLength, gzippedSize.toString().length + 1 );
+
+ return { filename, raw: size, gz: gzippedSize };
+ } )
+ );
+
+ const sizeHeader = "raw".padStart( rawPadLength ) +
+ "gz".padStart( gzPadLength + 1 ) +
+ " Filename";
+
+ const sizes = results.map( function( result ) {
+ const rawSize = result.raw.toString().padStart( rawPadLength );
+ const gzSize = result.gz.toString().padStart( gzPadLength );
+ return `${rawSize} ${gzSize} ${result.filename}`;
+ } );
+
+ const comparisons = Object.keys( sizeCache ).map( function( branch ) {
+ const meta = sizeCache[ branch ].meta || {};
+ const commit = meta.commit;
+
+ const files = sizeCache[ branch ].files;
+ const branchSizes = Object.keys( files ).map( function( filename ) {
+ const branchResult = files[ filename ];
+ const compareResult = results.find( function( result ) {
+ return result.filename === filename;
+ } ) || {};
+
+ const compareRaw = compareSizes( branchResult.raw, compareResult.raw, rawPadLength );
+ const compareGz = compareSizes( branchResult.gz, compareResult.gz, gzPadLength );
+ return `${compareRaw} ${compareGz} ${filename}`;
+ } );
+
+ return [
+ "", // New line before each branch
+ getBranchHeader( branch, commit ),
+ sizeHeader,
+ ...branchSizes
+ ].join( "\n" );
+ } );
+
+ const output = [
+ "", // Opening new line
+ chalk.bold( "Sizes" ),
+ sizeHeader,
+ ...sizes,
+ ...comparisons,
+ "" // Closing new line
+ ].join( "\n" );
+
+ console.log( output );
+
+ // Always save the last run
+ // Save version under last run
+ sizeCache[ " last run" ] = {
+ meta: { version: VERSION },
+ files: cacheResults( results )
+ };
+
+ // Only save cache for the current branch
+ // if the working directory is clean.
+ if ( await isCleanWorkingDir() ) {
+ sizeCache[ branch ] = {
+ meta: { commit },
+ files: cacheResults( results )
+ };
+ console.log( `Saved cache for ${branch}.` );
+ }
+
+ await saveCache( cache, sizeCache );
+
+ return results;
+}
diff --git a/build/tasks/dist.js b/build/tasks/dist.js
index 33a26ca35..f15689e3d 100644
--- a/build/tasks/dist.js
+++ b/build/tasks/dist.js
@@ -1,71 +1,31 @@
"use strict";
-module.exports = function( grunt ) {
- const fs = require( "fs" );
- const filename = grunt.option( "filename" );
- const distPaths = [
- `dist/${ filename }`,
- `dist/${ filename.replace( ".js", ".min.js" ) }`,
- `dist/${ filename.replace( ".js", ".min.map" ) }`
- ];
-
- // Process files for distribution
- grunt.registerTask( "dist", function() {
- let stored, flags, paths, nonascii;
-
- // Check for stored destination paths
- // ( set in dist/.destination.json )
- stored = Object.keys( grunt.config( "dst" ) );
-
- // Allow command line input as well
- flags = Object.keys( this.flags );
-
- // Combine all output target paths
- paths = [].concat( stored, flags ).filter( function( path ) {
- return path !== "*";
- } );
-
- // Ensure the dist files are pure ASCII
- nonascii = false;
-
- distPaths.forEach( function( filename ) {
- let i, c;
- const text = fs.readFileSync( filename, "utf8" );
-
- // Ensure files use only \n for line endings, not \r\n
- if ( /\x0d\x0a/.test( text ) ) {
- grunt.log.writeln( filename + ": Incorrect line endings (\\r\\n)" );
- nonascii = true;
+// Process files for distribution.
+module.exports = function processForDist( text, filename ) {
+ if ( !text ) {
+ throw new Error( "text required for processForDist" );
+ }
+
+ if ( !filename ) {
+ throw new Error( "filename required for processForDist" );
+ }
+
+ // Ensure files use only \n for line endings, not \r\n
+ if ( /\x0d\x0a/.test( text ) ) {
+ throw new Error( filename + ": Incorrect line endings (\\r\\n)" );
+ }
+
+ // Ensure only ASCII chars so script tags don't need a charset attribute
+ if ( text.length !== Buffer.byteLength( text, "utf8" ) ) {
+ let message = filename + ": Non-ASCII characters detected:\n";
+ for ( let i = 0; i < text.length; i++ ) {
+ const c = text.charCodeAt( i );
+ if ( c > 127 ) {
+ message += "- position " + i + ": " + c + "\n";
+ message += "==> " + text.substring( i - 20, i + 20 );
+ break;
}
-
- // Ensure only ASCII chars so script tags don't need a charset attribute
- if ( text.length !== Buffer.byteLength( text, "utf8" ) ) {
- grunt.log.writeln( filename + ": Non-ASCII characters detected:" );
- for ( i = 0; i < text.length; i++ ) {
- c = text.charCodeAt( i );
- if ( c > 127 ) {
- grunt.log.writeln( "- position " + i + ": " + c );
- grunt.log.writeln( "-- " + text.substring( i - 20, i + 20 ) );
- break;
- }
- }
- nonascii = true;
- }
-
- // Optionally copy dist files to other locations
- paths.forEach( function( path ) {
- let created;
-
- if ( !/\/$/.test( path ) ) {
- path += "/";
- }
-
- created = path + filename.replace( "dist/", "" );
- grunt.file.write( created, text );
- grunt.log.writeln( "File '" + created + "' created." );
- } );
- } );
-
- return !nonascii;
- } );
+ }
+ throw new Error( message );
+ }
};
diff --git a/build/tasks/lib/getTimestamp.js b/build/tasks/lib/getTimestamp.js
new file mode 100644
index 000000000..c3c8aed11
--- /dev/null
+++ b/build/tasks/lib/getTimestamp.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = function getTimestamp() {
+ const now = new Date();
+ const hours = now.getHours().toString().padStart( 2, "0" );
+ const minutes = now.getMinutes().toString().padStart( 2, "0" );
+ const seconds = now.getSeconds().toString().padStart( 2, "0" );
+ return `${hours}:${minutes}:${seconds}`;
+};
diff --git a/build/tasks/lib/isCleanWorkingDir.js b/build/tasks/lib/isCleanWorkingDir.js
new file mode 100644
index 000000000..16c87fd9d
--- /dev/null
+++ b/build/tasks/lib/isCleanWorkingDir.js
@@ -0,0 +1,9 @@
+"use strict";
+
+const util = require( "util" );
+const exec = util.promisify( require( "child_process" ).exec );
+
+module.exports = async function isCleanWorkingDir() {
+ const { stdout } = await exec( "git status --untracked-files=no --porcelain" );
+ return !stdout.trim();
+};
diff --git a/build/tasks/lib/slim-build-flags.js b/build/tasks/lib/slim-exclude.js
index ac34692fa..8f5d84e43 100644
--- a/build/tasks/lib/slim-build-flags.js
+++ b/build/tasks/lib/slim-exclude.js
@@ -2,6 +2,6 @@
// NOTE: keep it in sync with test/data/testinit.js
module.exports = [
- "-ajax",
- "-effects"
+ "ajax",
+ "effects"
];
diff --git a/build/tasks/lib/spawn_test.js b/build/tasks/lib/spawn_test.js
deleted file mode 100644
index 146155411..000000000
--- a/build/tasks/lib/spawn_test.js
+++ /dev/null
@@ -1,16 +0,0 @@
-"use strict";
-
-// Run Node with provided parameters: the first one being the Grunt
-// done function and latter ones being files to be tested.
-// See the comment in ../node_smoke_tests.js for more information.
-module.exports = function spawnTest( done, command ) {
- var spawn = require( "child_process" ).spawn;
-
- spawn( command, {
- stdio: "inherit",
- shell: true
- } )
- .on( "close", function( code ) {
- done( code === 0 );
- } );
-};
diff --git a/build/tasks/lib/verifyNodeVersion.js b/build/tasks/lib/verifyNodeVersion.js
new file mode 100644
index 000000000..80d57ed6e
--- /dev/null
+++ b/build/tasks/lib/verifyNodeVersion.js
@@ -0,0 +1,12 @@
+"use strict";
+
+const { version } = require( "process" );
+const nodeV16OrNewer = !/^v1[0-5]\./.test( version );
+
+module.exports = function verifyNodeVersion() {
+ if ( !nodeV16OrNewer ) {
+ console.log( "Old Node.js detected, task skipped..." );
+ return false;
+ }
+ return true;
+};
diff --git a/build/tasks/minify.js b/build/tasks/minify.js
new file mode 100644
index 000000000..edb289cd3
--- /dev/null
+++ b/build/tasks/minify.js
@@ -0,0 +1,83 @@
+"use strict";
+
+const UglifyJS = require( "uglify-js" );
+const fs = require( "fs" );
+const path = require( "path" );
+const processForDist = require( "./dist" );
+const getTimestamp = require( "./lib/getTimestamp" );
+
+const rjs = /\.js$/;
+
+module.exports = async function minify( { dir, filename } ) {
+ const filepath = path.join( dir, filename );
+ const contents = await fs.promises.readFile( filepath, "utf8" );
+ const version = /jQuery JavaScript Library ([^\n]+)/.exec( contents )[ 1 ];
+ const banner = `/*! jQuery ${version}` +
+ " | (c) OpenJS Foundation and other contributors" +
+ " | jquery.org/license */";
+
+ const minFilename = filename.replace( rjs, ".min.js" );
+ const mapFilename = filename.replace( rjs, ".min.map" );
+
+ const { code, error, map: incompleteMap, warning } = UglifyJS.minify(
+ contents,
+ {
+ compress: {
+ hoist_funs: false,
+ loops: false,
+
+ // Support: IE <11
+ // typeofs transformation is unsafe for IE9-10
+ // See https://github.com/mishoo/UglifyJS2/issues/2198
+ typeofs: false
+ },
+ output: {
+ ascii_only: true,
+
+ // Support: Android 4.0 only
+ // UglifyJS 3 breaks Android 4.0 if this option is not enabled.
+ // This is in lieu of setting ie for all of mangle, compress, and output
+ ie8: true,
+ preamble: banner
+ },
+ sourceMap: {
+ filename: minFilename
+ }
+ }
+ );
+
+ if ( error ) {
+ throw new Error( error );
+ }
+
+ if ( warning ) {
+ console.warn( warning );
+ }
+
+ // The map's `sources` property is set to an array index.
+ // Fix it by setting it to the correct filename.
+ const map = JSON.stringify( {
+ ...JSON.parse( incompleteMap ),
+ file: minFilename,
+ sources: [ filename ]
+ } );
+
+ await Promise.all( [
+ fs.promises.writeFile(
+ path.join( dir, minFilename ),
+ code
+ ),
+ fs.promises.writeFile(
+ path.join( dir, mapFilename ),
+ map
+ )
+ ] );
+
+ // Always process files for dist
+ // Doing it here avoids extra file reads
+ processForDist( contents, filename );
+ processForDist( code, minFilename );
+ processForDist( map, mapFilename );
+
+ console.log( `[${getTimestamp()}] ${minFilename} ${version} with ${mapFilename} created.` );
+};
diff --git a/build/tasks/node_smoke_tests.js b/build/tasks/node_smoke_tests.js
index a814b6b01..dd74d180c 100644
--- a/build/tasks/node_smoke_tests.js
+++ b/build/tasks/node_smoke_tests.js
@@ -1,42 +1,42 @@
"use strict";
-module.exports = ( grunt ) => {
- const fs = require( "fs" );
- const spawnTest = require( "./lib/spawn_test.js" );
- const nodeV16OrNewer = !/^v1[0-5]\./.test( process.version );
-
- grunt.registerTask( "node_smoke_tests", function( jQueryModuleSpecifier = "./dist/jquery.js" ) {
- if ( !nodeV16OrNewer ) {
- grunt.log.writeln( "Old Node.js detected, running the task " +
- `"node_smoke_tests:${ jQueryModuleSpecifier }" skipped...` );
- return;
- }
-
- const testsDir = "./test/node_smoke_tests";
- const nodeSmokeTests = [];
-
- // Fire up all tests defined in test/node_smoke_tests/*.js in spawned sub-processes.
- // All the files under test/node_smoke_tests/*.js are supposed to exit with 0 code
- // on success or another one on failure. Spawning in sub-processes is
- // important so that the tests & the main process don't interfere with
- // each other, e.g. so that they don't share the `require` cache.
-
- fs.readdirSync( testsDir )
- .filter( ( testFilePath ) =>
- fs.statSync( `${ testsDir }/${ testFilePath }` ).isFile() &&
- /\.[cm]?js$/.test( testFilePath )
- )
- .forEach( ( testFilePath ) => {
- const taskName = `node_${ testFilePath.replace( /\.[cm]?js$/, "" ) }:${ jQueryModuleSpecifier }`;
-
- grunt.registerTask( taskName, function() {
- spawnTest( this.async(), `node "${ testsDir }/${
- testFilePath }" ${ jQueryModuleSpecifier }` );
- } );
-
- nodeSmokeTests.push( taskName );
- } );
-
- grunt.task.run( nodeSmokeTests );
- } );
-};
+const fs = require( "fs" );
+const util = require( "util" );
+const exec = util.promisify( require( "child_process" ).exec );
+const verifyNodeVersion = require( "./lib/verifyNodeVersion" );
+
+if ( !verifyNodeVersion() ) {
+ return;
+}
+
+// Fire up all tests defined in test/node_smoke_tests/*.js in spawned sub-processes.
+// All the files under test/node_smoke_tests/*.js are supposed to exit with 0 code
+// on success or another one on failure. Spawning in sub-processes is
+// important so that the tests & the main process don't interfere with
+// each other, e.g. so that they don't share the `require` cache.
+
+async function runTests( { module } ) {
+ const dir = "./test/node_smoke_tests";
+ const files = await fs.promises.readdir( dir, { withFileTypes: true } );
+ const testFiles = files.filter( ( testFilePath ) => testFilePath.isFile() );
+
+ if ( !testFiles.length ) {
+ throw new Error( `No test files found for "${module}"` );
+ }
+
+ await Promise.all(
+ testFiles.map( ( testFile ) =>
+ exec( `node "${dir}/${testFile.name}" ${module}` )
+ )
+ );
+ console.log( `Node smoke tests passed for "${module}".` );
+}
+
+async function runDefaultTests() {
+ await Promise.all( [
+ runTests( { module: "./dist/jquery.js" } ),
+ runTests( { module: "./dist/jquery.slim.js" } )
+ ] );
+}
+
+runDefaultTests();
diff --git a/build/tasks/npmcopy.js b/build/tasks/npmcopy.js
new file mode 100644
index 000000000..c57acc2e7
--- /dev/null
+++ b/build/tasks/npmcopy.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const fs = require( "fs" );
+const path = require( "path" );
+
+const projectDir = path.resolve( __dirname, "..", ".." );
+
+const files = {
+ "bootstrap/bootstrap.css": "bootstrap/dist/css/bootstrap.css",
+ "bootstrap/bootstrap.min.css": "bootstrap/dist/css/bootstrap.min.css",
+ "bootstrap/bootstrap.min.css.map": "bootstrap/dist/css/bootstrap.min.css.map",
+
+ "core-js-bundle/core-js-bundle.js": "core-js-bundle/minified.js",
+ "core-js-bundle/LICENSE": "core-js-bundle/LICENSE",
+
+ "npo/npo.js": "native-promise-only/lib/npo.src.js",
+
+ "qunit/qunit.js": "qunit/qunit/qunit.js",
+ "qunit/qunit.css": "qunit/qunit/qunit.css",
+ "qunit/LICENSE.txt": "qunit/LICENSE.txt",
+
+ "requirejs/require.js": "requirejs/require.js",
+
+ "sinon/sinon.js": "sinon/pkg/sinon.js",
+ "sinon/LICENSE.txt": "sinon/LICENSE"
+};
+
+async function npmcopy() {
+ await fs.promises.mkdir( path.resolve( projectDir, "external" ), {
+ recursive: true
+ } );
+ for ( const [ dest, source ] of Object.entries( files ) ) {
+ const from = path.resolve( projectDir, "node_modules", source );
+ const to = path.resolve( projectDir, "external", dest );
+ const toDir = path.dirname( to );
+ await fs.promises.mkdir( toDir, { recursive: true } );
+ await fs.promises.copyFile( from, to );
+ console.log( `${source} → ${dest}` );
+ }
+}
+
+npmcopy();
diff --git a/build/tasks/promises_aplus_tests.js b/build/tasks/promises_aplus_tests.js
index cb1a4fddb..b078e3c5b 100644
--- a/build/tasks/promises_aplus_tests.js
+++ b/build/tasks/promises_aplus_tests.js
@@ -1,27 +1,32 @@
"use strict";
-module.exports = grunt => {
- const timeout = 2000;
- const spawnTest = require( "./lib/spawn_test.js" );
+const { spawn } = require( "child_process" );
+const verifyNodeVersion = require( "./lib/verifyNodeVersion" );
+const path = require( "path" );
+const os = require( "os" );
- grunt.registerTask( "promises_aplus_tests",
- [ "promises_aplus_tests:deferred", "promises_aplus_tests:when" ] );
+if ( !verifyNodeVersion() ) {
+ return;
+}
- grunt.registerTask( "promises_aplus_tests:deferred", function() {
- spawnTest( this.async(),
- "\"" + __dirname + "/../../node_modules/.bin/promises-aplus-tests\"" +
- " test/promises_aplus_adapters/deferred.js" +
- " --reporter dot" +
- " --timeout " + timeout
- );
- } );
+const command = path.resolve(
+ __dirname,
+ `../../node_modules/.bin/promises-aplus-tests${os.platform() === "win32" ? ".cmd" : ""}`
+);
+const args = [ "--reporter", "dot", "--timeout", "2000" ];
+const tests = [
+ "test/promises_aplus_adapters/deferred.js",
+ "test/promises_aplus_adapters/when.js"
+];
- grunt.registerTask( "promises_aplus_tests:when", function() {
- spawnTest( this.async(),
- "\"" + __dirname + "/../../node_modules/.bin/promises-aplus-tests\"" +
- " test/promises_aplus_adapters/when.js" +
- " --reporter dot" +
- " --timeout " + timeout
+async function runTests() {
+ tests.forEach( ( test ) => {
+ spawn(
+ command,
+ [ test ].concat( args ),
+ { stdio: "inherit" }
);
} );
-};
+}
+
+runTests();
diff --git a/build/tasks/qunit-fixture.js b/build/tasks/qunit-fixture.js
new file mode 100644
index 000000000..3059bb929
--- /dev/null
+++ b/build/tasks/qunit-fixture.js
@@ -0,0 +1,17 @@
+"use strict";
+
+const fs = require( "fs" );
+
+async function generateFixture() {
+ const fixture = await fs.promises.readFile( "./test/data/qunit-fixture.html", "utf8" );
+ await fs.promises.writeFile(
+ "./test/data/qunit-fixture.js",
+ "// Generated by build/tasks/qunit-fixture.js\n" +
+ "QUnit.config.fixture = " +
+ JSON.stringify( fixture.replace( /\r\n/g, "\n" ) ) +
+ ";\n"
+ );
+ console.log( "Updated ./test/data/qunit-fixture.js" );
+}
+
+generateFixture();
diff --git a/build/tasks/qunit_fixture.js b/build/tasks/qunit_fixture.js
deleted file mode 100644
index 138ca662d..000000000
--- a/build/tasks/qunit_fixture.js
+++ /dev/null
@@ -1,22 +0,0 @@
-"use strict";
-
-var fs = require( "fs" );
-
-module.exports = function( grunt ) {
- grunt.registerTask( "qunit_fixture", function() {
- var dest = "./test/data/qunit-fixture.js";
- fs.writeFileSync(
- dest,
- "// Generated by build/tasks/qunit_fixture.js\n" +
- "QUnit.config.fixture = " +
- JSON.stringify(
- fs.readFileSync(
- "./test/data/qunit-fixture.html",
- "utf8"
- ).toString().replace( /\r\n/g, "\n" )
- ) +
- ";\n"
- );
- grunt.log.ok( "Updated " + dest + "." );
- } );
-};
diff --git a/build/tasks/sourcemap.js b/build/tasks/sourcemap.js
deleted file mode 100644
index 509374f2e..000000000
--- a/build/tasks/sourcemap.js
+++ /dev/null
@@ -1,17 +0,0 @@
-"use strict";
-
-var fs = require( "fs" );
-
-module.exports = function( grunt ) {
- var config = grunt.config( "uglify.all.files" );
- grunt.registerTask( "remove_map_comment", function() {
- var minLoc = grunt.config.process( Object.keys( config )[ 0 ] );
-
- // Remove the source map comment; it causes way too many problems.
- // The map file is still generated for manual associations
- // https://github.com/jquery/jquery/issues/1707
- var text = fs.readFileSync( minLoc, "utf8" )
- .replace( /\/\/# sourceMappingURL=\S+/, "" );
- fs.writeFileSync( minLoc, text );
- } );
-};
diff --git a/build/tasks/testswarm.js b/build/tasks/testswarm.js
deleted file mode 100644
index d2653e0e0..000000000
--- a/build/tasks/testswarm.js
+++ /dev/null
@@ -1,62 +0,0 @@
-"use strict";
-
-module.exports = function( grunt ) {
- grunt.registerTask( "testswarm", function( commit, configFile, projectName, browserSets,
- timeout, testMode ) {
- var jobName, config, tests,
- testswarm = require( "testswarm" ),
- runs = {},
- done = this.async(),
- pull = /PR-(\d+)/.exec( commit );
-
- projectName = projectName || "jquery";
- config = grunt.file.readJSON( configFile )[ projectName ];
- browserSets = browserSets || config.browserSets;
- if ( browserSets[ 0 ] === "[" ) {
-
- // We got an array, parse it
- browserSets = JSON.parse( browserSets );
- }
- timeout = timeout || 1000 * 60 * 15;
- tests = grunt.config( [ this.name, "tests" ] );
-
- if ( pull ) {
- jobName = "Pull <a href='https://github.com/jquery/jquery/pull/" +
- pull[ 1 ] + "'>#" + pull[ 1 ] + "</a>";
- } else {
- jobName = "Commit <a href='https://github.com/jquery/jquery/commit/" +
- commit + "'>" + commit.substr( 0, 10 ) + "</a>";
- }
-
- if ( testMode === "basic" ) {
- runs.basic = config.testUrl + commit + "/test/index.html?module=basic";
- } else {
- tests.forEach( function( test ) {
- runs[ test ] = config.testUrl + commit + "/test/index.html?module=" + test;
- } );
- }
-
- testswarm.createClient( {
- url: config.swarmUrl
- } )
- .addReporter( testswarm.reporters.cli )
- .auth( {
- id: config.authUsername,
- token: config.authToken
- } )
- .addjob(
- {
- name: jobName,
- runs: runs,
- runMax: config.runMax,
- browserSets: browserSets,
- timeout: timeout
- }, function( err, passed ) {
- if ( err ) {
- grunt.log.error( err );
- }
- done( passed );
- }
- );
- } );
-};