HY
Published on

Using ESM modules in content scripts in Chrome extensions

Authors

I've been creating Chrome browser extensions for a while. In fact, I started Chrome extension development when the browser extensions were allowed in Chrome for the first time. (That is, many many years ago.) When I did this last time, however, the manifest file version was v2. The current version is v3, and hence it was probably a few years ago.

I just started building some AI/LLM apps, which were suitable as browser extensions. Naturally, I dug up my old Chrome extension source code to use it as a "template". (The first mistake. ๐Ÿ˜„) The more recent extensions were all written in Ionic/Angular. At the time, Angular was very popular, and I loved Ionic, and its clear look and feel. Unfortunately, my old extensions did not build. The Ionic framework used Gulp and Webpack, etc., and I couldn't even figure out what was the problem, or where the error originally came from. At the time, again, tools like Gulp and Grunt were rather widely used. NPM packages typically use dozens, if not hundreds, of (directly or indirectly) dependent packages. The extensions' dependent packages were all outdated, with numerous errors. Who said that software is forever? After spending a couple of days, I gave up. It was the apps that I wrote (except for the framework, and tools, and those other hundreds of dependent packages, which occupy over 1+ Gbytes of space in the node_modules folder), and yet I couldn't figure out why I could not even build them. (If you fix one error, another error pops up, and so on and on.)

The lesson? When you use a "framework", always think about its long term consequences. Incidentally, Ionic is, in case you don't know, a framework for building hybrid apps, similar to PhoneGap/Cordova and React Mobile. But, it is pretty much dead now, and it will stop commercial support in a short little while.

Now I have to build a Chrome extension from scratch. Naturally, I thought I would use React. Of course, React is not Ionic. React is not even Angular. ๐Ÿ˜„ It is here to stay, no? Then, of course, again naturally, I decided to use Typescript. (The second mistake.) It was a slog, every step of the way. There were no good resources on the Web, or on YouTube, that explained the Chrome extension development in detail, for real developers. All blogs and videos were geared toward beginners. All they teach was, "if you add a manifest.json file to your existing Web app, it is a Chrome extension." In principle, it is not wrong. But, that's not how you build a real-world app. (BTW, there were some frameworks that could help you "easily" build Chrome browser extensions. Some were rather "popular" on GitHub, but I resolutely decided against using them.)

Anyways, my choice of tools/libraries were React/Tailwind CSS, Vite, and Typescript, among other things. A lot of people use React. A lot of people use Vite. A lot of people use Typescript. A lot of people build Chrome extensions. (I myself built many Chrome extensions before.) Etc. Etc. But, if you combine them together, and especially if you are trying to build real apps, and not just a Hello-Chrome-Extension toy extension, then you will run into a lot of issues. If you are an expert in all these things, it should be no problem. But, if you are relatively new to any of these things, even one, then your dev experience can be rather painful. (Last time I did any serious Web development, I didn't even know that words like "vite" existed.)

After spending numerous hours of toil and sweat, I finally understood how Chrome extension worked, and how I (would normally) use the service worker vs content scripts, etc., and how Chrome extension is ultimately based on an event architecture, and so forth. Hurray! ๐ŸŽ‰

Then, I ran into another problem.

Uncaught SyntaxError: Cannot use import statement outside a module

I googled it:

chrome extension error Uncaught SyntaxError: Cannot use import statement outside a module. How to use ES modules in Chrome extensions?

There were no good answers. (I am not sure how many people have noticed it, but Google search has been deteriorating for the last few years.) I tried all different word combinations, with words like Typescript, content scripts, etc. etc. Still no good answers. Initially, I was mostly focusing on this question: "how to specify a content script as an ES module?"

Gemini integrated into Google Search was hallucinating. For example,

AI Overview: To specify that a Chrome extension content script is an ES module and allow the use of import statements, you need to declare it as a module in your manifest.json file.

Steps to Declare a Content Script as an ES Module: Add type: "module" to the content script entry in manifest.json: Within the content_scripts array in your manifest.json, for the specific content script you want > to treat as a module, include the type: "module" property.

(Example code omitted...)

Of course, I believed it, without knowing that it was a complete fabrication. It didn't work. Gemini even carefully explained this to me:

Important Considerations: Manifest V3: The type: "module" declaration for content scripts is primarily supported in Manifest V3 extensions.

BTW, GitHub Copilot did not even try much. Gemini CLI (with Gemini 2.5 pro) tried to "fix" my problem multiple times, in many different ways, but eventually gave up. Is this such a difficult question?

After spending a couple of days on this problem, I finally had a breakthrough. (At least, that was what I thought.) ES modules have been around for many many years. Typescript has been around for many many years. Chrome extension supports ES modules for service workers. But, is it possible that it does not support using the modern ES modules for content scripts? Is it possible? Is it even conceivable? The official doc,

Does not say anything about ES modules. Of course, it goes without saying that ES modules are supported anywhere Javascript is supported, no? It is 2025 after all. No?

I started to change my search queries, more to the tune of "Can you use ESM modules for content scripts in Chrome extensions?" Google Search Gemini kept hallucinating, and I don't blame Gemini. The answer should be "of course, yes", especially when the official doc is silent on this issue.

Then, I found this:

As of two and a half years ago, the Chrome extension did not support ES modules for content scripts. It seems to remain to be so till today. This post points to this StackOverflow question:

The accepted answer suggests to use dynamic import, instead of static import, in the content scripts of a Chrome browser extension. Unfortunately, however, neither this answer nor any other answers to this SO question worked for me. (Note that they were all from several years ago, when the manifest file version was v2.) That was it. Another day is gone, and I am no closer to the solution. I should probably give up at this point. Or, should I?

import log from 'loglevel';
log.warn("Who said vibe coding is forever? ๐Ÿ˜œ");