1import chalk from 'chalk';
2import fs from 'fs-extra';
3import glob from 'glob-promise';
4import path from 'path';
5
6import { IOS_DIR } from '../../Constants';
7import logger from '../../Logger';
8import { copyFileWithTransformsAsync } from '../../Transforms';
9import { FileTransforms } from '../../Transforms.types';
10import { searchFilesAsync } from '../../Utils';
11import vendoredModulesTransforms from './transforms/vendoredModulesTransforms';
12
13const IOS_VENDORED_DIR = path.join(IOS_DIR, 'vendored');
14
15/**
16 * Versions iOS vendored modules.
17 */
18export async function versionVendoredModulesAsync(
19  sdkNumber: number,
20  filterModules: string[] | null
21): Promise<void> {
22  const prefix = `ABI${sdkNumber}_0_0`;
23  const config = vendoredModulesTransforms(prefix);
24  const baseTransforms = baseTransformsFactory(prefix);
25  const unversionedDir = path.join(IOS_VENDORED_DIR, 'unversioned');
26  const versionedDir = vendoredDirectoryForSDK(sdkNumber);
27  let vendoredModuleNames = await getVendoredModuleNamesAsync(unversionedDir);
28  if (filterModules) {
29    vendoredModuleNames = vendoredModuleNames.filter((name) => filterModules.includes(name));
30  }
31
32  for (const name of vendoredModuleNames) {
33    logger.info('�� Versioning vendored module %s', chalk.green(name));
34
35    const moduleConfig = config[name];
36    const sourceDirectory = path.join(unversionedDir, name);
37    const targetDirectory = path.join(versionedDir, name);
38    const files = await searchFilesAsync(sourceDirectory, '**');
39
40    await fs.remove(targetDirectory);
41
42    for (const sourceFile of files) {
43      await copyFileWithTransformsAsync({
44        sourceFile,
45        sourceDirectory,
46        targetDirectory,
47        transforms: {
48          path: [...baseTransforms.path, ...(moduleConfig?.path ?? [])],
49          content: [...baseTransforms.content, ...(moduleConfig?.content ?? [])],
50        },
51      });
52    }
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)(\w+)\b/g,
113        replaceWith: `${prefix}$1$2`,
114      },
115      {
116        find: /facebook::/g,
117        replaceWith: `${prefix}facebook::`,
118      },
119      {
120        find: /react::/g,
121        replaceWith: `${prefix}React::`,
122      },
123      {
124        find: /using namespace (facebook|react)/g,
125        replaceWith: (_, p1) => {
126          return `using namespace ${prefix}${p1 === 'react' ? 'React' : p1}`;
127        },
128      },
129      {
130        // Objective-C only, see the comment in the rule below.
131        paths: '*.{h,m,mm}',
132        find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex)/gi,
133        replaceWith: `${prefix}R$1`,
134      },
135      {
136        // Swift translates uppercased letters at the beginning of the method name to lowercased letters, we must comply with it.
137        paths: '*.swift',
138        find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex)/gi,
139        replaceWith: (_, p1) => `${prefix.toLowerCase()}R${p1}`,
140      },
141      {
142        find: /<jsi\/(.*)\.h>/,
143        replaceWith: `<${prefix}jsi/${prefix}$1.h>`,
144      },
145      {
146        find: /(JSCExecutorFactory|HermesExecutorFactory)\.h/g,
147        replaceWith: `${prefix}$1.h`,
148      },
149      {
150        find: /viewForReactTag/g,
151        replaceWith: `viewFor${prefix}ReactTag`,
152      },
153      {
154        find: /isReactRootView/g,
155        replaceWith: `is${prefix}ReactRootView`,
156      },
157      {
158        // Prefix only unindented `@objc` (notice `^` and `m` flag in the pattern). Method names shouldn't get prefixed.
159        paths: '*.swift',
160        find: /^@objc\(([^)]+)\)/gm,
161        replaceWith: `@objc(${prefix}$1)`,
162      },
163      {
164        paths: '*.podspec.json',
165        find: new RegExp(`${prefix}React-${prefix}RCT`, 'g'),
166        replaceWith: `${prefix}React-RCT`,
167      },
168      {
169        paths: '*.podspec.json',
170        find: new RegExp(`${prefix}React-Core\\/${prefix}RCT`, 'g'),
171        replaceWith: `${prefix}React-Core/RCT`,
172      },
173    ],
174  };
175}
176
177/**
178 * Returns the vendored directory for given SDK number.
179 */
180function vendoredDirectoryForSDK(sdkNumber: number): string {
181  return path.join(IOS_VENDORED_DIR, `sdk${sdkNumber}`);
182}
183