Compare commits

..

33 Commits

Author SHA1 Message Date
GitHub Actions ba3d8838e3 chore(release): @wxt-dev/module-svelte v2.0.3
vhs / vhs (push) Cancelled after 0s
2025-01-28 21:17:33 +00:00
Eli e9b6f5f97c fix: Upgrade vite-plugin-svelte to support Vite 6 (#1375)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-28 15:09:01 -06:00
Aaron 5b6c82d38f chore: Fix formatting 2025-01-28 14:43:38 -06:00
Aaron 5b41b70abe docs: Add SECURITY.md 2025-01-28 14:41:56 -06:00
Okinea Dev 93f624610f chore: add pull request template (#1372)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-28 08:03:26 -06:00
Okinea Dev 73bdd49471 chore: simplify FUNDING.yml (#1371) 2025-01-28 07:32:59 -06:00
GitHub Actions 635f3f0898 chore(release): wxt v0.19.26
vhs / vhs (push) Cancelled after 0s
2025-01-28 06:46:49 +00:00
Deniz Uğur a53ee6d590 fix(context): Deduplicate wxt:content-script-started (#1364)
Co-authored-by: DenizUgur <DenizUgur@users.noreply.github.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-28 00:41:12 -06:00
GitHub Actions 665b9194cf chore(release): @wxt-dev/storage v1.1.0
vhs / vhs (push) Cancelled after 0s
2025-01-28 06:40:20 +00:00
Gabriel Roldán 4690418c94 Fix: Remove duplicated css rule from vue template (#1369) 2025-01-27 16:00:09 -06:00
Chengxi 20eea2a7d1 feat: Add storage.clear (#1368)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-27 07:22:19 -06:00
rxliuli ac7b6760f2 docs: Add missing bracket in example (#1367) 2025-01-25 01:14:48 -06:00
GitHub Actions a593cfbbb6 chore(release): wxt v0.19.25
vhs / vhs (push) Cancelled after 0s
2025-01-22 05:08:46 +00:00
Aaron a205f2a0a8 chore: Update lockfile 2025-01-21 23:03:09 -06:00
Aaron 8e2badc92b fix: Drop support for vite 6.0.9+ until websocket fix is finished 2025-01-21 23:02:22 -06:00
redwoodlid 7c18d9c0b8 docs: Add react language ID to README (#1347) 2025-01-15 10:25:40 -06:00
Aaron 55a1f39450 Update changelog 2025-01-15 10:23:36 -06:00
GitHub Actions d708eca78e chore(release): wxt v0.19.24
vhs / vhs (push) Cancelled after 0s
2025-01-12 22:48:50 +00:00
1natsu 5d7e2516b6 fix: autoMount not working if anchor is already present (#1350) 2025-01-12 16:43:07 -06:00
Eric fb6a29872b docs: Add "HTML to Markdown" to the homepage (#1345) 2025-01-10 11:28:27 -06:00
rxliuli 187206062e docs: Update content script UI examples to use Svelte v5 (#1342) 2025-01-07 23:51:46 -06:00
Aaron 2615e29e52 docs: Add section to FAQ about teleports/portals 2025-01-07 23:37:53 -06:00
Aaron 8ca176d37e docs: Update i18n.md 2025-01-06 17:40:57 -06:00
Aaron cb2bf43f30 docs: Update content-scripts.md 2025-01-06 17:36:43 -06:00
Aaron 1719d17cff docs: Fix typo 2025-01-06 17:28:31 -06:00
Aaron 32afb8d97c docs: Improve manifest docs, mention entrypoint-specific options 2025-01-06 17:28:09 -06:00
Aaron 72b8a29d76 docs: Improve entrypoint docs 2025-01-06 17:09:20 -06:00
Aaron af0a32d478 docs: Add missing tabs in project structure 2025-01-06 16:59:23 -06:00
Nikolay Ponomarev 060e85f90e docs: Add missing web_accessible_resources to injectScript example (#1337) 2025-01-06 16:23:41 -06:00
Aaron 654a54ac79 docs: Update link 2025-01-05 10:14:49 -06:00
ntnyq ef24358942 docs: Fix unocss readme (#1329)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-04 17:01:43 -06:00
Aaron e5e67e39bd docs: Add paragraph on browser binary auto-discovery 2025-01-01 17:23:04 -06:00
Ahmed Dinar 228304bef9 docs: Added "Blync" to the homepage (#1319) 2025-01-01 08:56:07 -06:00
44 changed files with 392 additions and 1114 deletions
+2 -14
View File
@@ -1,15 +1,3 @@
# These are supported funding model platforms
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository#about-funding-files
github: [wxt-dev] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
github: wxt-dev
+13
View File
@@ -0,0 +1,13 @@
### Overview
<!-- Describe your changes and why you made them -->
### Manual Testing
<!-- Describe how to test your changes to make sure the PR works as intended -->
### Related Issue
<!-- If this PR is related to an issue, please link it here -->
This PR closes #<issue_number>
-1
View File
@@ -7,7 +7,6 @@ on:
default: wxt
type: choice
options:
- analytics
- auto-icons
- i18n
- module-react
-1
View File
@@ -7,7 +7,6 @@ on:
default: wxt
type: choice
options:
- analytics
- auto-icons
- i18n
- module-react
+15
View File
@@ -0,0 +1,15 @@
# Security Policy
While WXT is in prerelease, only the latest version will recieve security updates. The latest version is:
<img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
<!--
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
-->
@@ -62,6 +62,8 @@ const chromeExtensionIds = [
'hjjkgbibknbahijglkffklflidncplkn', // Show IP Live View of Website IPs for Developers
'ilbikcehnpkmldojkcmlldkoelofnbde', // Strong Password Generator
'ocllfkhcdopiafndigclebelbecaiocp', // ZenGram: Mindful Instagram, Your Way
'odffpjnpocjfcaclnenaaaddghkgijdb', // Blync: Preview Links, Selection Search, AI Assistant
'kofbbilhmnkcmibjbioafflgmpkbnmme', // HTML to Markdown - Convert webpages to markdown
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 132 KiB

@@ -41,6 +41,8 @@ export default defineRunnerConfig({
});
```
By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.
### Persist Data
By default, to keep from modifying your browser's existing profiles, `web-ext` creates a brand new profile every time you run the `dev` script.
+12 -20
View File
@@ -1,10 +1,17 @@
# Manifest
In WXT, there is no `manifest.json` file in your source code. Instead, WXT generates it during the build process based off files in your project.
In WXT, there is no `manifest.json` file in your source code. Instead, WXT generates the manifest from multiple sources:
## Manifest Config
- Global options [defined in your `wxt.config.ts` file](#global-options)
- Entrypoint-specific options [defined in your entrypoints](/guide/essentials/entrypoints#defining-manifest-options)
- [WXT Modules](/guide/essentials/wxt-modules) added to your project can modify your manifest
- [Hooks](/guide/essentials/config/hooks) defined in your project can modify your manifest
To manually add a property to the `manifest.json` output during builds, use the `manifest` config inside `wxt.config.ts`:
Your extension's `manifest.json` will be output to `.output/{target}/manifest.json` when running `wxt build`.
## Global Options
To add a property to your manifest, use the `manifest` config inside your `wxt.config.ts`:
```ts
export default defineConfig({
@@ -130,7 +137,7 @@ public/
└─ icon-128.png
```
Specifically, if an icon must match one of these regex to be discovered:
Specifically, an icon must match one of these regex to be discovered:
<<< @/../packages/wxt/src/core/utils/manifest.ts#snippet
@@ -216,22 +223,7 @@ By default, whenever an `action` is generated, WXT falls back to `browser_action
### Action With Popup
To generate a manifest where a UI appears after clicking the icon, just create a [Popup entrypoint](/guide/essentials/entrypoints#popup).
```ts
export default defineConfig({
hooks: {
build: {
manifestGenerated(manifest) {
// Update the manifest variable by reference
manifest.name = 'Overriden name';
},
},
},
});
```
If you want to use a `page_action` for MV2, add the following meta tag to the HTML document's head:
To generate a manifest where a UI appears after clicking the icon, just create a [Popup entrypoint](/guide/essentials/entrypoints#popup). If you want to use a `page_action` for MV2, add the following meta tag to the HTML document's head:
```html
<meta name="manifest.type" content="page_action" />
+32 -16
View File
@@ -4,12 +4,14 @@ outline: deep
# Content Scripts
> To create a content script, see [Entrypoint Types](/guide/essentials/entrypoints#content-scripts).
## Context
The first argument to a content script's `main` function is it's "context".
```ts
// entrypoints/content.ts
// entrypoints/example.content.ts
export default defineContentScript({
main(ctx) {},
});
@@ -62,7 +64,7 @@ In regular web extensions, CSS for content scripts is usually a separate CSS fil
In WXT, to add CSS to a content script, simply import the CSS file into your JS entrypoint, and WXT will automatically add the bundled CSS output to the `css` array.
```ts
// entrypoints/content/index.ts
// entrypoints/example.content/index.ts
import './style.css';
export default defineContentScript({
@@ -78,16 +80,16 @@ To create a standalone content script that only includes a CSS file:
// wxt.config.ts
export default defineConfig({
hooks: {
"build:manifestGenerated": (wxt, manifest) => {
'build:manifestGenerated': (wxt, manifest) => {
manifest.content_scripts ??= [];
manifest.content_scripts.push({
// Build extension once to see where your CSS get's written to
css: ["content-scripts/example.css"],
matches: ["*://*/*"]
)
}
}
})
css: ['content-scripts/example.css'],
matches: ['*://*/*'],
});
},
},
});
```
## UI
@@ -198,6 +200,7 @@ export default defineContentScript({
```ts [Svelte]
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
matches: ['<all_urls>'],
@@ -208,14 +211,13 @@ export default defineContentScript({
anchor: 'body',
onMount: (container) => {
// Create the Svelte app inside the UI container
const app = new App({
mount(App, {
target: container,
});
return app;
},
onRemove: (app) => {
// Destroy the app when the UI is removed
app.$destroy();
unmount(app);
},
});
@@ -379,6 +381,7 @@ export default defineContentScript({
// 1. Import the style
import './style.css';
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
matches: ['<all_urls>'],
@@ -393,14 +396,13 @@ export default defineContentScript({
anchor: 'body',
onMount: (container) => {
// Create the Svelte app inside the UI container
const app = new App({
mount(App, {
target: container,
});
return app;
},
onRemove: (app) => {
onRemove: () => {
// Destroy the app when the UI is removed
app?.$destroy();
unmount(app);
},
});
@@ -566,6 +568,20 @@ export default defineContentScript({
});
```
```json
export default defineConfig({
manifest: {
// ...
web_accessible_resources: [
{
resources: ["example-main-world.js"],
matches: ["*://*/*"],
}
]
}
});
```
`injectScript` works by creating a `script` element on the page pointing to your script. This loads the script into the page's context so it runs in the main world.
`injectScript` returns a promise, that when resolved, means the script has been evaluated by the browser and you can start communicating with it.
+14 -3
View File
@@ -28,7 +28,7 @@ For web extensions, there are two types of entrypoints:
- **Listed**: Referenced in the `manifest.json`
- **Unlisted**: Not referenced in the `manifest.json`
Throughout the rest of WXT's documentation, listed files are referred to by name. For example:
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
- Popup
- Options
@@ -42,7 +42,7 @@ Some examples of "unlisted" entrypoints:
- JS files injected by content scripts into the page's main world
:::tip
Regardless of whether a entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
Regardless of whether an entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
:::
## Adding Entrypoints
@@ -72,7 +72,7 @@ Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of
## Defining Manifest Options
Most listed entrypoints have options that need to be added to the `manifest.json`. With WXT however, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
Most listed entrypoints have options that need to be added to the `manifest.json`. However with WXT, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
For example, here's how to define `matches` for content scripts:
@@ -86,6 +86,17 @@ export default defineContentScript({
});
```
For HTML entrypoints, options are configured as `<meta>` tags. For example, to use a `page_action` for your MV2 popup:
```html
<!doctype html>
<html lang="en">
<head>
<meta name="manifest.type" content="page_action" />
</head>
</html>
```
> Refer to the [Entrypoint Types](#entrypoint-types) sections for a list of options configurable inside each entrypoint, and how to define them.
When building your extension, WXT will look at the options defined in your entrypoints, and generate the manifest accordingly.
+4
View File
@@ -81,6 +81,10 @@ However, it is recommended you stick with the vanilla API (or a package based on
- Translations are not bundled multiple times, keeping your extension small
- Zero configuration
However, there is one major downside to the vanilla API and any packages built on top of it:
- Language cannot be changed without changing your browser/system language
Here are some examples of how to setup a third party i18n library:
- [vue-i18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n)
+18 -18
View File
@@ -59,24 +59,24 @@ After enabling it, your project structure should look like this:
<!-- prettier-ignore -->
```html
📂 {rootDir}/
📁 .output/
📁 .wxt/
📂 src/
📁 assets/
📁 components/
📁 composables/
📁 entrypoints/
📁 hooks/
📁 modules/
📁 public/
📁 utils/
📄 app.config.ts
📄 .env
📄 .env.publish
📄 package.json
📄 tsconfig.json
📄 web-ext.config.ts
📄 wxt.config.ts
📁 .output/
📁 .wxt/
📂 src/
📁 assets/
📁 components/
📁 composables/
📁 entrypoints/
📁 hooks/
📁 modules/
📁 public/
📁 utils/
📄 app.config.ts
📄 .env
📄 .env.publish
📄 package.json
📄 tsconfig.json
📄 web-ext.config.ts
📄 wxt.config.ts
```
## Customizing Other Directories
+126 -36
View File
@@ -28,47 +28,137 @@ See https://wxt.dev/guide/essentials/config/browser-startup.html#persist-data
## My component library doesn't work in content scripts!
Component libraries place their CSS in the document's `<head>` by default. When using `createShadowRootUi`, your UI is isolated from the document's styles because it's inside a ShadowRoot.
This is usually caused by one of two things (or both) when using `createShadowRootUi`:
To fix this, you need to tell your component library to insert it's CSS inside the shadow root. Here's the docs for a couple of popular libraries:
1. Styles are added outside the `ShadowRoot`
- React
- Ant Design: [`StyleProvider`](https://ant.design/docs/react/compatible-style#shadow-dom-usage)
- Mantine: [`MantineProvider#getRootElement` and `MantineProvider#cssVariablesSelector`](https://mantine.dev/theming/mantine-provider/)
:::details
Some component libraries manually add CSS to the page by adding a `<style>` or `<link>` element. They place this element in the document's `<head>` by default. This causes your styles to be placed outside the `ShadowRoot` and it's isolation blocks the styles from being applied to your UI.
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container".
When a library does this, **you need to tell the library where to put its styles**. Here's the documentation for a few popular component libraries:
`createShadowRootUi` provides it's own `<head>` element inside the shadow root, so that were you should tell the library to add the CSS. Here's an example with Ant Design:
- Ant Design: [`StyleProvider`](https://ant.design/docs/react/compatible-style#shadow-dom-usage)
- Mantine: [`MantineProvider#getRootElement` and `MantineProvider#cssVariablesSelector`](https://mantine.dev/theming/mantine-provider/)
```tsx
import { StyleProvider } from '@ant-design/cssinjs'; // [!code ++]
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container". Not all libraries support shadow DOMs, you may have to open an issue to request this feature.
const ui = await createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: 'body',
onMount: (container) => { // [!code --]
onMount: (container, shadow) => { // [!code ++]
const cssContainer = shadow.querySelector("head")!; // [!code ++]
const root = ReactDOM.createRoot(container);
root.render(
<StyleProvider container={cssContainer}> // [!code ++]
<App />
</StyleProvider> // [!code ++]
);
return root;
},
onRemove: (root) => {
root?.unmount();
},
});
```
Here's an example of configuring Antd's styles:
Note that this doesn't effect all component libraries, just ones that inject CSS themselves rather than having you import their CSS. This approach is more prevailent in the React community, but not limited to it. That's why only React libraries are listed above. Vuetify, for example, works just fine because you import its CSS - WXT picks up on this and the CSS is added inside the shadow root automatically:
```tsx
import { StyleProvider } from '@ant-design/cssinjs';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
```ts
import 'vuetify/styles'; // <-- This line imports the CSS, just like importing a .css file
import { createVuetify } from 'vuetify';
```
const ui = await create`ShadowRoot`Ui(ctx, {
// ...
onMount: (container, shadow) => {
const cssContainer = shadow.querySelector('head')!;
const root = ReactDOM.createRoot(container);
root.render(
<StyleProvider container={cssContainer}>
<App />
</StyleProvider>,
);
return root;
},
});
```
:::
2. UI elements are added outside the `ShadowRoot`
::::::details
This is mostly caused by `Teleport` or `Portal` components that render an element somewhere else in the DOM, usually in the document's `<body>`. This is usually done for dialogs or popover components. This renders the element is outside the `ShadowRoot`, so styles are not applied to it.
To fix this, **you need to both provide a target to your app AND pass the target to the `Teleport`/`Portal`**.
First, store the reference to the `ShadowRoot`'s `<body>` element (not the document's `<body>`):
:::code-group
```ts [Vue]
import { createApp } from 'vue';
import App from './App.vue';
const ui = await create`ShadowRoot`Ui(ctx, {
// ...
onMount: (container, shadow) => {
const teleportTarget = shadow.querySelector('body')!;
const app = createApp(App)
.provide('TeleportTarget', teleportTarget)
.mount(container);
return app;
},
});
ui.mount();
```
```tsx [React]
// hooks/PortalTargetContext.ts
import { createContext } from 'react';
export const PortalTargetContext = createContext<HTMLElement>();
// entrypoints/example.content.ts
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import PortalTargetContext from '~/hooks/PortalTargetContext';
const ui = await create`ShadowRoot`Ui(ctx, {
// ...
onMount: (container, shadow) => {
const portalTarget = shadow.querySelector('body')!;
const root = ReactDOM.createRoot(container);
root.render(
<PortalTargetContext.Provider value={portalTarget}>
<App />
</PortalTargetContext.Provider>,
);
return root;
},
});
ui.mount();
```
:::
Then use the reference when teleporting/portaling part of your UI to a different place in the DOM:
:::code-group
```vue [Vue]
<script lang="ts" setup>
import { Teleport } from 'vue';
const teleportTarget = inject('TeleportTarget');
</script>
<template>
<div>
<Teleport :to="teleportTarget">
<dialog>My dialog</dialog>
</Teleport>
</div>
</template>
```
```tsx [React]
import { useContext } from 'react';
import { createPortal } from 'react-dom';
import PortalTargetContext from '~/hooks/PortalTargetContext';
const MyComponent = () => {
const portalTarget = useContext(PortalTargetContext);
return <div>{createPortal(<dialog>My dialog</dialog>, portalTarget)}</div>;
};
```
:::
::::::
Both issues have the same cause: the library puts something outside the `ShadowRoot`, and the `ShadowRoot`'s isolation prevents CSS from being applied to your UI.
Both issues have the same fix: tell the library to put elements inside the `ShadowRoot`, not outside it. See the details above for more information and example fixes for each problem.
-174
View File
@@ -1,174 +0,0 @@
# WXT Analytics
Report analytics events from your web extension extension.
## Supported Analytics Providers
- [Google Analytics 4 (Measurement Protocol)](#google-analytics-4-measurement-protocol)
- [Umami](#umami)
## Install With WXT
1. Install the NPM package:
```bash
pnpm i @wxt-dev/analytics
```
2. In your `wxt.config.ts`, add the WXT module:
```ts
export default defineConfig({
modules: ['@wxt-dev/analytics/module'],
});
```
3. In your `<srcDir>/app.config.ts`, add a provider:
```ts
// <srcDir>/app.config.ts
import { umami } from '@wxt-dev/analytics/providers/umami';
export default defineAppConfig({
analytics: {
debug: true,
providers: [
// ...
],
},
});
```
4. Then use the `#analytics` module to report events:
```ts
import { analytics } from '#analytics';
await analytics.track('some-event');
await analytics.page();
await analytics.identify('some-user-id');
analytics.autoTrack(document.body);
```
## Install Without WXT
1. Install the NPM package:
```bash
pnpm i @wxt-dev/analytics
```
2. Create an `analytics` instance:
```ts
// utils/analytics.ts
import { createAnalytics } from '@wxt-dev/analytics';
export const analytics = createAnalytics({
providers: [
// ...
],
});
```
3. Import your analytics module in the background to initialize the message listener:
```ts
// background.ts
import './utils/analytics';
```
4. Then use your `analytics` instance to report events:
```ts
import { analytics } from './utils/analytics';
await analytics.track('some-event');
await analytics.page();
await analytics.identify('some-user-id');
analytics.autoTrack(document.body);
```
## Providers
### Google Analytics 4 (Measurement Protocol)
The [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4) is an alternative to GTag for reporting events to Google Analytics for MV3 extensions.
> [Why use the Measurement Protocol instead of GTag?](https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4#measurement-protocol)
Follow [Google's documentation](https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4#setup-credentials) to obtain your credentials and put them in your `.env` file:
```dotenv
WXT_GA_API_SECRET='...'
```
Then add the `googleAnalytics4` provider to your `<srcDir>/app.config.ts` file:
```ts
import { googleAnalytics4 } from '@wxt-dev/analytics/providers/google-analytics-4';
export default defineAppConfig({
analytics: {
providers: [
googleAnalytics4({
apiSecret: import.meta.env.WXT_GA_API_SECRET,
measurementId: '...',
}),
],
},
});
```
### Umami
[Umami](https://umami.is/) is a privacy-first, open source analytics platform.
In Umami's dashboard, create a new website. The website's name and domain can be anything. Obviously, an extension doesn't have a domain, so make one up if you don't have one.
After the website has been created, save the website ID and domain to your `.env` file:
```dotenv
WXT_UMAMI_WEBSITE_ID='...'
WXT_UMAMI_DOMAIN='...'
```
Then add the `umami` provider to your `<srcDir>/app.config.ts` file:
```ts
import { umami } from '@wxt-dev/analytics/providers/umami';
export default defineAppConfig({
analytics: {
providers: [
umami({
apiUrl: 'https://<your-umami-instance>/api',
websiteId: import.meta.env.WXT_UMAMI_WEBSITE_ID,
domain: import.meta.env.WXT_UMAMI_DOMAIN,
}),
],
},
});
```
### Custom Provider
If your analytics platform is not supported, you can provide an implementation of the `AnalyticsProvider` type in your `app.config.ts` instead:
```ts
import { defineAnalyticsProvider } from '@wxt-dev/analytics/client';
interface CustomAnalyticsOptions {
// ...
}
const customAnalytics = defineAnalyticsProvider<CustomAnalyticsOptions>(
(analytics, analyticsConfig, providerOptions) => {
// ...
},
);
export default defineAppConfig({
analytics: {
providers: [
customAnalytics({
// ...
}),
],
},
});
```
Example `AnalyticsProvider` implementations can be found at [`./modules/analytics/providers`](https://github.com/wxt-dev/wxt/tree/main/packages/analytics/modules/analytics/providers).
-20
View File
@@ -1,20 +0,0 @@
import { defineAppConfig } from 'wxt/sandbox';
import { googleAnalytics4 } from './modules/analytics/providers/google-analytics-4';
import { umami } from './modules/analytics/providers/umami';
export default defineAppConfig({
analytics: {
debug: true,
providers: [
googleAnalytics4({
apiSecret: '...',
measurementId: '...',
}),
umami({
apiUrl: 'https://umami.aklinker1.io/api',
domain: 'analytics.wxt.dev',
websiteId: '8f1c2aa4-fad3-406e-a5b2-33e8d4501716',
}),
],
},
});
-21
View File
@@ -1,21 +0,0 @@
import { defineBuildConfig } from 'unbuild';
import { resolve } from 'node:path';
// Build module and plugins
export default defineBuildConfig({
rootDir: 'modules/analytics',
outDir: resolve(__dirname, 'dist'),
entries: [
{ input: 'index.ts', name: 'module' },
{ input: 'client.ts', name: 'index' },
'background-plugin.ts',
'types.ts',
'providers/google-analytics-4.ts',
'providers/umami.ts',
],
externals: ['#analytics'],
replace: {
'import.meta.env.NPM': 'true',
},
declaration: true,
});
@@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
</head>
<body>
<label>
<input id="enabledCheckbox" type="checkbox" />
&emsp;Analytics enabled
</label>
<button id="button1">Button 1</button>
<button class="cool-button">Button 2</button>
<script type="module" src="./main.ts"></script>
</body>
</html>
@@ -1,9 +0,0 @@
import { analytics } from '#analytics';
declare const enabledCheckbox: HTMLInputElement;
analytics.autoTrack(document);
enabledCheckbox.oninput = () => {
void analytics.setEnabled(enabledCheckbox.checked);
};
@@ -1,3 +0,0 @@
import '#analytics';
export default () => {};
@@ -1,288 +0,0 @@
import { UAParser } from 'ua-parser-js';
import type {
Analytics,
AnalyticsConfig,
AnalyticsPageViewEvent,
AnalyticsStorageItem,
AnalyticsTrackEvent,
BaseAnalyticsEvent,
AnalyticsEventMetadata,
AnalyticsProvider,
} from './types';
const ANALYTICS_PORT = '@wxt-dev/analytics';
export function createAnalytics(config?: AnalyticsConfig): Analytics {
if (typeof chrome === 'undefined' || !chrome?.runtime?.id)
throw Error(
'Cannot use WXT analytics in contexts without access to the browser.runtime APIs',
);
if (config == null) {
console.warn(
"[@wxt-dev/analytics] Config not provided to createAnalytics. If you're using WXT, add the 'analytics' property to '<srcDir>/app.config.ts'.",
);
}
// TODO: This only works for standard WXT extensions, add a more generic
// background script detector that works with non-WXT projects.
if (location.pathname === '/background.js')
return createBackgroundAnalytics(config);
return createFrontendAnalytics();
}
/**
* Creates an analytics client in the background responsible for uploading events to the server to avoid CORS errors.
*/
function createBackgroundAnalytics(
config: AnalyticsConfig | undefined,
): Analytics {
// User properties storage
const userIdStorage =
config?.userId ?? defineStorageItem<string>('wxt-analytics:user-id');
const userPropertiesStorage =
config?.userProperties ??
defineStorageItem<Record<string, string>>(
'wxt-analytics:user-properties',
{},
);
const enabled =
config?.enabled ??
defineStorageItem<boolean>('local:wxt-analytics:enabled', false);
// Cached values
const platformInfo = chrome.runtime.getPlatformInfo();
const userAgent = UAParser();
let userId = Promise.resolve(userIdStorage.getValue()).then(
(id) => id ?? globalThis.crypto.randomUUID(),
);
let userProperties = userPropertiesStorage.getValue();
const manifest = chrome.runtime.getManifest();
const getBackgroundMeta = () => ({
timestamp: Date.now(),
// Don't track sessions for the background, it can be running
// indefinitely, and will inflate session duration stats.
sessionId: undefined,
language: navigator.language,
referrer: undefined,
screen: undefined,
url: location.href,
title: undefined,
});
const getBaseEvent = async (
meta: AnalyticsEventMetadata,
): Promise<BaseAnalyticsEvent> => {
const platform = await platformInfo;
return {
meta,
user: {
id: await userId,
properties: {
version: config?.version ?? manifest.version_name ?? manifest.version,
wxtMode: import.meta.env.MODE,
wxtBrowser: import.meta.env.BROWSER,
arch: platform.arch,
os: platform.os,
browser: userAgent.browser.name,
browserVersion: userAgent.browser.version,
...(await userProperties),
},
},
};
};
const analytics = {
identify: async (
newUserId: string,
newUserProperties: Record<string, string> = {},
meta: AnalyticsEventMetadata = getBackgroundMeta(),
) => {
// Update in-memory cache for all providers
userId = Promise.resolve(newUserId);
userProperties = Promise.resolve(newUserProperties);
// Persist user info to storage
await Promise.all([
userIdStorage.setValue?.(newUserId),
userPropertiesStorage.setValue?.(newUserProperties),
]);
// Notify providers
const event = await getBaseEvent(meta);
if (config?.debug) console.debug('[@wxt-dev/analytics] identify', event);
if (await enabled.getValue()) {
await Promise.allSettled(
providers.map((provider) => provider.identify(event)),
);
} else if (config?.debug) {
console.debug(
'[@wxt-dev/analytics] Analytics disabled, identify() not uploaded',
);
}
},
page: async (
location: string,
meta: AnalyticsEventMetadata = getBackgroundMeta(),
) => {
const baseEvent = await getBaseEvent(meta);
const event: AnalyticsPageViewEvent = {
...baseEvent,
page: {
url: meta?.url ?? globalThis.location?.href,
location,
title: meta?.title ?? globalThis.document?.title,
},
};
if (config?.debug) console.debug('[@wxt-dev/analytics] page', event);
if (await enabled.getValue()) {
await Promise.allSettled(
providers.map((provider) => provider.page(event)),
);
} else if (config?.debug) {
console.debug(
'[@wxt-dev/analytics] Analytics disabled, page() not uploaded',
);
}
},
track: async (
eventName: string,
eventProperties?: Record<string, string>,
meta: AnalyticsEventMetadata = getBackgroundMeta(),
) => {
const baseEvent = await getBaseEvent(meta);
const event: AnalyticsTrackEvent = {
...baseEvent,
event: { name: eventName, properties: eventProperties },
};
if (config?.debug) console.debug('[@wxt-dev/analytics] track', event);
if (await enabled.getValue()) {
await Promise.allSettled(
providers.map((provider) => provider.track(event)),
);
} else if (config?.debug) {
console.debug(
'[@wxt-dev/analytics] Analytics disabled, track() not uploaded',
);
}
},
setEnabled: async (newEnabled) => {
await enabled.setValue?.(newEnabled);
},
autoTrack: () => {
// Noop, background doesn't have a UI
return () => {};
},
} satisfies Analytics;
const providers =
config?.providers?.map((provider) => provider(analytics, config)) ?? [];
// Listen for messages from the rest of the extension
chrome.runtime.onConnect.addListener((port) => {
if (port.name === ANALYTICS_PORT) {
port.onMessage.addListener(({ fn, args }) => {
// @ts-expect-error: Untyped fn key
void analytics[fn]?.(...args);
});
}
});
return analytics;
}
/**
* Creates an analytics client for non-background contexts.
*/
function createFrontendAnalytics(): Analytics {
const port = chrome.runtime.connect({ name: ANALYTICS_PORT });
const sessionId = Date.now();
const getFrontendMetadata = (): AnalyticsEventMetadata => ({
sessionId,
timestamp: Date.now(),
language: navigator.language,
referrer: globalThis.document?.referrer || undefined,
screen: globalThis.window
? `${globalThis.window.screen.width}x${globalThis.window.screen.height}`
: undefined,
url: location.href,
title: document.title || undefined,
});
const methodForwarder =
(fn: string) =>
(...args: any[]) => {
port.postMessage({ fn, args: [...args, getFrontendMetadata()] });
};
const analytics: Analytics = {
identify: methodForwarder('identify'),
page: methodForwarder('page'),
track: methodForwarder('track'),
setEnabled: methodForwarder('setEnabled'),
autoTrack: (root) => {
const onClick = (event: Event) => {
const element = event.target as any;
if (
!element ||
(!INTERACTIVE_TAGS.has(element.tagName) &&
!INTERACTIVE_ROLES.has(element.getAttribute('role')))
)
return;
void analytics.track('click', {
tagName: element.tagName?.toLowerCase(),
id: element.id || undefined,
className: element.className || undefined,
textContent: element.textContent?.substring(0, 50) || undefined, // Limit text content length
href: element.href,
});
};
root.addEventListener('click', onClick, { capture: true, passive: true });
return () => {
root.removeEventListener('click', onClick);
};
},
};
return analytics;
}
function defineStorageItem<T>(
key: string,
defaultValue?: NonNullable<T>,
): AnalyticsStorageItem<T> {
return {
getValue: async () =>
(await chrome.storage.local.get(key))[key] ?? defaultValue,
setValue: (newValue) => chrome.storage.local.set({ [key]: newValue }),
};
}
const INTERACTIVE_TAGS = new Set([
'A',
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
]);
const INTERACTIVE_ROLES = new Set([
'button',
'link',
'checkbox',
'menuitem',
'tab',
'radio',
]);
export function defineAnalyticsProvider<T = never>(
definition: (
/** The analytics object. */
analytics: Analytics,
/** Config passed into the analytics module from `app.config.ts`. */
config: AnalyticsConfig,
/** Provider options */
options: T,
) => ReturnType<AnalyticsProvider>,
): (options: T) => AnalyticsProvider {
return (options) => (analytics, config) =>
definition(analytics, config, options);
}
@@ -1,76 +0,0 @@
import 'wxt';
import 'wxt/sandbox';
import { addAlias, addWxtPlugin, defineWxtModule } from 'wxt/modules';
import { relative, resolve } from 'node:path';
import type { AnalyticsConfig } from './types';
declare module 'wxt/sandbox' {
export interface WxtAppConfig {
analytics: AnalyticsConfig;
}
}
export default defineWxtModule({
name: 'analytics',
imports: [{ name: 'analytics', from: '#analytics' }],
setup(wxt) {
// Paths
const wxtAnalyticsFolder = resolve(wxt.config.wxtDir, 'analytics');
const wxtAnalyticsIndex = resolve(wxtAnalyticsFolder, 'index.ts');
const clientModuleId = import.meta.env.NPM
? '@wxt-dev/analytics'
: resolve(wxt.config.modulesDir, 'analytics/client');
const pluginModuleId = import.meta.env.NPM
? '@wxt-dev/analytics/background-plugin'
: resolve(wxt.config.modulesDir, 'analytics/background-plugin');
// Add required permissions
wxt.hook('build:manifestGenerated', (_, manifest) => {
manifest.permissions ??= [];
if (!manifest.permissions.includes('storage')) {
manifest.permissions.push('storage');
}
});
// Generate #analytics module
const wxtAnalyticsCode = [
`import { createAnalytics } from '${
import.meta.env.NPM
? clientModuleId
: relative(wxtAnalyticsFolder, clientModuleId)
}';`,
`import { useAppConfig } from 'wxt/client';`,
``,
`export const analytics = createAnalytics(useAppConfig().analytics);`,
``,
].join('\n');
addAlias(wxt, '#analytics', wxtAnalyticsIndex);
wxt.hook('prepare:types', async (_, entries) => {
entries.push({
path: wxtAnalyticsIndex,
text: wxtAnalyticsCode,
});
});
// Ensure there is a background entrypoint
wxt.hook('entrypoints:resolved', (_, entrypoints) => {
const hasBackground = entrypoints.find(
(entry) => entry.type === 'background',
);
if (!hasBackground) {
entrypoints.push({
type: 'background',
inputPath: 'virtual:user-background',
name: 'background',
options: {},
outputDir: wxt.config.outDir,
skipped: false,
});
}
});
// Ensure analytics is initialized in every context, mainly the background.
// TODO: Once there's a way to filter which entrypoints a plugin is applied to, only apply this to the background
addWxtPlugin(wxt, pluginModuleId);
},
});
@@ -1,73 +0,0 @@
import { defineAnalyticsProvider } from '../client';
import type { BaseAnalyticsEvent } from '../types';
const DEFAULT_ENGAGEMENT_TIME_IN_MSEC = 100;
export interface GoogleAnalytics4ProviderOptions {
apiSecret: string;
measurementId: string;
}
export const googleAnalytics4 =
defineAnalyticsProvider<GoogleAnalytics4ProviderOptions>(
(_, config, options) => {
const send = async (
data: BaseAnalyticsEvent,
eventName: string,
eventProperties: Record<string, string | undefined> | undefined,
): Promise<void> => {
const url = new URL(
config?.debug ? '/debug/mp/collect' : '/mp/collect',
'https://www.google-analytics.com',
);
if (options.apiSecret)
url.searchParams.set('api_secret', options.apiSecret);
if (options.measurementId)
url.searchParams.set('measurement_id', options.measurementId);
const userProperties = {
language: data.meta.language,
screen: data.meta.screen,
...data.user.properties,
};
const mappedUserProperties = Object.fromEntries(
Object.entries(userProperties).map(([name, value]) => [
name,
value == null ? undefined : { value },
]),
);
await fetch(url.href, {
method: 'POST',
body: JSON.stringify({
client_id: data.user.id,
consent: {
ad_user_data: 'DENIED',
ad_personalization: 'DENIED',
},
user_properties: mappedUserProperties,
events: [
{
name: eventName,
params: {
session_id: data.meta.sessionId,
engagement_time_msec: DEFAULT_ENGAGEMENT_TIME_IN_MSEC,
...eventProperties,
},
},
],
}),
});
};
return {
identify: () => Promise.resolve(), // No-op, user data uploaded in page/track
page: (event) =>
send(event, 'page_view', {
page_title: event.page.title,
page_location: event.page.location,
}),
track: (event) => send(event, event.event.name, event.event.properties),
};
},
);
@@ -1,70 +0,0 @@
import { defineAnalyticsProvider } from '../client';
export interface UmamiProviderOptions {
apiUrl: string;
websiteId: string;
domain: string;
}
export const umami = defineAnalyticsProvider<UmamiProviderOptions>(
(_, config, options) => {
const send = (payload: UmamiPayload) => {
if (config.debug) {
console.debug('[@wxt-dev/analytics] Sending event to Umami:', payload);
}
return fetch(`${options.apiUrl}/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ type: 'event', payload }),
});
};
return {
identify: () => Promise.resolve(), // No-op, user data uploaded in page/track
page: async (event) => {
await send({
name: 'page_view',
website: options.websiteId,
url: event.page.url,
hostname: options.domain,
language: event.meta.language ?? '',
referrer: event.meta.referrer ?? '',
screen: event.meta.screen ?? '',
title: event.page.title ?? '<blank>',
data: event.user.properties,
});
},
track: async (event) => {
await send({
name: event.event.name,
website: options.websiteId,
url: event.meta.url ?? '/',
title: '<blank>',
hostname: options.domain,
language: event.meta.language ?? '',
referrer: event.meta.referrer ?? '',
screen: event.meta.screen ?? '',
data: {
...event.event.properties,
...event.user.properties,
},
});
},
};
},
);
/** @see https://umami.is/docs/api/sending-stats#post-/api/send */
interface UmamiPayload {
hostname?: string;
language?: string;
referrer?: string;
screen?: string;
title?: string;
url?: string;
website: string;
name: string;
data?: Record<string, string | undefined>;
}
@@ -1,98 +0,0 @@
export interface Analytics {
/** Report a page change */
page: (url: string) => void;
/** Report a custom event */
track: (eventName: string, eventProperties?: Record<string, string>) => void;
/** Save information about the user */
identify: (userId: string, userProperties?: Record<string, string>) => void;
/** Automatically setup and track user interactions, returning a function to remove any listeners that were setup. */
autoTrack: (root: Document | ShadowRoot | Element) => () => void;
/** Calls `config.enabled.setValue` */
setEnabled: (enabled: boolean) => void;
}
export interface AnalyticsConfig {
/**
* Array of providers to send analytics to.
*/
providers: AnalyticsProvider[];
/**
* Enable debug logs and other provider-specific debugging features.
*/
debug?: boolean;
/**
* Extension version, defaults to `browser.runtime.getManifest().version`.
*/
version?: string;
/**
* Configure how the enabled flag is persisted. Defaults to using `""` in local extension storage.
*/
enabled?: AnalyticsStorageItem<boolean>;
/**
* Configure how the user Id is persisted
*/
userId?: AnalyticsStorageItem<string>;
/**
* Configure how user properties are persisted
*/
userProperties?: AnalyticsStorageItem<Record<string, string>>;
}
export interface AnalyticsStorageItem<T> {
getValue: () => T | Promise<T>;
setValue?: (newValue: T) => void | Promise<void>;
}
export type AnalyticsProvider = (
analytics: Analytics,
config: AnalyticsConfig,
) => {
/** Upload a page view event */
page: (event: AnalyticsPageViewEvent) => Promise<void>;
/** Upload a custom event */
track: (event: AnalyticsTrackEvent) => Promise<void>;
/** Upload information about the user */
identify: (event: BaseAnalyticsEvent) => Promise<void>;
};
export interface BaseAnalyticsEvent {
meta: AnalyticsEventMetadata;
user: {
id: string;
properties: Record<string, string | undefined>;
};
}
export interface AnalyticsEventMetadata {
/** Identifier of the session the event was fired from */
sessionId: number | undefined;
/** `Date.now()` of when the event was reported */
timestamp: number;
/** `"1920x1080"` */
screen: string | undefined;
/** `document.referrer` */
referrer: string | undefined;
/** `navigator.language` */
language: string | undefined;
/** `location.href` */
url: string | undefined;
/** `document.title` */
title: string | undefined;
}
export interface AnalyticsPageInfo {
url: string;
title: string | undefined;
location: string | undefined;
}
export interface AnalyticsPageViewEvent extends BaseAnalyticsEvent {
page: AnalyticsPageInfo;
}
export interface AnalyticsTrackEvent extends BaseAnalyticsEvent {
event: {
name: string;
properties?: Record<string, string>;
};
}
-66
View File
@@ -1,66 +0,0 @@
{
"name": "@wxt-dev/analytics",
"version": "0.4.0",
"description": "Add analytics to your web extension",
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git",
"directory": "packages/analytics"
},
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./module": {
"types": "./dist/module.d.mts",
"default": "./dist/module.mjs"
},
"./background-plugin": {
"types": "./dist/background-plugin.d.mts",
"default": "./dist/background-plugin.mjs"
},
"./types": {
"types": "./dist/types.d.mts"
},
"./providers/google-analytics-4": {
"types": "./dist/providers/google-analytics-4.d.mts",
"default": "./dist/providers/google-analytics-4.mjs"
},
"./providers/umami": {
"types": "./dist/providers/umami.d.mts",
"default": "./dist/providers/umami.mjs"
}
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"dev": "buildc --deps-only -- wxt",
"dev:build": "buildc --deps-only -- wxt build",
"check": "pnpm build && check",
"build": "buildc -- unbuild",
"prepack": "pnpm -s build",
"prepare": "buildc --deps-only -- wxt prepare"
},
"peerDependencies": {
"wxt": ">=0.19.23"
},
"devDependencies": {
"@aklinker1/check": "^1.3.1",
"@types/chrome": "^0.0.268",
"@types/ua-parser-js": "^0.7.39",
"prettier": "^3.3.2",
"publint": "^0.2.8",
"typescript": "^5.5.2",
"unbuild": "^2.0.0",
"wxt": "workspace:*"
},
"dependencies": {
"ua-parser-js": "^1.0.38"
}
}
View File
-10
View File
@@ -1,10 +0,0 @@
{
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
"compilerOptions": {
"paths": {
"#analytics": ["./.wxt/analytics/index.ts"]
},
"types": ["chrome"]
},
"exclude": ["node_modules", "dist"]
}
-11
View File
@@ -1,11 +0,0 @@
import { defineConfig } from 'wxt';
export default defineConfig({
// Unimport doesn't look for imports in node_modules, so when developing a
// WXT module, we need to disable this to simplify the build process
imports: false,
manifest: {
name: 'Analytics Demo',
},
});
+1
View File
@@ -354,6 +354,7 @@ You'll need to configure it the extension so it knows where your localization fi
# You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers
languageIds:
- typescript
- typescriptreact
# Look for t("...")
usageMatchRegex:
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## v2.0.3
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.2...module-svelte-v2.0.3)
### 🩹 Fixes
- Upgrade `vite-plugin-svelte` to support Vite 6 ([#1375](https://github.com/wxt-dev/wxt/pull/1375))
### ❤️ Contributors
- Eli ([@lishaduck](http://github.com/lishaduck))
## v2.0.2
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.1...module-svelte-v2.0.2)
+2 -2
View File
@@ -17,7 +17,7 @@
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"version": "2.0.2",
"version": "2.0.3",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
@@ -46,7 +46,7 @@
"svelte": ">=5"
},
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0"
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0"
},
"devDependencies": {
"@aklinker1/check": "^1.4.5",
+17
View File
@@ -1,5 +1,22 @@
# Changelog
## v1.1.0
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.0.1...storage-v1.1.0)
### 🚀 Enhancements
- Add `storage.clear` ([#1368](https://github.com/wxt-dev/wxt/pull/1368))
### 📖 Documentation
- Update link ([654a54a](https://github.com/wxt-dev/wxt/commit/654a54a))
### ❤️ Contributors
- Chengxi ([@chengxilo](http://github.com/chengxilo))
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v1.0.1
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.0.0...storage-v1.0.1)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/storage",
"description": "Web extension storage API provided by WXT, supports all browsers.",
"version": "1.0.1",
"version": "1.1.0",
"type": "module",
"repository": {
"type": "git",
@@ -489,6 +489,18 @@ describe('Storage Utils', () => {
});
});
describe('clear', () => {
it('should remove all items', async () => {
await fakeBrowser.storage[storageArea].set({
one: 1,
two: 2,
});
await storage.clear(storageArea);
expect(await fakeBrowser.storage[storageArea].get()).toEqual({});
});
});
describe('removeMeta', () => {
it('should remove all metadata', async () => {
await fakeBrowser.storage[storageArea].set({ count$: { v: 4 } });
+15 -1
View File
@@ -326,6 +326,10 @@ function createStorage(): WxtStorage {
}),
);
},
clear: async (base) => {
const driver = getDriver(base);
await driver.clear();
},
removeMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await removeMeta(driver, driverKey, properties);
@@ -534,6 +538,9 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
removeItems: async (keys) => {
await getStorageArea().remove(keys);
},
clear: async () => {
await getStorageArea().clear();
},
snapshot: async () => {
return await getStorageArea().get();
},
@@ -674,6 +681,12 @@ export interface WxtStorage {
| { item: WxtStorageItem<any, any>; options?: RemoveItemOptions }
>,
): Promise<void>;
/**
* Removes all items from the provided storage area.
*/
clear(base: StorageArea): Promise<void>;
/**
* Remove the entire metadata for a key, or specific properties by name.
*
@@ -712,7 +725,7 @@ export interface WxtStorage {
/**
* Define a storage item with a default value, type, or versioning.
*
* Read full docs: https://wxt.dev/guide/extension-apis/storage.html#defining-storage-items
* Read full docs: https://wxt.dev/storage.html#defining-storage-items
*/
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
@@ -730,6 +743,7 @@ interface WxtStorageDriver {
setItems(values: Array<{ key: string; value: any }>): Promise<void>;
removeItem(key: string): Promise<void>;
removeItems(keys: string[]): Promise<void>;
clear(): Promise<void>;
snapshot(): Promise<Record<string, unknown>>;
restoreSnapshot(data: Record<string, unknown>): Promise<void>;
watch<T>(key: string, cb: WatchCallback<T | null>): Unwatch;
+3 -3
View File
@@ -38,10 +38,10 @@ The module can be configured via the `unocss` config:
export default defineConfig({
modules: ['@wxt-dev/unocss'],
unocss: {
// Will only apply unocss for popup/main.ts
entrypoints: ['popup/main.ts'],
// Exclude unocss from running for the background
excludeEntrypoints: ['background'],
},
});
```
Options have JSDocs available in your editor, or you can read them in the source code: [`UnoCSSOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/src/index.ts).
Options have JSDocs available in your editor, or you can read them in the source code: [`UnoCSSOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/unocss/src/index.ts).
+45 -1
View File
@@ -1,5 +1,49 @@
# Changelog
## v0.19.26
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.25...wxt-v0.19.26)
### 🩹 Fixes
- **context:** Deduplicate `wxt:content-script-started` ([#1364](https://github.com/wxt-dev/wxt/pull/1364))
### ❤️ Contributors
- Deniz Uğur ([@DenizUgur](http://github.com/DenizUgur))
## v0.19.25
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.24...wxt-v0.19.25)
### 🩹 Fixes
- Drop support for vite 6.0.9+ until websocket fix is finished ([8e2badc](https://github.com/wxt-dev/wxt/commit/8e2badc))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v0.19.24
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.23...wxt-v0.19.24)
### 🩹 Fixes
- **init:** Remove "experimental" from bun package manager ([b150a52](https://github.com/wxt-dev/wxt/commit/b150a52))
- **web-ext:** Correctly pass `browserConsole` option to open devtools in Firefox ([#1308](https://github.com/wxt-dev/wxt/pull/1308))
- `autoMount` not working if anchor is already present ([#1350](https://github.com/wxt-dev/wxt/pull/1350))
### 🏡 Chore
- Fix typo in CLI docs ([028c601](https://github.com/wxt-dev/wxt/commit/028c601))
### ❤️ Contributors
- 1natsu ([@1natsu172](http://github.com/1natsu172))
- Paulo Pinto ([@psrpinto](https://github.com/psrpinto))
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v0.19.23
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.22...wxt-v0.19.23)
@@ -2775,4 +2819,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
### 🤖 CI
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.19.23",
"version": "0.19.26",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -123,7 +123,7 @@
"publish-browser-extension": "^2.2.2",
"scule": "^1.3.0",
"unimport": "^3.13.1",
"vite": "^5.0.0 || ^6.0.0",
"vite": "^5.0.0 || <=6.0.8",
"vite-node": "^2.1.4",
"web-ext-run": "^0.2.1",
"webextension-polyfill": "^0.12.0"
@@ -42,6 +42,7 @@ export class ContentScriptContext implements AbortController {
private isTopFrame = window.self === window.top;
private abortController: AbortController;
private locationWatcher = createLocationWatcher(this);
private receivedMessageIds = new Set<string>();
constructor(
private readonly contentScriptName: string,
@@ -233,19 +234,28 @@ export class ContentScriptContext implements AbortController {
{
type: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
contentScriptName: this.contentScriptName,
messageId: Math.random().toString(36).slice(2),
},
'*',
);
}
verifyScriptStartedEvent(event: MessageEvent) {
const isScriptStartedEvent =
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE;
const isSameContentScript =
event.data?.contentScriptName === this.contentScriptName;
const isNotDuplicate = !this.receivedMessageIds.has(event.data?.messageId);
return isScriptStartedEvent && isSameContentScript && isNotDuplicate;
}
listenForNewerScripts(options?: { ignoreFirstEvent?: boolean }) {
let isFirst = true;
const cb = (event: MessageEvent) => {
if (
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE &&
event.data?.contentScriptName === this.contentScriptName
) {
if (this.verifyScriptStartedEvent(event)) {
this.receivedMessageIds.add(event.data.messageId);
const wasFirst = isFirst;
isFirst = false;
if (wasFirst && options?.ignoreFirstEvent) return;
@@ -546,6 +546,25 @@ describe('Content Script UIs', () => {
] as const)(
'built-in UI type: $name',
({ name, createUiFunction, uiSelector }) => {
it('should mount if an anchor already exists at the initialization', async () => {
const onMount = vi.fn(appendTestApp);
ui = await createUiFunction(ctx, {
position: 'inline',
onMount,
anchor: `#parent > #${DYNAMIC_CHILD_ID}`,
page: name === 'iframe' ? '/page.html' : undefined,
name: 'test-component',
});
appendTestElement({ id: DYNAMIC_CHILD_ID });
ui.autoMount();
await runMicrotasks();
await expect
.poll(() => document.querySelector(uiSelector))
.not.toBeNull();
});
it('should mount when an anchor is dynamically added and unmount when an anchor is removed', async () => {
const onMount = vi.fn(appendTestApp);
const onRemove = vi.fn();
@@ -355,6 +355,11 @@ function autoMountUi(
async function observeElement(selector: string | null | undefined) {
let isAnchorExist = !!getAnchor(options);
// Mount if anchor exists at initialization.
if (isAnchorExist) {
uiCallbacks.mount();
}
while (!abortController.signal.aborted) {
try {
const changedAnchor = await waitElement(selector ?? 'body', {
+4 -46
View File
@@ -83,37 +83,6 @@ importers:
specifier: workspace:*
version: link:packages/wxt
packages/analytics:
dependencies:
ua-parser-js:
specifier: ^1.0.38
version: 1.0.40
devDependencies:
'@aklinker1/check':
specifier: ^1.3.1
version: 1.4.5(typescript@5.6.3)
'@types/chrome':
specifier: ^0.0.268
version: 0.0.268
'@types/ua-parser-js':
specifier: ^0.7.39
version: 0.7.39
prettier:
specifier: ^3.3.2
version: 3.3.3
publint:
specifier: ^0.2.8
version: 0.2.12
typescript:
specifier: ^5.5.2
version: 5.6.3
unbuild:
specifier: ^2.0.0
version: 2.0.0(sass@1.80.7)(typescript@5.6.3)
wxt:
specifier: workspace:*
version: link:../wxt
packages/auto-icons:
dependencies:
defu:
@@ -250,7 +219,7 @@ importers:
packages/module-svelte:
dependencies:
'@sveltejs/vite-plugin-svelte':
specifier: ^4.0.0
specifier: ^4.0.0 || ^5.0.0
version: 4.0.0(svelte@5.1.6)(vite@5.4.11(@types/node@20.17.6)(sass@1.80.7))
svelte:
specifier: '>=5'
@@ -488,7 +457,7 @@ importers:
specifier: ^3.13.1
version: 3.13.1(rollup@4.24.0)(webpack-sources@3.2.3)
vite:
specifier: ^5.0.0 || ^6.0.0
specifier: ^5.0.0 || <=6.0.8
version: 5.4.11(@types/node@20.17.6)(sass@1.80.7)
vite-node:
specifier: ^2.1.4
@@ -2030,9 +1999,6 @@ packages:
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/ua-parser-js@0.7.39':
resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==}
'@types/unist@3.0.2':
resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==}
@@ -4805,10 +4771,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
ua-parser-js@1.0.40:
resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==}
hasBin: true
ufo@1.5.3:
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
@@ -6480,8 +6442,6 @@ snapshots:
'@types/resolve@1.20.2': {}
'@types/ua-parser-js@0.7.39': {}
'@types/unist@3.0.2': {}
'@types/web-bluetooth@0.0.20': {}
@@ -8017,7 +7977,7 @@ snapshots:
is-reference@1.2.1:
dependencies:
'@types/estree': 1.0.5
'@types/estree': 1.0.6
is-reference@3.0.2:
dependencies:
@@ -8871,7 +8831,7 @@ snapshots:
dependencies:
nanoid: 3.3.7
picocolors: 1.1.1
source-map-js: 1.2.0
source-map-js: 1.2.1
postcss@8.4.47:
dependencies:
@@ -9489,8 +9449,6 @@ snapshots:
typescript@5.6.3: {}
ua-parser-js@1.0.40: {}
ufo@1.5.3: {}
ufo@1.5.4: {}
@@ -23,15 +23,6 @@ a:hover {
color: #535bf2;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;