shadow-cljs icon indicating copy to clipboard operation
shadow-cljs copied to clipboard

Support for Manifest V3 in Browser Extension Development

Open groundedsage opened this issue 4 years ago • 5 comments

Although not immediately pressing. I would like to discuss what is required to update shadow-cljs so that it supports V3 of the extension manifest https://developer.chrome.com/docs/extensions/mv3/intro/

V2 will eventually be phased out and V3 made mandatory so starting to work on support for building V3 extensions will be helpful for extension developers opting to use ClojureScript.

groundedsage avatar Jun 15 '21 17:06 groundedsage

Can you be more precice in what shadow needs to do here? Can't you just set "manifest_version": 3 in the JSON file? shadow really doesn't care whats in your manifest.

Someone actually working on a Chrome Extension should probably take this on.

thheller avatar Jun 15 '21 19:06 thheller

One big change in manifest v3 is user can't use unsafe-eval in development build and remove it in production build any more. (user can still use unsafe-eval csp in sandboxed pages)

https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/#content-security-policy

So is it possible to still has a working repl and don't use eval in development build?

yqrashawn avatar Jan 26 '22 07:01 yqrashawn

Well, it is read-eval-print-loop. So no, you can't have a REPL without eval.

thheller avatar Jan 26 '22 07:01 thheller

That's sad. The quickest solution might be build a chromium without the extension csp check and use it in dev env.

The related code might be in this file https://github.com/chromium/chromium/blob/main/extensions/common/csp_validator.cc

yqrashawn avatar Jan 26 '22 08:01 yqrashawn

I am about to start dev'ing a chrome extension and thought I'd try figuring this out.

I started with this tutorial extension: https://developer.chrome.com/docs/extensions/mv3/getstarted and attempted to load it with this addition to the manifest.json file:

"content_security_policy": {
  "extension_pages": "default-src 'self'; script-src 'self' 'unsafe-eval' http://localhost:9630; connect-src * data: blob: filesystem:; style-src 'self' data: chrome-extension-resource: 'unsafe-inline'; img-src 'self' data: chrome-extension-resource:; frame-src 'self' data: chrome-extension-resource:; font-src 'self' data: chrome-extension-resource:; media-src * data: blob: filesystem:;"
}

I also needed to add the directive: "host_permissions": ["<all_urls>"], (separate from our issue of desiring to evaluate code - yep, their own tutorial is broken).

And indeed there is a validation error and the extension cannot be loaded.

Shout out to @yqrashawn for the idea to augment the source (and a starting point to look at).

I managed to build chromium fairly easily (took about an hour to figure out and 40 mins to compile) having never done this before.

Then another hour or two tinkering and re-building - but it's working now! The extension loads successfully.

The source changes I made in csp_validator.cc were to add this on line 47:

const char kExtensionUnsafeEvalSource[] = "'unsafe-eval'";

and make use of it in the implementation of DoesCSPDisallowRemoteCode the block starting on line 706 becomes:

auto directive_values = mapping.directive->directive_values;
auto it = std::find_if_not(
    directive_values.begin(), directive_values.end(),
    [](base::StringPiece source) {
      std::string source_lower = base::ToLowerASCII(source);

      return source_lower == kSelfSource || source_lower == kNoneSource ||
             IsLocalHostSource(source_lower) ||
             source_lower == kExtensionUnsafeEvalSource || // <-- added
             source_lower == kWasmUnsafeEvalSource;
    });

I haven't actually attempted loading any code though - the chrome extension written in cljs I'm trying this with outputs background scripts which are not supported in v3 (uses service workers instead). I'm hoping there won't be more checks in the chromium codebase but, at least the first hurdle is passed.

dvingo avatar May 11 '22 19:05 dvingo

I created an example v3 extension. See https://github.com/thheller/shadow-cljs/issues/1051

I made no attempt at eval but :target :chrome-extension should be considered obsolete by now and :target :esm seems to be the way forward.

thheller avatar Oct 03 '22 06:10 thheller

@dvingo @yqrashawn Hi! I know this is a closed issue, but I wanted to say thanks for the inspiration and starting point within the Chromium source. I'm porting an MV2 Clojurescript extension to MV3 at this moment and as of yesterday I have hot reloading working within a modified Chromium build.

If anybody else is building an MV3 extension with Shadow CLJS, hopefully this might be helpful. https://gist.github.com/blake-ctrl/778db8715556d1bc1af00338a8d755b9

blake-ctrl avatar Sep 19 '23 19:09 blake-ctrl

@dvingo @yqrashawn Hi! I know this is a closed issue, but I wanted to say thanks for the inspiration and starting point within the Chromium source. I'm porting an MV2 Clojurescript extension to MV3 at this moment and as of yesterday I have hot reloading working within a modified Chromium build.

If anybody else is building an MV3 extension with Shadow CLJS, hopefully this might be helpful. https://gist.github.com/blake-ctrl/778db8715556d1bc1af00338a8d755b9

Thanks! I am in the same boat: migrating a mv2 extension to mv3. However, I can't seem to get the REPL to work on the patched Chromium build.

I am trying to run thheller's mv3 example: https://github.com/thheller/chrome-ext-v3

After I changed the ":runtime" to "browser" i could see a bunch of websocket errors that indicate outputted JS is unable to connect to shadow-cljs

I am curious what changes you had to make to the shadow-cljs config to make it work.

ketansrivastav avatar Oct 08 '23 02:10 ketansrivastav

My shadow-cljs.edn looks something like this:

...
 :dev-http {8080 {:root "public"
                  :host "127.0.0.1"}}
 :builds
 {:extension
  {:target :esm
   :compiler-options {:source-map true}
   :output-dir "extension/js"
   :runtime :browser ;:custom

   :devtools {:preloads [devtools.preload]
              :use-document-host false}
   :modules {:shared {:entries []}
             :serviceworker
             {:init-fn xyz.serviceworker/init
              :depends-on #{:shared}}
             :popup
             {:init-fn xyz.popup/init
              :depends-on #{:shared}}
...

I don't know if it makes a difference but my manifest explicitly sets the CSP policy:

  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self';"
  },

It doesn't include unsafe-eval, but the patch to Chromium should make it always act as if unsafe-eval is allowed.

For me, before patching, the websockets connected without issue but the js-eval from a reload or repl evaluation would throw an error visible from the console in Chromium Devtools.

blake-ctrl avatar Oct 08 '23 03:10 blake-ctrl

thank you @blake-ctrl

Posting this comment for posterity for anyone else who might be facing similar issues:

in shadow-cljs.edn This fixed the WebSockets issues:

   :devtools {
              :use-document-host false}

in manifest.json This fixed the eval issue:

  "content_security_policy": {
      "extension_pages": "script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval';"

  }

So after applying your patch and building Chromium i had to make the 2 mentioned changes in my extension and voila!

Thanks you for your reply and for the Chromium fix :slightly_smiling_face:

ketansrivastav avatar Oct 29 '23 03:10 ketansrivastav