├── .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 |
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 |
--------------------------------------------------------------------------------