I want to enable the extension icon under certain conditions using declarativeContent. The manifest is V3.
rule1 is applied when host is nory-soft.web.app and password is entered.
rule2 applies if host ends in .wikipedia.org.
const rule1 = {
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: "nory-soft.web.app", schemes: ["https"] },
css: ["input[type='password']"]
})
],
actions: [new chrome.declarativeContent.ShowAction()]
};
const rule2 = {
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostSuffix: ".wikipedia.org", schemes: ["https"] }
})
],
actions: [new chrome.declarativeContent.ShowAction()]
};
chrome.runtime.onInstalled.addListener(() => {
console.log("onInstalled");
chrome.action.disable();
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([rule1, rule2]);
});
});
chrome.runtime.onStartup.addListener(() => {
console.log("onStartup");
chrome.action.disable();
});
Below is the Test URL for rule1.
https://nory-soft.web.app/password.html
rule2 worked as expected.
When I installed the extension with Test URL open, rule1 was enabled.
After installing the extension without Test URL open, rule1 will not be enabled even if Test URL is opened.
Could this be a Chrome bug?
Related
In my chrome extension with manifest V2 I was using chrome.webRequest.onBeforeRequest to get all requests of current tab. Here is
const dataSet = {};
chrome.webRequest.onBeforeRequest.addListener(function (details) {
if (details && details.url && details.type == "image") {
if (!dataSet[tabId]) {
dataSet[tabId] = new Set([]);
}
const currentSet = dataSet[tabId];
currentSet.add(details.url);
}
}, {
urls: ["<all_urls>"]
});
I'm trying same code in manifest version 3 but event didn't triggers. Also I've tried this workaround but it still didn't works.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
// this event is not being triggered
chrome.webRequest.onBeforeRequest.addListener(function(details){
},{urls: ["<all_urls>"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
Also tried to use webNavigation.onHistoryStateUpdated but still onBeforeRequest didn't triggers
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up', details);
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
console.log(details);
},
{
urls: ['<all_urls>'],
},
);
});
Console output of background page
I'm trying to develop a simple chrome extension. There is a pageAction's default icon that should appear on the pages with a specific URL (http://www.example.com/*).
There is a two file
manifest.json
{
"manifest_version": 2,
"name": "name",
"description": "description",
"version": "1.0",
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"page_action": {
"default_icon" : "images/icons/19.png"
},
"permissions": [
"declarativeContent"
]
}
background.js
chrome.runtime.onInstalled.addListener(function () {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
chrome.declarativeContent.onPageChanged.addRules([
{
// rule1
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {urlPrefix : 'http://www.example.com/'}
})
],
actions : [
new chrome.declarativeContent.ShowPageAction()
]
},
{
// rule2
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [
new chrome.declarativeContent.SetIcon({
path : {"19" : "images/icons/green.png"}
})
]
}
]);
});
});
rule1 should show pageAction's icon and rule2 should change icon to green version on the pages with URL that looks like http://www.example.com/?q1=green
But during installation of extension things come to:
Error in response to events.removeRules: Error: Invalid value for argument 1. Property '.0': Value does not match any valid type choices.
I dug deeply into this error, and it seems like the documentation does not reflect well the fact that using path parameter is not implemented. This is certainly a bug, tracked here.
For now, to fix this you need to load the image and convert it to ImageData format before calling SetIcon.
// Takes a local path to intended 19x19 icon
// and passes a correct SetIcon action to the callback
function createSetIconAction(path, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image = new Image();
image.onload = function() {
ctx.drawImage(image,0,0,19,19);
var imageData = ctx.getImageData(0,0,19,19);
var action = new chrome.declarativeContent.SetIcon({imageData: imageData});
callback(action);
}
image.src = chrome.runtime.getURL(path);
}
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
createSetIconAction("images/icons/green.png", function(setIconAction) {
chrome.declarativeContent.onPageChanged.addRules([
/* rule1, */
{
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [ setIconAction ]
}
]);
});
});
If needed, this can be generalized to support high-DPI icon (19 + 38):
function createSetIconAction(path19, path38, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image19 = new Image();
image19.onload = function() {
ctx.drawImage(image19,0,0,19,19); // fixed
var imageData19 = ctx.getImageData(0,0,19,19);
var image38 = new Image();
image38.onload = function() {
ctx.drawImage(image38,0,0,38,38);
var imageData38 = ctx.getImageData(0,0,38,38);
var action = new chrome.declarativeContent.SetIcon({
imageData: {19: imageData19, 38: imageData38}
});
callback(action);
}
image38.src = chrome.runtime.getURL(path38);
}
image19.src = chrome.runtime.getURL(path19);
}
In fact, you can use new chrome.declarativeContent.SetIcon({ path:'yourPath.png' }),
No need to specify size path: {"19": "images/icons/green.png"}, its default value is: 16
Use declarativeContent.SetIcon need to pay attention to a problem, it is actually a bug.
Actual use of path will eventually be automatically converted to ImageData.
see screenshot:
The root cause of the error of declarativeContent.SetIcon is: it is an asynchronous API, but at the same time it has no asynchronous callback. The only thing you can do is wait.
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
console.log(action.imageData); // => undefined
see screenshot:
// invalid
new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' }, action => console.log(action));
It takes a while to wait:
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
setTimeout(() => {
console.log(action.imageData); // {16: ArrayBuffer(1060)}
}, 5);
see screenshot:
When you understand the reason for the error of SetIcon, the problem will be solved well.
You only need to put the operation of addRules in the event.
onInstalled event
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
});
});
pageAction.onClicked
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.pageAction.onClicked.addListener(() => {
if (condition) {
chrome.declarativeContent.onPageChanged.removeRules(['hideAction']);
} else {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
}
});
There are some related information:
SetIcon source code
declarativeContent.SetIcon = function (parameters) {
// TODO(devlin): This is very, very wrong. setIcon() is potentially
// asynchronous (in the case of a path being specified), which means this
// becomes an "asynchronous constructor". Errors can be thrown *after* the
// `new declarativeContent.SetIcon(...)` call, and in the async cases,
// this wouldn't work when we immediately add the action via an API call
// (e.g.,
// chrome.declarativeContent.onPageChange.addRules(
// [{conditions: ..., actions: [ new SetIcon(...) ]}]);
// ). Some of this is tracked in http://crbug.com/415315.
setIcon(
parameters,
$Function.bind(function (data) {
// Fake calling the original function as a constructor.
$Object.setPrototypeOf(this, nativeSetIcon.prototype);
$Function.apply(nativeSetIcon, this, [data]);
}, this)
);
};
Discussion of related issues:
http://crbug.com/415315
No solution
As the guys before me mentioned, this is a bug. There are no solutions, only workarounds.
Workarounds
#1: Draw icon using canvas
As Xan described in his answer already.
#2 Wait for icon load (timeout hack)
Thanks to weiya-ou's answer I realized that I can just wait for the async icon data transformation to finish.
// Make your handler `async`
chrome.runtime.onInstalled.addListener(async () => {
const action = await new chrome.declarativeContent.SetIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
})
// THE WAIT STARTS
// Wait max. 10 loops
for (let i = 0; i < 10; i++) {
// Create a promise
const checkAvailability = new Promise((resolve) => {
// Resolve promise after 100ms
setTimeout(() => resolve(!!action.imageData), 100)
})
// Wait for the promise resolution
const isAvailable = await checkAvailability
// When image available, we are done here
if (isAvailable) break
}
// THE WAIT ENDS
const condition = new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: 'my.page.net' },
})
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [condition],
actions: [action],
},
]);
});
});
#3 Use chrome.tabs
You would need the tabs permission (as said here).
chrome.tabs.onUpdated.addListener((tabId, { status }, { url }) => {
// Only check when URL is resolved
if (status !== 'complete') return
// What is our target page?
const isOurPage = url?.match(/my\.page\.net/)
if (isOurPage) {
// Show active icon
chrome.pageAction.setIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
tabId,
})
} else {
// Show inactive icon
chrome.pageAction.setIcon({
path: {
19: 'images/19-inactive.png',
38: 'images/38-inactive.png',
},
tabId,
})
}
})
I'm trying to implement a rewrite that will mask the destination if it's '/dest' to '/new'
The Next.js documentation suggested the following
module.exports = {
async rewrites() {
return [
{
source: '/dest',
destination: '/new',
},
]
},
}
I'm having a hard time plugging it into my code which contains
rewrites: async () => nextI18NextRewrites(localeSubpaths),
publicRuntimeConfig: {
localeSubpaths,
},
I believe I understand what the issue is. Multiple rewrites of one request does not work, it simple takes the first matching rewrite. In my case it was { source: '/:lang(en)/:path*', destination: '/:path*' }.
So it works if I add my rewrite above ...nextI18NextRewrites(localeSubpaths) while also manually adding the localeSubpath for it, e.g.
module.exports = {
rewrites: async () => {
return [
{
source: '/en/gardening/london',
destination: '/services/gardening/london',
},
...nextI18NextRewrites(localeSubpaths),
];
},
publicRuntimeConfig: {
localeSubpaths,
},
};
Update
It seems like webpack is causing the issues.
If I replace the dist/background.js with:
console.log("background is running"); // Now visible ✅
const handler = (req, sender, sendResponse) => {
switch (req.type) {
case "message":
sendResponse({ data: "hi" });
break;
default:
break;
}
};
chrome.runtime.onMessage.addListener(handler);
Both the console log (in service worker) and response (in popup) are observed. Also, there are no errors.
Time to investigate further
Update #2
Upon further inspection, I noticed that the webpack output is wrapped in a function, but never called:
{
/***/ "./src/background.ts":
/*!***************************!*\
!*** ./src/background.ts ***!
\***************************/
/***/ (function () {
console.log("background is running");
const handler = (req, sender, sendResponse) => {
switch (req.type) {
case "message":
sendResponse({ data: "hi" });
break;
default:
break;
}
};
chrome.runtime.onMessage.addListener(handler);
/***/
}) // <==== adding () will make it an IIFE and everything works!
},
Question is how to automate this?
Update #3
Seems like the IIFE trick I mentioned above only works when there are no imports in background.js. As soon as I add any import, I get an error that the background script is not valid.
Adding module type property to background does not help:
// manifest.json
{
...
"background": {
"service_worker": "background.js",
"type": "module"
},
...
}
Update #4
Turns out this was caused by vendor splitting optimization in webpack:
// webpack.config.json
{
...
optimization: {
runtimeChunk: "single",
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
enforce: true,
chunks: "all",
},
},
},
},
...
}
Once I removed this, everything started working properly!
Would be nice to keep this around, but it is just an optimization after all, so if it breaks things, best to get rid of it.
How I figured this out? Well as I mentioned, everything worked a couple of commits ago. Back then I didn't have this optimization, so I tried removing it again, and everything started working again like magic.
Original Question
I had this working previously, so I am sure my setup is correct, but regardless of what I try, I now get
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
Also, I cannot find a solution online which indicates anything that differs from my setup.
Here is a MWE
dist folder structure:
dist/background.js
dist/index.html
dist/manifest.json
dist/popup.js
dist/runtime.js
dist/vendors.js
other misc files
// manifest.json
{
...
"background": {
"service_worker": "background.js"
},
...
}
// src/components/App.tsx
export default function App(): JSX.Element {
...
useEffect(() => {
chrome.runtime.sendMessage({ type: 'someMessage' }, ({ data }) => {
console.log(data);
});
}, []);
...
}
// src/background.ts
import { TSentResponse } from "./typings/background";
import { executeResponse } from "./utils/background";
console.log('sanity check'); // <=== does not fire 🤔
// also doesn't seem to be called 😥
const handleMessage = (req: { type: string }, sender: chrome.runtime.MessageSender, res: (response?: unknown) => void) => {
switch (req.type) {
case 'someMessage':
// an IIFE (worked fine before)
break;
default:
break;
}
return true; // due to asynchronous nature
};
chrome.runtime.onMessage.addListener(handleMessage);
My service worker is registered properly:
When I said above that I cannot see the logs for background, I mean when I check in the service worker dev tools (from above image), not the popup dev tool.
When I open the popup, I get the following errors:
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
Error handling response: TypeError: Cannot destructure property 'data' of 'undefined' as it is undefined.
I also don't see the service worker actually being registered - it did register before...
Is this a bug with MV3?
My repository (not fully up to date, but can be used to quickly check the above)
Follow these steps:
To send a message from the popup.js to the background service worker, first you need to get current tab id. to get the current tab id do as below:
popup.js
const messageKey = 'key-message-from-popup-to-background';
// Listen to get current tab info from the content_popup
document.addEventListener('_listener_getCurrentTabInfo', function (e) {
const tab = e.detail.response;
// Send a message to the background,js
chrome.tabs.sendMessage(tab.id, messageKey);
});
// Send message to content_popup to get current tab info
document.dispatchEvent(new CustomEvent('_dispatch_getCurrentTabInfo'));
content_popup.js
const config = {
scripts: [
// add ".js" files to web_accessible_resources in manifest.json
"popup/popup.js"
]
};
// Listen to get current tab info from the background.js
document.addEventListener('_dispatch_getCurrentTabInfo', function (e) {
// Key to help
const type = 'get_current_tab_info';
// Send a message to the background.js to get current tab info
chrome.runtime.sendMessage({type: type}, response => {
document.dispatchEvent(new CustomEvent('_listener_getCurrentTabInfo', {detail: {'response': response}}));
});
});
// prepare and add scripts
var scriptList = config['scripts'];
for (var i = 0; i < scriptList.length; i++) {
var s = document.createElement('script');
s.src = chrome.runtime.getURL(scriptList[i]);
s.onload = function () {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
}
background.js
// Get messages
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
// If request related to fetch data from API
if (request.url && request.url !== '') {
// Fetch http request and send back the response
fetch(request.url, request.init).then(function (response) {
return response.text().then(function (text) {
sendResponse([{
body: text,
status: response.status,
statusText: response.statusText,
}, null]);
});
}, function (error) {
sendResponse([null, error]);
});
// If request do not related to fetch data from API
} else {
detectMessageType(request, sender, sendResponse);
}
return true;
});
function detectMessageType(request, sender, sendResponse) {
// Check background request type
if (request && request.type) {
switch (request.type) {
case 'get_current_tab_info': {
getCurrentTabInfo(tab => {
// Send current tab info back to content_popup
sendResponse(tab);
});
break;
}
}
}
});
function getCurrentTabInfo(callback) {
chrome.tabs.query({
active: true,
currentWindow: true
}, function (tab) {
callback(tab[0]);
});
}
Get message in contentScript
chrome.runtime.onMessage.addListener((message, sender, response) => {
const message = message;
// Write your codes
});
This is the project structure (partial)
Project
-- popup
---- popup.js
---- content_popup.js
---- popup.html
---- popup.css
-- content_extension.js
-- background.js
-- manifest.js
Add the following code to the end of the body tag inside the popup.html file
<script src="content_popup.js"></script>
Update the manifest.json file like below
"manifest_version": 3,
.
.
.
"content_scripts": [
{
"js": [
"content_extension.js"
]
}
],
"action": {
"default_icon": ...,
"default_title": ...,
"default_popup": "popup/popup.html"
},
"background": {
"service_worker": "background.js"
},
"web_accessible_resources": [
{
"resources": [
"popup/popup.html",
"popup/popup.js"
]
}
],
...
I tried to define redirects in my NextJS app.
but it is not working.
This is how I tried to do it in my next.config.js file:
const withImages = require('next-images')
const withPlugins = require("next-compose-plugins");
const optimizedImages = require("next-optimized-images");
module.exports = withPlugins(
[
[optimizedImages, {
inlineImageLimit: 512
}]
],
{
async redirects() {
return [
{
source: "/sales/guest/form",
destination: "/",
permanent: true
}
]
},
env:{
testEnvVar: 'vallll'
}
}
);
This is the documentation of how to do it:
https://nextjs.org/docs/api-reference/next.config.js/redirects
For redirects and rewrites to work properly in NextJs, you also need to ensure one more thing:
If you are using trailingSlash: true then your source paths must end with a slash.
{
source: '/old/:id/', // Notice the slash at the end
destination: '/new/:id',
},
Any other plugins or configurations that interfere with routing also need to be taken into account.
you can add all you imports and also const definitions to first array parameter like this
const withPlugins = require('next-compose-plugins');
const css = require('#zeit/next-css');
const less = require('#zeit/next-less');
const nextConfig = {
target: 'serverless',
webpack(config, { isServer, webpack }) {
// al your config
return config;
},
};
const redirects = {
async redirects() {
return [
{
source: '/old/blogs/:slug*',
destination: 'whatever your new rewrite url',
permanent: true,
},
];
},
};
module.exports = withPlugins(
[
[css],
[less],
[redirects], // you can directly drop your redirect rules here
],
nextConfig
);
What NextJS Version are you on? Redirects are supported from 9.5 upwards
For anyone who has this problem, try restarting the server. The config file will be reloaded then.
In my case, I tried to redirect to external link. I had trailingSlash: true and I ended my source path with slash.
It didn't work because I use Link component from next/link
I changed it to normal a tag and it worked.
Before:
<Link href="/some-path" passHref>
<a>
to external
</a>
</Link>
After:
{/* eslint-disable-next-line #next/next/no-html-link-for-pages */}
<a href="/some-path">
to external
</a>
You need to disable eslint rule #next/next/no-html-link-for-pages so it won't raise error while building
in next.config.js file:
module.exports = {
trailingSlash: true,
reactStrictMode: true,
async redirects() {
return [
{
source: "/some-path",
destination: "https://example.com",
permanent: true,
},
]
},
}