Menu

Migrating an Astro Project to Webpack 2 and ES6

Table of contents

Who is this guide for? #

This guide is for Astro developers looking to upgrade a project to the Webpack 2 build system and ES6 syntax. It goes over what Webpack replaces and describes in detail what changes need to be made to your codebase.

Pre-requisites #

This guide assumes you have an Astro app running on version 0.17.x or earlier that builds and runs without any errors.

Highlight of Changes #

Webpack 2 is a modern JavaScript build system that has many advanced features, such as code splitting, loaders, intelligent parser, and plugins. Webpack configuration is very extensible and will allow you to develop in the JavaScript flavor of your choice.

Previously Astro used Grunt and RequireJS to build and package an app's JavaScript code into a single app.js file which is bundled with the app. In Astro 0.18 these tools have been replaced with Webpack. Along with removing all Grunt configuration, some changes need to be made to your codebase to make it compatible with the latest version of Astro.

The biggest differences to your app after upgrading to Webpack will be how dependencies are imported. Dependencies are now relative to the current file, instead of always relative to the root directory with a bunch of module aliases. This should make importing modules and plugins from your app and Astro much simpler and intuitive.

Typical upgrade process #

The Webpack changes will only affect the app, so first cd native/. For reference, this is the PR that made the Extra app compatible with Webpack.

Point to the latest Astro

Make sure you are pointing to at least Astro version 0.18.0 version in package.json.

Install Webpack 2 and Eslint

npm i --save-dev webpack@2.2.0 eslint eslint-loader

Install Babel and the Babel plugins

npm i --save-dev \
    babel-plugin-syntax-async-functions \
    babel-plugin-transform-async-to-module-method \
    babel-plugin-transform-runtime \
    babel-plugin-transform-es2015-block-scoping
npm i --save babel-runtime

Add NPM script

In package.json, add an NPM script to build app.js with Webpack.

"scripts": {
    "build": "webpack --config webpack.app.config.js"
}

Update build scripts

In app/build-js.sh, change all instances of EXTRA_GRUNT_ARGS to EXTRA_WEBPACK_ARGS. Right after we echo "Building app.js", replace with

ROOT=$MYPATH/..
pushd $ROOT
    npm run build -- $EXTRA_WEBPACK_ARGS
popd

Babel Configuration

ES6 is not supported on the lowest versions of Android and iOS that Astro supports. Babel allows us to move to more modern JavaScript without abandoning these older OSes by transpiling our JavaScript down to ES5 (classic JavaScript).

We've already installed the dependencies and now just need to create a file called .babelrc at the root of your project (right beside the package.json file) and populate it with...

{
    "presets": ["es2015"],
    "plugins": [
        "babel-plugin-syntax-async-functions",
        ["transform-async-to-module-method", {
            "module": "bluebird",
            "method": "coroutine"
        }],
        ["transform-runtime", {
            "polyfill": true,
            "regenerator": true
        }],
        "transform-es2015-block-scoping"
    ]
}

This configuration allows you to write ES6 and the async/await syntax. It is also easily customizable if you want to take advantage of other emerging JavaScript language features before they are fully supported on all the OS versions that Astro needs to support.

Webpack Configuration File

Now we need to add the configuration file. This tells Webpack how our JavaScript should be bundled together.

Create a new file called webpack.app.config.js in the native/ directory and populate it with this configuration.

You can read more about Webpack 2 configuration in the official configuration documentation.

JavaScript source changes

Import and Export

Now comes the tedious part. In every JavaScript file we are going to change how dependencies are imported.

Modules imported from the Astro SDK need to be prefixed with astro/ and then the module is relative to node_modules/mobify-progressive-app-sdk/js/src/.

Before

define([
    'plugins/alertViewPlugin'
],
function(AlertViewPlugin) {
    // Code
});

After

import AlertViewPlugin from 'astro/plugins/alertViewPlugin';

// Code

Modules imported from the local project are relative to the current file.

Before

define([
    'app-controllers/modalController'
],
function(ModalController) {
    // Code
});

After

// Assuming modalController.js exists in same directory as this file
import ModalController from './modalController';

// Code

Exporting modules from files now uses the ES6 export default syntax instead of returning the module.

Before

define([deps...], function(Deps...) {
    var ThingToExport = function() {};
    return ThingToExport;
});

After

import Deps... from 'deps...';

const ThingToExport = function() {};

export default ThingToExport;

const and let

This is not required, but we recommend embracing the new variable declaration keywords const and let instead of var.

const declares a constant value that cannot be assigned a second time. If the value is a reference type though, it can be modified so be aware of that.

For example:

const user = {
    id: 1,
    username: 'Alice'
}

user.username = 'Bob'

console.log(user.username)
// output: Bob

user = { id: 2, username: 'Jane'}
// ERROR!

let declares a block-scoped variable. This is similiar to a var except that the scoping is what you'd expect from other languages (ie. it's not hoisted ).

After the migration #

After you have successfully migrated your app, it should build and run like normal. You will also be able to use JavaScript ES6 syntax and Async/Await.

Known migration issues #

Some apps may need some project specific configuration. An example of this is where locally bundled JavaScript in app-www/ depends on Astro or app.js code. In these cases you need to get the JavaScript building with Webpack. All JavaScript files included in bundled HTML via a <script> tag need to be defined as a Webpack entry point with corresponding output location.

var entry = {};
[
    'local-javascript-file.js',
    'local-javascript-file-2.js'
].forEach(function(file) {
    entry['app-www/js/build/' + file] = './app/app-www/js/' + file;
});
entry['./build/app'] = './app/app.js';

var config = {
    entry: entry,
    output: {
        path: path.resolve(rootDir, 'app')
    }
    ...
};

In the above code, entry is an object where the key is the output file and the value is the input file. We add all JavaScript files in app-www/ as well as the app.js file.