Snowpack with Svelte, Typescript and Tailwind CSS is a very pleasant surprise
Tags: webdev, typescript, svelte, css
Published: 2020-05-17 20:46:42

---
title: Snowpack with Svelte, Typescript and Tailwind CSS is a very pleasant surprise
published: true
cover_image: https://res.cloudinary.com/codechips/image/upload/v1589748624/simon-matzinger-dT8vKOEW1Mc-unsplash_1_convi4.jpg
date: 2020-05-17 00:00:00 UTC
tags: webdev, typescript, svelte, css
canonical_url: https://codechips.me/snowpack-svelte-typescript-tailwindcss/
---

I like bundlers. Correction, I like **fast** bundlers. Bundlers that help me tighten the feedback loop and help me focus on the code. Not bundlers that make me doze off, waiting for the recompilation to finish, while my CPU fan sounds like an old hair dryer.

## Update

Technology moves at the speed of light and since this article was written Snowpack has released a stable version. This means that some of the things explained here (like Typescript support) are baked in and Snowpack also has support for pluggable bundlers now. I have updated example Github repo. You can find the link at the end of the article.

## Enter Snowpack

I've been keeping an eye on [Snowpack](https://www.snowpack.dev) for some time now - an (un)bundler that leverages the power of the ES modules. Personally, I like the [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) idea and hope that we all soon will align and come to a consensus. One promising thing in Snowpack's favour is that almost every modern browser already [supports](https://caniuse.com/#feat=es6-module-dynamic-import) ES modules.

Ok, ok, but what does it actually mean? This is Snowpack's elevator pitch taken straight from its [website](https://snowpack.dev).

> Snowpack helps you build web apps faster by replacing your traditional app bundler during development. No bundling means less unnecessary tooling, instant dev startup time and faster updates on every save.
>
> When you're ready to deploy your site, Snowpack automatically optimizes and bundles your assets for production.

Now, how can one **NOT** like that?! However, it all sounds too good to be true and thus requires some actual proof. Let's try to climb the mountain.

Snowpack recently re-caught my attention on Twitter, where its author, [@FredKSchott](https://twitter.com/FredKSchott), announced the pre-release of Snowpack v2 and also posted a link to a bunch of starter templates - [Create Snowpack App (CSA)](https://www.snowpack.dev/#create-snowpack-app-(csa)). One of those was a template for [Svelte](https://github.com/pikapkg/create-snowpack-app/tree/master/templates/app-template-svelte). It was too tempting not to try it.

So I did.

## The Setup Experience

The setup experience was very pleasant. You can tell CSA to `yarn` instead of default `npm` with the `--use-yarn` option when bootstrapping, if Yarn is your thing.

```text
$ npx create-snowpack-app snow-drive --template @snowpack/app-template-svelte
$ cd snow-drive && npm start
```
My first impression is that it starts really fast! Or more precisely in **9ms**. That's crazy fast!

![terminal output](https://res.cloudinary.com/codechips/image/upload/v1589736909/first-run_ms1wcy.webp)

The browser opens up automatically with a nice clean page that has an animated Svelte logo and clear instructions on how to proceed.

![initial page](https://res.cloudinary.com/codechips/image/upload/v1589736909/start-page_qicmb4.webp)

I changed some text and deleted some elements to see how fast it would discover the changes and reload. I tried not to blink, but couldn't notice the actual reload. That fast! Had to try it a few times more to make sure that Snowpack wasn't fooling me. Nope, same thing. Blazing fast reloads!

I don't think Snowpack has HMR (Hot Module Reload) support yet for Svelte as Svelte doesn't have support for it itself yet, so Snowpack reloads the whole app when you change something.

**NOTE**: There are some hacky and experimental ways you can get HMR support with Rollup and Webpack, I believe.

## Adding Typescript support

I like to write most of the logic in separate files and just use Svelte as a thin view layer that glues all the pieces together. For that I usually use Typescript, mostly because it gives me nice autocomplete in my editor. Some of you are probably rolling your eyes right now, but hey, tooling and DX is important!

I had some trouble understanding what's required to get Typescript support, so I asked the question on Twitter hoping to get some pointers in the right direction.

To get Typescript support working in Showpack's Svelte template you have to tweak a few things.

First, you need to install Snowpack's [Babel plugin](https://github.com/pikapkg/create-snowpack-app/tree/master/packages/plugin-babel), Babel's Typescript preset and the actual Typescript compiler.

```text
$ npm add -D @snowpack/plugin-babel @babel/preset-typescript typescript
```

Second, we need to tell Babel that we want to transpile Typescript to Javascript. Add the `babel.config.json` in the project root folder with the contents below. Snowpack's Babel plugin will pick it up automatically if the file exists.

```json
{
  "presets": [
    "@babel/preset-typescript"
  ]
}
```

Third, you have to tell Snowpack that you want to transpile and lint Typescript. Change your `snowpack.config.json` to the code below.

```json
{
  "extends": "@snowpack/app-scripts-svelte",
  "scripts": {
    "plugin:ts": "@snowpack/plugin-babel",
    "run:tsc": "tsc --noEmit",
    "run:tsc::watch": "$1 --watch"
  },
  "devOptions": {},
  "installOptions": {}
}
```

Snowpack uses Babel for transpiling TS to JS, because Babel is much faster than TS compiler. The reason for that is that Babel only strips out the types and does not do any type checking. For TS linting Snowpack uses the actual Typescript compiler. However, before we can lint we need to create a `tsconfig.json`, so that Typescript will know what to do.

```json
{
  "include": ["src"],
  "exclude": ["node_modules/*"],
  "compilerOptions": {
    "target": "es2019",
    "types": ["svelte"],
    "moduleResolution": "node"
  }
}

```

Just for the kicks, let's install [RxJS](https://rxjs-dev.firebaseapp.com/) and use that too. I am curious to see how this will play out with Snowpack's production builds later.

```text
$ npm add -D rxjs
```

When you add another dependency Snowpack will automatically convert it to an ES module and install it in `web_modules` folder. How convenient!

Now we will actually add some Typescript files and see how it flies. Create a `src/timer.ts` file with the following contents.

```ts
// timer.ts

import { readable } from 'svelte/store';

const timer = readable(0, set => {
  let current: number = 0;

  const id = setInterval(() => {
    current += 1;
    set(current);
  }, 1000);

  return () => clearInterval(id);
});

export { timer };

```

Now, change the `App.svelte` to the code below.

```html
<!-- App.svelte -->

<style>
  :global(body) {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 5rem;
    height: 100vh;
    display: flex;
    background: azure;
  }
  .app {
    display: grid;
    align-items: center;
    justify-content: center;
    width: 100%;
  }
  .box {
    margin: 0 auto;
  }
</style>

<script>
  import { timer } from './timer';
  import { interval } from 'rxjs';
  import { map, startWith } from 'rxjs/operators';

  const rxTimer = interval(1000).pipe(
    startWith(0),
    map(v => v * 2)
  );
</script>

<div class="app">
  <div class="box">
    <div>TS Timer {$timer}</div>
    <div>Rx Timer {$rxTimer}</div>
  </div>
</div>
```

If you start the app now everything should work as expected.

![new page with timers](https://res.cloudinary.com/codechips/image/upload/v1589736909/page-v1_jhodq5.webp)

As I wrote earlier Snowpack converts your dependencies and puts them into the `web_modules` folder. Delete the `web_modules` folder and see everything blow up. To re-create it just run `npm prepare`. That script runs `snowpack install` [command](https://www.snowpack.dev/#snowpack-install), which will scan the source code for all `import` statements to find every NPM package used by your application and package it as a ESM in the `web modules` folder.

```text
$ npm prepare # or npx snowpack
```

We should now have Typescript support. How nice!

![web tools sources tab](https://res.cloudinary.com/codechips/image/upload/v1589736909/console-sources_e45idy.webp)

Let's climb higher and try to enable PostCSS together with [TailwindCSS](https://tailwindcss.com/).

## Adding Tailwind CSS

Tailwind is a popular atomic CSS framework that I often use and since Snowpack promise us [PostCSS](https://www.snowpack.dev/#postcss) support we should be able to use Tailwind.

```bash
# install tailwind, autoprefixer, cssnano for css minification
$ npm add -D tailwindcss autoprefixer cssnano

# create tailwind config
$ npx taiwindcss init

# we also need to install postcss
$ npm add -D postcss-cli
```

Next we need to create a `postcss.config.js`.

```js
// postcss.config.js

const tailwind = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');

const plugins = process.env.NODE_ENV === 'production'
	? [tailwind, autoprefixer, cssnano]
	: [tailwind, autoprefixer];

module.exports = { plugins };
```

Latest version of Tailwind has [PurgeCSS](https://purgecss.com/) built-in, a library that removes unused CSS. We need to tweak our `tailwind.config.js` file a bit to use that.

```js
// tailwind.config.js

module.exports = {
  purge: ['./src/**/*.svelte', './public/*.html'],
  theme: {
    extend: {}
  },
  variants: {},
  plugins: []
};

```

We now have to create a base Tailwind style sheet and then import it into our app.

```css
/* src/main.css */

@tailwind base;

@tailwind components;

@tailwind utilities;
```

Import it at the top of the `<script>` tag in `App.svelte`

```js
import `./main.css`;
```
Add a few Tailwind classes to the "box" div in our `App.svelte`.

```html
<div class="app">
  <div class="px-5 bg-green-300 box">
    <div>TS Timer {$timer}</div>
    <div>Rx Timer {$rxTimer}</div>
  </div>
</div>
```

Before we can use PostCSS we need to tell Snowpack that we want to use it by adding it to the `"scripts"` property in `snowpack.config.json`.

```json
{
  "extends": "@snowpack/app-scripts-svelte",
  "scripts": {
    "plugin:ts": "@snowpack/plugin-babel",
    "run:tsc": "tsc --noEmit",
    "run:tsc::watch": "$1 --watch",
    "build:css": "postcss"
  },
  "devOptions": {},
  "installOptions": {}
}
```

Fire up the app and we should now have Tailwind support. Yay!

![Page with Taiwlind CSS](https://res.cloudinary.com/codechips/image/upload/v1589736909/page-v2_sokhae.webp)

## Building for production

The development flow is very fast, code reloads are blazing, but you are running on the local computer. Real applications will need to download all assets through a network. Let's see how Snowpack deals with bundling stuff for production by running `npm run build`.

![snowpack build output](https://res.cloudinary.com/codechips/image/upload/v1589736909/build-no-bundle_cnrnej.webp)

What's in the `build` folder now?

```text
$ tree -h build
build
├── [4.0K]  _dist_
│   ├── [2.3K]  App.js
│   ├── [  99]  index.js
│   ├── [3.5K]  main.css
│   ├── [3.7K]  main.css.proxy.js
│   └── [ 247]  timer.js
├── [1.1K]  favicon.ico
├── [ 882]  index.html
├── [1.1K]  logo.svg
├── [  67]  robots.txt
└── [4.0K]  web_modules
    ├── [4.0K]  common
    │   └── [ 97K]  zip-9224ffb3.js
    ├── [ 179]  import-map.json
    ├── [4.0K]  rxjs
    │   └── [154K]  operators.js
    ├── [ 30K]  rxjs.js
    └── [4.0K]  svelte
        ├── [ 50K]  internal.js
        └── [3.2K]  store.js

5 directories, 15 files
```

I can see that the CSS file is very small. It means that PurgeCSS is working as expected. We haven't bundle the app so everything will be served as modules. In a large app that might be a problem, many files. But they will be cached on subsequent requests.

![dev tools network tab](https://res.cloudinary.com/codechips/image/upload/v1589736909/console-network_l0lqja.webp)

Snowpack recommends that we add [Parcel](https://parceljs.org) as a bundler, so we will follow the recommendation and add it to the mix.

```text
$ npm add -D parcel-bundler && rm -rf build && npm run build
```

![console build with parcel](https://res.cloudinary.com/codechips/image/upload/v1589736909/build-bundle_lpmae8.webp)

Tailwind is spitting out some warnings, but we can safely ignore them. Let's inspect the `build` directory again.

```text
$ tree -h build
build
├── [4.0K]  _dist_
│   ├── [2.3K]  App.js
│   ├── [  99]  index.js
│   ├── [3.5K]  main.css
│   └── [ 247]  timer.js
├── [142K]  _dist_.337b8775.js
├── [543K]  _dist_.337b8775.js.map
├── [3.4K]  _dist_.5fe4eb89.css
├── [5.1K]  _dist_.5fe4eb89.css.map
├── [1.1K]  favicon.fadd3cc5.ico
├── [1.1K]  favicon.ico
├── [ 470]  index.html
├── [1.1K]  logo.svg
├── [  67]  robots.txt
└── [4.0K]  web_modules
    ├── [4.0K]  common
    │   └── [ 97K]  zip-9224ffb3.js
    ├── [ 179]  import-map.json
    ├── [4.0K]  rxjs
    │   └── [154K]  operators.js
    ├── [ 30K]  rxjs.js
    └── [4.0K]  svelte
        ├── [ 50K]  internal.js
        └── [3.2K]  store.js

5 directories, 19 files
```

The total size of the minified JS bundle in 142 kb. It seems unnecessary large for such small and simple app. I suspect that it's due that Snowpack uses Parcel as bundler. I've written a benchmarking post about most popular bundlers that you can read [here](https://codechips.me/rollup-vs-parcel-vs-webpack-for-svelte-development/).

Another thing that puzzles me is why Snowpack spits out the modules to build directory when we are not using them in production?

You can try to run the production bundle yourself by installing a simple web server like [serve](https://www.npmjs.com/package/serve).

```text
$ npx add -D serve && npx serve build
```

![web tools network with parcel bundle](https://res.cloudinary.com/codechips/image/upload/v1589736909/prod-bundle-console_yxxrm5.webp)

## Final Thoughts

Snowpack v2 is currently in beta. It's a moving target with almost daily changes, so expect API and things to break until it reaches stability. Overall, it gives developers a great DX with lots of tips and clever easy to understand stats. However, there are some things that I hope can be improved in the future.

- **New browser window opens every time I start the app**. I ended up with like five windows open. Not sure how to disable this behavior yet, but probably it's a very simple thing that I just missed.
- **Only support for Parcel bundler at the [moment](https://github.com/pikapkg/create-snowpack-app/issues/29)**. Parcel is not as efficient as Rollup and produces almost 7 times larger bundles. Hopefully, we will be able to freely choose between different bundlers soon.
- **Unclear build assets**. Not sure I understand what's what in the build directory when building for production. Looks like Snowpack is producing some files that shouldn't be there.

Some people seem skeptical of ES modules. [Svelvet](https://github.com/jakedeichert/svelvet) was a promising experimental Svelte bundler built on top of Snowpack, but the author decided to stop working on it after running some [networking benchmarks](https://github.com/jakedeichert/svelvet/issues/83). The browsers and network are becoming faster and faster, and with parallel requests, HTTP/2/3 and Brotli compression this should not be such a big issue as it seems now.

Personally, I am very exited about the project and [Pika](https://www.pika.dev/) ecosystem in general. Hopefully all the wrinkles will be ironed out soon, and when they are, this might become my new favorite setup for Svelte development.

As usual, you can find the example code in Github https://github.com/codechips/svelte-snowpack-testdrive

You should follow me on [Twitter](https://twitter.com/codechips) to know when a new post comes out and also to get some of my regular byte-sized wisdom.

---
Photo by Simon Matzinger on Unsplash