aboutsummaryrefslogtreecommitdiffstats
path: root/build
diff options
context:
space:
mode:
authorTimmy Willison <timmywil@users.noreply.github.com>2023-07-27 11:24:49 -0400
committerTimmy Willison <timmywil@users.noreply.github.com>2024-07-29 15:25:14 -0400
commit2cf659189e65b36c6cea9625a3f9c848e3ed3e0d (patch)
tree14bf32c46089e4da880fd07508384e9cda4ae243 /build
parent3b2330240c9941f8749b4162167b18de8f13ea38 (diff)
downloadjquery-2cf659189e65b36c6cea9625a3f9c848e3ed3e0d.tar.gz
jquery-2cf659189e65b36c6cea9625a3f9c848e3ed3e0d.zip
Release: migrate release process to release-it
*Authors* - Checking and updating authors has been migrated to a custom script in the repo *Changelog* - changelogplease is no longer maintained - generate changelog in markdown for GitHub releases - generate changelog in HTML for blog posts - generate contributors list in HTML for blog posts *dist* - clone dist repo, copy files, and commit/push - commit tag with dist files on main branch; remove dist files from main branch after release *cdn* - clone cdn repo, copy files, and commit/push - create versioned and unversioned copies in cdn/ - generate md5 sums and archives for Google and MSFT *build* - implement reproducible builds and verify release builds * uses the last modified date for the latest commit * See https://reproducible-builds.org/ - the verify workflow also ensures all files were properly published to the CDN and npm *docs* - the new release workflow is documented at build/release/README.md *verify* - use the last modified date of the commit before the tag - use versioned filenames when checking map files on the CDN - skip factory and package.json files when verifying CDN *misc* - now that we don't need the jquery-release script and now that we no longer need to build on Node 10, we can use ESM in all files in the build folder - limit certain workflows to the main repo (not forks) - version has been set to the previously released version 3.7.1, as release-it expects - release-it added the `preReleaseBase` option and we now always set it to `1` in the npm script. This is a noop for stable releases. - include post-release script to be run manually after a release, with further steps that should be verified manually Ref jquery/jquery-release#114 Closes gh-5522
Diffstat (limited to 'build')
-rwxr-xr-xbuild/command.js8
-rw-r--r--build/package.json3
-rw-r--r--build/release.js82
-rw-r--r--build/release/README.md123
-rw-r--r--build/release/archive.js59
-rw-r--r--build/release/authors.js27
-rw-r--r--build/release/cdn.js253
-rw-r--r--build/release/changelog.js239
-rw-r--r--build/release/dist.js294
-rw-r--r--build/release/post-release.sh60
-rw-r--r--build/release/pre-release.sh21
-rw-r--r--build/release/verify.js243
-rw-r--r--build/tasks/build.js63
-rw-r--r--build/tasks/dist.js6
-rw-r--r--build/tasks/lib/compareSize.js (renamed from build/tasks/compare_size.mjs)4
-rw-r--r--build/tasks/lib/getTimestamp.js6
-rw-r--r--build/tasks/lib/isCleanWorkingDir.js10
-rw-r--r--build/tasks/lib/slim-exclude.js4
-rw-r--r--build/tasks/lib/verifyNodeVersion.js12
-rw-r--r--build/tasks/minify.js16
-rw-r--r--build/tasks/node_smoke_tests.js13
-rw-r--r--build/tasks/npmcopy.js8
-rw-r--r--build/tasks/promises_aplus_tests.js20
-rw-r--r--build/tasks/qunit-fixture.js4
24 files changed, 1065 insertions, 513 deletions
diff --git a/build/command.js b/build/command.js
index a89db5167..362fa01d5 100755
--- a/build/command.js
+++ b/build/command.js
@@ -1,8 +1,6 @@
-"use strict";
-
-const { build } = require( "./tasks/build" );
-const yargs = require( "yargs/yargs" );
-const slimExclude = require( "./tasks/lib/slim-exclude" );
+import yargs from "yargs/yargs";
+import { build } from "./tasks/build.js";
+import slimExclude from "./tasks/lib/slim-exclude.js";
const argv = yargs( process.argv.slice( 2 ) )
.version( false )
diff --git a/build/package.json b/build/package.json
new file mode 100644
index 000000000..3dbc1ca59
--- /dev/null
+++ b/build/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/build/release.js b/build/release.js
deleted file mode 100644
index f7d30f4db..000000000
--- a/build/release.js
+++ /dev/null
@@ -1,82 +0,0 @@
-"use strict";
-
-const fs = require( "node:fs" );
-
-module.exports = function( Release ) {
-
- const distFiles = [
- "dist/jquery.js",
- "dist/jquery.min.js",
- "dist/jquery.min.map",
- "dist/jquery.slim.js",
- "dist/jquery.slim.min.js",
- "dist/jquery.slim.min.map"
- ];
- const filesToCommit = [
- ...distFiles,
- "src/core.js"
- ];
- const cdn = require( "./release/cdn" );
- const dist = require( "./release/dist" );
- const { buildDefaultFiles } = require( "./tasks/build" );
-
- const npmTags = Release.npmTags;
-
- Release.define( {
- npmPublish: true,
- issueTracker: "github",
-
- /**
- * Set the version in the src folder for distributing AMD
- */
- _setSrcVersion: function() {
- var corePath = __dirname + "/../src/core.js",
- contents = fs.readFileSync( corePath, "utf8" );
- contents = contents.replace( /@VERSION/g, Release.newVersion );
- fs.writeFileSync( corePath, contents, "utf8" );
- },
-
- /**
- * Generates any release artifacts that should be included in the release.
- * The callback must be invoked with an array of files that should be
- * committed before creating the tag.
- * @param {Function} callback
- */
- generateArtifacts: async function( callback ) {
- await buildDefaultFiles( { version: Release.newVersion } );
-
- cdn.makeReleaseCopies( Release );
- Release._setSrcVersion();
- callback( filesToCommit );
- },
-
- /**
- * Acts as insertion point for restoring Release.dir.repo
- * It was changed to reuse npm publish code in jquery-release
- * for publishing the distribution repo instead
- */
- npmTags: function() {
-
- // origRepo is not defined if dist was skipped
- Release.dir.repo = Release.dir.origRepo || Release.dir.repo;
- return npmTags();
- },
-
- /**
- * Publish to distribution repo and npm
- * @param {Function} callback
- */
- dist: function( callback ) {
- cdn.makeArchives( Release, function() {
- dist( Release, distFiles, callback );
- } );
- }
- } );
-};
-
-module.exports.dependencies = [
- "archiver@5.2.0",
- "shelljs@0.8.4",
- "inquirer@8.0.0",
- "chalk@4.1.0"
-];
diff --git a/build/release/README.md b/build/release/README.md
new file mode 100644
index 000000000..9aef670a1
--- /dev/null
+++ b/build/release/README.md
@@ -0,0 +1,123 @@
+# Releasing jQuery
+
+This document describes the process for releasing a new version of jQuery. It is intended for jQuery team members and collaborators who have been granted permission to release new versions.
+
+## Prerequisites
+
+Before you can release a new version of jQuery, you need to have the following tools installed:
+
+- [Node.js](https://nodejs.org/) (latest LTS version)
+- [npm](https://www.npmjs.com/) (comes with Node.js)
+- [git](https://git-scm.com/)
+
+## Setup
+
+1. Clone the jQuery repo:
+
+ ```sh
+ git clone git@github.com:jquery/jquery.git
+ cd jquery
+ ```
+
+1. Install the dependencies:
+
+ ```sh
+ npm install
+ ```
+
+1. Log into npm with a user that has access to the `jquery` package.
+
+ ```sh
+ npm login
+ ```
+
+The release script will not run if not logged in.
+
+1. Set `JQUERY_GITHUB_TOKEN` in the shell environment that will be used to run `npm run release`. The token can be [created on GitHub](https://github.com/settings/tokens/new?scopes=repo&description=release-it) and only needs the `repo` scope. This token is used to publish GitHub release notes and generate a list of contributors for the blog post.
+
+ ```sh
+ export JQUERY_GITHUB_TOKEN=...
+ ```
+
+The release script will not run without this token.
+
+## Release Process
+
+1. Ensure all milestoned issues/PRs are closed, or reassign to a new milestone.
+1. Verify all tests are passing in [CI](https://github.com/jquery/jquery/actions).
+1. Run any release-only tests, such as those in the [`test/integration`](../../test/integration/) folder.
+1. Ensure AUTHORS.txt file is up to date (this will be verified by the release script).
+
+ - Use `npm run authors:update` to update.
+
+1. Create draft blog post on blog.jquery.com; save the link before publishing. The link is required to run the release.
+
+ - Highlight major changes and reason for release.
+ - Add HTML from the `changelog.html` generated in the below release script.
+ - Use HTML from the `contributors.html` generated in the below release script in the "Thanks" section.
+
+1. Run a dry run of the release script:
+
+ ```sh
+ BLOG_URL=https://blog.jquery.com/... npm run release -- -d
+ ```
+
+1. If the dry run is successful, run the release script:
+
+ ```sh
+ BLOG_URL=https://blog.jquery.com/... npm run release
+ ```
+
+ This will run the pre-release script, which includes checking authors, running tests, running the build, and cloning the CDN and jquery-dist repos in the `tmp/` folder.
+
+ It will then walk you through the rest of the release process: creating the tag, publishing to npm, publishing release notes on GitHub, and pushing the updated branch and new tag to the jQuery repo.
+
+ Finally, it will run the post-release script, which will ask you to confirm the files prepared in `tmp/release/cdn` and `tmp/release/dist` are correct before pushing to the respective repos. It will also prepare a commit for the jQuery repo to remove the release files and update the AUTHORS.txt URL in the package.json. It will ask for confirmation before pushing that commit as well.
+
+ For a pre-release, run:
+
+ ```sh
+ BLOG_URL=https://blog.jquery.com/... npm run release -- --preRelease=beta
+ ```
+
+ `preRelease` can also be set to `alpha` or `rc`.
+
+ **Note**: `preReleaseBase` is set in the npm script to `1` to ensure any pre-releases start at `.1` instead of `.0`. This does not interfere with stable releases.
+
+1. Run the post-release script:
+
+ ```sh
+ ./build/release/post-release.sh $VERSION $BLOG_URL
+ ```
+
+ This will push the release files to the CDN and jquery-dist repos, and push the commit to the jQuery repo to remove the release files and update the AUTHORS.txt URL in the package.json.
+
+1. Once the release is complete, publish the blog post.
+
+## Stable releases
+
+Stable releases have a few more steps:
+
+1. Close the milestone matching the current release: https://github.com/jquery/jquery/milestones. Ensure there is a new milestone for the next release.
+1. Update jQuery on https://github.com/jquery/jquery-wp-content.
+1. Update jQuery on https://github.com/jquery/blog.jquery.com-theme.
+1. Update latest jQuery version for [healthyweb.org](https://github.com/jquery/healthyweb.org/blob/main/wrangler.toml).
+1. Update the shipping version on [jquery.com home page](https://github.com/jquery/jquery.com).
+
+ ```sh
+ git pull jquery/jquery.com
+ # Edit index.html and download.md
+ git commit
+ npm version patch
+ git push origin main --tags
+ ```
+
+1. Update the version used in [jQuery docs demos](https://github.com/jquery/api.jquery.com/blob/main/entries2html.xsl).
+
+1. Email archives to CDNs.
+
+| CDN | Emails | Include |
+| --- | ------ | ------- |
+| Google | hosted-libraries@google | `tmp/archives/googlecdn-jquery-*.zip` |
+| Microsoft | damian.edwards@microsoft, Chris.Sfanos@microsoft | `tmp/archives/mscdn-jquery-*.zip` |
+| CDNJS | ryan@ryankirkman, thomasalwyndavis@gmail | Blog post link |
diff --git a/build/release/archive.js b/build/release/archive.js
new file mode 100644
index 000000000..2325d6ccb
--- /dev/null
+++ b/build/release/archive.js
@@ -0,0 +1,59 @@
+import { readdir, writeFile } from "node:fs/promises";
+import { createReadStream, createWriteStream } from "node:fs";
+import path from "node:path";
+import util from "node:util";
+import os from "node:os";
+import { exec as nodeExec } from "node:child_process";
+import archiver from "archiver";
+
+const exec = util.promisify( nodeExec );
+
+async function md5sum( files, folder ) {
+ if ( os.platform() === "win32" ) {
+ const rmd5 = /[a-f0-9]{32}/;
+ const sum = [];
+
+ for ( let i = 0; i < files.length; i++ ) {
+ const { stdout } = await exec( "certutil -hashfile " + files[ i ] + " MD5", {
+ cwd: folder
+ } );
+ sum.push( rmd5.exec( stdout )[ 0 ] + " " + files[ i ] );
+ }
+
+ return sum.join( "\n" );
+ }
+
+ const { stdout } = await exec( "md5 -r " + files.join( " " ), { cwd: folder } );
+ return stdout;
+}
+
+export default function archive( { cdn, folder, version } ) {
+ return new Promise( async( resolve, reject ) => {
+ console.log( `Creating production archive for ${ cdn }...` );
+
+ const md5file = cdn + "-md5.txt";
+ const output = createWriteStream(
+ path.join( folder, cdn + "-jquery-" + version + ".zip" )
+ );
+
+ output.on( "close", resolve );
+ output.on( "error", reject );
+
+ const archive = archiver( "zip" );
+ archive.pipe( output );
+
+ const files = await readdir( folder );
+ const sum = await md5sum( files, folder );
+ await writeFile( path.join( folder, md5file ), sum );
+ files.push( md5file );
+
+ files.forEach( ( file ) => {
+ const stream = createReadStream( path.join( folder, file ) );
+ archive.append( stream, {
+ name: path.basename( file )
+ } );
+ } );
+
+ archive.finalize();
+ } );
+}
diff --git a/build/release/authors.js b/build/release/authors.js
index 3a0f3bd32..fec5104c5 100644
--- a/build/release/authors.js
+++ b/build/release/authors.js
@@ -1,8 +1,11 @@
-"use strict";
-const fs = require( "node:fs/promises" );
-const util = require( "node:util" );
-const exec = util.promisify( require( "node:child_process" ).exec );
+
+import fs from "node:fs/promises";
+import util from "node:util";
+import { exec as nodeExec } from "node:child_process";
+
+const exec = util.promisify( nodeExec );
+
const rnewline = /\r?\n/;
const rdate = /^\[(\d+)\] /;
@@ -47,7 +50,7 @@ async function getLastAuthor() {
async function logAuthors( preCommand ) {
let command = "git log --pretty=format:\"[%at] %aN <%aE>\"";
if ( preCommand ) {
- command = preCommand + " && " + command;
+ command = `${ preCommand } && ${ command }`;
}
const { stdout } = await exec( command );
return uniq( stdout.trim().split( rnewline ).reverse() );
@@ -63,21 +66,21 @@ async function getSizzleAuthors() {
function sortAuthors( a, b ) {
const [ , aDate ] = rdate.exec( a );
const [ , bDate ] = rdate.exec( b );
- return parseInt( aDate ) - parseInt( bDate );
+ return Number( aDate ) - Number( bDate );
}
function formatAuthor( author ) {
return author.replace( rdate, "" );
}
-async function getAuthors() {
+export async function getAuthors() {
console.log( "Getting authors..." );
const authors = await logAuthors();
const sizzleAuthors = await getSizzleAuthors();
return uniq( authors.concat( sizzleAuthors ) ).sort( sortAuthors ).map( formatAuthor );
}
-async function checkAuthors() {
+export async function checkAuthors() {
const authors = await getAuthors();
const lastAuthor = await getLastAuthor();
@@ -89,7 +92,7 @@ async function checkAuthors() {
console.log( "AUTHORS.txt is up to date" );
}
-async function updateAuthors() {
+export async function updateAuthors() {
const authors = await getAuthors();
const authorsTxt = "Authors ordered by first contribution.\n\n" + authors.join( "\n" ) + "\n";
@@ -97,9 +100,3 @@ async function updateAuthors() {
console.log( "AUTHORS.txt updated" );
}
-
-module.exports = {
- checkAuthors,
- getAuthors,
- updateAuthors
-};
diff --git a/build/release/cdn.js b/build/release/cdn.js
index 8576ade42..d45fd929e 100644
--- a/build/release/cdn.js
+++ b/build/release/cdn.js
@@ -1,151 +1,128 @@
-"use strict";
-
-var fs = require( "node:fs" ),
- shell = require( "shelljs" ),
- path = require( "node:path" ),
- os = require( "node:os" ),
- cdnFolder = "dist/cdn",
- releaseFiles = {
- "jquery-VER.js": "dist/jquery.js",
- "jquery-VER.min.js": "dist/jquery.min.js",
- "jquery-VER.min.map": "dist/jquery.min.map",
- "jquery-VER.slim.js": "dist/jquery.slim.js",
- "jquery-VER.slim.min.js": "dist/jquery.slim.min.js",
- "jquery-VER.slim.min.map": "dist/jquery.slim.min.map"
- },
- googleFilesCDN = [
- "jquery.js",
- "jquery.min.js",
- "jquery.min.map",
- "jquery.slim.js",
- "jquery.slim.min.js",
- "jquery.slim.min.map"
- ],
- msFilesCDN = [
- "jquery-VER.js",
- "jquery-VER.min.js",
- "jquery-VER.min.map",
- "jquery-VER.slim.js",
- "jquery-VER.slim.min.js",
- "jquery-VER.slim.min.map"
- ];
-
-/**
- * Generates copies for the CDNs
- */
-function makeReleaseCopies( Release ) {
- shell.mkdir( "-p", cdnFolder );
-
- Object.keys( releaseFiles ).forEach( function( key ) {
- var text,
- builtFile = releaseFiles[ key ],
- unpathedFile = key.replace( /VER/g, Release.newVersion ),
- releaseFile = cdnFolder + "/" + unpathedFile;
-
- if ( /\.map$/.test( releaseFile ) ) {
-
- // Map files need to reference the new uncompressed name;
- // assume that all files reside in the same directory.
- // "file":"jquery.min.js" ... "sources":["jquery.js"]
- text = fs
- .readFileSync( builtFile, "utf8" )
- .replace(
- /"file":"([^"]+)"/,
- `"file":"${ unpathedFile.replace( /\.min\.map/, ".min.js" ) }"`
- )
- .replace(
- /"sources":\["([^"]+)"\]/,
- `"sources":["${ unpathedFile.replace( /\.min\.map/, ".js" ) }"]`
- );
- fs.writeFileSync( releaseFile, text );
- } else if ( builtFile !== releaseFile ) {
- shell.cp( "-f", builtFile, releaseFile );
- }
- } );
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import path from "node:path";
+import { argv } from "node:process";
+import util from "node:util";
+import { exec as nodeExec } from "node:child_process";
+import { rimraf } from "rimraf";
+import archive from "./archive.js";
+
+const exec = util.promisify( nodeExec );
+
+const version = argv[ 2 ];
+
+if ( !version ) {
+ throw new Error( "No version specified" );
}
-function makeArchives( Release, callback ) {
- Release.chdir( Release.dir.repo );
+const archivesFolder = "tmp/archives";
+const versionedFolder = `${ archivesFolder }/versioned`;
+const unversionedFolder = `${ archivesFolder }/unversioned`;
+
+// The cdn repo is cloned during release
+const cdnRepoFolder = "tmp/release/cdn";
+
+// .min.js and .min.map files are expected
+// in the same directory as the uncompressed files.
+const sources = [
+ "dist/jquery.js",
+ "dist/jquery.slim.js"
+];
+
+const rminmap = /\.min\.map$/;
+const rjs = /\.js$/;
+
+function clean() {
+ console.log( "Cleaning any existing archives..." );
+ return rimraf( archivesFolder );
+}
- function makeArchive( cdn, files, callback ) {
- if ( Release.preRelease ) {
- console.log(
- `Skipping archive creation for ${ cdn }; this is a beta release.`
+// Map files need to reference the new uncompressed name;
+// assume that all files reside in the same directory.
+// "file":"jquery.min.js" ... "sources":["jquery.js"]
+// This is only necessary for the versioned files.
+async function convertMapToVersioned( file, folder ) {
+ const mapFile = file.replace( /\.js$/, ".min.map" );
+ const filename = path
+ .basename( mapFile )
+ .replace( "jquery", "jquery-" + version );
+
+ const contents = JSON.parse( await readFile( mapFile, "utf8" ) );
+
+ return writeFile(
+ path.join( folder, filename ),
+ JSON.stringify( {
+ ...contents,
+ file: filename.replace( rminmap, ".min.js" ),
+ sources: [ filename.replace( rminmap, ".js" ) ]
+ } )
+ );
+}
+
+async function makeUnversionedCopies() {
+ await mkdir( unversionedFolder, { recursive: true } );
+
+ return Promise.all(
+ sources.map( async( file ) => {
+ const filename = path.basename( file );
+ const minFilename = filename.replace( rjs, ".min.js" );
+ const mapFilename = filename.replace( rjs, ".min.map" );
+
+ await exec( `cp -f ${ file } ${ unversionedFolder }/${ filename }` );
+ await exec(
+ `cp -f ${ file.replace(
+ rjs,
+ ".min.js"
+ ) } ${ unversionedFolder }/${ minFilename }`
);
- callback();
- return;
- }
-
- console.log( "Creating production archive for " + cdn );
-
- var i,
- sum,
- result,
- archiver = require( "archiver" )( "zip" ),
- md5file = cdnFolder + "/" + cdn + "-md5.txt",
- output = fs.createWriteStream(
- cdnFolder + "/" + cdn + "-jquery-" + Release.newVersion + ".zip"
- ),
- rmd5 = /[a-f0-9]{32}/,
- rver = /VER/;
-
- output.on( "close", callback );
-
- output.on( "error", function( err ) {
- throw err;
- } );
-
- archiver.pipe( output );
-
- files = files.map( function( item ) {
- return (
- "dist" +
- ( rver.test( item ) ? "/cdn" : "" ) +
- "/" +
- item.replace( rver, Release.newVersion )
+ await exec(
+ `cp -f ${ file.replace(
+ rjs,
+ ".min.map"
+ ) } ${ unversionedFolder }/${ mapFilename }`
);
- } );
-
- if ( os.platform() === "win32" ) {
- sum = [];
- for ( i = 0; i < files.length; i++ ) {
- result = Release.exec(
- "certutil -hashfile " + files[ i ] + " MD5",
- "Error retrieving md5sum"
- );
- sum.push( rmd5.exec( result )[ 0 ] + " " + files[ i ] );
- }
- sum = sum.join( "\n" );
- } else {
- sum = Release.exec(
- "md5 -r " + files.join( " " ),
- "Error retrieving md5sum"
+ } )
+ );
+}
+
+async function makeVersionedCopies() {
+ await mkdir( versionedFolder, { recursive: true } );
+
+ return Promise.all(
+ sources.map( async( file ) => {
+ const filename = path
+ .basename( file )
+ .replace( "jquery", "jquery-" + version );
+ const minFilename = filename.replace( rjs, ".min.js" );
+
+ await exec( `cp -f ${ file } ${ versionedFolder }/${ filename }` );
+ await exec(
+ `cp -f ${ file.replace(
+ rjs,
+ ".min.js"
+ ) } ${ versionedFolder }/${ minFilename }`
);
- }
- fs.writeFileSync( md5file, sum );
- files.push( md5file );
+ await convertMapToVersioned( file, versionedFolder );
+ } )
+ );
+}
+
+async function copyToRepo( folder ) {
+ return exec( `cp -f ${ folder }/* ${ cdnRepoFolder }/cdn/` );
+}
- files.forEach( function( file ) {
- archiver.append( fs.createReadStream( file ), { name: path.basename( file ) } );
- } );
+async function cdn() {
+ await clean();
- archiver.finalize();
- }
+ await Promise.all( [ makeUnversionedCopies(), makeVersionedCopies() ] );
- function buildGoogleCDN( callback ) {
- makeArchive( "googlecdn", googleFilesCDN, callback );
- }
+ await copyToRepo( versionedFolder );
- function buildMicrosoftCDN( callback ) {
- makeArchive( "mscdn", msFilesCDN, callback );
- }
+ await Promise.all( [
+ archive( { cdn: "googlecdn", folder: unversionedFolder, version } ),
+ archive( { cdn: "mscdn", folder: versionedFolder, version } )
+ ] );
- buildGoogleCDN( function() {
- buildMicrosoftCDN( callback );
- } );
+ console.log( "Files ready for CDNs." );
}
-module.exports = {
- makeReleaseCopies: makeReleaseCopies,
- makeArchives: makeArchives
-};
+cdn();
diff --git a/build/release/changelog.js b/build/release/changelog.js
new file mode 100644
index 000000000..5a3722d9e
--- /dev/null
+++ b/build/release/changelog.js
@@ -0,0 +1,239 @@
+import { writeFile } from "node:fs/promises";
+import { argv } from "node:process";
+import { exec as nodeExec } from "node:child_process";
+import util from "node:util";
+import { marked } from "marked";
+
+const exec = util.promisify( nodeExec );
+
+const rbeforeHash = /.#$/;
+const rendsWithHash = /#$/;
+const rcherry = / \(cherry picked from commit [^)]+\)/;
+const rcommit = /Fix(?:e[sd])? ((?:[a-zA-Z0-9_-]{1,39}\/[a-zA-Z0-9_-]{1,100}#)|#|gh-)(\d+)/g;
+const rcomponent = /^([^ :]+):\s*([^\n]+)/;
+const rnewline = /\r?\n/;
+
+const prevVersion = argv[ 2 ];
+const nextVersion = argv[ 3 ];
+const blogUrl = process.env.BLOG_URL;
+
+if ( !prevVersion || !nextVersion ) {
+ throw new Error( "Usage: `node changelog.js PREV_VERSION NEXT_VERSION`" );
+}
+
+function ticketUrl( ticketId ) {
+ return `https://github.com/jquery/jquery/issues/${ ticketId }`;
+}
+
+function getTicketsForCommit( commit ) {
+ var tickets = [];
+
+ commit.replace( rcommit, function( _match, refType, ticketId ) {
+ var ticket = {
+ url: ticketUrl( ticketId ),
+ label: "#" + ticketId
+ };
+
+ // If the refType has anything before the #, assume it's a GitHub ref
+ if ( rbeforeHash.test( refType ) ) {
+
+ // console.log( refType );
+ refType = refType.replace( rendsWithHash, "" );
+ ticket.url = `https://github.com/${ refType }/issues/${ ticketId }`;
+ ticket.label = refType + ticket.label;
+ }
+
+ tickets.push( ticket );
+ } );
+
+ return tickets;
+}
+
+async function getCommits() {
+ const format =
+ "__COMMIT__%n%s (__TICKETREF__[%h](https://github.com/jquery/jquery/commit/%H))%n%b";
+ const { stdout } = await exec(
+ `git log --format="${ format }" ${ prevVersion }..${ nextVersion }`
+ );
+ const commits = stdout.split( "__COMMIT__" ).slice( 1 );
+
+ return removeReverts( commits.map( parseCommit ).sort( sortCommits ) );
+}
+
+function parseCommit( commit ) {
+ const tickets = getTicketsForCommit( commit )
+ .map( ( ticket ) => {
+ return `[${ ticket.label }](${ ticket.url })`;
+ } )
+ .join( ", " );
+
+ // Drop the commit message body
+ let message = `${ commit.trim().split( rnewline )[ 0 ] }`;
+
+ // Add any ticket references
+ message = message.replace( "__TICKETREF__", tickets ? `${ tickets }, ` : "" );
+
+ // Remove cherry pick references
+ message = message.replace( rcherry, "" );
+
+ return message;
+}
+
+function sortCommits( a, b ) {
+ const aComponent = rcomponent.exec( a );
+ const bComponent = rcomponent.exec( b );
+
+ if ( aComponent && bComponent ) {
+ if ( aComponent[ 1 ] < bComponent[ 1 ] ) {
+ return -1;
+ }
+ if ( aComponent[ 1 ] > bComponent[ 1 ] ) {
+ return 1;
+ }
+ return 0;
+ }
+
+ if ( a < b ) {
+ return -1;
+ }
+ if ( a > b ) {
+ return 1;
+ }
+ return 0;
+}
+
+/**
+ * Remove all revert commits and the commit it is reverting
+ */
+function removeReverts( commits ) {
+ const remove = [];
+
+ commits.forEach( function( commit ) {
+ const match = /\*\s*Revert "([^"]*)"/.exec( commit );
+
+ // Ignore double reverts
+ if ( match && !/^Revert "([^"]*)"/.test( match[ 0 ] ) ) {
+ remove.push( commit, match[ 0 ] );
+ }
+ } );
+
+ remove.forEach( function( message ) {
+ const index = commits.findIndex( ( commit ) => commit.includes( message ) );
+ if ( index > -1 ) {
+
+ // console.log( "Removing ", commits[ index ] );
+ commits.splice( index, 1 );
+ }
+ } );
+
+ return commits;
+}
+
+function addHeaders( commits ) {
+ const components = {};
+ let markdown = "";
+
+ commits.forEach( function( commit ) {
+ const match = rcomponent.exec( commit );
+ if ( match ) {
+ let component = match[ 1 ];
+ if ( !/^[A-Z]/.test( component ) ) {
+ component =
+ component.slice( 0, 1 ).toUpperCase() +
+ component.slice( 1 ).toLowerCase();
+ }
+ if ( !components[ component.toLowerCase() ] ) {
+ markdown += "\n## " + component + "\n\n";
+ components[ component.toLowerCase() ] = true;
+ }
+ markdown += `- ${ match[ 2 ] }\n`;
+ } else {
+ markdown += `- ${ commit }\n`;
+ }
+ } );
+
+ return markdown;
+}
+
+async function getGitHubContributor( sha ) {
+ const response = await fetch(
+ `https://api.github.com/repos/jquery/jquery/commits/${ sha }`,
+ {
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${ process.env.JQUERY_GITHUB_TOKEN }`,
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+ }
+ );
+ const data = await response.json();
+
+ if ( !data.commit || !data.author ) {
+
+ // The data may contain multiple helpful fields
+ throw new Error( JSON.stringify( data ) );
+ }
+ return { name: data.commit.author.name, url: data.author.html_url };
+}
+
+function uniqueContributors( contributors ) {
+ const seen = {};
+ return contributors.filter( ( contributor ) => {
+ if ( seen[ contributor.name ] ) {
+ return false;
+ }
+ seen[ contributor.name ] = true;
+ return true;
+ } );
+}
+
+async function getContributors() {
+ const { stdout } = await exec(
+ `git log --format="%H" ${ prevVersion }..${ nextVersion }`
+ );
+ const shas = stdout.split( rnewline ).filter( Boolean );
+ const contributors = await Promise.all( shas.map( getGitHubContributor ) );
+
+ return uniqueContributors( contributors )
+
+ // Sort by last name
+ .sort( ( a, b ) => {
+ const aName = a.name.split( " " );
+ const bName = b.name.split( " " );
+ return aName[ aName.length - 1 ].localeCompare( bName[ bName.length - 1 ] );
+ } )
+ .map( ( { name, url } ) => {
+ if ( name === "Timmy Willison" || name.includes( "dependabot" ) ) {
+ return;
+ }
+ return `<a href="${ url }">${ name }</a>`;
+ } )
+ .filter( Boolean ).join( "\n" );
+}
+
+async function generate() {
+ const commits = await getCommits();
+ const contributors = await getContributors();
+
+ let changelog = "# Changelog\n";
+ if ( blogUrl ) {
+ changelog += `\n${ blogUrl }\n`;
+ }
+ changelog += addHeaders( commits );
+
+ // Write markdown to changelog.md
+ await writeFile( "changelog.md", changelog );
+
+ // Write HTML to changelog.html for blog post
+ await writeFile( "changelog.html", marked.parse( changelog ) );
+
+ // Write contributors HTML for blog post
+ await writeFile( "contributors.html", contributors );
+
+ // Log regular changelog for release-it
+ console.log( changelog );
+
+ return changelog;
+}
+
+generate();
diff --git a/build/release/dist.js b/build/release/dist.js
index 7b0afae85..ec3890032 100644
--- a/build/release/dist.js
+++ b/build/release/dist.js
@@ -1,177 +1,125 @@
-"use strict";
-
-module.exports = function( Release, files, complete ) {
-
- const fs = require( "node:fs/promises" );
- const shell = require( "shelljs" );
- const inquirer = require( "inquirer" );
- const pkg = require( `${ Release.dir.repo }/package.json` );
- const distRemote = Release.remote
-
- // For local and github dists
- .replace( /jquery(\.git|$)/, "jquery-dist$1" );
-
- // These files are included with the distribution
- const extras = [
- "src",
- "LICENSE.txt",
- "AUTHORS.txt"
- ];
-
- /**
- * Clone the distribution repo
- */
- function clone() {
- Release.chdir( Release.dir.base );
- Release.dir.dist = `${ Release.dir.base }/dist`;
-
- console.log( "Using distribution repo: ", distRemote );
- Release.exec( `git clone ${ distRemote } ${ Release.dir.dist }`,
- "Error cloning repo." );
-
- // Distribution always works on main
- Release.chdir( Release.dir.dist );
- Release.exec( "git checkout main", "Error checking out branch." );
- console.log();
- }
-
- /**
- * Generate bower file for jquery-dist
- */
- function generateBower() {
- return JSON.stringify( {
+import { readFile, writeFile } from "node:fs/promises";
+import util from "node:util";
+import { argv } from "node:process";
+import { exec as nodeExec } from "node:child_process";
+import { rimraf } from "rimraf";
+
+const pkg = JSON.parse( await readFile( "./package.json", "utf8" ) );
+
+const exec = util.promisify( nodeExec );
+
+const version = argv[ 2 ];
+const blogURL = argv[ 3 ];
+
+if ( !version ) {
+ throw new Error( "No version specified" );
+}
+
+if ( !blogURL || !blogURL.startsWith( "https://blog.jquery.com/" ) ) {
+ throw new Error( "Invalid blog post URL" );
+}
+
+// The dist repo is cloned during release
+const distRepoFolder = "tmp/release/dist";
+
+// Files to be included in the dist repo.
+// README.md and bower.json are generated.
+// package.json is a simplified version of the original.
+const files = [
+ "dist",
+ "src",
+ "LICENSE.txt",
+ "AUTHORS.txt",
+ "changelog.md"
+];
+
+async function generateBower() {
+ return JSON.stringify(
+ {
name: pkg.name,
main: pkg.main,
license: "MIT",
- ignore: [
- "package.json"
- ],
+ ignore: [ "package.json" ],
keywords: pkg.keywords
- }, null, 2 );
- }
-
- /**
- * Replace the version in the README
- * @param {string} readme
- * @param {string} blogPostLink
- */
- function editReadme( readme, blogPostLink ) {
- return readme
- .replace( /@VERSION/g, Release.newVersion )
- .replace( /@BLOG_POST_LINK/g, blogPostLink );
- }
-
- /**
- * Copy necessary files over to the dist repo
- */
- async function copy() {
-
- // Copy dist files
- const distFolder = `${ Release.dir.dist }/dist`;
- const readme = await fs.readFile(
- `${ Release.dir.repo }/build/fixtures/README.md`, "utf8" );
- const rmIgnore = [ ...files, "node_modules" ]
- .map( file => `${ Release.dir.dist }/${ file }` );
-
- shell.config.globOptions = {
- ignore: rmIgnore
- };
-
- const { blogPostLink } = await inquirer.prompt( [ {
- type: "input",
- name: "blogPostLink",
- message: "Enter URL of the blog post announcing the jQuery release...\n"
- } ] );
-
- // Remove extraneous files before copy
- shell.rm( "-rf", `${ Release.dir.dist }/**/*` );
-
- shell.mkdir( "-p", distFolder );
- files.forEach( function( file ) {
- shell.cp( "-f", `${ Release.dir.repo }/${ file }`, distFolder );
- } );
-
- // Copy other files
- extras.forEach( function( file ) {
- shell.cp( "-rf", `${ Release.dir.repo }/${ file }`, Release.dir.dist );
- } );
-
- // Remove the wrapper & the ESLint config from the dist repo
- shell.rm( "-f", `${ Release.dir.dist }/src/wrapper.js` );
- shell.rm( "-f", `${ Release.dir.dist }/src/.eslintrc.json` );
-
- // Write package.json
- // Remove scripts and other superfluous properties,
- // especially the prepare script, which fails on the dist repo
- const packageJson = Object.assign( {}, pkg );
- delete packageJson.scripts;
- delete packageJson.devDependencies;
- delete packageJson.dependencies;
- delete packageJson.commitplease;
- packageJson.version = Release.newVersion;
- await fs.writeFile(
- `${ Release.dir.dist }/package.json`,
- JSON.stringify( packageJson, null, 2 )
- );
-
- // Write generated bower file
- await fs.writeFile( `${ Release.dir.dist }/bower.json`, generateBower() );
-
- await fs.writeFile( `${ Release.dir.dist }/README.md`,
- editReadme( readme, blogPostLink ) );
-
- console.log( "Files ready to add." );
- }
-
- /**
- * Add, commit, and tag the dist files
- */
- function commit() {
- console.log( "Adding files to dist..." );
- Release.exec( "git add -A", "Error adding files." );
- Release.exec(
- `git commit -m "Release ${ Release.newVersion }"`,
- "Error committing files."
- );
- console.log();
-
- console.log( "Tagging release on dist..." );
- Release.exec( `git tag ${ Release.newVersion }`,
- `Error tagging ${ Release.newVersion } on dist repo.` );
- Release.tagTime = Release.exec( "git log -1 --format=\"%ad\"",
- "Error getting tag timestamp." ).trim();
- }
-
- /**
- * Push files to dist repo
- */
- function push() {
- Release.chdir( Release.dir.dist );
-
- console.log( "Pushing release to dist repo..." );
- Release.exec(
- `git push ${
- Release.isTest ? " --dry-run" : ""
- } ${ distRemote } main --tags`,
- "Error pushing main and tags to git repo."
- );
-
- // Set repo for npm publish
- Release.dir.origRepo = Release.dir.repo;
- Release.dir.repo = Release.dir.dist;
- }
-
- Release.walk( [
- Release._section( "Copy files to distribution repo" ),
- clone,
- copy,
- Release.confirmReview,
-
- Release._section( "Add, commit, and tag files in distribution repo" ),
- commit,
- Release.confirmReview,
-
- Release._section( "Pushing files to distribution repo" ),
- push
- ], complete );
-};
+ },
+ null,
+ 2
+ );
+}
+
+async function generateReadme() {
+ const readme = await readFile(
+ "./build/fixtures/README.md",
+ "utf8"
+ );
+
+ return readme
+ .replace( /@VERSION/g, version )
+ .replace( /@BLOG_POST_LINK/g, blogURL );
+}
+
+/**
+ * Copy necessary files over to the dist repo
+ */
+async function copyFiles() {
+
+ // Remove any extraneous files before copy
+ await rimraf( [
+ `${ distRepoFolder }/dist`,
+ `${ distRepoFolder }/dist-module`,
+ `${ distRepoFolder }/src`
+ ] );
+
+ // Copy all files
+ await Promise.all(
+ files.map( function( path ) {
+ console.log( `Copying ${ path }...` );
+ return exec( `cp -rf ${ path } ${ distRepoFolder }/${ path }` );
+ } )
+ );
+
+ // Remove the wrapper from the dist repo
+ await rimraf( [
+ `${ distRepoFolder }/src/wrapper.js`
+ ] );
+
+ // Set the version in src/core.js
+ const core = await readFile( `${ distRepoFolder }/src/core.js`, "utf8" );
+ await writeFile(
+ `${ distRepoFolder }/src/core.js`,
+ core.replace( /@VERSION/g, version )
+ );
+
+ // Write generated README
+ console.log( "Generating README.md..." );
+ const readme = await generateReadme();
+ await writeFile( `${ distRepoFolder }/README.md`, readme );
+
+ // Write generated Bower file
+ console.log( "Generating bower.json..." );
+ const bower = await generateBower();
+ await writeFile( `${ distRepoFolder }/bower.json`, bower );
+
+ // Write simplified package.json
+ console.log( "Writing package.json..." );
+ await writeFile(
+ `${ distRepoFolder }/package.json`,
+ JSON.stringify(
+ {
+ ...pkg,
+ scripts: undefined,
+ dependencies: undefined,
+ devDependencies: undefined,
+ commitplease: undefined
+ },
+ null,
+ 2
+
+ // Add final newline
+ ) + "\n"
+ );
+
+ console.log( "Files copied to dist repo." );
+}
+
+copyFiles();
diff --git a/build/release/post-release.sh b/build/release/post-release.sh
new file mode 100644
index 000000000..6cc4d6517
--- /dev/null
+++ b/build/release/post-release.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+set -euo pipefail
+
+# $1: Version
+# $2: Blog URL
+
+cdn=tmp/release/cdn
+dist=tmp/release/dist
+
+if [[ -z "$1" ]] then
+ echo "Version is not set (1st argument)"
+ exit 1
+fi
+
+if [[ -z "$2" ]] then
+ echo "Blog URL is not set (2nd argument)"
+ exit 1
+fi
+
+# Push files to cdn repo
+npm run release:cdn $1
+cd $cdn
+git add -A
+git commit -m "jquery: Add version $1"
+
+# Wait for confirmation from user to push changes to cdn repo
+read -p "Press enter to push changes to cdn repo"
+git push
+cd -
+
+# Push files to dist repo
+npm run release:dist $1 $2
+cd $dist
+git add -A
+git commit -m "Release: $1"
+# -s to sign and annotate tag (recommended for releases)
+git tag -s $1 -m "Release: $1"
+
+# Wait for confirmation from user to push changes to dist repo
+read -p "Press enter to push changes to dist repo"
+git push --follow-tags
+cd -
+
+# Restore AUTHORS URL
+sed -i "s/$1\/AUTHORS.txt/main\/AUTHORS.txt/" package.json
+git add package.json
+
+# Remove built files from tracking.
+# Leave the changelog.md committed.
+# Leave the tmp folder as some files are needed
+# after the release (such as for emailing archives).
+npm run build:clean
+git rm --cached -r dist/ dist-module/
+git add dist/package.json dist/wrappers dist-module/package.json dist-module/wrappers
+git commit -m "Release: remove dist files from main branch"
+
+# Wait for confirmation from user to push changes
+read -p "Press enter to push changes to main branch"
+git push
diff --git a/build/release/pre-release.sh b/build/release/pre-release.sh
new file mode 100644
index 000000000..f469b0da0
--- /dev/null
+++ b/build/release/pre-release.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -euo pipefail
+
+# Install dependencies
+npm ci
+
+# Clean all release and build artifacts
+npm run build:clean
+npm run release:clean
+
+# Check authors
+npm run authors:check
+
+# Run tests
+npm test
+
+# Clone dist and cdn repos to the tmp/release directory
+mkdir -p tmp/release
+git clone https://github.com/jquery/jquery-dist tmp/release/dist
+git clone https://github.com/jquery/codeorigin.jquery.com tmp/release/cdn
diff --git a/build/release/verify.js b/build/release/verify.js
new file mode 100644
index 000000000..423a63c8c
--- /dev/null
+++ b/build/release/verify.js
@@ -0,0 +1,243 @@
+/**
+ * Verify the latest release is reproducible
+ */
+import { exec as nodeExec } from "node:child_process";
+import crypto from "node:crypto";
+import { createWriteStream } from "node:fs";
+import { mkdir, readdir, readFile } from "node:fs/promises";
+import path from "node:path";
+import { Readable } from "node:stream";
+import { finished } from "node:stream/promises";
+import util from "node:util";
+import { gunzip as nodeGunzip } from "node:zlib";
+import { rimraf } from "rimraf";
+
+const exec = util.promisify( nodeExec );
+const gunzip = util.promisify( nodeGunzip );
+
+const SRC_REPO = "https://github.com/jquery/jquery.git";
+const CDN_URL = "https://code.jquery.com";
+const REGISTRY_URL = "https://registry.npmjs.org/jquery";
+
+const excludeFromCDN = [
+ /^package\.json$/,
+ /^jquery\.factory\./
+];
+
+const rjquery = /^jquery/;
+
+async function verifyRelease( { version } = {} ) {
+ if ( !version ) {
+ version = process.env.VERSION || ( await getLatestVersion() );
+ }
+ const release = await buildRelease( { version } );
+
+ console.log( `Verifying jQuery ${ version }...` );
+
+ let verified = true;
+ const matchingFiles = [];
+ const mismatchingFiles = [];
+
+ // Check all files against the CDN
+ await Promise.all(
+ release.files
+ .filter( ( file ) => excludeFromCDN.every( ( re ) => !re.test( file.name ) ) )
+ .map( async( file ) => {
+ const url = new URL( file.cdnName, CDN_URL );
+ const response = await fetch( url );
+ if ( !response.ok ) {
+ throw new Error(
+ `Failed to download ${
+ file.cdnName
+ } from the CDN: ${ response.statusText }`
+ );
+ }
+ const cdnContents = await response.text();
+ if ( cdnContents !== file.cdnContents ) {
+ mismatchingFiles.push( url.href );
+ verified = false;
+ } else {
+ matchingFiles.push( url.href );
+ }
+ } )
+ );
+
+ // Check all files against npm.
+ // First, download npm tarball for version
+ const npmPackage = await fetch( REGISTRY_URL ).then( ( res ) => res.json() );
+
+ if ( !npmPackage.versions[ version ] ) {
+ throw new Error( `jQuery ${ version } not found on npm!` );
+ }
+ const npmTarball = npmPackage.versions[ version ].dist.tarball;
+
+ // Write npm tarball to file
+ const npmTarballPath = path.join( "tmp/verify", version, "npm.tgz" );
+ await downloadFile( npmTarball, npmTarballPath );
+
+ // Check the tarball checksum
+ const tgzSum = await sumTarball( npmTarballPath );
+ if ( tgzSum !== release.tgz.contents ) {
+ mismatchingFiles.push( `npm:${ version }.tgz` );
+ verified = false;
+ } else {
+ matchingFiles.push( `npm:${ version }.tgz` );
+ }
+
+ await Promise.all(
+ release.files.map( async( file ) => {
+
+ // Get file contents from tarball
+ const { stdout: npmContents } = await exec(
+ `tar -xOf ${ npmTarballPath } package/${ file.path }/${ file.name }`
+ );
+
+ if ( npmContents !== file.contents ) {
+ mismatchingFiles.push( `npm:${ file.path }/${ file.name }` );
+ verified = false;
+ } else {
+ matchingFiles.push( `npm:${ file.path }/${ file.name }` );
+ }
+ } )
+ );
+
+ if ( verified ) {
+ console.log( `jQuery ${ version } is reproducible! All files match!` );
+ } else {
+ console.log();
+ for ( const file of matchingFiles ) {
+ console.log( `✅ ${ file }` );
+ }
+ console.log();
+ for ( const file of mismatchingFiles ) {
+ console.log( `❌ ${ file }` );
+ }
+
+ throw new Error( `jQuery ${ version } is NOT reproducible!` );
+ }
+}
+
+async function buildRelease( { version } ) {
+ const releaseFolder = path.join( "tmp/verify", version );
+
+ // Clone the release repo
+ console.log( `Cloning jQuery ${ version }...` );
+ await rimraf( releaseFolder );
+ await mkdir( releaseFolder, { recursive: true } );
+
+ // Uses a depth of 2 so we can get the commit date of
+ // the commit used to build, which is the commit before the tag
+ await exec(
+ `git clone -q -b ${ version } --depth=2 ${ SRC_REPO } ${ releaseFolder }`
+ );
+
+ // Install node dependencies
+ console.log( `Installing dependencies for jQuery ${ version }...` );
+ await exec( "npm ci", { cwd: releaseFolder } );
+
+ // Find the date of the commit just before the release,
+ // which was used as the date in the built files
+ const { stdout: date } = await exec( "git log -1 --format=%ci HEAD~1", {
+ cwd: releaseFolder
+ } );
+
+ // Build the release
+ console.log( `Building jQuery ${ version }...` );
+ const { stdout: buildOutput } = await exec( "npm run build:all", {
+ cwd: releaseFolder,
+ env: {
+
+ // Keep existing environment variables
+ ...process.env,
+ RELEASE_DATE: date,
+ VERSION: version
+ }
+ } );
+ console.log( buildOutput );
+
+ // Pack the npm tarball
+ console.log( `Packing jQuery ${ version }...` );
+ const { stdout: packOutput } = await exec( "npm pack", { cwd: releaseFolder } );
+ console.log( packOutput );
+
+ // Get all top-level /dist and /dist-module files
+ const distFiles = await readdir(
+ path.join( releaseFolder, "dist" ),
+ { withFileTypes: true }
+ );
+ const distModuleFiles = await readdir(
+ path.join( releaseFolder, "dist-module" ),
+ { withFileTypes: true }
+ );
+
+ const files = await Promise.all(
+ [ ...distFiles, ...distModuleFiles ]
+ .filter( ( dirent ) => dirent.isFile() )
+ .map( async( dirent ) => {
+ const contents = await readFile(
+ path.join( dirent.parentPath, dirent.name ),
+ "utf8"
+ );
+ return {
+ name: dirent.name,
+ path: path.basename( dirent.parentPath ),
+ contents,
+ cdnName: dirent.name.replace( rjquery, `jquery-${ version }` ),
+ cdnContents: dirent.name.endsWith( ".map" ) ?
+
+ // The CDN has versioned filenames in the maps
+ convertMapToVersioned( contents, version ) :
+ contents
+ };
+ } )
+ );
+
+ // Get checksum of the tarball
+ const tgzFilename = `jquery-${ version }.tgz`;
+ const sum = await sumTarball( path.join( releaseFolder, tgzFilename ) );
+
+ return {
+ files,
+ tgz: {
+ name: tgzFilename,
+ contents: sum
+ },
+ version
+ };
+}
+
+async function downloadFile( url, dest ) {
+ const response = await fetch( url );
+ const fileStream = createWriteStream( dest );
+ const stream = Readable.fromWeb( response.body ).pipe( fileStream );
+ return finished( stream );
+}
+
+async function getLatestVersion() {
+ const { stdout: sha } = await exec( "git rev-list --tags --max-count=1" );
+ const { stdout: tag } = await exec( `git describe --tags ${ sha.trim() }` );
+ return tag.trim();
+}
+
+function shasum( data ) {
+ const hash = crypto.createHash( "sha256" );
+ hash.update( data );
+ return hash.digest( "hex" );
+}
+
+async function sumTarball( filepath ) {
+ const contents = await readFile( filepath );
+ const unzipped = await gunzip( contents );
+ return shasum( unzipped );
+}
+
+function convertMapToVersioned( contents, version ) {
+ const map = JSON.parse( contents );
+ return JSON.stringify( {
+ ...map,
+ file: map.file.replace( rjquery, `jquery-${ version }` ),
+ sources: map.sources.map( ( source ) => source.replace( rjquery, `jquery-${ version }` ) )
+ } );
+}
+
+verifyRelease();
diff --git a/build/tasks/build.js b/build/tasks/build.js
index 68c85dc6f..39b530255 100644
--- a/build/tasks/build.js
+++ b/build/tasks/build.js
@@ -4,20 +4,20 @@
* and includes/excludes specified modules
*/
-"use strict";
-
-const fs = require( "node:fs/promises" );
-const path = require( "node:path" );
-const util = require( "node:util" );
-const exec = util.promisify( require( "node: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" );
+import { exec as nodeExec } from "node:child_process";
+import { writeFileSync } from "node:fs";
+import fs from "node:fs/promises";
+import path from "node:path";
+import util from "node:util";
+import requirejs from "requirejs";
+import { compareSize } from "./lib/compareSize.js";
+import getTimestamp from "./lib/getTimestamp.js";
+import isCleanWorkingDir from "./lib/isCleanWorkingDir.js";
+import excludedFromSlim from "./lib/slim-exclude.js";
+import minify from "./minify.js";
+
+const exec = util.promisify( nodeExec );
+const pkg = JSON.parse( await fs.readFile( "./package.json", "utf8" ) );
const rdefineEnd = /\}\s*?\);[^}\w]*$/;
@@ -38,14 +38,14 @@ const removeWith = {
};
async function read( filename ) {
- return fs.readFile( path.join( srcFolder, filename ), "utf8" );
+ return fs.readFile( path.join( "./src", 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( new RegExp( `.*\\${ path.sep }src\\${ path.sep }` ), "" )
.replace( /\.js$/, "" )
.split( path.sep )
.join( path.posix.sep );
@@ -54,7 +54,7 @@ function moduleName( filename ) {
async function readdirRecursive( dir, all = [] ) {
let files;
try {
- files = await fs.readdir( path.join( srcFolder, dir ), {
+ files = await fs.readdir( path.join( "./src", dir ), {
withFileTypes: true
} );
} catch ( _ ) {
@@ -212,7 +212,12 @@ async function checkExclude( exclude, include ) {
return [ unique( excluded ), unique( included ) ];
}
-async function build( {
+async function getLastModifiedDate() {
+ const { stdout } = await exec( "git log -1 --format=\"%at\"" );
+ return new Date( parseInt( stdout, 10 ) * 1000 );
+}
+
+export async function build( {
amd,
dir = "dist",
exclude = [],
@@ -242,6 +247,11 @@ async function build( {
);
const config = await getRequireConfig( { amd } );
+ // Use the last modified date so builds are reproducible
+ const date = process.env.RELEASE_DATE ?
+ new Date( process.env.RELEASE_DATE ) :
+ await getLastModifiedDate();
+
// Replace exports/global with a noop noConflict
if ( excluded.includes( "exports/global" ) ) {
const index = excluded.indexOf( "exports/global" );
@@ -286,7 +296,7 @@ async function build( {
* Handle Final output from the optimizer
* @param {String} compiled
*/
- config.out = async function( compiled ) {
+ config.out = function( compiled ) {
const compiledContents = compiled
// Embed Version
@@ -294,10 +304,11 @@ async function build( {
// Embed Date
// yyyy-mm-ddThh:mmZ
- .replace( /@DATE/g, new Date().toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );
+ .replace( /@DATE/g, date.toISOString().replace( /:\d+\.\d+Z$/, "Z" ) );
// Write concatenated source to file
- await fs.writeFile(
+ // Cannot use async in config.out
+ writeFileSync(
path.join( dir, filename ),
compiledContents
);
@@ -320,7 +331,7 @@ async function build( {
await minify( { filename, dir } );
}
-async function buildDefaultFiles( {
+export async function buildDefaultFiles( {
version = process.env.VERSION
} = {} ) {
await Promise.all( [
@@ -328,12 +339,6 @@ async function buildDefaultFiles( {
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",
@@ -341,5 +346,3 @@ async function buildDefaultFiles( {
]
} );
}
-
-module.exports = { build, buildDefaultFiles };
diff --git a/build/tasks/dist.js b/build/tasks/dist.js
index f15689e3d..9441dca35 100644
--- a/build/tasks/dist.js
+++ b/build/tasks/dist.js
@@ -1,7 +1,5 @@
-"use strict";
-
// Process files for distribution.
-module.exports = function processForDist( text, filename ) {
+export default function processForDist( text, filename ) {
if ( !text ) {
throw new Error( "text required for processForDist" );
}
@@ -28,4 +26,4 @@ module.exports = function processForDist( text, filename ) {
}
throw new Error( message );
}
-};
+}
diff --git a/build/tasks/compare_size.mjs b/build/tasks/lib/compareSize.js
index 36d8136f8..008f91151 100644
--- a/build/tasks/compare_size.mjs
+++ b/build/tasks/lib/compareSize.js
@@ -1,9 +1,9 @@
-import chalk from "chalk";
import fs from "node:fs/promises";
import { promisify } from "node:util";
import zlib from "node:zlib";
import { exec as nodeExec } from "node:child_process";
-import isCleanWorkingDir from "./lib/isCleanWorkingDir.js";
+import chalk from "chalk";
+import isCleanWorkingDir from "./isCleanWorkingDir.js";
const VERSION = 1;
const lastRunBranch = " last run";
diff --git a/build/tasks/lib/getTimestamp.js b/build/tasks/lib/getTimestamp.js
index 4706353c5..bca40bc6b 100644
--- a/build/tasks/lib/getTimestamp.js
+++ b/build/tasks/lib/getTimestamp.js
@@ -1,9 +1,7 @@
-"use strict";
-
-module.exports = function getTimestamp() {
+export default 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
index 3ad8f89bc..5466cbdfd 100644
--- a/build/tasks/lib/isCleanWorkingDir.js
+++ b/build/tasks/lib/isCleanWorkingDir.js
@@ -1,9 +1,9 @@
-"use strict";
+import util from "node:util";
+import { exec as nodeExec } from "node:child_process";
-const util = require( "node:util" );
-const exec = util.promisify( require( "node:child_process" ).exec );
+const exec = util.promisify( nodeExec );
-module.exports = async function isCleanWorkingDir() {
+export default async function isCleanWorkingDir() {
const { stdout } = await exec( "git status --untracked-files=no --porcelain" );
return !stdout.trim();
-};
+}
diff --git a/build/tasks/lib/slim-exclude.js b/build/tasks/lib/slim-exclude.js
index 8f5d84e43..0889de3f6 100644
--- a/build/tasks/lib/slim-exclude.js
+++ b/build/tasks/lib/slim-exclude.js
@@ -1,7 +1,5 @@
-"use strict";
-
// NOTE: keep it in sync with test/data/testinit.js
-module.exports = [
+export default [
"ajax",
"effects"
];
diff --git a/build/tasks/lib/verifyNodeVersion.js b/build/tasks/lib/verifyNodeVersion.js
deleted file mode 100644
index 8ad700d19..000000000
--- a/build/tasks/lib/verifyNodeVersion.js
+++ /dev/null
@@ -1,12 +0,0 @@
-"use strict";
-
-const { version } = require( "process" );
-const nodeV18OrNewer = !/^v1[0-7]\./.test( version );
-
-module.exports = function verifyNodeVersion() {
- if ( !nodeV18OrNewer ) {
- console.log( "Old Node.js detected, task skipped..." );
- return false;
- }
- return true;
-};
diff --git a/build/tasks/minify.js b/build/tasks/minify.js
index bfb059b7b..147b9a89d 100644
--- a/build/tasks/minify.js
+++ b/build/tasks/minify.js
@@ -1,14 +1,12 @@
-"use strict";
-
-const UglifyJS = require( "uglify-js" );
-const fs = require( "node:fs/promises" );
-const path = require( "node:path" );
-const processForDist = require( "./dist" );
-const getTimestamp = require( "./lib/getTimestamp" );
+import fs from "node:fs/promises";
+import path from "node:path";
+import UglifyJS from "uglify-js";
+import processForDist from "./dist.js";
+import getTimestamp from "./lib/getTimestamp.js";
const rjs = /\.js$/;
-module.exports = async function minify( { dir, filename } ) {
+export default async function minify( { dir, filename } ) {
const filepath = path.join( dir, filename );
const contents = await fs.readFile( filepath, "utf8" );
const version = /jQuery JavaScript Library ([^\n]+)/.exec( contents )[ 1 ];
@@ -82,4 +80,4 @@ module.exports = async function minify( { dir, filename } ) {
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 043a227b5..5452669dd 100644
--- a/build/tasks/node_smoke_tests.js
+++ b/build/tasks/node_smoke_tests.js
@@ -1,13 +1,8 @@
-"use strict";
+import fs from "node:fs/promises";
+import util from "node:util";
+import { exec as nodeExec } from "node:child_process";
-const fs = require( "node:fs/promises" );
-const util = require( "node:util" );
-const exec = util.promisify( require( "node:child_process" ).exec );
-const verifyNodeVersion = require( "./lib/verifyNodeVersion" );
-
-if ( !verifyNodeVersion() ) {
- return;
-}
+const exec = util.promisify( nodeExec );
// 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
diff --git a/build/tasks/npmcopy.js b/build/tasks/npmcopy.js
index 93c0658b9..91cfae95f 100644
--- a/build/tasks/npmcopy.js
+++ b/build/tasks/npmcopy.js
@@ -1,9 +1,7 @@
-"use strict";
+import fs from "node:fs/promises";
+import path from "node:path";
-const fs = require( "node:fs/promises" );
-const path = require( "node:path" );
-
-const projectDir = path.resolve( __dirname, "..", ".." );
+const projectDir = path.resolve( "." );
const files = {
"bootstrap/bootstrap.css": "bootstrap/dist/css/bootstrap.css",
diff --git a/build/tasks/promises_aplus_tests.js b/build/tasks/promises_aplus_tests.js
index 4624b3a9a..6f49f0230 100644
--- a/build/tasks/promises_aplus_tests.js
+++ b/build/tasks/promises_aplus_tests.js
@@ -1,22 +1,14 @@
-"use strict";
-
-const { spawn } = require( "node:child_process" );
-const verifyNodeVersion = require( "./lib/verifyNodeVersion" );
-const path = require( "node:path" );
-const os = require( "node:os" );
-
-if ( !verifyNodeVersion() ) {
- return;
-}
+import path from "node:path";
+import os from "node:os";
+import { spawn } from "node:child_process";
const command = path.resolve(
- __dirname,
- `../../node_modules/.bin/promises-aplus-tests${ os.platform() === "win32" ? ".cmd" : "" }`
+ `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"
+ "test/promises_aplus_adapters/deferred.cjs",
+ "test/promises_aplus_adapters/when.cjs"
];
async function runTests() {
diff --git a/build/tasks/qunit-fixture.js b/build/tasks/qunit-fixture.js
index dbb789b60..a8b90653f 100644
--- a/build/tasks/qunit-fixture.js
+++ b/build/tasks/qunit-fixture.js
@@ -1,6 +1,4 @@
-"use strict";
-
-const fs = require( "node:fs/promises" );
+import fs from "node:fs/promises";
async function generateFixture() {
const fixture = await fs.readFile( "./test/data/qunit-fixture.html", "utf8" );