The PET Stack - Part I
We are living in weird times right now. The Corona Virus is shutting down many parts of public life world-wide. Today is the first weekend of a two-weeks-long government-mandated stay-at-home period in the German state of Bavaria. Time to find something productive to do from home. I have worked on 8+ web apps in the past year. Today I want to share my favorite way of making them with you.
There are many tooling choices to make, when starting a new project. Which
language or JavaScript flavor to use? How to do CSS? And how do we package
our application?
The JavaScript ecosystem offers lots of options, so many in-fact it can be quite
overwhelming at times. I have tried out and have worked with many tools, but I
couldn't be happier about the combination of tools, that I have found for myself
right now, which I have started calling the PET Stack
. It's ParcelJS +
Elm + TailwindCSS.
I will describe how to use each of them in more detail in this post and throughout
the next couple of posts.
P is for Parcel
Bundlers package your app typically into an HTML, a JS and a CSS file at build time into something browsers can run efficiently. So most likely, if you're building apps for the browser, you're gonna need a bundler. You're gonna want to have a dev-server with hot-reloading to preview your progress and you're gonna want small and optimized bundles for production. Parcel can do all of that for you in a super simple way. It has out-of-the-box support for many different languages and packages HTML, CSS, JavaScript and luckily among others: Elm.
Here's a typical config file for Parcel:
# emptiness ... tumbleweed rolling along ...
Ok, I admit, the joke was not great, but it's true Parcel has great defaults and does not even need any configuration in most cases. Let's see how to set up a simple build with live reloading and transpiling JavaScript with babel.
Project setup
First, we'll set up a new project and install Parcel
so it can be built.
If you're following along, create a new folder and do the following in a terminal.
npm init -y
npm i -D parcel-bundler
Now, open up package.json
in your editor of choice and add a couple of scripts.
{
"scripts": {
"start": "parcel src/index.html",
"build": "parcel build src/index.html"
}
}
This way we're telling parcel to run a dev server on npm start
.
Running npm run build
will produce a production build. Both commands reference
a src/index.html
file, which we will create in a second. Parcel will
automatically take care of injecting necessary hot reloading runtimes for
development and performing minification for production.
Next, let's add some actual content.
Adding a basic structure
We're gonna need three files for now and will put them into a src
directory
under the project root, like this:
.
|-src
|-index.html
|-style.css
|-index.js
Let's add index.html
with a basic structure. It is used as Parcel's entrypoint
in our npm
commands. Note how we reference the other two files here.
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trying out Parcel</title>
<!-- we are referencing our stylesheet here-->
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- we will need this div later -->
<div id="app"></div>
<!-- we are referencing our js file here-->
<script src="./index.js"></script>
</body>
</html>
Now we need a basic stylesheet. Nothing special yet, we just wanna get our first build working.
h1 {
text-decoration: underline;
background-color: lightgray;
border-radius: 0.25rem;
text-align: center;
padding: 0.25rem;
}
Here's some dummy JavaScript, which simulates our web app until we add something fancier.
window.addEventListener('load', () => {
document.querySelector('#app').innerHTML =
`<h1>
Hello from JS!
</h1>`;
});
Finally, run npm start
in your terminal and you should see parcel building
everything and firing up a dev-server on http://localhost:1234
. Open your
browser and make sure you're seeing this:
That's what our web app looks like at the moment.
What just happened?
Wow, that was quick. How does Parcel know what to do? Parcel uses a set of great defaults and a little info it got from us, to give us what we need. But let's go through what happened step-by-step.
- We call Parcel through
npm start
, where we told Parcel to usesrc/index.html
as the entrypoint. - Parcel grabs
index.html
and finds a link tostyle.css
. We are not doing any CSS processing at the moment but Parcel will just include the CSS as is. - Parcel also finds a script tag which points to
./index.js
and also includes it in the final bundle. If you peek around in the browser dev tools, you'll see that the JS file has been transpiled down to ES5. That's because Parcel usesbabel
by default and applies thepreset-env
to it. If you add a.babelrc
you can configure Babel's behavior if you need more transforms than that. - Parcel swaps out the
src
andhref
attributes in the final html output and replaces them with links to the final JS and CSS file. Parcel also injects hot module replacement runtimes and source maps into the output build. Try to change any one of the files we added, you should see the change immediately in the browser. Wohoo, hot-module-replacement for free 🥳.
We got a lot of what you would want from a bundler with zero-configuration from Parcel.
Now, let's look at the production build. npm run build
. This time we can
find the production assets inside the dist/
folder. Open them up and you'll
find that they all have been minified without us having to do anything for it.
Parcel even takes care of adding a hash like src.3e2ab70.js
to the files, which is great for setting long-term caching headers. (Side note:
I found this post by Jake Archibald about http-caching
best practices very good.)
Adding Elm to the mix
So far we built our HTML, JS and CSS. But I prefer to build apps with a functional language with strong type safety. If you have never heard about Elm and if I had to describe it, I would say, Elm is how I want programming to feel.
Enough praise for now. First let's do the following in a terminal.
npm i -D elm # install the elm compiler
npx elm init # create answer all the prompts with the default values
This will create an elm.json
file, which works similar to a package.json
file
only for Elm. Next we'll create a src/Main.elm
file with the following
content.
module Main exposing (..)
import Html exposing (h1, text)
main =
h1 [] [ text "Hello from Elm" ]
This is a tiny Elm program that simply renders a <h1>
tag with some text to the screen.
Last thing we need to do is initialize our Elm app from JavaScript. For this
we open src/index.js
and replace the contents with the following.
import { Elm } from './Main.elm';
Elm.Main.init({ node: document.querySelector('#app') });
Parcel will automatically compile our Elm code to JavaScript and import it.
The second line with then initialize the Elm app into out app
container.
Now, run npm start
again. Our Elm code is running correctly if you see this:
Elm says Hello.
Recap
We saw that Parcel let's us build our app with basically zero-configuration.
We checked out development and production builds. We added an Elm asset,
which was also immediately built by Parcel and the resulting JS was bundled up
into our build output. Compared to other tools like Webpack
, we
had the benefit of not having to write a config file or having to install and
configure a bunch of plugins.
So overall, we pieced everything together with Parcel pretty quickly and easily.
Some more noteworthy things about Parcel:
- Parcel also has support for Webmanifest, Service-Worker, JSON, Web Assembly, environment variables and a ton more, as shown on the docs page.
- Parcel v2 coming out. You can follow the progress on the road map.
Throughout the rest of this series, I'll continue to show how to build an application with Parcel, Elm and TailwindCSS. In Part II, we'll add some Elm code to make interesting things happen on the screen, while letting the compiler make sure we stay clear of bugs and bad practices.