Webpack

[Flight Assist Off] Part 2 – Building Angular Without ng build

Welcome back to Flight Assist Off, the series in which I attempt to cover everything you need to know to run Angular without Angular CLI. If you missed the first part of this series explaining why anyone would ever want to do that, please check it out.

Additionally, in order to provide a more in-depth example of some of these processes, I will be referencing a seed project I have built using all the techniques I intend to cover in this series. You can try them out by cloning, or just reference the relevant files I will mention on its github page here: https://github.com/swimmadude66/AngularPWASeed.

Lastly, I want to add a disclaimer to this post. It is entirely possible I am missing something which could make everything work better, or faster, or easier. I am presenting the methods I use in my projects right now, and those methods change as I learn newer and better techniques. This is a slow process sometimes, which kind of accentuates my original point of the series. I cannot be certain I am building the framework optimally, because the documentation assumes I am using the CLI. Until that documentation is updated, or someone WAY smarter than myself can reverse engineer the CLI’s exact build process, this is my best guess at duplicating the results.

The Easy Way

When using Angular CLI, your entire build process is one command: ng build. Obviously, its tough to beat the ease of this process. One command, eight keystrokes, and suddenly your source code begins its transformation in to a run-able application. It feels like magic and it may as well be. All your SCSS files are transformed to renderable CSS, your code is bundled in to small js files that can be loaded more easily than one large one, and your index.html is updated with references to your new bundles. It begs the question, “How did the CLI do that?”

Webpack

Under the hood, Angular CLI uses one of the most popular build tools available right now: Webpack. I will not get in to webpack basics in this post, or else I will never have time to cover anything else. True story, the first draft of this post had to be scrapped because I spent 1500+ words explaining webpack in general instead of how to use it for Angular. Instead, please allow me to simply point the uninitiated to the webpack docs, as well as this fantastic introduction article. That should give you the groundwork to understand the specific topics covered below, and it means I can keep this post under an hour’s read.

INFINITE POWER

Webpack is nearly infinitely powerful, because it is nearly infinitely configurable. The configuration object webpack uses is un-opinionated in almost every way, and the loaders and plugins used inside can be configured independently. This combination means that with different configurations, you could use the same tool to build almost any project, whether its a web app, or static site, or server-side library. Alternatively, you could use a plethora of benchmarking and monitoring tools to create a configuration file which is absolutely optimal for your chosen tech stack and reuse it on all your projects with very few modifications. Angular CLI has chosen the latter approach, and unless your tech stack includes something they didn’t expect, your goal is going to be primarily to duplicate that functionality.

Let’s discuss what functionality we want to duplicate.

  1. Compile Angular code in to bundles
  2. Compile/Copy/Clean styles and templates
  3. Create an index.html file which references all these generated styles and bundle

No matter what languages or frameworks you are using for your Angular application, these steps should still apply. At the end of the build process, we need to have some dist folder with an index.html file referencing all the styles, bundles, and assets we need.

Where to Begin?

To get started, let’s define some entry points. These files serve as the roots of large graphs of code dependencies. Essentially, this means your entry points need to be files which are not referenced at all by each other, or by any file referenced by the other entrypoints, or files referenced by THOSE files, etc. This sounds more complicated than it is. If you are avoiding circular dependencies in your app, then your main file (the one which bootstraps your application) should be a safe root for ALL your application’s project files. It references your app module, which references your services, components, and other modules via routes, so that should be your app bundle.

The next logical choice is a polyfills bundle. Angular suggests including a file of polyfills for IE and Safari browsers which need them, which should just contain several import statements for things like core-js. Since it is used implicitly by your app code, nothing directly imports polyfills, and it can be a safe entry point.

After those, things get a little more personal. Some projects use web workers (not to be confused with service workers, more on those in Part 4) which need to be built as separate bundles, so they make candidates for additional entrypoints. Personally, I like to include a global ​​styles.css file in my projects, so I use a specific styles.scssfile as an entrypoint which references things like my layout framework, fonts, and any imported styles from ​​node_modules. There’s no right or wrong set of entrypoints, so long as their graphs don’t overlap and you cover all the code you want to include in your build.

Compile the Angular Code

