Introduction
This page covers the details of dependency management in latest built-in bundler. The new implementation, auto tracing, brings the user experience of built-in bundler to the level of Webpack, while maintaining unique flexibility thanks for the runtime capability of RequireJS and SystemJS.
Auto Tracing
CLI's built-in bundler behaves very similar to Webpack plus aurelia-webpack-plugin.
- auto traces JavaScript dependencies, supports CommonJS, AMD, UMD, and Native ES Module format.
- auto stubs core Node.js modules for running in browser, use exact same stubs that Webpack and Browserify use.
- auto traces js/html/css dependencies in Aurelia view templates
<require from="..."></require>
.
It also provides features above Webpack.
- wrapping
"moduleName"
withPLATFORM.moduleName("moduleName")
is not required. The built-in bundler understands all Aurelia's convention without the help ofPLATFORM.moduleName
. - in esnext app,
au run --auto-install
to help you install npm packages. You can keep writing code, CLI will install missing npm packages, bundle them, and refresh you browser, see auto-install at end of this page for more details.
Limitation of Stubbing Core Node.js Modules
Same as Webpack and Browserify, not all core Node.js functionality can be replicated in browser environment. This trick does not magically bring all npm packages to browser.
Known problem of auto stubbing core Node.js modules
Sometimes you may see missing process
or Buffer
variable, as they are Node.js globals.
To fix the missing global, add these to your main${context.language.fileExtension}.
import process from 'process';
window.process = process;
import {Buffer} from 'buffer';
window.Buffer = Buffer;
import * as process from 'process';
window.process = process;
import {Buffer} from 'buffer';
window.Buffer = Buffer;
Make sure you don't load those npm packages rely on those globals in main${context.language.fileExtension}. You need to delay them after the globals were ready, typically you can load them in app${context.language.fileExtension} or any other component delayed by Aurelia module loading.
In this scenario, you'd better not use aurelia.setRoot(App)
, use aurelia.setRoot(PLATFORM.moduleName('app'))
or aurelia.setRoot('app')
or aurelia.setRoot()
to make sure app
module is delayed.
Manual Tracing
There are two scenarios that auto tracing could not cover.
1. modules not explicitly required by your code
For modules not explicitly required (directly or indirectly), we need to tell the bundler to bundle them in.
Let's look at the default aurelia_project/aurelia.json
of a new app.
"bundles": [
{
"name": "app-bundle.js",
"source": [
"**/*.{js,css,html}"
]
},
{
"name": "vendor-bundle.js",
"prepend": [ /* ... */ ],
"dependencies": [
"aurelia-bootstrapper",
"aurelia-loader-default",
"aurelia-pal-browser",
{
"name": "aurelia-testing",
// conditional bundling for only dev environment
"env": "dev"
},
// text module is slightly different for app with SystemJS
"text"
]
}
],
"loader": {
"type": "require",
"configTarget": "vendor-bundle.js",
/* ... */
}
Note we hard coded five npm packages in dependencies
of vendor-bundle
. They are not directly required by your code, so we need to explicitly ask CLI bundler to bundle them.
It will be very rare for you to have additional dependencies like them.
Conditional Dependency
aurelia-testing
is special, it's required by your source code but behind a condition check.
if (environment.testing) {
aurelia.use.plugin('aurelia-testing');
}
Because tracing is based on static code analysis, CLI bundler cannot smartly make conditional decision. To avoid bundling unnecessary package for production build, CLI bundler removes those conditional plugin(s) from auto traced dependencies. This kind of conditional plugin need to be configured manually.
In the above config of aurelia-testing
, we use "env": "dev"
to target only dev environment , if you want to target both dev and stage, use "env": "dev & stage"
.
2. legacy modules that doesn't support any module format
There are not many npm packages in this category. If you do encounter one of those stubborn packages, there are two choices: prepend or shim.
Prepend
This is the best solution to save your time on stubborn npm packages.
Those stubborn npm packages were not designed to work with any module loader. So why wast your time to try wrapping them into AMD modules? Why not just let them do what they were designed to do (polluting the global name space)?
In aurelia_project/aurelia.json
vendor-bundle, there is a prepend
section.
{
"name": "vendor-bundle.js",
"prepend": [
"node_modules/bluebird/js/browser/bluebird.core.js",
{
"path": "node_modules/aurelia-cli/lib/resources/scripts/configure-bluebird-no-long-stacktraces.js",
"env": "stage & prod"
},
{
"path": "node_modules/aurelia-cli/lib/resources/scripts/configure-bluebird.js",
"env": "dev"
},
"node_modules/requirejs/require.js"
],
"dependencies": [
// ...
]
}
vendor-bundle is the entry bundle file which you load up in index.html
, the entry bundle is specified by loader.configTarget
in aurelia.json
.
"loader": {
"type": "require",
"configTarget": "vendor-bundle.js",
// ...
}
CLI bundler writes those prepend files at the beginning of vendor-bundle.js
, most importantly writes the require.js
at end of the prepend
list.
require.js
brings AMD module loader up in browser.
- everything before it are executed in browser without AMD module loader.
- all bundled
"dependencies"
(and"source"
if there is) are afterrequire.js
, they are all executed in an AMD environment prepared byrequire.js
.
Because of the preparation of AMD environment, prepend
only makes sense in vendor-bundle.
tempusdominus-bootstrap-4
We will take a troublesome legacy JavaScript library tempusdominus-bootstrap-4
to demonstrate both prepend
and shim
.
tempusdominus-bootstrap-4
is a jQuery plugin of date picker. It doesn't support either AMD or CommonJS module loader. It depends on jQuery, moment, font-awesome v4 and Bootstrap v4. First we install all npm packages. Bootstrap v4 also needs additional popper.js.
npm install tempusdominus-bootstrap-4 jquery bootstrap moment font-awesome popper.js
or yarn add tempusdominus-bootstrap-4 jquery bootstrap moment font-awesome popper.js
Prepend them before require.js
so all of them can create JavaScript global objects. Note you need to list them in right order of dependency.
"bundles": [
// ...
{
"name": "vendor-bundle.js",
"prepend": [
/* ... omit bluebird stuff */
"node_modules/jquery/dist/jquery.min.js",
"node_modules/moment/min/moment-with-locales.min.js",
// need to use umd build of popper.js
"node_modules/popper.js/dist/umd/popper.min.js",
"node_modules/bootstrap/dist/js/bootstrap.min.js",
"node_modules/tempusdominus-bootstrap-4/build/js/tempusdominus-bootstrap-4.min.js",
"node_modules/requirejs/require.js"
],
"dependencies": [ /* ... */ ]
}
],
"copyFiles": {
// need font-awesome v4 fonts files
"node_modules/font-awesome/fonts/*": "font-awesome/fonts"
},
Now $
, jQuery
and moment
are global JavaScript objects to your app. You can use them freely without any import (import $ from 'jquery'
).
To calm eslint down on warning of '$' is not defined
, add following line to top of any file that uses those globals.
/* global $, jQuery, moment */
With jquery in prepend, please don't import $ from 'jquery'
in any code. Otherwise, auto tracing will bring in jquery npm package again, causes duplicated jquery loading (one prepend before require.js
, one npm package after require.js
).
There is still a way to allow you to write import $ from 'jquery'
when using prepend, read onRequiringModule in CLI Bundler advanced for more details.
An example of using tempusdominus-bootstrap-4
:
/* global $, moment */
export class App {
value = moment();
attached() {
// for better reuse, wrap datetimepicker
// behind an Aurelia custom element.
$('#datetimepicker1').datetimepicker({
date: this.value
});
$('#datetimepicker1').on('change.datetimepicker', e => {
// sync back to value
this.value = e.date;
});
}
detached() {
$('#datetimepicker1').datetimepicker('destroy');
}
}
/* global $, moment */
export class App {
value = moment();
attached() {
// for better reuse, wrap datetimepicker
// behind an Aurelia custom element.
$('#datetimepicker1').datetimepicker({
date: this.value
});
$('#datetimepicker1').on('change.datetimepicker', e => {
// sync back to value
this.value = e.date;
});
}
detached() {
$('#datetimepicker1').datetimepicker('destroy');
}
}
<template>
<require from="font-awesome/css/font-awesome.min.css"></require>
<require from="bootstrap/css/bootstrap.min.css"></require>
<require from="tempusdominus-bootstrap-4/build/css/tempusdominus-bootstrap-4.min.css"></require>
<div class="container">
<div class="row">
<div class="col-sm-6">
<p>Value: ${value}</p>
<div class="form-group">
<div class="input-group date" id="datetimepicker1" data-target-input="nearest">
<input type="text" class="form-control datetimepicker-input" data-target="#datetimepicker1">
<div class="input-group-append" data-target="#datetimepicker1" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
Conditional Prepend
You can conditionally prepend a JavaScript file per environment, like this one conditionally adds a bluebird config for stage and prod environment.
{
"path": "node_modules/aurelia-cli/lib/resources/scripts/configure-bluebird-no-long-stacktraces.js",
"env": "stage & prod"
}
Shim
Shim is a technique to wrap a legacy JavaScript library into an AMD module. Here is how you wrap tempusdominus-bootstrap-4
into AMD module, add following to aurelia_project/aurelia.json
vendor-bundle dependencies:
// remember to remove those jquery/moment... from your prepend
"dependencies": [
// ...
{
"name": "tempusdominus-bootstrap-4",
"deps": [
"jquery",
"bootstrap",
"popper.js",
"moment"
],
"wrapShim": true
}
]
We only need explicit config for tempusdominus-bootstrap-4
, all other jquery, bootstrap, popper.js, and moment are handled by auto tracing.
The above dependency configuration is called "shim", a config using deps
or exports
or both.
deps
- This is an array of dependencies which must be loaded and available before the legacy library can be evaluated.exports
- (optional) This is the name of the global variable that should be used as the exported value of the module. You don't need to setexports
if you only doimport "tempusdominus-bootstrap-4";
without using any exported object.wrapShim
- (optional, try this if normal shim does not work) wrap the legacy code in a function. This will delay the execution of the legacy code to module loading time.
wrapShim
CLI also supports global wrapShim setting in aurelia.json
loader.config.wrapShim
which forces wrapShim
on all shim dependencies. More details in CLI Bundler advanced chapter.
For Long Time CLI Bundler Users
Unlike old version of CLI Bundler, we don't need path
and main
. Auto-trace takes care of that. path
and main
are now only for flexible use cases such as npm package alias or packages not in node_modules folder. Check CLI Bundler Advanced chapter for more details.
Unfortunately, the above config is still not enough for tempusdominus-bootstrap-4
. tempusdominus-bootstrap-4
expects a global moment
object. But as of momentjs
version 2.10.0, momentjs
no longer exports global object in AMD module environment. We need to manually expose moment
object to global namespace before loading up tempusdominus-bootstrap-4
.
For anyone without much experience of AMD shim, this is quite hard to figure out. That's why we recommend prepend over shim for any legacy JavaScript library.
We can expose moment
to global namespace in main.js
, then loads tempusdominus-bootstrap-4
in app.js
. Here is a full setup.
// aurelia_project/aurelia.json
"bundles": [
// ...
{
"name": "vendor-bundle.js",
"prepend": [ /* ... */ ],
"dependencies": [
// ...
{
"name": "tempusdominus-bootstrap-4",
"deps": [
"jquery",
"bootstrap",
"popper.js",
"moment"
],
"wrapShim": true
}
]
}
],
"copyFiles": {
"node_modules/font-awesome/fonts/*": "font-awesome/fonts"
}
import moment from 'moment';
window.moment = moment;
import * as moment from 'moment';
window.moment = moment;
import $ from 'jquery';
import moment from 'moment';
import 'tempusdominus-bootstrap-4';
export class App {
value = moment();
attached() {
// for better reuse, wrap datetimepicker
// behind an Aurelia custom element.
$('#datetimepicker1').datetimepicker({
date: this.value
});
$('#datetimepicker1').on('change.datetimepicker', e => {
// sync back to value
this.value = e.date;
});
}
detached() {
$('#datetimepicker1').datetimepicker('destroy');
}
}
import * as $ from 'jquery';
import * as moment from 'moment';
import 'tempusdominus-bootstrap-4';
export class App {
value = moment();
attached() {
// for better reuse, wrap datetimepicker
// behind an Aurelia custom element.
$('#datetimepicker1').datetimepicker({
date: this.value
});
$('#datetimepicker1').on('change.datetimepicker', e => {
// sync back to value
this.value = e.date;
});
}
detached() {
$('#datetimepicker1').datetimepicker('destroy');
}
}
<template>
<require from="font-awesome/css/font-awesome.min.css"></require>
<require from="bootstrap/css/bootstrap.min.css"></require>
<require from="tempusdominus-bootstrap-4/build/css/tempusdominus-bootstrap-4.min.css"></require>
<div class="container">
<div class="row">
<div class="col-sm-6">
<p>Value: ${value}</p>
<div class="form-group">
<div class="input-group date" id="datetimepicker1" data-target-input="nearest">
<input type="text" class="form-control datetimepicker-input" data-target="#datetimepicker1">
<div class="input-group-append" data-target="#datetimepicker1" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
au run --auto-install
When you run au run
with --auto-install
, it auto installs missing npm packages while tracing your source code. You can keep writing code, CLI bundler will take care of npm install some-package
(or yarn add some-package
).
TypeScript App is Not Supported
Due to TypeScript stops compilation when detecting missing npm package, CLI bundler could not get any information on the missing package.
This is lots of fun! You could just write <require from="bootstrap/css/bootstrap.min.css"></require>
in app.html
, then shortly after, boom! you will see style change in browser.
Limitation
auto-install only installs latest version of the package, if you want a special version, do manual install.