Want to migrate the Aurelia 1 application toolkit to Webpack? This step-by-step tutorial will show you how to do it – using a custom Aurelia & Webpack solution based on several well-known plugins.
This article delves into the process of modernizing an Aurelia 1 application development toolkit built on top of the aurelia-skeleton-plugin (utilizing Gulp 3, JSPM, and SystemJS). The goal is to transition to a more contemporary Webpack-based setup, leveraging NPM for dependency management instead of the potentially unreliable JSPM.
Starting point
I begin modernization by using the au new command from the Aurelia CLI to generate a template project. This project already includes a webpack.config.js setup file with the aurelia-webpack-plugin supporting the Aurelia resource loading syntax.
Resource import
One of the fundamental differences between Gulp 3, JSPM and SystemJS skeleton and the Webpack skeleton is the approach to importing resources:
- Gulp build task: Automatic loading of all *.js and *.html Aurelia resources (custom elements, attributes, etc.). All resources matching the given path regex are loaded into the Gulp pipeline and processed according to its type.
- Webpack: Requires explicit imports for optimal performance. The aurelia-webpack-plugin helps Webpack understand Aurelia-specific imports through, for example, <require> or @useView().
Usage of PLATFORM.moduleName(“moduleA”)
One of the most important steps in migration is to wrap all string references to Aurelia modules (resources) with the PLATFORM.moduleName(“moduleA”) expression. This allows aurelia-webpack-plugin to identify and include these modules as Webpack dependencies. These modules can then be dynamically loaded by the aurelia-loader at runtime (you can find more information about this here).
The problem with dynamic module names
The application I’m using is based on the dynamic generation of the routing tree, which involves using a function called getModulePath(moduleId). This function determines the module name based on a given ID.
The first obstacle is an attempt to pass the getModulePath(moduleId) method call as a PLATFORM.moduleName() argument. The result would be:
PLATFORM.moduleName(getModulePath(moduleId))
However, it is an incorrect approach as modules are not added as Webpack dependencies, which causes runtime errors. As official Aurelia documentation states:
“Whenever you reference a module by string, you need to use PLATFORM.moduleName(“moduleName”) to wrap the bare string”.
The keyword here is the “bare string”. What it really means is that method calls are not evaluated by the aurelia-webpack-plugin when it processes the PLATFORM.moduleName() occurrences. Valid PLATFORM.moduleName(moduleName) arguments are, for example:
- PLATFORM.moduleName(“moduleA”)
- PLATFORM.moduleName(“module” + “B”)
Overcoming challenges in large codebases
The project I am working on has a large codebase with hundreds of Aurelia resources. Unfortunately, PLATFORM.moduleName() only accepts bare-string module names. This means that dynamically generating module names, which would seem ideal for maintaining a large and evolving codebase, isn’t possible.
On the other hand, the prospect of manually writing hundreds of module names or relying on directory traversal tools is both time-consuming and hard to maintain in the future. So, let’s explore alternative solutions.
Leveraging the includeAll option (with a caveat)
The aurelia-webpack-plugin comes with the includeAll configuration option. To quote from the source:
“includeAll: “src” – A shortcut that says “just grab everything in the src folder and include it in the bundle, bypassing the instrumentation requirement”. It’s a quick, easy way to migrate existing applications.”
This seems like the perfect solution, and indeed, it works flawlessly! All Aurelia resources are successfully added as Webpack dependencies. However, this approach introduces a new challenge: the import order of LESS style resources.
The includeAll option is designed to automatically include all files within a specified directory, including stylesheets like LESS or SASS files. While this simplifies the configuration process, it can lead to issues with the order in which styles are imported.
In this particular case, the project has external style dependencies and internal common styles that need to be imported before the specific styles from Aurelia resource directories. However, there’s no straightforward way to configure Webpack to prioritize these imports.
GlobDependenciesPlugin to the rescue
This issue made me dive into the aurelia-webpack-plugin code in search of a way to configure a path regex for importing Aurelia resources with the includeAll option. Eventually, my research led me to discover the GlobDependenciesPlugin. To quote from the source:
“This module is similar to the ModuleDependenciesPlugin above, but instead of adding specific dependencies to a module, it adds every file that matches one (or several) Globs. Note that globs are evaluated relative to your Webpack config location”.
This is what AureliaPlugin uses to implement its includeAll option (as seen in the documentation) “.
Final solution
I ultimately opt for a combined approach: using the default AureliaPlugin configuration alongside a custom GlobDependenciesPlugin. This plugin, configured with the import path regex src/**/*.{ts, js}, ensures that only TypeScript and JavaScript files from the src directory are included by the aurelia-webpack-plugin. This selective inclusion prevents unnecessary files from being bundled.
Here’s the code I use for my Webpack configuration (the webpack.config.js file):
module.exports = { // ... other webpack configuration options Plugins: [ // ... other plugins new AureliaPlugin(), new GlobDependenciesPlugin({ ‘aurelia-webpack-plugin/runtime/empty-entry’: ‘src/**/*.{ts, js}’, }), // ... other plugins ], // ... other webpack configuration options }
This configuration enables me to import all necessary LESS files in the desired order within a dedicated index.less file, which serves as the primary entry point for the application’s LESS styles and is explicitly imported into the main JavaScript or TypeScript file.
Conclusion
And that’s about it. I hope you liked the custom solution I came up with – may it serve you well in your own projects and challenges. If you like this sort of informative guides, check out some of the other articles on our company blog written by my colleagues from the Pretius team:
- PWA push notifications: How to do it using Firebase Cloud Messaging?
- Keycloak-Angular integration: Practical tutorial for connecting your app to a powerful IAM system
- GitHub Copilot tutorial: We’ve tested it with Java and here’s how you can do it too
- Angular 19 features: An in-depth analysis
- Angular SSR: Your server-side rendering implementation guide