├── .babelrc ├── .env.chrome ├── .env.firefox ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── README_EN.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── _locales │ ├── en │ │ └── messages.json │ └── zh_CN │ │ └── messages.json └── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon32.png │ └── icon48.png ├── scripts ├── crx-pack.js └── manifest.js ├── src ├── background │ └── background.js ├── contentScript │ └── content.js ├── options │ ├── index.html │ ├── index.js │ └── index.scss ├── popup │ ├── index.html │ ├── index.js │ └── index.scss ├── styles │ ├── _components.scss │ ├── _fonts.scss │ ├── _reset.scss │ └── _variables.scss └── utils │ └── emoji-log.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | // Latest stable ECMAScript features 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": "88", 9 | "firefox": "112", 10 | "opera": "36", 11 | "edge": "79" 12 | } 13 | } 14 | ] 15 | ], 16 | "plugins": [ 17 | // Some transforms (such as object-rest-spread) 18 | // don't work without it: https://github.com/babel/babel/issues/7215 19 | ["@babel/plugin-transform-destructuring", { "useBuiltIns": true }], 20 | ["@babel/plugin-proposal-object-rest-spread", { "useBuiltIns": true }], 21 | [ 22 | // Polyfills the runtime needed for async/await and generators 23 | "@babel/plugin-transform-runtime", 24 | { 25 | "helpers": false, 26 | "regenerator": true 27 | } 28 | ] 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.env.chrome: -------------------------------------------------------------------------------- 1 | VITE_BROWSER=chrome 2 | VITE_CHROME_MIN_VER=88 -------------------------------------------------------------------------------- /.env.firefox: -------------------------------------------------------------------------------- 1 | VITE_BROWSER=firefox 2 | VITE_GECKO_ID=addon@example.com 3 | VITE_GECKO_MIN_VER=112.0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | extension/ 4 | .yarn/ 5 | .pnp.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "ecmaVersion": 2015 6 | }, 7 | "rules": { 8 | "no-console": "off", 9 | "no-extend-native": "off", 10 | "class-methods-use-this": "off", 11 | "max-classes-per-file": "off", 12 | "node/no-unpublished-require": "off", 13 | "node/no-missing-import": "off", 14 | "node/no-unpublished-import": "off" 15 | }, 16 | "env": { 17 | "browser": true, 18 | "webextensions": true 19 | } 20 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.ai binary -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | id-token: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | tags: 12 | - '*' 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v4 22 | 23 | - name: Install Node.js 18 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 18 27 | 28 | - name: Install dependencies 29 | run: | 30 | npm install 31 | 32 | - name: Build Firefox extension 33 | run: | 34 | npm run build:firefox 35 | cd dist 36 | zip -r ../extension-firefox.zip . 37 | cd .. 38 | 39 | - name: Build Chrome extension 40 | run: | 41 | npm run build:chrome 42 | cd dist 43 | zip -r ../extension-chrome.zip . 44 | cd .. 45 | 46 | - name: upload .zip as Artifact 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: packed-extensions 50 | path: | 51 | extension-firefox.zip 52 | extension-chrome.zip 53 | 54 | - name: Publish to GitHub Release 55 | uses: softprops/action-gh-release@v2 56 | if: startsWith(github.ref, 'refs/tags/') 57 | with: 58 | files: | 59 | extension-firefox.zip 60 | extension-chrome.zip 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | db.json 4 | *.log 5 | node_modules/ 6 | dist/ 7 | web-ext-artifacts/ 8 | .deploy*/ 9 | _multiconfig.yml 10 | .amo-upload-uuid 11 | update.xml 12 | myExtension.crx -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true # Until https://github.com/xojs/stylelint-config-xo/issues/19 -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "semi": false 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 waahah 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 |

🚀 VExt

2 |

🖥构建”一次编写,在任何浏览器上运行“的 Web 扩展启动器🔋

3 |
4 | 5 | LICENSE 6 | 7 | 8 | github 9 | 10 | English 11 | 简体中文 12 |
13 |

🙋‍♂️ Made by @waahah

