Building a Chrome Extension with Preact and Webpack: A Step-by-Step Guide

Harsh Kumar Raghav
7 min readJan 22, 2024

--

So the first question is “Why might one prefer Preact over React for building Chrome extensions?”

Have you come across Preact? Preact, being a lightweight alternative to React, offers a swift integration into Chrome extensions, enabling the development of high-performance extensions with the added advantage of a minimal 3kb footprint.

While React can be used for extensions, leveraging Preact’s concise API can significantly reduce your bundle size, providing a more efficient solution for extension development.

Before we get started

This guide assumes familiarity with the following concepts:

  • Proficiency in NPM and PNPM.
  • Fundamental comprehension of Webpack and Preact/React

You don’t need expertise in these topics to stay in the loop — just bring your curiosity and a sense of adventure!

Setting up our extension is the first step

Create the Project:

Open your terminal or command prompt.

Navigate to the desired directory where you want to create the project

cd path/to/your/directory

Once you’re in the right directory, run the following command to create the package.json file:

pnpm init

This will generate a default package.json file with default values.

Install the project dependencies:

Since we’re diving into Preact and Webpack, let’s get our toolkit ready by installing the necessary dependencies.

You can do so with the following commands:

To install our development dependencies, type the following:

Pnpm install --save-dev webpack webpack-cli html-webpack-plugin copy-webpack-plugin babel-loader @babel/core @babel/preset-env @babel/plugin-transform-react-jsx 

To install our dependencies type:

pnpm install preact

Notice that the only non-dev dependency here is Preact, ensuring a lightweight and streamlined setup!

Explanation of the dependencies listed above:

webpack and webpack-cli: We are using Webpack to build our extension, having a build process lets us automatize, and include features that help our development, without impacting the final bundle for our users

html-webpack-plugin: Extensions can have multiple files, we use the HTML Webpack Plugin to generate HTML files based on a template we provide, this way we don’t have to create several HTML files with the same content or do something hacky

copy-webpack-plugin: This plugin allows us to copy files from our project folder to the final build folder, or any kind of copy you may want to do, we are using it because our extension has files like the manifest.json that need to be on our final build

babel-loader @babel/core @babel/preset-env @babel/plugin-transform-react-jsx: We are adding Babel to allow us to write JSX, you also can extend upon it to use all the neat features Babel allows you to setup.

preact: This is Preact itself, it’s the only dependency exposed on our bundle.

Initializing the project:

To get a glimpse of the end result, begin by incorporating a basic ‘hello world’ message. Generate a new file named popup.js in the project root, and copy the following content into it:

import { render, h } from 'preact'; 

render(
<div>Hello world</div>,
document.getElementById('app')
);

I’m not utilizing preact/compact, but feel free to adjust the configurations later if needed. As demonstrated, Preact works seamlessly within a Chrome extension!

Adding the configuration files

Certain boilerplate is required to implement the previous example successfully. We’ll have to set up Webpack and Babel for this.

Begin by creating a new file named webpack.config.js and populate it with the following content:

const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
mode: "production",
entry: {
popup: "./src/popup.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
plugins: [
[
"@babel/plugin-transform-react-jsx",
{
pragma: "h",
pragmaFrag: "Fragment",
},
],
],
},
},
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.(jpg|png|gif|svg|woff|ttf|eot)$/,
use: {
loader: "url-loader",
},
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
],
},
plugins: [
new CopyPlugin({
patterns: [{ from: "public" }],
}),
new HtmlWebpackPlugin({
template: "./src/template.html",
filename: "popup.html",
chunks: ["popup"],
}),
],
};

Webpack Configuration Demystified for Your Extension Project

If you’re diving into Webpack for your extension development, let’s unravel the magic happening within that webpack.config.js file. This breakdown aims to provide you with a clear understanding of the build process. For deeper insights, don’t hesitate to explore more about the plugins and settings used here.

Mode:

We’ve set the mode to production to optimize the final build. This ensures that your extension is ready to shine in its polished form.

mode: 'production'

Output:

In the output section, we’ve defined three key settings — filename, path, and clean.

Filename: Using [name] dynamically generates a file that mirrors the entry name. This keeps things organized.

Path: We’ve specified that the files should be created inside a ‘dist’ folder. You have the flexibility to change it to suit your project structure.

Clean: Webpack is instructed to wipe out the contents of the ‘dist’ folder before each build, ensuring a fresh start.

output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true
}

Module:

The module configuration utilizes babel-loader, enabling swift transpilation of JavaScript through Babel. This ensures compatibility and a smooth development experience.

