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 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 => { browser.runtime.onInstalled.addListener((): void => {
console.log('extension installed'); console.log('Extension installed');
}); });
// Listen for messages from popup or content scripts // Listen for messages from popup or content scripts
browser.runtime.onMessage.addListener((message: unknown): void => { browser.runtime.onMessage.addListener(
const msg = message as ExtensionMessage; (
console.log('Background received message:', msg.type); 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 browser from 'webextension-polyfill';
import {ExtensionMessage, PongMessage} from '../types/messages'; import {
ExtensionMessage,
PageInfo,
PageInfoResponseMessage,
} from '../types/messages';
import {getStorage} from '../utils/storage'; 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 // Listen for messages from popup or background
browser.runtime.onMessage.addListener( browser.runtime.onMessage.addListener(
(message: unknown): Promise<PongMessage> | undefined => { (message: unknown): Promise<PageInfoResponseMessage> | undefined => {
const msg = message as ExtensionMessage; const msg = message as ExtensionMessage;
if (msg.type === 'PING') { if (msg.type === 'GET_PAGE_INFO') {
return Promise.resolve({ return Promise.resolve({
type: 'PONG', type: 'PAGE_INFO_RESPONSE',
timestamp: Date.now(), 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) // Log when content script loads (if logging is enabled)
getStorage(['enableLogging']).then(({enableLogging}) => { getStorage(['enableLogging']).then(({enableLogging}) => {
if (enableLogging) { if (enableLogging) {

View File

@ -42,3 +42,69 @@
.tabCard { .tabCard {
margin-bottom: 14px; 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 {useEffect, useState} from 'react';
import type {FC} from 'react'; import type {FC} from 'react';
import browser, {Tabs} from 'webextension-polyfill'; import browser, {Tabs} from 'webextension-polyfill';
import {getStorage} from '../utils/storage'; import {getStorage} from '../utils/storage';
import {
PageInfo,
PageInfoResponseMessage,
VisitCountResponseMessage,
} from '../types/messages';
import {TabInfo} from './components/TabInfo/TabInfo'; import {TabInfo} from './components/TabInfo/TabInfo';
import {FooterActions} from './components/FooterActions/FooterActions'; import {FooterActions} from './components/FooterActions/FooterActions';
import styles from './Popup.module.scss'; import styles from './Popup.module.scss';
@ -18,9 +51,12 @@ interface TabData {
const Popup: FC = () => { const Popup: FC = () => {
const [tabInfo, setTabInfo] = useState<TabData | null>(null); 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>(''); const [username, setUsername] = useState<string>('');
useEffect(() => { useEffect(() => {
// Get current tab info
browser.tabs.query({active: true, currentWindow: true}).then((tabs) => { browser.tabs.query({active: true, currentWindow: true}).then((tabs) => {
const tab = tabs[0]; const tab = tabs[0];
if (tab) { if (tab) {
@ -29,9 +65,38 @@ const Popup: FC = () => {
url: tab.url || 'Unknown', url: tab.url || 'Unknown',
favIconUrl: tab.favIconUrl, 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}) => { getStorage(['username']).then(({username: storedUsername}) => {
setUsername(storedUsername); setUsername(storedUsername);
}); });
@ -66,6 +131,33 @@ const Popup: FC = () => {
</div> </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 <FooterActions
onSettings={(): Promise<Tabs.Tab> => onSettings={(): Promise<Tabs.Tab> =>
openWebPage('/Options/options.html') 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 { // Page info collected by content script
type: 'PONG'; export interface PageInfo {
url: string;
title: string;
wordCount: number;
linkCount: number;
imageCount: number;
timestamp: 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 { export interface StorageSchema {
username: string; username: string;
enableLogging: boolean; enableLogging: boolean;
visitCount: number;
} }
export const defaultStorage: StorageSchema = { export const defaultStorage: StorageSchema = {
username: '', username: '',
enableLogging: false, enableLogging: false,
visitCount: 0,
}; };