1import chalk from 'chalk';
2import { readFile, writeFile } from 'fs-extra';
3import * as path from 'path';
4import terminalLink from 'terminal-link';
5
6import { EXPO_DIR } from '../Constants';
7import logger from '../Logger';
8import { AndroidProjectDependenciesUpdates } from './types';
9import { addColorBasedOnSemverDiff, calculateSemverDiff } from './utils';
10
11function replaceVersionInGradleFile(
12  body: string,
13  {
14    oldVersion,
15    newVersion,
16    fullName,
17    group,
18    name,
19  }: {
20    oldVersion: string;
21    newVersion: string;
22    fullName: string;
23    name: string;
24    group: string;
25  }
26): string {
27  let modifiedBody = body;
28
29  const regexVersionVariable = new RegExp(`${fullName}:\\\${?(\\w+)}?`, 'ig');
30
31  // 'de.kevcodez:pubg-api-wrapper:$myVar'
32  // 'de.kevcodez:pubg-api-wrapper:${myVar}'
33  const versionWithVariableMatches = regexVersionVariable.exec(modifiedBody);
34  if (versionWithVariableMatches?.length === 2) {
35    const variableName = versionWithVariableMatches[1];
36
37    const regexVariableDefinition = new RegExp(
38      `(${variableName}(\\s+)?=(\\s+)?('|")${oldVersion}('|"))`,
39      'ig'
40    );
41
42    regexVariableDefinition
43      .exec(modifiedBody)
44      ?.filter((it) => it.includes(oldVersion))
45      .forEach((match) => {
46        modifiedBody = modifiedBody.replace(match, match.replace(oldVersion, newVersion));
47      });
48
49    // val PUBG_API_WRAPPER by extra("0.8.1")
50    // eslint-disable-next-line no-useless-escape
51    const regexKotlinValExtra = new RegExp(`${variableName}.+\(("|')${oldVersion}("|')\)`);
52    regexKotlinValExtra
53      .exec(modifiedBody)
54      ?.filter((it) => it.includes(oldVersion))
55      .forEach((match) => {
56        modifiedBody = modifiedBody.replace(match, match.replace(oldVersion, newVersion));
57      });
58  }
59
60  // compile 'de.kevcodez:pubg-api-wrapper:1.0.0'
61  const regexVersionInline = new RegExp(`${fullName}:${oldVersion}`, 'g');
62  regexVersionInline
63    .exec(modifiedBody)
64    ?.filter((it) => it.includes(`${fullName}:${oldVersion}`))
65    .forEach((match) => {
66      modifiedBody = modifiedBody.replace(match, `${fullName}:${newVersion}`);
67    });
68
69  // id 'com.github.ben-manes.versions' version "0.21.0"
70  // id("com.github.ben-manes.versions") version "0.22.0"
71  const regexPluginVersionWithPrefix = new RegExp(
72    `${group}("|')\\)?(\\s+)?version(\\s+)?("|')${oldVersion}("|')`
73  );
74  regexPluginVersionWithPrefix
75    .exec(modifiedBody)
76    ?.filter((it) => it.includes(oldVersion)) // filter out all groups not containing version
77    .forEach((match) => {
78      modifiedBody = modifiedBody.replace(match, match.replace(oldVersion, newVersion));
79    });
80
81  // compile group: 'de.kevcodez.pubg', name: 'pubg-api-wrapper', version: '0.8.1'
82  const regexDependencyWithVersionPrefix = new RegExp(
83    `${name}('|"),(\\s+)?version:(\\s+)('|")${oldVersion}('|")`
84  );
85  regexDependencyWithVersionPrefix
86    .exec(modifiedBody)
87    ?.filter((it) => it.includes(oldVersion))
88    .forEach((match) => {
89      modifiedBody = modifiedBody.replace(match, match.replace(oldVersion, newVersion));
90    });
91
92  return modifiedBody;
93}
94
95async function readGradleFiles(
96  updates: AndroidProjectDependenciesUpdates[]
97): Promise<Record<string, string>> {
98  const buildFiles = (
99    await Promise.all(
100      updates.map(async ({ report: { gradleFilePath } }) => [
101        gradleFilePath,
102        await readFile(gradleFilePath, 'utf-8'),
103      ])
104    )
105  ).reduce<Record<string, string>>((acc, [filePath, content]) => {
106    acc[filePath] = content;
107    return acc;
108  }, {});
109
110  return buildFiles;
111}
112
113async function writeGradleFiles(gradleFiles: Record<string, string>) {
114  await Promise.all(
115    Object.entries(gradleFiles).map(
116      async ([filePath, content]) => await writeFile(filePath, content)
117    )
118  );
119}
120
121export async function updateGradleDependencies(
122  updatesList: AndroidProjectDependenciesUpdates[]
123): Promise<void> {
124  logger.log(chalk.white.bold(`\nUpdating gradle files.`));
125  const buildFiles = await readGradleFiles(updatesList);
126  for (const updates of updatesList) {
127    if (updates.updates.length === 0) {
128      continue;
129    }
130
131    logger.log(
132      `\n�� Updating %s native dependencies in file: %s`,
133      chalk.blue(updates.report.projectName),
134      terminalLink(
135        chalk.italic.grey(path.relative(EXPO_DIR, updates.report.gradleFilePath)),
136        updates.report.gradleFilePath
137      )
138    );
139
140    let buildFile = buildFiles[updates.report.gradleFilePath];
141    for (const singleUpdate of updates.updates) {
142      logger.log(
143        `  ▶︎ ${chalk.blueBright(singleUpdate.fullName)}:${
144          singleUpdate.oldVersion
145        } ➡️  ${addColorBasedOnSemverDiff(
146          singleUpdate.newVersion,
147          calculateSemverDiff(singleUpdate.oldVersion, singleUpdate.newVersion)
148        )}`
149      );
150      buildFile = replaceVersionInGradleFile(buildFile, singleUpdate);
151    }
152    buildFiles[updates.report.gradleFilePath] = buildFile;
153  }
154
155  await writeGradleFiles(buildFiles);
156}
157