Developing your own Chrome Extension in Bun and Typescript (Part 2)
Tags: bunjs, chrome, typescript, webdev
Published: 2024-03-02 21:43:45

In the previous [part of the series](https://dev.to/jolodev/developing-your-own-chrome-extension-the-theory-part-1-2d93), we learned about the structure of building a Chrome extension. We are ready to build and structure our first Chrome Extension with that knowledge. Let’s get started.

## Idea

This is the idea of the project

![Idea of Preview Linker](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/xbISRydtki7syju6H1DX0.png)

## Getting Started

First, we create an empty directory with a `manifest.json`. Initially, we want to inject a script to the current page in the tab by using `content_scripts`.

```json
{
  "manifest_version": 3,
  "name": "Link previewer",
  "version": "0.0.1",
  "description": "It sneak preview the link by mouse hovering and show key facts using OpenAI GPT model.",
  "icons": {
    "16": "images/icon-16.png",
    "32": "images/icon-32.png",
    "48": "images/icon-48.png",
    "128": "images/icon-128.png"
  },
  "content_scripts": [{
    "js": ["dist/index.js"]
  }]
}
```

Note that I started with a `dist/index.js`. This is where we put the bundled code into. For now, let’s create a `src/index.ts`. Now, we can add Bun by `bun init` . It will ask if you want to use Typescript. Go ahead and use it. Then, we need to adjust the `package.json`.

```json
{
  // ..
  "scripts": {
    "dev": "bun build --watch src/index.ts --outfile dist/index.js"
  }
  // ..
}
```

We want to plan Typescript, so let’s install it: `bun install -D chrome-types`.
Now, we need to add the types to the `tsconfig.json`

```json
{
  // tsconfig.json
  // ..
  "types": ["chrome-types"]
}
```

We need that `chrome`- [object](https://chromewebstore.google.com/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid). Otherwise, our IDE may yell at me.

![VS Code yelling that cannot find `chrome`- object](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/3It-lIApwDCl7-1hJYXz6.png)

> If you enter their code from their [Chrome Extension Getting Started page](https://developer.chrome.com/docs/extensions/get-started/tutorial/scripts-activetab), Typescript reveals so many errors🫢

## Developing

Okay, it’s time to get our hands dirty. Before that, we need to load an unpacked extension:
1. Open `chrome://extensions` in a new tab
2. Enable the `Developer Mode` and click on the `Load unpacked`
   
   ![Click on Load unpacked](https://developer.chrome.com/static/docs/extensions/get-started/tutorial/hello-world/image/extensions-page-e0d64d89a6acf_1440.png)
3. Now, go to the directory of your extension and click on the reload button
   
   ![Click on Reload Button](https://developer.chrome.com/static/docs/extensions/get-started/tutorial/hello-world/image/reload-extension-241cc5378fffb_1440.png)

You need to reload the extension every time you change, which is quite annoying. [Extension Reloader](https://chromewebstore.google.com/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid) is another extension that can do that for you.

### Start Bun

Do you remember the `scripts` in the `package.json` from [above](#getting-started)? First, you get Hot Module reloading whenever a change happens on that file. Second, Bun is bundling the `index.ts` to `index.js` and put it into the `dist`- folder.

```sh
bun dev
```

### Add a listener on each link

From our [idea](#idea), we want to show a tooltip whenever we hover over a link on that page. In doing that, we have to add a listener.

```typescript
const links = document.querySelectorAll('a')

console.log(links) // Debugging purpose
```

Go ahead and `console.log` it and check the links. The `querySelectorAll` returns a list of Anchor elements.
But here is a little problem. We get a `NodeList` that is not iterable, and what if an Anchor tag does not `href` , which we need in our case? So, let’s modify it a bit

```typescript
const links = Array.from(document.querySelectorAll("a")).filter((link) =>
  link.hasAttribute("href"),
); // Create an Array from an iterable object

// to verify
links.forEach((link) => {
  console.log(link.href);
});
```

We can add an event listener whenever the mouse enters the link.

```typescript
const links = Array.from(document.querySelectorAll('a')).filter(link => link.hasAttribute('href'));

links.forEach(link => {
    link.addEventListener('mouseenter', () => {
        console.log(link.href);
    });
});
```

Each link gets an event listener `mouseenter` whenever the mouse is entering or hovering over the link.

![Add listener when mouse enters links](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iyl8fid6h1coz40q9vr4.gif)

But what if the page has many links? On Arc, you can hold shift on a link, but it seems to be impossible with vanilla JavaScript. Let’s extend it and add a timer when the user has been on the link for 3 seconds.

```typescript
const links = Array.from(document.querySelectorAll('a')).filter(link => link.hasAttribute('href'));

links.forEach(link => {
    link.addEventListener('mouseenter', () => {
        console.log(link.href);
        const timer = setTimeout(function() {
            // Here we put the logic after 3000 ms aka 3 seconds
        }, 3000); // Adjust the time (in milliseconds) as needed
        
        link.addEventListener("mouseleave", function() {
            // Clear the timer if the mouse leaves the link before the specified time
            clearTimeout(timer);
        });
    });
});
```

We execute the logic after 3 seconds via `setTimeout` and add another event listener when the user leaves the mouse after leaving the link.

### Show the Tooltip

Now, we want to add the tooltip. The approach is that we create a tooltip element, but where to add it? What happens if we hover on the same link again?

The idea is that we create the tooltip and put the element at the bottom of the `<body>` .

Here, we outsource the code logic in `tooltip.ts`, to make it more readable. In addition, we create some listeners on the link we’ve just hovered over.

```typescript
// in tooltip.ts
export function createTooltip(link: HTMLAnchorElement) {
    const tooltip = document.createElement('div');
    tooltip.classList.add('tooltip');
    tooltip.dataset.url = link.href;
    tooltip.innerHTML = 'This is a tooltip';
    document.body.appendChild(tooltip);
    createTooltipAction(link, tooltip);
    return tooltip;
}


function createTooltipAction(link: HTMLAnchorElement, tooltip: HTMLElement) {
    function onMouseEnter() {
        console.log('onMouseEnter');
    }

    function onMouseMove(event: MouseEvent) {
        console.log('onMouseMove');
        tooltip.style.backgroundColor = 'white';
        tooltip.style.color = 'black';
        tooltip.style.display = 'block';
        tooltip.style.position = 'absolute';
        tooltip.style.top = `${event.pageY - 25}px`;
        tooltip.style.left = `${event.pageX - 10}px`;
    }

    function onMouseLeave() {
        console.log('onMouseLeave');
        tooltip.style.display = 'none';
    }

    link.addEventListener('mouseenter', onMouseEnter);
    link.addEventListener('mousemove', onMouseMove);
    link.addEventListener('mouseleave', onMouseLeave);

    return {
        destroy() {
            link.removeEventListener('mouseenter', onMouseEnter);
            link.removeEventListener('mousemove', onMouseMove);
            link.removeEventListener('mouseleave', onMouseLeave);
        },
    };
}
```

Notice that the tooltip contains the attribute `data-url={link.href}` . That helps us to find the tooltip and avoid recreating it.

```typescript
// in index.ts
import { createTooltip } from './tooltip';

const links = Array.from(document.querySelectorAll('a')).filter(link => link.hasAttribute('href'));

links.forEach(link => {
    link.addEventListener('mouseenter', () => {
        const tooltip = document.querySelector<HTMLElement>(
          `[data-url="${link.href}"]`,
        );
        if (tooltip === null) {
          createTooltip(link);
        }
    });
});
```

![Tooltip follows the mouse when entering](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c2dj2cswal5gojslmydd.gif)


Let’s add a nice skeleton animation when the tooltip appears

```typescript
// tooltip.ts
export function createTooltip(link: HTMLAnchorElement) {
    // ...
    createTooltipSkeleton();
    return tooltip;
}

function createTooltipSkeleton() {
    let linkElement = document.createElement("style");
    linkElement.innerHTML = `
      .tooltip {
        width: 220px;
        height: 80px;
        border-radius: 5px;
      }
      .tooltip .avatar {
        float: left;
        width: 52px;
        height: 52px;
        background-color: #ccc;
        border-radius: 25%;
        margin: 8px;
        background-image: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
        background-size: 600px;
        animation: shine-avatar 1.6s infinite linear;
      }
      .tooltip .line {
        float: left;
        width: 140px;
        height: 16px;
        margin-top: 12px;
        border-radius: 7px;
        background-image: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
        background-size: 600px;
        animation: shine-lines 1.6s infinite linear;
      }
      .tooltip .avatar + .line {
        margin-top: 11px;
        width: 100px;
      }
      .tooltip .line ~ .line {
        background-color: #ddd;
      }
      
      @keyframes shine-lines {
        0% {
          background-position: -100px;
        }
        40%, 100% {
          background-position: 140px;
        }
      }
      @keyframes shine-avatar {
        0% {
          background-position: -32px;
        }
        40%, 100% {
          background-position: 208px;
        }
      }
      `;
    // Append the link element to the head of the document
    document.getElementsByTagName("head")[0].appendChild(linkElement);
}
```

At this point, we can ask if a framework like React or Vue is not a better choice for better maintenance. But since we use Bun, it is supposed to bundle `.tsx` files out of the box. That is unfortutately not the case as you need a third party library such as React or Preact. I chose the latter: `bun add -D preact` and adjust the `tsconfig.json` :

```json
{
  "compilerOptions": {
    // ...
    /* Preact */
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "baseUrl": "./",
    "paths": {
      "react": ["./node_modules/preact/compat/"],
      "react-dom": ["./node_modules/preact/compat/"]
    }
  }
}
```

Now, we can rewrite the `Tooltip.tsx`:

```typescript
import * as React from "preact";

export default function Tooltip(props: { link: HTMLAnchorElement }) {
  console.log("Tooltip", props.link.href);
  const tooltip = React.createRef();
  createTooltipSkeleton();
  function onMouseMove(event: MouseEvent) {
    console.log("onMouseMove");
    tooltip.current.style.backgroundColor = "white";
    tooltip.current.style.color = "black";
    tooltip.current.style.display = "block";
    tooltip.current.style.position = "absolute";
    tooltip.current.style.top = `${event.pageY - 100}px`;
    tooltip.current.style.left = `${event.pageX - 100}px`;
  }

  function onMouseLeave() {
    console.log("onMouseLeave");
    tooltip.current.style.display = "none";
  }

  props.link.addEventListener("mousemove", onMouseMove);
  props.link.addEventListener("mouseleave", onMouseLeave);
  return (
    <div className="tooltip" data-url={props.link.href} ref={tooltip}>
      <div className="avatar"></div>
      <div className="line"></div>
      <div className="line"></div>
    </div>
  );
}
```

The compiler complains if we don’t import `React` somewhere as if JSX is only possible with React… The VS Code compiler also put React in favor when using JSX

![Linting error when JSX without React](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/Wj8M_RKBvxHTWJfaByG1R.gif)

I honestly don’t like that approach. There may be good alternatives, such as [VanJS,](https://vanjs.org/) which does not use JSX, and [nanoJSX](https://nanojsx.io/). VanJS results in a smaller bundle ( **< 8kb) **but is not so nicely readable as Preact** ( < 19kb**).

![Comparison between Preact, and VanJS](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/9u981-fS6woEisWVG9B51.gif)

Anyway, I will put both codes in the repository.

The result is the same 🤩

![The Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v001t9ikifl3pib7ruue.gif)

## Conclusion

The blog post discusses building a basic Chrome extension that displays a tooltip with additional information when the user hovers their mouse over web links. It covers setting up the project structure, adding Typescript for type safety, using Bun to bundle the code, selecting links and adding `mouseenter` listeners, conditionally displaying a tooltip after 3 seconds, creating the tooltip element and styling it, and positioning the tooltip near the mouse pointer. The post also explores using Preact to manage the tooltip component and compares it to other frameworks like VanJS and NanoJSX that could potentially reduce the bundle size. Overall, the post provides a helpful tutorial for creating a foundational Chrome extension.

Find the repository [here](https://github.com/jolo-dev/worth-the-click).

Check out this repository if you decide to use another framework for your Chrome extension: [https://github.com/guocaoyi/create-](https://github.com/guocaoyi/create-chrome-ext/tree/main/template-alpine-ts/src)[chrome](https://github.com/guocaoyi/create-chrome-ext/)[-ext](https://github.com/guocaoyi/create-chrome-ext/tree/main/template-alpine-ts/src).

In the next section, we will integrate OpenAI’s SDK and explore the capabilities of Langchain.