im

Snowpack for Svelte development revisited

I tested Snowpack for Svelte before v2 was released. Has anything changed since?

As I am writing this I realize that I have no good introduction. This is another post about finding the best bundler for Svelte and this time it's Snowpack's turn to be evaluated.

Building a good bundler is not easy. Building a fast bundler is almost impossible. Mad props to those who dare.

I discovered Snowpack before version 2 was officially released and was very excited about it. Today I decided to revisit it to see how and if things have changed since.

My goal is to compare different bundlers for Svelte development in order to find the best one. I've already tested Vite, Svite and now it's Snowpack's turn.

Requirements#

For the purpose of evaluation I created a simple web app. Its functionality is simple. You press a button and it fetches a random Kanye West tweet from Kanye as a Service.

Kanye Says app screenshot

While simple, the application has interesting parts.

I also have a list of my own requirements when it comes to bundlers.

Let's see if Snowpack can satisfy all of them.

Bootstrapping the app#

Snowpack comes with many official framework templates that you can use as base for your apps. Since the app is written in TypeScript we will use @snowpack/app-template-svelte-typescript. It's not listed on the website, but in Github.

$ npx create-snowpack-app svelte-snowpack-typescript \
--template @snowpack/app-template-svelte-typescript --use-pnpm
$ cd svelte-snowpack-typescript
# add test app's dependencies
$ pnpm add -D date-fns svelte-inline-svg
$ pnpm start

I like how Snowpack lets me specify the package manager. pnpm, the package manger I often use, is not in the main documentation, but I found the option somewhere in Github.

Dev server starts instantly and Snowpack is nice enough to open the browser for me. I appreciate it, but I don't like it or need it.

Another thing I like about Snowpack is that it uses npm start script and not dev to start everything up. The npm run dev command is so 2015, right?

I also noticed that app template's config file has changed from JSON to JS. I like it because it makes it easy to comment and do other advanced JS gymnastics.

module.exports = {
mount: {
public: '/',
src: '/_dist_',
},
plugins: [
'@snowpack/plugin-svelte',
'@snowpack/plugin-dotenv',
'@snowpack/plugin-typescript',
[
'@snowpack/plugin-run-script',
{cmd: 'svelte-check --output human', watch: '$1 --watch', output: 'stream'},
],
],
install: [
/* ... */
],
installOptions: {
/* ... */
},
devOptions: {
// don't open browser
open: 'none',
// don't clear the output
output: 'stream'
},
buildOptions: {
/* ... */
},
proxy: {
/* ... */
},
alias: {
/* ... */
},
};

So far so good.

What's in the box?#

Let's see what files Svelte template generates for us.

$ ls -1
babel.config.json
jest.config.js
jest.setup.js
LICENSE
node_modules
package.json
pnpm-lock.yaml
public
README.md
snowpack.config.js
src
svelte.config.js
tsconfig.json
types

Not bad. I don't know why Babel config has to be explicit and I am usually not writing any unit tests, so jest has no use for me. But other that that it looks nice and clean.

The app entry file comes with an explicit HMR section. I like that it's explicit and how it tells you where to find more information. Good!

import App from "./App.svelte";

var app = new App({
target: document.body,
});

export default app;

// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://www.snowpack.dev/#hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose(() => {
app.$destroy();
});
}

By now I've copied the code from my test web app, added missing dependencies and tried to start the dev server. Things don't quite work yet.

External Svelte libraries#

This is where I hit the first road block. Looks like Snowpack uses pkg.module file if it exists. The svelte-inline-svg library I am using had the module property defined, but the file was missing in the NPM package. Showpack borked on this. I wish that it could have fallbacks in place and try with something else if the file is missing. The author of the SVG library has fixed it after I filed a bug report.

PostCSS + TailwindCSS#

Snowpack has no PostCSS support out-of-the box, because in Snowpack it's all about plugins and build scripts, but it offers instructions on the main documentation page on how to enable PostCSS. But turns out there is some ambiguity. The main documentation webpage tells one thing while Github tells another. Which one to trust?

Alright, let's go with the second one and use Snowpack's plugin-postcss. Not sure I understand why I have to explicitly install postcss and postcss⁠-⁠cli. Wouldn't it be better if they were baked into the Snowpack's PostCSS plugin?

