loading...
Cover image for Snowpack with Svelte, Typescript and Tailwind CSS is a very pleasant surprise

Snowpack with Svelte, Typescript and Tailwind CSS is a very pleasant surprise

codechips profile image ilia mikhailov Originally published at codechips.me on ・9 min read

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 for some time now - an (un)bundler that leverages the power of the ES modules. Personally, I like the ESM 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 ES modules.

Ok, ok, but what does it actually mean? This is Snowpack's elevator pitch taken straight from its website.

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, announced the pre-release of Snowpack v2 and also posted a link to a bunch of starter templates - Create Snowpack App (CSA). One of those was a template for 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.

$ 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

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

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, Babel's Typescript preset and the actual Typescript compiler.

$ 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.

{
  "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.

{
  "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.

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

Just for the kicks, let's install RxJS and use that too. I am curious to see how this will play out with Snowpack's production builds later.

$ 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.

// 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.

<!-- 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

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, 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.

$ npm prepare # or npx snowpack

We should now have Typescript support. How nice!

web tools sources tab

Let's climb higher and try to enable PostCSS together with TailwindCSS.

Adding Tailwind CSS

Tailwind is a popular atomic CSS framework that I often use and since Snowpack promise us PostCSS support we should be able to use Tailwind.

# 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.

// 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 built-in, a library that removes unused CSS. We need to tweak our tailwind.config.js file a bit to use that.

// 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.

/* src/main.css */

@tailwind base;

@tailwind components;

@tailwind utilities;

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

import `./main.css`;

Add a few Tailwind classes to the "box" div in our App.svelte.

<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.

{
  "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

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

What's in the build folder now?

$ 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

Snowpack recommends that we add Parcel as a bundler, so we will follow the recommendation and add it to the mix.

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

console build with parcel

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

$ 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.

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.

$ npx add -D serve && npx serve build

web tools network with parcel bundle

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. 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 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. 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 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 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

Posted on Apr 16 by:

codechips profile

ilia mikhailov

@codechips

CTO. Maker. Friend. Manager by day, hacker by night. A big Svelte.js fan. #rxjs #firebase #js

Discussion

markdown guide