Vue-CLI and Single File Webpack Bundles

As I spend more and more time in the Vue.js ecosystem, I’m developing an a much greater appreciation for the time the core team has spent developing tools that make managing the project configuration.

That said, a big part of managing a Vue.js project still requires a lot of Webpack knowledge… More than I have, unfortunately.

Today I was working on a sort of micro-application that would be embedded on various web pages that I really have no control over. More specifically, it’s a web app that enables consent management for the CCPA privacy regulations, and it handles conditionally popping open a small UI for California visitors and then providing the opt-out signals to upstream code like prebid.js.

This app needs to load on all sorts of third-party web pages, and ideally the functionality would be enabled by simply including a JavaScript in the header of the page before the rest of the site’s advertising technology stack would load. This means the Vue.js app doesn’t really own the page, and it really needed to load as a cleanly as possible in a single JavaScript file.

This took a little more wrangling that I’d expected, and much like my deeper dives into doing things like adding Snap.svg to Vue and Nuxt projects, it required a bit of a deeper dive into Webpack and the configuration chaining behavior of the newer vue-cli generated apps.

Injecting your own mount DIV

One of the things your Vue.js project always needs is a place to mount itself on the web page. Generally, your project will have some sort of HTML template that hosts it, and often your whole site may be the actual Vue.js app and that simple HTML file is all you need to deploy.

More often than not, however, I’m loading Vue.js apps onto existing pages, and that involves just adding a <div> element somewhere with the right ID in an existing layout.

For this project, I had zero control over the host pages so I had to dynamically inject a mount point. To do this, I had to modify the main.js file in my Vue project to do something like this…

import Vue from 'vue'
import App from './App.vue'

let gMyApp = undefined;

document.addEventListener("DOMContentLoaded", () => {
  
    // Inject a private mount point div to display UI...

    let body = document.getElementsByTagName('body')[0];
    body.innerHTML = body.innerHTML + "<div id=myApp_mount />";
    
    gMyApp = new Vue( { render: h => h(App), } ).$mount('#myApp_mount')

} );

Inspecting the generated webpack configuration redux

In my previous article, I talked about inspecting the vue-cli generated Webpack configuration by using ‘vue inspect’ to eject a copy of the configuration, and then modifying it using webpack-chain in the vue.config.js file.

One detail that I missed in this earlier exploration was the need to eject a production configuration. To do this, you use the following command and your shell…

vue inspect --mode production > ejected.js

Without the mode command line variable, what you’re seeing is your development configuration, which can run a whole different chain of loaders and perform other Webpack mischief you’re not expecting.

vendors.js must go!

But back to my original problem of getting a single bundle as output from the project. By default, the Vue.js CLI will kick out separate bundles for your code, vendor libraries and any extracted CSS. Trying to hand this off to a third party and explaining they needed to include a dog’s breakfast of separate files on all of their site pages was a deal breaker, so figuring out how to merge all of these files in the production build was important.

The easy first step was to just stop Webpack from splitting the JavaScript into separate chunks. That’s actually Webpack’s default behavior, so all we need to do is get rid of some of the configuration passed into the split chunks plugin by vue-cli’s defaults. Using chain-webpack in vue.config.js, that looks something like this…

module.exports = {

chainWebpack: config=> {

   // Disable splitChunks plugin, all the code goes into one bundle.

   config.optimization
     .splitChunks( ).clear();

} }

Combining CSS and JavaScript in one Webpack bundle

That got the project down to a single emitted JavaScript file and a single CSS bundle. And that’s where things got interesting.

To get the CSS to reside with the JavaScript code, it needed to be packaged and injected. That mean undoing the extraction of the CSS into its separate file and then separately using style-loader to sneak it into the DOM when the JavaScript was executed on the page.

The first step was disabling the existing extraction process handled by the extract-css-loader. Again, inside vue.config.js in the chainWebpack:config function…

config.module.rule('css').oneOf('vue').uses.delete('extract-css-loader');
Running the build with that command in place got rid of the separate CSS in the dist directory. But, it didn’t load on the page. The bundle size actually went up, so it was making it into the bundle, but we needed one more step to actually do the DOM injection. Inspecting the config again made it clear nobody was picking up the output from the chain of CSS loaders, and what we really needed was to add style-loader to the end of the list to get it to do that last step for us…
config.module
   .rule('css')
      .oneOf('vue')
         .use('style-loader')
            .before('css-loader')
            .loader('style-loader')
            .end();

With that in place, the project kicked out a single .js file that had everything in it… vendor and project code, plus the css. And, the css actually got injected when the script ran. Perfect!

