LiquidPlayer / LiquidCore

Node.js virtual machine for Android and iOS
MIT License
1.01k stars 127 forks source link

A LiquidCore for React Native? #153

Open emclab opened 4 years ago

emclab commented 4 years ago

I am installing LiquidCore 0.7.2 in a React Native (0.61.5) app on macOS Catalina, the node installed is 12.16.0 by following instruction here. The React Native app has a iOS subdirectory which holds iOS related files. After yarn add liquid core successfully, the installation is at the last step under /iOS subdirectories:

pod install

Here is the error created:

pod install
Analyzing dependencies
[!] No podspec found for `liquidcore_bundle` in `.liquidcore/liquidcore_bundle.podspec`

Here is the content of `./liquidcore/liquidcore_bundle.podspec`:

cat liquidcore_bundle.podspec
Pod::Spec.new do |s|
  s.name = 'liquidcore_bundle'
  s.version = '1.0.0'
  s.summary = 'Bundled JS files for LiquidCore'
  s.description = 'A pod containing the files generated from the JS bundler'
  s.author = ''
  s.homepage = 'http'
  s.source = { :git => "" }
  s.resource_bundles = {
   'LiquidCore' => [
      'ios_bundle/*.js'
   ]
 }
 s.script_phase = { :name => 'Bundle JavaScript Files', :script => '(cd ..; node node_modules/liquidcore/lib/cli.js bundle --platform=ios)' }

Here is app's `package.json`:

  {
  "name": "ipat_test2",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "server": "node node_modules/liquidcore/lib/cli.js server",
    "bundler": "node node_modules/liquidcore/lib/cli.js bundle",
    "init": "node node_modules/liquidcore/lib/cli.js init",
    "gradle-config": "node node_modules/liquidcore/lib/cli.js gradle",
    "pod-config": "node node_modules/liquidcore/lib/cli.js pod",
    "postinstall": "node node_modules/liquidcore/lib/cli.js postinstall"
  },
  "dependencies": {
    "liquidcore": "^0.7.2",
    "react": "16.9.0",
    "react-native": "0.61.5"
  },
  "devDependencies": {
    "@babel/core": "^7.6.2",
    "@babel/runtime": "^7.6.2",
    "@react-native-community/eslint-config": "^0.0.5",
    "babel-jest": "^24.9.0",
    "eslint": "^6.5.1",
    "jest": "^24.9.0",
    "metro-react-native-babel-preset": "^0.56.0",
    "react-test-renderer": "16.9.0"
  },
  "jest": {
    "preset": "react-native"
  },
  "liquidcore": {
    "entry": [
      "example.js"
    ],
    "pod_options": {
      "target": "ipat_test2",
      "podfile": "/ios/"
    },
    "bundler_output": {
      "ios": ".liquidcore/ios_bundle"
    }
  }
}

Any comment is welcome!

ericwlange commented 4 years ago

Hi @emclab ,

React Native on LiquidCore is not officially supported, but I have successfully hacked it to work. The issues:

That said, what you can do is use a ReactNativeSurface addon to LiquidCore, which works with a caraml-core view.

The documentation of caraml-core is up-to-date, but unfortunately ReactNativeSurface is not, even though the code is.

You are welcome to try to get it to work, but it may be difficult without much guidance.

The challenge with RN is getting it to use the same JavaScript context (and in the case of Android, to even use the same JavaScript engine) as LiquidCore. You should be able to easily use them both side-by-side, however they don't talk to each other at all. ReactNativeSurface is a hacked version of RN that forces them to use the same context/engine. The advantage is that you can seamlessly use both RN and Node.js in the same code. The downside is that it is extremely difficult (for me) to maintain, and unlike Node, where LTS versions are pretty stable for a long time, RN changes constantly and each version is incompatible with the last.

The other option is to write a RN plugin that can spawn a LiquidCore instance. You would still need separate code for the RN pieces and the Node.js pieces, but it would be possible for them to communicate via emitters. This is actually a really good idea and I will look into what it takes to write, but I have no timeframe to give.

As for the error you are getting, that is very curious as obviously the podspec does exist. Are you updated to the latest cocoapods version? Is there any more output you can share?

emclab commented 4 years ago

Hi, thank you for the comments and ideas. My current installed version of cocoapods is 1.8.4 which is the latest stable release. Before installing liquidCore, I tried nodejs-mobile-react-native and ipfs with no success. I am not sure why those 2 modules are not working together. My mobile project requires both nodejs and ipfs

emclab commented 4 years ago

