DISCUSSION: bundler-friendly aurelia

with the almost done pr by @bigopon it should be possible to define a component as global by passing the class directly instead of a string. this is a step in the right direction in terms of getting aurelia to work correctly with the already standard module syntax. after all aurelia prides itself in following standards.


I understand that when aurelia came to be, the module syntax was not part of the spec (similar to how angularjs came up with it’s own module system). so it had to bolt itself on to already existing loaders to reduce friction.

but the times have changed. now modules are part of the spec, and bundlers (like webpack, parcel, fusebox) use the standard syntax to make crazy optimizations to our code (tree shaking, dead code elimination, and all the buss words).

this post is not to dump on aurelia, as I said, at the time that was probably the best course of action. my purpose is to start a discussion on whether it’s time for aurelia’s module loading to align itself with the spec and let developers make better use of tools in the ecosystem without some required plugin (webpack plugin, fusebox plugin) to accomodate the framework.

some routing examples

adapted from the documentation

this is the current way a router is defined

import {RouterConfiguration, Router} from 'aurelia-router';

export class App {
  router: Router;

  configureRouter(config: RouterConfiguration, router: Router): void {
    this.router = router;
    config.title = 'Aurelia';
    config.map([
      { route: ['', 'home'],       name: 'home',       moduleId: 'home/index' },
      { route: 'users',            name: 'users',      moduleId: 'users/index', nav: true, title: 'Users' },
      { route: 'users/:id/detail', name: 'userDetail', moduleId: 'users/detail' },
      { route: 'files/*path',      name: 'files',      moduleId: 'files/index', nav: 0,    title: 'Files', href:'#files' }
    ]);
  }
}

my proposal is to allow us to pass the components directly instead of a string.

import {RouterConfiguration, Router} from 'aurelia-router';
import {HomePage} from './home/index';
import {UsersPage} from './users/index';

export class App {
  router: Router;

  configureRouter(config: RouterConfiguration, router: Router): void {
    this.router = router;
    config.title = 'Aurelia';
    config.map([
      { route: ['', 'home'],       name: 'home',       module: HomePage  },
      { route: 'users',            name: 'users',      module: UsersPage, nav: true, title: 'Users' },
    ]);
  }
}

“but code splitting!!!”, I hear you say…

import {RouterConfiguration, Router} from 'aurelia-router';

export class App {
  router: Router;
  
  configureRouter(config: RouterConfiguration, router: Router): void {
    this.router = router;
    config.title = 'Aurelia';
    config.map([
      { route: ['', 'home'],       name: 'home',       module: () => import('./home/index')  },
      { route: 'users',            name: 'users',      module: () => import('./users/index'), nav: true, title: 'Users' },
    ]);
  }
}

that way the code is understood by bundlers and they’ll know to split at that point, and aurelia doesn’t have to concern itself with loading modules.

view templates using @inlineView

not a commonly used feature, inlineView might just be the key to make all of this possible. according to the documentation, inlineView has the following ts signature

inlineView(markup: string, dependencies?: Array, dependencyBaseUrl?: string): any

if you know about inlineView then you probably know about the first argument: the view of the component.

const view = `
  <template>
    sooper cool content
  </template>
`;

@inlineView(view)
class App {}

