From efec97f2be535def834e0c2da061bb25875224c3 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 7 Jun 2019 12:23:55 -0700 Subject: [PATCH] Add view config babel plugin Summary: This diff adds a babel plugin for the generated view configs which will inline them in the file instead of needing to check the view configs in (fb only) The way it works is: - babel reads the code - looks for type alias `CodegenNativeComponent` in `*NativeComponet.js` files - run the flow parser on the file source to create a schema - run the schema into codegen to get the view config source code - inject the generated source code back into the NativeComponent.js file - remove the original export - profit After this diff we will remove the `js1 build viewconfigs` command and the checked-in NativeViewConfig.js files Note: since this plugin is not published to open source, for now OSS will continue using the `requireNativeComponent` function Reviewed By: cpojer Differential Revision: D15516062 fbshipit-source-id: a8efb077773e04fd9753a7036682eeaae9175e09 --- .../PullToRefreshViewNativeComponent.js | 21 +++---- .../babel-plugin-inline-view-configs/BUCK | 23 ++++++++ .../__test_fixtures__/fixtures.js | 54 +++++++++++++++++ .../__snapshots__/index-test.js.snap | 52 ++++++++++++++++ .../__tests__/index-test.js | 32 ++++++++++ .../babel-plugin-inline-view-configs/index.js | 59 +++++++++++++++++++ .../package.json | 16 +++++ packages/react-native-codegen/BUCK | 23 ++++++++ packages/react-native-codegen/package.json | 2 +- .../src/cli/parser/parser.js | 5 +- .../cli/viewconfigs/generate-view-configs.js | 2 +- .../src/generators/RNCodegen.js | 14 +++++ .../src/parsers/flow/__tests__/parser-test.js | 2 +- .../src/parsers/flow/index.js | 16 +++-- 14 files changed, 298 insertions(+), 23 deletions(-) create mode 100644 packages/babel-plugin-inline-view-configs/BUCK create mode 100644 packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js create mode 100644 packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap create mode 100644 packages/babel-plugin-inline-view-configs/__tests__/index-test.js create mode 100644 packages/babel-plugin-inline-view-configs/index.js create mode 100644 packages/babel-plugin-inline-view-configs/package.json diff --git a/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js b/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js index 5274896fb75..43105157841 100644 --- a/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js +++ b/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js @@ -10,17 +10,13 @@ 'use strict'; -import type { - BubblingEvent, - WithDefault, - CodegenNativeComponent, -} from '../../Types/CodegenTypes'; - -const requireNativeComponent = require('../../ReactNative/requireNativeComponent'); - +import type {BubblingEvent, WithDefault} from '../../Types/CodegenTypes'; +import type {NativeComponent} from '../../Renderer/shims/ReactNative'; import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; import type {ViewProps} from '../View/ViewPropTypes'; +const requireNativeComponent = require('../../ReactNative/requireNativeComponent'); + type NativeProps = $ReadOnly<{| ...ViewProps, @@ -48,13 +44,10 @@ type NativeProps = $ReadOnly<{| refreshing: WithDefault, |}>; -type PullToRefreshViewType = CodegenNativeComponent< - 'PullToRefreshView', - NativeProps, ->; - -// TODO: Switch this over to require('./PullToRefreshNativeViewConfig') +// TODO: Switch this over to CodegenNativeComponent // once the native components are renamed in paper and fabric +type PullToRefreshViewType = Class>; + module.exports = ((requireNativeComponent( 'RCTRefreshControl', ): any): PullToRefreshViewType); diff --git a/packages/babel-plugin-inline-view-configs/BUCK b/packages/babel-plugin-inline-view-configs/BUCK new file mode 100644 index 00000000000..8fd38289431 --- /dev/null +++ b/packages/babel-plugin-inline-view-configs/BUCK @@ -0,0 +1,23 @@ +load("@fbsource//tools/build_defs/third_party:yarn_defs.bzl", "yarn_workspace") + +yarn_workspace( + name = "yarn-workspace", + srcs = glob( + ["**/*.js"], + exclude = [ + "**/__fixtures__/**", + "**/__flowtests__/**", + "**/__mocks__/**", + "**/__server_snapshot_tests__/**", + "**/__tests__/**", + "**/node_modules/**", + "**/node_modules/.bin/**", + "**/.*", + "**/.*/**", + "**/.*/.*", + "**/*.xcodeproj/**", + "**/*.xcworkspace/**", + ], + ), + visibility = ["PUBLIC"], +) diff --git a/packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js b/packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000..8af50cd1e1f --- /dev/null +++ b/packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; +const NOT_A_NATIVE_COMPONENT = ` +const requireNativeComponent = require('requireNativeComponent'); + +export default 'Not a view config' +`; + +const FULL_NATIVE_COMPONENT = ` +const requireNativeComponent = require('requireNativeComponent'); + +import type { + BubblingEvent, + DirectEvent, + WithDefault, + CodegenNativeComponent, +} from 'CodegenFlowtypes'; + +import type {ViewProps} from 'ViewPropTypes'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + boolean_default_true_optional_both?: ?WithDefault, + + // Events + onDirectEventDefinedInlineNull: (event: DirectEvent) => void, + onBubblingEventDefinedInlineNull: (event: BubblingEvent) => void, +|}>; + +type Options = { + interfaceOnly: true, + isDeprecatedPaperComponentNameRCT: true, +}; + +type ModuleType = CodegenNativeComponent<'Module', ModuleProps, Options>; + +module.exports = ((requireNativeComponent('RCTModule'): any): ModuleType); +`; + +module.exports = { + 'NotANativeComponent.js': NOT_A_NATIVE_COMPONENT, + 'FullNativeComponent.js': FULL_NATIVE_COMPONENT, +}; diff --git a/packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap b/packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap new file mode 100644 index 00000000000..7413b19d349 --- /dev/null +++ b/packages/babel-plugin-inline-view-configs/__tests__/__snapshots__/index-test.js.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Babel plugin inline view configs can inline config for FullNativeComponent.js 1`] = ` +"const requireNativeComponent = require('requireNativeComponent'); + +import type { BubblingEvent, DirectEvent, WithDefault, CodegenNativeComponent } from 'CodegenFlowtypes'; +import type { ViewProps } from 'ViewPropTypes'; +type ModuleProps = $ReadOnly<{| ...ViewProps, + // Props + boolean_default_true_optional_both?: ?WithDefault, + // Events + onDirectEventDefinedInlineNull: (event: DirectEvent) => void, + onBubblingEventDefinedInlineNull: (event: BubblingEvent) => void, +|}>; +type Options = { + interfaceOnly: true, + isDeprecatedPaperComponentNameRCT: true, +}; +type ModuleType = CodegenNativeComponent<'Module', ModuleProps, Options>; + +const registerGeneratedViewConfig = require('registerGeneratedViewConfig'); + +const ModuleViewConfig = { + uiViewClassName: 'RCTModule', + bubblingEventTypes: { + topBubblingEventDefinedInlineNull: { + phasedRegistrationNames: { + captured: 'onBubblingEventDefinedInlineNullCapture', + bubbled: 'onBubblingEventDefinedInlineNull' + } + } + }, + directEventTypes: { + topDirectEventDefinedInlineNull: { + registrationName: 'onDirectEventDefinedInlineNull' + } + }, + validAttributes: { + boolean_default_true_optional_both: true, + onDirectEventDefinedInlineNull: true, + onBubblingEventDefinedInlineNull: true + } +}; +registerGeneratedViewConfig('RCTModule', ModuleViewConfig); +module.exports = 'RCTModule'; // RCT prefix present for paper support" +`; + +exports[`Babel plugin inline view configs can inline config for NotANativeComponent.js 1`] = ` +"const requireNativeComponent = require('requireNativeComponent'); + +export default 'Not a view config';" +`; diff --git a/packages/babel-plugin-inline-view-configs/__tests__/index-test.js b/packages/babel-plugin-inline-view-configs/__tests__/index-test.js new file mode 100644 index 00000000000..da09f29a5df --- /dev/null +++ b/packages/babel-plugin-inline-view-configs/__tests__/index-test.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 {transform: babelTransform} = require('@babel/core'); +const fixtures = require('../__test_fixtures__/fixtures.js'); + +function transform(filename) { + return babelTransform(fixtures[filename], { + plugins: [require('@babel/plugin-syntax-flow'), require('../index')], + babelrc: false, + filename, + }).code; +} + +describe('Babel plugin inline view configs', () => { + Object.keys(fixtures) + .sort() + .forEach(fixtureName => { + it(`can inline config for ${fixtureName}`, () => { + expect(transform(fixtureName)).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/babel-plugin-inline-view-configs/index.js b/packages/babel-plugin-inline-view-configs/index.js new file mode 100644 index 00000000000..235e870912c --- /dev/null +++ b/packages/babel-plugin-inline-view-configs/index.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 {parseString} = require('react-native-codegen/src/parsers/flow'); +const RNCodegen = require('react-native-codegen/src/generators/RNCodegen'); +const path = require('path'); + +function generateViewConfig(filename, code) { + const schema = parseString(code); + + const libraryName = path + .basename(filename) + .replace(/NativeComponent\.js$/, ''); + + return RNCodegen.generateViewConfig({ + schema, + libraryName, + }); +} + +module.exports = function(context) { + return { + pre(state) { + this.code = state.code; + this.filename = state.opts.filename; + this.inserted = false; + }, + visitor: { + TypeAlias(nodePath, state) { + if ( + !this.inserted && + nodePath.node.right && + nodePath.node.right.type === 'GenericTypeAnnotation' && + nodePath.node.right.id.name === 'CodegenNativeComponent' + ) { + const code = generateViewConfig(this.filename, this.code); + + // Remove the original export + nodePath.parentPath.traverse({ + MemberExpression(exportPath) { + if (exportPath.node.property.name === 'exports') { + exportPath.parentPath.remove(); + } + }, + }); + nodePath.insertAfter(context.parse(code).program.body); + this.inserted = true; + } + }, + }, + }; +}; diff --git a/packages/babel-plugin-inline-view-configs/package.json b/packages/babel-plugin-inline-view-configs/package.json new file mode 100644 index 00000000000..1c7b21410cf --- /dev/null +++ b/packages/babel-plugin-inline-view-configs/package.json @@ -0,0 +1,16 @@ +{ + "version": "0.0.0", + "name": "babel-plugin-inline-view-configs", + "description": "Babel plugin to inline view configs for React Native", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git" + }, + "dependencies": { + "react-native-codegen": "*" + }, + "devDependencies": { + "@babel/core": "^7.0.0" + }, + "license": "MIT" +} diff --git a/packages/react-native-codegen/BUCK b/packages/react-native-codegen/BUCK index 8cd540b1244..9fab520a513 100644 --- a/packages/react-native-codegen/BUCK +++ b/packages/react-native-codegen/BUCK @@ -2,6 +2,7 @@ load("@fbsource//tools/build_defs:default_platform_defs.bzl", "ANDROID", "APPLE" load("@fbsource//tools/build_defs:fb_native_wrapper.bzl", "fb_native") load("@fbsource//tools/build_defs:fb_xplat_cxx_binary.bzl", "fb_xplat_cxx_binary") load("@fbsource//tools/build_defs/oss:rn_defs.bzl", "rn_xplat_cxx_library") +load("@fbsource//tools/build_defs/third_party:yarn_defs.bzl", "yarn_workspace") load("@fbsource//xplat/js/react-native-github/packages/react-native-codegen:DEFS.bzl", "rn_codegen_test") fb_native.sh_binary( @@ -173,3 +174,25 @@ rn_xplat_cxx_library( ":generated_components-TWO_COMPONENTS_SAME_FILE", ], ) + +yarn_workspace( + name = "yarn-workspace", + srcs = glob( + ["**/*.js"], + exclude = [ + "**/__fixtures__/**", + "**/__flowtests__/**", + "**/__mocks__/**", + "**/__server_snapshot_tests__/**", + "**/__tests__/**", + "**/node_modules/**", + "**/node_modules/.bin/**", + "**/.*", + "**/.*/**", + "**/.*/.*", + "**/*.xcodeproj/**", + "**/*.xcworkspace/**", + ], + ), + visibility = ["PUBLIC"], +) diff --git a/packages/react-native-codegen/package.json b/packages/react-native-codegen/package.json index 7fffd481876..85a9de6d5ac 100644 --- a/packages/react-native-codegen/package.json +++ b/packages/react-native-codegen/package.json @@ -1,6 +1,6 @@ { "version": "0.0.1", - "name": "react-native-codgen", + "name": "react-native-codegen", "description": "⚛️ Code generation tools for React Native", "repository": { "type": "git", diff --git a/packages/react-native-codegen/src/cli/parser/parser.js b/packages/react-native-codegen/src/cli/parser/parser.js index c1ec866f8a8..1cc5247033b 100644 --- a/packages/react-native-codegen/src/cli/parser/parser.js +++ b/packages/react-native-codegen/src/cli/parser/parser.js @@ -14,7 +14,10 @@ const FlowParser = require('../../parsers/flow'); function parseFiles(files: Array) { files.forEach(filename => { - console.log(filename, JSON.stringify(FlowParser.parse(filename), null, 2)); + console.log( + filename, + JSON.stringify(FlowParser.parseFile(filename), null, 2), + ); }); } diff --git a/packages/react-native-codegen/src/cli/viewconfigs/generate-view-configs.js b/packages/react-native-codegen/src/cli/viewconfigs/generate-view-configs.js index 64abcb27935..9730f9722ed 100644 --- a/packages/react-native-codegen/src/cli/viewconfigs/generate-view-configs.js +++ b/packages/react-native-codegen/src/cli/viewconfigs/generate-view-configs.js @@ -33,7 +33,7 @@ function generateFilesWithResults( return files.reduce((aggregated, filename) => { const schema = config.parser === 'flow' - ? FlowParser.parse(filename) + ? FlowParser.parseFile(filename) : SchemaParser.parse(filename); if (schema && schema.modules) { const libraryName = path diff --git a/packages/react-native-codegen/src/generators/RNCodegen.js b/packages/react-native-codegen/src/generators/RNCodegen.js index ac9383f4f2f..1a83cc27f90 100644 --- a/packages/react-native-codegen/src/generators/RNCodegen.js +++ b/packages/react-native-codegen/src/generators/RNCodegen.js @@ -118,4 +118,18 @@ module.exports = { return writeMapToFiles(filesToUpdate, outputDirectory); }, + generateViewConfig({libraryName, schema}: Options): string { + schemaValidator.validate(schema); + + const result = generateViewConfigJs + .generate(libraryName, schema) + .values() + .next(); + + if (typeof result.value !== 'string') { + throw new Error(`Failed to generate view config for ${libraryName}`); + } + + return result.value; + }, }; diff --git a/packages/react-native-codegen/src/parsers/flow/__tests__/parser-test.js b/packages/react-native-codegen/src/parsers/flow/__tests__/parser-test.js index 05c7374569a..3807a1670ba 100644 --- a/packages/react-native-codegen/src/parsers/flow/__tests__/parser-test.js +++ b/packages/react-native-codegen/src/parsers/flow/__tests__/parser-test.js @@ -22,7 +22,7 @@ describe('RN Codegen Flow Parser', () => { .sort() .forEach(fixtureName => { it(`can generate fixture ${fixtureName}`, () => { - expect(FlowParser.parse(fixtureName)).toMatchSnapshot(); + expect(FlowParser.parseFile(fixtureName)).toMatchSnapshot(); }); }); }); diff --git a/packages/react-native-codegen/src/parsers/flow/index.js b/packages/react-native-codegen/src/parsers/flow/index.js index 861b444412e..019560fcf97 100644 --- a/packages/react-native-codegen/src/parsers/flow/index.js +++ b/packages/react-native-codegen/src/parsers/flow/index.js @@ -71,8 +71,7 @@ function getPropProperties(propsTypeName, types) { } } -function parseFileAst(filename: string) { - const contents = fs.readFileSync(filename, 'utf8'); +function processString(contents: string) { const ast = flowParser.parse(contents); const types = getTypes(ast); @@ -96,10 +95,17 @@ function parseFileAst(filename: string) { }; } -function parse(filename: string): ?SchemaType { - return buildSchema(parseFileAst(filename)); +function parseFile(filename: string): ?SchemaType { + const contents = fs.readFileSync(filename, 'utf8'); + + return buildSchema(processString(contents)); +} + +function parseString(contents: string): ?SchemaType { + return buildSchema(processString(contents)); } module.exports = { - parse, + parseFile, + parseString, };