"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. And with this week's NativeScript 6.0 release 🎉, 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:
$ 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!
$ 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:
$ 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:
$ 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:
- Modify the webpack configuration (in
webpack.config.js
) to work with existing file names - 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:
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
orbuilder.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:
<Page xmlns:ui="../../shared/ui/loadingSpinner"
To make that work with webpack, it now looks like this:
<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:
- Create a new top level view with "root" in the name (ex:
myapp-root.xml
) - In the new root view, add the following XML (with a reference to whatever your default "page" should be)
<Frame id="my-app-root" defaultPage="views/myApp-page"></Frame>
- Update your
app.ts
to load theroot
module when your app starts
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.)
To fix this problem in my code, I changed this:
let newViewJs = require("views/dynamicView.js");
let newView = builder.load("views/dynamicView.xml", newViewJs);
To this:
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 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:
-
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. For relative webpack n00bs (like me), this report helps understand what webpack is doing. 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 yourwebpack.config.js
-
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 thewebpack.config.js
to load these static resources when your app is built.
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:
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. 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
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:
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, 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.
Top comments (0)