chore: upgrade wxt

This commit is contained in:
molvqingtai 2024-02-23 16:37:37 +08:00
parent 9fca355c99
commit 0571682e73
18 changed files with 2195 additions and 1801 deletions

View file

@ -34,6 +34,8 @@
"prettier/prettier": "error", "prettier/prettier": "error",
"react/prop-types": "off", "react/prop-types": "off",
"import/order": "error", "import/order": "error",
"import/no-absolute-path": "off",
"n/no-callback-literal": "off",
"@typescript-eslint/naming-convention": "off", "@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
@ -43,7 +45,6 @@
"@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/consistent-type-assertions": "off", "@typescript-eslint/consistent-type-assertions": "off",
"import/no-absolute-path": "off",
"@typescript-eslint/no-base-to-string": "off", "@typescript-eslint/no-base-to-string": "off",
"@typescript-eslint/no-unused-vars": "warn" "@typescript-eslint/no-unused-vars": "warn"
} }

View file

@ -6,7 +6,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
"dev:signaling": "y-webrtc-signaling", "dev:server": "y-webrtc-signaling",
"dev:firefox": "wxt -b firefox", "dev:firefox": "wxt -b firefox",
"build": "wxt build", "build": "wxt build",
"build:firefox": "wxt build -b firefox", "build:firefox": "wxt build -b firefox",
@ -44,8 +44,8 @@
}, },
"homepage": "https://github.com/molvqingtai/WebChat#readme", "homepage": "https://github.com/molvqingtai/WebChat#readme",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.3.2", "@hookform/resolvers": "^3.3.4",
"@perfsee/jsonr": "^1.8.4", "@perfsee/jsonr": "^1.12.2",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
@ -58,61 +58,61 @@
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.1.0",
"date-fns": "^2.30.0", "date-fns": "^3.3.1",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"lucide-react": "^0.294.0", "lucide-react": "^0.336.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.6",
"peerjs": "^1.5.1", "peerjs": "^1.5.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.48.2", "react-hook-form": "^7.50.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-nice-avatar": "^1.5.0", "react-nice-avatar": "^1.5.0",
"react-use": "^17.4.2", "react-use": "^17.5.0",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remesh": "^4.2.0", "remesh": "^4.2.1",
"remesh-logger": "^4.1.0", "remesh-logger": "^4.1.0",
"remesh-react": "^4.1.0", "remesh-react": "^4.1.2",
"remesh-yjs": "^4.1.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sonner": "^1.2.4", "sonner": "^1.4.0",
"tailwind-merge": "^2.1.0", "tailwind-merge": "^2.2.1",
"type-fest": "^4.8.3", "type-fest": "^4.10.3",
"valibot": "^0.22.0", "unstorage": "^1.10.1",
"y-webrtc": "^10.2.6" "valibot": "^0.29.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^18.4.3", "@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.4.3", "@commitlint/config-conventional": "^18.6.2",
"@types/node": "^20.10.3", "@types/node": "^20.11.20",
"@types/react": "^18.2.41", "@types/react": "^18.2.57",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.19",
"@types/webextension-polyfill": "^0.10.7",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.17",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.55.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^40.0.0", "eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.29.0", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.3.1", "eslint-plugin-n": "^16.6.2",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tailwindcss": "^3.13.0", "eslint-plugin-tailwindcss": "^3.14.3",
"husky": "^8.0.3", "husky": "^9.0.11",
"lint-staged": "^15.2.0", "lint-staged": "^15.2.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss": "^8.4.32", "postcss": "^8.4.35",
"prettier": "^3.1.0", "prettier": "^3.2.5",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.3.2", "typescript": "^5.3.3",
"webext-bridge": "^6.0.1", "webext-bridge": "^6.0.1",
"wxt": "^0.10.4" "wxt": "^0.17.0"
}, },
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix" "*.{js,jsx,ts,tsx}": "eslint --fix"

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import { browser } from 'wxt/browser' import { browser } from 'wxt/browser'
import { defineBackground } from 'wxt/client' import { defineBackground } from 'wxt/sandbox'
export default defineBackground({ export default defineBackground({
// Set manifest options // Set manifest options

View file

@ -3,33 +3,34 @@ import { createRoot } from 'react-dom/client'
import { Remesh } from 'remesh' import { Remesh } from 'remesh'
import { RemeshRoot } from 'remesh-react' import { RemeshRoot } from 'remesh-react'
import { RemeshLogger } from 'remesh-logger' import { RemeshLogger } from 'remesh-logger'
import { defineContentScript, createContentScriptUi } from 'wxt/client' import { defineContentScript } from 'wxt/sandbox'
import * as Y from 'yjs' import { createShadowRootUi } from 'wxt/client'
import { RemeshYjs, RemeshYjsExtern } from 'remesh-yjs'
import { WebrtcProvider } from 'y-webrtc'
import App from './App' import App from './App'
import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/impl/Storage' import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/impl/Storage'
import { PeerClientImpl } from '@/impl/PeerClient'
import '@/assets/styles/tailwind.css' import '@/assets/styles/tailwind.css'
export default defineContentScript({ export default defineContentScript({
cssInjectionMode: 'ui', cssInjectionMode: 'ui',
matches: ['*://*.example.com/*', '*://*.google.com/*', '*://*.v2ex.com/*'], matches: ['*://*.example.com/*', '*://*.google.com/*', '*://*.v2ex.com/*'],
async main(ctx) { async main(ctx) {
const doc = new Y.Doc()
// eslint-disable-next-line no-new
new WebrtcProvider(__NAME__, doc, { signaling: ['ws://localhost:4444'] })
const store = Remesh.store({ const store = Remesh.store({
externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, RemeshYjsExtern.impl({ doc })], externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerClientImpl],
inspectors: [RemeshLogger()] inspectors: [RemeshLogger()]
}) })
const ui = await createContentScriptUi(ctx, { const ui = await createShadowRootUi(ctx, {
name: __NAME__, name: __NAME__,
type: 'overlay', position: 'inline',
mount(container) { // anchor: 'body',
const root = createRoot(container) // append: 'first',
onMount: (container) => {
const app = document.createElement('div')
app.id = 'app'
container.append(app)
const root = createRoot(app)
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<RemeshRoot store={store}> <RemeshRoot store={store}>
@ -39,8 +40,8 @@ export default defineContentScript({
) )
return root return root
}, },
onRemove(root) { onRemove: (root) => {
root.unmount() root?.unmount()
} }
}) })
ui.mount() ui.mount()

View file

@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="./index.tsx"></script> <script type="module" src="./main.tsx"></script>
</body> </body>
</html> </html>

View file

@ -2,14 +2,19 @@ import { Remesh } from 'remesh'
import { ListModule } from 'remesh/modules/list' import { ListModule } from 'remesh/modules/list'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { from, map, tap, merge } from 'rxjs' import { from, map, tap, merge } from 'rxjs'
import { RemeshYjs } from 'remesh-yjs'
import { IndexDBStorageExtern } from './externs/Storage' import { IndexDBStorageExtern } from './externs/Storage'
import { PeerClientExtern } from './externs/PeerClient'
import { callbackToObservable, stringToHex } from '@/utils'
const hostRoomId = stringToHex(document.location.host)
const MessageListDomain = Remesh.domain({ const MessageListDomain = Remesh.domain({
name: 'MessageListDomain', name: 'MessageListDomain',
impl: (domain) => { impl: (domain) => {
const storage = domain.getExtern(IndexDBStorageExtern) const storage = domain.getExtern(IndexDBStorageExtern)
const peerClient = domain.getExtern(PeerClientExtern)
const storageKey = `MESSAGE_LIST` as const const storageKey = `MESSAGE_LIST` as const
peerClient.connect(hostRoomId)
const MessageListModule = ListModule<Message>(domain, { const MessageListModule = ListModule<Message>(domain, {
name: 'MessageListModule', name: 'MessageListModule',
@ -100,17 +105,31 @@ const MessageListDomain = Remesh.domain({
} }
}) })
RemeshYjs(domain, { domain.effect({
key: 'MessageList', name: 'FormStateToPeerClientEffect',
dataType: 'array', impl: ({ fromEvent }) => {
onSend: ({ get }): Message[] => { const createItem$ = fromEvent(CreateItemEvent).pipe(
return get(ListQuery()) tap(async (message) => {
}, await peerClient.sendMessage(JSON.stringify(message))
onReceive: (_, messages: Message[]) => { })
return InitListCommand(messages) )
return merge(createItem$).pipe(map(() => null))
} }
}) })
// domain.effect({
// name: 'FormPeerClientToStateEffect',
// impl: () => {
// return callbackToObservable(peerClient.onMessage.bind(peerClient)).pipe(
// map((message) => {
// console.log(message)
// // debugger
// return CreateItemCommand(message)
// })
// )
// }
// })
return { return {
query: { query: {
ItemQuery, ItemQuery,

View file

@ -1,7 +1,8 @@
import { Remesh } from 'remesh' import { Remesh } from 'remesh'
import { forkJoin, from, map, merge, switchMap, tap } from 'rxjs' import { forkJoin, from, map, merge, switchMap, tap } from 'rxjs'
import { BrowserSyncStorageExtern } from './externs/Storage' import { BrowserSyncStorageExtern } from './externs/Storage'
import { isNullish, storageToObservable } from '@/utils' import { isNullish } from '@/utils'
import callbackToObservable from '@/utils/callbackToObservable'
const UserInfoDomain = Remesh.domain({ const UserInfoDomain = Remesh.domain({
name: 'UserInfoDomain', name: 'UserInfoDomain',
@ -104,7 +105,7 @@ const UserInfoDomain = Remesh.domain({
domain.effect({ domain.effect({
name: 'WatchStorageToStateEffect', name: 'WatchStorageToStateEffect',
impl: () => { impl: () => {
return storageToObservable(storage).pipe( return callbackToObservable(storage.watch, storage.unwatch).pipe(
switchMap(() => { switchMap(() => {
return forkJoin({ return forkJoin({
id: from(storage.get<UserInfo['id']>(storageKeys.USER_INFO_ID)), id: from(storage.get<UserInfo['id']>(storageKeys.USER_INFO_ID)),

View file

@ -0,0 +1,25 @@
import { Remesh } from 'remesh'
export interface PeerClient {
connect: (id: string) => Promise<void>
sendMessage: (message: string) => Promise<void>
onMessage: (callback: (message: string) => void) => void
close: () => Promise<void> | void
}
export const PeerClientExtern = Remesh.extern<PeerClient>({
default: {
connect: async () => {
throw new Error('"connect" not implemented.')
},
sendMessage: async () => {
throw new Error('"sendMessage" not implemented.')
},
onMessage: () => {
throw new Error('"onMessage" not implemented.')
},
close: () => {
throw new Error('"close" not implemented.')
}
}
})

View file

@ -20,22 +20,22 @@ export const IndexDBStorageExtern = Remesh.extern<Storage>({
default: { default: {
name: 'STORAGE', name: 'STORAGE',
get: async () => { get: async () => {
throw new Error('"get" not implemented') throw new Error('"get" not implemented.')
}, },
set: async () => { set: async () => {
throw new Error('"set" not implemented') throw new Error('"set" not implemented.')
}, },
remove: async () => { remove: async () => {
throw new Error('"remove" not implemented') throw new Error('"remove" not implemented.')
}, },
clear: async () => { clear: async () => {
throw new Error('"clear" not implemented') throw new Error('"clear" not implemented.')
}, },
watch: () => { watch: () => {
throw new Error('"watch" not implemented') throw new Error('"watch" not implemented.')
}, },
unwatch: () => { unwatch: () => {
throw new Error('"unwatch" not implemented') throw new Error('"unwatch" not implemented.')
} }
} }
}) })
@ -44,22 +44,22 @@ export const BrowserSyncStorageExtern = Remesh.extern<Storage>({
default: { default: {
name: 'STORAGE', name: 'STORAGE',
get: async () => { get: async () => {
throw new Error('"get" not implemented') throw new Error('"get" not implemented.')
}, },
set: async () => { set: async () => {
throw new Error('"set" not implemented') throw new Error('"set" not implemented.')
}, },
remove: async () => { remove: async () => {
throw new Error('"remove" not implemented') throw new Error('"remove" not implemented.')
}, },
clear: async () => { clear: async () => {
throw new Error('"clear" not implemented') throw new Error('"clear" not implemented.')
}, },
watch: () => { watch: () => {
throw new Error('"watch" not implemented') throw new Error('"watch" not implemented.')
}, },
unwatch: () => { unwatch: () => {
throw new Error('"unwatch" not implemented') throw new Error('"unwatch" not implemented.')
} }
} }
}) })

100
src/impl/PeerClient.ts Normal file
View file

@ -0,0 +1,100 @@
import Peer, { type DataConnection } from 'peerjs'
import { nanoid } from 'nanoid'
import { PeerClientExtern } from '../domain/externs/PeerClient'
class PeerClient {
private peer: Peer | undefined
private connection: DataConnection | undefined
async connect(id: string) {
const connect = (id: string) => {
this.peer = new Peer(nanoid())
this.peer.on('connection', (e) => {
console.log('connection2', e)
})
const connection = this.peer.connect(id)
connection.on('open', () => {
console.log('connection open')
this.connection = connection
})
connection.on('error', (error) => {
console.log('error', error)
})
}
this.peer = new Peer(id)
this.peer.on('connection', (e) => {
console.log('connection1', e)
})
this.peer.on('open', (e) => {
console.log('open', e)
this.peer!.on('connection', (e) => {
console.log('connection1', e)
})
})
this.peer.on('error', (error) => {
if (error.type === 'unavailable-id') {
console.log('unavailable-id')
connect(id)
}
})
// return await new Promise((resolve, reject) => {
// try {
// this.peer = new Peer(id)
// this.peer.on('connection', (e) => {
// console.log('connection1', e)
// })
// this.peer
// .once('open', (e) => {
// resolve(e)
// })
// .once('error', (error) => {
// if (error.type === 'unavailable-id') {
// const connection = this.peer!.connect(id)!
// connection
// .once('open', () => {
// console.log('open')
// console.log('connection', connection)
// this.connection = connection
// resolve(id)
// })
// .once('error', (error) => {
// reject(error)
// })
// } else {
// debugger
// reject(error)
// }
// })
// } catch (error) {
// reject(error)
// }
// })
}
async sendMessage(message: string) {
return await new Promise<void>((resolve, reject) => {
if (this.connection) {
this.connection.send(message)
resolve(undefined)
} else {
reject(new Error('Connection not established.'))
}
})
}
onMessage(callback: (message: string) => void) {
this.connection?.on('data', (data: any) => {
// callback(data)
})
}
close() {
this.connection?.close()
}
}
export const PeerClientImpl = PeerClientExtern.impl(new PeerClient())

View file

@ -1,7 +1,8 @@
import { createStorage } from 'unstorage'
import indexedDbDriver from 'unstorage/drivers/indexedb' import indexedDbDriver from 'unstorage/drivers/indexedb'
import { webExtensionDriver, createStorage } from 'wxt/storage'
import { IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage' import { IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage'
import { STORAGE_NAME } from '@/constants' import { STORAGE_NAME } from '@/constants'
import { webExtensionDriver } from '@/utils/webExtensionDriver'
const indexDBStorage = createStorage({ const indexDBStorage = createStorage({
driver: indexedDbDriver({ base: `${STORAGE_NAME}:` }) driver: indexedDbDriver({ base: `${STORAGE_NAME}:` })

View file

@ -0,0 +1,18 @@
import { Observable } from 'rxjs'
export type Subscribe<T> = (callback: (event: T) => void) => void
const callbackToObservable = <T>(subscribe: Subscribe<T>, unsubscribe?: () => void) => {
return new Observable((subscriber) => {
subscribe((event: T) => {
subscriber.next(event)
})
return () => {
unsubscribe?.()
subscriber.complete()
}
})
}
export default callbackToObservable

View file

@ -7,4 +7,5 @@ export { default as chunk } from './chunk'
export { default as compressImage } from './compressImage' export { default as compressImage } from './compressImage'
export { default as isNullish } from './isNullish' export { default as isNullish } from './isNullish'
export { default as checkSystemDarkMode } from './checkSystemDarkMode' export { default as checkSystemDarkMode } from './checkSystemDarkMode'
export { default as storageToObservable } from './storageToObservable' export { default as callbackToObservable } from './callbackToObservable'
export { default as stringToHex } from './stringToHex'

View file

@ -1,15 +0,0 @@
import { Observable } from 'rxjs'
import { type Storage } from '@/domain/externs/Storage'
const storageToObservable = (storage: Storage) => {
return new Observable((subscriber) => {
storage.watch((event) => {
subscriber.next(event)
})
return () => {
storage.unwatch()
}
})
}
export default storageToObservable

5
src/utils/stringToHex.ts Normal file
View file

@ -0,0 +1,5 @@
const stringToHex = (string: string) => {
return [...string].map((char) => char.charCodeAt(0).toString(16)).join('')
}
export default stringToHex

View file

@ -0,0 +1,82 @@
import { type Driver, type WatchCallback, defineDriver } from 'unstorage'
import browser, { type Storage as BrowserStorage } from 'webextension-polyfill'
export interface WebExtensionDriverOptions {
storageArea: 'sync' | 'local' | 'managed' | 'session'
}
export const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver = defineDriver((opts) => {
const checkPermission = () => {
if (browser.storage == null) throw Error("You must request the 'storage' permission to use webExtensionDriver")
}
const _storageListener: (changes: BrowserStorage.StorageAreaSyncOnChangedChangesType) => void = (changes) => {
Object.entries(changes).forEach(([key, { newValue }]) => {
_listeners.forEach((callback) => {
callback(newValue ? 'update' : 'remove', key)
})
})
}
const _listeners = new Set<WatchCallback>()
return {
name: 'web-extension:' + opts.storageArea,
async hasItem(key) {
checkPermission()
const res = await browser.storage[opts.storageArea].get(key)
return res[key] != null
},
async getItem(key) {
checkPermission()
const res = await browser.storage[opts.storageArea].get(key)
return res[key] ?? null
},
async getItems(items) {
checkPermission()
const res = await browser.storage[opts.storageArea].get(items.map((item) => item.key))
return items.map((item) => ({
key: item.key,
value: res[item.key] ?? null
}))
},
async setItem(key, value) {
checkPermission()
await browser.storage[opts.storageArea].set({ [key]: value ?? null })
},
async setItems(items) {
checkPermission()
const map = items.reduce<Record<string, any>>((map, item) => {
map[item.key] = item.value ?? null
return map
}, {})
await browser.storage[opts.storageArea].set(map)
},
async removeItem(key) {
checkPermission()
await browser.storage[opts.storageArea].remove(key)
},
async getKeys() {
checkPermission()
const all = await browser.storage[opts.storageArea].get()
return Object.keys(all)
},
async clear() {
checkPermission()
await browser.storage[opts.storageArea].clear()
},
watch(callback) {
checkPermission()
_listeners.add(callback)
if (_listeners.size === 1) {
browser.storage[opts.storageArea].onChanged.addListener(_storageListener)
}
return () => {
_listeners.delete(callback)
if (_listeners.size === 0) {
browser.storage[opts.storageArea].onChanged.removeListener(_storageListener)
}
}
}
}
})