Add script to find the directory that contains the Xcodeproj (#53669)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53669

## Context

When configuring an app to build with SwiftPM from source, there is a sequence of operations we need to run in order to prepare the project correctly.

## Changed

Add a function that given the root of the app and the name of the xcodeproject file, can return the path to the Xcode project file

## Changelog:
[Internal] -

Reviewed By: cortinico

Differential Revision: D81778456

fbshipit-source-id: f7050bcb049d75a5b1cabf340a5b98f4736e60b3
This commit is contained in:
Riccardo Cipolleschi
2025-10-10 11:19:38 -07:00
committed by meta-codesync[bot]
parent 53464e8483
commit f0725f7802
2 changed files with 203 additions and 0 deletions
@@ -0,0 +1,157 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @noflow
*/
'use strict';
const {findXcodeProjectDirectory} = require('../prepare-app-utils');
// Mock child_process module
jest.mock('child_process');
describe('findXcodeProjectDirectory', () => {
let mockExecSync;
beforeEach(() => {
// Setup mock
const childProcess = require('child_process');
mockExecSync = childProcess.execSync;
// Reset all mocks
jest.clearAllMocks();
});
it('should find Xcode project directory successfully', () => {
// Setup
const appPath = '/path/to/app';
const xcodeProjectName = 'MyApp.xcodeproj';
const mockResult = '/path/to/app/ios/MyApp.xcodeproj';
mockExecSync.mockReturnValue(mockResult + '\n');
// Execute
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
// Assert
expect(result).toBe('/path/to/app/ios');
expect(mockExecSync).toHaveBeenCalledWith(
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
{encoding: 'utf8'},
);
expect(mockExecSync).toHaveBeenCalledTimes(1);
});
it('should find Xcode project in nested subdirectory', () => {
// Setup
const appPath = '/Users/developer/ReactNativeApp';
const xcodeProjectName = 'ReactNativeApp.xcodeproj';
const mockResult =
'/Users/developer/ReactNativeApp/ios/sub/ReactNativeApp.xcodeproj';
mockExecSync.mockReturnValue(mockResult + '\n');
// Execute
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
// Assert
expect(result).toBe('/Users/developer/ReactNativeApp/ios/sub');
expect(mockExecSync).toHaveBeenCalledWith(
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
{encoding: 'utf8'},
);
});
it('should handle project found at root level', () => {
// Setup
const appPath = '/path/to/project';
const xcodeProjectName = 'RootProject.xcodeproj';
const mockResult = '/path/to/project/RootProject.xcodeproj';
mockExecSync.mockReturnValue(mockResult + '\n');
// Execute
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
// Assert
expect(result).toBe('/path/to/project');
expect(mockExecSync).toHaveBeenCalledWith(
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
{encoding: 'utf8'},
);
});
it('should handle paths with spaces in directory names', () => {
// Setup
const appPath = '/path/to/my app';
const xcodeProjectName = 'My App.xcodeproj';
const mockResult = '/path/to/my app/ios folder/My App.xcodeproj';
mockExecSync.mockReturnValue(mockResult + '\n');
// Execute
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
// Assert
expect(result).toBe('/path/to/my app/ios folder');
expect(mockExecSync).toHaveBeenCalledWith(
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
{encoding: 'utf8'},
);
});
it('should throw error when Xcode project is not found', () => {
// Setup
const appPath = '/path/to/app';
const xcodeProjectName = 'NonExistent.xcodeproj';
mockExecSync.mockReturnValue('');
// Execute & Assert
expect(() => findXcodeProjectDirectory(appPath, xcodeProjectName)).toThrow(
`Xcode project 'NonExistent.xcodeproj' not found in '/path/to/app' or its subdirectories`,
);
expect(mockExecSync).toHaveBeenCalledWith(
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
{encoding: 'utf8'},
);
});
it('should throw error when find command returns only whitespace', () => {
// Setup
const appPath = '/path/to/app';
const xcodeProjectName = 'Missing.xcodeproj';
mockExecSync.mockReturnValue(' \n \t ');
// Execute & Assert
expect(() => findXcodeProjectDirectory(appPath, xcodeProjectName)).toThrow(
`Xcode project 'Missing.xcodeproj' not found in '/path/to/app' or its subdirectories`,
);
});
it('should properly escape quotes in app path', () => {
// Setup
const appPath = '/path/to/app with "quotes"';
const xcodeProjectName = 'MyApp.xcodeproj';
const mockResult = '/path/to/app with "quotes"/ios/MyApp.xcodeproj';
mockExecSync.mockReturnValue(mockResult + '\n');
// Execute
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
// Assert
expect(result).toBe('/path/to/app with "quotes"/ios');
expect(mockExecSync).toHaveBeenCalledWith(
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
{encoding: 'utf8'},
);
});
});
@@ -0,0 +1,46 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
const {execSync} = require('child_process');
const path = require('path');
/**
* Find the directory containing the Xcode project within the app path
* @param {string} appPath - The root app path to search in
* @param {string} xcodeProjectName - The name of the Xcode project file (e.g., 'HelloWorld.xcodeproj')
* @returns {string} - The path to the directory containing the Xcode project
*/
function findXcodeProjectDirectory(
appPath /*: string */,
xcodeProjectName /*: string */,
) /*: string */ {
try {
// Use find command to search for the Xcode project
const findCommand = `find "${appPath}" -name "${xcodeProjectName}" -type d -print`;
const result = execSync(findCommand, {encoding: 'utf8'}).trim();
if (!result) {
throw new Error(
`Xcode project '${xcodeProjectName}' not found in '${appPath}' or its subdirectories`,
);
}
// Return the directory containing the Xcode project (parent of the .xcodeproj file)
return path.dirname(result);
} catch (error) {
throw new Error(
`Failed to find Xcode project '${xcodeProjectName}': ${error.message}`,
);
}
}
module.exports = {
findXcodeProjectDirectory,
};