$ pnpm add @snowpack/plugin-postcss postcss postcss-cli

Next, we need to add the plugin to snowpack.config.js.

...
plugins: [
...
'@snowpack/plugin-postcss',
],
...

Talking about plugins. Snowpack has quite strange plugin configuration. You can either supply plugin as string, or as an array if plugin needs some arguments, where first item is the name of the plugin and second is an object of the arguments that will be passed to the plugin.

[
'@snowpack/plugin-run-script',
{ cmd: 'svelte-check --output human', watch: '$1 --watch', output: 'stream' },
],

Let's install and setup Tailwind and friends.

$ pnpm add -D tailwindcss postcss-preset-env
$ pnpx tailwind init

Replace Tailwind configuration file with the following contents.

// tailwind.config.js

module.exports = {
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
experimental: {
uniformColorPalette: true,
extendedFontSizeScale: true,
},
purge: {
content: ['./src/**/*.svelte', './public/*.html'],
whitelistPatterns: [/svelte-/],
},
theme: {
extend: {},
},
variants: {},
plugins: [],
};

And postcss.config.js with this contents.

// postcss.config.js

const tailwind = require('tailwindcss');
const purge = require('@fullhuman/postcss-purgecss');
const cssnano = require('cssnano');
const presetEnv = require('postcss-preset-env')({ stage: 1 });

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

module.exports = { plugins };

Environment Variables#

Svelte template comes with dotenv pre-packaged as Snowpack plugin. This is nice as it doesn't require a working dotenv environment in the shell.

The ES2020 import.meta.env works out of the box, but not in my Vim editor.

svelte + ts error in vim

VS Code works fine and you get autocompletion too. I figured that coc⁠-⁠svelte plugin I am using is way behind VS Codes Svelte extension. As soon as I forked and updated dependencies, Vim stopped complaining.

NOTE: If you are using Vim, coc.nvim and coc-svelte extension, you can use mine. It's more up-to-date than the original one @codechips/coc-svelte.

You have to prefix all env variables with SNOWPACK_ if you want them to be accessible in the app. Most bundlers today follow this convention in order to avoid environment variable clashes.

For my app I had to create an .env.local file so that dotnenv could pick it up, but any dotenv convention works.

# .env.local
SNOWPACK_PUBLIC_KANYE_API=https://api.kanye.rest

Snowpack and svelte.config.js#

As I understand svelte.config.js file is mostly only used by code editors for autocompletion. Not with Snowpack. Snowpack is one of the few bundlers that relies on that file to exist. It's used by its Svelte plugin. Good thing that it comes with the template.

Snowpack, Svelte and TypesScript#

TypeScript is nice, but getting TypeScript configuration right is usually a game of trial and error for me. Snowpack's Svelte template has a dependency on the @tsconfig/svelte NPM package, but from what I can see it's not used anywhere?

The svelte-inline⁠-⁠svg module does not come with any TypeScript declaration. We need to fake it locally.

Create svelte-inline⁠-⁠svg.d.ts in the types directory with the following contents.

declare module 'svelte-inline-svg';

By the way, I really like how the svelte⁠-⁠check tool, which comes already injected into the build pipeline, warns me about it and provides instruction on what to do.

I also like that Snowpack has a dedicated types directory for your own TypeScript definitions. Somehow, I always struggle to configure type paths. Snowpack is the first bundler that makes this clear for me.

Building#

The test app is now finally working! Lets build it for production.

$ pnpm build
> snowpack build
...

[@snowpack/plugin-typescript] src/index.ts(2,17): error TS2307: Cannot find module './App.svelte' or its corresponding type declarations.
[@snowpack/plugin-typescript] Error: Command failed with exit code 2: tsc --noEmit
src/index.ts(2,17): error TS2307: Cannot find module './App.svelte' or its corresponding type declarations.
 ERROR  Command failed with exit code 1.

Oops. Something is wrong, but what? I actually never figured out what, so I recreated the whole project from scratch, which helped. Who told you that this was going to be easy, right?

When building Snowpack shows you a nice stats table of all the build artifacts.

Snowpack stats table

But what's in the build?

