Back to Blog Home
← all posts

Upgrading to NativeScript Webpack 0.12.0

June 4, 2018 — by Stanimira Vlaeva

The NativeScript 4.1 release includes an update for the nativescript-dev-webpack plugin. The new version of the plugin is 0.12.0 and brings a few important changes.

Don't miss the next NativeScript webinar that covers {N} 4.0/4.1 along with Angular and Vue.js code sharing strategies!

Webpack 4 and Angular 6

The plugin now requires webpack v4+, Angular v6+ and {N} v4+.

Faster startup for Android

The extended UglifyJS support for Android and the improved snapshot bundle generation led to smaller bundle sizes and faster launch times. Adding that to the new super fast Android runtime, the total launch time improvement is ~50% for every app that we measured. Please share the results for your apps! The table below shows the launch times before and now for a few NativeScript apps. They are built in release mode with enabled ahead-of-time compilation, minification and snapshot.

Android
runtime 4.0
{N}-webpack 0.11.0
Android
runtime 4.1
{N}-webpack 0.11.0
Android
runtime 4.1
{N}-webpack 0.12.0
Improvement
App: Groceries
Device: Pixel 2
1467ms 1156ms 764ms 51.76%
App: Hello world Angular
Device: Pixel 2
1277ms 976ms 600ms 46.99%
App: SDK samples
Device: Nexus 5x (slower than Pixel 2)
2301ms 1816ms 1333ms 57.93%


Easier setup

For non-Angular projects you had to configure a few things manually to make webpack work. Usually, you would add a bundle-config.js file in your project to require the xml pages, register external UI plugins, load the css, etc.

You don't have to do that anymore and can safely delete the bundle-config file, because the nativescript-dev-webpack plugin will configure all of the above things for you. The plugin achieves that with a couple of webpack loaders, that are now part of your webpack configuration. We'll go through each loader in the next sections.

Dependencies

