I tried to get ambitious this weekend, with mixed success. After a coworker suggested I try to turn a commonly used bash script into a simple Chrome extension, I thought it would be a good idea for a blog. I had never attempted an extension before, so I realized that it might be too difficult to complete in one weekend, but at least I could blog about what I had learned.
About 30 minutes into my research, I had a familiar concern (which incidentally has never actually turned out to be legitimate). I was worried that writing about a Chrome extension might actually be too easy because of how clear the Google documentation is, not to mention the countless other “your first extension” blogs and videos. I watched An Object Is A write an extension in 30 minutes, and 8 hours later, I was still stuck between steps 2 and 3.
First thing’s first: read the docs. When building an extension, we want to start with a manifest.json file. A manifest contains our project’s metadata and will require certain values and updates to run. My app is currently incomplete, but here’s a preview where I’m at, for reference:
We see a few self-explanatory values here: name, description, and version. These all exist for the developer rather than the program itself. Even the version simply refers to what version of our own extension we’re on, so changing that won’t have any effect on functionality. Manifest_version, however, is critical to our development, and the first roadblock that slowed my progress.
Google released extensions manifest v3 in late 2020. This means that, as of the writing of this post, most tutorials are still written using v2 and therefore contain some manifest syntax that won’t work with v3. My app started failing to compile when I mixed and matched features from v2 and v3, so I had to get more strict with my Google searches and read up on the differences between the versions here.
After manifest_version in the manifest file, we see permissions, host_permissions, and action. These are all examples of what’s changed between v2 and v3. In both cases, permissions is an array of strings that give us access to different Chrome APIs in our extension’s logic. Scripting, for instance, is associated with the chrome.scripting API, which contains functions that can inject JS and CSS into websites. It’s a big part of the Getting Started documentation. But it’s exclusive to manifest v3. If you’re looking at an older tutorial, they’ll be invoking functions completely differently.
V3 permissions also separate host_permissions into their own key-value pair, which determines which URLs will allow an extension to run. In v2, we could add a URL string to our permissions array, but in v3, we should use the host_permissions key instead. Conversely, v3 combines what were previously browser_action and page_action into a single key: action. This points to the files that kick off our display and logic. There’s nothing special about popup.html in my case — it looks like any other simple HTML page:
By assigning this HTML to the default_popup key under action, we are directing our app to build a display based on that code. This display will be the image a user will see when they open our extension. It’s a simple form with one input field and a submit button. All we have to do to add JS logic to this HTML is write a script in the head element.
Everything else we see in my example associates images with icons. The default_icon property is associated with the image you’ll see in the Chrome toolbar, and the icons property is what you’ll see on the extension management page, permissions warning, and favicon. They’re purely aesthetic and have nothing to do with functionality.
The one value that isn’t included here, but is present in most “getting started” examples, is background. Working through that definitely threw me for a loop.
The background value directs our extension to logic that will run in the background when it is added to the page or activated. We can use the aforementioned Chrome APIs to define what logic will run and what will trigger it, but getting any logic to run was again challenging because of changes between manifest v2 and v3.
In manifest v2, the background object included a scripts property that pointed to an array of files. In v3, background instead uses a service_worker that points to one single file. The functional difference here is that background scripts will always run in the background, but service workers use listeners to run logic conditionally.
I slowly started to understand how my background.js file should look and even got some of my desired functionality working onLoad. But my extension needs to accept some user input before running, so I needed to include a popup. After an hour of struggling (and after walking away to decompress), I started looking more closely at some of Simeon Vincent’s examples, specifically cookie-clearer. This extension works exactly the way I’d like mine to: a popup with a field and a submit button. And, believe it or not, it works without any background file at all.
I would guess that most Chrome extensions use a background.js script, because every tutorial I saw included it. But if our app doesn’t need any service workers, we can completely forego that file and work directly with the popup. This granted me the simplicity I was seeking, but it caused another issue for me. That’s because I had been using the Inspect views option in Chrome Extensions Management (chrome://extensions/).
That option only appears if we declare a service_worker in our manifest, so don’t worry if it’s missing if you’ve taken my path and omitted that value. Clicking the link will open up a console that can be used to log actions in your background script. I used it to make sure my triggers were working when I expected them to. One thing I didn’t realize, though, is that this console does not display logs from other JS files. It may be that I had misconfigured it, but once I started testing logic in popup.js, I stopped seeing any response. And once I removed the service_worker from my manifest, the option disappeared completely. I’m making sure to note this because it would have been really helpful to read it while I was struggling with the behavior.
My other big pitfall came when I started to see a cannot access a chrome:// url error in my console. I was so frantically looking around for a simple solution that I didn’t stop to read and see that my extension wouldn’t work properly if I was testing it on the chrome://extensions page. According to the linked Github response, this issue was resolved in 2017, but I still ran into it. So if you’re testing out your extension, make sure to check it on something other than an internal Chrome URL.
The actual logic of my extension isn’t going to make it into this blog post. First of all, it’s incomplete because I spent so much time wrestling with configuration that I didn’t make it all the way to MVP. And even if I had, it wouldn’t be too exciting — all I’m looking to do is open a new tab with a URL of an inputted string + an added param. But that’s not really what this weekend was about. The basics of Chrome extension setup weren’t even the most important lesson here.
I started working on my app around 8am Saturday morning and closed my computer around 8pm. But I certainly didn’t work for 12 straight hours. I had the luxury of setting my own timeline and being able to walk away when things got difficult or my brain felt full. And more than one of my breakthroughs came while I was away from the computer, bringing me back later with a newfound energy that pushed the project forward. This is advice we hear often, but it’s easy to forget — the best solution to a problem is often to take space away from it and decompress.
I also learned the value of thoroughly viewing available resources on a topic, especially code samples. Readmes and video walkthroughs are often challenging because we’ll be shown code snippets without being able to fully view a project and see where exactly everything fits in. The Chrome extension samples on Github (as linked in the Google documentation) were huge in supporting me as I wrote my first extension. Simeon Vincent built different extensions in different ways that used different APIs and even annotated them with comments. If you’re like me, it’s much easier to iterate on existing code than start from scratch, and these samples allowed me to do that.
At the end of the day, I didn’t have a fully functional app, but I had developed something that I could pick up next weekend and had a better understanding of the framework and my process. That’s a win!
- Getting started, Chrome extensions docs
- How To Build A Chrome Extension (2021 Web Development), An Object Is A
- Migrating to Manifest V3, Chrome extensions docs
- chrome.scripting, Chrome extensions docs
- chrome-extensions-samples, Simeon Vincent
- Chrome Extension Tutorial: Migrating from Manifest V3 from V2, Shahid Nasser