DEV Community

loading...
Cover image for Build a Chrome Extension with Parcel + Preact + TailwindCSS

Build a Chrome Extension with Parcel + Preact + TailwindCSS

kevinwong150 profile image kevinwong150 ใƒป6 min read

Background

Recently, I started a toy project 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 + Preact + TailwindCSS 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, 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:

{
  "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
  }
}
Enter fullscreen mode Exit fullscreen mode

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

/src/background.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"]
}, () => {});
Enter fullscreen mode Exit fullscreen mode

/src/main.html:

<!-- /src/main.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Main</title>
  </head>
  <body>
    this is main page
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

/src/options.html:

<!-- /src/options.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Options</title>
  </head>
  <body>
    this is options page
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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",
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: if you decide not to install the static files plugin, just do it yourself by changing the "scripts" to:

"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/"
},
Enter fullscreen mode Exit fullscreen mode

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

ii. open extension context menu and click this "Open Main Page"
right click and click the item

iii. check the content and celebrate๐ŸŽ‰
check the opened tab


3. Set up Preact

add preact package:
yarn add preact

then add below "alias" to package.json:

"alias": {
  "react": "preact/compat",
  "react-dom/test-utils": "preact/test-utils",
  "react-dom": "preact/compat"
}
Enter fullscreen mode Exit fullscreen mode

create file .babelrc:

{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "pragma": "h",
        "pragmaFrag": "Fragment"
      }
    ]
  ]
}
Enter fullscreen mode Exit fullscreen mode

finally create a file /src/js/app.js:

// /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);
});
Enter fullscreen mode Exit fullscreen mode

and in main.html if we change the body to:

<body>
  <script src="./js/app.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

rebuild and check again๐ŸŽ‰๐ŸŽ‰
here comes 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:

// /postcss.config.js
module.exports = {
 plugins: {
   tailwindcss: {},
   autoprefixer: {},
 },
}
Enter fullscreen mode Exit fullscreen mode

you will want to uncomment the purgeLayersByDefault: true, and also add

purge: [
  './src/**/*.js',
],
Enter fullscreen mode Exit fullscreen mode

into tailwind.config.js like this:

// /tailwind.config.js
module.exports = {
  future: {
    // removeDeprecatedGapUtilities: true,
    purgeLayersByDefault: true,
  },
  purge: [
    './src/**/*.js',
  ],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

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!

so we just have to go ahead and create the /src/style/app.less:

/* /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;
Enter fullscreen mode Exit fullscreen mode

also add some styles to the preact <header> to see if it works in app.js:

<header class="h-24 bg-blue-200">This is header by Preact</header>
Enter fullscreen mode Exit fullscreen mode

don't forget to link the stylesheet into the main.html:

<link rel="stylesheet" href="./style/app.less">
Enter fullscreen mode Exit fullscreen mode

let's do our final testing, flexbox works + text is large + background color pale blue and height is right, awesome๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰
Preact header with TailwindCSS!


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:

# 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 .
Enter fullscreen mode Exit fullscreen mode

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!๐Ÿ‘๐Ÿ‘

Discussion (0)

pic
Editor guide