Anton

Web Workers: Playing Nice With Visual Studio Code’s Intellisense

April 03, 2017

Have you recently tried writing a Web Worker using TypeScript on Visual Studio Code? I don’t know about you, but I sure had a bad experience with the intellisense and in compiling the code.

VS Code is a great editor and TypeScript is a very powerful superset of JavaScript, but like any piece of software, they have their quirks. This is my attempt at taming one such quirk in order to write web workers in peace. 😉

You can follow along by taking a look at the sample application which you can find here: https://github.com/antonmata/Web-Workers-With-Visual-Studio-Code-s-Intellisense

The Problem

There were 2 main issues to my initial setup that contributed, greatly, to my headaches. These 2 issues are actually related to each other. One is with the cosmetic, IDE intellisense experience, and the other is with the TypeScript compiler, which is trying to compile the postMessage call, expecting it to follow the definition in lib.d.ts.

Issue 1: Visual Studio Code’s default typings

VS Code will always load lib.d.ts by default. Since I am writing a web worker, I need to find a way to load lib.webworker.d.ts which has the proper typings for all web worker related code. One of the main differences between the 2 typings is the definition of the postMessage function:

// lib.d.ts
declare function postMessage(message: any, targetOrigin: string, transfer?: any[]): void;

// lib.webworker.d.ts
declare function postMessage(message: any, transfer?: any[]): void;

Sure, I could just work around the issue by declaring an ambient definition:

declare var postMessage: any;

But that won’t be the right thing to do. It is an inelegant workaround that adds an unnecessary line of code to fix something that we can actually remedy by configuration.

Issue 2: The TypeScript compiler also uses the default typings

Using just a single tsconfig.json for the whole project will not work. You cannot use the same config for both your regular code and your web worker code. The TypeScript compiler will complain about the postMessage signature.

The Solution (AKA “how I went about it”)

After spending the better part of a day scouring the Internet, I never found an exact solution. So, I was left to experiment on a number of different configurations. Luckily I found a structure that works, and is not as hacky as my other ideas. I’m not sure if this is how it should be done, but it feels stable as a pattern.

Here is how I organized my files:

📄 index.html
📄 webpack.config.js
📂 config/
    📄 browser.webpack.config.js
    📄 workers.webpack.config.js
📂 js/
    📂 browser/
        📄 index.ts
        📄 tsconfig.json
    📂 common/
        📄 CalculatePi.ts
        📄 tsconfig.json
    📂 workers/
        📄 Pi.worker.ts
        📄 tsconfig.json

Looking at the structure, there are a couple of things that stand out.

The Browser, Common, and Workers folders

Under the js folder, the sources are organized into 3 folders and each folder has its own tsconfig.json:

Browser

This is the folder for all front-end scripts. Here is the tsconfig.json for the folder:

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "alwaysStrict": true,
        "strictNullChecks": true,
        "noEmitOnError": true,
        "removeComments": false,
        "module": "commonjs",
        "target": "es5"
    },
    "exclude": [
        "node_modules",
        "wwwroot"
    ]
}

Common

This folder generally contains any business logic related code. The tsconfig.json for this folder is similar to browser‘s. It is wise to keep them separate since I might want to make changes to the browser’s config to add support for React or other front-end related changes in the future.

Workers

This folder contains all the web workers. For its tsconfig.json, I needed to add an explicit list of libraries that a compiler needs to take into account when compiling the sources under this folder.

"lib": [
    "webworker",
    "es5",
    "scripthost"
]

The list of libraries also tells VS Code what typings to use for the web worker files. It excludes the default libraries in order to prevent any conflict in the function signatures.

For more information about tsconfig.json and the libraries, please check out these links:

Webpack config files

The Webpack config files are broken down into 3 files. This is really just for sanity; since I am using the multi-compiler pattern to run separate configs for the front-end scripts and for the web worker scripts, the config file turned out to be huge, so I had to break them down to separate files.

webpack.config.js

The main webpack config that I run to build the scripts. It just simply loads the other 2 config files.

"use strict";

let browserConfig = require("./config/webpack.browser.config");
let workerConfig = require("./config/webpack.workers.config");

module.exports = [
    browserConfig(),
    workerConfig()
];

browser.webpack.config.js

This config is set up to build the front-end related scripts. It also loads CleanWebpackPlugin to delete the dist folder. Of note is how I set up the options for the awesome-typescript-loader.

{
    test: /\.ts$/,
    include: [
        path.resolve(__dirname, "../js/browser"),
        path.resolve(__dirname, "../js/common")
    ],
    loader: "awesome-typescript-loader",
    options: {
        configFileName: "./js/browser/tsconfig.json"
    }
}

I used the include property to create a whitelist of folders that I’d like to include during compilation. Notice I excluded the workers folder and I explicitly declared a configFileName to make sure it is using the correct tsconfig.json.

workers.webpack.config.js

This is the Webpack config to build the web worker scripts. The entry points are set up to be a list of all web workers to be built. For this instance, I only have 1.

entry: {
    pi: "./js/workers/Pi.worker.ts",
    // Add other web workers here!
    // ex.
    // test: "./js/workers/Test.worker.ts",
},

The output property is configured to write individual files for each entry point:

output: {
    filename: "./js/[name].worker.js",
    path: path.resolve(__dirname, "../dist")
},

I also have a custom set up for the awesome-typescript-loader which also has a whitelist of folders to include in the compilation (in this case, I am only including workers and common). The proper tsconfig.json is also declared for configFileName.

{
    test: /\.ts$/,
    include: [
        path.resolve(__dirname, "../js/workers"),
        path.resolve(__dirname, "../js/common")
    ],
    loader: "awesome-typescript-loader",
    options: {
        configFileName: "./js/workers/tsconfig.json"
    }
},

Take note of the relative paths within the 2 config files inside the config folder. ../ is only used when calling path.resolve since __dirname points to the current folder (in this case, it is ./config hence the need for ../).

We can use ./ even though the files are inside a folder because once they are being processed by Webpack, the ./ will just resolve to where the base webpack.config.js is located.

Using The Compiled Web Worker Scripts

The web worker scripts are compiled into their separate JavaScript files. Using them is as simple as using the standard way of instantiating a worker object:

let piWorker = new Worker("dist/js/pi.worker.js");

Conclusion

The solution I presented, admittedly, is far from ideal. No one should be submitted to tinkering with multiple config files just to use an API that is readily available if I was to write them in Vanilla JS.

However, I do see the strength in structuring TypeScript code this way. I serendipitously ended up with a lot more control over the configuration of each folder of scripts, and in the context of an enterprise grade application, control is paramount.


Anton Mata

Written by Anton Mata who lives and works in Manila building useful things. You should follow him on Twitter