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    // Remove this after we upgrade reanimated
30    'react-native-reanimated': '2.10.0',
31  };
32  await addPinnedPackagesAsync(pinnedPackages);
33
34  logger.info('Yarning...');
35  await workspaceInstallAsync();
36
37  await updateReactNativePackageAsync();
38
39  await patchReanimatedAsync();
40
41  // Workaround for deprecated `Linking.removeEventListener`
42  // Remove this after we migrate to @react-navigation/native@^6.0.12
43  // https://linear.app/expo/issue/ENG-4148/upgrade-react-navigation-across-expoexpo-monorepo
44  // https://github.com/react-navigation/react-navigation/commit/bd5cd55e130cba2c6c35bbf360e3727a9fcf00e4
45  await patchReactNavigationAsync();
46
47  logger.info('Setting up project files for bare-expo.');
48  await updateBareExpoAsync();
49}
50
51async function addPinnedPackagesAsync(packages: Record<string, string>) {
52  const workspacePackageJsonPath = path.join(EXPO_DIR, 'package.json');
53  const json = await JsonFile.readAsync(workspacePackageJsonPath);
54  json.resolutions ||= {};
55  for (const [name, version] of Object.entries(packages)) {
56    logger.log('  ', `${name}@${version}`);
57    json.resolutions[name] = version;
58  }
59  await JsonFile.writeAsync(workspacePackageJsonPath, json);
60}
61
62async function updateReactNativePackageAsync() {
63  const root = path.join(EXPO_DIR, 'node_modules', 'react-native');
64
65  // Third party libraries used to use react-native minor version, update the version 1000.999.0 as the latest version
66  await transformFileAsync(path.join(root, 'package.json'), [
67    {
68      find: '"version": "0.0.0-',
69      replaceWith: '"version": "1000.999.0-',
70    },
71  ]);
72  await transformFileAsync(path.join(root, 'ReactAndroid', 'gradle.properties'), [
73    {
74      find: 'VERSION_NAME=1000.0.0-',
75      replaceWith: 'VERSION_NAME=1000.999.0-',
76    },
77  ]);
78
79  // Build hermes source from the main branch
80  await fs.writeFile(path.join(root, 'sdks', '.hermesversion'), 'main');
81  await transformFileAsync(path.join(root, 'sdks', 'hermes-engine', 'hermes-engine.podspec'), [
82    {
83      // Use the fake version to force building hermes from source
84      find: "version = package['version']",
85      replaceWith: "version = '1000.0.0'",
86    },
87  ]);
88
89  // Remove unused hermes build artifacts to reduce build time
90  await transformFileAsync(path.join(root, 'sdks', 'hermes-engine', 'hermes-engine.podspec'), [
91    {
92      find: './utils/build-mac-framework.sh',
93      replaceWith: '',
94    },
95  ]);
96  await transformFileAsync(
97    path.join(root, 'sdks', 'hermes-engine', 'utils', 'build-ios-framework.sh'),
98    [
99      {
100        find: 'build_apple_framework "iphoneos" "arm64" "$ios_deployment_target"',
101        replaceWith: '',
102      },
103      {
104        find: 'build_apple_framework "catalyst" "x86_64;arm64" "$ios_deployment_target"',
105        replaceWith: '',
106      },
107      {
108        find: 'create_universal_framework "iphoneos" "iphonesimulator" "catalyst"',
109        replaceWith: 'create_universal_framework "iphonesimulator"',
110      },
111    ]
112  );
113}
114
115async function patchReanimatedAsync() {
116  // Workaround for reanimated doesn't support the hermes where building from source
117  const root = path.join(EXPO_DIR, 'node_modules', 'react-native-reanimated');
118  await transformFileAsync(path.join(root, 'android', 'build.gradle'), [
119    {
120      find: /\bdef hermesAAR = file\(.+\)/g,
121      replaceWith:
122        'def hermesAAR = file("$reactNative/ReactAndroid/hermes-engine/build/outputs/aar/hermes-engine-debug.aar")',
123    },
124  ]);
125
126  // Remove this after reanimated support react-native 0.71
127  await transformFileAsync(path.join(root, 'android', 'CMakeLists.txt'), [
128    {
129      find: /(\s*"\$\{NODE_MODULES_DIR\}\/react-native\/ReactAndroid\/src\/main\/jni")/g,
130      replaceWith:
131        '$1\n        "${NODE_MODULES_DIR}/react-native/ReactAndroid/src/main/jni/react/turbomodule"',
132    },
133  ]);
134}
135
136async function patchReactNavigationAsync() {
137  const root = path.join(EXPO_DIR, 'node_modules', '@react-navigation');
138  await transformFileAsync(path.join(root, 'native', 'src', 'useLinking.native.tsx'), [
139    {
140      find: `\
141      Linking.addEventListener('url', callback);
142
143      return () => Linking.removeEventListener('url', callback);`,
144      replaceWith: `\
145      const subscription = Linking.addEventListener('url', callback);
146
147      return () => subscription.remove();`,
148    },
149  ]);
150}
151
152async function updateBareExpoAsync() {
153  const gradlePropsFile = path.join(EXPO_DIR, 'apps', 'bare-expo', 'android', 'gradle.properties');
154  let content = await fs.readFile(gradlePropsFile, 'utf8');
155  if (!content.match('reactNativeNightly=true')) {
156    content += `\nreactNativeNightly=true\n`;
157    await fs.writeFile(gradlePropsFile, content);
158  }
159}
160