A note on Webpack's tree-shaking and why PLATFORM.moduleName is necessary

I’ve seen this question pop up from time to time, so for those who are having problems with modules not being loaded or are generally curious why/how this works:

Webpack applies a technique called tree-shaking. In summary, it statically analyzes your project files looking for import statements in order to determine what needs to be included in the bundle.
It starts doing this at your bundle entry and recursively calls each imported module until every import is resolved. Any project file that is not imported anywhere, will not be included in the bundle.

Before webpack

Aurelia has always had alternative means to reference modules outside of import statements. Examples:

  • RouterConfiguration moduleId: "path-to-module"
  • au.setRoot("path-to-app-module")
  • au.use.plugin("plugin-name")
  • au.use.feature("path-to-feature-module")
  • au.use.globalResources("path-to-resource-module")

Traditionally this has never posed any problems because with pure RequireJS, SystemJS, as well as aurelia-cli projects, you explicitly defined your bundles with paths and globbing patterns. Any files resolved from those were included in the bundles.

With webpack

In webpack, you don’t define your bundles anymore - rather, you define an entry point and that’s it. It uses the static structure of ES2015 module syntax to automatically determine the contents of the bundle.

Consequently, the “aurelia way” of referencing modules are not recognized as imports and the files are never included.

As described in the webpack plugin docs there is an includeAll option you can pass the aurelia plugin (new AureliaPlugin({ includeAll: "src" })) which forces all files under src to be included in the bundle and the “old” way will then work again, but this is typically not desirable for production apps.

Enter PLATFORM.moduleName

You can think of PLATFORM.moduleName as aurelia’s version of import.

PLATFORM.moduleName in itself doesn’t do anything interesting in terms of ensuring webpack imports the module.
However aurelia-webpack-plugin uses it to determine what files to include in the bundle, in much the same way webpack itself uses import statements for that.

In simple terms, the plugin goes through the raw javascript of each project file and looks for “PLATFORM.moduleName” as a magic string of sorts. It extracts the string from the call and gives that back to webpack in the form of a dependency. (additionally, as @jods4 pointed out, it disables optimizations which are not compatible with aurelia-loader)

As you might expect from a static analysis tool, this then won’t work for any programmatically created strings. Which is a bit unfortunate for those (including me) who used to programmatically create some of these configurations to keep things clean, but makes perfect sense when you realize that this never worked for import either.

And finally…

It’s worth noting that once a module has been included via PLATFORM.moduleName, it doesn’t need to be included again. This allows you, for example, to do something like this as a replacement for what used to be bundle configurations:

src/main.ts:

// ensure the includes files is resolvable via the entry point
import "./includes"; 

src/includes.ts

import { PLATFORM } from "aurelia-pal";

// declare all pages we need
PLATFORM.moduleName("pages/page1");
PLATFORM.moduleName("pages/page2");
PLATFORM.moduleName("components/component1");
PLATFORM.moduleName("components/component2");

Now, wherever you need the modules declared above, you don’t need to use PLATFORM.moduleName anymore and you can even programmatically generate those route configs and such. They are already included in the bundle via includes.ts and don’t need to be included again.

Whether this is a nice solution or not depends on the situation, but it’s another tool in the toolbox which may come in handy.

As @jods4 pointed out, the modules listed above will then be a dependency of includes.ts and this will affect code splitting. Arguably it’s simply a slightly-less-harmful variant of the includeAll option, but still not something you’d want to do in a large production app where you want every optimization.

4 Likes

@fkleuver, man… I can only hope for it to be commited here

1 Like

This is a good summary!

Two points I feel are worth adding:

  1. aurelia-webpack-plugin does a little more than replacing an import. It also disables a bunch of optimizations on the target module, such as tree shaking or scope hoisting.
    The reason being that those optimizations are not compatible with aurelia-loader.
    This is the reason why using includeAll is great to migrate your existing projects, do small demos or PoCs but it’s not recommended. When using includeAll you loose optimizations on all your files, even if they are loaded through import and you won’t be able to use code splitting.

  2. The include.ts script has some drawbacks. Because the dependency graph is not correct (everything is a dependency of include.ts), you won’t be able to use code splitting either.
    A better alternative if you go this way is to use ModuleDependenciesPlugin in your webpack config: syntax is better (no PLATFORM.moduleName); you can create Aurelia dependencies for any file in your dependency graph; and because webpack config is just JS, you can create those dependencies dynamically if you want to (programmatically created strings as you said).

2 Likes

Thanks for pointing that out, I included some notes of your points as to not put future readers on the wrong track with includes.ts :slight_smile:

There is another issue with includeAll and css. This parameter imports all it can find from a dummy empty-entry.js module and if you don’t have a specific rule to use css-loader for css imported from empty-entry.js your build will fail.

1 Like

a PR is very velcome (-: https://github.com/aurelia-contrib/aurelia-getting-started/blob/master/knowledge-base/webpack-tree-shaking-and-platform-modulename.md