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 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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user