react-native
Version:
A framework for building native apps using React
224 lines (193 loc) • 7.02 kB
JavaScript
/**
* 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
*/
;
const {TEMPLATES_FOLDER_PATH} = require('./constants');
const {
codegenLog,
isReactNativeCoreLibrary,
parseiOSAnnotations,
} = require('./utils');
const fs = require('fs');
const path = require('path');
const THIRD_PARTY_COMPONENTS_H_TEMPLATE_PATH = path.join(
TEMPLATES_FOLDER_PATH,
'RCTThirdPartyComponentsProviderH.template',
);
const THIRD_PARTY_COMPONENTS_MM_TEMPLATE_PATH = path.join(
TEMPLATES_FOLDER_PATH,
'RCTThirdPartyComponentsProviderMM.template',
);
function generateRCTThirdPartyComponents(libraries, outputDir) {
fs.mkdirSync(outputDir, {recursive: true});
// Generate Header File
codegenLog('Generating RCTThirdPartyComponentsProvider.h');
const templateH = fs.readFileSync(
THIRD_PARTY_COMPONENTS_H_TEMPLATE_PATH,
'utf8',
);
const finalPathH = path.join(outputDir, 'RCTThirdPartyComponentsProvider.h');
fs.writeFileSync(finalPathH, templateH);
codegenLog(`Generated artifact: ${finalPathH}`);
codegenLog('Generating RCTThirdPartyComponentsProvider.mm');
let componentsInLibraries = {};
const componentLibraries = libraries.filter(({config, libraryPath}) => {
if (isReactNativeCoreLibrary(config.name) || config.type === 'modules') {
return false;
}
return true;
});
const librariesToCrawl = {};
// Using new API explicitly or not using any config field to define components.
const componentLibrariesUsingNewApi = [];
const componentLibrariesUsingOldApi = [];
for (const library of componentLibraries) {
if (
library.config.ios?.components ||
!library.config.ios?.componentProvider
) {
componentLibrariesUsingNewApi.push(library);
} else {
componentLibrariesUsingOldApi.push(library);
}
}
// Old API
componentLibrariesUsingOldApi.forEach(library => {
const {config, libraryPath} = library;
const libraryName = JSON.parse(
fs.readFileSync(path.join(libraryPath, 'package.json')),
).name;
librariesToCrawl[libraryName] = library;
const componentsProvider = config.ios?.componentProvider;
delete librariesToCrawl[libraryName];
componentsInLibraries[libraryName] =
componentsInLibraries[libraryName] || [];
Object.keys(componentsProvider).forEach(componentName => {
componentsInLibraries[libraryName].push({
componentName,
className: componentsProvider[componentName],
});
});
});
// New API
const iosAnnotations = parseiOSAnnotations(componentLibrariesUsingNewApi);
for (const [libraryName, annotationMap] of Object.entries(iosAnnotations)) {
const {library, components} = annotationMap;
librariesToCrawl[libraryName] = library;
for (const [componentName, annotation] of Object.entries(components)) {
if (annotation.className) {
delete librariesToCrawl[libraryName];
componentsInLibraries[libraryName] =
componentsInLibraries[libraryName] || [];
componentsInLibraries[libraryName].push({
componentName,
className: annotation.className,
});
}
}
}
Object.entries(librariesToCrawl).forEach(([libraryName, library]) => {
const {libraryPath} = library;
codegenLog(`Crawling ${libraryName} library for components`);
// crawl all files and subdirectories for file with the ".mm" extension
const files = findFilesWithExtension(libraryPath, '.mm');
const componentsMapping = files
.flatMap(file => findRCTComponentViewProtocolClass(file))
.filter(Boolean);
if (componentsMapping.length !== 0) {
codegenLog(
`[DEPRECATED] ${libraryName} should add the 'ios.componentProvider' property in their codegenConfig`,
true,
);
}
componentsInLibraries[libraryName] = componentsMapping;
});
const thirdPartyComponentsMapping = Object.keys(componentsInLibraries)
.flatMap(library => {
const components = componentsInLibraries[library];
return components.map(({componentName, className}) => {
return `\t\t@"${componentName}": NSClassFromString(@"${className}"), // ${library}`;
});
})
.join('\n');
// Generate implementation file
const templateMM = fs
.readFileSync(THIRD_PARTY_COMPONENTS_MM_TEMPLATE_PATH, 'utf8')
.replace(/{thirdPartyComponentsMapping}/, thirdPartyComponentsMapping);
const finalPathMM = path.join(
outputDir,
'RCTThirdPartyComponentsProvider.mm',
);
fs.writeFileSync(finalPathMM, templateMM);
codegenLog(`Generated artifact: ${finalPathMM}`);
}
// Given a path, return the paths of all the files with extension .mm in
// the path dir and all its subdirectories.
function findFilesWithExtension(filePath, extension) {
const files = [];
const dir = fs.readdirSync(filePath);
dir.forEach(file => {
const absolutePath = path.join(filePath, file);
// Exclude files provided by react-native
if (absolutePath.includes(`${path.sep}react-native${path.sep}`)) {
return null;
}
// Skip hidden folders, that starts with `.` but allow `.pnpm`
if (
absolutePath.includes(`${path.sep}.`) &&
!absolutePath.includes(`${path.sep}.pnpm`)
) {
return null;
}
if (
fs.existsSync(absolutePath) &&
fs.statSync(absolutePath).isDirectory()
) {
files.push(...findFilesWithExtension(absolutePath, extension));
} else if (file.endsWith(extension)) {
files.push(absolutePath);
}
});
return files;
}
// Given a filepath, read the file and look for a string that starts with 'Class<RCTComponentViewProtocol> '
// and ends with 'Cls(void)'. Return the string between the two.
function findRCTComponentViewProtocolClass(filepath) {
const fileContent = fs.readFileSync(filepath, 'utf8');
const regex = /Class<RCTComponentViewProtocol> (.*)Cls\(/;
const match = fileContent.match(regex);
if (match) {
const componentName = match[1];
// split the file by \n
// remove all the lines before the one that matches the regex above
// find the first return statement after that that ends with .class
// return what's between return and `.class`
const lines = fileContent.split('\n');
const signatureIndex = lines.findIndex(line => regex.test(line));
const returnRegex = /return (.*)\.class/;
const classNameMatch = String(lines.slice(signatureIndex).join('\n')).match(
returnRegex,
);
if (classNameMatch) {
const className = classNameMatch[1];
codegenLog(`Match found ${componentName} -> ${className}`);
return {
componentName,
className,
};
}
console.warn(
`Could not find class name for component ${componentName}. Register it manually`,
);
return null;
}
return null;
}
module.exports = {
generateRCTThirdPartyComponents,
};