Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Support for Offscreen Documents #527

Open
3 tasks done
jkkrow opened this issue Mar 31, 2023 · 16 comments
Open
3 tasks done

[RFC] Support for Offscreen Documents #527

jkkrow opened this issue Mar 31, 2023 · 16 comments
Assignees
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@jkkrow
Copy link

jkkrow commented Mar 31, 2023

How do you envision this feature/change to look/work like?

The Offscreen API has been added since Chrome 109 version. This solves a problem with the service worker which can't access the DOM or the window interface. It would be great if this feature is natively supported in Plasmo.

As quoted in this link, Manifest V3 extensions are service worker-based, but service workers don't provide support for the same APIs and mechanisms that full document-based pages (which include background and event pages) do. Additionally, using content scripts to access DOM APIs on web pages leaves the extension at the mercy of different content security policies on a page-to-page basis.

Offscreen Documents supports DOM-related features and APIs by allowing Manifest V3 extensions to open minimal, scoped, and relatively un-permissioned offscreen documents at runtime through a dedicated API.

What is the purpose of this change/feature? Why?

The Offscreen API requires a dedicated html page just like other extension pages, so currently it seems impossible to implement this within the Plasmo environment using React and Typescript which automatically bundles the tsx files into html and js.

(OPTIONAL) Example implementations

Like other extension pages or background sw, automatically building an offscreen environment by creating a file named offscreen.ts in a root directory or src folder looks like an ideal implementation.

(OPTIONAL) Contact Details

No response

Verify canary release

  • I verified that the issue exists in plasmo canary release

Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I checked the current issues for duplicate problems.
@jkkrow jkkrow added the enhancement New feature or request label Mar 31, 2023
@dimadolgopolovn
Copy link

Is there a workaround to implement this in Plasmo now?

@louisgv
Copy link
Contributor

louisgv commented May 12, 2023

@dimadolgopolovn yeah you can just use it as-is. All low-level chrome API are available at your disposal.

@dimadolgopolovn
Copy link

dimadolgopolovn commented May 13, 2023

Adding the solution here so that people don't waste time on it.

Add offscreen.html and offscreen.js to project root as per Chrome documentation.

offscreen.html:

<!DOCTYPE html>
  <script src="offscreen.js"></script>

background.js:

import OFFSCREEN_DOCUMENT_PATH from 'url:~offscreen.html'

async function createOffscreenDocument() {
  if (!(await hasDocument())) {

    await chrome.offscreen.createDocument({
      url: OFFSCREEN_DOCUMENT_PATH,
      reasons: [chrome.offscreen.Reason.WEB_RTC],
      justification: 'P2P data transfer'
    }).then(e => {
      // Now that we have an offscreen document, we can dispatch the
      // message.
    })
  }
}
createOffscreenDocument()

async function hasDocument() {
  // Check all windows controlled by the service worker if one of them is the offscreen document
  // @ts-ignore clients
  const matchedClients = await clients.matchAll()
  for (const client of matchedClients) {
    if (client.url.endsWith(OFFSCREEN_DOCUMENT_PATH)) {
      return true
    }
  }
  return false
}

@jkkrow
Copy link
Author

jkkrow commented May 14, 2023

@dimadolgopolovn Thank you for the solution! I struggled to connect the html file to the document and that url import did the trick!

import OFFSCREEN_DOCUMENT_PATH from 'url:~offscreen.html';

It seems I can also use typescript file as a script.

<script src="offscreen.ts" type="module"></script>

@anooppoommen
Copy link

anooppoommen commented Jun 12, 2023

@jkkrow @dimadolgopolovn Plasmo doesn't seem to pick up the files when compiling how did you add this to the project.

@dimadolgopolovn
Copy link

@anooppoommen, same as with adding a background.ts. You put it in root and you just need to add “export {}” as the first line.
Plasmo picks up any ts file like this. (that’s what the “~” is there for) There’s official documentation on this.

@anooppoommen
Copy link

@dimadolgopolovn I tried creating an offscreen.ts file in the root directory and an offscreen.html both these weren't included in the build should I modify something in the package.json too?

@jkkrow
Copy link
Author

jkkrow commented Jun 12, 2023

@anooppoommen

You should include "offscreen" to permissions in the package.json.

{
  "permissions": [
    "offscreen",
  ]
}

@anooppoommen
Copy link

@jkkrow added the permission too but the file doesn't appear in the final build did you add it to the tsconfig or anywhere to let Plasmo pick it up

@jkkrow
Copy link
Author

jkkrow commented Jun 12, 2023

@anooppoommen

I didn't set any configuration regarding this. I only put offscreen.ts and offscreen.html in the src directory.

This is my tsconfig.json:

{
  "extends": "plasmo/templates/tsconfig.base",
  "exclude": [
    "node_modules"
  ],
  "include": [
    ".plasmo/index.d.ts",
    "./**/*.ts",
    "./**/*.tsx",
  ],
  "compilerOptions": {
    "paths": {
      "~*": [
        "./src/*"
      ]
    },
    "baseUrl": ".",
    "strictNullChecks": true,
    "ignoreDeprecations": "5.0"
  },
}

This is where I use offscreen in message handler:

import type { PlasmoMessaging } from '@plasmohq/messaging';

import OFFSCREEN_DOCUMENT_PATH from 'url:~src/offscreen.html';

