Sam Broner

Sam Broner

Software in Seattle & NYC

Trying out Snowpack with Fluid Framework's Hello World

Fluid Framework Tech

I had heard quite a bit about Snowpack, first as a faster build tool, but later as part of a larger toolchain ecosystem. So I went to https://www.snowpack.dev/#get-started to get started.

I had talked with my friend @FKS after hearing about the library. I was particularly interested in how Snowpack and Skypack could be used to make the explicit dependency model in the Fluid Framework easier to reason over and easier to develop. I also suspected it would speed up our developer inner loop, which is currently affected by some classic monorepo sluggishness. (Curtis Man even made us our own “build:fast” command, which may seem like overkill but works quite nicely).

Getting Started

The create-snowpack-app is very good. I decided to start with the @snowpack/app-template-blank-typescript because I vastly prefer typescript.

npx create-snowpack-app snowpack-test --template @snowpack/app-template-blank-typescript

I was sitting in my kitchen, which has relatively slow WiFi speeds, but the install was reasonable and the CLI dialogue was useful. Success. Hello World!

Heeelloooo World!

So what do we get?

The directory structure is somewhat bare. We have a src/index.ts which fetches a div from the root index.html file; a public with html, css, etc; and a decent number of configuration files.

Neat

There’s also a curious /types folder, which I hadn’t seen in other create-react-app style templaters, but seems designed to let me extend .d.ts files natively or import common file types natively instead of using webpack style plugins.

/* Use this file to declare any custom file extensions for importing */
/* Use this folder to also add/extend a package d.ts file, if needed. */

I think this could use some documentation because it felt like an unusual solution to pre-type common file types. I could see these typings being useful in the snowpack scenario. All dependencies get imported individually, so Snowpack better assume that we’ll use css-loader style semantics! A quick google makes this clear.

“Snowpack supports basic CSS imports inside of your JavaScript files. When you import a CSS file via the import keyword, Snowpack will automatically apply those styles to the page. This works for CSS and compile-to-CSS languages like Sass & Less.”

https://www.snowpack.dev/

In fact, Snowpack supports the following file types by default. Although I could have used some clearer documentation, this is in line with the type definitions from the .d.ts file and makes sense to me.

  • JavaScript (.js, .mjs)
  • TypeScript (.ts, .tsx)
  • JSX (.jsx, .tsx)
  • CSS (.css)
  • CSS Modules (.module.css)
  • Images (.svg, .jpg, .png, etc.)

The tsconfig file does make some of this clear. It imports the src and types directories. It references an unusual directory, “web_modules,” as a path.

"paths": { "*": ["web_modules/.types/*"] }

A quick google tells me this is an alternative nomenclature for node_modules, although it seems extra relevant because I know about Skypack, Snowpack’s npmjs-like (unpkg-like?) cousin. Paths is a way of indicating where certain files should be looked up from. I expected that web_modules would be created when I ran npm run build. It turns out web_modules is created, but it’s hiding in the build directory.

These files are all accessible from the dev server on npm run start e.g. I can see the canvas-confetti.js file at http://localhost:8080/web_modules/canvas-confetti.js This makes sense.

Even for a such a simple project the npm start felt unusually snappy although I didn’t do any experiments, but if we can trust the CLI, it says that npm start in the “Hello World” template typically build in 1-2 milliseconds (in watch mode.)

2ms!!

Before we get started moving the @fluid-example/hello-world to this folder, I did want to examine the snowpack.config.js file and the extension model.

Extensions & Plugins

The @fluid-example/hello-world uses HtmlWebpackPlugin by default to bind in HTML and the ts-loader to compile Typescript. Based on our earlier examination of the .d.ts and our assumptions about imports, I assume we don’t need to install an HTML parsing plugin. I couldn’t find documentation specifically indicating this, but the Snowpack.config.js file does use the mount keyword and reference a few folders. I assume there’s some automagic happening here to pull in the HTML.

The snowpack.config.js does include the @snowpack/plugin-typescript plugin. This feels pretty natural and a quick google says there’s a bunch of plugins. Many of them are officially supported. Very convenient having spent some time finagling community supported webpack plugins. I also like that “plugins” can just be CLI commands (This is worth digging into, but it allows you to use tools like postcss, eslint, etc directly from snowpack!). That feels like an obviously useful syntax for ad hoc definition of plugins.

I took a quick glance at the documentation for snowpack.config.js. It seems like there’s a decent amount that can be done here, but I didn’t take the time to understand it. I’m curious about the install, proxy, and alias configurations in particular. I can’t immediately imagine what I’d want to install at the snowpack level rather than the npm level, but this may be my misunderstanding of what’s actually going on under the hood. Either way, the template I used leaves most of these blank except mount and plugins as previously described.

With my preliminary understanding intact, I started porting over the Fluid Hello World. Step 1, install dependencies. Nothing to see here, this worked as expected. Next up… copy and paste the typescript files in. While we have previously done some weird things with the tsconfig, the template tsconfig and Fluid Hello World tsconfigs are pretty aligned. So I figure this should work.

And it mostly does seem to work. Snowpack does a much better job handling new files than webpack does. Webpack dev server always seemed to break when new files were added to the src code.

My only errors are around importing types, which we do a lot. It seems like I could modify the importsNotUsedAsValues flag in the tsconfig or just be more explicit in my imports. I chose the latter and resolved the five errors.

2ms!!

Errors resolved, I’m not quite out of the gate. I’m mishandling a dependency from Aqueduct. Aqueduct is Fluid’s convenience library (get it 😉?)

I know it's becoming standard, but I do like this error dialogue. While we’ll have to see how clear the instructions are, the error and source are both clear, I can click through the possible errors, and I can clear the error dialogue! What a feature! If I click the X, I see the default HTML of the page.

I’m also getting a ton of polyfill errors. This is a known usability issue with Fluid and one that we’re addressing. Snowpack made the resolution pretty easy, although I still had lingering issues with assert. Luckily @christianGo just fixed this last month and an upgrade to 0.29 Fluid libraries resolved the issue.

With all CLI and pop up errors gone, I tried to load again and hit a pernicious issue with our nifty TypedEventEmitter binding to scope. For some reason, the parent here doesn’t have the off parameter, so binding it to my scope fails.

Interestingly, I could try to catch and ignore this error because off is just an alias for removeListener. My guess is this is an artifact from the polyfill and a quick google confirms this. I opened a PR with a solution, but saved the polyfill locally to continue on with the experiment.

npm install --save-dev ~/Code/rollup-plugin-node-polyfills

And imported the rollup plugin from my dev dependencies.

module.exports = {
  mount: {
    public: '/',
    src: '/_dist_',
  },
  plugins: ['@snowpack/plugin-typescript'],
  install: [/* ... */],
  installOptions: {
    installTypes: true,
    rollup: {
      // @ts-ignore
      plugins: [require('rollup-plugin-node-polyfills')(
        {
          events: true,
          url: true,
          assert:true,
          querystring: true,
       }
      )],
    },
  },
  devOptions: {/* ... */},
  buildOptions: {/* ... */},
  proxy: {/* ... */},
  alias: {/* ... */},
};

Now past the initial polyfill issue, I quickly hit another. This time, my global variable is undefined in the buffer polyfill. Jeeze!

While I am generally curious why I’m hitting this issue, at this point I’m eager to get my familiar Fluid Hello World up and running, so I hard coded const global = {}, which would bypass the error message.

Review

All up... I was impressed and interested. I think there's DEFINITELY room for innovation on build tools that use ESModules more clearly.