Aurelia CLI Bundler

In depth guide to the integrated Aurelia CLI Bundler.

Introduction

This part of the documentation is only relevant if you are using the Aurelia CLI Bundler. If you're using Webpack then the Webpack chapter is what you need.

ASP.NET Core

If you would like to use ASP.NET Core, first begin by using Visual Studio to create your ASP.NET Core project. Select whatever options make the most sense based on your .NET project plans. After you have created the project, open a command line and change directory into your web project's project folder. This is the folder that contains the .xproj file. From within this folder, you can execute the following command au new --here which will setup Aurelia "here" inside this project folder. You will be prompted to choose the platform you want. Simply select "ASP.NET Core". Follow the prompts for the rest of the process, just like above.

Since Aurelia-CLI should be in charge of building your client side code, make sure before running the new command from Aurelia-CLI you add <typescriptcompileblocked>true to your .xproj file inside the first <propertygroup> you find to stop Visual Studio from compiling the .ts files in your project. If you build your solution before doing this, Visual Studio will compile your .ts files breaking some of the Aurelia-CLI commands.

When developing an ASP.NET Core application you will want to set the ASPNETCORE_ENVIRONMENT environment variable. Detailed instructions can be found on the Microsoft Docs .

Deploying Your App

Run the following build command:


  au build --env prod
  

Then copy the file index.html and the folder /scripts to the main deployment folder on your server.

Unit Testing

If you selected a project setup that includes unit tests, you can run your tests with au test. If you would like to adopt a tdd-based workflow, writing code and tests with continual test evaluation, you can use the --watch flag. For example: au test --watch.

Build Revisions

To create builds with revision numbers, you must set rev to be true under the build options. This will cause a unique revision number to be added to the bundled files. For example:


  "options": {
    "minify": "stage & prod",
    "sourcemaps": "dev & stage",
    "rev": true
  }
  

You are also able to set specific flags so that build revisions only take place while staging or in production. For example:


  "options": {
    "minify": "stage & prod",
    "sourcemaps": "dev & stage",
    "rev": "stage & prod"
  }
  

Now, if you were to run au build --env prod, the output would contain build revisions, while au build --env dev would not. Setting the build revisions to only compile while in production can help the development process, since it keeps your workspace clean of various build revisions.

Modifying The Index File

In order for your index.html file to be updated to load up the correct revisioned bundle, you must ensure that the "index" property located in build/targets section is correctly pointing to the index.html (or starting page) for your project. For example:


  "build": {
    "targets": [
      {
        "id": "web",
        "displayName": "Web",
        "output": "scripts",
        "index": "index.html"
      }
    ]
  }
  

Bundling Your Project

By default, the Aurelia CLI creates two bundles, an app-bundle.js, and a vendor-bundle.js. An example of the default app-bundle.js looks like this:


  {
    "name": "app-bundle.js",
    "source": [
      "[**/*.js]",
      "**/*.{css,html}"
    ]
  }
  

In this setup, we've named the bundle app-bundle.js, and have defined what's included by setting the source property to be an array of patterns that match to file paths (the patterns are using glob patterns, minimatch to be specific, to find files that match).
Optionally, you can define an exclude list by setting the source property to be an object containing both an include and exclude array of patterns. This is helpful when you're trying to define multiple bundles from your source code.


  {
    "name": "app-bundle.js",
    "source": {
      "include": [
        "[**/*.js]",
        "**/*.{css,html}"
      ],
      "exclude": [
        "**/sub-module/**/*",
      ]
    }
  },
  {
    "name": "sub-module-bundle.js",
    "source": [
      "**/sub-module/**/*",
    ]
  }
  

Adding Client Libraries to Your Project

The CLI provides two commands to help you add 3rd party client libraries, au install <library> and au import <library>. The install command will download, install and add the library to the configuration file aurelia_project/aurelia.json. The import command will add a library that you've previously installed with npm to the configuration file. Finally, both commands will give you instructions on how to access the library from your code.

