diff --git a/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap b/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap index 65fbda45a44..b77c4e8d702 100644 --- a/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap +++ b/packages/react-native/scripts/codegen/__tests__/__snapshots__/generate-artifacts-executor-test.js.snap @@ -111,12 +111,12 @@ exports[`execute "RCTModuleProviders.mm" should match snapshot 1`] = ` dispatch_once(&onceToken, ^{ NSDictionary * moduleMapping = @{ - @\\"TestAppImageURLLoader\\": @\\"RCTTestAppImageURLLoader\\", // test-app - @\\"TestAppURLRequestHandler\\": @\\"RCTTestAppURLRequestHandler\\", // test-app - @\\"TestAppImageDataDecoder\\": @\\"RCTTestAppImageDataDecoder\\", // test-app - @\\"TestLibraryImageURLLoader\\": @\\"RCTTestLibraryImageURLLoader\\", // test-library - @\\"TestLibraryURLRequestHandler\\": @\\"RCTTestLibraryURLRequestHandler\\", // test-library - @\\"TestLibraryImageDataDecoder\\": @\\"RCTTestLibraryImageDataDecoder\\", // test-library + @\\"TestAppImageURLLoader\\": @\\"RCTTestAppImageURLLoader\\", // test-app + @\\"TestAppURLRequestHandler\\": @\\"RCTTestAppURLRequestHandler\\", // test-app + @\\"TestAppImageDataDecoder\\": @\\"RCTTestAppImageDataDecoder\\", // test-app + @\\"TestLibraryImageURLLoader\\": @\\"RCTTestLibraryImageURLLoader\\", // test-library + @\\"TestLibraryURLRequestHandler\\": @\\"RCTTestLibraryURLRequestHandler\\", // test-library + @\\"TestLibraryImageDataDecoder\\": @\\"RCTTestLibraryImageDataDecoder\\", // test-library }; NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:moduleMapping.count]; @@ -190,7 +190,7 @@ exports[`execute "RCTModulesConformingToProtocolsProvider.mm" should match snaps dispatch_once(&onceToken, ^{ classNames = @[ @\\"RCTTestAppImageURLLoader\\", - @\\"RCTTestLibraryImageURLLoader\\" + @\\"RCTTestLibraryImageURLLoader\\" ]; }); @@ -205,7 +205,7 @@ exports[`execute "RCTModulesConformingToProtocolsProvider.mm" should match snaps dispatch_once(&onceToken, ^{ classNames = @[ @\\"RCTTestAppImageDataDecoder\\", - @\\"RCTTestLibraryImageDataDecoder\\" + @\\"RCTTestLibraryImageDataDecoder\\" ]; }); @@ -220,7 +220,7 @@ exports[`execute "RCTModulesConformingToProtocolsProvider.mm" should match snaps dispatch_once(&onceToken, ^{ classNames = @[ @\\"RCTTestAppURLRequestHandler\\", - @\\"RCTTestLibraryURLRequestHandler\\" + @\\"RCTTestLibraryURLRequestHandler\\" ]; }); @@ -274,8 +274,8 @@ exports[`execute "RCTThirdPartyComponentsProvider.mm" should match snapshot 1`] dispatch_once(&nativeComponentsToken, ^{ thirdPartyComponents = @{ - @\\"TestAppComponent\\": NSClassFromString(@\\"RCTTestAppComponentClass\\"), // test-app - @\\"TestLibraryComponent\\": NSClassFromString(@\\"RCTTestLibraryComponentClass\\"), // test-library + @\\"TestAppComponent\\": NSClassFromString(@\\"RCTTestAppComponentClass\\"), // test-app + @\\"TestLibraryComponent\\": NSClassFromString(@\\"RCTTestLibraryComponentClass\\"), // test-library }; }); diff --git a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js index 877bc0e2146..139ec02b23b 100644 --- a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js +++ b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -11,7 +11,14 @@ 'use strict'; const fixtures = require('../__test_fixtures__/fixtures'); -const underTest = require('../generate-artifacts-executor'); +const {execute} = require('../generate-artifacts-executor'); +const { + extractSupportedApplePlatforms, +} = require('../generate-artifacts-executor/generateSchemaInfos'); +const { + cleanupEmptyFilesAndFolders, + extractLibrariesFromJSON, +} = require('../generate-artifacts-executor/utils'); const fs = require('fs'); const path = require('path'); @@ -26,7 +33,7 @@ describe('execute', () => { const outputDir = path.join(appDir, 'temp'); beforeAll(() => { - underTest.execute(appDir, 'ios', outputDir, 'app', false); + execute(appDir, 'ios', outputDir, 'app', false); }); afterAll(() => { @@ -57,7 +64,7 @@ describe('execute', () => { describe('extractLibrariesFromJSON', () => { it('extracts a single dependency when config has no libraries', () => { let configFile = fixtures.noLibrariesConfigFile; - let libraries = underTest._extractLibrariesFromJSON(configFile, '.'); + let libraries = extractLibrariesFromJSON(configFile, '.'); expect(libraries.length).toBe(1); expect(libraries[0]).toEqual({ config: { @@ -71,13 +78,13 @@ describe('extractLibrariesFromJSON', () => { it("doesn't extract libraries when they are present but empty", () => { const configFile = {codegenConfig: {libraries: []}}; - let libraries = underTest._extractLibrariesFromJSON(configFile, rootPath); + let libraries = extractLibrariesFromJSON(configFile, rootPath); expect(libraries.length).toBe(0); }); it('extracts libraries when they are present and not empty', () => { const configFile = fixtures.singleLibraryCodegenConfig; - let libraries = underTest._extractLibrariesFromJSON(configFile, rootPath); + let libraries = extractLibrariesFromJSON(configFile, rootPath); expect(libraries.length).toBe(1); expect(libraries[0]).toEqual({ config: { @@ -93,10 +100,7 @@ describe('extractLibrariesFromJSON', () => { const configFile = fixtures.multipleLibrariesCodegenConfig; const myDependency = 'my-dependency'; const myDependencyPath = path.join(__dirname, myDependency); - let libraries = underTest._extractLibrariesFromJSON( - configFile, - myDependencyPath, - ); + let libraries = extractLibrariesFromJSON(configFile, myDependencyPath); expect(libraries.length).toBe(3); expect(libraries[0]).toEqual({ config: { @@ -132,7 +136,7 @@ describe('extractSupportedApplePlatforms', () => { __dirname, `../__test_fixtures__/${myDependency}`, ); - let platforms = underTest._extractSupportedApplePlatforms( + let platforms = extractSupportedApplePlatforms( myDependency, myDependencyPath, ); @@ -150,7 +154,7 @@ describe('extractSupportedApplePlatforms', () => { __dirname, `../__test_fixtures__/${myDependency}`, ); - let platforms = underTest._extractSupportedApplePlatforms( + let platforms = extractSupportedApplePlatforms( myDependency, myDependencyPath, ); @@ -194,7 +198,7 @@ describe('delete empty files and folders', () => { readFileSync: () => packageJson, })); - underTest._cleanupEmptyFilesAndFolders(targetFilepath); + cleanupEmptyFilesAndFolders(targetFilepath); expect(statSyncInvocationCount).toBe(1); expect(rmSyncInvocationCount).toBe(1); expect(rmdirSyncInvocationCount).toBe(0); @@ -228,7 +232,7 @@ describe('delete empty files and folders', () => { readFileSync: () => packageJson, })); - underTest._cleanupEmptyFilesAndFolders(targetFilepath); + cleanupEmptyFilesAndFolders(targetFilepath); expect(statSyncInvocationCount).toBe(1); expect(rmSyncInvocationCount).toBe(0); expect(rmdirSyncInvocationCount).toBe(0); @@ -267,7 +271,7 @@ describe('delete empty files and folders', () => { readFileSync: () => packageJson, })); - underTest._cleanupEmptyFilesAndFolders(targetFolder); + cleanupEmptyFilesAndFolders(targetFolder); expect(statSyncInvocationCount).toBe(1); expect(readdirInvocationCount).toBe(2); expect(rmSyncInvocationCount).toBe(0); @@ -317,7 +321,7 @@ describe('delete empty files and folders', () => { readFileSync: () => packageJson, })); - underTest._cleanupEmptyFilesAndFolders(targetFolder); + cleanupEmptyFilesAndFolders(targetFolder); expect(statSyncInvocation).toEqual([ path.normalize('build'), path.normalize('build/emptyFolder'), @@ -372,7 +376,7 @@ describe('delete empty files and folders', () => { readFileSync: () => packageJson, })); - underTest._cleanupEmptyFilesAndFolders(targetFolder); + cleanupEmptyFilesAndFolders(targetFolder); expect(statSyncInvocation).toEqual([ path.normalize('build'), path.normalize('build/emptyFolder1'), diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor.js b/packages/react-native/scripts/codegen/generate-artifacts-executor.js deleted file mode 100644 index 8c55707869e..00000000000 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor.js +++ /dev/null @@ -1,1130 +0,0 @@ -/** - * 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 codegenConfig key - * in a package.json file. - */ - -const utils = require('./codegen-utils'); -const generateSpecsCLIExecutor = require('./generate-specs-cli-executor'); -const {execSync} = require('child_process'); -const fs = require('fs'); -const glob = require('glob'); -const os = require('os'); -const path = require('path'); - -const REACT_NATIVE_REPOSITORY_ROOT = path.join( - __dirname, - '..', - '..', - '..', - '..', -); -const REACT_NATIVE_PACKAGE_ROOT_FOLDER = path.join(__dirname, '..', '..'); -const CODEGEN_REPO_PATH = `${REACT_NATIVE_REPOSITORY_ROOT}/packages/react-native-codegen`; -const RNCORE_CONFIGS = { - ios: path.join(REACT_NATIVE_PACKAGE_ROOT_FOLDER, 'ReactCommon'), - android: path.join( - REACT_NATIVE_PACKAGE_ROOT_FOLDER, - 'ReactAndroid', - 'build', - 'generated', - 'source', - 'codegen', - ), -}; -const CORE_LIBRARIES_WITH_OUTPUT_FOLDER = { - rncore: RNCORE_CONFIGS, - FBReactNativeSpec: { - ios: path.join( - REACT_NATIVE_PACKAGE_ROOT_FOLDER, - 'React', - 'FBReactNativeSpec', - ), - android: path.join( - REACT_NATIVE_PACKAGE_ROOT_FOLDER, - 'ReactAndroid', - 'build', - 'generated', - 'source', - 'codegen', - ), - }, -}; - -const packageJsonPath = path.join( - REACT_NATIVE_PACKAGE_ROOT_FOLDER, - 'package.json', -); -const packageJson = JSON.parse(fs.readFileSync(packageJsonPath)); -const REACT_NATIVE = packageJson.name; - -const TEMPLATES_FOLDER_PATH = path.join( - REACT_NATIVE_PACKAGE_ROOT_FOLDER, - 'scripts', - 'codegen', - 'templates', -); - -const MODULES_PROTOCOLS_H_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'RCTModulesConformingToProtocolsProviderH.template', -); - -const MODULES_PROTOCOLS_MM_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'RCTModulesConformingToProtocolsProviderMM.template', -); - -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', -); - -const MODULE_PROVIDERS_H_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'RCTModuleProvidersH.template', -); - -const MODULE_PROVIDERS_MM_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'RCTModuleProvidersMM.template', -); - -const APP_DEPENDENCY_PROVIDER_H_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'RCTAppDependencyProviderH.template', -); - -const APP_DEPENDENCY_PROVIDER_MM_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'RCTAppDependencyProviderMM.template', -); - -const APP_DEPENDENCY_PROVIDER_PODSPEC_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'ReactAppDependencyProvider.podspec.template', -); - -const REACT_CODEGEN_PODSPEC_TEMPLATE_PATH = path.join( - TEMPLATES_FOLDER_PATH, - 'ReactCodegen.podspec.template', -); - -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}`); -}; - -// HELPERS - -function pkgJsonIncludesGeneratedCode(pkgJson) { - return pkgJson.codegenConfig && pkgJson.codegenConfig.includesGeneratedCode; -} - -function isReactNativeCoreLibrary(libraryName) { - return libraryName in CORE_LIBRARIES_WITH_OUTPUT_FOLDER; -} - -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 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": "." - } - } - `); -} - -// Reading Libraries -function extractLibrariesFromConfigurationArray(configFile, dependencyPath) { - return configFile.codegenConfig.libraries.map(config => { - return { - config, - libraryPath: dependencyPath, - }; - }); -} - -function extractLibrariesFromJSON(configFile, dependencyPath) { - if (configFile.codegenConfig == null) { - return []; - } - codegenLog(`Found ${configFile.name}`); - if (configFile.codegenConfig.libraries == null) { - const config = configFile.codegenConfig; - return [ - { - config, - libraryPath: dependencyPath, - }, - ]; - } else { - printDeprecationWarningIfNeeded(configFile.name); - return extractLibrariesFromConfigurationArray(configFile, dependencyPath); - } -} - -const APPLE_PLATFORMS = ['ios', 'macos', 'tvos', 'visionos']; - -// Cocoapods specific platform keys -function getCocoaPodsPlatformKey(platformName) { - if (platformName === 'macos') { - return 'osx'; - } - return platformName; -} - -function extractSupportedApplePlatforms(dependency, dependencyPath) { - codegenLog('Searching for podspec in the project dependencies.', true); - const podspecs = glob.sync('*.podspec', {cwd: dependencyPath}); - - if (podspecs.length === 0) { - return; - } - - // Take the first podspec found - const podspec = fs.readFileSync( - path.join(dependencyPath, podspecs[0]), - 'utf8', - ); - - /** - * Podspec can have platforms defined in two ways: - * 1. `spec.platforms = { :ios => "11.0", :tvos => "11.0" }` - * 2. `s.ios.deployment_target = "11.0"` - * `s.tvos.deployment_target = "11.0"` - */ - const supportedPlatforms = podspec - .split('\n') - .filter( - line => line.includes('platform') || line.includes('deployment_target'), - ) - .join(''); - - // Generate a map of supported platforms { [platform]: true/false } - const supportedPlatformsMap = APPLE_PLATFORMS.reduce( - (acc, platform) => ({ - ...acc, - [platform]: supportedPlatforms.includes( - getCocoaPodsPlatformKey(platform), - ), - }), - {}, - ); - - const supportedPlatformsList = Object.keys(supportedPlatformsMap).filter( - key => supportedPlatformsMap[key], - ); - - if (supportedPlatformsList.length > 0) { - codegenLog( - `Supported Apple platforms: ${supportedPlatformsList.join( - ', ', - )} for ${dependency}`, - ); - } - - return supportedPlatformsMap; -} - -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 findLibrariesFromReactNativeConfig(projectRoot) { - const rnConfigFileName = 'react-native.config.js'; - - codegenLog( - `Searching for codegen-enabled libraries in ${rnConfigFileName}`, - true, - ); - - const rnConfigFilePath = path.resolve(projectRoot, rnConfigFileName); - - if (!fs.existsSync(rnConfigFilePath)) { - return []; - } - const rnConfig = require(rnConfigFilePath); - - if (rnConfig.dependencies == null) { - 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 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); -} - -// CodeGen -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', - }); -} - -function readOutputDirFromPkgJson(pkgJson, platform) { - const codegenConfig = pkgJson.codegenConfig; - if (codegenConfig == null || typeof codegenConfig !== 'object') { - return null; - } - const outputDir = codegenConfig.outputDir; - if (outputDir == null) { - return null; - } - if (typeof outputDir === 'string') { - return outputDir; - } - if (typeof outputDir === 'object') { - return outputDir[platform]; - } - return null; -} - -function defaultOutputPathForAndroid(baseOutputPath) { - return path.join( - baseOutputPath, - 'android', - 'app', - 'build', - 'generated', - 'source', - 'codegen', - ); -} - -function defaultOutputPathForIOS(baseOutputPath) { - return path.join(baseOutputPath, 'build', 'generated', 'ios'); -} - -function computeOutputPath(projectRoot, baseOutputPath, pkgJson, platform) { - if (baseOutputPath == null) { - const outputDirFromPkgJson = readOutputDirFromPkgJson(pkgJson, platform); - if (outputDirFromPkgJson != null) { - baseOutputPath = path.join(projectRoot, outputDirFromPkgJson); - } else { - baseOutputPath = projectRoot; - } - } - if (pkgJsonIncludesGeneratedCode(pkgJson)) { - // Don't create nested directories for libraries to make importing generated headers easier. - return baseOutputPath; - } - if (platform === 'android') { - return defaultOutputPathForAndroid(baseOutputPath); - } - if (platform === 'ios') { - return defaultOutputPathForIOS(baseOutputPath); - } - return baseOutputPath; -} - -function reactNativeCoreLibraryOutputPath(libraryName, platform) { - return CORE_LIBRARIES_WITH_OUTPUT_FOLDER[libraryName] - ? CORE_LIBRARIES_WITH_OUTPUT_FOLDER[libraryName][platform] - : null; -} - -function generateSchemaInfo(library, platform) { - const pathToJavaScriptSources = path.join( - library.libraryPath, - library.config.jsSrcsDir, - ); - codegenLog(`Processing ${library.config.name}`); - - const supportedApplePlatforms = extractSupportedApplePlatforms( - library.config.name, - library.libraryPath, - ); - - // Generate one schema for the entire library... - return { - library: library, - supportedApplePlatforms, - schema: utils - .getCombineJSToSchema() - .combineSchemasInFileList( - [pathToJavaScriptSources], - platform, - /NativeSampleTurboModule/, - ), - }; -} - -function shouldSkipGenerationForRncore(schemaInfo, platform) { - if (platform !== 'ios' || schemaInfo.library.config.name !== 'rncore') { - return false; - } - const rncoreOutputPath = CORE_LIBRARIES_WITH_OUTPUT_FOLDER.rncore.ios; - const rncoreAbsolutePath = path.resolve(rncoreOutputPath); - return ( - rncoreAbsolutePath.includes('node_modules') && - fs.existsSync(rncoreAbsolutePath) && - fs.readdirSync(rncoreAbsolutePath).length > 0 - ); -} - -function shouldSkipGenerationForFBReactNativeSpec(schemaInfo, platform) { - if ( - platform !== 'ios' || - schemaInfo.library.config.name !== 'FBReactNativeSpec' - ) { - return false; - } - - const fbReactNativeSpecOutputPath = - CORE_LIBRARIES_WITH_OUTPUT_FOLDER.FBReactNativeSpec.ios; - const fbReactNativeSpecAbsolutePath = path.resolve( - fbReactNativeSpecOutputPath, - ); - return ( - fbReactNativeSpecAbsolutePath.includes('node_modules') && - fs.existsSync(fbReactNativeSpecAbsolutePath) && - fs.readdirSync(fbReactNativeSpecAbsolutePath).length > 0 - ); -} - -function generateCode(outputPath, schemaInfo, includesGeneratedCode, platform) { - if (shouldSkipGenerationForRncore(schemaInfo, platform)) { - codegenLog( - '[Codegen - rncore] Skipping iOS code generation for rncore as it has been generated already.', - true, - ); - return; - } - - if (shouldSkipGenerationForFBReactNativeSpec(schemaInfo, platform)) { - codegenLog( - '[Codegen - FBReactNativeSpec] Skipping iOS code generation for FBReactNativeSpec as it has been generated already.', - true, - ); - return; - } - - const libraryName = schemaInfo.library.config.name; - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), libraryName)); - const tmpOutputDir = path.join(tmpDir, 'out'); - fs.mkdirSync(tmpOutputDir, {recursive: true}); - - codegenLog(`Generating Native Code for ${libraryName} - ${platform}`); - const useLocalIncludePaths = includesGeneratedCode; - generateSpecsCLIExecutor.generateSpecFromInMemorySchema( - platform, - schemaInfo.schema, - tmpOutputDir, - libraryName, - 'com.facebook.fbreact.specs', - schemaInfo.library.config.type, - useLocalIncludePaths, - ); - - // Finally, copy artifacts to the final output directory. - const outputDir = - reactNativeCoreLibraryOutputPath(libraryName, platform) ?? outputPath; - fs.mkdirSync(outputDir, {recursive: true}); - fs.cpSync(tmpOutputDir, outputDir, {recursive: true}); - codegenLog(`Generated artifacts: ${outputDir}`); -} - -function generateSchemaInfos(libraries) { - return libraries.map(generateSchemaInfo); -} - -function generateNativeCode( - outputPath, - schemaInfos, - includesGeneratedCode, - platform, -) { - return schemaInfos.map(schemaInfo => { - generateCode(outputPath, schemaInfo, includesGeneratedCode, platform); - }); -} - -function mustGenerateNativeCode(includeLibraryPath, schemaInfo) { - // If library's 'codegenConfig' sets 'includesGeneratedCode' to 'true', - // then we assume that native code is shipped with the library, - // and we don't need to generate it. - return ( - schemaInfo.library.libraryPath === includeLibraryPath || - !schemaInfo.library.config.includesGeneratedCode - ); -} - -function findCodegenEnabledLibraries(pkgJson, projectRoot) { - const projectLibraries = findProjectRootLibraries(pkgJson, projectRoot); - if (pkgJsonIncludesGeneratedCode(pkgJson)) { - return projectLibraries; - } else { - return [ - ...projectLibraries, - ...findExternalLibraries(pkgJson, projectRoot), - ...findLibrariesFromReactNativeConfig(projectRoot), - ]; - } -} - -function generateCustomURLHandlers(libraries, outputDir) { - const customImageURLLoaderClasses = libraries - .flatMap( - library => - library?.config?.ios?.modulesConformingToProtocol?.RCTImageURLLoader, - ) - .filter(Boolean) - .map(className => `@"${className}"`) - .join(',\n\t\t\t'); - - const customImageDataDecoderClasses = libraries - .flatMap( - library => - library?.config?.ios?.modulesConformingToProtocol?.RCTImageDataDecoder, - ) - .filter(Boolean) - .map(className => `@"${className}"`) - .join(',\n\t\t\t'); - - const customURLHandlerClasses = libraries - .flatMap( - library => - library?.config?.ios?.modulesConformingToProtocol?.RCTURLRequestHandler, - ) - .filter(Boolean) - .map(className => `@"${className}"`) - .join(',\n\t\t\t'); - - const template = fs.readFileSync(MODULES_PROTOCOLS_MM_TEMPLATE_PATH, 'utf8'); - const finalMMFile = template - .replace(/{imageURLLoaderClassNames}/, customImageURLLoaderClasses) - .replace(/{imageDataDecoderClassNames}/, customImageDataDecoderClasses) - .replace(/{requestHandlersClassNames}/, customURLHandlerClasses); - - fs.mkdirSync(outputDir, {recursive: true}); - - fs.writeFileSync( - path.join(outputDir, 'RCTModulesConformingToProtocolsProvider.mm'), - finalMMFile, - ); - - const templateH = fs.readFileSync(MODULES_PROTOCOLS_H_TEMPLATE_PATH, 'utf8'); - fs.writeFileSync( - path.join(outputDir, 'RCTModulesConformingToProtocolsProvider.h'), - templateH, - ); -} - -function generateAppDependencyProvider(outputDir) { - fs.mkdirSync(outputDir, {recursive: true}); - codegenLog('Generating RCTAppDependencyProvider'); - - const templateH = fs.readFileSync( - APP_DEPENDENCY_PROVIDER_H_TEMPLATE_PATH, - 'utf8', - ); - const finalPathH = path.join(outputDir, 'RCTAppDependencyProvider.h'); - fs.writeFileSync(finalPathH, templateH); - codegenLog(`Generated artifact: ${finalPathH}`); - - const templateMM = fs.readFileSync( - APP_DEPENDENCY_PROVIDER_MM_TEMPLATE_PATH, - 'utf8', - ); - const finalPathMM = path.join(outputDir, 'RCTAppDependencyProvider.mm'); - fs.writeFileSync(finalPathMM, templateMM); - codegenLog(`Generated artifact: ${finalPathMM}`); - - // Generate the podspec file - const templatePodspec = fs - .readFileSync(APP_DEPENDENCY_PROVIDER_PODSPEC_TEMPLATE_PATH, 'utf8') - .replace(/{react-native-version}/, packageJson.version) - .replace(/{react-native-licence}/, packageJson.license); - const finalPathPodspec = path.join( - outputDir, - 'ReactAppDependencyProvider.podspec', - ); - fs.writeFileSync(finalPathPodspec, templatePodspec); - codegenLog(`Generated podspec: ${finalPathPodspec}`); -} - -function generateRCTModuleProviders( - projectRoot, - pkgJson, - libraries, - outputDir, -) { - fs.mkdirSync(outputDir, {recursive: true}); - // Generate Header File - codegenLog('Generating RCTModulesProvider.h'); - const templateH = fs.readFileSync(MODULE_PROVIDERS_H_TEMPLATE_PATH, 'utf8'); - const finalPathH = path.join(outputDir, 'RCTModuleProviders.h'); - fs.writeFileSync(finalPathH, templateH); - codegenLog(`Generated artifact: ${finalPathH}`); - - codegenLog('Generating RCTModuleProviders.mm'); - let modulesInLibraries = {}; - - let app = pkgJson.codegenConfig - ? {config: pkgJson.codegenConfig, libraryPath: projectRoot} - : null; - libraries - .concat(app) - .filter(Boolean) - .forEach(({config, libraryPath}) => { - if ( - isReactNativeCoreLibrary(config.name) || - config.type === 'components' - ) { - return; - } - - const libraryName = JSON.parse( - fs.readFileSync(path.join(libraryPath, 'package.json')), - ).name; - if (config.ios?.modulesProvider) { - modulesInLibraries[libraryName] = Object.keys( - config.ios?.modulesProvider, - ).map(moduleName => { - return { - moduleName, - className: config.ios?.modulesProvider[moduleName], - }; - }); - } - }); - - const modulesMapping = Object.keys(modulesInLibraries) - .flatMap(library => { - const modules = modulesInLibraries[library]; - return modules.map(({moduleName, className}) => { - return `\t\t\t@"${moduleName}": @"${className}", // ${library}`; - }); - }) - .join('\n'); - - // Generate implementation file - const templateMM = fs - .readFileSync(MODULE_PROVIDERS_MM_TEMPLATE_PATH, 'utf8') - .replace(/{moduleMapping}/, modulesMapping); - const finalPathMM = path.join(outputDir, 'RCTModuleProviders.mm'); - fs.writeFileSync(finalPathMM, templateMM); - codegenLog(`Generated artifact: ${finalPathMM}`); -} - -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 = {}; - libraries.forEach(({config, libraryPath}) => { - if (isReactNativeCoreLibrary(config.name) || config.type === 'modules') { - return; - } - - const libraryName = JSON.parse( - fs.readFileSync(path.join(libraryPath, 'package.json')), - ).name; - if (config.ios?.componentProvider) { - componentsInLibraries[libraryName] = Object.keys( - config.ios?.componentProvider, - ).map(componentName => { - return { - componentName, - className: config.ios?.componentProvider[componentName], - }; - }); - return; - } - 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\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 ' -// and ends with 'Cls(void)'. Return the string between the two. -function findRCTComponentViewProtocolClass(filepath) { - const fileContent = fs.readFileSync(filepath, 'utf8'); - const regex = /Class (.*)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)).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; -} - -// 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 generateRNCoreComponentsIOS(projectRoot /*: string */) /*: void*/ { - const ios = 'ios'; - buildCodegenIfNeeded(); - const pkgJson = readPkgJsonInDirectory(projectRoot); - const rncoreLib = findProjectRootLibraries(pkgJson, projectRoot).filter( - library => library.config.name === 'rncore', - )[0]; - if (!rncoreLib) { - throw new Error( - "[Codegen] Can't find rncore library. Failed to generate rncore artifacts", - ); - } - const rncoreSchemaInfo = generateSchemaInfo(rncoreLib, ios); - generateCode('', rncoreSchemaInfo, false, ios); -} - -function generateFBReactNativeSpecIOS(projectRoot /*: string */) /*: void*/ { - const ios = 'ios'; - buildCodegenIfNeeded(); - const pkgJson = readPkgJsonInDirectory(projectRoot); - const fbReactNativeSpecLib = findProjectRootLibraries( - pkgJson, - projectRoot, - ).filter(library => library.config.name === 'FBReactNativeSpec')[0]; - if (!fbReactNativeSpecLib) { - throw new Error( - "[Codegen] Can't find FBReactNativeSpec library. Failed to generate rncore artifacts", - ); - } - const fbReactNativeSchemaInfo = generateSchemaInfo(fbReactNativeSpecLib, ios); - generateCode('', fbReactNativeSchemaInfo, false, ios); -} - -function generateReactCodegenPodspec( - appPath, - appPkgJson, - outputPath, - baseOutputPath, -) { - const inputFiles = getInputFiles(appPath, appPkgJson); - const codegenScript = codegenScripts(appPath, baseOutputPath); - const template = fs.readFileSync(REACT_CODEGEN_PODSPEC_TEMPLATE_PATH, 'utf8'); - const finalPodspec = template - .replace(/{react-native-version}/, packageJson.version) - .replace(/{input-files}/, inputFiles) - .replace(/{codegen-script}/, codegenScript); - const finalPathPodspec = path.join(outputPath, 'ReactCodegen.podspec'); - fs.writeFileSync(finalPathPodspec, finalPodspec); - codegenLog(`Generated podspec: ${finalPathPodspec}`); -} - -function getInputFiles(appPath, appPkgJSon) { - const jsSrcsDir = appPkgJSon.codegenConfig?.jsSrcsDir; - if (!jsSrcsDir) { - return '[]'; - } - - const xcodeproj = String( - execSync(`find ${appPath} -type d -name "*.xcodeproj"`), - ) - .trim() - .split('\n') - .filter( - projectPath => - !projectPath.includes('/Pods/') && // exclude Pods/Pods.xcodeproj - !projectPath.includes('/node_modules/'), // exclude all the xcodeproj in node_modules of libraries - )[0]; - const jsFiles = '-name "Native*.js" -or -name "*NativeComponent.js"'; - const tsFiles = '-name "Native*.ts" -or -name "*NativeComponent.ts"'; - const findCommand = `find ${path.join(appPath, jsSrcsDir)} -type f \\( ${jsFiles} -or ${tsFiles} \\)`; - const list = String(execSync(findCommand)) - .trim() - .split('\n') - .sort() - .map(filepath => `"\${PODS_ROOT}/${path.relative(xcodeproj, filepath)}"`) - .join(',\n'); - return `[${list}]`; -} - -function codegenScripts(appPath, outputPath) { - const relativeAppPath = path.relative(outputPath, appPath); - return `<<-SCRIPT -pushd "$PODS_ROOT/../" > /dev/null -RCT_SCRIPT_POD_INSTALLATION_ROOT=$(pwd) -popd >/dev/null - -export RCT_SCRIPT_RN_DIR="$RCT_SCRIPT_POD_INSTALLATION_ROOT/${path.relative(outputPath, REACT_NATIVE_PACKAGE_ROOT_FOLDER)}" -export RCT_SCRIPT_APP_PATH="$RCT_SCRIPT_POD_INSTALLATION_ROOT/${relativeAppPath.length === 0 ? '.' : relativeAppPath}", -export RCT_SCRIPT_OUTPUT_DIR="$RCT_SCRIPT_POD_INSTALLATION_ROOT", -export RCT_SCRIPT_TYPE="withCodegenDiscovery", - -SCRIPT_PHASES_SCRIPT="$RCT_SCRIPT_RN_DIR/scripts/react_native_pods_utils/script_phases.sh" -WITH_ENVIRONMENT="$RCT_SCRIPT_RN_DIR/scripts/xcode/with-environment.sh" -/bin/sh -c "$WITH_ENVIRONMENT $SCRIPT_PHASES_SCRIPT" -SCRIPT`; -} - -// 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 projectRoot: the directory with the app source code, where the package.json lives. - * @parameter baseOutputPath: the base output path for the CodeGen. - * @parameter targetPlatform: the target platform. Supported values: 'android', 'ios', 'all'. - * @parameter source: the source that is invoking codegen. Supported values: 'app', 'library'. - * @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( - projectRoot, - targetPlatform, - baseOutputPath, - source, - runReactNativeCodegen = true, -) { - try { - codegenLog(`Analyzing ${path.join(projectRoot, 'package.json')}`); - - const supportedPlatforms = ['android', 'ios']; - if ( - targetPlatform !== 'all' && - !supportedPlatforms.includes(targetPlatform) - ) { - throw new Error( - `Invalid target platform: ${targetPlatform}. Supported values are: ${supportedPlatforms.join( - ', ', - )}, all`, - ); - } - - const pkgJson = readPkgJsonInDirectory(projectRoot); - - if (runReactNativeCodegen) { - buildCodegenIfNeeded(); - } - - const libraries = findCodegenEnabledLibraries(pkgJson, projectRoot); - - if (libraries.length === 0) { - codegenLog('No codegen-enabled libraries found.', true); - return; - } - - let platforms = - targetPlatform === 'all' ? supportedPlatforms : [targetPlatform]; - - for (const platform of platforms) { - const outputPath = computeOutputPath( - projectRoot, - baseOutputPath, - pkgJson, - platform, - ); - - if (runReactNativeCodegen) { - const schemaInfos = generateSchemaInfos(libraries); - generateNativeCode( - outputPath, - schemaInfos.filter(schemaInfo => - mustGenerateNativeCode(projectRoot, schemaInfo), - ), - pkgJsonIncludesGeneratedCode(pkgJson), - platform, - ); - } - - if (source === 'app') { - // These components are only required by apps, not by libraries - generateRCTThirdPartyComponents(libraries, outputPath); - generateRCTModuleProviders(projectRoot, pkgJson, libraries, outputPath); - generateCustomURLHandlers(libraries, outputPath); - generateAppDependencyProvider(outputPath); - } - generateReactCodegenPodspec( - projectRoot, - pkgJson, - outputPath, - baseOutputPath, - ); - - cleanupEmptyFilesAndFolders(outputPath); - } - } catch (err) { - codegenLog(err); - process.exitCode = 1; - } - - codegenLog('Done.', true); - return; -} - -module.exports = { - execute, - generateRNCoreComponentsIOS, - generateFBReactNativeSpecIOS, - // exported for testing purposes only: - _extractLibrariesFromJSON: extractLibrariesFromJSON, - _cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders, - _extractSupportedApplePlatforms: extractSupportedApplePlatforms, -}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/constants.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/constants.js new file mode 100644 index 00000000000..6f828737daa --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/constants.js @@ -0,0 +1,80 @@ +/** + * 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 fs = require('fs'); +const path = require('path'); + +const REACT_NATIVE_REPOSITORY_ROOT = path.join( + __dirname, + '..', + '..', + '..', + '..', + '..', +); + +const REACT_NATIVE_PACKAGE_ROOT_FOLDER = path.join(__dirname, '..', '..', '..'); +const CODEGEN_REPO_PATH = `${REACT_NATIVE_REPOSITORY_ROOT}/packages/react-native-codegen`; + +const RNCORE_CONFIGS = { + ios: path.join(REACT_NATIVE_PACKAGE_ROOT_FOLDER, 'ReactCommon'), + android: path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'ReactAndroid', + 'build', + 'generated', + 'source', + 'codegen', + ), +}; + +const CORE_LIBRARIES_WITH_OUTPUT_FOLDER = { + rncore: RNCORE_CONFIGS, + FBReactNativeSpec: { + ios: path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'React', + 'FBReactNativeSpec', + ), + android: path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'ReactAndroid', + 'build', + 'generated', + 'source', + 'codegen', + ), + }, +}; + +const packageJsonPath = path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'package.json', +); + +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath)); +const REACT_NATIVE = packageJson.name; + +const TEMPLATES_FOLDER_PATH = path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'scripts', + 'codegen', + 'templates', +); + +module.exports = { + CODEGEN_REPO_PATH, + CORE_LIBRARIES_WITH_OUTPUT_FOLDER, + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + REACT_NATIVE, + TEMPLATES_FOLDER_PATH, + packageJson, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateAppDependencyProvider.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateAppDependencyProvider.js new file mode 100644 index 00000000000..8e3e60615ad --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateAppDependencyProvider.js @@ -0,0 +1,66 @@ +/** + * 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 {TEMPLATES_FOLDER_PATH, packageJson} = require('./constants'); +const {codegenLog} = require('./utils'); +const fs = require('fs'); +const path = require('path'); + +const APP_DEPENDENCY_PROVIDER_H_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'RCTAppDependencyProviderH.template', +); + +const APP_DEPENDENCY_PROVIDER_MM_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'RCTAppDependencyProviderMM.template', +); + +const APP_DEPENDENCY_PROVIDER_PODSPEC_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'ReactAppDependencyProvider.podspec.template', +); + +function generateAppDependencyProvider(outputDir) { + fs.mkdirSync(outputDir, {recursive: true}); + codegenLog('Generating RCTAppDependencyProvider'); + + const templateH = fs.readFileSync( + APP_DEPENDENCY_PROVIDER_H_TEMPLATE_PATH, + 'utf8', + ); + const finalPathH = path.join(outputDir, 'RCTAppDependencyProvider.h'); + fs.writeFileSync(finalPathH, templateH); + codegenLog(`Generated artifact: ${finalPathH}`); + + const templateMM = fs.readFileSync( + APP_DEPENDENCY_PROVIDER_MM_TEMPLATE_PATH, + 'utf8', + ); + const finalPathMM = path.join(outputDir, 'RCTAppDependencyProvider.mm'); + fs.writeFileSync(finalPathMM, templateMM); + codegenLog(`Generated artifact: ${finalPathMM}`); + + // Generate the podspec file + const templatePodspec = fs + .readFileSync(APP_DEPENDENCY_PROVIDER_PODSPEC_TEMPLATE_PATH, 'utf8') + .replace(/{react-native-version}/, packageJson.version) + .replace(/{react-native-licence}/, packageJson.license); + const finalPathPodspec = path.join( + outputDir, + 'ReactAppDependencyProvider.podspec', + ); + fs.writeFileSync(finalPathPodspec, templatePodspec); + codegenLog(`Generated podspec: ${finalPathPodspec}`); +} + +module.exports = { + generateAppDependencyProvider, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateCustomURLHandlers.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateCustomURLHandlers.js new file mode 100644 index 00000000000..bfabac31bab --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateCustomURLHandlers.js @@ -0,0 +1,76 @@ +/** + * 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 {TEMPLATES_FOLDER_PATH} = require('./constants'); +const fs = require('fs'); +const path = require('path'); + +const MODULES_PROTOCOLS_H_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'RCTModulesConformingToProtocolsProviderH.template', +); + +const MODULES_PROTOCOLS_MM_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'RCTModulesConformingToProtocolsProviderMM.template', +); + +function generateCustomURLHandlers(libraries, outputDir) { + const customImageURLLoaderClasses = libraries + .flatMap( + library => + library?.config?.ios?.modulesConformingToProtocol?.RCTImageURLLoader, + ) + .filter(Boolean) + .map(className => `@"${className}"`) + .join(',\n\t\t'); + + const customImageDataDecoderClasses = libraries + .flatMap( + library => + library?.config?.ios?.modulesConformingToProtocol?.RCTImageDataDecoder, + ) + .filter(Boolean) + .map(className => `@"${className}"`) + .join(',\n\t\t'); + + const customURLHandlerClasses = libraries + .flatMap( + library => + library?.config?.ios?.modulesConformingToProtocol?.RCTURLRequestHandler, + ) + .filter(Boolean) + .map(className => `@"${className}"`) + .join(',\n\t\t'); + + const template = fs.readFileSync(MODULES_PROTOCOLS_MM_TEMPLATE_PATH, 'utf8'); + const finalMMFile = template + .replace(/{imageURLLoaderClassNames}/, customImageURLLoaderClasses) + .replace(/{imageDataDecoderClassNames}/, customImageDataDecoderClasses) + .replace(/{requestHandlersClassNames}/, customURLHandlerClasses); + + fs.mkdirSync(outputDir, {recursive: true}); + + fs.writeFileSync( + path.join(outputDir, 'RCTModulesConformingToProtocolsProvider.mm'), + finalMMFile, + ); + + const templateH = fs.readFileSync(MODULES_PROTOCOLS_H_TEMPLATE_PATH, 'utf8'); + fs.writeFileSync( + path.join(outputDir, 'RCTModulesConformingToProtocolsProvider.h'), + templateH, + ); +} + +module.exports = { + generateCustomURLHandlers, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateFBReactNativeSpecIOS.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateFBReactNativeSpecIOS.js new file mode 100644 index 00000000000..9b029c58943 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateFBReactNativeSpecIOS.js @@ -0,0 +1,39 @@ +/** + * 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 {generateCode} = require('./generateNativeCode'); +const {generateSchemaInfo} = require('./generateSchemaInfos'); +const { + buildCodegenIfNeeded, + findProjectRootLibraries, + readPkgJsonInDirectory, +} = require('./utils'); + +function generateFBReactNativeSpecIOS(projectRoot /*: string */) /*: void*/ { + const ios = 'ios'; + buildCodegenIfNeeded(); + const pkgJson = readPkgJsonInDirectory(projectRoot); + const fbReactNativeSpecLib = findProjectRootLibraries( + pkgJson, + projectRoot, + ).filter(library => library.config.name === 'FBReactNativeSpec')[0]; + if (!fbReactNativeSpecLib) { + throw new Error( + "[Codegen] Can't find FBReactNativeSpec library. Failed to generate rncore artifacts", + ); + } + const fbReactNativeSchemaInfo = generateSchemaInfo(fbReactNativeSpecLib, ios); + generateCode('', fbReactNativeSchemaInfo, false, ios); +} + +module.exports = { + generateFBReactNativeSpecIOS, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateNativeCode.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateNativeCode.js new file mode 100644 index 00000000000..f505a32c185 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateNativeCode.js @@ -0,0 +1,114 @@ +/** + * 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 generateSpecsCLIExecutor = require('../generate-specs-cli-executor'); +const {CORE_LIBRARIES_WITH_OUTPUT_FOLDER} = require('./constants'); +const {codegenLog} = require('./utils'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function generateNativeCode( + outputPath, + schemaInfos, + includesGeneratedCode, + platform, +) { + return schemaInfos.map(schemaInfo => { + generateCode(outputPath, schemaInfo, includesGeneratedCode, platform); + }); +} + +function generateCode(outputPath, schemaInfo, includesGeneratedCode, platform) { + if (shouldSkipGenerationForRncore(schemaInfo, platform)) { + codegenLog( + '[Codegen - rncore] Skipping iOS code generation for rncore as it has been generated already.', + true, + ); + return; + } + + if (shouldSkipGenerationForFBReactNativeSpec(schemaInfo, platform)) { + codegenLog( + '[Codegen - FBReactNativeSpec] Skipping iOS code generation for FBReactNativeSpec as it has been generated already.', + true, + ); + return; + } + + const libraryName = schemaInfo.library.config.name; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), libraryName)); + const tmpOutputDir = path.join(tmpDir, 'out'); + fs.mkdirSync(tmpOutputDir, {recursive: true}); + + codegenLog(`Generating Native Code for ${libraryName} - ${platform}`); + const useLocalIncludePaths = includesGeneratedCode; + generateSpecsCLIExecutor.generateSpecFromInMemorySchema( + platform, + schemaInfo.schema, + tmpOutputDir, + libraryName, + 'com.facebook.fbreact.specs', + schemaInfo.library.config.type, + useLocalIncludePaths, + ); + + // Finally, copy artifacts to the final output directory. + const outputDir = + reactNativeCoreLibraryOutputPath(libraryName, platform) ?? outputPath; + fs.mkdirSync(outputDir, {recursive: true}); + fs.cpSync(tmpOutputDir, outputDir, {recursive: true}); + codegenLog(`Generated artifacts: ${outputDir}`); +} + +function shouldSkipGenerationForRncore(schemaInfo, platform) { + if (platform !== 'ios' || schemaInfo.library.config.name !== 'rncore') { + return false; + } + const rncoreOutputPath = CORE_LIBRARIES_WITH_OUTPUT_FOLDER.rncore.ios; + const rncoreAbsolutePath = path.resolve(rncoreOutputPath); + return ( + rncoreAbsolutePath.includes('node_modules') && + fs.existsSync(rncoreAbsolutePath) && + fs.readdirSync(rncoreAbsolutePath).length > 0 + ); +} + +function reactNativeCoreLibraryOutputPath(libraryName, platform) { + return CORE_LIBRARIES_WITH_OUTPUT_FOLDER[libraryName] + ? CORE_LIBRARIES_WITH_OUTPUT_FOLDER[libraryName][platform] + : null; +} + +function shouldSkipGenerationForFBReactNativeSpec(schemaInfo, platform) { + if ( + platform !== 'ios' || + schemaInfo.library.config.name !== 'FBReactNativeSpec' + ) { + return false; + } + + const fbReactNativeSpecOutputPath = + CORE_LIBRARIES_WITH_OUTPUT_FOLDER.FBReactNativeSpec.ios; + const fbReactNativeSpecAbsolutePath = path.resolve( + fbReactNativeSpecOutputPath, + ); + return ( + fbReactNativeSpecAbsolutePath.includes('node_modules') && + fs.existsSync(fbReactNativeSpecAbsolutePath) && + fs.readdirSync(fbReactNativeSpecAbsolutePath).length > 0 + ); +} + +module.exports = { + generateNativeCode, + generateCode, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTModuleProviders.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTModuleProviders.js new file mode 100644 index 00000000000..b3c4fdc7189 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTModuleProviders.js @@ -0,0 +1,92 @@ +/** + * 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 {TEMPLATES_FOLDER_PATH} = require('./constants'); +const {codegenLog, isReactNativeCoreLibrary} = require('./utils'); +const fs = require('fs'); +const path = require('path'); + +const MODULE_PROVIDERS_H_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'RCTModuleProvidersH.template', +); + +const MODULE_PROVIDERS_MM_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'RCTModuleProvidersMM.template', +); + +function generateRCTModuleProviders( + projectRoot, + pkgJson, + libraries, + outputDir, +) { + fs.mkdirSync(outputDir, {recursive: true}); + // Generate Header File + codegenLog('Generating RCTModulesProvider.h'); + const templateH = fs.readFileSync(MODULE_PROVIDERS_H_TEMPLATE_PATH, 'utf8'); + const finalPathH = path.join(outputDir, 'RCTModuleProviders.h'); + fs.writeFileSync(finalPathH, templateH); + codegenLog(`Generated artifact: ${finalPathH}`); + + codegenLog('Generating RCTModuleProviders.mm'); + let modulesInLibraries = {}; + + let app = pkgJson.codegenConfig + ? {config: pkgJson.codegenConfig, libraryPath: projectRoot} + : null; + libraries + .concat(app) + .filter(Boolean) + .forEach(({config, libraryPath}) => { + if ( + isReactNativeCoreLibrary(config.name) || + config.type === 'components' + ) { + return; + } + + const libraryName = JSON.parse( + fs.readFileSync(path.join(libraryPath, 'package.json')), + ).name; + if (config.ios?.modulesProvider) { + modulesInLibraries[libraryName] = Object.keys( + config.ios?.modulesProvider, + ).map(moduleName => { + return { + moduleName, + className: config.ios?.modulesProvider[moduleName], + }; + }); + } + }); + + const modulesMapping = Object.keys(modulesInLibraries) + .flatMap(library => { + const modules = modulesInLibraries[library]; + return modules.map(({moduleName, className}) => { + return `\t\t@"${moduleName}": @"${className}", // ${library}`; + }); + }) + .join('\n'); + + // Generate implementation file + const templateMM = fs + .readFileSync(MODULE_PROVIDERS_MM_TEMPLATE_PATH, 'utf8') + .replace(/{moduleMapping}/, modulesMapping); + const finalPathMM = path.join(outputDir, 'RCTModuleProviders.mm'); + fs.writeFileSync(finalPathMM, templateMM); + codegenLog(`Generated artifact: ${finalPathMM}`); +} + +module.exports = { + generateRCTModuleProviders, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js new file mode 100644 index 00000000000..e73cc8cea12 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js @@ -0,0 +1,168 @@ +/** + * 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 {TEMPLATES_FOLDER_PATH} = require('./constants'); +const {codegenLog, isReactNativeCoreLibrary} = 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 = {}; + libraries.forEach(({config, libraryPath}) => { + if (isReactNativeCoreLibrary(config.name) || config.type === 'modules') { + return; + } + + const libraryName = JSON.parse( + fs.readFileSync(path.join(libraryPath, 'package.json')), + ).name; + if (config.ios?.componentProvider) { + componentsInLibraries[libraryName] = Object.keys( + config.ios?.componentProvider, + ).map(componentName => { + return { + componentName, + className: config.ios?.componentProvider[componentName], + }; + }); + return; + } + 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 ' +// and ends with 'Cls(void)'. Return the string between the two. +function findRCTComponentViewProtocolClass(filepath) { + const fileContent = fs.readFileSync(filepath, 'utf8'); + const regex = /Class (.*)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)).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, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRNCoreComponentsIOS.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRNCoreComponentsIOS.js new file mode 100644 index 00000000000..ced30a1a3c1 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRNCoreComponentsIOS.js @@ -0,0 +1,38 @@ +/** + * 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 {generateCode} = require('./generateNativeCode'); +const {generateSchemaInfo} = require('./generateSchemaInfos'); +const { + buildCodegenIfNeeded, + findProjectRootLibraries, + readPkgJsonInDirectory, +} = require('./utils'); + +function generateRNCoreComponentsIOS(projectRoot /*: string */) /*: void*/ { + const ios = 'ios'; + buildCodegenIfNeeded(); + const pkgJson = readPkgJsonInDirectory(projectRoot); + const rncoreLib = findProjectRootLibraries(pkgJson, projectRoot).filter( + library => library.config.name === 'rncore', + )[0]; + if (!rncoreLib) { + throw new Error( + "[Codegen] Can't find rncore library. Failed to generate rncore artifacts", + ); + } + const rncoreSchemaInfo = generateSchemaInfo(rncoreLib, ios); + generateCode('', rncoreSchemaInfo, false, ios); +} + +module.exports = { + generateRNCoreComponentsIOS, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateReactCodegenPodspec.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateReactCodegenPodspec.js new file mode 100644 index 00000000000..acbc691ff67 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateReactCodegenPodspec.js @@ -0,0 +1,93 @@ +/** + * 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 { + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + TEMPLATES_FOLDER_PATH, + packageJson, +} = require('./constants'); +const {codegenLog} = require('./utils'); +const {execSync} = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const REACT_CODEGEN_PODSPEC_TEMPLATE_PATH = path.join( + TEMPLATES_FOLDER_PATH, + 'ReactCodegen.podspec.template', +); + +function generateReactCodegenPodspec( + appPath, + appPkgJson, + outputPath, + baseOutputPath, +) { + const inputFiles = getInputFiles(appPath, appPkgJson); + const codegenScript = codegenScripts(appPath, baseOutputPath); + const template = fs.readFileSync(REACT_CODEGEN_PODSPEC_TEMPLATE_PATH, 'utf8'); + const finalPodspec = template + .replace(/{react-native-version}/, packageJson.version) + .replace(/{input-files}/, inputFiles) + .replace(/{codegen-script}/, codegenScript); + const finalPathPodspec = path.join(outputPath, 'ReactCodegen.podspec'); + fs.writeFileSync(finalPathPodspec, finalPodspec); + codegenLog(`Generated podspec: ${finalPathPodspec}`); +} + +function getInputFiles(appPath, appPkgJSon) { + const jsSrcsDir = appPkgJSon.codegenConfig?.jsSrcsDir; + if (!jsSrcsDir) { + return '[]'; + } + + const xcodeproj = String( + execSync(`find ${appPath} -type d -name "*.xcodeproj"`), + ) + .trim() + .split('\n') + .filter( + projectPath => + !projectPath.includes('/Pods/') && // exclude Pods/Pods.xcodeproj + !projectPath.includes('/node_modules/'), // exclude all the xcodeproj in node_modules of libraries + )[0]; + const jsFiles = '-name "Native*.js" -or -name "*NativeComponent.js"'; + const tsFiles = '-name "Native*.ts" -or -name "*NativeComponent.ts"'; + const findCommand = `find ${path.join(appPath, jsSrcsDir)} -type f \\( ${jsFiles} -or ${tsFiles} \\)`; + const list = String(execSync(findCommand)) + .trim() + .split('\n') + .sort() + .map(filepath => `"\${PODS_ROOT}/${path.relative(xcodeproj, filepath)}"`) + .join(',\n'); + return `[${list}]`; +} + +function codegenScripts(appPath, outputPath) { + const relativeAppPath = path.relative(outputPath, appPath); + return `<<-SCRIPT +pushd "$PODS_ROOT/../" > /dev/null +RCT_SCRIPT_POD_INSTALLATION_ROOT=$(pwd) +popd >/dev/null + +export RCT_SCRIPT_RN_DIR="$RCT_SCRIPT_POD_INSTALLATION_ROOT/${path.relative(outputPath, REACT_NATIVE_PACKAGE_ROOT_FOLDER)}" +export RCT_SCRIPT_APP_PATH="$RCT_SCRIPT_POD_INSTALLATION_ROOT/${relativeAppPath.length === 0 ? '.' : relativeAppPath}", +export RCT_SCRIPT_OUTPUT_DIR="$RCT_SCRIPT_POD_INSTALLATION_ROOT", +export RCT_SCRIPT_TYPE="withCodegenDiscovery", + +SCRIPT_PHASES_SCRIPT="$RCT_SCRIPT_RN_DIR/scripts/react_native_pods_utils/script_phases.sh" +WITH_ENVIRONMENT="$RCT_SCRIPT_RN_DIR/scripts/xcode/with-environment.sh" +/bin/sh -c "$WITH_ENVIRONMENT $SCRIPT_PHASES_SCRIPT" +SCRIPT`; +} + +module.exports = { + generateReactCodegenPodspec, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateSchemaInfos.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateSchemaInfos.js new file mode 100644 index 00000000000..a222b7f211e --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateSchemaInfos.js @@ -0,0 +1,113 @@ +/** + * 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 CodegenUtils = require('../codegen-utils'); +const {codegenLog} = require('./utils'); +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +function generateSchemaInfos(libraries) { + return libraries.map(generateSchemaInfo); +} + +function generateSchemaInfo(library, platform) { + const pathToJavaScriptSources = path.join( + library.libraryPath, + library.config.jsSrcsDir, + ); + codegenLog(`Processing ${library.config.name}`); + + const supportedApplePlatforms = extractSupportedApplePlatforms( + library.config.name, + library.libraryPath, + ); + + // Generate one schema for the entire library... + return { + library: library, + supportedApplePlatforms, + schema: CodegenUtils.getCombineJSToSchema().combineSchemasInFileList( + [pathToJavaScriptSources], + platform, + /NativeSampleTurboModule/, + ), + }; +} + +const APPLE_PLATFORMS = ['ios', 'macos', 'tvos', 'visionos']; + +function extractSupportedApplePlatforms(dependency, dependencyPath) { + codegenLog('Searching for podspec in the project dependencies.', true); + const podspecs = glob.sync('*.podspec', {cwd: dependencyPath}); + + if (podspecs.length === 0) { + return; + } + + // Take the first podspec found + const podspec = fs.readFileSync( + path.join(dependencyPath, podspecs[0]), + 'utf8', + ); + + /** + * Podspec can have platforms defined in two ways: + * 1. `spec.platforms = { :ios => "11.0", :tvos => "11.0" }` + * 2. `s.ios.deployment_target = "11.0"` + * `s.tvos.deployment_target = "11.0"` + */ + const supportedPlatforms = podspec + .split('\n') + .filter( + line => line.includes('platform') || line.includes('deployment_target'), + ) + .join(''); + + // Generate a map of supported platforms { [platform]: true/false } + const supportedPlatformsMap = APPLE_PLATFORMS.reduce( + (acc, platform) => ({ + ...acc, + [platform]: supportedPlatforms.includes( + getCocoaPodsPlatformKey(platform), + ), + }), + {}, + ); + + const supportedPlatformsList = Object.keys(supportedPlatformsMap).filter( + key => supportedPlatformsMap[key], + ); + + if (supportedPlatformsList.length > 0) { + codegenLog( + `Supported Apple platforms: ${supportedPlatformsList.join( + ', ', + )} for ${dependency}`, + ); + } + + return supportedPlatformsMap; +} + +// Cocoapods specific platform keys +function getCocoaPodsPlatformKey(platformName) { + if (platformName === 'macos') { + return 'osx'; + } + return platformName; +} + +module.exports = { + generateSchemaInfos, + generateSchemaInfo, + extractSupportedApplePlatforms, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js new file mode 100644 index 00000000000..ce4c7d80258 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js @@ -0,0 +1,206 @@ +/** + * 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 codegenConfig key + * in a package.json file. + */ + +const { + generateAppDependencyProvider, +} = require('./generateAppDependencyProvider'); +const {generateCustomURLHandlers} = require('./generateCustomURLHandlers'); +const {generateNativeCode} = require('./generateNativeCode'); +const {generateRCTModuleProviders} = require('./generateRCTModuleProviders'); +const { + generateRCTThirdPartyComponents, +} = require('./generateRCTThirdPartyComponents'); +const {generateReactCodegenPodspec} = require('./generateReactCodegenPodspec'); +const {generateSchemaInfos} = require('./generateSchemaInfos'); +const { + buildCodegenIfNeeded, + cleanupEmptyFilesAndFolders, + codegenLog, + findCodegenEnabledLibraries, + pkgJsonIncludesGeneratedCode, + readPkgJsonInDirectory, +} = require('./utils'); +const path = require('path'); + +/** + * 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 projectRoot: the directory with the app source code, where the package.json lives. + * @parameter baseOutputPath: the base output path for the CodeGen. + * @parameter targetPlatform: the target platform. Supported values: 'android', 'ios', 'all'. + * @parameter source: the source that is invoking codegen. Supported values: 'app', 'library'. + * @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( + projectRoot, + targetPlatform, + baseOutputPath, + source, + runReactNativeCodegen = true, +) { + try { + codegenLog(`Analyzing ${path.join(projectRoot, 'package.json')}`); + + const supportedPlatforms = ['android', 'ios']; + if ( + targetPlatform !== 'all' && + !supportedPlatforms.includes(targetPlatform) + ) { + throw new Error( + `Invalid target platform: ${targetPlatform}. Supported values are: ${supportedPlatforms.join( + ', ', + )}, all`, + ); + } + + const pkgJson = readPkgJsonInDirectory(projectRoot); + + if (runReactNativeCodegen) { + buildCodegenIfNeeded(); + } + + const libraries = findCodegenEnabledLibraries(pkgJson, projectRoot); + + if (libraries.length === 0) { + codegenLog('No codegen-enabled libraries found.', true); + return; + } + + let platforms = + targetPlatform === 'all' ? supportedPlatforms : [targetPlatform]; + + for (const platform of platforms) { + const outputPath = computeOutputPath( + projectRoot, + baseOutputPath, + pkgJson, + platform, + ); + + if (runReactNativeCodegen) { + const schemaInfos = generateSchemaInfos(libraries); + generateNativeCode( + outputPath, + schemaInfos.filter(schemaInfo => + mustGenerateNativeCode(projectRoot, schemaInfo), + ), + pkgJsonIncludesGeneratedCode(pkgJson), + platform, + ); + } + + if (source === 'app') { + // These components are only required by apps, not by libraries + generateRCTThirdPartyComponents(libraries, outputPath); + generateRCTModuleProviders(projectRoot, pkgJson, libraries, outputPath); + generateCustomURLHandlers(libraries, outputPath); + generateAppDependencyProvider(outputPath); + } + generateReactCodegenPodspec( + projectRoot, + pkgJson, + outputPath, + baseOutputPath, + ); + + cleanupEmptyFilesAndFolders(outputPath); + } + } catch (err) { + codegenLog(err); + process.exitCode = 1; + } + + codegenLog('Done.', true); + return; +} + +function readOutputDirFromPkgJson(pkgJson, platform) { + const codegenConfig = pkgJson.codegenConfig; + if (codegenConfig == null || typeof codegenConfig !== 'object') { + return null; + } + const outputDir = codegenConfig.outputDir; + if (outputDir == null) { + return null; + } + if (typeof outputDir === 'string') { + return outputDir; + } + if (typeof outputDir === 'object') { + return outputDir[platform]; + } + return null; +} + +function computeOutputPath(projectRoot, baseOutputPath, pkgJson, platform) { + if (baseOutputPath == null) { + const outputDirFromPkgJson = readOutputDirFromPkgJson(pkgJson, platform); + if (outputDirFromPkgJson != null) { + baseOutputPath = path.join(projectRoot, outputDirFromPkgJson); + } else { + baseOutputPath = projectRoot; + } + } + if (pkgJsonIncludesGeneratedCode(pkgJson)) { + // Don't create nested directories for libraries to make importing generated headers easier. + return baseOutputPath; + } + if (platform === 'android') { + return defaultOutputPathForAndroid(baseOutputPath); + } + if (platform === 'ios') { + return defaultOutputPathForIOS(baseOutputPath); + } + return baseOutputPath; +} + +function defaultOutputPathForAndroid(baseOutputPath) { + return path.join( + baseOutputPath, + 'android', + 'app', + 'build', + 'generated', + 'source', + 'codegen', + ); +} + +function defaultOutputPathForIOS(baseOutputPath) { + return path.join(baseOutputPath, 'build', 'generated', 'ios'); +} + +function mustGenerateNativeCode(includeLibraryPath, schemaInfo) { + // If library's 'codegenConfig' sets 'includesGeneratedCode' to 'true', + // then we assume that native code is shipped with the library, + // and we don't need to generate it. + return ( + schemaInfo.library.libraryPath === includeLibraryPath || + !schemaInfo.library.config.includesGeneratedCode + ); +} + +module.exports = { + execute, +}; diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/utils.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/utils.js new file mode 100644 index 00000000000..e9af9e52269 --- /dev/null +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/utils.js @@ -0,0 +1,289 @@ +/** + * 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; + } +} + +/** + * Finding libraries! + */ +function findCodegenEnabledLibraries(pkgJson, projectRoot) { + const projectLibraries = findProjectRootLibraries(pkgJson, projectRoot); + if (pkgJsonIncludesGeneratedCode(pkgJson)) { + return projectLibraries; + } else { + return [ + ...projectLibraries, + ...findExternalLibraries(pkgJson, projectRoot), + ...findLibrariesFromReactNativeConfig(projectRoot), + ]; + } +} + +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) { + const rnConfigFileName = 'react-native.config.js'; + + codegenLog( + `Searching for codegen-enabled libraries in ${rnConfigFileName}`, + true, + ); + + const rnConfigFilePath = path.resolve(projectRoot, rnConfigFileName); + + if (!fs.existsSync(rnConfigFilePath)) { + return []; + } + const rnConfig = require(rnConfigFilePath); + + if (rnConfig.dependencies == null) { + 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 [ + { + 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 { + config, + libraryPath: dependencyPath, + }; + }); +} + +function isReactNativeCoreLibrary(libraryName) { + return libraryName in CORE_LIBRARIES_WITH_OUTPUT_FOLDER; +} + +module.exports = { + buildCodegenIfNeeded, + pkgJsonIncludesGeneratedCode, + codegenLog, + readPkgJsonInDirectory, + isReactNativeCoreLibrary, + cleanupEmptyFilesAndFolders, + findCodegenEnabledLibraries, + findProjectRootLibraries, + extractLibrariesFromJSON, +}; diff --git a/packages/react-native/scripts/generate-codegen-artifacts.js b/packages/react-native/scripts/generate-codegen-artifacts.js index 0f0ecdda7b4..b77bbad9e3d 100644 --- a/packages/react-native/scripts/generate-codegen-artifacts.js +++ b/packages/react-native/scripts/generate-codegen-artifacts.js @@ -9,7 +9,7 @@ 'use strict'; -const executor = require('./codegen/generate-artifacts-executor.js'); +const executor = require('./codegen/generate-artifacts-executor'); const yargs = require('yargs'); const argv = yargs diff --git a/packages/react-native/scripts/prepack.js b/packages/react-native/scripts/prepack.js index c88cb4638f1..3553493d874 100644 --- a/packages/react-native/scripts/prepack.js +++ b/packages/react-native/scripts/prepack.js @@ -11,8 +11,10 @@ const { generateFBReactNativeSpecIOS, +} = require('./codegen/generate-artifacts-executor/generateFBReactNativeSpecIOS'); +const { generateRNCoreComponentsIOS, -} = require('./codegen/generate-artifacts-executor'); +} = require('./codegen/generate-artifacts-executor/generateRNCoreComponentsIOS'); const fs = require('fs'); function main() {