tree -h build
build
├── [4.0K] _dist_
│   ├── [1.4K] App.js
│   ├── [4.0K] assets
│   │   ├── [ 899] usa.svg
│   │   └── [ 40] usa.svg.proxy.js
│   ├── [ 14K] index.css
│   ├── [ 15K] index.css.proxy.js
│   ├── [ 334] index.js
│   ├── [1.5K] Time.js
│   ├── [ 539] timer.js
│   ├── [ 721] Wisdom.css
│   ├── [1.0K] Wisdom.css.proxy.js
│   └── [5.9K] Wisdom.js
├── [1.1K] favicon.ico
├── [ 882] index.html
├── [1.1K] logo.svg
├── [ 67] robots.txt
├── [4.0K] __snowpack__
│   └── [ 126] env.js
└── [4.0K] web_modules
├── [4.0K] common
│   └── [ 16K] index-2f302f93.js
├── [ 82K] date-fns.js
├── [ 273] import-map.json
├── [4.0K] svelte
│   ├── [ 448] internal.js
│   ├── [2.0K] store.js
│   └── [ 308] transition.js
├── [4.9K] svelte-inline-svg.js
└── [ 59] svelte.js

6 directories, 24 files

You have __dist__ folder with all your application files, __snowpack__ folder with the env variables, and web_modules folder with all the dependencies.

Snowpack bundles your assets (CSS, SVG) files as JS files, the .proxy.js ones. I wonder why it also puts raw SVG and CSS file in the build directory if they are not used in production.

The dependencies are not bundled, but server to browser as ES modules. After all, Snowpack is an ESM bundler.

This has its advantages and drawbacks. The development is super-fast as only the files changes need to be recompiled and reloaded. The files and dependencies are not optimized and served as is, which leads to many more requests if you have many dependencies and also to larger downloads as you need to download the whole module even if you only use one function in it.

You might need to only do it once on the first load, then the files might be cached, but I am not the expert on this.

Bundling a Snowpack project#

Ah! The ultimate test for a bundler. How efficient bundles a bundler can produce. Snowpack doesn't have bundler support built-in, but there are some plugins that we can use.

Some time while ago Snowpack released an optimization plugin. It's not a bundler plugin, but more of a minifier. It helps you minify CSS, HTML and JS. From what I have read, this plugin uses ESBuild's minifying functionality and from what I understand it's almost on par with Rollup's.

Enough talking, let's test!

$ pnpm add -D @snowpack/plugin-optimize

We need to add it to the list of plugins in snowpack.config.js and do a new build.

Snowpack stats table after optimization

Strange. I see no difference in the stats table output.

The terminal output is saying that file size didn't change, but if I look at the actual file on disk it's actually 21K and not 83K as Snowpack tells me.

$ tree -h build
build
├── [4.0K] _dist_
│   ├── [ 768] App.js
│   ├── [4.0K] assets
│   │   ├── [ 899] usa.svg
│   │   └── [ 40] usa.svg.proxy.js
│   ├── [5.0K] index.css
│   ├── [ 14K] index.css.proxy.js
│   ├── [ 251] index.js
│   ├── [ 860] Time.js
│   ├── [ 305] timer.js
│   ├── [ 554] Wisdom.css
│   ├── [ 893] Wisdom.css.proxy.js
│   └── [2.7K] Wisdom.js
├── [1.1K] favicon.ico
├── [ 416] index.html
├── [1.1K] logo.svg
├── [ 67] robots.txt
├── [4.0K] __snowpack__
│   └── [ 126] env.js
└── [4.0K] web_modules
├── [4.0K] common
│   └── [6.1K] index-2f302f93.js
├── [ 21K] date-fns.js
├── [ 273] import-map.json
├── [4.0K] svelte
│   ├── [ 421] internal.js
│   ├── [ 564] store.js
│   └── [ 217] transition.js
├── [4.8K] svelte-inline-svg.js
└── [ 54] svelte.js

6 directories, 24 files

Weird. I don't know if it's cached somewhere or if the numbers are showing something else.

Testing @snowpack/plugin-webpack#

Looks like Snowpack's plugin-optimize does some light optimization, but Showpack also has a real official bundler plugin - plugin-snowpack. It actually use to have an official Parcel plugin, but looks like it has been removed.

Last time I tried Snowpack's Parcel and Snowpack plugin none of them worked. Has anything changed? Let's try Webpack plugin.

Nope. Still getting the same "'babel-loader' not found" error from it.

Entry module not found: Error: Can't resolve 'babel-loader'