Manual configuration

Unfortunately, not all 3rd party libraries can be successfully configured automatically by the install and import commands. In order the remedy this by manual configuration, open the aurelia_project/aurelia.json file and scroll down to the build.bundles section. You'll need to add the library into one of your bundle's dependencies sections.

Below is some guidance for how to manually configure several different common 3rd party library scenarios:

A Single-File Module

If the library you have installed is a single CommonJS or AMD file, you can add an entry similar to the following to the dependencies of your bundle:


    "dependencies": [
      {
        "name": "library-name",
        "path": "../node_modules/library-name/dist/library-name"
      }
    ]
  
  • name - This is the name of the library as you will import it in your JavaScript or TypeScript code.
  • path - This is a path to the single module file itself. This path is relative to your application's src folder. Also, you should not include the file extension. .js will be appended automatically.

If the main field of the library's package.json points to the single file that you need to bundle, then you can opt for a simplified configuration by just adding the package name to your dependencies directly:


    "dependencies": [
      "library-name"
    ]
  

A CommonJS Package

Many modules installed through NPM are packages made up of multiple source files. Configuring a library like this is a bit different than the single-file scenario above. Here's an example configuration for a multi-file package:


    "dependencies": [
      {
        "name": "aurelia-testing",
        "path": "../node_modules/aurelia-testing/dist/amd",
        "main": "aurelia-testing",
        "env": "dev"
      }
    ]
  
  • name - This is the name of the library as you will import it in your JavaScript or TypeScript code.
  • path - This is a path to the folder where the package's source is located. This path is relative to your application's src folder.
  • main - This is the main module (entry point) of the package, relative to the path. You should not include the file extension. .js will be appended automatically. Set main to false when the package does not have a main file.

Environment-Specific Dependencies

We've also shown how to use the env setting on a dependency. This can be used on any dependency in the bundle to indicate what environment builds the dependency should be included in. By default, dependencies are included in all builds. The example above shows how to include the library only in builds targeting the "dev" environment. You can also specify multiple environments like dev & stage.

A Legacy Library

Libraries that predate module systems can be a pain because they often rely on global scripts which must be loaded before the library. These libraries also add their own global variables. An example of one such library is bootstrap . Let's take a look at how to handle a legacy library like that.


    "dependencies": [
      {
        "name":"jquery",
        "path":"../node_modules/jquery/dist",
        "main":"jquery.min",
        "exports": "$"
      },
      {
          "name": "bootstrap",
          "path": "../node_modules/bootstrap/dist",
          "main": "js/bootstrap.min",
          "deps": ["jquery"]
      }
    ]
  
  • name - This is the name of the library as you will import it in your JavaScript or TypeScript code.
  • path - This is a path to the folder where the package's source is located. This path is relative to your application's src folder.
  • main - This is the main module (entry point) of the package, relative to the path. You should not include the file extension. .js will be appended automatically. Set main to false when the package does not have a main file.
  • deps - This is an array of dependencies which must be loaded and available before the legacy library can be evaluated.
  • exports - This is the name of the global variable that should be used as the exported value of the module.

Notice first that we've included "jquery" as one of our dependencies, and specifically at the beginning of the dependency list.
We are using the exports property to export the jQuery object since jQuery plugin, like Bootstrap, attach their APIs to the jQuery object itself. (This could be any global variable, though.) Finally, below that we configure bootstrap. The first three properties are the same as in our package example above. However, now we have a deps list. We've included jquery since Bootstrap needs it to be present before it can load.

A Library with Additional Resources

The Bootstrap example above results in the bundling of the JavaScript portions of the library. But, as you probably know, Bootstrap is mostly about CSS. The CSS files distributed with Bootstrap aren't traceable through the module system so this still doesn't result in the Bootstrap CSS being bundled. Here's how we solve that problem:


    "dependencies": [
      {
        "name":"jquery",
        "path":"../node_modules/jquery/dist",
        "main":"jquery.min",
        "exports": "$"
      },
      {
          "name": "bootstrap",
          "path": "../node_modules/bootstrap/dist",
          "main": "js/bootstrap.min",
          "deps": ["jquery"],
          "resources": [
            "css/bootstrap.css"
          ]
      }
    ]
  

Notice that we've added a resources array. Here we can provide a list of additional files to be included with the bundle. These files are relative to the path designated above and must include the file extension. You can also use glob patterns in place of exact file names.

The final step to make Bootstrap work is to copy the necessary font files to the bootstrap/fonts folder, which by default is where Bootstrap will look for the font files. To do this, we should declare these files in the copyFiles property, after the bundles property.


  "bundles": [ ... ], 
  "copyFiles": {
      "node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2": "bootstrap/fonts",
      "node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff": "bootstrap/fonts",
      "node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf": "bootstrap/fonts"
    }
  

Now, the font files will be copied to the bootstrap/fonts folder when building the application.

Setup for copying files

The copyFiles works as a 'from':'to' setup, where 'from' is the location of the file you want to copy, and 'to' is the destination folder. Both paths are relative to project folder. The sintax is:


  "bundles": [ ... ], 
  "copyFiles": {
    FILE_YOU_WANT_TO_COPY_BASED_ON_PROJECT_FOLDER: DESTINATION_FOLDER_BASED_ON_PROJECT_FOLDER
  }
  

If you run the application providing the --watch flag, the files will be recopied when changed.

Remember that CSS bundled in this way is bundled as a text resource designed to be required in your view. To load the Bootstrap css file in a view, use <require from="bootstrap/css/bootstrap.css"></require>. Notice that the module name derives from combining the name property with the resource.

A Very Stubborn Legacy Library

Sometimes you can't get a library to work with the module loading system. That's ok. You can still include it in the bundle, using traditional concatenation techniques. In fact, this is how the CLI bundles up the loader and promise polyfills. These items don't go into the dependencies section but instead go into the prepend section. This is because they aren't module dependencies. They also aren't relative to the src, but relative to the project folder. Using the prepend section causes the scripts to be prepended to the beginning of the bundle, using normal script concatenation techniques. Here's a full vendor bundle example, showing this and the rest of the techniques listed above.


    {
      "name": "vendor-bundle.js",
      "prepend": [
        "node_modules/bluebird/js/browser/bluebird.core.js",
        "scripts/require.js"
      ],
      "dependencies": [
        "aurelia-binding",
        "aurelia-bootstrapper",
        "aurelia-dependency-injection",
        "aurelia-event-aggregator",
        "aurelia-framework",
        "aurelia-history",
        "aurelia-history-browser",
        "aurelia-loader",
        "aurelia-loader-default",
        "aurelia-logging",
        "aurelia-logging-console",
        "aurelia-metadata",
        "aurelia-pal",
        "aurelia-pal-browser",
        "aurelia-path",
        "aurelia-polyfills",
        "aurelia-route-recognizer",
        "aurelia-router",
        "aurelia-task-queue",
        "aurelia-templating",
        "aurelia-templating-binding",
        "nprogress",
        "jquery",
        {
          "name": "bootstrap",
          "path": "../node_modules/bootstrap/dist",
          "main": "js/bootstrap.min",
          "deps": ["jquery"],
          "exports": "$",
          "resources": [
            "css/bootstrap.css"
          ]
        },
        {
          "name": "text",
          "path": "../scripts/text"
        },
        {
          "name": "aurelia-templating-resources",
          "path": "../node_modules/aurelia-templating-resources/dist/amd",
          "main": "aurelia-templating-resources"
        },
        {
          "name": "aurelia-templating-router",
          "path": "../node_modules/aurelia-templating-router/dist/amd",
          "main": "aurelia-templating-router"
        },
        {
          "name": "aurelia-testing",
          "path": "../node_modules/aurelia-testing/dist/amd",
          "main": "aurelia-testing",
          "env": "dev"
        }
      ]
    }
  

