chore(all): init config
This commit is contained in:
commit
a719363e82
49 changed files with 9116 additions and 0 deletions
5
.commitlintrc
Normal file
5
.commitlintrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"@commitlint/config-conventional"
|
||||||
|
]
|
||||||
|
}
|
4
.eslintignore
Normal file
4
.eslintignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
public
|
||||||
|
extension
|
43
.eslintrc
Normal file
43
.eslintrc
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"es2021": true,
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"standard-with-typescript",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "react", "react-hooks", "prettier"],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.eslint.json",
|
||||||
|
"warnOnUnsupportedTypeScriptVersion": false
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"@typescript-eslint/naming-convention": "off",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||||
|
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off"
|
||||||
|
}
|
||||||
|
}
|
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
|
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx --no-install commitlint --edit "$1"
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm lint-staged
|
2
.npmrc
Normal file
2
.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
shamefully-hoist=true
|
||||||
|
auto-install-peers=true
|
6
.postcssrc
Normal file
6
.postcssrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"tailwindcss": {},
|
||||||
|
"autoprefixer": {}
|
||||||
|
}
|
||||||
|
}
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
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.
|
10
README.md
Normal file
10
README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# [WIP]WebChat
|
||||||
|
|
||||||
|
> Chatting Anonymously with People on the Same Website.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
- [vitesse-webext](https://github.com/antfu/vitesse-webext) : 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.
|
||||||
|
|
16
components.json
Normal file
16
components.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "@/styles/main.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/utils/index.ts"
|
||||||
|
}
|
||||||
|
}
|
15
e2e/basic.spec.ts
Normal file
15
e2e/basic.spec.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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('options page', async ({ page, extensionId }) => {
|
||||||
|
await page.goto(`chrome-extension://${extensionId}/dist/options/index.html`)
|
||||||
|
await expect(page.locator('img')).toHaveAttribute('alt', 'extension icon')
|
||||||
|
})
|
47
e2e/fixtures.ts
Normal file
47
e2e/fixtures.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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 |
104
package.json
Normal file
104
package.json
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"name": "web-chat",
|
||||||
|
"displayName": "WebChat",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Chatting Anonymously with People on the Same Website.",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npm run clear && cross-env NODE_ENV=development run-p dev:*",
|
||||||
|
"dev-firefox": "npm run clear && cross-env NODE_ENV=development EXTENSION=firefox 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": "npx eslint . --ext .js,.jsx,.ts,.tsx --cache --fix",
|
||||||
|
"test": "vitest test",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"tsc:check": "tsc --noEmit",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/molvqingtai/WebChat.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Chat",
|
||||||
|
"Browser"
|
||||||
|
],
|
||||||
|
"author": "molvqingtai",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/molvqingtai/WebChat/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/molvqingtai/WebChat#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^17.6.6",
|
||||||
|
"@commitlint/config-conventional": "^17.6.6",
|
||||||
|
"@ffflorian/jszip-cli": "^3.4.1",
|
||||||
|
"@iconify/json": "^2.2.89",
|
||||||
|
"@playwright/test": "^1.36.0",
|
||||||
|
"@svgr/core": "^8.0.0",
|
||||||
|
"@svgr/plugin-jsx": "^8.0.1",
|
||||||
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"@types/node": "^20.4.1",
|
||||||
|
"@types/react": "^18.2.14",
|
||||||
|
"@types/react-dom": "^18.2.6",
|
||||||
|
"@types/webextension-polyfill": "^0.10.1",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"crx": "^5.0.1",
|
||||||
|
"eslint": "^8.44.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-config-standard-with-typescript": "^36.0.0",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"eslint-plugin-n": "^16.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"esno": "^0.16.3",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"kolorist": "^1.8.0",
|
||||||
|
"lint-staged": "^13.2.3",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"postcss": "^8.4.25",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"rimraf": "^5.0.1",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"typescript": "^5.1.6",
|
||||||
|
"unplugin-auto-import": "^0.16.6",
|
||||||
|
"unplugin-icons": "^0.16.3",
|
||||||
|
"vite": "^4.4.3",
|
||||||
|
"vitest": "^0.33.0",
|
||||||
|
"web-ext": "^7.6.2",
|
||||||
|
"webext-bridge": "^6.0.1",
|
||||||
|
"webextension-polyfill": "^0.10.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx}": "eslint --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"class-variance-authority": "^0.6.1",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"tailwind-merge": "^1.13.2",
|
||||||
|
"tailwindcss-animate": "^1.0.6"
|
||||||
|
}
|
||||||
|
}
|
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
|
||||||
|
}
|
||||||
|
})
|
8026
pnpm-lock.yaml
Normal file
8026
pnpm-lock.yaml
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()
|
55
scripts/prepare.ts
Normal file
55
scripts/prepare.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// generate stub index.html files for dev entry
|
||||||
|
import { execSync } from 'node:child_process'
|
||||||
|
import path from 'node:path'
|
||||||
|
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', 'background', 'sidebar']
|
||||||
|
|
||||||
|
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('</head>', '<script type="module" src="/dist/refreshPreamble.js"></script></head>')
|
||||||
|
.replace('"./main.tsx"', `"http://localhost:${port}/${view}/main.tsx"`)
|
||||||
|
.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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This enables hot module reloading
|
||||||
|
async function writeRefreshPreamble() {
|
||||||
|
const data = `
|
||||||
|
import RefreshRuntime from "http://localhost:${port}/@react-refresh";
|
||||||
|
RefreshRuntime.injectIntoGlobalHook(window);
|
||||||
|
window.$RefreshReg$ = () => {};
|
||||||
|
window.$RefreshSig$ = () => (type) => type;
|
||||||
|
window.__vite_plugin_react_preamble_installed__ = true;
|
||||||
|
`
|
||||||
|
|
||||||
|
await fs.ensureDir(r('extension/dist'))
|
||||||
|
await fs.writeFile(path.join(r('extension/dist/'), 'refreshPreamble.js'), data, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeManifest() {
|
||||||
|
execSync('npx esno ./scripts/manifest.ts', { stdio: 'inherit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
writeManifest()
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
writeRefreshPreamble()
|
||||||
|
stubIndexHtml()
|
||||||
|
chokidar.watch(r('src/**/*.html')).on('change', () => {
|
||||||
|
stubIndexHtml()
|
||||||
|
})
|
||||||
|
chokidar.watch([r('src/manifest.ts'), r('package.json')]).on('change', () => {
|
||||||
|
writeManifest()
|
||||||
|
})
|
||||||
|
}
|
11
scripts/utils.ts
Normal file
11
scripts/utils.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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 const isFirefox = process.env.EXTENSION === 'firefox'
|
||||||
|
|
||||||
|
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 |
19
src/background/contentScriptHMR.ts
Normal file
19
src/background/contentScriptHMR.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import browser from 'webextension-polyfill'
|
||||||
|
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>
|
51
src/background/main.ts
Normal file
51
src/background/main.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { onMessage, sendMessage } from 'webext-bridge/background'
|
||||||
|
import type { Tabs } from 'webextension-polyfill'
|
||||||
|
import browser from 'webextension-polyfill'
|
||||||
|
|
||||||
|
// only on dev mode
|
||||||
|
if (import.meta.hot != null) {
|
||||||
|
// @ts-expect-error for background HMR
|
||||||
|
import('@vite/client')
|
||||||
|
// load latest content script
|
||||||
|
import('./contentScriptHMR')
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.runtime.onInstalled.addListener((): void => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
15
src/components/Logo.tsx
Normal file
15
src/components/Logo.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import IconPower from '~icons/pixelarticons/power'
|
||||||
|
|
||||||
|
export default function Logo() {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="icon-btn mx-2 text-2xl"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="https://github.com/antfu/vitesse-webext"
|
||||||
|
target="_blank"
|
||||||
|
title="GitHub"
|
||||||
|
>
|
||||||
|
<IconPower />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
33
src/contentScripts/index.ts
Normal file
33
src/contentScripts/index.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { onMessage } from 'webext-bridge/content-script'
|
||||||
|
import browser from 'webextension-polyfill'
|
||||||
|
|
||||||
|
import { renderApp } from './render'
|
||||||
|
|
||||||
|
// Firefox `browser.tabs.executeScript()` requires scripts return a primitive value
|
||||||
|
;(() => {
|
||||||
|
console.info(`[__NAME__] Hello world from content script`)
|
||||||
|
|
||||||
|
// communication example: send previous tab title from background page
|
||||||
|
onMessage('tab-prev', ({ data }) => {
|
||||||
|
console.log(`[__NAME__] 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 appRoot = createRoot(root)
|
||||||
|
renderApp({
|
||||||
|
root: appRoot,
|
||||||
|
frameUrl: browser.runtime.getURL('dist/sidebar/index.html')
|
||||||
|
})
|
||||||
|
})()
|
34
src/contentScripts/pages/App.tsx
Normal file
34
src/contentScripts/pages/App.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import IconPower from '~icons/pixelarticons/power'
|
||||||
|
|
||||||
|
import '@/styles/main.css'
|
||||||
|
|
||||||
|
export default function App({ frameUrl }: { frameUrl: string }) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [openedOnce, setOpenedOnce] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="fixed left-0 bottom-0 m-5 z-100 flex font-sans select-none leading-1em">
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center w-10 h-10 rounded-full shadow cursor-pointer bg-blue-400 hover:bg-blue-600"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen((open) => !open)
|
||||||
|
setOpenedOnce(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPower />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{openedOnce && (
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 right-0 h-full w-1/4 z-50 bg-white drop-shadow-xl transition-transform ${
|
||||||
|
open ? 'translate-x-0' : 'translate-x-full'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<iframe src={frameUrl} className="w-full h-full border-0" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
13
src/contentScripts/render.tsx
Normal file
13
src/contentScripts/render.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// use a separate file from index.ts to keep the diff as simple as possible
|
||||||
|
import React from 'react'
|
||||||
|
import type { Root } from 'react-dom/client'
|
||||||
|
|
||||||
|
import App from './pages/App'
|
||||||
|
|
||||||
|
export const renderApp = ({ root, frameUrl }: { root: Root; frameUrl: string }) => {
|
||||||
|
return root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App frameUrl={frameUrl} />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
|
}
|
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')
|
3
src/global.d.ts
vendored
Normal file
3
src/global.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare const __DEV__: boolean
|
||||||
|
/** Extension name, defined in packageJson.name */
|
||||||
|
declare const __NAME__: string
|
29
src/hooks/useStorage.ts
Normal file
29
src/hooks/useStorage.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import type { Storage } from 'webextension-polyfill'
|
||||||
|
import { storage } from 'webextension-polyfill'
|
||||||
|
|
||||||
|
export function useStorage(key: string, initialValue: string) {
|
||||||
|
const [value, setValue] = useState(async () => {
|
||||||
|
const storedValue = await storage.local.get(key)
|
||||||
|
return storedValue !== null ? storedValue : initialValue
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onChange = (changes: Record<string, Storage.StorageChange>) => {
|
||||||
|
if (changes[key]) setValue(changes[key].newValue)
|
||||||
|
}
|
||||||
|
storage.onChanged.addListener(onChange)
|
||||||
|
;(async () => {
|
||||||
|
const value = await storage.local.get(key)
|
||||||
|
setValue(value[key])
|
||||||
|
})()
|
||||||
|
return () => storage.onChanged.removeListener(onChange)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setValueToStorage = async (newValue: string) => {
|
||||||
|
setValue(newValue)
|
||||||
|
await storage.local.set({ [key]: newValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value, setValueToStorage]
|
||||||
|
}
|
59
src/manifest.ts
Normal file
59
src/manifest.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import type { Manifest } from 'webextension-polyfill'
|
||||||
|
import type PkgType from '../package.json'
|
||||||
|
import { isDev, isFirefox, port, r } from '../scripts/utils'
|
||||||
|
|
||||||
|
export async function getManifest(): Promise<Manifest.WebExtensionManifest> {
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
options_ui: {
|
||||||
|
page: './dist/options/index.html',
|
||||||
|
open_in_tab: true
|
||||||
|
},
|
||||||
|
background: isFirefox
|
||||||
|
? {
|
||||||
|
scripts: ['dist/background/index.mjs'],
|
||||||
|
type: 'module'
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
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', 'dist/sidebar/index.html'],
|
||||||
|
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'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest
|
||||||
|
}
|
16
src/options/Options.tsx
Normal file
16
src/options/Options.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import IconSliders from '~icons/pixelarticons/sliders'
|
||||||
|
import IconZap from '~icons/pixelarticons/zap'
|
||||||
|
|
||||||
|
export default function Options() {
|
||||||
|
return (
|
||||||
|
<main className="px-4 py-10 text-center text-gray-700 dark:text-gray-200">
|
||||||
|
<IconSliders className="icon-btn mx-2 text-2xl" />
|
||||||
|
<div>Options</div>
|
||||||
|
<p className="mt-2 opacity-50">This is the options page</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
Powered by Vite <IconZap className="align-middle" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
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.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
src/options/main.tsx
Normal file
13
src/options/main.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
|
import '@/styles/main.css'
|
||||||
|
import App from './Options'
|
||||||
|
|
||||||
|
const container = document.getElementById('app')
|
||||||
|
const root = createRoot(container!)
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
11
src/sidebar/Sidebar.tsx
Normal file
11
src/sidebar/Sidebar.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import Logo from '@/components/Logo'
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
return (
|
||||||
|
<main className="w-[300px] px-4 py-5 text-center text-gray-700">
|
||||||
|
<Logo />
|
||||||
|
<div>Sidebar</div>
|
||||||
|
<p className="mt-2 opacity-50 text-blue-600">This is the sidebar page</p>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
12
src/sidebar/index.html
Normal file
12
src/sidebar/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<base target="_blank">
|
||||||
|
<title>Sidebar</title>
|
||||||
|
</head>
|
||||||
|
<body style="min-width: 100px">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
src/sidebar/main.tsx
Normal file
13
src/sidebar/main.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
|
import '@/styles/main.css'
|
||||||
|
import App from './Sidebar'
|
||||||
|
|
||||||
|
const container = document.getElementById('app')
|
||||||
|
const root = createRoot(container!)
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
78
src/styles/main.css
Normal file
78
src/styles/main.css
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
|
||||||
|
--ring: 217.2 32.6% 17.5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
6
src/utils/index.ts
Normal file
6
src/utils/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
71
tailwind.config.ts
Normal file
71
tailwind.config.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ['class'],
|
||||||
|
content: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '2rem',
|
||||||
|
screens: {
|
||||||
|
'2xl': '1400px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
'accordion-down': {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: 'var(--radix-accordion-content-height)' }
|
||||||
|
},
|
||||||
|
'accordion-up': {
|
||||||
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
|
to: { height: 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
|
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [require('tailwindcss-animate')]
|
||||||
|
}
|
4
tsconfig.eslint.json
Normal file
4
tsconfig.eslint.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx"]
|
||||||
|
}
|
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"incremental": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"types": ["vite/client", "unplugin-icons/types/react"],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
34
vite.config.background.ts
Normal file
34
vite.config.background.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
34
vite.config.content.ts
Normal file
34
vite.config.content.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
71
vite.config.ts
Normal file
71
vite.config.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { dirname, relative } from 'node:path'
|
||||||
|
import type { UserConfig } from 'vite'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import Icons from 'unplugin-icons/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: [
|
||||||
|
react(),
|
||||||
|
|
||||||
|
// https://github.com/antfu/unplugin-icons
|
||||||
|
Icons({ compiler: 'jsx', jsx: 'react' }),
|
||||||
|
|
||||||
|
// 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: ['react', 'webextension-polyfill']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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'),
|
||||||
|
sidebar: r('src/sidebar/index.html')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom'
|
||||||
|
}
|
||||||
|
}))
|
Loading…
Reference in a new issue