using my boilerplate yuh

This commit is contained in:
Sriram Hariharan
2023-02-22 22:51:38 -06:00
parent 21d7056aae
commit bce2717088
91 changed files with 32400 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
import { MessageListener } from 'chrome-extension-toolkit';
import { BACKGROUND_MESSAGES } from 'src/shared/messages';
import { generateRandomId } from 'src/shared/util/random';
import onHistoryStateUpdated from './events/onHistoryStateUpdated';
import onInstall from './events/onInstall';
import onNewChromeSession from './events/onNewChromeSession';
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
import onUpdate from './events/onUpdate';
import { sessionStore } from '../shared/storage/sessionStore';
import browserActionHandler from './handler/browserActionHandler';
import hotReloadingHandler from './handler/hotReloadingHandler';
import tabManagementHandler from './handler/tabManagementHandler';
onServiceWorkerAlive();
/**
* will be triggered on either install or update
* (will also be triggered on a user's sync'd browsers (on other devices)))
*/
chrome.runtime.onInstalled.addListener(details => {
switch (details.reason) {
case 'install':
onInstall();
break;
case 'update':
onUpdate();
break;
default:
break;
}
});
// This event is fired when any tab's url changes.
chrome.webNavigation.onHistoryStateUpdated.addListener(onHistoryStateUpdated);
// initialize the message listener that will listen for messages from the content script
const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
...browserActionHandler,
...hotReloadingHandler,
...tabManagementHandler,
});
messageListener.listen();
sessionStore.getChromeSessionId().then(async chromeSessionId => {
if (!chromeSessionId) {
await sessionStore.setChromeSessionId(generateRandomId(10));
onNewChromeSession();
}
});

View File

@@ -0,0 +1,11 @@
/**
* This event is fired when any tab's url changes.
* This is useful for content scripts to know when SPA navigations occur.
* @param details
*/
export default function onHistoryStateUpdated(
details: chrome.webNavigation.WebNavigationTransitionCallbackDetails
): void {
const { tabId, url } = details;
// TODO: send a message to tab with tabId to reanalyze the page
}

View File

@@ -0,0 +1,26 @@
import { SECOND } from 'src/shared/util/time';
/**
* Called when the extension is first installed or synced onto a new machine
*/
export default async function onInstall() {
// set the uninstall url
chrome.runtime.setUninstallURL('https://www.google.com');
logOnInstallEvent();
}
/**
* making sure we are not sending duplicate install event for users that have synced browsers
* sync storage get's cleared on browser uninstall, so re-installing the browser will trigger the event
*/
function logOnInstallEvent() {
setTimeout(async () => {
const manifest = chrome.runtime.getManifest();
const INSTALL_KEY = `${manifest.short_name}-installed`;
const storage = await chrome.storage.sync.get(INSTALL_KEY);
if (!storage[INSTALL_KEY]) {
// TODO: send install event
await chrome.storage.sync.set({ [INSTALL_KEY]: true });
}
}, 5 * SECOND);
}

View File

@@ -0,0 +1,4 @@
/**
* This function is called when the user's browser opens for the first time
*/
export default function onNewChromeSession() {}

View File

@@ -0,0 +1,9 @@
import { openDebugTab } from '../util/openDebugTab';
/**
* Called whenever the background service worker comes alive
* (usually around 30 seconds to 5 minutes after it was last alive)
*/
export default function onServiceWorkerAlive() {
openDebugTab();
}

View File

@@ -0,0 +1,10 @@
import { hotReloadTab } from 'src/background/util/hotReloadTab';
/**
* Called when the extension is updated (or when the extension is reloaded in development mode)
*/
export default function onUpdate() {
if (process.env.NODE_ENV === 'development') {
hotReloadTab();
}
}

View File

