Building Your Own NativeScript Modules for npm

Since releasing NativeScript to the public two weeks ago, we’ve received a flood of feedback and questions, but the most common one we get is “how do I do {{ thing }} in NativeScript?“, where {{ thing }} is one of the thousands of functions that you might want to include in a native mobile app — maps, QR-code readers, bluetooth, Apple Pay, and so forth.

The great thing about NativeScript is that you can absolutely do all of those things in a NativeScript app, as the NativeScript runtime makes all Android and iOS APIs directly available. The tricky part is accomplishing many of those {{ thing }}s requires that you to write platform-specific code.

For example, let’s say you want to use a native map in your new app. To do that, you’d ideally write code like new Map(), or even better <Map>, but in today’s NativeScript you have to write code like MKMapView.alloc() for iOS and com.google.gms.maps.MapFragment for Android.

But don’t worry; there’s good news: NativeScript has a really slick mechanism for abstracting away this platform-specific code called NativeScript modules. Let’s look at what NativeScript modules are, how they help prevent you from writing platform-specific code, how you can help the NativeScript community out by writing your own, and how to distribute those modules via npm.

What are NativeScript Modules?

NativeScript modules are JavaScript modules that implement functionality for one to many mobile platforms. You can think of NativeScript modules as similar to Cordova plugins, as both abstract away platform-specific iOS, Android, and Windows code behind JavaScript APIs. However, there are some important differences between how NativeScript modules and Cordova plugins work, most notably in how you access native APIs.

