feat: dummy extension that uses background scripts and content scripts

This commit is contained in:
Abhijith Vijayan [FLUXON] 2026-01-03 21:51:18 +05:30
parent e14efb7c4b
commit 8bbcb174d1
6 changed files with 367 additions and 17 deletions

View File

@ -1,12 +1,72 @@
/**
* Background Script (Service Worker in Chrome MV3)
*
* This script runs in the background and acts as a central hub for
* communication between different parts of the extension.
*
* Communication Flow:
*
* BACKGROUND SCRIPT
*
* Content Script PAGE_VISITED Background
* (page loaded)
*
* Increment visitCount
* in browser.storage
*
* Popup GET_VISIT_COUNT Background
*
*
* Read visitCount
* from storage
*
* Popup VISIT_COUNT_RESPONSE
*
*
* Message Types:
* - PAGE_VISITED (incoming from content): A page was visited
* - GET_VISIT_COUNT (incoming from popup): Request for total visit count
* - VISIT_COUNT_RESPONSE (outgoing to popup): Response with visit count
*/
import browser from 'webextension-polyfill';
import {ExtensionMessage} from '../types/messages';
import {ExtensionMessage, VisitCountResponseMessage} from '../types/messages';
import {getStorage, setStorage} from '../utils/storage';
browser.runtime.onInstalled.addListener((): void => {
console.log('extension installed');
console.log('Extension installed');
});
// Listen for messages from popup or content scripts
browser.runtime.onMessage.addListener((message: unknown): void => {
const msg = message as ExtensionMessage;
console.log('Background received message:', msg.type);
});
browser.runtime.onMessage.addListener(
(
message: unknown
): Promise<VisitCountResponseMessage> | undefined => {
const msg = message as ExtensionMessage;
// Content script notifies us when a page is visited
if (msg.type === 'PAGE_VISITED') {
console.log('Page visited:', msg.data.title, '-', msg.data.url);
console.log(
` Words: ${msg.data.wordCount}, Links: ${msg.data.linkCount}, Images: ${msg.data.imageCount}`
);
// Increment visit count
getStorage(['visitCount']).then(({visitCount}) => {
setStorage({visitCount: visitCount + 1});
});
return undefined;
}
// Popup requests the visit count
if (msg.type === 'GET_VISIT_COUNT') {
return getStorage(['visitCount']).then(({visitCount}) => ({
type: 'VISIT_COUNT_RESPONSE',
count: visitCount,
}));
}
return undefined;
}
);

View File

