129

In Chrome 61, support for modules in JavaScript was added. Right now I am running Chrome 63.

I am trying to use import/export syntax in Chrome extension content script to use modules.

In manifest.json:

"content_scripts": [
    {
        "js": [
            "content.js"
        ],
    }
]

In my-script.js (same directory as content.js):

'use strict';

const injectFunction = () => window.alert('hello world');

export default injectFunction;

In content.js:

'use strict';

import injectFunction from './my-script.js';
injectFunction();

I receive this error: Uncaught SyntaxError: Unexpected identifier

If I change the import syntax to import {injectFunction} from './my-script.js'; I get this error: Uncaught SyntaxError: Unexpected token {

Is there some issue with using this syntax in content.js in Chrome extension (since in HTML you have to use <script type="module" src="script.js"> syntax), or am I doing something wrong? It seems strange that Google would ignore support for extensions.

Zearin
  • 1,474
  • 2
  • 17
  • 36
Ragnar
  • 4,292
  • 4
  • 31
  • 41
  • 1
    Chrome has support for modules with an asterisk: Only works when your script is a module script. The `import` statement is synchronous which doesn't work well with normal JavaScript. – Derek 朕會功夫 Jan 04 '18 at 22:24
  • How do you make a script to be a module script? – Ragnar Jan 04 '18 at 22:28
  • 1
    You don't need to. Simply add the path to `my-script.js` to the list in your `manifest.json` and it will be loaded according to the order you specified. – Derek 朕會功夫 Jan 04 '18 at 22:30
  • 1
    Can you please provide an example as an answer so it can be potentially marked as a correct anwer please? – Ragnar Jan 04 '18 at 22:32
  • 11
    Content scripts are unusual scripts, which are injected through an absolutely different mechanism, not like page scripts, so there's no support for ES6 modules. You can list the scripts files in manifest so they all get injected and run in the same execution context i.e. sharing the global isolated world. – wOxxOm Jan 04 '18 at 22:41

12 Answers12

88

Use the dynamic import() function.

Unlike the unsafe workaround using a <script> element, this one runs in the same safe JS environment (the isolated world of the content scripts), where your imported module can still access the global variables and functions of the initial content script, including the built-in chrome API like chrome.runtime.sendMessage.

In content_script.js, it looks like

(async () => {
  const src = chrome.runtime.getURL("your/content_main.js");
  const contentMain = await import(src);
  contentMain.main();
})();

You'll also need to declare the imported scripts in manifest's Web Accessible Resources:

// ManifestV3

  "web_accessible_resources": [{
     "matches": ["<all_urls>"],
     "resources": ["your/content_main.js"]
   }],

// ManifestV2

  "web_accessible_resources": [
     "your/content_main.js"
  ]

For more details:

Hope it helps.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
otiai10
  • 4,289
  • 5
  • 38
  • 50
  • 15
    Nice solution. It might be worth mentioning that scripts imported using this technique need to be allowlisted as `web_accessible_resources` in manifest. Not allowlisting them will result in `GET chrome-extension://invalid/ net::ERR_FAILED` error. This may not be obvious because regular content scripts don't need to be allowlisted. – Petr Srníček Jan 27 '19 at 21:58
  • 1
    This worked for me a few weeks ago, and now is throwing `Request scheme 'chrome-extension' is unsupported at sw.js:42` in Chrome >= 71. I see by stepping through the code that sw.js is using fetch to load my js files from an extension sandbox. Trying to make sure I haven't changed something else, but if someone else sees the same behavior please confirm. – SimplGy Mar 02 '19 at 20:31
  • 1
    This works in Chrome but not in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1536094 – fregante May 12 '20 at 16:19
  • 2
    The bug linked above has been resolved, and this strategy should work in Firefox 89. It already works in the latest Nightly! – eritbh Mar 24 '21 at 02:40
  • Any quick way to make the import function work synchronously? Otherwise you can't just wrap this in a function and rely on having the library imported. – Dror Jun 29 '21 at 14:21
  • 1
    Important to note that you only need to dynamically import the initial script, nested imports can be done in `esm` format – Madacol Mar 10 '22 at 22:43
  • 2
    If you're using webpack, add `/*webpackIgnore: true*/` inside of your `import` call to get webpack to leave the import alone (this method didn't work without it for me). – fisch2 Mar 16 '22 at 08:16
  • @fisch2 saved my day! lol. `import(/*webpackIgnore: true*/ 'path_to_your.js');` – dataviews Jan 13 '23 at 14:35
  • I don't understand why the dynamic import still doesn't work for me. When transpiled, Chrome complains about the `require` keyword in .js file – dsenese Jan 20 '23 at 19:47
  • @otiaai10 How we can import another js into content_main.js ? – Ali Zarei May 14 '23 at 13:08
  • I found `import("your/content_main.js");` works the same, which is easier to remember. Also, it seems important to note that all other files can use `import { x } from "./y.js"` style imports. I assumed I would have to use`import()` in all files, but that is not the case, only the first file requires it. – yeerk Jul 25 '23 at 20:03
  • `webextension-polyfill` solutions? – Elijah Mock Jul 27 '23 at 15:55
