James Thorpe

Vue 3, TypeScript and webpack

Feb 15, 2022 typescript vue webpack

Installing Vue.js 3, TypeScript and webpack from scratch

One of my hobby projects has me wanting to use Vue.js. I started off using a CDN version of Vue along with a dynamic loader for loading individual .vue files, but as the project was growing, I was finding it was becoming cumbersome with long load times as individual files were pulled down, plus I also wanted to also introduce TypeScript, so it was time to bring in a compile/bundle step for this part of the project.

There are various options for creating Vue.js applications, from using a CLI tool provided by the Vue project, up to and including just using a Visual Studio template to do so, but I didn't find any good information on what's actually going on under the hood when you use these options. I like to know what's going on in my build systems, and as I wanted to introduce it to an existing project, rather than start one from scratch (with the potential to possibly need to do this mutiple times), I set off to install the individual components one at a time. It's also my first time using npm in a while, so I've documented what I did to get up and running.

Prerequisite - Node.js

When I first started looking at this, as above I was using a Visual Studio template, which builds in support for https certificates for local dev and the like too. I haven't got that far in what's described below yet, but one issue I hit with the above was that I'd used the latest version of Node.js, which at the time of writing was 17.x, and it didn't seem to play well with this stack. As such, I downgraded to Node.js 16.x, which is the current LTS version. Whether or not the https issues will be addressed in 17, I don't know - I haven't looked into it as yet. Bottom line is that all of the below is written with Node.js 16 in mind.

webpack

The first thing I wanted to get up and running was the core bundler. There are several. I picked webpack. Before we install it, we need to create our initial package.json. You can either use npm init and run through the prompts to create it, or you can use a fairly minimal one:

{
    "name": "vuetest",
    "private": true
}

"private": true prevents accidentally publishing something to npm you didn't mean to.

Once that's in place, you can then install webpack and the cli:

npm install --save-dev webpack webpack-cli

Now's also a good time to create a .gitignore file for the downloaded node modules, plus a dist/ folder which is where we'll be building the output to.

node_modules/
dist/

We also create a minimal webpack.config.js, which controls the options for webpack when it runs.

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'development',
};

You can now run webpack from the command line, or we can add a small script to package.json:

"scripts": {
    "build": "webpack --config webpack.config.js"
}

We can now run a build using npm run build, but before we do so, let's create some test files:

/src/test.js:

export function logtest(msg) {
    console.log("Hello from test: " + msg);
}

/src/index.js:

import { logtest } from './test'

logtest("Hello World");

If you now run npm run build, a main.js file should be created in /dist. In order to make use of it, create a minimal index.html inside /dist and open it in your browser:

<html>
    <body>
        <script src="main.js"></script>
    </body>
</html>

All being well, you should see Hello from test: Hello World being logged in the browser console.

We don't want to have the html file sat in the /dist folder though, we want to also generate html output. Move index.html to /src, and remove the script tag - webpack will automatically insert a script tag when building. To build the HTML as well, we use the html-webpack-plugin:

npm install --save-dev html-webpack-plugin

We also need to add it to the webpack config in webpack.config.js. First, require it into the file:

const HtmlWebpackPlugin = require('html-webpack-plugin');

Then add a new plugins section telling it to make use of our html file.

  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html'
    })
  ]

npm run build now also builds the html file - it should create it inside /dist, and manually opening it in the browser should work as before.

Rather than manually opening files though, it's better to serve them - webpack has a development server for this purpose:

npm install --save-dev webpack-dev-server

By default it serves from /public, let's update the config to use our /dist folder - again it's an extra section in webpack.config.js:

  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'),
    }
  }

You can now start the webserver with npx webpack serve and open a browser to localhost:8080. Note that running this command also does a build, and also live reloads the page when things change, so we don't necessarily need to use the previous npm run build anymore. We can augment that script with another one in package.json:

"serve": "webpack serve --config webpack.config.js"

Now npm run serve also works to build and load it. Note that neither of the scripts we've specified technically need the --config webpack.config.js option, it would use that file by default - but that's another small bit of "magic" worth being aware of.

TypeScript

We've got our small webpack JavaScript and HTML bundler up and running, we can now add JavaScript modules and import them etc, and have live reloading going on - let's add TypeScript support for a better dev experience.

npm install --save-dev typescript ts-loader

As well as installing the typescript package, we also install ts-loader which is an addon for webpack dealing with TypeScript files. Now they're installed, we again need to update the webpack.config.js to make use of them with some new sections:

resolve: {
    extensions: ['.ts', '.tsx', '.js']
},
module:{
    rules:[{
        test: /\.tsx?$/,
        loader: 'ts-loader'
    }]
}