Bonus Episode! Renaming the Webpack Output File

As a final bit of convenience, I modified the webpack generated filename to use a date format necessary for this project. While normally the cache-busting hashes are super useful for making sure everything deploys in sync, in this case we wanted the filename to reflect the build date.
// Get the filename into the format we use for different builds.

let today = newDate();
let mm = today.getMonth() +1; // getMonth() is zero-based
let dd = today.getDate();
let yy = today.getFullYear().toString().substr(-2);

let dateStr = [ yy, (mm>9?'':'0') + mm, (dd>9?'':'0') + dd ].join('');

config.output.filename( 'myapp-'+ dateStr +'.js' )

Perhaps a little wordy, but it gets the job done.

Putting it all together in one webpack chain configuration

Putting all the parts together…
module.exports = {

chainWebpack: config=> {

   // Get the filename into the format we use for different builds.

   let today = newDate();
   let mm = today.getMonth() +1; // getMonth() is zero-based
   let dd = today.getDate();
   let yy = today.getFullYear().toString().substr(-2);

   let dateStr = [ yy, (mm>9?'':'0') + mm, (dd>9?'':'0') + dd ].join('');

   config.output.filename( 'myapp-'+ dateStr +'.js' )


   // Disable splitChunks plugin, all the code goes into one bundle.

   config.optimization.splitChunks( ).clear();


   // Disable the CSS extraction into a separate file.

   config.module.rule('css').oneOf('vue').uses.delete('extract-css-loader');

   // Take the CSS from the bundle and inject it in the DOM when
   // the page loads...

   config.module
      .rule('css')
      .oneOf('vue')
         .use('style-loader')
            .before('css-loader')
            .loader('style-loader')
            .end();
} }


One further caveat with this solution… If you’re using various CSS preprocessors, you’ll see when you inspect the Webpack configuration various loader chains for each of the individual CSS preprocessors (less, sass, scss, stylus) and you’ll need similar sets of rules to delete extract-css-loader and insert style-loader for each of those scenarios. For my project, I only needed to update the loader chains in…
config.module.rule('css').oneOf('vue')...
…but looking at the configurations there are a variety of other loader chains for different module types and for the different CSS preprocessors, so depending on what your project uses you made need to repeat deleting the extract loader and inserting the style-loader for those using additional chain rules.

Adding Snap.svg to Vue.js Projects Created with vue-cli 3.0

My front end tool chain has finally settled into something I’m getting comfortable with. But these tools don’t stand still, so the learning continues.

One of the higher traffic posts on this blog has been my discussion of how I integrated Snap.svg into my Vue.js and Nuxt.js projects. I wrote that article because while it wasn’t fun, it helped me learn a lot about Webpack configuration.

That article was written at a point in time when where I was maintaining standalone Webpack configuration files for my projects. A big part of the trouble with this approach is that it intrinsically couples a large number of external dependencies together. My Vue.js projects rely on a number of third party libraries, Webpack uses a collection of loaders and the build tool chain itself has a number of dependencies like ESLint and Babel that bring along their own school of remora fish.

So, you can understand why once you’ve got a fairly complicated project and build process configured in Webpack, that the last thing you typically want to do is touch it. It’s even worse when you introduce dependencies like Snap.svg that, because of their lack of module support, require mystical Webpack incantations to get them to work.

I just had to extract this slope intercept calculator from a project that was generated without the CLI, so I was using the older Webpack mysticism to get Snap.svg working. With the need to create this in a new project, startig with the latest Vue.js and the vue-cli was the natural approach… But that meant getting our friend Snap.svg working again. Read on to find out how…

How The vue-cli reduces webpack Configuration hell

Let’s be honest, not many people like working on Webpack configurations.

One of the things the vue-cli sets out to do is to hide away some of the complexity associated with Webpack configurations. This makes building compiled Vue.js projects a bit less intimidating, but it also allows the whole tool chain to be upgraded without breaking what would have been a hand-coded Webpack configuration.

There’s still a Webpack configuration hiding under the hood of course, it’s just that the CLI manages it. And if you upgrade Vue.js and it’s tool chain, in theory it becomes a seamless process because the configuration is “owned” by the CLI and it’s not a visible part of your project that you’ve been tempted to monkey around with internally.

This management of the Webpack configuration by vue-cli is in contrast to many other tools that “eject” a configuration when the project is first setup, or when you might need to make an unsupported modification. At that point, the Webpack configuration file is owned by the developer (lucky you!) and a future upgrade becomes something to potentially be feared.

Fortunately, vue-cli 3.0 introduced a new way to make modifications to the generated Webpack configuration without having to eject a configuration and be responsible for maintaining it forever forward.

