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