-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[et] Add
promote-packages
expotools command (#8322)
- Loading branch information
Showing
8 changed files
with
403 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Command } from '@expo/commander'; | ||
|
||
import { TaskRunner, Task } from '../TasksRunner'; | ||
import { CommandOptions, TaskArgs } from '../promote-packages/types'; | ||
|
||
import { promotePackages } from '../promote-packages/tasks/promotePackages'; | ||
import { listPackagesToPromote } from '../promote-packages/tasks/listPackagesToPromote'; | ||
|
||
export default (program: Command) => { | ||
program | ||
.command('promote-packages [packageNames...]') | ||
.alias('promote-pkgs') | ||
.option( | ||
'-e, --exclude <packageName>', | ||
'Name of the package to be excluded from promoting. Can be passed multiple times to exclude more than one package. It has higher priority than the list of package names to promote.', | ||
(value, previous) => previous.concat(value), | ||
[] | ||
) | ||
.option( | ||
'-t, --tag <tag>', | ||
'Tag to which packages should be promoted. Defaults to `latest`.', | ||
'latest' | ||
) | ||
.option( | ||
'--no-select', | ||
'With this flag the script will not prompt to select packages, they all will be selected by default.', | ||
false | ||
) | ||
.option( | ||
'--no-drop', | ||
'Without this flag, existing tags for the local version would be dropped after all.', | ||
false | ||
) | ||
.option( | ||
'-d, --demote', | ||
'Enables tag demoting. If passed, the tag can be overriden even if its current version is higher than locally.', | ||
false | ||
) | ||
.option( | ||
'-l, --list', | ||
'Lists packages with unpublished changes since the previous version.', | ||
false | ||
) | ||
|
||
/* debug */ | ||
.option('-D, --dry', 'Whether to skip `npm dist-tag add` command.', false) | ||
|
||
.description('Promotes local versions of monorepo packages to given tag on NPM repository.') | ||
.asyncAction(async (packageNames: string[], options: CommandOptions) => { | ||
// Commander doesn't put arguments to options object, let's add it for convenience. In fact, this is an option. | ||
options.packageNames = packageNames; | ||
|
||
const taskRunner = new TaskRunner<TaskArgs>({ | ||
tasks: tasksForOptions(options), | ||
}); | ||
|
||
await taskRunner.runAndExitAsync([], options); | ||
}); | ||
}; | ||
|
||
/** | ||
* Returns target task instances based on provided command options. | ||
*/ | ||
function tasksForOptions(options: CommandOptions): Task<TaskArgs>[] { | ||
if (options.list) { | ||
return [listPackagesToPromote]; | ||
} | ||
return [promotePackages]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import chalk from 'chalk'; | ||
|
||
import { Parcel } from './types'; | ||
import * as Changelogs from '../Changelogs'; | ||
import { GitDirectory } from '../Git'; | ||
import logger from '../Logger'; | ||
import { Package } from '../Packages'; | ||
|
||
const { cyan, green, magenta, red, gray } = chalk; | ||
|
||
/** | ||
* Wraps `Package` object into a parcels - convenient wrapper providing more package-related helpers. | ||
*/ | ||
export async function createParcelAsync(pkg: Package): Promise<Parcel> { | ||
return { | ||
pkg, | ||
pkgView: await pkg.getPackageViewAsync(), | ||
changelog: Changelogs.loadFrom(pkg.changelogPath), | ||
gitDir: new GitDirectory(pkg.path), | ||
state: {}, | ||
}; | ||
} | ||
|
||
/** | ||
* Formats version change from version A to version B. | ||
*/ | ||
export function formatVersionChange( | ||
fromVersion: string | null | undefined, | ||
toVersion: string | ||
): string { | ||
const from = fromVersion ? cyan.bold(fromVersion) : gray.bold('none'); | ||
const to = cyan.bold(toVersion); | ||
return `from ${from} to ${to}`; | ||
} | ||
|
||
/** | ||
* Prints a lists of packages to promote or demote. | ||
*/ | ||
export function printPackagesToPromote(parcels: Parcel[]): void { | ||
const toPromote = parcels.filter(({ state }) => !state.isDemoting); | ||
const toDemote = parcels.filter(({ state }) => state.isDemoting); | ||
|
||
printPackagesToPromoteInternal( | ||
toPromote, | ||
`Following packages would be ${green.bold('promoted')}:` | ||
); | ||
printPackagesToPromoteInternal( | ||
toDemote, | ||
`Following packages could be ${red.bold('demoted')} ${gray(`(requires --demote flag)`)}:` | ||
); | ||
} | ||
|
||
function printPackagesToPromoteInternal(parcels: Parcel[], headerText: string): void { | ||
if (parcels.length > 0) { | ||
logger.log(' ', magenta(headerText)); | ||
|
||
for (const { pkg, state } of parcels) { | ||
logger.log( | ||
' ', | ||
green(pkg.packageName), | ||
formatVersionChange(state.versionToReplace, pkg.packageVersion) | ||
); | ||
} | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
tools/expotools/src/promote-packages/tasks/findPackagesToPromote.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import semver from 'semver'; | ||
|
||
import logger from '../../Logger'; | ||
import { Task } from '../../TasksRunner'; | ||
import { CommandOptions, Parcel, TaskArgs } from '../types'; | ||
|
||
/** | ||
* Finds packages whose local version is not tagged as the target tag provided as a command option (defaults to `latest`). | ||
*/ | ||
export const findPackagesToPromote = new Task<TaskArgs>( | ||
{ | ||
name: 'findPackagesToPromote', | ||
}, | ||
async (parcels: Parcel[], options: CommandOptions): Promise<symbol | TaskArgs> => { | ||
logger.info('\n👀 Searching for packages to promote...'); | ||
|
||
const newParcels: Parcel[] = []; | ||
|
||
await Promise.all( | ||
parcels.map(async (parcel) => { | ||
const { pkg, pkgView, state } = parcel; | ||
const currentDistTags = await pkg.getDistTagsAsync(); | ||
const versionToReplace = pkgView?.['dist-tags']?.[options.tag] ?? null; | ||
const canPromote = pkgView && !currentDistTags.includes(options.tag); | ||
|
||
state.distTags = currentDistTags; | ||
state.versionToReplace = versionToReplace; | ||
state.isDemoting = !!versionToReplace && semver.lt(pkg.packageVersion, versionToReplace); | ||
|
||
if (canPromote && (!state.isDemoting || options.list || options.demote)) { | ||
newParcels.push(parcel); | ||
} | ||
}) | ||
); | ||
|
||
if (newParcels.length === 0) { | ||
logger.success('\n✅ No packages to promote.\n'); | ||
return Task.STOP; | ||
} | ||
return [newParcels, options]; | ||
} | ||
); |
29 changes: 29 additions & 0 deletions
29
tools/expotools/src/promote-packages/tasks/listPackagesToPromote.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import chalk from 'chalk'; | ||
|
||
import logger from '../../Logger'; | ||
import { Task } from '../../TasksRunner'; | ||
import { printPackagesToPromote } from '../helpers'; | ||
import { CommandOptions, Parcel, TaskArgs } from '../types'; | ||
import { findPackagesToPromote } from './findPackagesToPromote'; | ||
import { prepareParcels } from './prepareParcels'; | ||
|
||
const { yellow } = chalk; | ||
|
||
/** | ||
* Lists packages that can be promoted to given tag. | ||
*/ | ||
export const listPackagesToPromote = new Task<TaskArgs>( | ||
{ | ||
name: 'listPackagesToPromote', | ||
dependsOn: [prepareParcels, findPackagesToPromote], | ||
}, | ||
async (parcels: Parcel[], options: CommandOptions): Promise<void | symbol> => { | ||
if (parcels.length === 0) { | ||
logger.success(`\n✅ No packages to promote.\n`); | ||
return Task.STOP; | ||
} | ||
|
||
logger.info(`\n📚 Packages to promote to ${yellow.bold(options.tag)}:`); | ||
printPackagesToPromote(parcels); | ||
} | ||
); |
30 changes: 30 additions & 0 deletions
30
tools/expotools/src/promote-packages/tasks/prepareParcels.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import logger from '../../Logger'; | ||
import { getListOfPackagesAsync } from '../../Packages'; | ||
import { Task } from '../../TasksRunner'; | ||
import { CommandOptions, Parcel, TaskArgs } from '../types'; | ||
import { createParcelAsync } from '../helpers'; | ||
|
||
/** | ||
* Gets a list of public packages in the monorepo, downloads `npm view` result of them, | ||
* creates their Changelog instance and fills in given parcels array (it's empty at the beginning). | ||
*/ | ||
export const prepareParcels = new Task<TaskArgs>( | ||
{ | ||
name: 'prepareParcels', | ||
}, | ||
async (parcels: Parcel[], options: CommandOptions) => { | ||
logger.info('🔎 Gathering data about packages...'); | ||
|
||
const { exclude, packageNames } = options; | ||
const allPackages = await getListOfPackagesAsync(); | ||
const filteredPackages = allPackages.filter((pkg) => { | ||
const isPrivate = pkg.packageJson.private; | ||
const isIncluded = packageNames.length === 0 || packageNames.includes(pkg.packageName); | ||
const isExcluded = exclude.includes(pkg.packageName); | ||
|
||
return !isPrivate && isIncluded && !isExcluded; | ||
}); | ||
|
||
parcels.push(...(await Promise.all(filteredPackages.map(createParcelAsync)))); | ||
} | ||
); |
61 changes: 61 additions & 0 deletions
61
tools/expotools/src/promote-packages/tasks/promotePackages.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import chalk from 'chalk'; | ||
|
||
import logger from '../../Logger'; | ||
import * as Npm from '../../Npm'; | ||
import { Task } from '../../TasksRunner'; | ||
import { formatVersionChange } from '../helpers'; | ||
import { CommandOptions, Parcel, TaskArgs } from '../types'; | ||
import { findPackagesToPromote } from './findPackagesToPromote'; | ||
import { prepareParcels } from './prepareParcels'; | ||
import { selectPackagesToPromote } from './selectPackagesToPromote'; | ||
|
||
const { yellow, red, green, cyan } = chalk; | ||
|
||
/** | ||
* Promotes local versions of selected packages to npm tag passed as an option. | ||
*/ | ||
export const promotePackages = new Task<TaskArgs>( | ||
{ | ||
name: 'promotePackages', | ||
dependsOn: [prepareParcels, findPackagesToPromote, selectPackagesToPromote], | ||
}, | ||
async (parcels: Parcel[], options: CommandOptions): Promise<void> => { | ||
logger.info(`\n🚀 Promoting packages to ${yellow.bold(options.tag)} tag...`); | ||
|
||
await Promise.all( | ||
parcels.map(async ({ pkg, state }) => { | ||
const currentVersion = pkg.packageVersion; | ||
const { versionToReplace } = state; | ||
|
||
const batch = logger.batch(); | ||
const action = state.isDemoting ? red('Demoting') : green('Promoting'); | ||
batch.log(' ', green.bold(pkg.packageName)); | ||
batch.log( | ||
' ', | ||
action, | ||
yellow(options.tag), | ||
formatVersionChange(versionToReplace, currentVersion) | ||
); | ||
|
||
// Tag the local version of the package. | ||
if (!options.dry) { | ||
await Npm.addTagAsync(pkg.packageName, pkg.packageVersion, options.tag); | ||
} | ||
|
||
// If the local version had any tags assigned, we can drop the old ones. | ||
if (options.drop && state.distTags && !state.distTags.includes(options.tag)) { | ||
for (const distTag of state.distTags) { | ||
batch.log(' ', `Dropping ${yellow(distTag)} tag (${cyan(currentVersion)})...`); | ||
|
||
if (!options.dry) { | ||
await Npm.removeTagAsync(pkg.packageName, distTag); | ||
} | ||
} | ||
} | ||
batch.flush(); | ||
}) | ||
); | ||
|
||
logger.success(`\n✅ Successfully promoted ${cyan(parcels.length + '')} packages.`); | ||
} | ||
); |
79 changes: 79 additions & 0 deletions
79
tools/expotools/src/promote-packages/tasks/selectPackagesToPromote.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import chalk from 'chalk'; | ||
import inquirer from 'inquirer'; | ||
import readline from 'readline'; | ||
import stripAnsi from 'strip-ansi'; | ||
|
||
import logger from '../../Logger'; | ||
import { Task } from '../../TasksRunner'; | ||
import { formatVersionChange } from '../helpers'; | ||
import { CommandOptions, Parcel, TaskArgs } from '../types'; | ||
import { findPackagesToPromote } from './findPackagesToPromote'; | ||
|
||
const { green, red } = chalk; | ||
|
||
/** | ||
* Prompts the user to select packages to promote or demote. | ||
* It's skipped if `--no-select` option is used or it's run on the CI. | ||
*/ | ||
export const selectPackagesToPromote = new Task<TaskArgs>( | ||
{ | ||
name: 'selectPackagesToPromote', | ||
dependsOn: [findPackagesToPromote], | ||
}, | ||
async (parcels: Parcel[], options: CommandOptions): Promise<void | TaskArgs> => { | ||
if (!options.select || process.env.CI) { | ||
return [parcels, options]; | ||
} | ||
|
||
logger.info('\n👉 Selecting packages to promote...\n'); | ||
|
||
const packageNames = await promptForPackagesToPromoteAsync(parcels); | ||
const newParcels = parcels.filter(({ pkg }) => packageNames.includes(pkg.packageName)); | ||
|
||
return [newParcels, options]; | ||
} | ||
); | ||
|
||
/** | ||
* Prompts the user to select packages to promote or demote. | ||
*/ | ||
async function promptForPackagesToPromoteAsync(parcels: Parcel[]): Promise<string[]> { | ||
const maxLength = parcels.reduce((acc, { pkg }) => Math.max(acc, pkg.packageName.length), 0); | ||
const choices = parcels.map(({ pkg, state }) => { | ||
const action = state.isDemoting ? red.bold('demote') : green.bold('promote'); | ||
|
||
return { | ||
name: `${green(pkg.packageName.padEnd(maxLength))} ${action} ${formatVersionChange( | ||
state.versionToReplace, | ||
pkg.packageVersion | ||
)}`, | ||
value: pkg.packageName, | ||
checked: !state.isDemoting, | ||
}; | ||
}); | ||
const { selectedPackageNames } = await inquirer.prompt([ | ||
{ | ||
type: 'checkbox', | ||
name: 'selectedPackageNames', | ||
message: 'Which packages do you want to promote?\n', | ||
choices: [ | ||
// Choices unchecked by default (these being demoted) should be on top. | ||
// We could sort them, but JS sorting algorithm is unstable :/ | ||
...choices.filter((choice) => !choice.checked), | ||
...choices.filter((choice) => choice.checked), | ||
], | ||
pageSize: Math.max(15, (process.stdout.rows ?? 15) - 15), | ||
}, | ||
]); | ||
// Inquirer shows all those selected choices by name and that looks so ugly due to line wrapping. | ||
// If possible, we clear everything that has been printed after the prompt. | ||
if (process.stdout.columns) { | ||
const bufferLength = choices.reduce( | ||
(acc, choice) => acc + stripAnsi(choice.name).length + 2, | ||
0 | ||
); | ||
readline.moveCursor(process.stdout, 0, -Math.ceil(bufferLength / process.stdout.columns)); | ||
readline.clearScreenDown(process.stdout); | ||
} | ||
return selectedPackageNames; | ||
} |
Oops, something went wrong.