Specifically, in Cordova plugins you write native code (Objective-C, Java, C#, etc) to access native APIs, whereas in NativeScript you access native APIs in JavaScript directly. To get an idea of what that looks like, let’s look at a simple NativeScript module.

The following code defines a NativeScript module named “device” that provides access to the device’s OS version (so “5.0”, “4.4.4”, “4.4.3”, etc for Android, and “8.2”, “8.1”, “8.0”, etc for iOS). There are three files:

  • device.ios.js has the module’s iOS implementation;
  • device.android.js has the module’s Android implementation;
  • some-other-file.js shows how to use the device module.
// device.ios.js
// Defines the device module's implementation for iOS
module.exports = {
    version: UIDevice.currentDevice().systemVersion
};

// device.android.js
// Defines the device module's implementation for Android
module.exports = {
    version: android.os.Build.VERSION.RELEASE
};

// some-other-file.js
// Code that uses the "device" module
var device = require( "./device" );
console.log( device.version );

And that’s it. Unlike Cordova you don’t need to write any configuration files to use NativeScript modules — you define the module, require() it, and use it. Let’s look at what’s exactly what’s happening here in greater detail, as there are several cool things worth mentioning.

Cool Thing #1: The .ios.* and .android.* naming convention

NativeScript has a built-in mechanism to choose the correct platform-specific implementation of a module at compile time. Meaning, given the example above, NativeScript automatically includes the device.ios.js in native iOS projects, and device.android.js in native Android projects.

This convention provides an elegant means of forking your code, as the code that uses your modules doesn’t have to care about which platform it’s running on. Notice how there’s no platform-specific code in some-other-file.js; the file just require()s the device module and uses it:

// some-other-file.js
// You don't need to include the platform-specific suffixes (i.e. "device.android" or "device.ios")
// as NativeScript figures that out for you automatically.
var device = require( "./device" );
console.log( device.version );

Cool Thing #2: The NativeScript runtime

The NativeScript runtime makes accessing native iOS and Android APIs beautifully succinct. Specifically, the NativeScript runtime is what makes the android.os.Build and UIDevice.currentDevice() APIs available to JavaScript. In fact, you can easily reference the iOS UIDevice and Android Build.VERSION docs this sample code uses.

For more information on how it works check out my article on How NativeScript Works.

Cool thing #3: CommonJS

NativeScript modules follow the same CommonJS spec that Node modules do. Meaning, if you already know how Node modules work, or even if you just have a rough idea how to use Grunt and Gulp, then you already mostly know what you need to use NativeScript modules.

If you don’t know how Node modules work, all you really need to know is how two keywords work: require and exports. For example, let’s refer back to the code from above:

// device.ios.js
module.exports = {
    version: UIDevice.currentDevice().systemVersion
};

// device.android.js
module.exports = {
    version: android.os.Build.VERSION.RELEASE
};

// some-other-file.js
var device = require( "./device" );
console.log( device.version );

In some-other-file.js the require() call imports the device module so that it can use it. The return value of require(), or the object that gets stored in the device variable, is determined by the use of the exports keyword in device.ios.js and device.android.js. In this case, because the device.\*.js files return an object with a version property, that object is returned and stored in some-other-file.js‘s device variable.

For a more detailed explanation of how CommonJS modules and the exports keyword work, check out this article.

Cool Thing #4: NativeScript’s core modules are all written using these conventions

The core of the NativeScript framework is nothing more than a series of NativeScript modules that follow the conventions discussed in this article. This means that if you want examples of how to write your own modules (and we’ll do that momentarily), you can peruse the NativeScript framework itself.

NativeScript’s core modules are stored in the nativescript/cross-platform-modules repo on GitHub. The modules are open source. You can file issues, and you can even contribute! In NativeScript projects these modules are stored in your tns_modules directory.

One thing to be aware of: the NativeScript core cross-platform modules are written in TypeScript. We use TypeScript because we find it speeds up our development processes, but that doesn’t mean you have to. You can write your own modules in TypeScript, as it’s a first-class citizen in NativeScript, or you can write them in JavaScript. If you do use TypeScript, you can get code completion for native APIs using our TypeScript declaration files. Here’s the iOS file and here’s the Android one.

With this in mind, the first thing to do when trying to do {{ thing }} in NativeScript is to look to see if there’s a cross-platform module that already does it. If so, great! If not, you file an issue to request one, or, if you’re looking for a super fun weekend project, you can build that module yourself. Let’s look at how.

Writing and Distributing NativeScript Modules on npm

Writing a NativeScript module is as simple as writing a CommonJS module that uses the NativeScript runtime to call native APIs. Having a bit of Objective-C and/or Java knowledge helps, but it’s certainly not a requirement.

I actually find that writing a NativeScript module provides an excellent introduction to native iOS and Android apps. I usually Google “What’s the API for {{ thing }} in [ Android | iOS ]” as a starting point. Having some npm experience also helps, but, even if you’ve never used npm, you’ll find registering a module to be a relatively easy and well documented process.

You can look at our docs for specifics of how the runtimes convert Objective-C/Java code to JavaScript (here’s Android’s; here’s iOS’s), but I find it’s easiest to look over a few existing NativeScript modules in the cross-platform-modules repo to get an idea of how things work.

Here are a few other best practices for writing NativeScript modules.

Best Practice #1: Start by copying an existing module

I recently created the NativeScript flashlight module, and I’ll be using that as the basis for the rest of this article. The module is fairly simple so I’d encourage you to copy the entire implementation and use it as a basis of your own. The module’s repo contains a fully functional demo of it in use (see the demo folder), which can be handy to refer to if you’re wondering how the pieces fit together.

Best Practice #2: Put shared code in a file named moduleName-common.js

Depending on the module you’re writing, you may have some code you want to share across platforms or you may not. If do want to share code, the NativeScript core modules use a convention of placing shared code in a moduleName-common.js file, and importing and extending that common code in the moduleName.android.js and moduleName.ios.js files.

To give you an idea of how this sharing works, take a look at the source of flashlight-common.js below:

// flashlight-common.js
var flashlight = {
    _on: false,
    toggle: function() {
        if ( flashlight._on ) {
            flashlight.off();
        } else {
            flashlight.on();
        }
        flashlight._on = !flashlight._on;
    },
    isOn: function() {
        return flashlight._on;
    }
};

module.exports = flashlight;

The common module implements two methods: toggle() and isOn(). There’s nothing about these two methods that’s specific to iOS or Android, so it makes sense to implement them in a common file to avoid duplicating code.

The methods that are platform-specific are on() and off(), and the two platform-specific files each contain an implementation shown below. Note how each module starts by requiring the common flashlight module, and then adds on the platform-specific implementations.

// flashlight.android.js
var flashlight = require( "./flashlight-common" );
var camera = android.hardware.Camera.open();
var p = camera.getParameters();

flashlight.on = function() {
    p.setFlashMode( android.hardware.Camera.Parameters.FLASH_MODE_TORCH );
    camera.setParameters( p );
    camera.startPreview();
};
flashlight.off = function() {
    p.setFlashMode( android.hardware.Camera.Parameters.FLASH_MODE_OFF );
    camera.setParameters( p );
    camera.stopPreview();
};

module.exports = flashlight;
// flashlight.ios.js
var flashlight = require( "./flashlight-common" );
var device = AVCaptureDevice.defaultDeviceWithMediaType( AVMediaTypeVideo );

flashlight.on = function() {
    device.lockForConfiguration( null );
    device.torchMode = AVCaptureTorchMode.AVCaptureTorchModeOn;
    device.flashMode = AVCaptureFlashMode.AVCaptureFlashModeOn;
    device.unlockForConfiguration();
};
flashlight.off = function() {
    device.lockForConfiguration( null );
    device.torchMode = AVCaptureTorchMode.AVCaptureTorchModeOff;
    device.flashMode = AVCaptureFlashMode.AVCaptureFlashModeOff;
    device.unlockForConfiguration();
};

module.exports = flashlight;

Best Practice #3: Name the repository nativescript-module-name

This is a Node convention that a lot of Node ecosystems (such as Grunt and Gulp) use to help users recognize modules that work in that ecosystem at a glance. Note that the NativeScript flashlight module is named “nativescript-flashlight”.

Best Practice #4: Register your module on npm

Because NativeScript modules are already Node modules they make perfect sense to include in npm. Registering your module in npm allows people to use npm install to include the modules in their NativeScript apps, and it also helps people discover that your module exists.

There are already a number of great articles on publishing npm modules (here’s my personal favorite), so I’m not going to reinvent the wheel here, but I will offer a few suggestions that are specific to NativeScript modules:

  • Make sure the “name” in your package.json uses the same “nativescript-module-name” convention discussed earlier.
  • Include “NativeScript” in your package.json under “keywords”. It’ll help us find your modules.

Tip: With a bit of extra configuration you can also allow usage of your module in XML. For some modules this is irrelevant (such as flashlight), but for some it’s incredibly useful — for example imagine using a map module in XML with <Map latitude="30" longitude="30" />. All XML elements you use in NativeScript apps — <Page>, <StackLayout>, <Button>, and so on — are all implemented as NativeScript modules using these exact same conventions.

Best Practice #5: Tell us about it!

We love hearing about new NativeScript modules and we’re happy to spread the word about new modules. Send us a tweet at @nativescript.

If you have any other questions about NativeScript modules, or you run into trouble building your own, feel free to reach out to us on Twitter or to comment on this article.

Can’t get enough NativeScript? Clark Sell, Sebastian Witalec, and I are running a six-hour workshop on NativeScript at TelerikNEXT this May. Come meet us and hack on NativeScript.

Comments