49

Disclaimer

First of all, it’s important to say that content scripts don’t support modules as of January 2018. This workaround sidesteps the limitation by embedding module script tag into the page that leads back to your extension.

THIS IS AN UNSAFE WORKAROUND!

A web page script (or another extension) can exploit your code and extract/spoof the data by using setters/getters on Object.prototype and other prototypes, proxying functions and/or global objects, because the code inside a script element runs in the JS context of the page, not in the safe isolated JS environment where content scripts run by default.

A safe workaround is the dynamic import() shown in another answer here.

Workaround

This is my manifest.json:

    "content_scripts": [ {
       "js": [
         "content.js"
       ]
    }],
    "web_accessible_resources": [
       "main.js",
       "my-script.js"
    ]

Note that I have two scripts in web_accessible_resources.

This is my content.js:

    'use strict';
    
    const script = document.createElement('script');
    script.setAttribute("type", "module");
    script.setAttribute("src", chrome.extension.getURL('main.js'));
    const head = document.head || document.getElementsByTagName("head")[0] || document.documentElement;
    head.insertBefore(script, head.lastChild);

This will insert main.js into the webpage as a module script.

All my business logic is now in main.js.

For this method to work, main.js (as well as all scripts that I will import) must be in web_accessible_resources in the manifest.

Example Usage: my-script.js

    'use strict';
    
    const injectFunction = () => window.alert('hello world');
    
    export {injectFunction};

And in main.js this is an example of importing the script:

    'use strict';
    
    import {injectFunction} from './my-script.js';
    injectFunction();

This works! No errors are thrown, and I am happy. :)

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
Ragnar
  • 4,292
  • 4
  • 31
  • 41
  • 31
    Nice solution. The only problem is that the chrome.runtime APIs become unavailable :( – Vincent McNabb Jan 15 '18 at 11:11
  • Can you be please more specific what your issue is? – Ragnar Jan 16 '18 at 15:40
  • 9
    For instance, if in your main.js above, you had `chrome.runtime.onMessage.addListener((request, sender, sendResponse), () => ...)` the handler will never be called. There's no workaround for that as that's by design. But I'm using Webpack to compile my extension into a single file, so it's not an issue for me. – Vincent McNabb Jan 17 '18 at 07:09
  • 2
    Another drawback: "modules" are async. If you are trying to injects js that should execute before the target page's own js, that doesn't work anymore. Of course, that may not matter depending on one's needs. – Offirmo Feb 28 '19 at 23:34
  • 16
    I don't recommend this pattern, since it removes the context isolation from your content scripts. In addition to that it allows websites to detect you extension. This is a security vulnerability. – HaNdTriX Jul 09 '19 at 12:10
  • 3
    ↑ no one's *recommending* it, it's a workaround until browser implementations catch-up. @ ragnar: thanks for the nice clear example; for the record, this technique seems to work well for me in firefox 56 as well (with `dom.moduleScripts.enabled`) – Cauterite Dec 30 '19 at 11:05
  • 2
    To make this work in 2021, I had to replace `chrome.extension.getURL` with `chrome.runtime.getURL`, and `"web_accessible_resources": ["main.js", "my-script.js"]` to `"web_accessible_resources": {"resources": ["main.js", "my-script.js"], "matches": ["*://*/*"]}` – Philippe-André Lorin Nov 20 '21 at 15:21
  • What is the state of it in 2022? I believe this workaround is still recommended – Varunkumar Nagarajan Jun 29 '22 at 03:20
25

imports are not available in content scripts.

Here's a workaround using global scope.

Since content scripts live in their own 'isolated world' - they share the same global namespace. It is only accessible to content scripts declared in manifest.json.

Here's the implementation:

manifest.json

"content_scripts": [
  {
    "matches": ["<all_urls>"],
    "js": [
      "content-scripts/globals.js",
      "content-scripts/script1.js",
      "content-scripts/script2.js"
    ]
  }
],

globals.js

globalThis.foo = 123;

script1.js

some_fn_that_needs_foo(globalThis.foo);

Same way you can factor out re-usable functions and other actors you would otherwise import in content script files.

N.B.: global namespace of content scripts is not available to any pages besides content scripts - so there is little to no global scope pollution.

In case you need to import some libs - you will have to use a bundler like Parcel to package up your content script files along with the needed libs into one huge-content-script.js and then metion it in manifest.json.

P.S.: docs on globalThis

avalanche1
  • 3,154
  • 1
  • 31
  • 38
19

The best way would be to use bundlers like webpack or Rollup.

I got away with basic configuration

const path = require('path');

module.exports = {
  entry: {
    background: './background.js',
    content: './content.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, '../build')
  }
};

Run the file with the command

webpack --config ./ext/webpack-ext.config.js

Bundlers combine the related files and we can use modularisation in chrome extensions! :D

You will need to keep all other files like manifest and static files in build folder.

Play around with it and you will eventually find a way to make it work!

Dhruvil Shah
  • 199
  • 1
  • 2
9

I just stumbled across this question while trying to solve the same thing myself.

Anyways, I think there's a simpler solution to injecting your own custom modules into your content script. I was looking at how Jquery is injected and it occurs to me you can do the same thing by creating an IIFE (Immediately Invoked Function Expression), and declaring it in your manifest.json

It goes something like this:

In your manifest.json:

"content_scripts": [
{
  "matches": ["https://*"],
  "css": ["css/popup.css"],
  "js": ["helpers/helpers.js"]
}],

Then just create an IIFE in your helpers/helpers.js:

var Helpers = (function() {
  var getRandomArbitrary = function(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
  }
  return {
    getRandomArbitrary: getRandomArbitrary
  }
})()

Now, you can freely use your helper functions in your content script:

Helpers.getRandomArbitrary(0, 10) // voila!

I think it's great if you use this method to refactor some of your generic functions. Hope this helps!

Daniel Chan
  • 296
  • 1
  • 3
  • 11
  • 1
    Nice idea! Just say if I'm wrong, but those content scripts are included immediatly each time the user loads a page? – Boiethios Aug 05 '20 at 08:14
  • @Boiethios if you put "run_at": "document_start" they run at the beginning of the page load, if you put "document_end"... they run at the end – Thorvarium Sep 23 '22 at 20:24
3

For Vite Users

There is an awesome plugin called crxjs , you just need to update it in vite.config.ts and give path to your manifest.json(it works with only mv3)

Follow the below steps to get your script running

1.Add crxjs to your project

npm install @crxjs/vite-plugin -D

2.Create or update manifest.json

{
  "manifest_version": 3,
  "name": "CRXJS React Vite Example",
  "version": "1.0.0",
  "action": { "default_popup": "index.html" }
}

3.Update your vite.config.ts file with path to manifest

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'

export default defineConfig({
  plugins: [
    react(),
    crx({ manifest }),
  ],
})

After this run your project , now config.js will be bundled and you can import packages in it

Goutham J.M
  • 1,726
  • 12
  • 25
  • 1
    Omg, I was configuring Vite myself until I ran into this problem. Thanks, this is the best tool ever! – Yulian Jan 19 '23 at 16:03
1

Short Answer:

You can mimic some of the functionality and get some of the benefits of import/export in browser extensions by creating the following file and listing it early in your manifest.json:

let exportVars, importVarsFrom;
{
  const modules = {};
  exportVars = varsObj => ({
    from(nameSpace) {
      modules[nameSpace] || (modules[nameSpace] = {});
      for (let [k,v] of Object.entries(varsObj)) {
        modules[nameSpace][k] = v;
      }
    }
  });
  importVarsFrom = nameSpace => modules[nameSpace];
}

Then, export from one file/module like this:

exportVars({ var1, var2, var3 }).from('my-utility');

Import into another file/module like this:

const { var1, var3: newNameForVar3 } = importVarsFrom('my-utility');

Discussion:

This strategy:

  • allows modular code in a browser extension such that you can split code into multiple files but don't have variable clashes due to shared global scope between different files,
  • still allows you to export and import variables out of and into different JavaScript files/modules,
  • introduces only two global variables, namely the exporting function and the importing function,
  • maintains full browser extension functionality in each file (e.g. chrome.runtime, etc.) that is eliminated by, e.g., the approach in another answer (currently the accepted answer) using module script tag embedding,
  • uses a concise syntax similar to the true import and export functions in JavaScript,
  • allows name-spacing which could be the file names of the exporting modules in a manner similar to how the true import and export commands work in JavaScript, but doesn't have to be (i.e. the name-space names could be anything you want), and
  • allows variable renaming upon import similar to how import { fn as myFn }... works.

To do this, your manifest.json needs to load your JavaScript as follows:

  • the file establishing the exporting/importing functions first (named modules-start.js in the example below),
  • the exporting files next, and
  • the importing files last.

Of course, you might have a file that both imports and exports. In that case, just ensure it is listed after the files it imports from but before the files it exports to.

Working Example

The following code demonstrates this strategy.

It is important to note that all of the code in each module/file is contained within curly braces. The only exception is the first line in modules-start.js which establishes the exporting and importing functions as global variables.

The code in the snippet below is necessarily contained in a single "place". In a real project, however, the code could be split into separate files. Note, though, that even in this artificial context here (i.e. within the single code snippet below), this strategy allows the different sections of code it contains to be modular and yet still interconnected.

// modules-start.js:
let exportVars, importVarsFrom; // the only line NOT within curly braces
{
  const modules = {};
  exportVars = varsObj => ({
    from(nameSpace) {
      modules[nameSpace] || (modules[nameSpace] = {});
      for (let [k,v] of Object.entries(varsObj)) {
        modules[nameSpace][k] = v;
      }
    }
  });
  importVarsFrom = nameSpace => modules[nameSpace];
}


// *** All of the following is just demo code
// *** showing how to use this export/import functionality:

// my-general-utilities.js (an example file that exports):
{
  const wontPolluteTheGlobalScope = 'f';
  const myString = wontPolluteTheGlobalScope + 'oo';
  const myFunction = (a, b) => a + b;
  
  // the export statement:
  exportVars({ myString, myFunction }).from('my-general-utilities');
}

// content.js (an example file that imports):
{
  // the import statement:
  const { myString, myFunction: sum } = importVarsFrom('my-general-utilities');

  console.log(`The imported string is "${myString}".`);
  console.log(`The renamed imported function shows that 2 + 3 = ${sum(2,3)}.`);
}

With this example, your manifest.json should list the files in the following order:

{ ...
  "content_scripts": [
    {
      "js": [
        "modules-start.js",
        "my-general-utilities.js",
        "content.js"
      ]
    }
  ], ...
}
Andrew Willems
  • 11,880
  • 10
  • 53
  • 70
1

Using Rollup bundler

full tutorial: https://www.extend-chrome.dev/rollup-plugin#usage


TL;DR

npm i -D rollup\
   rollup-plugin-chrome-extension@latest\
   @rollup/plugin-node-resolve\
   @rollup/plugin-commonjs

rollup.config.js:

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'

import { chromeExtension, simpleReloader } from 'rollup-plugin-chrome-extension'

export default {
  input: 'src/manifest.json',
  output: {
    dir: 'dist',
    format: 'esm',
  },
  plugins: [
    // always put chromeExtension() before other plugins
    chromeExtension(),
    simpleReloader(),
    // the plugins below are optional
    resolve(),
    commonjs(),
  ],
}

package.json:


{
  "scripts": {
    "build": "rollup -c",
    "start": "rollup -c -w"
  }
}
Madacol
  • 3,611
  • 34
  • 33
1

Using esbuild

Further to Dhruvil's answer, here's a GitHub repo showing how to use esbuild to bundle content scripts written in TypeScript and React - therefore enabling you to import es6 modules.

It also includes bundling the background service worker and popup, with scripts that enable Hot Module Reloading when running the popup locally.

Matt Burrell
  • 340
  • 2
  • 10
0

Add simply in manifest.json in V2

Note! After changing in manifest.json, make sure to reload the extension and browser tab

{ ...
  "content_scripts": [
    {
      "js": [
        "modules-start.js",
        "my-general-utilities.js",
        "content.js"
      ]
    }
  ], ...
}
Abdul Rehman
  • 139
  • 1
  • 5
0

Quick & Simple Bundling With esbuild

If your project requirements permit, consider using a bundler like esbuild to help avoid compatibility issues by transpiling the syntax into browser-compatible code. The setup is quick and unlike other bundlers such as webpack, setup of a config file is not required.

Step-by-Step

esbuild is a fast and modern bundler with quick set-up. Other bundlers like parcel could work too (see detailed guide).

  1. Installing the Bundler

    Run the following command to install esbuild as a dependency (see documentation):

    npm install --save-exact esbuild
    
  2. Bundling the Scripts

    Once installed, you can execute the following command in your root directory (i.e., where package.json is located) to bundle your client-side code:

    esbuild src/content.js --bundle --outfile=dist/bundle.js
    

    Using esbuild to bundle content.js will handle all imported modules, including my-script.js, and include them in the output bundle.js.

    Please note that src should be replaced with the correct file path for content.js, which I assume is a single entry point. Depending on your project requirements, esbuild supports set up of multiple entry points too (see documentation).

  3. Copying manifest.json

    The dist folder should also contain a copy of the manifest.json file, revised with the updated path to the bundled file.

    Before:

    "content_scripts": [
        {
            "js": [
                "content.js"
            ],
        }
    ]
    

    After:

    "content_scripts": [
        {
            "js": [
                "bundle.js"
            ],
        }
    ]
    

Explanation

Checking Syntax

Using a bundler like esbuild will transpile the files as browser-compatible ES syntax by default, meaning it can handle scenarios where there are files with a mix of ES and CommonJS syntax or files with only CommonJS syntax (see documentation).

Browser Compatibility

Browsers can return errors if ES syntax is not a supported version or is not used consistently for imports and exports in scripts. To handle version incompatibility, we can target a specific version of the ES syntax by using the --target flag in esbuild (see documentation).

-10

Export the module as a object:

'use strict';

const injectFunction = () => window.alert('hello world');

export {injectFunction};

Then you can import its property:

'use strict';
import {injectFunction} from './my-script.js';
亚里士朱德
  • 510
  • 5
  • 15