UNPKG

react-native

Version:

A framework for building native apps using React

399 lines (355 loc) • 11.2 kB
/** * 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'; const { CODEGEN_REPO_PATH, CORE_LIBRARIES_WITH_OUTPUT_FOLDER, REACT_NATIVE, } = require('./constants'); const {execSync} = require('child_process'); const fs = require('fs'); const path = require('path'); function pkgJsonIncludesGeneratedCode(pkgJson) { return pkgJson.codegenConfig && pkgJson.codegenConfig.includesGeneratedCode; } const codegenLog = (text, info = false) => { // ANSI escape codes for colors and formatting const reset = '\x1b[0m'; const cyan = '\x1b[36m'; const yellow = '\x1b[33m'; const bold = '\x1b[1m'; const color = info ? yellow : ''; console.log(`${cyan}${bold}[Codegen]${reset} ${color}${text}${reset}`); }; function readPkgJsonInDirectory(dir) { const pkgJsonPath = path.join(dir, 'package.json'); if (!fs.existsSync(pkgJsonPath)) { throw `[Codegen] Error: ${pkgJsonPath} does not exist.`; } return JSON.parse(fs.readFileSync(pkgJsonPath)); } function buildCodegenIfNeeded() { if (!fs.existsSync(CODEGEN_REPO_PATH)) { return; } // Assuming we are working in the react-native repo. We might need to build the codegen. // This will become unnecessary once we start using Babel Register for the codegen package. const libPath = path.join(CODEGEN_REPO_PATH, 'lib'); if (fs.existsSync(libPath) && fs.readdirSync(libPath).length > 0) { return; } codegenLog('Building react-native-codegen package.', true); execSync('yarn install', { cwd: CODEGEN_REPO_PATH, stdio: 'inherit', }); execSync('yarn build', { cwd: CODEGEN_REPO_PATH, stdio: 'inherit', }); } // 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 whether 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; } } function readReactNativeConfig(projectRoot) { const rnConfigFilePath = path.resolve(projectRoot, 'react-native.config.js'); if (!fs.existsSync(rnConfigFilePath)) { return {}; } return require(rnConfigFilePath); } /** * Finding libraries! */ function findCodegenEnabledLibraries(pkgJson, projectRoot, reactNativeConfig) { const projectLibraries = findProjectRootLibraries(pkgJson, projectRoot); if (pkgJsonIncludesGeneratedCode(pkgJson)) { return projectLibraries; } else { return [ ...projectLibraries, ...findExternalLibraries(pkgJson, projectRoot), ...findLibrariesFromReactNativeConfig(projectRoot, reactNativeConfig), ]; } } function findProjectRootLibraries(pkgJson, projectRoot) { codegenLog('Searching for codegen-enabled libraries in the app.', true); if (pkgJson.codegenConfig == null) { codegenLog( 'The "codegenConfig" field is not defined in package.json. Assuming there is nothing to generate at the app level.', true, ); return []; } if (typeof pkgJson.codegenConfig !== 'object') { throw 'The "codegenConfig" field must be an Object.'; } return extractLibrariesFromJSON(pkgJson, projectRoot); } function findLibrariesFromReactNativeConfig(projectRoot, rnConfig) { codegenLog( `Searching for codegen-enabled libraries in react-native.config.js`, true, ); if (!rnConfig.dependencies) { return []; } return Object.keys(rnConfig.dependencies).flatMap(name => { const dependencyConfig = rnConfig.dependencies[name]; if (!dependencyConfig.root) { return []; } const codegenConfigFileDir = path.resolve( projectRoot, dependencyConfig.root, ); let configFile; try { configFile = readPkgJsonInDirectory(codegenConfigFileDir); } catch { return []; } return extractLibrariesFromJSON(configFile, codegenConfigFileDir); }); } function findExternalLibraries(pkgJson, projectRoot) { const dependencies = { ...pkgJson.dependencies, ...pkgJson.devDependencies, ...pkgJson.peerDependencies, }; // Determine which of these are codegen-enabled libraries codegenLog( 'Searching for codegen-enabled libraries in the project dependencies.', true, ); // Handle third-party libraries return Object.keys(dependencies).flatMap(dependency => { let configFilePath = ''; try { configFilePath = require.resolve(path.join(dependency, 'package.json'), { paths: [projectRoot], }); } catch (e) { // require.resolve fails if the dependency is a local node module. if ( dependencies[dependency].startsWith('.') || // handles relative paths dependencies[dependency].startsWith('/') // handles absolute paths ) { configFilePath = path.join( projectRoot, pkgJson.dependencies[dependency], 'package.json', ); } else { return []; } } const configFile = JSON.parse(fs.readFileSync(configFilePath)); const codegenConfigFileDir = path.dirname(configFilePath); return extractLibrariesFromJSON(configFile, codegenConfigFileDir); }); } function extractLibrariesFromJSON(configFile, dependencyPath) { if (configFile.codegenConfig == null) { return []; } codegenLog(`Found ${configFile.name}`); if (configFile.codegenConfig.libraries == null) { const config = configFile.codegenConfig; return [ { name: configFile.name, config, libraryPath: dependencyPath, }, ]; } else { printDeprecationWarningIfNeeded(configFile.name); return extractLibrariesFromConfigurationArray(configFile, dependencyPath); } } function printDeprecationWarningIfNeeded(dependency) { if (dependency === REACT_NATIVE) { return; } codegenLog(`CodegenConfig Deprecated Setup for ${dependency}. The configuration file still contains the codegen in the libraries array. If possible, replace it with a single object. `); codegenLog(`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": "." } } `); } function extractLibrariesFromConfigurationArray(configFile, dependencyPath) { return configFile.codegenConfig.libraries.map(config => { return { name: config.name, config, libraryPath: dependencyPath, }; }); } function isReactNativeCoreLibrary(libraryName) { return libraryName in CORE_LIBRARIES_WITH_OUTPUT_FOLDER; } /** * Returns a map of this shape: * { * "libraryName": { * "library": { ... } * "modules": { * "moduleName": { * "conformsToProtocols": [ "protocol1", "protocol2" ], * "className": "RCTFooModuler", * } * }, * "components": { * "componentName": { * "className": "RCTFooComponent", * } * } * } * } * * Validates that modules are defined in at most one library. * Validates that components are defined in at most one library. */ function parseiOSAnnotations(libraries) { const mLibraryMap = {}; const cLibraryMap = {}; const map = {}; for (const library of libraries) { const iosConfig = library?.config?.ios; if (!iosConfig) { continue; } const libraryName = getLibraryName(library); map[libraryName] = map[libraryName] || { library, modules: {}, components: {}, }; const {modules, components} = iosConfig; if (modules) { for (const [moduleName, annotation] of Object.entries(modules)) { mLibraryMap[moduleName] = mLibraryMap[moduleName] || new Set(); mLibraryMap[moduleName].add(libraryName); map[libraryName].modules[moduleName] = {...annotation}; } } if (components) { for (const [moduleName, annotation] of Object.entries(components)) { cLibraryMap[moduleName] = cLibraryMap[moduleName] || new Set(); cLibraryMap[moduleName].add(libraryName); map[libraryName].components[moduleName] = {...annotation}; } } } const moduleConflicts = Object.entries(mLibraryMap) .filter(([_, libraryNames]) => libraryNames.size > 1) .map(([moduleName, libraryNames]) => { const libraryNamesString = Array.from(libraryNames).join(', '); return ` Module { "${moduleName}" } => Libraries{ ${libraryNamesString} }\n`; }); const componentConflicts = Object.entries(cLibraryMap) .filter(([_, libraryNames]) => libraryNames.size > 1) .map(([moduleName, libraryNames]) => { const libraryNamesString = Array.from(libraryNames).join(', '); return ` Component { "${moduleName}" } => Libraries{ ${libraryNamesString} }\n`; }); if (moduleConflicts.length > 0 || componentConflicts.legnth > 0) { throw new Error( 'Some components or modules are declared in more than one libraries: \n' + [...moduleConflicts, ...componentConflicts].join('\n'), ); } return map; } function getLibraryName(library) { return JSON.parse( fs.readFileSync(path.join(library.libraryPath, 'package.json')), ).name; } /** * Finds all disabled libraries by platform based the react native config. * * This is needed when selectively disabling libraries in react-native.config.js since codegen should exclude those libraries as well. */ function findDisabledLibrariesByPlatform(reactNativeConfig, platform) { const dependencies = reactNativeConfig.dependencies ?? {}; return Object.keys(dependencies).filter( dependency => dependencies[dependency].platforms?.[platform] === null, ); } module.exports = { buildCodegenIfNeeded, pkgJsonIncludesGeneratedCode, codegenLog, readPkgJsonInDirectory, isReactNativeCoreLibrary, cleanupEmptyFilesAndFolders, findCodegenEnabledLibraries, findProjectRootLibraries, extractLibrariesFromJSON, parseiOSAnnotations, readReactNativeConfig, findDisabledLibrariesByPlatform, };