A Modern JavaScript Production Setup
By: Marc Römmelt
Tags: Typescript, Vuejs, JavaScript, Programming, Webpack

Init the project

I will describe the individual steps I go through when creating a new project. If you’re looking for input on a specific topic, just skip ahead.

TL;DR: You can also just clone this GitHub repo.

Visual Studio Code

Let’s start by setting up VS Code. Create a project folder, and open it in VS Code. Then, save it as workspace in that folder (File > Save Workspace As > …). This will result in a xx.code-workspace file that allows us to specify configurations for this workspace. Opening this file will also open your workspace in VS Code.

Our final workspace config will look like this:

Next, we’ll install a few (I’d almost go as far and say essential) extensions:

ESLint will integrate our linting into our code editor and will inform you when you break your code’s style rules or commit even worse atrocities

will integrate our linting into our code editor and will inform you when you break your code’s style rules or commit even worse atrocities stylelint integrates linting for style files into the editor and supports autofixing

integrates linting for style files into the editor and supports autofixing GitLens integrates with Git version control. It allows you to do most of the amazing things Git can do — but directly in your code editor.

integrates with Git version control. It allows you to do most of the amazing things Git can do — but directly in your code editor. Vetur brings a lot of Vue-specific tooling with it, from syntax highlighting to code snippets

brings a lot of Vue-specific tooling with it, from syntax highlighting to code snippets Debugger for Chrome allows us to debug our application source code in VS Code while running the application in Chrome

.editorconfig

This short config file is used to communicate basic editor style choices across different editors — e.g., tab-size . You can read more about it here.

I usually run with the same setting for every file type:

.nvmrc

This is almost trivial, but if you happen to use different versions of Node.js across your projects, it’s quite handy. Having a .nvmrc file in you root directory allows you to lock down the Node.js version you’re using. It contains a single line with the version number — e.g., 12.14.1 . See here.

package.json

Next, we’ll create the package.json file of our project. This file belongs to the Node package manager (npm) ecosystem. It contains some configuration about your project, is used to manage the third-party code your app uses (dependencies), and allows you to define simple scripts that are commonly used to trigger command-line-tools with the correct options .

It’s typically created by the npm init command — but you can also just create the client/ package.json file manually. If you used npm init and accepted all the defaults, your file will look like this:

We’ll gradually update this file — for now we can just leave it this way. I encourage you to learn more about it here.

Git

In case you’ve never heard of it, Git is the most widely used version-control system (and by quite a margin). Simply put, Git allows you to copy, paste, revert, and merge code. It helps developers to work on the same codebase without interfering with each other’s work (mostly) and simultaneously provides a detailed change history of the entire project. Let’s just say it’s pretty awesome.

We create a new Git repository with the command git init .

If you want to share your code, it’s quite helpful to add a remote repository to your project (e.g., on GitHub). It allows you to push your code to a publicly accessible server . You add a remote repository with the command:

git remote add <remoteName> <remoteURL>

Don’t forget you can exclude files from being tracked with Git by adding them to a .gitignore file. This file contains file patterns that will also be ignored. The .gitignore file itself should be tracked. Read more here.

TypeScript

TypeScript is a superset of JavaScript (it contains all the functionality of JavaScript but adds some functionality on top — like static typing). It became overwhelmingly popular for building production applications with JavaScript and will probably be the default way to write JavaScript quite soon. If you’d like to read up on the (dis)advantages of TypeScript, I recommend this article.

To run TypeScript, we simply need to install a global npm package:

npm install -g typescript

This package enables us to compile .ts files to .js with the tsc command — e.g., tsc index.ts This command will create a .js file with the same name in the same folder as the original .ts file. (We won’t use this command since we use webpack.)

Next run tsc --init in your root folder ( Client ). This will create a tsconfig.json file filled with a neat summary of available compiler options (which is great for people with a trash-panda memory, like myself).

We;ll add some more config to it — please take a look:

You can read a more comprehensive explanation of all of the options on the official TypeScript website.

Linting/formatting

Clean code looks like it was written by a single person. Linters allow us to enforce specific rules to make our code overall more readable. Generally, linters consist of two types of rules to ensure a consistent coding style: formatting rules (e.g., lines cannot be longer than 100 characters) and code-quality rules (e.g., avoid using let in favor of const ).

The most commonly used linter in JavaScript is ESLint. We’ll use it as well as its amazing community already created several useful plugins — for example, one that applies Vue-specific styles for single file components.

Note: Before the release of typescript-eslint , you had to rely on TSLint to lint TypeScript code. However, it’s not possible to use the additional tooling in the ESLint ecosystem with TSLint. This is why I recommend everyone use typescript-eslint although it’s slightly more complex in its configuration.

Let’s install all required dependencies:

eslint : the base ESLint package

