Skip to content

Commit

Permalink
Refactor generate-specs-cli and add tests
Browse files Browse the repository at this point in the history
Summary:
This Diff splits the `generate-specs-cli.js` script into:
* `generate-specs-cli-executor.js`: which contains the logic to generate the code for iOS.
* `generate-specs-cli.js`: which contains the argument parsing logic and invokes the executor.

Finally it introduces some tests.

## Changelog
[iOS][Changed] - Refactor part of the codegen scripts and add tests.

Reviewed By: cortinico, dmitryrykun

Differential Revision: D35892576

fbshipit-source-id: 6a3205af049bf55aa4ecaf64544ebc4eb7b13f73
  • Loading branch information
Riccardo Cipolleschi authored and facebook-github-bot committed May 4, 2022
1 parent 305a054 commit 0465c3f
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 121 deletions.
87 changes: 87 additions & 0 deletions scripts/codegen/__test_fixtures__/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* 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 SCHEMA_TEXT = `
{
"modules": {
"ColoredView": {
"type": "Component",
"components": {
"ColoredView": {
"extendsProps": [
{
"type": "ReactNativeBuiltInType",
"knownTypeName": "ReactNativeCoreViewProps"
}
],
"events": [],
"props": [
{
"name": "color",
"optional": false,
"typeAnnotation": {
"type": "StringTypeAnnotation",
"default": null
}
}
],
"commands": []
}
}
},
"NativeCalculator": {
"type": "NativeModule",
"aliases": {},
"spec": {
"properties": [
{
"name": "add",
"optional": false,
"typeAnnotation": {
"type": "FunctionTypeAnnotation",
"returnTypeAnnotation": {
"type": "PromiseTypeAnnotation"
},
"params": [
{
"name": "a",
"optional": false,
"typeAnnotation": {
"type": "NumberTypeAnnotation"
}
},
{
"name": "b",
"optional": false,
"typeAnnotation": {
"type": "NumberTypeAnnotation"
}
}
]
}
}
]
},
"moduleNames": [
"Calculator"
]
}
}
}
`;

const SCHEMA = JSON.parse(SCHEMA_TEXT);

module.exports = {
schemaText: SCHEMA_TEXT,
schema: SCHEMA,
};
90 changes: 90 additions & 0 deletions scripts/codegen/__tests__/generate-specs-cli-executor-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* 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 sut = require('../generate-specs-cli-executor');
const fixtures = require('../__test_fixtures__/fixtures');

describe('generateSpec', () => {
it('invokes RNCodegen with the right params', () => {
const platform = 'ios';
const libraryType = 'all';
const schemaPath = './';
const componentsOutputDir =
'app/ios/build/generated/ios/react/renderer/components/library';
const modulesOutputDir = 'app/ios/build/generated/ios/./library';
const outputDirectory = 'app/ios/build/generated/ios';
const libraryName = 'library';
const packageName = 'com.library';
const generators = ['componentsIOS', 'modulesIOS'];

jest.mock('fs', () => ({
readFileSync: (path, encoding) => {
expect(path).toBe(schemaPath);
expect(encoding).toBe('utf-8');
return fixtures.schemaText;
},
}));

let mkdirpSyncInvoked = 0;
jest.mock('mkdirp', () => ({
sync: folder => {
if (mkdirpSyncInvoked === 0) {
expect(folder).toBe(componentsOutputDir);
}

if (mkdirpSyncInvoked === 1) {
expect(folder).toBe(modulesOutputDir);
}

if (mkdirpSyncInvoked === 2) {
expect(folder).toBe(outputDirectory);
}

mkdirpSyncInvoked += 1;
},
}));

// We cannot mock directly the `RNCodegen` object because the
// code access the `lib` folder directly and request a file explicitly.
// This makes testing harder than usually. To overcome this, we created a utility
// to retrieve the `Codegen`. By doing that, we can mock the wrapper so that it returns
// an object with the same interface of the `RNCodegen` object.
jest.mock('../codegen-utils', () => ({
getCodegen: () => ({
generate: (libraryConfig, generatorConfigs) => {
expect(libraryConfig.libraryName).toBe(libraryName);
expect(libraryConfig.schema).toStrictEqual(fixtures.schema);
expect(libraryConfig.outputDirectory).toBe(outputDirectory);
expect(libraryConfig.packageName).toBe(packageName);
expect(libraryConfig.componentsOutputDir).toBe(componentsOutputDir);
expect(libraryConfig.modulesOutputDir).toBe(modulesOutputDir);

expect(generatorConfigs.generators).toStrictEqual(generators);
expect(generatorConfigs.test).toBeUndefined();
},
}),
}));

sut.execute(
platform,
schemaPath,
outputDirectory,
libraryName,
packageName,
libraryType,
componentsOutputDir,
modulesOutputDir,
);

expect(mkdirpSyncInvoked).toBe(3);
});
});
37 changes: 37 additions & 0 deletions scripts/codegen/codegen-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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';

