├── .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 |
13 |
14 |
15 |
16 | 
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 | | [](/) | [](/) | [](/) | [](/) | [](/) | [](/) | [](/) |
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 |
16 |
17 |
18 | 
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 | | [](/) | [](/) | [](/) | [](/) | [](/) | [](/) | [](/) |
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 |
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 | });
--------------------------------------------------------------------------------