1/** 2 * Copyright (c) Meta Platforms, Inc. and affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 * 7 * @format 8 */ 9 10'use strict'; 11 12/** 13 * This script crawls through a React Native application's dependencies and invokes the codegen 14 * for any libraries that require it. 15 * To enable codegen support, the library should include a config in the codegenConfigKey key 16 * in a codegenConfigFilename file. 17 */ 18 19const {execSync} = require('child_process'); 20const fs = require('fs'); 21const os = require('os'); 22const path = require('path'); 23 24const REACT_NATIVE_REPOSITORY_ROOT = path.join( 25 __dirname, 26 '..', 27 '..', 28 '..', 29 '..', 30); 31const REACT_NATIVE_PACKAGE_ROOT_FOLDER = path.join(__dirname, '..', '..'); 32 33const CODEGEN_DEPENDENCY_NAME = '@react-native/codegen'; 34const CODEGEN_REPO_PATH = `${REACT_NATIVE_REPOSITORY_ROOT}/packages/react-native-codegen`; 35const CODEGEN_NPM_PATH = `${REACT_NATIVE_PACKAGE_ROOT_FOLDER}/../${CODEGEN_DEPENDENCY_NAME}`; 36const CORE_LIBRARIES_WITH_OUTPUT_FOLDER = { 37 rncore: path.join(REACT_NATIVE_PACKAGE_ROOT_FOLDER, 'ReactCommon'), 38 FBReactNativeSpec: null, 39}; 40const REACT_NATIVE_DEPENDENCY_NAME = 'react-native'; 41 42// HELPERS 43 44function isReactNativeCoreLibrary(libraryName) { 45 return libraryName in CORE_LIBRARIES_WITH_OUTPUT_FOLDER; 46} 47 48function executeNodeScript(node, script) { 49 execSync(`${node} ${script}`); 50} 51 52function isAppRootValid(appRootDir) { 53 if (appRootDir == null) { 54 console.error('Missing path to React Native application'); 55 process.exitCode = 1; 56 return false; 57 } 58 return true; 59} 60 61function readPackageJSON(appRootDir) { 62 return JSON.parse(fs.readFileSync(path.join(appRootDir, 'package.json'))); 63} 64 65function printDeprecationWarningIfNeeded(dependency) { 66 if (dependency === REACT_NATIVE_DEPENDENCY_NAME) { 67 return; 68 } 69 console.log(`[Codegen] CodegenConfig Deprecated Setup for ${dependency}. 70 The configuration file still contains the codegen in the libraries array. 71 If possible, replace it with a single object. 72 `); 73 console.debug(`BEFORE: 74 { 75 // ... 76 "codegenConfig": { 77 "libraries": [ 78 { 79 "name": "libName1", 80 "type": "all|components|modules", 81 "jsSrcsRoot": "libName1/js" 82 }, 83 { 84 "name": "libName2", 85 "type": "all|components|modules", 86 "jsSrcsRoot": "libName2/src" 87 } 88 ] 89 } 90 } 91 92 AFTER: 93 { 94 "codegenConfig": { 95 "name": "libraries", 96 "type": "all", 97 "jsSrcsRoot": "." 98 } 99 } 100 `); 101} 102 103// Reading Libraries 104function extractLibrariesFromConfigurationArray( 105 configFile, 106 codegenConfigKey, 107 libraries, 108 dependency, 109 dependencyPath, 110) { 111 console.log(`[Codegen] Found ${dependency}`); 112 configFile[codegenConfigKey].libraries.forEach(config => { 113 const libraryConfig = { 114 library: dependency, 115 config, 116 libraryPath: dependencyPath, 117 }; 118 libraries.push(libraryConfig); 119 }); 120} 121 122function extractLibrariesFromJSON( 123 configFile, 124 libraries, 125 codegenConfigKey, 126 dependency, 127 dependencyPath, 128) { 129 var isBlocking = false; 130 if (dependency == null) { 131 dependency = REACT_NATIVE_DEPENDENCY_NAME; 132 dependencyPath = REACT_NATIVE_PACKAGE_ROOT_FOLDER; 133 // If we are exploring the ReactNative libraries, we want to raise an error 134 // if the codegen is not properly configured. 135 isBlocking = true; 136 } 137 138 if (configFile[codegenConfigKey] == null) { 139 if (isBlocking) { 140 throw `[Codegen] Error: Could not find codegen config for ${dependency} .`; 141 } 142 return; 143 } 144 145 if (configFile[codegenConfigKey].libraries == null) { 146 console.log(`[Codegen] Found ${dependency}`); 147 var config = configFile[codegenConfigKey]; 148 libraries.push({ 149 library: dependency, 150 config, 151 libraryPath: dependencyPath, 152 }); 153 } else { 154 printDeprecationWarningIfNeeded(dependency); 155 extractLibrariesFromConfigurationArray( 156 configFile, 157 codegenConfigKey, 158 libraries, 159 dependency, 160 dependencyPath, 161 ); 162 } 163} 164 165function handleReactNativeCodeLibraries( 166 libraries, 167 codegenConfigFilename, 168 codegenConfigKey, 169) { 170 // Handle react-native core libraries. 171 // This is required when react-native is outside of node_modules. 172 console.log('[Codegen] Processing react-native core libraries'); 173 const reactNativePkgJson = path.join( 174 REACT_NATIVE_PACKAGE_ROOT_FOLDER, 175 codegenConfigFilename, 176 ); 177 if (!fs.existsSync(reactNativePkgJson)) { 178 throw '[Codegen] Error: Could not find config file for react-native.'; 179 } 180 const reactNativeConfigFile = JSON.parse(fs.readFileSync(reactNativePkgJson)); 181 extractLibrariesFromJSON(reactNativeConfigFile, libraries, codegenConfigKey); 182} 183 184function handleThirdPartyLibraries( 185 libraries, 186 baseCodegenConfigFileDir, 187 dependencies, 188 codegenConfigFilename, 189 codegenConfigKey, 190) { 191 // Determine which of these are codegen-enabled libraries 192 const configDir = 193 baseCodegenConfigFileDir || 194 path.join(REACT_NATIVE_PACKAGE_ROOT_FOLDER, '..'); 195 console.log( 196 `\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${configDir}`, 197 ); 198 199 // Handle third-party libraries 200 Object.keys(dependencies).forEach(dependency => { 201 if (dependency === REACT_NATIVE_DEPENDENCY_NAME) { 202 // react-native should already be added. 203 return; 204 } 205 const codegenConfigFileDir = path.join(configDir, dependency); 206 const configFilePath = path.join( 207 codegenConfigFileDir, 208 codegenConfigFilename, 209 ); 210 if (fs.existsSync(configFilePath)) { 211 const configFile = JSON.parse(fs.readFileSync(configFilePath)); 212 extractLibrariesFromJSON( 213 configFile, 214 libraries, 215 codegenConfigKey, 216 dependency, 217 codegenConfigFileDir, 218 ); 219 } 220 }); 221} 222 223function handleLibrariesFromReactNativeConfig( 224 libraries, 225 codegenConfigKey, 226 codegenConfigFilename, 227 appRootDir, 228) { 229 const rnConfigFileName = 'react-native.config.js'; 230 231 console.log( 232 `\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${rnConfigFileName}`, 233 ); 234 235 const rnConfigFilePath = path.resolve(appRootDir, rnConfigFileName); 236 237 if (fs.existsSync(rnConfigFilePath)) { 238 const rnConfig = require(rnConfigFilePath); 239 240 if (rnConfig.dependencies != null) { 241 Object.keys(rnConfig.dependencies).forEach(name => { 242 const dependencyConfig = rnConfig.dependencies[name]; 243 244 if (dependencyConfig.root) { 245 const codegenConfigFileDir = path.resolve( 246 appRootDir, 247 dependencyConfig.root, 248 ); 249 const configFilePath = path.join( 250 codegenConfigFileDir, 251 codegenConfigFilename, 252 ); 253 const pkgJsonPath = path.join(codegenConfigFileDir, 'package.json'); 254 255 if (fs.existsSync(configFilePath)) { 256 const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath)); 257 const configFile = JSON.parse(fs.readFileSync(configFilePath)); 258 extractLibrariesFromJSON( 259 configFile, 260 libraries, 261 codegenConfigKey, 262 pkgJson.name, 263 codegenConfigFileDir, 264 ); 265 } 266 } 267 }); 268 } 269 } 270} 271 272function handleInAppLibraries( 273 libraries, 274 pkgJson, 275 codegenConfigKey, 276 appRootDir, 277) { 278 console.log( 279 '\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in the app', 280 ); 281 282 extractLibrariesFromJSON( 283 pkgJson, 284 libraries, 285 codegenConfigKey, 286 pkgJson.name, 287 appRootDir, 288 ); 289} 290 291// CodeGen 292function getCodeGenCliPath() { 293 let codegenCliPath; 294 if (fs.existsSync(CODEGEN_REPO_PATH)) { 295 codegenCliPath = CODEGEN_REPO_PATH; 296 297 if (!fs.existsSync(path.join(CODEGEN_REPO_PATH, 'lib'))) { 298 console.log('\n\n[Codegen] >>>>> Building react-native-codegen package'); 299 execSync('yarn install', { 300 cwd: codegenCliPath, 301 stdio: 'inherit', 302 }); 303 execSync('yarn build', { 304 cwd: codegenCliPath, 305 stdio: 'inherit', 306 }); 307 } 308 } else if (fs.existsSync(CODEGEN_NPM_PATH)) { 309 codegenCliPath = CODEGEN_NPM_PATH; 310 } else { 311 throw `error: Could not determine ${CODEGEN_DEPENDENCY_NAME} location. Try running 'yarn install' or 'npm install' in your project root.`; 312 } 313 return codegenCliPath; 314} 315 316function computeIOSOutputDir(outputPath, appRootDir) { 317 return path.join(outputPath ? outputPath : appRootDir, 'build/generated/ios'); 318} 319 320function generateSchema(tmpDir, library, node, codegenCliPath) { 321 const pathToSchema = path.join(tmpDir, 'schema.json'); 322 const pathToJavaScriptSources = path.join( 323 library.libraryPath, 324 library.config.jsSrcsDir, 325 ); 326 327 console.log(`\n\n[Codegen] >>>>> Processing ${library.config.name}`); 328 // Generate one schema for the entire library... 329 executeNodeScript( 330 node, 331 `${path.join( 332 codegenCliPath, 333 'lib', 334 'cli', 335 'combine', 336 'combine-js-to-schema-cli.js', 337 )} --platform ios ${pathToSchema} ${pathToJavaScriptSources}`, 338 ); 339 console.log(`[Codegen] Generated schema: ${pathToSchema}`); 340 return pathToSchema; 341} 342 343function generateCode(iosOutputDir, library, tmpDir, node, pathToSchema) { 344 // ...then generate native code artifacts. 345 const libraryTypeArg = library.config.type 346 ? `--libraryType ${library.config.type}` 347 : ''; 348 349 const tmpOutputDir = path.join(tmpDir, 'out'); 350 fs.mkdirSync(tmpOutputDir, {recursive: true}); 351 352 executeNodeScript( 353 node, 354 `${path.join( 355 REACT_NATIVE_PACKAGE_ROOT_FOLDER, 356 'scripts', 357 'generate-specs-cli.js', 358 )} \ 359 --platform ios \ 360 --schemaPath ${pathToSchema} \ 361 --outputDir ${tmpOutputDir} \ 362 --libraryName ${library.config.name} \ 363 ${libraryTypeArg}`, 364 ); 365 366 // Finally, copy artifacts to the final output directory. 367 const outputDir = 368 CORE_LIBRARIES_WITH_OUTPUT_FOLDER[library.config.name] ?? iosOutputDir; 369 fs.mkdirSync(outputDir, {recursive: true}); 370 execSync(`cp -R ${tmpOutputDir}/* ${outputDir}`); 371 console.log(`[Codegen] Generated artifacts: ${iosOutputDir}`); 372} 373 374function generateNativeCodegenFiles( 375 libraries, 376 fabricEnabled, 377 iosOutputDir, 378 node, 379 codegenCliPath, 380 schemaPaths, 381) { 382 let fabricEnabledTypes = ['components', 'all']; 383 libraries.forEach(library => { 384 if ( 385 !fabricEnabled && 386 fabricEnabledTypes.indexOf(library.config.type) >= 0 387 ) { 388 console.log( 389 `[Codegen] ${library.config.name} skipped because fabric is not enabled.`, 390 ); 391 return; 392 } 393 const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), library.config.name)); 394 const pathToSchema = generateSchema(tmpDir, library, node, codegenCliPath); 395 generateCode(iosOutputDir, library, tmpDir, node, pathToSchema); 396 397 // Filter the react native core library out. 398 // In the future, core library and third party library should 399 // use the same way to generate/register the fabric components. 400 if (!isReactNativeCoreLibrary(library.config.name)) { 401 schemaPaths[library.config.name] = pathToSchema; 402 } 403 }); 404} 405 406function createComponentProvider( 407 fabricEnabled, 408 schemaPaths, 409 node, 410 iosOutputDir, 411) { 412 if (fabricEnabled) { 413 console.log('\n\n>>>>> Creating component provider'); 414 // Save the list of spec paths to a temp file. 415 const schemaListTmpPath = `${os.tmpdir()}/rn-tmp-schema-list.json`; 416 const fd = fs.openSync(schemaListTmpPath, 'w'); 417 fs.writeSync(fd, JSON.stringify(schemaPaths)); 418 fs.closeSync(fd); 419 console.log(`Generated schema list: ${schemaListTmpPath}`); 420 421 const outputDir = path.join( 422 REACT_NATIVE_PACKAGE_ROOT_FOLDER, 423 'React', 424 'Fabric', 425 ); 426 427 // Generate FabricComponentProvider. 428 // Only for iOS at this moment. 429 executeNodeScript( 430 node, 431 `${path.join( 432 REACT_NATIVE_PACKAGE_ROOT_FOLDER, 433 'scripts', 434 'generate-provider-cli.js', 435 )} --platform ios --schemaListPath "${schemaListTmpPath}" --outputDir ${outputDir}`, 436 ); 437 console.log(`Generated provider in: ${outputDir}`); 438 } 439} 440 441function findCodegenEnabledLibraries( 442 appRootDir, 443 baseCodegenConfigFileDir, 444 codegenConfigFilename, 445 codegenConfigKey, 446) { 447 const pkgJson = readPackageJSON(appRootDir); 448 const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies}; 449 const libraries = []; 450 451 handleReactNativeCodeLibraries( 452 libraries, 453 codegenConfigFilename, 454 codegenConfigKey, 455 ); 456 handleThirdPartyLibraries( 457 libraries, 458 baseCodegenConfigFileDir, 459 dependencies, 460 codegenConfigFilename, 461 codegenConfigKey, 462 ); 463 handleLibrariesFromReactNativeConfig( 464 libraries, 465 codegenConfigKey, 466 codegenConfigFilename, 467 appRootDir, 468 ); 469 handleInAppLibraries(libraries, pkgJson, codegenConfigKey, appRootDir); 470 471 return libraries; 472} 473 474// It removes all the empty files and empty folders 475// it finds, starting from `filepath`, recursively. 476// 477// This function is needed since, after aligning the codegen between 478// iOS and Android, we have to create empty folders in advance and 479// we don't know whether they will be populated up until the end of the process. 480// 481// @parameter filepath: the root path from which we want to remove the empty files and folders. 482function cleanupEmptyFilesAndFolders(filepath) { 483 const stats = fs.statSync(filepath); 484 485 if (stats.isFile() && stats.size === 0) { 486 fs.rmSync(filepath); 487 return; 488 } else if (stats.isFile()) { 489 return; 490 } 491 492 const dirContent = fs.readdirSync(filepath); 493 dirContent.forEach(contentPath => 494 cleanupEmptyFilesAndFolders(path.join(filepath, contentPath)), 495 ); 496 497 // The original folder may be filled with empty folders 498 // if that the case, we would also like to remove the parent. 499 // Hence, we need to read the folder again. 500 const newContent = fs.readdirSync(filepath); 501 if (newContent.length === 0) { 502 fs.rmdirSync(filepath); 503 return; 504 } 505} 506 507// Execute 508 509/** 510 * This function is the entry point for the codegen. It: 511 * - reads the package json 512 * - extracts the libraries 513 * - setups the CLI to generate the code 514 * - generate the code 515 * 516 * @parameter appRootDir: the directory with the app source code, where the `codegenConfigFilename` lives. 517 * @parameter outputPath: the base output path for the CodeGen. 518 * @parameter node: the path to the node executable, used to run the codegen scripts. 519 * @parameter codegenConfigFilename: the file that contains the codeGen configuration. The default is `package.json`. 520 * @parameter codegenConfigKey: the key in the codegenConfigFile that controls the codegen. 521 * @parameter baseCodegenConfigFileDir: the directory of the codeGenConfigFile. 522 * @parameter fabricEnabled: whether fabric is enabled or not. 523 * @throws If it can't find a config file for react-native. 524 * @throws If it can't find a CodeGen configuration in the file. 525 * @throws If it can't find a cli for the CodeGen. 526 */ 527function execute( 528 appRootDir, 529 outputPath, 530 node, 531 codegenConfigFilename, 532 codegenConfigKey, 533 baseCodegenConfigFileDir, 534 fabricEnabled, 535) { 536 if (!isAppRootValid(appRootDir)) { 537 return; 538 } 539 540 try { 541 const libraries = findCodegenEnabledLibraries( 542 appRootDir, 543 baseCodegenConfigFileDir, 544 codegenConfigFilename, 545 codegenConfigKey, 546 ); 547 548 if (libraries.length === 0) { 549 console.log('[Codegen] No codegen-enabled libraries found.'); 550 return; 551 } 552 553 const codegenCliPath = getCodeGenCliPath(); 554 555 const schemaPaths = {}; 556 557 const iosOutputDir = computeIOSOutputDir(outputPath, appRootDir); 558 559 generateNativeCodegenFiles( 560 libraries, 561 fabricEnabled, 562 iosOutputDir, 563 node, 564 codegenCliPath, 565 schemaPaths, 566 ); 567 568 createComponentProvider(fabricEnabled, schemaPaths, node, iosOutputDir); 569 cleanupEmptyFilesAndFolders(iosOutputDir); 570 } catch (err) { 571 console.error(err); 572 process.exitCode = 1; 573 } 574 575 console.log('\n\n[Codegen] Done.'); 576 return; 577} 578 579module.exports = { 580 execute: execute, 581 // exported for testing purposes only: 582 _extractLibrariesFromJSON: extractLibrariesFromJSON, 583 _findCodegenEnabledLibraries: findCodegenEnabledLibraries, 584 _executeNodeScript: executeNodeScript, 585 _generateCode: generateCode, 586 _cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders, 587}; 588