Front-End development without node_modules
Tags: javascript, snowpack, node, webdev
Published: 2021-02-08 14:06:30

Once upon a time, we could simply put an html and a script file into an FTP server, quickly have a working website and call it a day.

Today, we have to jump through many hoops just to get the right things in the right places. Suppose Alice wants to spend her weekend making a simple to-do app or whatever little idea she enjoys. First, she has to install a big heap of 10k npm packages files. Then she spends a few hours searching how to get this week's js bundler working with the latest typescript with the latest trending UI framework. And a lot frustrating when things don't work or the articles have just outdated. Once she has actually started building the first feature for her little fun app, the weekend has mostly gone!

But things are changing...

---

# 1. A little backstory: CommonJS and ES Module

Working with NodeJS, we all get familiar with CommonJS, the standard (read: legacy) way for NodeJS to load dependency code. After installing a module, for example `lodash`, we can load it into our code by using `require('lodash')`. This is how NodeJS handles dependency code from the beginning:

```js
const {snakeCase} = require('lodash');

['HelloWorld', 'left pad', 'ECMAScript'].forEach(text => {
  console.log(snakeCase(text));
});
```

```js
// somewhere in lodash package
function snakeCase(input) {
  // where magic happens
}

exports.snakeCase = snakeCase;
```

ECMAScript 2015 (ES6) introduced ES Module - an official, standardized module system for JavaScript. It took a while to get here. Nowadays, [all major browsers](https://caniuse.com/es6-module) and NodeJS ([since v13.2.0](https://nodejs.medium.com/announcing-core-node-js-support-for-ecmascript-modules-c5d6dc29b663)) ship support for ES Module by default. ES Module has the advantage of static analysis, tree shaking and asynchronous.

```js
import {snakeCase} from 'lodash';

['HelloWorld', 'left pad', 'ECMAScript'].forEach(text => {
  console.log(snakeCase(text));
});
```

```js
// somewhere in lodash package
export function snakeCase(input) {
  // where magic happens
}
```

In NodeJS, to enable ES Module, we have two choices: use **.mjs** extension or set `"type": "module"` in **package.json**. And while most development tools understand ES Module, there are still many incompatibles. For example, TypeScript still [does not support outputting to .mjs files](https://github.com/microsoft/TypeScript/issues/18442). Or Vercel [does not work with ES Module](https://github.com/vercel/next.js/issues/9607). So some transpilers and workarounds are still required. Hopefully the situation will change soon™.

Many packages in NodeJS are already shipped with ES Module files. But many packages are not. At the time of this writing, in [the top 10 depended packages on npm](https://www.npmjs.com/browse/depended), only [tslib](https://www.npmjs.com/package/tslib) supports [ES Module file](https://github.com/microsoft/tslib/blob/e7a115533a28b90e48139e77462e0b5812983847/package.json#L25) by including `"exports"` in *package.json*. Many other top packages still don't ship ES Module: lodash, react, moment, request, axios, chalk, commander, express... It's actually not a problem for NodeJS, because NodeJS allows using [`import` to work with both ES Module and CommonJS format](https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs).

But browsers don't have that privilege. What if you want to import your favorite node module in the browser? Well, you have to be lucky. At the time of this writing, [the recommended way](https://reactjs.org/docs/getting-started.html#add-react-to-a-website) for React to get started in the browser is to include the UMD version in `<script>` tag, and use the global variable `window.ReactDOM`:

```html

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script>
  ReactDOM.render(
      <h1>Hello, world!</h1>,
      document.getElementById('root')
  );
</script>
```

No ES Module for Alice.

# 2. Skypack

[Skypack](https://skypack.dev) is a wonderful CDN service that transpiles node packages to be able to work well in the browser. It's backed by Snowpack team. Simply put the package **name@version** after **cdn.skypack.dev** and you are ready to go:

```html
<script type="module" src="myscript.js"></script>
```

```js
// myscript.js
import {snakeCase} from 'https://cdn.skypack.dev/lodash@4';

['HelloWorld', 'left pad', 'ECMAScript'].forEach(text => {
  console.log(snakeCase(text));
});
```

It just works! Of course, you may ask, there is "[lodash-es](https://www.npmjs.com/package/lodash-es)" that we can import. But many packages don't have their doppelgängers. Or they are not frequently updated. Here, [skypack.dev](https://skypack.dev) comes to the rescue.

There are still some problems, though. For unclear reasons, some versions did not work. When visiting [cdn.skypack.dev/react@16](https://cdn.skypack.dev/react@16), the React version 17 is served instead. But the future is bright. Alice can now start working right on her app without spending most of her weekend configuring this week's js bundler ...

_**Side note**: I also put my own version at [espkg.vercel.app/react@16](https://espkg.vercel.app/react@16). You can
use [espkg.vercel.app](https://espkg.vercel.app) as an alternative until Skypack fixes the problem. Other packages also work, for example, [espkg.vercel.app/lodash@4](https://espkg.vercel.app/lodash@4) (give it a little time for building, then the response will be cached by Vercel)._

# 3. Snowpack

Okay, I lied, a bit. *TypeScript* won't work directly in the browser. You still need more work. Here come the real power of [Snowpack](https://snowpack.dev): minimal config and remote packages. You don't even have to install *node_modules* to start working with your little fun app. Simply run 2 setup commands:

```bash
yarn global add snowpack
snowpack init
```

This will give you an empty skeleton **snowpack.config.js**. Then add the single line config `source: 'remote'` under `packageOptions`:

```js
// snowpack.config.js
packageOptions: {
  source: 'remote',
},
```

That's all! Now run `snowpack dev` and start adding your **index.html** and **myscript.ts** (yes, it's *TypeScript*):

```html
<!doctype html>
<html lang="en">
<head>
  <title>My little app</title>
</head>
<body>
<script type="module" src="myscript.js"></script>
</body>
</html>
```

```typescript
// myscript.ts
import {snakeCase} from 'lodash';

const words: string[] = ['HelloWorld', 'left pad', 'ECMAScript'];
words.forEach(text => {
  console.log(snakeCase(text));
});
```

It just works! 🎉 Look ma, no *node_modules*! No *package.json*! We even got *TypeScript* and *hot-reload* for free. Yay!

---

# Recap

The example code can be downloaded here: [gist.github.com/olvrng](https://gist.github.com/olvrng/e9729d550cb5c2e023ed0c1f8290978f). There are other configs on [snowpack.config.js](https://www.snowpack.dev/reference/configuration) that you may need. Let's save that for another day. Now start tinkering with your app and spend your precious time on the most valuable feature! 🚀🚀

---

# P.S.

Oh, but Alice wants to use **[less](http://lesscss.org)**. No worry, just a single line config... I swear! She could add a file **mystyle.less** and one more line for **snowpack.config.js**. Everything will be okay. Well, this time she must remember to run `yarn add snowpack-plugin-less`! Only this time...

```js
// snowpack.config.js
plugins: [
  'snowpack-plugin-less',
],
```

```typescript
// myscript.ts
import {snakeCase} from 'lodash';
import './style.less'; // the less file

const words: string[] = ['HelloWorld', 'left pad', 'ECMAScript'];
words.forEach(text => {
  console.log(snakeCase(text));
});
```

Thank you for reading! And don't forget my little page [espkg.vercel.app](https://espkg.vercel.app).