Switching an application to aurelia-cli
Posted on 2016-08-17 in Aurelia
About a month ago, I started to make experiments with the webpack plugin for Aurelia in order to split my applications into multiple bundles. The application in question is a strategy game called Arena of Titans. You can see it there (click play to create a game, or use this link).
It was working well but the initial load of any page was slow. It was easy to find out why: the bundle loaded by webpack was over 1.3 megabytes big. Why is it that big? Well, it contained the game board which is about 1.1 megabytes minified. Hence my will to split it into several bundles in order to speed up the loading of the site. We still have to load the big board on the game page but at least the site would work at an acceptable speed.
I didn't manage to do it with webpack and after some time trying I thought what about the new aurelia-cli? I was able to do it just fine. Here's how I did it. You can skip to the part about bundles if you're not interested in the rest.
Sommaire
Presentation of aurelia-cli
aurelia-cli is a project aimed to provide a command line interface to create and run projects written with the Aurelia JavaScript framework. You can:
- Create a new project with au new <project-name>. You will then be guided by an assistant to configure the project: standard ES6 or typescript, pure CSS or SCSS/Less, …
- Run the project with au run or au run --watch to rebuild the app when you save a modification.
- Test the project with au test or au test --watch to retest the app when you save a modification.
It relies on gulp to launch its tasks and on Browsersync to provide a small HTTP server that will reload you page when you do a modification.
Preparation
To avoid messing with the current code, I created a new aurelia-cli project in a separate folder and I copied the proper files in my project:
- The folder aurelia_project which contains the gulp tasks required by aurelia-cli to run and the configuration of the project.
- The scripts folder which contains requirejs the module loader used by project running with aurelia-cli and its text plugin to load the HTML and CSS.
- test/aurelia-karma.js a small file that allows you to use karma with aurelia-cli projects. See the section about tests to learn more on that.
- karma.conf.js so my karma configuration file doesn't rely on webpack any more and meets the expectations of aurelia-cli.
I then updated my package.json file. To do that, I just compared my current package.json with the one from the aurelia-cli project: I added the proper dependencies (like aurelia-cli) and removed the old ones (like webpack).
To avoid style warning with on the environement.js file, I added it to the .eslintignore. This files contains utilities values to configure aurelia (are we testing? are we in debug mode?):
export default { debug: true, testing: false };
I then removed all of webpack configuration files and run rm -rf node_modules && npm install to update the dependencies.
Then, I adapted aurelia.json to my needs:
- The sources of my project are in a folder named app and not in src as it is by default. So I changed all paths (a replace src by app did it). I also had to correct aurelia-karma.js. See the section about tests to learn more about that.
- I configured the router to use push states. Which means my URLs look like this: /game/create and not like this #/game/create. The problem is that, by default, requirejs will load its bundle under /game/scripts/bundle.js instead of /scripts/bundle.js. So I did my first patch in aurelia-cli to add an option to use absolute path in requirejs. This way if you use "useAbsolutePath": true in the build.targets section of your aurelia.json file, your bundles will always be loaded in /scripts/bundle.js.
I also updated my global.scss file to load fonts with an absolute URL and aurelia_project/tasks/run.js to change the port of Browsersync and disable the ghost mode which mimics on all browsers the action you do in one: I want to be able to test the game in different browsers running different players. I don't want it to reproduce what I do in every browsers I have opened.
Updates in main.js
I reverted Aurelia's configuration function to the classical export function configure(aurelia) and I added the two lines below to enable development logging only on debug mode and to enable the testing plugin when testing:
if (environment.debug) { aurelia.use.developmentLogging(); } if (environment.testing) { aurelia.use.plugin('aurelia-testing'); }
I also updated my configuration of bluebird.js a promise library for javascript in main.js from:
let Promise = require('bluebird').config({ longStackTraces: false, warnings: false, });
to:
Promise.config({ longStackTraces: false, warnings: false, });
Style
With webpack, you include the SCSS files like javascript but with the extension, like this: import ./style/global.scss. Webpack will then compile the file and include it in the bundle thanks the loader you've configured. With aurelia-cli, things are a little different. You include the file in your HTML files like you require custom elements:
<require from="./style/global.css"></require>
Note that the extension of the file must be .css whether you're using true CSS or another language compiled to CSS like SCSS. The reason is that it will be loaded as CSS. You just have to configure the cssPreprosessor in your aurelia.json for the code to be correctly processed:
"cssProcessor": { "id": "sass", "displayName": "Sass", "fileExtension": ".scss", "source": "app/**/*.scss" }
Bundles
The ability to split my app into multiple bundles is the main reason I switched to aurelia-cli. To create a bundle, all you have to do is to add an object with a name and a list of dependencies to the build.bundles array of your aurelia.json file.
It works great but the first time I tried, my bundles were not filled correctly. I tried to include files like this: "[site/**/*.js]" but it didn't work. I did some search and I found this comment by TylerJPresley on a issue about creating multiple bundles with aurelia-cli which gave me the solution: you have to put stars in front of the file name. Like this: "[**/site/**/*.js]":
{ "name": "site-bundle.js", "source": [ "[**/site/**/*.js]", "**/site/**/*.{css,html}" ] }
With that, my bundles were correctly created and filled but they were not loaded. However, it turns out that I was missing some files in them: I did put the site and game files but not app.js, environment.js and main.js. As you might expect, without these files Aurelia could not work correctly. So I create a common-bundle.js for them and other files needed in the whole application:
{ "name": "common-bundle.js", "source": [ "[**/locale/**/*.js]", "[**/app.js]", "[**/environment.js]", "[**/main.js]", "[**/services/options.js]", "[**/services/storage.js]", "[**/services/browser-sniffer.js]", "[**/widgets/aot-options/*.js]", "**/app.html", "**/widgets/aot-options/*.html", "**/widgets/aot-options/*.css", "**/style/*.css" ] }
In addition to bundling your app files, you can also include non Aurelia dependencies from outside your project if they rely on AMD. For instance, here's how I define my game-create-bundle.js with clipboardjs:
{ "name": "game-create-bundle.js", "dependencies": [ { "name": "clipboard", "path": "../node_modules/clipboard/dist", "main": "clipboard" } ], "source": [ "[**/game/create/**/*.js]", "**/game/create/**/*.{css,html}" ] }
You can also prepend javascript files to a bundle. That's how requirejs and bluebird are loaded. Here's the relevant excerpt from the vendor-bundle.js (defined here):
"prepend": [ "node_modules/bluebird/js/browser/bluebird.js", "scripts/require.js" ]
At last but not least, some Aurelia modules contains multiple files that must be resolved. To bundle those correctly, you need to use an object instead of the name of the module in you dependencies array. Like this:
"dependencies": [ "aurelia-binding", { "name": "i18next", "path": "../node_modules/i18next/dist/commonjs", "main": "index" } ]
You can view the full definition of my vendor-bundle.js where this example is taken here and the definition of all my bundles here.
All in all, I think that bundling with aurelia-cli is very powerful, works well and, once you know for the double stars, is quite easy to setup with the help of a new project to give you the base configuration.
In the end, I have 6 bundles for the application:
- vendor-bundle.js with requirejs, bluebird.js a promise library and all Aurelia related files.
- common-bundle.js that contains the translations, app.js, main.js, environement.js and various services and widgets common to the site and the game.
- site-bundle.js that contains all pages and widgets for the sites.
- game-common-bundle.js that contains files required to create and play the game.
- game-create-bundle.js that contains all the widgets to create the game.
- game-play-bundle.js that contains all you need to actually play the game.
Problems encountered with bundles and their solutions
aurelia-cli loads its bundles with requirejs which I find great. Until during a test session a friend reported that he was not redirected to the game page when he first tried to create a game. The second time he tried everything worked fine.
I dug into the problem and found out why it was failing. The bundle containing the game is still big (about 1.3 megabytes). On some slow connection, it took more than 7 seconds to load it. Which means that we encountered the default timeout for requirejs and the app would consider the bundle could not be loaded and thus didn't redirect the player to the page even after the script was loaded. requirejs nicely logged an error with this link in the console.
The solution is to use the waitSeconds option of requirejs to increase the value of the timeout (you can also disable it as the documentation says). I did some tests directly in the generated bundle and it work.
The problem is that at the time of this writing, aurelia-cli doesn't allow you to give custom options to the loader. So, I made a pull request to allow the user to do just that in order to correctly solve my problem while still using the cli. I hope it will be merged soon.
In order to further improve load time on these slow connection, I preload the big game-play-bundle.js with the board as soon as a user reaches the create game page. To do that, I added the line below in the constructor of the create game page:
require(['game/play/widgets/board/board'], () => {});
You must use this syntax to load the script asynchronously. If you use:
require('game/play/widgets/board/board')
the script will be loaded synchronously which means the user can't prepare the game until it is completely loaded. That is of course not my goal.
Tests
First, since the code is not in the src folder but in app, I had to correct aurelia-karma.js (see Preparation for where it comes from) the file that boostrap karma for usage with aurelia-cli. I had to correct this line (52 at the time of this writing):
originalDefine('/base/src/' + name, [name], function (result) { return result; });
into:
originalDefine('/base/app/' + name, [name], function (result) { return result; });
I think it would be better for aurelia-karma to use paths.root for aurelia.json (more general). I gave it a try but didn't succeed. But there may be other ways to do this. See my issue on the subject for more details paths.root from aurelia.json.
In my test files, I had to change some import paths, remove my import ../setup since it is automatically loaded by aurelia-karma. The test/unit/setup.js has the same content as before to import ployfills and initialize Aurelia:
import 'aurelia-polyfills'; import { initialize } from 'aurelia-pal-browser'; initialize();
I also had to move my test-utils.js which contains various stubs I use in my unit tests from the test/unit folder into the app folder so it is correctly transpiled by babel. I also created a dedicated bundle named test-utils-bundle.js to avoid loading it in the true application.
Conclusion
This is it! Converting the project wasn't very hard but some problems (absolute loading of bundles, some problems with testing and the inability to configure requirejs) required some time to make it work correctly. Now I think that the project is ready for the future of Aurelia tooling and I don't think I'll need to switch tools again.
If you want all the gory details, take a look at this merge commit and the commits before it in the aurelia-cli branch.
Otherwise, you also take a look at my aurelia.json file here, browse the complete code of the app there and see it in action on the website (click play to create a game, or use this link).
If you have a remark or question about this article or the game, leave a comment below or contact me on twitter.
Next step on my agenda, setup code coverage of the original sources of the app (and not the bundle, that would be too easy). I hope I'll be able to make it work and write about it soon. Stay tuned!