DEV Community

Cover image for Step Into Chrome Extension Development: Easy Setup with TypeScript and Webpack
Gaurav Nadkarni
Gaurav Nadkarni

Posted on • Originally published at Medium

Step Into Chrome Extension Development: Easy Setup with TypeScript and Webpack

As of December 2024, Chrome remains the most popular browser worldwide. Learning to develop extensions for Chrome can open exciting opportunities to explore browser-based development and enhance your understanding of how browsers work under the hood. The Chrome Web Store offers a vast array of extensions that allow users to customize the browser’s default behavior and extend the functionality of various websites.

In this blog, we’ll go through setting up a local development environment for creating Chrome extensions using TypeScript and Webpack. This guide is perfect for anyone looking to grasp the basics of Chrome extension development. By the end, you’ll have a functioning development environment ready to experiment with your first extension. Before diving in, make sure you have a foundational understanding of web technologies, JavaScript, and commonly used tools in the JavaScript ecosystem. Details are listed in the prerequisites section.

Quick Overview of Chrome Extension Components

Before we jump into the setup, let’s briefly understand some key components of a Chrome extension:

Popup : Manages the extension’s user interface but does not have direct access to the DOM of the parent web page.

Content Script: Has direct access to the DOM of the parent web page but runs in a separate execution context. This separation means it cannot directly access JavaScript objects of the parent page.

Injected Script : Shares the same execution context as the parent web page, allowing access to its DOM and JavaScript objects.

Background Script : Operates in an isolated context, with no direct access to the DOM or JavaScript objects of the parent page.

The Popup, Content, and Background scripts operate in the extension’s contexts, whereas the Injected script runs in the context of the parent web page. The parent page refers to the active web page on which the extension performs its functionality. Permissions and access to these pages are defined in the manifest.json file, which we’ll cover later in this blog.

Prerequisites

To follow along with this tutorial, ensure you have the following tools installed:

  • Node.js (v18.16 LTS)

  • NPM (Node Package Manager)

  • TypeScript

  • Webpack

  • VS Code Editor (or any code editor of your choice)

Setup

Every Chrome extension requires a file named manifest.json at the root level of the project. This file serves as the configuration blueprint for your extension, containing essential details about the project. While there’s no strict rule about the overall project structure, we’ll begin by creating this file and progressively build the project as outlined in this blog.

Before proceeding, ensure that all prerequisites are installed on your machine.

Follow the steps below to set up your project and its dependencies:

Create a directory for your project and go inside that directory. This will be the root of your project. Everything from here on will be based of the root of your project unless stated otherwise.

mkdir chrome-extension && cd ./chrome-extension\

Create a file called as manifest.json

{
 "manifest_version": 3,
 "name": "My First Chrome App",
 "version": "1.0",
 "description": "A Chrome extension built with TypeScript using webpack.",
 "action": {
 "default_popup": "popup.html",
 "default_icon": "icon.png"
 }
}
Enter fullscreen mode Exit fullscreen mode

Most of the content in the manifest.json is self explanatory except action object. The default_icon is the icon of your app that will appear next to your app’s name in Chrome and default_popup specifies the HTML file to display as a popup when the extension’s icon is clicked.

Create a file called as popup.html with the following content,

<html>
 <head>
  <title>First Chrome Extension</title>
 </head>
 <body>
  <h1>This is my app popup</h1>
 </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Include an image file named icon.png in the root directory. This will serve as your app’s icon, displayed in the Chrome toolbar. Make sure the image is in a supported format (e.g., PNG) and appropriately sized.

Quick Test with bare minimum setup

Before diving into more complex functionality, let’s test this basic extension to ensure everything is set up correctly. This initial test will serve as the foundation for our development process and confirm that changes we make later are working as expected.

Open Chrome’s Extension Management : Open Chrome and navigate to the Manage Extensions page by typing chrome://extensions/ in the address bar. This will take you to the Extensions screen.

Enable Developer Mode : Locate the Developer mode toggle at the top right of the screen and switch it on if it’s not already enabled. Enabling this mode allows Chrome to load extensions built locally in addition to those downloaded from the Chrome Web Store.

Load Your Extension : Click the Load unpacked button at the top of the page. Browse to and select the root directory of your project.

Verify Installation : Once loaded, your extension should appear in the list of installed extensions.

Render the Popup : Click on the My First Chrome App icon in the top-right corner of Chrome, just above the Manage Extensions button. This action should render the popup.html file, displaying the content you defined earlier.

Screenshot of the installed app

If you have managed to get it to work then our first test was successful and we can build on this. If not then please read through the above content carefully to make sure that you have not missed any step along the way.

The next step is to create a package.json file for managing project dependencies. Run the following command:

npm init -y\

