1import { Command } from '@expo/commander';
2import JsonFile from '@expo/json-file';
3import fs from 'fs-extra';
4import path from 'path';
5
6import { EXPO_DIR } from '../Constants';
7import logger from '../Logger';
8import { getPackageViewAsync } from '../Npm';
9import { transformFileAsync } from '../Transforms';
10import { installAsync as workspaceInstallAsync } from '../Workspace';
11
12export default (program: Command) => {
13  program
14    .command('setup-react-native-nightly')
15    .description('Setup expo/expo monorepo to install react-native nightly build for testing')
16    .asyncAction(main);
17};
18
19async function main() {
20  const nightlyVersion = (await getPackageViewAsync('react-native'))?.['dist-tags'].nightly;
21  if (!nightlyVersion) {
22    throw new Error('Unable to get react-native nightly version.');
23  }
24
25  logger.info('Adding pinned packages:');
26  const pinnedPackages = {
27    'react-native': nightlyVersion,
28  };
29  await addPinnedPackagesAsync(pinnedPackages);
30
31  logger.info('Yarning...');
32  await workspaceInstallAsync();
33
34  await updateReactNativePackageAsync();
35
36  await patchReanimatedAsync();
37
38  logger.info('Setting up project files for bare-expo.');
39  await updateBareExpoAsync();
40}
41
42async function addPinnedPackagesAsync(packages: Record<string, string>) {
43  const workspacePackageJsonPath = path.join(EXPO_DIR, 'package.json');
44  const json = await JsonFile.readAsync(workspacePackageJsonPath);
45  json.resolutions ||= {};
46  for (const [name, version] of Object.entries(packages)) {
47    logger.log('  ', `${name}@${version}`);
48    json.resolutions[name] = version;
49  }
50  await JsonFile.writeAsync(workspacePackageJsonPath, json);
51}
52
53async function updateReactNativePackageAsync() {
54  const root = path.join(EXPO_DIR, 'node_modules', 'react-native');
55
56  // Third party libraries used to use react-native minor version, update the version 9999.9999.9999 as the latest version
57  await transformFileAsync(path.join(root, 'package.json'), [
58    {
59      find: '"version": "0.0.0-',
60      replaceWith: '"version": "9999.9999.9999-',
61    },
62  ]);
63  await transformFileAsync(path.join(root, 'ReactAndroid', 'gradle.properties'), [
64    {
65      find: 'VERSION_NAME=0.0.0-',
66      replaceWith: 'VERSION_NAME=9999.9999.9999-',
67    },
68  ]);
69
70  // Build hermes source from the main branch
71  await fs.writeFile(path.join(root, 'sdks', '.hermesversion'), 'main');
72  await transformFileAsync(path.join(root, 'sdks', 'hermes-engine', 'hermes-engine.podspec'), [
73    {
74      // Because we changed the version in package.json, the `isNightly` check in hermes-engine.podspec is broken
75      find: "isNightly = version.start_with?('0.0.0-')",
76      replaceWith: 'isNightly = true',
77    },
78  ]);
79
80  // Remove unused hermes build artifacts to reduce build time
81  await transformFileAsync(path.join(root, 'sdks', 'hermes-engine', 'hermes-engine.podspec'), [
82    {
83      find: './utils/build-mac-framework.sh',
84      replaceWith: '',
85    },
86  ]);
87  await transformFileAsync(
88    path.join(root, 'sdks', 'hermes-engine', 'utils', 'build-ios-framework.sh'),
89    [
90      {
91        find: 'build_apple_framework "iphoneos" "arm64" "$ios_deployment_target"',
92        replaceWith: '',
93      },
94      {
95        find: 'build_apple_framework "catalyst" "x86_64;arm64" "$ios_deployment_target"',
96        replaceWith: '',
97      },
98      {
99        find: 'create_universal_framework "iphoneos" "iphonesimulator" "catalyst"',
100        replaceWith: 'create_universal_framework "iphonesimulator"',
101      },
102    ]
103  );
104}
105
106async function patchReanimatedAsync() {
107  // Workaround for reanimated doesn't support the hermes where building from source
108  const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated');
109  await transformFileAsync(path.join(root, 'android', 'build.gradle'), [
110    {
111      find: /\bdef hermesAAR = file\(.+\)/g,
112      replaceWith:
113        'def hermesAAR = file("$reactNative/ReactAndroid/hermes-engine/build/outputs/aar/hermes-engine-debug.aar")',
114    },
115  ]);
116
117  // Remove this after reanimated support react-native 0.71
118  await transformFileAsync(path.join(root, 'android', 'CMakeLists.txt'), [
119    {
120      find: /(\s*"\$\{NODE_MODULES_DIR\}\/react-native\/ReactAndroid\/src\/main\/jni")/g,
121      replaceWith:
122        '$1\n        "${NODE_MODULES_DIR}/react-native/ReactAndroid/src/main/jni/react/turbomodule"',
123    },
124  ]);
125}
126
127async function updateBareExpoAsync() {
128  const gradlePropsFile = path.join(EXPO_DIR, 'apps', 'bare-expo', 'android', 'gradle.properties');
129  let content = await fs.readFile(gradlePropsFile, 'utf8');
130  if (!content.match('reactNativeNightly=true')) {
131    content += `\nreactNativeNightly=true\n`;
132    await fs.writeFile(gradlePropsFile, content);
133  }
134}
135