├── .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 |
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 | )
26 | }
27 |
28 | return (
29 | <>
30 |
31 | {title}
32 |
33 | {
39 | setLoading(true);
40 | await onSignIn(email, password)
41 | setLoading(false);
42 | }}
43 | >
44 |
75 |
76 | >
77 | );
78 | }
79 | export default SignIn;
--------------------------------------------------------------------------------