State of affairs
Currently NativeScript provides support for what we call modules. Those are CommonJS compatible JavaScript modules that usually expose underlying native platform APIs by providing a common JavaScript hat on top of them. Out of the box we provide support for all platform stock APIs (part of the native SDKs) and we do have the option to reference a 3rd party libraries via the tns library add
command.
Introducing NativeScript Plugins (Modules ++)
Writing real world application requires much more than what we currently provide in the form of NativeScript modules. A common use case is that a module usage it will require a change in a platform specific configuration file. Here are a two examples that illustrates this:
- The canonical one is geolocation which requires adding permissions in
AndroidManifest.xml
as well as a key in the Info.plist
file for iOS.
- Another example is adding a chart component to your application. Charts are usually not available in the stock SDKs so you will be required to use a 3rd party library and here comes the problem if you want to go cross-platform. You will have to add each platform library separately and then expose a common JavaScript API. What if you want to share this with someone else?
There are a lot of other examples that can be given to proof that we need something more than what we currently have in the form of modules. We want a common abstraction that will allow us to easily package and redistribute those extended modules. We want to call this NativeScript Plugin.
What is a NativeScript plugin?
NativeScript plugin is a package that consist of the following:
- Metadata about the package itself: name, version, supported runtime versions, etc.
- CommonJS module (one or many) that expose the native API via a single JavaScript API.
- Declarative way of defining transformations for platform specific configuration files.
- Native libraries and resources.
The package format should be extensible so we can add new capabilities in the future.
Implementation details
The key thing about the implementation is that we will strive to sit on top of existing standards and we will use conventions over configurations.
Packing and distribution
We are already discussing that we want to standardize on NPM as distribution platform. It is logical that we want to use it for NativeScript plugins as well.
We can use an upcoming feature of NPM called ecosystems by adding ecosystem:nativescript
in the keywords
section of the plugin's package.json
. Eventually NPM will provide dedicated search and package weighting for the ecosystem. More info on ecosystems can be found here.
We can use the engines
key to designate which version of the runtime the plugin supports:
{ "engines" : { "nativescript" : ">=0.9 <0.10" } }
or
{
"engines" : {
"nativescript-android" : "0.9.0",
"nativescript-ios" : "0.9.3"
}
}
JavaScript module
The JavaScript module loading will use the same technique we are currently using in the runtimes when a module is required. If a JavaScript file is specified in the package.json
's main
key we will load that otherwise we will look for index.js
file in the root.
Native libraries
For referencing native libraries we can use a convention we are currently using in the CLI. We can have a platforms
folder with sub-folder for each platform. We can then search the platform specific folders and execute add library
upon installation of the plugin. Here is an example of a sample chart plugin's file structure:
nativescript-charts/
|-- platforms/
|-- |-- Android/
|-- |-- `-- charts.jar
|-- |-- iOS/
|-- |-- `-- ChartKit.framework
`-- index.js/
`-- package.json
Platform configuration files transformations
Usually those platform configuration files are XML based (AndroidManifest.xml
, Info.plist
, etc.) which can be tricky to incorporate in our JavaScript/JSON based tooling. Here are two possible ways we can approach this.
Incorporate the transformations in package.json
We can use the package.json
file to declare the required transformation we want to apply. For example we can have the following:
{
"name": "nativescript-geolocation",
"version": "0.9",
"nativescript": {
"android": {
"permissions" : [
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION"
]
}
}
}
The problem with such definition is that it is is not general purpose and is very platform specific. We will have to embed the knowledge that the permissions has to go in AndroidManifest.xml
and also that they will have to be inserted as <uses-permission>
tags in the XML. The same goes if we want to insert other elements.
In order to make this more generic we will have to tweak the JSON a little bit:
{
"name": "nativescript-geolocation",
"version": "0.9",
"nativescript": {
"android": {
"AndroidManifest" : {
"uses-permissions" : [
{ "android:name" : "android.permission.ACCESS_COARSE_LOCATION" },
{ "android:name" : "android.permission.ACCESS_FINE_LOCATION" }
]
}
}
}
}
Having this structure we can transform the JSON to XML in a generic way and just merge the transformed XML segments into the AndroidManifest.xml
file. The downside of this is that the JSON becomes more verbose and the processing itself can be more complex.
Note: For plist files we will have to create a separate output generator due to the strange structure of the property list.
Provide XML segments in designated file per platform
We can use /platforms
structure of the plugin and place platform specific XML files in each platform directory. Here is an example:
nativescript-geolocation/
|-- platforms/
|-- |-- Android/
|-- |-- `-- AndroidManifest.xml
|-- |-- iOS/
|-- |-- `-- Info.plist
`-- ...
The XML files will contain just the segments we want to merge in the final configuration file. This technique give us better flexibility but will require a little bit more setup from plugin developers.
Update : There is an agreement that the approach with separate config files and merging is the one we should go after.
Other use cases
Here are some other use cases that can be considered for future implementation.
Including native source code files
There might be cases when we need to package native source code files. These cases should really be rare ones as by definition the native code should be able to be expressed in JavaScript but nevertheless. By convention those could go into the specific platform sub-folder of /platforms
and will be included in the native projects for compilation.
Using packages from the platform's package manager
Each platform has its own package (dependency) manager and it would be good if we can declaratively include packages from them to be used from JavaScript. The de-facto package managers are Cocoa Pods for iOS and jCenter (Maven Central) for Android. Here is a an example of a possible package.json
declaration of those:
{
"name": "nativescript-http",
"version": "0.9",
"nativescript": {
"android": {
"dependencies": {
"com.github.kevinsawicki:http-request" : "6.0"
}
},
"ios" : {
"pods" : {
"AFNetworking" : ">=2.0"
}
}
}
}
Note: In order to support Android packages we will have to migrate the Android tooling to Gradle.
Prior art
Apache Cordova's plugin format
Cordova uses a single plugin.xml
file for its plugins definition. It is very verbose but handles most of the cases described above. Cordova is also moving to NPM for package distribution.
Telerik AppBuilder's .abproject file
AppBuilder uses JSON based project definition which includes Android, iOS and Windows Phone specific properties. Those are processed in a custom way for each platform and there is no generic way to define a platform specific property if it is not part of the JSON schema format defined for the project.