Related
I'm working on an extension system for my web app. Third-party developers should be able to extend the app by providing named AMD modules exporting constants and functions following a predefined spec and bundled into a single .js JavaScript file.
Example JavaScript bundle:
define('module1', ['exports', 'module3'], (function (exports, module3) {
exports.spec = 'http://example.com/spec/extension/v1'
exports.onRequest = function (request) { return module3.respond('Hello, World.') }
}));
define('module2', ['exports', 'module3'], (function (exports, module3) {
exports.spec = 'http://example.com/spec/extension/v1'
exports.onRequest = function (request) { return module3.respond('Foo. Bar.') }
}));
define('module3', ['exports'], (function (exports) {
exports.respond = function (message) { return { type: 'message', message: message } }
}));
In the above example module1 and module2 are extension modules (identified by the spec export) and module3 is a shared dependency (e.g. coming from an NPM package). Extension bundles will be loaded in a worker within a sandboxed iframe to seal of the untrusted code in the browser.
Example TypeScript source:
// module1.ts
import respond from 'module3'
export const spec = 'http://example.com/spec/extension/v1'
export const onRequest = (request: Request): Response => respond('Hello, World.')
// module2.ts
import respond from 'module3'
export const spec = 'http://example.com/spec/extension/v1'
export const onRequest = (request: Request): Response => respond('Foo. Bar.')
// module3.ts
import dep from 'some-npm-package'
export respond = (message: string) => dep.createMessageObject(message)
Here is my list of requirements to bundling:
All necessary dependencies (e.g. shared module, NPM package logic) must be included in the bundle
The source code needs to be transpiled to browser compatible code if necessary
The AMD format is required by the custom extension loader implementation
The AMD modules must not be anonymous as the module file names are lost while bundling
No relative paths must be used among dependencies (e.g. ./path/to/module3 instead of module3)
The result should be one JavaScript bundle, thus ONE JavaScript file and ONE sourcemaps file
What's the easiest way to do this?
This is the closest solution I found using rollup and the following rollup.config.js:
import { nodeResolve } from '#rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import typescript from '#rollup/plugin-typescript'
export default {
input: [
'src/module1.ts',
'src/module2.ts'
],
output: {
dir: 'dist',
format: 'amd',
sourcemap: true,
amd: {
autoId: true
}
},
plugins: [
typescript(),
nodeResolve(),
terser()
]
}
From this I get the desired named AMD modules (one for each entry point and chunk) in separate .js files. Problems:
Some dependencies are referenced by ./module3 while being named module3.
The modules appear in separate JavaScript and Sourcemap files instead of being concatenated into a single bundle.
Questions:
Is there an easy fix to the above rollup.config.js config to solve the problem?
I tried to write a small rollup plugin but I failed to get the final AMD module code within it to concatenate it to a bundle. Only the transpiled code is available to me. In addition I don't know how to handle sourcemaps during concatenation.
Is there an alternative to rollup better suited to this bundling scenario?
The big picture: Am I completely on the wrong track when it comes to building an extension system? Is AMD the wrong choice?
I found a way to extend the rollup.config.js mentioned in the question with a custom concatChunks rollup plugin to bundle multiple AMD chunks within a single file and having the source maps rendered, too. The only issue I didn't find an answer to was the relative module names that kept popping up. However, this may be resolved in the AMD loader.
Here's the full rollup.config.js that worked for me:
import Concat from 'concat-with-sourcemaps'
import glob from 'glob'
import typescript from '#rollup/plugin-typescript'
import { nodeResolve } from '#rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
const concatChunks = (
fileName = 'bundle.js',
sourceMapFileName = 'bundle.js.map'
) => {
return {
name: 'rollup-plugin-concat-chunks',
generateBundle: function (options, bundle, isWrite) {
const concat = new Concat(true, fileName, '\n')
// Go through each chunk in the bundle
let hasSourceMaps = false
Object.keys(bundle).forEach(fileId => {
const fileInfo = bundle[fileId]
if (fileInfo.type === 'chunk') {
let hasSourceMap = fileInfo.map !== null
hasSourceMaps = hasSourceMaps || hasSourceMap
// Concat file content and source maps with bundle
concat.add(
fileInfo.fileName,
fileInfo.code,
hasSourceMap ? JSON.stringify(fileInfo.map) : null
)
// Prevent single chunks from being emitted
delete bundle[fileId]
}
})
// Emit concatenated chunks
this.emitFile({
type: 'asset',
name: fileName,
fileName: fileName,
source: concat.content
})
// Emit concatenated source maps, if any
if (hasSourceMaps) {
this.emitFile({
type: 'asset',
name: sourceMapFileName,
fileName: sourceMapFileName,
source: concat.sourceMap
})
}
}
}
}
export default {
input: glob.sync('./src/*.{ts,js}'),
output: {
dir: 'dist',
format: 'amd',
sourcemap: true,
amd: {
autoId: true
}
},
plugins: [
typescript(),
nodeResolve(),
terser(),
concatChunks()
]
}
Please make sure you npm install the dependencies referenced in the import statements to make this work.
Considering the big picture, i.e. the extension system itself, I am moving away from a "one AMD module equals one extension/contribution" approach, as current developer tools and JavaScript bundlers are not ready for that (as this question shows). I'll go with an approach similar to the Visual Studio Code Extension API and will use a single "default" module with an activate export to register contributions a bundle has to offer. I hope that this will make extension bundling an easy task no matter what tools or languages are being used.
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';
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';
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';
Suppose I have the following module:
var modulesReq = require.context('.', false, /\.js$/);
modulesReq.keys().forEach(function(module) {
modulesReq(module);
});
Jest complains because it doesn't know about require.context:
FAIL /foo/bar.spec.js (0s)
● Runtime Error
- TypeError: require.context is not a function
How can I mock it? I tried using setupTestFrameworkScriptFile Jest configuration but the tests can't see any changes that I've made in require.
I had the same problem, then I've made a 'solution'.
I'm pretty sure that this is not the best choice. I ended up stopping using it, by the points answered here:
https://github.com/facebookincubator/create-react-app/issues/517
https://github.com/facebook/jest/issues/2298
But if you really need it, you should include the polyfill below in every file that you call it (not on the tests file itself, because the require will be no global overridden in a Node environment).
// This condition actually should detect if it's an Node environment
if (typeof require.context === 'undefined') {
const fs = require('fs');
const path = require('path');
require.context = (base = '.', scanSubDirectories = false, regularExpression = /\.js$/) => {
const files = {};
function readDirectory(directory) {
fs.readdirSync(directory).forEach((file) => {
const fullPath = path.resolve(directory, file);
if (fs.statSync(fullPath).isDirectory()) {
if (scanSubDirectories) readDirectory(fullPath);
return;
}
if (!regularExpression.test(fullPath)) return;
files[fullPath] = true;
});
}
readDirectory(path.resolve(__dirname, base));
function Module(file) {
return require(file);
}
Module.keys = () => Object.keys(files);
return Module;
};
}
With this function, you don't need to change any require.context call, it will execute with the same behavior as it would (if it's on webpack it will just use the original implementation, and if it's inside Jest execution, with the polyfill function).
After spending some hours trying each of the answers above. I would like to contribute.
Adding babel-plugin-transform-require-context plugin to .babelrc for test env fixed all the issues.
Install - babel-plugin-transform-require-context here https://www.npmjs.com/package/babel-plugin-transform-require-context (available with yarn too)
Now add plugin to .babelrc
{
"env": {
"test": {
"plugins": ["transform-require-context"]
}
}
}
It will simply transform require-context for test env into dummy fn calls so that code can run safely.
If you are using Babel, look at babel-plugin-require-context-hook. Configuration instructions for Storybook are available at Storyshots | Configure Jest to work with Webpack's require.context(), but they are not Storyshots/Storybook specific.
To summarise:
Install the plugin.
yarn add babel-plugin-require-context-hook --dev
Create a file .jest/register-context.js with the following contents:
import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
registerRequireContextHook();
Configure Jest (the file depends on where you are storing your Jest configuration, e.g. package.json):
setupFiles: ['<rootDir>/.jest/register-context.js']
Add the plugin to .babelrc
{
"presets": ["..."],
"plugins": ["..."],
"env": {
"test": {
"plugins": ["require-context-hook"]
}
}
}
Alternatively, add it to babel.config.js:
module.exports = function(api) {
api.cache(true)
const presets = [...]
const plugins = [...]
if (process.env.NODE_ENV === "test") {
plugins.push("require-context-hook")
}
return {
presets,
plugins
}
}
It may be worth noting that using babel.config.js rather than .babelrc may cause issues. For example, I found that when I defined the require-context-hook plugin in babel.config.js:
Jest 22 didn't pick it up;
Jest 23 picked it up; but
jest --coverage didn't pick it up (perhaps Istanbul isn't up to speed with Babel 7?).
In all cases, a .babelrc configuration was fine.
Remarks on Edmundo Rodrigues's answer
This babel-plugin-require-context-hook plugin uses code that is similar to Edmundo Rodrigues's answer here. Props to Edmundo! Because the plugin is implemented as a Babel plugin, it avoids static analysis issues. e.g. With Edmundo's solution, Webpack warns:
Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
Despite the warnings, Edmundo's solution is the most robust because it doesn't depend on Babel.
Extract the call to a separate module:
// src/js/lib/bundle-loader.js
/* istanbul ignore next */
module.exports = require.context('bundle-loader?lazy!../components/', false, /.*\.vue$/)
Use the new module in the module where you extracted it from:
// src/js/lib/loader.js
const loadModule = require('lib/bundle-loader')
Create a mock for the newly created bundle-loader module:
// test/unit/specs/__mocks__/lib/bundle-loader.js
export default () => () => 'foobar'
Use the mock in your test:
// test/unit/specs/lib/loader.spec.js
jest.mock('lib/bundle-loader')
import Loader from 'lib/loader'
describe('lib/loader', () => {
describe('Loader', () => {
it('should load', () => {
const loader = new Loader('[data-module]')
expect(loader).toBeInstanceOf(Loader)
})
})
})
Alrighty! I had major issues with this and managed to come to a solution that worked for me by using a combination of other answers and the Docs. (Took me a good day though)
For anyone else who is struggling:
Create a file called bundle-loader.js and add something like:
module.exports = {
importFiles: () => {
const r = require.context(<your_path_to_your_files>)
<your_processing>
return <your_processed_files>
}
}
In your code import like:
import bundleLoader from '<your_relative_Path>/bundle-loader'
Use like
let <your_var_name> = bundleLoader.importFiles()
In your test file right underneath other imports:
jest.mock('../../utils/bundle-loader', () => ({
importFiles: () => {
return <this_will_be_what_you_recieve_in_the_test_from_import_files>
}
}))
Installing
babel-plugin-transform-require-context
package and adding the plugin in the .babelrc resolved the issue for me.
Refer to the documentation here:
https://www.npmjs.com/package/babel-plugin-transform-require-context
The easiest and fastest way to solve this problem will be to install require-context.macro
npm install --save-dev require-context.macro
then just replace:
var modulesReq = require.context('.', false, /\.js$/);
with:
var modulesReq = requireContext('.', false, /\.js$/);
Thats it, you should be good to go!
Cheers and good luck!
Implementation problems not mentioned:
Jest prevents out-of-scope variables in mock, like __dirname.
Create React App limits Babel and Jest customization. You need to use src/setupTests.js which is run before every test.
fs is not supported in the browser. You will need something like browserFS. Now your app has file system support, just for dev.
Potential race condition. Export after this import. One of your require.context imports includes that export. I'm sure require takes care of this, but now we are adding a lot of fs work on top of it.
Type checking.
Either #4 or #5 created undefined errors. Type out the imports, no more errors. No more concerns about what can or can't be imported and where.
Motivation for all this? Extensibility. Keeping future modifications limited to one new file. Publishing separate modules is a better approach.
If there's an easier way to import, node would do it. Also this smacks of premature optimization. You end up scrapping everything anyways because you're now using an industry leading platform or utility.
If you're using Jest with test-utils in Vue.
Install these packages:
#vue/cli-plugin-babel
and
babel-plugin-transform-require-context
Then define babel.config.js at the root of the project with this configuration:
module.exports = function(api) {
api.cache(true);
const presets = [
'#vue/cli-plugin-babel/preset'
];
const plugins = [];
if (process.env.NODE_ENV === 'test') {
plugins.push('transform-require-context');
}
return {
presets,
plugins
};
};
This will check if the current process is initiated by Jest and if so, it mocks all the require.context calls.
I faced the same issue with an ejected create-react-app project
and no one from the answers above helped me...
My solution were to copy to config/babelTransform.js the follwoing:
module.exports = babelJest.createTransformer({
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
plugins:["transform-require-context"],
babelrc: false,
configFile: false,
});
Simpleset Solution for this
Just Do
var modulesReq = require.context && require.context('.', false, /\.js$/);
if(modulesReq) {
modulesReq.keys().forEach(function(module) {
modulesReq(module);
});
}
So Here I have added extra check if require.context is defined then only execute By Doing this jest will no longer complain