It installed it, but it didn't help. I give up. Don't feel like I want do go down the rabbit hole.

As of Rollup, no official Rollup plugin exists (yet), but Svelte crew promised that it's coming soon.

Building an SWC compiler plugin for Snowpack#

Snowpack is all about plugins and they have great documentation on how to build your own. They actually encourage people to build them.

I decided to test and build an SWC compiler plugin for TypeScript files to see how hard it would be. Just for fun.

Let's get dirty. Install the SWC compiler and create a plugins directory.

$ pnpm add -D @swc/core && mkdir plugins

In the new plugins directory create a file named plugin⁠-⁠swc.js with the following contents.

// plugins/plugin-swc.js

const swc = require('@swc/core');
const fs = require('fs');

module.exports = function (snowpackConfig) {
// read options from the main Snowpack config file
const useSourceMaps = snowpackConfig.buildOptions.sourceMaps;

return {
name: 'snowpack-swc',
resolve: {
input: ['.ts'],
output: ['.js'],
},
async load({ filePath }) {
// read the TypeScript file
const contents = await fs.promises.readFile(filePath, 'utf-8');

// transform it with SWC compiler
const output = await swc.transform(contents, {
filename: filePath,
sourceMaps: useSourceMaps,
isModule: true,
jsc: {
parser: {
syntax: 'typescript',
},
target: 'esnext',
},
});

return {
'.js': {
code: output.code,
map: output.map,
},
};
},
};
};

Now, add it to the Snowpack config.

	...
plugins: [
'@snowpack/plugin-svelte',
'@snowpack/plugin-dotenv',
'@snowpack/plugin-typescript',
'@snowpack/plugin-optimize',
'@snowpack/plugin-postcss',
'./plugins/snowpack-swc.js',
[
'@snowpack/plugin-run-script',
{ cmd: 'svelte-check --output human', watch: '$1 --watch', output: 'stream' },
],
],
...

Start the dev server and be amazed. Your TypeScript files are now being transpiled by the SWC compiler.

Well, I am not actually 100% sure about that, because Snowpack comes with ESBuild plugin baked-in and I don't know if my SWC plugin overrides it or not.

Anyhow, this was for demonstration purpose only to show how easy it is to build a plugin in Snowpack with only a few lines of code.

If you look at Snowpack's official plugins, most of them are really simple to understand and only a few hundred lines long.

I really like this plugin approach. It makes it very easy to build your own and developers have already started building all sorts of plugins.

Requirements Revisited#

Sorry for getting side-tracked. The test web app now actually works. Let us revisit the list of initial requirements and see how well Snowpack did.

Plugins Mentioned#

Conclusion#

Snowpack's underlying build pipeline design is very good. It feels thought through.

There are a few parts that I like. I like that it uses start script as the entry point to start the dev server. It always felt more intuitive to me than npm run dev.

When the dev server is disconnected the web app does not spam Dev Tools console log trying to reconnect to the dev server.

SvelteKit, the newly announced Svelte framework that will replace Sapper, is betting on Snowpack. According to Svelte's crew they evaluated Vite.js as an alternative, but found it not to be framework agnostic enough.

Snowpack does not have any official bundler plugin that works with Svelte, but as I understand the Svelte gang will work on making a Rollup plugin. Don't know if it will be only as a part of SvelteKit's build pipeline or an official framework-agnostic Snowpack Rollup plugin.

But maybe Rollup plugin is not needed at all? From what I've read esbuild has tree shaking functionality that is almost on par with Rollup's. I don't think that it's used in Snowpack yet and don't know either if there are any plans to implement it.

As I wrote earlier, the official Webpack plugin didn't work for me, but since it's easy to build plugins in Snowpack and the documentation is good, I bet that we can expect a lot of new and interesting plugins from the developer community in the near future. Like this one for example, Svelte with Snowpack with Google Closure Compiler.

Overall, Snowpack is an interesting and future-glancing project with a helpful community and a fierce development speed. But somehow it feels like it's rushing forward a little too fast for its own good. A lot of features feel half-finished and documentation, although great, often lags behind or is outdated.

Sorry Snowpack, I like you, but I will use another bundler for my Svelte projects for now. But don't be sad! I will come and visit in a few months. I am sure it will be a nice visit.

You can find the final code here https://github.com/codechips/svelte-typescript-setups