Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'},
);
});
});
46 changes: 46 additions & 0 deletions packages/react-native/scripts/swiftpm/prepare-app-utils.js
Original file line number Diff line number Diff line change
@@ -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,
};
Loading