Yarn 2 and TypeScript: Adding Webpack

Yarn Modern ships with its own Node instance which can be a pain, but Webflow fixes that once and for all!

xsmith
DataSeries
8 min readJun 22, 2020

--

This extends my first article, Getting Started with Yarn 2 and TypeScript, and you can find the updated code this article is based on, Webpack Starter.

How fragile is your code? Interlinked. Interlinked. How badly do you want clean code? Interlinked… Interlinked.
Photo by Sigmund on Unsplash

Rationale

So you ran yarn tsc and got dist/file-example.js and ran node dist/file-example.js but what you got was far from what you expected.

A screenshot representing a failed module request in Yarn Modern comiled JavaScript.
Well, that’s not what we wanted.

If you read my first article, then you’d know what went wrong. If not, I’ll explain. The TL;DR is, Yarn Modern ships with its own instance of Node with special environment variables and hooks so that Node, and by extension TypeScript, see the files correctly and output them accordingly. However, this means only Yarn can interpret those module paths.

For the longer explanation, Yarn Modern ships with its own Node executable with special settings that allow Node to correctly point module requests to their appropriate locations. Yarn does this because it uses a virtual filesystem instead of the old node_modules system. In theory, it’s beautiful, in practice without proper knowledge it just becomes a headache. I talked about this a little in my last article, Getting Started with Yarn 2 and TypeScript. Some of you may be thinking, “Well I use TypeScript which compiles to JavaScript, I shouldn’t have this issue!”, and unfortunately you’re wrong. You see to get TypeScript and Yarn 2 to play nice… in Plug’n’Play, you have to install pnpify and then use it to set up TypeScript. Reiterating from my part one.

PS C:\…> yarn add @yarnpkg/pnpify — dev
➤ YN0000: ┌ Resolution step
➤ YN0000: └ Completed in 1.45s
➤ YN0000: ┌ Fetch step
➤ YN0013: │ …
➤ YN0013: │ … “can’t be found and will be fetched etc…”
➤ YN0013: │ …
➤ YN0000: └ Completed in 1.44s
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed
➤ YN0000: Done in 3.26s
PS C:\…> yarn pnpify — sdk vscode
➤ YN0000: ┌ Generating SDKs inside .yarn/sdks
➤ YN0000: │ ✓ Typescript
➤ YN0000: │ • 5 SDKs were skipped based on your root dependencies
➤ YN0000: └ Completed
➤ YN0000: ┌ Generating settings
➤ YN0000: │ ✓ Vscode (new ✨)
➤ YN0000: └ Completed

If you haven’t figured it out yet, behind the scenes Pnpify tells TypeScript about the new file system, which is why even though yarn tsc spits out JavaScript, you have to think past what’s in front of you. This JavaScript is making requests, simple enough. However, Node is looking for node_modules which doesn’t exist, remember? So yarn node <file> is required, but this is tedious. The solution? Skip the requests to the modules directory by bundling your modules and code together! How do we do that?

The compiled code seems simple enough, but it requires external modules, that’s the problem.

Introducing, Webpack! 📦🎉

In order to bundle everything together nicely, we’ll need a Bundler. In short, it takes everything we need and creates… bundles. Huh. What this means is that say you have a few lines of code, but you require('fs'). What makes modules powerful is the amount of backend code that you don’t have to type yourself. But we do rely on them, and that’s where Yarn’s new virtual filesystem kind of works against us. So, what’s Webpack, and how can we use it?

Webpack is an open-source JavaScript module bundler. It is made primarily for JavaScript, but it can transform front-end assets like HTML, CSS, and images if the corresponding loaders are included. webpack takes modules with dependencies and generates static assets representing those modules. — Wikipedia

Webpack can do a lot for us. But in this case, we only need it to throw everything into a single file, eliminating our MODULE_NOT_FOUND problem. It’s important to note that if you haven’t actually gotten started with Yarn 2 and TypeScript you might want to before moving forward. So let’s install Webpack and its CLI.

PS C:\...> yarn add --dev webpack webpack-cli clean-webpack-plugin ts-loader cache-loader fork-ts-checker-webpack-plugin pnp-webpack-plugin source-map-loader thread-loader ts-node
➤ YN0000: ┌ Resolution step
➤ YN0061: │ … DEPRECATED_PACKAGE warnings
➤ YN0032: │ … NODE_GYP_INJECTED warnings
➤ YN0000: └ Completed in 3.66s
➤ YN0000: ┌ Fetch step
➤ YN0013: │ … FETCH_NOT_CACHED notifications
➤ YN0000: └ Completed in 4.59s
➤ YN0000: ┌ Link step
➤ YN0007: │ … MUST_BUILD notifications
➤ YN0000: └ Completed in 1.99s
➤ YN0000: Done with warnings in 10.33s

