|
| 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