Aurelia

Bundling Aurelia Apps

Introduction

Aurelia

Aurelia


Bundling Aurelia Apps

Posted by Aurelia on .
Featured

Bundling Aurelia Apps

Posted by Aurelia on .

Our latest Aurelia release has made significant advances, with more on the horizon. Today, Core Team Member Ahmed Shuhel will share how our bundling strategy is evolving to support the framework and our community.


Previously, Aurelia Loader used HTML Imports to load all views. Now, as it is apparent that HTML Imports is not going to be standardized in its current form, we have replaced our default view loading mechanism with a SystemJS text-based solution. The same solution has been applied to CSS loading as well. To learn more about this change, you can read our recent release notes post. We are emphasizing this again here because these changes affect bundling, as you will see below.

Also, previously we were using Aurelia CLI to bundle our apps. However, we have decided to set aside the CLI effort for now since it mostly duplicated great work already done by tools like gulp, grunt, yeoman, etc. Instead, we are providing first class support for these tools via a small focused bundling library Aurelia Bundler that can be used from a gulp task or any other tool.

In the remainder of this post we will see how we can use Aurelia Bundler to create a gulp task for bundling our app. Let's jump right into it. We will use skeleton-navigation as our app to bundle. If you don't have that setup. Follow these steps.


Now that we have our app running proudly, let's start by installing aurelia-bundler. To do so cd into skeleton-navigation and run the following command:

npm install aurelia-bundler --save-dev  

Now, let's create a bundle.js file in build/tasks/bundle.js as:

var gulp = require('gulp');  
var bundler = require('aurelia-bundler');

var config = {  
  force: true,
  packagePath: '.',
  bundles: {
    "dist/app-build": {
      includes: [
        '*',
        '*.html!text',
        '*.css!text',
        'bootstrap/css/bootstrap.css!text'
      ],
      options: {
        inject: true,
        minify: true
      }
    },
    "dist/aurelia": {
      includes: [
        'aurelia-bootstrapper',
        'aurelia-fetch-client',
        'aurelia-router',
        'aurelia-animator-css',
        'github:aurelia/templating-binding',
        'github:aurelia/templating-resources',
        'github:aurelia/templating-router',
        'github:aurelia/loader-default',
        'github:aurelia/history-browser',
        'github:aurelia/logging-console'
      ],
      options: {
        inject: true,
        minify: true
      }
    }
  }
};

gulp.task('bundle', function() {  
 return bundler.bundle(config);
});

gulp.task('unbundle', function() {  
 return bundler.unbundle(config);
});

Note that the bundle function returns a Promise.

With that file in place, let's run the command bellow:

gulp bundle  

Here are the things that happened after gulp is finished executing the task:

  • A file, dist/app-build.js is created.
  • A file, dist/aurelia.js is created.
  • config.js is updated.

Now, if we refresh/reload the app from the browser, we will see much less network traffic, which means our app is properly bundled.

Let us now take a closer look at the config object. We will skip force and packagePath for the moment. bundles is where we will focus first.

We can create as many bundles as we want. Here we have created two: one for the app source and another for the Aurelia libs. Again, we can create just a single bundle if we want that combines both application source and Aurelia libs. The number of bundles we would like to have mostly depends on our application structure and the usage patterns of our app. For example, if our app has a design that actually makes it a collection of child-apps/sections, then a "common" bundle and a "bundle per section" makes much more sense and performs better than a huge single bundle that needs to be loaded upfront.

Here is a typical bundle configuration with all it's glory:

    "dist/app-build": {
      includes: [
        '*',
        '*.html!text',
        '*.css!text',
        'bootstrap/css/bootstrap.css!text'
      ],
      excludes: [
        'npm:core-js',
        'github:jspm/nodelibs-process'
      ],
      options: {
        inject: true,
        minify: true
      }
    }
  • dist/app-build : This is the name of the bundle and also where the bundle file will be placed. The name of the bundle file will be app-build.js. Since the baseURL for skeleton-navigation pointed to the dist folder, we named it dist/app-build.
  • includes : We will specify all the modules/files that we want to include here. Since all our JavaScript modules are in the dist folder and we have a path rule configured in config.js that points to the dist folder, if we simply specify *, all our JS modules will be included. We can specify */**/* here if we want to include all the subfolders.
  • *.html!text: This includes all the templates/views in the bundle. the !text tells the Bundler and Loader that these files will be bundled and loaded using the text plugin.
  • *.css!text: Like html templates, we are including all the css here. If you have previously used plugin-css, note that we are not using !css here. The Aurelia Loader uses text plugin for loading css to analyze and do other interesting stuff like scoping etc.
  • excludes: This is where we specify what we want to exclude from the bundle. For example, if we used * to include all the JS files in the dist folder, and for some reason we wanted app.js to be excluded from the bundle, we would write:
