@@ -694,9 +694,327 @@ function shouldAutoDeintegrate(
694694 return true ;
695695}
696696
697+ // The Podfile DSL calls that wire up React Native's CocoaPods integration.
698+ const RN_PODFILE_CALLS = [
699+ 'use_react_native!' ,
700+ 'use_native_modules!' ,
701+ 'prepare_react_native_project!' ,
702+ ] ;
703+
704+ // Strip every occurrence of the RN Podfile calls above, including their
705+ // argument list when the call spans multiple lines, e.g. the stock template's
706+ // use_react_native!(
707+ // :path => "...",
708+ // :app_path => "..."
709+ // )
710+ // A plain line-filter only removes the opening line and leaves the argument
711+ // lines + closing paren behind, producing a syntactically broken Podfile.
712+ // Only strips the call's own line(s); doesn't touch surrounding code, so a
713+ // call assigned to a variable (`config = use_native_modules!(...)`) keeps its
714+ // line but loses the call — matching prior (single-line) behavior.
715+ function stripReactNativeFromPodfile ( contents /*: string */ ) /*: string */ {
716+ let text = contents ;
717+ for ( const name of RN_PODFILE_CALLS ) {
718+ let out = '' ;
719+ let i = 0 ;
720+ while ( i < text . length ) {
721+ const idx = text . indexOf ( name , i ) ;
722+ if ( idx === - 1 ) {
723+ out += text . slice ( i ) ;
724+ break ;
725+ }
726+ out += text . slice ( i , idx ) ;
727+ let end = idx + name . length ;
728+ let k = end ;
729+ while ( k < text . length && ( text [ k ] === ' ' || text [ k ] === '\t' ) ) k ++ ;
730+ if ( text [ k ] === '(' ) {
731+ let depth = 0 ;
732+ for ( let m = k ; m < text . length ; m ++ ) {
733+ if ( text [ m ] === '(' ) depth ++ ;
734+ else if ( text [ m ] === ')' ) {
735+ depth -- ;
736+ if ( depth === 0 ) {
737+ end = m + 1 ;
738+ break ;
739+ }
740+ }
741+ }
742+ }
743+ // If the rest of the line (after the call) is blank, drop the trailing
744+ // newline too, so we don't leave an empty line behind.
745+ let lineEnd = text . indexOf ( '\n' , end ) ;
746+ if ( lineEnd === - 1 ) lineEnd = text . length ;
747+ if ( text . slice ( end , lineEnd ) . trim ( ) === '' ) {
748+ end = lineEnd < text . length ? lineEnd + 1 : lineEnd ;
749+ }
750+ // If everything before the call on its line is just indentation, drop
751+ // that indentation too, so we don't leave a whitespace-only line.
752+ const lineStart = out . lastIndexOf ( '\n' ) + 1 ;
753+ if ( out . slice ( lineStart ) . trim ( ) === '' ) {
754+ out = out . slice ( 0 , lineStart ) ;
755+ }
756+ i = end ;
757+ }
758+ text = out ;
759+ }
760+ return text ;
761+ }
762+
763+ // Finds the matching ` }` for the `{` at `openIdx`, or null if unbalanced.
764+ function matchingBrace ( text /*: string */ , openIdx /*: number */ ) /*: number | null */ {
765+ let depth = 0 ;
766+ for ( let i = openIdx ; i < text . length ; i ++ ) {
767+ if ( text [ i ] === '{' ) depth ++ ;
768+ else if ( text [ i ] === '}' ) {
769+ depth -- ;
770+ if ( depth === 0 ) return i ;
771+ }
772+ }
773+ return null ;
774+ }
775+
776+ // Finds `key: {` within `[start, end)`, but only occurrences at brace-depth 0
777+ // relative to `start` — i.e. a direct property of the object being scanned,
778+ // not a same-named key nested inside some other property's value. Returns
779+ // the `{...}` range of that key's object value, or null if absent.
780+ function findTopLevelKeyObjectRange (
781+ text /*: string */ ,
782+ key /*: string */ ,
783+ start /*: number */ ,
784+ end /*: number */ ,
785+ ) /*: {open: number, close: number} | null */ {
786+ const re = new RegExp ( '\\b' + key + '\\s*:\\s*{' , 'g' ) ;
787+ re . lastIndex = start ;
788+ let m ;
789+ while ( ( m = re . exec ( text ) ) && m . index < end ) {
790+ let depth = 0 ;
791+ for ( let i = start ; i < m . index ; i ++ ) {
792+ if ( text [ i ] === '{' ) depth ++ ;
793+ else if ( text [ i ] === '}' ) depth -- ;
794+ }
795+ if ( depth === 0 ) {
796+ const openIdx = m . index + m [ 0 ] . length - 1 ;
797+ const closeIdx = matchingBrace ( text , openIdx ) ;
798+ if ( closeIdx != null && closeIdx <= end ) {
799+ return { open : openIdx , close : closeIdx } ;
800+ }
801+ }
802+ }
803+ return null ;
804+ }
805+
806+ // Inserts `propertyText` (no trailing comma/newline) as the first property of
807+ // the object whose `{` is at `openIdx`, matching the existing content's
808+ // line-break style so we don't smash an empty `{}` and a populated object
809+ // into the same shape.
810+ function insertFirstProperty (
811+ text /*: string */ ,
812+ openIdx /*: number */ ,
813+ indent /*: string */ ,
814+ propertyText /*: string */ ,
815+ ) /*: string */ {
816+ const rest = text . slice ( openIdx + 1 ) ;
817+ const needsNewlineAfter = ! / ^ [ \t ] * \r ? \n / . test ( rest ) ;
818+ return (
819+ text . slice ( 0 , openIdx + 1 ) +
820+ '\n' +
821+ indent +
822+ propertyText +
823+ ',' +
824+ ( needsNewlineAfter ? '\n' + indent . slice ( 0 , - 2 ) : '' ) +
825+ rest
826+ ) ;
827+ }
828+
829+ // Sets `project.ios.automaticPodsInstallation` to `false` in the contents of
830+ // a react-native.config.js, inserting whichever of `project` / `ios` /
831+ // `automaticPodsInstallation` are missing. Returns null when `contents`
832+ // doesn't look like a plain `module.exports = {...}` object literal — the
833+ // caller should warn instead of risking a corrupt rewrite.
834+ function withAutomaticPodsInstallationDisabled (
835+ contents /*: string */ ,
836+ ) /*: string | null */ {
837+ if ( / a u t o m a t i c P o d s I n s t a l l a t i o n \s * : \s * f a l s e \b / . test ( contents ) ) {
838+ return contents ;
839+ }
840+ if ( / a u t o m a t i c P o d s I n s t a l l a t i o n \s * : \s * t r u e \b / . test ( contents ) ) {
841+ return contents . replace (
842+ / a u t o m a t i c P o d s I n s t a l l a t i o n \s * : \s * t r u e \b / ,
843+ 'automaticPodsInstallation: false' ,
844+ ) ;
845+ }
846+ const exportsMatch = / m o d u l e \. e x p o r t s \s * = \s * { / . exec ( contents ) ;
847+ if ( ! exportsMatch ) {
848+ return null ;
849+ }
850+ const exportsOpen = exportsMatch . index + exportsMatch [ 0 ] . length - 1 ;
851+ const exportsClose = matchingBrace ( contents , exportsOpen ) ;
852+ if ( exportsClose == null ) {
853+ return null ;
854+ }
855+
856+ const projectRange = findTopLevelKeyObjectRange (
857+ contents ,
858+ 'project' ,
859+ exportsOpen + 1 ,
860+ exportsClose ,
861+ ) ;
862+ if ( projectRange == null ) {
863+ return insertFirstProperty (
864+ contents ,
865+ exportsOpen ,
866+ ' ' ,
867+ 'project: {\n ios: {\n automaticPodsInstallation: false,\n },\n }' ,
868+ ) ;
869+ }
870+
871+ const iosRange = findTopLevelKeyObjectRange (
872+ contents ,
873+ 'ios' ,
874+ projectRange . open + 1 ,
875+ projectRange . close ,
876+ ) ;
877+ if ( iosRange == null ) {
878+ return insertFirstProperty (
879+ contents ,
880+ projectRange . open ,
881+ ' ' ,
882+ 'ios: {\n automaticPodsInstallation: false,\n }' ,
883+ ) ;
884+ }
885+
886+ return insertFirstProperty (
887+ contents ,
888+ iosRange . open ,
889+ ' ' ,
890+ 'automaticPodsInstallation: false' ,
891+ ) ;
892+ }
893+
894+ // Disables automatic `pod install` on future `react-native run-ios` /
895+ // `build-ios` invocations by setting `project.ios.automaticPodsInstallation`
896+ // to `false` in react-native.config.js (default is `true` — see
897+ // @react -native-community/cli-config's schema). Left on, it's a landmine: the
898+ // CLI silently re-runs CocoaPods on the next build and re-breaks the SPM
899+ // package graph, the same class of problem `podfileHasRnIntegration` warns
900+ // about for the Podfile itself.
901+ function disableAutomaticPodsInstallation ( appRoot /*: string */ ) /*: void */ {
902+ const configPath = path . join ( appRoot , 'react-native.config.js' ) ;
903+ if ( ! fs . existsSync ( configPath ) ) {
904+ fs . writeFileSync (
905+ configPath ,
906+ 'module.exports = {\n' +
907+ ' project: {\n' +
908+ ' ios: {\n' +
909+ ' automaticPodsInstallation: false,\n' +
910+ ' },\n' +
911+ ' },\n' +
912+ '};\n' ,
913+ 'utf8' ,
914+ ) ;
915+ log (
916+ 'Created react-native.config.js with `automaticPodsInstallation: false`.' ,
917+ ) ;
918+ return ;
919+ }
920+ const orig = fs . readFileSync ( configPath , 'utf8' ) ;
921+ const updated = withAutomaticPodsInstallationDisabled ( orig ) ;
922+ if ( updated == null ) {
923+ log (
924+ "\x1b[33mNote: couldn't automatically disable automaticPodsInstallation " +
925+ "in react-native.config.js (unrecognized format). Set `project.ios." +
926+ 'automaticPodsInstallation` to `false` yourself, or a future `pod ' +
927+ 'install` will re-break the SPM package graph.\x1b[0m' ,
928+ ) ;
929+ return ;
930+ }
931+ if ( updated !== orig ) {
932+ fs . writeFileSync ( configPath , updated , 'utf8' ) ;
933+ log ( 'Disabled `automaticPodsInstallation` in react-native.config.js.' ) ;
934+ }
935+ }
936+
937+ // Locate the .xcworkspace CocoaPods manages alongside the .xcodeproj — same
938+ // basename by convention (what `pod install` creates), falling back to the
939+ // single *.xcworkspace in appRoot when the basenames don't line up. Returns
940+ // null when there's no workspace at all (never `pod install`-ed) or when the
941+ // fallback scan is ambiguous.
942+ function findXcworkspace (
943+ appRoot /*: string */ ,
944+ xcodeprojPath /*: string */ ,
945+ ) /*: string | null */ {
946+ const sibling = path . join (
947+ path . dirname ( xcodeprojPath ) ,
948+ path . basename ( xcodeprojPath , '.xcodeproj' ) + '.xcworkspace' ,
949+ ) ;
950+ if ( fs . existsSync ( sibling ) ) {
951+ return sibling ;
952+ }
953+ const names /*: Array<string> */ = [ ] ;
954+ let entries /*: Array<{name: string, isDirectory(): boolean}> */ = [ ] ;
955+ try {
956+ // $FlowFixMe[incompatible-type] Dirent typing
957+ entries = fs . readdirSync ( appRoot , { withFileTypes : true } ) ;
958+ } catch {
959+ return null ;
960+ }
961+ for ( const entry of entries ) {
962+ if ( ! entry . isDirectory ( ) ) continue ;
963+ // $FlowFixMe[incompatible-type] Dirent.name is string|Buffer in Flow stubs
964+ const name /*: string */ = entry . name ;
965+ if ( name . endsWith ( '.xcworkspace' ) ) {
966+ names . push ( name ) ;
967+ }
968+ }
969+ return names . length === 1 ? path . join ( appRoot , names [ 0 ] ) : null ;
970+ }
971+
972+ // Strip the `group:Pods/Pods.xcodeproj` FileRef CocoaPods adds to the
973+ // .xcworkspace's contents.xcworkspacedata, e.g.:
974+ // <FileRef
975+ // location = "group:Pods/Pods.xcodeproj">
976+ // </FileRef>
977+ // `pod deintegrate` removes the Pods project/integration but doesn't touch
978+ // the workspace, so this reference dangles — Xcode shows a permanent red,
979+ // missing Pods.xcodeproj row in the workspace navigator otherwise.
980+ function removeDanglingPodsFileRef ( xml /*: string */ ) /*: string */ {
981+ return xml . replace (
982+ / [ \t ] * < F i l e R e f \s + l o c a t i o n \s * = \s * " g r o u p : P o d s \/ P o d s \. x c o d e p r o j " \s * (?: \/ > | > \s * < \/ F i l e R e f > ) \r ? \n ? / g,
983+ '' ,
984+ ) ;
985+ }
986+
987+ // Called by `add --deintegrate` after `pod deintegrate`. Only touches the
988+ // reference when Pods/Pods.xcodeproj is actually gone from disk, so a
989+ // side-by-side non-RN CocoaPods integration is never disturbed. No-op when
990+ // the workspace, its contents.xcworkspacedata, or the reference is absent.
991+ function cleanupDanglingPodsWorkspaceRef (
992+ appRoot /*: string */ ,
993+ xcodeprojPath /*: string */ ,
994+ ) /*: boolean */ {
995+ if ( fs . existsSync ( path . join ( appRoot , 'Pods' , 'Pods.xcodeproj' ) ) ) {
996+ return false ;
997+ }
998+ const workspacePath = findXcworkspace ( appRoot , xcodeprojPath ) ;
999+ if ( workspacePath == null ) {
1000+ return false ;
1001+ }
1002+ const dataPath = path . join ( workspacePath , 'contents.xcworkspacedata' ) ;
1003+ if ( ! fs . existsSync ( dataPath ) ) {
1004+ return false ;
1005+ }
1006+ const orig = fs . readFileSync ( dataPath , 'utf8' ) ;
1007+ const cleaned = removeDanglingPodsFileRef ( orig ) ;
1008+ if ( cleaned === orig ) {
1009+ return false ;
1010+ }
1011+ fs . writeFileSync ( dataPath , cleaned , 'utf8' ) ;
1012+ return true ;
1013+ }
1014+
6971015// Run `pod deintegrate` then strip React Native from the Podfile (leaving any
6981016// non-RN pods). Requires CocoaPods on PATH (fail-loud otherwise). Flag-gated ⇒
699- // no prompt ⇒ CI-safe. Does NOT touch the .xcworkspace.
1017+ // no prompt ⇒ CI-safe.
7001018function runDeintegrate ( appRoot /*: string */ ) /*: void */ {
7011019 try {
7021020 execFileSync ( 'pod' , [ '--version' ] , { stdio : 'ignore' } ) ;
@@ -714,20 +1032,14 @@ function runDeintegrate(appRoot /*: string */) /*: void */ {
7141032 const podfilePath = path . join ( appRoot , 'Podfile' ) ;
7151033 if ( fs . existsSync ( podfilePath ) ) {
7161034 const orig = fs . readFileSync ( podfilePath , 'utf8' ) ;
717- const stripped = orig
718- . split ( '\n' )
719- . filter (
720- l =>
721- ! / u s e _ r e a c t _ n a t i v e ! | u s e _ n a t i v e _ m o d u l e s ! | p r e p a r e _ r e a c t _ n a t i v e _ p r o j e c t ! / . test (
722- l ,
723- ) ,
724- )
725- . join ( '\n' ) ;
1035+ const stripped = stripReactNativeFromPodfile ( orig ) ;
7261036 if ( stripped !== orig ) {
7271037 fs . writeFileSync ( podfilePath , stripped , 'utf8' ) ;
7281038 log ( 'Stripped React Native integration from Podfile.' ) ;
7291039 }
7301040 }
1041+
1042+ disableAutomaticPodsInstallation ( appRoot ) ;
7311043}
7321044
7331045// Pick the .xcodeproj to inject into: --xcodeproj override > a prior in-place
@@ -810,6 +1122,11 @@ async function setupXcodeproj(
8101122 if ( cleanupLeftoverPodsGroup ( xcodeprojPath ) ) {
8111123 log ( 'Removed the leftover empty `Pods` group from the project.' ) ;
8121124 }
1125+ if ( cleanupDanglingPodsWorkspaceRef ( appRoot , xcodeprojPath ) ) {
1126+ log (
1127+ 'Removed the dangling Pods.xcodeproj reference from the .xcworkspace.' ,
1128+ ) ;
1129+ }
8131130 }
8141131
8151132 // Preflight: a still-CocoaPods-integrated pbxproj is the real build-breaker.
@@ -1278,6 +1595,10 @@ module.exports = {
12781595 resolveAction,
12791596 resolveConfigCommandToPin,
12801597 resolveExplicitConfigCommand,
1598+ cleanupDanglingPodsWorkspaceRef,
1599+ removeDanglingPodsFileRef,
12811600 shouldAutoDeintegrate,
1601+ stripReactNativeFromPodfile,
1602+ withAutomaticPodsInstallationDisabled,
12821603 ensureBothArtifactFlavors,
12831604} ;
0 commit comments