diff --git a/packages/config/src/Errors.ts b/packages/config/src/Errors.ts index 906b22ef8b..0874d466b1 100644 --- a/packages/config/src/Errors.ts +++ b/packages/config/src/Errors.ts @@ -10,6 +10,12 @@ export class ConfigError extends Error { } } +export class UnexpectedError extends Error { + constructor(message: string) { + super(`${message}\nPlease report this as an issue on https://github.com/expo/expo-cli/issues`); + } +} + export function errorFromJSON({ name, ...json }: any): Error { let error: any; if (name === 'TypeError') { diff --git a/packages/config/src/Modules.ts b/packages/config/src/Modules.ts index ebdc43fab9..9d95fc3eb0 100644 --- a/packages/config/src/Modules.ts +++ b/packages/config/src/Modules.ts @@ -1,4 +1,4 @@ -import { stat, statSync } from 'fs-extra'; +import { stat, Stats, statSync } from 'fs-extra'; import { join, resolve } from 'path'; import resolveFrom from 'resolve-from'; @@ -35,14 +35,27 @@ export function moduleNameFromPath(modulePath: string): string { return packageName ? packageName : modulePath; } -export async function fileExistsAsync(file: string): Promise { +/** + * A non-failing version of async FS stat. + * + * @param file + */ +async function statAsync(file: string): Promise { try { - return (await stat(file)).isFile(); - } catch (e) { - return false; + return await stat(file); + } catch { + return null; } } +export async function fileExistsAsync(file: string): Promise { + return (await statAsync(file))?.isFile() ?? false; +} + +export async function directoryExistsAsync(file: string): Promise { + return (await statAsync(file))?.isDirectory() ?? false; +} + export function fileExists(file: string): boolean { try { return statSync(file).isFile(); diff --git a/packages/config/src/android/Paths.ts b/packages/config/src/android/Paths.ts index 9da59c7a67..a08cf19383 100644 --- a/packages/config/src/android/Paths.ts +++ b/packages/config/src/android/Paths.ts @@ -2,6 +2,7 @@ import fs from 'fs-extra'; import { sync as globSync } from 'glob'; import * as path from 'path'; +import { directoryExistsAsync } from '../Modules'; import { ResourceKind } from './Resources'; export async function getMainActivityAsync( @@ -60,24 +61,3 @@ export async function getResourceXMLPathAsync( const filePath = path.join(projectPath, `app/src/main/res/${kind}/${name}.xml`); return filePath; } - -/** - * A non-failing version of async FS stat. - * - * @param file - */ -async function statAsync(file: string): Promise { - try { - return await fs.stat(file); - } catch { - return null; - } -} - -export async function fileExistsAsync(file: string): Promise { - return (await statAsync(file))?.isFile() ?? false; -} - -async function directoryExistsAsync(file: string): Promise { - return (await statAsync(file))?.isDirectory() ?? false; -} diff --git a/packages/config/src/android/__tests__/Resources-test.ts b/packages/config/src/android/__tests__/Resources-test.ts index cc1371b243..447729be44 100644 --- a/packages/config/src/android/__tests__/Resources-test.ts +++ b/packages/config/src/android/__tests__/Resources-test.ts @@ -1,6 +1,7 @@ import { vol } from 'memfs'; -import { fileExistsAsync, getResourceXMLPathAsync } from '../Paths'; +import { fileExistsAsync } from '../../Modules'; +import { getResourceXMLPathAsync } from '../Paths'; import { readResourcesXMLAsync } from '../Resources'; jest.mock('fs'); diff --git a/packages/config/src/ios/BundleIdentifier.ts b/packages/config/src/ios/BundleIdentifier.ts index b84c411a18..0a2ea9e138 100644 --- a/packages/config/src/ios/BundleIdentifier.ts +++ b/packages/config/src/ios/BundleIdentifier.ts @@ -1,10 +1,10 @@ import plist, { PlistObject } from '@expo/plist'; import fs from 'fs-extra'; -import { sync as globSync } from 'glob'; import xcode from 'xcode'; import { ExpoConfig } from '../Config.types'; import { InfoPlist } from './IosConfig.types'; +import { getAllInfoPlistPaths, getAllPBXProjectPaths, getPBXProjectPath } from './Paths'; import { ConfigurationSectionEntry, findFirstNativeTarget, @@ -41,12 +41,10 @@ function setBundleIdentifier(config: ExpoConfig, infoPlist: InfoPlist): InfoPlis * @returns {string | null} bundle identifier of the Xcode project or null if the project is not configured */ function getBundleIdentifierFromPbxproj(projectRoot: string): string | null { - // TODO(dsokal): - // I'm not sure if it's either possible or common that an iOS project has multiple project.pbxproj files. - // For now, I'm assuming that the glob returns at last one file. - const pbxprojPaths = globSync('ios/*/project.pbxproj', { absolute: true, cwd: projectRoot }); - const pbxprojPath = pbxprojPaths.length > 0 ? pbxprojPaths[0] : undefined; - if (!pbxprojPath) { + let pbxprojPath: string; + try { + pbxprojPath = getPBXProjectPath(projectRoot); + } catch { return null; } const project = xcode.project(pbxprojPath); @@ -128,7 +126,10 @@ function setBundleIdentifierForPbxproj( updateProductName: boolean = true ): void { // Get all pbx projects in the ${projectRoot}/ios directory - const pbxprojPaths = globSync('ios/*/project.pbxproj', { absolute: true, cwd: projectRoot }); + let pbxprojPaths: string[] = []; + try { + pbxprojPaths = getAllPBXProjectPaths(projectRoot); + } catch {} for (const pbxprojPath of pbxprojPaths) { updateBundleIdentifierForPbxproj(pbxprojPath, bundleIdentifier, updateProductName); @@ -142,7 +143,7 @@ function setBundleIdentifierForPbxproj( const defaultBundleId = '$(PRODUCT_BUNDLE_IDENTIFIER)'; function resetAllPlistBundleIdentifiers(projectRoot: string): void { - const infoPlistPaths = globSync('ios/*/Info.plist', { absolute: true, cwd: projectRoot }); + const infoPlistPaths = getAllInfoPlistPaths(projectRoot); for (const plistPath of infoPlistPaths) { resetPlistBundleIdentifier(plistPath); diff --git a/packages/config/src/ios/DeviceFamily.ts b/packages/config/src/ios/DeviceFamily.ts index 06f9ff8f5b..f9ccee132e 100644 --- a/packages/config/src/ios/DeviceFamily.ts +++ b/packages/config/src/ios/DeviceFamily.ts @@ -4,19 +4,11 @@ import { ExpoConfig } from '../Config.types'; import { getPbxproj } from './utils/Xcodeproj'; export function getSupportsTablet(config: ExpoConfig): boolean { - if (config.ios?.supportsTablet) { - return !!config.ios?.supportsTablet; - } - - return false; + return !!config.ios?.supportsTablet; } export function getIsTabletOnly(config: ExpoConfig): boolean { - if (config.ios?.isTabletOnly) { - return !!config.ios.isTabletOnly; - } - - return false; + return !!config?.ios?.isTabletOnly; } export function getDeviceFamilies(config: ExpoConfig): number[] { @@ -29,18 +21,18 @@ export function getDeviceFamilies(config: ExpoConfig): number[] { } else if (supportsTablet) { return [1, 2]; } else { + // is iPhone only return [1]; } } /** * Wrapping the families in double quotes is the only way to set a value with a comma in it. - * Use a number when only value is returned, this better emulates Xcode. * * @param deviceFamilies */ -export function formatDeviceFamilies(deviceFamilies: number[]): string | number { - return deviceFamilies.length === 1 ? deviceFamilies[0] : `"${deviceFamilies.join(',')}"`; +export function formatDeviceFamilies(deviceFamilies: number[]): string { + return `"${deviceFamilies.join(',')}"`; } /** diff --git a/packages/config/src/ios/Entitlements.ts b/packages/config/src/ios/Entitlements.ts index b0eb13aec0..d23e972248 100644 --- a/packages/config/src/ios/Entitlements.ts +++ b/packages/config/src/ios/Entitlements.ts @@ -1,10 +1,10 @@ import fs from 'fs-extra'; -import { sync as globSync } from 'glob'; import path from 'path'; import { ExpoConfig } from '../Config.types'; import { addWarningIOS } from '../WarningAggregator'; import { InfoPlist } from './IosConfig.types'; +import * as Paths from './Paths'; import { getPbxproj, getProjectName, @@ -90,7 +90,7 @@ export function setAssociatedDomains( } export function getEntitlementsPath(projectRoot: string): string { - return getExistingEntitlementsPath(projectRoot) ?? createEntitlementsFile(projectRoot); + return Paths.getEntitlementsPath(projectRoot) ?? createEntitlementsFile(projectRoot); } function createEntitlementsFile(projectRoot: string) { @@ -135,24 +135,3 @@ const ENTITLEMENTS_TEMPLATE = ` `; - -/** - * Get the path to an existing entitlements file or use the default - */ -function getExistingEntitlementsPath(projectRoot: string): string | null { - const entitlementsPaths = globSync('ios/*/.entitlements', { absolute: true, cwd: projectRoot }); - if (entitlementsPaths.length === 0) { - return null; - } - const [entitlementsPath, ...otherEntitlementsPaths] = entitlementsPaths[0]; - - if (entitlementsPaths.length > 1) { - console.warn( - `Found multiple entitlements paths, using ${entitlementsPath}. Other paths ${JSON.stringify( - otherEntitlementsPaths - )} ignored.` - ); - } - - return entitlementsPath; -} diff --git a/packages/config/src/ios/Google.ts b/packages/config/src/ios/Google.ts index 9b21632872..b09bd6f716 100644 --- a/packages/config/src/ios/Google.ts +++ b/packages/config/src/ios/Google.ts @@ -3,8 +3,9 @@ import path from 'path'; import { ExpoConfig } from '../Config.types'; import { InfoPlist } from './IosConfig.types'; +import { getSourceRoot } from './Paths'; import { appendScheme } from './Scheme'; -import { addFileToGroup, getPbxproj, getProjectName, getSourceRoot } from './utils/Xcodeproj'; +import { addFileToGroup, getPbxproj, getProjectName } from './utils/Xcodeproj'; export function getGoogleMapsApiKey(config: ExpoConfig) { return config.ios?.config?.googleMapsApiKey ?? null; diff --git a/packages/config/src/ios/Paths.ts b/packages/config/src/ios/Paths.ts new file mode 100644 index 0000000000..0ae8ac0b01 --- /dev/null +++ b/packages/config/src/ios/Paths.ts @@ -0,0 +1,183 @@ +// @ts-ignore +import { pathExistsSync } from 'fs-extra'; +import { sync as globSync } from 'glob'; +import * as path from 'path'; + +import { UnexpectedError } from '../Errors'; +import { addWarningIOS } from '../WarningAggregator'; + +const ignoredPaths = ['**/@(Carthage|Pods|node_modules)/**']; + +export function getAppDelegate(projectRoot: string): { path: string; language: 'objc' | 'swift' } { + const [using, ...extra] = globSync('ios/*/AppDelegate.{m,swift}', { + absolute: true, + cwd: projectRoot, + ignore: ignoredPaths, + }); + + if (!using) { + throw new UnexpectedError(`Could not locate a valid AppDelegate at root: "${projectRoot}"`); + } + + if (extra.length) { + warnMultipleFiles({ + tag: 'app-delegate', + fileName: 'AppDelegate', + using, + extra, + }); + } + + const isSwift = using.match(/^.*\.(swift)$/); + return { path: using, language: isSwift ? 'swift' : 'objc' }; +} + +export function getSourceRoot(projectRoot: string): string { + const appDelegate = getAppDelegate(projectRoot); + return path.dirname(appDelegate.path); +} + +export function findSchemeNames(projectRoot: string): string[] { + const schemePaths = globSync('ios/*.xcodeproj/xcshareddata/xcschemes/*.xcscheme', { + absolute: true, + cwd: projectRoot, + ignore: ignoredPaths, + }); + return schemePaths.map(schemePath => path.basename(schemePath).split('.')[0]); +} + +export function getAllXcodeProjectPaths(projectRoot: string): string[] { + const iosFolder = 'ios'; + const pbxprojPaths = globSync('**/*.xcodeproj', { cwd: projectRoot, ignore: ignoredPaths }) + .filter(project => !/test|example|sample/i.test(project) || path.dirname(project) === iosFolder) + .sort(project => (path.dirname(project) === iosFolder ? -1 : 1)) + // sort alphabetically to ensure this works the same across different devices (Fail in CI (linux) without this) + .sort(); + + if (!pbxprojPaths.length) { + throw new UnexpectedError( + `Failed to locate the ios/*.xcodeproj files relative to path "${projectRoot}".` + ); + } + return pbxprojPaths.map(value => path.join(projectRoot, value)); +} + +/** + * Get the pbxproj for the given path + */ +export function getXcodeProjectPath(projectRoot: string): string { + const [using, ...extra] = getAllXcodeProjectPaths(projectRoot); + + if (extra.length) { + warnMultipleFiles({ + tag: 'xcodeproj', + fileName: '*.xcodeproj', + using, + extra, + }); + } + + return using; +} + +export function getAllPBXProjectPaths(projectRoot: string): string[] { + const projectPaths = getAllXcodeProjectPaths(projectRoot); + const paths = projectPaths + .map(value => path.join(value, 'project.pbxproj')) + .filter(value => pathExistsSync(value)); + + if (!paths.length) { + throw new UnexpectedError( + `Failed to locate the ios/*.xcodeproj/project.pbxproj files relative to path "${projectRoot}".` + ); + } + return paths; +} + +export function getPBXProjectPath(projectRoot: string): string { + const [using, ...extra] = getAllPBXProjectPaths(projectRoot); + + if (extra.length) { + warnMultipleFiles({ + tag: 'project-pbxproj', + fileName: 'project.pbxproj', + using, + extra, + }); + } + + return using; +} + +export function getAllInfoPlistPaths(projectRoot: string): string[] { + const paths = globSync('ios/*/Info.plist', { + absolute: true, + cwd: projectRoot, + ignore: ignoredPaths, + }); + + if (!paths.length) { + throw new UnexpectedError( + `Failed to locate Info.plist files relative to path "${projectRoot}".` + ); + } + return paths; +} + +export function getInfoPlistPath(projectRoot: string): string { + const [using, ...extra] = getAllInfoPlistPaths(projectRoot); + + if (extra.length) { + warnMultipleFiles({ + tag: 'info-plist', + fileName: 'Info.plist', + using, + extra, + }); + } + + return using; +} + +/** + * Get the entitlements file path if it exists. + * + * @param projectRoot + */ +export function getEntitlementsPath(projectRoot: string): string | null { + const [using, ...extra] = globSync('ios/*/.entitlements', { + absolute: true, + cwd: projectRoot, + ignore: ignoredPaths, + }); + + if (extra.length) { + warnMultipleFiles({ + tag: 'entitlements', + fileName: '*.entitlements', + using, + extra, + }); + } + + return using ?? null; +} + +function warnMultipleFiles({ + tag, + fileName, + using, + extra, +}: { + tag: string; + fileName: string; + using: string; + extra: string[]; +}) { + addWarningIOS( + `paths-${tag}`, + `Found multiple ${fileName} file paths, using "${using}". Ignored paths: ${JSON.stringify( + extra + )}` + ); +} diff --git a/packages/config/src/ios/Scheme.ts b/packages/config/src/ios/Scheme.ts index b73997a72b..f488021a5f 100644 --- a/packages/config/src/ios/Scheme.ts +++ b/packages/config/src/ios/Scheme.ts @@ -1,6 +1,6 @@ import { ExpoConfig } from '../Config.types'; import { InfoPlist, URLScheme } from './IosConfig.types'; -import { findSchemeNames } from './utils/Xcodeproj'; +import { findSchemeNames } from './Paths'; export function getScheme(config: { scheme?: string | string[] }): string[] { if (Array.isArray(config.scheme)) { diff --git a/packages/config/src/ios/__tests__/BundleIdentifier-test.ts b/packages/config/src/ios/__tests__/BundleIdentifier-test.ts index 5bedea4412..20b404465f 100644 --- a/packages/config/src/ios/__tests__/BundleIdentifier-test.ts +++ b/packages/config/src/ios/__tests__/BundleIdentifier-test.ts @@ -90,6 +90,7 @@ describe('BundleIdentifier module', () => { describe(setBundleIdentifierForPbxproj, () => { const projectRoot = '/testproject'; const pbxProjPath = 'ios/testproject.xcodeproj/project.pbxproj'; + const otherPbxProjPath = 'ios/otherproject.xcodeproj/project.pbxproj'; beforeEach(() => { vol.fromJSON( @@ -98,6 +99,10 @@ describe('BundleIdentifier module', () => { path.join(__dirname, 'fixtures/project.pbxproj'), 'utf-8' ), + [otherPbxProjPath]: originalFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), }, projectRoot ); @@ -107,7 +112,13 @@ describe('BundleIdentifier module', () => { it('sets the bundle identifier in the pbxproj file', () => { setBundleIdentifierForPbxproj(projectRoot, 'com.swmansion.dominik.abcd.v2'); const pbxprojContents = memfs.readFileSync(path.join(projectRoot, pbxProjPath), 'utf-8'); + const otherPbxprojContents = memfs.readFileSync( + path.join(projectRoot, otherPbxProjPath), + 'utf-8' + ); expect(pbxprojContents).toMatchSnapshot(); + // Ensure all paths are modified + expect(pbxprojContents).toBe(otherPbxprojContents); }); }); }); diff --git a/packages/config/src/ios/__tests__/DeviceFamily-test.ts b/packages/config/src/ios/__tests__/DeviceFamily-test.ts index 380bbfe8e5..583fb3d0e9 100644 --- a/packages/config/src/ios/__tests__/DeviceFamily-test.ts +++ b/packages/config/src/ios/__tests__/DeviceFamily-test.ts @@ -1,10 +1,27 @@ +import { fs, vol } from 'memfs'; +import * as path from 'path'; + import { formatDeviceFamilies, getDeviceFamilies, getIsTabletOnly, getSupportsTablet, + setDeviceFamily, } from '../DeviceFamily'; +const actualFs = jest.requireActual('fs') as typeof fs; + +jest.mock('fs'); + +jest.mock('../../WarningAggregator', () => ({ + addWarningIOS: jest.fn(), +})); + +afterAll(() => { + jest.unmock('fs'); + jest.unmock('../../WarningAggregator'); +}); + const TABLET_AND_PHONE_SUPPORTED = [1, 2]; const ONLY_PHONE_SUPPORTED = [1]; const ONLY_TABLET_SUPPORTED = [2]; @@ -46,7 +63,7 @@ describe('device family', () => { // It's important that this format is always correct. // Otherwise the xcode parser will throw `Expected ".", "/*", ";", or [0-9] but "," found.` when we attempt to write to it. it(`formats the families correctly`, () => { - expect(formatDeviceFamilies([1])).toEqual(1); + expect(formatDeviceFamilies([1])).toEqual(`"1"`); expect(formatDeviceFamilies([1, 2, 3])).toEqual(`"1,2,3"`); }); @@ -61,3 +78,29 @@ describe('device family', () => { // }); // }); }); + +describe(setDeviceFamily, () => { + const projectRoot = '/tablet'; + beforeAll(async () => { + vol.fromJSON( + { + 'ios/testproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/testproject/AppDelegate.m': '', + }, + projectRoot + ); + }); + + afterAll(() => { + vol.reset(); + }); + + it('updates device families without throwing', async () => { + setDeviceFamily({ name: '', slug: '', ios: {} }, projectRoot); + setDeviceFamily({ name: '', slug: '', ios: { supportsTablet: true } }, projectRoot); + setDeviceFamily({ name: '', slug: '', ios: { isTabletOnly: true } }, projectRoot); + }); +}); diff --git a/packages/config/src/ios/__tests__/Paths-test.ts b/packages/config/src/ios/__tests__/Paths-test.ts new file mode 100644 index 0000000000..ff5874654e --- /dev/null +++ b/packages/config/src/ios/__tests__/Paths-test.ts @@ -0,0 +1,162 @@ +import { fs, vol } from 'memfs'; +import * as path from 'path'; + +import { UnexpectedError } from '../../Errors'; +import { addWarningIOS } from '../../WarningAggregator'; +import { getAppDelegate, getXcodeProjectPath } from '../Paths'; +const actualFs = jest.requireActual('fs') as typeof fs; + +jest.mock('fs'); + +jest.mock('../../WarningAggregator', () => ({ + addWarningIOS: jest.fn(), +})); + +afterAll(() => { + jest.unmock('fs'); + jest.unmock('../../WarningAggregator'); +}); + +describe(getXcodeProjectPath, () => { + beforeAll(async () => { + vol.fromJSON( + { + 'ios/testproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/Podfile': 'content', + 'ios/TestPod.podspec': 'noop', + 'ios/testproject/AppDelegate.m': '', + }, + '/app' + ); + + // More than one + vol.fromJSON( + { + 'ios/otherproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/testproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/testproject/AppDelegate.m': '', + }, + '/multiple' + ); + }); + + afterAll(() => { + vol.reset(); + }); + + it(`returns project path`, () => { + expect(getXcodeProjectPath('/app')).toBe('/app/ios/testproject.xcodeproj'); + }); + + it(`throws when no paths are found`, () => { + expect(() => getXcodeProjectPath('/none')).toThrow(UnexpectedError); + }); + + it(`warns when multiple paths are found`, () => { + expect(getXcodeProjectPath('/multiple')).toBe('/multiple/ios/otherproject.xcodeproj'); + expect(addWarningIOS).toHaveBeenLastCalledWith( + 'paths-xcodeproj', + 'Found multiple *.xcodeproj file paths, using "/multiple/ios/otherproject.xcodeproj". Ignored paths: ["/multiple/ios/testproject.xcodeproj"]' + ); + }); +}); + +describe(getAppDelegate, () => { + beforeAll(async () => { + vol.fromJSON( + { + 'ios/testproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/Podfile': 'content', + 'ios/TestPod.podspec': 'noop', + 'ios/testproject/AppDelegate.m': '', + 'ios/testproject/AppDelegate.h': '', + }, + '/objc' + ); + + vol.fromJSON( + { + 'ios/testproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/Podfile': 'content', + 'ios/TestPod.podspec': 'noop', + 'ios/testproject/AppDelegate.swift': '', + }, + '/swift' + ); + + vol.fromJSON( + { + 'ios/testproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/Podfile': 'content', + 'ios/TestPod.podspec': 'noop', + }, + '/invalid' + ); + + vol.fromJSON( + { + 'ios/testproject.xcodeproj/project.pbxproj': actualFs.readFileSync( + path.join(__dirname, 'fixtures/project.pbxproj'), + 'utf-8' + ), + 'ios/Podfile': 'content', + 'ios/TestPod.podspec': 'noop', + 'ios/testproject/AppDelegate.swift': '', + 'ios/testproject/AppDelegate.m': '', + 'ios/testproject/AppDelegate.h': '', + }, + '/confusing' + ); + }); + + afterAll(() => { + vol.reset(); + }); + + it(`returns objc path`, () => { + expect(getAppDelegate('/objc')).toStrictEqual({ + path: '/objc/ios/testproject/AppDelegate.m', + language: 'objc', + }); + }); + it(`returns swift path`, () => { + expect(getAppDelegate('/swift')).toStrictEqual({ + path: '/swift/ios/testproject/AppDelegate.swift', + language: 'swift', + }); + }); + + it(`throws on invalid project`, () => { + expect(() => getAppDelegate('/invalid')).toThrow(UnexpectedError); + expect(() => getAppDelegate('/invalid')).toThrow(/AppDelegate/); + }); + + it(`warns when multiple paths are found`, () => { + expect(getAppDelegate('/confusing')).toStrictEqual({ + path: '/confusing/ios/testproject/AppDelegate.m', + language: 'objc', + }); + expect(addWarningIOS).toHaveBeenLastCalledWith( + 'paths-app-delegate', + 'Found multiple AppDelegate file paths, using "/confusing/ios/testproject/AppDelegate.m". Ignored paths: ["/confusing/ios/testproject/AppDelegate.swift"]' + ); + }); +}); diff --git a/packages/config/src/ios/index.ts b/packages/config/src/ios/index.ts index c2839d35d0..583e4a43ce 100644 --- a/packages/config/src/ios/index.ts +++ b/packages/config/src/ios/index.ts @@ -10,6 +10,7 @@ import { InfoPlist } from './IosConfig.types'; import * as Locales from './Locales'; import * as Name from './Name'; import * as Orientation from './Orientation'; +import { getSourceRoot } from './Paths'; import * as ProvisioningProfile from './ProvisioningProfile'; import * as RequiresFullScreen from './RequiresFullScreen'; import * as Scheme from './Scheme'; @@ -18,7 +19,6 @@ import * as Updates from './Updates'; import * as UserInterfaceStyle from './UserInterfaceStyle'; import * as UsesNonExemptEncryption from './UsesNonExemptEncryption'; import * as Version from './Version'; -import { getSourceRoot } from './utils/Xcodeproj'; // We can change this to export * as X with TypeScript 3.8+ // https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#export-star-as-namespace-syntax diff --git a/packages/config/src/ios/utils/Xcodeproj.ts b/packages/config/src/ios/utils/Xcodeproj.ts index 5ccd6aa372..a4617bfb3c 100644 --- a/packages/config/src/ios/utils/Xcodeproj.ts +++ b/packages/config/src/ios/utils/Xcodeproj.ts @@ -1,6 +1,4 @@ -// @ts-ignore -import { sync as globSync } from 'glob'; -import path from 'path'; +import * as path from 'path'; import xcode, { PBXGroup, PBXNativeTarget, @@ -12,6 +10,8 @@ import xcode, { } from 'xcode'; import pbxFile from 'xcode/lib/pbxFile'; +import * as Paths from '../Paths'; + export type ProjectSectionEntry = [string, PBXProject]; export type NativeTargetSection = Record; @@ -25,22 +25,10 @@ export type ConfigurationListEntry = [string, XCConfigurationList]; export type ConfigurationSectionEntry = [string, XCBuildConfiguration]; export function getProjectName(projectRoot: string) { - const sourceRoot = getSourceRoot(projectRoot); + const sourceRoot = Paths.getSourceRoot(projectRoot); return path.basename(sourceRoot); } -export function getSourceRoot(projectRoot: string): string { - // Account for Swift or Objective-C - const paths = globSync('ios/*/AppDelegate.*', { - absolute: true, - cwd: projectRoot, - }); - if (!paths.length) { - throw new Error(`Could not locate a valid iOS project at root: ${projectRoot}`); - } - return path.dirname(paths[0]); -} - // TODO(brentvatne): I couldn't figure out how to do this with an existing // higher level function exposed by the xcode library, but we should find out how to do // that and replace this with it @@ -138,30 +126,12 @@ export function ensureGroupRecursively(project: XcodeProject, filepath: string): return topMostGroup ?? null; } -export function findSchemeNames(projectRoot: string): string[] { - const schemePaths = globSync('ios/*.xcodeproj/xcshareddata/xcschemes/*.xcscheme', { - absolute: true, - cwd: projectRoot, - }); - return schemePaths.map(schemePath => path.basename(schemePath).split('.')[0]); -} - /** * Get the pbxproj for the given path */ export function getPbxproj(projectRoot: string): XcodeProject { - const pbxprojPaths = globSync('ios/*/project.pbxproj', { absolute: true, cwd: projectRoot }); - const [pbxprojPath, ...otherPbxprojPaths] = pbxprojPaths; - - if (pbxprojPaths.length > 1) { - console.warn( - `Found multiple pbxproject files paths, using ${pbxprojPath}. Other paths ${JSON.stringify( - otherPbxprojPaths - )} ignored.` - ); - } - - const project = xcode.project(pbxprojPath); + const projectPath = Paths.getPBXProjectPath(projectRoot); + const project = xcode.project(projectPath); project.parseSync(); return project; } diff --git a/packages/expo-cli/src/commands/apply/configureIOSProjectAsync.ts b/packages/expo-cli/src/commands/apply/configureIOSProjectAsync.ts index ec8268a9d8..27bc872c59 100644 --- a/packages/expo-cli/src/commands/apply/configureIOSProjectAsync.ts +++ b/packages/expo-cli/src/commands/apply/configureIOSProjectAsync.ts @@ -14,7 +14,6 @@ export default async function configureIOSProjectAsync(projectRoot: string) { const username = await UserManager.getCurrentUsernameAsync(); IOSConfig.Google.setGoogleServicesFile(exp, projectRoot); - IOSConfig.DeviceFamily.setDeviceFamily(exp, projectRoot); // Configure the Info.plist await modifyInfoPlistAsync(projectRoot, infoPlist => { @@ -72,6 +71,7 @@ export default async function configureIOSProjectAsync(projectRoot: string) { await IOSConfig.Icons.setIconsAsync(exp, projectRoot); await IOSConfig.SplashScreen.setSplashScreenAsync(exp, projectRoot); await IOSConfig.Locales.setLocalesAsync(exp, projectRoot); + IOSConfig.DeviceFamily.setDeviceFamily(exp, projectRoot); } async function modifyEntitlementsPlistAsync(projectRoot: string, callback: (plist: any) => any) {