@ -1,16 +1,62 @@
/**
* Content Script
*
* This script is injected into every web page that matches the patterns
* defined in manifest.json's content_scripts section.
*
* Communication Flow:
*
* CONTENT SCRIPT
*
* 1. Page loads Collects page stats Sends PAGE_VISITED to
* background script
*
* 2. Popup requests GET_PAGE_INFO Content script responds with
* PAGE_INFO_RESPONSE containing current page stats
*
*
* Message Types:
* - PAGE_VISITED (outgoing to background): Notify that a page was loaded
* - GET_PAGE_INFO (incoming from popup): Request for current page stats
* - PAGE_INFO_RESPONSE (outgoing to popup): Response with page stats
*/
import browser from 'webextension-polyfill';
import {ExtensionMessage, PongMessage} from '../types/messages';
import {
ExtensionMessage,
PageInfo,
PageInfoResponseMessage,
} from '../types/messages';
import {getStorage} from '../utils/storage';
// Collect page information (word count, links, images)
function getPageInfo(): PageInfo {
const bodyText = document.body?.innerText || '';
const wordCount = bodyText
.split(/\s+/)
.filter((word) => word.length > 0).length;
const linkCount = document.querySelectorAll('a').length;
const imageCount = document.querySelectorAll('img').length;
return {
url: window.location.href,
title: document.title,
wordCount,
linkCount,
imageCount,
timestamp: Date.now(),
};
}
// Listen for messages from popup or background
browser.runtime.onMessage.addListener(
(message: unknown): Promise<PongMessage> | undefined => {
(message: unknown): Promise<PageInfoResponseMessage> | undefined => {
const msg = message as ExtensionMessage;
if (msg.type === 'PING') {
if (msg.type === 'GET_PAGE_INFO') {
return Promise.resolve({
type: 'PONG',
timestamp: Date.now(),
type: 'PAGE_INFO_RESPONSE',
data: getPageInfo(),
});
}
@ -18,6 +64,27 @@ browser.runtime.onMessage.addListener(
}
);
// Notify background script when page loads
function notifyPageVisit(): void {
const pageInfo = getPageInfo();
browser.runtime
.sendMessage({
type: 'PAGE_VISITED',
data: pageInfo,
})
.catch(() => {
// Background script might not be ready yet, ignore error
});
}
// Wait for page to fully load before collecting info
if (document.readyState === 'complete') {
notifyPageVisit();
} else {
window.addEventListener('load', notifyPageVisit);
}
// Log when content script loads (if logging is enabled)
getStorage(['enableLogging']).then(({enableLogging}) => {
if (enableLogging) {

View File

@ -42,3 +42,69 @@
.tabCard {
margin-bottom: 14px;
}
.statsCard {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.statsTitle {
font-size: 13px;
font-weight: variables.$semibold;
color: variables.$skyBlue;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.statItem {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
background: variables.$greyWhite;
border-radius: 8px;
}
.statValue {
font-size: 20px;
font-weight: variables.$bold;
color: variables.$primary;
}
.statLabel {
font-size: 11px;
color: variables.$skyBlue;
margin-top: 4px;
}
.visitCard {
display: flex;
justify-content: space-between;
align-items: center;
background: white;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.visitLabel {
font-size: 13px;
color: variables.$skyBlue;
}
.visitCount {
font-size: 18px;
font-weight: variables.$bold;
color: variables.$primary;
}

View File

@ -1,7 +1,40 @@
/**
* Popup Component
*
* This is the main UI that appears when the user clicks the extension icon.
* It communicates with both the content script and background script.
*
* Communication Flow:
*
* POPUP
*
* On mount:
*
* 1. Popup GET_PAGE_INFO Content Script (via browser.tabs)
* Popup PAGE_INFO_RESPONSE Content Script
* Displays word count, link count, image count
*
* 2. Popup GET_VISIT_COUNT Background Script (via runtime)
* Popup VISIT_COUNT_RESPONSE Background Script
* Displays total pages tracked
*
* 3. Popup browser.storage.local
* Reads username for greeting
*
*
* Note: browser.tabs.sendMessage() sends to content script in specific tab
* browser.runtime.sendMessage() sends to background script
*/
import {useEffect, useState} from 'react';
import type {FC} from 'react';
import browser, {Tabs} from 'webextension-polyfill';
import {getStorage} from '../utils/storage';
import {
PageInfo,
PageInfoResponseMessage,
VisitCountResponseMessage,
} from '../types/messages';
import {TabInfo} from './components/TabInfo/TabInfo';
import {FooterActions} from './components/FooterActions/FooterActions';
import styles from './Popup.module.scss';
@ -18,9 +51,12 @@ interface TabData {
const Popup: FC = () => {
const [tabInfo, setTabInfo] = useState<TabData | null>(null);
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
const [visitCount, setVisitCount] = useState<number>(0);
const [username, setUsername] = useState<string>('');
useEffect(() => {
// Get current tab info
browser.tabs.query({active: true, currentWindow: true}).then((tabs) => {
const tab = tabs[0];
if (tab) {
@ -29,9 +65,38 @@ const Popup: FC = () => {
url: tab.url || 'Unknown',
favIconUrl: tab.favIconUrl,
});
// Request page info from content script
if (tab.id) {
browser.tabs
.sendMessage(tab.id, {type: 'GET_PAGE_INFO'})
.then((response: unknown) => {
const res = response as PageInfoResponseMessage;
if (res?.data) {
setPageInfo(res.data);
}
})
.catch(() => {
// Content script might not be injected on this page
});
}
}
});
// Get visit count from background script
browser.runtime
.sendMessage({type: 'GET_VISIT_COUNT'})
.then((response: unknown) => {
const res = response as VisitCountResponseMessage;
if (res?.count !== undefined) {
setVisitCount(res.count);
}
})
.catch(() => {
// Background script might not be ready
});
// Get username from storage
getStorage(['username']).then(({username: storedUsername}) => {
setUsername(storedUsername);
});
@ -66,6 +131,33 @@ const Popup: FC = () => {
</div>
)}
{/* Page Stats from Content Script */}
{pageInfo && (
<div className={styles.statsCard}>
<h3 className={styles.statsTitle}>Page Stats</h3>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<span className={styles.statValue}>{pageInfo.wordCount}</span>
<span className={styles.statLabel}>Words</span>
</div>
<div className={styles.statItem}>
<span className={styles.statValue}>{pageInfo.linkCount}</span>
<span className={styles.statLabel}>Links</span>
</div>
<div className={styles.statItem}>
<span className={styles.statValue}>{pageInfo.imageCount}</span>
<span className={styles.statLabel}>Images</span>
</div>
</div>
</div>
)}
{/* Visit Count from Background Script */}
<div className={styles.visitCard}>
<span className={styles.visitLabel}>Pages tracked:</span>
<span className={styles.visitCount}>{visitCount}</span>
</div>
<FooterActions
onSettings={(): Promise<Tabs.Tab> =>
openWebPage('/Options/options.html')

View File

@ -1,10 +1,73 @@
export interface PingMessage {
type: 'PING';
}
/**
* Extension Message Types
*
* This file defines all message types used for communication between
* the different parts of the extension.
*
* Overall Communication Architecture:
*
*
* PAGE_VISITED
*
* Content Background
* Script Script
*
*
*
* GET_PAGE_INFO GET_VISIT_COUNT
* PAGE_INFO_RESPONSE VISIT_COUNT_RESPONSE
*
*
*
* Popup
*
*
*
*
*
* Message Flow:
* 1. Content Script Background: PAGE_VISITED (on page load)
* 2. Popup Content Script: GET_PAGE_INFO / PAGE_INFO_RESPONSE
* 3. Popup Background: GET_VISIT_COUNT / VISIT_COUNT_RESPONSE
*/
export interface PongMessage {
type: 'PONG';
// Page info collected by content script
export interface PageInfo {
url: string;
title: string;
wordCount: number;
linkCount: number;
imageCount: number;
timestamp: number;
}
export type ExtensionMessage = PingMessage | PongMessage;
// Messages
export interface GetPageInfoMessage {
type: 'GET_PAGE_INFO';
}
export interface PageInfoResponseMessage {
type: 'PAGE_INFO_RESPONSE';
data: PageInfo;
}
export interface PageVisitedMessage {
type: 'PAGE_VISITED';
data: PageInfo;
}
export interface GetVisitCountMessage {
type: 'GET_VISIT_COUNT';
}
export interface VisitCountResponseMessage {
type: 'VISIT_COUNT_RESPONSE';
count: number;
}
export type ExtensionMessage =
| GetPageInfoMessage
| PageInfoResponseMessage
| PageVisitedMessage
| GetVisitCountMessage
| VisitCountResponseMessage;

View File

@ -1,9 +1,11 @@
export interface StorageSchema {
username: string;
enableLogging: boolean;
visitCount: number;
}
export const defaultStorage: StorageSchema = {
username: '',
enableLogging: false,
visitCount: 0,
};