@@ -20,6 +20,7 @@ const {
2020const { findField, findObjectByUuid, quoteIfNeeded} = require ( '../spm-pbxproj' ) ;
2121const { isBalanced} = require ( './pbxproj-oracles' ) ;
2222const fs = require ( 'node:fs' ) ;
23+ const os = require ( 'node:os' ) ;
2324const path = require ( 'node:path' ) ;
2425
2526const PLAIN = fs . readFileSync (
@@ -270,6 +271,171 @@ describe('planInjection', () => {
270271 } ) ;
271272} ) ;
272273
274+ // FOREIGN_XCCONFIG with the reference inside a `Config` group, the way Xcode
275+ // files an xcconfig dragged into a folder of the navigator.
276+ const FOREIGN_XCCONFIG_IN_GROUP = FOREIGN_XCCONFIG . replace (
277+ 'path = Config/App.xcconfig;' ,
278+ 'path = App.xcconfig;' ,
279+ )
280+ . replace (
281+ '\t\t\t\tAA00000000000000000000F1 /* Products */,\n' ,
282+ '\t\t\t\tAA00000000000000000000F1 /* Products */,\n\t\t\t\tCC0000000000000000000002 /* Config */,\n' ,
283+ )
284+ . replace (
285+ '/* End PBXGroup section */' ,
286+ '\t\tCC0000000000000000000002 /* Config */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCC0000000000000000000001 /* App.xcconfig */,\n\t\t\t);\n\t\t\tpath = Config;\n\t\t\tsourceTree = "<group>";\n\t\t};\n/* End PBXGroup section */' ,
287+ ) ;
288+
289+ describe ( "planInjection — a configuration based on the user's own xcconfig" , ( ) => {
290+ let roots = [ ] ;
291+ afterEach ( ( ) => {
292+ for ( const root of roots ) {
293+ fs . rmSync ( root , { recursive : true , force : true } ) ;
294+ }
295+ roots = [ ] ;
296+ } ) ;
297+
298+ // An app root (== SRCROOT) holding `files`, app-root-relative path → content.
299+ function appRootWith ( files ) {
300+ const root = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'spm-include-' ) ) ;
301+ roots . push ( root ) ;
302+ for ( const [ relPath , content ] of Object . entries ( files ) ) {
303+ fs . mkdirSync ( path . dirname ( path . join ( root , relPath ) ) , { recursive : true } ) ;
304+ fs . writeFileSync ( path . join ( root , relPath ) , content , 'utf8' ) ;
305+ }
306+ return root ;
307+ }
308+
309+ function plan ( files , text = FOREIGN_XCCONFIG ) {
310+ const root = appRootWith ( files ) ;
311+ return planInjection ( text , { appRoot : root , srcRoot : root } ) ;
312+ }
313+
314+ const INCLUDES_GENERATED = `#include "../${ XCCONFIG_PATH } "\n` ;
315+
316+ it ( 'accepts it when the xcconfig #includes the generated one' , ( ) => {
317+ const result = plan ( { 'Config/App.xcconfig' : INCLUDES_GENERATED } ) ;
318+ expect ( result . ok ) . toBe ( true ) ;
319+ expect ( result . includedByConfigUuids ) . toEqual ( [ APP_DEBUG_CONFIG ] ) ;
320+ } ) ;
321+
322+ it ( 'includes no configuration it bases on the generated xcconfig itself' , ( ) => {
323+ expect ( planInjection ( PLAIN , { } ) . includedByConfigUuids ) . toEqual ( [ ] ) ;
324+ } ) ;
325+
326+ it ( 'finds an xcconfig filed under a navigator group' , ( ) => {
327+ expect (
328+ plan (
329+ { 'Config/App.xcconfig' : INCLUDES_GENERATED } ,
330+ FOREIGN_XCCONFIG_IN_GROUP ,
331+ ) . ok ,
332+ ) . toBe ( true ) ;
333+ } ) ;
334+
335+ it ( 'follows #include? chains, each path relative to the file that holds it' , ( ) => {
336+ expect (
337+ plan ( {
338+ 'Config/App.xcconfig' :
339+ '// Shared settings\n#include? "Shared/Base.xcconfig"\n' ,
340+ 'Config/Shared/Base.xcconfig' : `#include "../../${ XCCONFIG_PATH } "\n` ,
341+ } ) . ok ,
342+ ) . toBe ( true ) ;
343+ } ) ;
344+
345+ it . each ( [
346+ [
347+ 'a path relative to the project directory' ,
348+ `#include "${ XCCONFIG_PATH } "\n` ,
349+ ] ,
350+ [ 'a commented-out include' , `// ${ INCLUDES_GENERATED } ` ] ,
351+ [ 'an include of a file that is missing' , '#include? "Missing.xcconfig"\n' ] ,
352+ ] ) ( 'refuses %s' , ( _label , content ) => {
353+ expect ( plan ( { 'Config/App.xcconfig' : content } ) . ok ) . toBe ( false ) ;
354+ } ) ;
355+
356+ it ( 'refuses an include cycle that never reaches the generated xcconfig' , ( ) => {
357+ const result = plan ( {
358+ 'Config/App.xcconfig' : '#include "Base.xcconfig"\n' ,
359+ 'Config/Base.xcconfig' : '#include "App.xcconfig"\n' ,
360+ } ) ;
361+ expect ( result . ok ) . toBe ( false ) ;
362+ } ) ;
363+
364+ it ( 'still refuses a list setting without $(inherited) on it' , ( ) => {
365+ const result = plan (
366+ { 'Config/App.xcconfig' : INCLUDES_GENERATED } ,
367+ FOREIGN_XCCONFIG . replaceAll (
368+ 'PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp;' ,
369+ 'HEADER_SEARCH_PATHS = /vendor;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp;' ,
370+ ) ,
371+ ) ;
372+ expect ( result . ok ) . toBe ( false ) ;
373+ expect ( result . reason ) . toContain ( 'HEADER_SEARCH_PATHS (Debug, Release)' ) ;
374+ } ) ;
375+
376+ it ( 'names the exact line to add, relative to the xcconfig that needs it' , ( ) => {
377+ const result = plan ( { 'Config/App.xcconfig' : 'MY_SETTING = 1\n' } ) ;
378+ expect ( result . ok ) . toBe ( false ) ;
379+ expect ( result . reason ) . toContain (
380+ `#include "../${ XCCONFIG_PATH } " to Config/App.xcconfig` ,
381+ ) ;
382+ expect ( result . reason ) . not . toContain ( 'project directory' ) ;
383+ } ) ;
384+
385+ it ( 'says the path is relative to the xcconfig when it cannot find the file' , ( ) => {
386+ const result = plan ( { } ) ;
387+ expect ( result . ok ) . toBe ( false ) ;
388+ expect ( result . reason ) . toContain (
389+ `#include ${ XCCONFIG_PATH } from Config/App.xcconfig` ,
390+ ) ;
391+ expect ( result . reason ) . toContain (
392+ 'relative to the folder that holds Config/App.xcconfig' ,
393+ ) ;
394+ expect ( result . reason ) . not . toContain ( 'project directory' ) ;
395+ } ) ;
396+
397+ it ( "keeps the user's base and records the configuration as including the generated xcconfig" , ( ) => {
398+ const root = appRootWith ( { 'Config/App.xcconfig' : INCLUDES_GENERATED } ) ;
399+ const accepted = planInjection ( FOREIGN_XCCONFIG , {
400+ appRoot : root ,
401+ srcRoot : root ,
402+ } ) ;
403+ const { text, xcconfig, generatedFiles} = injectSpmIntoPbxproj (
404+ FOREIGN_XCCONFIG ,
405+ {
406+ rootUuid : accepted . rootUuid ,
407+ targetUuid : accepted . target . uuid ,
408+ configUuids : accepted . configUuids ,
409+ includedByConfigUuids : accepted . includedByConfigUuids ,
410+ frameworksPhaseUuid : accepted . frameworksPhaseUuid ,
411+ sourcesPhaseUuid : accepted . sourcesPhaseUuid ,
412+ } ,
413+ RN_PATHS ,
414+ null ,
415+ [ ] ,
416+ TEST_FRAMEWORKS ,
417+ ) ;
418+ const baseOf = configUuid =>
419+ findField (
420+ text ,
421+ findObjectByUuid ( text , configUuid ) ,
422+ 'baseConfigurationReference' ,
423+ ) ?. value ;
424+ expect ( baseOf ( APP_DEBUG_CONFIG ) ) . toBe (
425+ 'CC0000000000000000000001 /* App.xcconfig */' ,
426+ ) ;
427+ expect ( baseOf ( APP_RELEASE_CONFIG ) ) . toBe (
428+ `${ xcconfig . fileRefUuid } /* ReactNativeSPM.xcconfig */` ,
429+ ) ;
430+ expect ( xcconfig . configUuids ) . toEqual ( [ APP_RELEASE_CONFIG ] ) ;
431+ expect ( xcconfig . includedByConfigUuids ) . toEqual ( [ APP_DEBUG_CONFIG ] ) ;
432+ expect ( text ) . toContain ( `path = ${ XCCONFIG_PATH } ;` ) ;
433+ expect ( generatedFiles [ XCCONFIG_PATH ] ) . toContain (
434+ 'RN_SPM_FLAVOR[config=Debug] = debug\n' ,
435+ ) ;
436+ } ) ;
437+ } ) ;
438+
273439describe ( 'injectSpmIntoPbxproj — Tier 1 (SPM graph)' , ( ) => {
274440 it ( 'adds the local package references and product dependencies' , ( ) => {
275441 const { text} = inject ( PLAIN ) ;
@@ -564,6 +730,19 @@ describe('injectSpmIntoPbxproj — Tier 2 (build settings + phase)', () => {
564730 '$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/React.framework' ,
565731 ) ;
566732 } ) ;
733+
734+ // The phase only runs the script file, so the file has to be an input for
735+ // Xcode to re-run the phase when the file alone changes.
736+ it ( 'lists its script file among the embed phase inputs' , ( ) => {
737+ const { text} = inject ( PLAIN ) ;
738+ const uuid =
739+ / ( [ 0 - 9 A - F ] { 24 } ) \/ \* E m b e d R e a c t N a t i v e F l a v o r e d F r a m e w o r k s \* \/ = \{ / . exec (
740+ text ,
741+ ) [ 1 ] ;
742+ expect (
743+ findField ( text , findObjectByUuid ( text , uuid ) , 'inputPaths' ) . value ,
744+ ) . toContain ( `${ quoteIfNeeded ( `$(SRCROOT)/${ EMBED_SCRIPT_PATH } ` ) } ,` ) ;
745+ } ) ;
567746} ) ;
568747
569748describe ( 'injectSpmIntoPbxproj — Tier 3 (plugin generated sources)' , ( ) => {
0 commit comments