As we know, CRA is built using webpack, which is configured to bundle our application’s code and generate chunks of JavaScript which are then loaded by the application. This way of chunking code results in randomly generated files whose names change every time we run the build command. Let’s recall the structure of chrome extensions: we have content and background scripts, and a manifest.json.
In the manifest, we pass paths to the scripts to tell the browser which files it should load. The problem is, the manifest is a static json file so we have no way to dynamically tell the names of the JavaScript chunks we want to load. To fix this problem, we need to reconfigure webpack in a way that it will generate JavaScript files with non-changing names. And to do that we need to eject out of CRA.
npm run eject
This command will create a /config directory and there expose webpack configuration files to us.
We need to change two files, ‘paths.js’ and ‘webpack.config.js’ to disable chunking and tell webpack what to name the files.
// Add popup and content index js files – paths.js, lines 59-61
appHtml: resolveApp('public/popup.html'),
appIndexJs: resolveModule(resolveApp, 'src/popup/index'),
appContentJs: resolveModule(resolveApp, 'src/contentScript/index'),
// Define popup and content entry points – webpack.config.js, lines 172-200
entry: {
popup:
isEnvDevelopment && !shouldUseReactRefresh
? [
webpackDevClientEntry,
paths.appIndexJs,
]
: paths.appIndexJs,
background: paths.appBackgroundJs,
content: paths.appContentJs,
},
// Define output, remove the [contenthash] part from the generated file names – webpack.config.js lines 208-215
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
Remove the [contenthash] part from the generated file names.
Disable code chunking – webpack.config.js line 302 & 306
// Pass popup.html as name and exclude content from being injected into popup.html – webpack.config.js, lines 558-571
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
filename: 'popup.html',
},
isEnvProduction
? {
excludeChunks: [
'background',
'content',
],
Pass popup.html as name and exclude content from being injected into popup.html
// Rename main into popup (since that’s what we called the main entrypoint) – webpack.config.js, lines 657-659
const entrypointFiles = entrypoints.popup.filter(
fileName => !fileName.endsWith('.map')
);
Rename main into popup (since that’s what we called the main entrypoint)
Whew, that was quite a bit!
This way we can control how and where our JavaScript is output, so we can keep our file paths static.
The last thing we have to do now is add the new static js file paths to our content script field in the manifest.json and the related permissions shown below.
// manifest.json
{
"name": "React App",
"description": "Create React App Sample",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_popup": "popup.html",
"default_icon": "logo192.png",
"default_title": "Open the popup"
},
"icons": {
"16": "logo192.png",
"48": "logo192.png",
"128": "logo192.png"
},
"background": {
"scripts": ["static/js/background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["static/js/content.js"]
}
],
"optional_permissions": ["<all_urls>"],
"permissions": ["tabs", "activeTab"]
}
We can now build our React application and run it as a content script. Yay!
Let’s add one more thing: a background script.
To do that, we’ll follow the previous steps of adding a new subfolder src/background with an index.js file for our background script to live in.
To allow communication between all the different extension parts and scripts, the chrome extension API provides a messaging functionality. We can send a message from the content script to the background script, execute some logic in the background script, and send (or not) a response back to the content script.
chrome.runtime.onMessage.addListener((message, sender) => {
chrome.tabs.sendMessage(sender.tab?.id, {
value: message.value + " + " + "hello from background script",
});
});
We’ll just grab the message text and add ” + hello from background” to it before sending it back.
We can then intercept the message within the content script and for example display it in the frontend.
chrome.runtime.onMessage.addListener((message) => {
setMessage(message.value);
if (message.value === "openPopup") {
setOpen(true);
}
});
We can get the message from the listener and display it with useState.
We’ll have to adjust the webpack configuration to exclude the background script from chunking too.
// Add background/index.js to paths.js – line 61
appBackgroundJs: resolveModule(resolveApp, 'src/background/index'),
Add background/index.js to paths.js
// Add background path in webpack config. – line 198
background: paths.appBackgroundJs,
Add background path in webpack config.
// Exclude background script from being injected into popup.html – webpack.config.js, line 571
isEnvProduction
? {
excludeChunks: [
'background',
'content',
],
Exclude background script from being injected into popup.html
The difference between a background script and a context script is that content scripts run within the context of a webpage in one tab – so, if you have many tabs open, you might be running multiple instances of the content script, one in each tab. The background script on the other hand runs as one background process in the browser, no matter how many tabs you have open.