Build a Chrome Extension with Parcel + Preact + TailwindCSS
Tags: parcel, preact, tailwindcss, extension
Published: 2020-11-12 10:00:07

## Background
Recently, I started a [toy project](https://chrome.google.com/webstore/detail/cbgokcojacngdokhalmlajpdjjkbdeob) of building a Chrome Extension helper tool for myself. As a rookie developer, it took me a while to set up the project. So I think it is better to document it with this post as a learning note.

## Why [Parcel](https://parceljs.org) + [Preact](https://preactjs.com) + [TailwindCSS](https://tailwindcss.com) stack?
Preact and TailwindCSS are light-weight, which is good for a small app like Chrome Extension. I never try Parcel before, but it is claimed that it is having a better benchmark than the popular Webpack. To be honest I am not sure about that, but the **zero configuration** of Parcel really got me. So, why not give it a go? 

Also feel free to check their official site from the above links for more details.
_____________________________________________

##Begin the setup

Before the setup, I have prepared all the files mentioned in this post in [this repo](https://github.com/kevinwong150/parcel-preact-tailwindcss-chrome-extension-demo), though we are about to go through them.

Let's begin the setup! Everything starts from an empty directory.

### 1. Prepare basic files `package.json` and `manifest.json`
Init a `package.json` with yarn, add flag `-y` to default answer yes to all questions:
`yarn init -y`

Then create `manifest.json`:
```json
{
  "name": "Chrome Extension boilerplate",
  "version": "1.0",
  "description": "Chrome Extension boilerplate with Parcel + Preact + TailwindCSS",
  "manifest_version": 2,
  "browser_action": {
    "default_icon": "static/icon_32.png"
  },
  "icons": {
    "48": "static/icon_32.png"
  },
  "permissions": [
    "tabs",
    "contextMenus"
  ],
  "background": {
    "scripts": ["./src/background.js"]
  },
  "options_ui": {
    "page": "./src/options.html",
    "open_in_tab": true
  }
}
```

also create other three files `/src/background.js`, `/src/options.html`, `/src/main.html` with very simple content

`/src/background.js`:
```js
// /src/background.js
// create context menu item, with simple onclick event that brings users to the main.html
chrome.contextMenus.create({
  id: "main_page",
  title: "Open Main Page", 
  onclick: () => {
    chrome.tabs.create({  
      url: chrome.runtime.getURL("src/main.html")
    });
  },
  contexts: ["browser_action"]
}, () => {});
```

`/src/main.html`:
```html
<!-- /src/main.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Main</title>
  </head>
  <body>
    this is main page
  </body>
</html>
```

`/src/options.html`:
```html
<!-- /src/options.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Options</title>
  </head>
  <body>
    this is options page
  </body>
</html>
```
_____________________________________________

### 2. Set up Parcel
add parcel package:
`yarn add parcel-bundler --dev`

(optional)add helper package for copying static files to generated files' destination:
`yarn add parcel-plugin-static-files-copy --dev`

Then add `"scripts"` and `"staticFiles"` to `/package.json`:
```json
{
  "name": "setup-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "clear": "rm -rf dist .cache",
    "watch": "yarn clear && parcel watch src/{main.html,options.html,background.js} -d dist/src --public-url ./ -t node --bundle-node-modules",
    "build": "yarn clear && parcel build src/{main.html,options.html,background.js} -d dist/src --public-url ./"
 },
  "staticFiles": {
    "staticPath": [
      {
        "staticPath": "static",
        "staticOutDir": "../static/"
      },
      {
        "staticPath": "manifest.json",
        "staticOutDir": "../"
      }
    ]
  },  
  "devDependencies": {
    "parcel-bundler": "^1.12.4",
    "parcel-plugin-static-files-copy": "^2.5.0",
  }
}
```


Note: if you decide not to install the static files plugin, just do it yourself by changing the `"scripts"` to:
```json
"scripts": {
  "clear": "rm -rf dist .cache",
  "watch": "yarn clear && mkdir dist && cp -R static dist/static && cp manifest.json dist/ && parcel watch src/{records.html,options.html,background.js} -t node --bundle-node-modules -d dist/src --public-url ./",
  "build": "yarn clear && parcel build src/{records.html,options.html,background.js} -d dist/src/ --public-url ./ && cp -R static dist/static && cp manifest.json dist/"
},
```

####Test the setup 
So now if we run:
`yarn build`

a `/dist` folder will be generated, go to browsers' extensions page(`chrome://extensions/` for Chrome browser) and "Load unpacked", then select the `/dist` folder

Check if the code is working:
i. load unpacked of the /dist folder in Extension page
![In Extension page](https://i.imgur.com/oBRnH7Y.png "Should be able to find this in Extension page")

ii. open extension context menu and click this "Open Main Page"
![right click and click the item](https://i.imgur.com/pP7GJ8N.png "Should be able to find this item in extension context menu")

iii. check the content and celebrate🎉
![check the opened tab](https://i.imgur.com/l0YuGgn.png "Should be able to find the correct content")
_____________________________________________

### 3. Set up Preact
add preact package:
`yarn add preact`

then add below `"alias"` to `package.json`:
```json
"alias": {
  "react": "preact/compat",
  "react-dom/test-utils": "preact/test-utils",
  "react-dom": "preact/compat"
}
```

create file `.babelrc`:
```json
{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "pragma": "h",
        "pragmaFrag": "Fragment"
      }
    ]
  ]
}
```

finally create a file `/src/js/app.js`:
```jsx
// /src/js/app.js
import { h, render, Component } from "preact";
 
let Header = () => {
  return (
    <header>This is header by Preact</header>
  )
}
 
document.addEventListener("DOMContentLoaded", () => {
  render(<Header />, document.body);
});
```
and in `main.html` if we change the body to:
```html
<body>
  <script src="./js/app.js"></script>
</body>
```

rebuild and check again🎉🎉
![here comes the Preact header!](https://i.imgur.com/yJndQNV.png "Should be able to see the Preact header")
_____________________________________________

### 4. Set up TailwindCSS
add tailwindcss package:
`yarn add tailwindcss --dev`

init config files for tailwindcss:
`npx tailwindcss init -p` (might have to `yarn global add tailwindcss` before this)

then you would find `postcss.config.js` and `tailwind.config.js` generated:
```js
// /postcss.config.js
module.exports = {
 plugins: {
   tailwindcss: {},
   autoprefixer: {},
 },
}
```

you will want to uncomment the `purgeLayersByDefault: true,` and also add 
```js
purge: [
  './src/**/*.js',
],
```
into `tailwind.config.js` like this:
```js
// /tailwind.config.js
module.exports = {
  future: {
    // removeDeprecatedGapUtilities: true,
    purgeLayersByDefault: true,
  },
  purge: [
    './src/**/*.js',
  ],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}
```

It's almost done, but I would love to write nested css for a more neater stylesheet. However, I failed at setting up the postcss plugin `postcss-nested` because of reasons. Glad to be a failure this time, I soon realized Parcel has [built-in support for LESS](https://parceljs.org/less.html)! 

so we just have to go ahead and create the `/src/style/app.less`:
```css
/* /src/style/app.less */
@tailwind base;
@tailwind components;
 
/* remember to put customize css before importing the tailwind utilities */
body {
  header { /* test nested feature */
    @apply flex items-center justify-center text-lg; /* test tailwind utilities */
  }
}
 
@tailwind utilities;
```

also add some styles to the preact `<header>` to see if it works in `app.js`:
```jsx
<header class="h-24 bg-blue-200">This is header by Preact</header>
```

don't forget to link the stylesheet into the `main.html`:
```html
<link rel="stylesheet" href="./style/app.less">
```

let's do our final testing, flexbox works + text is large + background color pale blue and height is right, awesome🎉🎉🎉
![Preact header with TailwindCSS!](https://i.imgur.com/hsTz4uu.png "Should be able to see the styled Preact header")

_____________________________________________

## Issues and Reflection
### Issue 1: Multiple Entry Point
So usually a bundle tool takes a single entry point for compilation, so do most of the Chrome Extension tutorial on the internet. Because the `manifest.json` has defined the file path, including the `main.html`, `options.js` and `background.js`, so I think I have make them all as entry points. It took me quite some time to figure out how to do it, so even though I am not actually doing anything meaningful with the `options.html` during the above setup, I would like to document it since it is one of my big lesson here.

### Issue 2: The Parcel version now does not support the latest PostCSS version 8
It was putting me on trouble, it limited me on using various PostCSS plugins like `postcss-nested`/`postcss-purgecss`/`postcss-import`. I tried using older version of these plugin but it was strangely not working. Then I figure out different way of doing the nesting CSS: __Using LESS!__ Parcel native support for LESS is just amazing. Also we can do CSS purging by simple configuration setting in `tailwind.config.js` now in the latest version of TailwindCSS, life saver.

### Bonus Issue 3: Develop in Windows 10
Usually I do development by Mac at my workplace, but at home I use Windows, which caused a lot of troubles even when installing the packages. So what I do to overcome this, is using Docker.
With `Dockerfile`:
```docker
# pull official base image
FROM node:10-slim

WORKDIR /app

# install parcel
RUN yarn global add parcel-bundler

# install app dependencies
COPY package.json ./
RUN yarn install

# add app
COPY . ./


# Windows steps:
# Step 1: from host to container
# docker cp . demo-container:app/

# Step 2: build in container
# yarn build

# Step 3: from container to host
# docker cp demo-container:app/dist .
```
I use it like a VM just to compile the `/dist`, though it might not be convenient to copy files back and forth. Good enough for a noob programmer I think.
_________________________

Thanks for reading such a long post. 
My way of setting up the project is absolutely not perfect at all, and IMO it is kinda silly on the Docker part. 
__Welcome to leave your comments and feedback, they will help me a lot for becoming a better developer!👍👍__