19657025fSTomasz Sapetaimport { Command } from '@expo/commander';
2a1a2c328SKudo Chienimport spawnAsync from '@expo/spawn-async';
3eeffdb10STomasz Sapetaimport chalk from 'chalk';
4eeffdb10STomasz Sapetaimport fs from 'fs-extra';
5eeffdb10STomasz Sapetaimport inquirer from 'inquirer';
69657025fSTomasz Sapetaimport os from 'os';
79657025fSTomasz Sapetaimport path from 'path';
8eeffdb10STomasz Sapeta
99bf0723bSKudo Chienimport { runReactNativeCodegenAsync } from '../Codegen';
109657025fSTomasz Sapetaimport { EXPO_DIR } from '../Constants';
119657025fSTomasz Sapetaimport { GitDirectory } from '../Git';
129657025fSTomasz Sapetaimport logger from '../Logger';
13a1a2c328SKudo Chienimport { downloadPackageTarballAsync } from '../Npm';
149657025fSTomasz Sapetaimport { PackageJson } from '../Packages';
159657025fSTomasz Sapetaimport { updateBundledVersionsAsync } from '../ProjectVersions';
169657025fSTomasz Sapetaimport * as Workspace from '../Workspace';
179657025fSTomasz Sapetaimport {
189657025fSTomasz Sapeta  getVendoringAvailablePlatforms,
199657025fSTomasz Sapeta  listAvailableVendoredModulesAsync,
209657025fSTomasz Sapeta  vendorPlatformAsync,
219657025fSTomasz Sapeta} from '../vendoring';
229657025fSTomasz Sapetaimport vendoredModulesConfig from '../vendoring/config';
23a1a0cab6STomasz Sapetaimport { legacyVendorModuleAsync } from '../vendoring/legacy';
241967a7daSKudo Chienimport { VendoringModuleConfig, VendoringTargetConfig } from '../vendoring/types';
25eeffdb10STomasz Sapeta
269657025fSTomasz Sapetatype ActionOptions = {
27eeffdb10STomasz Sapeta  list: boolean;
28eeffdb10STomasz Sapeta  listOutdated: boolean;
299657025fSTomasz Sapeta  target: string;
30eeffdb10STomasz Sapeta  module: string;
319657025fSTomasz Sapeta  platform: string;
32eeffdb10STomasz Sapeta  commit: string;
33eeffdb10STomasz Sapeta  semverPrefix: string;
349657025fSTomasz Sapeta  updateDependencies?: boolean;
35eeffdb10STomasz Sapeta};
36eeffdb10STomasz Sapeta
379657025fSTomasz Sapetaconst EXPO_GO_TARGET = 'expo-go';
38eeffdb10STomasz Sapeta
39eeffdb10STomasz Sapetaexport default (program: Command) => {
40eeffdb10STomasz Sapeta  program
41a1a0cab6STomasz Sapeta    .command('update-vendored-module')
42a1a0cab6STomasz Sapeta    .alias('update-module', 'uvm')
43eeffdb10STomasz Sapeta    .description('Updates 3rd party modules.')
44eeffdb10STomasz Sapeta    .option('-l, --list', 'Shows a list of available 3rd party modules.', false)
45eeffdb10STomasz Sapeta    .option('-o, --list-outdated', 'Shows a list of outdated 3rd party modules.', false)
469657025fSTomasz Sapeta    .option(
479657025fSTomasz Sapeta      '-t, --target <string>',
489657025fSTomasz Sapeta      'The target to update, e.g. Expo Go or development client.',
499657025fSTomasz Sapeta      EXPO_GO_TARGET
509657025fSTomasz Sapeta    )
51eeffdb10STomasz Sapeta    .option('-m, --module <string>', 'Name of the module to update.')
52eeffdb10STomasz Sapeta    .option(
53eeffdb10STomasz Sapeta      '-p, --platform <string>',
54eeffdb10STomasz Sapeta      'A platform on which the vendored module will be updated.',
55eeffdb10STomasz Sapeta      'all'
56eeffdb10STomasz Sapeta    )
57eeffdb10STomasz Sapeta    .option(
58eeffdb10STomasz Sapeta      '-c, --commit <string>',
59eeffdb10STomasz Sapeta      'Git reference on which to checkout when copying 3rd party module.',
60eeffdb10STomasz Sapeta      'master'
61eeffdb10STomasz Sapeta    )
62eeffdb10STomasz Sapeta    .option(
63eeffdb10STomasz Sapeta      '-s, --semver-prefix <string>',
64eeffdb10STomasz Sapeta      'Setting this flag forces to use given semver prefix. Some modules may specify them by the config, but in case we want to update to alpha/beta versions we should use an empty prefix to be more strict.',
65eeffdb10STomasz Sapeta      null
66eeffdb10STomasz Sapeta    )
679657025fSTomasz Sapeta    .option(
689657025fSTomasz Sapeta      '-u, --update-dependencies',
699657025fSTomasz Sapeta      'Whether to update workspace dependencies and bundled native modules.',
709657025fSTomasz Sapeta      true
719657025fSTomasz Sapeta    )
72eeffdb10STomasz Sapeta    .asyncAction(action);
73eeffdb10STomasz Sapeta};
749657025fSTomasz Sapeta
759657025fSTomasz Sapetaasync function action(options: ActionOptions) {
769657025fSTomasz Sapeta  const target = await resolveTargetNameAsync(options.target);
779657025fSTomasz Sapeta  const targetConfig = vendoredModulesConfig[target];
789657025fSTomasz Sapeta
799657025fSTomasz Sapeta  if (options.list || options.listOutdated) {
809657025fSTomasz Sapeta    if (target !== EXPO_GO_TARGET) {
819657025fSTomasz Sapeta      throw new Error(`Listing vendored modules for target "${target}" is not supported.`);
829657025fSTomasz Sapeta    }
839657025fSTomasz Sapeta    await listAvailableVendoredModulesAsync(targetConfig.modules, options.listOutdated);
849657025fSTomasz Sapeta    return;
859657025fSTomasz Sapeta  }
869657025fSTomasz Sapeta
879657025fSTomasz Sapeta  const moduleName = await resolveModuleNameAsync(options.module, targetConfig);
88*d58f61aeSKudo Chien  const downloadSourceDir = path.join(os.tmpdir(), 'ExpoVendoredModules', moduleName);
899657025fSTomasz Sapeta  const moduleConfig = targetConfig.modules[moduleName];
909657025fSTomasz Sapeta
919657025fSTomasz Sapeta  try {
92*d58f61aeSKudo Chien    await downloadSourceAsync(downloadSourceDir, moduleName, moduleConfig, options);
93*d58f61aeSKudo Chien    const sourceDirectory = moduleConfig.rootDir
94*d58f61aeSKudo Chien      ? path.join(downloadSourceDir, moduleConfig.rootDir)
95*d58f61aeSKudo Chien      : downloadSourceDir;
969657025fSTomasz Sapeta
979657025fSTomasz Sapeta    const platforms = resolvePlatforms(options.platform);
989657025fSTomasz Sapeta
999657025fSTomasz Sapeta    for (const platform of platforms) {
1009657025fSTomasz Sapeta      if (!targetConfig.platforms[platform]) {
1019657025fSTomasz Sapeta        continue;
1029657025fSTomasz Sapeta      }
1031967a7daSKudo Chien      await runCodegenIfNeeded(sourceDirectory, moduleConfig, platform);
104a1a0cab6STomasz Sapeta
105a1a0cab6STomasz Sapeta      // TODO(@tsapeta): Remove this once all vendored modules are migrated to the new system.
106a1a0cab6STomasz Sapeta      if (!targetConfig.modules[moduleName][platform]) {
107a1a0cab6STomasz Sapeta        // If the target doesn't support this platform, maybe legacy vendoring does.
108a1a0cab6STomasz Sapeta        logger.info('‼️  Using legacy vendoring for platform %s', chalk.yellow(platform));
109a1a0cab6STomasz Sapeta        await legacyVendorModuleAsync(moduleName, platform, sourceDirectory);
110a1a0cab6STomasz Sapeta        continue;
111a1a0cab6STomasz Sapeta      }
112a1a0cab6STomasz Sapeta
1139657025fSTomasz Sapeta      const relativeTargetDirectory = path.join(
1149657025fSTomasz Sapeta        targetConfig.platforms[platform].targetDirectory,
1159657025fSTomasz Sapeta        moduleName
1169657025fSTomasz Sapeta      );
1179657025fSTomasz Sapeta      const targetDirectory = path.join(EXPO_DIR, relativeTargetDirectory);
1189657025fSTomasz Sapeta
1199657025fSTomasz Sapeta      logger.log(
1209657025fSTomasz Sapeta        '�� Vendoring for %s to %s',
1219657025fSTomasz Sapeta        chalk.yellow(platform),
1229657025fSTomasz Sapeta        chalk.magenta(relativeTargetDirectory)
1239657025fSTomasz Sapeta      );
1249657025fSTomasz Sapeta
1259657025fSTomasz Sapeta      // Clean up previous version
1269657025fSTomasz Sapeta      await fs.remove(targetDirectory);
1279657025fSTomasz Sapeta
1289657025fSTomasz Sapeta      // Delegate further steps to platform's provider
1299657025fSTomasz Sapeta      await vendorPlatformAsync(platform, sourceDirectory, targetDirectory, moduleConfig[platform]);
1309657025fSTomasz Sapeta    }
1319657025fSTomasz Sapeta
1329657025fSTomasz Sapeta    // Update dependency versions only for Expo Go target.
1339657025fSTomasz Sapeta    if (options.updateDependencies !== false && target === EXPO_GO_TARGET) {
1341967a7daSKudo Chien      const packageJsonPath = path.join(
1351967a7daSKudo Chien        sourceDirectory,
1361967a7daSKudo Chien        moduleConfig.packageJsonPath ?? 'package.json'
1371967a7daSKudo Chien      );
1381032c4b9STomasz Sapeta      const packageJson = require(packageJsonPath) as PackageJson;
1399657025fSTomasz Sapeta      const semverPrefix =
1409657025fSTomasz Sapeta        (options.semverPrefix != null ? options.semverPrefix : moduleConfig.semverPrefix) || '';
1419657025fSTomasz Sapeta      const newVersionRange = `${semverPrefix}${packageJson.version}`;
1429657025fSTomasz Sapeta
1439657025fSTomasz Sapeta      await updateDependenciesAsync(moduleName, newVersionRange);
1449657025fSTomasz Sapeta    }
1459657025fSTomasz Sapeta  } finally {
1469657025fSTomasz Sapeta    // Clean cloned repo
147*d58f61aeSKudo Chien    await fs.remove(downloadSourceDir);
1489657025fSTomasz Sapeta  }
1499657025fSTomasz Sapeta  logger.success('�� Successfully updated %s\n', chalk.bold(moduleName));
1509657025fSTomasz Sapeta}
1519657025fSTomasz Sapeta
1529657025fSTomasz Sapeta/**
153a1a2c328SKudo Chien * Downloads vendoring module source code either from git repository or npm
154a1a2c328SKudo Chien */
155a1a2c328SKudo Chienasync function downloadSourceAsync(
156a1a2c328SKudo Chien  sourceDirectory: string,
157a1a2c328SKudo Chien  moduleName: string,
158a1a2c328SKudo Chien  moduleConfig: VendoringModuleConfig,
159a1a2c328SKudo Chien  options: ActionOptions
160a1a2c328SKudo Chien) {
161a1a2c328SKudo Chien  if (moduleConfig.sourceType === 'npm') {
162a1a2c328SKudo Chien    const version = options.commit ?? 'latest';
163a1a2c328SKudo Chien    logger.log('�� Downloading %s@%s from npm', chalk.green(moduleName), chalk.cyan(version));
164a1a2c328SKudo Chien
165a1a2c328SKudo Chien    const tarball = await downloadPackageTarballAsync(
166a1a2c328SKudo Chien      sourceDirectory,
167a1a2c328SKudo Chien      moduleConfig.source,
168a1a2c328SKudo Chien      version
169a1a2c328SKudo Chien    );
170a1a2c328SKudo Chien    // `--strip-component 1` to extract files from package/ folder
171a1a2c328SKudo Chien    await spawnAsync('tar', ['--strip-component', '1', '-xf', tarball], { cwd: sourceDirectory });
172a1a2c328SKudo Chien    return;
173a1a2c328SKudo Chien  }
174a1a2c328SKudo Chien
175a1a2c328SKudo Chien  // Clone repository from the source
176a1a2c328SKudo Chien  logger.log(
177a1a2c328SKudo Chien    '�� Cloning %s#%s from %s',
178a1a2c328SKudo Chien    chalk.green(moduleName),
179a1a2c328SKudo Chien    chalk.cyan(options.commit),
180a1a2c328SKudo Chien    chalk.magenta(moduleConfig.source)
181a1a2c328SKudo Chien  );
182a1a2c328SKudo Chien
183a1a2c328SKudo Chien  await GitDirectory.shallowCloneAsync(
184a1a2c328SKudo Chien    sourceDirectory,
185a1a2c328SKudo Chien    moduleConfig.source,
186a1a2c328SKudo Chien    options.commit ?? 'master'
187a1a2c328SKudo Chien  );
188a1a2c328SKudo Chien}
189a1a2c328SKudo Chien
190a1a2c328SKudo Chien/**
1919657025fSTomasz Sapeta * Updates versions in bundled native modules and workspace projects.
1929657025fSTomasz Sapeta */
1939657025fSTomasz Sapetaasync function updateDependenciesAsync(moduleName: string, versionRange: string) {
1949657025fSTomasz Sapeta  logger.log('✍️  Updating bundled native modules');
1959657025fSTomasz Sapeta
1969657025fSTomasz Sapeta  await updateBundledVersionsAsync({
1979657025fSTomasz Sapeta    [moduleName]: versionRange,
1989657025fSTomasz Sapeta  });
1999657025fSTomasz Sapeta
2009657025fSTomasz Sapeta  logger.log('✍️  Updating workspace dependencies');
2019657025fSTomasz Sapeta
2029657025fSTomasz Sapeta  await Workspace.updateDependencyAsync(moduleName, versionRange);
2039657025fSTomasz Sapeta}
2049657025fSTomasz Sapeta
2059657025fSTomasz Sapeta/**
2069657025fSTomasz Sapeta * Validates provided target name or prompts for the valid one.
2079657025fSTomasz Sapeta */
2089657025fSTomasz Sapetaasync function resolveTargetNameAsync(providedTargetName: string): Promise<string> {
2099657025fSTomasz Sapeta  const targets = Object.keys(vendoredModulesConfig);
2109657025fSTomasz Sapeta
2119657025fSTomasz Sapeta  if (providedTargetName) {
2129657025fSTomasz Sapeta    if (targets.includes(providedTargetName)) {
2139657025fSTomasz Sapeta      return providedTargetName;
2149657025fSTomasz Sapeta    }
2159657025fSTomasz Sapeta    throw new Error(`Couldn't find config for ${providedTargetName} target.`);
2169657025fSTomasz Sapeta  }
2179657025fSTomasz Sapeta  const { targetName } = await inquirer.prompt([
2189657025fSTomasz Sapeta    {
2199657025fSTomasz Sapeta      type: 'list',
2209657025fSTomasz Sapeta      name: 'targetName',
2219657025fSTomasz Sapeta      prefix: '❔',
2229657025fSTomasz Sapeta      message: 'In which target do you want to update vendored module?',
2239657025fSTomasz Sapeta      choices: targets.map((target) => ({
2249657025fSTomasz Sapeta        name: vendoredModulesConfig[target].name,
2259657025fSTomasz Sapeta        value: target,
2269657025fSTomasz Sapeta      })),
2279657025fSTomasz Sapeta    },
2289657025fSTomasz Sapeta  ]);
2299657025fSTomasz Sapeta  return targetName;
2309657025fSTomasz Sapeta}
2319657025fSTomasz Sapeta
2329657025fSTomasz Sapeta/**
2339657025fSTomasz Sapeta * Validates provided module name or prompts for the valid one.
2349657025fSTomasz Sapeta */
2359657025fSTomasz Sapetaasync function resolveModuleNameAsync(
2369657025fSTomasz Sapeta  providedModuleName: string,
2379657025fSTomasz Sapeta  targetConfig: VendoringTargetConfig
2389657025fSTomasz Sapeta): Promise<string> {
2399657025fSTomasz Sapeta  const moduleNames = Object.keys(targetConfig.modules);
2409657025fSTomasz Sapeta
2419657025fSTomasz Sapeta  if (providedModuleName) {
2429657025fSTomasz Sapeta    if (moduleNames.includes(providedModuleName)) {
2439657025fSTomasz Sapeta      return providedModuleName;
2449657025fSTomasz Sapeta    }
2459657025fSTomasz Sapeta    throw new Error(`Couldn't find config for ${providedModuleName} module.`);
2469657025fSTomasz Sapeta  }
2479657025fSTomasz Sapeta  const { moduleName } = await inquirer.prompt([
2489657025fSTomasz Sapeta    {
2499657025fSTomasz Sapeta      type: 'list',
2509657025fSTomasz Sapeta      name: 'moduleName',
2519657025fSTomasz Sapeta      prefix: '❔',
2529657025fSTomasz Sapeta      message: 'Which vendored module do you want to update?',
2539657025fSTomasz Sapeta      choices: moduleNames,
2549657025fSTomasz Sapeta    },
2559657025fSTomasz Sapeta  ]);
2569657025fSTomasz Sapeta  return moduleName;
2579657025fSTomasz Sapeta}
2589657025fSTomasz Sapeta
2599657025fSTomasz Sapetafunction resolvePlatforms(platform: string): string[] {
2609657025fSTomasz Sapeta  const all = getVendoringAvailablePlatforms();
2619657025fSTomasz Sapeta  return all.includes(platform) ? [platform] : all;
2629657025fSTomasz Sapeta}
2631967a7daSKudo Chien
2641967a7daSKudo Chienasync function runCodegenIfNeeded(
2651967a7daSKudo Chien  sourceDirectory: string,
2661967a7daSKudo Chien  moduleConfig: VendoringModuleConfig,
2671967a7daSKudo Chien  platform: string
2681967a7daSKudo Chien) {
2691967a7daSKudo Chien  const packageJsonPath = path.join(
2701967a7daSKudo Chien    sourceDirectory,
2711967a7daSKudo Chien    moduleConfig.packageJsonPath ?? 'package.json'
2721967a7daSKudo Chien  );
2731967a7daSKudo Chien  const packageJson = require(packageJsonPath) as PackageJson;
2741967a7daSKudo Chien  const libs = packageJson?.codegenConfig?.libraries ?? [];
2751967a7daSKudo Chien  const fabricDisabledLibs = libs.filter((lib) => lib.type !== 'components');
2761967a7daSKudo Chien  if (!fabricDisabledLibs.length) {
2771967a7daSKudo Chien    return;
2781967a7daSKudo Chien  }
2791967a7daSKudo Chien  if (platform !== 'android' && platform !== 'ios') {
2801967a7daSKudo Chien    throw new Error(`Unsupported platform - ${platform}`);
2811967a7daSKudo Chien  }
2821967a7daSKudo Chien
2831967a7daSKudo Chien  const reactNativeRoot = path.join(EXPO_DIR, 'react-native-lab', 'react-native');
2841967a7daSKudo Chien  const codegenPkgRoot = path.join(reactNativeRoot, 'packages', 'react-native-codegen');
2851967a7daSKudo Chien
2861967a7daSKudo Chien  await Promise.all(
2871967a7daSKudo Chien    fabricDisabledLibs.map((lib) =>
2889bf0723bSKudo Chien      runReactNativeCodegenAsync({
2891967a7daSKudo Chien        reactNativeRoot,
2901967a7daSKudo Chien        codegenPkgRoot,
2911967a7daSKudo Chien        outputDir: path.join(sourceDirectory, platform),
2921967a7daSKudo Chien        name: lib.name,
2931967a7daSKudo Chien        type: lib.type,
2941967a7daSKudo Chien        platform,
2951967a7daSKudo Chien        jsSrcsDir: path.join(sourceDirectory, lib.jsSrcsDir),
2961967a7daSKudo Chien      })
2971967a7daSKudo Chien    )
2981967a7daSKudo Chien  );
2991967a7daSKudo Chien}
300