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