const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
  if (!(await chrome.offscreen.hasDocument())) {
    await chrome.offscreen.createDocument({
      url: OFFSCREEN_DOCUMENT_PATH,
      reasons: [chrome.offscreen.Reason.BLOBS],
      justification: 'Testing',
    });
  }

  res.send('');
};

export default handler;

@anooppoommen
Copy link

anooppoommen commented Jun 12, 2023

@jkkrow thanks for the example it's really helpful. the issue for me seems to be the tsconfig in my project.

@anooppoommen
Copy link

anooppoommen commented Jun 12, 2023

adding the sample offscreen.html here for others looking for adding this to their project

<!DOCTYPE html>
<html>
  <head>
    <title>Offscreen</title>
  </head>
  <body>
    <script src="offscreen.ts" type="module"></script>
  </body>
</html>

@louisgv louisgv pinned this issue Jun 13, 2023
@louisgv louisgv added the help wanted Extra attention is needed label Jun 13, 2023
@louisgv louisgv self-assigned this Jun 13, 2023
@louisgv
Copy link
Contributor

louisgv commented Jun 29, 2023

What should we abstract out for this API? Would love for folks who've played around with this API to chime in. Should we design it like this:

// offscreen.ts

export const config: OffscreenConfig = {} // typed config that get passed down to `chrome.offscreen.createDocument`

// do your offscreen bidding

The above then get compiled into a chrome.offscreen.createDocument call in the default bgsw? The problem is that for folks who want to do dynamic offscreen spawning like #527 (comment), this abstraction is pretty useless :d....

To abstract out the html, you can use the tab-page feature: https://docs.plasmo.com/framework/tab-pages

Make a file in tabs/offscreen.ts -> this generates a tabs/offscreen.html. Then mount it like this:

    await chrome.offscreen.createDocument({
      url: chrome.runtime.getURL("tabs/offscreen.html"),
      reasons: [chrome.offscreen.Reason.BLOBS],
      justification: 'Testing',
    });

@louisgv louisgv unpinned this issue Jun 29, 2023
@jkkrow
Copy link
Author

jkkrow commented Jun 30, 2023

I think creating the offscreen document manually is better for more versatile usage. Let's say if someone needs offscreen documents for different reasons. If the config is bound to offscreen.ts file and run it as a default, we would only have to use offscreen with single reasons option.

In most cases it won't matter, but this option decides different lifespan of offscreen. According to this,

AUDIO_PLAYBACK: Closed after 30 seconds without audio playing.
All other reasons: the lifetime is unbounded (the page can remain open forever).

So I think it would be better to let developers create the offscreen manually, but with abstracted function (Such as checking the existing document before create one).

For example,

import { createOffscreen } from '@plasmohq/offscreen'; 

// This function should check for existing document behind the scene 
await createOffscreen({
  reasons: [chrome.offscreen.Reason.BLOBS],
  justification: 'Testing'
});

Or maybe abstract this further so that document is created only when user request an operation to it.

For example,

import { sendToOffscreen } from '@plasmohq/offscreen';

// Automatically create document behind the scene if not exist
await sendToOffscreen(
  // Request body
  {
    message: 'Test Message',
  },
  // Config
  {
    reasons: [chrome.offscreen.Reason.BLOBS],
    justification: 'Testing',
  }
);

@AaronMBMorse
Copy link

AaronMBMorse commented Aug 13, 2023

I added the three files below, and offscreen permission to my manifest/package.json and it worked.

I am creating this post to compile all the code for a fully working example in one place. The code is primarily from @dimadolgopolovn and @anooppoommen. Thank you guys!

All of these files are should be placed at the root of your project. In my case that is in the src/ directory.

Note: You can move the background.ts file into a background folder if your prefer. In which case it would look like this background/index.ts.

Chrome Developer Docs

packages.json

{
    "permissions": [
      "offscreen"
    ],
}

background.ts
Note: chrome.offscreen.Reason.WEB_RTC has a few options

import OFFSCREEN_DOCUMENT_PATH from "url:~src/offscreen.html"

async function createOffscreenDocument() {
  if (!(await hasDocument())) {
    await chrome.offscreen
      .createDocument({
        url: OFFSCREEN_DOCUMENT_PATH,
        reasons: [chrome.offscreen.Reason.WEB_RTC],
        justification: "P2P data transfer"
      })
      .then((e) => {
        // Now that we have an offscreen document, we can dispatch the
        // message.
      })
  }
}
createOffscreenDocument()

async function hasDocument() {
  // Check all windows controlled by the service worker if one of them is the offscreen document
  // @ts-ignore clients
  const matchedClients = await clients.matchAll()
  for (const client of matchedClients) {
    if (client.url.endsWith(OFFSCREEN_DOCUMENT_PATH)) {
      return true
    }
  }
  return false
}

offscreen.ts
console.log("Offscreen script loaded")

offscreen.html

<!DOCTYPE html>
<html>

<head>
  <title>Offscreen</title>
</head>

<body>
  <script src="offscreen.ts" type="module"></script>
</body>

</html>

@amitrahav-darrow
Copy link

On chromium 128.0.6*** this method raises a Content Security Policy error.
And from the v3 of manifest there is no option to include outside scripts, also from localhost.

Any thoughts about how to implement it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

6 participants