Here is an post on stackoverflow.com about mobile nodejs modules. I notice on IOS LiquidCore uses JavascriptCore which is used by IOS as well and nodejs-mobile uses chakra engine instead. Also LiquidCore claims complete nodejs runtime environment which my understanding is seamless compatibility with current nodejs modules. Can you comment on LiquidCore vs nodejs-mobile about compatibility (with hundreds of existing nodejs modules) and performance? Also what is the preferring develop platform for the LiquidCore? Native IOS and Android app? React Native is getting more popular but it is not truly native. Many thanks.

ericwlange commented 4 years ago

Hi @emclab. I have a proposal for how to handle this, which correlates to the second option mentioned above. Doing a deep integration with LiquidCore and RN would be ideal, but it is impossible for me to keep up with the pace of development on RN. As a workaround, I propose building a LiquidCore plugin for React Native. This would effectively decouple the two platforms. The downside is that you would have two separate asynchronous JavaScript codebases only connected through a messaging API.

I have created a proposed API definition, which is attached as a zip file containing the jsdoc output. Could you take a look at it and see if makes sense, and post any comments/questions?

liquidcore-rn.zip

ericwlange commented 4 years ago

Classes

MicroService

A MicroService is the basic building block of LiquidCore. It encapsulates the runtime environment for a client-side micro app. A MicroService is a complete virtual machine whose operation is defined by the code referenced by the service URI. When a MicroService is instantiated, its Node.js environment is set up, its code downloaded (or fetched from cache) from the URI, and is executed in a VM. The host may interact with the VM via a simple message-based API.

Functions

bundle(bundleName, [options])string

Generates a URL for fetching from the LiquidCore bundle. If app is compiled in DEBUG mode, this will attempt to first download from the development server. If the server is unreachable, then it will default to the packaged bundle. In release mode, this will always reference the packaged bundle.

serviceFromInstanceId(instanceId)MicroService

Each MicroService instance is mapped to a unique string id. This id can be serialized in UIs and the instance retrieved by a call to this method.

uninstall(serviceURI)

Uninstalls the MicroService from this host, and removes any global data associated with the service.

MicroService

A MicroService is the basic building block of LiquidCore. It encapsulates the runtime environment for a client-side micro app. A MicroService is a complete virtual machine whose operation is defined by the code referenced by the service URI. When a MicroService is instantiated, its Node.js environment is set up, its code downloaded (or fetched from cache) from the URI, and is executed in a VM. The host may interact with the VM via a simple message-based API.

Kind: global class

new MicroService(serviceURI)

Creates a new instance of the micro service referenced by serviceURI.

Param Type Description
serviceURI string The URI (can be a network URL or local file/resource) of the micro service code

microService.serviceURI : string

The URI from which the service was started

Kind: instance property of MicroService
Read only: true

microService.state : integer

The current state of the MicroService

Kind: instance property of MicroService

microService.instanceId : string

Each MicroService instance is mapped to a unique string id. This id can be serialized in UIs and the instance retrieved by a call to serviceFromInstanceId.

Kind: instance property of MicroService
Read only: true

microService.addListener(eventName, listener) ⇒ MicroService

Alias for MicroService.on

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
eventName string The event to listen for
listener function A callback function

microService.emit(eventName, [...args]) ⇒ boolean

Synchronously calls each of the listeners registered for the event named eventName, in the order they were registered, passing the supplied arguments to each.

Note that the arguments will be serialized. That is, functions will be passed as empty objects and cannot be called.

Returns true if the event had listeners, false otherwise.

Kind: instance method of MicroService
Returns: boolean - true if the event had listeners, false otherwise

Param Type Description
eventName string The event to emit
[...args] any Arguments to be passed to listeners

microService.eventNames() ⇒ Array.<string>

Returns an array listing the events for which the emitter has registered listeners. The values in the array will be strings.

Kind: instance method of MicroService
Returns: Array.<string> - Array of events for which there are registered emitters

microService.getMaxListeners() ⇒ integer

Returns the current max listener value for the EventEmitter which is either set by MicroService.setMaxListeners or defaults to defaultMaxListeners.

Kind: instance method of MicroService
Returns: integer - current max listener value

microService.listenerCount(eventName) ⇒

Returns the number of listeners listening to the event named {@param eventName}.

Kind: instance method of MicroService
Returns: the number of listeners listening to the event named eventName.

Param Type Description
eventName string The name of the event being listened for

microService.listeners(eventName) ⇒ Array.<function()>

Returns a copy of the array of listeners for the event named {@param eventName}.

Kind: instance method of MicroService
Returns: Array.<function()> - copy of the array of listeners for the event