This command initializes a package.json file with default values. If you’d like to customize it, omit the -y flag and answer the prompts interactively.

A default package.json file will look like this:

{
  "name": "chrome-extension",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

Now we can install all the required dependencies into the project. You can use the following commands to do so.

  • npm install -D typescript — To install TypeScript and add it to the devDependencies section of package.json

  • npm install -D @types/chrome — To install types for Chrome and add it to the devDependencies section of package.json

  • npm install -D webpack — To install webpack into the project and add it to the devDependencies section of package.json

  • npm install -D webpack-cli — This is required because we will be doing a hot reload whenever we make a change to the codebase

  • npm i -D copy-webpack-plugin — This is required to copy any static assets into the output directory or the dist directory

  • npm i -D path — This is required to resolve the path of the static assets in the webpack configurations later on

  • npm i -D @babel/core @babel/preset-env babel-loader ts-loader — This is required to compile the code during the webpack build process

For the next test we will have to make this app work with typescript and webpack. We already have the required dependencies installed. Now we will have to create some configuration files and write some code to get this to work.

We are going to create two configuration files, one for TypeScript and other for webpack. Create a file called as tsconfig.json with following content.

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

The above is for TypeScript compiler to correctly identify the locations of .ts files and compile them properly. Per above configurations, the .ts files are expected to be present under the src directory or its sub-directories. and the transpiled .js files will be created in the dist directory.

Now create webpack.config.cjs file which will have the configurations required for building and generating the compiled files along with other static assets ex. images

// [webpack.config.cjs]
const path = require("path");
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  mode: "development", // Use 'production' for production builds
  target: "web",
  devtool: "source-map",
  entry: {
   "popup": path.resolve(
      __dirname,
      "src/scripts/popup.ts"
    ),
  },
  output: {
    filename: "[name].js", // Output file name for each entry
    path: path.resolve(__dirname, "dist"), // Output directory
    clean: true, // Clean the output directory before each build
    libraryTarget: "umd", // Universal Module Definition
  },

  resolve: {
    extensions: [".ts", ".js"], // Resolve .ts and .js extensions
  },

  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env"],
            },
          },
          "ts-loader",
        ],
      },
    ],
  },

  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "popup-style.css"), // Source directory
          to: path.resolve(__dirname, "dist"), // Destination directory
        },
        {
          from: path.resolve(__dirname, "content-style.css"), // Source directory
          to: path.resolve(__dirname, "dist"), // Destination directory
        },
     {
          from: path.resolve(__dirname, "icon.png"), // Source directory
          to: path.resolve(__dirname, "dist"), // Destination directory
        },
        {
          from: path.resolve(__dirname, "popup.html"), // Source directory
          to: path.resolve(__dirname, "dist"), // Destination directory
        },
        {
          from: path.resolve(__dirname, "manifest.json"), // Source directory
          to: path.resolve(__dirname, "dist"), // Destination directory
        },
      ],
    }),
  ],
}
Enter fullscreen mode Exit fullscreen mode

Note: CopyWebpackPlugin copies the static assets from the source code to the dist directory.

Now create the required directories using the following command:

mkdir src/scripts -p\

Inside the script directory, create a simple Typescript file with the following content,

src/scripts/popup.ts\

(() => {
    const message:string = "This is a console statement from the popup script";
    console.log(message)
})();
Enter fullscreen mode Exit fullscreen mode

The above code will print the message to the console whenever the popup is rendered. We also have to include the link of this popup.ts file (which will be compiled to popup.js file) in popup.html. Also, create files called popup-style.css and content-style.css. We can later use these files for styling the popup and other pages. So lets do that,

Include the links to popup-style.css and popup.js in popup.html that we created earlier

popup.html\

....
<link href="popup-style.css" rel="stylesheet"/>
....
<script src="popup.js"></script>
....
Enter fullscreen mode Exit fullscreen mode

Create popup-style.css file:

body {
    background-color: #d1bba2;
}
Enter fullscreen mode Exit fullscreen mode

Create content-style.css file:

/* [content-style.css] */
/* just as placeholder */
Enter fullscreen mode Exit fullscreen mode

Now its time to add the webpack related command to the package.json file so that we can build the extension.

{
 ....
 "script": {
  "build": "webpack --watch",
  ....
 }
 ....
}
Enter fullscreen mode Exit fullscreen mode

Run the following command to start the build process:

npm run build\

This command watches for changes in your files and rebuilds the project automatically.

Quick Test with bare minimum setup

We can do another test now to check if our webpack related configurations are working as expected. But before you try to test, delete the old extension from the first test where you had uploaded the projects root directory to Chrome. Now this time we will have to upload the dist directory as the compiled code will be in this directory and not the project’s root directory. After you upload the newer version of the extension and render the popup, right click on the popup and open the dev console and check if you can see the console statement from the popup.ts. The statement should be present, if not , please check for any mistakes done in the previous steps.

