From eb57ab0edc4d10c94cd27cceff4094a1f1b6b492 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Thu, 1 Sep 2022 11:47:47 +0200 Subject: [PATCH 1/2] feat: support custom paths in react-native.config.js for codegen on iOS Currently for codegen to work for a library on iOS, it needs to be located inside `node_modules`. This patch adds support for libraries defined in `react-native.config.js`. This is useful when developing libraries as well as monorepos where the library may exist outside of the `node_modules`. Example: ```js // react-native.config.js const path = require('path'); module.exports = { dependencies: { 'react-native-library-name': { root: path.join(__dirname, '..'), }, }, }; ``` --- .../generate-artifacts-executor-test.js | 2 +- .../codegen/generate-artifacts-executor.js | 93 +++++++++++++++---- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/scripts/codegen/__tests__/generate-artifacts-executor-test.js index c59592201c981e..ecc390486f1c2c 100644 --- a/scripts/codegen/__tests__/generate-artifacts-executor-test.js +++ b/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -19,7 +19,7 @@ const reactNativeDependencyName = 'react-native'; const rootPath = path.join(__dirname, '../../..'); describe('generateCode', () => { - it('executeNodes with the right arguents', () => { + it('executeNodes with the right arguments', () => { // Define variables and expected values const iosOutputDir = 'app/ios/build/generated/ios'; const library = {config: {name: 'library', type: 'all'}}; diff --git a/scripts/codegen/generate-artifacts-executor.js b/scripts/codegen/generate-artifacts-executor.js index 458495baba452e..eb73a871b247d5 100644 --- a/scripts/codegen/generate-artifacts-executor.js +++ b/scripts/codegen/generate-artifacts-executor.js @@ -204,6 +204,53 @@ function handleThirdPartyLibraries( }); } +function handleLibrariesFromReactNativeConfig( + libraries, + codegenConfigKey, + codegenConfigFilename, + appRootDir, +) { + const rnConfigFileName = 'react-native.config.js'; + + console.log( + `\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${rnConfigFileName}`, + ); + + const rnConfigFilePath = path.join(appRootDir, rnConfigFileName); + + if (fs.existsSync(rnConfigFilePath)) { + const rnConfig = require(rnConfigFilePath); + + if (rnConfig.dependencies != null) { + Object.keys(rnConfig.dependencies).forEach(dependencyName => { + const dependencyConfig = rnConfig.dependencies[dependencyName]; + + if (dependencyConfig.root) { + const codegenConfigFileDir = path.resolve( + appRootDir, + dependencyConfig.root, + ); + const configFilePath = path.join( + codegenConfigFileDir, + codegenConfigFilename, + ); + + if (fs.existsSync(configFilePath)) { + const configFile = JSON.parse(fs.readFileSync(configFilePath)); + extractLibrariesFromJSON( + configFile, + libraries, + codegenConfigKey, + dependencyName, + codegenConfigFileDir, + ); + } + } + }); + } + } +} + function handleInAppLibraries( libraries, pkgJson, @@ -362,6 +409,34 @@ function createComponentProvider( } } +function findCodegenEnabledLibraries(appRootDir) { + const pkgJson = readPackageJSON(appRootDir); + const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies}; + const libraries = []; + + handleReactNativeCodeLibraries( + libraries, + codegenConfigFilename, + codegenConfigKey, + ); + handleThirdPartyLibraries( + libraries, + baseCodegenConfigFileDir, + dependencies, + codegenConfigFilename, + codegenConfigKey, + ); + handleLibrariesFromReactNativeConfig( + libraries, + codegenConfigKey, + codegenConfigFilename, + appRootDir, + ); + handleInAppLibraries(libraries, pkgJson, codegenConfigKey, appRootDir); + + return libraries; +} + // It removes all the empty files and empty folders // it finds, starting from `filepath`, recursively. // @@ -429,23 +504,7 @@ function execute( } try { - const pkgJson = readPackageJSON(appRootDir); - const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies}; - const libraries = []; - - handleReactNativeCodeLibraries( - libraries, - codegenConfigFilename, - codegenConfigKey, - ); - handleThirdPartyLibraries( - libraries, - baseCodegenConfigFileDir, - dependencies, - codegenConfigFilename, - codegenConfigKey, - ); - handleInAppLibraries(libraries, pkgJson, codegenConfigKey, appRootDir); + const libraries = findCodegenEnabledLibraries(appRootDir); if (libraries.length === 0) { console.log('[Codegen] No codegen-enabled libraries found.'); From 357bbbcf37923a36345a74b97740f94f71aefef3 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Fri, 2 Sep 2022 14:10:02 +0200 Subject: [PATCH 2/2] test: add tests for finding libraries from react-native.config.js --- package.json | 1 + .../generate-artifacts-executor-test.js | 146 +++++++++++++----- .../codegen/generate-artifacts-executor.js | 23 ++- yarn.lock | 5 + 4 files changed, 132 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index b3307805f7a3e2..67771203bbb319 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "devDependencies": { "flow-bin": "^0.186.0", "hermes-eslint": "0.8.0", + "mock-fs": "^5.1.4", "react": "18.2.0", "react-test-renderer": "^18.2.0" }, diff --git a/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/scripts/codegen/__tests__/generate-artifacts-executor-test.js index ecc390486f1c2c..68faeda9480428 100644 --- a/scripts/codegen/__tests__/generate-artifacts-executor-test.js +++ b/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -13,12 +13,19 @@ const underTest = require('../generate-artifacts-executor'); const fixtures = require('../__test_fixtures__/fixtures'); const path = require('path'); +const fs = require('fs'); +const child_process = require('child_process'); const codegenConfigKey = 'codegenConfig'; const reactNativeDependencyName = 'react-native'; const rootPath = path.join(__dirname, '../../..'); describe('generateCode', () => { + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + }); + it('executeNodes with the right arguments', () => { // Define variables and expected values const iosOutputDir = 'app/ios/build/generated/ios'; @@ -32,46 +39,32 @@ describe('generateCode', () => { const tmpOutDir = path.join(tmpDir, 'out'); // mock used functions - let mkdirSyncInvocationCount = 0; - jest.mock('fs', () => ({ - mkdirSync: (location, config) => { - if (mkdirSyncInvocationCount === 0) { - expect(location).toEqual(tmpOutDir); - } - if (mkdirSyncInvocationCount === 1) { - expect(location).toEqual(iosOutputDir); - } - - mkdirSyncInvocationCount += 1; - }, - })); - - let execSyncInvocationCount = 0; - jest.mock('child_process', () => ({ - execSync: command => { - if (execSyncInvocationCount === 0) { - const expectedCommand = `${node} ${path.join( - rnRoot, - 'generate-specs-cli.js', - )} \ - --platform ios \ - --schemaPath ${pathToSchema} \ - --outputDir ${tmpOutDir} \ - --libraryName ${library.config.name} \ - --libraryType ${libraryType}`; - expect(command).toEqual(expectedCommand); - } - - if (execSyncInvocationCount === 1) { - expect(command).toEqual(`cp -R ${tmpOutDir}/* ${iosOutputDir}`); - } - - execSyncInvocationCount += 1; - }, - })); + jest.spyOn(fs, 'mkdirSync').mockImplementation(); + jest.spyOn(child_process, 'execSync').mockImplementation(); underTest._generateCode(iosOutputDir, library, tmpDir, node, pathToSchema); - expect(mkdirSyncInvocationCount).toBe(2); + + const expectedCommand = `${node} ${path.join( + rnRoot, + 'generate-specs-cli.js', + )} --platform ios --schemaPath ${pathToSchema} --outputDir ${tmpOutDir} --libraryName ${ + library.config.name + } --libraryType ${libraryType}`; + + expect(child_process.execSync).toHaveBeenCalledTimes(2); + expect(child_process.execSync).toHaveBeenNthCalledWith(1, expectedCommand); + expect(child_process.execSync).toHaveBeenNthCalledWith( + 2, + `cp -R ${tmpOutDir}/* ${iosOutputDir}`, + ); + + expect(fs.mkdirSync).toHaveBeenCalledTimes(2); + expect(fs.mkdirSync).toHaveBeenNthCalledWith(1, tmpOutDir, { + recursive: true, + }); + expect(fs.mkdirSync).toHaveBeenNthCalledWith(2, iosOutputDir, { + recursive: true, + }); }); }); @@ -202,6 +195,83 @@ describe('extractLibrariesFromJSON', () => { }); }); +describe('findCodegenEnabledLibraries', () => { + const mock = require('mock-fs'); + const { + _findCodegenEnabledLibraries: findCodegenEnabledLibraries, + } = require('../generate-artifacts-executor'); + + afterEach(() => { + mock.restore(); + }); + + it('returns libraries defined in react-native.config.js', () => { + const projectDir = path.join(__dirname, '../../../../test-project'); + const baseCodegenConfigFileDir = path.join(__dirname, '../../..'); + const baseCodegenConfigFilePath = path.join( + baseCodegenConfigFileDir, + 'package.json', + ); + + mock({ + [baseCodegenConfigFilePath]: ` + { + "codegenConfig": {} + } + `, + [projectDir]: { + app: { + 'package.json': `{ + "name": "my-app" + }`, + 'react-native.config.js': '', + }, + 'library-foo': { + 'package.json': `{ + "name": "react-native-foo", + "codegenConfig": { + "name": "RNFooSpec", + "type": "modules", + "jsSrcsDir": "src" + } + }`, + }, + }, + }); + + jest.mock(path.join(projectDir, 'app', 'react-native.config.js'), () => ({ + dependencies: { + 'react-native-foo': { + root: path.join(projectDir, 'library-foo'), + }, + 'react-native-bar': { + root: path.join(projectDir, 'library-bar'), + }, + }, + })); + + const libraries = findCodegenEnabledLibraries( + `${projectDir}/app`, + baseCodegenConfigFileDir, + `package.json`, + 'codegenConfig', + ); + + expect(libraries).toEqual([ + { + library: 'react-native', + config: {}, + libraryPath: baseCodegenConfigFileDir, + }, + { + library: 'react-native-foo', + config: {name: 'RNFooSpec', type: 'modules', jsSrcsDir: 'src'}, + libraryPath: path.join(projectDir, 'library-foo'), + }, + ]); + }); +}); + describe('delete empty files and folders', () => { beforeEach(() => { jest.resetModules(); diff --git a/scripts/codegen/generate-artifacts-executor.js b/scripts/codegen/generate-artifacts-executor.js index eb73a871b247d5..6fc398a8bcb9d6 100644 --- a/scripts/codegen/generate-artifacts-executor.js +++ b/scripts/codegen/generate-artifacts-executor.js @@ -222,8 +222,8 @@ function handleLibrariesFromReactNativeConfig( const rnConfig = require(rnConfigFilePath); if (rnConfig.dependencies != null) { - Object.keys(rnConfig.dependencies).forEach(dependencyName => { - const dependencyConfig = rnConfig.dependencies[dependencyName]; + Object.keys(rnConfig.dependencies).forEach(name => { + const dependencyConfig = rnConfig.dependencies[name]; if (dependencyConfig.root) { const codegenConfigFileDir = path.resolve( @@ -234,14 +234,16 @@ function handleLibrariesFromReactNativeConfig( codegenConfigFileDir, codegenConfigFilename, ); + const pkgJsonPath = path.join(codegenConfigFileDir, 'package.json'); if (fs.existsSync(configFilePath)) { + const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath)); const configFile = JSON.parse(fs.readFileSync(configFilePath)); extractLibrariesFromJSON( configFile, libraries, codegenConfigKey, - dependencyName, + pkgJson.name, codegenConfigFileDir, ); } @@ -409,7 +411,12 @@ function createComponentProvider( } } -function findCodegenEnabledLibraries(appRootDir) { +function findCodegenEnabledLibraries( + appRootDir, + baseCodegenConfigFileDir, + codegenConfigFilename, + codegenConfigKey, +) { const pkgJson = readPackageJSON(appRootDir); const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies}; const libraries = []; @@ -504,7 +511,12 @@ function execute( } try { - const libraries = findCodegenEnabledLibraries(appRootDir); + const libraries = findCodegenEnabledLibraries( + appRootDir, + baseCodegenConfigFileDir, + codegenConfigFilename, + codegenConfigKey, + ); if (libraries.length === 0) { console.log('[Codegen] No codegen-enabled libraries found.'); @@ -541,6 +553,7 @@ module.exports = { execute: execute, // exported for testing purposes only: _extractLibrariesFromJSON: extractLibrariesFromJSON, + _findCodegenEnabledLibraries: findCodegenEnabledLibraries, _executeNodeScript: executeNodeScript, _generateCode: generateCode, _cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders, diff --git a/yarn.lock b/yarn.lock index 3a65b3a581d5ce..62304e281548d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5301,6 +5301,11 @@ mkdirp@^0.5.1: dependencies: minimist "0.0.8" +mock-fs@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.4.tgz#d64dc37b2793613ca7148b510b1167b5b8afb6b8" + integrity sha512-sudhLjCjX37qWIcAlIv1OnAxB2wI4EmXByVuUjILh1rKGNGpGU8GNnzw+EAbrhdpBe0TL/KONbK1y3RXZk8SxQ== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"