Skip to content

Commit 87d6043

Browse files
cipolleschimeta-codesync[bot]
authored andcommitted
Add function to configure the app for swift by creating modulemap and umbrella header (#53673)
Summary: Pull Request resolved: #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
1 parent c827474 commit 87d6043

2 files changed

Lines changed: 364 additions & 0 deletions

File tree

packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'use strict';
1212

1313
const {
14+
configureAppForSwift,
1415
findXcodeProjectDirectory,
1516
runIosPrebuild,
1617
runPodDeintegrate,
@@ -19,6 +20,19 @@ const {
1920
// Mock child_process module
2021
jest.mock('child_process');
2122

23+
// Mock fs module
24+
jest.mock('fs');
25+
26+
// Mock path module for absolute paths
27+
jest.mock('path', () => {
28+
const actualPath = jest.requireActual('path');
29+
return {
30+
...actualPath,
31+
join: jest.fn((...args) => args.join('/')),
32+
relative: jest.fn((from, to) => to.replace(from + '/', '')),
33+
};
34+
});
35+
2236
// Mock console methods - disable React Native's strict console checking
2337
const originalConsole = global.console;
2438

@@ -468,3 +482,272 @@ describe('runPodDeintegrate', () => {
468482
);
469483
});
470484
});
485+
486+
describe('configureAppForSwift', () => {
487+
let mockFs;
488+
let mockPath;
489+
let mockConsoleLog;
490+
491+
beforeEach(() => {
492+
// Setup mocks
493+
mockFs = require('fs');
494+
mockPath = require('path');
495+
mockConsoleLog = console.log;
496+
497+
// Clear and reset all mocks completely
498+
jest.clearAllMocks();
499+
jest.resetAllMocks();
500+
501+
// Set up fresh mock implementations
502+
mockFs.existsSync = jest.fn();
503+
mockFs.mkdirSync = jest.fn();
504+
mockFs.unlinkSync = jest.fn();
505+
mockFs.linkSync = jest.fn();
506+
mockFs.symlinkSync = jest.fn();
507+
mockFs.writeFileSync = jest.fn();
508+
509+
// Mock path.join to return realistic paths
510+
mockPath.join.mockImplementation((...args) => args.join('/'));
511+
512+
// Mock path.relative to return realistic relative paths
513+
mockPath.relative.mockImplementation((from, to) => {
514+
// Simple implementation for tests
515+
return to.replace(from + '/', '');
516+
});
517+
});
518+
519+
it('should configure app for Swift integration successfully', async () => {
520+
// Setup
521+
const reactNativePath = '/path/to/react-native';
522+
523+
// Mock file system calls
524+
mockFs.existsSync
525+
.mockReturnValueOnce(true) // reactIncludesReactPath exists
526+
.mockReturnValueOnce(false) // destUmbrellaPath doesn't exist
527+
.mockReturnValueOnce(true); // sourceUmbrellaPath exists
528+
529+
mockFs.mkdirSync.mockImplementation(() => {});
530+
mockFs.symlinkSync.mockImplementation(() => {});
531+
mockFs.writeFileSync.mockImplementation(() => {});
532+
533+
// Execute
534+
await configureAppForSwift(reactNativePath);
535+
536+
// Assert file system calls
537+
expect(mockFs.existsSync).toHaveBeenCalledWith(
538+
'/path/to/react-native/React/includes/React',
539+
);
540+
expect(mockFs.existsSync).toHaveBeenCalledWith(
541+
'/path/to/react-native/React/includes/React/React-umbrella.h',
542+
);
543+
expect(mockFs.existsSync).toHaveBeenCalledWith(
544+
'/path/to/react-native/scripts/ios-prebuild/React-umbrella.h',
545+
);
546+
547+
expect(mockFs.symlinkSync).toHaveBeenCalledWith(
548+
'/path/to/react-native/scripts/ios-prebuild/React-umbrella.h',
549+
'/path/to/react-native/React/includes/React/React-umbrella.h',
550+
);
551+
552+
expect(mockFs.linkSync).not.toHaveBeenCalled();
553+
554+
// Verify module.modulemap content
555+
const expectedModuleMapContent = `framework module React {
556+
umbrella header "/path/to/react-native/React/includes/React/React-umbrella.h"
557+
export *
558+
module * { export * }
559+
}
560+
`;
561+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
562+
'/path/to/react-native/React/includes/module.modulemap',
563+
expectedModuleMapContent,
564+
'utf8',
565+
);
566+
567+
// Verify console output
568+
expect(mockConsoleLog).toHaveBeenCalledWith(
569+
'Configuring app for Swift integration...',
570+
);
571+
expect(mockConsoleLog).toHaveBeenCalledWith(
572+
'✓ Created hardlink: React-umbrella.h -> scripts/ios-prebuild/React-umbrella.h',
573+
);
574+
expect(mockConsoleLog).toHaveBeenCalledWith(
575+
'✓ Generated module.modulemap file',
576+
);
577+
expect(mockConsoleLog).toHaveBeenCalledWith(
578+
'✓ App configured for Swift integration',
579+
);
580+
});
581+
582+
it('should remove existing hardlink before creating new one', async () => {
583+
// Setup
584+
const reactNativePath = '/path/to/react-native';
585+
586+
mockFs.existsSync
587+
.mockReturnValueOnce(true) // reactIncludesReactPath exists
588+
.mockReturnValueOnce(true) // destUmbrellaPath exists (should be removed)
589+
.mockReturnValueOnce(true); // sourceUmbrellaPath exists
590+
591+
mockFs.mkdirSync.mockImplementation(() => {});
592+
mockFs.unlinkSync.mockImplementation(() => {});
593+
mockFs.linkSync.mockImplementation(() => {});
594+
mockFs.writeFileSync.mockImplementation(() => {});
595+
596+
// Execute
597+
await configureAppForSwift(reactNativePath);
598+
599+
// Assert
600+
expect(mockFs.unlinkSync).toHaveBeenCalledWith(
601+
'/path/to/react-native/React/includes/React/React-umbrella.h',
602+
);
603+
});
604+
605+
it('should handle different React Native paths', async () => {
606+
// Setup
607+
const reactNativePath = '/Users/developer/react-native';
608+
609+
mockFs.existsSync
610+
.mockReturnValueOnce(true)
611+
.mockReturnValueOnce(false)
612+
.mockReturnValueOnce(true);
613+
614+
mockFs.linkSync.mockImplementation(() => {});
615+
mockFs.writeFileSync.mockImplementation(() => {});
616+
617+
// Execute
618+
await configureAppForSwift(reactNativePath);
619+
620+
// Assert
621+
expect(mockFs.symlinkSync).toHaveBeenCalledWith(
622+
'/Users/developer/react-native/scripts/ios-prebuild/React-umbrella.h',
623+
'/Users/developer/react-native/React/includes/React/React-umbrella.h',
624+
);
625+
626+
expect(mockFs.linkSync).not.toHaveBeenCalled();
627+
});
628+
629+
it('should throw error when source umbrella header does not exist', async () => {
630+
// Setup
631+
const reactNativePath = '/path/to/react-native';
632+
633+
mockFs.existsSync
634+
.mockReturnValueOnce(true) // reactIncludesReactPath exists
635+
.mockReturnValueOnce(false) // destUmbrellaPath doesn't exist
636+
.mockReturnValueOnce(false); // sourceUmbrellaPath doesn't exist
637+
638+
// Execute & Assert
639+
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
640+
'Swift configuration failed: Source umbrella header not found: /path/to/react-native/scripts/ios-prebuild/React-umbrella.h',
641+
);
642+
643+
expect(mockConsoleLog).toHaveBeenCalledWith(
644+
'Configuring app for Swift integration...',
645+
);
646+
expect(mockConsoleLog).not.toHaveBeenCalledWith(
647+
'✓ App configured for Swift integration',
648+
);
649+
});
650+
651+
it('should throw error when hardlink creation fails', async () => {
652+
// Setup
653+
const reactNativePath = '/path/to/react-native';
654+
655+
mockFs.existsSync
656+
.mockReturnValueOnce(true)
657+
.mockReturnValueOnce(false)
658+
.mockReturnValueOnce(true);
659+
660+
mockFs.symlinkSync.mockImplementation(() => {
661+
throw new Error('Permission denied');
662+
});
663+
664+
// Execute & Assert
665+
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
666+
'Swift configuration failed: Permission denied',
667+
);
668+
});
669+
670+
it('should throw error when module.modulemap write fails', async () => {
671+
// Setup
672+
const reactNativePath = '/path/to/react-native';
673+
674+
mockFs.existsSync
675+
.mockReturnValueOnce(true)
676+
.mockReturnValueOnce(false)
677+
.mockReturnValueOnce(true);
678+
679+
mockFs.symlinkSync.mockImplementation(() => {});
680+
mockFs.writeFileSync.mockImplementation(() => {
681+
throw new Error('Disk full');
682+
});
683+
684+
// Execute & Assert
685+
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
686+
'Swift configuration failed: Disk full',
687+
);
688+
});
689+
690+
it('should throw error when directory creation fails', async () => {
691+
// Setup
692+
const reactNativePath = '/path/to/react-native';
693+
694+
mockFs.existsSync.mockReturnValueOnce(false); // reactIncludesReactPath doesn't exist
695+
696+
mockFs.mkdirSync.mockImplementation(() => {
697+
throw new Error('Permission denied');
698+
});
699+
700+
// Execute & Assert
701+
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
702+
'Swift configuration failed: Permission denied',
703+
);
704+
});
705+
706+
it('should handle file unlink errors gracefully', async () => {
707+
// Setup
708+
const reactNativePath = '/path/to/react-native';
709+
710+
mockFs.existsSync
711+
.mockReturnValueOnce(true)
712+
.mockReturnValueOnce(true) // destUmbrellaPath exists
713+
.mockReturnValueOnce(true);
714+
715+
mockFs.unlinkSync.mockImplementation(() => {
716+
throw new Error('File in use');
717+
});
718+
719+
// Execute & Assert
720+
await expect(configureAppForSwift(reactNativePath)).rejects.toThrow(
721+
'Swift configuration failed: File in use',
722+
);
723+
});
724+
725+
it('should generate correct module.modulemap content with absolute path', async () => {
726+
// Setup
727+
const reactNativePath = '/custom/path/to/react-native';
728+
729+
mockFs.existsSync
730+
.mockReturnValueOnce(true)
731+
.mockReturnValueOnce(false)
732+
.mockReturnValueOnce(true);
733+
734+
mockFs.linkSync.mockImplementation(() => {});
735+
mockFs.writeFileSync.mockImplementation(() => {});
736+
737+
// Execute
738+
await configureAppForSwift(reactNativePath);
739+
740+
// Assert module.modulemap content with correct absolute path
741+
const expectedModuleMapContent = `framework module React {
742+
umbrella header "/custom/path/to/react-native/React/includes/React/React-umbrella.h"
743+
export *
744+
module * { export * }
745+
}
746+
`;
747+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
748+
'/custom/path/to/react-native/React/includes/module.modulemap',
749+
expectedModuleMapContent,
750+
'utf8',
751+
);
752+
});
753+
});

