From 5205c4ac4152974ebab99a298c485acf085f6b4b Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Thu, 28 Apr 2022 03:55:47 -0700 Subject: [PATCH] Refactor generate-specs-cli and add tests 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. Differential Revision: https://www.internalfb.com/diff/D35892576?entry_point=27 fbshipit-source-id: 8a94f9ee7fafe8e3e7ba980b5d78dd87c0d6e4a3 --- scripts/codegen/__test_fixtures__/fixtures.js | 87 ++++++++++++ .../generate-specs-cli-executor-test.js | 90 +++++++++++++ scripts/codegen/codegen-utils.js | 37 +++++ .../codegen/generate-specs-cli-executor.js | 127 ++++++++++++++++++ scripts/generate-specs-cli.js | 123 +---------------- 5 files changed, 343 insertions(+), 121 deletions(-) create mode 100644 scripts/codegen/__test_fixtures__/fixtures.js create mode 100644 scripts/codegen/__tests__/generate-specs-cli-executor-test.js create mode 100644 scripts/codegen/codegen-utils.js create mode 100644 scripts/codegen/generate-specs-cli-executor.js diff --git a/scripts/codegen/__test_fixtures__/fixtures.js b/scripts/codegen/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..ec6e6188440e56 --- /dev/null +++ b/scripts/codegen/__test_fixtures__/fixtures.js @@ -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, +}; diff --git a/scripts/codegen/__tests__/generate-specs-cli-executor-test.js b/scripts/codegen/__tests__/generate-specs-cli-executor-test.js new file mode 100644 index 00000000000000..41b8d3f2f28583 --- /dev/null +++ b/scripts/codegen/__tests__/generate-specs-cli-executor-test.js @@ -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); + }); +}); diff --git a/scripts/codegen/codegen-utils.js b/scripts/codegen/codegen-utils.js new file mode 100644 index 00000000000000..0943555e6a43df --- /dev/null +++ b/scripts/codegen/codegen-utils.js @@ -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, +}; diff --git a/scripts/codegen/generate-specs-cli-executor.js b/scripts/codegen/generate-specs-cli-executor.js new file mode 100644 index 00000000000000..7ce95807b91134 --- /dev/null +++ b/scripts/codegen/generate-specs-cli-executor.js @@ -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, +}; diff --git a/scripts/generate-specs-cli.js b/scripts/generate-specs-cli.js index 91c6039d81f7d0..3e5bc9549e3820 100644 --- a/scripts/generate-specs-cli.js +++ b/scripts/generate-specs-cli.js @@ -9,20 +9,8 @@ 'use strict'; -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.'; - } -} - -const fs = require('fs'); -const mkdirp = require('mkdirp'); -const path = require('path'); const yargs = require('yargs'); +const executor = require('./codegen/generate-specs-cli-executor'); const argv = yargs .option('p', { @@ -67,115 +55,8 @@ const argv = yargs 'Please provide platform, schema path, and output directory.', ).argv; -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, - modulesOutputDirs, -) { - validateLibraryType(libraryType); - - let schema = readAndParseSchema(schemaPath); - - createFolderIfDefined(componentsOutputDir); - createFolderIfDefined(modulesOutputDirs); - deprecated_createOutputDirectoryIfNeeded(outputDirectory, libraryName); - - RNCodegen.generate( - { - libraryName, - schema, - outputDirectory, - packageName, - componentsOutputDir, - modulesOutputDirs, - }, - { - 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}`); - }); - } -} - function main() { - generateSpec( + executor.execute( argv.platform, argv.schemaPath, argv.outputDir,