Open svicalifornia opened 1 year ago
I encountered this issue also and used a workaround.
In manifest.json add the scripting
permission and add either the activeTab
permission or the allowed host with the host_permissions
key.
In background.ts register the content script using chrome.scripting.registerContentScripts
chrome.scripting.registerContentScripts([
{
id: 'XMLOverride',
js: ['src/content/XMLOverride.js'],
matches: ['https://*.example.com/*'],
runAt: 'document_start',
world: 'MAIN',
},
]);
Having the same issue here as well.
Using the workaround @adam-s provided. Works okay, but it doesn't hot-reload the file changes and I have to build + reload the extension to see changes, which is not ideal. Would love to see this fixed. π
Just putting it here in case it helps any of you, I found this post written by @jacksteamdev where talks about MAIN world script. But, he mentions injecting script through the ISOLATED content script into the DOM. Which feels like a roundabout way to go about it. Having the dreict Manifest way would be amazing. But at least with that approach, I now get all the dev-tool magic working.
@faahim Thanks for posting that link. As you said, it is a roundabout way to go about it. Also, that post also seems to assume that all content scripts are in the isolated world, ignoring the option to set "world": "MAIN"
on a content script in the manifest. The post says, "Unfortunately, the loader relies on the Chrome API only available to content scripts," but really, the loader relies on Chrome API only available in isolated content scripts.
All that said, I realized soon after I posted this GitHub issue that all content scripts in the manifest of a CRXJS project automatically get wrapped in the Vite loader code, and since that code relies on Chrome Extension API that is not available in main world, those main-world content scripts simply won't work from a CRXJS project manifest.
Perhaps @jacksteamdev could add an option for us to specify which content scripts in the manifest should not be wrapped with Vite loader code. However:
tsc
, Babel, or some other transpiler.Even if there was a way to make Vite/HMR loader code work in the main world, there's another problem: using the Vite loader introduces a delay to code execution, which prevents those content scripts from running before other scripts on the page. This means that those content scripts will not be effective in overriding built-in functions used by other scripts on the page, and therefore a lot of potential use cases of Chrome extensions are lost by that Vite delay.
In the end, I switched to using Webpack and reloading my extension manually. It's a bummer to lose Vite and HMR, but at least I could be sure that my extension scripts will load first and be able to do everything in the expected order.
@jacksteamdev I'm interested in discussing this further and trying to find solutions to the above concerns if you have time.
Even if there was a way to make Vite/HMR loader code work in the main world, there's another problem: using the Vite loader introduces a delay to code execution, which prevents those content scripts from running before other scripts on the page. This means that those content scripts will not be effective in overriding built-in functions used by other scripts on the page, and therefore a lot of potential use cases of Chrome extensions are lost by that Vite delay.
Correct, @svicalifornia!
This is exactly the issue I'm facing with the approach described in the post. The project I'm doing involves wrapping the built-in fetch()
which NEEDS to happen before the page script runs and I simply can't guarantee that with CRXJS.
I love all the other aspects of CRXJS so much that it feels painful to not be able to work with it.
I hope @jacksteamdev will take a minute to shed some more light on this. π
I found a method of dynamically loading scripts that will execute extension code before the rest of the page is loaded. This should suffice for intercepting fetch API calls, websocket connections and the like.
document_start
(see below)/* manifest.json */
{
/*...*/
"content_scripts": [
/*...*/
{
"matches": ["myfilter.url.example.com"],
"js": ["path/to/loader_script.js"],
"run_at": "document_start"
}
]
@svicalifornia unfortunately this won't solve the issues with vite or HMR. My gut feeling is that running across multiple window scopes may not be something that either module can handle, though I haven't investigated thoroughly.
My approach to this so far has been to keep the world scripts as lightweight as possible and emitting events on the document
object from the world script and listening for them in the extension (note that the document is shared, the window is not).
Hi @gf-skorpach π
Thanks for these pointers. While this works, it still isn't ideal. I could be totally wrong here, but the way I understand it, when you add a loader script via the manifest with document_start
directive, it ensures the loader script will be executed at the document start, not the actual script your loader is going to inject. Now, I've tried this and it works, but I just don't feel very confident shipping it on production cause it's not guaranteed.
I think if we're just looking for inserting script in the MAIN world AND guarantee that it runs before the page script, using registerContentScripts
through the background script is a much more straightforward and cleaner way. But with the downside of no Vite/HMR.
Ideally, we want to have both, i.e. inserting MAIN world script directly through the manifest and having Vite/HMR for the script. π
In the end, I would prefer having MAIN world injection with static declaration (manifest.json) and just give up on having Vite/HMR functionality.
In my personal fork I ended up doing this: if content_scripts entry has "world": "MAIN"
, load it as module type (without HMR functionality), without loader, otherwise by default act like it needs loader and "world": "ISOLATED"
.
This solution is probably not desirable for @crxjs/vite-plugin...
@adam-s thank you for workaround. Is it possible to still have the script processed by Vite even thought without hot reloading?
@flexchar
I tried creating a custom plugin as a hack to watch for changes in vite.config.js so that the content script could be compiled from ts to js and moved to the .dist folder. However, I ran into a problem where the plugin runs and compiles after crx runs. The content script injected into the main world needs to be declared in manifest.json web_accessable_resources. Because the file isn't available when crx runs, crx throws an error not knowing the file will be made available later in the compile process. Perhaps have a placeholder file and put the content scripts which you want to compiled and moved into a different folder.
import { defineConfig } from 'vite';
import { crx } from '@crxjs/vite-plugin';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import manifest from './manifest.json';
import { rollup, watch } from 'rollup';
import typescript from '@rollup/plugin-typescript';
const customPlugin = {
name: 'custom-plugin',
buildStart() {
const watcher = watch({
input: './src/content/example.ts',
plugins: [
typescript({ allowImportingTsExtensions: true, noEmit: false }),
],
output: {
file: './dist/src/content/example.js',
format: 'es',
sourcemap: false,
},
});
},
};
export default defineConfig({
//@ts-ignore
plugins: [
customPlugin,
svelte({
onwarn: (warning, handler) => {
const { code, frame } = warning;
if (code === 'css-unused-selector') return;
},
emitCss: false,
}),
crx({ manifest }),
],
server: {
port: 5173,
strictPort: true,
hmr: {
port: 5173,
},
},
build: {
rollupOptions: {
output: {
sourcemap: 'inline',
},
},
},
});
Hi, first of all I want to thank you for this project.
Couldn't we instruct vite for content-scripts with "world main" to bundle all dependencies in one single file without any import/exports? This way, everything else that is needed, such as auto-reload, could also work.
Here is how I resolved it, I let the content.js remain in ISOLATED world , and then inject my other script in the MAIN world using the manifest.json
{
"manifest_version": 3,
"name": "ExampleBot",
"version" : "1.0",
"description": "Automatically place bids once they are available",
"permissions": ["activeTab", "tabs", "storage", "scripting"],
"background":{
"service_worker":"./background.js"
},
"content_scripts":[
{
"content_security_policy": "script-src 'self' https://localhost:* 'nonce-randomNonceValue'; object-src 'self'",
"matches":["https://place_your_url_here.com/*/*"],
"js":["./content.js","./jquery.min.js"],
"run_at": "document_idle",
"world": "ISOLATED"
},
{
"content_security_policy": "script-src 'self' https://localhost:* 'nonce-randomNonceValue'; object-src 'self'",
"matches":["https://e*.com/*/*/*"],
"js":["./extractGlobals.js"],
"run_at": "document_idle",
"world": "MAIN"
}
],
"host_permissions": [
"https://place_your_url_here_of_site_to_inject.com/product/orders/*"
]
}
The code within ./extractGlobals.js will be executed at document_idle will be available in ISOLATED world from MAIN world where the content script is running at.
If you want to extract the background.js or service to grab globals from MAIN land where the content js cannot run, you can use background.js to send a request to inject the file using a Promise like this and you receive the globals in main page, remember you have to specify the globals that are to be returned.
// Listen for messages from the content script
//background.js
// Listen for messages from the content script
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.action === "extractGlobals") {
// Execute the script in the main world
chrome.scripting.executeScript({
target: { tabId: sender.tab.id },
files: ["extractGlobals.js"],
world: "MAIN"
}).then(result => {
// Script execution successful, extract globals from the result
const globals = result[0];
sendResponse({ success: true, globals: globals });
}).catch(error => {
// Error executing the script
console.error("Error injecting script:", error.message);
sendResponse({ success: false, error: error.message });
});
// Return true to indicate that sendResponse will be called asynchronously
return true;
}
});
Bumping this. I would love for a way for the loader script to be able to inject in main without having to do a workaround. Potentially run all the loader script in ISOLATED but detect and inject the script into MAIN when appropriate.
I have the similar issue.so I try to remove the βworldβ:βMAINβ to fix it and it works.
Build tool
Vite
Where do you see the problem?
Describe the bug
When my manifest (V3) has a content script with
"world": "MAIN"
, the following error appears in the Chrome console:In the Sources tab, I see that my_content_script.js-loader.js contains:
CRXJS (or Vite?) is trying to call
chrome.runtime.getURL
to get the JS paths to import. However,chrome.runtime
is not defined in the MAIN world.These imports should be rewritten to first check for the existence of
chrome.runtime
β if it doesn't exist, then some sort of shim should be defined forchrome.runtime.getURL
to get the desired paths.As one way to do this, CRXJS could:
document.documentElement
)CRXJS_getRootURL
event to from MAIN to the isolated worldchrome.runtime.getURL('')
and sending it back to MAINchrome.runtime.getURL
, then continuing with the imports as shown in the code block above.Reproduction
mainfest.json:
Logs
System Info
Severity
blocking an upgrade