14 |
15 | 16 | ![](https://s2.loli.net/2025/04/01/naCU38g5qI9wANK.png) 17 | 18 | ### 特性 19 | - ✅ 跨浏览器支持 (Web 扩展 API) 20 | 21 | - 🌈 代码更改时自动构建和 HMR 热重载 22 | 23 | - 📦 自动打包特定于浏览器的构建文件 24 | 25 | - 📄 浏览器定制清单生成 26 | 27 | - 🎨 与前端框架无关 28 | 29 | - 🤖 自动发布 30 | 31 | - ✨ 持续集成 32 | 33 | - 🥢 SASS 样式 34 | 35 | - 🎯 ES6 模块支持 36 | 37 | - 📊 智能重新加载 38 | 39 | 40 | ### 浏览器支持 41 | 42 | | [![Chrome](https://s2.loli.net/2025/03/31/rAg93eNCOj8P54y.png)](/) | [![Firefox](https://s2.loli.net/2025/03/31/yJiWdqaP6Y5ozsr.png)](/) | [![Opera](https://s2.loli.net/2025/03/31/qKPSGrilQvmgy9p.png)](/) | [![Edge](https://s2.loli.net/2025/03/31/KTsyfa3QDZ6o9dn.png)](/) | [![Yandex](https://s2.loli.net/2025/03/31/arqyo6B2w7VeAld.png)](/) | [![Brave](https://s2.loli.net/2025/03/31/YlTZOBo5AsWqSia.png)](/) | [![vivaldi](https://s2.loli.net/2025/03/31/a9OmnPAipo6F7Yz.png)](/) | 43 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | 44 | | 79 & later ✔ | 52 & later ✔ | 36 & later ✔ | 79 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔ | 45 | 46 | ### 🚀 快速开始 47 | 48 | 使用 [vext-cli](https://github.com/waahah/vext-cli) 创建一个新的项目非常简单,只需运行以下命令: 49 | 50 | ```bash 51 | npm install -g create-vext 52 | # 或者使用 yarn 53 | yarn global add create-vext 54 | # 创建新项目 55 | vext create 56 | ``` 57 | 58 | 59 | ### 📦 前置要求 60 | 61 | - `node >= 18` 62 | - `npm 9.x+` 63 | 64 | 65 | ### 🔧 安装依赖 66 | 67 | ```bash 68 | git clone https://gitee.com/waahah/VExt.git 69 | cd VExt 70 | npm install 71 | ``` 72 | 73 | 74 | ### 开发模式 75 | 76 | ```bash 77 | npm run dev # 启动开发服务器(HMR) 78 | npm run watch:firefox # Firefox热更新构建 79 | npm run watch:chrome # Chrome热更新构建 80 | ``` 81 | 82 | 83 | ### 生产构建 84 | 85 | ```bash 86 | npm run build:firefox # Firefox生产构建 87 | npm run build:chrome # Chrome生产构建 88 | ``` 89 | 90 | 91 | ### 本地运行 92 | 93 | ```bash 94 | npm run start:firefox # 启动Firefox测试实例 95 | npm run start:chrome # 启动Chrome测试实例 96 | ``` 97 | 运行后会启动对应浏览器,并自动加载运行项目。 98 | 99 | 100 | ### 代码质量 101 | 102 | ```bash 103 | npm run lint # 执行构建并代码校验 104 | ``` 105 | 106 | 107 | ### 打包ZIP 108 | ```bash 109 | npm run pack:firefox # Firefox打包 110 | npm run pack:chrome # Chrome打包 111 | ``` 112 | 打包后的文件会输出到 `web-ext-artifacts/` 目录。 113 | 114 | 115 | ### CRX打包 116 | ```bash 117 | npm run pack:crx # 生成.crx扩展包(需配置scripts/crx-pack.js) 118 | ``` 119 | 120 | 121 | ### 签名发布 122 | ```bash 123 | npm run sign:firefox # 签名发布Firefox扩展(需配置API密钥) 124 | npm run sign:chrome # 签名发布Chrome扩展(需配置Web Store凭据) 125 | ``` 126 | 自动上传扩展商店签名并发布,签名后的.XPI文件会输出到 `web-ext-artifacts/` 目录。 127 | 128 | 129 | ### ⚙️ 配置说明 130 | 1. 环境模式:更改manifest文件 131 | 2. 构建输出:构建产物默认生成至/dist目录 132 | 3. 签名配置:需在package.json中配置以下凭证: 133 | ``` 134 | Firefox: 135 | Chrome: 136 | 137 | 138 | 139 | 140 | ``` 141 | - `firefox`获取API密钥请查看[此指南](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#web-ext-sign) 142 | - `chrome`获取API密钥请查看[此指南](https://github.com/fregante/chrome-webstore-upload-keys) 143 | 144 | 145 | ### 持续集成 146 | 147 | 可以通过在 GitHub Actions 中添加以下 secrets 来同时自动发布到 Chrome Web Store 和 Mozilla Addons: 148 | 149 | - **CLIENT_ID** 和 **CLIENT_SECRET** 以及来自 Google APIs 的 **REFRESH_TOKEN **和 **EXTENSION-ID** 150 | - **WEB_EXT_API_KEY** 和来自 AMO 的 **WEB_EXT_API_SECRET** 151 | 152 | GitHub Actions 工作流将会: 153 | 154 | - 构建扩展 155 | - 将扩展部署到两个商店 156 | 157 | 158 | 借助包含的 [GitHub Action](https://github.com/waahah/VExt/actions) 工作流,CI可以: 159 | 160 | - 在有新的 tag 提交时触发 161 | - 手动触发,只需点击 Actions 标签页中的 “Run workflow” 162 | 163 | 164 | > 立即开始构建跨浏览器扩展项目! ✨ 165 | 166 | ### 🧹使用此项目构建的扩展 167 | - [Meow](https://github.com/waahah/Meow) 168 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 |

🚀 VExt

2 |

🖥 Build "Write once, run in any browser" Web Extension Starter 🔋

3 | 4 | 14 | 15 |

🙋‍♂️ Made by @waahah

16 |
17 | 18 | ![](https://s2.loli.net/2025/04/01/naCU38g5qI9wANK.png) 19 | 20 | ### Features 21 | - ✅ Cross-browser support (Web Extension API) 22 | - 📄 Browser-specific manifest generation 23 | - 🌈 Auto-build and HMR hot reload on code changes 24 | - 📦 Automatic packaging for browser-specific builds 25 | - 🎨 Framework-agnostic 26 | - 🤖 Auto publishing 27 | - ✨ Continuous integration 28 | - 🥢 SASS styling 29 | - 🎯 ES6 module support 30 | - 📊 Smart reload 31 | 32 | 33 | ### Browser Support 34 | 35 | | [![Chrome](https://s2.loli.net/2025/03/31/rAg93eNCOj8P54y.png)](/) | [![Firefox](https://s2.loli.net/2025/03/31/yJiWdqaP6Y5ozsr.png)](/) | [![Opera](https://s2.loli.net/2025/03/31/qKPSGrilQvmgy9p.png)](/) | [![Edge](https://s2.loli.net/2025/03/31/KTsyfa3QDZ6o9dn.png)](/) | [![Yandex](https://s2.loli.net/2025/03/31/arqyo6B2w7VeAld.png)](/) | [![Brave](https://s2.loli.net/2025/03/31/YlTZOBo5AsWqSia.png)](/) | [![vivaldi](https://s2.loli.net/2025/03/31/a9OmnPAipo6F7Yz.png)](/) | 36 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | 37 | | 79 & later ✔ | 52 & later ✔ | 36 & later ✔ | 79 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔ | 38 | 39 | ### 🚀 Get started fast 40 | 41 | Creating a new project with [vext-cli](https://github.com/waahah/vext-cli) is as simple as running the following command: 42 | 43 | ```bash 44 | npm install -g create-vext 45 | # Or use yarn 46 | yarn global add create-vext 47 | # create new project 48 | vext create 49 | ``` 50 | 51 | ### 📦 Prerequisites 52 | 53 | - `node >= 18` 54 | - `npm 9.x+` 55 | 56 | ### 🔧 Install Dependencies 57 | 58 | ```bash 59 | git clone https://gitee.com/waahah/VExt.git 60 | cd VExt 61 | npm install 62 | ``` 63 | 64 | ### Development Mode 65 | 66 | ```bash 67 | npm run dev # Start development server (HMR) 68 | npm run watch:firefox # Firefox hot reload build 69 | npm run watch:chrome # Chrome hot reload build 70 | ``` 71 | 72 | ### Production Build 73 | 74 | ```bash 75 | npm run build:firefox # Build for Firefox 76 | npm run build:chrome # Build for Chrome 77 | ``` 78 | 79 | ### Run Locally 80 | 81 | ```bash 82 | npm run start:firefox # Start a test instance in Firefox 83 | npm run start:chrome # Start a test instance in Chrome 84 | ``` 85 | After running, the corresponding browser will launch and automatically load the project. 86 | 87 | ### Code Quality 88 | 89 | ```bash 90 | npm run lint # Run build and code linting 91 | ``` 92 | 93 | ### Package as ZIP 94 | ```bash 95 | npm run pack:firefox # Package for Firefox 96 | npm run pack:chrome # Package for Chrome 97 | ``` 98 | The packaged files will be output to the `web-ext-artifacts/` directory. 99 | 100 | ### CRX Packaging 101 | ```bash 102 | npm run pack:crx # Generate .crx extension package (requires scripts/crx-pack.js configuration) 103 | ``` 104 | 105 | ### Signing & Publishing 106 | ```bash 107 | npm run sign:firefox # Sign and publish for Firefox (requires API key configuration) 108 | npm run sign:chrome # Sign and publish for Chrome (requires Web Store credentials) 109 | ``` 110 | Automatically uploads the extension for signing and publishing. Signed .XPI files will be output to `web-ext-artifacts/`. 111 | 112 | ### ⚙️ Configuration Guide 113 | 1. **Environment Mode**: Modify the manifest file. 114 | 2. **Build Output**: Default build output is generated in the `/dist` directory. 115 | 3. **Signing Configuration**: Configure credentials in `package.json`: 116 | ``` 117 | Firefox: and 118 | Chrome: 119 | 120 | 121 | 122 | 123 | ``` 124 | - To get `firefox` API keys, refer to [this guide](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#web-ext-sign) 125 | - To get `chrome` API keys, refer to [this guide](https://github.com/fregante/chrome-webstore-upload-keys) 126 | 127 | ### Continuous Integration 128 | 129 | You can automatically publish to both the Chrome Web Store and Mozilla Addons by adding the following secrets in GitHub Actions: 130 | 131 | - **CLIENT_ID** and **CLIENT_SECRET**, along with **REFRESH_TOKEN** and **EXTENSION-ID** from Google APIs 132 | - **WEB_EXT_API_KEY** and **WEB_EXT_API_SECRET** from AMO 133 | 134 | The GitHub Actions workflow will: 135 | 136 | - Build the extension 137 | - Deploy the extension to both stores 138 | 139 | Thanks to the included [GitHub Action](https://github.com/waahah/VExt/actions) workflow, CI can: 140 | 141 | - Trigger when a new tag is committed 142 | - Be triggered manually by clicking "Run workflow" in the Actions tab 143 | 144 | 145 | > Start building your cross-browser extension today! ✨ 146 | 147 | ### 🧹 Extensions Built with This Project 148 | - [Meow](https://github.com/waahah/Meow) 149 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~assets/*": [ 6 | "src/assets/*" 7 | ], 8 | "~background/*": [ 9 | "src/background/*" 10 | ], 11 | "~contentScript/*": [ 12 | "src/contentScript/*" 13 | ], 14 | "~options/*": [ 15 | "src/options/*" 16 | ], 17 | "~popup/*": [ 18 | "src/popup/*" 19 | ], 20 | "~styles/*": [ 21 | "src/styles/*" 22 | ], 23 | "~/*": [ 24 | "src/*" 25 | ] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vext", 3 | "version": "1.0.0", 4 | "description": "Project Website: https://github.com/waahah/Vext", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build:firefox": "vite build --mode firefox", 9 | "build:chrome": "vite build --mode chrome", 10 | "dev": "vite dev", 11 | "watch:firefox": "npm run build:firefox --watch", 12 | "watch:chrome": "npm run build:chrome --watch", 13 | "lint": "npm run build:firefox && web-ext lint --source-dir=dist", 14 | "start:firefox": "npm run build:firefox && web-ext run --source-dir=dist", 15 | "start:chrome": "npm run build:chrome && web-ext run --target=chromium --source-dir=dist", 16 | "pack:firefox": "npm run build:firefox && web-ext build --no-input --ignore-files=package.json package-lock.json --source-dir=dist --artifacts-dir=dist", 17 | "pack:chrome": "npm run build:chrome && web-ext build --no-input --ignore-files=package.json package-lock.json --source-dir=dist --artifacts-dir=dist", 18 | "pack:crx": "npm run build:chrome && node scripts/crx-pack.js", 19 | "sign:firefox": "npm run build:firefox && web-ext sign --source-dir=dist --api-key= --api-secret=", 20 | "sign:chrome": "chrome-webstore-upload-cli --client-id --client-secret --refresh-token --extension-id --zip " 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/waahah/Vext" 25 | }, 26 | "keywords": [], 27 | "author": "", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/waahah/Vext/issues" 31 | }, 32 | "homepage": "https://github.com/waahah/Vext#readme", 33 | "devDependencies": { 34 | "@crxjs/vite-plugin": "^2.0.0-beta.32", 35 | "@rollup/plugin-commonjs": "^28.0.1", 36 | "child_process": "^1.0.2", 37 | "chrome-webstore-upload-cli": "^3.3.1", 38 | "crx": "^5.0.1", 39 | "eslint": "^8.11.0", 40 | "eslint-config-prettier": "^8.5.0", 41 | "eslint-plugin-import": "^2.23.3", 42 | "eslint-plugin-jsx-a11y": "^6.4.1", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-plugin-prettier": "^3.4.0", 45 | "sass": "^1.81.0", 46 | "typescript": "^5.8.2", 47 | "vite": "^5.4.11", 48 | "web-ext": "^8.4.0" 49 | }, 50 | "dependencies": { 51 | "advanced-css-reset": "^1.2.2", 52 | "webext-base-css": "^1.3.1", 53 | "webextension-polyfill": "^0.12.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "VEXT" 4 | }, 5 | "extDescription": { 6 | "message": "Meow" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "VEXT" 4 | }, 5 | "extDescription": { 6 | "message": "Meow" 7 | } 8 | } -------------------------------------------------------------------------------- /public/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waahah/VExt/7622c6d72649427a2a1be10b0b1dd8b32d5574dd/public/icons/icon128.png -------------------------------------------------------------------------------- /public/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waahah/VExt/7622c6d72649427a2a1be10b0b1dd8b32d5574dd/public/icons/icon16.png -------------------------------------------------------------------------------- /public/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waahah/VExt/7622c6d72649427a2a1be10b0b1dd8b32d5574dd/public/icons/icon32.png -------------------------------------------------------------------------------- /public/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waahah/VExt/7622c6d72649427a2a1be10b0b1dd8b32d5574dd/public/icons/icon48.png -------------------------------------------------------------------------------- /scripts/crx-pack.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const crypto = require('crypto'); 4 | 5 | const ChromeExtension = require('crx'); 6 | 7 | const ARTIFACTS_DIR = path.resolve(__dirname, '../web-ext-artifacts'); 8 | const PRIVATE_KEY_PATH = path.resolve(__dirname, '../private-key.pem'); 9 | 10 | if (!fs.existsSync(PRIVATE_KEY_PATH)) { 11 | console.log('Generating new private key...'); 12 | const { privateKey } = crypto.generateKeyPairSync('rsa', { 13 | modulusLength: 2048, 14 | publicKeyEncoding: { type: 'spki', format: 'pem' }, 15 | privateKeyEncoding: { type: 'pkcs8', format: 'pem' } 16 | }); 17 | fs.writeFileSync(PRIVATE_KEY_PATH, privateKey); 18 | } 19 | 20 | if (!fs.existsSync(ARTIFACTS_DIR)) { 21 | fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); 22 | console.log(`Created artifacts directory: ${ARTIFACTS_DIR}`); 23 | } 24 | 25 | const crx = new ChromeExtension({ 26 | codebase: 'http://localhost:8000/myExtension.crx', 27 | privateKey: fs.readFileSync(PRIVATE_KEY_PATH) 28 | }); 29 | 30 | crx.load( path.resolve(__dirname, '../dist') ) 31 | .then(crx => crx.pack()) 32 | .then(crxBuffer => { 33 | const updateXML = crx.generateUpdateXML() 34 | 35 | fs.writeFileSync( 36 | path.join(ARTIFACTS_DIR, 'update.xml'), 37 | updateXML 38 | ); 39 | fs.writeFileSync( 40 | path.join(ARTIFACTS_DIR, 'myExtension.crx'), 41 | crxBuffer 42 | ); 43 | console.log('CRX file created successfully!'); 44 | }) 45 | .catch(err=>{ 46 | console.error( err ); 47 | }); -------------------------------------------------------------------------------- /scripts/manifest.js: -------------------------------------------------------------------------------- 1 | import { defineManifest } from '@crxjs/vite-plugin' 2 | //const targetBrowser = process.env.VITE_BROWSER; 3 | 4 | export default (env) => defineManifest({ 5 | manifest_version: 3, 6 | name: '__MSG_extName__', 7 | version: '1.0.0', 8 | description: '__MSG_extDescription__', 9 | default_locale: 'zh_CN', 10 | permissions: [ 11 | 'bookmarks', 12 | 'storage', 13 | 'favicon', 14 | 'webRequest', 15 | 'webRequestBlocking', 16 | 'activeTab', 17 | "storage", 18 | "tabs", 19 | "notifications", 20 | "scripting" 21 | ], 22 | 23 | host_permissions: [ 24 | "http://*/*", 25 | "https://*/*" 26 | ], 27 | 28 | action: { 29 | default_icon: { 30 | 16: "icons/icon16.png", 31 | 48: "icons/icon48.png", 32 | 128: "icons/icon128.png" 33 | }, 34 | default_title: "__MSG_extName__", 35 | default_popup: "src/popup/index.html" 36 | }, 37 | 38 | background: env.VITE_BROWSER === 'firefox' ? { 39 | scripts: ['src/background/background.js'], 40 | type: 'module' 41 | } : { 42 | service_worker: 'src/background/background.js', 43 | type: 'module' 44 | }, 45 | 46 | ...(env.VITE_BROWSER === 'firefox' ? { 47 | developer: { 48 | name: "your_name", 49 | url: "https://github.com/waahah/VExt" 50 | } 51 | } : { 52 | author: "your_name", 53 | homepage_url: "https://github.com/waahah/VExt" 54 | }), 55 | 56 | ...(env.VITE_BROWSER === 'chrome' && { 57 | options_page: "src/options/index.html", 58 | }), 59 | 60 | options_ui: { 61 | page: "src/options/index.html", 62 | open_in_tab: true 63 | }, 64 | 65 | icons: { 66 | 16: 'icons/icon16.png', 67 | 48: 'icons/icon48.png', 68 | 128: 'icons/icon128.png', 69 | 32: 'icons/icon32.png' 70 | }, 71 | 72 | content_scripts: [ 73 | { 74 | matches: [ 75 | "*://*.bing.com/*", 76 | "*://*.google.com/*" 77 | ], 78 | all_frames: false, 79 | run_at: "document_idle", 80 | js: [ 81 | "src/contentScript/content.js" 82 | ] 83 | } 84 | ], 85 | 86 | ...(env.VITE_BROWSER === 'firefox' && { 87 | browser_specific_settings: { 88 | gecko: { 89 | id: env.VITE_GECKO_ID, 90 | strict_min_version: env.VITE_GECKO_MIN_VER 91 | } 92 | } 93 | }), 94 | 95 | ...(env.VITE_BROWSER === 'chrome' && { 96 | minimum_chrome_version: env.VITE_CHROME_MIN_VER 97 | }), 98 | 99 | web_accessible_resources: [{ 100 | resources: [ 101 | '*.html', 102 | '*.js' 103 | ], 104 | matches: [''], 105 | ...(env.VITE_BROWSER === 'chrome' && { 106 | use_dynamic_url: true 107 | }) 108 | }].filter(Boolean) 109 | }) -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- 1 | import * as browser from 'webextension-polyfill'; 2 | import '../utils/emoji-log'; 3 | 4 | // 添加调试开关 5 | const DEBUG = { 6 | enabled: false // 生产环境默认关闭 7 | }; 8 | 9 | // 添加调试日志函数 10 | function debugLog(...args) { 11 | if (DEBUG.enabled) { 12 | console.log(...args); 13 | } 14 | } 15 | 16 | function debugGroup(...args) { 17 | if (DEBUG.enabled) { 18 | console.group(...args); 19 | } 20 | } 21 | 22 | function debugGroupEnd() { 23 | if (DEBUG.enabled) { 24 | console.groupEnd(); 25 | } 26 | } 27 | 28 | function debugTable(...args) { 29 | if (DEBUG.enabled) { 30 | console.table(...args); 31 | } 32 | } 33 | 34 | // 添加获取本地化消息的辅助函数 35 | function getMessage(messageName, substitutions = null) { 36 | return browser.i18n.getMessage(messageName, substitutions); 37 | } 38 | 39 | // 配置常量 40 | const CONFIG = { 41 | TIMEOUT: { 42 | DEFAULT: 15000, 43 | MIN: 5000, 44 | MAX: 30000 45 | } 46 | }; 47 | 48 | // 添加 onInstalled 事件监听器 49 | browser.runtime.onInstalled.addListener((details) => { 50 | console.emoji('🦄', 'onInstalled....'); 51 | // 仅在首次安装时打开页面 52 | if (details.reason === 'install') { 53 | browser.tabs.create({ 54 | url: browser.runtime.getURL('./src/options/index.html') 55 | }); 56 | } 57 | }); 58 | 59 | // 保留原有的 action 点击事件 60 | browser.action.onClicked.addListener((tab) => { 61 | console.emoji('🦄', 'onClicked....'); 62 | }); 63 | 64 | browser.runtime.onMessage.addListener((_request, _sender, _sendResponse) => { 65 | // Do something with the message! 66 | // alert(request.url); 67 | 68 | // And respond back to the sender. 69 | return Promise.resolve('got your message, thanks!'); 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /src/contentScript/content.js: -------------------------------------------------------------------------------- 1 | import '../utils/emoji-log'; 2 | 3 | console.emoji('🦄', 'start script....'); 4 | console.log('helloworld from content script'); -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Options 8 | 9 | 10 | 11 |
12 |

13 |
14 | 15 |

16 |

17 | 21 |

22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/options/index.js: -------------------------------------------------------------------------------- 1 | import '../utils/emoji-log'; 2 | import './index.scss'; 3 | 4 | console.emoji('🦄', 'Hello World from options main file!'); 5 | 6 | const input = document.getElementById('username'); 7 | const button = document.querySelector('input[name="logging"]'); 8 | button.onclick = () => { 9 | const value = input.value; 10 | console.emoji('🐱', value); 11 | }; -------------------------------------------------------------------------------- /src/options/index.scss: -------------------------------------------------------------------------------- 1 | @use "../styles/fonts"; 2 | @use "../styles/reset"; 3 | @use "../styles/variables"; 4 | 5 | @use "webext-base"; 6 | 7 | body { 8 | color: variables.$black; 9 | background-color: variables.$greyWhite; 10 | } -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Popup 8 | 9 | 10 | 11 | 12 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/popup/index.js: -------------------------------------------------------------------------------- 1 | import * as browser from 'webextension-polyfill'; 2 | import '../utils/emoji-log'; 3 | 4 | import './index.scss'; 5 | 6 | function openWebPage(url) { 7 | return browser.tabs.create({url}); 8 | } 9 | 10 | document.addEventListener('DOMContentLoaded', async () => { 11 | const tabs = await browser.tabs.query({ 12 | active: true, 13 | lastFocusedWindow: true, 14 | }); 15 | 16 | const url = tabs.length && tabs[0].url; 17 | 18 | const response = await browser.runtime.sendMessage({ 19 | msg: 'hello', 20 | url, 21 | }); 22 | 23 | console.emoji('🦄', response); 24 | 25 | document.getElementById('github__button').addEventListener('click', () => { 26 | return openWebPage( 27 | 'https://github.com/waahah/VExt' 28 | ); 29 | }); 30 | 31 | document.getElementById('donate__button').addEventListener('click', () => { 32 | return openWebPage('https://www.waahah.xyz/about'); 33 | }); 34 | 35 | document.getElementById('options__button').addEventListener('click', () => { 36 | return openWebPage(browser.runtime.getURL('./src/options/index.html')); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/popup/index.scss: -------------------------------------------------------------------------------- 1 | @use "../styles/fonts"; 2 | @use "../styles/reset"; 3 | @use "../styles/variables"; 4 | 5 | body { 6 | color: variables.$black; 7 | background-color: variables.$greyWhite; 8 | } 9 | 10 | #popup { 11 | min-width: 350px; 12 | padding: 30px 20px; 13 | 14 | h2 { 15 | font-size: 25px; 16 | text-align: center; 17 | } 18 | 19 | #options__button { 20 | width: 50%; 21 | background: green; 22 | color: white; 23 | font-weight: 500; 24 | border-radius: 15px; 25 | padding: 5px 10px; 26 | justify-content: center; 27 | margin: 20px auto; 28 | cursor: pointer; 29 | opacity: 0.8; 30 | display: flex; 31 | } 32 | 33 | .links__holder { 34 | ul { 35 | display: flex; 36 | margin-top: 1em; 37 | justify-content: space-around; 38 | 39 | li { 40 | button { 41 | border-radius: 25px; 42 | font-size: 20px; 43 | font-weight: 600; 44 | padding: 10px 17px; 45 | background-color: rgba(0, 0, 255, 0.7); 46 | color: white; 47 | cursor: pointer; 48 | } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/styles/_components.scss: -------------------------------------------------------------------------------- 1 | .d-none { 2 | display: none !important; 3 | } 4 | 5 | .v-none { 6 | visibility: hidden !important; 7 | } 8 | 9 | .text-center { 10 | text-align: center; 11 | } 12 | 13 | .mt-3 { 14 | margin-top: 3em; 15 | } 16 | 17 | .mb-2 { 18 | margin-bottom: 2em; 19 | } 20 | 21 | .my-2 { 22 | margin-top: 1em; 23 | margin-bottom: 2em; 24 | } 25 | 26 | .py-2 { 27 | padding: 1em 24px; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Nunito:400,600"); 2 | -------------------------------------------------------------------------------- /src/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | //@import "~advanced-css-reset/dist/reset.css"; 2 | @use "advanced-css-reset"; 3 | // Add your custom reset rules here 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | } -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $black: #0d0d0d; 3 | $greyWhite: #f3f3f3; 4 | $skyBlue: #8892b0; 5 | 6 | // fonts 7 | $nunito: "Nunito", sans-serif; 8 | 9 | // font weights 10 | $thin: 100; 11 | $exlight: 200; 12 | $light: 300; 13 | $regular: 400; 14 | $medium: 500; 15 | $semibold: 600; 16 | $bold: 700; 17 | $exbold: 800; 18 | $exblack: 900; 19 | 20 | // other 21 | -------------------------------------------------------------------------------- /src/utils/emoji-log.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * emoji-log 4 | * 5 | * @license MIT License 6 | * 7 | * Art by Colin J. Randall 8 | * 9 | * \ 10 | * \ 11 | * \\ 12 | * \\ 13 | * >\/7 14 | * _.-(6' \ 15 | * (=___._/` \ 16 | * ) \ | 17 | * / / | 18 | * / > / 19 | * j < _\ 20 | * _.-' : ``. 21 | * \ r=._\ `. 22 | * <`\\_ \ .`-. 23 | * \ r-7 `-. ._ ' . `\ 24 | * \`, `-.`7 7) ) 25 | * \/ \| \' / `-._ 26 | * || .' 27 | * \\ ( 28 | * >\ > 29 | * ,.-' >.' 30 | * <.'_.'' 31 | * <' 32 | * 33 | */ 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | // For TS to stop screaming 36 | class CustomConsole { 37 | } 38 | exports.CustomConsole = CustomConsole; 39 | var Constants; 40 | (function (Constants) { 41 | Constants[Constants["LINE_LENGTH_VARIABLE"] = 0.66] = "LINE_LENGTH_VARIABLE"; 42 | Constants[Constants["DEFAULT_LINE_LENGTH"] = 4] = "DEFAULT_LINE_LENGTH"; 43 | Constants[Constants["ONE"] = 1] = "ONE"; 44 | Constants[Constants["TWO"] = 2] = "TWO"; 45 | Constants[Constants["THREE"] = 3] = "THREE"; 46 | })(Constants || (Constants = {})); 47 | function instanceOfError(e) { 48 | return (e && (e === null || e === void 0 ? void 0 : e.stack) && (e === null || e === void 0 ? void 0 : e.message) && 49 | typeof e.stack === 'string' && 50 | typeof e.message === 'string'); 51 | } 52 | function logToConsole(error, emoji = '🐶', length) { 53 | // ToDo: optionally log time & date 54 | const isError = instanceOfError(error); 55 | const message = isError ? error.message : error; 56 | const len = length || 57 | (error === null || error === void 0 ? void 0 : error.toString().length) * Constants.LINE_LENGTH_VARIABLE || 58 | Constants.DEFAULT_LINE_LENGTH; 59 | console.log(` 60 | /‾${`‾‾`.repeat(len)}‾ 61 | ${emoji} < `, message, ` 62 | \\_${`__`.repeat(len)}_ 63 | `); 64 | if (isError) { 65 | // Node.js 66 | if (typeof window !== 'undefined') { 67 | console.groupCollapsed(`${emoji} > Stack Trace:`); 68 | console.error(error.stack); 69 | console.groupEnd(); 70 | } 71 | else { 72 | console.log(`${emoji} > Stack Trace:`); 73 | console.error(error.stack); 74 | } 75 | } 76 | } 77 | console.emoji = function (...args) { 78 | const log = []; 79 | log[0] = function () { 80 | logToConsole('Meow', '🐱'); 81 | return this; 82 | }; 83 | log[Constants.ONE] = function (error) { 84 | logToConsole(error); 85 | return this; 86 | }; 87 | log[Constants.TWO] = function (emoji, error) { 88 | logToConsole(error, emoji); 89 | return this; 90 | }; 91 | log[Constants.THREE] = function (emoji, error, length) { 92 | logToConsole(error, emoji, length); 93 | return this; 94 | }; 95 | this.emoji = function (...passedArgs) { 96 | // call according to index 97 | log[passedArgs.length](...passedArgs); 98 | return this; 99 | }; 100 | this.emoji(...args); 101 | return this; 102 | }; 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": [ 7 | "ES2020", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "preserve", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": false, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": false, 24 | "types": [ 25 | "" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "src/**/*.tsx", 31 | "src/**/*.vue" 32 | ], 33 | "references": [ 34 | { 35 | "path": "./tsconfig.node.json" 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": [ 11 | //"vite.config.ts" 12 | ] 13 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, loadEnv } from 'vite'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import { crx } from '@crxjs/vite-plugin'; 5 | import manifest from './scripts/manifest'; 6 | 7 | export default defineConfig(({ mode }) =>{ 8 | const env = loadEnv(mode, process.cwd(), 'VITE_'); 9 | return { 10 | build: { 11 | outDir: 'dist', // 指定输出目录 12 | assetsDir: 'assets', // 静态资源目录 13 | rollupOptions: { 14 | input: { 15 | background: resolve(__dirname, 'src/background/background.js'), 16 | contentScript: resolve(__dirname, 'src/contentScript/content.js'), 17 | options: resolve(__dirname, 'src/options/index.html'), 18 | popup: resolve(__dirname, 'src/popup/index.html') 19 | }, 20 | output: { 21 | entryFileNames: 'js/[name].bundle.js' // 控制输出文件名 22 | //inlineDynamicImports: true, // 将动态导入内联 23 | //manualChunks: undefined, // 禁止代码分块 24 | }, 25 | //external: ['commonjsHelpers-*.js'], // 将辅助文件排除 26 | }, 27 | commonjsOptions: { 28 | requireReturnsDefault: 'auto', 29 | transformMixedEsModules: true 30 | //include: [/node_modules/], // 确保 CommonJS 依赖也被转换 31 | } 32 | }, 33 | publicDir: 'public', // 静态文件目录,自动拷贝到 dist 34 | plugins: [ 35 | crx({ 36 | manifest: manifest(env) 37 | }), 38 | // 转换 CommonJS 模块 39 | commonjs({ 40 | include: ['node_modules/**'], 41 | exclude: ['node_modules/webextension-polyfill/**'], 42 | transformMixedEsModules: true, 43 | requireReturnsDefault: 'auto' 44 | //include: /node_modules/, 45 | // 其他配置 46 | }) 47 | ], 48 | resolve: { 49 | alias: { 50 | 'webextension-polyfill': './node_modules/webextension-polyfill/dist/browser-polyfill.js', 51 | 'advanced-css-reset': resolve(__dirname, './node_modules/advanced-css-reset/dist/reset.css'), 52 | 'webext-base': resolve(__dirname, './node_modules/webext-base-css/webext-base.css') 53 | }, 54 | }, 55 | } 56 | 57 | }); --------------------------------------------------------------------------------