1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types';
2082815dcSEvan Baconimport plist, { PlistObject } from '@expo/plist';
3082815dcSEvan Baconimport assert from 'assert';
4082815dcSEvan Baconimport fs from 'fs';
5082815dcSEvan Baconimport xcode, { XCBuildConfiguration } from 'xcode';
6082815dcSEvan Bacon
7082815dcSEvan Baconimport { InfoPlist } from './IosConfig.types';
8082815dcSEvan Baconimport { getAllInfoPlistPaths, getAllPBXProjectPaths, getPBXProjectPath } from './Paths';
9082815dcSEvan Baconimport { findFirstNativeTarget, getXCBuildConfigurationFromPbxproj } from './Target';
10082815dcSEvan Baconimport { ConfigurationSectionEntry, getBuildConfigurationsForListId } from './utils/Xcodeproj';
11082815dcSEvan Baconimport { trimQuotes } from './utils/string';
12*8a424bebSJames Ideimport { ConfigPlugin } from '../Plugin.types';
13*8a424bebSJames Ideimport { withDangerousMod } from '../plugins/withDangerousMod';
14082815dcSEvan Bacon
15082815dcSEvan Baconexport const withBundleIdentifier: ConfigPlugin<{ bundleIdentifier?: string }> = (
16082815dcSEvan Bacon  config,
17082815dcSEvan Bacon  { bundleIdentifier }
18082815dcSEvan Bacon) => {
19082815dcSEvan Bacon  return withDangerousMod(config, [
20082815dcSEvan Bacon    'ios',
21082815dcSEvan Bacon    async (config) => {
22082815dcSEvan Bacon      const bundleId = bundleIdentifier ?? config.ios?.bundleIdentifier;
23082815dcSEvan Bacon      assert(
24082815dcSEvan Bacon        bundleId,
25082815dcSEvan Bacon        '`bundleIdentifier` must be defined in the app config (`expo.ios.bundleIdentifier`) or passed to the plugin `withBundleIdentifier`.'
26082815dcSEvan Bacon      );
27082815dcSEvan Bacon      await setBundleIdentifierForPbxproj(config.modRequest.projectRoot, bundleId!);
28082815dcSEvan Bacon      return config;
29082815dcSEvan Bacon    },
30082815dcSEvan Bacon  ]);
31082815dcSEvan Bacon};
32082815dcSEvan Bacon
33082815dcSEvan Baconfunction getBundleIdentifier(config: Pick<ExpoConfig, 'ios'>): string | null {
34082815dcSEvan Bacon  return config.ios?.bundleIdentifier ?? null;
35082815dcSEvan Bacon}
36082815dcSEvan Bacon
37082815dcSEvan Bacon/**
38082815dcSEvan Bacon * In Turtle v1 we set the bundleIdentifier directly on Info.plist rather
39082815dcSEvan Bacon * than in pbxproj
40082815dcSEvan Bacon */
41082815dcSEvan Baconfunction setBundleIdentifier(config: ExpoConfig, infoPlist: InfoPlist): InfoPlist {
42082815dcSEvan Bacon  const bundleIdentifier = getBundleIdentifier(config);
43082815dcSEvan Bacon
44082815dcSEvan Bacon  if (!bundleIdentifier) {
45082815dcSEvan Bacon    return infoPlist;
46082815dcSEvan Bacon  }
47082815dcSEvan Bacon
48082815dcSEvan Bacon  return {
49082815dcSEvan Bacon    ...infoPlist,
50082815dcSEvan Bacon    CFBundleIdentifier: bundleIdentifier,
51082815dcSEvan Bacon  };
52082815dcSEvan Bacon}
53082815dcSEvan Bacon
54082815dcSEvan Bacon/**
55082815dcSEvan Bacon * Gets the bundle identifier defined in the Xcode project found in the project directory.
56082815dcSEvan Bacon *
57082815dcSEvan Bacon * A bundle identifier is stored as a value in XCBuildConfiguration entry.
58082815dcSEvan Bacon * Those entries exist for every pair (build target, build configuration).
59082815dcSEvan Bacon * Unless target name is passed, the first target defined in the pbxproj is used
60082815dcSEvan Bacon * (to keep compatibility with the inaccurate legacy implementation of this function).
61082815dcSEvan Bacon * The build configuration is usually 'Release' or 'Debug'. However, it could be any arbitrary string.
62082815dcSEvan Bacon * Defaults to 'Release'.
63082815dcSEvan Bacon *
64082815dcSEvan Bacon * @param {string} projectRoot Path to project root containing the ios directory
65082815dcSEvan Bacon * @param {string} targetName Target name
66082815dcSEvan Bacon * @param {string} buildConfiguration Build configuration. Defaults to 'Release'.
67082815dcSEvan Bacon * @returns {string | null} bundle identifier of the Xcode project or null if the project is not configured
68082815dcSEvan Bacon */
69082815dcSEvan Baconfunction getBundleIdentifierFromPbxproj(
70082815dcSEvan Bacon  projectRoot: string,
71082815dcSEvan Bacon  {
72082815dcSEvan Bacon    targetName,
73082815dcSEvan Bacon    buildConfiguration = 'Release',
74082815dcSEvan Bacon  }: { targetName?: string; buildConfiguration?: string } = {}
75082815dcSEvan Bacon): string | null {
76082815dcSEvan Bacon  let pbxprojPath: string;
77082815dcSEvan Bacon  try {
78082815dcSEvan Bacon    pbxprojPath = getPBXProjectPath(projectRoot);
79082815dcSEvan Bacon  } catch {
80082815dcSEvan Bacon    return null;
81082815dcSEvan Bacon  }
82082815dcSEvan Bacon  const project = xcode.project(pbxprojPath);
83082815dcSEvan Bacon  project.parseSync();
84082815dcSEvan Bacon
85082815dcSEvan Bacon  const xcBuildConfiguration = getXCBuildConfigurationFromPbxproj(project, {
86082815dcSEvan Bacon    targetName,
87082815dcSEvan Bacon    buildConfiguration,
88082815dcSEvan Bacon  });
89082815dcSEvan Bacon  if (!xcBuildConfiguration) {
90082815dcSEvan Bacon    return null;
91082815dcSEvan Bacon  }
92082815dcSEvan Bacon  return getProductBundleIdentifierFromBuildConfiguration(xcBuildConfiguration);
93082815dcSEvan Bacon}
94082815dcSEvan Bacon
95082815dcSEvan Baconfunction getProductBundleIdentifierFromBuildConfiguration(
96082815dcSEvan Bacon  xcBuildConfiguration: XCBuildConfiguration
97082815dcSEvan Bacon): string | null {
98082815dcSEvan Bacon  const bundleIdentifierRaw = xcBuildConfiguration.buildSettings.PRODUCT_BUNDLE_IDENTIFIER;
99082815dcSEvan Bacon  if (bundleIdentifierRaw) {
100082815dcSEvan Bacon    const bundleIdentifier = trimQuotes(bundleIdentifierRaw);
101082815dcSEvan Bacon    // it's possible to use interpolation for the bundle identifier
102082815dcSEvan Bacon    // the most common case is when the last part of the id is set to `$(PRODUCT_NAME:rfc1034identifier)`
103082815dcSEvan Bacon    // in this case, PRODUCT_NAME should be replaced with its value
104082815dcSEvan Bacon    // the `rfc1034identifier` modifier replaces all non-alphanumeric characters with dashes
105082815dcSEvan Bacon    const bundleIdentifierParts = bundleIdentifier.split('.');
106082815dcSEvan Bacon    if (
107082815dcSEvan Bacon      bundleIdentifierParts[bundleIdentifierParts.length - 1] ===
108082815dcSEvan Bacon        '$(PRODUCT_NAME:rfc1034identifier)' &&
109082815dcSEvan Bacon      xcBuildConfiguration.buildSettings.PRODUCT_NAME
110082815dcSEvan Bacon    ) {
111082815dcSEvan Bacon      bundleIdentifierParts[bundleIdentifierParts.length - 1] =
112082815dcSEvan Bacon        xcBuildConfiguration.buildSettings.PRODUCT_NAME.replace(/[^a-zA-Z0-9]/g, '-');
113082815dcSEvan Bacon    }
114082815dcSEvan Bacon    return bundleIdentifierParts.join('.');
115082815dcSEvan Bacon  } else {
116082815dcSEvan Bacon    return null;
117082815dcSEvan Bacon  }
118082815dcSEvan Bacon}
119082815dcSEvan Bacon
120082815dcSEvan Bacon/**
121082815dcSEvan Bacon * Updates the bundle identifier for a given pbxproj
122082815dcSEvan Bacon *
123082815dcSEvan Bacon * @param {string} pbxprojPath Path to pbxproj file
124082815dcSEvan Bacon * @param {string} bundleIdentifier Bundle identifier to set in the pbxproj
125082815dcSEvan Bacon * @param {boolean} [updateProductName=true]  Whether to update PRODUCT_NAME
126082815dcSEvan Bacon */
127082815dcSEvan Baconfunction updateBundleIdentifierForPbxproj(
128082815dcSEvan Bacon  pbxprojPath: string,
129082815dcSEvan Bacon  bundleIdentifier: string,
130082815dcSEvan Bacon  updateProductName: boolean = true
131082815dcSEvan Bacon): void {
132082815dcSEvan Bacon  const project = xcode.project(pbxprojPath);
133082815dcSEvan Bacon  project.parseSync();
134082815dcSEvan Bacon
135082815dcSEvan Bacon  const [, nativeTarget] = findFirstNativeTarget(project);
136082815dcSEvan Bacon
137082815dcSEvan Bacon  getBuildConfigurationsForListId(project, nativeTarget.buildConfigurationList).forEach(
138082815dcSEvan Bacon    ([, item]: ConfigurationSectionEntry) => {
139082815dcSEvan Bacon      if (item.buildSettings.PRODUCT_BUNDLE_IDENTIFIER === bundleIdentifier) {
140082815dcSEvan Bacon        return;
141082815dcSEvan Bacon      }
142082815dcSEvan Bacon
143082815dcSEvan Bacon      item.buildSettings.PRODUCT_BUNDLE_IDENTIFIER = `"${bundleIdentifier}"`;
144082815dcSEvan Bacon
145082815dcSEvan Bacon      if (updateProductName) {
146082815dcSEvan Bacon        const productName = bundleIdentifier.split('.').pop();
147082815dcSEvan Bacon        if (!productName?.includes('$')) {
148082815dcSEvan Bacon          item.buildSettings.PRODUCT_NAME = productName;
149082815dcSEvan Bacon        }
150082815dcSEvan Bacon      }
151082815dcSEvan Bacon    }
152082815dcSEvan Bacon  );
153082815dcSEvan Bacon  fs.writeFileSync(pbxprojPath, project.writeSync());
154082815dcSEvan Bacon}
155082815dcSEvan Bacon
156082815dcSEvan Bacon/**
157082815dcSEvan Bacon * Updates the bundle identifier for pbx projects inside the ios directory of the given project root
158082815dcSEvan Bacon *
159082815dcSEvan Bacon * @param {string} projectRoot Path to project root containing the ios directory
160082815dcSEvan Bacon * @param {string} bundleIdentifier Desired bundle identifier
161082815dcSEvan Bacon * @param {boolean} [updateProductName=true]  Whether to update PRODUCT_NAME
162082815dcSEvan Bacon */
163082815dcSEvan Baconfunction setBundleIdentifierForPbxproj(
164082815dcSEvan Bacon  projectRoot: string,
165082815dcSEvan Bacon  bundleIdentifier: string,
166082815dcSEvan Bacon  updateProductName: boolean = true
167082815dcSEvan Bacon): void {
168082815dcSEvan Bacon  // Get all pbx projects in the ${projectRoot}/ios directory
169082815dcSEvan Bacon  let pbxprojPaths: string[] = [];
170082815dcSEvan Bacon  try {
171082815dcSEvan Bacon    pbxprojPaths = getAllPBXProjectPaths(projectRoot);
172082815dcSEvan Bacon  } catch {}
173082815dcSEvan Bacon
174082815dcSEvan Bacon  for (const pbxprojPath of pbxprojPaths) {
175082815dcSEvan Bacon    updateBundleIdentifierForPbxproj(pbxprojPath, bundleIdentifier, updateProductName);
176082815dcSEvan Bacon  }
177082815dcSEvan Bacon}
178082815dcSEvan Bacon
179082815dcSEvan Bacon/**
180082815dcSEvan Bacon * Reset bundle identifier field in Info.plist to use PRODUCT_BUNDLE_IDENTIFIER, as recommended by Apple.
181082815dcSEvan Bacon */
182082815dcSEvan Bacon
183082815dcSEvan Baconconst defaultBundleId = '$(PRODUCT_BUNDLE_IDENTIFIER)';
184082815dcSEvan Bacon
185082815dcSEvan Baconfunction resetAllPlistBundleIdentifiers(projectRoot: string): void {
186082815dcSEvan Bacon  const infoPlistPaths = getAllInfoPlistPaths(projectRoot);
187082815dcSEvan Bacon
188082815dcSEvan Bacon  for (const plistPath of infoPlistPaths) {
189082815dcSEvan Bacon    resetPlistBundleIdentifier(plistPath);
190082815dcSEvan Bacon  }
191082815dcSEvan Bacon}
192082815dcSEvan Bacon
193082815dcSEvan Baconfunction resetPlistBundleIdentifier(plistPath: string): void {
194082815dcSEvan Bacon  const rawPlist = fs.readFileSync(plistPath, 'utf8');
195082815dcSEvan Bacon  const plistObject = plist.parse(rawPlist) as PlistObject;
196082815dcSEvan Bacon
197082815dcSEvan Bacon  if (plistObject.CFBundleIdentifier) {
198082815dcSEvan Bacon    if (plistObject.CFBundleIdentifier === defaultBundleId) return;
199082815dcSEvan Bacon
200082815dcSEvan Bacon    // attempt to match default Info.plist format
201082815dcSEvan Bacon    const format = { pretty: true, indent: `\t` };
202082815dcSEvan Bacon
203082815dcSEvan Bacon    const xml = plist.build(
204082815dcSEvan Bacon      {
205082815dcSEvan Bacon        ...plistObject,
206082815dcSEvan Bacon        CFBundleIdentifier: defaultBundleId,
207082815dcSEvan Bacon      },
208082815dcSEvan Bacon      format
209082815dcSEvan Bacon    );
210082815dcSEvan Bacon
211082815dcSEvan Bacon    if (xml !== rawPlist) {
212082815dcSEvan Bacon      fs.writeFileSync(plistPath, xml);
213082815dcSEvan Bacon    }
214082815dcSEvan Bacon  }
215082815dcSEvan Bacon}
216082815dcSEvan Bacon
217082815dcSEvan Baconexport {
218082815dcSEvan Bacon  getBundleIdentifier,
219082815dcSEvan Bacon  setBundleIdentifier,
220082815dcSEvan Bacon  getBundleIdentifierFromPbxproj,
221082815dcSEvan Bacon  updateBundleIdentifierForPbxproj,
222082815dcSEvan Bacon  setBundleIdentifierForPbxproj,
223082815dcSEvan Bacon  resetAllPlistBundleIdentifiers,
224082815dcSEvan Bacon  resetPlistBundleIdentifier,
225082815dcSEvan Bacon};
226