1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types'; 2082815dcSEvan Baconimport assert from 'assert'; 3082815dcSEvan Baconimport path from 'path'; 4082815dcSEvan Baconimport slugify from 'slugify'; 5082815dcSEvan Baconimport xcode, { 6082815dcSEvan Bacon PBXFile, 7082815dcSEvan Bacon PBXGroup, 8082815dcSEvan Bacon PBXNativeTarget, 9082815dcSEvan Bacon PBXProject, 10082815dcSEvan Bacon UUID, 11082815dcSEvan Bacon XCBuildConfiguration, 12082815dcSEvan Bacon XCConfigurationList, 13082815dcSEvan Bacon XcodeProject, 14082815dcSEvan Bacon} from 'xcode'; 15082815dcSEvan Baconimport pbxFile from 'xcode/lib/pbxFile'; 16082815dcSEvan Bacon 17*8a424bebSJames Ideimport { trimQuotes } from './string'; 18082815dcSEvan Baconimport { addWarningIOS } from '../../utils/warnings'; 19082815dcSEvan Baconimport * as Paths from '../Paths'; 20082815dcSEvan Bacon 21082815dcSEvan Baconexport type ProjectSectionEntry = [string, PBXProject]; 22082815dcSEvan Bacon 23082815dcSEvan Baconexport type NativeTargetSection = Record<string, PBXNativeTarget>; 24082815dcSEvan Bacon 25082815dcSEvan Baconexport type NativeTargetSectionEntry = [string, PBXNativeTarget]; 26082815dcSEvan Bacon 27082815dcSEvan Baconexport type ConfigurationLists = Record<string, XCConfigurationList>; 28082815dcSEvan Bacon 29082815dcSEvan Baconexport type ConfigurationListEntry = [string, XCConfigurationList]; 30082815dcSEvan Bacon 31082815dcSEvan Baconexport type ConfigurationSectionEntry = [string, XCBuildConfiguration]; 32082815dcSEvan Bacon 33082815dcSEvan Baconexport function getProjectName(projectRoot: string) { 34082815dcSEvan Bacon const sourceRoot = Paths.getSourceRoot(projectRoot); 35082815dcSEvan Bacon return path.basename(sourceRoot); 36082815dcSEvan Bacon} 37082815dcSEvan Bacon 38082815dcSEvan Baconexport function resolvePathOrProject( 39082815dcSEvan Bacon projectRootOrProject: string | XcodeProject 40082815dcSEvan Bacon): XcodeProject | null { 41082815dcSEvan Bacon if (typeof projectRootOrProject === 'string') { 42082815dcSEvan Bacon try { 43082815dcSEvan Bacon return getPbxproj(projectRootOrProject); 44082815dcSEvan Bacon } catch { 45082815dcSEvan Bacon return null; 46082815dcSEvan Bacon } 47082815dcSEvan Bacon } 48082815dcSEvan Bacon return projectRootOrProject; 49082815dcSEvan Bacon} 50082815dcSEvan Bacon 51082815dcSEvan Bacon// TODO: come up with a better solution for using app.json expo.name in various places 52082815dcSEvan Baconexport function sanitizedName(name: string) { 53082815dcSEvan Bacon // Default to the name `app` when every safe character has been sanitized 54082815dcSEvan Bacon return sanitizedNameForProjects(name) || sanitizedNameForProjects(slugify(name)) || 'app'; 55082815dcSEvan Bacon} 56082815dcSEvan Bacon 57082815dcSEvan Baconfunction sanitizedNameForProjects(name: string) { 58082815dcSEvan Bacon return name 59082815dcSEvan Bacon .replace(/[\W_]+/g, '') 60082815dcSEvan Bacon .normalize('NFD') 61082815dcSEvan Bacon .replace(/[\u0300-\u036f]/g, ''); 62082815dcSEvan Bacon} 63082815dcSEvan Bacon 64082815dcSEvan Bacon// TODO: it's silly and kind of fragile that we look at app config to determine 65082815dcSEvan Bacon// the ios project paths. Overall this function needs to be revamped, just a 66082815dcSEvan Bacon// placeholder for now! Make this more robust when we support applying config 67082815dcSEvan Bacon// at any time (currently it's only applied on eject). 68082815dcSEvan Baconexport function getHackyProjectName(projectRoot: string, config: ExpoConfig): string { 69082815dcSEvan Bacon // Attempt to get the current ios folder name (apply). 70082815dcSEvan Bacon try { 71082815dcSEvan Bacon return getProjectName(projectRoot); 72082815dcSEvan Bacon } catch { 73082815dcSEvan Bacon // If no iOS project exists then create a new one (eject). 74082815dcSEvan Bacon const projectName = config.name; 75082815dcSEvan Bacon assert(projectName, 'Your project needs a name in app.json/app.config.js.'); 76082815dcSEvan Bacon return sanitizedName(projectName); 77082815dcSEvan Bacon } 78082815dcSEvan Bacon} 79082815dcSEvan Bacon 80082815dcSEvan Baconfunction createProjectFileForGroup({ filepath, group }: { filepath: string; group: PBXGroup }) { 81082815dcSEvan Bacon const file = new pbxFile(filepath); 82082815dcSEvan Bacon 83082815dcSEvan Bacon const conflictingFile = group.children.find((child) => child.comment === file.basename); 84082815dcSEvan Bacon if (conflictingFile) { 85082815dcSEvan Bacon // This can happen when a file like the GoogleService-Info.plist needs to be added and the eject command is run twice. 86082815dcSEvan Bacon // Not much we can do here since it might be a conflicting file. 87082815dcSEvan Bacon return null; 88082815dcSEvan Bacon } 89082815dcSEvan Bacon return file; 90082815dcSEvan Bacon} 91082815dcSEvan Bacon 92082815dcSEvan Bacon/** 93082815dcSEvan Bacon * Add a resource file (ex: `SplashScreen.storyboard`, `Images.xcassets`) to an Xcode project. 94082815dcSEvan Bacon * This is akin to creating a new code file in Xcode with `⌘+n`. 95082815dcSEvan Bacon */ 96082815dcSEvan Baconexport function addResourceFileToGroup({ 97082815dcSEvan Bacon filepath, 98082815dcSEvan Bacon groupName, 99082815dcSEvan Bacon // Should add to `PBXBuildFile Section` 100082815dcSEvan Bacon isBuildFile, 101082815dcSEvan Bacon project, 102082815dcSEvan Bacon verbose, 103082815dcSEvan Bacon targetUuid, 104082815dcSEvan Bacon}: { 105082815dcSEvan Bacon filepath: string; 106082815dcSEvan Bacon groupName: string; 107082815dcSEvan Bacon isBuildFile?: boolean; 108082815dcSEvan Bacon project: XcodeProject; 109082815dcSEvan Bacon verbose?: boolean; 110082815dcSEvan Bacon targetUuid?: string; 111082815dcSEvan Bacon}): XcodeProject { 112082815dcSEvan Bacon return addFileToGroupAndLink({ 113082815dcSEvan Bacon filepath, 114082815dcSEvan Bacon groupName, 115082815dcSEvan Bacon project, 116082815dcSEvan Bacon verbose, 117082815dcSEvan Bacon targetUuid, 118082815dcSEvan Bacon addFileToProject({ project, file }) { 119082815dcSEvan Bacon project.addToPbxFileReferenceSection(file); 120082815dcSEvan Bacon if (isBuildFile) { 121082815dcSEvan Bacon project.addToPbxBuildFileSection(file); 122082815dcSEvan Bacon } 123082815dcSEvan Bacon project.addToPbxResourcesBuildPhase(file); 124082815dcSEvan Bacon }, 125082815dcSEvan Bacon }); 126082815dcSEvan Bacon} 127082815dcSEvan Bacon 128082815dcSEvan Bacon/** 129082815dcSEvan Bacon * Add a build source file (ex: `AppDelegate.m`, `ViewController.swift`) to an Xcode project. 130082815dcSEvan Bacon * This is akin to creating a new code file in Xcode with `⌘+n`. 131082815dcSEvan Bacon */ 132082815dcSEvan Baconexport function addBuildSourceFileToGroup({ 133082815dcSEvan Bacon filepath, 134082815dcSEvan Bacon groupName, 135082815dcSEvan Bacon project, 136082815dcSEvan Bacon verbose, 137082815dcSEvan Bacon targetUuid, 138082815dcSEvan Bacon}: { 139082815dcSEvan Bacon filepath: string; 140082815dcSEvan Bacon groupName: string; 141082815dcSEvan Bacon project: XcodeProject; 142082815dcSEvan Bacon verbose?: boolean; 143082815dcSEvan Bacon targetUuid?: string; 144082815dcSEvan Bacon}): XcodeProject { 145082815dcSEvan Bacon return addFileToGroupAndLink({ 146082815dcSEvan Bacon filepath, 147082815dcSEvan Bacon groupName, 148082815dcSEvan Bacon project, 149082815dcSEvan Bacon verbose, 150082815dcSEvan Bacon targetUuid, 151082815dcSEvan Bacon addFileToProject({ project, file }) { 152082815dcSEvan Bacon project.addToPbxFileReferenceSection(file); 153082815dcSEvan Bacon project.addToPbxBuildFileSection(file); 154082815dcSEvan Bacon project.addToPbxSourcesBuildPhase(file); 155082815dcSEvan Bacon }, 156082815dcSEvan Bacon }); 157082815dcSEvan Bacon} 158082815dcSEvan Bacon 159082815dcSEvan Bacon// TODO(brentvatne): I couldn't figure out how to do this with an existing 160082815dcSEvan Bacon// higher level function exposed by the xcode library, but we should find out how to do 161082815dcSEvan Bacon// that and replace this with it 162082815dcSEvan Baconexport function addFileToGroupAndLink({ 163082815dcSEvan Bacon filepath, 164082815dcSEvan Bacon groupName, 165082815dcSEvan Bacon project, 166082815dcSEvan Bacon verbose, 167082815dcSEvan Bacon addFileToProject, 168082815dcSEvan Bacon targetUuid, 169082815dcSEvan Bacon}: { 170082815dcSEvan Bacon filepath: string; 171082815dcSEvan Bacon groupName: string; 172082815dcSEvan Bacon project: XcodeProject; 173082815dcSEvan Bacon verbose?: boolean; 174082815dcSEvan Bacon targetUuid?: string; 175082815dcSEvan Bacon addFileToProject: (props: { file: PBXFile; project: XcodeProject }) => void; 176082815dcSEvan Bacon}): XcodeProject { 177082815dcSEvan Bacon const group = pbxGroupByPathOrAssert(project, groupName); 178082815dcSEvan Bacon 179082815dcSEvan Bacon const file = createProjectFileForGroup({ filepath, group }); 180082815dcSEvan Bacon 181082815dcSEvan Bacon if (!file) { 182082815dcSEvan Bacon if (verbose) { 183082815dcSEvan Bacon // This can happen when a file like the GoogleService-Info.plist needs to be added and the eject command is run twice. 184082815dcSEvan Bacon // Not much we can do here since it might be a conflicting file. 185082815dcSEvan Bacon addWarningIOS( 186082815dcSEvan Bacon 'ios-xcode-project', 187082815dcSEvan Bacon `Skipped adding duplicate file "${filepath}" to PBXGroup named "${groupName}"` 188082815dcSEvan Bacon ); 189082815dcSEvan Bacon } 190082815dcSEvan Bacon return project; 191082815dcSEvan Bacon } 192082815dcSEvan Bacon 193082815dcSEvan Bacon if (targetUuid != null) { 194082815dcSEvan Bacon file.target = targetUuid; 195082815dcSEvan Bacon } else { 196082815dcSEvan Bacon const applicationNativeTarget = project.getTarget('com.apple.product-type.application'); 197082815dcSEvan Bacon file.target = applicationNativeTarget?.uuid; 198082815dcSEvan Bacon } 199082815dcSEvan Bacon 200082815dcSEvan Bacon file.uuid = project.generateUuid(); 201082815dcSEvan Bacon file.fileRef = project.generateUuid(); 202082815dcSEvan Bacon 203082815dcSEvan Bacon addFileToProject({ project, file }); 204082815dcSEvan Bacon 205082815dcSEvan Bacon group.children.push({ 206082815dcSEvan Bacon value: file.fileRef, 207082815dcSEvan Bacon comment: file.basename, 208082815dcSEvan Bacon }); 209082815dcSEvan Bacon return project; 210082815dcSEvan Bacon} 211082815dcSEvan Bacon 212082815dcSEvan Baconexport function getApplicationNativeTarget({ 213082815dcSEvan Bacon project, 214082815dcSEvan Bacon projectName, 215082815dcSEvan Bacon}: { 216082815dcSEvan Bacon project: XcodeProject; 217082815dcSEvan Bacon projectName: string; 218082815dcSEvan Bacon}) { 219082815dcSEvan Bacon const applicationNativeTarget = project.getTarget('com.apple.product-type.application'); 220082815dcSEvan Bacon assert( 221082815dcSEvan Bacon applicationNativeTarget, 222082815dcSEvan Bacon `Couldn't locate application PBXNativeTarget in '.xcodeproj' file.` 223082815dcSEvan Bacon ); 224082815dcSEvan Bacon assert( 225082815dcSEvan Bacon String(applicationNativeTarget.target.name) === projectName, 226082815dcSEvan Bacon `Application native target name mismatch. Expected ${projectName}, but found ${applicationNativeTarget.target.name}.` 227082815dcSEvan Bacon ); 228082815dcSEvan Bacon return applicationNativeTarget; 229082815dcSEvan Bacon} 230082815dcSEvan Bacon 231082815dcSEvan Bacon/** 232082815dcSEvan Bacon * Add a framework to the default app native target. 233082815dcSEvan Bacon * 234082815dcSEvan Bacon * @param projectName Name of the PBX project. 235082815dcSEvan Bacon * @param framework String ending in `.framework`, i.e. `StoreKit.framework` 236082815dcSEvan Bacon */ 237082815dcSEvan Baconexport function addFramework({ 238082815dcSEvan Bacon project, 239082815dcSEvan Bacon projectName, 240082815dcSEvan Bacon framework, 241082815dcSEvan Bacon}: { 242082815dcSEvan Bacon project: XcodeProject; 243082815dcSEvan Bacon projectName: string; 244082815dcSEvan Bacon framework: string; 245082815dcSEvan Bacon}) { 246082815dcSEvan Bacon const target = getApplicationNativeTarget({ project, projectName }); 247082815dcSEvan Bacon return project.addFramework(framework, { target: target.uuid }); 248082815dcSEvan Bacon} 249082815dcSEvan Bacon 250082815dcSEvan Baconfunction splitPath(path: string): string[] { 251082815dcSEvan Bacon // TODO: Should we account for other platforms that may not use `/` 252082815dcSEvan Bacon return path.split('/'); 253082815dcSEvan Bacon} 254082815dcSEvan Bacon 255082815dcSEvan Baconconst findGroup = ( 256082815dcSEvan Bacon group: PBXGroup | undefined, 257082815dcSEvan Bacon name: string 258082815dcSEvan Bacon): 259082815dcSEvan Bacon | { 260082815dcSEvan Bacon value: UUID; 261082815dcSEvan Bacon comment?: string; 262082815dcSEvan Bacon } 263082815dcSEvan Bacon | undefined => { 264082815dcSEvan Bacon if (!group) { 265082815dcSEvan Bacon return undefined; 266082815dcSEvan Bacon } 267082815dcSEvan Bacon 268082815dcSEvan Bacon return group.children.find((group) => group.comment === name); 269082815dcSEvan Bacon}; 270082815dcSEvan Bacon 271082815dcSEvan Baconfunction findGroupInsideGroup( 272082815dcSEvan Bacon project: XcodeProject, 273082815dcSEvan Bacon group: PBXGroup | undefined, 274082815dcSEvan Bacon name: string 275082815dcSEvan Bacon): null | PBXGroup { 276082815dcSEvan Bacon const foundGroup = findGroup(group, name); 277082815dcSEvan Bacon if (foundGroup) { 278082815dcSEvan Bacon return project.getPBXGroupByKey(foundGroup.value) ?? null; 279082815dcSEvan Bacon } 280082815dcSEvan Bacon return null; 281082815dcSEvan Bacon} 282082815dcSEvan Bacon 283082815dcSEvan Baconfunction pbxGroupByPathOrAssert(project: XcodeProject, path: string): PBXGroup { 284082815dcSEvan Bacon const { firstProject } = project.getFirstProject(); 285082815dcSEvan Bacon 286082815dcSEvan Bacon let group = project.getPBXGroupByKey(firstProject.mainGroup); 287082815dcSEvan Bacon 288082815dcSEvan Bacon const components = splitPath(path); 289082815dcSEvan Bacon for (const name of components) { 290082815dcSEvan Bacon const nextGroup = findGroupInsideGroup(project, group, name); 291082815dcSEvan Bacon if (nextGroup) { 292082815dcSEvan Bacon group = nextGroup; 293082815dcSEvan Bacon } else { 294082815dcSEvan Bacon break; 295082815dcSEvan Bacon } 296082815dcSEvan Bacon } 297082815dcSEvan Bacon 298082815dcSEvan Bacon if (!group) { 299082815dcSEvan Bacon throw Error(`Xcode PBXGroup with name "${path}" could not be found in the Xcode project.`); 300082815dcSEvan Bacon } 301082815dcSEvan Bacon 302082815dcSEvan Bacon return group; 303082815dcSEvan Bacon} 304082815dcSEvan Bacon 305082815dcSEvan Baconexport function ensureGroupRecursively(project: XcodeProject, filepath: string): PBXGroup | null { 306082815dcSEvan Bacon const components = splitPath(filepath); 307082815dcSEvan Bacon const hasChild = (group: PBXGroup, name: string) => 308082815dcSEvan Bacon group.children.find(({ comment }) => comment === name); 309082815dcSEvan Bacon const { firstProject } = project.getFirstProject(); 310082815dcSEvan Bacon 311082815dcSEvan Bacon let topMostGroup = project.getPBXGroupByKey(firstProject.mainGroup); 312082815dcSEvan Bacon 313082815dcSEvan Bacon for (const pathComponent of components) { 314082815dcSEvan Bacon if (topMostGroup && !hasChild(topMostGroup, pathComponent)) { 315082815dcSEvan Bacon topMostGroup.children.push({ 316082815dcSEvan Bacon comment: pathComponent, 317082815dcSEvan Bacon value: project.pbxCreateGroup(pathComponent, '""'), 318082815dcSEvan Bacon }); 319082815dcSEvan Bacon } 320082815dcSEvan Bacon topMostGroup = project.pbxGroupByName(pathComponent); 321082815dcSEvan Bacon } 322082815dcSEvan Bacon return topMostGroup ?? null; 323082815dcSEvan Bacon} 324082815dcSEvan Bacon 325082815dcSEvan Bacon/** 326082815dcSEvan Bacon * Get the pbxproj for the given path 327082815dcSEvan Bacon */ 328082815dcSEvan Baconexport function getPbxproj(projectRoot: string): XcodeProject { 329082815dcSEvan Bacon const projectPath = Paths.getPBXProjectPath(projectRoot); 330082815dcSEvan Bacon const project = xcode.project(projectPath); 331082815dcSEvan Bacon project.parseSync(); 332082815dcSEvan Bacon return project; 333082815dcSEvan Bacon} 334082815dcSEvan Bacon 335082815dcSEvan Bacon/** 336082815dcSEvan Bacon * Get the productName for a project, if the name is using a variable `$(TARGET_NAME)`, then attempt to get the value of that variable. 337082815dcSEvan Bacon * 338082815dcSEvan Bacon * @param project 339082815dcSEvan Bacon */ 340082815dcSEvan Baconexport function getProductName(project: XcodeProject): string { 341082815dcSEvan Bacon let productName = '$(TARGET_NAME)'; 342082815dcSEvan Bacon try { 343082815dcSEvan Bacon // If the product name is numeric, this will fail (it's a getter). 344082815dcSEvan Bacon // If the bundle identifier' final component is only numeric values, then the PRODUCT_NAME 345082815dcSEvan Bacon // will be a numeric value, this results in a bug where the product name isn't useful, 346082815dcSEvan Bacon // i.e. `com.bacon.001` -> `1` -- in this case, use the first target name. 347082815dcSEvan Bacon productName = project.productName; 348082815dcSEvan Bacon } catch {} 349082815dcSEvan Bacon 350082815dcSEvan Bacon if (productName === '$(TARGET_NAME)') { 351082815dcSEvan Bacon const targetName = project.getFirstTarget()?.firstTarget?.productName; 352082815dcSEvan Bacon productName = targetName ?? productName; 353082815dcSEvan Bacon } 354082815dcSEvan Bacon 355082815dcSEvan Bacon return productName; 356082815dcSEvan Bacon} 357082815dcSEvan Bacon 358082815dcSEvan Baconexport function getProjectSection(project: XcodeProject) { 359082815dcSEvan Bacon return project.pbxProjectSection(); 360082815dcSEvan Bacon} 361082815dcSEvan Bacon 362082815dcSEvan Baconexport function getXCConfigurationListEntries(project: XcodeProject): ConfigurationListEntry[] { 363082815dcSEvan Bacon const lists = project.pbxXCConfigurationList(); 364082815dcSEvan Bacon return Object.entries(lists).filter(isNotComment); 365082815dcSEvan Bacon} 366082815dcSEvan Bacon 367082815dcSEvan Baconexport function getBuildConfigurationsForListId( 368082815dcSEvan Bacon project: XcodeProject, 369082815dcSEvan Bacon configurationListId: string 370082815dcSEvan Bacon): ConfigurationSectionEntry[] { 371082815dcSEvan Bacon const configurationListEntries = getXCConfigurationListEntries(project); 372082815dcSEvan Bacon const [, configurationList] = configurationListEntries.find( 373082815dcSEvan Bacon ([key]) => key === configurationListId 374082815dcSEvan Bacon ) as ConfigurationListEntry; 375082815dcSEvan Bacon 376082815dcSEvan Bacon const buildConfigurations = configurationList.buildConfigurations.map((i) => i.value); 377082815dcSEvan Bacon 378082815dcSEvan Bacon return Object.entries(project.pbxXCBuildConfigurationSection()) 379082815dcSEvan Bacon .filter(isNotComment) 380082815dcSEvan Bacon .filter(isBuildConfig) 381082815dcSEvan Bacon .filter(([key]: ConfigurationSectionEntry) => buildConfigurations.includes(key)); 382082815dcSEvan Bacon} 383082815dcSEvan Bacon 384082815dcSEvan Baconexport function getBuildConfigurationForListIdAndName( 385082815dcSEvan Bacon project: XcodeProject, 386082815dcSEvan Bacon { 387082815dcSEvan Bacon configurationListId, 388082815dcSEvan Bacon buildConfiguration, 389082815dcSEvan Bacon }: { configurationListId: string; buildConfiguration: string } 390082815dcSEvan Bacon): ConfigurationSectionEntry { 391082815dcSEvan Bacon const xcBuildConfigurationEntry = getBuildConfigurationsForListId( 392082815dcSEvan Bacon project, 393082815dcSEvan Bacon configurationListId 394082815dcSEvan Bacon ).find((i) => trimQuotes(i[1].name) === buildConfiguration); 395082815dcSEvan Bacon if (!xcBuildConfigurationEntry) { 396082815dcSEvan Bacon throw new Error( 397082815dcSEvan Bacon `Build configuration '${buildConfiguration}' does not exist in list with id '${configurationListId}'` 398082815dcSEvan Bacon ); 399082815dcSEvan Bacon } 400082815dcSEvan Bacon return xcBuildConfigurationEntry; 401082815dcSEvan Bacon} 402082815dcSEvan Bacon 403082815dcSEvan Baconexport function isBuildConfig([, sectionItem]: ConfigurationSectionEntry): boolean { 404082815dcSEvan Bacon return sectionItem.isa === 'XCBuildConfiguration'; 405082815dcSEvan Bacon} 406082815dcSEvan Bacon 407082815dcSEvan Baconexport function isNotTestHost([, sectionItem]: ConfigurationSectionEntry): boolean { 408082815dcSEvan Bacon return !sectionItem.buildSettings.TEST_HOST; 409082815dcSEvan Bacon} 410082815dcSEvan Bacon 411082815dcSEvan Baconexport function isNotComment([key]: 412082815dcSEvan Bacon | ConfigurationSectionEntry 413082815dcSEvan Bacon | ProjectSectionEntry 414082815dcSEvan Bacon | ConfigurationListEntry 415082815dcSEvan Bacon | NativeTargetSectionEntry): boolean { 416082815dcSEvan Bacon return !key.endsWith(`_comment`); 417082815dcSEvan Bacon} 418082815dcSEvan Bacon 419082815dcSEvan Bacon// Remove surrounding double quotes if they exist. 420082815dcSEvan Baconexport function unquote(value: string): string { 421082815dcSEvan Bacon // projects with numeric names will fail due to a bug in the xcode package. 422082815dcSEvan Bacon if (typeof value === 'number') { 423082815dcSEvan Bacon value = String(value); 424082815dcSEvan Bacon } 425082815dcSEvan Bacon return value.match(/^"(.*)"$/)?.[1] ?? value; 426082815dcSEvan Bacon} 427