chore(all): init config

This commit is contained in:
molvqingtai 2023-07-13 22:27:59 +08:00
parent c4e26b99d9
commit d5d8c60e53
42 changed files with 434 additions and 4167 deletions

View file

@ -1,4 +0,0 @@
dist
node_modules
public
extension

16
.gitignore vendored
View file

@ -1,17 +1,13 @@
dist
node_modules
.DS_Store .DS_Store
.idea/ .eslintcache
.vite-ssg-dist
.vite-ssg-temp
*.crx *.crx
*.local *.local
*.log *.log
*.pem *.pem
*.xpi *.xpi
*.zip *.zip
dist
dist-ssr
extension/manifest.json
node_modules
src/auto-imports.d.ts
src/components.d.ts
.eslintcache

View file

@ -1,4 +1,4 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1" npx commitlint --edit "$1"

2
.npmrc
View file

@ -1,2 +0,0 @@
shamefully-hoist=true
auto-install-peers=true

View file

@ -1,10 +1,3 @@
# [WIP]WebChat # [WIP]WebChat
> Chatting Anonymously with People on the Same Website. > 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.

View file

@ -1,15 +0,0 @@
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')
})

View file

@ -1,47 +0,0 @@
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')
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 366 B

14
manifest.ts Normal file
View file

@ -0,0 +1,14 @@
import { defineManifest } from '@crxjs/vite-plugin'
import pkgJson from './package.json'
export default defineManifest({
manifest_version: 3,
name: pkgJson.displayName,
version: pkgJson.version,
content_scripts: [
{
js: ['src/content/main.tsx'],
matches: ['*://www.example.com//*']
}
]
})

View file

@ -4,27 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"description": "Chatting Anonymously with People on the Same Website.", "description": "Chatting Anonymously with People on the Same Website.",
"scripts": { "scripts": {
"dev": "npm run clear && cross-env NODE_ENV=development run-p dev:*", "dev": "vite",
"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", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx --cache --fix",
"test": "vitest test",
"test:e2e": "playwright test",
"tsc:check": "tsc --noEmit", "tsc:check": "tsc --noEmit",
"prepare": "husky install" "prepare": "husky install"
}, },
@ -45,21 +26,16 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.6.6", "@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "^17.6.6", "@commitlint/config-conventional": "^17.6.6",
"@ffflorian/jszip-cli": "^3.4.1", "@crxjs/vite-plugin": "1.0.14",
"@iconify/json": "^2.2.89", "@iconify/json": "^2.2.90",
"@playwright/test": "^1.36.0",
"@svgr/core": "^8.0.0", "@svgr/core": "^8.0.0",
"@svgr/plugin-jsx": "^8.0.1", "@svgr/plugin-jsx": "^8.0.1",
"@types/fs-extra": "^11.0.1", "@types/node": "^20.4.2",
"@types/node": "^20.4.1",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6", "@types/react-dom": "^18.2.7",
"@types/webextension-polyfill": "^0.10.1", "@types/webextension-polyfill": "^0.10.1",
"@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"chokidar": "^3.5.3",
"cross-env": "^7.0.3",
"crx": "^5.0.1",
"eslint": "^8.44.0", "eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-config-standard-with-typescript": "^36.0.0", "eslint-config-standard-with-typescript": "^36.0.0",
@ -69,25 +45,16 @@
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"esno": "^0.16.3",
"fs-extra": "^11.1.1",
"husky": "^8.0.3", "husky": "^8.0.3",
"jsdom": "^22.1.0",
"kolorist": "^1.8.0",
"lint-staged": "^13.2.3", "lint-staged": "^13.2.3",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.25", "postcss": "^8.4.25",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rimraf": "^5.0.1",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"unplugin-auto-import": "^0.16.6", "unplugin-icons": "^0.16.5",
"unplugin-icons": "^0.16.3",
"vite": "^4.4.3", "vite": "^4.4.3",
"vitest": "^0.33.0",
"web-ext": "^7.6.2",
"webext-bridge": "^6.0.1", "webext-bridge": "^6.0.1",
"webextension-polyfill": "^0.10.0" "webextension-polyfill": "^0.10.0"
}, },

View file