@@ -0,0 +1,20 @@
import { MessageHandler } from 'chrome-extension-toolkit';
import BrowserActionMessages from 'src/shared/messages/BrowserActionMessages';
const browserActionHandler: MessageHandler<BrowserActionMessages> = {
disableBrowserAction({ sender, sendResponse }) {
// by setting the popup to an empty string, clicking the browser action will not open the popup.html.
// we can then add an onClickListener to it from the content script
chrome.action.setPopup({ tabId: sender.tab?.id, popup: '' }).then(sendResponse);
},
enableBrowserAction({ sender, sendResponse }) {
chrome.action
.setPopup({
tabId: sender.tab?.id,
popup: 'popup.html',
})
.then(sendResponse);
},
};
export default browserActionHandler;

View File

@@ -0,0 +1,21 @@
import HotReloadingMessages from 'src/shared/messages/HotReloadingMessages';
import { MessageHandler } from 'chrome-extension-toolkit';
import { devStore } from 'src/shared/storage/devStore';
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
async reloadExtension({ sendResponse }) {
const isExtensionReloading = await devStore.getIsExtensionReloading();
if (!isExtensionReloading) return sendResponse();
const isTabReloading = await devStore.getIsExtensionReloading();
if (isTabReloading) {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tabToReload = tabs[0];
await devStore.setReloadTabId(tabToReload?.id);
}
chrome.runtime.reload();
},
};
export default hotReloadingHandler;

View File

@@ -0,0 +1,18 @@
import { MessageHandler } from 'chrome-extension-toolkit';
import TabManagementMessages from 'src/shared/messages/TabManagementMessages';
const tabManagementHandler: MessageHandler<TabManagementMessages> = {
getTabId({ sendResponse, sender }) {
sendResponse(sender.tab?.id ?? -1);
},
openNewTab({ data, sendResponse }) {
const { url } = data;
chrome.tabs.create({ url }).then(sendResponse);
},
removeTab({ data, sendResponse }) {
const { tabId } = data;
chrome.tabs.remove(tabId).then(sendResponse);
},
};
export default tabManagementHandler;

View File

@@ -0,0 +1,39 @@
import { devStore } from 'src/shared/storage/devStore';
/**
* A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao)
*/
const HOT_RELOADING_WHITELIST = [
'youtube.com',
'twitch.tv',
'github.dev',
'figma.com',
'netflix.com',
'disneyplus.com',
'hbomax.com',
'spotify.com',
'localhost:6006',
'docs.google.com',
'reddit.com',
'gmail.com',
'photopea.com',
];
/**
* Reloads the tab that was open when the extension was reloaded
* @returns a promise that resolves when the tab is reloaded
*/
export async function hotReloadTab(): Promise<void> {
const { getIsTabReloading, getReloadTabId } = devStore;
const [isTabReloading, reloadTabId] = await Promise.all([getIsTabReloading(), getReloadTabId()]);
if (!isTabReloading || !reloadTabId) return;
chrome.tabs.get(reloadTabId, tab => {
if (!tab?.id) return;
if (!HOT_RELOADING_WHITELIST.find(url => tab.url?.includes(url))) {
chrome.tabs.reload(tab.id);
}
});
}

View File

@@ -0,0 +1,24 @@
import { devStore } from 'src/shared/storage/devStore';
/**
* Open the debug tab as the first tab
*/
export async function openDebugTab() {
if (process.env.NODE_ENV === 'development') {
const debugTabId = await devStore.getDebugTabId();
const isAlreadyOpen = await (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
if (isAlreadyOpen) return;
const wasVisible = await devStore.getWasDebugTabVisible();
const tab = await chrome.tabs.create({
url: chrome.runtime.getURL('debug.html'),
active: wasVisible,
pinned: true,
index: 0,
});
await devStore.setDebugTabId(tab.id);
}
}

33
src/debug/hotReload.ts Normal file
View File

@@ -0,0 +1,33 @@
import io from 'socket.io-client';
import { bMessenger } from 'src/shared/messages';
const socket = io('http://localhost:9090');
let reBuilding = false;
socket.on('disconnect', async reason => {
reBuilding = reason.includes('transport') && !reason.includes('client');
});
socket.onAny(args => {
console.log(args);
});
socket.on('connect', async () => {
if (!reBuilding) {
console.log('%c[hot-reloading] listening for changes...', 'color:white; background-color: orange;');
} else {
console.log(
'%c[hot-reloading] changes detected, rebuilding and refreshing...',
'color:white; background-color: orange;'
);
}
});
socket.on('reload', async () => {
console.log('%c[hot-reloading] reloading...', 'color:white; background-color: orange;');
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
if (tabs?.[0]?.id) {
bMessenger.reloadExtension();
}
});
});