excludes : [  
   'app'
]
  • inject: If set to true, this will inject the bundle in config.js, so whenever the application needs a file within that bundle, the loader will load the entire bundle the first time. This is how we can achieve lazy bundle loading. For a large app with multiple sub sections, this will help us avoid loading everything upfront.
  • minify: As the name suggests, if this is set to true, the the source files will be minified as well.

Note that we are using system-builder under the hood so all the systemjs-builder options should work here.

  • force : If this is set to true the task will overwrite any existing file/bundle with the same name. Set it to false if you are not sure about it.
  • packagePath : By default it's '.'. You can change this if your package.json file is somewhere else other than the base of your app. aurelia-bundler uses this file to find config.js, baseURL, the jspm_packages folder and other important project configuration.

At this point you may be thinking, "Well, this is all good but we already have developed an application that uses Polymer and HTML Imports extensively. And we want to bundle them as well." You may have already picked up in the last post that we have created a separate plugin aurelia-html-import-template-loader exclusively for this use case. We have bundling support for that too.

This is how we can do it. There are two parts to the process:

First let's install aurelia-html-import-template-loader with the command bellow:

 jspm install aurelia-html-import-template-loader

Now, let's open src/main.js and add this line: aurelia.use.plugin('aurelia-html-import-template-loader'). After that change, main.js should look like this:

import 'bootstrap';

export function configure(aurelia) {  
  aurelia.use
    .standardConfiguration()
    .developmentLogging();

  aurelia.use.plugin('aurelia-html-import-template-loader')

  aurelia.start().then(a => a.setRoot());
}

With this little change Aurelia Loader will now use HTML Imports to load all the views. Now, back in our bundle task, we will add a config like this:

    "dist/view-bundle": {
      htmlimport: true,
      includes: 'dist/*.html',
      options: {
        inject: {
          indexFile : 'index.html',
          destFile : 'dest_index.html',
        }
      }
    }

And, we will also change the first bundle a little bit to exclude all the html and css files. Finally our bundle.js file should look like this:

var gulp = require('gulp');  
var bundle = require('aurelia-bundler').bundle;

var config = {  
  force: true,
  packagePath: '.',
  bundles: {
    "dist/app-build": {
      includes: [
        '*'
      ],
      options: {
        inject: true,
        minify: true
      }
    },
    "dist/aurelia": {
      includes: [
        'aurelia-bootstrapper',
        'aurelia-fetch-client',
        'aurelia-router',
        'aurelia-animator-css',
        'github:aurelia/templating-binding',
        'github:aurelia/templating-resources',
        'github:aurelia/templating-router',
        'github:aurelia/loader-default',
        'github:aurelia/history-browser',
        'github:aurelia/logging-console'
      ],
      options: {
        inject: true,
        minify: true
      }
    },
    "dist/view-bundle": {
      htmlimport: true,
      includes: 'dist/*.html',
      options: {
        inject: {
          indexFile : 'index.html',
          destFile : 'dest_index.html',
        }
      }
    }
  }
};

We have changed the source code (src/main.js), so we need to rebuild our app. The command bellow should do that:

 gulp serve

Now, let's run gulp bundle in a new command/console tab. If we now refresh/reload our app from the browser keeping the developer tools open, we should see the difference.

Note that order of running the tasks is important here. The build clears/removes all the files in the dist folder. So, any bundle file in that folder will be deleted too. This is why we always have to run the gulp bundle after the build task is finished. If you are using watch you will have to be extra careful because every change you make in the source file will trigger a build task that clears the dist folder and any bundles as well.

Let's examine the configuration now. If you were using the CLI previously this may look familiar. The only difference here is that we have introduced some uniformity in the config api. Let's examine this config one property at a time:

  • dist/view-bundle : The name of the bundle file is view-bundle.html and will be placed in the dist folder.
  • htmlimport : This is what makes it different from other bundles. If this is set to true the bundler will treat it as an html import-based bundle and the Aurelia loader will give it a different treatment as well.
  • includes: This is where we will specify what goes in the bundle. All the glob patterns are supported here including arrays of patterns and ! based exclusion. For example:
includes : ['dist/**/*.html', '!dist/movie/*.html']  

The above pattern will bundle all the views in dist and it's child folders except everything in the dist/movie folder.

  • options : if inject is set to true then a <link rel="import" href="path/of/bundle.html" > will be injected in the body of index.html. If you would like to keep your index.html clean and create a separate index file then you have to set indexFile and destFile.
indexFile: 'index.html'  
destFile : 'dest_index.html'  

There are two final important notes about bundling. First, our new bundling is designed to work with the latest version of Aurelia. So, you will need to update your libraries to use this. Second, now that our default view loading is based on the text plugin, you must install plugin-text with jspm install text for text-based bundling to work. This dependency is only needed at build time.

We hope this makes clear how to use bundling, as well as simplifies the process for you and helps you integrate it into your existing tool chain. If you have any issues regarding bundling be sure to raise issues here. Thanks and we look forward to seeing what great things you will build!

View Comments...