Skip to content

Commit acb7554

Browse files
committed
docs: Add docs about handling updates
Related to #620
1 parent 25b45b9 commit acb7554

File tree

3 files changed

+118
-47
lines changed

3 files changed

+118
-47
lines changed

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default defineConfig({
9595
{ text: 'Vite', link: '/guide/vite.md' },
9696
{ text: 'Remote Code', link: '/guide/remote-code.md' },
9797
{ text: 'Publishing', link: '/guide/publishing.md' },
98+
{ text: 'Handling Updates', link: '/guide/handling-updates.md' },
9899
{ text: 'Development', link: '/guide/development.md' },
99100
{ text: 'Testing', link: '/guide/testing.md' },
100101
],

docs/entrypoints/content-scripts.md

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default defineContentScript({
4040
// Configure how/when content script will be registered
4141
registration: undefined | "manifest" | "runtime",
4242

43-
main(ctx) {
43+
main(ctx: ContentScriptContext) {
4444
// Executed when content script is loaded
4545
},
4646
});
@@ -50,52 +50,6 @@ export default defineContentScript({
5050
5151
When defining multiple content scripts, content script entrypoints that have the same set of options will be merged into a single `content_script` item in the manifest.
5252

53-
## Context
54-
55-
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
56-
57-
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
58-
59-
When your extension updates or is uninstalled, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
60-
61-
```ts
62-
export default defineContentScript({
63-
// ...
64-
main(ctx: ContentScriptContext) {
65-
// Add custom listeners for stopping work
66-
ctx.onInvalidated(() => {
67-
// ...
68-
});
69-
70-
// Timeout utilities that are automatically cleared when invalidated
71-
ctx.setTimeout(() => {
72-
// ...
73-
}, 5e3);
74-
ctx.setInterval(() => {
75-
// ...
76-
}, 60e3);
77-
78-
// Or add event listeners that get removed when invalidated
79-
ctx.addEventListener(document, 'visibilitychange', (event) => {
80-
// ...
81-
});
82-
83-
// You can also stop fetch requests
84-
fetch('...url', { signal: ctx.signal });
85-
},
86-
});
87-
```
88-
89-
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
90-
91-
:::warning
92-
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
93-
94-
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
95-
96-
If you're using a framework like React, Vue, Svelte, etc., make sure you're unmounting your UI properly in the `onRemove` option of [`createShadowRootUi`](https://wxt.dev/guide/content-script-ui.html#shadow-root).
97-
:::
98-
9953
## CSS
10054

10155
To include CSS with your content script, import the CSS file at the top of your entrypoint.

docs/guide/handling-updates.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Handling Extension Updates
2+
3+
When releasing an update to your extension, there's a couple of things you need to keep in mind:
4+
5+
[[toc]]
6+
7+
## Content Script Cleanup
8+
9+
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
10+
11+
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of each content script.
12+
13+
When your extension updates or reloads, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
14+
15+
```ts
16+
export default defineContentScript({
17+
main(ctx) {
18+
ctx.onInvalidated(() => {
19+
// Do something
20+
});
21+
},
22+
)
23+
```
24+
25+
The `ctx` also provides other convenient APIs for stopping your content script without manually calling `onInvalidated` to add a listener:
26+
27+
1. Setting timers:
28+
```ts
29+
ctx.setTimeout(() => { ... }, ...);
30+
ctx.setInterval(() => { ... }, ...);
31+
ctx.requestAnimationFrame(() => { ... });
32+
```
33+
1. Adding DOM events:
34+
```ts
35+
ctx.addEventListener(window, "mousemove", (event) => { ... });
36+
```
37+
1. Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) for canceling standard APIs:
38+
```ts
39+
fetch('...', {
40+
signal: ctx.signal,
41+
});
42+
```
43+
44+
Other WXT APIs require a `ctx` object so they can clean themselves up. For example, [`createIntegratedUi`](/guide/content-script-ui#integrated), [`createShadowRootUi`](/guide/content-script-ui#shadow-root), and [`createIframeUi`](/guide/content-script-ui#iframe) automatically unmount and stop a UI when the script is invalidated.
45+
46+
:::warning
47+
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
48+
49+
This prevents old content scripts from interfering with new content scripts, and prevents error messages from being logged to the console in production.
50+
:::
51+
52+
## Testing Permission Changes
53+
54+
When `permissions`/`host_permissions` change during an update, depending on what exactly changed, Chrome will disable your extension until the user accepts the new permissions.
55+
56+
It is possible to test this before you release an update, but it's not a simple process:
57+
58+
1. Get 2 ZIPs of your extension, both generated by `wxt zip`. The first contains a previous version of your extension, the second contains the latest version. Make sure the second ZIP's version is higher than the first's.
59+
2. Unzip the two ZIP files somewhere next to each other that's easy to locate.
60+
3. In Chrome, open `chrome://extensions` and make sure developer mode is enabled
61+
4. Pack the first extension into a CRX, generating a new private key:
62+
1. Click "Pack Extension" in the top left
63+
2. For "Extension root directory", enter the path to the first extracted zip directory. The directory should contain a `manifest.json` file
64+
3. Leave "Private key file" blank
65+
4. Click "Pack Extension". This will generate a `.crx` and `.pem` file
66+
5. Pack the second extension into a CRX, reusing the private key generated by the previous step
67+
1. Click "Pack Extension" in the top left
68+
2. For "Extension root directory", enter the path to the second extracted zip directory.
69+
3. For "Private key file", enter the path to the generated `.pem` private key file
70+
4. Click "Pack Extension". This will generate a second `.crx` file.
71+
6. Install the first CRX file by dragging and dropping it onto the `chrome://extensions` page
72+
7. Install the second CRX file by dragging and dropping it onthe the `chrome://extensions` page
73+
74+
If a new permission must be accepted, you'll be prompted to accept it after dropping the second CRX file onto the page.
75+
76+
:::Info Note
77+
Note: Chrome no longer allows self-signed CRX extensions to run, but that's OK for this test case. We're still prompted to accept new permissions, even if we can't interact with the installed extension.
78+
79+
To validate this, you can create a third ZIP file with a rare permission like `geolocation` in the manifest, that's guarenteed to reprompt permissions when added.
80+
:::
81+
82+
## Update Event
83+
84+
You can setup a callback that runs after your extension updates like so:
85+
86+
```ts
87+
browser.runtime.onInstalled.addEventListener(({ reason }) => {
88+
if (reason === 'update') {
89+
// Do something
90+
}
91+
});
92+
```
93+
94+
If the logic is simple, write a unit test to cover this logic. If you feel the need to manually test this callback, you can either:
95+
96+
1. In dev mode, remove the `if` statement and reload the extension from `chrome://extensions`
97+
2. Build two ZIPs with the same runtime ID and actually update the extension
98+
99+
The first approach is very straightforward. The second is more complicated...
100+
101+
Here are the steps:
102+
103+
So the steps:
104+
105+
1. Checkout an old commit.
106+
2. [Add a `key`](https://developer.chrome.com/docs/extensions/reference/manifest/key#keep-consistent-id) to the `manifest` in your `wxt.config.ts`.
107+
3. Run `wxt zip` to create the first ZIP.
108+
4. Stash or reset changes and checkout your latest code.
109+
5. Add back the same `key` to your manifest.
110+
6. Make sure the extension's version is higher than the first zip. It can be any version that's higher, since you won't be releasing this version.
111+
7. Run `wxt zip` to create the second ZIP.
112+
8. In a fresh chrome profile, go to `chrome://extensions`, enable dev mode, and drag and drop the first zip onto the page to install it.
113+
9. In the extension, play around and setup your test case.
114+
10. Back on `chrome://extensions`, drag and drop your second zip onto the page.
115+
116+
If you setup the `key` correctly, it will cause the extension to act like it was updated instead of installing a second version of your extension.

0 commit comments

Comments
 (0)