Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .github/workflows/auto-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: ✨ Auto-label PR

on:
pull_request_target:
types: [opened, synchronized, reopened]

jobs:
update-pr:
name: Update PR
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Gather Info
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});

// Check if PR has assignees
const hasAssignees = pr.assignees && pr.assignees.length > 0;
core.setOutput('has_assignees', hasAssignees);
core.setOutput('author', pr.user.login);

// Get list of changed files
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});

// Find all packages that were modified
const packagesRegex = /^packages\/([^\/]+)\//;
const affectedPackages = new Set();

for (const file of files) {
const match = file.filename.match(packagesRegex);
if (match) {
affectedPackages.add(match[1]);
}
}

const labels = Array.from(affectedPackages).map(pkg => `pkg/${pkg}`);
core.setOutput('labels', JSON.stringify(labels));
console.log('Detected package labels:', labels);

// Get current labels on the PR that match pkg/* pattern
const currentPkgLabels = pr.labels
.map(label => label.name)
.filter(name => name.startsWith('pkg/'));

core.setOutput('current_pkg_labels', JSON.stringify(currentPkgLabels));
console.log('Current pkg labels:', currentPkgLabels);

- name: Sync Author
if: steps.check.outputs.has_assignees == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
assignees: ['${{ steps.check.outputs.author }}']
});

console.log('Assigned PR author: ${{ steps.check.outputs.author }}');

- name: Sync Labels
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const newLabels = ${{ steps.check.outputs.labels }};
const currentLabels = ${{ steps.check.outputs.current_pkg_labels }};

// Find labels to add (in newLabels but not in currentLabels)
const labelsToAdd = newLabels.filter(label => !currentLabels.includes(label));

// Find labels to remove (in currentLabels but not in newLabels)
const labelsToRemove = currentLabels.filter(label => !newLabels.includes(label));

// Add new labels
if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labelsToAdd
});
console.log('Added labels:', labelsToAdd);
}

// Remove obsolete labels
for (const label of labelsToRemove) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
name: label
});
console.log('Removed label:', label);
}

if (labelsToAdd.length === 0 && labelsToRemove.length === 0) {
console.log('No label changes needed');
}
56 changes: 56 additions & 0 deletions docs/guide/essentials/wxt-modules.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
outline: deep
---

# WXT Modules

WXT provides a "module system" that let's you run code at different steps in the build process to modify it.
Expand Down Expand Up @@ -137,6 +141,58 @@ console.log(config.myModule);

