Files
react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js
T
Satyajit Sahoo a5622165c1 feat: support custom library paths in react-native.config.js for codegen 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: https://github.com/facebook/react-native/pull/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
2022-09-08 03:09:16 -07:00

501 lines
14 KiB
JavaScript

/**
* 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.
*
* @emails oncall+react_native
* @format
*/
'use strict';
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';
const library = {config: {name: 'library', type: 'all'}};
const tmpDir = 'tmp';
const node = 'usr/bin/node';
const pathToSchema = 'app/build/schema.json';
const rnRoot = path.join(__dirname, '../..');
const libraryType = 'all';
const tmpOutDir = path.join(tmpDir, 'out');
// mock used functions
jest.spyOn(fs, 'mkdirSync').mockImplementation();
jest.spyOn(child_process, 'execSync').mockImplementation();
underTest._generateCode(iosOutputDir, library, tmpDir, node, pathToSchema);
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,
});
});
});
describe('extractLibrariesFromJSON', () => {
it('throws if in react-native and no dependencies found', () => {
let libraries = [];
let configFile = {};
expect(() => {
underTest._extractLibrariesFromJSON(
configFile,
libraries,
codegenConfigKey,
);
}).toThrow();
});
it('it skips if not into react-native and no dependencies found', () => {
let libraries = [];
let configFile = {};
underTest._extractLibrariesFromJSON(
configFile,
libraries,
codegenConfigKey,
'some-node-module',
'node_modules/some',
);
expect(libraries.length).toBe(0);
});
it('extracts a single dependency when config has no libraries', () => {
let libraries = [];
let configFile = fixtures.noLibrariesConfigFile;
underTest._extractLibrariesFromJSON(
configFile,
libraries,
codegenConfigKey,
'my-app',
'.',
);
expect(libraries.length).toBe(1);
expect(libraries[0]).toEqual({
library: 'my-app',
config: {
name: 'AppModules',
type: 'all',
jsSrcsDir: '.',
},
libraryPath: '.',
});
});
it("extract codegenConfig when it's empty", () => {
const configFile = {codegenConfig: {libraries: []}};
let libraries = [];
underTest._extractLibrariesFromJSON(
configFile,
codegenConfigKey,
libraries,
reactNativeDependencyName,
rootPath,
);
expect(libraries.length).toBe(0);
});
it('extract codegenConfig when dependency is one', () => {
const configFile = fixtures.singleLibraryCodegenConfig;
let libraries = [];
underTest._extractLibrariesFromJSON(
configFile,
libraries,
codegenConfigKey,
reactNativeDependencyName,
rootPath,
);
expect(libraries.length).toBe(1);
expect(libraries[0]).toEqual({
library: reactNativeDependencyName,
config: {
name: 'react-native',
type: 'all',
jsSrcsDir: '.',
},
libraryPath: rootPath,
});
});
it('extract codegenConfig with multiple dependencies', () => {
const configFile = fixtures.multipleLibrariesCodegenConfig;
const myDependency = 'my-dependency';
const myDependencyPath = path.join(__dirname, myDependency);
let libraries = [];
underTest._extractLibrariesFromJSON(
configFile,
libraries,
codegenConfigKey,
myDependency,
myDependencyPath,
);
expect(libraries.length).toBe(3);
expect(libraries[0]).toEqual({
library: myDependency,
config: {
name: 'react-native',
type: 'all',
jsSrcsDir: '.',
},
libraryPath: myDependencyPath,
});
expect(libraries[1]).toEqual({
library: myDependency,
config: {
name: 'my-component',
type: 'components',
jsSrcsDir: 'component/js',
},
libraryPath: myDependencyPath,
});
expect(libraries[2]).toEqual({
library: myDependency,
config: {
name: 'my-module',
type: 'module',
jsSrcsDir: 'module/js',
},
libraryPath: myDependencyPath,
});
});
});
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();
});
it('when path is empty file, deletes it', () => {
const targetFilepath = 'my-file.txt';
let statSyncInvocationCount = 0;
let rmSyncInvocationCount = 0;
let rmdirSyncInvocationCount = 0;
jest.mock('fs', () => ({
statSync: filepath => {
statSyncInvocationCount += 1;
expect(filepath).toBe(targetFilepath);
return {
isFile: () => {
return true;
},
size: 0,
};
},
rmSync: filepath => {
rmSyncInvocationCount += 1;
expect(filepath).toBe(targetFilepath);
},
rmdirSync: filepath => {
rmdirSyncInvocationCount += 1;
},
}));
underTest._cleanupEmptyFilesAndFolders(targetFilepath);
expect(statSyncInvocationCount).toBe(1);
expect(rmSyncInvocationCount).toBe(1);
expect(rmdirSyncInvocationCount).toBe(0);
});
it('when path is not an empty file, does nothing', () => {
const targetFilepath = 'my-file.txt';
const size = 128;
let statSyncInvocationCount = 0;
let rmSyncInvocationCount = 0;
let rmdirSyncInvocationCount = 0;
jest.mock('fs', () => ({
statSync: filepath => {
statSyncInvocationCount += 1;
expect(filepath).toBe(targetFilepath);
return {
isFile: () => {
return true;
},
size: size,
};
},
rmSync: filepath => {
rmSyncInvocationCount += 1;
},
rmdirSync: filepath => {
rmdirSyncInvocationCount += 1;
},
}));
underTest._cleanupEmptyFilesAndFolders(targetFilepath);
expect(statSyncInvocationCount).toBe(1);
expect(rmSyncInvocationCount).toBe(0);
expect(rmdirSyncInvocationCount).toBe(0);
});
it("when path is folder and it's empty, removes it", () => {
const targetFolder = 'build';
const content = [];
let statSyncInvocationCount = 0;
let readdirInvocationCount = 0;
let rmSyncInvocationCount = 0;
let rmdirSyncInvocationCount = 0;
jest.mock('fs', () => ({
statSync: filepath => {
statSyncInvocationCount += 1;
expect(filepath).toBe(targetFolder);
return {
isFile: () => {
return false;
},
};
},
rmSync: filepath => {
rmSyncInvocationCount += 1;
},
rmdirSync: filepath => {
rmdirSyncInvocationCount += 1;
expect(filepath).toBe(targetFolder);
},
readdirSync: filepath => {
readdirInvocationCount += 1;
return content;
},
}));
underTest._cleanupEmptyFilesAndFolders(targetFolder);
expect(statSyncInvocationCount).toBe(1);
expect(readdirInvocationCount).toBe(2);
expect(rmSyncInvocationCount).toBe(0);
expect(rmdirSyncInvocationCount).toBe(1);
});
it("when path is folder and it's not empty, removes only empty folders and files", () => {
const targetFolder = 'build';
const content = ['emptyFolder', 'emptyFile', 'notEmptyFile'];
const files = [
path.normalize('build/emptyFile'),
path.normalize('build/notEmptyFile'),
];
const emptyContent = [];
let fileSizes = {};
fileSizes[path.normalize('build/emptyFile')] = 0;
fileSizes[path.normalize('build/notEmptyFile')] = 32;
let statSyncInvocation = [];
let rmSyncInvocation = [];
let rmdirSyncInvocation = [];
let readdirInvocation = [];
jest.mock('fs', () => ({
statSync: filepath => {
statSyncInvocation.push(filepath);
return {
isFile: () => {
return files.includes(filepath);
},
size: fileSizes[filepath],
};
},
rmSync: filepath => {
rmSyncInvocation.push(filepath);
},
rmdirSync: filepath => {
rmdirSyncInvocation.push(filepath);
},
readdirSync: filepath => {
readdirInvocation.push(filepath);
return filepath === targetFolder ? content : emptyContent;
},
}));
underTest._cleanupEmptyFilesAndFolders(targetFolder);
expect(statSyncInvocation).toEqual([
path.normalize('build'),
path.normalize('build/emptyFolder'),
path.normalize('build/emptyFile'),
path.normalize('build/notEmptyFile'),
]);
expect(readdirInvocation).toEqual([
path.normalize('build'),
path.normalize('build/emptyFolder'),
path.normalize('build/emptyFolder'),
path.normalize('build'),
]);
expect(rmSyncInvocation).toEqual([path.normalize('build/emptyFile')]);
expect(rmdirSyncInvocation).toEqual([path.normalize('build/emptyFolder')]);
});
it('when path is folder and it contains only empty folders, removes everything', () => {
const targetFolder = 'build';
const content = ['emptyFolder1', 'emptyFolder2'];
const emptyContent = [];
let statSyncInvocation = [];
let rmSyncInvocation = [];
let rmdirSyncInvocation = [];
let readdirInvocation = [];
jest.mock('fs', () => ({
statSync: filepath => {
statSyncInvocation.push(filepath);
return {
isFile: () => {
return false;
},
};
},
rmSync: filepath => {
rmSyncInvocation.push(filepath);
},
rmdirSync: filepath => {
rmdirSyncInvocation.push(filepath);
},
readdirSync: filepath => {
readdirInvocation.push(filepath);
return filepath === targetFolder
? content.filter(
element =>
!rmdirSyncInvocation.includes(path.join(targetFolder, element)),
)
: emptyContent;
},
}));
underTest._cleanupEmptyFilesAndFolders(targetFolder);
expect(statSyncInvocation).toEqual([
path.normalize('build'),
path.normalize('build/emptyFolder1'),
path.normalize('build/emptyFolder2'),
]);
expect(readdirInvocation).toEqual([
path.normalize('build'),
path.normalize('build/emptyFolder1'),
path.normalize('build/emptyFolder1'),
path.normalize('build/emptyFolder2'),
path.normalize('build/emptyFolder2'),
path.normalize('build'),
]);
expect(rmSyncInvocation).toEqual([]);
expect(rmdirSyncInvocation).toEqual([
path.normalize('build/emptyFolder1'),
path.normalize('build/emptyFolder2'),
path.normalize('build'),
]);
});
});