Webpack chain: how to modify a vue-cli webpack configuration without ejecting

The magic comes from webpack-chain, a package that came out of the Neutrino.js project. It’s bundled into the vue-cli and the implementation details are a slightly different, but the documentation at the link above is useful for digging into individual options. There’s some examples specific to vue-cli at this link although they fell short of the complete reference at the actual webpack-chain documentation.

Webpack-chain allows you to create little configuration patches that get applied programmatically to the Webpack configuration that is built by a vue-cli project each time you compile. The advantage of this is that you only need to specify the eccentric bits of your project’s build configuration, and if you subsequently upgrade to Webpack 7 or Vue.js 5 all of the stock configuration provided by the rest of the toolchain should hopefully get upgraded appropriately.

An example of this is getting CSS preprocessors like Sass or Less running. Normally this would take some Webpack gymnastics, but the vue-cli does this for you when you configure a project and you never have to touch the actual Webpack configuration. As vue-cli gets updated to use later versions of Webpack or whatever CSS preprocessor you’re using, these upgrades will come along for free in your project without having to go back and touch the Webpack configuration. Woohoo!

Where you do need to go outside the box, you can do it by setting up a “chain” in separate configuration file. You may still have update your custom configuration bits in your chain on undertaking a major version upgrade, but those changes are scoped and isolated, and they’re definitely more succinct. I much prefer this compared to picking through 1000 lines of Webpack boilerplate and finding where I’ve done something outside the box.

The slightly inconvenient part of this is that the syntax of webpack-chain is slightly different from the JSON object structure you may be more familiar with if you’re a Webpack old timer.

As an example, let’s look at our friend Snap.svg.

Getting Snap.svg Working with VUE-CLI

As in manual configurations, we’ll use the imports-loader to load Snap.svg in our project. So once we’ve created a new project with vue-cli, we’ll need to install Snap.svg and imports-loader as appropriate dependencies…

npm install --save snapsvg
npm install --savedev imports-loader

We don’t need to explicitly install webpack-chain as part of our project as vue-cli already bundles this into itself.

To create a chain that gets applied to the vue-cli generated Webpack configuration, you need to create a file named vue.config.js in your project’s root folder. Here’s the entire file I’m using to pull Snap.svg into my project…

module.exports = {
  chainWebpack: config => {
    config.module
      .rule("Snap")
        .test(require.resolve("snapsvg/dist/snap.svg.js"))
        .use("imports-loader")
        .loader("imports-loader?this=>window,fix=>module.exports=0");

    config.resolve.alias.set("snapsvg", "snapsvg/dist/snap.svg.js");
  }
};

That will handle getting Snap.svg resolved and imported when we need to. Again, the webpack-chain documentation illuminate how this is mapped back to traditional JSON Webpack configuration structures, but you can see readily how you might setup other behaviors like plugins or other loaders.

All that’s left is to reference it in the source to one of our components, something in very broad strokes like this…

<template>
  <div >
    <svg id="mySVG"  />
  </div>
</template>
<script>

import Snap from "snapsvg";  // <-- triggers our import resolution

export default {

  data() {
    return { mySVG: undefined };
  },

  mounted() { 
    this.mySVG = window.Snap("#mySVG");
  },

  methods: { 
     // draw amazing stuff here with this.mySVG
  }
}

</script>

Troubleshooting webpack-chain

The vue-cli includes commands to dump a copy of the Webpack configuration with your webpack-chain modifications applied to it.

vue inspect > ejected.js

You can open this file to look and see how the chain was applied to generate corresponding Webpack rules. In our case, this is what we got for the vue.config.js file described above…

  resolve: {
    alias: {
      '@': '/Volumes/Hactar/Users/jscheller/vue/myproject/src',
      vue$: 'vue/dist/vue.runtime.esm.js',
      snapsvg: 'snapsvg/dist/snap.svg.js'
    },
   
    [ ... and about 1000 lines later... ]
 
      /* config.module.rule('Snap') */
      {
        test: '/Volumes/Hactar/Users/jscheller/vue/myproject/node_modules/snapsvg/dist/snap.svg.js',
        use: [
          /* config.module.rule('Snap').use('imports-loader') */
          {
            loader: 'imports-loader?this=>window,fix=>module.exports=0'
          }
        ]
      }

A great thing about running the ‘vue inspect’ command is that it will give you console output if your chain rules have a problem. If you build up a webpack-chain in your vue.config.js file that doesn’t seem to be working right, this is probably one of the first things you should check to see if it’s actually getting applied correctly.