/**
* Wrapper required to abstract away from the actual codegen.
* This is needed because, when running tests in Sandcastle, not everything is setup as usually.
* For example, the `react-native-codegen` lib is not present.
*
* Thanks to this wrapper, we are able to mock the getter for the codegen in a way that allow us to return
* a custom object which mimics the Codegen interface.
*
* @return an object that can generate the code for the New Architecture.
*/
function getCodegen() {
let RNCodegen;
try {
RNCodegen = require('../../packages/react-native-codegen/lib/generators/RNCodegen.js');
} catch (e) {
RNCodegen = require('../react-native-codegen/lib/generators/RNCodegen.js');
}
if (!RNCodegen) {
throw 'RNCodegen not found.';
}
return RNCodegen;
}

module.exports = {
getCodegen: getCodegen,
};
127 changes: 127 additions & 0 deletions scripts/codegen/generate-specs-cli-executor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* 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 mkdirp = require('mkdirp');
const path = require('path');
const utils = require('./codegen-utils');
const RNCodegen = utils.getCodegen();

const GENERATORS = {
all: {
android: ['componentsAndroid', 'modulesAndroid'],
ios: ['componentsIOS', 'modulesIOS'],
},
components: {
android: ['componentsAndroid'],
ios: ['componentsIOS'],
},
modules: {
android: ['modulesAndroid'],
ios: ['modulesIOS'],
},
};

function deprecated_createOutputDirectoryIfNeeded(
outputDirectory,
libraryName,
) {
if (!outputDirectory) {
outputDirectory = path.resolve(__dirname, '..', 'Libraries', libraryName);
}
mkdirp.sync(outputDirectory);
}

function createFolderIfDefined(folder) {
if (folder) {
mkdirp.sync(folder);
}
}

/**
* This function read a JSON schema from a path and parses it.
* It throws if the schema don't exists or it can't be parsed.
*
* @parameter schemaPath: the path to the schema
* @return a valid schema
* @throw an Error if the schema doesn't exists in a given path or if it can't be parsed.
*/
function readAndParseSchema(schemaPath) {
const schemaText = fs.readFileSync(schemaPath, 'utf-8');

if (schemaText == null) {
throw new Error(`Can't find schema at ${schemaPath}`);
}

try {
return JSON.parse(schemaText);
} catch (err) {
throw new Error(`Can't parse schema to JSON. ${schemaPath}`);
}
}

function validateLibraryType(libraryType) {
if (GENERATORS[libraryType] == null) {
throw new Error(`Invalid library type. ${libraryType}`);
}
}

function generateSpec(
platform,
schemaPath,
outputDirectory,
libraryName,
packageName,
libraryType,
componentsOutputDir,
modulesOutputDir,
) {
validateLibraryType(libraryType);

let schema = readAndParseSchema(schemaPath);

createFolderIfDefined(componentsOutputDir);
createFolderIfDefined(modulesOutputDir);
deprecated_createOutputDirectoryIfNeeded(outputDirectory, libraryName);

RNCodegen.generate(
{
libraryName,
schema,
outputDirectory,
packageName,
componentsOutputDir,
modulesOutputDir,
},
{
generators: GENERATORS[libraryType][platform],
},
);

if (platform === 'android') {
// Move all components C++ files to a structured jni folder for now.
// Note: this should've been done by RNCodegen's generators, but:
// * the generators don't support platform option yet
// * this subdir structure is Android-only, not applicable to iOS
const files = fs.readdirSync(outputDirectory);
const jniOutputDirectory = `${outputDirectory}/jni/react/renderer/components/${libraryName}`;
mkdirp.sync(jniOutputDirectory);
files
.filter(f => f.endsWith('.h') || f.endsWith('.cpp'))
.forEach(f => {
fs.renameSync(`${outputDirectory}/${f}`, `${jniOutputDirectory}/${f}`);
});
}
}

module.exports = {
execute: generateSpec,
};
Loading

0 comments on commit 0465c3f

Please sign in to comment.