Screenshot of the successful run for popup scripts

Enhancing the setup

At this point, we have a basic version of the extension which is functional but there are some components which we have to add so that we can do the local development with ease.

Until now we have seen popup component. Now its time to add the other components. Create three files namely injected.ts,content.ts\ and background.ts in the src/scripts directory with the following content,

src/scripts/injected.ts

(() => {
    const message:string = "This is a console statement from the injected script";
    console.log(message)
})();
Enter fullscreen mode Exit fullscreen mode

injected.[ts|js] is a special file which has access to the Dom as well as the JavaScript object exposed by the parent site. We need to add this file to the host site dynamically using the content script.

Create content.ts file:

src/scripts/content.ts\

(() => {
    const message:string = "This is a console statement from the content script";
    console.log(message);

    const script = document.createElement("script");
    script.src = chrome.runtime.getURL('injected.js'); // Load the script from the extension
    document.documentElement.appendChild(script);
    script.onload = function () {
      script.remove(); // Clean up once the script is loaded
 };
})();
Enter fullscreen mode Exit fullscreen mode

Create background.ts file:

src/scripts/background.ts\

(() => {
    const message:string = "This is a console statement from the background script";
    console.log(message)
})();
Enter fullscreen mode Exit fullscreen mode

Now we will have to update the webpack.config.cjs file to add these three entries as the entry points.

// [webpack.config.cjs]
.....
{
.....
entry: {
    .......
    "content": path.resolve(
      __dirname,
      "src/scripts/content.ts"
    ),
    "background": path.resolve(
      __dirname,
      "src/scripts/background.ts"
    ),
    "injected": path.resolve(
      __dirname,
      "src/scripts/injected.ts"
    ),
  },
.....
}
......
Enter fullscreen mode Exit fullscreen mode

As a last step, we will have to update the manifest.json file to include all these configurations so that Chrome’s environment can detect them.

// [manifest.json]
...
 "background": {
     "service_worker": "background.js",
     "type": "module"
    },
 "content_scripts": [
    {
  "matches": ["https://*.google.com/*"],
  "css": ["popup-style.css"],
     "run_at": "document_start"
    },{
     "matches": ["https://*.google.com/*"],
     "js": ["content.js"],
     "run_at": "document_end"
    }
  ],
  "permissions": [
        "scripting"
    ],
    "host_permissions": [
     "https://*.google.com/*"
    ],
    "web_accessible_resources": [{
     "resources": ["injected.js"],
     "matches": ["https://*.google.com/*"]
    }]
....
Enter fullscreen mode Exit fullscreen mode

In the manifest.json above we configured the extension to work with google.com meaning this extension will execute its logic only when google.com is loaded in the browser. You can change this to any other website of your choice.

Quick Test with Enhanced setup

We can do the final testing now with the enhanced setup. Before proceeding, please make sure that you reinstall the app in chrome so that you avoid the issues due to mismatch with older version of the code. To see if all the components are working properly, check the different console.log statements. Remember that we have put console.log statements in four different files: injected.ts, background.ts, content.ts and popup.ts. There should be four messages logged in the console. So, here are the steps,

  • Stop the npm run build command in the terminal if it was running and start it again so that it picks up the new files that we just created

  • Remove and Install the app again

  • Open the popup from the extension settings screen, right click on that popup and open the developer console. You should see the messages coming from background script and popup script in this console

  • Open https://www.google.com in the browser in another tab

  • Right click on the opened google site and open developer console. You should be able to see the messages coming from content script and injected script

Screenshot of the successful run for background and popup scripts

Screenshot of the successful run for content and injected scripts

Congratulations on successfully setting up and running your first Chrome extension! If you encounter any issues, please double-check the steps you’ve followed to ensure everything is in place. Additionally, a link to the GitHub repository is provided at the end of this blog for your reference.

Key Takeaways

In this blog, we have learned how to set up a local development environment for Chrome extension development. Here are a few key takeaways:

  • We explored the main components involved in Chrome extension development.

  • We learned how to set up a development environment with TypeScript and Webpack.

  • We also covered how to set up and test the extension in Chrome.

Glimpse Ahead

I’m currently working on another blog where we’ll explore a simple use case for the Chrome extension, showcasing how all the components of the Chrome development environment we discussed in this blog come together to create a functional extension.

Thank you for taking the time to read this blog! Your interest and time are greatly appreciated. I’m excited to share more as I continue this journey. Happy coding!

GitHub link — https://github.com/gauravnadkarni/chrome-extension-starter-ts

This article was originally published on Medium.

Top comments (0)