129
src/debug/index.tsx Normal file
View File

@@ -0,0 +1,129 @@
import './hotReload';
import React, { useEffect } from 'react';
import { render } from 'react-dom';
import { devStore } from 'src/shared/storage/devStore';
const manifest = chrome.runtime.getManifest();
interface JSONEditorProps {
data: any;
onChange: (updates: any) => void;
}
function JSONEditor(props: JSONEditorProps) {
const { data, onChange } = props;
const [isEditing, setIsEditing] = React.useState(false);
const [json, setJson] = React.useState(JSON.stringify(data, null, 2));
useEffect(() => {
setJson(JSON.stringify(data, null, 2));
}, [data]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setJson(e.target.value);
};
const handleSave = () => {
try {
const updates = JSON.parse(json);
onChange(updates);
setIsEditing(false);
} catch (e) {
console.error(e);
alert('Invalid JSON');
}
};
return (
<div>
{isEditing ? (
<div>
<div style={{ flex: 1, marginBottom: 10, gap: 10, display: 'flex' }}>
<button style={{ color: 'green' }} onClick={handleSave}>
Save
</button>
<button style={{ color: 'red' }} onClick={() => setIsEditing(false)}>
Cancel
</button>
</div>
<textarea style={{ width: '100%', height: '300px' }} value={json} onChange={handleChange} />
</div>
) : (
<div>
<pre onClick={() => setIsEditing(true)}>{json}</pre>
</div>
)}
</div>
);
}
// const PrettyPrintJson = React.memo(({ data }: any) => (
// <div>
// <pre>{JSON.stringify(data, null, 2)}</pre>
// </div>
// ));
function DevDashboard() {
const [localStorage, setLocalStorage] = React.useState<any>({});
const [syncStorage, setSyncStorage] = React.useState<any>({});
const [sessionStorage, setSessionStorage] = React.useState<any>({});
useEffect(() => {
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
devStore.setWasDebugTabVisible(true);
} else {
devStore.setWasDebugTabVisible(false);
}
};
document.addEventListener('visibilitychange', onVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
};
}, []);
useEffect(() => {
chrome.storage.local.get(null, result => {
setLocalStorage(result);
});
chrome.storage.sync.get(null, result => {
setSyncStorage(result);
});
chrome.storage.session.get(null, result => {
setSessionStorage(result);
});
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'local') {
setLocalStorage({ ...localStorage, ...changes });
} else if (areaName === 'sync') {
setSyncStorage({ ...syncStorage, ...changes });
} else if (areaName === 'session') {
setSessionStorage({ ...sessionStorage, ...changes });
}
});
}, []);
const handleEditStorage = (areaName: string) => (changes: Record<string, any>) => {
chrome.storage[areaName].set(changes);
};
return (
<div>
<h1>
{manifest.name} {manifest.version} - {process.env.NODE_ENV}
</h1>
<p>This tab is used for hot reloading and debugging. We will update this tab further in the future.</p>
<h2>Local Storage</h2>
<JSONEditor data={localStorage} onChange={handleEditStorage('local')} />
<h2>Sync Storage</h2>
<JSONEditor data={syncStorage} onChange={handleEditStorage('sync')} />
<h2>Session Storage</h2>
<JSONEditor data={sessionStorage} onChange={handleEditStorage('session')} />
<br />
</div>
);
}
render(<DevDashboard />, document.getElementById('root'));

View File

