WebCake

While the solution in this post does work, I personally didn’t like it at scale, so I found a different solution that I felt works much better. You can find out more about that in a newer post. I’ll leave this one up as an alternative should anyone choose to go with this solution instead.

One of the challenges I’m facing at work is that several applications will eventually need to be deployed on multiple platforms. Some on web and Cordova, others on Electron, and still others on the Predix Mobile SDK – a hybrid app container built at GE for offline-first apps, using Couchbase. To facilitate this process, a few devs and I got together and constructed a base client-side seed app that has built-in services for multiple platforms.

TL;DR: You can find a sample app that uses this functionality on my GitHub.

As it grew almost too quickly, its multi-platform functionality became hard to manage without getting into complex conditional statements. The platform might not be Predix Mobile, and therefore might be assumed to be web-based and not offline-first; but then it might be on Cordova which means it still has access to native functionality, has access to an internal SQLite database, etc. We realized that the ideal state would be to have the same code base for this base client build output files for each platform, and the application consuming the core code would determine in its build process which platform file to pull in.

Platform-Specific Imports

One we started looking at Webpack, it became obvious that we would be able to set up multiple builds. The question then became how do we target different files based on the build, when Webpack pulls in files based on import or require statements. For example, I have a platform-based UserService. It might come in the form of the mobile version, which taps into a Predix-Mobile built-in authentication; or it might be Cordova-based, which opens up an in-app browser to handle auth redirects. The following import statement clearly won’t work, since the platform-specific file name is coded into the imported file path.

import { UserService } from '../services/user/user.cordova.js';

// go do some stuff with the UserService...

How do I change that import statement based on a build configuration, or build-time constant?

Leveraging the Resolve Alias property

It turns out that Webpack has a built-in aliasing system, which can be used to dynamically generate file path resolutions. The table on the Webpack documentation gives a very vivid set of examples, but for my purposes, this is what the resolutions would ultimately look like:

// webpack.cordova.js

{
    alias: {
        Platform: path.resolve(__dirname, 'platform', 'cordova')
    }
}

// webpack.web.js

{
    alias: {
        Platform: path.resolve(__dirname, 'platform', 'web')
    }
}

I would have to modify my file structure so that each copy of my service files lived within its own folder. So rather than have my files named something like user.cordova.js and user.web.js, they would be moved to a platform folder: platform/cordova/user.js and platform/web/user.js. Not a problem at all.

The snippet above simply becomes the following:

import { UserService } from 'Platform/user.js';

// go do some stuff with the UserService...

Demo

While the final product is proprietary to GE and therefore not something I can share, I did whip up a demo app that I put on my public GitHub to demonstrate.

If you pull that repo, you’ll see that it mostly consists of build files, with an index.html and some JS files hanging off to the side. The way I have it structured, all of the builds pull from the same common build steps, and for the purposes of the demo all output to the same file. That’s just for ease of demonstrating how quickly you can switch between platforms when building.

If you open package.json, you’ll see that I have NPM scripts for each of the platform builds. So, to run the demo, you simply run:

$ npm run web

You’ll see the following show up in your browser:

Synchronous console logs

Now, try building for a different platform and refresh the browser.

$ npm run cordova

You should see the text change to reflect the platform you’ve targeted.

Synchronous console logs

Note that I included a common import file to act as a constant during this experiment. It doesn’t really serve too much of a purpose other than to keep me honest and make sure I’m not doing any demo-magic trickery.

I’ll be the first to say that the demo project could be cleaner – parallel builds, passing in constants in the build process rather than hard-coding them in the configurations. In my opinion, those were beyond the scope of the demo.

Leave a Reply

Your email address will not be published. Required fields are marked *