Param Type Description
eventName string The name of the event being listened for

microService.off(eventName, listener) ⇒ MicroService

Alias for MicroService.removeListener.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
eventName string The name of the event to remove listener from
listener function A callback function

microService.on(eventName, listener) ⇒ MicroService

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

Returns a reference to the MicroService, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The MicroService.prependListener method can be used as an alternative to add the event listener to the beginning of the listeners array.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
eventName string The name of the event
listener function A callback function

microService.once(eventName, listener) ⇒ MicroService

Adds a one-time listener function for the event named eventName. The next time eventName is triggered, this listener is removed and then invoked.

Returns a reference to the MicroService, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The MicroService.prependOnceListener method can be used as an alternative to add the event listener to the beginning of the listeners array.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
eventName string The name of the event
listener function A callback function

microService.prependListener(eventName, listener) ⇒ MicroService

Adds the listener function to the beginning of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

Returns a reference to the MicroService, so that calls can be chained.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
eventName string The name of the event
listener function A callback function

microService.prependOnceListener(eventName, listener) ⇒ MicroService

Adds a one-time listener function for the event named eventName to the beginning of the listeners array. The next time eventName is triggered, this listener is removed, and then invoked.

Returns a reference to the MicroService, so that calls can be chained.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
eventName string The name of the event
listener function A callback function

microService.removeAllListeners([eventName]) ⇒ MicroService

Removes all listeners, or those of the specified eventName.

It is bad practice to remove listeners added elsewhere in the code, particularly when the MicroService instance was created by some other component or module.

Returns a reference to the MicroService, so that calls can be chained.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
[eventName] string The optional name of the event

microService.removeListener(eventName, listener) ⇒ MicroService

Removes the specified listener from the listener array for the event named eventName.

removeListener() will remove, at most, one instance of a listener from the listener array. If any single listener has been added multiple times to the listener array for the specified eventName, then removeListener() must be called multiple times to remove each instance.

Once an event has been emitted, all listeners attached to it at the time of emitting will be called in order. This implies that any removeListener() or removeAllListeners() calls after emitting and before the last listener finishes execution will not remove them from emit() in progress. Subsequent events will behave as expected.

Because listeners are managed using an internal array, calling this will change the position indices of any listener registered after the listener being removed. This will not impact the order in which listeners are called, but it means that any copies of the listener array as returned by the MicroService.listeners method will need to be recreated.

Returns a reference to the MicroService, so that calls can be chained.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
eventName string The name of the event
listener function A callback function

microService.setMaxListeners(n) ⇒ MicroService

By default EventEmitters will print a warning if more than 10 listeners are added for a particular event. This is a useful default that helps finding memory leaks. Obviously, not all events should be limited to just 10 listeners. setMaxListeners() method allows the limit to be modified for this specific EventEmitter instance. The value can be set to Infinity (or 0) to indicate an unlimited number of listeners.

Returns a reference to the MicroService, so that calls can be chained.

Kind: instance method of MicroService
Returns: MicroService - - this object

Param Type Description
n integer The number of listeners to allow

microService.start([args]) ⇒ Promise.<MicroService>

Starts the MicroService. This method will return immediately and initialization and startup will occur asynchronously in a separate thread. It will download the code from the service URI (if not cached), set the arguments in process.argv and execute the script.

Kind: instance method of MicroService
Returns: Promise.<MicroService> - A promise that will be resolved with this object upon successful starting of the process.

