Exporting and Importing JS modules in google extensions [duplicate] - javascript

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.

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:
How to use ES6 “import” with Chrome Extension
Working Example of ES6 import in Chrome Extension
chrome.runtime.getURL
Hope it helps.

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. :)

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

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!

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!

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

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"
]
}
], ...
}

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"
}
}

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"
]
}
], ...
}

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.

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';

Related

My code stops working when i add an import statement [duplicate]

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.
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:
How to use ES6 “import” with Chrome Extension
Working Example of ES6 import in Chrome Extension
chrome.runtime.getURL
Hope it helps.
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. :)
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
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!
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!
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
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"
]
}
], ...
}
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"
}
}
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"
]
}
], ...
}
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.
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';

Writing a firefox extension - trying to import my own module into a content script causes a silent error [duplicate]

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.
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:
How to use ES6 “import” with Chrome Extension
Working Example of ES6 import in Chrome Extension
chrome.runtime.getURL
Hope it helps.
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. :)
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
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!
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!
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
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"
]
}
], ...
}
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"
}
}
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"
]
}
], ...
}
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.
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';

"Uncaught SyntaxError: Cannot use import statement outside a module" Trying to import library into a JavaScript file [duplicate]

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.
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:
How to use ES6 “import” with Chrome Extension
Working Example of ES6 import in Chrome Extension
chrome.runtime.getURL
Hope it helps.
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. :)
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
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!
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!
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
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"
]
}
], ...
}
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"
}
}
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"
]
}
], ...
}
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.
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';

Keep a ES module outside the bundle

My application has a configuration file
external-config.js
import {constants} from "./constants.js";
export const ExternalConfig = {
title: "My application",
version: "2.0",
constants: constants
list: ["uno", "due", "tre"]
}
I don't want it to be bundled with the application, I want to keep it outside. I tried the IgnorePlugin, but apparently this simply breaks the dependency graph and I get the error ReferenceError: Config is not defined even if the reference to the path of the config file in the budle is correct.
plugins: [
new webpack.IgnorePlugin({
checkResource (resource) {
if (resource === "./conf/external-config.js") return true;
return false;
}
})
]
I cannot even import it in the main html page like
<script type="module" src="./conf/config.js"></script>
because in this way I couldn't access the object outside its own script.
Is there a way to do that?
EDIT: following #raz-nonen advice, I tried null-loader, it seems it could be a solution. The problem with that is the physical position of the configuration file.
rules: [
{
test: path.resolve(__dirname, "src/conf/external-config.js"),
use: "null-loader"
}
...]
And this is the result in the built script
// EXTERNAL MODULE: ./src/conf/external-config.js
var external_config = __webpack_require__(13);
But the actual position of the configuration in the dist folder is ./conf/external-config.js, not ./src/conf/external-config.js
This is the chunk of my app that consumes the external file
import {ExternalConfig} from "./conf/external-config.js";
class MyApp extends LitElement {
constructor() {
super();
console.log(ExternalConfig.list)
}
}
You'll need to make this file available in the dist folder. You can do that with copy-webpack-plugin
Tell webpack that ExternalConfig will be imported from external. It means that you'll have to take care that it'll be available at runtime. You can do it simply by importing your conf/config.js that you copied from a <script> tag in your index.html.
Add:
externals: {
'conf/external-config': 'conf/external-config'
}
In your webpack configuration.

webpack - transpile 1 ts file without bundling it (2 entries)

TL;DR
(vue files) + background.ts => ...[webpack]... => (bundled vue files) + background.js
can't execute background.js
expected background.js to contain only "console.log('test');"
I have a vue project with webpack and typescript.
I want my build step to produce a "background.js" file aside from the [vue JS related files].
I have a source file in typescript: "background.ts".
Through the vue.config.js I added a webpack entry "background".
It does build a file "background.js" as I expected
but it is bundled(I think) and it can't be executed by a chrome plugin.
For now all I want is to have a "background.js" file which execute the "console.log('test');" instruction it contains when the script is called.
Thank you, webpack is hell
edit: adding files:
// vue.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
filenameHashing: false,
chainWebpack: config => {
// add your custom entry point
config
.entry('background')
.add('./src/background.ts');
},
configureWebpack: {
plugins: [
new CopyWebpackPlugin([
{ from: 'manifest.json', to: 'manifest.json', flatten: true },
]),
]
}
}
content of "$vue inspect"
$vue inspect > https://pastebin.com/6F3zwLhC
What I tried:
exporting a function instead of my plain code:
export default function() {
console.log("gboDebug: background.ts dans export function");
}
// instead of just
console.log("gboDebug: background.ts dans export function");
at the end of the file adding this because I saw it somewhere:
export default null;
checked that my console.log was in the background.js bundled file
pasted the result of background.js in the navigator
played with the webpackJsonp global var the scripts creates
What I thought about:
having a npm script which 1-bundle-vue-webpack and then 2-transpile my file with babel-loader
playing with the library output option in webpack but I think it makes code available for use in a variable, it doesn't auto-execute code when loaded
webpack output in IIFE: https://webpack.js.org/configuration/output/#outputiife
In short – you don't need a bundler for transpiling a single typescript file. Just use tsc.
Specifically to this question where the Vue app is used as part of chrome extension, it may make sense to separate building an app and the extension related files.
Another possible option is to use something like Vue CLI Browser Extension Plugin.

Categories

Resources