Let’s start with the simplest step first. The Angular team has published a webpack loader called @ngtools/webpack which is used by the CLI, and can compile Angular code Ahead Of Time (AOT). It handles EVERYTHING related to Angular-specific code; it transpiles the typescript, handles lazy-loaded modules, and even replaces templateUrl and styleUrls in components to  webpack-friendly require statements so that we can process them with loaders later on.

The @ngtools/webpack loader uses a plugin as well, which accepts configuration options used by the compiler. It has quite a few useful properties, allowing it to work with almost any folder structure and set of compiler options. You can read about them, but I’ve had success with simply pointing it to a tsconfig.app.json file and the mainPath for my app. Side note: the tsconfig.app.json is nothing special, it simply extends the tsconfig.json by adding an exclusion for test files, so that app builds can skip those files. 

Styles and Templates

Assuming at least one of your components uses templateUrl or styleUrls in lieu of inline templates and styles, webpack will need to know how to handle those files. In the simplest case of using html for templates and css for styles, your rules just need to define loaders which will read the content and inject them as a string. A rule which matches files ending in .html could use the html-loader and another rule to match .css files could use the css-loader followed by the to-string-loader to inject the raw strings from the files in to your component.

The amazing thing about webpack loaders is that they chain, though. The CSS rule processed the input CSS via css-loader, but then passed that output to to-string-loader before it was done. By extending this chain, you can use virtually any technology for your styles/templates. So long as the output of one loader is compatible with the input of the next, your end result will be the same. You could use another templating language like haml or ​ejs by just adding the relevant loader before the ​html-loader.  I prefer SCSS for styling, so I use the sass-loader to compile my source files to CSS. However, before I pass that CSS to the css-loader, I also run it through postcss-loader which runs autoprefixer to add all the necessary moz, ms, and webkit prefixes to styles which need them and then runs cssnano to minify it. By then end of this chain, the output is still a raw string of CSS to be injected in to the component, so nothing in the app has to change!

For global styles file I have a similar rule. It loads in a styles.scss file and compiles it, runs postcss-loader on it and passes it to css-loader, just like the component styles. However, instead of passing it to to-string-loader for injection, it gets passed to another loader from the mini-css-extract-plugin, which outputs the results as a standalone file.

index.html

At this point, you should have a webpack configuration which is capable of building your angular application in to workable bundles of js (and a styles.css file if you also like your global styles). However, you have no way to reliably load those bundles! You need an index.html file which references all the bundles you just built, in a way that will allow Angular to run. One approach is to write your index.html to expect certain named files. By naming your bundles predictably, you can just copy that html file to your dist folder after a build, and everything will work. However, in a production environment with caching layers, it may be beneficial to include a hash or build-id in your bundle file names. By doing so, you now would have to update your index.html to use those new hashed filenames.

Enter HtmlWebpackPlugin, possibly the greatest of the webpack plugins, which does exactly that. It is able to read an html file from your source code and add in references to all the files output by webpack. It’s even got its own plugin system for extending that functionality. For instance, if I want to add a nomodule attribute to my polyfill bundles to prevent it from being loaded by modern browsers, I can use the (shamelessly self-promoted) webpack-nomodule-plugin.  Once you’ve configured your input file and output location along with any additional plugin options you need, your build will produce a valid html file referencing all your build output.

Optimize

With that, your webpack configuration is technically complete. Webpack has a few more tricks it can do, though. The mode property can apply different optimizations depending on whether you are running a production or development build. These include adding a variety types of sourcemaps in development, or minfy-ing all js bundles in production. Unlike with Angular, this shortcut is well-documented and configurable. You could even pass a mode of none to prevent any and all implicit behavior from webpack. The reference project does this in its webpack.config.js file, but as of today the optimizations implemented are exactly identical to those passed by production mode.

Conclusion

As I mentioned in the introduction, this may not be the best way to build. It is however a working way to build, with full visibility and control over how the pieces of your app are assembled. If I missed something obvious, or if you know of some improvement that can be added, please leave a comment and let me know. Thank you for reading, and happy building!

Keep Reading

  1. Part 1 – Series Intro and Problem Statement
  2. Part 2 – How to Build an Angular App Without ng build
  3. Part 3 – Testing an Angular App Without ng test
  4. Part 4 – Cool Shit You Can Do Without Angular-cli
  5. Part 5 – How We Made Angular Fix Their Docs! (Hopefully)