Param Type Description
[args] any The list of arguments to sent to the MicroService. This is similar to running node from a command line. The first two arguments will be the application (node) followed by the local module code (/home/module/[service.js). args will then be appended in process.argv[2:]

microService.exit(exitCode) ⇒ Promise.<MicroService>

Instructs the VM to halt execution as quickly as possible

Kind: instance method of MicroService
Returns: Promise.<MicroService> - A promise that will be resolved upon successful exit

Param Type Description
exitCode integer The exit code

"start"

Event indicating when the MicroService has successfully started.

Triggers when the MicroService has been inititialized and the environment is ready to receive event listeners. This is called after the Node.js environment has been set up, but before the micro service JavaScript code has been executed. It is safe to add any event listeners here, but emitted events will not be seen by the JavaScript service until its code has been run. The JavaScript code should emit an event to let the host know that it is ready to receive events.

Kind: event emitted by MicroService

"exit" (exitCode)

Event indicating when the MicroService has successfully exited.

Triggers when the MicroService has exited gracefully. The MicroService is no longer available and is shutting down. Called immediately before the MicroService exits. This is a graceful exit, and is mutually exclusive with the 'error' event. Only one of either the exit event or error event will be triggered from any MicroService.

A single parameter is the exit code emitted by the Node.js process

Kind: event emitted by MicroService

Param Type Description
exitCode integer The exit code emitted by the Node.js process

"error" (errorMessage)

Event indicating when the MicroService has exited unexpectedly.

Triggers on any errors that may cause the MicroService to shut down unexpectedly. The MicroService is no longer available and may have already crashed.

Triggered upon an exception state. This is an unexpected exit, and is mutually exclusive with the 'exit' event. Only one of either the exit event or error event will be triggered from any MicroService.

A single parameter is the error message of the thrown exception

Kind: event emitted by MicroService

Param Type Description
errorMessage string The error message of the thrown exception

"newListener" (eventName, listener)

The MicroService instance will emit its own 'newListener' event before a listener is added to its internal array of listeners.

Listeners registered for the 'newListener' event will be passed the event name and a reference to the listener being added.

The fact that the event is triggered before adding the listener has a subtle but important side effect: any additional listeners registered to the same name within the 'newListener' callback will be inserted before the listener that is in the process of being added.

Kind: event emitted by MicroService

Param Type Description
eventName string The name of the event being listened for
listener function The event handler function

"removeListener" (eventName, listener)

The 'removeListener' event is emitted after the listener is removed.

Kind: event emitted by MicroService

Param Type Description
eventName string The name of the event being listened for
listener function The event handler function

MicroService.STATE_INIT : string

State indicating that MicroService is initializing

Kind: static property of MicroService
Read only: true

MicroService.STATE_RUNNING : string

State indicating that MicroService is running

Kind: static property of MicroService
Read only: true

MicroService.STATE_DEFUNCT : string

State indicating that the MicroService is no longer valid (either exited normally or failed)

Kind: static property of MicroService
Read only: true

MicroService.defaultMaxListeners : integer

The default number of max listeners allowed

Kind: static property of MicroService
Read only: true

bundle(bundleName, [options]) ⇒ string

Generates a URL for fetching from the LiquidCore bundle. If app is compiled in DEBUG mode, this will attempt to first download from the development server. If the server is unreachable, then it will default to the packaged bundle. In release mode, this will always reference the packaged bundle.

Kind: global function
Returns: string - A service URL for use in the MicroService constructor

Param Type Description
bundleName string The name of the bundled file (ex. 'index' or 'example.js')
[options] Object Development server options
[options.port] integer The port on which the server resides
[options.serverURL] string An alternate server URL
[options.requestParams] Object An object containing key-value pairs to be added to the GET request

serviceFromInstanceId(instanceId) ⇒ MicroService

Each MicroService instance is mapped to a unique string id. This id can be serialized in UIs and the instance retrieved by a call to this method.

Kind: global function
Returns: MicroService - The associated MicroService or undefined if no such service is active.

Param Type Description
instanceId string An id returned by the instanceId property

uninstall(serviceURI)

Uninstalls the MicroService from this host, and removes any global data associated with the service.

Kind: global function

Param Type Description
serviceURI string The URI of the service (should be the same URI that the service was started with).
ericwlange commented 4 years ago

I converted to markdown (jsdoc-to-markdown project is great!)

ericwlange commented 4 years ago

The file level documentation did not get generated. Included here:

React Native LiquidCore API

This is the API for interacting with LiquidCore from React Native. Although both LiquidCore and React Native use JavaScript as their programming language, and npm to distribute, they utlize separate JavaScript context groups on iOS and completely different core JavaScript implementations on Android. Thus, they cannot directly run the same code. This API enables launching a LiquidCore MicroService instance from React Native, and communicating through a messaging wormhole.

Sample integration of the example.js micro service with a simple React Native app:

 import React from 'react';
 import { StyleSheet, Text, View } from 'react-native';
 import { MicroService, bundle } from 'liquidcore-rn';

 const ExampleBundle = bundle('example.js');
 let service = new MicroService(ExampleBundle);
 let hello = {
   text: 'Loading ...'
 };
 service.start().then(()=>{
   service.on('ready', () => service.emit('ping'))
   service.on('pong',  payload => { hello.text = payload.message })
 })

 export default class App extends React.Component {
   render() {
     return (
       <View style={styles.container}>
         <Text>{hello.text}</Text>
       </View>
     );
   }
 }
 const styles = StyleSheet.create({
   container: {
     flex: 1,
     backgroundColor: '#fff',
     alignItems: 'center',
     justifyContent: 'center'
   }
 });