This is very useful when [generating runtime code](#generate-runtime-module).

#### Add custom entrypoint options

Modules can add custom options to entrypoints by augmenting the entrypoint options types. This allows you to add custom configuration that can be accessed during the build process.

```ts
import { defineWxtModule } from 'wxt/modules';
import 'wxt';

declare module 'wxt' {
export interface BackgroundEntrypointOptions {
// Add custom options to the background entrypoint
myCustomOption?: string;
}
}

export default defineWxtModule({
setup(wxt) {
wxt.hook('entrypoints:resolved', (_, entrypoints) => {
const background = entrypoints.find((e) => e.type === 'background');
if (background) {
console.log('Custom option:', background.options.myCustomOption);
}
});
},
});
```

Now users can set the custom option in their entrypoint:

```ts [entrypoints/background.ts]
export default defineBackground({
myCustomOption: 'custom value',
main() {
// ...
},
});
```

This works for all other JS and HTML entrypoints, here's an example of how to pass a custom option from an HTML file.

```html [entrypoints/popup.html]
<html>
<head>
<meta name="wxt.myHtmlOption" content="custom value" />
<title>Popup</title>
</head>
<body>
<!-- ... -->
</body>
</html>
```

#### Generate output file

```ts
Expand Down
55 changes: 36 additions & 19 deletions packages/analytics/modules/analytics/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,30 @@ import { UAParser } from 'ua-parser-js';
import type {
Analytics,
AnalyticsConfig,
AnalyticsEventMetadata,
AnalyticsPageViewEvent,
AnalyticsProvider,
AnalyticsStorageItem,
AnalyticsTrackEvent,
BaseAnalyticsEvent,
AnalyticsEventMetadata,
AnalyticsProvider,
} from './types';
import { browser } from '@wxt-dev/browser';

type AnalyticsMessage = {
[K in keyof Analytics]: {
fn: K;
args: Parameters<Analytics[K]>;
};
}[keyof Analytics];

type AnalyticsMethod =
| ((...args: Parameters<Analytics[keyof Analytics]>) => void)
| undefined;

type MethodForwarder = <K extends keyof Analytics>(
fn: K,
) => (...args: Parameters<Analytics[K]>) => void;

const ANALYTICS_PORT = '@wxt-dev/analytics';

const INTERACTIVE_TAGS = new Set([
Expand Down Expand Up @@ -163,7 +178,7 @@ function createBackgroundAnalytics(
},
track: async (
eventName: string,
eventProperties?: Record<string, string>,
eventProperties?: Record<string, string | undefined>,
meta: AnalyticsEventMetadata = getBackgroundMeta(),
) => {
const baseEvent = await getBaseEvent(meta);
Expand Down Expand Up @@ -197,9 +212,8 @@ function createBackgroundAnalytics(
// Listen for messages from the rest of the extension
browser.runtime.onConnect.addListener((port) => {
if (port.name === ANALYTICS_PORT) {
port.onMessage.addListener(({ fn, args }) => {
// @ts-expect-error: Untyped fn key
void analytics[fn]?.(...args);
port.onMessage.addListener(({ fn, args }: AnalyticsMessage) => {
void (analytics[fn] as AnalyticsMethod)?.(...args);
});
}
});
Expand All @@ -217,17 +231,15 @@ function createFrontendAnalytics(): Analytics {
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,
referrer: document.referrer || undefined,
screen: `${window.screen.width}x${window.screen.height}`,
url: location.href,
title: document.title || undefined,
});

const methodForwarder =
(fn: string) =>
(...args: any[]) => {
const methodForwarder: MethodForwarder =
(fn) =>
(...args) => {
port.postMessage({ fn, args: [...args, getFrontendMetadata()] });
};

Expand All @@ -238,11 +250,11 @@ function createFrontendAnalytics(): Analytics {
setEnabled: methodForwarder('setEnabled'),
autoTrack: (root) => {
const onClick = (event: Event) => {
const element = event.target as any;
const element = event.target as HTMLElement | null;
if (
!element ||
(!INTERACTIVE_TAGS.has(element.tagName) &&
!INTERACTIVE_ROLES.has(element.getAttribute('role')))
!INTERACTIVE_ROLES.has(element.getAttribute('role') ?? ''))
)
return;

Expand All @@ -251,7 +263,7 @@ function createFrontendAnalytics(): Analytics {
id: element.id || undefined,
className: element.className || undefined,
textContent: element.textContent?.substring(0, 50) || undefined, // Limit text content length
href: element.href,
href: (element as HTMLAnchorElement).href,
});
};
root.addEventListener('click', onClick, { capture: true, passive: true });
Expand All @@ -263,13 +275,18 @@ function createFrontendAnalytics(): Analytics {
return analytics;
}

function defineStorageItem<T>(key: string): AnalyticsStorageItem<T | undefined>;
function defineStorageItem<T>(
key: string,
defaultValue?: NonNullable<T>,
): AnalyticsStorageItem<T> {
defaultValue: T,
): AnalyticsStorageItem<T>;
function defineStorageItem(
key: string,
defaultValue?: unknown,
): AnalyticsStorageItem<unknown> {
return {
getValue: async () =>
(await browser.storage.local.get<Record<string, any>>(key))[key] ??
(await browser.storage.local.get<Record<string, unknown>>(key))[key] ??
defaultValue,
setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),
};
Expand Down
9 changes: 6 additions & 3 deletions packages/analytics/modules/analytics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ export interface Analytics {
/** Report a page change. */
page: (url: string) => void;
/** Report a custom event. */
track: (eventName: string, eventProperties?: Record<string, string>) => void;
track: (
eventName: string,
eventProperties?: Record<string, string | undefined>,
) => 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. */
Expand Down Expand Up @@ -32,7 +35,7 @@ export interface AnalyticsConfig {
/**
* Configure how the user Id is persisted. Defaults to using `browser.storage.local`.
*/
userId?: AnalyticsStorageItem<string>;
userId?: AnalyticsStorageItem<string | undefined>;
/**
* Configure how user properties are persisted. Defaults to using `browser.storage.local`.
*/
Expand Down Expand Up @@ -94,6 +97,6 @@ export interface AnalyticsPageViewEvent extends BaseAnalyticsEvent {
export interface AnalyticsTrackEvent extends BaseAnalyticsEvent {
event: {
name: string;
properties?: Record<string, string>;
properties?: Record<string, string | undefined>;
};
}
11 changes: 11 additions & 0 deletions packages/wxt-demo/src/entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ export default defineBackground({
storage.setItem('session:startTime', Date.now());
},
});

function _otherTypeChecksNotEvaluated() {
browser.scripting.executeScript({
target: { tabId: 1 },
files: [
'/background.js',
// @ts-expect-error: Should error for non-existing paths
'/other.js',
],
});
}
2 changes: 1 addition & 1 deletion packages/wxt/e2e/tests/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestProject } from '../utils';
import { WxtHooks } from '../../src/types';
import type { WxtHooks } from '../../src';

const hooks: WxtHooks = {
ready: vi.fn(),
Expand Down
4 changes: 2 additions & 2 deletions packages/wxt/e2e/tests/modules.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect, vi } from 'vitest';
import { TestProject } from '../utils';
import type { GenericEntrypoint, InlineConfig } from '../../src/types';
import type { GenericEntrypoint, InlineConfig } from '../../src';
import { readFile } from 'fs-extra';
import { normalizePath } from '../../src/core/utils/paths';
import { normalizePath } from '../../src';

describe('Module Helpers', () => {
describe('options', () => {
Expand Down
23 changes: 23 additions & 0 deletions packages/wxt/e2e/tests/typescript-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,29 @@ describe('TypeScript Project', () => {
`);
});

it('should include CSS entrypoints in browser.runtime.getURL paths', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html', '<html></html>');
project.addFile(
'entrypoints/plain.css',
`body {
color: red;
}`,
);
project.addFile(
'entrypoints/overlay.content.css',
`body {
color: blue;
}`,
);

await project.prepare();

const output = await project.serializeFile('.wxt/types/paths.d.ts');
expect(output).toContain('| "/plain.css"');
expect(output).toContain('| "/content-scripts/overlay.css"');
});

it('should augment the types for browser.i18n.getMessage', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html', '<html></html>');
Expand Down
Loading
Loading