@@ -0,0 +1,6 @@
export default interface BrowserActionMessages {
/** make it so that clicking the browser action will open the popup.html */
enableBrowserAction: () => void;
/** make it so that clicking the browser action will respond to interactions from the content script */
disableBrowserAction: () => void;
}

View File

@@ -0,0 +1,3 @@
export default interface HotReloadingMessages {
reloadExtension: () => void;
}

View File

@@ -0,0 +1,21 @@
/**
* Messages for managing the user's open tabs list
*/
export default interface TabManagementMessages {
/**
* Opens a new tab with the given URL
* @param data The URL to open
*/
openNewTab: (data: { url: string }) => chrome.tabs.Tab;
/**
* Gets the ID of the current tab (the tab that sent the message)
* @returns The ID of the current tab
*/
getTabId: () => number;
/**
* Removes the tab with the given ID
* @param data The ID of the tab to remove
* @returns The ID of the tab that was removed
*/
removeTab: (data: { tabId: number }) => void;
}

View File

@@ -0,0 +1,6 @@
/**
* This is a type with all the message definitions that can be sent TO specific tabs
*/
export default interface TAB_MESSAGES {
reAnalyzePage: (data: { url: string }) => void;
}

View File

@@ -0,0 +1,21 @@
import { createMessenger } from 'chrome-extension-toolkit';
import TAB_MESSAGES from './TabMessages';
import BrowserActionMessages from './BrowserActionMessages';
import HotReloadingMessages from './HotReloadingMessages';
import TabManagementMessages from './TabManagementMessages';
/**
* This is a type with all the message definitions that can be sent TO the background script
*/
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & HotReloadingMessages;
/**
* A utility object that can be used to send type-safe messages to the background script
*/
export const bMessenger = createMessenger<BACKGROUND_MESSAGES>('background');
/**
* A utility object that can be used to send type-safe messages to specific tabs
*/
export const tabMessenger = createMessenger<TAB_MESSAGES>('tab');

View File

@@ -0,0 +1,25 @@
import { createStore } from 'chrome-extension-toolkit';
/**
* A store that is used to store data that is only relevant during development
*/
interface IDevStore {
/** the tabId for the debug tab */
debugTabId?: number;
/** whether the debug tab is visible */
wasDebugTabVisible?: boolean;
/** whether we should enable extension reloading */
isExtensionReloading?: boolean;
/** whether we should enable tab reloading */
isTabReloading?: boolean;
/** The id of the tab that we want to reload (after the extension reloads itself ) */
reloadTabId?: number;
}
export const devStore = createStore<IDevStore>('DEV_STORE', {
debugTabId: undefined,
isTabReloading: true,
wasDebugTabVisible: false,
isExtensionReloading: true,
reloadTabId: undefined,
});

View File

@@ -0,0 +1,15 @@
import { createStore, Store } from 'chrome-extension-toolkit';
interface ISessionStore {
chromeSessionId?: string;
}
export const sessionStore = createStore<ISessionStore>(
'SESSION_STORE',
{
chromeSessionId: undefined,
},
{
area: 'session',
}
);

