Vue, Webpack, Babel and EsLint in 2019

It’s definitely time to review my base configuration I use for creating a new Vue project. I wrote a git repo here: https://github.com/xerocross/vue-project-template. I wrote it quite a while back, and looking back now I see there was some thing I misunderstood. Also, it just needs to be brought up-to-date with the libraries I use.

Typically, I use a setup like this.

I write my Vue components using .vue files, which relies on a package called vue-loader to appropriately translate these files into scripts and styles and templates.

I don’t exactly try to use every latest new feature of the evolving JavaScript language, but some new features are helpful enough that they got my attention right away, like the let keyword. Since nobody really trusts browsers to actually implement these new features yet, we use packages like Babel. Babel is a transpiler. It reads modern JavaScript and transpiles the code into a target version of JavaScript like ECMAScript 2015 (ES6). That is a common transpilation target because at this point most browsers have implemented ES6.

Webpack is a script bundler. Webpack helps if you want to write your JavaScript in small files representing single modules and tie them together with import and export statements.

ESlint helps us write pretty code with a uniform syntax, and it has to be wired together with other things in our package so it doesn’t throw errors incorrectly.

The landscape as changed a little since I first learned these tools. Babel for one got a lot more complicated to use, all in the name of progress. So basically I’m going to rebuild my vue-project-template project again from scratch and I’ll try to use modern packages and wire things up correctly.

I like to use yarn as my package manager, but you can use npm commands as you prefer with minor changes. Let’s start by adding some dev dependencies I know I will need, in no particular order.

yarn add webpack -D
yarn add eslint -D
yarn add vue

As for Babel, I seem to recall that the basic way you install Babel and wire it up with your project has changed, so let’s investigate that and make sure we’re not using deprecated packages. From the docs at https://babeljs.io/en/setup/#installation we see that we can install it like this: npm install --save-dev babel-loader @babel/core. Using yarn, that becomes yarn add babel-loader @babel/core -D. So far this installs without incident.

I already had a webpack config file, but I’m going to strip it down to some bare essentials I know I will need in place. I will put the confg in a file called webpack.config.js in the base directory, with something like this to start.

const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const webpack = require('webpack')

module.exports = [{
    entry : {
        'index' : './src/index.js'
    },
    output : {
        path : path.resolve(__dirname, './dist'),
        publicPath : '/dist',
        filename : '[name].js',
    },
    externals : {
        vue : "Vue" 
        // remove this line if you want to 
        // bundle vue with the script
    },
    module : {
        rules : [
            {
                test : /\.vue$/,
                exclude : /node_modules/,
                loader : 'vue-loader'
            },
            {
                test : /\.js$/,
                exclude : /node_modules/,
                use : {
                    loader : 'babel-loader'
                }
            },
            {
                test : /\.scss$/,
                use : [
                    {
                        loader : 'style-loader' 
                        // creates style nodes 
                        // from JS strings
                    },
                    {
                        loader : 'css-loader' 
                        // translates CSS into CommonJS
                    },
                    {
                        loader : 'sass-loader' 
                        // compiles Sass to CSS
                    }
                ]
            }
        ]
    },
    optimization : {
        minimize : true
    },
    plugins : [
        new webpack.DefinePlugin({
            "env" : JSON.stringify({
                "NODE_ENV" : process.env.NODE_ENV
            })
        }),
        new VueLoaderPlugin()
    ]
}]

Now I need to install several packages that are referenced by the webpack config.

yarn add vue-loader style-loader css-loader sass-loader -D

These install without incident. Webpack passes files through these loaders according to the rules and the order given in the config file.

To help with development and testing, I also want the webpack development server, so yarn add webpack-dev-server -D.

The dev server uses this bit of configuration, which you can put in the webpack config file.

devServer : {
    contentBase : "./public",
    compress : true,
    port : 9000,
    watchContentBase : true
}

We might explore all the config stuff in more detail another time. For now let’s just say that the public directory is where I put static files like images. The dist directory is where I put files that result from my build process.

Our index.js file contains the main boostrapping of the app using Vue, which looks like this.

import VueProjectTemplate from "./components/vue-project-template.vue";
import Vue from "vue";

new Vue({
    el : "#vue-project-template",
    components : {
        VueProjectTemplate
    },
    render : function (createElement) {
        return createElement(VueProjectTemplate);
    }
});

It is necessary to import Vue so that below in the “new Vue” line Vue is defined, but we have also configured Webpack to consider Vue an external. The Vue engine itself will not be bundled into our files. Instead, out bundled script will expect Vue to be defined in the global scope.

We included this in the config file.

externals : {
  vue : "Vue" 
},

It says that if one of our packages imports “vue”, then what it gets will be whatever the identifier “Vue” resolves to in scope. Therefore, to demo our little template, inside the index.html file we need to link to a Vue script—either one saved locally or a CDN. This script tag needs to go before the script tag for our main index.js file.

Now lets try start the dev server. By the way, I have this script defined in my package.json file: "local" : "cross-env NODE_ENV=local webpack-dev-server --hot --inline". This allows us to just execute yarn local at the console to launch the dev server. Here cross-env is a convenience package that I forgot to add, so now let’s add that as a dev dependency.

Running the dev server tells us that we need to also install webpack-cli, so we do that. Bear in mind that anything related to webpack or babel is a development dependency in our case. The final build files will only have Vue as a dependency, and even that will not be included in the bundles.

It looks like we also need some other packages: vue-template-compiler and node-sass. Having installed those as dev dependencies, now finally the dev server runs and compiles successfully. If I browse to http://localhost:9000 then I see my widget.

