diff --git a/scripts/bumpVersions.js b/scripts/bumpVersions.js index d97bc47e113..41142ced1b6 100644 --- a/scripts/bumpVersions.js +++ b/scripts/bumpVersions.js @@ -16,6 +16,8 @@ const fetch = require('node-fetch'); const semver = require('semver'); const readline = require("readline"); const chalk = require('chalk'); +const http = require('http'); +const qs = require('querystring'); let levels = { alpha: 1, @@ -24,306 +26,340 @@ let levels = { released: 4 }; -// Packages to release -let publicPackages = { - '@adobe/react-spectrum': 'released', - '@react-spectrum/actiongroup': 'released', - '@react-spectrum/breadcrumbs' : 'released', - '@react-spectrum/button': 'released', - '@react-spectrum/buttongroup': 'released', - '@react-spectrum/checkbox': 'released', - '@react-spectrum/dialog': 'released', - '@react-spectrum/divider': 'released', - '@react-spectrum/form': 'released', - '@react-spectrum/icon': 'released', - '@react-spectrum/illustratedmessage': 'released', - '@react-spectrum/image': 'released', - '@react-spectrum/label': 'released', - '@react-spectrum/layout': 'released', - '@react-spectrum/link': 'released', - '@react-spectrum/listbox': 'released', - '@react-spectrum/menu': 'released', - '@react-spectrum/meter': 'released', - '@react-spectrum/overlays': 'released', - '@react-spectrum/picker': 'released', - '@react-spectrum/progress': 'released', - '@react-spectrum/provider': 'released', - '@react-spectrum/radio': 'released', - '@react-spectrum/searchfield': 'released', - '@react-spectrum/statuslight': 'released', - '@react-spectrum/switch': 'released', - '@react-spectrum/table': 'alpha', - '@react-types/table': 'rc', - '@react-spectrum/text': 'released', - '@react-spectrum/textfield': 'released', - '@react-spectrum/theme-dark': 'released', - '@react-spectrum/theme-default': 'released', - '@react-spectrum/utils': 'released', - '@react-spectrum/view': 'released', - '@react-spectrum/well': 'released', - '@spectrum-icons/color': 'released', - '@spectrum-icons/workflow': 'released', - '@spectrum-icons/illustrations': 'released', - '@react-stately/data': 'released', - '@react-aria/aria-modal-polyfill': 'released' -}; - // Packages never to release let excludedPackages = new Set([ '@adobe/spectrum-css-temp', '@react-spectrum/test-utils', - '@spectrum-icons/build-tools' + '@spectrum-icons/build-tools', + '@react-spectrum/docs' ]); -// Get dependency tree from yarn workspaces, and build full list of packages to release -// based on dependencies of the public packages. -let info = JSON.parse(exec('yarn workspaces info --json').toString().split('\n').slice(1, -2).join('\n')); -let releasedPackages = new Map(); - -// If releasing an individual package, bump that package and all packages that depend on it. -// Otherwise, add all public packages and their dependencies. -let arg = process.argv[process.argv.length - 1]; -let bump = /^(major|minor|patch)$/.test(arg) ? arg : 'patch'; -if (arg.startsWith('@')) { - if (!info[arg]) { - throw new Error('Invalid package ' + arg); +class VersionManager { + constructor() { + // Get dependency tree from yarn workspaces + this.workspacePackages = JSON.parse(exec('yarn workspaces info --json').toString().split('\n').slice(1, -2).join('\n')); + this.existingPackages = new Set(); + this.changedPackages = new Set(); + this.versionBumps = {}; + this.releasedPackages = new Map(); } - let addPackage = (pkg, status) => { - if (excludedPackages.has(pkg)) { - return; - } - - let filePath = info[pkg].location + '/package.json'; - let pkgJSON = JSON.parse(fs.readFileSync(filePath, 'utf8')); - if (pkgJSON.private) { - return; - } - - if (releasedPackages.has(pkg)) { - let cur = releasedPackages.get(pkg); - if (levels[status] > levels[cur.level]) { - cur.status = status; - } + async run() { + await this.getExistingPackages(); + this.getChangedPackages(); - return; - } - - releasedPackages.set(pkg, { - location: info[pkg].location, - status - }); - - for (let p in info) { - if (releasedPackages.has(p)) { + await this.getVersionBumps(); + for (let pkg in this.versionBumps) { + let bump = this.versionBumps[pkg]; + if (bump === 'unreleased' || bump === 'unchanged') { continue; } - if (info[p].workspaceDependencies.includes(pkg)) { - addPackage(p, status); - } + this.addReleasedPackage(pkg, bump); } - }; - addPackage(arg, publicPackages[arg]); + let versions = this.getVersions(); + await this.promptVersions(versions); + this.bumpVersions(versions); + this.commit(versions); + } - for (let pkg in info) { - if (!releasedPackages.has(pkg)) { - excludedPackages.add(pkg); + async getExistingPackages() { + // Find what packages already exist on npm + let promises = []; + for (let name in this.workspacePackages) { + promises.push( + fetch(`https://registry.npmjs.com/${name}`, {method: 'HEAD'}) + .then(res => { + if (res.ok) { + this.existingPackages.add(name); + } + }) + ); } + + await Promise.all(promises); } -} else { - let addPackage = (pkg, status) => { - if (excludedPackages.has(pkg)) { - return; - } - if (releasedPackages.has(pkg)) { - let cur = releasedPackages.get(pkg); - if (levels[status] > levels[cur.level] && !publicPackages[pkg]) { - cur.status = status; - } + getChangedPackages() { + let res = exec("git diff $(git describe --tags --abbrev=0)..HEAD --name-only packages ':!**/dev/**' ':!**/docs/**' ':!**/test/**' ':!**/stories/**' ':!**/chromatic/**'", {encoding: 'utf8'}); - return; + for (let line of res.trim().split('\n')) { + let parts = line.split('/'); + let name = parts.slice(1, 3).join('/'); + let pkg = JSON.parse(fs.readFileSync(`packages/${name}/package.json`, 'utf8')); + this.changedPackages.add(name); } + } - releasedPackages.set(pkg, { - location: info[pkg].location, - status: publicPackages[pkg] || status - }); - - for (let dep of info[pkg].workspaceDependencies) { - addPackage(dep, status); - } - }; + async getVersionBumps() { + return new Promise((resolve) => { + let server = http.createServer(async (req, res) => { + if (req.method === 'POST') { + await this.serveVersionBumps(req, res); + server.close(); + req.connection.destroy(); + resolve(); + } else { + this.serveChangedPackages(res); + } + }); - for (let pkg in publicPackages) { - addPackage(pkg, publicPackages[pkg]); + server.listen(9000, () => { + exec('open http://localhost:9000'); + }); + }); } -} - -run(); -async function run() { - let existingPackages = await getExistingPackages(); - let versions = getVersions(existingPackages); - await promptVersions(versions); - bumpVersions(versions); - commit(versions); -} + serveChangedPackages(res) { + res.setHeader('Content-Type', 'text/html'); + res.end(` + + +
+ + + + `); + } -async function getExistingPackages() { - // Find what packages already exist on npm - let existing = new Set(); - let promises = []; - for (let [name, {location}] of releasedPackages) { - promises.push( - fetch(`https://registry.npmjs.com/${name}`, {method: 'HEAD'}) - .then(res => { - if (res.ok) { - existing.add(name); - } - }) - ); + serveVersionBumps(req, res) { + return new Promise((resolve) => { + let body = ''; + req.on('data', (data) => { + body += data; + }); + + req.on('end', () => { + this.versionBumps = qs.parse(body); + res.setHeader('Content-Type', 'text/html'); + res.end('Done!', () => { + resolve(); + }); + }); + }); } - await Promise.all(promises); - return existing; -} + addReleasedPackage(pkg, bump, isDep = false) { + bump = this.versionBumps[pkg] || bump; + if (excludedPackages.has(pkg) || bump === 'unpublished' || bump === 'unchanged') { + return; + } -function getVersions(existingPackages) { - let versions = new Map(); - for (let [name, {location, status}] of releasedPackages) { - let filePath = location + '/package.json'; - let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); - - // If the package already exists on npm, then increment the version - // number to the correct status. If it's a new package, then ensure - // the package.json version is correct according to the status. - if (existingPackages.has(name)) { - let newVersion = status === 'released' - ? semver.inc(pkg.version, bump) - : semver.inc(pkg.version, 'prerelease', status) - versions.set(name, [pkg.version, newVersion, pkg.private]); - } else { - let parsed = semver.parse(pkg.version); - let newVersion = pkg.version; - if (parsed.prerelease.length > 0) { - if (status === 'released') { - newVersion = semver.inc(pkg.version, bump); - } else if (parsed.prerelease[0] !== status) { - newVersion = semver.inc(pkg.version, 'prerelease', status); - } else { - parsed.prerelease[1] = 0; - newVersion = parsed.format(); - } + let status = bump === 'alpha' || bump === 'beta' || bump === 'rc' ? bump : 'released'; + + if (this.releasedPackages.has(pkg)) { + let cur = this.releasedPackages.get(pkg); + if (!isDep || levels[status] > levels[cur.status]) { + cur.status = status; + cur.bump = bump; } else { - if (status === 'released') { - newVersion = '3.0.0'; - } else { - newVersion = semver.inc(pkg.version, 'prerelease', status); - } + status = cur.status; + bump = cur.bump; } - - versions.set(name, [pkg.version, newVersion, pkg.private]); + } else { + this.releasedPackages.set(pkg, { + location: this.workspacePackages[pkg].location, + status, + bump + }); } - } - - return versions; -} -async function promptVersions(versions) { - console.log(''); - for (let [name, [oldVersion, newVersion, private]] of versions) { - if (newVersion !== oldVersion || private) { - console.log(`${name}: ${chalk.blue(oldVersion)}${private ? chalk.red(' (private)') : ''} => ${chalk.green(newVersion)}`); - } - } + // Bump anything that depends on this package if it's a prerelease + // because dependencies will be pinned rather than caret ranges. + if (status !== 'released') { + for (let p in this.workspacePackages) { + if (this.releasedPackages.has(p)) { + continue; + } - let loggedSpace = false; - for (let name in info) { - if (!releasedPackages.has(name) && !excludedPackages.has(name)) { - let filePath = info[name].location + '/package.json'; - let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); - if (!pkg.private) { - if (!loggedSpace) { - console.log(''); - loggedSpace = true; + if (this.workspacePackages[p].workspaceDependencies.includes(pkg)) { + if (this.existingPackages.has(p)) { + this.addReleasedPackage(p, bump, true); + } } + } + } - console.warn(chalk.red(`${name} will change from public to private`)); + // Ensure all dependencies of this package are published and up to date + for (let dep of this.workspacePackages[pkg].workspaceDependencies) { + if (!this.existingPackages.has(dep) || this.changedPackages.has(dep)) { + this.addReleasedPackage(dep, bump); } } } - console.log(''); + getVersions() { + let versions = new Map(); + for (let [name, {location, status, bump}] of this.releasedPackages) { + let filePath = this.workspacePackages[name].location + '/package.json'; + let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); - let rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + if (!bump || bump === 'unreleased' || bump === 'unchanged') { + versions.set(name, [pkg.version, pkg.version, pkg.private]); + continue; + } - return new Promise((resolve, reject) => { - rl.question('Do you want to continue? (y/n) ', function(c) { - rl.close(); - if (c === 'n') { - reject('Not continuing'); - } else if (c === 'y') { - resolve(); + // If the package already exists on npm, then increment the version + // number to the correct status. If it's a new package, then ensure + // the package.json version is correct according to the status. + if (this.existingPackages.has(name)) { + let newVersion = status === 'released' + ? semver.inc(pkg.version, bump) + : semver.inc(pkg.version, 'prerelease', status) + versions.set(name, [pkg.version, newVersion, pkg.private]); } else { - reject('Invalid answer'); + let newVersion = '3.0.0'; + if (status !== 'released') { + newVersion += `-${status}.0`; + } + + versions.set(name, [pkg.version, newVersion, pkg.private]); } - }); - }); -} + } -function bumpVersions(versions) { - for (let [name, {location}] of releasedPackages) { - let filePath = location + '/package.json'; - let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); - pkg.version = versions.get(name)[1]; + return versions; + } - if (pkg.private) { - delete pkg.private; + async promptVersions(versions) { + console.log(''); + for (let [name, [oldVersion, newVersion, isPrivate]] of versions) { + if (newVersion !== oldVersion || isPrivate) { + console.log(`${name}: ${chalk.blue(oldVersion)}${isPrivate ? chalk.red(' (private)') : ''} => ${chalk.green(newVersion)}`); + } } - for (let dep in pkg.dependencies) { - if (versions.has(dep)) { - let {status} = releasedPackages.get(dep); - pkg.dependencies[dep] = (status === 'released' ? '^' : '') + versions.get(dep)[1]; + let loggedSpace = false; + for (let name in this.workspacePackages) { + if (!this.releasedPackages.has(name) && !excludedPackages.has(name) && !this.existingPackages.has(name)) { + let filePath = this.workspacePackages[name].location + '/package.json'; + let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); + if (!pkg.private) { + if (!loggedSpace) { + console.log(''); + loggedSpace = true; + } + + console.warn(chalk.red(`${name} will change from public to private`)); + } } } - fs.writeFileSync(filePath, JSON.stringify(pkg, false, 2) + '\n'); + console.log(''); + + let rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve, reject) => { + rl.question('Do you want to continue? (y/n) ', function(c) { + rl.close(); + if (c === 'n') { + reject('Not continuing'); + } else if (c === 'y') { + resolve(); + } else { + reject('Invalid answer'); + } + }); + }); } - for (let name in info) { - if (!releasedPackages.has(name) && !excludedPackages.has(name)) { - let filePath = info[name].location + '/package.json'; + bumpVersions(versions) { + for (let [name, {location}] of this.releasedPackages) { + let filePath = location + '/package.json'; let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); - if (!pkg.private) { - pkg = insertKey(pkg, 'license', 'private', true); + pkg.version = versions.get(name)[1]; + + if (pkg.private) { + delete pkg.private; } for (let dep in pkg.dependencies) { if (versions.has(dep)) { - let {status} = releasedPackages.get(dep); + let {status} = this.releasedPackages.get(dep); pkg.dependencies[dep] = (status === 'released' ? '^' : '') + versions.get(dep)[1]; } } fs.writeFileSync(filePath, JSON.stringify(pkg, false, 2) + '\n'); } + + for (let name in this.workspacePackages) { + if (!this.releasedPackages.has(name)) { + let filePath = this.workspacePackages[name].location + '/package.json'; + let pkg = JSON.parse(fs.readFileSync(filePath, 'utf8')); + + // Mark package as private if it wasn't already published. + if (!pkg.private && !excludedPackages.has(name) && !this.existingPackages.has(name)) { + pkg = insertKey(pkg, 'license', 'private', true); + } + + // Ensure pinned dependencies of unpublished packages in the monorepo are updated. + for (let dep in pkg.dependencies) { + if (versions.has(dep)) { + let {status} = this.releasedPackages.get(dep); + if (status !== 'released') { + pkg.dependencies[dep] = versions.get(dep)[1]; + } + } + } + + fs.writeFileSync(filePath, JSON.stringify(pkg, false, 2) + '\n'); + } + } } -} -function commit(versions) { - exec('git commit -a -m "Publish"', {stdio: 'inherit'}); - for (let [name, [, newVersion]] of versions) { - exec(`git tag ${name}@${newVersion}`, {stdio: 'inherit'}); + commit(versions) { + exec('git commit -a -m "Publish"', {stdio: 'inherit'}); + for (let [name, [, newVersion]] of versions) { + exec(`git tag ${name}@${newVersion}`, {stdio: 'inherit'}); + } } } +new VersionManager().run(); + function insertKey(obj, afterKey, key, value) { let res = {}; for (let k in obj) {