module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}

CopyPlugin:

Webpack is instructed to copy all files from the ‘public’ folder to the ‘dist’ folder. This includes essentials like manifest.json and any other files you wish to include in the final build.

plugins: [
new CopyPlugin({
patterns: [
{ from: 'public', to: 'dist' }
]
})
]

HtmlWebpackPlugin:

For each specified entry, HtmlWebpackPlugin creates an HTML file. This file includes the bundled JavaScript for that entry. The pattern [name].html ensures that the generated HTML aligns with the entry names.

plugins: [
new HtmlWebpackPlugin({
template: './template.html',
filename: '[name].html',
chunks: ['popup']
})
]

Entry:

In this example, we’re creating a simple extension with a popup. The entry object is where you specify your different entry points. Each property corresponds to a distinct entry.

entry: {
popup: './popup.js'
}

As said above, the HTML Webpack Plugin uses the name of the entry to generate the HTML file, so we don’t have to change anything else if we change an entry.

The output of Webpack is also using the name of the entry, so it works out of the box!

When updating files within the manifest.json for the extension, you’ll now refer to each entry by its name. This shift is a result of relocating the manifest.json from the ‘public’ folder to the ‘dist’ folder, where a uniquely generated file corresponding to each entry name resides.

Creating the manifest.json

Creating the manifest.json file is a crucial step in defining the identity and functionality of your extension. Unlike Webpack or Babel configurations, the manifest.json file communicates directly with the browser, outlining the essence of your extension and specifying the files essential for its operation.

Let’s embark on creating the manifest.json file:

  1. Begin by establishing a ‘public’ folder within your project’s root directory.
  2. Inside the ‘public’ folder, initiate a new file named manifest.json.

Note: Keep in mind that everything within the ‘public’ folder is considered public, making it the ideal location for files like manifest.json and icons. This aligns with the configuration in Webpack settings.

Populate the manifest.json file with the following content:

{
"manifest_version": 3,
"name": "Periodically GPT",
"version": "1.1.1",
"description": "A chatbot powered by GPT technology to provide information and answer questions related to the periodic table of elements.",
"action": {
"default_popup": "popup.html",
"default_icon": "./Periodically.png"
}
}

In this file, we set the extension name and its description that will be shown in the browser. As you can see in the action property, our extension will provide a popup.html file that will show the popup content.

Remember our Webpack entry?

entry: { popup: './popup.js' },

The entry is named popup, so Webpack will generate an HTML file and include the js file we specified(popup.js).

Defining the template file

Establishing the template.html file is crucial for HTMLWebpackPlugin to generate HTML files in line with your extension’s configuration. Follow these steps to define the template:

  1. In the project root, initiate a new file named template.html.
  2. Populate the template.html file with the following content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="app"></div>
</body>
</html>

This template serves as the foundation for generating HTML files for each entry specified in your Webpack configuration. Customize the HTML structure and include additional scripts or styles as needed for your extension.

By defining this template, you ensure consistency and ease of maintenance across different entry points in your extension project.

Building the extension

Open the package.json file and add the following property:

"scripts": {
"build": "webpack --config webpack.config.js"
},

This is a quick shorthand to call the webpack command.

And to build the extension type pnpm run build from the cmd or terminal, while you are CD into the project's root folder.

After that Webpack will build the project, and the files will be inside a dist folder.

You can test your extension on the browser, by opening it using the folder option in the extension developer’s tab, but remember that the extension files are inside the dist folder, so select the dist folder when testing and shipping your extension.

Concluding Thoughts:

As we wrap up, it’s worth highlighting the remarkable API that Preact provides for crafting efficient front-end applications. Its minimal size makes it versatile, fitting seamlessly into various scenarios. When combined with the prowess of Babel and Webpack, you unlock an exceptional developer experience, especially tailored for web extensions.

Your Starting Point: A Solid Boilerplate Feel free to adopt the provided code as your initial boilerplate when embarking on extension development. This setup, enriched by Preact, can serve as a sturdy foundation for your projects. Additionally, consider extending it further by creating a development configuration for Webpack and exploring other enhancements.

Remember, this article aims to furnish you with a robust base and a jumpstart for your extension endeavors. Feel empowered to customize and expand upon it to suit the unique requirements of your projects.

Happy coding!

Source Code for my extension:

https://github.com/HarshKumarraghav/Periodically-GPT

--

--

Harsh Kumar Raghav

Skilled in Front-end Development, Algorithms, C, C++, Strong media and communication professional with a Bachelor’s degree focused in Information Technology.