25
src/shared/util/random.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Generate a random ID
*
* @returns string of size 10 made up of random numbers and letters
* @param length the length of the ID to generate
* @example "cdtl9l88pj"
*/
export function generateRandomId(length: number = 10): string {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i += 1) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* Generate a random number between min and max
* @param min the minimum number
* @param max the maximum number
* @returns a random number between min and max
*/
export function rangeRandom(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

34
src/shared/util/string.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* Given a string, returns a string with the first letter capitalized.
* @input The string to capitalize.
*/
export function capitalize(input: string): string {
try {
return input.charAt(0).toUpperCase() + input.substring(1).toLowerCase();
} catch (err) {
return input;
}
}
/**
* Given a string, returns a string with the first letter capitalized.
* @param input capitalize the first letter of this string
* @returns the string with the first letter capitalized
*/
export function capitalizeFirstLetter(input: string): string {
return input.charAt(0).toUpperCase() + input.slice(1);
}
/**
* Cuts the
* @param input The string to ellipsify.
* @param length The length of the string to return.
* @returns The ellipsified string.
*/
export const ellipsify = (input: string, chars: number): string => {
let ellipisifed = input;
if (input && input.length > chars) {
ellipisifed = `${input.substring(0, chars)}...`;
}
return ellipisifed;
};

19
src/shared/util/time.ts Normal file
View File

@@ -0,0 +1,19 @@
export const MILLISECOND = 1;
export const SECOND = 1000 * MILLISECOND;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
/**
*
*/
export const sleep = (milliseconds: number): Promise<void> => new Promise(resolve => setTimeout(resolve, milliseconds));
/**
* Checks to see if expired by the time first stored and the time frame that it is stored for
*
* @param time time it was stored
* @param threshold time frame it can be stored for
* @return true if expired, false if the time frame is still in range
*/
export const didExpire = (time: number, threshold: number): boolean => time + threshold <= Date.now();

17
src/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": true,
"lib": [
"DOM",
"es2021"
],
"types": [
"chrome",
"node"
],
},
"exclude": [
"../webpack"
]
}

View File

@@ -0,0 +1,19 @@
@import 'src/views/styles/base.module.scss';
.button {
background-color: #000;
color: #fff;
padding: 10px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
font-family: 'Inter';
&:hover {
background-color: #fff;
color: #000;
}
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { bMessenger } from 'src/shared/messages';
import styles from './Button.module.scss';
export function Button(): JSX.Element {
const handleOpenUrl = (url: string) => () => {
bMessenger.openNewTab({ url });
};
return (
<button className={styles.button} onClick={handleOpenUrl('https://www.google.com')}>
Click me
</button>
);
}

View File

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { render } from 'react-dom';
import { bMessenger } from 'src/shared/messages';
import { ContextInvalidated, createShadowDOM, onContextInvalidated } from 'chrome-extension-toolkit';
import { Button } from './components/Button/Button';
bMessenger.getTabId().then(tabId => {
console.log('tabId', tabId);
});
injectReact();
async function injectReact() {
const shadowDom = createShadowDOM('extension-dom-container');
render(<Button />, shadowDom.shadowRoot);
await shadowDom.addStyle('static/css/content.css');
}
if (process.env.NODE_ENV === 'development') {
onContextInvalidated(() => {
const div = document.createElement('div');
div.id = 'context-invalidated-container';
document.body.appendChild(div);
render(<ContextInvalidated color='black' backgroundColor='orange' />, div);
});
}

View File

@@ -0,0 +1,4 @@
import { createUseMessage } from 'chrome-extension-toolkit';
import TAB_MESSAGES from 'src/shared/messages/TabMessages';
export const useTabMessage = createUseMessage<TAB_MESSAGES>();

17
src/views/popup/popup.tsx Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import { render } from 'react-dom';
console.log('test');
console.log('test2');
// Path: src/views/popup/popup.tsx
console.log('test3');
render(
<div>
<h1>Test</h1>
</div>,
document.getElementById('root')
);

View File

@@ -0,0 +1,24 @@
// this is a custom wrapper around react-devtools
// that changes it so that we only send messages to the devtools when the current tab is active;
import { connectToDevTools } from 'react-devtools-core';
// connect to the devtools server
let ws = new WebSocket('ws://localhost:8097');
connectToDevTools({
websocket: ws,
});
// when the tab's visibile state changes, we connect or disconnect from the devtools
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
ws = new WebSocket('ws://localhost:8097');
connectToDevTools({
websocket: ws,
});
} else {
ws.close();
}
};
document.addEventListener('visibilitychange', onVisibilityChange);

View File

@@ -0,0 +1,2 @@
@import './colors.module.scss';
@import './fonts.module.scss';

View File

View File

@@ -0,0 +1,9 @@
@each $weights in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
@font-face {
font-family: 'Inter';
src: url('chrome-extension://__MSG_@@extension_id__/fonts/inter-#{$weights}.woff2') format('woff2');
font-display: auto;
font-style: normal;
font-weight: #{$weights};
}
}