1import JsonFile from '@expo/json-file';
2import chalk from 'chalk';
3import path from 'path';
4import semver from 'semver';
5
6import { EXPO_DIR } from '../../Constants';
7import logger from '../../Logger';
8import { Task } from '../../TasksRunner';
9import * as Workspace from '../../Workspace';
10import { Parcel, TaskArgs } from '../types';
11
12const { green, yellow, cyan } = chalk;
13
14/**
15 * Updates versions of packages to be published in other workspace projects depending on them.
16 */
17export const updateWorkspaceProjects = new Task<TaskArgs>(
18  {
19    name: 'updateWorkspaceProjects',
20    filesToStage: ['**/package.json', 'yarn.lock'],
21  },
22  async (parcels: Parcel[]) => {
23    logger.info('\n�� Updating workspace projects...');
24
25    const workspaceInfo = await Workspace.getInfoAsync();
26    const dependenciesKeys = [
27      'dependencies',
28      'devDependencies',
29      'peerDependencies',
30      'optionalDependencies',
31    ];
32
33    const parcelsObject = parcels.reduce((acc, parcel) => {
34      acc[parcel.pkg.packageName] = parcel;
35      return acc;
36    }, {});
37
38    await Promise.all(
39      Object.entries(workspaceInfo).map(async ([projectName, projectInfo]) => {
40        const projectDependencies = [
41          ...projectInfo.workspaceDependencies,
42          ...projectInfo.mismatchedWorkspaceDependencies,
43        ]
44          .map((dependencyName) => parcelsObject[dependencyName])
45          .filter(Boolean);
46
47        // If this project doesn't depend on any package we're going to publish.
48        if (projectDependencies.length === 0) {
49          return;
50        }
51
52        // Get copy of project's `package.json`.
53        const projectPackageJsonPath = path.join(EXPO_DIR, projectInfo.location, 'package.json');
54        const projectPackageJson = await JsonFile.readAsync(projectPackageJsonPath);
55        const batch = logger.batch();
56
57        batch.log('  ', green(projectName));
58
59        // Iterate through different dependencies types.
60        for (const dependenciesKey of dependenciesKeys) {
61          const dependenciesObject = projectPackageJson[dependenciesKey];
62
63          if (!dependenciesObject) {
64            continue;
65          }
66
67          for (const { pkg, state } of projectDependencies) {
68            const currentVersionRange = dependenciesObject[pkg.packageName];
69
70            if (
71              !currentVersionRange ||
72              !shouldUpdateDependencyVersion(projectName, currentVersionRange, state.releaseVersion)
73            ) {
74              continue;
75            }
76
77            // Leave tilde and caret as they are, just replace the version.
78            const newVersionRange = currentVersionRange.replace(
79              /([\^~]?).*/,
80              `$1${state.releaseVersion}`
81            );
82            dependenciesObject[pkg.packageName] = newVersionRange;
83
84            batch.log(
85              '    ',
86              `Updating ${yellow(`${dependenciesKey}.${pkg.packageName}`)}`,
87              `from ${cyan(currentVersionRange)} to ${cyan(newVersionRange)}`
88            );
89          }
90        }
91
92        // Save project's `package.json`.
93        await JsonFile.writeAsync(projectPackageJsonPath, projectPackageJson);
94
95        // Flush batched logs if there is at least one version change in the project.
96        if (batch.batchedLogs.length > 1) {
97          batch.flush();
98        }
99      })
100    );
101  }
102);
103
104/**
105 * Returns boolean indicating if the version range should be updated. Our policy assumes that `expo` package controls versions
106 * of other expo packages (e.g. expo-modules-core, expo-modules-autolinking). Any other package (or workspace project)
107 * doesn't need to be updated as long as the new version still satisfies the version range.
108 *
109 * @param packageName Name of the package to update
110 * @param currentRange Current version range of the dependency
111 * @param version The new version of the dependency
112 */
113function shouldUpdateDependencyVersion(packageName: string, currentRange: string, version: string) {
114  return packageName === 'expo' || !semver.satisfies(version, currentRange);
115}
116