Here’s the YouTube version of Migrating from Parcel to Snowpack if you would rather?

A couple of years ago I wrote about moving from Gulp to Parcel. Parcel has been a good tool for me. In the day job I’m typically building prototypes of new features. As what I build is almost never destined for a production environment, output of the build tool is less important to me than the ease of getting it running and it getting out of the way while I iterate on whatever I’m building.

So, suffice it to say, WebPack was never an option. 😉 But this is something Parcel excels at, living up to its promise of being almost configuration free.

Parcel has been moving to version 2 for some time. Now, I don’t know anyone in the Parcel team but from an outsiders point of view the transition from v1 to v2 has been a little chaotic. I’ve never been quite sure when (if!) v2 was ready or when the time to transition was right.

In the meantime, a newer tool called Snowpack has been piquing my interest, and to a lesser extent, Vite.

The USP of Snowpack is it does less. Rather than compile and transpile everything on every save, it only compiles the files that have changed. And it some cases, if your writing modern JavaScript, it won’t even transpile it, it will just send it ‘as is’, directly to the browser without writing anything to the file system at all.

We’ll look at what that means in practice in a moment but conceptually, this approach appeals to me a lot. I basically want my CSS (or PostCSS/Sass) processed, my TypeScript compiled and a fast reloading server spun up with as little configuration as possible. And I want my feedback loop as fast as possible as I iterate.

So I decided to try Snowpack. It’s version 3.2.2 as I write this.

Migrating from Parcel to Snowpack wasn’t quite as straightforward as I thought or hoped it might be. Whether that is due to naivety or ineptitude on my part, or a lack of clear documentation, I cannot say for sure. Probably all three in some measure. But this serves as a few notes that I hope saves others some time if you’re following a similar path.

I’ll start with the more important, conceptual differences between Snowpack and Parcel and move on to things like running Snowpack, getting Sass compiling quickly, and getting module import paths for scripts working.

There is no build destination

It took an embarrassing amount of time for me to get my head around how Snowpack fundamentally works.

In virtually every build tool I have used or pieced together myself (earliest post on this subject is from back in 2013!) over the last 9 years or so, you have a ‘src’ folder of some sort, where your ‘authoring’ SCSS/TS files live, and a ‘dist’ or ‘build’ folder where the compiled JS/CSS/Whatever files end up. The built in server uses the dist/build folder as the root it serves up in the browser.

And that’s essentially what Parcel does.

How Parcel works

What I love about Parcel is you don’t have to tell it where all your source files are. It is smart enough to look at what is referenced in your index.html, your entry point in build tool parlance, and pull things in from there. For example, your simplified index.html might look like this :

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, viewport-fit=cover"
        />
        <link rel="manifest" href="manifest.webmanifest" />
        <title>Parcel</title>
        <link rel="shortcut icon" href="./faviconAlt.ico" />
        <link rel="stylesheet" type="text/css" href="styles.scss" />
    </head>

    <body>
        <script src="app.ts"></script>
    </body>
</html>

To be clear here, notice how with Parcel, the index.html is referencing the authoring languages (Sass and TypeScript) for the styles (styles.scss) and scripts (app.ts)? That’s how Parcel is able to deduce all the things it needs to pull in and compile.

It reads those initial files and then reaches out its build tool tentacles, like some ‘file system Kraken’, and drags in all needed dependencies. It then does its magic, transforming these dependencies, and spits that all out to a ‘dist’ folder with all files at pretty much the same root level, regardless of how your files and folders are organised when authoring.

So, given a folder with this kind of (simplified) hierarchy:

index.html
styles.css
app.ts
interface/
  component-one/
    component-one.scss
    component-one.ts
  component-two/
    component-two.scss
    component-two.ts
mockserver/
  server-responses.ts
  mock-data.json
global-images/
dist/

Parcel will flatten the files, and cache bust the names, as needed, into that dist/ folder. For example, you might end up with something like this, with components compiled down into that single app.7a013482.js file, and all styles compiled into the styles.d3ll04e9.css:

index.html
styles.d3ll04e9.css
app.7a013482.js

So that’s the essence of Parcel.

How Snowpack works

Things are a little different in Snowpack land. In Snowpack land, your index.html file needs to reference the transformed version of the files – even though they don’t exist on your file system.

Wait, what?

