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-11 10:00:56 -0400
commit2646a8b07fcc2cf7cf384724f622eb0c27f9166c (patch)
tree3367ad18d492486a692bb4a7a23b216ba155451f /build
parent3a98ef91dfa0b4897df7562f40bfd1715f5fc30e (diff)
downloadjquery-2646a8b07fcc2cf7cf384724f622eb0c27f9166c.tar.gz
jquery-2646a8b07fcc2cf7cf384724f622eb0c27f9166c.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 *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 - move dist wrappers to "wrappers" folders for easy removal of all built files - limit certain workflows to the main repo (not forks) - version in package.json has been set to beta.1 so that the next release will be beta.2 - 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. Fixes jquery/jquery-release#114 Closes gh-5512
Diffstat (limited to 'build')
-rwxr-xr-xbuild/command.js8
-rw-r--r--build/package.json3
-rw-r--r--build/release.js96
-rw-r--r--build/release/README.md116
-rw-r--r--build/release/archive.js59
-rw-r--r--build/release/authors.js23
-rw-r--r--build/release/cdn.js283
-rw-r--r--build/release/changelog.js239
-rw-r--r--build/release/dist.js307
-rw-r--r--build/release/post-release.sh56
-rw-r--r--build/release/pre-release.sh15
-rw-r--r--build/release/verify.js198
-rw-r--r--build/tasks/build.js87
-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/rollupFileOverridesPlugin.js (renamed from build/tasks/lib/rollup-plugin-file-overrides.js)6
-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.js16
-rw-r--r--build/tasks/qunit-fixture.js4
25 files changed, 1014 insertions, 581 deletions
diff --git a/build/command.js b/build/command.js
index e976fac90..af172bace 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
deleted file mode 100644
index 5bbefffba..000000000
--- a/build/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "type": "commonjs"
-}
diff --git a/build/release.js b/build/release.js
deleted file mode 100644
index bdfb0bc3b..000000000
--- a/build/release.js
+++ /dev/null
@@ -1,96 +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",
- "dist/jquery.factory.js",
- "dist/jquery.factory.slim.js",
- "dist-module/jquery.module.js",
- "dist-module/jquery.module.min.js",
- "dist-module/jquery.module.min.map",
- "dist-module/jquery.slim.module.js",
- "dist-module/jquery.slim.module.min.js",
- "dist-module/jquery.slim.module.min.map",
- "dist-module/jquery.factory.module.js",
- "dist-module/jquery.factory.slim.module.js"
- ];
- 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;
-
- function setSrcVersion( filepath ) {
- var contents = fs.readFileSync( filepath, "utf8" );
- contents = contents.replace( /@VERSION/g, Release.newVersion );
- fs.writeFileSync( filepath, contents, "utf8" );
- }
-
- Release.define( {
- npmPublish: true,
- issueTracker: "github",
-
- // Update cdn location to versioned files
- cdnPublish: "dist/cdn/versioned",
-
- /**
- * Set the version in the src folder for distributing ES modules.
- */
- _setSrcVersion: function() {
- setSrcVersion( `${ __dirname }/../src/core.js` );
- },
-
- /**
- * 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: async function( callback ) {
- await cdn.makeArchives( Release );
- dist( Release, distFiles, callback );
- }
- } );
-};
-
-module.exports.dependencies = [
- "archiver@5.2.0",
- "shelljs@0.8.4",
- "inquirer@8.0.0"
-];
diff --git a/build/release/README.md b/build/release/README.md
new file mode 100644
index 000000000..2cdd65e8a
--- /dev/null
+++ b/build/release/README.md
@@ -0,0 +1,116 @@
+# 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 contributor list generated in the below release script.
+ - 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. 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 bf72b8af7..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+)\] /;
@@ -70,14 +73,14 @@ 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 610299661..7d9492623 100644
--- a/build/release/cdn.js
+++ b/build/release/cdn.js
@@ -1,175 +1,130 @@
-"use strict";
-
-const fs = require( "node:fs" );
-const shell = require( "shelljs" );
-const path = require( "node:path" );
-const os = require( "node:os" );
-
-const cdnFolderContainer = "dist/cdn";
-const cdnFolderVersioned = `${ cdnFolderContainer }/versioned`;
-const cdnFolderUnversioned = `${ cdnFolderContainer }/unversioned`;
-
-const versionedReleaseFiles = {
- "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",
- "jquery-@VER.module.js": "dist-module/jquery.module.js",
- "jquery-@VER.module.min.js": "dist-module/jquery.module.min.js",
- "jquery-@VER.module.min.map": "dist-module/jquery.module.min.map",
- "jquery-@VER.slim.module.js": "dist-module/jquery.slim.module.js",
- "jquery-@VER.slim.module.min.js": "dist-module/jquery.slim.module.min.js",
- "jquery-@VER.slim.module.min.map": "dist-module/jquery.slim.module.min.map"
-};
-
-const unversionedReleaseFiles = {
- "jquery.js": "dist/jquery.js",
- "jquery.min.js": "dist/jquery.min.js",
- "jquery.min.map": "dist/jquery.min.map",
- "jquery.slim.js": "dist/jquery.slim.js",
- "jquery.slim.min.js": "dist/jquery.slim.min.js",
- "jquery.slim.min.map": "dist/jquery.slim.min.map",
- "jquery.module.js": "dist-module/jquery.module.js",
- "jquery.module.min.js": "dist-module/jquery.module.min.js",
- "jquery.module.min.map": "dist-module/jquery.module.min.map",
- "jquery.slim.module.js": "dist-module/jquery.slim.module.js",
- "jquery.slim.module.min.js": "dist-module/jquery.slim.module.min.js",
- "jquery.slim.module.min.map": "dist-module/jquery.slim.module.min.map"
-};
-
-/**
- * Generates copies for the CDNs
- */
-function makeReleaseCopies( Release ) {
- [
- { filesMap: versionedReleaseFiles, cdnFolder: cdnFolderVersioned },
- { filesMap: unversionedReleaseFiles, cdnFolder: cdnFolderUnversioned }
- ].forEach( ( { filesMap, cdnFolder } ) => {
- shell.mkdir( "-p", cdnFolder );
-
- Object.keys( filesMap ).forEach( ( key ) => {
- let text;
- const builtFile = filesMap[ key ];
- const unpathedFile = key.replace( /@VER/g, Release.newVersion );
- const 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" );
}
-async function makeArchives( Release ) {
- Release.chdir( Release.dir.repo );
-
- async function makeArchive( { cdn, filesMap, cdnFolder } ) {
- return new Promise( ( resolve, reject ) => {
- if ( Release.preRelease ) {
- console.log(
- `Skipping archive creation for ${ cdn }; this is a beta release.`
- );
- resolve();
- return;
- }
-
- console.log( "Creating production archive for " + cdn );
-
- let i, sum, result;
- const archiver = require( "archiver" )( "zip" );
- const md5file = cdnFolder + "/" + cdn + "-md5.txt";
- const output = fs.createWriteStream(
- cdnFolder + "/" + cdn + "-jquery-" + Release.newVersion + ".zip"
- );
- const rmd5 = /[a-f0-9]{32}/;
- const rver = /@VER/;
+const archivesFolder = "tmp/archives";
+const versionedFolder = `${ archivesFolder }/versioned`;
+const unversionedFolder = `${ archivesFolder }/unversioned`;
- output.on( "close", resolve );
+// The cdn repo is cloned during release
+const cdnRepoFolder = "tmp/release/cdn";
- output.on( "error", ( err ) => {
- reject( err );
- } );
+// .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",
+ "dist-module/jquery.module.js",
+ "dist-module/jquery.slim.module.js"
+];
- archiver.pipe( output );
+const rminmap = /\.min\.map$/;
+const rjs = /\.js$/;
- let finalFilesMap = Object.create( null );
- for ( const [ releaseFile, builtFile ] of Object.entries( filesMap ) ) {
- finalFilesMap[ releaseFile.replace( rver, Release.newVersion ) ] =
- builtFile;
- }
+function clean() {
+ console.log( "Cleaning any existing archives..." );
+ return rimraf( archivesFolder );
+}
+
+// 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 }`
+ );
+ await exec(
+ `cp -f ${ file.replace(
+ rjs,
+ ".min.map"
+ ) } ${ unversionedFolder }/${ mapFilename }`
+ );
+ } )
+ );
+}
- const files = Object.keys( filesMap ).map(
- ( item ) => `${ cdnFolder }/${ item.replace( rver, Release.newVersion ) }`
+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 }`
);
+ await convertMapToVersioned( file, versionedFolder );
+ } )
+ );
+}
+
+async function copyToRepo( folder ) {
+ return exec( `cp -f ${ folder }/* ${ cdnRepoFolder }/cdn/` );
+}
+
+async function cdn() {
+ await clean();
+
+ await Promise.all( [ makeUnversionedCopies(), makeVersionedCopies() ] );
+
+ await copyToRepo( versionedFolder );
+
+ await Promise.all( [
+ archive( { cdn: "googlecdn", folder: unversionedFolder, version } ),
+ archive( { cdn: "mscdn", folder: versionedFolder, version } )
+ ] );
- 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"
- );
- }
- fs.writeFileSync( md5file, sum );
- files.push( md5file );
-
- files.forEach( ( file ) => {
- archiver.append( fs.createReadStream( file ), {
- name: path.basename( file )
- } );
- } );
-
- archiver.finalize();
- } );
- }
-
- async function buildGoogleCDN() {
- await makeArchive( {
- cdn: "googlecdn",
- filesMap: unversionedReleaseFiles,
- cdnFolder: cdnFolderUnversioned
- } );
- }
-
- async function buildMicrosoftCDN() {
- await makeArchive( {
- cdn: "mscdn",
- filesMap: versionedReleaseFiles,
- cdnFolder: cdnFolderVersioned
- } );
- }
-
- await buildGoogleCDN();
- await buildMicrosoftCDN();
+ 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 78dc5064f..5f1545b71 100644
--- a/build/release/dist.js
+++ b/build/release/dist.js
@@ -1,190 +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",
- "dist/package.json",
- "dist/jquery.bundler-require-wrapper.js",
- "dist/jquery.bundler-require-wrapper.slim.js",
- "dist-module/package.json",
- "dist-module/jquery.node-module-wrapper.js",
- "dist-module/jquery.node-module-wrapper.slim.js"
- ];
-
- /**
- * 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.
+const files = [
+ "dist",
+ "dist-module",
+ "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() {
- 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 }/**/*` );
-
- // Copy dist files
- shell.mkdir( "-p", `${ Release.dir.dist }/dist` );
- shell.mkdir( "-p", `${ Release.dir.dist }/dist-module` );
- files.forEach( function( file ) {
- shell.cp(
- "-f",
- `${ Release.dir.repo }/${ file }`,
- `${ Release.dir.dist }/${ file }`
- );
- } );
-
- // Copy other files
- extras.forEach( function( file ) {
- shell.cp(
- "-rf",
- `${ Release.dir.repo }/${ file }`,
- `${ Release.dir.dist }/${ file }`
- );
- } );
-
- // 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..f5dd1a165
--- /dev/null
+++ b/build/release/post-release.sh
@@ -0,0 +1,56 @@
+#!/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
+npm version $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 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..a7b4f18d7
--- /dev/null
+++ b/build/release/pre-release.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+set -euo pipefail
+
+# Cleans all release and build artifacts
+npm run build:clean
+npm run release:clean
+npm ci
+npm run authors:check
+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..43d77f800
--- /dev/null
+++ b/build/release/verify.js
@@ -0,0 +1,198 @@
+/**
+ * Verify the latest release is reproducible
+ * Works with versions 4.0.0-beta.2 and later
+ */
+import chalk from "chalk";
+import * as Diff from "diff";
+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 rstable = /^(\d+\.\d+\.\d+)$/;
+
+export async function verifyRelease( { version } = {} ) {
+ if ( !version ) {
+ version = process.env.VERSION || ( await getLatestVersion() );
+ }
+ console.log( `Checking jQuery ${ version }...` );
+ const release = await buildRelease( { version } );
+
+ let verified = true;
+
+ // Only check stable versions against the CDN
+ if ( rstable.test( version ) ) {
+ await Promise.all(
+ release.files.map( async( file ) => {
+ const cdnContents = await fetch( new URL( file.name, CDN_URL ) ).then(
+ ( res ) => res.text()
+ );
+ if ( cdnContents !== file.contents ) {
+ console.log( `${ file.name } is different from the CDN:` );
+ diffFiles( file.contents, cdnContents );
+ verified = false;
+ }
+ } )
+ );
+ }
+
+ // Check all releases 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 ) {
+ console.log( `${ version }.tgz is different from npm:` );
+ diffFiles( release.tgz.contents, tgzSum );
+ verified = false;
+ }
+
+ 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 ) {
+ console.log( `${ file.name } is different from the CDN:` );
+ diffFiles( file.contents, npmContents );
+ verified = false;
+ }
+ } )
+ );
+
+ if ( verified ) {
+ console.log( `jQuery ${ version } is reproducible!` );
+ } else {
+ 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 } );
+ await exec(
+ `git clone -q -b ${ version } --depth=1 ${ SRC_REPO } ${ releaseFolder }`
+ );
+
+ // Install node dependencies
+ console.log( `Installing dependencies for jQuery ${ version }...` );
+ await exec( "npm ci", { cwd: releaseFolder } );
+
+ // Build the release
+ console.log( `Building jQuery ${ version }...` );
+ const { stdout: buildOutput } = await exec( "npm run build:all", {
+ cwd: releaseFolder,
+ env: {
+ 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 ) => ( {
+ name: dirent.name,
+ path: path.basename( dirent.path ),
+ contents: await readFile( path.join( dirent.path, dirent.name ), "utf8" )
+ } ) )
+ );
+
+ // 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 diffFiles( a, b ) {
+ const diff = Diff.diffLines( a, b );
+
+ diff.forEach( ( part ) => {
+ if ( part.added ) {
+ console.log( chalk.green( part.value ) );
+ } else if ( part.removed ) {
+ console.log( chalk.red( part.value ) );
+ } else {
+ console.log( part.value );
+ }
+ } );
+}
+
+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 );
+}
diff --git a/build/tasks/build.js b/build/tasks/build.js
index 5ed1b9c32..1a3ed1d82 100644
--- a/build/tasks/build.js
+++ b/build/tasks/build.js
@@ -4,22 +4,21 @@
* 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 rollup = require( "rollup" );
-const excludedFromSlim = require( "./lib/slim-exclude" );
-const rollupFileOverrides = require( "./lib/rollup-plugin-file-overrides" );
-const pkg = require( "../../package.json" );
-const isCleanWorkingDir = require( "./lib/isCleanWorkingDir" );
-const processForDist = require( "./dist" );
-const minify = require( "./minify" );
-const getTimestamp = require( "./lib/getTimestamp" );
-const verifyNodeVersion = require( "./lib/verifyNodeVersion" );
-const srcFolder = path.resolve( __dirname, "../../src" );
+import fs from "node:fs/promises";
+import path from "node:path";
+import util from "node:util";
+import { exec as nodeExec } from "node:child_process";
+import * as rollup from "rollup";
+import excludedFromSlim from "./lib/slim-exclude.js";
+import rollupFileOverrides from "./lib/rollupFileOverridesPlugin.js";
+import isCleanWorkingDir from "./lib/isCleanWorkingDir.js";
+import processForDist from "./dist.js";
+import minify from "./minify.js";
+import getTimestamp from "./lib/getTimestamp.js";
+import { compareSize } from "./lib/compareSize.js";
+
+const exec = util.promisify( nodeExec );
+const pkg = JSON.parse( await fs.readFile( "./package.json", "utf8" ) );
const minimum = [ "core" ];
@@ -38,14 +37,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 +53,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 ( e ) {
@@ -141,7 +140,15 @@ async function checkExclude( exclude, include ) {
return [ unique( excluded ), unique( included ) ];
}
+async function getLastModifiedDate() {
+ const { stdout } = await exec( "git log -1 --format=\"%at\"" );
+ return new Date( parseInt( stdout, 10 ) * 1000 );
+}
+
async function writeCompiled( { code, dir, filename, version } ) {
+
+ // Use the last modified date so builds are reproducible
+ const date = await getLastModifiedDate();
const compiledContents = code
// Embed Version
@@ -149,14 +156,14 @@ async function writeCompiled( { code, dir, filename, version } ) {
// 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" ) );
await fs.writeFile( path.join( dir, filename ), compiledContents );
console.log( `[${ getTimestamp() }] ${ filename } v${ version } created.` );
}
// Build jQuery ECMAScript modules
-async function build( {
+export async function build( {
amd,
dir = "dist",
exclude = [],
@@ -206,7 +213,7 @@ async function build( {
if ( excluded.includes( "exports/global" ) ) {
const index = excluded.indexOf( "exports/global" );
setOverride(
- `${ srcFolder }/exports/global.js`,
+ "./src/exports/global.js",
"import { jQuery } from \"../core.js\";\n\n" +
"jQuery.noConflict = function() {};"
);
@@ -225,7 +232,7 @@ async function build( {
// No name means an anonymous define
const amdExportContents = await read( "exports/amd.js" );
setOverride(
- `${ srcFolder }/exports/amd.js`,
+ "./src/exports/amd.js",
amdExportContents.replace(
// Remove the comma for anonymous defines
@@ -248,7 +255,7 @@ async function build( {
}
const inputOptions = {
- input: `${ srcFolder }/jquery.js`
+ input: "./src/jquery.js"
};
const includedImports = included
@@ -274,7 +281,7 @@ async function build( {
// Replace excluded modules with empty sources.
for ( const module of excluded ) {
setOverride(
- `${ srcFolder }/${ module }.js`,
+ `./src/${ module }.js`,
// The `selector` module is not removed, but replaced
// with `selector-native`.
@@ -290,7 +297,7 @@ async function build( {
output: [ outputOptions ],
plugins: [ rollupFileOverrides( fileOverrides ) ],
watch: {
- include: `${ srcFolder }/**`,
+ include: "./src/**",
skipWrite: true
}
} );
@@ -352,7 +359,7 @@ async function build( {
}
}
-async function buildDefaultFiles( {
+export async function buildDefaultFiles( {
version = process.env.VERSION,
watch
} = {} ) {
@@ -407,20 +414,16 @@ async function buildDefaultFiles( {
} )
] );
- // Earlier Node.js versions do not support the ESM format.
- if ( !verifyNodeVersion() ) {
- return;
+ if ( watch ) {
+ console.log( "Watching files..." );
+ } else {
+ return compareSize( {
+ files: [
+ "dist/jquery.min.js",
+ "dist/jquery.slim.min.js",
+ "dist-module/jquery.module.min.js",
+ "dist-module/jquery.slim.module.min.js"
+ ]
+ } );
}
-
- const { compareSize } = await import( "./compare_size.mjs" );
- return compareSize( {
- files: [
- "dist/jquery.min.js",
- "dist/jquery.slim.min.js",
- "dist-module/jquery.module.min.js",
- "dist-module/jquery.slim.module.min.js"
- ]
- } );
}
-
-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 a94cf42f0..729bb37f2 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/rollup-plugin-file-overrides.js b/build/tasks/lib/rollupFileOverridesPlugin.js
index c494e4e5d..fecb4efaa 100644
--- a/build/tasks/lib/rollup-plugin-file-overrides.js
+++ b/build/tasks/lib/rollupFileOverridesPlugin.js
@@ -1,5 +1,3 @@
-"use strict";
-
/**
* A Rollup plugin accepting a file overrides map and changing
* module sources to the overridden ones where provided. Files
@@ -7,7 +5,7 @@
*
* @param {Map<string, string>} fileOverrides
*/
-module.exports = ( fileOverrides ) => {
+export default function rollupFileOverrides( fileOverrides ) {
return {
name: "jquery-file-overrides",
load( id ) {
@@ -21,4 +19,4 @@ module.exports = ( fileOverrides ) => {
return null;
}
};
-};
+}
diff --git a/build/tasks/lib/slim-exclude.js b/build/tasks/lib/slim-exclude.js
index cc74cbac5..8ffc22728 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",
"callbacks",
"deferred",
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 8c536c1ef..aff942b32 100644
--- a/build/tasks/minify.js
+++ b/build/tasks/minify.js
@@ -1,14 +1,12 @@
-"use strict";
-
-const swc = require( "@swc/core" );
-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 swc from "@swc/core";
+import processForDist from "./dist.js";
+import getTimestamp from "./lib/getTimestamp.js";
const rjs = /\.js$/;
-module.exports = async function minify( { filename, dir, esm } ) {
+export default async function minify( { filename, dir, esm } ) {
const contents = await fs.readFile( path.join( dir, filename ), "utf8" );
const version = /jQuery JavaScript Library ([^\n]+)/.exec( contents )[ 1 ];
@@ -67,4 +65,4 @@ module.exports = async function minify( { filename, dir, esm } ) {
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 7d6588648..fa0922618 100644
--- a/build/tasks/node_smoke_tests.js
+++ b/build/tasks/node_smoke_tests.js
@@ -1,17 +1,12 @@
-"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" );
+const exec = util.promisify( nodeExec );
const allowedLibraryTypes = new Set( [ "regular", "factory" ] );
const allowedSourceTypes = new Set( [ "commonjs", "module", "dual" ] );
-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
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 5c3c8fbee..6f49f0230 100644
--- a/build/tasks/promises_aplus_tests.js
+++ b/build/tasks/promises_aplus_tests.js
@@ -1,17 +1,9 @@
-"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 = [
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" );