├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── extension └── assets │ ├── icon-512.png │ └── icon.svg ├── package.json ├── scripts ├── manifest.ts ├── prepare.ts └── utils.ts ├── shim.d.ts ├── src ├── background │ ├── contentScriptHMR.ts │ ├── index.html │ └── main.ts ├── contentScripts │ ├── index.tsx │ └── views │ │ └── ContentApp.tsx ├── env.ts ├── global.d.ts ├── manifest.ts ├── options │ ├── Options.tsx │ ├── index.html │ └── main.tsx ├── popup │ ├── Popup.tsx │ ├── index.html │ └── main.tsx ├── styles │ ├── index.ts │ └── main.css └── vite-env.d.ts ├── tsconfig.json ├── vite.config.content.ts ├── vite.config.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'react-app', 8 | 'plugin:jsx-a11y/recommended', 9 | 'prettier', 10 | 'plugin:react-hooks/recommended', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 12, 18 | sourceType: 'module', 19 | tsconfigRootDir: './', 20 | project: './tsconfig.json', 21 | }, 22 | plugins: ['react', 'jsx-a11y'], 23 | rules: {}, 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: antfu 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .vite-ssg-dist 4 | .vite-ssg-temp 5 | *.crx 6 | *.local 7 | *.log 8 | *.pem 9 | *.xpi 10 | *.zip 11 | dist 12 | dist-ssr 13 | extension/manifest.json 14 | node_modules 15 | src/auto-imports.d.ts 16 | src/components.d.ts 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "johnsoncodehk.volar", 4 | "antfu.iconify", 5 | "dbaeumer.vscode-eslint", 6 | "voorjaar.windicss-intellisense", 7 | "csstools.postcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Vitesse"], 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "volar.tsPlugin": true, 5 | "volar.tsPluginStatus": false, 6 | "vite.autoStart": false, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true, 9 | }, 10 | "files.associations": { 11 | "*.css": "postcss", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Anthony Fu 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebExtension Vite Starter 2 | 3 | A [Vite](https://vitejs.dev/) powered WebExtension ([Chrome](https://developer.chrome.com/docs/extensions/reference/), [FireFox](https://addons.mozilla.org/en-US/developers/), etc.) starter template. 4 | 5 | Made based on https://github.com/antfu/vitesse-webext, big thanks to @antfu! 6 | 7 | ## Features 8 | 9 | - ⚡️ **Instant HMR** - use **Vite** on dev (no more refresh!) 10 | - 🦾 [TypeScript](https://www.typescriptlang.org/) - type safe 11 | - 🖥 Content Script - UseReact even in content script 12 | - 🌍 WebExtension - isomorphic extension for Chrome, Firefox, and others 13 | - 📃 Dynamic `manifest.json` with full type support 14 | 15 | ## Pre-packed 16 | 17 | ### WebExtension Libraries 18 | 19 | - [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) - WebExtension browser API Polyfill with types 20 | - [`webext-bridge`](https://github.com/antfu/webext-bridge) - effortlessly communication between contexts 21 | 22 | ### Coding Style 23 | 24 | - [ESLint](https://eslint.org/) 25 | 26 | ### Dev tools 27 | 28 | - [TypeScript](https://www.typescriptlang.org/) 29 | - [esno](https://github.com/antfu/esno) - TypeScript / ESNext node runtime powered by esbuild 30 | - [npm-run-all](https://github.com/mysticatea/npm-run-all) - Run multiple npm-scripts in parallel or sequential 31 | - [web-ext](https://github.com/mozilla/web-ext) - Streamlined experience for developing web extensions 32 | 33 | ## Use the Template 34 | 35 | ### GitHub Template 36 | 37 | [Create a repo from this template on GitHub](https://github.com/quolpr/react-vite-webext/generate). 38 | 39 | ### Clone to local 40 | 41 | If you prefer to do it manually with the cleaner git history 42 | 43 | ```bash 44 | npx degit quolpr/react-vite-webext my-webext 45 | cd my-webext 46 | yarn 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### Folders 52 | 53 | - `src` - main source. 54 | - `contentScript` - scripts and components to be injected as `content_script` 55 | - `background` - scripts for background. 56 | - `styles` - styles shared in popup and options page 57 | - `manifest.ts` - manifest for the extension. 58 | - `extension` - extension package root. 59 | - `assets` - static assets. 60 | - `dist` - built files, also serve stub entry for Vite on development. 61 | - `scripts` - development and bundling helper scripts. 62 | 63 | ### Development 64 | 65 | ```bash 66 | yarn dev 67 | ``` 68 | 69 | Then **load extension in browser with the `extension/` folder**. 70 | 71 | For Firefox developers, you can run the following command instead: 72 | 73 | ```bash 74 | yarn start:firefox 75 | ``` 76 | 77 | `web-ext` auto reload the extension when `extension/` files changed. 78 | 79 | > While Vite handles HMR automatically in the most of the case, [Extensions Reloader](https://chrome.google.com/webstore/detail/fimgfedafeadlieiabdeeaodndnlbhid) is still recommanded for cleaner hard reloading. 80 | 81 | ### Build 82 | 83 | To build the extension, run 84 | 85 | ```bash 86 | yarn build 87 | ``` 88 | 89 | And then pack files under `extension`, you can upload `extension.crx` or `extension.xpi` to appropriate extension store. 90 | 91 | ## Credits 92 | 93 | This repo was made based on https://github.com/antfu/vitesse-webext 94 | -------------------------------------------------------------------------------- /extension/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quolpr/react-vite-webext/5a366dd9f187e5e46de6c715994f5b445b51ef3e/extension/assets/icon-512.png -------------------------------------------------------------------------------- /extension/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vitesse-webext", 3 | "displayName": "Vitesse WebExt", 4 | "version": "0.0.1", 5 | "description": "[description]", 6 | "private": true, 7 | "scripts": { 8 | "dev": "npm run clear && cross-env NODE_ENV=development run-p dev:*", 9 | "dev:prepare": "esno scripts/prepare.ts", 10 | "dev:web": "vite", 11 | "dev:js": "npm run build:js -- --mode development", 12 | "build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:js", 13 | "build:prepare": "esno scripts/prepare.ts", 14 | "build:web": "vite build", 15 | "build:js": "vite build --config vite.config.content.ts", 16 | "pack": "cross-env NODE_ENV=production run-p pack:*", 17 | "pack:zip": "rimraf extension.zip && jszip-cli add extension -o ./extension.zip", 18 | "pack:crx": "crx pack extension -o ./extension.crx", 19 | "pack:xpi": "cross-env WEB_EXT_ARTIFACTS_DIR=./ web-ext build --source-dir ./extension --filename extension.xpi --overwrite-dest", 20 | "start:chromium": "web-ext run --source-dir ./extension --target=chromium", 21 | "start:firefox": "web-ext run --source-dir ./extension --target=firefox-desktop", 22 | "clear": "rimraf extension/dist extension/manifest.json extension.*", 23 | "lint": "eslint 'src/**/*.{json,ts,js}'" 24 | }, 25 | "devDependencies": { 26 | "@ffflorian/jszip-cli": "^3.1.5", 27 | "@iconify/json": "^1.1.408", 28 | "@types/fs-extra": "^9.0.13", 29 | "@types/node": "^16.10.2", 30 | "@types/react": "^17.0.33", 31 | "@types/react-dom": "^17.0.10", 32 | "@types/webextension-polyfill": "^0.8.0", 33 | "@typescript-eslint/eslint-plugin": "^4.32.0", 34 | "@typescript-eslint/parser": "^5.2.0", 35 | "@vitejs/plugin-react": "^1.0.6", 36 | "chokidar": "^3.5.2", 37 | "cross-env": "^7.0.3", 38 | "crx": "^5.0.1", 39 | "eslint": "^7.32.0", 40 | "eslint-config-prettier": "^8.3.0", 41 | "eslint-config-react-app": "^6.0.0", 42 | "eslint-plugin-flowtype": "^8.0.2", 43 | "eslint-plugin-import": "^2.25.2", 44 | "eslint-plugin-jsx-a11y": "^6.4.1", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "eslint-plugin-react": "^7.26.1", 47 | "eslint-plugin-react-hooks": "^4.2.0", 48 | "esno": "^0.10.0", 49 | "fs-extra": "^10.0.0", 50 | "kolorist": "^1.5.0", 51 | "npm-run-all": "^4.1.5", 52 | "rimraf": "^3.0.2", 53 | "typescript": "^4.4.3", 54 | "unplugin-auto-import": "^0.4.8", 55 | "vite": "^2.6.2", 56 | "web-ext": "^6.4.0" 57 | }, 58 | "dependencies": { 59 | "react": "^17.0.2", 60 | "react-dom": "^17.0.2", 61 | "webext-bridge": "^5.0.0", 62 | "webextension-polyfill": "^0.8.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scripts/manifest.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { getManifest } from '../src/manifest' 3 | import { r, log } from './utils' 4 | 5 | export async function writeManifest() { 6 | await fs.writeJSON(r('extension/manifest.json'), await getManifest(), { spaces: 2 }) 7 | log('PRE', 'write manifest.json') 8 | } 9 | 10 | writeManifest() 11 | -------------------------------------------------------------------------------- /scripts/prepare.ts: -------------------------------------------------------------------------------- 1 | // generate stub index.html files for dev entry 2 | import { execSync } from "child_process"; 3 | import fs from "fs-extra"; 4 | import chokidar from "chokidar"; 5 | import { r, port, isDev, log } from "./utils"; 6 | 7 | /** 8 | * Stub index.html to use Vite in development 9 | */ 10 | async function stubIndexHtml() { 11 | const views = ["options", "popup", "background"]; 12 | 13 | for (const view of views) { 14 | await fs.ensureDir(r(`extension/dist/${view}`)); 15 | let data = await fs.readFile(r(`src/${view}/index.html`), "utf-8"); 16 | data = data 17 | .replace('"./main.tsx"', `"http://localhost:${port}/${view}/main.tsx"`) 18 | .replace('"./main.ts"', `"http://localhost:${port}/${view}/main.ts"`) 19 | .replace( 20 | '
', 21 | '
Vite server did not start
' 22 | ); 23 | await fs.writeFile(r(`extension/dist/${view}/index.html`), data, "utf-8"); 24 | log("PRE", `stub ${view}`); 25 | } 26 | } 27 | 28 | function writeManifest() { 29 | execSync("npx esno ./scripts/manifest.ts", { stdio: "inherit" }); 30 | } 31 | 32 | writeManifest(); 33 | 34 | if (isDev) { 35 | stubIndexHtml(); 36 | chokidar.watch(r("src/**/*.html")).on("change", () => { 37 | stubIndexHtml(); 38 | }); 39 | chokidar.watch([r("src/manifest.ts"), r("package.json")]).on("change", () => { 40 | writeManifest(); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { bgCyan, black } from 'kolorist' 3 | 4 | export const port = parseInt(process.env.PORT || '') || 3303 5 | export const r = (...args: string[]) => resolve(__dirname, '..', ...args) 6 | export const isDev = process.env.NODE_ENV !== 'production' 7 | 8 | export function log(name: string, message: string) { 9 | // eslint-disable-next-line no-console 10 | console.log(black(bgCyan(` ${name} `)), message) 11 | } 12 | -------------------------------------------------------------------------------- /shim.d.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolWithReturn } from 'webext-bridge' 2 | 3 | declare module 'webext-bridge' { 4 | export interface ProtocolMap { 5 | // define message protocol types 6 | // see https://github.com/antfu/webext-bridge#type-safe-protocols 7 | 'tab-prev': { title: string | undefined } 8 | 'get-current-tab': ProtocolWithReturn<{ tabId: number }, { title: string }> 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/background/contentScriptHMR.ts: -------------------------------------------------------------------------------- 1 | import { isFirefox, isForbiddenUrl } from "~/env"; 2 | import browser from "webextension-polyfill"; 3 | 4 | // Firefox fetch files from cache instead of reloading changes from disk, 5 | // hmr will not work as Chromium based browser 6 | browser.webNavigation.onCommitted.addListener(({ tabId, frameId, url }) => { 7 | // Filter out non main window events. 8 | if (frameId !== 0) return; 9 | 10 | if (isForbiddenUrl(url)) return; 11 | 12 | // inject the latest scripts 13 | browser.tabs 14 | .executeScript(tabId, { 15 | file: `${isFirefox ? "" : "."}/dist/contentScripts/index.global.js`, 16 | runAt: "document_end", 17 | }) 18 | .catch((error) => console.error(error)); 19 | }); 20 | -------------------------------------------------------------------------------- /src/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Background 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/background/main.ts: -------------------------------------------------------------------------------- 1 | import { sendMessage } from "webext-bridge"; 2 | import { Tabs } from "webextension-polyfill"; 3 | import browser from "webextension-polyfill"; 4 | 5 | // only on dev mode 6 | if (import.meta.hot) { 7 | // @ts-expect-error for background HMR 8 | import("/@vite/client"); 9 | // load latest content script 10 | import("./contentScriptHMR"); 11 | } 12 | 13 | browser.runtime.onInstalled.addListener((): void => { 14 | // eslint-disable-next-line no-console 15 | console.log("Extension installed"); 16 | }); 17 | 18 | let previousTabId = 0; 19 | 20 | // communication example: send previous tab title from background page 21 | // see shim.d.ts for type declaration 22 | browser.tabs.onActivated.addListener(async ({ tabId }) => { 23 | if (!previousTabId) { 24 | previousTabId = tabId; 25 | return; 26 | } 27 | 28 | let tab: Tabs.Tab; 29 | 30 | try { 31 | tab = await browser.tabs.get(previousTabId); 32 | previousTabId = tabId; 33 | } catch { 34 | return; 35 | } 36 | 37 | // eslint-disable-next-line no-console 38 | console.log("previous tab", tab); 39 | sendMessage( 40 | "tab-prev", 41 | { title: tab.title }, 42 | { context: "content-script", tabId } 43 | ); 44 | }); 45 | 46 | // onMessage("get-current-tab", async () => { 47 | // try { 48 | // const tab = await browser.tabs.get(previousTabId); 49 | // return { 50 | // title: tab?.id, 51 | // }; 52 | // } catch { 53 | // return { 54 | // title: undefined, 55 | // }; 56 | // } 57 | // }); 58 | -------------------------------------------------------------------------------- /src/contentScripts/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { onMessage } from "webext-bridge"; 5 | import browser from "webextension-polyfill"; 6 | import { ContentApp } from "./views/ContentApp"; 7 | 8 | // Firefox `browser.tabs.executeScript()` requires scripts return a primitive value 9 | (() => { 10 | console.info("[vitesse-webext] Hello world from content script"); 11 | 12 | // communication example: send previous tab title from background page 13 | onMessage("tab-prev", ({ data }) => { 14 | console.log(`[vitesse-webext] Navigate from page "${data}"`); 15 | }); 16 | 17 | // mount component to context window 18 | const container = document.createElement("div"); 19 | const root = document.createElement("div"); 20 | const styleEl = document.createElement("link"); 21 | const shadowDOM = 22 | container.attachShadow?.({ mode: __DEV__ ? "open" : "closed" }) || 23 | container; 24 | styleEl.setAttribute("rel", "stylesheet"); 25 | styleEl.setAttribute( 26 | "href", 27 | browser.runtime.getURL("dist/contentScripts/style.css") 28 | ); 29 | shadowDOM.appendChild(styleEl); 30 | shadowDOM.appendChild(root); 31 | document.body.appendChild(container); 32 | 33 | ReactDOM.render( 34 | 35 | 36 | , 37 | root 38 | ); 39 | })(); 40 | -------------------------------------------------------------------------------- /src/contentScripts/views/ContentApp.tsx: -------------------------------------------------------------------------------- 1 | export const ContentApp = () => { 2 | return
Hey!
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | const forbiddenProtocols = [ 2 | 'chrome-extension://', 3 | 'chrome-search://', 4 | 'chrome://', 5 | 'devtools://', 6 | 'edge://', 7 | 'https://chrome.google.com/webstore', 8 | ] 9 | 10 | export function isForbiddenUrl(url: string): boolean { 11 | return forbiddenProtocols.some(protocol => url.startsWith(protocol)) 12 | } 13 | 14 | export const isFirefox = navigator.userAgent.includes('Firefox') 15 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const __DEV__: boolean 2 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import type { Manifest } from 'webextension-polyfill' 3 | import type PkgType from '../package.json' 4 | import { isDev, port, r } from '../scripts/utils' 5 | 6 | export async function getManifest() { 7 | const pkg = await fs.readJSON(r('package.json')) as typeof PkgType 8 | 9 | // update this file to update this manifest.json 10 | // can also be conditional based on your need 11 | const manifest: Manifest.WebExtensionManifest = { 12 | manifest_version: 2, 13 | name: pkg.displayName || pkg.name, 14 | version: pkg.version, 15 | description: pkg.description, 16 | browser_action: { 17 | default_icon: './assets/icon-512.png', 18 | default_popup: './dist/popup/index.html', 19 | }, 20 | options_ui: { 21 | page: './dist/options/index.html', 22 | open_in_tab: true, 23 | chrome_style: false, 24 | }, 25 | background: { 26 | page: './dist/background/index.html', 27 | persistent: false, 28 | }, 29 | icons: { 30 | 16: './assets/icon-512.png', 31 | 48: './assets/icon-512.png', 32 | 128: './assets/icon-512.png', 33 | }, 34 | permissions: [ 35 | 'tabs', 36 | 'storage', 37 | 'activeTab', 38 | 'http://*/', 39 | 'https://*/', 40 | ], 41 | content_scripts: [{ 42 | matches: ['http://*/*', 'https://*/*'], 43 | js: ['./dist/contentScripts/index.global.js'], 44 | }], 45 | web_accessible_resources: [ 46 | 'dist/contentScripts/style.css', 47 | ], 48 | } 49 | 50 | if (isDev) { 51 | // for content script, as browsers will cache them for each reload, 52 | // we use a background script to always inject the latest version 53 | // see src/background/contentScriptHMR.ts 54 | delete manifest.content_scripts 55 | manifest.permissions?.push('webNavigation') 56 | 57 | // this is required on dev for Vite script to load 58 | manifest.content_security_policy = `script-src \'self\' http://localhost:${port}; object-src \'self\'` 59 | } 60 | 61 | return manifest 62 | } 63 | -------------------------------------------------------------------------------- /src/options/Options.tsx: -------------------------------------------------------------------------------- 1 | export const OptionsApp = () => { 2 | return
Options
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Options 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/options/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "../styles"; 4 | import { OptionsApp } from "./Options"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /src/popup/Popup.tsx: -------------------------------------------------------------------------------- 1 | export const Popup = () => { 2 | return
Hey!
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Popup 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/popup/main.tsx: -------------------------------------------------------------------------------- 1 | import "../styles"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { Popup } from "./Popup"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import './main.css' 2 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .btn { 9 | } 10 | 11 | .icon-btn { 12 | font-size: 0.9em; 13 | } 14 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "es2016", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "incremental": false, 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "paths": { 17 | "~/*": ["src/*"] 18 | }, 19 | "jsx": "react-jsx" 20 | }, 21 | "exclude": ["dist", "node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.content.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { sharedConfig } from "./vite.config"; 3 | import { r, isDev } from "./scripts/utils"; 4 | import packageJson from "./package.json"; 5 | 6 | // bundling the content script using Vite 7 | export default defineConfig({ 8 | ...sharedConfig, 9 | build: { 10 | watch: isDev 11 | ? { 12 | include: [r("src/contentScripts/**/*"), r("src/components/**/*")], 13 | } 14 | : undefined, 15 | outDir: r("extension/dist/contentScripts"), 16 | cssCodeSplit: false, 17 | emptyOutDir: false, 18 | sourcemap: isDev ? "inline" : false, 19 | lib: { 20 | entry: r("src/contentScripts/index.tsx"), 21 | name: packageJson.name, 22 | formats: ["iife"], 23 | }, 24 | rollupOptions: { 25 | output: { 26 | entryFileNames: "index.global.js", 27 | extend: true, 28 | }, 29 | }, 30 | }, 31 | plugins: [...sharedConfig.plugins!], 32 | }); 33 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { dirname, relative } from "path"; 2 | import { defineConfig, UserConfig } from "vite"; 3 | import AutoImport from "unplugin-auto-import/vite"; 4 | import { r, port, isDev } from "./scripts/utils"; 5 | import react from "@vitejs/plugin-react"; 6 | 7 | export const sharedConfig: UserConfig = { 8 | root: r("src"), 9 | resolve: { 10 | alias: { 11 | "~/": `${r("src")}/`, 12 | }, 13 | }, 14 | define: { 15 | __DEV__: isDev, 16 | }, 17 | plugins: [ 18 | // React fast refresh doesn't work, cause injecting of preambleCode into index.html 19 | // TODO: fix it 20 | react({ fastRefresh: false }), 21 | AutoImport({ 22 | imports: [ 23 | { 24 | "webextension-polyfill": [["default", "browser"]], 25 | }, 26 | ], 27 | dts: r("src/auto-imports.d.ts"), 28 | }), 29 | 30 | // rewrite assets to use relative path 31 | { 32 | name: "assets-rewrite", 33 | enforce: "post", 34 | apply: "build", 35 | transformIndexHtml(html, { path }) { 36 | return html.replace( 37 | /"\/assets\//g, 38 | `"${relative(dirname(path), "/assets")}/` 39 | ); 40 | }, 41 | }, 42 | ], 43 | optimizeDeps: { 44 | include: ["webextension-polyfill"], 45 | }, 46 | }; 47 | 48 | export default defineConfig(({ command }) => ({ 49 | ...sharedConfig, 50 | base: command === "serve" ? `http://localhost:${port}/` : "/dist/", 51 | server: { 52 | port, 53 | hmr: { 54 | host: "localhost", 55 | }, 56 | }, 57 | build: { 58 | outDir: r("extension/dist"), 59 | emptyOutDir: false, 60 | sourcemap: isDev ? "inline" : false, 61 | // https://developer.chrome.com/docs/webstore/program_policies/#:~:text=Code%20Readability%20Requirements 62 | terserOptions: { 63 | mangle: false, 64 | }, 65 | rollupOptions: { 66 | input: { 67 | background: r("src/background/index.html"), 68 | options: r("src/options/index.html"), 69 | popup: r("src/popup/index.html"), 70 | }, 71 | }, 72 | }, 73 | plugins: [...sharedConfig.plugins!], 74 | })); 75 | --------------------------------------------------------------------------------