: the base ESLint package vue-eslint-parser : a custom parser allowing ESLint to parse .vue files. The output of this parser will be a .js file with imported dependencies representing the blocks of your file (e.g., <template> or <style> ).

: a custom parser allowing ESLint to parse files. The output of this parser will be a file with imported dependencies representing the blocks of your file (e.g., or ). @typescript-eslint/parser: We’ll tell vue-eslint-parser to use this parser to interpret the <script> block written in our .vue files

We’ll tell to use this parser to interpret the block written in our files @typescript-eslint/eslint-plugin : the ESLint plugin that applies TypeScript-specific rules

: the ESLint plugin that applies TypeScript-specific rules eslint-plugin-vue : applies Vue-specific linting rules to single-file components — e.g., the ordering of component options or element attributes

: applies Vue-specific linting rules to single-file components — e.g., the ordering of component options or element attributes prettier && eslint-config-prettier && eslint-plugin-prettier : These packages allow you to use Prettier together with ESLint. Prettier is a powerful code formatter with great autofixing capabilities.

npm i -D eslint vue-eslint-parser @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue prettier eslint-config-pretter eslint-plugin-prettier

To put all these bits together we have to define the .eslintrc file that contains the configuration for ESLint.

Of course, you can change the rules to your liking — the only important thing is to stick to one set of rules. You can read more about ESLint here.

Note: We also add configuration for Prettier in a .prettierrc file.

{

"arrowParens": "always",

"trailingComma": "all",

"singleQuote": true,

"printWidth": 120,

"tabWidth": 2

}

stylelint

You might have guessed it — stylelint allows you to enforce rules for your various style files. It’s not nearly as important as JavaScript linting; however, I like my files tidy.

npm i -D stylelint stylelint-webpack-plugin stylelint-config-sass-guidelines stylelint-config-standard

Add a .stylelintrc file to your root folder, and add your config:

Note: Turn of the no-empty-source rule — otherwise stylelint will throw errors for .vue files without style blocks.

Autofixing

A great feature of ESLint, Prettier, and stylelint alike is the autofixing of common problems by running the commands with a --fix flag. Even better: The VS Code extensions we installed earlier provide our code editor with autofixing capabilities. The responsible configuration is in our workspace configuration ( <projectname>.code-workspace) we created above.

Note: Remember to disable the editor-owned code formatter to avoid conflicts.

Git Hooks

At this point, I’d also like to introduce some tooling to you. The Husky library provides us with a simple way to set Git hooks, allowing you to run some code every time you dispatch a Git action (e.g., commit ).

The responsible bit of code is found at the root level of our package.json file. We use the pre-commit hook to check our project for linting errors and run our unit tests. If ESLint reports an error, the commit aborts.

“husky”: {

“hooks”: {

“pre-commit”: “npm run lint && npm run test”

}

},

Read more about Husky.

webpack 4

Webpack is an open-source module bundler for JavaScript that works just as well for TypeScript. What webpack allows us to do is basically walk through all files of our project and combine them into a single output files (or multiple output files) — and process all of the files while we’re at it. webpack, therefore, serves as an entry point for an entire world of tooling. You can:

Write code with the newest ES2020 features and automatically transpile it down to ES5 that’s understood by every toaster

Automatically minimize your code to save file size

Easily integrate CSS preprocessors, like SASS

Automatically apply CSS prefixes with PostCSS where necessary

Employ a large set of tooling meant to analyze your code

Easily apply code-splitting

Unfortunately, the wealth of features and plugins makes webpack also notoriously difficult to set up for new developers. But bare with me, it’s actually quite reasonable once you read the documentation.

Before we dive into the configuration file, let’s install all the dependencies:

webpack : the base package

: the base package webpack-cli : the command-line interface (we use it with an npm script)

: the command-line interface (we use it with an npm script) webpack-dev-server : a development server, allowing hot reloads on file changes and serving our app in development mode

: a development server, allowing hot reloads on file changes and serving our app in development mode clean-webpack-plugin : used to clean/remove our output folder

: used to clean/remove our output folder html-webpack-plugin : creates an HTML file to serve our app bundle. It dynamically inserts link tags with the corresponding hashes and works together with other plugins to uphold a sensible preloading strategy.

: creates an HTML file to serve our app bundle. It dynamically inserts link tags with the corresponding hashes and works together with other plugins to uphold a sensible preloading strategy. copy-webpack-plugin : copies individual files or entire directories into the build folder

: copies individual files or entire directories into the build folder optimize-css-assets-webpack-plugin : optimizes/minimizes CSS assets

: optimizes/minimizes CSS assets mini-css-extract-plugin : loads the CSS of our single-file components into separate files

: loads the CSS of our single-file components into separate files terser-webpack-plugin : used to minify JavaScript

