Skip to content

Commit

Permalink
feat: support custom library paths in react-native.config.js for co…
Browse files Browse the repository at this point in the history
…degen on iOS (#34580)

Summary:
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, '..'),
    },
  },
};
```

## Changelog

[Internal] [Added] - Support custom library paths in `react-native.config.js` for codegen on iOS

Pull Request resolved: #34580

Test Plan:
Tested on a test application and ensured that codegen finds the library specified in `react-native.config.js`

https://user-images.githubusercontent.com/1174278/188141056-bce03730-2a13-4648-8889-9727aaf2c3c4.mp4

I have also added a basic test case for this scenario.

Reviewed By: jacdebug, cortinico

Differential Revision: D39257919

Pulled By: cipolleschi

fbshipit-source-id: 131189f1941128a59b9b1e28af61a9038eb4536b
  • Loading branch information
satya164 authored and Dmitry Rykun committed Sep 14, 2022
1 parent ad582f5 commit f0ffd22
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 52 deletions.
3 changes: 3 additions & 0 deletions repo-config/package.json
Expand Up @@ -50,5 +50,8 @@
"signedsource": "^1.0.0",
"ws": "^6.1.4",
"yargs": "^15.3.1"
},
"devDependencies": {
"mock-fs": "^5.1.4"
}
}
148 changes: 109 additions & 39 deletions scripts/codegen/__tests__/generate-artifacts-executor-test.js
Expand Up @@ -13,13 +13,20 @@
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', () => {
it('executeNodes with the right arguents', () => {
afterEach(() => {
jest.resetModules();
jest.resetAllMocks();
});

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'}};
Expand All @@ -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,
});
});
});

Expand Down Expand Up @@ -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();
Expand Down
98 changes: 85 additions & 13 deletions scripts/codegen/generate-artifacts-executor.js
Expand Up @@ -204,6 +204,55 @@ 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(name => {
const dependencyConfig = rnConfig.dependencies[name];

if (dependencyConfig.root) {
const codegenConfigFileDir = path.resolve(
appRootDir,
dependencyConfig.root,
);
const configFilePath = path.join(
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,
pkgJson.name,
codegenConfigFileDir,
);
}
}
});
}
}
}

function handleInAppLibraries(
libraries,
pkgJson,
Expand Down Expand Up @@ -362,6 +411,39 @@ function createComponentProvider(
}
}

function findCodegenEnabledLibraries(
appRootDir,
baseCodegenConfigFileDir,
codegenConfigFilename,
codegenConfigKey,
) {
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.
//
Expand Down Expand Up @@ -429,23 +511,12 @@ function execute(
}

try {
const pkgJson = readPackageJSON(appRootDir);
const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies};
const libraries = [];

handleReactNativeCodeLibraries(
libraries,
codegenConfigFilename,
codegenConfigKey,
);
handleThirdPartyLibraries(
libraries,
const libraries = findCodegenEnabledLibraries(
appRootDir,
baseCodegenConfigFileDir,
dependencies,
codegenConfigFilename,
codegenConfigKey,
);
handleInAppLibraries(libraries, pkgJson, codegenConfigKey, appRootDir);

if (libraries.length === 0) {
console.log('[Codegen] No codegen-enabled libraries found.');
Expand Down Expand Up @@ -482,6 +553,7 @@ module.exports = {
execute: execute,
// exported for testing purposes only:
_extractLibrariesFromJSON: extractLibrariesFromJSON,
_findCodegenEnabledLibraries: findCodegenEnabledLibraries,
_executeNodeScript: executeNodeScript,
_generateCode: generateCode,
_cleanupEmptyFilesAndFolders: cleanupEmptyFilesAndFolders,
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -5335,6 +5335,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"
Expand Down

0 comments on commit f0ffd22

Please sign in to comment.