diff --git a/packages/analytics/README.md b/packages/analytics/README.md
new file mode 100644
index 000000000..06f041d7a
--- /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 000000000..99c54acb4
--- /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 000000000..5f3dbc679
--- /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 000000000..eb37be630
--- /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 000000000..6d4726a7f
--- /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 000000000..9e779c04d
--- /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 000000000..faeb567f7
--- /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 000000000..e04bc41eb
--- /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 000000000..7387912a0
--- /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 000000000..c08651030
--- /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 000000000..e69de29bb
diff --git a/packages/analytics/tsconfig.json b/packages/analytics/tsconfig.json
new file mode 100644
index 000000000..47d6ba416
--- /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 000000000..0ebb6402c
--- /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 e290003dd..ebf8573e2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -90,6 +90,33 @@ importers:
specifier: ^2.4.5
version: 2.4.5
+ packages/analytics:
+ devDependencies:
+ '@aklinker1/check':
+ specifier: ^1.3.1
+ version: 1.3.1(typescript@5.5.2)
+ '@types/chrome':
+ specifier: ^0.0.268
+ version: 0.0.268
+ prettier:
+ specifier: ^3.3.2
+ version: 3.3.2
+ publint:
+ specifier: ^0.2.8
+ version: 0.2.8
+ typescript:
+ specifier: ^5.5.2
+ version: 5.5.2
+ unbuild:
+ specifier: ^2.0.0
+ version: 2.0.0(typescript@5.5.2)
+ vite:
+ specifier: ^5.3.1
+ version: 5.3.2(@types/node@20.14.9)
+ wxt:
+ specifier: ^0.18.10
+ version: link:../wxt
+
packages/module-react:
dependencies:
'@vitejs/plugin-react':
@@ -1897,9 +1924,26 @@ packages:
'@babel/types': 7.24.7
dev: false
+ /@types/chrome@0.0.268:
+ resolution: {integrity: sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==}
+ dependencies:
+ '@types/filesystem': 0.0.36
+ '@types/har-format': 1.2.15
+ dev: true
+
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
+ /@types/filesystem@0.0.36:
+ resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==}
+ dependencies:
+ '@types/filewriter': 0.0.33
+ dev: true
+
+ /@types/filewriter@0.0.33:
+ resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==}
+ dev: true
+
/@types/fs-extra@11.0.4:
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
dependencies:
@@ -1907,6 +1951,10 @@ packages:
'@types/node': 20.10.3
dev: true
+ /@types/har-format@1.2.15:
+ resolution: {integrity: sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==}
+ dev: true
+
/@types/http-cache-semantics@4.0.1:
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
dev: false