Let me say that again as it’s jolly important. You link to files that don’t exist. Here is the index file as it would exist in Snowpack land:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, viewport-fit=cover"
        />
        <link rel="manifest" href="manifest.webmanifest" />
        <title>Snowpack</title>
        <link rel="shortcut icon" href="./faviconAlt.ico" />
        <link rel="stylesheet" type="text/css" href="/styles.css" />
    </head>

    <body>
        <script src="/app.js"></script>
    </body>
</html>

See how we are linking to files in the root with the forward slash? Yes, that /app.js and /styles.css doesn’t actually exist on your file system. I found that utterly bonkers. Not in a bad way – in a magical, ‘what the actual f*** is this doing’ kind of way.

When you are in dev with Snowpack, you will not see files written. They are transformed (if needed) not onto the file system, but only in memory, for the browser to consume directly. This changes when you build (npx run build) – at that point Snowpack goes the extra distance and writes things into a build folder. But to keep things fast in dev land it doesn’t bother. That’s a good thing!

Snowpack mount options

By default, Snowpack will assume your entire project directory is a website, and will mount and scan the entire project for dependency imports. That may be what you want, but more likely you want some folders available but not others – maybe you have a folder for internal documentation for example, or a document with all the production site SQL database passwords as plain text – I’m JOKING! Don’t do that – I’m just checking you’re paying attention!! The point is we only want some folders ‘served’.

To facilitate this, when moving go Snowpack, I found it useful to reorganise things a little. Where, with Parcel, I had my authoring files in the root, and Parcel would dutifully create a dist folder to send the compiled and bundled output to, with Snowpack, I moved all my authoring files to a src folder.

Now, you might also want a folder for static assets, which I did, and called ‘public’ you can tell Snowpack to just serve that up ‘as is’ – that ‘public’ naming seems to be an established convention with the Snowpack examples I have looked at so I rolled with it. But be aware there isn’t a right way to organise and name things, just whatever works for you.

So to illustrate, my simplified setup for Snowpack I had this kind of file structure:

src/
  index.html
  styles.css
  app.ts
  interface/
    component-one/
      component-one.scss
      component-one.ts
    component-two/
      component-two.scss
      component-two.ts
  mockserver/
    server-responses.ts
    mock-data.json
public/

And here is a basic mount setup to achieve this:

mount: {
    public: { url: "/public", static: true },
    src: "/",
},

Couple of things worth noting. I’m using the expanded object notation for the public folder here. I want that in this case because static: true allows me to tell Snowpack that I don’t want anything in that folder transformed. The default for static is false.

So what this means in practice is if I have my index.html inside the src folder, and I want to path to a favicon, for example, that lives in the public then it needs to be written like this:

<link rel="icon" type="image/svg+xml" href="/public/faviconBF.svg">

The /public there in the link tag corresponds to the /public in the value of the public key of the mount object.

That means that when Snowpack is spun up with:

npx snowpack dev

The http://localhost:8080/ address is going to serve up the index.html correctly from the src folder and reference the assets in the public correctly.

Note that we’ve used a root relative link there for the icon. Hold on to that thought when we get to module importing in a moment.

The benefits of serving up your source files

I also found it useful with Snowpack that as it is serving up your source files, if you don’t want the hassle of JS routing for simple prototypes/POCs you can just use your source file structure. So, say I want a ‘benefits’ page, I can just create a benefits/index.html inside the src folder and link to that and I get a nice sensible url to navigate to and display e.g. http://localhost:8080/benefits/

If however you do want to use it with a SPA using a JS router, you probably want this in your snowpack.config.js file:

routes: [{ match: "routes", src: ".*", dest: "/index.html" }],

Which basically says, don’t do anything with any routes – just keep them on the index page.

That pretty much covers the basic principals of Snowpack, next I want to get into the nitty-gritty of some particulars. Things that differed when moving a codebase to Snowpack and some optimisations I think are worth making – in particular if you want nice and fast Sass compiles.

TypeScript. Without TypeScript

If you are coming from Parcel and writing in TypeScript, there’s a fair chance you will have a tsconfig.json file that TypeScript uses to build out your TypeScript. Well, you can likely just go ahead and delete that. Snowpack uses Babel to transform your TypeScript so it bypasses your typical TypeScript config – or worse, tries to do something with it, modules wise, based on that config file that no longer makes sense.

So the short version is, if you don’t need the sophistication of tsc doing the compiling, you link to your resultant JS file and Snowpack will have been smart enough to transform it for you via Babel which is considerably faster. You can tell it to compile with TSC but my TypeScript needs are so modest for the most part I can live without it.

Module resolution

Parcel forced me to start writing my JavaScript as modules. That improved things no end for me, even as someone rarely writing anything for a ‘production’ environment.

However, depending how you ‘pathed’ your modules in Parcel, getting them all to talk to one another in Snowpack land may be problematic. It was an easy but laborious fix in my case. The Parcel project I work on day to day has heaps of individual components, typically a JS/TS file alongside a CSS/Sass file. The HTMl side of things gets handled in the TS/JS by lit-html (which I still love BTW).

As there are nested components its typically been easiest to write import statements like this:

import { clone } from "~server/utilities/rfdc";

Using the tilde to reference the project root. But you don’t seem able to do root relative paths in Snowpack. Whether, I start them with a / or a tilde or the main folder, they just straight up don’t behave. Or at least that’s been my experience so far from failing to get them working and reading they didn’t work in the Snowpack Discord.

You need all your import paths to be written relative. So that prior import might need to be rewritten as:

import { clone } from "../../../server/utilities/rfdc";

And you can also use the current folder ./ type too. But no way, it seems, to write root relative paths. I can’t be sure, this may just be some config I have neglected to uncover. If someone knows otherwise, please enlighten me.

Honestly, this was the most time consuming part of the migration. With a hundred TypeScript files to ‘repath’ it was simple but tedious to fix.

Sass is straightforward, and can be fast

By default, Sass is slower to build in Node than the existing PostCSS CSS processing setup I had that was giving comparable functionality. However, Sass gives fewer setup and onboarding considerations. Less moving parts if you will. I’m all about less moving parts!

Sass is a simple addition to Snowpack. You just need to add it as a plugin:

npm i @snowpack/plugin-sass

Now, don’t stop there. Sass has a much faster, Dart based compiler Snowpack can use instead of the default npm based version.

Make Sass great again!

The Sass Snowpack plugin enables you to configure Snowpack to call out to the Dart Sass compiler. You’ll need to install the Dart compiler for Sass. I’m on macOS so used the Homebrew option:

 brew install sass/sass/sass

With that installed we can configure Snowpack to use Dart instead.

At this point, if you haven’t already, you will need to make a snowpack.config.js file, which you can do by running snowpack init. Here is how mine looks at this point:

module.exports = {
    mount: {
        public: { url: "/public", static: true },
        src: "/dist",
        /* ... */
        // static: { url: "/static", static: true, resolve: false },
    },
    plugins: ["@snowpack/plugin-sass"],

};

We need that plugin option amended to look something like this:

plugins: [
    [
        "@snowpack/plugin-sass",
        {
            native: true,
            compilerOptions: {
                style: "compressed",
            },
        },
    ],
],

Now, pay attention 007, notice the extra square brackets in there, around the plugin? Don’t forget those all you will get an error like, TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received an instance of Object but obviously who would be so stupid as to miss those and spend 30 minutes trying to figure out why it isn’t working, right?

With that in place your Sass will be using Dart to compile, and your compiles will be fast and you will look upon your build tool and be happy(er).

You can see with that object we are also passing options to the Dart compiler; in this case I’m asking for the output to be compressed. You could just leave that bit out though if you’d rather.

Summary

Snowpack is a little bit of a sea-change from traditional build tools. Conceptually just a little to the left of what you are perhaps used to.

If you are starting a greenfield project I’d have zero qualms opting for Snowpack. It doesn’t have the depth of support documentation or stack overflow questions if you find yourself in the weeds, but generally speaking, it is solid enough to pick up and run with.

Migrating an existing project, which was setup for a different build tool, Parcel in my instance is, as expected, significantly more problematic.

I’m out the other side but I’ve not used Snowpack for long enough to judge categorically whether jumping onto Snowpack instead of continuing with Parcel was worth the hassle. Upgrading the same project to Parcel v2 was pretty straightforward in comparison.

But I do appreciate this subtle shift in tools like Snowpack that do as little as possible. I hope to see this trend continue. Next up, I’m off to give Vite a whirl.

Learn to use CSS effectively, 'Enduring CSS' is out now

Get $5 off HERE ↠

Write and maintain large scale modular CSS and embrace modern tooling including PostCSS, Stylelint and Gulp