import { ExpoConfig } from '@expo/config-types' ; import assert from 'assert' ; import path from 'path' ; import slugify from 'slugify' ; import xcode , { PBXFile , PBXGroup , PBXNativeTarget , PBXProject , UUID , XCBuildConfiguration , XCConfigurationList , XcodeProject , } from 'xcode' ; import pbxFile from 'xcode/lib/pbxFile' ; import { addWarningIOS } from '../../utils/warnings' ; import * as Paths from '../Paths' ; export type ProjectSectionEntry = [ string , PBXProject ] ; export type NativeTargetSection = Record < string , PBXNativeTarget > ; export type NativeTargetSectionEntry = [ string , PBXNativeTarget ] ; export type ConfigurationLists = Record < string , XCConfigurationList > ; export type ConfigurationListEntry = [ string , XCConfigurationList ] ; export type ConfigurationSectionEntry = [ string , XCBuildConfiguration ] ; export function getProjectName ( projectRoot ) { const sourceRoot = Paths . getSourceRoot ( projectRoot ) ; return path . basename ( sourceRoot ) ; } export function resolvePathOrProject ( projectRootOrProject ) { if ( typeof projectRootOrProject === 'string' ) { try { return getPbxproj ( projectRootOrProject ) ; } catch { return null ; } } return projectRootOrProject ; } export function sanitizedName ( name ) { return sanitizedNameForProjects ( name ) || sanitizedNameForProjects ( slugify ( name ) ) || 'app' ; } function sanitizedNameForProjects ( name ) { return name . replace ( /[\W_]+/g , '' ) . normalize ( 'NFD' ) . replace ( /[\u0300-\u036f]/g , '' ) ; } export function getHackyProjectName ( projectRoot , config ) { try { return getProjectName ( projectRoot ) ; } catch { const projectName = config . name ; assert ( projectName , 'Your project needs a name in app.json/app.config.js.' ) ; return sanitizedName ( projectName ) ; } } function createProjectFileForGroup ( { filepath , group } ) { const file = new pbxFile ( filepath ) ; const conflictingFile = group . children . find ( child => child . comment === file . basename ) ; if ( conflictingFile ) { return null ; } return file ; } /**
 * Add a resource file (ex: `SplashScreen.storyboard`, `Images.xcassets`) to an Xcode project.
 * This is akin to creating a new code file in Xcode with `⌘+n`.
 */ export function addResourceFileToGroup ( { filepath , groupName , isBuildFile , project , verbose , } ) { return addFileToGroupAndLink ( { filepath , groupName , project , verbose , addFileToProject ( { project , file } ) { project . addToPbxFileReferenceSection ( file ) ; if ( isBuildFile ) { project . addToPbxBuildFileSection ( file ) ; } project . addToPbxResourcesBuildPhase ( file ) ; } } ) ; } /**
 * Add a build source file (ex: `AppDelegate.m`, `ViewController.swift`) to an Xcode project.
 * This is akin to creating a new code file in Xcode with `⌘+n`.
 */ export function addBuildSourceFileToGroup ( { filepath , groupName , project , verbose , targetUuid , } ) { return addFileToGroupAndLink ( { filepath , groupName , project , verbose , addFileToProject ( { project , file } ) { project . addToPbxFileReferenceSection ( file ) ; project . addToPbxBuildFileSection ( file ) ; project . addToPbxSourcesBuildPhase ( file ) ; } } ) ; } export function addFileToGroupAndLink ( { filepath , groupName , project , verbose , addFileToProject , targetUuid , } ) { const group = pbxGroupByPathOrAssert ( project , groupName ) ; const file = createProjectFileForGroup ( { filepath , group } ) ; if ( ! file ) { if ( verbose ) { addWarningIOS ( 'ios-xcode-project' , `Skipped adding duplicate file "${ filepath }" to PBXGroup named "${ groupName }"` ) ; } return project ; } if ( targetUuid != null ) { file . target = targetUuid ; } else { const applicationNativeTarget = project . getTarget ( 'com.apple.product-type.application' ) ; file . target = applicationNativeTarget ?. uuid ; } file . uuid = project . generateUuid ( ) ; file . fileRef = project . generateUuid ( ) ; addFileToProject ( { project , file } ) ; group . children . push ( { value : file . fileRef , comment : file . basename } ) ; return project ; } export function getApplicationNativeTarget ( { project , projectName , } ) { const applicationNativeTarget = project . getTarget ( 'com.apple.product-type.application' ) ; assert ( applicationNativeTarget , `Couldn't locate application PBXNativeTarget in '.xcodeproj' file.` ) ; assert ( String ( applicationNativeTarget . target . name ) === projectName , `Application native target name mismatch. Expected ${ projectName }, but found ${ applicationNativeTarget . target . name }.` ) ; return applicationNativeTarget ; } param projectName param framework export function addFramework ( { project , projectName , framework , } ) { const target = getApplicationNativeTarget ( { project , projectName } ) ; return project . addFramework ( framework , { target : target . uuid } ) ; } function splitPath ( path ) { return path . split ( '/' ) ; } const findGroup = ( group , name ) : { value : UUID ; comment ? : string ; } | undefined => { if ( ! group ) { return undefined ; } return group . children . find ( group => group . comment === name ) ; } ; function findGroupInsideGroup ( project , group , name ) { const foundGroup = findGroup ( group , name ) ; if ( foundGroup ) { return project . getPBXGroupByKey ( foundGroup . value ) ?? null ; } return null ; } function pbxGroupByPathOrAssert ( project , path ) { const { firstProject } = project . getFirstProject ( ) ; let group = project . getPBXGroupByKey ( firstProject . mainGroup ) ; const components = splitPath ( path ) ; for ( const name of components ) { const nextGroup = findGroupInsideGroup ( project , group , name ) ; if ( nextGroup ) { group = nextGroup ; } else { break ; } } if ( ! group ) { throw Error ( `Xcode PBXGroup with name "${ path }" could not be found in the Xcode project.` ) ; } return group ; } export function ensureGroupRecursively ( project , filepath ) { const components = splitPath ( filepath ) ; const hasChild = ( group , name ) => group . children . find ( ( { comment } ) => comment === name ) ; const { firstProject } = project . getFirstProject ( ) ; let topMostGroup = project . getPBXGroupByKey ( firstProject . mainGroup ) ; for ( const pathComponent of components ) { if ( topMostGroup && ! hasChild ( topMostGroup , pathComponent ) ) { topMostGroup . children . push ( { comment : pathComponent , value : project . pbxCreateGroup ( pathComponent , '""' ) } ) ; } topMostGroup = project . pbxGroupByName ( pathComponent ) ; } return topMostGroup ?? null ; } /**
 * Get the pbxproj for the given path
 */ export function getPbxproj ( projectRoot ) { const projectPath = Paths . getPBXProjectPath ( projectRoot ) ; const project = xcode . project ( projectPath ) ; project . parseSync ( ) ; return project ; } param project export function getProductName ( project ) { let productName = '$(TARGET_NAME)' ; try { productName = project . productName ; } catch { } if ( productName === '$(TARGET_NAME)' ) { const targetName = project . getFirstTarget ( ) ?. firstTarget ?. productName ; productName = targetName ?? productName ; } return productName ; } export function getProjectSection ( project ) { return project . pbxProjectSection ( ) ; } export function getXCConfigurationListEntries ( project ) { const lists = project . pbxXCConfigurationList ( ) ; return Object . entries ( lists ) . filter ( isNotComment ) ; } export function getBuildConfigurationsForListId ( project , configurationListId ) { const configurationListEntries = getXCConfigurationListEntries ( project ) ; const [ , configurationList ] = configurationListEntries . find ( ( [ key ] ) => key === configurationListId ) as ConfigurationListEntry ; const buildConfigurations = configurationList . buildConfigurations . map ( i => i . value ) ; return Object . entries ( project . pbxXCBuildConfigurationSection ( ) ) . filter ( isNotComment ) . filter ( isBuildConfig ) . filter ( ( [ key ] ) => buildConfigurations . includes ( key ) ) ; } export function getBuildConfigurationForListIdAndName ( project , { configurationListId , buildConfiguration , } ) { const xcBuildConfigurationEntry = getBuildConfigurationsForListId ( project , configurationListId ) . find ( i => i [ 1 ] . name === buildConfiguration ) ; if ( ! xcBuildConfigurationEntry ) { throw new Error ( `Build configuration '${ buildConfiguration }' does not exist in list with id '${ configurationListId }'` ) ; } return xcBuildConfigurationEntry ; } export function isBuildConfig ( [ , sectionItem ] ) { return sectionItem . isa === 'XCBuildConfiguration' ; } export function isNotTestHost ( [ , sectionItem ] ) { return ! sectionItem . buildSettings . TEST_HOST ; } export function isNotComment ( [ key ] ) { return ! key . endsWith ( `_comment` ) ; } export function unquote ( value ) { if ( typeof value === 'number' ) { value = String ( value ) ; } return value . match ( /^"(.*)"$/ ) ?. [ 1 ] ?? value ; }