mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
meta-codesync[bot]
parent
c827474b58
commit
87d6043788
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user