Add function to configure the app for swift by creating modulemap and umbrella header (#53673)

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

## 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 configure React Native to be swift compatible by creating the React-umbrella and modulemap

## Changelog:
[Internal] -

Reviewed By: cortinico

Differential Revision: D81778437

fbshipit-source-id: 8bfd0df3abed813a221e5ef7972f4b7a6e292e76
This commit is contained in:
Riccardo Cipolleschi
2025-10-13 16:30:51 -07:00
committed by meta-codesync[bot]
parent c827474b58
commit 87d6043788
2 changed files with 364 additions and 0 deletions
@@ -11,6 +11,7 @@
'use strict';
const {
configureAppForSwift,
findXcodeProjectDirectory,
runIosPrebuild,
runPodDeintegrate,
@@ -19,6 +20,19 @@ const {
// Mock child_process module
jest.mock('child_process');
// Mock fs module
jest.mock('fs');
// Mock path module for absolute paths
jest.mock('path', () => {
const actualPath = jest.requireActual('path');
return {
...actualPath,
join: jest.fn((...args) => args.join('/')),
relative: jest.fn((from, to) => to.replace(from + '/', '')),
};
});
// Mock console methods - disable React Native's strict console checking
const originalConsole = global.console;
@@ -468,3 +482,272 @@ describe('runPodDeintegrate', () => {
);
});
});
describe('configureAppForSwift', () => {
let mockFs;
let mockPath;
let mockConsoleLog;
beforeEach(() => {
// Setup mocks
mockFs = require('fs');
mockPath = require('path');
mockConsoleLog = console.log;
// Clear and reset all mocks completely
jest.clearAllMocks();
jest.resetAllMocks();
// Set up fresh mock implementations
mockFs.existsSync = jest.fn();
mockFs.mkdirSync = jest.fn();
mockFs.unlinkSync = jest.fn();
mockFs.linkSync = jest.fn();
mockFs.symlinkSync = jest.fn();
mockFs.writeFileSync = jest.fn();
// Mock path.join to return realistic paths
mockPath.join.mockImplementation((...args) => args.join('/'));
// Mock path.relative to return realistic relative paths
mockPath.relative.mockImplementation((from, to) => {
// Simple implementation for tests
return to.replace(from + '/', '');
});
});
it('should configure app for Swift integration successfully', async () => {
// Setup
const reactNativePath = '/path/to/react-native';
// Mock file system calls
mockFs.existsSync
.mockReturnValueOnce(true) // reactIncludesReactPath exists
.mockReturnValueOnce(false) // destUmbrellaPath doesn't exist
.mockReturnValueOnce(true); // sourceUmbrellaPath exists
mockFs.mkdirSync.mockImplementation(() => {});
mockFs.symlinkSync.mockImplementation(() => {});
mockFs.writeFileSync.mockImplementation(() => {});
// Execute
await configureAppForSwift(reactNativePath);
// Assert file system calls
expect(mockFs.existsSync).toHaveBeenCalledWith(
'/path/to/react-native/React/includes/React',
);
expect(mockFs.existsSync).toHaveBeenCalledWith(
'/path/to/react-native/React/includes/React/React-umbrella.h',
);
expect(mockFs.existsSync).toHaveBeenCalledWith(
'/path/to/react-native/scripts/ios-prebuild/React-umbrella.h',
);
expect(mockFs.symlinkSync).toHaveBeenCalledWith(
'/path/to/react-native/scripts/ios-prebuild/React-umbrella.h',
'/path/to/react-native/React/includes/React/React-umbrella.h',
);
expect(mockFs.linkSync).not.toHaveBeenCalled();
// Verify module.modulemap content
const expectedModuleMapContent = `framework module React {
umbrella header "/path/to/react-native/React/includes/React/React-umbrella.h"
export *
module * { export * }
}
`;
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
'/path/to/react-native/React/includes/module.modulemap',
expectedModuleMapContent,
'utf8',
);
// Verify console output
expect(mockConsoleLog).toHaveBeenCalledWith(
'Configuring app for Swift integration...',
);
expect(mockConsoleLog).toHaveBeenCalledWith(
'✓ Created hardlink: React-umbrella.h -> scripts/ios-prebuild/React-umbrella.h',
);
expect(mockConsoleLog).toHaveBeenCalledWith(
'✓ Generated module.modulemap file',
);
expect(mockConsoleLog).toHaveBeenCalledWith(
'✓ App configured for Swift integration',
);
});
it('should remove existing hardlink before creating new one', async () => {
// Setup
const reactNativePath = '/path/to/react-native';
mockFs.existsSync
.mockReturnValueOnce(true) // reactIncludesReactPath exists
.mockReturnValueOnce(true) // destUmbrellaPath exists (should be removed)
.mockReturnValueOnce(true); // sourceUmbrellaPath exists
mockFs.mkdirSync.mockImplementation(() => {});
mockFs.unlinkSync.mockImplementation(() => {});
mockFs.linkSync.mockImplementation(() => {});
mockFs.writeFileSync.mockImplementation(() => {});
// Execute
await configureAppForSwift(reactNativePath);
// Assert
expect(mockFs.unlinkSync).toHaveBeenCalledWith(
'/path/to/react-native/React/includes/React/React-umbrella.h',
);
});
it('should handle different React Native paths', async () => {
// Setup
const reactNativePath = '/Users/developer/react-native';
mockFs.existsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true);
mockFs.linkSync.mockImplementation(() => {});
mockFs.writeFileSync.mockImplementation(() => {});
// Execute
await configureAppForSwift(reactNativePath);
// Assert
expect(mockFs.symlinkSync).toHaveBeenCalledWith(
'/Users/developer/react-native/scripts/ios-prebuild/React-umbrella.h',
'/Users/developer/react-native/React/includes/React/React-umbrella.h',
);
expect(mockFs.linkSync).not.toHaveBeenCalled();
});
it('should throw error when source umbrella header does not exist', async () => {
// Setup
const reactNativePath = '/path/to/react-native';
mockFs.existsSync
.mockReturnValueOnce(true) // reactIncludesReactPath exists
.mockReturnValueOnce(false) // destUmbrellaPath doesn't exist
.mockReturnValueOnce(false); // sourceUmbrellaPath doesn't exist
// Execute & Assert
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
'Swift configuration failed: Source umbrella header not found: /path/to/react-native/scripts/ios-prebuild/React-umbrella.h',
);
expect(mockConsoleLog).toHaveBeenCalledWith(
'Configuring app for Swift integration...',
);
expect(mockConsoleLog).not.toHaveBeenCalledWith(
'✓ App configured for Swift integration',
);
});
it('should throw error when hardlink creation fails', async () => {
// Setup
const reactNativePath = '/path/to/react-native';
mockFs.existsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true);
mockFs.symlinkSync.mockImplementation(() => {
throw new Error('Permission denied');
});
// Execute & Assert
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
'Swift configuration failed: Permission denied',
);
});
it('should throw error when module.modulemap write fails', async () => {
// Setup
const reactNativePath = '/path/to/react-native';
mockFs.existsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true);
mockFs.symlinkSync.mockImplementation(() => {});
mockFs.writeFileSync.mockImplementation(() => {
throw new Error('Disk full');
});
// Execute & Assert
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
'Swift configuration failed: Disk full',
);
});
it('should throw error when directory creation fails', async () => {
// Setup
const reactNativePath = '/path/to/react-native';
mockFs.existsSync.mockReturnValueOnce(false); // reactIncludesReactPath doesn't exist
mockFs.mkdirSync.mockImplementation(() => {
throw new Error('Permission denied');
});
// Execute & Assert
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
'Swift configuration failed: Permission denied',
);
});
it('should handle file unlink errors gracefully', async () => {
// Setup
const reactNativePath = '/path/to/react-native';
mockFs.existsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(true) // destUmbrellaPath exists
.mockReturnValueOnce(true);
mockFs.unlinkSync.mockImplementation(() => {
throw new Error('File in use');
});
// Execute & Assert
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
'Swift configuration failed: File in use',
);
});
it('should generate correct module.modulemap content with absolute path', async () => {
// Setup
const reactNativePath = '/custom/path/to/react-native';
mockFs.existsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true);
mockFs.linkSync.mockImplementation(() => {});
mockFs.writeFileSync.mockImplementation(() => {});
// Execute
await configureAppForSwift(reactNativePath);
// Assert module.modulemap content with correct absolute path
const expectedModuleMapContent = `framework module React {
umbrella header "/custom/path/to/react-native/React/includes/React/React-umbrella.h"
export *
module * { export * }
}
`;
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
'/custom/path/to/react-native/React/includes/module.modulemap',
expectedModuleMapContent,
'utf8',
);
});
});
@@ -9,6 +9,7 @@
*/
const {execSync} = require('child_process');
const fs = require('fs');
const path = require('path');
/**
@@ -85,8 +86,88 @@ async function runIosPrebuild(
throw new Error(`iOS prebuild failed: ${error.message}`);
}
}
/**
* Configure app for Swift integration
*/
async function configureAppForSwift(
reactNativePath /*: string */,
) /*: Promise<void> */ {
try {
console.log('Configuring app for Swift integration...');
// 1. Create hardlink from React-umbrella.h to React-umbrella.h
const sourceUmbrellaPath = path.join(
reactNativePath,
'scripts',
'ios-prebuild',
'React-umbrella.h',
);
const reactIncludesReactPath = path.join(
reactNativePath,
'React',
'includes',
'React',
);
const destUmbrellaPath = path.join(
reactIncludesReactPath,
'React-umbrella.h',
);
// Ensure the React/includes/React directory exists
if (!fs.existsSync(reactIncludesReactPath)) {
fs.mkdirSync(reactIncludesReactPath, {recursive: true});
}
// Remove existing hardlink if it exists
if (fs.existsSync(destUmbrellaPath)) {
fs.unlinkSync(destUmbrellaPath);
}
// Create hardlink for umbrella header
if (fs.existsSync(sourceUmbrellaPath)) {
fs.symlinkSync(sourceUmbrellaPath, destUmbrellaPath);
console.log(
`✓ Created hardlink: React-umbrella.h -> ${path.relative(
reactNativePath,
sourceUmbrellaPath,
)}`,
);
} else {
throw new Error(
`Source umbrella header not found: ${sourceUmbrellaPath}`,
);
}
// 2. Generate module.modulemap file
const reactIncludesPath = path.join(reactNativePath, 'React', 'includes');
const moduleMapPath = path.join(reactIncludesPath, 'module.modulemap');
const absoluteUmbrellaPath = path.join(
reactNativePath,
'React',
'includes',
'React',
'React-umbrella.h',
);
const moduleMapContent = `framework module React {
umbrella header "${absoluteUmbrellaPath}"
export *
module * { export * }
}
`;
fs.writeFileSync(moduleMapPath, moduleMapContent, 'utf8');
console.log('✓ Generated module.modulemap file');
console.log('✓ App configured for Swift integration');
} catch (error) {
throw new Error(`Swift configuration failed: ${error.message}`);
}
}
module.exports = {
findXcodeProjectDirectory,
runPodDeintegrate,
runIosPrebuild,
configureAppForSwift,
};