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

View file

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

View file

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

View file

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

View file

@ -2,14 +2,19 @@ import { Remesh } from 'remesh'
import { ListModule } from 'remesh/modules/list'
import { nanoid } from 'nanoid'
import { from, map, tap, merge } from 'rxjs'
import { RemeshYjs } from 'remesh-yjs'
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({
name: 'MessageListDomain',
impl: (domain) => {
const storage = domain.getExtern(IndexDBStorageExtern)
const peerClient = domain.getExtern(PeerClientExtern)
const storageKey = `MESSAGE_LIST` as const
peerClient.connect(hostRoomId)
const MessageListModule = ListModule<Message>(domain, {
name: 'MessageListModule',
@ -100,17 +105,31 @@ const MessageListDomain = Remesh.domain({
}
})
RemeshYjs(domain, {
key: 'MessageList',
dataType: 'array',
onSend: ({ get }): Message[] => {
return get(ListQuery())
},
onReceive: (_, messages: Message[]) => {
return InitListCommand(messages)
domain.effect({
name: 'FormStateToPeerClientEffect',
impl: ({ fromEvent }) => {
const createItem$ = fromEvent(CreateItemEvent).pipe(
tap(async (message) => {
await peerClient.sendMessage(JSON.stringify(message))
})
)
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 {
query: {
ItemQuery,

View file

@ -1,7 +1,8 @@
import { Remesh } from 'remesh'
import { forkJoin, from, map, merge, switchMap, tap } from 'rxjs'
import { BrowserSyncStorageExtern } from './externs/Storage'
import { isNullish, storageToObservable } from '@/utils'
import { isNullish } from '@/utils'
import callbackToObservable from '@/utils/callbackToObservable'
const UserInfoDomain = Remesh.domain({
name: 'UserInfoDomain',
@ -104,7 +105,7 @@ const UserInfoDomain = Remesh.domain({
domain.effect({
name: 'WatchStorageToStateEffect',
impl: () => {
return storageToObservable(storage).pipe(
return callbackToObservable(storage.watch, storage.unwatch).pipe(
switchMap(() => {
return forkJoin({
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: {
name: 'STORAGE',
get: async () => {
throw new Error('"get" not implemented')
throw new Error('"get" not implemented.')
},
set: async () => {
throw new Error('"set" not implemented')
throw new Error('"set" not implemented.')
},
remove: async () => {
throw new Error('"remove" not implemented')
throw new Error('"remove" not implemented.')
},
clear: async () => {
throw new Error('"clear" not implemented')
throw new Error('"clear" not implemented.')
},
watch: () => {
throw new Error('"watch" not implemented')
throw new Error('"watch" not implemented.')
},
unwatch: () => {
throw new Error('"unwatch" not implemented')
throw new Error('"unwatch" not implemented.')
}
}
})
@ -44,22 +44,22 @@ export const BrowserSyncStorageExtern = Remesh.extern<Storage>({
default: {
name: 'STORAGE',
get: async () => {
throw new Error('"get" not implemented')
throw new Error('"get" not implemented.')
},
set: async () => {
throw new Error('"set" not implemented')
throw new Error('"set" not implemented.')
},
remove: async () => {
throw new Error('"remove" not implemented')
throw new Error('"remove" not implemented.')
},
clear: async () => {
throw new Error('"clear" not implemented')
throw new Error('"clear" not implemented.')
},
watch: () => {
throw new Error('"watch" not implemented')
throw new Error('"watch" not implemented.')
},
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 { webExtensionDriver, createStorage } from 'wxt/storage'
import { IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage'
import { STORAGE_NAME } from '@/constants'
import { webExtensionDriver } from '@/utils/webExtensionDriver'
const indexDBStorage = createStorage({
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 isNullish } from './isNullish'
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)
}
}
}
}
})