1082815dcSEvan Baconimport { existsSync, readFileSync } from 'fs';
2082815dcSEvan Baconimport { sync as globSync } from 'glob';
3082815dcSEvan Baconimport * as path from 'path';
4082815dcSEvan Bacon
5*8a424bebSJames Ideimport * as Entitlements from './Entitlements';
6082815dcSEvan Baconimport { UnexpectedError } from '../utils/errors';
7082815dcSEvan Baconimport { addWarningIOS } from '../utils/warnings';
8082815dcSEvan Bacon
9082815dcSEvan Baconconst ignoredPaths = ['**/@(Carthage|Pods|vendor|node_modules)/**'];
10082815dcSEvan Bacon
11082815dcSEvan Baconinterface ProjectFile<L extends string = string> {
12082815dcSEvan Bacon  path: string;
13082815dcSEvan Bacon  language: L;
14082815dcSEvan Bacon  contents: string;
15082815dcSEvan Bacon}
16082815dcSEvan Bacon
17082815dcSEvan Bacontype AppleLanguage = 'objc' | 'objcpp' | 'swift';
18082815dcSEvan Bacon
19082815dcSEvan Baconexport type AppDelegateProjectFile = ProjectFile<AppleLanguage>;
20082815dcSEvan Bacon
21082815dcSEvan Baconexport function getAppDelegateHeaderFilePath(projectRoot: string): string {
22082815dcSEvan Bacon  const [using, ...extra] = globSync('ios/*/AppDelegate.h', {
23082815dcSEvan Bacon    absolute: true,
24082815dcSEvan Bacon    cwd: projectRoot,
25082815dcSEvan Bacon    ignore: ignoredPaths,
26082815dcSEvan Bacon  });
27082815dcSEvan Bacon
28082815dcSEvan Bacon  if (!using) {
29082815dcSEvan Bacon    throw new UnexpectedError(
30082815dcSEvan Bacon      `Could not locate a valid AppDelegate header at root: "${projectRoot}"`
31082815dcSEvan Bacon    );
32082815dcSEvan Bacon  }
33082815dcSEvan Bacon
34082815dcSEvan Bacon  if (extra.length) {
35082815dcSEvan Bacon    warnMultipleFiles({
36082815dcSEvan Bacon      tag: 'app-delegate-header',
37082815dcSEvan Bacon      fileName: 'AppDelegate',
38082815dcSEvan Bacon      projectRoot,
39082815dcSEvan Bacon      using,
40082815dcSEvan Bacon      extra,
41082815dcSEvan Bacon    });
42082815dcSEvan Bacon  }
43082815dcSEvan Bacon
44082815dcSEvan Bacon  return using;
45082815dcSEvan Bacon}
46082815dcSEvan Bacon
47082815dcSEvan Baconexport function getAppDelegateFilePath(projectRoot: string): string {
48082815dcSEvan Bacon  const [using, ...extra] = globSync('ios/*/AppDelegate.@(m|mm|swift)', {
49082815dcSEvan Bacon    absolute: true,
50082815dcSEvan Bacon    cwd: projectRoot,
51082815dcSEvan Bacon    ignore: ignoredPaths,
52082815dcSEvan Bacon  });
53082815dcSEvan Bacon
54082815dcSEvan Bacon  if (!using) {
55082815dcSEvan Bacon    throw new UnexpectedError(`Could not locate a valid AppDelegate at root: "${projectRoot}"`);
56082815dcSEvan Bacon  }
57082815dcSEvan Bacon
58082815dcSEvan Bacon  if (extra.length) {
59082815dcSEvan Bacon    warnMultipleFiles({
60082815dcSEvan Bacon      tag: 'app-delegate',
61082815dcSEvan Bacon      fileName: 'AppDelegate',
62082815dcSEvan Bacon      projectRoot,
63082815dcSEvan Bacon      using,
64082815dcSEvan Bacon      extra,
65082815dcSEvan Bacon    });
66082815dcSEvan Bacon  }
67082815dcSEvan Bacon
68082815dcSEvan Bacon  return using;
69082815dcSEvan Bacon}
70082815dcSEvan Bacon
71082815dcSEvan Baconexport function getAppDelegateObjcHeaderFilePath(projectRoot: string): string {
72082815dcSEvan Bacon  const [using, ...extra] = globSync('ios/*/AppDelegate.h', {
73082815dcSEvan Bacon    absolute: true,
74082815dcSEvan Bacon    cwd: projectRoot,
75082815dcSEvan Bacon    ignore: ignoredPaths,
76082815dcSEvan Bacon  });
77082815dcSEvan Bacon
78082815dcSEvan Bacon  if (!using) {
79082815dcSEvan Bacon    throw new UnexpectedError(`Could not locate a valid AppDelegate.h at root: "${projectRoot}"`);
80082815dcSEvan Bacon  }
81082815dcSEvan Bacon
82082815dcSEvan Bacon  if (extra.length) {
83082815dcSEvan Bacon    warnMultipleFiles({
84082815dcSEvan Bacon      tag: 'app-delegate-objc-header',
85082815dcSEvan Bacon      fileName: 'AppDelegate.h',
86082815dcSEvan Bacon      projectRoot,
87082815dcSEvan Bacon      using,
88082815dcSEvan Bacon      extra,
89082815dcSEvan Bacon    });
90082815dcSEvan Bacon  }
91082815dcSEvan Bacon
92082815dcSEvan Bacon  return using;
93082815dcSEvan Bacon}
94082815dcSEvan Bacon
95082815dcSEvan Baconfunction getLanguage(filePath: string): AppleLanguage {
96082815dcSEvan Bacon  const extension = path.extname(filePath);
97082815dcSEvan Bacon  switch (extension) {
98082815dcSEvan Bacon    case '.mm':
99082815dcSEvan Bacon      return 'objcpp';
100082815dcSEvan Bacon    case '.m':
101082815dcSEvan Bacon    case '.h':
102082815dcSEvan Bacon      return 'objc';
103082815dcSEvan Bacon    case '.swift':
104082815dcSEvan Bacon      return 'swift';
105082815dcSEvan Bacon    default:
106082815dcSEvan Bacon      throw new UnexpectedError(`Unexpected iOS file extension: ${extension}`);
107082815dcSEvan Bacon  }
108082815dcSEvan Bacon}
109082815dcSEvan Bacon
110082815dcSEvan Baconexport function getFileInfo(filePath: string) {
111082815dcSEvan Bacon  return {
112082815dcSEvan Bacon    path: path.normalize(filePath),
113082815dcSEvan Bacon    contents: readFileSync(filePath, 'utf8'),
114082815dcSEvan Bacon    language: getLanguage(filePath),
115082815dcSEvan Bacon  };
116082815dcSEvan Bacon}
117082815dcSEvan Bacon
118082815dcSEvan Baconexport function getAppDelegate(projectRoot: string): AppDelegateProjectFile {
119082815dcSEvan Bacon  const filePath = getAppDelegateFilePath(projectRoot);
120082815dcSEvan Bacon  return getFileInfo(filePath);
121082815dcSEvan Bacon}
122082815dcSEvan Bacon
123082815dcSEvan Baconexport function getSourceRoot(projectRoot: string): string {
124082815dcSEvan Bacon  const appDelegate = getAppDelegate(projectRoot);
125082815dcSEvan Bacon  return path.dirname(appDelegate.path);
126082815dcSEvan Bacon}
127082815dcSEvan Bacon
128082815dcSEvan Baconexport function findSchemePaths(projectRoot: string): string[] {
129082815dcSEvan Bacon  return globSync('ios/*.xcodeproj/xcshareddata/xcschemes/*.xcscheme', {
130082815dcSEvan Bacon    absolute: true,
131082815dcSEvan Bacon    cwd: projectRoot,
132082815dcSEvan Bacon    ignore: ignoredPaths,
133082815dcSEvan Bacon  });
134082815dcSEvan Bacon}
135082815dcSEvan Bacon
136082815dcSEvan Baconexport function findSchemeNames(projectRoot: string): string[] {
137082815dcSEvan Bacon  const schemePaths = findSchemePaths(projectRoot);
138082815dcSEvan Bacon  return schemePaths.map((schemePath) => path.parse(schemePath).name);
139082815dcSEvan Bacon}
140082815dcSEvan Bacon
141082815dcSEvan Baconexport function getAllXcodeProjectPaths(projectRoot: string): string[] {
142082815dcSEvan Bacon  const iosFolder = 'ios';
143082815dcSEvan Bacon  const pbxprojPaths = globSync('ios/**/*.xcodeproj', { cwd: projectRoot, ignore: ignoredPaths })
144082815dcSEvan Bacon    .filter(
145082815dcSEvan Bacon      (project) => !/test|example|sample/i.test(project) || path.dirname(project) === iosFolder
146082815dcSEvan Bacon    )
147082815dcSEvan Bacon    // sort alphabetically to ensure this works the same across different devices (Fail in CI (linux) without this)
148082815dcSEvan Bacon    .sort()
149082815dcSEvan Bacon    .sort((a, b) => {
150082815dcSEvan Bacon      const isAInIos = path.dirname(a) === iosFolder;
151082815dcSEvan Bacon      const isBInIos = path.dirname(b) === iosFolder;
152082815dcSEvan Bacon      // preserve previous sort order
153082815dcSEvan Bacon      if ((isAInIos && isBInIos) || (!isAInIos && !isBInIos)) {
154082815dcSEvan Bacon        return 0;
155082815dcSEvan Bacon      }
156082815dcSEvan Bacon      return isAInIos ? -1 : 1;
157082815dcSEvan Bacon    });
158082815dcSEvan Bacon
159082815dcSEvan Bacon  if (!pbxprojPaths.length) {
160082815dcSEvan Bacon    throw new UnexpectedError(
161082815dcSEvan Bacon      `Failed to locate the ios/*.xcodeproj files relative to path "${projectRoot}".`
162082815dcSEvan Bacon    );
163082815dcSEvan Bacon  }
164082815dcSEvan Bacon  return pbxprojPaths.map((value) => path.join(projectRoot, value));
165082815dcSEvan Bacon}
166082815dcSEvan Bacon
167082815dcSEvan Bacon/**
168082815dcSEvan Bacon * Get the pbxproj for the given path
169082815dcSEvan Bacon */
170082815dcSEvan Baconexport function getXcodeProjectPath(projectRoot: string): string {
171082815dcSEvan Bacon  const [using, ...extra] = getAllXcodeProjectPaths(projectRoot);
172082815dcSEvan Bacon
173082815dcSEvan Bacon  if (extra.length) {
174082815dcSEvan Bacon    warnMultipleFiles({
175082815dcSEvan Bacon      tag: 'xcodeproj',
176082815dcSEvan Bacon      fileName: '*.xcodeproj',
177082815dcSEvan Bacon      projectRoot,
178082815dcSEvan Bacon      using,
179082815dcSEvan Bacon      extra,
180082815dcSEvan Bacon    });
181082815dcSEvan Bacon  }
182082815dcSEvan Bacon
183082815dcSEvan Bacon  return using;
184082815dcSEvan Bacon}
185082815dcSEvan Bacon
186082815dcSEvan Baconexport function getAllPBXProjectPaths(projectRoot: string): string[] {
187082815dcSEvan Bacon  const projectPaths = getAllXcodeProjectPaths(projectRoot);
188082815dcSEvan Bacon  const paths = projectPaths
189082815dcSEvan Bacon    .map((value) => path.join(value, 'project.pbxproj'))
190082815dcSEvan Bacon    .filter((value) => existsSync(value));
191082815dcSEvan Bacon
192082815dcSEvan Bacon  if (!paths.length) {
193082815dcSEvan Bacon    throw new UnexpectedError(
194082815dcSEvan Bacon      `Failed to locate the ios/*.xcodeproj/project.pbxproj files relative to path "${projectRoot}".`
195082815dcSEvan Bacon    );
196082815dcSEvan Bacon  }
197082815dcSEvan Bacon  return paths;
198082815dcSEvan Bacon}
199082815dcSEvan Bacon
200082815dcSEvan Baconexport function getPBXProjectPath(projectRoot: string): string {
201082815dcSEvan Bacon  const [using, ...extra] = getAllPBXProjectPaths(projectRoot);
202082815dcSEvan Bacon
203082815dcSEvan Bacon  if (extra.length) {
204082815dcSEvan Bacon    warnMultipleFiles({
205082815dcSEvan Bacon      tag: 'project-pbxproj',
206082815dcSEvan Bacon      fileName: 'project.pbxproj',
207082815dcSEvan Bacon      projectRoot,
208082815dcSEvan Bacon      using,
209082815dcSEvan Bacon      extra,
210082815dcSEvan Bacon    });
211082815dcSEvan Bacon  }
212082815dcSEvan Bacon
213082815dcSEvan Bacon  return using;
214082815dcSEvan Bacon}
215082815dcSEvan Bacon
216082815dcSEvan Baconexport function getAllInfoPlistPaths(projectRoot: string): string[] {
217082815dcSEvan Bacon  const paths = globSync('ios/*/Info.plist', {
218082815dcSEvan Bacon    absolute: true,
219082815dcSEvan Bacon    cwd: projectRoot,
220082815dcSEvan Bacon    ignore: ignoredPaths,
221082815dcSEvan Bacon  }).sort(
222082815dcSEvan Bacon    // longer name means more suffixes, we want the shortest possible one to be first.
223082815dcSEvan Bacon    (a, b) => a.length - b.length
224082815dcSEvan Bacon  );
225082815dcSEvan Bacon
226082815dcSEvan Bacon  if (!paths.length) {
227082815dcSEvan Bacon    throw new UnexpectedError(
228082815dcSEvan Bacon      `Failed to locate Info.plist files relative to path "${projectRoot}".`
229082815dcSEvan Bacon    );
230082815dcSEvan Bacon  }
231082815dcSEvan Bacon  return paths;
232082815dcSEvan Bacon}
233082815dcSEvan Bacon
234082815dcSEvan Baconexport function getInfoPlistPath(projectRoot: string): string {
235082815dcSEvan Bacon  const [using, ...extra] = getAllInfoPlistPaths(projectRoot);
236082815dcSEvan Bacon
237082815dcSEvan Bacon  if (extra.length) {
238082815dcSEvan Bacon    warnMultipleFiles({
239082815dcSEvan Bacon      tag: 'info-plist',
240082815dcSEvan Bacon      fileName: 'Info.plist',
241082815dcSEvan Bacon      projectRoot,
242082815dcSEvan Bacon      using,
243082815dcSEvan Bacon      extra,
244082815dcSEvan Bacon    });
245082815dcSEvan Bacon  }
246082815dcSEvan Bacon
247082815dcSEvan Bacon  return using;
248082815dcSEvan Bacon}
249082815dcSEvan Bacon
250082815dcSEvan Baconexport function getAllEntitlementsPaths(projectRoot: string): string[] {
251082815dcSEvan Bacon  const paths = globSync('ios/*/*.entitlements', {
252082815dcSEvan Bacon    absolute: true,
253082815dcSEvan Bacon    cwd: projectRoot,
254082815dcSEvan Bacon    ignore: ignoredPaths,
255082815dcSEvan Bacon  });
256082815dcSEvan Bacon  return paths;
257082815dcSEvan Bacon}
258082815dcSEvan Bacon
259082815dcSEvan Bacon/**
260082815dcSEvan Bacon * @deprecated: use Entitlements.getEntitlementsPath instead
261082815dcSEvan Bacon */
262082815dcSEvan Baconexport function getEntitlementsPath(projectRoot: string): string | null {
263082815dcSEvan Bacon  return Entitlements.getEntitlementsPath(projectRoot);
264082815dcSEvan Bacon}
265082815dcSEvan Bacon
266082815dcSEvan Baconexport function getSupportingPath(projectRoot: string): string {
267082815dcSEvan Bacon  return path.resolve(projectRoot, 'ios', path.basename(getSourceRoot(projectRoot)), 'Supporting');
268082815dcSEvan Bacon}
269082815dcSEvan Bacon
270082815dcSEvan Baconexport function getExpoPlistPath(projectRoot: string): string {
271082815dcSEvan Bacon  const supportingPath = getSupportingPath(projectRoot);
272082815dcSEvan Bacon  return path.join(supportingPath, 'Expo.plist');
273082815dcSEvan Bacon}
274082815dcSEvan Bacon
275082815dcSEvan Baconfunction warnMultipleFiles({
276082815dcSEvan Bacon  tag,
277082815dcSEvan Bacon  fileName,
278082815dcSEvan Bacon  projectRoot,
279082815dcSEvan Bacon  using,
280082815dcSEvan Bacon  extra,
281082815dcSEvan Bacon}: {
282082815dcSEvan Bacon  tag: string;
283082815dcSEvan Bacon  fileName: string;
284082815dcSEvan Bacon  projectRoot?: string;
285082815dcSEvan Bacon  using: string;
286082815dcSEvan Bacon  extra: string[];
287082815dcSEvan Bacon}) {
288082815dcSEvan Bacon  const usingPath = projectRoot ? path.relative(projectRoot, using) : using;
289082815dcSEvan Bacon  const extraPaths = projectRoot ? extra.map((v) => path.relative(projectRoot, v)) : extra;
290082815dcSEvan Bacon  addWarningIOS(
291082815dcSEvan Bacon    `paths-${tag}`,
292082815dcSEvan Bacon    `Found multiple ${fileName} file paths, using "${usingPath}". Ignored paths: ${JSON.stringify(
293082815dcSEvan Bacon      extraPaths
294082815dcSEvan Bacon    )}`
295082815dcSEvan Bacon  );
296082815dcSEvan Bacon}
297