: used to minify JavaScript critters-webpack-plugin : used to inline critical CSS and lazy load the rest (documentation here)

: used to inline critical CSS and lazy load the rest (documentation here) stylelint-webpack-plugin : allows us to integrate style linting into our build process

: allows us to integrate style linting into our build process webpack-node-externals : allows us to easily tell webpack it shouldn’t include node_modules in our bundle. We’ll use this for our unit tests — no need to include them there.

npm install -D webpack webpack-cli webpack-dev-server clean-webpack-plugin html-webpack-plugin copy-webpack-plugin optimize-css-assets-webpack-plugin mini-css-extract-plugin terser-webpack-plugin critters-webpack-plugin stylelint-webpack-plugin webpack-node-externals

webpack transforms all files with the help of so-called loaders.

Oversimplified, these are functions webpack calls for every file that match a defined condition. Loaders then parse/interpret/transform the file and pass it onto the next loader or back to webpack. webpack then writes the file into its output folder.

We’ll use a whole set of loaders (and some dependencies). Let’s install them:

npm install -D vue-loader vue-template-compiler url-loader ts-loader style-loader sass-loader node-sass raw-loader file-loader css-loader postcss-loader autoprefixer babel-loader @babel/core @babel/preset-env core-js thread-loader cache-loader eslint-loader

url-loader : does what it says

: does what it says ts-loader : the TypeScript loader for webpack

: the TypeScript loader for webpack style-loader : injects CSS into the DOM/HTML with <style> tags

: injects CSS into the DOM/HTML with tags vue-loader && vue-template-compiler : loads .vue files and allows us to use separate loaders for all blocks (including custom blocks). The template compiler is required to precompile HTML templates.

: loads files and allows us to use separate loaders for all blocks (including custom blocks). The template compiler is required to precompile HTML templates. sass-loader && node-sass : loads Sass/SCSS and compiles it to CSS. Note: If you use Sass in your project, you need to also install node-sass , which allows node to understand Sass (read more).

: loads Sass/SCSS and compiles it to CSS. If you use Sass in your project, you need to also install , which allows node to understand Sass (read more). raw-loader : allows us to import files as a string

: allows us to import files as a string file-loader : resolves imported files to an URL (used for files such as images)

: resolves imported files to an URL (used for files such as images) css-loader : interprets and resolves imports of CSS

: interprets and resolves imports of CSS postcss-loader && autoprefixer : a loader to processes your CSS with PostCSS. autoprefixer is a module used by PostCSS that prefixes your CSS to ensure cross-browser compatibility. You can configure PostCSS (to use autoprefixer ) either in your webpack config or in a separate postcss.config.js file.

module.exports = {

plugins: [

require(‘autoprefixer’),

],

}

babel-loader && @babel/core && @babel/preset-env && core-js : allows us to use Babel together with webpack. Babel transpiles the JavaScript output of ts-loader to an earlier version of JavaScript and adds core-js polyfills to ensure browser compatibility.

Babel requires some basic configuration to function. Create a .babelrc file at your project root with this content:

We simply tell Babel to use the preset-env configuration, transpile our JavaScript to target Internet Explorer 11, and polyfil using core-js@3.

thread-loader : allows the following loaders in this loader chain to use multiple threads. This will speed up heavy-duty loaders like babel-loader .

: allows the following loaders in this loader chain to use multiple threads. This will speed up heavy-duty loaders like . cache-loader : caches the output of loaders, which speeds up consecutive builds

: caches the output of loaders, which speeds up consecutive builds eslint-loader : allows us to Lint all files before processing them

Lastly, let’s throw in another addon that analyzes the bundle size of our app:

npm install -D webpack-bundle-analyzer

The config that puts everything together looks like this:

This config will also serve as our configuration for webpack-dev-server , a handy tool serving our app during development and offering Hot Module Reloading. To run and build our app, we use the above defined npm scripts :

// Client/package.json {

...

"scripts": {

"start": "webpack-dev-server --host 0.0.0.0 --hot",

"build": "webpack -p --progress",

...

},

...

}

The --hot flag enables hot reloading, -p sets env.mode to production , and --progress tells webpack to display the build progress as a percentage.

Testing (Mocha, Sinon, Chai)

I don’t know about you, but every time I’ve started a new feature by writing tests for it, it just worked. Also, test-driven development (TDD) is a great buzzword on every CV. So here we go.

We use a wrapper library around Mocha to run our tests with Mochapack. It allows us to first build our project with webpack and subsequently run our unit tests with our built project.

Let’s install all of the dependencies:

npm install -D mocha mochapack jsdom jsdom-global @types/mocha chai @types/chai sinon @types/sinon @vue/test-utils

mocha && @types/mocha: our test runner (the process in which our tests run)