That is a good start. As a sanity check on the dev server, I also add a tiny visible change to the template of my component, save the file, and see if it gets updated in my browser. It does, so we have verified that the dev server is functioning as expected.

I also include a script in package.json defined simply "build": "webpack". Let’s run yarn build and see if it builds successfully. Affirmative. It builds without incident. Now, the dev server is nice, but sometimes I want to build my files and then serve them almost as they would be served in a production environment. Running our build script builds the files, but we don’t have a server yet.

This is why I include an extremely simple node server in a file called app.js as follows.

const express = require('express')
const app = express()
const port = process.env.PORT || 3000;
app.use(express.static('public'))
app.use('/dist', express.static('dist'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

We need to install express for this. Again, this is still just a dev dependency. I add two more convenience scripts to package.json.

"dev": "cross-env NODE_ENV=development webpack && yarn start",
"start": "node app.js"

The start script just turns on the server. The dev script executes a webpack build and then turns on the server. It’s configured to use a different port from the dev server, so you could actually use them both at once if you want.

Let’s look at how the process environment (“local”, “development”, etc) gets passed into the scripts. Putting cross-env NODE_ENV=development in front of webpack sets the value of process.env[“NODE_ENV”] so that variable is available inside the webpack config script. That does not automatically make it available to your application. We include this code in the webpack config file for that reason.

new webpack.DefinePlugin({
    "env" : JSON.stringify({
        "NODE_ENV" : process.env.NODE_ENV
    })
})

This creates the global “env” variable that we can now use inside, say, our Vue components. We shouldn’t though. We should only access a global like that at the highest possible place, and here that means the bootstrap file index.js. You could pass this data into a component, but more likely you would do some high-level switching, like setting endpoints for logging based on the value of NODE_ENV. For my template example, however, I decided to actually pass the value into my top-level component as prop. The component simply displays the environment so you can see that things work as expected when running local or dev.

Next let’s start setting up ESLint. I have eslint installed locally, and I have an eslint config filed called .eslintrc.js, which is standard. I needed to install eslint-plugin-vue and babel-eslint. My config file is below. I overrode many of the standard rules. With this in place, ESLint now appears to be working with my VS Code instance. When I look over my source files, I see spacing problems and such. I will correct those now.

module.exports = {
    "parserOptions": {
        "parser": "babel-eslint"
    },
    "env": {
        browser: true,
        commonjs: true,
        node: true
    },
    "extends": [
        "eslint:recommended",
        "plugin:vue/recommended"
    ],
    rules: {
        "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0,
        "indent": ["error", 4],
        "vue/html-indent": ["error", 4, {
            "attribute": 1,
            "closeBracket": 0,
            "alignAttributesVertically": true,
            "ignores": []
        }],
        "key-spacing": ["error", {
            "beforeColon": true,
            "afterColon": true,
            "mode": "strict"
        }],
        "vue/html-self-closing" : [0]
    },
    globals: {
        "env" : false
    }
};

Now let’s configure babel. I don’t even know what the language target is right now because it’s doing something but so far I haven’t specified the target. So let’s dive into that. I warn you: this used to be simple, but the people at Babel have since made it annoyingly complicated—or “smart” to use their words. For example, they suggest using a preset called @babel/preset-env. It seems like they want me to just trust that @babel/preset-env works, somehow, without actually telling us what it does. Previously, I know I could tell it to compile to ES6. Now, evidently, Babel wants to make that decision for me. That doesn’t appeal to me.

So, can I use @babel/preset-env to transpile into ES2015? If I try to ask someone on StackOverflow how to do that, would they answer? Or would they instead tell me that what I want to do is wrong followed by a lengthy explanation that doesn’t answer my question.

Here’s the top StackOverflow search result on this question, and it does exactly what I just said above: https://stackoverflow.com/questions/34747693/how-do-i-get-babel-6-to-compile-to-es5-javascript. That is: it doesn’t answer the question at all. It contains a lengthy and opinionated non-answer about what the OP should have been doing instead.

Here’s something in the official docs where they explain that what I want to do is wrong and their way is better: https://babeljs.io/docs/en/env. As of now, I don’t see any way to use the current version of Babel and still specify the actual ECMAScript target you want. Instead, I have to specify some shifting-sand nonsense like "targets": "> 0.25%, not dead". If what I want to do is not available anymore, for now I’ll use the defaults and maybe start researching alternatives to Babel. Another option would be to just use an older version of Babel that still supports directly choosing the target specification. I’m not ready to go that route yet.

Next and final thing I want is to set up my testing environment and test runner. I will yarn add jest @vue/test-utils -D. I also want to configure ESLint so it doesn’t complain about my Jest testing files. I will use eslint-plugin-jest for that. We add some Jest configuration to package.json like this.

"jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "vue"
    ],
    "transform": {
      ".*\\.(vue)$": "vue-jest",
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    }
  }

This configuration, along with the vue-jest and babel-jest packages, will let us import Vue components into our test files. For this to work, we also have to add this package to our dev dependencies. It’s some kind of bridge between the old babel-core and the new @babel/core. "babel-core": "7.0.0-bridge.0".

To see if all this is working together, I wrote a little test file. All it does is try to mount our little Vue component. If it mounts without incident, the test passes. We add a new script to package.json: "test": "jest". Now at the command like, “yarn test” runs jest and our little test executes and succeeds.

For good measure, I try building again. Everything appears to still be working as expected. I now consider this a good, up-to-date template on which I can build new Vue projects. I have pushed all of these changes to my repo, so you can see the product here: https://github.com/xerocross/vue-project-template.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s