snowpack: experimenting with a new build tool

2021-09-19

 | 

~7 min read

 | 

1238 words

Snowpack is a new(er) addition to the build tools available on the front-end.

I had the opportunity to start a new project recently and I was trying to get up and running quickly. I’d heard great things about Snowpack, so I thought I’d give it a shot.

Like CRA (create-react-app), Snowpack has a general CLI that can be called without actually needing to install it (using npx or yarn create). And, like CRA, Snowpack has a growing library of templates.

To be clear, CRA is still a great project, however, between it being in maintenance mode after 2.0 and the intentional limitations on the project, it no longer appears to be the best solution for production apps today.1

So, what is it like actually getting started with Snowpack?

Well, for my project I wanted to use a few different technologies:

  1. React & Typescript
  2. Sass
  3. PostCSS
  4. MirageJS
  5. Jest

React & Typescript Template

The first two technologies on my list can be handled in one fell swoop using a template:

% npx create-snowpack-app my-project --template @snowpack/app-template-react-typescript
# or
% yarn create snowpack-app my-project --template @snowpack/app-template-react-typescript

I will say this part confused me initially. The syntax for Snowpack CLI to add a a template is @snowpack/app-template-<name>. All of the templates are included in the create-snowpack-app repo.

SASS

Adding SASS to a project requires adding the dependency and updating the snowpack.config.js:

% yarn add @snowpack/plugin-sass
snowpack.config.js
module.exports = {
  plugins: [["@snowpack/plugin-sass"]],
}

In my case, I installed sass globally so that I could take advantage of the speed increases of not using the pure JS implementation. (Instructions.)

This did mean that my config looked slightly different:

snowpack.config.js
module.exports = {
  plugins: [
    [
      "@snowpack/plugin-sass",
      {
        native: true,
      },
    ],
  ],
}

PostCSS

Adding PostCSS is similarly direct:

  1. Install @snowpack/plugin-postcss,
  2. Install postcss
  3. Install any plugins
  4. Update the snowpack.config.js

    snowpack.config.mjs
    // snowpack.config.mjs
    export default {
    +  plugins: ['@snowpack/plugin-postcss'],
    };
  5. Create and add any plugins to postcss.config.js

    postcss.config.js
    // postcss.config.js
    module.exports = {
     plugins: [
       // Replace below with your plugins
       require("cssnano"),
       require("postcss-preset-env"),
     ],
    }

MirageJS

MirageJS is a fantastic way to test your endpoints and iterate quickly.

In order to get it running, first we need to get some dependencies installed:

% yarn add -D miragejs @types/node

In order to get the node types to take, we need to update tsconfig.json:

tsconfig.json
{ "types": [, /*... others */ "node"] }

In my case, I was also using environment variables, some of which were injected from the command line and some of which were stored in a local .env file, to determine when to use a Mirage server.

For the latter, Snowpack has a plugin to install, @snowpack/plugin-dotenv:

% yarn add -D @snowpack/plugin-dotenv

And which requires configuration in the snowpack.config.js:

snowpack.config.mjs
// snowpack.config.mjs
export default {
  plugins: ["@snowpack/plugin-dotenv"],
}

An additional note regarding environment variables in Snowpack. Snowpack does not have access to process.env. In order to read environment variables then, we need to replace all of the references with import.meta.env. This, though, is not quite sufficient.

All environment variables in Snowpack must be prefixed with SNOWPACK_PUBLIC. This is intended to remind authors that these variables will be shared with the world.

So, if you previously had

if (process.env.FOO) {
}

You would now have:

if (import.meta.env.SNOWPACK_PUBLIC_FOO) {
}

Unfortunately, the documentation suggests there’s a magic string that’s accessible __SNOWPACK_ENV__, which never worked for me.

For typescript, I also added @types/snowpack-env as a dev dependency.

From here, we could follow a standard setup for MirageJS in React.

Jest

I initially thought I’d want/need Jest. After all, it’s a fantastic test runner and I’ve spent a lot of time learning how to configure Jest for Javascript applications.

It made sense to me that that’s where I’d start. But it wasn’t all smooth sailing.

ERROR: /Users/stephen.weiss/code/sandbox/react-snowpack-ts-ds/src/index.tsx: Support for the experimental syntax 'jsx' isn't currently enabled (7:3):

After a few more hiccups like that, I came to realize that even if I got it to work (by adding a Babel config, which also mostly isn’t necessary with Snowpack, etc.), I wasn’t going to get much benefit from it.

In part, this is because Snowpack currently recommends different tools for pieces that Jest normally handles. For assertions, I’d be using mocha and chai. For helpers, I could use testing-library. And for a test runner, Snowpack’s recommendation is @web/test-runner, which the team says is a faster, more reliable alternative with fewer dependencies and which takes advantage of the Snowpack build pipeline.

So, I’m leaving this section here mostly to say it’s not necessary and that hopefully next time I read this before I go down the path of trying to set it up.

Rollup

Snowpack is a build tool, not necessarily a bundler. Here’s a pretty good guide.

Snowpack recommends two different approaches:

  1. Built-in with esbuild
  2. Plugins (webpack, rollup, etc. ) It’s working on optimizing its bundles with esbuild, but that’s still not quite ready for production.

Unfortunately, I don’t think I’ll be able to use esbuild until it’s more mature.

Since I was migrating from TSDX, which uses Rollup by default, I thought I’d just steal the config and tweak it as needed.

This is a project for another day, however.

Conclusion

Getting up and running with Snowpack was amazingly easy.

The biggest issues I ran into were:

  1. When I didn’t trust the recommended defaults (@web/test-runner) and pretended I was smarter (by installing Jest).
  2. Understanding how to read environment variables
  3. Once I had my environment variables, remembering to set them! (I went down quite a rabbit hole trying to understand why MirageJS wasn’t working and it was because I wasn’t actually creating the server!)

On the other hand, once I got up and running, it was mostly smooth sailing.

The Snowpack bundler is much more particular than others I’ve used in the past. Two notable examples:

  1. I was using a library with a peerDependency on axios. In the past, I would have gotten console warnings telling me that axios was missing and that I should install it. Snowpack goes further and forces its installation - refusing to build the app if it’s not present. Normally, I’d love this - except that in this particular instance I knew axios was mislabeled as a peer dependency. Of course, this also meant I could go back to the library maintainers and try to get this fixed.
  2. The same library also isn’t using ESM, which meant that importing its components was not as straightforward - nor did it match the library’s recommended approach. Fortunately, Snowpack has some great documentation on some of the most common problems. In my case, the solution was to simply import from the root and drill down:

    // Recommended... but doesn't work
    import { Button as LibButton } from "my-lib"
    const { default: Button } = LibButton
    
    // This works though!
    import lib from "my-lib"
    const { default: Button } = lib.Button

Finally, I still have work when it comes to configuring a production grade bundle, but I know it’s possible. So, I can come back to that when it’s time to actually ship!

Footnotes

  • 1 I’m really not passing a value judgment here, but rather parroting Dan Abramov’s perspective.

Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!