You may want to prepend or append scripts based on the active environment. That's entirely possible through the following syntax:


    {
      "name": "vendor-bundle.js",
      "prepend": [{
		 "path": "scripts/require.js",
		 "env": "stage & prod"
	  }]
    }
  

The above configuration would prepend the scripts/require.js file only for the stage and prod environments.

A Very Stubborn Legacy Library With Plugins

Some legacy libraries may support plugins which you also want included in your bundle. In some cases these plugins depend on a global object defined by the main library, so it is important that the plugins exist later in the bundle than the main library scripts. These plugins can go in the append section, which works exactly the same as the prepend section but the scripts are appended to the end of the bundle, after all other items. Like the prepend section all items are relative to the project folder, not the src.

A note on NPM's scoped packages

The CLI treats scoped packages in the same way as unscoped ones, you just need to remember that the scope is always part of its name.

So, for example, if you need to consume a scoped package in a CLI project, you need the following in your aurelia.json:


  dependencies: [
    {
      "name": "@scope/packagename",
      "path": "../node_modules/@scope/packagename/dist/amd",
      "main": "packagename"
    }
  ]
  

Your imports must be scoped too:


  import { SomeClass } from '@scope/packagename';
  

And this is an example of loading a @scope/packagename plugin during app startup:


  aurelia.use.standardConfiguration().plugin('@scope/packagename');
  

Reference packages outside of the node_modules folder

It is possible to use packages outside of the node_modules folder. The only difference is that you need to define what the packageRoot is. In aurelia.json, you can define a package that lives outside of the node_modules folder as follows:


    dependencies: [{
      "name": "my-standalone-folder",
      "path": "../my-standalone-folder/dist/amd",
      "main": "index",
      "packageRoot": "../my-standalone-folder"
    }]
  

The packageRoot is the root folder of the package. This is often the folder which contains the package.json file of the package.

Configuring the Loader

You can configure the loader by adding a config key to build.loader with the options you want to add. For instance, if you want to increase the timeout for requirejs, you would do this:


  "build": {
      "loader": {
          "type": "require",
          "configTarget": "vendor-bundle.js",
          "includeBundleMetadataInConfig": "auto",
          "config": {
              "waitSeconds": 60
          }
      }
  }
  

Setting the baseUrl

