Add function to extract headers from a folder (#53737)

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

This change add a utility function to extract headers (both .h and .hpp) from a folder. It also allow to exclude some specific folders (e.g.: `tests`, non supported platforms, ...)

## Context
SwiftPM is very picky in how the header structure must be.
In order to preserve the import/include statements as much as possible when building from source, we can recreate the header structure in a temporary folder inside the react-native package using symlinks.

In this way, users can still modify the headers and build RNTester and HelloWorld using SwiftPM without breaking changes.

## Changelog:
[Internal] -

Reviewed By: cortinico

Differential Revision: D82205692

fbshipit-source-id: 4fa6dee2ae4790c583beb96a959b0c3045c7b50a
This commit is contained in:
Riccardo Cipolleschi
2025-09-22 10:14:58 -07:00
committed by Facebook GitHub Bot
parent c1827fcd04
commit 072b105f52
2 changed files with 170 additions and 1 deletions
+133 -1
View File
@@ -10,7 +10,7 @@
'use strict';
const {setupSymlink} = require('../utils');
const {listHeadersInFolder, setupSymlink} = require('../utils');
const fs = require('fs');
const os = require('os');
const path = require('path');
@@ -107,3 +107,135 @@ describe('setupSymlink', () => {
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();
});
});
+37
View File
@@ -8,9 +8,45 @@
* @format
*/
const {execSync} = require('child_process');
const fs = require('fs');
const path = require('path');
function listHeadersInFolder(
folder /*: string */,
excludeSubfolders /*: Array<string> */,
) /*: Array<string> */ {
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 */,
@@ -33,4 +69,5 @@ function setupSymlink(
module.exports = {
setupSymlink,
listHeadersInFolder,
};