mirror of
https://github.com/abhijithvijayan/web-extension-starter.git
synced 2026-01-30 09:48:12 +01:00
feat: dummy extension that uses background scripts and content scripts
This commit is contained in:
parent
e14efb7c4b
commit
8bbcb174d1
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
export interface StorageSchema {
|
||||
username: string;
|
||||
enableLogging: boolean;
|
||||
visitCount: number;
|
||||
}
|
||||
|
||||
export const defaultStorage: StorageSchema = {
|
||||
username: '',
|
||||
enableLogging: false,
|
||||
visitCount: 0,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user