diff options
author | Timmy Willison <timmywil@users.noreply.github.com> | 2023-09-20 18:18:42 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-20 18:18:42 -0400 |
commit | ec8802bafefaebd99e4bfc0956fb409d9054d871 (patch) | |
tree | 19fb82ea61bae624b35bd31bee8ba1e6bd080fa0 /build/tasks | |
parent | 6fe88690a336a883ea310da76762e5a456dc487a (diff) | |
download | jquery-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.js | 531 | ||||
-rw-r--r-- | build/tasks/compare_size.mjs | 174 | ||||
-rw-r--r-- | build/tasks/dist.js | 94 | ||||
-rw-r--r-- | build/tasks/lib/getTimestamp.js | 9 | ||||
-rw-r--r-- | build/tasks/lib/isCleanWorkingDir.js | 9 | ||||
-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.js | 16 | ||||
-rw-r--r-- | build/tasks/lib/verifyNodeVersion.js | 12 | ||||
-rw-r--r-- | build/tasks/minify.js | 83 | ||||
-rw-r--r-- | build/tasks/node_smoke_tests.js | 80 | ||||
-rw-r--r-- | build/tasks/npmcopy.js | 42 | ||||
-rw-r--r-- | build/tasks/promises_aplus_tests.js | 45 | ||||
-rw-r--r-- | build/tasks/qunit-fixture.js | 17 | ||||
-rw-r--r-- | build/tasks/qunit_fixture.js | 22 | ||||
-rw-r--r-- | build/tasks/sourcemap.js | 17 | ||||
-rw-r--r-- | build/tasks/testswarm.js | 62 |
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 ); - } - ); - } ); -}; |