diff --git a/packages/analytics/README.md b/packages/analytics/README.md
new file mode 100644
index 00000000..06f041d7
--- /dev/null
+++ b/packages/analytics/README.md
@@ -0,0 +1,65 @@
+
+
+# My Module
+
+My new WXT module for doing amazing things.
+
+## Features
+
+
+
+- ⛰ Foo
+- 🚠 Bar
+- 🌲 Baz
+
+## Installation
+
+Install the module to your WXT extension with one command:
+
+```bash
+pnpm i my-module
+```
+
+Then add the module to your `wxt.config.ts` file:
+
+```ts
+export default defineConfig({
+ modules: ['my-module'],
+});
+```
+
+That's it! You can now use My Module in your WXT extension ✨
+
+## Contribution
+
+
+ Local development
+
+```bash
+# Install dependencies
+pnpm install
+
+# Generate type stubs
+pnpm wxt prepare
+
+# Develop test extension
+pnpm dev
+
+# Build the test extension
+pnpm dev:build
+
+# Run prettier, publint, and type checks
+pnpm check
+```
+
+
diff --git a/packages/analytics/app.config.ts b/packages/analytics/app.config.ts
new file mode 100644
index 00000000..99c54acb
--- /dev/null
+++ b/packages/analytics/app.config.ts
@@ -0,0 +1,21 @@
+import { googleAnalytics } from './modules/analytics/providers/google-analytics';
+import { AnalyticsConfig } from './modules/analytics/types';
+
+interface AppConfig {
+ analytics: AnalyticsConfig;
+}
+function defineAppConfig(config: AppConfig): AppConfig {
+ return config;
+}
+
+export default defineAppConfig({
+ analytics: {
+ providers: [
+ googleAnalytics({
+ apiSecret: '...',
+ measurementId: '...',
+ }),
+ ],
+ debug: true,
+ },
+});
diff --git a/packages/analytics/build.config.ts b/packages/analytics/build.config.ts
new file mode 100644
index 00000000..5f3dbc67
--- /dev/null
+++ b/packages/analytics/build.config.ts
@@ -0,0 +1,33 @@
+import { defineBuildConfig } from 'unbuild';
+import * as vite from 'vite';
+import { resolve } from 'node:path';
+
+// Build module and plugins
+export default defineBuildConfig({
+ rootDir: 'modules/my-module',
+ outDir: resolve(__dirname, 'dist'),
+ entries: ['index.ts', 'plugin.ts'],
+ replace: {
+ 'process.env.NPM': 'true',
+ },
+ declaration: true,
+ hooks: {
+ 'build:done': prebuildEntrypoints,
+ },
+});
+
+// Prebuild entrypoints
+async function prebuildEntrypoints() {
+ await vite.build({
+ root: 'modules/my-module',
+ build: {
+ emptyOutDir: false,
+ rollupOptions: {
+ input: 'modules/my-module/example.html',
+ output: {
+ dir: 'dist/prebuilt',
+ },
+ },
+ },
+ });
+}
diff --git a/packages/analytics/entrypoints/popup.html b/packages/analytics/entrypoints/popup.html
new file mode 100644
index 00000000..eb37be63
--- /dev/null
+++ b/packages/analytics/entrypoints/popup.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Popup
+
+
+
+
+
+
diff --git a/packages/analytics/modules/analytics/client.ts b/packages/analytics/modules/analytics/client.ts
new file mode 100644
index 00000000..6d4726a7
--- /dev/null
+++ b/packages/analytics/modules/analytics/client.ts
@@ -0,0 +1,27 @@
+import { defineWxtPlugin } from 'wxt/sandbox';
+import { Analytics } from './types';
+
+export let analytics: Analytics;
+
+export default defineWxtPlugin(() => {
+ const isBackground = globalThis.window == null; // TODO: Support MV2
+ analytics = isBackground
+ ? createBackgroundAnalytics()
+ : createAnalyticsForwarder();
+});
+
+function createBackgroundAnalytics(): Analytics {
+ return {
+ identify: () => {},
+ page: () => {},
+ track: () => {},
+ };
+}
+
+function createAnalyticsForwarder(): Analytics {
+ return {
+ identify: () => {},
+ page: () => {},
+ track: () => {},
+ };
+}
diff --git a/packages/analytics/modules/analytics/index.ts b/packages/analytics/modules/analytics/index.ts
new file mode 100644
index 00000000..9e779c04
--- /dev/null
+++ b/packages/analytics/modules/analytics/index.ts
@@ -0,0 +1,19 @@
+import 'wxt';
+import { addWxtPlugin, defineWxtModule } from 'wxt/modules';
+import { resolve } from 'node:path';
+
+const pluginId = process.env.NPM
+ ? 'analytics/client'
+ : resolve(__dirname, 'client.ts');
+
+export default defineWxtModule({
+ name: 'analytics',
+ imports: [{ name: 'analytics', from: pluginId }],
+ setup(wxt, options) {
+ // Add a plugin
+ addWxtPlugin(
+ wxt,
+ resolve(__dirname, process.env.NPM ? 'plugin.mjs' : 'plugin.ts'),
+ );
+ },
+});
diff --git a/packages/analytics/modules/analytics/providers/google-analytics.ts b/packages/analytics/modules/analytics/providers/google-analytics.ts
new file mode 100644
index 00000000..faeb567f
--- /dev/null
+++ b/packages/analytics/modules/analytics/providers/google-analytics.ts
@@ -0,0 +1,13 @@
+import type { AnalyticsProvider } from '../types';
+
+export interface GoogleAnalyticsProviderOptions {}
+
+export const googleAnalytics =
+ (options: GoogleAnalyticsProviderOptions): AnalyticsProvider =>
+ (analytics, config) => {
+ return {
+ identify: async () => {},
+ page: async () => {},
+ track: async () => {},
+ };
+ };
diff --git a/packages/analytics/modules/analytics/providers/umami.ts b/packages/analytics/modules/analytics/providers/umami.ts
new file mode 100644
index 00000000..e04bc41e
--- /dev/null
+++ b/packages/analytics/modules/analytics/providers/umami.ts
@@ -0,0 +1,5 @@
+export interface UmamiProviderOptions {}
+
+export const umami = (options: UmamiProviderOptions) => (analytics, config) => {
+ throw Error('TODO');
+};
diff --git a/packages/analytics/modules/analytics/types.ts b/packages/analytics/modules/analytics/types.ts
new file mode 100644
index 00000000..7387912a
--- /dev/null
+++ b/packages/analytics/modules/analytics/types.ts
@@ -0,0 +1,50 @@
+export interface Analytics {
+ /** Report a page change */
+ page: (url: string | URL) => void;
+ /** Report a custom event */
+ track: (eventName: string, eventProperties: string) => void;
+ /** Save information about the user */
+ identify: (userId: string, userProperties?: Record) => 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;
+}
+
+export type AnalyticsProvider = (
+ analytics: Analytics,
+ config: AnalyticsConfig,
+) => {
+ /** Upload a page view event */
+ page: (event: AnalyticsPageViewEvent) => Promise;
+ /** Upload a custom event */
+ track: (event: AnalyticsTrackEvent) => Promise;
+ /** Upload or save information about the user */
+ identify: (event: AnalyticsIdentifyEvent) => Promise;
+};
+
+export interface BaseAnalyticsEvent {
+ /** Identifier of the session the event was fired from */
+ sessionId: string;
+ /** `Date.now()` of when the event was reported */
+ time: number;
+}
+
+export interface AnalyticsPageViewEvent extends BaseAnalyticsEvent {
+ url: string;
+ sessionId: string;
+ title: string;
+ location: string;
+}
+
+export interface AnalyticsTrackEvent extends BaseAnalyticsEvent {
+ eventName: string;
+ eventProperties: Record;
+}
+
+export interface AnalyticsIdentifyEvent extends BaseAnalyticsEvent {}
diff --git a/packages/analytics/package.json b/packages/analytics/package.json
new file mode 100644
index 00000000..c0865103
--- /dev/null
+++ b/packages/analytics/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "my-module",
+ "version": "1.0.0",
+ "description": "My new WXT module",
+ "repository": "your-org/my-module",
+ "license": "MIT",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "./plugin": {
+ "types": "./dist/plugin.d.mts",
+ "default": "./dist/plugin.mjs"
+ }
+ },
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "dev": "wxt",
+ "dev:build": "wxt build",
+ "check": "check",
+ "build": "unbuild",
+ "prepack": "unbuild",
+ "postinstall": "wxt prepare"
+ },
+ "peerDependencies": {
+ "wxt": ">=0.18.10"
+ },
+ "devDependencies": {
+ "@aklinker1/check": "^1.3.1",
+ "@types/chrome": "^0.0.268",
+ "prettier": "^3.3.2",
+ "publint": "^0.2.8",
+ "typescript": "^5.5.2",
+ "unbuild": "^2.0.0",
+ "vite": "^5.3.1",
+ "wxt": "^0.18.10"
+ }
+}
diff --git a/packages/analytics/public/.keep b/packages/analytics/public/.keep
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/analytics/tsconfig.json b/packages/analytics/tsconfig.json
new file mode 100644
index 00000000..47d6ba41
--- /dev/null
+++ b/packages/analytics/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
+ "compilerOptions": {
+ "types": ["chrome"]
+ }
+}
diff --git a/packages/analytics/wxt.config.ts b/packages/analytics/wxt.config.ts
new file mode 100644
index 00000000..0ebb6402
--- /dev/null
+++ b/packages/analytics/wxt.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'wxt';
+
+export default defineConfig({
+ vite: () => ({
+ define: {
+ 'process.env.NPM': 'false',
+ },
+ }),
+ myModule: {
+ example: 'options',
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 602ac57c..673250ef 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -84,6 +84,33 @@ importers:
specifier: ^2.5.0
version: 2.5.0
+ packages/analytics:
+ devDependencies:
+ '@aklinker1/check':
+ specifier: ^1.3.1
+ version: 1.4.5(typescript@5.5.4)
+ '@types/chrome':
+ specifier: ^0.0.268
+ version: 0.0.268
+ prettier:
+ specifier: ^3.3.2
+ version: 3.3.3
+ publint:
+ specifier: ^0.2.8
+ version: 0.2.9
+ typescript:
+ specifier: ^5.5.2
+ version: 5.5.4
+ unbuild:
+ specifier: ^2.0.0
+ version: 2.0.0(sass@1.77.8)(typescript@5.5.4)
+ vite:
+ specifier: ^5.3.1
+ version: 5.3.5(@types/node@20.14.12)(sass@1.77.8)
+ wxt:
+ specifier: ^0.18.10
+ version: 0.18.15(@types/node@20.14.12)(rollup@4.19.0)(sass@1.77.8)
+
packages/auto-icons:
dependencies:
defu:
@@ -1913,6 +1940,9 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ async-mutex@0.4.1:
+ resolution: {integrity: sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==}
+
async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
@@ -3553,6 +3583,10 @@ packages:
resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ ora@7.0.1:
+ resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==}
+ engines: {node: '>=16'}
+
ora@8.1.0:
resolution: {integrity: sha512-GQEkNkH/GHOhPFXcqZs3IDahXEQcQxsSjEkK4KvEEST4t7eNzoMjxTzef+EZ+JluDEV+Raoi3WQ2CflnRdSVnQ==}
engines: {node: '>=18'}
@@ -4279,6 +4313,10 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
+ string-width@6.1.0:
+ resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==}
+ engines: {node: '>=16'}
+
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
@@ -4742,6 +4780,9 @@ packages:
resolution: {integrity: sha512-5D11VcjdGkA1/xax5UWL0YeAbDySKHzWFe6EpsoPNUMw5Uk9tKk9p6GUOfcaI5N7sINKfBMZYNsTBiu5dzJB9A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ webextension-polyfill@0.10.0:
+ resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==}
+
webextension-polyfill@0.12.0:
resolution: {integrity: sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==}
@@ -4825,6 +4866,10 @@ packages:
utf-8-validate:
optional: true
+ wxt@0.18.15:
+ resolution: {integrity: sha512-rzFLinUyugBsoC+mJZFMIQILrA4OUk/XG0mAW30zF3pqegIwawrEx5JXFXg/D1XW6//uew/FLTm12t3mcpbw+Q==}
+ hasBin: true
+
xdg-basedir@5.1.0:
resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
engines: {node: '>=12'}
@@ -6173,6 +6218,10 @@ snapshots:
assertion-error@2.0.1: {}
+ async-mutex@0.4.1:
+ dependencies:
+ tslib: 2.6.0
+
async-mutex@0.5.0:
dependencies:
tslib: 2.6.0
@@ -8008,6 +8057,18 @@ snapshots:
strip-ansi: 7.1.0
wcwidth: 1.0.1
+ ora@7.0.1:
+ dependencies:
+ chalk: 5.3.0
+ cli-cursor: 4.0.0
+ cli-spinners: 2.9.2
+ is-interactive: 2.0.0
+ is-unicode-supported: 1.3.0
+ log-symbols: 5.1.0
+ stdin-discarder: 0.1.0
+ string-width: 6.1.0
+ strip-ansi: 7.1.0
+
ora@8.1.0:
dependencies:
chalk: 5.3.0
@@ -8770,6 +8831,12 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.0
+ string-width@6.1.0:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 10.3.0
+ strip-ansi: 7.1.0
+
string-width@7.2.0:
dependencies:
emoji-regex: 10.3.0
@@ -9354,6 +9421,8 @@ snapshots:
- supports-color
- utf-8-validate
+ webextension-polyfill@0.10.0: {}
+
webextension-polyfill@0.12.0: {}
webidl-conversions@7.0.0: {}
@@ -9436,6 +9505,61 @@ snapshots:
ws@8.18.0: {}
+ wxt@0.18.15(@types/node@20.14.12)(rollup@4.19.0)(sass@1.77.8):
+ dependencies:
+ '@aklinker1/rollup-plugin-visualizer': 5.12.0(rollup@4.19.0)
+ '@types/webextension-polyfill': 0.10.7
+ '@webext-core/fake-browser': 1.3.1
+ '@webext-core/isolated-element': 1.1.2
+ '@webext-core/match-patterns': 1.0.3
+ async-mutex: 0.4.1
+ c12: 1.11.1(magicast@0.3.4)
+ cac: 6.7.14
+ chokidar: 3.6.0
+ ci-info: 4.0.0
+ defu: 6.1.4
+ dequal: 2.0.3
+ esbuild: 0.19.12
+ fast-glob: 3.3.2
+ filesize: 10.1.4
+ fs-extra: 11.2.0
+ get-port: 7.1.0
+ giget: 1.2.3
+ hookable: 5.5.3
+ is-wsl: 3.1.0
+ jiti: 1.21.6
+ json5: 2.2.3
+ jszip: 3.10.1
+ linkedom: 0.18.4
+ magicast: 0.3.4
+ minimatch: 9.0.5
+ natural-compare: 1.4.0
+ normalize-path: 3.0.0
+ nypm: 0.3.9
+ ohash: 1.1.3
+ open: 10.1.0
+ ora: 7.0.1
+ picocolors: 1.0.1
+ prompts: 2.4.2
+ publish-browser-extension: 2.1.3
+ unimport: 3.11.1(rollup@4.19.0)
+ vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)
+ vite-node: 1.6.0(@types/node@20.14.12)(sass@1.77.8)
+ web-ext-run: 0.2.1
+ webextension-polyfill: 0.10.0
+ transitivePeerDependencies:
+ - '@types/node'
+ - bufferutil
+ - less
+ - lightningcss
+ - rollup
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - utf-8-validate
+
xdg-basedir@5.1.0: {}
xml2js@0.5.0: