├── .gitignore ├── postcss.config.cjs ├── public ├── logo128.png ├── manifestv3.json └── manifest.json ├── tsconfig.node.json ├── background.html ├── content-script ├── index.css ├── index.tsx ├── App.tsx └── SignIn.tsx ├── tailwind.config.cjs ├── index.html ├── src └── supabase_client.ts ├── .github ├── workflows │ ├── build.yml │ └── auto-merge.yml └── dependabot.yml ├── tsconfig.json ├── vite.content.config.ts ├── vite.config.ts ├── vite.chrome.config.ts ├── README.md ├── LICENSE ├── background.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akoskm/vite-react-tailwindcss-browser-extension-supabase/HEAD/public/logo128.png -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vite + React + TS 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /content-script/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | h1 { 7 | @apply text-slate-700 text-xl 8 | } 9 | .loading-button { 10 | display: inline; 11 | } 12 | } -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | important: '#extension-root', 4 | content: [ 5 | "./content-script/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | container: { 9 | padding: '2rem', 10 | } 11 | }, 12 | plugins: [ 13 | require('@tailwindcss/forms')({ 14 | strategy: 'class' 15 | }), 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + TailwindCSS 8 | 9 | 10 |

Vite + React + TS + TailwindCSS

11 | 12 | 13 | -------------------------------------------------------------------------------- /src/supabase_client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | const supabaseUrl = 'https://orktigushjmxmztfwmpd.supabase.co' 4 | const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9ya3RpZ3VzaGpteG16dGZ3bXBkIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzAwNTA2NTksImV4cCI6MTk4NTYyNjY1OX0.4XZvJi1jWbj5GWqRHB7Jgri7jnGMFGk-vkFKgeNDU0s'; 5 | 6 | const supabase = createClient(supabaseUrl, supabaseKey) 7 | 8 | export default supabase; -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Workflow 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Use Node.js LTS 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: "lts/*" 17 | 18 | - name: Install dependencies 19 | run: npm ci 20 | 21 | - name: Run build 22 | run: npm run build 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" # How often to check for updates 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "commonjs", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /vite.content.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import react from '@vitejs/plugin-react' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | define: { 9 | 'process.env': {} 10 | }, 11 | build: { 12 | emptyOutDir: true, 13 | outDir: resolve(__dirname, 'dist'), 14 | lib: { 15 | formats: ['iife'], 16 | entry: resolve(__dirname, './content-script/index.tsx'), 17 | name: 'Tweton' 18 | }, 19 | rollupOptions: { 20 | output: { 21 | entryFileNames: 'index.global.js', 22 | extend: true, 23 | } 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /public/manifestv3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cat Facts", 3 | "description": "Learn random facts about cats", 4 | "version": "1.0.0", 5 | "manifest_version": 3, 6 | "action": { 7 | "default_popup": "index.html", 8 | "default_title": "Open the popup" 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [ 13 | "https://blank.org/*" 14 | ], 15 | "run_at": "document_end", 16 | "js": [ 17 | "./index.global.js" 18 | ], 19 | "css": [ 20 | "./style.css" 21 | ] 22 | } 23 | ], 24 | "permissions": [ 25 | "storage" 26 | ], 27 | "background": { 28 | "service_worker": "./background.global.js" 29 | }, 30 | "icons": { 31 | "128": "logo128.png" 32 | } 33 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cat Facts", 3 | "description": "Learn random facts about cats", 4 | "version": "1.0.0", 5 | "manifest_version": 2, 6 | "browser_action": { 7 | "default_popup": "index.html", 8 | "default_title": "Open the popup" 9 | }, 10 | "optional_permissions": [ 11 | "" 12 | ], 13 | "content_scripts": [ 14 | { 15 | "matches": [ 16 | "https://blank.org/*" 17 | ], 18 | "run_at": "document_end", 19 | "js": [ 20 | "./index.global.js" 21 | ], 22 | "css": [ 23 | "./style.css" 24 | ] 25 | } 26 | ], 27 | "background": { 28 | "page": "background.html", 29 | "persistent": false 30 | }, 31 | "icons": { 32 | "128": "logo128.png" 33 | } 34 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | const fetchVersion = () => { 5 | return { 6 | name: 'html-transform', 7 | transformIndexHtml(html) { 8 | return html.replace( 9 | /__APP_VERSION__/, 10 | `v${process.env.npm_package_version}` 11 | ) 12 | } 13 | } 14 | } 15 | 16 | // https://vitejs.dev/config/ 17 | export default defineConfig({ 18 | plugins: [react(), fetchVersion()], 19 | build: { 20 | outDir: 'dist', 21 | emptyOutDir: false, 22 | rollupOptions: { 23 | input: { 24 | index: new URL('./index.html', import.meta.url).pathname, 25 | background: new URL('./background.html', import.meta.url).pathname, 26 | } 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch' # Only auto-merge minor and patch updates 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /vite.chrome.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import {resolve} from "path"; 4 | 5 | const fetchVersion = () => { 6 | return { 7 | name: 'html-transform', 8 | transformIndexHtml(html: string) { 9 | return html.replace( 10 | /__APP_VERSION__/, 11 | `v${process.env.npm_package_version}` 12 | ) 13 | } 14 | } 15 | } 16 | 17 | // https://vitejs.dev/config/ 18 | export default defineConfig({ 19 | plugins: [react(), fetchVersion()], 20 | build: { 21 | emptyOutDir: false, 22 | outDir: resolve(__dirname, 'dist'), 23 | lib: { 24 | formats: ['iife'], 25 | entry: resolve(__dirname, './background.ts'), 26 | name: 'Tweton' 27 | }, 28 | rollupOptions: { 29 | output: { 30 | entryFileNames: 'background.global.js', 31 | extend: true, 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cross Platform Browser Extension template repository 2 | 3 | React + TailwindCSS bundled with Vite. 4 | 5 | # Installation 6 | ``` 7 | npm i 8 | ``` 9 | 10 | # Building the Extension: 11 | 12 | ## Firefox 13 | `npm run build` builds the extension by default for Firefox. 14 | 15 | The generated files are in `dist/`. 16 | 17 | To load the extension in Firefox go to `about:debugging#/runtime/this-firefox` or 18 | 19 | Firefox > Preferences > Extensions & Themes > Debug Add-ons > Load Temporary Add-on... 20 | 21 | Here locate the `dist/` directory and open `manifest.json` 22 | 23 | ## Chrome 24 | `npm run build:chrome` builds the extensions for Google Chrome. 25 | 26 | The generated files are in `dist/`. 27 | To load the extensions in Google Chrome go to `chrome://extensions/` and click `Load unpacked`. Locate the dist directory and select `manifest.json`. 28 | 29 | # Files: 30 | 31 | - content-script - UI files 32 | - background.ts - Background script/Service worker 33 | - index.html - popup UI 34 | 35 | If you have any questions feel free to open an issue. -------------------------------------------------------------------------------- /content-script/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | const pluginTagId = 'extension-root'; 7 | const existingInstance = document.getElementById('extension-root'); 8 | if (existingInstance) { 9 | console.log('existing instance found, removing'); 10 | existingInstance.remove(); 11 | } 12 | 13 | const index = document.createElement('div') 14 | index.id = pluginTagId; 15 | 16 | // Make sure the element that you want to mount the app to has loaded. You can 17 | // also use `append` or insert the app using another method: 18 | // https://developer.mozilla.org/en-US/docs/Web/API/Element#methods 19 | // 20 | // Also control when the content script is injected from the manifest.json: 21 | // https://developer.chrome.com/docs/extensions/mv3/content_scripts/#run_time 22 | const body = document.querySelector('body') 23 | if (body) { 24 | body.append(index) 25 | } 26 | 27 | ReactDOM.createRoot(index).render( 28 | 29 | 30 | 31 | ) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Akos Kemives 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /background.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | import supabase from './src/supabase_client'; 3 | 4 | type Message = { 5 | action: 'fetch' | 'getSession' | 'signout', 6 | value: null 7 | } | { 8 | action: 'signup' | 'signin', 9 | value: { 10 | email: string, 11 | password: string, 12 | } 13 | } 14 | 15 | type ResponseCallback = (data: any) => void 16 | 17 | async function handleMessage({action, value}: Message, response: ResponseCallback) { 18 | if (action === 'fetch') { 19 | const result = await fetch('https://meowfacts.herokuapp.com/'); 20 | 21 | const { data } = await result.json(); 22 | 23 | response({ message: 'Successfully signed up!', data }); 24 | } else if (action === 'signup') { 25 | const result = await supabase.auth.signUp(value) 26 | response({message: 'Successfully signed up!', data: result}); 27 | } else if (action === 'signin') { 28 | console.log('requesting auth'); 29 | const {data, error} = await supabase.auth.signInWithPassword(value); 30 | response({data, error}); 31 | } else if (action === 'getSession') { 32 | supabase.auth.getSession().then(response) 33 | } else if (action === 'signout') { 34 | const {error} = await supabase.auth.signOut() 35 | response({data: null, error}); 36 | } else { 37 | response({data: null, error: 'Unknown action'}); 38 | } 39 | } 40 | 41 | // @ts-ignore 42 | browser.runtime.onMessage.addListener((msg, sender, response) => { 43 | handleMessage(msg, response); 44 | return true; 45 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react-tailwindcss-browser-extension", 3 | "version": "1.0.0", 4 | "description": "A Cross Browser Extension template", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vite", 8 | "build:manifest:chrome": "mv dist/manifestv3.json dist/manifest.json", 9 | "build:background:chrome": "vite build --config vite.chrome.config.ts", 10 | "build:js": "vite build --config vite.content.config.ts", 11 | "build:web": "tsc && vite build", 12 | "build": "NODE_ENV=production run-s build:js build:web", 13 | "build:chrome": "NODE_ENV=production run-s build:js build:background:chrome build:web build:manifest:chrome", 14 | "package": "zip -r extension.zip dist/*", 15 | "preview": "vite preview" 16 | }, 17 | "type": "module", 18 | "author": "", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@tailwindcss/forms": "^0.5.9", 22 | "@types/node": "^20.14.11", 23 | "@types/react": "^18.3.12", 24 | "@types/react-dom": "^18.3.1", 25 | "@types/webextension-polyfill": "^0.12.1", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "autoprefixer": "^10.4.20", 28 | "npm-run-all": "^4.1.5", 29 | "postcss": "^8.5.1", 30 | "tailwindcss": "^3.4.17", 31 | "typescript": "^5.7.3", 32 | "vite": "^5.4.11", 33 | "webextension-polyfill": "^0.12.0" 34 | }, 35 | "dependencies": { 36 | "@supabase/supabase-js": "^2.48.1", 37 | "formik": "^2.4.6", 38 | "react": "^18.2.0", 39 | "react-dom": "^18.3.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /content-script/App.tsx: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | import {useEffect, useState} from "react"; 3 | import SignIn from "./SignIn"; 4 | 5 | enum SCREEN { 6 | SIGN_IN, SIGN_UP, FACTS 7 | } 8 | 9 | const App = () => { 10 | const [fact, setFact] = useState('Click the button to fetch a fact!'); 11 | const [loading, setLoading] = useState(false); 12 | const [session, setSession] = useState(null); 13 | const [screen, setScreen] = useState(SCREEN.FACTS); 14 | const [error, setError] = useState(''); 15 | 16 | async function getSession() { 17 | const {data: {session}} = await browser.runtime.sendMessage({action: 'getSession'}); 18 | setSession(session); 19 | } 20 | 21 | useEffect(() => { 22 | getSession(); 23 | }, []); 24 | 25 | async function handleOnClick() { 26 | setLoading(true); 27 | const {data} = await browser.runtime.sendMessage({action: 'fetch'}); 28 | setFact(data); 29 | setLoading(false); 30 | } 31 | 32 | async function handleSignUp(email: string, password: string) { 33 | await browser.runtime.sendMessage({action: 'signup', value: {email, password}}); 34 | setScreen(SCREEN.SIGN_IN) 35 | } 36 | 37 | async function handleSignIn(email: string, password: string) { 38 | const {data, error} = await browser.runtime.sendMessage({action: 'signin', value: {email, password}}); 39 | if (error) return setError(error.message) 40 | 41 | setSession(data.session) 42 | } 43 | 44 | async function handleSignOut() { 45 | const signOutResult = await browser.runtime.sendMessage({action: 'signout'}); 46 | setScreen(SCREEN.SIGN_IN); 47 | setSession(signOutResult.data); 48 | } 49 | 50 | function renderApp() { 51 | if (!session) { 52 | if (screen === SCREEN.SIGN_UP) { 53 | return { 54 | setScreen(SCREEN.SIGN_IN); 55 | setError(''); 56 | }} helpText={'Got an account? Sign in'} error={error}/>; 57 | } 58 | return { 59 | setScreen(SCREEN.SIGN_UP) 60 | setError(''); 61 | }} helpText={'Create an account'} error={error}/> 62 | } 63 | 64 | return ( 65 | <> 66 | 70 |

{fact}

71 |
72 | Sign out 73 |
74 | 75 | ) 76 | } 77 | 78 | return ( 79 |
80 |
81 |

Cat Facts!

82 | {renderApp()} 83 |
84 |
85 | ) 86 | } 87 | export default App; -------------------------------------------------------------------------------- /content-script/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import {Field, Form, Formik} from "formik"; 2 | import {useState} from "react"; 3 | 4 | interface Props { 5 | onSignIn: (email: string, password: string) => void, 6 | onScreenChange: () => void; 7 | title: string; 8 | helpText?: string; 9 | error?: string; 10 | } 11 | 12 | const SignIn = ({onSignIn, title, onScreenChange, helpText, error}: Props) => { 13 | const [loading, setLoading] = useState(false); 14 | 15 | function renderLoadingSpinner() { 16 | if (!loading) return null; 17 | 18 | return ( 19 | 22 | 23 | 25 | ) 26 | } 27 | 28 | return ( 29 | <> 30 |

31 | {title} 32 |

33 | { 39 | setLoading(true); 40 | await onSignIn(email, password) 41 | setLoading(false); 42 | }} 43 | > 44 |
45 | 54 | 62 | 66 | {error && ( 67 |

{error}

68 | )} 69 | {helpText && ( 70 |

71 | {helpText} 72 |

73 | )} 74 |
75 |
76 | 77 | ); 78 | } 79 | export default SignIn; --------------------------------------------------------------------------------