As mentioned above, the plugin requires webpack 4. Clearly, the first thing you need to do in order to upgrade to webpack 4 is to… update your webpack dependency to version 4. However, if you are developing {N} apps, there is a chance you are using a whole bunch of webpack loaders and plugins. And if your app is built with Angular, you also need to update your Angular packages. Here are a few steps you can follow to automate the update process:

  1. [Angular only] Update the nativescript-angular package:
  2. npm i [email protected]
  3. [Angular only] Update your @angular/* packages. There's a script distributed with the nativescript-angular package which will do that for you:
    ./node_modules/.bin/update-app-ng-deps
  4. [Angular only] You have to migrate your code to Angular 6. This will help you a lot - https://update.angular.io.

  5. Update the nativescript-dev-webpack package:

    npm i --save-dev [email protected]
  6. Update your webpack plugins and loaders by running the special script distributed with the nativescript-dev-webpack package:
    ./node_modules/.bin/update-ns-webpack --deps

  7. Make sure you are using {N} runtimes and tns-core-modules 4.0 or higher version. If it's possible, upgrade to 4.1 to take advantage of the awesome performance improvements in the Android runtime and all the new features in that release.

    // package.json
    {
      "name": "MyAwesomeProject",
      "version": "0.0.0",
      "nativescript": {
        "id": "org.nativescript.myawesomeproject",
        "tns-ios": {
          "version": "4.1.0"
        },
        "tns-android": {
          "version": "4.1.0"
        }

      },
      "dependencies": {
        // ...
        "nativescript-angular": "~6.0.0",
        "nativescript-theme-core": "~1.0.4",
        "tns-core-modules": "^4.1.0",
        "zone.js": "~0.8.2"
      }
    }

Webpack config

The webpack.config.js file that's distributed from the plugin is quite different from the previous one. If you have made manual configurations, make sure to create a backup. Then pregenerate the config and apply your changes to it.

# back up your modified config
mv webpack.config.js webpack.config.js.bak
# get the latest version added to your project ./node_modules/.bin/update-ns-webpack --configs
# find the differences and apply the manual changes that you need diff webpack.config.js webpack.config.js.bak

EcmaScript modules

All TypeScript and Angular apps now use target EcmaScript modules when built with webpack. That allows webpack to remove all unused exports from your code and improves the final bundle size.
A special tsconfig.esm.json file, which specifies that, is added to your project when you install the nativescript-dev-webpack plugin. It looks like that:


// tsconfig.esm.json
{
   "extends": "./tsconfig",
   "compilerOptions": {
       "module": "es2015",
       "moduleResolution": "node"
   }
}

That introduces a breaking change. You can't use import assignment when targeting ES2015 modules. If you see the following error:

ERROR in app/calendar/calendar-localization/calendar-localization.component.ts(2,1): error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.

You need to migrate your import assignments in the following way:

BEFORE

import buttonModule = require("tns-core-modules/ui/button");
const testButton = new buttonModule.Button();

AFTER


import * as buttonModule from "tns-core-modules/ui/button";
const testButton = new buttonModule.Button();

or even better:


import { Button } from "tns-core-modules/ui/button";
const testButton = new Button();


Importing only the Button class will allow webpack to tree-shake all unused code from the tns-core-modules/ui/button module. This will lead to smaller app sizes and faster launch times.

Split chunks plugin

The CommonsChunkPlugin is gone in webpack 4 in favor of the SplitChunksPlugin. We changed the way we generate chunks to be compliant with the new webpack splitting strategies.

  1. All external packages and custom Android app components that you use in your app are extracted to the vendor.js chunk.
  2. Because all external packages are in the common chunk, you don't have to add explicit requires inside app/vendor.ts. That's why app/vendor.ts, app/vendor-platform.android.ts and app/vendor-platform.ios.ts are gone. If you have them in your app, you can delete them.
  3. If you want something else to be part of the vendor.js chunk, modify the splitChunks configuration inside webpack.config.js.

Snapshot generation changes and customization

The changes in the vendor.jsgeneration also affect the snapshot plugin. The cool thing is that now you have all your NativeScript plugins snapshotted out-of-the-box and you don't have to worry if they have native API access inside of them or not. That's because they are not executed. If you want to explicitly execute some plugin on launch, you need to add it to the requireModules array that's part of the NativeScriptSnapshotPlugin configuration. That will be beneficial only for quite big plugins and you probably won't ever need to do it. For example, the main Angular packages are executed when generating snapshot for NativeScript Angular apps:


// webpack.config.js
// ... new nsWebpack.NativeScriptSnapshotPlugin({
    chunk: "vendor",
    requireModules: [
        "reflect-metadata",
        "@angular/platform-browser",
        "@angular/core",
        "@angular/common",
        "@angular/router",
        "nativescript-angular/platform-static",
        "nativescript-angular/router",
    ],
    projectRoot,
    webpackConfig: config,
});

Minification improvements

UglifyJS's compress option was breaking the NativeScript static binding generator for Android and was disabled for that platform. We've identified the causes and the option is now enabled, which results in smaller app bundle and better launch time.

Bundle config

As mentioned above, the new version of the nativescript-dev-webpack plugin comes with a few loaders that reduce the number of manual steps you have to do to make your project webpack-ready.

Bundle config loader

The bundle-config-loader is applied to the entry module and its job is to insert a few predefined configurations. It has the following options:

  • registerPages <boolean>

Default: true

Registers your app’s XML, CSS and JavaScript pages with a regex. All your pages' resources should be named so that they end with either root or page. For example - main-page.xml, main-page.css, main-page.js.

This options should be false for Angular apps because components there are registered in the NgModules.

  • loadCss <boolean>

Default: true

Loads the application css inside the bundle.js chunk (your application code). Should be false if you are building with snapshot, because in that case the snapshot plugin will load the application css in the chunk that's executed for "snapshotting". Note that you need that option both for Angular and non-Angular projects.

For sample usage of the plugin, you check out the default webpack configurations for Angular projects and for non-Angular projects.

XML namespace loader

The xml-namespace-loader is another loader for non-Angular projects. It parses your xml templates and registers all custom components that are used there.
Let's say you are using RadSideDrawer in your main-page.xml:

<nsDrawer:RadSideDrawer xmlns:nsDrawer="nativescript-ui-sidedrawer">
<!-- some content... --> 
</nsDrawer:RadSideDrawer>

Before you had to do register the nativescript-ui-sidedrawer module:

global.registerModule("nativescript-ui-sidedrawer",
() => require("nativescript-ui-sidedrawer"));

Now the loader will do it for you.

Custom Android app components

NativeScript allows you to have custom Android app components, such as Activities and Services (docs). You should extend the original Java class with a JavaScript class and then declare the newly created component in the AndroidManifest.xml file.

When you build your app, NativeScript will perform a static analysis of your JavaScript code and create a Java class for the component. That's why when you are using webpack, the file that contains your JavaScript extender should be part of the bundle.

This gets a bit more complicated when you are using snapshots. In that case, the modules containing JavaScript extenders should get into the snapshotted bundle but shouldn't be evaluated. Evaluating them at build time will fail because the JavaScript class is extending native Android class, which is only available when you run the app on an actual Android device/emulator. That's why, if you are using an old version of the nativescript-dev-webpack plugin,  you may have something like this in your app:

if (!global["__snapshot"]) {
    require("ui/frame");
    require("ui/frame/activity");
}
You don't need that code anymore. Instead, if you have custom app components, add them to the array of app components on top of your webpack config file:
const appComponents = [
// ...
resolve(__dirname, "app/main-activity.android.ts"),
];
The default config is set up in a way that the components are included in the common chunk:
splitChunks: {
cacheGroups: {
vendor: {
            test: (module, chunks) => {
                const moduleName = module.nameForCondition ? module.nameForCondition() : '';
                return /[\\/]node_modules[\\/]/.test(moduleName) ||
                        appComponents.some(comp => comp === moduleName);
            },
}
}
}

And finally, the default config uses the android-app-components-loader which will require the components and include them in your bundle:
{
    loader: "nativescript-dev-webpack/android-app-components-loader",
    options: { modules: appComponents }
}

Common error messages and how to fix them

Error:

Module build failed: Error: Final loader didn't return a Buffer or String

Solution:

Update TypeScript to 2.7.2.

Error:

Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.

- configuration has an unknown property 'optimization'. These properties are valid:
  object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry, 

Solution:

Update your webpack dependencies. This script automates the process: 

./node_modules/.bin/update-ns-webpack --deps