1import chalk from 'chalk';
2import inquirer from 'inquirer';
3import stripAnsi from 'strip-ansi';
4
5import logger from '../Logger';
6import {
7  AndroidProjectDependenciesUpdates,
8  AndroidProjectReport,
9  GradleDependency,
10  GradleDependencyUpdate,
11} from './types';
12import {
13  addColorBasedOnSemverDiff,
14  calculateSemverDiff,
15  getChangelogLink,
16  SemverDiff,
17} from './utils';
18
19function generateAndroidProjectsSelectionChoice({
20  projectName,
21  gradleReport: { outdated, exceeded, unresolved },
22}: AndroidProjectReport) {
23  const deprecationMarking =
24    outdated.length > 0 ? ` ${chalk.yellow(`(${outdated.length} ⚠️ )`)}` : '';
25  const hasUnresolvedOrExceedeedMarking =
26    exceeded.length > 0 || unresolved.length > 0
27      ? ` ${chalk.red(`(${exceeded.length + unresolved.length} ❗️)`)}`
28      : '';
29  const name = `${projectName}${deprecationMarking}${hasUnresolvedOrExceedeedMarking}`;
30  return {
31    name,
32    value: projectName,
33    checked: outdated.length > 0 || exceeded.length > 0 || unresolved.length > 0,
34  };
35}
36
37export async function promptForAndroidProjectsSelection(
38  reports: AndroidProjectReport[]
39): Promise<AndroidProjectReport[]> {
40  const { selectedProjects } = await inquirer.prompt<{ selectedProjects: string[] }>([
41    {
42      type: 'checkbox',
43      name: 'selectedProjects',
44      message: `Choose which projects need updates. ${chalk.yellow(
45        '(<number> ⚠️ )'
46      )} shows how many dependencies are outdated. ${chalk.red(
47        '(<number> ❗️)'
48      )} shows other problems with respective project's dependencies.`,
49      choices: reports.map(generateAndroidProjectsSelectionChoice),
50      pageSize: Math.min(reports.length, (process.stdout.rows || 100) - 2),
51    },
52  ]);
53  return reports.filter(({ projectName }) => selectedProjects.includes(projectName));
54}
55
56async function promptForDependenciesVersions(
57  dependencies: GradleDependency[]
58): Promise<GradleDependencyUpdate[]> {
59  const updates: GradleDependencyUpdate[] = [];
60
61  const sortedDependencies = dependencies.sort((a, b) => a.fullName.localeCompare(b.fullName));
62  for (const dependency of sortedDependencies) {
63    logger.log(
64      `  ▶︎ ${chalk.blueBright(dependency.fullName)} ${getChangelogLink(
65        dependency.fullName,
66        dependency.projectUrl
67      )}`
68    );
69    const semverDiff = calculateSemverDiff(dependency.currentVersion, dependency.availableVersion);
70    const version = await promptForDependencyVersion(dependency, semverDiff);
71    if (version !== false) {
72      updates.push({
73        name: dependency.name,
74        group: dependency.group,
75        fullName: dependency.fullName,
76        oldVersion: dependency.currentVersion,
77        newVersion: stripAnsi(version),
78      });
79    }
80  }
81
82  return updates;
83}
84
85async function promptForDependencyVersion(dependency: GradleDependency, semverDiff: SemverDiff) {
86  let version = (
87    await inquirer.prompt<{ version: string | boolean }>([
88      {
89        type: 'list',
90        name: 'version',
91        message: `Choose version to update to:`,
92        choices: [
93          {
94            name: `Latest version – (${addColorBasedOnSemverDiff(
95              dependency.availableVersion,
96              semverDiff
97            )})`,
98            value: dependency.availableVersion,
99          },
100          {
101            name: `Don't update – (${dependency.currentVersion})`,
102            value: false,
103          },
104          {
105            name: `Different version – will ask in the next step`,
106            value: true,
107          },
108        ],
109        default: 0,
110        prefix: `  ${chalk.green('?')}`,
111      },
112    ])
113  ).version;
114  if (version === true) {
115    version = (
116      await inquirer.prompt<{ version: string }>([
117        {
118          type: 'input',
119          name: 'version',
120          message: `${dependency.fullName}:${dependency.currentVersion} ➡️ `,
121          default: addColorBasedOnSemverDiff(dependency.availableVersion, semverDiff),
122          prefix: `  ${chalk.green('?')}`,
123        },
124      ])
125    ).version;
126  }
127  return version;
128}
129
130async function promptForDependenciesUpdatesSelection(
131  report: AndroidProjectReport
132): Promise<GradleDependencyUpdate[]> {
133  const result: GradleDependencyUpdate[] = [];
134
135  logger.log(`\n● project: ${chalk.blue(report.projectName)}`);
136  result.push(...(await promptForDependenciesVersions(report.gradleReport.outdated)));
137
138  if (report.gradleReport.exceeded.length > 0) {
139    logger.log(`�� these dependencies ${chalk.yellow('exceed')} available version:`);
140    result.push(...(await promptForDependenciesVersions(report.gradleReport.exceeded)));
141  }
142  if (report.gradleReport.unresolved.length > 0) {
143    logger.log(`�� ${chalk.red('Failed to resolve')} these dependencies:`);
144    result.push(...(await promptForDependenciesVersions(report.gradleReport.unresolved)));
145  }
146
147  return result;
148}
149
150export async function promptForNativeDependenciesUpdates(
151  reports: AndroidProjectReport[]
152): Promise<AndroidProjectDependenciesUpdates[]> {
153  const selectedDependenciesUpdates: AndroidProjectDependenciesUpdates[] = [];
154  logger.log(
155    chalk.white.bold(
156      '\nProvide new native dependencies versions for each project. Check their changes in respective CHANGELOGs. To skip dependency provide no value.'
157    )
158  );
159  for (const report of reports) {
160    const updates = await promptForDependenciesUpdatesSelection(report);
161    if (updates.length > 0) {
162      selectedDependenciesUpdates.push({
163        report,
164        updates,
165      });
166    }
167  }
168  return selectedDependenciesUpdates;
169}
170