our test runner (the process in which our tests run) moochapack : the wrapper around Mocha so we can build our project with webpack before we run tests on it

: the wrapper around Mocha so we can build our project with webpack before we run tests on it chai && @types/chai : an assertion library. It gives us helper functions to compare values in a nice, syntactic way.

: an assertion library. It gives us helper functions to compare values in a nice, syntactic way. sinon && @types/sinon: a stubbing library. It enables us to easily replace properties/methods on objects and comes with many useful helper functions to test their behavior.

a stubbing library. It enables us to easily replace properties/methods on objects and comes with many useful helper functions to test their behavior. @vue/test-utils : utility functions to test Vue components

: utility functions to test Vue components jsdom && jsdom-global : a JavaScript implementation of many web standarts (HTML5/Web APIs) commonly used to mock browser behavior. jsdom-global injects this DOM implementation into the Node runtime.

Typically, you’ll create a setup.ts file in your client/tests folder, in which you initialize the environment you might need to run your tests. In a basic setup, this might just be jsdom :

// client/tests/setup.ts

require('jsdom-global')();

Now, we can simply create our unit tests — how to test Vue is nicely documented by their team.

Finally, let’s set up a npm script to run them:

"scripts": {

...

“test”: “mochapack”,

“test:watch”: “mochapack --watch”

}

The --watch flag tells Mocha to not exit the process but instead watch for file changes and rerun tests in the changes files (convenient for development).

On execution, mochapack will automatically look for a mochapack.opts file in the folder of your package.json . We use this file to define some command-line options:

// mochapack.opts --colors

--webpack-config webpack.config.js

--webpack-env test

--require test/unit/setup.ts

--recursive

'test/**/*.spec.{js,ts}'

--colors : Forces Mocha to use colors in its terminal output

: Forces Mocha to use colors in its terminal output --webpack-config : Specifies the webpack config we want to use

: Specifies the webpack config we want to use --webpack-env : Specifies the webpack environment. If you look at our webpack config, you’ll notice we have slightly different behavior for our test mode

: Specifies the webpack environment. If you look at our webpack config, you’ll notice we have slightly different behavior for our test mode --require : Tells Mocha to run and execute a file before it runs the tests

: Tells Mocha to run and execute a file before it runs the tests --recursive : Look for tests in subdirectories

: Look for tests in subdirectories ’test/**/*.spec.{js,ts}' : The glob to match files that should be included, bundled by webpack and run by Mocha. In this case, all our unit tests are located in a test directory and end in .spec.ts or .spec.js .

Hint: I usually don’t worry too much about complete test coverage as I only test code that seems likely to cause problems in the future. However, if you do, I suggest you take a look at the test-coverage tool Istanbul — more precisely, their CLI-tool nyc (link).

Debugging

Debugging in VS Code is luckily quite simple. Simply add a Client/.vscode folder and create a launch.json file inside.

We’ll configure three lauch configurations for our app:

Our first configurations allow us to debug our unit tests. Simply set breakpoints, select this configurations, and start the debugger (F5).

The second config uses a VS Code extension called Debugger for Chrome. It allows us to run our application in Chrome while using breakpoints in our source code. Simply set breakpoints (e.g., in a mounted() hook), run your application ( npm run start ), and then start the debugger by selecting the config and pressing F5.

Setting up Vue

Finally, now it’s time to work on your actual project! I assume everyone making it this far is already familiar with Vue itself (they also have great documentation). Hence, I’ll only mention a few common issues when working with TypeScript and name a few great modules I use for specific use cases.

vue-property-decorator

The easiest way to write Vue components with TypeScript is the class-based syntax. This syntax requires several TypeScript decorators to work. You might have heard of the vue-class-component in this context. However, this library is designed to support Babel users next to TypeScript users — and Babel conflicts with some decorators contained in vue-property-decorator .

Since we compile all our decorators with TypeScript, we can use this library without any problems. More on this GitHub issue.

Declaring *.vue modules

For TypeScript to be able to import *.vue files, we have to declare them as modules. The CLI does this in a shims.vue.d.ts file with the following contents:

declare module ‘*.vue’ {

import Vue from ‘vue’;

export default Vue;

}

If you want to augment Vue’s interfaces, I recomment creating a types folder on the root level of your project. In this folder, you can create subfolders containing index.d.ts files. In these files, you may import the module you wish to augment. For example:

// client/types/vue/index.d.ts import Vue from ‘vue’; declare module ‘vue/types/vue’ {

interface Vue {

$myProp: string; // allows this.$myProp or vm.$myProp

}

}

You can find more information on how to augment Vue types here.

To tell TypeScript it should include our type definitions in types/* , we already included the following line in our tsconfig.json .

“typeRoots”: [“./types”, “./node_modules/@types”]