1import JsonFile from '@expo/json-file';
2import chalk from 'chalk';
3import fs from 'fs-extra';
4import glob from 'glob-promise';
5import path from 'path';
6
7import { IOS_DIR } from '../../Constants';
8import logger from '../../Logger';
9import { copyFileWithTransformsAsync } from '../../Transforms';
10import { FileTransforms } from '../../Transforms.types';
11import { searchFilesAsync } from '../../Utils';
12import vendoredModulesTransforms from './transforms/vendoredModulesTransforms';
13
14const IOS_VENDORED_DIR = path.join(IOS_DIR, 'vendored');
15
16/**
17 * Versions iOS vendored modules.
18 */
19export async function versionVendoredModulesAsync(
20  sdkNumber: number,
21  filterModules: string[] | null
22): Promise<void> {
23  const prefix = `ABI${sdkNumber}_0_0`;
24  const config = vendoredModulesTransforms(prefix);
25  const baseTransforms = baseTransformsFactory(prefix);
26  const unversionedDir = path.join(IOS_VENDORED_DIR, 'unversioned');
27  const versionedDir = vendoredDirectoryForSDK(sdkNumber);
28  let vendoredModuleNames = await getVendoredModuleNamesAsync(unversionedDir);
29  if (filterModules) {
30    vendoredModuleNames = vendoredModuleNames.filter((name) => filterModules.includes(name));
31  }
32
33  for (const name of vendoredModuleNames) {
34    logger.info('�� Versioning vendored module %s', chalk.green(name));
35
36    const moduleConfig = config[name];
37    const sourceDirectory = path.join(unversionedDir, name);
38    const targetDirectory = path.join(versionedDir, name);
39    const files = await searchFilesAsync(sourceDirectory, '**');
40
41    await fs.remove(targetDirectory);
42
43    for (const sourceFile of files) {
44      await copyFileWithTransformsAsync({
45        sourceFile,
46        sourceDirectory,
47        targetDirectory,
48        transforms: {
49          path: [...baseTransforms.path, ...(moduleConfig?.path ?? [])],
50          content: [...baseTransforms.content, ...(moduleConfig?.content ?? [])],
51        },
52      });
53    }
54    await postTransformHooks[name]?.(sourceDirectory, targetDirectory);
55  }
56}
57
58/**
59 * Gets the library name of each vendored module in a specific directory.
60 */
61async function getVendoredModuleNamesAsync(directory: string): Promise<string[]> {
62  const vendoredPodspecPaths = await glob(`**/*.podspec.json`, {
63    cwd: directory,
64    nodir: true,
65    realpath: true,
66  });
67
68  const podspecPattern = new RegExp(`${directory}/(.*)/.*podspec.json`, 'i');
69
70  return vendoredPodspecPaths.reduce((result, podspecPath) => {
71    const moduleName = podspecPath.match(podspecPattern);
72    if (moduleName) {
73      result.push(moduleName[1]);
74    }
75    return result;
76  }, [] as string[]);
77}
78
79/**
80 * Removes the directory with vendored modules for given SDK number.
81 */
82export async function removeVersionedVendoredModulesAsync(sdkNumber: number): Promise<void> {
83  const versionedDir = vendoredDirectoryForSDK(sdkNumber);
84  await fs.remove(versionedDir);
85}
86
87/**
88 * Generates base transforms to apply for all vendored modules.
89 */
90function baseTransformsFactory(prefix: string): Required<FileTransforms> {
91  return {
92    path: [
93      {
94        find: /([^/]+\.podspec\.json)$\b/,
95        replaceWith: `${prefix}$1`,
96      },
97      {
98        find: /\b(RCT|RNC|RNG|RNR|REA|RNS)([^/]*\.)(h|m|mm)/,
99        replaceWith: `${prefix}$1$2$3`,
100      },
101    ],
102    content: [
103      {
104        paths: '*.podspec.json',
105        find: /"name": "([\w-]+)"/,
106        replaceWith: `"name": "${prefix}$1"`,
107      },
108      {
109        // Prefixes `React` with word-boundary and also in `insertReactSubview`, `removeReactSubview`.
110        find: /(\b|insert|remove)(React)/g,
111        replaceWith: `$1${prefix}$2`,
112      },
113      {
114        find: /\b(RCT|RNC|RNG|RNR|REA|RNS|YG)(\w+)\b/g,
115        replaceWith: `${prefix}$1$2`,
116      },
117      {
118        find: /(facebook|react|hermes)::/g,
119        replaceWith: (_, p1) => {
120          return `${prefix}${p1 === 'react' ? 'React' : p1}::`;
121        },
122      },
123      {
124        find: /namespace (facebook|react|hermes)/g,
125        replaceWith: (_, p1) => {
126          return `namespace ${prefix}${p1 === 'react' ? 'React' : p1}`;
127        },
128      },
129      {
130        // namespace ABI48_0_0React = ABI48_0_0facebook::react -> namespace ABI48_0_0React = ABI48_0_0facebook::ABI48_0_0React
131        // using namespace ABI48_0_0facebook::react -> using namespace ABI48_0_0facebook::ABI48_0_0React
132        find: /namespace ([\w\s=]+facebook)::react/g,
133        replaceWith: `namespace $1::${prefix}React`,
134      },
135      {
136        // Objective-C only, see the comment in the rule below.
137        paths: '*.{h,m,mm}',
138        find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex|eactLayoutDirection)/gi,
139        replaceWith: `${prefix}R$1`,
140      },
141      {
142        // Swift translates uppercased letters at the beginning of the method name to lowercased letters, we must comply with it.
143        paths: '*.swift',
144        find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex)/gi,
145        replaceWith: (_, p1) => `${prefix.toLowerCase()}R${p1}`,
146      },
147      {
148        // Modules written in Swift are registered using `RCT_EXTERN_MODULE` macro in Objective-C.
149        // These modules are usually unprefixed at this point as they don't include any common prefixes (e.g. RCT, RNC).
150        // We have to remap them to prefixed names. It's necessary for at least Stripe, Lottie and FlashList.
151        paths: '*.m',
152        find: new RegExp(`RCT_EXTERN_MODULE\\((?!${prefix})(\\w+)`, 'g'),
153        replaceWith: `RCT_EXTERN_REMAP_MODULE($1, ${prefix}$1`,
154      },
155      {
156        find: /<jsi\/(.*)\.h>/,
157        replaceWith: `<${prefix}jsi/${prefix}$1.h>`,
158      },
159      {
160        find: /(JSCExecutorFactory|HermesExecutorFactory)\.h/g,
161        replaceWith: `${prefix}$1.h`,
162      },
163      {
164        find: /viewForReactTag/g,
165        replaceWith: `viewFor${prefix}ReactTag`,
166      },
167      {
168        find: /isReactRootView/g,
169        replaceWith: `is${prefix}ReactRootView`,
170      },
171      {
172        find: /IsReactRootView/g,
173        replaceWith: `Is${prefix}ReactRootView`,
174      },
175      {
176        find: `UIView+${prefix}React.h`,
177        replaceWith: `${prefix}UIView+React.h`,
178      },
179      {
180        // Prefix only unindented `@objc` (notice `^` and `m` flag in the pattern). Method names shouldn't get prefixed.
181        paths: '*.swift',
182        find: /^@objc\(([^)]+)\)/gm,
183        replaceWith: `@objc(${prefix}$1)`,
184      },
185      {
186        paths: '*.podspec.json',
187        find: new RegExp(`${prefix}React-${prefix}RCT`, 'g'),
188        replaceWith: `${prefix}React-RCT`,
189      },
190      {
191        paths: '*.podspec.json',
192        find: /\b(hermes-engine)\b/g,
193        replaceWith: `${prefix}$1`,
194      },
195      {
196        paths: '*.podspec.json',
197        find: new RegExp(`${prefix}React-Core\\/${prefix}RCT`, 'g'),
198        replaceWith: `${prefix}React-Core/RCT`,
199      },
200      {
201        find: `${prefix}${prefix}`,
202        replaceWith: `${prefix}`,
203      },
204    ],
205  };
206}
207
208/**
209 * Provides a hook for vendored module to do some custom tasks after transforming files
210 */
211type PostTramsformHook = (sourceDirectory: string, targetDirectory: string) => Promise<void>;
212const postTransformHooks: Record<string, PostTramsformHook> = {
213  '@shopify/react-native-skia': async (sourceDirectory: string, targetDirectory: string) => {
214    const podspecPath = (await glob('*.podspec.json', { cwd: targetDirectory, absolute: true }))[0];
215    const podspec = await JsonFile.readAsync(podspecPath);
216    // remove the shared vendored_frameworks
217    delete podspec?.['ios']?.['vendored_frameworks'];
218    await JsonFile.writeAsync(podspecPath, podspec);
219  },
220};
221
222/**
223 * Returns the vendored directory for given SDK number.
224 */
225function vendoredDirectoryForSDK(sdkNumber: number): string {
226  return path.join(IOS_VENDORED_DIR, `sdk${sdkNumber}`);
227}
228