import { type ConfigPlugin, withXcodeProject } from 'expo/config-plugins'; import path from 'node:path'; import type { PluginConfig } from '../types'; import { applyPatchToFile, configureBuildPhases, configureBuildSettings, createFileFromTemplate, createFileFromTemplateAs, createFramework, createGroup, inferProjectName, mkdir, } from '../utils'; const withXcodeProjectPlugin: ConfigPlugin = (config, pluginConfig) => { return withXcodeProject(config, (config) => { const projectName = config.modRequest.projectName ?? inferProjectName(config.modRequest.platformProjectRoot); const projectRoot = config.modRequest.projectRoot; const xcodeProject = config.modResults; // Create a target for the framework const target = createFramework( xcodeProject, pluginConfig.targetName, pluginConfig.bundleIdentifier ); // Create a directory for the framework files const groupPath = path.join(projectRoot, 'ios', pluginConfig.targetName); mkdir(groupPath); const templateFiles = [ // React Native host manager 'ReactNativeHostManager.swift', // Messaging proxy 'Messaging.swift', // State proxy 'State.swift', // State wrapper 'StateWrapper.swift', //SwiftUI brownfield entrypoint 'ReactNativeView.swift', // UIKit brownfield view controller 'ReactNativeViewController.swift', // ExpoAppDelegate symlinked and reexported from the main Expo package 'ExpoAppDelegate.swift', // ReactNativeDelegate 'ReactNativeDelegate.swift', ]; // Per-target prefix is interpolated into `@objc(...)` annotations so the // ObjC runtime sees a unique class name per inner-app framework. // Swift type names themselves stay unprefixed: each brownfield framework // has a unique Swift module name, which is enough namespace isolation, and // typealias-based unprefixing breaks linking under library-evolution mode // (clients reference symbols by the typealias path, but the framework // exports symbols under the underlying class name → undefined symbol). const templateVars = { prefix: pluginConfig.targetName }; templateFiles.forEach((templateFile) => createFileFromTemplate(templateFile, groupPath, templateVars) ); // Apply patch to ExpoAppDelegate.swift to make it compatible with the brownfield framework applyPatchToFile('ExpoAppDelegate.patch', path.join(groupPath, 'ExpoAppDelegate.swift')); // Create and properly add a new group for the framework createGroup(xcodeProject, pluginConfig.targetName, groupPath, templateFiles); // Create 'Info.plist' and '.entitlements' based on the templates createFileFromTemplate('Info.plist', groupPath, { bundleIdentifier: pluginConfig.bundleIdentifier, targetName: pluginConfig.targetName, }); createFileFromTemplateAs( 'Target.entitlements', groupPath, pluginConfig.targetName + '.entitlements' ); // Configure build phases: // - Reference Expo app target's RN bundle script // - Add custom script for patching ExpoModulesProvider // - Add template files to the compile sources phase configureBuildPhases( xcodeProject, target, pluginConfig.targetName, projectName, templateFiles.map((file) => `${pluginConfig.targetName}/${file}`) ); // Add the required build settings configureBuildSettings( xcodeProject, pluginConfig.targetName, config.ios?.buildNumber || '1', pluginConfig.bundleIdentifier, config.ios?.version || config.version ); // Add Expo.plist to the framework target's resources so expo-updates // can read its configuration from the framework bundle at runtime. // Uses addBuildPhase to create a PBXResourcesBuildPhase for the framework target // since addResourceFile requires an existing Resources phase which framework targets lack. xcodeProject.addBuildPhase( [`${projectName}/Supporting/Expo.plist`], 'PBXResourcesBuildPhase', 'Resources', target.uuid, 'framework', '""' ); return config; }); }; export default withXcodeProjectPlugin;