1import chalk from 'chalk';
2import fs from 'fs-extra';
3import path from 'path';
4
5import { IOS_DIR } from '../../Constants';
6import logger from '../../Logger';
7import { copyFileWithTransformsAsync } from '../../Transforms';
8import { FileTransforms } from '../../Transforms.types';
9import { searchFilesAsync } from '../../Utils';
10import vendoredModulesTransforms from './transforms/vendoredModulesTransforms';
11
12const IOS_VENDORED_DIR = path.join(IOS_DIR, 'vendored');
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  const sourceDirents = (await fs.readdir(unversionedDir, { withFileTypes: true })).filter(
27    (dirent) => {
28      return dirent.isDirectory() && (!filterModules || filterModules.includes(dirent.name));
29    }
30  );
31
32  for (const { name } of sourceDirents) {
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 * Removes the directory with vendored modules for given SDK number.
58 */
59export async function removeVersionedVendoredModulesAsync(sdkNumber: number): Promise<void> {
60  const versionedDir = vendoredDirectoryForSDK(sdkNumber);
61  await fs.remove(versionedDir);
62}
63
64/**
65 * Generates base transforms to apply for all vendored modules.
66 */
67function baseTransformsFactory(prefix: string): Required<FileTransforms> {
68  return {
69    path: [
70      {
71        find: /([^/]+\.podspec\.json)$\b/,
72        replaceWith: `${prefix}$1`,
73      },
74      {
75        find: /\b(RCT|RNC|RNG|RNR|REA|RNS)([^/]*\.)(h|m|mm)/,
76        replaceWith: `${prefix}$1$2$3`,
77      },
78    ],
79    content: [
80      {
81        paths: '*.podspec.json',
82        find: /"name": "([\w-]+)"/,
83        replaceWith: `"name": "${prefix}$1"`,
84      },
85      {
86        // Prefixes `React` with word-boundary and also in `insertReactSubview`, `removeReactSubview`.
87        find: /(\b|insert|remove)(React)/g,
88        replaceWith: `$1${prefix}$2`,
89      },
90      {
91        find: /\b(RCT|RNC|RNG|RNR|REA|RNS)(\w+)\b/g,
92        replaceWith: `${prefix}$1$2`,
93      },
94      {
95        find: /facebook::/g,
96        replaceWith: `${prefix}facebook::`,
97      },
98      {
99        find: /react::/g,
100        replaceWith: `${prefix}React::`,
101      },
102      {
103        find: /using namespace (facebook|react)/g,
104        replaceWith: (_, p1) => {
105          return `using namespace ${prefix}${p1 === 'react' ? 'React' : p1}`;
106        },
107      },
108      {
109        // Objective-C only, see the comment in the rule below.
110        paths: '*.{h,m,mm}',
111        find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex)/gi,
112        replaceWith: `${prefix}R$1`,
113      },
114      {
115        // Swift translates uppercased letters at the beginning of the method name to lowercased letters, we must comply with it.
116        paths: '*.swift',
117        find: /r(eactTag|eactSubviews|eactSuperview|eactViewController|eactSetFrame|eactAddControllerToClosestParent|eactZIndex)/gi,
118        replaceWith: (_, p1) => `${prefix.toLowerCase()}R${p1}`,
119      },
120      {
121        find: /<jsi\/(.*)\.h>/,
122        replaceWith: `<${prefix}jsi/${prefix}$1.h>`,
123      },
124      {
125        find: /(JSCExecutorFactory|HermesExecutorFactory)\.h/g,
126        replaceWith: `${prefix}$1.h`,
127      },
128      {
129        find: /viewForReactTag/g,
130        replaceWith: `viewFor${prefix}ReactTag`,
131      },
132      {
133        find: /isReactRootView/g,
134        replaceWith: `is${prefix}ReactRootView`,
135      },
136      {
137        // Prefix only unindented `@objc` (notice `^` and `m` flag in the pattern). Method names shouldn't get prefixed.
138        paths: '*.swift',
139        find: /^@objc\(([^)]+)\)/gm,
140        replaceWith: `@objc(${prefix}$1)`,
141      },
142      {
143        paths: '*.podspec.json',
144        find: new RegExp(`${prefix}React-${prefix}RCT`, 'g'),
145        replaceWith: `${prefix}React-RCT`,
146      },
147      {
148        paths: '*.podspec.json',
149        find: new RegExp(`${prefix}React-Core\\/${prefix}RCT`, 'g'),
150        replaceWith: `${prefix}React-Core/RCT`,
151      },
152    ],
153  };
154}
155
156/**
157 * Returns the vendored directory for given SDK number.
158 */
159function vendoredDirectoryForSDK(sdkNumber: number): string {
160  return path.join(IOS_VENDORED_DIR, `sdk${sdkNumber}`);
161}
162