TypeScript also needs a config file, called tsconfig.json - for now we can just use a(n almost) blank one:

{}

We can now switch from using JavaScript files to TypeScript - update index.js and test.js to be .ts files, and tweak the content:

test.ts:

export function logtest(msg:string):void {
    console.log("Hello from test: " + msg);
}

index.ts:

import { logtest } from './test'

logtest("Hello World");

Also update the webpack.config.js entry point to be /src/index.ts instead of /src/index.js. Building and running should result in the same behaviour as before. We can also "prove" that TypeScript is doing it's thing by adding an additional line of logging to index.ts:

logtest(123); //causes build error: TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

Debugging - sourcemaps

One thing I know I'll probably spend a long time doing is debugging in the browser. Let's make that experience better.

Add debugger; before the call to logtest(); in index.ts. When the browser breaks at that point, the shown code isn't great:

exports.__esModule = true;
var test_1 = __webpack_require__(/*! ./test */ "./src/test.ts");
debugger;
(0, test_1.logtest)("Hello World");

It vaguely resembles the original source, but the source isn't exactly complex at the moment. As the project grows, the more this will stop looking like the original source. This is where sourcemaps come in - let's add support for them.

Firstly, since we're using TypeScript, we need to tell it to generate them in the tsconfig.json:

{
    "compilerOptions": {
        "sourceMap": true
    }
}

And we need to tell webpack about them too in the webpack.config.js:

devtool: "source-map"

(There are various options available here, "source-map" is a simple starting point.)

If you build and run it now, the browser dev tools show what you'd expect:

import { logtest } from './test'
debugger;
logtest("Hello World");

And stepping in to the logtest function works as expected:

export function logtest(msg:string ):void {
    console.log("Hello from test: " + msg);
}

Vue.js

We've now got our static site building and bundling TypeScript, with good debugging support. Time to add Vue.js. At the time of writing, Vue.js 3 is now the current version, so you don't need to use any of the @next type syntax to install it:

npm install --save vue
npm install --save-dev vue-loader

Once installed, as before, we need to tell webpack about it in webpack.config.js. Firstly include it:

const { VueLoaderPlugin } = require('vue-loader');

Then instantiate the plugin alongside the HtmlWebpackPlugin:

new VueLoaderPlugin()

The .vue files we'll be using require runtime compilation/transformation - at the time of writing I haven't yet worked out precompilation of them, so we need to alias vue to its runtime bundler. To do so, add an alias section within the existing resolve section - alongside where we're telling it which extensions to use:

  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    alias: {
      'vue':'vue/dist/vue.esm-bundler.js'
    }
  }

We also need to update our rules files, firstly to tell webpack to use the vue-loader for .vue files, and secondly to tell the ts-loader to also work with .vue files:

rules: [{
    test: /\.tsx?$/,
    loader: 'ts-loader',
    options: {
        appendTsSuffixTo: [/\.vue$/]
    }
}, {
    test: /\.vue$/,
    loader: 'vue-loader'
}]

Finally we need to create a small shims file to get Vue to work nicely with our TypeScript setup - create vue-shims.d.ts:

declare module "*.vue" {
    import type { DefineComponent } from "vue";
    const component: DefineComponent<{}, {}, any>;
    export default component;
}

We can now update our application to make use of Vue. Firstly, let's introduce an element to mount it in in /src/index.html:

We don't need test.ts anymore, let's delete it and create a test vue instead, in /src/test.vue:

<template>
    <div>{{Name}}</div>
</template>

<script lang="ts">
class Test {
    Name:string = "";
}
export default {
    data(): Test {
        return {
            Name: "abcd"
        }
    }
}
</script>

And now update /src/index.ts to be the entry point for the new Vue application:

import { createApp } from 'vue'

import VueTest from './test.vue'


createApp({
  components: {
    "vue-test": VueTest
  },
  template: "<vue-test />"
}).mount("#app");

You should now be able to build and load the site as before, but with it running the simple Vue app we've created. Once again, to prove that TypeScript is working as expected within the .vue files, we can update the test.vue file to return a number where a string should be being returned:

export default {
    data(): Test {
        return {
            Name: 123
        }
    }
}

Doing so should cause a build issue:

TS2322: Type 'number' is not assignable to type 'string'.

Next Steps

That's it for now. Having done it, it all seems pretty straightforward, but coming from a .NET viewpoint it took me a while to gather the relevant information and get things going. This is a good start for developing Vue apps, my next steps are to integrate it into my Visual Studio pipeline to get it to run alongside the .NET projects that will form the backend.

Back to posts