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!
The plugin now requires webpack v4+, Angular v6+ and {N} v4+.
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% |
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.
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:
nativescript-angular
package:npm i [email protected]
@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
[Angular only] You have to migrate your code to Angular 6. This will help you a lot - https://update.angular.io.
Update the nativescript-dev-webpack
package:
npm i --save-dev [email protected]
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
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"
}
}
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
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.
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.
vendor.js
chunk.
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.
vendor.js
chunk, modify the splitChunks
configuration inside webpack.config.js
.
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,
});
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.
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.
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.
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.
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 }
}
Module build failed: Error: Final loader didn't return a Buffer or String
Update TypeScript to 2.7.2.
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,
Update your webpack dependencies. This script automates the process:
./node_modules/.bin/update-ns-webpack --deps