Init
This commit is contained in:
parent
7a4009bc4d
commit
b15e10aa15
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vite-ssg-dist
|
||||
.vite-ssg-temp
|
||||
*.crx
|
||||
*.local
|
||||
*.log
|
||||
*.pem
|
||||
*.xpi
|
||||
*.zip
|
||||
dist
|
||||
dist-ssr
|
||||
extension/manifest.json
|
||||
node_modules
|
||||
src/auto-imports.d.ts
|
||||
src/components.d.ts
|
||||
.eslintcache
|
||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"antfu.iconify",
|
||||
"antfu.unocss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"csstools.postcss"
|
||||
]
|
||||
}
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"cSpell.words": ["Vitesse"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"vite.autoStart": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"files.associations": {
|
||||
"*.css": "postcss"
|
||||
}
|
||||
}
|
||||
21
LICENSE.~1~
Normal file
21
LICENSE.~1~
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Anthony Fu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
132
README.md.~1~
Normal file
132
README.md.~1~
Normal file
@ -0,0 +1,132 @@
|
||||
# WebExtension Vite Starter
|
||||
|
||||
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.
|
||||
|
||||
<p align="center">
|
||||
<sub>Popup</sub><br/>
|
||||
<img width="655" src="https://user-images.githubusercontent.com/11247099/126741643-813b3773-17ff-4281-9737-f319e00feddc.png"><br/>
|
||||
<sub>Options Page</sub><br/>
|
||||
<img width="655" src="https://user-images.githubusercontent.com/11247099/126741653-43125b62-6578-4452-83a7-bee19be2eaa2.png"><br/>
|
||||
<sub>Inject Vue App into the Content Script</sub><br/>
|
||||
<img src="https://user-images.githubusercontent.com/11247099/130695439-52418cf0-e186-4085-8e19-23fe808a274e.png">
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- ⚡️ **Instant HMR** - use **Vite** on dev (no more refresh!)
|
||||
- 🥝 Vue 3 - Composition API, [`<script setup>` syntax](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md) and more!
|
||||
- 💬 Effortless communications - powered by [`webext-bridge`](https://github.com/antfu/webext-bridge) and [VueUse](https://github.com/antfu/vueuse) storage
|
||||
- 🌈 [UnoCSS](https://github.com/unocss/unocss) - The instant on-demand Atomic CSS engine.
|
||||
- 🦾 [TypeScript](https://www.typescriptlang.org/) - type safe
|
||||
- 📦 [Components auto importing](./src/components)
|
||||
- 🌟 [Icons](./src/components) - Access to icons from any iconset directly
|
||||
- 🖥 Content Script - Use Vue even in content script
|
||||
- 🌍 WebExtension - isomorphic extension for Chrome, Firefox, and others
|
||||
- 📃 Dynamic `manifest.json` with full type support
|
||||
|
||||
## Pre-packed
|
||||
|
||||
### WebExtension Libraries
|
||||
|
||||
- [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) - WebExtension browser API Polyfill with types
|
||||
- [`webext-bridge`](https://github.com/antfu/webext-bridge) - effortlessly communication between contexts
|
||||
|
||||
### Vite Plugins
|
||||
|
||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use `browser` and Vue Composition API without importing
|
||||
- [`unplugin-vue-components`](https://github.com/antfu/vite-plugin-components) - components auto import
|
||||
- [`unplugin-icons`](https://github.com/antfu/unplugin-icons) - icons as components
|
||||
- [Iconify](https://iconify.design) - use icons from any icon sets [🔍Icônes](https://icones.netlify.app/)
|
||||
|
||||
### Vue Plugins
|
||||
|
||||
- [VueUse](https://github.com/antfu/vueuse) - collection of useful composition APIs
|
||||
|
||||
### UI Frameworks
|
||||
|
||||
- [UnoCSS](https://github.com/unocss/unocss) - the instant on-demand Atomic CSS engine
|
||||
|
||||
### Coding Style
|
||||
|
||||
- Use Composition API with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||
- [ESLint](https://eslint.org/) with [@antfu/eslint-config](https://github.com/antfu/eslint-config), single quotes, no semi
|
||||
|
||||
### Dev tools
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
|
||||
- [esno](https://github.com/antfu/esno) - TypeScript / ESNext node runtime powered by esbuild
|
||||
- [npm-run-all](https://github.com/mysticatea/npm-run-all) - Run multiple npm-scripts in parallel or sequential
|
||||
- [web-ext](https://github.com/mozilla/web-ext) - Streamlined experience for developing web extensions
|
||||
|
||||
## Use the Template
|
||||
|
||||
### GitHub Template
|
||||
|
||||
[Create a repo from this template on GitHub](https://github.com/antfu/vitesse-webext/generate).
|
||||
|
||||
### Clone to local
|
||||
|
||||
If you prefer to do it manually with the cleaner git history
|
||||
|
||||
> If you don't have pnpm installed, run: npm install -g pnpm
|
||||
|
||||
```bash
|
||||
npx degit antfu/vitesse-webext my-webext
|
||||
cd my-webext
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Folders
|
||||
|
||||
- `src` - main source.
|
||||
- `contentScript` - scripts and components to be injected as `content_script`
|
||||
- `background` - scripts for background.
|
||||
- `components` - auto-imported Vue components that are shared in popup and options page.
|
||||
- `styles` - styles shared in popup and options page
|
||||
- `assets` - assets used in Vue components
|
||||
- `manifest.ts` - manifest for the extension.
|
||||
- `extension` - extension package root.
|
||||
- `assets` - static assets (mainly for `manifest.json`).
|
||||
- `dist` - built files, also serve stub entry for Vite on development.
|
||||
- `scripts` - development and bundling helper scripts.
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Then **load extension in browser with the `extension/` folder**.
|
||||
|
||||
For Firefox developers, you can run the following command instead:
|
||||
|
||||
```bash
|
||||
pnpm start:firefox
|
||||
```
|
||||
|
||||
`web-ext` auto reload the extension when `extension/` files changed.
|
||||
|
||||
> 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.
|
||||
|
||||
### Build
|
||||
|
||||
To build the extension, run
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
And then pack files under `extension`, you can upload `extension.crx` or `extension.xpi` to appropriate extension store.
|
||||
|
||||
## Credits
|
||||
|
||||
[](https://volta.net)
|
||||
|
||||
This template is originally made for the [volta.net](https://volta.net) browser extension.
|
||||
|
||||
## Variations
|
||||
|
||||
This is a variant of [Vitesse](https://github.com/antfu/vitesse), check out the [full variations list](https://github.com/antfu/vitesse#variations).
|
||||
20
e2e/basic.spec.ts
Normal file
20
e2e/basic.spec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { expect, isDevArtifact, name, test } from './fixtures'
|
||||
|
||||
test('example test', async ({ page }, testInfo) => {
|
||||
testInfo.skip(!isDevArtifact(), 'contentScript is in closed ShadowRoot mode')
|
||||
|
||||
await page.goto('https://example.com')
|
||||
|
||||
await page.locator(`#${name} button`).click()
|
||||
await expect(page.locator(`#${name} h1`)).toHaveText('Vitesse WebExt')
|
||||
})
|
||||
|
||||
test('popup page', async ({ page, extensionId }) => {
|
||||
await page.goto(`chrome-extension://${extensionId}/dist/popup/index.html`)
|
||||
await expect(page.locator('button')).toHaveText('Open Options')
|
||||
})
|
||||
|
||||
test('options page', async ({ page, extensionId }) => {
|
||||
await page.goto(`chrome-extension://${extensionId}/dist/options/index.html`)
|
||||
await expect(page.locator('img')).toHaveAttribute('alt', 'extension icon')
|
||||
})
|
||||
48
e2e/fixtures.ts
Normal file
48
e2e/fixtures.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import path from 'node:path'
|
||||
import { setTimeout as sleep } from 'node:timers/promises'
|
||||
import fs from 'fs-extra'
|
||||
import { type BrowserContext, test as base, chromium } from '@playwright/test'
|
||||
import type { Manifest } from 'webextension-polyfill'
|
||||
|
||||
export { name } from '../package.json'
|
||||
|
||||
export const extensionPath = path.join(__dirname, '../extension')
|
||||
|
||||
export const test = base.extend<{
|
||||
context: BrowserContext
|
||||
extensionId: string
|
||||
}>({
|
||||
context: async ({ headless }, use) => {
|
||||
// workaround for the Vite server has started but contentScript is not yet.
|
||||
await sleep(1000)
|
||||
const context = await chromium.launchPersistentContext('', {
|
||||
headless,
|
||||
args: [
|
||||
...(headless ? ['--headless=new'] : []),
|
||||
`--disable-extensions-except=${extensionPath}`,
|
||||
`--load-extension=${extensionPath}`,
|
||||
],
|
||||
})
|
||||
await use(context)
|
||||
await context.close()
|
||||
},
|
||||
extensionId: async ({ context }, use) => {
|
||||
// for manifest v3:
|
||||
let [background] = context.serviceWorkers()
|
||||
if (!background)
|
||||
background = await context.waitForEvent('serviceworker')
|
||||
|
||||
const extensionId = background.url().split('/')[2]
|
||||
await use(extensionId)
|
||||
},
|
||||
})
|
||||
|
||||
export const expect = test.expect
|
||||
|
||||
export function isDevArtifact() {
|
||||
const manifest: Manifest.WebExtensionManifest = fs.readJsonSync(path.resolve(extensionPath, 'manifest.json'))
|
||||
return Boolean(
|
||||
typeof manifest.content_security_policy === 'object'
|
||||
&& manifest.content_security_policy.extension_pages?.includes('localhost'),
|
||||
)
|
||||
}
|
||||
BIN
extension/assets/icon-512.png
Normal file
BIN
extension/assets/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
3
extension/assets/icon.svg
Normal file
3
extension/assets/icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.6667 1.66667H24V7H8V9.66667H5.33333V20.3333H8V23H10.6667V28.3333H21.3333V25.6667H26.6667V23H21.3333V20.3333H26.6667V17.6667H21.3333V15H10.6667V20.3333H8V9.66667H24V7H26.6667V1.66667ZM18.6667 25.6667H13.3333V17.6667H18.6667V25.6667Z" fill="#888888"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
10
modules.d.ts
vendored
Normal file
10
modules.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$app: {
|
||||
context: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/64189046/479957
|
||||
export {}
|
||||
77
package.json
Normal file
77
package.json
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "vitesse-webext",
|
||||
"displayName": "Vitesse WebExt",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@8.3.1",
|
||||
"description": "[description]",
|
||||
"scripts": {
|
||||
"dev": "npm run clear && cross-env NODE_ENV=development run-p dev:*",
|
||||
"dev:prepare": "esno scripts/prepare.ts",
|
||||
"dev:background": "npm run build:background -- --mode development",
|
||||
"dev:web": "vite",
|
||||
"dev:js": "npm run build:js -- --mode development",
|
||||
"build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:background build:js",
|
||||
"build:prepare": "esno scripts/prepare.ts",
|
||||
"build:background": "vite build --config vite.config.background.ts",
|
||||
"build:web": "vite build",
|
||||
"build:js": "vite build --config vite.config.content.ts",
|
||||
"pack": "cross-env NODE_ENV=production run-p pack:*",
|
||||
"pack:zip": "rimraf extension.zip && jszip-cli add extension/* -o ./extension.zip",
|
||||
"pack:crx": "crx pack extension -o ./extension.crx",
|
||||
"pack:xpi": "cross-env WEB_EXT_ARTIFACTS_DIR=./ web-ext build --source-dir ./extension --filename extension.xpi --overwrite-dest",
|
||||
"start:chromium": "web-ext run --source-dir ./extension --target=chromium",
|
||||
"start:firefox": "web-ext run --source-dir ./extension --target=firefox-desktop",
|
||||
"clear": "rimraf --glob extension/dist extension/manifest.json extension.*",
|
||||
"lint": "eslint --cache .",
|
||||
"test": "vitest test",
|
||||
"test:e2e": "playwright test",
|
||||
"postinstall": "simple-git-hooks",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.36.0",
|
||||
"@ffflorian/jszip-cli": "^3.4.1",
|
||||
"@iconify/json": "^2.2.61",
|
||||
"@playwright/test": "^1.33.0",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^18.16.5",
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@unocss/reset": "^0.51.12",
|
||||
"@vitejs/plugin-vue": "^4.2.1",
|
||||
"@vue/compiler-sfc": "^3.2.47",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"crx": "^5.0.1",
|
||||
"eslint": "^8.40.0",
|
||||
"esno": "^0.16.3",
|
||||
"fs-extra": "^11.1.1",
|
||||
"jsdom": "^21.1.2",
|
||||
"kolorist": "^1.8.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rimraf": "^4.4.1",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"typescript": "^4.9.5",
|
||||
"unocss": "^0.51.12",
|
||||
"unplugin-auto-import": "^0.15.3",
|
||||
"unplugin-icons": "^0.16.1",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^4.3.5",
|
||||
"vitest": "^0.31.0",
|
||||
"vue": "^3.2.47",
|
||||
"vue-demi": "^0.14.0",
|
||||
"web-ext": "^7.6.2",
|
||||
"webext-bridge": "^6.0.1",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
}
|
||||
15
playwright.config.ts
Normal file
15
playwright.config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @see {@link https://playwright.dev/docs/chrome-extensions Chrome extensions | Playwright}
|
||||
*/
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
retries: 2,
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
// start e2e test after the Vite server is fully prepared
|
||||
url: 'http://localhost:3303/popup/main.ts',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
})
|
||||
7370
pnpm-lock.yaml
generated
Normal file
7370
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
scripts/manifest.ts
Normal file
10
scripts/manifest.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import fs from 'fs-extra'
|
||||
import { getManifest } from '../src/manifest'
|
||||
import { log, r } from './utils'
|
||||
|
||||
export async function writeManifest() {
|
||||
await fs.writeJSON(r('extension/manifest.json'), await getManifest(), { spaces: 2 })
|
||||
log('PRE', 'write manifest.json')
|
||||
}
|
||||
|
||||
writeManifest()
|
||||
44
scripts/prepare.ts
Normal file
44
scripts/prepare.ts
Normal file
@ -0,0 +1,44 @@
|
||||
// generate stub index.html files for dev entry
|
||||
import { execSync } from 'node:child_process'
|
||||
import fs from 'fs-extra'
|
||||
import chokidar from 'chokidar'
|
||||
import { isDev, log, port, r } from './utils'
|
||||
|
||||
/**
|
||||
* Stub index.html to use Vite in development
|
||||
*/
|
||||
async function stubIndexHtml() {
|
||||
const views = [
|
||||
'options',
|
||||
'popup',
|
||||
'background',
|
||||
]
|
||||
|
||||
for (const view of views) {
|
||||
await fs.ensureDir(r(`extension/dist/${view}`))
|
||||
let data = await fs.readFile(r(`src/${view}/index.html`), 'utf-8')
|
||||
data = data
|
||||
.replace('"./main.ts"', `"http://localhost:${port}/${view}/main.ts"`)
|
||||
.replace('<div id="app"></div>', '<div id="app">Vite server did not start</div>')
|
||||
await fs.writeFile(r(`extension/dist/${view}/index.html`), data, 'utf-8')
|
||||
log('PRE', `stub ${view}`)
|
||||
}
|
||||
}
|
||||
|
||||
function writeManifest() {
|
||||
execSync('npx esno ./scripts/manifest.ts', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
writeManifest()
|
||||
|
||||
if (isDev) {
|
||||
stubIndexHtml()
|
||||
chokidar.watch(r('src/**/*.html'))
|
||||
.on('change', () => {
|
||||
stubIndexHtml()
|
||||
})
|
||||
chokidar.watch([r('src/manifest.ts'), r('package.json')])
|
||||
.on('change', () => {
|
||||
writeManifest()
|
||||
})
|
||||
}
|
||||
10
scripts/utils.ts
Normal file
10
scripts/utils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { bgCyan, black } from 'kolorist'
|
||||
|
||||
export const port = parseInt(process.env.PORT || '') || 3303
|
||||
export const r = (...args: string[]) => resolve(__dirname, '..', ...args)
|
||||
export const isDev = process.env.NODE_ENV !== 'production'
|
||||
|
||||
export function log(name: string, message: string) {
|
||||
console.log(black(bgCyan(` ${name} `)), message)
|
||||
}
|
||||
10
shim.d.ts
vendored
Normal file
10
shim.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import type { ProtocolWithReturn } from 'webext-bridge'
|
||||
|
||||
declare module 'webext-bridge' {
|
||||
export interface ProtocolMap {
|
||||
// define message protocol types
|
||||
// see https://github.com/antfu/webext-bridge#type-safe-protocols
|
||||
'tab-prev': { title: string | undefined }
|
||||
'get-current-tab': ProtocolWithReturn<{ tabId: number }, { title?: string }>
|
||||
}
|
||||
}
|
||||
3
src/assets/icon.svg
Normal file
3
src/assets/icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.6667 1.66667H24V7H8V9.66667H5.33333V20.3333H8V23H10.6667V28.3333H21.3333V25.6667H26.6667V23H21.3333V20.3333H26.6667V17.6667H21.3333V15H10.6667V20.3333H8V9.66667H24V7H26.6667V1.66667ZM18.6667 25.6667H13.3333V17.6667H18.6667V25.6667Z" fill="#69717d"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
18
src/background/contentScriptHMR.ts
Normal file
18
src/background/contentScriptHMR.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { isFirefox, isForbiddenUrl } from '~/env'
|
||||
|
||||
// Firefox fetch files from cache instead of reloading changes from disk,
|
||||
// hmr will not work as Chromium based browser
|
||||
browser.webNavigation.onCommitted.addListener(({ tabId, frameId, url }) => {
|
||||
// Filter out non main window events.
|
||||
if (frameId !== 0)
|
||||
return
|
||||
|
||||
if (isForbiddenUrl(url))
|
||||
return
|
||||
|
||||
// inject the latest scripts
|
||||
browser.tabs.executeScript(tabId, {
|
||||
file: `${isFirefox ? '' : '.'}/dist/contentScripts/index.global.js`,
|
||||
runAt: 'document_end',
|
||||
}).catch(error => console.error(error))
|
||||
})
|
||||
12
src/background/index.html
Normal file
12
src/background/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base target="_blank">
|
||||
<title>Background</title>
|
||||
</head>
|
||||
<body style="min-width: 100px">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
src/background/main.ts
Normal file
54
src/background/main.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { onMessage, sendMessage } from 'webext-bridge/background'
|
||||
import type { Tabs } from 'webextension-polyfill'
|
||||
|
||||
// only on dev mode
|
||||
if (import.meta.hot) {
|
||||
// @ts-expect-error for background HMR
|
||||
import('/@vite/client')
|
||||
// load latest content script
|
||||
import('./contentScriptHMR')
|
||||
}
|
||||
|
||||
browser.runtime.onInstalled.addListener((): void => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Extension installed')
|
||||
})
|
||||
|
||||
let previousTabId = 0
|
||||
|
||||
// communication example: send previous tab title from background page
|
||||
// see shim.d.ts for type declaration
|
||||
browser.tabs.onActivated.addListener(async ({ tabId }) => {
|
||||
if (!previousTabId) {
|
||||
previousTabId = tabId
|
||||
return
|
||||
}
|
||||
|
||||
let tab: Tabs.Tab
|
||||
|
||||
try {
|
||||
tab = await browser.tabs.get(previousTabId)
|
||||
previousTabId = tabId
|
||||
}
|
||||
catch {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('previous tab', tab)
|
||||
sendMessage('tab-prev', { title: tab.title }, { context: 'content-script', tabId })
|
||||
})
|
||||
|
||||
onMessage('get-current-tab', async () => {
|
||||
try {
|
||||
const tab = await browser.tabs.get(previousTabId)
|
||||
return {
|
||||
title: tab?.title,
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
title: undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
5
src/components/Logo.vue
Normal file
5
src/components/Logo.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<a class="icon-btn mx-2 text-2xl" rel="noreferrer" href="https://github.com/antfu/vitesse-webext" target="_blank" title="GitHub">
|
||||
<pixelarticons-power />
|
||||
</a>
|
||||
</template>
|
||||
11
src/components/README.md
Normal file
11
src/components/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
## Components
|
||||
|
||||
Components in this dir will be auto-registered and on-demand, powered by [`vite-plugin-components`](https://github.com/antfu/vite-plugin-components).
|
||||
|
||||
Components can be shared in all views.
|
||||
|
||||
### Icons
|
||||
|
||||
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
|
||||
|
||||
It will only bundle the icons you use. Check out [vite-plugin-icons](https://github.com/antfu/vite-plugin-icons) for more details.
|
||||
5
src/components/SharedSubtitle.vue
Normal file
5
src/components/SharedSubtitle.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<p class="mt-2 opacity-50">
|
||||
This is the {{ $app.context }} page
|
||||
</p>
|
||||
</template>
|
||||
11
src/components/__tests__/Logo.test.ts
Normal file
11
src/components/__tests__/Logo.test.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Logo from '../Logo.vue'
|
||||
|
||||
describe('Logo Component', () => {
|
||||
it('should render', () => {
|
||||
const wrapper = mount(Logo)
|
||||
|
||||
expect(wrapper.html()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
30
src/composables/useStorageLocal.ts
Normal file
30
src/composables/useStorageLocal.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { storage } from 'webextension-polyfill'
|
||||
import type {
|
||||
MaybeRef,
|
||||
RemovableRef,
|
||||
StorageLikeAsync,
|
||||
UseStorageAsyncOptions,
|
||||
} from '@vueuse/core'
|
||||
import {
|
||||
useStorageAsync,
|
||||
} from '@vueuse/core'
|
||||
|
||||
const storageLocal: StorageLikeAsync = {
|
||||
removeItem(key: string) {
|
||||
return storage.local.remove(key)
|
||||
},
|
||||
|
||||
setItem(key: string, value: string) {
|
||||
return storage.local.set({ [key]: value })
|
||||
},
|
||||
|
||||
async getItem(key: string) {
|
||||
return (await storage.local.get(key))[key]
|
||||
},
|
||||
}
|
||||
|
||||
export const useStorageLocal = <T>(
|
||||
key: string,
|
||||
initialValue: MaybeRef<T>,
|
||||
options?: UseStorageAsyncOptions<T>,
|
||||
): RemovableRef<T> => useStorageAsync(key, initialValue, storageLocal, options)
|
||||
30
src/contentScripts/index.ts
Normal file
30
src/contentScripts/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/* eslint-disable no-console */
|
||||
import { onMessage } from 'webext-bridge/content-script'
|
||||
import { createApp } from 'vue'
|
||||
import App from './views/App.vue'
|
||||
import { setupApp } from '~/logic/common-setup'
|
||||
|
||||
// Firefox `browser.tabs.executeScript()` requires scripts return a primitive value
|
||||
(() => {
|
||||
console.info('[vitesse-webext] Hello world from content script')
|
||||
|
||||
// communication example: send previous tab title from background page
|
||||
onMessage('tab-prev', ({ data }) => {
|
||||
console.log(`[vitesse-webext] Navigate from page "${data.title}"`)
|
||||
})
|
||||
|
||||
// mount component to context window
|
||||
const container = document.createElement('div')
|
||||
container.id = __NAME__
|
||||
const root = document.createElement('div')
|
||||
const styleEl = document.createElement('link')
|
||||
const shadowDOM = container.attachShadow?.({ mode: __DEV__ ? 'open' : 'closed' }) || container
|
||||
styleEl.setAttribute('rel', 'stylesheet')
|
||||
styleEl.setAttribute('href', browser.runtime.getURL('dist/contentScripts/style.css'))
|
||||
shadowDOM.appendChild(styleEl)
|
||||
shadowDOM.appendChild(root)
|
||||
document.body.appendChild(container)
|
||||
const app = createApp(App)
|
||||
setupApp(app)
|
||||
app.mount(root)
|
||||
})()
|
||||
30
src/contentScripts/views/App.vue
Normal file
30
src/contentScripts/views/App.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { useToggle } from '@vueuse/core'
|
||||
import 'uno.css'
|
||||
|
||||
const [show, toggle] = useToggle(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed right-0 bottom-0 m-5 z-100 flex items-end font-sans select-none leading-1em">
|
||||
<div
|
||||
class="bg-white text-gray-800 rounded-lg shadow w-max h-min"
|
||||
p="x-4 y-2"
|
||||
m="y-auto r-2"
|
||||
transition="opacity duration-300"
|
||||
:class="show ? 'opacity-100' : 'opacity-0'"
|
||||
>
|
||||
<h1 class="text-lg">
|
||||
Vitesse WebExt
|
||||
</h1>
|
||||
<SharedSubtitle />
|
||||
</div>
|
||||
<button
|
||||
class="flex w-10 h-10 rounded-full shadow cursor-pointer border-none"
|
||||
bg="teal-600 hover:teal-700"
|
||||
@click="toggle()"
|
||||
>
|
||||
<pixelarticons-power class="block m-auto text-white text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
14
src/env.ts
Normal file
14
src/env.ts
Normal file
@ -0,0 +1,14 @@
|
||||
const forbiddenProtocols = [
|
||||
'chrome-extension://',
|
||||
'chrome-search://',
|
||||
'chrome://',
|
||||
'devtools://',
|
||||
'edge://',
|
||||
'https://chrome.google.com/webstore',
|
||||
]
|
||||
|
||||
export function isForbiddenUrl(url: string): boolean {
|
||||
return forbiddenProtocols.some(protocol => url.startsWith(protocol))
|
||||
}
|
||||
|
||||
export const isFirefox = navigator.userAgent.includes('Firefox')
|
||||
8
src/global.d.ts
vendored
Normal file
8
src/global.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
declare const __DEV__: boolean
|
||||
/** Extension name, defined in packageJson.name */
|
||||
declare const __NAME__: string
|
||||
|
||||
declare module '*.vue' {
|
||||
const component: any
|
||||
export default component
|
||||
}
|
||||
15
src/logic/common-setup.ts
Normal file
15
src/logic/common-setup.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { App } from 'vue'
|
||||
|
||||
export function setupApp(app: App) {
|
||||
// Inject a globally available `$app` object in template
|
||||
app.config.globalProperties.$app = {
|
||||
context: '',
|
||||
}
|
||||
|
||||
// Provide access to `app` in script setup with `const app = inject('app')`
|
||||
app.provide('app', app.config.globalProperties.$app)
|
||||
|
||||
// Here you can install additional plugins for all contexts: popup, options page and content-script.
|
||||
// example: app.use(i18n)
|
||||
// example excluding content-script context: if (context !== 'content-script') app.use(i18n)
|
||||
}
|
||||
1
src/logic/index.ts
Normal file
1
src/logic/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './storage'
|
||||
3
src/logic/storage.ts
Normal file
3
src/logic/storage.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { useStorageLocal } from '~/composables/useStorageLocal'
|
||||
|
||||
export const storageDemo = useStorageLocal('webext-demo', 'Storage Demo')
|
||||
72
src/manifest.ts
Normal file
72
src/manifest.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import fs from 'fs-extra'
|
||||
import type { Manifest } from 'webextension-polyfill'
|
||||
import type PkgType from '../package.json'
|
||||
import { isDev, port, r } from '../scripts/utils'
|
||||
|
||||
export async function getManifest() {
|
||||
const pkg = await fs.readJSON(r('package.json')) as typeof PkgType
|
||||
|
||||
// update this file to update this manifest.json
|
||||
// can also be conditional based on your need
|
||||
const manifest: Manifest.WebExtensionManifest = {
|
||||
manifest_version: 3,
|
||||
name: pkg.displayName || pkg.name,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
action: {
|
||||
default_icon: './assets/icon-512.png',
|
||||
default_popup: './dist/popup/index.html',
|
||||
},
|
||||
options_ui: {
|
||||
page: './dist/options/index.html',
|
||||
open_in_tab: true,
|
||||
},
|
||||
background: {
|
||||
service_worker: './dist/background/index.mjs',
|
||||
},
|
||||
icons: {
|
||||
16: './assets/icon-512.png',
|
||||
48: './assets/icon-512.png',
|
||||
128: './assets/icon-512.png',
|
||||
},
|
||||
permissions: [
|
||||
'tabs',
|
||||
'storage',
|
||||
'activeTab',
|
||||
],
|
||||
host_permissions: ['*://*/*'],
|
||||
content_scripts: [
|
||||
{
|
||||
matches: [
|
||||
'<all_urls>',
|
||||
],
|
||||
js: [
|
||||
'dist/contentScripts/index.global.js',
|
||||
],
|
||||
},
|
||||
],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['dist/contentScripts/style.css'],
|
||||
matches: ['<all_urls>'],
|
||||
},
|
||||
],
|
||||
content_security_policy: {
|
||||
extension_pages: isDev
|
||||
// this is required on dev for Vite script to load
|
||||
? `script-src \'self\' http://localhost:${port}; object-src \'self\'`
|
||||
: 'script-src \'self\'; object-src \'self\'',
|
||||
},
|
||||
}
|
||||
|
||||
// FIXME: not work in MV3
|
||||
if (isDev && false) {
|
||||
// for content script, as browsers will cache them for each reload,
|
||||
// we use a background script to always inject the latest version
|
||||
// see src/background/contentScriptHMR.ts
|
||||
delete manifest.content_scripts
|
||||
manifest.permissions?.push('webNavigation')
|
||||
}
|
||||
|
||||
return manifest
|
||||
}
|
||||
17
src/options/Options.vue
Normal file
17
src/options/Options.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { storageDemo } from '~/logic/storage'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="px-4 py-10 text-center text-gray-700 dark:text-gray-200">
|
||||
<img src="/assets/icon.svg" class="icon-btn mx-2 text-2xl" alt="extension icon">
|
||||
<div>Options</div>
|
||||
<SharedSubtitle />
|
||||
|
||||
<input v-model="storageDemo" class="border border-gray-400 rounded px-2 py-1 mt-2">
|
||||
|
||||
<div class="mt-4">
|
||||
Powered by Vite <pixelarticons-zap class="align-middle inline-block" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
12
src/options/index.html
Normal file
12
src/options/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base target="_blank">
|
||||
<title>Options</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
src/options/main.ts
Normal file
8
src/options/main.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './Options.vue'
|
||||
import { setupApp } from '~/logic/common-setup'
|
||||
import '../styles'
|
||||
|
||||
const app = createApp(App)
|
||||
setupApp(app)
|
||||
app.mount('#app')
|
||||
22
src/popup/Popup.vue
Normal file
22
src/popup/Popup.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { storageDemo } from '~/logic/storage'
|
||||
|
||||
function openOptionsPage() {
|
||||
browser.runtime.openOptionsPage()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="w-[300px] px-4 py-5 text-center text-gray-700">
|
||||
<Logo />
|
||||
<div>Popup</div>
|
||||
<SharedSubtitle />
|
||||
|
||||
<button class="btn mt-2" @click="openOptionsPage">
|
||||
Open Options
|
||||
</button>
|
||||
<div class="mt-2">
|
||||
<span class="opacity-50">Storage:</span> {{ storageDemo }}
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
12
src/popup/index.html
Normal file
12
src/popup/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base target="_blank">
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
<body style="min-width: 100px">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
src/popup/main.ts
Normal file
8
src/popup/main.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './Popup.vue'
|
||||
import { setupApp } from '~/logic/common-setup'
|
||||
import '../styles'
|
||||
|
||||
const app = createApp(App)
|
||||
setupApp(app)
|
||||
app.mount('#app')
|
||||
3
src/styles/index.ts
Normal file
3
src/styles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import './main.css'
|
||||
import 'uno.css'
|
||||
20
src/styles/main.css
Executable file
20
src/styles/main.css
Executable file
@ -0,0 +1,20 @@
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-4 py-1 rounded inline-block
|
||||
bg-teal-600 text-white cursor-pointer
|
||||
hover:bg-teal-700
|
||||
disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
@apply inline-block cursor-pointer select-none
|
||||
opacity-75 transition duration-200 ease-in-out
|
||||
hover:opacity-100 hover:text-teal-600;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
7
src/tests/demo.spec.ts
Normal file
7
src/tests/demo.spec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('Demo', () => {
|
||||
it('should work', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
})
|
||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "es2016",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": false,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
13
unocss.config.ts
Normal file
13
unocss.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'unocss/vite'
|
||||
import { presetAttributify, presetIcons, presetUno, transformerDirectives } from 'unocss'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetAttributify(),
|
||||
presetIcons(),
|
||||
],
|
||||
transformers: [
|
||||
transformerDirectives(),
|
||||
],
|
||||
})
|
||||
36
vite.config.background.ts
Normal file
36
vite.config.background.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { sharedConfig } from './vite.config'
|
||||
import { isDev, r } from './scripts/utils'
|
||||
import packageJson from './package.json'
|
||||
|
||||
// bundling the content script using Vite
|
||||
export default defineConfig({
|
||||
...sharedConfig,
|
||||
define: {
|
||||
'__DEV__': isDev,
|
||||
'__NAME__': JSON.stringify(packageJson.name),
|
||||
// https://github.com/vitejs/vite/issues/9320
|
||||
// https://github.com/vitejs/vite/issues/9186
|
||||
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
||||
},
|
||||
build: {
|
||||
watch: isDev
|
||||
? {}
|
||||
: undefined,
|
||||
outDir: r('extension/dist/background'),
|
||||
cssCodeSplit: false,
|
||||
emptyOutDir: false,
|
||||
sourcemap: isDev ? 'inline' : false,
|
||||
lib: {
|
||||
entry: r('src/background/main.ts'),
|
||||
name: packageJson.name,
|
||||
formats: ['iife'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'index.mjs',
|
||||
extend: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
36
vite.config.content.ts
Normal file
36
vite.config.content.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { sharedConfig } from './vite.config'
|
||||
import { isDev, r } from './scripts/utils'
|
||||
import packageJson from './package.json'
|
||||
|
||||
// bundling the content script using Vite
|
||||
export default defineConfig({
|
||||
...sharedConfig,
|
||||
define: {
|
||||
'__DEV__': isDev,
|
||||
'__NAME__': JSON.stringify(packageJson.name),
|
||||
// https://github.com/vitejs/vite/issues/9320
|
||||
// https://github.com/vitejs/vite/issues/9186
|
||||
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
||||
},
|
||||
build: {
|
||||
watch: isDev
|
||||
? {}
|
||||
: undefined,
|
||||
outDir: r('extension/dist/contentScripts'),
|
||||
cssCodeSplit: false,
|
||||
emptyOutDir: false,
|
||||
sourcemap: isDev ? 'inline' : false,
|
||||
lib: {
|
||||
entry: r('src/contentScripts/index.ts'),
|
||||
name: packageJson.name,
|
||||
formats: ['iife'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'index.global.js',
|
||||
extend: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
113
vite.config.ts
Normal file
113
vite.config.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/// <reference types="vitest" />
|
||||
|
||||
import { dirname, relative } from 'node:path'
|
||||
import type { UserConfig } from 'vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import IconsResolver from 'unplugin-icons/resolver'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import { isDev, port, r } from './scripts/utils'
|
||||
import packageJson from './package.json'
|
||||
|
||||
export const sharedConfig: UserConfig = {
|
||||
root: r('src'),
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/': `${r('src')}/`,
|
||||
},
|
||||
},
|
||||
define: {
|
||||
__DEV__: isDev,
|
||||
__NAME__: JSON.stringify(packageJson.name),
|
||||
},
|
||||
plugins: [
|
||||
Vue(),
|
||||
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
{
|
||||
'webextension-polyfill': [
|
||||
['*', 'browser'],
|
||||
],
|
||||
},
|
||||
],
|
||||
dts: r('src/auto-imports.d.ts'),
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/unplugin-vue-components
|
||||
Components({
|
||||
dirs: [r('src/components')],
|
||||
// generate `components.d.ts` for ts support with Volar
|
||||
dts: r('src/components.d.ts'),
|
||||
resolvers: [
|
||||
// auto import icons
|
||||
IconsResolver({
|
||||
componentPrefix: '',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
// https://github.com/antfu/unplugin-icons
|
||||
Icons(),
|
||||
|
||||
// https://github.com/unocss/unocss
|
||||
UnoCSS(),
|
||||
|
||||
// rewrite assets to use relative path
|
||||
{
|
||||
name: 'assets-rewrite',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
transformIndexHtml(html, { path }) {
|
||||
return html.replace(/"\/assets\//g, `"${relative(dirname(path), '/assets')}/`)
|
||||
},
|
||||
},
|
||||
],
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'vue',
|
||||
'@vueuse/core',
|
||||
'webextension-polyfill',
|
||||
],
|
||||
exclude: [
|
||||
'vue-demi',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default defineConfig(({ command }) => ({
|
||||
...sharedConfig,
|
||||
base: command === 'serve' ? `http://localhost:${port}/` : '/dist/',
|
||||
server: {
|
||||
port,
|
||||
hmr: {
|
||||
host: 'localhost',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
watch: isDev
|
||||
? {}
|
||||
: undefined,
|
||||
outDir: r('extension/dist'),
|
||||
emptyOutDir: false,
|
||||
sourcemap: isDev ? 'inline' : false,
|
||||
// https://developer.chrome.com/docs/webstore/program_policies/#:~:text=Code%20Readability%20Requirements
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
},
|
||||
rollupOptions: {
|
||||
input: {
|
||||
options: r('src/options/index.html'),
|
||||
popup: r('src/popup/index.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
},
|
||||
}))
|
||||
Loading…
x
Reference in New Issue
Block a user