Sometimes you may want to keep the scripts folder somewhere other than the default location, or move the index.html file a few folders up from the project root. In that case it is possible to set the baseUrl property so that the build system uses the correct paths and that bundles get loaded correctly in the browser. The baseUrl property should be set in both the platform object as well as the build.targets object:


    "targets": [
      {
        "id": "web",
        "displayName": "Web",
        "output": "some/dir/scripts",
        "index": "index.html",
        "baseUrl": "some/dir/scripts"
      }
  

The script tag for the bundle in index.html file needs to point to the modified location of the scripts folder as well: <script src="some/dir/scripts/vendor-bundle.js" data-main="aurelia-bootstrapper"></script>

Styling your Application

There are many ways to style components in Aurelia. The CLI sets up your project to only process styles inside your application's src folder. Those styles can then be imported into a view using Aurelia's require element.

  • If you aren't using any CSS preprocessor, you write css and then simply require it in the view like this:

    <require from="./styles.css"></require>
  
  • For projects that use a CSS preprocessor (chosen from the cli setup questions):
    • Write your styles in the format you chose (styl, sass, less ...).
    • Require the style by [filename].css instead of [filename].[extension]. This is because your style file is transpiled into a module that encodes the resulting css file extension.

    <require from ="./main.css"></require>
  

Bear in mind that you can always configure things any way you want by modifying the tasks in the aurelia_project/tasks folder. For styling purposes, you can modify the process-css.js file.

Updating A Single Library

To update a single library use the command npm install library-name where library-name is the library that you wish to update.

Updating Multiple Libraries

  • Add the following section to the project's package.json file

  "scripts": {
      "au-update": "npm install aurelia-binding@latest aurelia-bootstrapper@latest ...
      }
  
  • List the libraries on a single line separated by a space.
  • Include all of the libraries from the dependencies section of aurelia.json that you want to update.
  • Use the command npm run au-update to update all of the libraries in the au-update list above.

Javascript Minification

The CLI will minify Javascript out of the box for the staging and production environments:


    "options": {
      "minify": "stage & prod",
      "sourcemaps": "dev & stage"
    },
  

These options can be found in the "build"."options" section of aurelia.json. If you wish to specify the options that are used in the minification process, then replace "minify": "stage & prod" with:


    "minify": {
      "dev": false,
      "default": {
        "indent_level": 2
      },
      "stage & prod": {
        "max-line-len": 100000
      } 
    },
  

The Aurelia-CLI uses UglifyJS2 for minification, so any option that UglifyJS2 supports is also supported by the Aurelia-CLI. With the above configuration, minification will occur for the stage and prod environments, but not for the dev environment. For the stage and prod environments, both the indent_level as well as the max-line-len option are passed to the minifier. The default key is optional, but allows you to reduce code duplication when multiple environments have similar options.

Loader plugins

Since RequireJS and SystemJS support the notion of loader plugins, the Aurelia CLI allows you to use these in your application. The configuration of these plugins can be found in aurelia.json in the build.loader object:


    "loader": {
      "type": "require",
      "configTarget": "vendor-bundle.js",
      "includeBundleMetadataInConfig": "auto",
      "plugins": [
        {
          "name": "text",
          "extensions": [
            ".html",
            ".css"
          ],
          "stub": true
        }
     ]
  

The plugins array is the collection of all loader plugins that your application needs. By default this is only the text plugin, but you can easily add others.

The extensions array tells the Aurelia CLI Bundler for which files to use the plugin. That allows the bundler to bundle .html and .css files.

The stub property is an interesting one. If it's set to true then the loader plugin won't be included into the bundle, and will result in the bundle being smaller. However, you're not able to use this loader plugin at runtime. So if you were to fetch HTML files using the html plugin, and those files aren't in the bundle, then that would not work as the loader would try to use the html plugin. When the stub property is set to true that plugin wouldn't be available and the loader would throw an error. A solution would be to set stub to false, but it's better to include the files that you're trying to fetch in the bundle.

Adding new plugins is usually a three step process. First you have to specify the plugin in the loader.plugins array. Then you have to specify the loader plugin as a dependency in one of your bundle configurations. Sometimes the name of the loader plugin is not how you would like to use it. Then you could add an entry to the paths object in aurelia.json to specify a new name:


  "paths": {
    "root": "src",
    "resources": "resources",
    "elements": "resources/elements",
    "attributes": "resources/attributes",
    "valueConverters": "resources/value-converters",
    "bindingBehaviors": "resources/binding-behaviors",
    "json": "../node_modules/requirejs-json/json"
  },
  

Path mappings

Do you have import statements that look like import {Customer} from '../../../../models/customer'; ?

Consider creating a path mapping for the models folder, so that you can do: import {Customer} from 'models/customer';.

Here's how to do it. Open up aurelia.json and in the paths object do the following:


  "paths": {
    "root": "src",
    "resources": "resources",
    "elements": "resources/elements",
    "attributes": "resources/attributes",
    "valueConverters": "resources/value-converters",
    "bindingBehaviors": "resources/binding-behaviors",
    "models": "models"
  },
  

That should make it work at runtime, but if you use Typescript it's going to tell you that it can't find models/customer. To fix that, open up tsconfig.json and in the compilerOptions add a paths and baseUrl property:


    "paths": {
      "models/*": ["src/models/*"]
    },
    "baseUrl": "."
  

You might have to restart your editor to see the effect of these changes.