Creating an Elm Project with Snowpack
Tags: elm, programming, functional
Published: 2020-11-28 17:15:49

# Intro

There is a new emerging trend of a development setup in the frontend world. As browsers now natively support esm modules, we don't need to use bundlers like [Webpack](https://webpack.js.org/), [Parcel](https://parceljs.org/) or [Rollup](https://rollupjs.org/).

Projects like [Snowpack](https://www.snowpack.dev/), [Vite](https://github.com/vitejs/vite) (for [Vue](https://vuejs.org/)), [Svite](https://github.com/dominikg/svite) (for [Svelte](https://svelte.dev/)) and recently [SvelteKit](https://svelte.dev/blog/whats-the-deal-with-sveltekit) (official one for Svelte) are starting to gain attention.

By skipping the bundling stage, the browser is immediately updated with the changes in the source files, without having to wait for re-bundling.
 
# Snowpack

Described in its own words as:

> ... a lightning-fast frontend build tool, designed for the modern web.

Snowpack has an out-of-the-box support for TypeScript, JSX, CSS Modules and more. It also can be extended with plugins, and there is a plugin for Elm: [Snowpack Elm Plugin](https://github.com/marc136/snowpack-plugin-elm) by [Marc Walter](https://github.com/marc136). Thank you Marc!

Today I would like to share a setup of Snowpack with [Elm](https://elm-lang.org/) and [TailwindCSS](https://tailwindcss.com/) - a very popular utility-first CSS framework.

# Project Setup

Let's create our project from scratch, as it is described in the documentation' ["Starting a New Project"](https://www.snowpack.dev/tutorials/getting-started)

```
mkdir elm-snowpack
cd elm-snowpack
npm init -y
npm install snowpack --save-dev
```

Those will perform several things:
* create `elm-snowpack` folder
* cd into it
* create `package.json` with predefined fields without prompting us (the `-y` or `--yes`)
* install Snowpack version `3.x.y`.

Now let's add `index.html` to the root of our project with the following content:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <h1>Welcome to Snowpack!</h1>
  </body>
</html>
```

and add a `start` script entry to the `package.json` file:

```json
  "scripts": {
    "start": "snowpack dev",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
```

with that ready let's start the Snowpack development server:

```
npm run start
```

We see some Snowpack's output:

![console output](https://dev-to-uploads.s3.amazonaws.com/i/vu74w905vimqyzpwm4kr.png)
  
The browser opens automatically (if it will not - open it manually and navigate to `http://localhost:8080`).

And we see our beautiful application:

![snowpack first run](https://dev-to-uploads.s3.amazonaws.com/i/wsm29nufdvb6d08qldzh.png)

Following the documentation, let's add an `index.js` and `index.css` files.

Create both those files in the root of our project:

### `index.js`
```js
console.log('Hello World!');
```

### `index.css`
```css
body {
  font-family: sans-serif;
}
```

And update `index.html` to include them:
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />
    <!-- add index.css -->
    <link rel="stylesheet" type="text/css" href="/index.css" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <h1>Welcome to Snowpack!</h1>
    <!-- add index.js -->
    <script type="module" src="/index.js"></script>
  </body>
</html>
```

After saving all the files, the browser is updated with our changes.

![browser output with js and css](https://dev-to-uploads.s3.amazonaws.com/i/ugrapkhcqxdzqkj1ymvv.png)
 
## Reorganizing

Having all the files in the root of our project is not practical. Usually source files reside in `src` folder. So let's reorganize our files.

Stop the Snowpack development server. Create an `src` folder and move `index.js` and `index.css` there. Then create `public` folder and move `index.html` there.

```
elm-snowpack/
  node_modules/
  public/
    index.html
  src/
    index.css
    index.js
  package.json
  package-lock.json
```

Since we have moved our files around, we need to inform Snowpack where to find them. Snowpack can be configured from the command line or by adding [`snowpack.config.js`](https://www.snowpack.dev/reference/configuration) to the project folder. So let's add this file in the root of our project and add the default content from the documentation:

```js
module.exports = {
  plugins: [
    /* ... */
  ],
  installOptions: {
    /* ... */
  },
  devOptions: {
    /* ... */
  },
  buildOptions: {
    /* ... */
  },
  proxy: {
    /* ... */
  },
  mount: {
    /* ... */
  },
  alias: {
    /* ... */
  },
};
```

This file can also be added automatically by Snowpack:

```
npx snowpack init
```

The entry we are interested in is [`mount`](https://www.snowpack.dev/reference/configuration#config.mount) and the example setting in the documentation is exactly suits our setup:

```js
// ...omitted
  {
    "mount": {
      "src": "/dist",
      "public": "/"
    }
  }
// ...omitted
```
You can look for the examples of how this mapping works in the documentation, but basically it maps requests of `/dist/*` to serve files under `src/*` (`index.js`, `index.css`) and requests of `/*` to serve files under `public/*` (`index.html`).

According to this mapping our `index.html` will be server from `/public` folder when requested as `/` and it's good, but our source files under `src/` folder should be requested from `/dist`. So let's change their URIs in the `index.html`:

```html
<link rel="stylesheet" type="text/css" href="/dist/index.css" />
```

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

### `index.html`

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />
    <!-- update URI to index.css -->    
    <link rel="stylesheet" type="text/css" href="/dist/index.css" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <h1>Welcome to Snowpack!</h1>
    <!-- update URI to index.js -->
    <script type="module" src="/dist/index.js"></script>
  </body>
</html>
```
Starting our development server should show our application as before.


## Adding Elm Support

Great, we have an empty application running and the changes that we make are propagated to the browser with lightning speed!

Let's add Elm support now.

As I mentioned in the beginning - There is a [Snowpack Elm Plugin](https://github.com/marc136/snowpack-plugin-elm) by [Marc Walter](https://github.com/marc136).

We need to install it (as a dev dependency using `-D` flag):
```
npm install -D snowpack-plugin-elm
```

And add it to the `plugins` entry in the `snowpack.config.js`:

```js
module.exports = {
  // ...
  plugins: ['snowpack-plugin-elm'],
  // ...
}
```

as it is described in the plugin's repo.

Good, now let's add `src/Main.elm` with the mandatory "Hello World!":

```elm
module Main exposing (main)

import Html exposing (Html)

main =
    Html.text "Hello, World!"
```

and import our Elm application in `src/index.js`:

```js
import Elm from './Main.elm';

console.log(Elm);

console.log('Elm and Tailwind with Snowpack');
```

If we run our application now (or it was already running) we'll see an error message that, very rightfully, tells us that we didn't initialize an Elm project with `elm.json` file:

![Elm not initialized](https://dev-to-uploads.s3.amazonaws.com/i/fsb3aktdaiwjxj0ru6xu.png)

So let's do exactly that:

```
elm init
```

Click `Enter` to agree with the prompt. This will create the `elm.json` file.

Running the application again shows no errors. And we see in the console that the Elm application is indeed created:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/745uc08xnpmjk2y4w7kk.png)

The wiring of the Elm application is described in the guide's [JavaScript Interop/Embedding in HTML](https://guide.elm-lang.org/interop/). We can copy the code from there to our `src/index.js` (I have deleted all the `console.log`):

```js
import Elm from './Main.elm';

const app = Elm.Main.init({
  node: document.getElementById('app'),
});
```

We also need to update our `index.html` file to have a placeholder for our Elm application:

```html
<div id="app"></div>
```

So now our `index.html` looks like this:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Elm with Snowpack" />  
    <link rel="stylesheet" type="text/css" href="/dist/index.css" />
    <title>Elm with Snowpack</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/dist/index.js"></script>
  </body>
</html>
```

After saving the file the browser is instantly updated and shows the "Hello, World!" - our application is working!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z90gosmfv2m5woalwh9n.png)

Try editing the text and saving it. The browser updates really fast!

# Adding Tailwind CSS

Now let's add a Tailwind CSS to our project. We partially follow the [Installing Tailwind CSS as a PostCSS plugin](https://tailwindcss.com/docs/installation#installing-tailwind-css-as-a-post-css-plugin) as it described in the documentation, only instead of `postcss` we'll install `postcss-cli`, that Snowpack needs.

```
npm install -D tailwindcss autoprefixer postcss-cli
```

The default presets of Tailwind CSS are absolutely sufficient to start playing with prototyping components/design. But in order to further customize Tailwind CSS we will need a configuration file - `tailwind.config.js`. So as an optional step we can create it by running: 

```
npx tailwindcss init
```

Let's replace the content of our `src/index.css` with the standard TailwindCSS presets:

```css
@tailwind base;

@tailwind components;

@tailwind utilities;
```

Let's also create `postcss.config.js` file:

```js
module.exports = {
  plugins: [
    require('tailwindcss'),
    require('autoprefixer'),
  ],
};
```

We tell PostCSS to use `tailwindcss` and [`autoprefixer`](https://autoprefixer.github.io/) (that is another plugin that add browser specific prefixes for the relevant CSS rules).

Good, now we need to tell Snowpack to execute the PostCSS when needed. Following the official guide on [PostCSS](https://www.snowpack.dev/guides/connecting-tools#examples), update `snowpack.config.js`, `plugins` entry:

```js
plugins: [
  [
    '@snowpack/plugin-build-script',
    { cmd: 'postcss', input: ['.css'], output: ['.css'] },
  ],
  'snowpack-plugin-elm',
],
```

And that should enable TailwindCSS in our application.

NOTE: There is also an official way to setup [TailwindCSS with Snowpack](https://www.snowpack.dev/guides/tailwind-css/), but it did not work for me.

I took a chunk of user card HTML from the Tailwind CSS website's (version 1.9) landing page and pasted it in our `public/index.html`. In addition, I have updated the `avatar` image with an online placeholder and added `shadow-lg mx-10` to the `<div>` in order to see the shadow and for the card not to spread all the way to the edges of the window:

```html
<!-- ... -->
<body>
  <div id="app"></div>
  <div class="md:flex bg-white rounded-lg p-6 shadow-lg m-10">
    <img
      class="h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6"
      src="https://i.pravatar.cc/100?img=1"
    />
    <div class="text-center md:text-left">
      <h2 class="text-lg">Erin Lindford</h2>
      <div class="text-purple-500">Product Engineer</div>
      <div class="text-gray-600">erinlindford@example.com</div>
      <div class="text-gray-600">(555) 765-4321</div>
    </div>
  </div>
<!-- ... -->
```

And after the page updates, we see Tailwind CSS in action:

![user card long](https://dev-to-uploads.s3.amazonaws.com/i/3zhhqr78yikak39x3uzc.png)

This style is responsive. Try narrowing the browser's window and see the card changes:

![user card short](https://dev-to-uploads.s3.amazonaws.com/i/04gqcxkxtemujrssl1qh.png)

We can re-create this HTML in our Elm application and delete the raw HTML:

```elm
module Main exposing (main)

import Browser
import Html exposing (Html, div, h2, img, text)
import Html.Attributes exposing (class, src)


type alias User =
    { fullName : String
    , position : String
    , email : String
    , phone : String
    , avatar : String
    }


erinLindford : User
erinLindford =
    { fullName = "Erin Lindford"
    , position = "Product Engineer"
    , email = "erinlindford@example.com"
    , phone = "(555) 765-4321"
    , avatar = "https://i.pravatar.cc/100?img=1"
    }


userCardView : User -> Html msg
userCardView user =
    div [ class "md:flex bg-white rounded-lg p-6 shadow-lg m-10" ]
        [ img
            [ class "h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6"
            , src user.avatar
            ]
            []
        , div
            [ class "text-center md:text-left" ]
            [ h2 [ class "text-lg" ] [ text user.fullName ]
            , div [ class "text-purple-500" ] [ text user.position ]
            , div [ class "text-gray-600" ] [ text user.email ]
            , div [ class "text-gray-600" ] [ text user.phone ]
            ]
        ]


update : msg -> User -> User
update _ model =
    model


view : User -> Html msg
view model =
    userCardView model


main : Program () User msg
main =
    Browser.sandbox { init = erinLindford, update = update, view = view }
```

Delete the raw HTML from `index.html` and refresh the page - the result is the same as before.

And at last we have a nice development setup with Snowpack, Elm and TailwindCSS.

Although the step-by-step tutorial may seem long, it's pretty quick to re-create this setup.

Hope you enjoyed creating our small project and find this useful.

Thank you for reading.