@ -1,15 +0,0 @@
/**
* @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
}
})

File diff suppressed because it is too large Load diff

View file

@ -1,10 +0,0 @@
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()

View file

@ -1,55 +0,0 @@
// 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()
})
}

View file

@ -1,11 +0,0 @@
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)
}

View file

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 366 B

BIN
src/assets/you.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -1,19 +0,0 @@
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))
})

View file

@ -1,12 +0,0 @@
<!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>

View file

@ -1,51 +0,0 @@
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
}
}
})

View file

@ -1,9 +1,9 @@
import { useState } from 'react' import { useState } from 'react'
import IconPower from '~icons/pixelarticons/power' import IconPower from '~icons/pixelarticons/power'
import '@/styles/main.css' import Sidebar from './Sidebar'
export default function App({ frameUrl }: { frameUrl: string }) { export default function App() {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [openedOnce, setOpenedOnce] = useState(false) const [openedOnce, setOpenedOnce] = useState(false)
@ -26,7 +26,7 @@ export default function App({ frameUrl }: { frameUrl: string }) {
open ? 'translate-x-0' : 'translate-x-full' open ? 'translate-x-0' : 'translate-x-full'
}`} }`}
> >
<iframe src={frameUrl} className="w-full h-full border-0" /> <Sidebar></Sidebar>
</div> </div>
)} )}
</div> </div>

View file

@ -0,0 +1,36 @@
import createElement from '@/utils'
import { type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
export interface RootOptions {
name: string
mode?: ShadowRootMode
css?: string
}
const createShadowRoot = (options: RootOptions): Root => {
const { name, mode = 'open', css = '' } = options
const shadowRoot = createElement(`<${name}></${name}>`)
const appStyle = createElement(`<style type="text/css">${css}</style>`)
const appRoot = createElement(`<div id="app"></div>`)
const reactRoot = createRoot(appRoot)
class WebChat extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode })
shadow.append(appStyle, appRoot)
}
}
return {
...reactRoot,
render: (children: ReactNode) => {
customElements.define(name, WebChat)
document.body.appendChild(shadowRoot)
reactRoot.render(children)
}
}
}
export default createShadowRoot

78
src/content/main.css Normal file
View 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;
}
}

13
src/content/main.tsx Normal file
View file

@ -0,0 +1,13 @@
import React from 'react'
import App from './App'
import createShadowRoot from './createShadowRoot'
import css from './main.css?inline'
void (() => {
createShadowRoot({ name: 'web-chat', css }).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
})()

View file

@ -1,33 +0,0 @@
/* 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')
})
})()

View file

@ -1,13 +0,0 @@
// 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>
)
}

View file

@ -1,14 +0,0 @@
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
View file

@ -1,3 +0,0 @@
declare const __DEV__: boolean
/** Extension name, defined in packageJson.name */
declare const __NAME__: string

View file

@ -1,29 +0,0 @@
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]
}

0
src/hooks/useUrl.ts Normal file
View file

View file

@ -1,59 +0,0 @@
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
}

View file

@ -1,16 +0,0 @@
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>
)
}

View file

@ -1,12 +0,0 @@
<!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>

View file

@ -1,13 +0,0 @@
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>
)

View file

@ -1,12 +0,0 @@
<!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>

View file

@ -1,13 +0,0 @@
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>
)

View file

@ -4,3 +4,9 @@ import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
const createElement = <T extends Element>(template: string) => {
return new Range().createContextualFragment(template).firstElementChild as unknown as T
}
export default createElement

View file

@ -1,34 +0,0 @@
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
}
}
}
})

View file

@ -1,34 +0,0 @@
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
}
}
}
})

View file

@ -1,71 +1,20 @@
import { dirname, relative } from 'node:path' import path from 'node:path'
import type { UserConfig } from 'vite'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
import Icons from 'unplugin-icons/vite' import Icons from 'unplugin-icons/vite'
import { isDev, port, r } from './scripts/utils' import manifest from './manifest'
import packageJson from './package.json'
export const sharedConfig: UserConfig = { export default defineConfig({
root: r('src'),
resolve: { resolve: {
alias: { alias: {
'@': `${r('src')}/` '@': path.resolve(__dirname, 'src')
} }
}, },
define: {
__DEV__: isDev,
__NAME__: JSON.stringify(packageJson.name)
},
plugins: [ plugins: [
react(), react(),
// https://github.com/antfu/unplugin-icons // https://github.com/antfu/unplugin-icons
Icons({ compiler: 'jsx', jsx: 'react' }), Icons({ compiler: 'jsx', jsx: 'react' }),
crx({ manifest })
// 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'
}
}))