Now that seems like a lot of dependencies, but don’t worry! Yarn 2 is super fast and handles them in no time, and of course, they don’t go into your final build so there’s no added weight either. So with those dependencies added, let’s start building our configuration if you want to see the final configuration you can check it out on GitHub.

The most basic config to get started with Webpack

This config is your average starter for JavaScript. But you’ll notice the config’s file extension is .ts, this is because we’re about to start integrating TypeScript support. What this does is tell Webpack where to find the file and where to put the new file. Using "build": "NODE_ENV=production && yarn _webpack" we also want to detect that in our config, alongside using the webpack.Configuration type to add some IntelliSense while we’re typing our config out, another reason I love TypeScript. If you’re not familiar with Webpack, please read up on its basics, because from here its anything but. As I said, this handles JavaScript (despite being written in TypeScript of course), so we need to tell Webpack two things off-hand; how to understand TypeScript, and how to understand Yarn’s Plug’n’Play. We’ll start with Plug’n’Play since you could stop there and just tell Webpack to take your compiled TypeScript and output fresh JavaScript.

Here we’ve added the PnpWebpackPlugin so that Webpack can correctly understand what it's looking at; resolve handles dependencies requests, and resolveLoader just tells Webpack where to find loaders such as the source-map-loader. As I said, you really could just stop here, yarn tsc out to dist, and then yarn build out to build and you’d have a functional setup. But you’re here for TypeScript support, so this adds a good amount of lines, so bear with me for this next stretch of code 😅.

As your project becomes bigger, compilation time increases linearly. It’s because typescript’s semantic checker has to inspect all files on every rebuild. The simple solution is to disable it by using the transpileOnly: true option, but doing so leaves you without type checking and will not output declaration files.

So this is where we really get the most of our configuration fluffed up, you could opt for just ts-loader but unfortunately, compile times say otherwise. So I’ve done a lot of toying around and came up with this configuration that uses three points of optimization. Starting with ts-loader, it takes in TypeScript and only transpiles it, leaving out type-checking for fork-ts-checker-webpack-plugin to do the heavy lifting on that. Now FTCWP also has some optimizations, such as only reporting errors after builds to keep things moving smoothly. Most importantly, thread-loader pulls ts-loader into a multithreaded workflow so that we really get te performance we want. At this point, you’re looking at roughly 14-second builds, and in watch mode, 4-millisecond rebuilds. This is the best I could do, but I’m sure you could research even faster implementations! It’s overkill for small projects, but proofs larger ones. If you’d like to see the entire workflow, check out my Yarnberry Cookbook where I store all my Yarn 2 and TypeScript variations.

Now Webflow now spits out the same file as before, file-example.js except for this time instead of ~40 lines of code, we’re given ~557. Like I mentioned before, Webpack pulls all of the modules we imported into the file and references them internally so that we’re working with pure JavaScript. The downside is its now a bloated file where your program only takes up maybe 3 lines. But it's a necessary evil.

However, you can also look into researching Webpack and learning about topics like Tree Skaking, Dead Code Elimination, Uglifying code, and more to learn how to shed a few pounds off your build files! I’d also take a look at performance optimization.

All in all, Webpack does its job, bundling files and making it just work. With Yarn Classic and hell, even without using TypeScript, you really don’t need it for such a small project. But either way, it’s good to have things set up so your project can expand quickly without build errors slowing you or your team down. So what about webpack-cli?

Yarn Tasks

Getting Webpack to work just requires a simple task. Again focusing on future-proofing, I split it into two smaller parts in case I needed to reuse or edit the main webpack command. So we’re looking at this:

A block of code, representing a basic npm package manifest in JSON.
The simple package.json that powers this beast.

Yarn 2 automatically handles environment variables for us, so if you’re used to cross-env it no longer works. So much so, trying to use it throws errors about NODE_ENV not existing at all. Here we use webpack-cli but later on with larger projects I recommend a combination of parallel-webpack and webpack-merge to create a dynamic configuration setup that runs in parallel. It’s actually pretty fast contrary to what I thought might happen. If you’re interested you can check out Reaxpress which uses this alongside my heavier paths object.

That pretty much sums up Webpack with Yarn 2 and TypeScript. A headache getting started, and a bit of overhead with TypeScript requiring multiple loaders and plugins to really get the performance you want. But other than that, I implore you to research Webpack and experiment with it! After all, reading a one-time article isn’t experience 😁.

I could have completely removed the warnings and error codes, but I include them for curious readers who may wonder what they mean. I love that Yarn is more talkative in a subtle way using YN00** each step so you know exactly what’s happening. Speaking of which, you’ll occasionally see YN0001 EXCEPTIONs, for me its always in Visual Studio Code when Node.js hangs onto files. Just reload VS Code and run the command again.

--

--

xsmith
DataSeries

Full Stack Developer. Started coding at 16 and haven't stopped. Always looking to explore new technologies and write about others.