mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
71692889b0
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/33891 This diff adds a function to clean up folders and empty files (if any) after the codegen runs in the OSS. To align how iOS and Android behave in the codegen, we now have to preemptively create folders that we may not need later. Thanks to this function, we recursively the folders in the codegen and we delete them if they are empty. ## Changelog [iOS][Added] - Add function to cleanup codegen folders Reviewed By: cortinico Differential Revision: D36594999 fbshipit-source-id: 904aa2442c0d9621b7ec77c31f7be9daa4e7b7e1
482 lines
13 KiB
JavaScript
482 lines
13 KiB
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
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* This script crawls through a React Native application's dependencies and invokes the codegen
|
|
* for any libraries that require it.
|
|
* To enable codegen support, the library should include a config in the codegenConfigKey key
|
|
* in a codegenConfigFilename file.
|
|
*/
|
|
|
|
const {execSync} = require('child_process');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const RN_ROOT = path.join(__dirname, '../..');
|
|
|
|
const CODEGEN_REPO_PATH = `${RN_ROOT}/packages/react-native-codegen`;
|
|
const CODEGEN_NPM_PATH = `${RN_ROOT}/../react-native-codegen`;
|
|
const CORE_LIBRARIES = new Set(['rncore', 'FBReactNativeSpec']);
|
|
const REACT_NATIVE_DEPENDENCY_NAME = 'react-native';
|
|
|
|
// HELPERS
|
|
|
|
function isReactNativeCoreLibrary(libraryName) {
|
|
return CORE_LIBRARIES.has(libraryName);
|
|
}
|
|
|
|
function executeNodeScript(node, script) {
|
|
execSync(`${node} ${script}`);
|
|
}
|
|
|
|
function isAppRootValid(appRootDir) {
|
|
if (appRootDir == null) {
|
|
console.error('Missing path to React Native application');
|
|
process.exitCode = 1;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function readPackageJSON(appRootDir) {
|
|
return JSON.parse(fs.readFileSync(path.join(appRootDir, 'package.json')));
|
|
}
|
|
|
|
// Reading Libraries
|
|
function extractLibrariesFromConfigurationArray(
|
|
configFile,
|
|
codegenConfigKey,
|
|
libraries,
|
|
dependency,
|
|
dependencyPath,
|
|
) {
|
|
console.log(`[Codegen] Found ${dependency}`);
|
|
configFile[codegenConfigKey].libraries.forEach(config => {
|
|
const libraryConfig = {
|
|
library: dependency,
|
|
config,
|
|
libraryPath: dependencyPath,
|
|
};
|
|
libraries.push(libraryConfig);
|
|
});
|
|
}
|
|
|
|
function extractLibrariesFromJSON(
|
|
configFile,
|
|
libraries,
|
|
codegenConfigKey,
|
|
dependency,
|
|
dependencyPath,
|
|
) {
|
|
var isBlocking = false;
|
|
if (dependency == null) {
|
|
dependency = REACT_NATIVE_DEPENDENCY_NAME;
|
|
dependencyPath = RN_ROOT;
|
|
// If we are exploring the ReactNative libraries, we want to raise an error
|
|
// if the codegen is not properly configured.
|
|
isBlocking = true;
|
|
}
|
|
|
|
if (configFile[codegenConfigKey] == null) {
|
|
if (isBlocking) {
|
|
throw `[Codegen] Error: Could not find codegen config for ${dependency} .`;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (configFile[codegenConfigKey].libraries == null) {
|
|
console.log(`[Codegen] Found ${dependency}`);
|
|
var config = configFile[codegenConfigKey];
|
|
libraries.push({
|
|
library: dependency,
|
|
config,
|
|
libraryPath: dependencyPath,
|
|
});
|
|
} else {
|
|
console.log(`[Codegen] CodegenConfig Deprecated Setup for ${dependency}.
|
|
The configuration file still contains the codegen in the libraries array.
|
|
If possible, replace it with a single object.
|
|
`);
|
|
console.debug(`BEFORE:
|
|
{
|
|
// ...
|
|
"codegenConfig": {
|
|
"libraries": [
|
|
{
|
|
"name": "libName1",
|
|
"type": "all|components|modules",
|
|
"jsSrcsRoot": "libName1/js"
|
|
},
|
|
{
|
|
"name": "libName2",
|
|
"type": "all|components|modules",
|
|
"jsSrcsRoot": "libName2/src"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
AFTER:
|
|
{
|
|
"codegenConfig": {
|
|
"name": "libraries",
|
|
"type": "all",
|
|
"jsSrcsRoot": "."
|
|
}
|
|
}
|
|
`);
|
|
extractLibrariesFromConfigurationArray(
|
|
configFile,
|
|
codegenConfigKey,
|
|
libraries,
|
|
dependency,
|
|
dependencyPath,
|
|
);
|
|
}
|
|
}
|
|
|
|
function handleReactNativeCodeLibraries(
|
|
libraries,
|
|
codegenConfigFilename,
|
|
codegenConfigKey,
|
|
) {
|
|
// Handle react-native core libraries.
|
|
// This is required when react-native is outside of node_modules.
|
|
console.log('[Codegen] Processing react-native core libraries');
|
|
const reactNativePkgJson = path.join(RN_ROOT, codegenConfigFilename);
|
|
if (!fs.existsSync(reactNativePkgJson)) {
|
|
throw '[Codegen] Error: Could not find config file for react-native.';
|
|
}
|
|
const reactNativeConfigFile = JSON.parse(fs.readFileSync(reactNativePkgJson));
|
|
extractLibrariesFromJSON(reactNativeConfigFile, libraries, codegenConfigKey);
|
|
}
|
|
|
|
function handleThirdPartyLibraries(
|
|
libraries,
|
|
baseCodegenConfigFileDir,
|
|
dependencies,
|
|
codegenConfigFilename,
|
|
codegenConfigKey,
|
|
) {
|
|
// Determine which of these are codegen-enabled libraries
|
|
const configDir = baseCodegenConfigFileDir || path.join(RN_ROOT, '..');
|
|
console.log(
|
|
`\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${configDir}`,
|
|
);
|
|
|
|
// Handle third-party libraries
|
|
Object.keys(dependencies).forEach(dependency => {
|
|
if (dependency === REACT_NATIVE_DEPENDENCY_NAME) {
|
|
// react-native should already be added.
|
|
return;
|
|
}
|
|
const codegenConfigFileDir = path.join(configDir, dependency);
|
|
const configFilePath = path.join(
|
|
codegenConfigFileDir,
|
|
codegenConfigFilename,
|
|
);
|
|
if (fs.existsSync(configFilePath)) {
|
|
const configFile = JSON.parse(fs.readFileSync(configFilePath));
|
|
extractLibrariesFromJSON(
|
|
configFile,
|
|
libraries,
|
|
codegenConfigKey,
|
|
dependency,
|
|
codegenConfigFileDir,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleInAppLibraries(
|
|
libraries,
|
|
pkgJson,
|
|
codegenConfigKey,
|
|
appRootDir,
|
|
) {
|
|
console.log(
|
|
'\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in the app',
|
|
);
|
|
|
|
extractLibrariesFromJSON(
|
|
pkgJson,
|
|
libraries,
|
|
codegenConfigKey,
|
|
pkgJson.name,
|
|
appRootDir,
|
|
);
|
|
}
|
|
|
|
// CodeGen
|
|
|
|
function getCodeGenCliPath() {
|
|
let codegenCliPath;
|
|
if (fs.existsSync(CODEGEN_REPO_PATH)) {
|
|
codegenCliPath = CODEGEN_REPO_PATH;
|
|
|
|
if (!fs.existsSync(path.join(CODEGEN_REPO_PATH, 'lib'))) {
|
|
console.log('\n\n[Codegen] >>>>> Building react-native-codegen package');
|
|
execSync('yarn install', {
|
|
cwd: codegenCliPath,
|
|
stdio: 'inherit',
|
|
});
|
|
execSync('yarn build', {
|
|
cwd: codegenCliPath,
|
|
stdio: 'inherit',
|
|
});
|
|
}
|
|
} else if (fs.existsSync(CODEGEN_NPM_PATH)) {
|
|
codegenCliPath = CODEGEN_NPM_PATH;
|
|
} else {
|
|
throw "error: Could not determine react-native-codegen location. Try running 'yarn install' or 'npm install' in your project root.";
|
|
}
|
|
return codegenCliPath;
|
|
}
|
|
|
|
function computeIOSOutputDir(outputPath, appRootDir) {
|
|
return path.join(outputPath ? outputPath : appRootDir, 'build/generated/ios');
|
|
}
|
|
|
|
function generateSchema(tmpDir, library, node, codegenCliPath) {
|
|
const pathToSchema = path.join(tmpDir, 'schema.json');
|
|
const pathToJavaScriptSources = path.join(
|
|
library.libraryPath,
|
|
library.config.jsSrcsDir,
|
|
);
|
|
|
|
console.log(`\n\n[Codegen] >>>>> Processing ${library.config.name}`);
|
|
// Generate one schema for the entire library...
|
|
executeNodeScript(
|
|
node,
|
|
`${path.join(
|
|
codegenCliPath,
|
|
'lib',
|
|
'cli',
|
|
'combine',
|
|
'combine-js-to-schema-cli.js',
|
|
)} ${pathToSchema} ${pathToJavaScriptSources}`,
|
|
);
|
|
console.log(`[Codegen] Generated schema: ${pathToSchema}`);
|
|
return pathToSchema;
|
|
}
|
|
|
|
function generateCode(iosOutputDir, library, tmpDir, node, pathToSchema) {
|
|
// ...then generate native code artifacts.
|
|
const libraryTypeArg = library.config.type
|
|
? `--libraryType ${library.config.type}`
|
|
: '';
|
|
|
|
const tmpOutputDir = path.join(tmpDir, 'out');
|
|
fs.mkdirSync(tmpOutputDir, {recursive: true});
|
|
|
|
executeNodeScript(
|
|
node,
|
|
`${path.join(RN_ROOT, 'scripts', 'generate-specs-cli.js')} \
|
|
--platform ios \
|
|
--schemaPath ${pathToSchema} \
|
|
--outputDir ${tmpOutputDir} \
|
|
--libraryName ${library.config.name} \
|
|
${libraryTypeArg}`,
|
|
);
|
|
|
|
// Finally, copy artifacts to the final output directory.
|
|
fs.mkdirSync(iosOutputDir, {recursive: true});
|
|
execSync(`cp -R ${tmpOutputDir}/* ${iosOutputDir}`);
|
|
console.log(`[Codegen] Generated artifacts: ${iosOutputDir}`);
|
|
}
|
|
|
|
function generateNativeCodegenFiles(
|
|
libraries,
|
|
fabricEnabled,
|
|
iosOutputDir,
|
|
node,
|
|
codegenCliPath,
|
|
schemaPaths,
|
|
) {
|
|
let fabricEnabledTypes = ['components', 'all'];
|
|
libraries.forEach(library => {
|
|
if (
|
|
!fabricEnabled &&
|
|
fabricEnabledTypes.indexOf(library.config.type) >= 0
|
|
) {
|
|
console.log(
|
|
`[Codegen] ${library.config.name} skipped because fabric is not enabled.`,
|
|
);
|
|
return;
|
|
}
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), library.config.name));
|
|
const pathToSchema = generateSchema(tmpDir, library, node, codegenCliPath);
|
|
generateCode(iosOutputDir, library, tmpDir, node, pathToSchema);
|
|
|
|
// Filter the react native core library out.
|
|
// In the future, core library and third party library should
|
|
// use the same way to generate/register the fabric components.
|
|
if (!isReactNativeCoreLibrary(library.config.name)) {
|
|
schemaPaths[library.config.name] = pathToSchema;
|
|
}
|
|
});
|
|
}
|
|
|
|
function createComponentProvider(
|
|
fabricEnabled,
|
|
schemaPaths,
|
|
node,
|
|
iosOutputDir,
|
|
) {
|
|
if (fabricEnabled) {
|
|
console.log('\n\n>>>>> Creating component provider');
|
|
// Save the list of spec paths to a temp file.
|
|
const schemaListTmpPath = `${os.tmpdir()}/rn-tmp-schema-list.json`;
|
|
const fd = fs.openSync(schemaListTmpPath, 'w');
|
|
fs.writeSync(fd, JSON.stringify(schemaPaths));
|
|
fs.closeSync(fd);
|
|
console.log(`Generated schema list: ${schemaListTmpPath}`);
|
|
|
|
// Generate FabricComponentProvider.
|
|
// Only for iOS at this moment.
|
|
executeNodeScript(
|
|
node,
|
|
`${path.join(
|
|
RN_ROOT,
|
|
'scripts',
|
|
'generate-provider-cli.js',
|
|
)} --platform ios --schemaListPath "${schemaListTmpPath}" --outputDir ${iosOutputDir}`,
|
|
);
|
|
console.log(`Generated provider in: ${iosOutputDir}`);
|
|
}
|
|
}
|
|
|
|
// It removes all the empty files and empty folders
|
|
// it finds, starting from `filepath`, recursively.
|
|
//
|
|
// This function is needed since, after aligning the codegen between
|
|
// iOS and Android, we have to create empty folders in advance and
|
|
// we don't know wheter they will be populated up until the end of the process.
|
|
//
|
|
// @parameter filepath: the root path from which we want to remove the empty files and folders.
|
|
function cleanupEmptyFilesAndFolders(filepath) {
|
|
const stats = fs.statSync(filepath);
|
|
|
|
if (stats.isFile() && stats.size === 0) {
|
|
fs.rmSync(filepath);
|
|
return;
|
|
} else if (stats.isFile()) {
|
|
return;
|
|
}
|
|
|
|
const dirContent = fs.readdirSync(filepath);
|
|
dirContent.forEach(contentPath =>
|
|
cleanupEmptyFilesAndFolders(path.join(filepath, contentPath)),
|
|
);
|
|
|
|
// The original folder may be filled with empty folders
|
|
// if that the case, we would also like to remove the parent.
|
|
// Hence, we need to read the folder again.
|
|
const newContent = fs.readdirSync(filepath);
|
|
if (newContent.length === 0) {
|
|
fs.rmdirSync(filepath);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Execute
|
|
|
|
/**
|
|
* This function is the entry point for the codegen. It:
|
|
* - reads the package json
|
|
* - extracts the libraries
|
|
* - setups the CLI to generate the code
|
|
* - generate the code
|
|
*
|
|
* @parameter appRootDir: the directory with the app source code, where the `codegenConfigFilename` lives.
|
|
* @parameter outputPath: the base output path for the CodeGen.
|
|
* @parameter node: the path to the node executable, used to run the codegen scripts.
|
|
* @parameter codegenConfigFilename: the file that contains the codeGen configuration. The default is `package.json`.
|
|
* @parameter codegenConfigKey: the key in the codegenConfigFile that controls the codegen.
|
|
* @parameter baseCodegenConfigFileDir: the directory of the codeGenConfigFile.
|
|
* @parameter fabricEnabled: whether fabric is enabled or not.
|
|
* @throws If it can't find a config file for react-native.
|
|
* @throws If it can't find a CodeGen configuration in the file.
|
|
* @throws If it can't find a cli for the CodeGen.
|
|
*/
|
|
function execute(
|
|
appRootDir,
|
|
outputPath,
|
|
node,
|
|
codegenConfigFilename,
|
|
codegenConfigKey,
|
|
baseCodegenConfigFileDir,
|
|
fabricEnabled,
|
|
) {
|
|
if (!isAppRootValid(appRootDir)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const pkgJson = readPackageJSON(appRootDir);
|
|
const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies};
|
|
const libraries = [];
|
|
|
|
handleReactNativeCodeLibraries(
|
|
libraries,
|
|
codegenConfigFilename,
|
|
codegenConfigKey,
|
|
);
|
|
handleThirdPartyLibraries(
|
|
libraries,
|
|
baseCodegenConfigFileDir,
|
|
dependencies,
|
|
codegenConfigFilename,
|
|
codegenConfigKey,
|
|
);
|
|
handleInAppLibraries(libraries, pkgJson, codegenConfigKey, appRootDir);
|
|
|
|
if (libraries.length === 0) {
|
|
console.log('[Codegen] No codegen-enabled libraries found.');
|
|
return;
|
|
}
|
|
|
|
const codegenCliPath = getCodeGenCliPath();
|
|
|
|
const schemaPaths = {};
|
|
|
|
const iosOutputDir = computeIOSOutputDir(outputPath, appRootDir);
|
|
|
|
generateNativeCodegenFiles(
|
|
libraries,
|
|
fabricEnabled,
|
|
iosOutputDir,
|
|
node,
|
|
codegenCliPath,
|
|
schemaPaths,
|
|
);
|
|
|
|
createComponentProvider(fabricEnabled, schemaPaths, node, iosOutputDir);
|
|
cleanupEmptyFilesAndFolders(iosOutputDir);
|
|
} catch (err) {
|
|
console.error(err);
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
console.log('\n\n[Codegen] Done.');
|
|
return;
|
|
}
|
|
|
|
module.exports = {
|
|
execute: execute,
|
|
// exported for testing purposes only:
|
|
_extractLibrariesFromJSON: extractLibrariesFromJSON,
|
|
_executeNodeScript: executeNodeScript,
|
|
_generateCode: generateCode,
|
|
_cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders,
|
|
};
|