Skip to content
Draft
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
66 changes: 12 additions & 54 deletions packages/wxt/src/utils/content-script-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,14 @@ import { createLocationWatcher } from './internal/location-watcher';
* ```
*/
export class ContentScriptContext implements AbortController {
private static SCRIPT_STARTED_MESSAGE_TYPE = getUniqueEventName(
'wxt:content-script-started',
);

private isTopFrame = window.self === window.top;
private abortController: AbortController;
private locationWatcher = createLocationWatcher(this);
private receivedMessageIds = new Set<string>();

constructor(
private readonly contentScriptName: string,
public readonly options?: Omit<ContentScriptDefinition, 'main'>,
) {
this.abortController = new AbortController();

if (this.isTopFrame) {
this.listenForNewerScripts({ ignoreFirstEvent: true });
this.stopOldScripts();
} else {
this.listenForNewerScripts();
}
}

get signal() {
Expand Down Expand Up @@ -221,8 +208,19 @@ export class ContentScriptContext implements AbortController {
if (this.isValid) this.locationWatcher.run();
}

const eventType = type.startsWith('wxt:') ? getUniqueEventName(type) : type

target.addEventListener?.(
eventType,
() => this.isInvalid, // signals the abort controller when invalid
Copy link

Copilot AI Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first addEventListener call is registering a function that returns a boolean as an event handler, which is incorrect. Event handlers should be functions that handle events, not functions that return boolean values.

Suggested change
() => this.isInvalid, // signals the abort controller when invalid
(event) => {
if (this.isInvalid) {
this.abortController.abort(); // Abort the controller if invalid
}
},

Copilot uses AI. Check for mistakes.
{
capture: true,
signal: this.signal,
},
);

target.addEventListener?.(
type.startsWith('wxt:') ? getUniqueEventName(type) : type,
eventType,
handler,
{
...options,
Comment on lines +211 to 226
Copy link

Copilot AI Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The eventType variable is calculated but then the same logic is duplicated by having two separate addEventListener calls. This creates unnecessary complexity and potential for inconsistency.

Copilot uses AI. Check for mistakes.
Expand All @@ -241,46 +239,6 @@ export class ContentScriptContext implements AbortController {
`Content script "${this.contentScriptName}" context invalidated`,
);
}

stopOldScripts() {
// Use postMessage so it get's sent to all the frames of the page.
window.postMessage(
{
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 (this.verifyScriptStartedEvent(event)) {
this.receivedMessageIds.add(event.data.messageId);

const wasFirst = isFirst;
isFirst = false;
if (wasFirst && options?.ignoreFirstEvent) return;

this.notifyInvalidated();
}
};

addEventListener('message', cb);
this.onInvalidated(() => removeEventListener('message', cb));
}
}

export interface WxtWindowEventMap extends WindowEventMap {
Expand Down