:wave: I'm in a context of a browser extension, a...
# javascript
b
πŸ‘‹ I'm in a context of a browser extension, and wondering how to manage this situation. I need to use a symbol (
chrome.offscreen.createDocument
) that exists only in Chrome and not in Firefox. I'm enclosing the piece of code with an
if
that knows how to detect that this symbol doesn't exist - but it's still referenced in a "imports" section of the generated JS code:
Copy code
}(function (_, kotlin_kotlin, kotlin_bbt_shared, kotlin_org_jetbrains_kotlinx_kotlinx_coroutines_core) {
  'use strict';
  //region block: imports
(... lots of other imports)
  var createDocument = chrome.offscreen.createDocument;  <- crashes with "Uncaught TypeError: chrome.offscreen is undefined"
(... lots of other imports)
Wondering if there's a proper way to deal with this? Or do I just need to use
js("chrome.offscreen.createDocument")
to avoid all this (but feels dirty)?
e
Lazy imports are not a thing yet, so yes, you need an untyped
js
call.
🚫 1
b
πŸ‘ gotcha - too bad :)
e
BTW, are you using the ES2015 target?
You might be able to solve this with ES2015 + per file.
b
ah no I don't! I can try πŸ™‚
e
Try and let me know, but it should do it
b
hmm it seems to be the same
e
Could you post the snippet that errors out?
b
kt:
Copy code
private suspend fun ensureOffscreenDocumentCreated() {
    val offscreenRelativePath = "offscreen.html"
    val fullyQualifiedPath = chrome.runtime.getURL(offscreenRelativePath)
    val existingContexts = chrome.runtime.getContexts(
      ContextFilter(
        contextTypes = arrayOf("OFFSCREEN_DOCUMENT"),
        documentUrls = arrayOf(fullyQualifiedPath),
      ),
    ).await()
    if (existingContexts.isNotEmpty()) {
      return
    }
    val createParameters = CreateParameters(
      justification = "Parse DOM",
      reasons = arrayOf("DOM_PARSER"),
      url = offscreenRelativePath,
    )
    val createDocumentPromise = chrome.offscreen.createDocument(createParameters)
//    val createDocumentPromise = js("chrome.offscreen.createDocument(createParameters)")
    createDocumentPromise.await()
  }
js:
Copy code
//region block: imports
var getURL = chrome.runtime.getURL;
var getContexts = chrome.runtime.getContexts;
var createDocument = chrome.offscreen.createDocument;
var onAlarm = chrome.alarms.onAlarm;
var onMessage = chrome.runtime.onMessage;
var setBadgeText = chrome.action.setBadgeText;
var setBadgeBackgroundColor = chrome.action.setBadgeBackgroundColor;
var get = chrome.alarms.get;
var create = chrome.alarms.create;
var clearAll = chrome.alarms.clearAll;
//endregion
//region block: pre-declaration
class BookmarkExtractor {
(...)
e
I would have to try it out myself to be honest. I don't know why Kotlin reassigns the imported objects like that.
b
Well that's all right - it works with the
js()
version. Thanks a lot for the help!
e
It's a pity because refactoring's gonna become a mess with string literals.
b
yeah far from ideal 😞 So far my use of
js
is limited to here so I guess it's manageable for now. Trying to make something work for both Chrome and Firefox is a horrible experience by the way (not Kotlin's fault at all)
e
Yup I know, that's why up until now I've only supported Chrome in case of extensions. The market share for Firefox is still too low to justify the price. Plus browsers like Brave are transitively supported.
b
yeah 😞 It's sad because I am trying to move to Firefox as my day to day browser (didn't like Chrome's handling of manifest v3 / uBlock Origin)... But if I switch, I want at least my little extension to work there πŸ˜…
e
Go with Brave. I've been using it since end of 2024 and it's been a straightforward switch from Chrome.
b
I've heard good things about it... I'll give it a try at some point.
e
Out of curiosity, could you share a subset of your Chrome externals so I can test this myself?
b
I can share the whole thing πŸ™‚ One sec
gratitude thank you 1
Copy code
git clone <https://github.com/BoD/bbt.git>
cd bbt
git checkout firefox-compat
./gradlew devDist
Result is in
build/devDist
. (in case you don't know or forgot) to load it in Firefox, go to
about:debugging#/runtime/this-firefox
and "Load Temporary Add-on", then pick
build/devDist/manifest.json
.
e
Thanks!
b
well thank YOU πŸ™‚
blob no problem 1
e
I'm not going to load the extension, I just want to inspect how Kotlin outputs JS code
πŸ‘ 1
t
Do you use any bundler?
b
hmm I use the default so WebPack? Not doing anything special in this area I think.
e
You can potentially make it work by removing
@JsQualifier
, and instead use
@JsName
, e.g.
Copy code
@JsName("chrome.runtime.getURL")
external fun getURL(path: String): String
πŸ‘€ 1
image.png
b
interesting! Let me try that
Yes! That worked βœ… πŸŽ‰
e
Just be aware of https://youtrack.jetbrains.com/issue/KT-31799 I'm not sure in which case the
JsName
is going to be escaped.
πŸ‘ 1
But nevertheless, related to KT-31799, I think an on/off switch in the form of annotation parameter would be nice to have, e.g.
Copy code
@JsName("chrome.runtime.getURL", escape = false)
Just an idea @Artem Kobzar
😞 1
Thanks for sharing the project, learned something new myself today.
b
thanks a lot for investigating this! and.... it looks like my extension now works ok on Firefox! I need to do a few more tests but it's looking good
t
> hmm I use the default so WebPack? You can use Webpack
DefinePlugin
and create separate strict builds, without redundant workarounds
b
I'm not familiar with it - would that be a way to have 2 distinct source sets, one for Chrome, one for FF?
t
@Edoardo Luppi already requested typings for Chrome extensions. And single option which I see for common solution - 2 separate bundler configurations (1 for Firefox, 1 for Chrome).
πŸ‘ 1
e
I personally don't like fiddling with Webpack when working with Kotlin, but if there is a better solution to
JsName
, why not. Had we have the capability of creating new source sets on top of the
js
one, I would have gone the route of expect-actual.
Copy code
_____ js _____
 |              |  
 |              |
jsChrome    jsFirefox
b
oh wow yes that would be very nice
there's already a long standing similar issue about jsBrowser / jsNode IIRC
e
2 separate bundler configurations
I'm trying to imagine how this would be set up, but I can't really see a straightforward path. Maybe I'm missing how the Kotlin sources would be organized
t
image.png
πŸ˜‚ 4
e
Well, luckily we've found a better alternative, for now at least
πŸ’― 1