but I bet few people know about the second and third arguments. the second one is an Array of objects with from and as properties (similar to the <require> element’s from and as attributes.

const view = `
  <template>
    sooper cool content
    <custom-element></custom-element>
  </template>
`;

@inlineView(view, [{ from: '../custom-element', as: 'custom-element' }])
class App {}

but this still suffers from the same problems as the router, it’s not using standard syntax and so you need the magical PLATFORM.moduleName to tell the bundler to form the relationship. in reality it should be possible to simply pass in the class.

// custom-element.ts
@customElement('custom-element')
export class CustomElement {}

// app.ts
import {CustomElement} from './custom-element';

const view = `
  <template>
    sooper cool content
    <custom-element></custom-element>
  </template>
`;

@inlineView(view, [CustomElement])
class App {}

I asked about this on #585. You can read @bigopon’s response there.

“but separation of concerns!!! I want my html on a different file”

although this is not part of the spec (and likely never will), all bundlers have the ability to import different file types (not sure about fusebox on this one), including html.

import view from './app.html';

@inlineView(view)
class App {}

“but other file types and async import are not part of the spec!!”

but neither are decorators… that is to say that something not being yet part of the spec hasn’t stopped aurelia from using it as part of the core api. there is precedent for these types of risks, and it’s very likely that dynamic imports will become standard. In fact it might get there before decorators do.

boostrapping

right now, manually bootstrapping an app is a mystery to me. but I believe this is a place that would also benefit from using standard module syntax.

if you’re using webpack, the way to boostrap right now is to set your app’s entry to aurelia-bootstrapper and it will magically know to use a main file and use the configure exported function. and it just happens. I would say this is not the way it should be. my dream api for this would be

<!-- src/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>the title</title>
  </head>
  <body>
    <main id="root"></main>
    <script src="./main"></script>
  </body>
</html>
// src/main.ts
import {bootstrap, Aurelia} from 'aurelia-framework';
import {App} from './app';

async function start(){
  const host = document.getElementById('root');
  const aurelia: Aurelia = await bootstrap(App, host);

  aurelia.use
    // add plugins here
    .standardConfiguration()
    .developmentLogging();

  await aurelia.start();
}

start();

and if you want to use parcel just point it to src/index.html and it knows what to do. if using webpack just point it to src/main.ts and it will know what to do without the need for special plugins. speaking of which…

"but conventions!!! :cry: "

this is where I believe plugins have their place. instead of being an absolute necessity, they would add extra features like:

  • no need to use @customElement to give the names yourself, the plugin would add the decorator based on the file name
  • same with @inlineView as the plugin could find the html view file, add the decorator, and actually inline the html in there
  • etc

for the noobs

let’s be honest starting with aurelia is not as easy as we would want it to be. in my case, this was due to it not being as popular (lack of examples) and the “conventions” to which aurelia aligned itself where not always entirely clear. hopefully, this shift would lessen the initial mental burden of learning something new. because it would be just javascript.

fin

hopefully my thoughts came through the text, and this can start a discussion to revamp/rethink aurelia’s module system

@Alexander-Taran the result of the gitter chat and some more thought

12 Likes

Excellent post and I agree with the idea of directly passing components rather than their module paths.

Ironically, in the way it currently works Aurelia makes quite a detour to get the constructor function corresponding to a module path. It needs the constructor function to begin with.
Not to mention there is always a certain set of modules that your app needs during initial load anyway - there is no benefit in lazy loading those.

Simply changing to direct references for modules that should not be lazy-loaded, aside from being bundler-friendly, could make a huge positive impact on initial load times.

I should note that aurelia-experiment looks very promising in terms of addressing this. The hard work of analyzing applied conventions can be done during build time, and highly efficient code can be generated that would take care of those imports among other things. We could keep the conventions without sacrificing any performance or bundler optimizations.

1 Like

for anyone that’s curious, you can folow the progress in this experimental repo

1 Like

The newest PR at templating will make it easier to achieve this https://github.com/aurelia/templating/pull/606

Please head there for a discussion :smile:

2 Likes

@obedm503

May I ask you about performance and memory allocation of the idea? Now Aurelia is so fast I want to know this changes can affect on the performance or not.

2

The benchmark you see is for runtime performance of frameworks, which has little todo with this feature. Also this is better in term of start up performance, as resources loading and registration are “short circuited”

3 Likes

The AOT compiler looks very interesting. It’s obviously early days but I’m guessing at some point it will also support sourcemaps back to the original source?

I’ve looked at migrating to Webpack but while the Aurelia plugin doesn’t support tree-shaking or most of its other optimisations I didn’t really see the point.

If the improvements are anything like what Angular saw, AOT plus some changes to support static analysis could potentially see drastic improvements in startup and bundle size?

:clap::clap::clap::clap::clap:

1 Like

Yes more static Aurelia means more tree shaking & scope hoisting friendly = less dynamic parts used => smaller bundle and faster startup.

Also there some ideas we can test out like Css modules and jsx for template creation. Example of jsx https://stackblitz.com/edit/statically-aurelia-zwqj67?file=calendar.js

1 Like

Update:

https://github.com/aurelia/templating-router/issues/75

Static router view model declaration for more bundler friendliness

demo

1 Like

Sounds like your work is very close to enable browserify to build aurelia app.

That can greatly simplify small app build. Personally I would not use browserify to build prod. But I really like to use it in testing together with tape. It is possible to cut karma off from test setup, and reduce all testing setup to a single line of npm script in package.json.

1 Like

Sorry to bump this but it seems like the most appropriate place to ask.

@bigopon Thanks for your hard work on this, I’ve been testing some of the new features in the latest releases.

Apart from templating-router/pull/75, what else is left to realise our dream of a bundler friendly Aurelia?

It looks like there will still be issues for those using separate HTML files and that probably includes almost everybody. From what I understood this could be solved with some build tooling? Is there anything that we can help out with?

Thanks for your hard work on this, I’ve been testing some of the new features in the latest releases.

That’s nice, did they help ?

Apart from templating-router/pull/75, what else is left to realise our dream of a bundler friendly Aurelia?

Because it’s decided to have viewModel, in addition to moduleId to indicate a direct reference, it will take a bit time to analyze the changes needed in aurelia-router, and also test. the last bit beside that is dialog, with the same ability. At the moment it somewhat supports it, but only when module id is preserved. Its an easy but breaking change one. @timfish

It looks like there will still be issues for those using separate HTML files and that probably includes almost everybody. From what I understood this could be solved with some build tooling?

Yes, build tool will be needed I think. The new features just helps to have an easy, static way of declaring a view template and its resources ( dependencies ), not loading it at runtime. I think it will mostly be :

import view from './my-el.html';

export class MyEl {
  static $view = view;
}

Is there anything that we can help out with?

Definitely, I’ll push another PR to Aurelia router soon, so you can help review and validate it to help progress it faster.

1 Like

Yeah, the new features appear to be working although I haven’t tried them with a bundler + tree shaking yet. Our app is going to take some significant reworking to make that possible.

How would the build tool handle dependencies <require>'ed in the HTML? Would they all need adding to the $resources of the vm?

I’ll test all your changes!

How would the build tool handle dependencies 'ed in the HTML? Would they all need adding to the $resources of the vm?

  • static $resource is a way to declare some metadata that was previously defined with decorators, like @customElement, @customAttribute, @valueConverter, @bindingBehavior, @templateController, @processContent. What previously was defined with value converter can be alternatively done via static $resource, for example

    @customElement('date-picker')
    export class DatePicker {
      
      @bindable({
        defaultBindingMode: bindingMode.twoWay
      })
      value
    
      @bindable displayFormat
    }
    

    can be

    export class DatePicker {
      static $resource = {
        // type: 'element', // no need for this if it is custom element
        name: 'date-picker',
        bindables: [
          {
            name: 'value',
            defaultBindingMode: 'twoWay' // it understands string now
          },
          'displayFormat'
        ]
      }
    }
    
  • static $view is a way to declare a view for a custom element specifically, so it will be something like this for <require/>:

<!-- app.html -->
<template>
  <require from='path/to/another-custom-element'></require>
  <require from='path/to/another-custom-attribute'></require>
  <require from='path/to/another-value-converter'></require>
  <require from='path/to/another-binding-behavior'></require>
</template>

Change to

import view from './app.html';

@view({
  template: view,
  dependencies: () => [
    // code splitting ready syntax, change to static import {} from '...' if needed
    import('path/to/resources'),
  ]
})
export class App {
}

or


import { SubElement } from 'path/to/other-element';
import view from './app.html';

// Note that if this value converter is local to the view of this element
// it can be stay local and be declared as a dependencies 
class LocalValueConverter {
  // ...
}

@view({
  template: view,
  dependencies: () => [
    SubElement,
    LocalCustomAttribute,
    LocalValueConverter
  ]
})
export class App {
}

@obedm503 Beside bundler friendly effort ongoing, with the PR at https://github.com/aurelia/script-tag/pull/11, you can now go bundler free. Have a check by pulling down PR branch and start a http-server in example folder

2 Likes

It is merged already… so woo-hoo!!!
Nice job

@bigopon did import() of routes get fixed/completed in vCurrent?

Ts conversion needs to happen first, and it’s getting merged soon. After that, I will kick start router with import()

2 Likes

Is it ready yet? Does it work? :slight_smile:

The PRs are at:

It will take a bit more time. But hopefully not too long. Implementation is basically done in router, now it’s the QA time with tests :slight_smile: