Make it possible to write JS only plugins

Summary:
This diff is part of the bigger task T60496135

This diff is based on D18706643, extracting only the react native module parts

It implements the entire Android client api for JavaScript, so that there is feature parity. However this implementation is happy path only, and edge cases will be handled in separate diffs

Reviewed By: jknoxville

Differential Revision: D19310265

fbshipit-source-id: 589716fe059952bdde98df84ed250c5c6feaa118
This commit is contained in:
Michel Weststrate
2020-01-16 04:45:03 -08:00
committed by Facebook Github Bot
parent 3fab1f8fd6
commit c7158f4517
19 changed files with 5308 additions and 0 deletions

View File

@@ -0,0 +1 @@
*.pbxproj -text

View File

@@ -0,0 +1,42 @@
# OSX
#
.DS_Store
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
# BUCK
buck-out/
\.buckd/
*.keystore

View File

@@ -0,0 +1,17 @@
# react-native-flipper
## Getting started
`$ npm install react-native-flipper --save`
### Mostly automatic installation
`$ react-native link react-native-flipper`
## Usage
```javascript
import Flipper from 'react-native-flipper';
// TODO: What to do with the module?
Flipper;
```

View File

@@ -0,0 +1,14 @@
README
======
If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm:
1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed
2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK
```
ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/{username}/Library/Android/sdk
```
3. Delete the `maven` folder
4. Run `./gradlew installArchives`
5. Verify that latest set of generated files is in the maven folder with the correct version number

View File

@@ -0,0 +1,163 @@
/*
* 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.
*/
// android/build.gradle
// based on:
//
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle
// original location:
// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle
//
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle
// original location:
// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle
def DEFAULT_COMPILE_SDK_VERSION = 28
def DEFAULT_BUILD_TOOLS_VERSION = '28.0.3'
def DEFAULT_MIN_SDK_VERSION = 16
def DEFAULT_TARGET_SDK_VERSION = 28
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
apply plugin: 'com.android.library'
apply plugin: 'maven'
buildscript {
// The Android Gradle plugin is only required when opening the android folder stand-alone.
// This avoids unnecessary downloads and potential conflicts when the library is included as a
// module dependency in an application project.
// ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies
if (project == rootProject) {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
}
}
}
apply plugin: 'com.android.library'
apply plugin: 'maven'
android {
compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
versionCode 1
versionName "1.0"
}
lintOptions {
abortOnError false
}
}
repositories {
// ref: https://www.baeldung.com/maven-local-repository
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Android JSC is installed from npm
url "$rootDir/../node_modules/jsc-android/dist"
}
google()
jcenter()
}
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
implementation 'com.facebook.soloader:soloader:0.6.0+'
implementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.yoga'
exclude group:'com.facebook.flipper', module: 'fbjni'
exclude group:'com.facebook.litho', module: 'litho-annotations'
exclude group:'com.squareup.okhttp3'
}
}
def configureReactNativePom(def pom) {
def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text)
pom.project {
name packageJson.title
artifactId packageJson.name
version = packageJson.version
group = "com.facebook.flipper.reactnative"
description packageJson.description
url packageJson.repository.baseUrl
licenses {
license {
name packageJson.license
url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename
distribution 'repo'
}
}
developers {
developer {
id packageJson.author.username
name packageJson.author.name
}
}
}
}
afterEvaluate { project ->
// some Gradle build hooks ref:
// https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html
task androidJavadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += files(android.bootClasspath)
classpath += files(project.getConfigurations().getByName('compile').asList())
include '**/*.java'
}
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
classifier = 'javadoc'
from androidJavadoc.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
include '**/*.java'
}
android.libraryVariants.all { variant ->
def name = variant.name.capitalize()
task "jar${name}"(type: Jar, dependsOn: variant.javaCompile) {
from variant.javaCompile.destinationDir
}
}
artifacts {
archives androidSourcesJar
archives androidJavadocJar
}
task installArchives(type: Upload) {
configuration = configurations.archives
repositories.mavenDeployer {
// Deploy to react-native-event-bridge/maven, ready to publish to npm
repository url: "file://${projectDir}/../android/maven"
configureReactNativePom pom
}
}
}

View File

@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.flipper.reactnative">
</manifest>

View File

@@ -0,0 +1,170 @@
/*
* 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.
*/
package com.facebook.flipper.reactnative;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.core.FlipperArray;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.core.FlipperConnection;
import com.facebook.flipper.core.FlipperObject;
import com.facebook.flipper.core.FlipperPlugin;
import com.facebook.flipper.core.FlipperReceiver;
import com.facebook.flipper.core.FlipperResponder;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
@ReactModule(name = FlipperModule.NAME)
public class FlipperModule extends ReactContextBaseJavaModule {
public static final String NAME = "Flipper";
private final ReactApplicationContext reactContext;
private final FlipperClient flipperClient;
private final Map<String, FlipperConnection> connections;
private final Map<String, FlipperResponder> responders;
private final AtomicLong responderId = new AtomicLong();
public FlipperModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
this.flipperClient = AndroidFlipperClient.getInstanceIfInitialized();
this.connections = new ConcurrentHashMap<>();
this.responders = new ConcurrentHashMap<>();
}
@Override
public String getName() {
return NAME;
}
@ReactMethod
public void registerPlugin(
final String pluginId,
final Boolean inBackground,
final Callback onConnect,
final Callback onDisconnect) {
FlipperPlugin plugin =
new FlipperPlugin() {
@Override
public String getId() {
return pluginId;
}
@Override
public void onConnect(FlipperConnection connection) throws Exception {
FlipperModule.this.connections.put(pluginId, connection);
onConnect.invoke();
}
@Override
public void onDisconnect() throws Exception {
FlipperModule.this.connections.remove(pluginId);
onDisconnect.invoke();
}
@Override
public boolean runInBackground() {
return inBackground;
}
};
this.flipperClient.addPlugin(plugin);
}
@ReactMethod
public void send(String pluginId, String method, String data) {
// Optimization: throwing raw strings around to the desktop would probably avoid some double
// parsing...
Object parsedData = FlipperModule.parseJSON(data);
FlipperConnection connection = this.connections.get(pluginId);
if (parsedData instanceof FlipperArray) {
connection.send(method, (FlipperArray) parsedData);
} else {
connection.send(method, (FlipperObject) parsedData);
}
}
@ReactMethod
public void reportErrorWithMetadata(String pluginId, String reason, String stackTrace) {
this.connections.get(pluginId).reportErrorWithMetadata(reason, stackTrace);
}
@ReactMethod
public void reportError(String pluginId, String error) {
this.connections.get(pluginId).reportError(new Error(error));
}
@ReactMethod
public void subscribe(String pluginId, String method, final Callback callback) {
this.connections
.get(pluginId)
.receive(
method,
new FlipperReceiver() {
@Override
public void onReceive(FlipperObject params, FlipperResponder responder)
throws Exception {
String id = String.valueOf(FlipperModule.this.responderId.incrementAndGet());
FlipperModule.this.responders.put(id, responder);
callback.invoke(params.toJsonString(), id);
}
});
}
@ReactMethod
public void respondSuccess(String responderId, String data) {
FlipperResponder responder = FlipperModule.this.responders.remove(responderId);
if (data == null) {
responder.success();
} else {
Object parsedData = FlipperModule.parseJSON(data);
if (parsedData instanceof FlipperArray) {
responder.success((FlipperArray) parsedData);
} else {
responder.success((FlipperObject) parsedData);
}
}
}
@ReactMethod
public void respondError(String responderId, String data) {
FlipperResponder responder = FlipperModule.this.responders.remove(responderId);
Object parsedData = FlipperModule.parseJSON(data);
if (parsedData instanceof FlipperArray) {
responder.success((FlipperArray) parsedData);
} else {
responder.success((FlipperObject) parsedData);
}
}
private static Object /* FlipperArray | FlipperObject */ parseJSON(String json) {
// returns either a FlipperObject or Flipper array, pending the data
try {
JSONTokener tokener = new JSONTokener(json);
if (tokener.nextClean() == '[') {
tokener.back();
return new FlipperArray(new JSONArray(tokener));
} else {
tokener.back();
return new FlipperObject(new JSONObject(tokener));
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.
*/
package com.facebook.flipper.reactnative;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class FlipperPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new FlipperModule(reactContext));
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,83 @@
/**
* 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
*/
declare namespace Flipper {
/**
* A FlipperPlugin is an object which exposes an API to the Desktop Flipper application. When a
* connection is established the plugin is given a FlipperConnection on which it can register
* request handlers and send messages. When the FlipperConnection is invalid onDisconnect is called.
* onConnect may be called again on the same plugin object if Flipper re-connects, this will provide
* a new FlipperConnection, do not attempt to re-use the previous connection.
*/
interface FlipperPlugin {
/**
* @return The id of this plugin. This is the namespace which Flipper desktop plugins will call
* methods on to route them to your plugin. This should match the id specified in your React
* plugin.
*/
getId(): string;
/**
* Called when a connection has been established. The connection passed to this method is valid
* until {@link FlipperPlugin#onDisconnect()} is called.
*/
onConnect(connection: FlipperConnection): void;
/**
* Called when the connection passed to `FlipperPlugin#onConnect(FlipperConnection)` is no
* longer valid. Do not try to use the connection in or after this method has been called.
*/
onDisconnect(): void;
/**
* Returns true if the plugin is meant to be run in background too, otherwise it returns false.
*/
runInBackground(): boolean;
}
export interface FlipperResponder {
success(response?: any): void;
error(response: any): void;
}
export interface FlipperConnection {
send(method: string, data: any): void;
reportErrorWithMetadata(reason: string, stackTrace: string): void;
reportError(error: Error): void;
receive(
method: string,
listener: (params: any, responder: FlipperResponder) => void,
): void;
}
}
declare module 'Flipper' {
export function registerPlugin(
pluginId: string,
runInBackground: boolean,
onConnect: () => void,
onDisconnect: () => void,
): void;
export function send(pluginId: string, method: string, data: string): void;
export function reportErrorWithMetadata(
pluginId: string,
reason: string,
stackTrace: string,
): void;
export function reportError(pluginId: string, error: string): void;
export function subscribe(
pluginId: string,
method: string,
listener: (data: string, responderId: number) => void,
): void;
export function respondSuccess(responderId: string, data?: string): void;
export function respondError(responderId: string, error: string): void;
}
export function addPlugin(plugin: Flipper.FlipperPlugin): void;

View File

@@ -0,0 +1,100 @@
/**
* 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
*/
// $FlowFixMe
import {NativeModules} from 'react-native';
const {Flipper} = NativeModules;
export default Flipper;
class Connection {
connected;
pluginId;
constructor(pluginId) {
this.connected = false;
this.pluginId = pluginId;
}
send(method, data) {
if (!this.connected) {
throw new Error('Cannot send data, not connected');
}
Flipper.send(this.pluginId, method, JSON.stringify(data));
}
reportErrorWithMetadata(reason, stackTrace) {
Flipper.reportErrorWithMetadata(this.pluginId, reason, stackTrace);
}
reportError(error) {
Flipper.reportError(this.pluginId, error);
}
receive(method, listener) {
if (!this.connected) {
throw new Error('Cannot receive data, not connected');
}
Flipper.subscribe(this.pluginId, method, (data, responderId) => {
const responder = new Responder(responderId);
listener(JSON.parse(data), responder);
});
}
}
class Responder {
responderId;
constructor(responderId) {
this.responderId = responderId;
}
success(response) {
Flipper.respondSuccess(
this.responderId,
response == null ? null : JSON.stringify(response),
);
}
error(response) {
Flipper.respondError(this.responderId, JSON.stringify(response));
}
}
// $FlowFixMe
export function addPlugin(plugin) {
if (!plugin || typeof plugin !== 'object') {
throw new Error('Expected plugin, got ' + plugin);
}
['getId', 'onConnect', 'onDisconnect'].forEach(method => {
if (typeof plugin[method] !== 'function') {
throw new Error(`Plugin misses an implementation for '${method}'`);
}
});
const runInBackground =
typeof plugin.runInBackground === 'function'
? !!plugin.runInBackground()
: false;
const id = plugin.getId();
const connection = new Connection(id);
Flipper.registerPlugin(
id,
runInBackground,
function onConnect() {
connection.connected = true;
plugin.onConnect(connection);
},
function onDisconnect() {
connection.connected = false;
plugin.onDisconnect();
},
);
}

View File

@@ -0,0 +1,12 @@
/*
* 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.
*/
#import <React/RCTBridgeModule.h>
@interface Flipper : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
#import "Flipper.h"
@implementation Flipper
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(sampleMethod:(NSString *)stringArgument numberParameter:(nonnull NSNumber *)numberArgument callback:(RCTResponseSenderBlock)callback)
{
// TODO: Implement some actually useful functionality
callback(@[[NSString stringWithFormat: @"numberArgument: %@ stringArgument: %@", numberArgument, stringArgument]]);
}
@end

View File

@@ -0,0 +1,34 @@
{
"name": "react-native-flipper",
"title": "React Native Flipper",
"version": "1.0.0",
"description": "TODO",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/github_account/react-native-flipper.git",
"baseUrl": "https://github.com/github_account/react-native-flipper"
},
"keywords": [
"react-native"
],
"author": {
"name": "Your Name",
"email": "yourname@email.com"
},
"license": "MIT",
"licenseFilename": "LICENSE",
"readmeFilename": "README.md",
"peerDependencies": {
"react": "^16.8.1",
"react-native": ">=0.59.0-rc.0 <1.0.x"
},
"devDependencies": {
"react": "^16.8.3",
"react-native": "^0.59.10"
}
}

View File

@@ -0,0 +1,31 @@
# 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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "react-native-flipper"
s.version = package["version"]
s.summary = package["description"]
s.description = <<-DESC
react-native-flipper
DESC
s.homepage = "https://github.com/github_account/react-native-flipper"
s.license = "MIT"
# s.license = { :type => "MIT", :file => "FILE_LICENSE" }
s.authors = { "Your Name" => "yourname@email.com" }
s.platforms = { :ios => "9.0" }
s.source = { :git => "https://github.com/github_account/react-native-flipper.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,swift}"
s.requires_arc = true
s.dependency "React"
# ...
# s.dependency "..."
end

File diff suppressed because it is too large Load Diff