Back to Blog Home
← all posts

How to update plugins for NativeScript 7

August 31, 2020 — by Technical Steering Committee (TSC)

As a NativeScript plugin author, there are 6 main points to understand about updating your plugin for NativeScript 7:

  1. Package dependencies updates
  2. es2017 and higher target (TypeScript based plugin only)
  3. @NativeClass() decorator (TypeScript based plugin only)
  4. Remove all occurrences of tns-core-modules and flatten all imports to come from just @nativescript/core
  5. Remove all occurrences of tns-platform-declarations and replace with @nativescript/types
  6. Does your plugin provide Angular specific's? Source code and build adjustments needed.

We will be introducing a new plugin seed soon with an improved architecture (over the current plugin seed), build tooling and setup to help you maintain all your plugins easier into the future. It will also allow easy drop in's of your existing plugins to use it (We will be dropping a blog post on this in next 2 weeks upon it's availability).

Let's address each of these points respectively.

1. Package dependencies update

"devDependencies": {
	"@nativescript/core": "~7.0.0",
	"@nativescript/types": "~7.0.0",
  "@nativescript/webpack": "~3.0.0",
	"typescript": "~3.9.0",

2. es2017 target

Update your tsconfig.json. This highlight's the key parts so only showing the most important.

BEFORE:

"compilerOptions": {
  "target": "es5",
  "module": "commonjs",

AFTER:

"compilerOptions": {
  "target": "ES2017",
  "module": "esnext",
  "moduleResolution": "node",

3. @NativeClass() decorator

For any native class you are extending using TypeScript's extends SomeNativeClass syntax you will want to decorate. For example:

BEFORE:

iOS:

export class MyPluginDelegateImpl extends NSObject {

Android:

export class ListViewItemClickListenerImpl extends java.lang.Object

AFTER:

iOS:

@NativeClass()
export class MyPluginDelegateImpl extends NSObject {

Android:

@NativeClass()
export class ListViewItemClickListenerImpl extends java.lang.Object {

The NativeClass decorator is part of @nativescript/core globals so you should not have to import it from anywhere as it should just be available as long as you're using @nativescript/core.

Ensure NativeClass is processed

In order for your plugin to process that NativeClass decorator properly you can add a nice extension to typescript and tsconfig.json via ts-patch package. This is used in combination with a new transformer which comes from the latest @nativescript/webpack (3.0.0 and above) and is the reason that package is listed in the devDependencies above to use.

  1. npm i ts-patch -D

  2. Modify package.json scripts to include the install after cleaning the plugin.

Here's an example:

"scripts": {
  "setup": "npm i && ts-patch install"
}
  1. Modify tsconfig.json to support the transformation (add the plugins section anywhere in compilerOptions):
"compilerOptions": {
  "plugins": [{
    "transform": "@nativescript/webpack/transformers/ns-transform-native-classes",
    "type": "raw"
  }]
}

Now when you build your plugin with tsc any NativeClass decorated classes will be properly compiled for the NativeScript runtimes.

4. Remove all occurrences of tns-core-modules and flatten all imports to come from just @nativescript/core

You can use @nativescript/core for all your imports now. Some symbols have been wrapped up into convenient rollups to avoid so many generic symbols flying about codebases. You can use this import reference guide to coalesce the usages.

Here's an example:

BEFORE:

import * as app from 'tns-core-modules/application';
import { messageType, write } from 'tns-core-modules/trace';
import { ObservableArray } from 'tns-core-modules/data/observable-array';
import { GridLayout } from 'tns-core-modules/ui/layouts/grid-layout';
import { KeyedTemplate, View } from 'tns-core-modules/ui/core/view';
import { layout } from 'tns-core-modules/utils/utils';

// sample code
const rootView = app.getRootView();

write('Some message', 'a category');

layout.getDisplayDensity();

alert('Hi');

AFTER:

import { 
  Application, 
  Trace,
  ObservableArray, 
  GridLayout, 
  KeyedTemplate, 
  View,
  Dialogs,
  Utils
} from '@nativescript/core';

// sample code
const rootView = Application.getRootView();

Trace.write('Some message', 'a category');

Utils.layout.getDisplayDensity();

// you can still use global alert just fine but to better identify {N} dialog usage you can now use the `Dialogs` rollup which contains all the dialog methods and interfaces
Dialogs.alert('Hi');

5. Remove all occurrences of tns-platform-declarations and replace with @nativescript/types

In your references.d.ts:

BEFORE:

/// <reference path="./node_modules/tns-platform-declarations/ios.d.ts" />
/// <reference path="./node_modules/tns-platform-declarations/android.d.ts" />

AFTER:

/// <reference path="./node_modules/@nativescript/types/index.d.ts" />

You can also include specific platform types as needed if you desire; for example, say you wanted different Android sdk types:

/// <reference path="./node_modules/@nativescript/types-ios/index.d.ts" />
/// <reference path="./node_modules/@nativescript/types-android/lib/android-29.d.ts" />

6. Angular handling

With NativeScript 7 comes official Angular 10 support. This is important since Angular 10 dropped official es5 support and are also targeting es2017+.

In order to compile your Angular specific plugin parts we can use ng-packagr to assist our build.

npm i ng-packagr -D

A couple interesting things about using ng-packagr is that the Angular pieces need to be self-contained and hence reference other "outside" dependencies from their actual bundle import package name. For example:

import { MyPluginClass } from '../some/vanilla-plugin-code';

This will not work because ../ is pulling in code into the Angular build which is not part of the "packaged" dist. To fix, that would become:

import { MyPluginClass } from '@nativescript-community/your-plugin';

For this reason there's a couple architecture choices you can make with your plugin organization and is one of several reasons we are introducing a new plugin seed which helps here.

A. You could create a src-angular folder parallel to your plugin's src folder and it would have a dependency on your plugin itself. B. You could keep your angular folder inside your plugin src but should structure your plugin in a monorepo style architecture which helps manage dependencies in a smarter way as well as build configuration settings.

We prefer option B above all because it not only elegantly solves that issue but solves many others like simplifying the ease of maintenance for managaing a number of plugin's you develop all from one single source of truth when it comes to dependencies and build configurations.

No matter which direction you go there's one thing you want in the Angular source of your plugin for ng-packagr. A package.json with at least the following contents:

{
	"name": "nativescript-community-your-plugin-angular",
	"ngPackage": {
		"lib": {
			"entryFile": "index.ts",
			"umdModuleIds": {
				"@nativescript/core": "ns-core",
				"@nativescript/angular": "ns-angular",
				"@nativescript-community/your-plugin": "ns-community-your-plugin"
			}
		},
		"whitelistedNonPeerDependencies": [
			"."
		]
	}
}

The name can be any identifier but we're just using a standard flattening of the plugin name and appending -angular to the end to signal this is the angular part of the plugin.

The entryFile is utmost important here and it should be named index.ts for safety. You could name it something else but if it's named the same as the plugin itself then it will fail so just use index.ts.

Here's example package.json structure from one of our own internal plugins @nativescript/datetimepicker:

datetimepicker
  / angular
        index.ts
        nativescript-datetimepicker.accessors.ts
        nativescript-datetimepicker.directives.ts

And the contents of the index.ts can be seen here.

Notice it imports the vanilla {N} plugin code from it's full plugin barrel name @nativescript/datetimepicker instead of relative import paths back a directory. This is important to package the angular build properly.

Once ng-packagr does it's build, it default places a dist directory inside the angular source which the contents can be copied to your plugin's output ready for publishing.

You can kick off the angular source build as follows:

ng-packagr -p angular/package.json

As mentioned we will be introducing a new plugin seed with the architecture in place ready to scale plugin development (as well as drop in existing plugins) and handle details like this better than the prior plugin seed. Look out for an announcement on this in the next 2 weeks.