1import { Command } from '@expo/commander';
2import JsonFile from '@expo/json-file';
3import spawnAsync from '@expo/spawn-async';
4import chalk from 'chalk';
5import fs from 'fs-extra';
6import path from 'path';
7
8import { PACKAGES_DIR } from '../Constants';
9import { Template, getAvailableProjectTemplatesAsync } from '../ProjectTemplates';
10import { getNewestSDKVersionAsync, sdkVersionAsync } from '../ProjectVersions';
11
12type ActionOptions = {
13  sdkVersion?: string;
14};
15
16const DEPENDENCIES_KEYS = ['dependencies', 'devDependencies', 'peerDependencies'];
17const BUNDLED_NATIVE_MODULES_PATH = path.join(PACKAGES_DIR, 'expo', 'bundledNativeModules.json');
18
19/**
20 * Finds target version range, that is usually `bundledModuleVersion` param,
21 * but in some specific cases we want to use different version range.
22 *
23 * @param targetVersionRange Version range that exists in `bundledNativeModules.json` file.
24 * @param currentVersion Version range that is currenty used in the template.
25 * @param sdkVersion SDK version string to which we're upgrading.
26 */
27function resolveTargetVersionRange(
28  targetVersionRange: string,
29  currentVersion: string,
30  sdkVersion: string
31) {
32  if (currentVersion === '*') {
33    return currentVersion;
34  }
35  if (/^https?:\/\/.*\/react-native\//.test(currentVersion)) {
36    return `https://github.com/expo/react-native/archive/sdk-${sdkVersion}.tar.gz`;
37  }
38  return targetVersionRange;
39}
40
41/**
42 * Updates single project template.
43 *
44 * @param template Template object containing name and path.
45 * @param modulesToUpdate An object with module names to update and their version ranges.
46 * @param sdkVersion SDK version string to which we're upgrading.
47 */
48async function updateTemplateAsync(
49  template: Template,
50  modulesToUpdate: object,
51  sdkVersion: string
52): Promise<void> {
53  console.log(`Updating ${chalk.bold.green(template.name)}...`);
54
55  const packageJsonPath = path.join(template.path, 'package.json');
56  const packageJson = require(packageJsonPath);
57
58  for (const dependencyKey of DEPENDENCIES_KEYS) {
59    const dependencies = packageJson[dependencyKey];
60
61    if (!dependencies) {
62      continue;
63    }
64    for (const dependencyName in dependencies) {
65      const currentVersion = dependencies[dependencyName];
66      const targetVersion = resolveTargetVersionRange(
67        modulesToUpdate[dependencyName],
68        currentVersion,
69        sdkVersion
70      );
71
72      if (targetVersion) {
73        if (targetVersion === currentVersion) {
74          console.log(
75            chalk.yellow('>'),
76            `Current version ${chalk.cyan(targetVersion)} of ${chalk.blue(
77              dependencyName
78            )} is up-to-date.`
79          );
80        } else {
81          console.log(
82            chalk.yellow('>'),
83            `Updating ${chalk.blue(dependencyName)} from ${chalk.cyan(
84              currentVersion
85            )} to ${chalk.cyan(targetVersion)}...`
86          );
87          packageJson[dependencyKey][dependencyName] = targetVersion;
88        }
89      }
90    }
91  }
92  await JsonFile.writeAsync(packageJsonPath, packageJson);
93}
94
95/**
96 * Removes template's `yarn.lock` and runs `yarn`.
97 *
98 * @param templatePath Root path of the template.
99 */
100async function yarnTemplateAsync(templatePath: string): Promise<void> {
101  console.log(chalk.yellow('>'), 'Yarning...');
102
103  const yarnLockPath = path.join(templatePath, 'yarn.lock');
104
105  if (await fs.pathExists(yarnLockPath)) {
106    // We do want to always install the newest possible versions that match bundledNativeModules versions,
107    // so let's remove yarn.lock before updating re-yarning dependencies.
108    await fs.remove(yarnLockPath);
109  }
110  await spawnAsync('yarn', [], {
111    stdio: 'ignore',
112    cwd: templatePath,
113    env: process.env,
114  });
115}
116
117async function action(options: ActionOptions) {
118  // At this point of the release process all platform should have the same newest SDK version.
119  const sdkVersion = options.sdkVersion ?? (await getNewestSDKVersionAsync('ios'));
120
121  if (!sdkVersion) {
122    throw new Error(
123      `Cannot infer current SDK version - please use ${chalk.gray('--sdkVersion')} flag.`
124    );
125  }
126
127  const bundledNativeModules = require(BUNDLED_NATIVE_MODULES_PATH);
128  const templates = await getAvailableProjectTemplatesAsync();
129  const expoVersion = await sdkVersionAsync();
130
131  const modulesToUpdate = {
132    ...bundledNativeModules,
133    expo: `~${expoVersion}`,
134  };
135
136  for (const template of templates) {
137    await updateTemplateAsync(template, modulesToUpdate, sdkVersion);
138    await yarnTemplateAsync(template.path);
139    console.log(chalk.yellow('>'), chalk.green('Success!'), '\n');
140  }
141}
142
143export default (program: Command) => {
144  program
145    .command('update-project-templates')
146    .alias('update-templates', 'upt')
147    .description(
148      'Updates dependencies of project templates to the versions that are defined in bundledNativeModules.json file.'
149    )
150    .option(
151      '-s, --sdkVersion [string]',
152      'SDK version for which the project templates should be updated. Defaults to the newest SDK version.'
153    )
154    .asyncAction(action);
155};
156