Upgrading Older NativeScript Apps to Use Webpack
Tags: nativescript, webpack, javascript, tutorial
Published: 2019-07-18 14:30:43

"Older" app sounds so negative.

And I'm not a fan of "legacy," "heritage," or "aging."

Let's go with "well loved" NativeScript apps.👍 No matter what you call them, they started before NativeScript fully embraced webpack, and now they need to join the webpack party to keep getting the latest NativeScript updates.

With NativeScript 5.4, the default build mode for NativeScript apps changed to use [Webpack](https://webpack.js.org/). And with [this week's NativeScript 6.0 release](https://www.nativescript.org/blog/announcing-nativescript-6.0---webpack-all-the-way-seamless-app-updates-new-core-theme-and-a-lot-more) 🎉, Webpack is the ONLY supported build mode. Webpack builds do offer big benefits, like reducing app package size and improving app performance (less JavaScript to load), but with that power comes a more complicated build configuration...especially for ~~an older~~ a well loved app that did not start with the Angular or Vue templates (that work with webpack out of the gate).

I recently updated my smart home controller app from NativeScript 5.1 to 5.4 and made the big switch to webpack. My app started in 2015, so it doesn't use Angular or Vue, but it does use TypeScript. My upgrade was anything but smooth, but hopefully my pain will be your gain. Here are some tips that will hopefully help make your NativeScript upgrade to webpack easier.

## Start the upgrade
Clearly, the first thing we need to do is upgrade our environment and project to the latest version of NativeScript. As of this week, that's {N} 6.0. Upgrade the CLI with:

```bash
$ npm install -g nativescript
```

_NOTE: When I went through this process, I updated to {N} 5.4, but much of the advice should still apply for {N} projects adopting webpack for the first time with {N} 6.0._

Then, in your project, update the `package.json` to use the latest versions of `tns-ios`, `tns-android` and `tns-core-modules`. It's generally a good idea to consider upgrading other dependencies and plugins at this time, too. In fact, with {N} 6.0, there is a new CLI command that will auto update some of these NativeScript dependencies for you!

```bash
$ tns migrate
```

When doing framework upgrades, I always delete the generated `platforms`, `hooks` and `node_modules` folders to minimize problems caused by cached assets. Once those folders are deleted, they can be regenerated with:

```bash
$ tns install
```
If your project is adding webpack for the first time, the install process will ask if you want to switch to webpack builds and will generate the required `webpack.config.js` and update the dev dependencies in `package.json`.

## Delete old generated JavaScript files

When you use TypeScript with NativeScript _pre-webpack_, the build process automatically compiles the `.ts` to `.js` in your working directory and then syncs the `.js` to the device or simulator. With webpack builds, `.js` files don't get generated in the working directory. That's great! But...if you forget to clean-out the old generated `.js` files, it can create some real debugging head aches. (Ask me how I know...)

So, first things first. Good hygiene. Clean out generated files from your working directory. If you are using git, this can be done quickly with a command like:

```bash
$ git clean -fX
```
This command will remove all ignored files from the working directory.

If you switch to webpack builds, and it seems like the changes you are making in your TypeScript are not working on the device, this is a likely cause.

## Rename files to register as modules
**This is the big one.** If you are upgrading a non-Angular/Vue NativeScript project, there is a good chance that you have your own convention for naming files and views in your project. **With the default NativeScript webpack configuration, module file names must include `page` or `root`.** If one of these two words is not in the file name, it will not be registered as a module.

_EDIT: With the final release of {N} 6.0, the default webpack configuration was changed to include all modules in your project. This improves backward compatibility and it means you don't have to rename files.👍 If you are adopting webpack with an upgrade to {N} 5.4, all of this still applies. And, if you still DO decide you want to adopt the standard {N} file naming convention, the tips that follow should help you do that more easily._

There are two solutions:

1. Modify the webpack configuration (in `webpack.config.js`) to work with existing file names
2. Rename all view file names so they will be registered as modules

While renaming a BUNCH of files is daunting (and has some cascading impacts), I ultimately opted to go down this route. It's a one time tax and it will make future NativeScript upgrades easier.

Where my project previously had files like:

- `settings.xml`
- `settings.css`
- `settings.ts`

To work with webpack, those files are now:

- `settings-page.xml`
- `settings-page.css`
- `settings-page.ts`

My project also has a lot of "partial" views that are dynamically composed at runtime. Using `page` or `root` in these file names felt wrong, so I slightly modified the `webpack.config.js` to accommodate a "`widget`" option:

```javascript
registerModules: /(root|page|widget)\.(xml|css|js|ts|scss)$/
```

This change is made in `webpack.config.js` > `config` > `module` > `rules` > loader options.

Meanwhile, TypeScript files that are not associated with a view **do not** need to be renamed. They will get picked-up by the separate `ts-loader` webpack configuration and work properly. Said to say, you don't need to rename _every_ file.

### Clean-up the rename mess
You've just renamed ALL of the files associated with views in your app. There's a good chance you broke some code if you use any of the following APIs that expect file names and paths:

- `.navigate`
- `.showModal`
- `builder.load` or `builder.parse`

Search your code base for any occurrences of these API calls and update the file names to match the new webpack-friendly names. This will catch a lot of the common problems that file renaming can have on your code. If you are `import`ing (or `require()`ing) any views programmatically, those paths must be updated, too.

## Relative path changes
With webpack, relative path resolution _can_ work differently than pre-webpack builds. This applies mostly to the way relative paths are resolved in `.xml` views. Fortunately, the "`~/`" relative to root syntax works very well everywhere with webpack, so any place you are using relative path navigation can be converted to ues the `~/` syntax.

For example, my app had several views importing a local custom component like this:

```xml
<Page xmlns:ui="../../shared/ui/loadingSpinner"
```

To make that work with webpack, it now looks like this:

```xml
<Page xmlns:ui="~/shared/ui/loadingSpinner-widget"
```
(Don't forget the file renaming.)

This works in TypeScript files for `import` statements, too. It's probably a good idea to use `~/` everywhere, but webpack won't break relative paths in code.

## `Frame` element is now required
This should be a quick and easy fix, but critical nonetheless. Around the time of {N} 5.0, support for multiple `frame` elements was added so that more complex views could be achieved with NativeScript (think: independent navigation in a split view). Until now, if you did not manually add a `frame` element to your `.xml` view, NativeScript could create it implicitly. With webpack, a missing `frame` element will cause a runtime error.

For most apps, only one `frame` is needed in the "root" view. This is the view in which all other "page" views will load. If your app does not have a "root" view:

1. Create a new top level view with "root" in the name (ex: `myapp-root.xml`)
2. In the new root view, add the following XML (with a reference to whatever your default "page" should be)
```xml
<Frame id="my-app-root" defaultPage="views/myApp-page"></Frame>
```
3. Update your `app.ts` to load the `root` module when your app starts
```typescript
app.run({ moduleName: "views/myapp-root" });
```
As you can gather, NativeScript with webpack builds is like running NativeScript in "strict" mode.

## Builder `parse` vs `load`
If you dynamically load modules at runtime using the `builder` API, you will likely need to update your code to use `builder.parse` instead of `builder.load`. Webpack pre-registers and loads JavaScript and XML at runtime, and this can create trouble for the `builder`. In short, the `builder` attempts to load something that has already been loaded (by webpack) at runtime. (There is more discussion on GitHub [in this issue](https://github.com/NativeScript/nativescript-dev-webpack/issues/620).)

To fix this problem in my code, I changed this:

```typescript
let newViewJs = require("views/dynamicView.js");
let newView = builder.load("views/dynamicView.xml", newViewJs);
```

To this:

```typescript
let newViewJs = require("~/views/dynamicView.ts");
let newView = builder.parse(<string>require("~/views/dynamicView-widget.xml"), newViewJs);
```
We are now looking for these TypeScript and XML paths (as registered with webpack) and loading the resources from webpack's compiled, minified runtime resources.

Frankly, this feels hacky. There could be a better way. And it appears the project is [actively refactoring](https://github.com/NativeScript/NativeScript/pull/7386) around these APIs.

## Debugging the errors
Even with the most thorough upgrade, you are likely to hit some new webpack related runtime errors when you first fire-up your app. Here are two common errors I hit and how I worked through them:

### Fixing `Module not found` errors

I hit this error _a lot_ when I first started trying to run my webpack built NativeScript project. In short, this error indicates that some path configured in your app (like the `moduleName` used by your `app.ts` on startup or a navigation path) can't be found in the registered webpack modules. Steps to resolve:

1. **Make sure the module is actually registered.** I didn't realize I had to change my file names to properly register the modules when I first upgraded, so that knowledge should already save you some time. Using the webpack "build reports" also helped me troubleshoot. 

      `$ tns build ios --env.report`

      This creates a folder called `report` with a HTML and JSON doc that can be analyzed with tools [like this](http://webpack.github.io/analyse/). For relative webpack n00bs (like me), this report helps understand what webpack is doing.

2. **Make sure the file name in code is correct.** Assuming you took the path I did and renamed files, make sure the code is looking for the new file name with `page`, `root` or whatever additional tokens you've added to your `webpack.config.js`

3. **Make sure JSON imports include a file extension.** There were a few places in my app where I was directly `require`-ing a JSON file (usually to load some static configuration values). Pre-webpack, these imports did not require an explicit file extension, so this worked:

      `let config = require("../shared/config");`

      That doesn't match a registered path with webpack, so to work now, the file extension must be added:

      `let config = require("../shared/config.json");`
    
      As an aside, if you are loading static JSON resources in your project using local HTTP (and not explicitly `require()`ing), you will need to further update the `webpack.config.js` to load these static resources when your app is built.

```javascript
    new CopyWebpackPlugin([
                { from: { glob: "assets/*.json" } }, //<- Add this
                { from: { glob: "fonts/**" } },
                { from: { glob: "**/*.jpg" } },
                { from: { glob: "**/*.png" } },
                ...
```

### Fixing `Module parse failed` errors

Another common error I ran in to was this `Module parse failed` error, usually along with `Unexpected token` or `Unexpected character` log messages.

In my case, all of these errors were caused by accidentally registering files as modules that did not need to be. It's likely an edge case when doing a NativeScript upgrade like this, but if you find yourself head scratching with this error, double check that you're not inadvertently loading files as modules.

For reference, the wonky file in my project that caused these inadvertent errors was trying to dynamically `require` files like this:

```typescript
let instance = require(`${dynamicPath}`);
```

Don't do that with webpack.

### Double-check for circular references in barrel files

There's NO WAY I added a circular reference to my app.

I know better. I wouldn't do that.

Leave it to webpack to serve me some humble pie. Circular references that _somebody_ added (checks contributor count: 1 😬) did not have any negative affect on the app before webpack, but once Mr. Strict joined the party, they triggered app crashes on startup.

These circular references crept in to my code via ["barrel" files](http://tattoocoder.com/angular2-barrels/). Basically, convenience modules that re-export lots of related modules to simplify imports later. It's a concept borrowed from Angular. For example, here is a barrel file from my app:

`app-factories.ts`
```typescript
export * from "./BaseController.factory";
export * from "./SceneProvider.factory";
export * from "./CameraProvider.factory";
export * from "./ThermostatProvider.factory";
```
Using this barrel file simplifies an import like this:

```typescript
import { BaseController, SceneProvider, CameraProvider } from "~/providers/app-factories";
```

My mistake was accidentally using a barrel file short cut in a file that was exported by that barrel file. Webpack made me aware of my error, but the error messages were cryptic and the problem was not immediately obvious. **Lesson learned: webpack may expose errors in your code that didn't stop your app from running previously.**

## Next Steps

Phew. It took some heavy lifting, but with a little debugging my app is now running on NativeScript 5.4 and working with webpack builds. It's also ready for NativeScript 6.0, which should now be a quick and easy upgrade. 🤞

There are a few more potential "gotchas" when upgrading to {N} 6.0 that the NativeScript team [highlighted in a new blog post this week](https://www.nativescript.org/blog/nativescript-6.0-application-migration), so be sure to check that out for additional tips.

With the foundation of my app updated, my next step is to address the app backend and start connecting with Azure. Now the real fun can begin! 'Til then, I hope these lessons learned through pain will make your big webpack upgrades for _well loved_ NativeScript apps easier.