packages/react-native/scripts/swiftpm/prepare-app-utils.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
const {execSync} = require('child_process');
12+
const fs = require('fs');
1213
const path = require('path');
1314

1415
/**
@@ -85,8 +86,88 @@ async function runIosPrebuild(
8586
throw new Error(`iOS prebuild failed: ${error.message}`);
8687
}
8788
}
89+
90+
/**
91+
* Configure app for Swift integration
92+
*/
93+
async function configureAppForSwift(
94+
reactNativePath /*: string */,
95+
) /*: Promise<void> */ {
96+
try {
97+
console.log('Configuring app for Swift integration...');
98+
99+
// 1. Create hardlink from React-umbrella.h to React-umbrella.h
100+
const sourceUmbrellaPath = path.join(
101+
reactNativePath,
102+
'scripts',
103+
'ios-prebuild',
104+
'React-umbrella.h',
105+
);
106+
const reactIncludesReactPath = path.join(
107+
reactNativePath,
108+
'React',
109+
'includes',
110+
'React',
111+
);
112+
const destUmbrellaPath = path.join(
113+
reactIncludesReactPath,
114+
'React-umbrella.h',
115+
);
116+
117+
// Ensure the React/includes/React directory exists
118+
if (!fs.existsSync(reactIncludesReactPath)) {
119+
fs.mkdirSync(reactIncludesReactPath, {recursive: true});
120+
}
121+
122+
// Remove existing hardlink if it exists
123+
if (fs.existsSync(destUmbrellaPath)) {
124+
fs.unlinkSync(destUmbrellaPath);
125+
}
126+
127+
// Create hardlink for umbrella header
128+
if (fs.existsSync(sourceUmbrellaPath)) {
129+
fs.symlinkSync(sourceUmbrellaPath, destUmbrellaPath);
130+
console.log(
131+
`✓ Created hardlink: React-umbrella.h -> ${path.relative(
132+
reactNativePath,
133+
sourceUmbrellaPath,
134+
)}`,
135+
);
136+
} else {
137+
throw new Error(
138+
`Source umbrella header not found: ${sourceUmbrellaPath}`,
139+
);
140+
}
141+
142+
// 2. Generate module.modulemap file
143+
const reactIncludesPath = path.join(reactNativePath, 'React', 'includes');
144+
const moduleMapPath = path.join(reactIncludesPath, 'module.modulemap');
145+
const absoluteUmbrellaPath = path.join(
146+
reactNativePath,
147+
'React',
148+
'includes',
149+
'React',
150+
'React-umbrella.h',
151+
);
152+
const moduleMapContent = `framework module React {
153+
umbrella header "${absoluteUmbrellaPath}"
154+
export *
155+
module * { export * }
156+
}
157+
`;
158+
159+
fs.writeFileSync(moduleMapPath, moduleMapContent, 'utf8');
160+
console.log('✓ Generated module.modulemap file');
161+
162+
console.log('✓ App configured for Swift integration');
163+
} catch (error) {
164+
throw new Error(`Swift configuration failed: ${error.message}`);
165+
}
166+
}
167+
88168
module.exports = {
89169
findXcodeProjectDirectory,
90170
runPodDeintegrate,
91171
runIosPrebuild,
172+
configureAppForSwift,
92173
};

0 commit comments

Comments
 (0)