diff --git a/packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js b/packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js new file mode 100644 index 000000000000..045f2532bba1 --- /dev/null +++ b/packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js @@ -0,0 +1,1045 @@ +/** + * 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 { + symlinkHeadersFromPath, + symlinkReactAppleHeaders, + symlinkReactCommonHeaders, + symlinkThirdPartyDependenciesHeaders, +} = require('../headers-utils'); + +// Mock all required modules +jest.mock('../utils'); +jest.mock('../headers-mappings'); +jest.mock('fs'); +jest.mock('path'); + +describe('symlinkHeadersFromPath', () => { + let mockUtils; + let mockFs; + let mockPath; + let originalConsoleWarn; + let originalConsoleLog; + + beforeEach(() => { + // Setup mocks + mockUtils = require('../utils'); + mockFs = require('fs'); + mockPath = require('path'); + + // Mock path functions + mockPath.relative.mockImplementation((from, to) => { + return to.replace(from + '/', ''); + }); + mockPath.join.mockImplementation((...args) => args.join('/')); + mockPath.dirname.mockImplementation(filePath => { + const parts = filePath.split('/'); + parts.pop(); + return parts.join('/'); + }); + mockPath.basename.mockImplementation(filePath => { + return filePath.split('/').pop(); + }); + + // Mock console methods to prevent test output noise + originalConsoleWarn = console.warn; + originalConsoleLog = console.log; + console.warn = jest.fn(); + console.log = jest.fn(); + + // Reset all mocks + jest.clearAllMocks(); + }); + + afterEach(() => { + // Restore console methods + console.warn = originalConsoleWarn; + console.log = originalConsoleLog; + }); + + it('should create symlinks for found header files without preserving structure', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const headerFiles = [ + '/source/path/subdir/header1.h', + '/source/path/header2.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockFs.existsSync.mockImplementation(filePath => { + return ( + filePath === '/source/path/subdir/header1.h' || + filePath === '/source/path/header2.h' + ); + }); + + // Execute + const result = symlinkHeadersFromPath(sourcePath, outputPath, false, []); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith(sourcePath, []); + expect(mockUtils.setupSymlink).toHaveBeenCalledTimes(2); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/source/path/subdir/header1.h', + '/output/path/header1.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/source/path/header2.h', + '/output/path/header2.h', + ); + expect(result).toBe(2); + }); + + it('should preserve directory structure when preserveStructure is true', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const headerFiles = [ + '/source/path/subdir/header1.h', + '/source/path/another/header2.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockFs.existsSync.mockImplementation(filePath => { + return ( + filePath === '/source/path/subdir/header1.h' || + filePath === '/source/path/another/header2.h' + ); + }); + + // Execute + const result = symlinkHeadersFromPath(sourcePath, outputPath, true, []); + + // Assert + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/source/path/subdir/header1.h', + '/output/path/subdir/header1.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/source/path/another/header2.h', + '/output/path/another/header2.h', + ); + expect(result).toBe(2); + }); + + it('should exclude specified folders from find command', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const excludeFolders = ['node_modules', 'test']; + + mockUtils.listHeadersInFolder.mockReturnValue([]); + + // Execute + symlinkHeadersFromPath(sourcePath, outputPath, false, excludeFolders); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + sourcePath, + excludeFolders, + ); + }); + + it('should create destination directories if they do not exist', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const headerFiles = ['/source/path/subdir/header1.h']; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockFs.existsSync.mockImplementation(filePath => { + return filePath === '/source/path/subdir/header1.h'; + }); + + // Execute + symlinkHeadersFromPath(sourcePath, outputPath, true, []); + + // Assert - setupSymlink should be called, which internally handles directory creation + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/source/path/subdir/header1.h', + '/output/path/subdir/header1.h', + ); + }); + + it('should remove existing symlink before creating new one', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const headerFiles = ['/source/path/header1.h']; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockFs.existsSync.mockImplementation(filePath => { + return filePath === '/source/path/header1.h'; // Only source file exists + }); + + // Execute + symlinkHeadersFromPath(sourcePath, outputPath, false, []); + + // Assert - setupSymlink should be called, which internally handles removing existing links + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/source/path/header1.h', + '/output/path/header1.h', + ); + }); + + it('should skip non-existent source files', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const headerFiles = [ + '/source/path/header1.h', + '/source/path/nonexistent.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockFs.existsSync.mockImplementation(filePath => { + return filePath === '/source/path/header1.h'; // Only header1.h exists + }); + + // Execute + const result = symlinkHeadersFromPath(sourcePath, outputPath, false, []); + + // Assert + expect(mockUtils.setupSymlink).toHaveBeenCalledTimes(1); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/source/path/header1.h', + '/output/path/header1.h', + ); + expect(result).toBe(1); + }); + + it('should handle empty find command output', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + + mockUtils.listHeadersInFolder.mockReturnValue([]); + + // Execute + const result = symlinkHeadersFromPath(sourcePath, outputPath, false, []); + + // Assert + expect(mockUtils.setupSymlink).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + + it('should handle whitespace-only find command output', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + + mockUtils.listHeadersInFolder.mockReturnValue([]); + + // Execute + const result = symlinkHeadersFromPath(sourcePath, outputPath, false, []); + + // Assert + expect(mockUtils.setupSymlink).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + + it('should handle listHeadersInFolder throwing an error', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const error = new Error('Command failed'); + + mockUtils.listHeadersInFolder.mockImplementation(() => { + throw error; + }); + + // Execute + const result = symlinkHeadersFromPath(sourcePath, outputPath, false, []); + + // Assert + expect(console.warn).toHaveBeenCalledWith( + 'Failed to process headers from /source/path:', + 'Command failed', + ); + expect(result).toBe(0); + }); + + it('should handle setupSymlink throwing an error', () => { + // Setup + const sourcePath = '/source/path'; + const outputPath = '/output/path'; + const headerFiles = ['/source/path/header1.h']; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockFs.existsSync.mockImplementation(filePath => { + return filePath === '/source/path/header1.h'; + }); + mockUtils.setupSymlink.mockImplementation(() => { + throw new Error('Link failed'); + }); + + // Execute + const result = symlinkHeadersFromPath(sourcePath, outputPath, false, []); + + // Assert + expect(console.warn).toHaveBeenCalledWith( + 'Failed to process headers from /source/path:', + 'Link failed', + ); + expect(result).toBe(0); + }); +}); + +describe('symlinkThirdPartyDependenciesHeaders', () => { + let mockUtils; + let mockFs; + let mockPath; + let originalConsoleWarn; + let originalConsoleLog; + + beforeEach(() => { + // Setup mocks + mockUtils = require('../utils'); + mockFs = require('fs'); + mockPath = require('path'); + + // Mock path functions + mockPath.relative.mockImplementation((from, to) => { + return to.replace(from + '/', ''); + }); + mockPath.join.mockImplementation((...args) => args.join('/')); + mockPath.dirname.mockImplementation(filePath => { + const parts = filePath.split('/'); + parts.pop(); + return parts.join('/'); + }); + mockPath.basename.mockImplementation(filePath => { + return filePath.split('/').pop(); + }); + + // Mock console methods to prevent test output noise + originalConsoleWarn = console.warn; + originalConsoleLog = console.log; + console.warn = jest.fn(); + console.log = jest.fn(); + + // Reset all mocks + jest.clearAllMocks(); + }); + + afterEach(() => { + // Restore console methods + console.warn = originalConsoleWarn; + console.log = originalConsoleLog; + }); + + it('should create symlinks for third-party dependencies headers with preserved structure', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output/folder'; + const folderName = 'headers'; + const thirdPartyHeadersPath = + '/path/to/react-native/third-party/ReactNativeDependencies.xcframework/Headers'; + const headerFiles = [ + `${thirdPartyHeadersPath}/boost/boost.h`, + `${thirdPartyHeadersPath}/glog/glog.h`, + `${thirdPartyHeadersPath}/fmt/format.h`, + `${thirdPartyHeadersPath}/folly/folly.hpp`, + ]; + + mockFs.existsSync.mockImplementation(filePath => { + if (filePath === thirdPartyHeadersPath) return true; + if ( + filePath.includes('third-party') && + (filePath.endsWith('.h') || filePath.endsWith('.hpp')) + ) + return true; + return false; + }); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkThirdPartyDependenciesHeaders( + reactNativePath, + outputFolder, + folderName, + ); + + // Assert + expect(mockFs.existsSync).toHaveBeenCalledWith(thirdPartyHeadersPath); + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + thirdPartyHeadersPath, + ['tests'], + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + `${thirdPartyHeadersPath}/boost/boost.h`, + '/output/folder/headers/boost/boost.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + `${thirdPartyHeadersPath}/glog/glog.h`, + '/output/folder/headers/glog/glog.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + `${thirdPartyHeadersPath}/fmt/format.h`, + '/output/folder/headers/fmt/format.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + `${thirdPartyHeadersPath}/folly/folly.hpp`, + '/output/folder/headers/folly/folly.hpp', + ); + expect(result).toBe(4); + }); + + it('should use default folder name when folderName parameter is not provided', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output/folder'; + const thirdPartyHeadersPath = + '/path/to/react-native/third-party/ReactNativeDependencies.xcframework/Headers'; + const headerFiles = [`${thirdPartyHeadersPath}/boost/boost.h`]; + + mockFs.existsSync.mockImplementation(filePath => { + if (filePath === thirdPartyHeadersPath) return true; + if (filePath.includes('third-party') && filePath.endsWith('.h')) + return true; + return false; + }); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute - without folderName parameter + const result = symlinkThirdPartyDependenciesHeaders( + reactNativePath, + outputFolder, + ); + + // Assert - should use default 'headers' folder + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + `${thirdPartyHeadersPath}/boost/boost.h`, + '/output/folder/headers/boost/boost.h', + ); + expect(result).toBe(1); + }); + + it('should warn and return 0 if third-party headers path does not exist', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output/folder'; + const folderName = 'headers'; + const thirdPartyHeadersPath = + '/path/to/react-native/third-party/ReactNativeDependencies.xcframework/Headers'; + + mockFs.existsSync.mockImplementation(filePath => { + if (filePath === thirdPartyHeadersPath) return false; // third-party headers path doesn't exist + return false; + }); + + // Execute + const result = symlinkThirdPartyDependenciesHeaders( + reactNativePath, + outputFolder, + folderName, + ); + + // Assert + expect(console.warn).toHaveBeenCalledWith( + `Third-party dependencies headers path does not exist: ${thirdPartyHeadersPath}`, + ); + expect(mockUtils.listHeadersInFolder).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + + it('should handle setupSymlink throwing an error gracefully', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output/folder'; + const folderName = 'headers'; + const thirdPartyHeadersPath = + '/path/to/react-native/third-party/ReactNativeDependencies.xcframework/Headers'; + const headerFiles = [`${thirdPartyHeadersPath}/boost/boost.h`]; + + mockFs.existsSync.mockImplementation(filePath => { + if (filePath === thirdPartyHeadersPath) return true; + if (filePath.includes('third-party') && filePath.endsWith('.h')) + return true; + return false; + }); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => { + throw new Error('Link failed'); + }); + + // Execute and Assert - function should throw the error from setupSymlink + expect(() => { + symlinkThirdPartyDependenciesHeaders( + reactNativePath, + outputFolder, + folderName, + ); + }).toThrow('Link failed'); + }); + + it('should handle both .h and .hpp files', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output/folder'; + const folderName = 'headers'; + const thirdPartyHeadersPath = + '/path/to/react-native/third-party/ReactNativeDependencies.xcframework/Headers'; + const headerFiles = [ + `${thirdPartyHeadersPath}/library1/header.h`, + `${thirdPartyHeadersPath}/library2/header.hpp`, + ]; + + mockFs.existsSync.mockImplementation(filePath => { + if (filePath === thirdPartyHeadersPath) return true; + if ( + filePath.includes('third-party') && + (filePath.endsWith('.h') || filePath.endsWith('.hpp')) + ) + return true; + return false; + }); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkThirdPartyDependenciesHeaders( + reactNativePath, + outputFolder, + folderName, + ); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + thirdPartyHeadersPath, + ['tests'], + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + `${thirdPartyHeadersPath}/library1/header.h`, + '/output/folder/headers/library1/header.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + `${thirdPartyHeadersPath}/library2/header.hpp`, + '/output/folder/headers/library2/header.hpp', + ); + expect(result).toBe(2); + }); +}); + +describe('symlinkReactAppleHeaders', () => { + let mockUtils; + let mockPath; + let originalConsoleWarn; + let originalConsoleLog; + + beforeEach(() => { + // Setup mocks + mockUtils = require('../utils'); + mockPath = require('path'); + + // Mock path functions + mockPath.relative.mockImplementation((from, to) => { + return to.replace(from + '/', ''); + }); + mockPath.join.mockImplementation((...args) => args.join('/')); + mockPath.dirname.mockImplementation(filePath => { + const parts = filePath.split('/'); + parts.pop(); + return parts.join('/'); + }); + mockPath.basename.mockImplementation(filePath => { + return filePath.split('/').pop(); + }); + mockPath.sep = '/'; + + // Mock console methods to prevent test output noise + originalConsoleWarn = console.warn; + originalConsoleLog = console.log; + console.warn = jest.fn(); + console.log = jest.fn(); + + // Reset all mocks + jest.clearAllMocks(); + }); + + afterEach(() => { + // Restore console methods + console.warn = originalConsoleWarn; + console.log = originalConsoleLog; + }); + + it('should create symlinks for ReactApple headers with Exported folder structure', () => { + // Setup + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + const headerFiles = [ + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactAppleHeaders(reactApplePath, headersOutput); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported', + ['tests'], + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + '/output/headers/RCTDeprecation/RCTDeprecation.h', + ); + expect(result).toBe(1); + }); + + it('should handle empty header files from mapping', () => { + // Setup + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + + mockUtils.listHeadersInFolder.mockReturnValue([]); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactAppleHeaders(reactApplePath, headersOutput); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported', + ['tests'], + ); + expect(mockUtils.setupSymlink).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + + it('should handle multiple header files in the same mapping', () => { + // Setup + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + const headerFiles = [ + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTUtility.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactAppleHeaders(reactApplePath, headersOutput); + + // Assert + expect(mockUtils.setupSymlink).toHaveBeenCalledTimes(2); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + '/output/headers/RCTDeprecation/RCTDeprecation.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTUtility.h', + '/output/headers/RCTDeprecation/RCTUtility.h', + ); + expect(result).toBe(2); + }); + + it('should handle setupSymlink throwing an error', () => { + // Setup + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + const headerFiles = [ + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => { + throw new Error('Link failed'); + }); + + // Execute and Assert - function throws the error from setupSymlink + expect(() => { + symlinkReactAppleHeaders(reactApplePath, headersOutput); + }).toThrow('Link failed'); + }); + + it('should work correctly with the symcoded mapping structure', () => { + // Setup + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + const headerFiles = [ + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactAppleHeaders(reactApplePath, headersOutput); + + // Assert - setupSymlink handles removing existing files and creating new ones internally + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + '/output/headers/RCTDeprecation/RCTDeprecation.h', + ); + expect(result).toBe(1); + }); + + it('should handle listHeadersInFolder returning empty array', () => { + // Setup + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + + mockUtils.listHeadersInFolder.mockReturnValue([]); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactAppleHeaders(reactApplePath, headersOutput); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported', + ['tests'], + ); + expect(mockUtils.setupSymlink).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + + it('should handle listHeadersInFolder throwing an error', () => { + // Setup + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + const error = new Error('Command failed'); + + mockUtils.listHeadersInFolder.mockImplementation(() => { + throw error; + }); + + // Execute and Assert - function throws the error from listHeadersInFolder + expect(() => { + symlinkReactAppleHeaders(reactApplePath, headersOutput); + }).toThrow('Command failed'); + }); + + it('should work correctly with single symcoded mapping', () => { + // Setup - Test the specific mapping defined in the function + const reactApplePath = '/path/to/ReactApple'; + const headersOutput = '/output/headers'; + const headerFiles = [ + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + ]; + + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactAppleHeaders(reactApplePath, headersOutput); + + // Assert - Test the specific mapping that's symcoded + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported', + ['tests'], + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported/RCTDeprecation.h', + '/output/headers/RCTDeprecation/RCTDeprecation.h', + ); + expect(result).toBe(1); + }); +}); + +describe('symlinkReactCommonHeaders', () => { + let mockUtils; + let mockReactCommonMappings; + let mockPath; + let originalConsoleWarn; + let originalConsoleLog; + + beforeEach(() => { + // Setup mocks + mockUtils = require('../utils'); + mockReactCommonMappings = + require('../headers-mappings').reactCommonMappings; + mockPath = require('path'); + + // Mock path functions + mockPath.relative.mockImplementation((from, to) => { + return to.replace(from + '/', ''); + }); + mockPath.join.mockImplementation((...args) => args.join('/')); + mockPath.dirname.mockImplementation(filePath => { + const parts = filePath.split('/'); + parts.pop(); + return parts.join('/'); + }); + mockPath.basename.mockImplementation(filePath => { + return filePath.split('/').pop(); + }); + mockPath.sep = '/'; + + // Mock console methods to prevent test output noise + originalConsoleWarn = console.warn; + originalConsoleLog = console.log; + console.warn = jest.fn(); + console.log = jest.fn(); + + // Reset all mocks + jest.clearAllMocks(); + }); + + afterEach(() => { + // Restore console methods + console.warn = originalConsoleWarn; + console.log = originalConsoleLog; + }); + + it('should use reactCommonMappings and create symlinks for mapped directories', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = { + '/path/to/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon': { + destination: '/output/headers/ReactCommon', + excludeFolders: ['tests'], + preserveStructure: false, + }, + }; + const headerFiles = [ + '/path/to/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/TurboModule.h', + ]; + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactCommonHeaders(reactCommonPath, headersOutput); + + // Assert + expect(mockReactCommonMappings).toHaveBeenCalledWith( + reactCommonPath, + headersOutput, + ); + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith( + '/path/to/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon', + ['tests'], + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/TurboModule.h', + '/output/headers/ReactCommon/TurboModule.h', + ); + expect(result).toBe(1); + }); + + it('should handle multiple mappings with different configurations', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = { + '/path/to/ReactCommon/react/renderer/core': { + destination: '/output/headers/react/renderer/core', + excludeFolders: ['tests'], + preserveStructure: true, + }, + '/path/to/ReactCommon/turbomodule/core': { + destination: '/output/headers/ReactCommon', + excludeFolders: ['tests'], + preserveStructure: false, + }, + }; + const headerFiles1 = [ + '/path/to/ReactCommon/react/renderer/core/Component.h', + ]; + const headerFiles2 = [ + '/path/to/ReactCommon/turbomodule/core/TurboModule.h', + ]; + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.listHeadersInFolder + .mockReturnValueOnce(headerFiles1) + .mockReturnValueOnce(headerFiles2); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactCommonHeaders(reactCommonPath, headersOutput); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledTimes(2); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactCommon/react/renderer/core/Component.h', + '/output/headers/react/renderer/core/Component.h', + ); + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactCommon/turbomodule/core/TurboModule.h', + '/output/headers/ReactCommon/TurboModule.h', + ); + expect(result).toBe(2); + }); + + it('should handle empty mappings from reactCommonMappings', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = {}; // Empty mappings + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactCommonHeaders(reactCommonPath, headersOutput); + + // Assert + expect(mockReactCommonMappings).toHaveBeenCalledWith( + reactCommonPath, + headersOutput, + ); + expect(mockUtils.listHeadersInFolder).not.toHaveBeenCalled(); + expect(mockUtils.setupSymlink).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + + it('should handle mapping with preserveStructure true', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = { + '/path/to/ReactCommon/react/renderer/core': { + destination: '/output/headers/react/renderer/core', + excludeFolders: ['tests'], + preserveStructure: true, + }, + }; + const headerFiles = [ + '/path/to/ReactCommon/react/renderer/core/subdir/Component.h', + ]; + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactCommonHeaders(reactCommonPath, headersOutput); + + // Assert + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactCommon/react/renderer/core/subdir/Component.h', + '/output/headers/react/renderer/core/subdir/Component.h', + ); + expect(result).toBe(1); + }); + + it('should handle mapping with preserveStructure false (flattened)', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = { + '/path/to/ReactCommon/turbomodule/core': { + destination: '/output/headers/ReactCommon', + excludeFolders: ['tests'], + preserveStructure: false, + }, + }; + const headerFiles = [ + '/path/to/ReactCommon/turbomodule/core/subdir/TurboModule.h', + ]; + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactCommonHeaders(reactCommonPath, headersOutput); + + // Assert + expect(mockUtils.setupSymlink).toHaveBeenCalledWith( + '/path/to/ReactCommon/turbomodule/core/subdir/TurboModule.h', + '/output/headers/ReactCommon/TurboModule.h', + ); + expect(result).toBe(1); + }); + + it('should handle listHeadersInFolder returning empty arrays for all mappings', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = { + '/path/to/ReactCommon/react/renderer/core': { + destination: '/output/headers/react/renderer/core', + excludeFolders: ['tests'], + preserveStructure: true, + }, + '/path/to/ReactCommon/turbomodule/core': { + destination: '/output/headers/ReactCommon', + excludeFolders: ['tests'], + preserveStructure: false, + }, + }; + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.listHeadersInFolder.mockReturnValue([]); + mockUtils.setupSymlink.mockImplementation(() => {}); + + // Execute + const result = symlinkReactCommonHeaders(reactCommonPath, headersOutput); + + // Assert + expect(mockUtils.listHeadersInFolder).toHaveBeenCalledTimes(2); + expect(mockUtils.setupSymlink).not.toHaveBeenCalled(); + expect(result).toBe(0); + }); + + it('should handle setupSymlink throwing an error', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = { + '/path/to/ReactCommon/react/renderer/core': { + destination: '/output/headers/react/renderer/core', + excludeFolders: ['tests'], + preserveStructure: true, + }, + }; + const headerFiles = [ + '/path/to/ReactCommon/react/renderer/core/Component.h', + ]; + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.listHeadersInFolder.mockReturnValue(headerFiles); + mockUtils.setupSymlink.mockImplementation(() => { + throw new Error('Setup failed'); + }); + + // Execute and Assert - function should throw the error from setupSymlink + expect(() => { + symlinkReactCommonHeaders(reactCommonPath, headersOutput); + }).toThrow('Setup failed'); + }); + + it('should handle listHeadersInFolder throwing an error', () => { + // Setup + const reactCommonPath = '/path/to/ReactCommon'; + const headersOutput = '/output/headers'; + const mappings = { + '/path/to/ReactCommon/react/renderer/core': { + destination: '/output/headers/react/renderer/core', + excludeFolders: ['tests'], + preserveStructure: true, + }, + }; + + mockReactCommonMappings.mockReturnValue(mappings); + mockUtils.listHeadersInFolder.mockImplementation(() => { + throw new Error('List failed'); + }); + + // Execute and Assert - function should throw the error from listHeadersInFolder + expect(() => { + symlinkReactCommonHeaders(reactCommonPath, headersOutput); + }).toThrow('List failed'); + }); +}); diff --git a/packages/react-native/scripts/swiftpm/__tests__/prepare-app-dependencies-headers-test.js b/packages/react-native/scripts/swiftpm/__tests__/prepare-app-dependencies-headers-test.js new file mode 100644 index 000000000000..c90c85cf25be --- /dev/null +++ b/packages/react-native/scripts/swiftpm/__tests__/prepare-app-dependencies-headers-test.js @@ -0,0 +1,454 @@ +/** + * 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'; + +// Mock the headers-utils module before importing anything +jest.mock('../headers-utils', () => ({ + symlinkHeadersFromPath: jest.fn(), + symlinkReactAppleHeaders: jest.fn(), + symlinkReactCommonHeaders: jest.fn(), +})); + +// Mock headers-mappings module +jest.mock('../headers-mappings', () => ({ + reactMappings: jest.fn(), + librariesMappings: jest.fn(), +})); + +// Mock fs and path modules +jest.mock('fs'); +jest.mock('path'); + +const {librariesMappings, reactMappings} = require('../headers-mappings'); +const { + symlinkHeadersFromPath, + symlinkReactAppleHeaders, + symlinkReactCommonHeaders, +} = require('../headers-utils'); +const { + symlinkReactNativeHeaders, +} = require('../prepare-app-dependencies-headers'); +const fs = require('fs'); +const path = require('path'); + +describe('symlinkReactNativeHeaders', () => { + let originalConsoleLog; + + beforeEach(() => { + // Mock path.join to simply join with '/' + path.join.mockImplementation((...args) => args.join('/')); + + // Setup mock return values for the helper functions + symlinkHeadersFromPath.mockReturnValue(5); + symlinkReactAppleHeaders.mockReturnValue(3); + symlinkReactCommonHeaders.mockReturnValue(7); + + // Setup mock mappings + reactMappings.mockReturnValue({ + '/path/to/react-native/React': { + destination: '/output/headers/React', + preserveStructure: false, + excludeFolders: ['includes', 'headers', 'tests'], + }, + }); + + librariesMappings.mockReturnValue({ + '/path/to/react-native/Libraries': { + destination: '/output/headers/React', + preserveStructure: false, + excludeFolders: ['tests'], + }, + }); + + // Mock console.log to prevent test output noise + originalConsoleLog = console.log; + console.log = jest.fn(); + + // Reset all mocks + jest.clearAllMocks(); + }); + + afterEach(() => { + // Restore console.log + console.log = originalConsoleLog; + }); + + it('should create headers directory and call all processing functions with correct paths', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + const folderName = 'headers'; + + // Mock fs.existsSync to return true for all React Native subdirectories + fs.existsSync.mockImplementation(filePath => { + if (filePath === '/output/headers') return false; // headers dir doesn't exist + if (filePath === '/output/headers/React') return false; // React dir doesn't exist + if (filePath === '/path/to/react-native/ReactApple') return true; + if (filePath === '/path/to/react-native/ReactCommon') return true; + return false; + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder, folderName); + + // Assert - Verify directories are created + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/headers', { + recursive: true, + }); + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/headers/React', { + recursive: true, + }); + + // Assert - Verify mapping functions are called + expect(reactMappings).toHaveBeenCalledWith( + reactNativePath, + '/output/headers', + ); + expect(librariesMappings).toHaveBeenCalledWith( + reactNativePath, + '/output/headers', + ); + + // Assert - Verify symlinkHeadersFromPath is called for each mapping + expect(symlinkHeadersFromPath).toHaveBeenCalledWith( + '/path/to/react-native/React', + '/output/headers/React', + false, + ['includes', 'headers', 'tests'], + ); + expect(symlinkHeadersFromPath).toHaveBeenCalledWith( + '/path/to/react-native/Libraries', + '/output/headers/React', + false, + ['tests'], + ); + + // Assert - Verify ReactApple folder processing + expect(symlinkReactAppleHeaders).toHaveBeenCalledWith( + '/path/to/react-native/ReactApple', + '/output/headers', + ); + + // Assert - Verify ReactCommon folder processing + expect(symlinkReactCommonHeaders).toHaveBeenCalledWith( + '/path/to/react-native/ReactCommon', + '/output/headers', + ); + + // Assert - Verify logging + expect(console.log).toHaveBeenCalledWith( + 'Creating symlinks for React Native headers...', + ); + expect(console.log).toHaveBeenCalledWith( + 'Created 3 symlinks from ReactApple folder', + ); + expect(console.log).toHaveBeenCalledWith( + 'Created 7 symlinks from ReactCommon folder', + ); + expect(console.log).toHaveBeenCalledWith( + 'Created symlinks for 10 React Native headers total', + ); + }); + + it('should use default folderName "headers" when not provided', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + // Note: not providing folderName parameter + + fs.existsSync.mockReturnValue(false); // No directories exist + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Should use default "headers" folder name + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/headers', { + recursive: true, + }); + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/headers/React', { + recursive: true, + }); + }); + + it('should use custom folderName when provided', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + const customFolderName = 'custom-headers'; + + fs.existsSync.mockReturnValue(false); // No directories exist + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder, customFolderName); + + // Assert - Should use custom folder name + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/custom-headers', { + recursive: true, + }); + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/custom-headers/React', { + recursive: true, + }); + }); + + it('should skip processing folders that do not exist', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + // Mock fs.existsSync to return false for some React Native subdirectories + fs.existsSync.mockImplementation(filePath => { + if (filePath === '/output/headers') return false; + if (filePath === '/output/headers/React') return false; + if (filePath === '/path/to/react-native/ReactApple') return false; // ReactApple doesn't exist + if (filePath === '/path/to/react-native/ReactCommon') return true; // ReactCommon exists + return false; + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Should process mappings regardless (handled by mappings functions) + expect(symlinkHeadersFromPath).toHaveBeenCalledTimes(2); // React and Libraries mappings + + // Assert - Should not call ReactApple processing since it doesn't exist + expect(symlinkReactAppleHeaders).not.toHaveBeenCalled(); + // Assert - Should call ReactCommon processing since it exists + expect(symlinkReactCommonHeaders).toHaveBeenCalledTimes(1); + + // Assert - Should not log processing for non-existent ReactApple folder + expect(console.log).not.toHaveBeenCalledWith( + 'Processing ReactApple folder...', + ); + // Assert - Should log processing for existing ReactCommon folder + expect(console.log).toHaveBeenCalledWith( + 'Processing ReactCommon folder...', + ); + }); + + it('should not create directories that already exist', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + // Mock fs.existsSync to return true for directories (they already exist) + fs.existsSync.mockImplementation(filePath => { + if (filePath === '/output/headers') return true; // headers dir exists + if (filePath === '/output/headers/React') return true; // React dir exists + if (filePath === '/path/to/react-native/ReactApple') return true; + if (filePath === '/path/to/react-native/ReactCommon') return true; + return false; + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Should not create directories that already exist + expect(fs.mkdirSync).not.toHaveBeenCalledWith('/output/headers', { + recursive: true, + }); + expect(fs.mkdirSync).not.toHaveBeenCalledWith('/output/headers/React', { + recursive: true, + }); + }); + + it('should aggregate total linked count correctly from all functions', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + // Set up different return values for each function + symlinkReactAppleHeaders.mockReturnValue(2); + symlinkReactCommonHeaders.mockReturnValue(12); + + fs.existsSync.mockImplementation(filePath => { + return ( + filePath.includes('/path/to/react-native/') || + filePath === '/output/headers' || + filePath === '/output/headers/React' + ); + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Should aggregate counts from ReactApple and ReactCommon: 2 + 12 = 14 + expect(console.log).toHaveBeenCalledWith( + 'Created symlinks for 14 React Native headers total', + ); + }); + + it('should handle case where only some functions are called due to missing directories', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + // Only ReactCommon exists + fs.existsSync.mockImplementation(filePath => { + if (filePath === '/output/headers') return false; + if (filePath === '/output/headers/React') return false; + if (filePath === '/path/to/react-native/ReactCommon') return true; + return false; // All other paths don't exist + }); + fs.mkdirSync.mockImplementation(() => {}); + + symlinkReactCommonHeaders.mockReturnValue(4); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Should still call symlinkHeadersFromPath for mappings (React/Libraries) + expect(symlinkHeadersFromPath).toHaveBeenCalledTimes(2); + expect(symlinkReactAppleHeaders).not.toHaveBeenCalled(); + expect(symlinkReactCommonHeaders).toHaveBeenCalledTimes(1); + + // Assert - Should only show total from ReactCommon + expect(console.log).toHaveBeenCalledWith( + 'Created symlinks for 4 React Native headers total', + ); + }); + + it('should pass correct mappings for React and Libraries folders', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + fs.existsSync.mockImplementation(filePath => { + return ( + filePath.includes('/path/to/react-native/') || + filePath === '/output/headers' || + filePath === '/output/headers/React' + ); + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Check that mappings functions are called + expect(reactMappings).toHaveBeenCalledWith( + reactNativePath, + '/output/headers', + ); + expect(librariesMappings).toHaveBeenCalledWith( + reactNativePath, + '/output/headers', + ); + + // Assert - Check symlinkHeadersFromPath is called for each mapping + expect(symlinkHeadersFromPath).toHaveBeenCalledWith( + '/path/to/react-native/React', + '/output/headers/React', + false, + ['includes', 'headers', 'tests'], + ); + expect(symlinkHeadersFromPath).toHaveBeenCalledWith( + '/path/to/react-native/Libraries', + '/output/headers/React', + false, + ['tests'], + ); + }); + + it('should pass correct parameters to ReactCommon processing function', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + fs.existsSync.mockImplementation(filePath => { + return ( + filePath === '/path/to/react-native/ReactCommon' || + filePath.includes('/output/headers') + ); + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Check ReactCommon function is called with correct parameters (simplified) + expect(symlinkReactCommonHeaders).toHaveBeenCalledWith( + '/path/to/react-native/ReactCommon', + '/output/headers', + ); + }); + + it('should handle the function being called without any React Native subdirectories', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + // None of the React Native subdirectories exist + fs.existsSync.mockImplementation(filePath => { + if (filePath === '/output/headers') return false; + if (filePath === '/output/headers/React') return false; + return false; // No React Native subdirectories exist + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Should still create base directories + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/headers', { + recursive: true, + }); + expect(fs.mkdirSync).toHaveBeenCalledWith('/output/headers/React', { + recursive: true, + }); + + // Assert - Should still call symlinkHeadersFromPath for mappings + expect(symlinkHeadersFromPath).toHaveBeenCalledTimes(2); + // Assert - Should not call folder-specific processing functions + expect(symlinkReactAppleHeaders).not.toHaveBeenCalled(); + expect(symlinkReactCommonHeaders).not.toHaveBeenCalled(); + + // Assert - Should show zero total from folder processing + expect(console.log).toHaveBeenCalledWith( + 'Created symlinks for 0 React Native headers total', + ); + }); + + it('should properly log the orchestration process', () => { + // Setup + const reactNativePath = '/path/to/react-native'; + const outputFolder = '/output'; + + fs.existsSync.mockImplementation(filePath => { + return ( + filePath.includes('/path/to/react-native/') || + filePath === '/output/headers' || + filePath === '/output/headers/React' + ); + }); + fs.mkdirSync.mockImplementation(() => {}); + + // Execute + symlinkReactNativeHeaders(reactNativePath, outputFolder); + + // Assert - Check logging sequence + const logCalls = console.log.mock.calls.map(call => call[0]); + + expect(logCalls[0]).toBe('Creating symlinks for React Native headers...'); + expect(logCalls[1]).toBe('Processing ReactApple folder...'); + expect(logCalls[2]).toBe('Created 3 symlinks from ReactApple folder'); + expect(logCalls[3]).toBe('Processing ReactCommon folder...'); + expect(logCalls[4]).toBe('Created 7 symlinks from ReactCommon folder'); + expect(logCalls[5]).toBe( + 'Created symlinks for 10 React Native headers total', + ); + }); +}); diff --git a/packages/react-native/scripts/swiftpm/__tests__/utils-test.js b/packages/react-native/scripts/swiftpm/__tests__/utils-test.js new file mode 100644 index 000000000000..8677e2656c9a --- /dev/null +++ b/packages/react-native/scripts/swiftpm/__tests__/utils-test.js @@ -0,0 +1,241 @@ +/** + * 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 {listHeadersInFolder, setupSymlink} = require('../utils'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +describe('setupSymlink', () => { + let tempDir; + let sourceFile; + let destFile; + let destDir; + + beforeEach(() => { + // Create a temporary directory for testing + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'utils-test-')); + sourceFile = path.join(tempDir, 'source.txt'); + destDir = path.join(tempDir, 'dest', 'subdir'); + destFile = path.join(destDir, 'dest.txt'); + + // Create a source file + fs.writeFileSync(sourceFile, 'test content'); + }); + + afterEach(() => { + // Clean up temporary directory + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, {recursive: true, force: true}); + } + }); + + it('should create destination directory if it does not exist', () => { + expect(fs.existsSync(destDir)).toBe(false); + + setupSymlink(sourceFile, destFile); + + expect(fs.existsSync(destDir)).toBe(true); + }); + + it('should create symlink when source file exists', () => { + setupSymlink(sourceFile, destFile); + + expect(fs.existsSync(destFile)).toBe(true); + expect(fs.lstatSync(destFile).isSymbolicLink()).toBe(true); + expect(fs.readFileSync(destFile, 'utf8')).toBe('test content'); + }); + + it('should remove existing symlink before creating new one', () => { + // Create initial symlink + fs.mkdirSync(destDir, {recursive: true}); + fs.symlinkSync(sourceFile, destFile); + expect(fs.existsSync(destFile)).toBe(true); + + // Create another source file + const newSourceFile = path.join(tempDir, 'newsource.txt'); + fs.writeFileSync(newSourceFile, 'new content'); + + // Setup symlink should remove the old one and create new one + setupSymlink(newSourceFile, destFile); + + expect(fs.existsSync(destFile)).toBe(true); + expect(fs.lstatSync(destFile).isSymbolicLink()).toBe(true); + expect(fs.readFileSync(destFile, 'utf8')).toBe('new content'); + }); + + it('should remove existing regular file before creating symlink', () => { + // Create destination directory and regular file + fs.mkdirSync(destDir, {recursive: true}); + fs.writeFileSync(destFile, 'regular file content'); + expect(fs.existsSync(destFile)).toBe(true); + expect(fs.lstatSync(destFile).isSymbolicLink()).toBe(false); + + setupSymlink(sourceFile, destFile); + + expect(fs.existsSync(destFile)).toBe(true); + expect(fs.lstatSync(destFile).isSymbolicLink()).toBe(true); + expect(fs.readFileSync(destFile, 'utf8')).toBe('test content'); + }); + + it('should not create symlink when source file does not exist', () => { + const nonExistentSource = path.join(tempDir, 'nonexistent.txt'); + + setupSymlink(nonExistentSource, destFile); + + expect(fs.existsSync(destDir)).toBe(true); // Directory should still be created + expect(fs.existsSync(destFile)).toBe(false); // But no symlink should be created + }); + + it('should work when destination directory already exists', () => { + // Pre-create destination directory + fs.mkdirSync(destDir, {recursive: true}); + + setupSymlink(sourceFile, destFile); + + expect(fs.existsSync(destFile)).toBe(true); + expect(fs.lstatSync(destFile).isSymbolicLink()).toBe(true); + expect(fs.readFileSync(destFile, 'utf8')).toBe('test content'); + }); +}); + +describe('listHeadersInFolder', () => { + let tempDir; + + beforeEach(() => { + // Create a temporary directory for testing + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'headers-test-')); + }); + + afterEach(() => { + // Clean up temporary directory + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, {recursive: true, force: true}); + } + }); + + it('should find both .h and .hpp header files', () => { + // Create mixed header files + fs.writeFileSync(path.join(tempDir, 'test1.h'), '// header file 1'); + fs.writeFileSync(path.join(tempDir, 'test2.hpp'), '// header file 2'); + fs.writeFileSync(path.join(tempDir, 'test3.h'), '// header file 3'); + + const result = listHeadersInFolder(tempDir, []); + + expect(result).toHaveLength(3); + expect(result).toContain(path.join(tempDir, 'test1.h')); + expect(result).toContain(path.join(tempDir, 'test2.hpp')); + expect(result).toContain(path.join(tempDir, 'test3.h')); + }); + + it('should find header files in subdirectories', () => { + // Create subdirectories with header files + const subDir1 = path.join(tempDir, 'subdir1'); + const subDir2 = path.join(tempDir, 'subdir2'); + fs.mkdirSync(subDir1, {recursive: true}); + fs.mkdirSync(subDir2, {recursive: true}); + + fs.writeFileSync(path.join(tempDir, 'root.h'), '// root header'); + fs.writeFileSync(path.join(subDir1, 'sub1.h'), '// sub1 header'); + fs.writeFileSync(path.join(subDir2, 'sub2.hpp'), '// sub2 header'); + + const result = listHeadersInFolder(tempDir, []); + + expect(result).toHaveLength(3); + expect(result).toContain(path.join(tempDir, 'root.h')); + expect(result).toContain(path.join(subDir1, 'sub1.h')); + expect(result).toContain(path.join(subDir2, 'sub2.hpp')); + }); + + it('should exclude multiple specified subfolders', () => { + // Create subdirectories + const keepDir = path.join(tempDir, 'keep'); + const excludeDir1 = path.join(tempDir, 'exclude1'); + const excludeDir2 = path.join(tempDir, 'exclude2'); + fs.mkdirSync(keepDir, {recursive: true}); + fs.mkdirSync(excludeDir1, {recursive: true}); + fs.mkdirSync(excludeDir2, {recursive: true}); + + // Create header files + fs.writeFileSync(path.join(keepDir, 'keep.h'), '// keep header'); + fs.writeFileSync( + path.join(excludeDir1, 'exclude1.h'), + '// exclude1 header', + ); + fs.writeFileSync( + path.join(excludeDir2, 'exclude2.h'), + '// exclude2 header', + ); + + const result = listHeadersInFolder(tempDir, ['exclude1', 'exclude2']); + + expect(result).toHaveLength(1); + expect(result).toContain(path.join(keepDir, 'keep.h')); + expect(result).not.toContain(path.join(excludeDir1, 'exclude1.h')); + expect(result).not.toContain(path.join(excludeDir2, 'exclude2.h')); + }); + + it('should return empty array when no header files found', () => { + // Create non-header files + fs.writeFileSync(path.join(tempDir, 'test.txt'), 'text file'); + fs.writeFileSync(path.join(tempDir, 'test.js'), 'javascript file'); + + const result = listHeadersInFolder(tempDir, []); + + expect(result).toHaveLength(0); + }); + + it('should handle empty folder', () => { + const result = listHeadersInFolder(tempDir, []); + + expect(result).toHaveLength(0); + }); + + it('should handle nested exclusions correctly', () => { + // Create nested directory structure + const includeDir = path.join(tempDir, 'include'); + const excludeDir = path.join(tempDir, 'exclude'); + const nestedInclude = path.join(includeDir, 'nested'); + const nestedExclude = path.join(excludeDir, 'nested'); + + fs.mkdirSync(nestedInclude, {recursive: true}); + fs.mkdirSync(nestedExclude, {recursive: true}); + + // Create header files + fs.writeFileSync(path.join(includeDir, 'include.h'), '// include header'); + fs.writeFileSync( + path.join(nestedInclude, 'nested_include.h'), + '// nested include header', + ); + fs.writeFileSync(path.join(excludeDir, 'exclude.h'), '// exclude header'); + fs.writeFileSync( + path.join(nestedExclude, 'nested_exclude.h'), + '// nested exclude header', + ); + + const result = listHeadersInFolder(tempDir, ['exclude']); + + expect(result).toHaveLength(2); + expect(result).toContain(path.join(includeDir, 'include.h')); + expect(result).toContain(path.join(nestedInclude, 'nested_include.h')); + expect(result).not.toContain(path.join(excludeDir, 'exclude.h')); + expect(result).not.toContain(path.join(nestedExclude, 'nested_exclude.h')); + }); + + it('should throw error when folder does not exist', () => { + const nonExistentFolder = path.join(tempDir, 'nonexistent'); + + expect(() => { + listHeadersInFolder(nonExistentFolder, []); + }).toThrow(); + }); +}); diff --git a/packages/react-native/scripts/swiftpm/headers-mappings.js b/packages/react-native/scripts/swiftpm/headers-mappings.js new file mode 100644 index 000000000000..f33cecdf60ed --- /dev/null +++ b/packages/react-native/scripts/swiftpm/headers-mappings.js @@ -0,0 +1,149 @@ +/** + * 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 path = require('path'); + +/*:: +type MappingOption = { + destination: string; + excludeFolders: Array; + preserveStructure: boolean +} +*/ + +function reactCommonMappings( + reactCommonPath /*: string */, + headersOutput /*: string */, +) /*: { [string]: MappingOption } */ { + let mappings /*: { [string]: MappingOption } */ = {}; + mappings[`${reactCommonPath}/react`] = { + destination: path.join(headersOutput, 'react'), + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/react/renderer/components/view/platform/cxx`] = { + destination: path.join(headersOutput, 'React/renderer/components/view'), + excludeFolders: [], + preserveStructure: false, + }; + mappings[`${reactCommonPath}/react/renderer/graphics/platform/ios`] = { + destination: path.join(headersOutput, 'react/renderer/graphics'), + excludeFolders: [], + preserveStructure: false, + }; + mappings[`${reactCommonPath}/react/runtime/platform/ios/ReactCommon`] = { + destination: path.join(headersOutput, 'ReactCommon'), + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/react/nativemodule/core/platform/ios`] = { + destination: headersOutput, + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/react/nativemodule/samples/platform/ios`] = { + destination: headersOutput, + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/callinvoker`] = { + destination: headersOutput, + excludeFolders: ['tests'], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/cxxreact`] = { + destination: path.join(headersOutput, 'cxxreact'), + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/jserrorhandler`] = { + destination: path.join(headersOutput, 'jserrorhandler'), + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/jsinspector-modern`] = { + destination: path.join(headersOutput, 'jsinspector-modern'), + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/oscompat`] = { + destination: path.join(headersOutput, 'oscompat'), + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/runtimeexecutor`] = { + destination: headersOutput, + excludeFolders: [], + preserveStructure: true, + }; + mappings[`${reactCommonPath}/yoga/yoga`] = { + destination: path.join(headersOutput, 'yoga'), + excludeFolders: [], + preserveStructure: true, + }; + + return mappings; +} + +function reactMappings( + reactNativePath /*: string */, + headersOutput /*: string */, +) /*: { [string]: MappingOption } */ { + let mappings /*: { [string]: MappingOption } */ = {}; + mappings[`${reactNativePath}/React`] = { + destination: path.join(headersOutput, 'React'), + excludeFolders: ['includes', 'headers', 'tests'], + preserveStructure: false, + }; + + mappings[`${reactNativePath}/React/FBReactNativeSpec`] = { + destination: headersOutput, + excludeFolders: ['tests'], + preserveStructure: true, + }; + return mappings; +} + +function librariesMappings( + reactNativePath /*: string */, + headersOutput /*: string */, +) /*: { [string]: MappingOption } */ { + let mappings /*: { [string]: MappingOption } */ = {}; + mappings[`${reactNativePath}/Libraries`] = { + destination: path.join(headersOutput, 'React'), + excludeFolders: ['tests'], + preserveStructure: false, + }; + + mappings[`${reactNativePath}/Libraries/FBLazyVector`] = { + destination: path.join(headersOutput, 'FBLazyVector'), + excludeFolders: ['tests'], + preserveStructure: false, + }; + + mappings[`${reactNativePath}/Libraries/Required`] = { + destination: path.join(headersOutput, 'RCTRequired'), + excludeFolders: ['tests'], + preserveStructure: false, + }; + + mappings[`${reactNativePath}/Libraries/TypeSafety`] = { + destination: path.join(headersOutput, 'RCTTypeSafety'), + excludeFolders: ['tests'], + preserveStructure: false, + }; + return mappings; +} + +module.exports = { + librariesMappings, + reactCommonMappings, + reactMappings, +}; diff --git a/packages/react-native/scripts/swiftpm/headers-utils.js b/packages/react-native/scripts/swiftpm/headers-utils.js new file mode 100644 index 000000000000..33c35a4c4d43 --- /dev/null +++ b/packages/react-native/scripts/swiftpm/headers-utils.js @@ -0,0 +1,222 @@ +/** + * 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 {reactCommonMappings} = require('./headers-mappings'); +const {listHeadersInFolder, setupSymlink} = require('./utils'); +const fs = require('fs'); +const path = require('path'); + +/** + * Helper function to create symlinks from a source path + * @param {string} sourcePath - Source directory to search for headers + * @param {string} outputPath - Default output directory for symlinks + * @param {boolean} preserveStructure - Whether to preserve directory structure + * @param {Array} excludeFolders - Folder names to exclude + * @returns {number} Number of symlinks created + */ +function symlinkHeadersFromPath( + sourcePath /*: string */, + outputPath /*: string */, + preserveStructure /*: boolean */, + excludeFolders /*: Array */, +) /*: number */ { + let linkedCount = 0; + + try { + // Build find command with exclusions using -prune + const headerFiles = listHeadersInFolder(sourcePath, excludeFolders); + headerFiles.forEach(sourceHeaderPath => { + if (fs.existsSync(sourceHeaderPath)) { + const relativePath = path.relative(sourcePath, sourceHeaderPath); + let destPath = ''; + let mappedOutputPath = outputPath; + + if (preserveStructure) { + // Preserve directory structure + destPath = path.join(mappedOutputPath, relativePath); + } else { + // Flatten structure - just use the header filename + const headerName = path.basename(sourceHeaderPath); + destPath = path.join(mappedOutputPath, headerName); + } + + // Create destination directory if it doesn't exist + setupSymlink(sourceHeaderPath, destPath); + linkedCount++; + } + }); + } catch (error) { + console.warn( + `Failed to process headers from ${sourcePath}:`, + error.message, + ); + } + + return linkedCount; +} + +/** + * Create symlinks for ReactApple headers with special path logic. + * ReactApple has a custom structure, which is: + * + * ReactApple + * ├── Libraries + * │ └── RCTFoundation + * │ ├── RCTDeprecation + * │ │ ├── BUCK + * │ │ ├── Exported + * │ │ │ └── RCTDeprecation.h + * │ │ ├── RCTDeprecation.m + * │ │ ├── RCTDeprecation.podspec + * │ │ └── README.md + * │ └── README.md + * └── README.md + * + * We need to create symlinks for the headers in the "Exported" folder to + * the headersOutput/ folder. + * @param {string} reactApplePath - Path to ReactApple directory + * @param {string} headersOutput - Base headers output directory + * @returns {number} Number of symlinks created + */ +function symlinkReactAppleHeaders( + reactApplePath /*: string */, + headersOutput /*: string */, +) /*: number */ { + let linkedCount = 0; + + const mappings /*: {[string]: string } */ = {}; + mappings[ + `${reactApplePath}/Libraries/RCTFoundation/RCTDeprecation/Exported` + ] = `${headersOutput}/RCTDeprecation`; + + // Iterate over the key-value pairs of the mappings object + for (const [sourceDir, destDir] of Object.entries(mappings)) { + const headerFiles = listHeadersInFolder(sourceDir, ['tests']); + headerFiles.forEach(sourceHeaderPath => { + const destFilePath = path.join(destDir, path.basename(sourceHeaderPath)); + setupSymlink(sourceHeaderPath, destFilePath); + linkedCount++; + }); + } + + return linkedCount; +} + +/** + * Create symlinks for ReactCommon headers with conditional path logic + * The proper way to map ReactCommon headers is a bit complicated, because we it collects + * packages that needs linking in various ways: + * - Headers in the react/renderer subpath needs to be mapped to React folder + * - Platform specific code in + * react/renderer/components//platform/ios/react/renderer/components// + * needs to be flattened to react/renderer/components//. + * - Some headers related to TurboModules needs to be mapped to ReactCommon + * - Some modules in specific folders, such as oscompat, needs to be mapped to their parent folder. + * - Yoga structure which is ReactCommon/yoga/yoga/ need to be flattened to just yoga/. + * + * This function implement the basic logic of scanning the ReactCommon folder and proceed with the mapping + * by following the rules above. It can also be customized by passing an array of path that needs to be flattened + * and a map of special mapping objects. + * @param {string} reactCommonPath - Path to ReactCommon directory + * @param {string} headersOutput - Base headers output directory + * @returns {number} Number of symlinks created + */ +function symlinkReactCommonHeaders( + reactCommonPath /*: string */, + headersOutput /*: string */, +) /*: number */ { + let linkedCount = 0; + const mappings = reactCommonMappings(reactCommonPath, headersOutput); + + // Iterate over the key-value pairs of the mappings object + for (const [sourceDir, options] of Object.entries(mappings)) { + const headerFiles = listHeadersInFolder(sourceDir, options.excludeFolders); + headerFiles.forEach(sourceHeaderPath => { + const relativePath = path.relative(sourceDir, sourceHeaderPath); + let destPath = ''; + let mappedOutputPath = options.destination; + + if (options.preserveStructure) { + // Preserve directory structure + destPath = path.join(mappedOutputPath, relativePath); + } else { + // Flatten structure - just use the header filename + const headerName = path.basename(sourceHeaderPath); + destPath = path.join(mappedOutputPath, headerName); + } + + setupSymlink(sourceHeaderPath, destPath); + linkedCount++; + }); + } + + return linkedCount; +} + +/** + * Create symlinks for third-party dependencies headers in the output folder + * @param {string} reactNativePath - Path to the React Native directory + * @param {string} outputFolder - Path to the output folder + * @param {string} folderName - Name of the folder where headers will be created (default: 'headers') + * @returns {number} Number of symlinks created + */ +function symlinkThirdPartyDependenciesHeaders( + reactNativePath /*: string */, + outputFolder /*: string */, + folderName /*: string */ = 'headers', +) /*: number */ { + console.log('Creating symlinks for Third-Party Dependencies headers...'); + + // Look for ReactNativeDependencies.xcframework/Headers folder specifically + const thirdPartyHeadersPath = path.join( + reactNativePath, + 'third-party', + 'ReactNativeDependencies.xcframework', + 'Headers', + ); + + if (!fs.existsSync(thirdPartyHeadersPath)) { + console.warn( + `Third-party dependencies headers path does not exist: ${thirdPartyHeadersPath}`, + ); + return 0; + } + + const headersOutput = path.join(outputFolder, folderName); + let linkedCount = 0; + + // No custom mappings needed for third-party dependencies + const headerFiles = listHeadersInFolder(thirdPartyHeadersPath, ['tests']); + headerFiles.forEach(sourceHeaderPath => { + if (fs.existsSync(sourceHeaderPath)) { + // Calculate relative path from Headers base to preserve structure + const relativePath = path.relative( + thirdPartyHeadersPath, + sourceHeaderPath, + ); + const destPath = path.join(headersOutput, relativePath); + + setupSymlink(sourceHeaderPath, destPath); + linkedCount++; + } + }); + + console.log( + `Created symlinks for ${linkedCount} Third-Party Dependencies headers with preserved directory structure`, + ); + return linkedCount; +} + +module.exports = { + symlinkHeadersFromPath, + symlinkReactAppleHeaders, + symlinkReactCommonHeaders, + symlinkThirdPartyDependenciesHeaders, +}; diff --git a/packages/react-native/scripts/swiftpm/prepare-app-dependencies-headers.js b/packages/react-native/scripts/swiftpm/prepare-app-dependencies-headers.js new file mode 100644 index 000000000000..57158b8ce063 --- /dev/null +++ b/packages/react-native/scripts/swiftpm/prepare-app-dependencies-headers.js @@ -0,0 +1,103 @@ +/** + * 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 {librariesMappings, reactMappings} = require('./headers-mappings'); +const { + symlinkHeadersFromPath, + symlinkReactAppleHeaders, + symlinkReactCommonHeaders, +} = require('./headers-utils'); +const fs = require('fs'); +const path = require('path'); + +/** + * Create symlinks for React Native headers in the output folder. + * This function orchestrate the creation of all the headers required by React Native: + * 1. It explores the `react-native/React` folder and creates links in the `output/React` folder + * i. While exploring the `react-native/React` folder, it special maps the FBReactNativeSpec folder + * to `output/FBReactNativeSpec` folder + * 2. It explores the `react-native/Libraries` folder, creating links to the headers in the + * `output/React` folder + * i. While exploring the `react-native/Libraries` folder, it applies special mappings for: Required, + * TypeSafety, FBLazyVector + * 3. Then it calls the previously defined symlinkReactAppleHeaders to setup the ReactApple headers + * 4. Then it calls the previously defined symlinkReactCommonHeaders to setup the ReactCommon headers + * + * @param {string} reactNativePath - Path to the React Native directory + * @param {string} outputFolder - Path to the output folder + * @param {string} folderName - Name of the folder where headers will be created (default: 'headers') + */ +function symlinkReactNativeHeaders( + reactNativePath /*: string */, + outputFolder /*: string */, + folderName /*: string */ = 'headers', +) /*: void */ { + console.log('Creating symlinks for React Native headers...'); + + const headersOutput = path.join(outputFolder, folderName); + if (!fs.existsSync(headersOutput)) { + fs.mkdirSync(headersOutput, {recursive: true}); + } + + let totalLinkedCount = 0; + + // Create React subdirectory for React and Libraries headers + const reactHeadersOutput = path.join(headersOutput, 'React'); + if (!fs.existsSync(reactHeadersOutput)) { + fs.mkdirSync(reactHeadersOutput, {recursive: true}); + } + + const mappings = { + ...reactMappings(reactNativePath, headersOutput), + ...librariesMappings(reactNativePath, headersOutput), + }; + + // Iterate over the key-value pairs of the mappings object + for (const [sourceDir, options] of Object.entries(mappings)) { + symlinkHeadersFromPath( + sourceDir, + options.destination, + options.preserveStructure, + options.excludeFolders, + ); + } + + // Process ReactApple folder - special structure preservation + const reactApplePath = path.join(reactNativePath, 'ReactApple'); + if (fs.existsSync(reactApplePath)) { + console.log('Processing ReactApple folder...'); + const reactAppleCount = symlinkReactAppleHeaders( + reactApplePath, + headersOutput, + ); + totalLinkedCount += reactAppleCount; + console.log(`Created ${reactAppleCount} symlinks from ReactApple folder`); + } + + // Process ReactCommon folder - conditional structure preservation + const reactCommonPath = path.join(reactNativePath, 'ReactCommon'); + if (fs.existsSync(reactCommonPath)) { + console.log('Processing ReactCommon folder...'); + const reactCommonCount = symlinkReactCommonHeaders( + reactCommonPath, + headersOutput, + ); + totalLinkedCount += reactCommonCount; + console.log(`Created ${reactCommonCount} symlinks from ReactCommon folder`); + } + + console.log( + `Created symlinks for ${totalLinkedCount} React Native headers total`, + ); +} + +module.exports = { + symlinkReactNativeHeaders, +}; diff --git a/packages/react-native/scripts/swiftpm/utils.js b/packages/react-native/scripts/swiftpm/utils.js new file mode 100644 index 000000000000..c19443ed4a2c --- /dev/null +++ b/packages/react-native/scripts/swiftpm/utils.js @@ -0,0 +1,73 @@ +/** + * 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 fs = require('fs'); +const path = require('path'); + +function listHeadersInFolder( + folder /*: string */, + excludeSubfolders /*: Array */, +) /*: Array */ { + try { + // Build find command with exclusions using -prune + let findCommand = `find "${folder}"`; + + // Add exclusions for specified folders using -prune + if (excludeSubfolders.length > 0) { + const pruneConditions = excludeSubfolders + .map(subfolder => `-name "${subfolder}"`) + .join(' -o '); + findCommand += ` \\( ${pruneConditions} \\) -prune -o`; + } + + findCommand += ` \\( -name "*.h" -o -name "*.hpp" \\) -type f -print`; + + const result = execSync(findCommand, { + encoding: 'utf8', + stdio: 'pipe', + }); + + const headerFiles = result + .trim() + .split('\n') + .filter(p => p.length > 0); + + return headerFiles; + } catch (error) { + console.error(`Failed to process headers from ${folder}:`, error.message); + throw error; + } +} + +function setupSymlink( + sourceFilePath /*: string */, + destFilePath /*: string */, +) { + const destFolderPath = path.dirname(destFilePath); + if (!fs.existsSync(destFolderPath)) { + fs.mkdirSync(destFolderPath, {recursive: true}); + } + + // Remove existing symlink if it exists + if (fs.existsSync(destFilePath)) { + fs.unlinkSync(destFilePath); + } + + // Create symlink for umbrella header + if (fs.existsSync(sourceFilePath)) { + fs.symlinkSync(sourceFilePath, destFilePath); + } +} + +module.exports = { + setupSymlink, + listHeadersInFolder, +};