feat: migrate extension from Plasmo to WXT (#1172)

* feat: migrate extension framework to wxt

- Replace Plasmo entrypoints, messaging, and storage usage with WXT-compatible implementations

- Update workflows, build scripts, and locale paths for WXT outputs

- Switch store submission to wxt submit with optional dry-run input

* fix(ci): fallback to vars for submit credentials

- Resolve missing required config in submit workflow when values are stored in GitHub Variables instead of Secrets

- Keep existing secrets first and add vars fallback for Chrome, Firefox, and Edge credentials
This commit is contained in:
DIYgod
2026-02-11 15:33:09 +08:00
committed by GitHub
parent c93d74d468
commit ec2f091235
50 changed files with 2814 additions and 4345 deletions
+5 -4
View File
@@ -6,8 +6,9 @@ on:
- "*"
env:
PLASMO_PUBLIC_UMAMI_ID: ${{ vars.PLASMO_PUBLIC_UMAMI_ID }}
PLASMO_PUBLIC_UMAMI_URL: ${{ vars.PLASMO_PUBLIC_UMAMI_URL }}
WXT_UMAMI_ID: ${{ vars.WXT_UMAMI_ID || vars.PLASMO_PUBLIC_UMAMI_ID }}
WXT_UMAMI_URL: ${{ vars.WXT_UMAMI_URL || vars.PLASMO_PUBLIC_UMAMI_URL }}
WXT_UMAMI_SAMPLE_RATE: ${{ vars.WXT_UMAMI_SAMPLE_RATE || vars.PLASMO_PUBLIC_UMAMI_SAMPLE_RATE }}
jobs:
build:
@@ -32,9 +33,9 @@ jobs:
node-version: 22.x
cache: "pnpm"
- name: Package the extension for Chrome
run: pnpm build --zip
run: pnpm zip
- name: Package the extension for Firefox
run: pnpm build:firefox --zip
run: pnpm zip:firefox
- name: Package the extension for Safari
run: pnpm build:safari:zip
- name: Upload file
+35 -12
View File
@@ -1,10 +1,17 @@
name: "Submit to Web Store"
on:
workflow_dispatch:
inputs:
dry_run:
description: "Run submission in dry-run mode"
required: false
default: false
type: boolean
env:
PLASMO_PUBLIC_UMAMI_ID: ${{ vars.PLASMO_PUBLIC_UMAMI_ID }}
PLASMO_PUBLIC_UMAMI_URL: ${{ vars.PLASMO_PUBLIC_UMAMI_URL }}
WXT_UMAMI_ID: ${{ vars.WXT_UMAMI_ID || vars.PLASMO_PUBLIC_UMAMI_ID }}
WXT_UMAMI_URL: ${{ vars.WXT_UMAMI_URL || vars.PLASMO_PUBLIC_UMAMI_URL }}
WXT_UMAMI_SAMPLE_RATE: ${{ vars.WXT_UMAMI_SAMPLE_RATE || vars.PLASMO_PUBLIC_UMAMI_SAMPLE_RATE }}
jobs:
build:
@@ -28,14 +35,30 @@ jobs:
node-version: 22.x
cache: "pnpm"
- name: Package the extension for Chrome
run: pnpm build --zip
run: pnpm zip
- name: Package the extension for Firefox
run: pnpm build:firefox --zip
- name: Browser Platform Publish
uses: PlasmoHQ/bpp@v3
with:
keys: ${{ secrets.SUBMIT_KEYS }}
chrome-file: build/chrome-mv3-prod.zip
edge-file: build/chrome-mv3-prod.zip
firefox-file: build/firefox-mv3-prod.zip
notes: "RSSHub Radar is an open source project, you can find the source code at https://github.com/DIYgod/RSSHub-Radar"
run: pnpm zip:firefox
- name: Submit to web stores
env:
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID || vars.CHROME_EXTENSION_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID || vars.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET || vars.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN || vars.CHROME_REFRESH_TOKEN }}
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID || vars.FIREFOX_EXTENSION_ID }}
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER || vars.FIREFOX_JWT_ISSUER }}
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET || vars.FIREFOX_JWT_SECRET }}
EDGE_PRODUCT_ID: ${{ secrets.EDGE_PRODUCT_ID || vars.EDGE_PRODUCT_ID }}
EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID || vars.EDGE_CLIENT_ID }}
EDGE_API_KEY: ${{ secrets.EDGE_API_KEY || vars.EDGE_API_KEY }}
run: |
DRY_RUN_FLAG=""
if [ "${{ inputs.dry_run }}" = "true" ]; then
DRY_RUN_FLAG="--dry-run"
fi
pnpm wxt submit \
--chrome-zip .output/*-chrome.zip \
--firefox-zip .output/*-firefox.zip \
--firefox-sources-zip .output/*-sources.zip \
--edge-zip .output/*-chrome.zip \
${DRY_RUN_FLAG}
+5 -4
View File
@@ -4,8 +4,9 @@ on:
push:
env:
PLASMO_PUBLIC_UMAMI_ID: ${{ vars.PLASMO_PUBLIC_UMAMI_ID }}
PLASMO_PUBLIC_UMAMI_URL: ${{ vars.PLASMO_PUBLIC_UMAMI_URL }}
WXT_UMAMI_ID: ${{ vars.WXT_UMAMI_ID || vars.PLASMO_PUBLIC_UMAMI_ID }}
WXT_UMAMI_URL: ${{ vars.WXT_UMAMI_URL || vars.PLASMO_PUBLIC_UMAMI_URL }}
WXT_UMAMI_SAMPLE_RATE: ${{ vars.WXT_UMAMI_SAMPLE_RATE || vars.PLASMO_PUBLIC_UMAMI_SAMPLE_RATE }}
jobs:
build:
@@ -30,9 +31,9 @@ jobs:
node-version: 22.x
cache: "pnpm"
- name: Package the extension for Chrome
run: pnpm build --zip
run: pnpm zip
- name: Package the extension for Firefox
run: pnpm build:firefox --zip
run: pnpm zip:firefox
- name: Package the extension for Safari
run: pnpm build:safari:zip
- name: Archive production artifacts
-2
View File
@@ -15,8 +15,6 @@ export default {
"<BUILTIN_MODULES>", // Node.js built-in modules
"<THIRD_PARTY_MODULES>", // Imports not matched by other special words or groups.
"", // Empty line
"^@plasmo/(.*)$",
"",
"^@plasmohq/(.*)$",
"",
"^~(.*)$",
+4 -4
View File
@@ -67,7 +67,7 @@ Open the upper right corner `Developer mode`
Click `Load unpacked extension` in the upper left corner
Select the unzipped `dist` directory
Select the unzipped extension directory containing `manifest.json`
**Firefox install extension:**
@@ -75,13 +75,13 @@ Open `about:debugging`
Click `Load Temporary Extension` in the upper right corner
Select the `manifest.json` file in the unzipped `dist` directory
Select the `manifest.json` file in the unzipped extension directory
## Join us
### Start the Development Server
Also refer to: https://docs.plasmo.com/framework/workflows/dev
Also refer to: https://wxt.dev/guide/installation
```
pnpm i
@@ -95,7 +95,7 @@ npm install
npm run dev
```
Get the `build/chrome-mv3-dev` directory, the installation method refers to [manual installation](#manual installation)
Get the `.output/chrome-mv3` directory, the installation method refers to [manual installation](#manual installation)
### Submit New RSSHub Radar Rules
+18 -31
View File
@@ -6,21 +6,22 @@
"author": "DIYgod",
"packageManager": "pnpm@9.12.3",
"scripts": {
"dev": "plasmo dev --no-cs-reload",
"build": "plasmo build",
"dev:firefox": "plasmo dev --target=firefox-mv3 --no-cs-reload",
"build:firefox": "plasmo build --target=firefox-mv3",
"build:safari": "plasmo build --target=safari-mv3",
"safari-convert": "xcrun safari-web-extension-converter build/safari-mv3-prod --project-location build --bundle-identifier app.rsshub.RSSHub-Radar --force",
"dev": "wxt",
"build": "wxt build",
"dev:firefox": "wxt -b firefox --mv3",
"build:firefox": "wxt build -b firefox --mv3",
"build:safari": "wxt build -b safari --mv3",
"zip": "wxt zip && mkdir -p build && cp .output/rsshub-radar-*-chrome.zip build/chrome-mv3-prod.zip",
"zip:firefox": "wxt zip -b firefox --mv3 && mkdir -p build && cp .output/rsshub-radar-*-firefox.zip build/firefox-mv3-prod.zip",
"safari-convert": "xcrun safari-web-extension-converter .output/safari-mv3 --project-location build --bundle-identifier app.rsshub.RSSHub-Radar --force",
"safari-zip": "zip -r build/safari-mv3-prod.zip \"build/RSSHub Radar\"",
"build:safari:zip": "npm run build:safari && npm run safari-convert && npm run safari-zip",
"package": "plasmo package",
"prepare": "husky install"
"build:safari:zip": "pnpm build:safari && pnpm safari-convert && pnpm safari-zip",
"package": "pnpm zip && pnpm zip:firefox",
"prepare": "husky install",
"postinstall": "wxt prepare"
},
"dependencies": {
"@iconify-json/mingcute": "^1.2.3",
"@plasmohq/messaging": "^0.6.2",
"@plasmohq/storage": "^1.13.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-dialog": "1.1.6",
"@radix-ui/react-label": "^2.1.2",
@@ -34,7 +35,6 @@
"lodash": "^4.17.21",
"lucide-react": "^0.475.0",
"md5.js": "^1.3.5",
"plasmo": "0.89.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hot-toast": "^2.5.1",
@@ -54,6 +54,7 @@
"@types/node": "22.13.1",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.1",
"@wxt-dev/module-react": "^1.1.5",
"autoprefixer": "^10.4.20",
"husky": "9.1.7",
"lint-staged": "15.4.3",
@@ -62,29 +63,15 @@
"prettier-package-json": "2.8.0",
"shadcn-ui": "^0.9.4",
"tailwindcss": "^3.4.13",
"typescript": "5.7.3"
"typescript": "5.7.3",
"wxt": "^0.20.14"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"manifest": {
"default_locale": "en",
"host_permissions": [
"https://*/*"
],
"permissions": [
"tabs",
"offscreen",
"storage",
"alarms"
],
"optional_permissions": [
"notifications"
],
"browser_specific_settings": {
"gecko": {
"id": "i@diygod.me"
}
"pnpm": {
"overrides": {
"publish-browser-extension": "3.0.3"
}
}
}
+2274 -4101
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+38
View File
@@ -1,3 +1,9 @@
import contentReady from "./messages/contentReady"
import popupReady from "./messages/popupReady"
import refreshRules from "./messages/refreshRules"
import requestDisplayedRules from "./messages/requestDisplayedRules"
import responseDisplayedRules from "./messages/responseDisplayedRules"
import responseRSS from "./messages/responseRSS"
import { deleteCachedRSS, getRSS } from "./rss"
import { initSchedule } from "./rules"
import { initUpdateNotifications } from "./update-notifications"
@@ -29,5 +35,37 @@ chrome.tabs.onRemoved.addListener((tabId) => {
deleteCachedRSS(tabId)
})
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const run = async () => {
switch (msg?.name) {
case "contentReady":
return contentReady(msg, sender)
case "popupReady":
return popupReady(msg, sender)
case "refreshRules":
return refreshRules(msg, sender)
case "requestDisplayedRules":
return requestDisplayedRules(msg, sender)
case "responseDisplayedRules":
return responseDisplayedRules(msg, sender)
case "responseRSS":
return responseRSS(msg, sender)
default:
return undefined
}
}
run()
.then((res) => {
sendResponse(res)
})
.catch((error) => {
console.error(error)
sendResponse(undefined)
})
return true
})
initSchedule()
initUpdateNotifications()
+5 -5
View File
@@ -1,10 +1,10 @@
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { getRSS } from "~/background/rss"
const handler: PlasmoMessaging.MessageHandler = (req, res) => {
getRSS(req.sender.tab.id, req.sender.tab.url)
res.send("")
const handler = (_message: unknown, sender: chrome.runtime.MessageSender) => {
if (sender.tab?.id && sender.tab?.url) {
getRSS(sender.tab.id, sender.tab.url)
}
return ""
}
export default handler
+15 -13
View File
@@ -1,18 +1,20 @@
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { getCachedRSS } from "~/background/rss"
const handler: PlasmoMessaging.MessageHandler = (req, res) => {
chrome.tabs.query(
{
active: true,
lastFocusedWindow: true,
},
([tab]) => {
res.send(getCachedRSS(tab.id))
},
)
return true
const handler = (
_message?: unknown,
_sender?: chrome.runtime.MessageSender,
) => {
return new Promise((resolve) => {
chrome.tabs.query(
{
active: true,
lastFocusedWindow: true,
},
([tab]) => {
resolve(getCachedRSS(tab.id))
},
)
})
}
export default handler
+5 -6
View File
@@ -1,11 +1,10 @@
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { refreshRules } from "~/background/rules"
const handler: PlasmoMessaging.MessageHandler = (req, res) => {
refreshRules().then(() => {
res.send(true)
})
const handler = async (
_message?: unknown,
_sender?: chrome.runtime.MessageSender,
) => {
await refreshRules()
return true
}
@@ -1,12 +1,8 @@
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { getDisplayedRules } from "~/background/rules"
const handler: PlasmoMessaging.MessageHandler = (req, res) => {
getDisplayedRules().then((rules) => {
res.send(rules)
})
return true
}
const handler = async (
_message?: unknown,
_sender?: chrome.runtime.MessageSender,
) => getDisplayedRules()
export default handler
@@ -1,10 +1,11 @@
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { setDisplayedRules } from "~/background/rules"
const handler: PlasmoMessaging.MessageHandler = (req, res) => {
setDisplayedRules(req.body.displayedRules)
res.send("")
const handler = (
message: { body?: { displayedRules?: any } },
_sender?: chrome.runtime.MessageSender,
) => {
setDisplayedRules(message?.body?.displayedRules)
return ""
}
export default handler
+9 -5
View File
@@ -1,10 +1,14 @@
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { setRSS } from "~/background/rss"
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
setRSS(req.body.tabId || req.sender.tab.id, req.body.rss)
res.send("")
const handler = async (
message: { body?: { tabId?: number; rss?: any } },
sender?: chrome.runtime.MessageSender,
) => {
const tabId = message?.body?.tabId ?? sender?.tab?.id
if (tabId) {
setRSS(tabId, message?.body?.rss)
}
return ""
}
export default handler
+16 -21
View File
@@ -1,19 +1,14 @@
import AsyncLock from "async-lock"
import { sendToContentScript } from "@plasmohq/messaging"
import { Storage } from "@plasmohq/storage"
import { sendToContentScript } from "~/lib/messaging"
import { setupOffscreenDocument } from "~/lib/offscreen"
import report from "~/lib/report"
import { getLocalStorage } from "~/lib/storage"
import type { RSSData } from "~/lib/types"
import { getRSSHub as sandboxGetRSSHub } from "~/sandboxes"
import { getRSSHub as sandboxGetRSSHub } from "~/tabs/sandboxes"
import { setBadge } from "./badge"
const storage = new Storage({
area: "local",
})
const savedRSS: {
[tabId: number]: {
pageRSSHub: RSSData[]
@@ -44,8 +39,8 @@ export const getRSS = async (tabId, url) => {
tabId,
})
if (chrome.offscreen && chrome.runtime.getContexts) {
await setupOffscreenDocument("tabs/offscreen.html")
if (chrome.offscreen && (chrome.runtime as any).getContexts) {
await setupOffscreenDocument("offscreen.html")
chrome.runtime.sendMessage({
target: "offscreen",
data: {
@@ -54,7 +49,7 @@ export const getRSS = async (tabId, url) => {
tabId,
html,
url,
rules: await storage.get("rules"),
rules: await getLocalStorage("rules"),
},
},
})
@@ -62,7 +57,7 @@ export const getRSS = async (tabId, url) => {
const rsshub = sandboxGetRSSHub({
html,
url,
rules: await storage.get("rules"),
rules: await getLocalStorage("rules"),
})
setRSS(tabId, rsshub)
}
@@ -84,12 +79,14 @@ export const getCachedRSS = (tabId) => {
export const setRSS = async (
tabId,
data: {
pageRSS: RSSData[]
} | {
pageRSSHub: RSSData[]
websiteRSSHub: RSSData[]
},
data:
| {
pageRSS: RSSData[]
}
| {
pageRSSHub: RSSData[]
websiteRSSHub: RSSData[]
},
) => {
if (!data) {
return
@@ -111,9 +108,7 @@ export const setRSS = async (
let text = ""
if (savedRSS[tabId].pageRSS.length || savedRSS[tabId].pageRSSHub.length) {
text =
savedRSS[tabId].pageRSS.length +
savedRSS[tabId].pageRSSHub.length +
""
savedRSS[tabId].pageRSS.length + savedRSS[tabId].pageRSSHub.length + ""
} else if (savedRSS[tabId].websiteRSSHub.length) {
text = " "
}
+8 -13
View File
@@ -1,19 +1,14 @@
import { Storage } from "@plasmohq/storage"
import { getConfig } from "~/lib/config"
import { setupOffscreenDocument } from "~/lib/offscreen"
import { getRemoteRules } from "~/lib/rules"
import { getDisplayedRules as sandboxGetDisplayedRules } from "~/sandboxes"
const storage = new Storage({
area: "local",
})
import { getLocalStorage, setLocalStorage } from "~/lib/storage"
import { getDisplayedRules as sandboxGetDisplayedRules } from "~/tabs/sandboxes"
export const refreshRules = async () => {
const rules = await getRemoteRules()
await storage.set("rules", rules)
if (chrome.offscreen && chrome.runtime.getContexts) {
await setupOffscreenDocument("tabs/offscreen.html")
await setLocalStorage("rules", rules)
if (chrome.offscreen && (chrome.runtime as any).getContexts) {
await setupOffscreenDocument("offscreen.html")
chrome.runtime.sendMessage({
target: "offscreen",
data: {
@@ -30,10 +25,10 @@ export const refreshRules = async () => {
return rules
}
export const getDisplayedRules = () => storage.get("displayedRules")
export const getDisplayedRules = () => getLocalStorage("displayedRules")
export const setDisplayedRules = (displayedRules) =>
storage.set("displayedRules", displayedRules)
setLocalStorage("displayedRules", displayedRules)
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "refreshRulesAlarm") {
@@ -43,7 +38,7 @@ chrome.alarms.onAlarm.addListener((alarm) => {
export async function initSchedule() {
const config = await getConfig()
const rules = await storage.get("rules")
const rules = await getLocalStorage("rules")
if (!rules) {
setTimeout(() => {
refreshRules()
+4 -9
View File
@@ -1,15 +1,10 @@
import RSSHubIcon from "data-base64:~/assets/icon.png"
import { Storage } from "@plasmohq/storage"
import RSSHubIcon from "~/assets/icon.png"
import { getLocalStorage, setLocalStorage } from "~/lib/storage"
import info from "../../package.json"
const storage = new Storage({
area: "local",
})
export const initUpdateNotifications = async () => {
const version = await storage.get("version")
const version = await getLocalStorage("version")
if (version === info.version) return
chrome.notifications?.create("RSSHubRadarUpdate", {
@@ -28,5 +23,5 @@ export const initUpdateNotifications = async () => {
chrome.notifications?.clear("RSSHubRadarUpdate")
}
})
await storage.set("version", info.version)
await setLocalStorage("version", info.version)
}
+1 -2
View File
@@ -1,5 +1,4 @@
import { sendToBackground } from "@plasmohq/messaging"
import { sendToBackground } from "~/lib/messaging"
import { getPageRSS } from "~/lib/rss"
sendToBackground({
+3
View File
@@ -0,0 +1,3 @@
export default defineBackground(() => {
import("~/background/index")
})
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ["<all_urls>"],
main() {
import("~/contents/index")
},
})
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RSSHub Radar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import ReactDOM from "react-dom/client"
import OffscreenPage from "~/tabs/offscreen"
ReactDOM.createRoot(document.getElementById("root")!).render(<OffscreenPage />)
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RSSHub Radar</title>
<meta name="manifest.open_in_tab" content="true" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import ReactDOM from "react-dom/client"
import Options from "~/options/index"
ReactDOM.createRoot(document.getElementById("root")!).render(<Options />)
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RSSHub Radar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import ReactDOM from "react-dom/client"
import IndexPopup from "~/popup/index"
ReactDOM.createRoot(document.getElementById("root")!).render(<IndexPopup />)
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RSSHub Radar</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import ReactDOM from "react-dom/client"
import PreviewPage from "~/tabs/preview"
ReactDOM.createRoot(document.getElementById("root")!).render(<PreviewPage />)
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RSSHub Radar</title>
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
import "~/tabs/sandboxes"
+4 -6
View File
@@ -1,9 +1,7 @@
import _ from "lodash"
import toast from "react-hot-toast"
import { Storage } from "@plasmohq/storage"
const storage = new Storage()
import { getLocalStorage, setLocalStorage } from "~/lib/storage"
export const defaultConfig = {
rsshubDomain: "https://rsshub.app",
@@ -45,7 +43,7 @@ export const defaultConfig = {
export async function getConfig() {
let storagedConfig = {}
try {
storagedConfig = await storage.get("config")
storagedConfig = await getLocalStorage("config")
} catch (error) {}
return _.merge({}, defaultConfig, storagedConfig) as typeof defaultConfig
}
@@ -54,10 +52,10 @@ let toastId: string | undefined
export async function setConfig(config: Partial<typeof defaultConfig>) {
let storagedConfig = {}
try {
storagedConfig = await storage.get("config")
storagedConfig = await getLocalStorage("config")
} catch (error) {}
config = _.merge({}, storagedConfig, config)
await storage.set("config", config)
await setLocalStorage("config", config)
toastId = toast.success("Saved", {
id: toastId,
})
+36
View File
@@ -0,0 +1,36 @@
export type ExtensionMessage = {
name: string
body?: any
}
export function sendToBackground<T = any>(message: ExtensionMessage) {
return new Promise<T>((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
return
}
resolve(response as T)
})
})
}
export function sendToContentScript<T = any>({
name,
tabId,
body,
}: {
name: string
tabId: number
body?: any
}) {
return new Promise<T>((resolve, reject) => {
chrome.tabs.sendMessage(tabId, { name, body }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
return
}
resolve(response as T)
})
})
}
+10 -9
View File
@@ -1,14 +1,15 @@
// https://developer.chrome.com/docs/extensions/reference/api/offscreen
let creating
async function setupOffscreenDocument(path) {
const offscreenUrl = chrome.runtime.getURL(path)
// @ts-ignore
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ["OFFSCREEN_DOCUMENT"],
documentUrls: [offscreenUrl],
})
if (existingContexts.length > 0) {
async function setupOffscreenDocument(path: string) {
const offscreenUrl = chrome.runtime.getURL(path)
const existingContexts = (await (chrome.runtime as any).getContexts({
contextTypes: ["OFFSCREEN_DOCUMENT"] as any,
documentUrls: [offscreenUrl],
})) as chrome.runtime.ExtensionContext[] | undefined
if (existingContexts?.length) {
return
}
@@ -16,7 +17,7 @@ async function setupOffscreenDocument(path) {
await creating
} else {
creating = chrome.offscreen.createDocument({
url: chrome.runtime.getURL("tabs/offscreen.html"),
url: offscreenUrl,
reasons: [chrome.offscreen.Reason.IFRAME_SCRIPTING],
justification: "Get RSS in the sandbox for enhanced security.",
})
+7 -7
View File
@@ -1,10 +1,10 @@
import FollowLogo from "data-base64:~/assets/follow.svg"
import InoreaderLogo from "data-base64:~/assets/inoreader.svg"
import MinifluxLogo from "data-base64:~/assets/miniflux.ico"
import FreshRSSLogo from "data-base64:~/assets/freshrss.svg"
import TinyTinyRSSLogo from "data-base64:~/assets/ttrss.png"
import FeedlyLogo from "data-base64:~/assets/feedly.png"
import NewsBlurLogo from "data-base64:~/assets/newsblur.png"
import FeedlyLogo from "~/assets/feedly.png"
import FollowLogo from "~/assets/follow.svg"
import FreshRSSLogo from "~/assets/freshrss.svg"
import InoreaderLogo from "~/assets/inoreader.svg"
import MinifluxLogo from "~/assets/miniflux.ico"
import NewsBlurLogo from "~/assets/newsblur.png"
import TinyTinyRSSLogo from "~/assets/ttrss.png"
export const logoMap = new Map<string, string>([
["follow", FollowLogo],
+10 -9
View File
@@ -1,6 +1,6 @@
function isSampled(rate) {
const randomNumber = Math.floor(Math.random() * 100);
return randomNumber < rate * 100;
const randomNumber = Math.floor(Math.random() * 100)
return randomNumber < rate * 100
}
function report({
@@ -10,11 +10,12 @@ function report({
url?: string
name?: string
}) {
if (
process.env.PLASMO_PUBLIC_UMAMI_ID &&
process.env.PLASMO_PUBLIC_UMAMI_URL &&
(name || isSampled(parseFloat(process.env.PLASMO_PUBLIC_UMAMI_SAMPLE_RATE) || 0.01))
) {
const umamiId = import.meta.env.WXT_UMAMI_ID
const umamiUrl = import.meta.env.WXT_UMAMI_URL
const sampleRate =
parseFloat(import.meta.env.WXT_UMAMI_SAMPLE_RATE || "") || 0.01
if (umamiId && umamiUrl && (name || isSampled(sampleRate))) {
let hostname = ""
try {
hostname = new URL(url).hostname
@@ -26,13 +27,13 @@ function report({
language: chrome?.i18n?.getUILanguage(),
referrer: hostname,
url: hostname,
website: process.env.PLASMO_PUBLIC_UMAMI_ID,
website: umamiId,
name: name,
},
type: "event",
}
fetch(`${process.env.PLASMO_PUBLIC_UMAMI_URL}/api/send`, {
fetch(`${umamiUrl}/api/send`, {
method: "POST",
headers: {
"content-type": "application/json",
+30
View File
@@ -0,0 +1,30 @@
export function getLocalStorage<T = any>(key: string) {
return new Promise<T>((resolve, reject) => {
chrome.storage.local.get(key, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
return
}
resolve(result[key] as T)
})
})
}
export function setLocalStorage<T = any>(key: string, value: T) {
return new Promise<void>((resolve, reject) => {
chrome.storage.local.set(
{
[key]: value,
},
() => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
return
}
resolve()
},
)
})
}
+2 -6
View File
@@ -1,7 +1,7 @@
import RSSHubIcon from "data-base64:~/assets/icon.png"
import { Link, useLocation } from "react-router"
import { useMediaQuery } from "usehooks-ts"
import RSSHubIcon from "~/assets/icon.png"
import { AppearanceSwitch } from "~/lib/components/AppearanceSwitch"
import {
Sheet,
@@ -13,11 +13,7 @@ import { cn } from "~/lib/utils"
import info from "../../package.json"
function SiderbarContent({
withClose,
}: {
withClose?: boolean
}) {
function SiderbarContent({ withClose }: { withClose?: boolean }) {
const location = useLocation()
const links = [
{
+64 -21
View File
@@ -3,33 +3,67 @@ import { Loader2 } from "lucide-react"
import { useEffect, useState } from "react"
import toast from "react-hot-toast"
import { sendToBackground } from "@plasmohq/messaging"
import { useStorage } from "@plasmohq/storage/hook"
import { Button } from "~/lib/components/Button"
import { Card, CardContent, CardHeader, CardTitle } from "~/lib/components/Card"
import { Input } from "~/lib/components/Input"
import { Label } from "~/lib/components/Label"
import { Switch } from "~/lib/components/Switch"
import { defaultConfig, setConfig } from "~/lib/config"
import { defaultConfig, getConfig, setConfig } from "~/lib/config"
import { sendToBackground } from "~/lib/messaging"
import { quickSubscriptions } from "~/lib/quick-subscriptions"
import { logoMap } from "~/lib/quick-subscriptions-logos"
import report from "~/lib/report"
import { getRulesCount, parseRules } from "~/lib/rules"
import type { Rules as IRules } from "~/lib/types"
import { getRadarRulesUrl } from "~/lib/utils"
import { logoMap } from "~/lib/quick-subscriptions-logos"
function General() {
let [config] = useStorage("config")
config = _.merge({}, defaultConfig, config)
const [config, setConfigState] = useState(defaultConfig)
const updateConfig = (partialConfig: Partial<typeof defaultConfig>) => {
setConfigState(
(currentConfig) =>
_.merge({}, currentConfig, partialConfig) as typeof defaultConfig,
)
void setConfig(partialConfig)
}
const [rules, setRules] = useState<IRules>({})
useEffect(() => {
getConfig().then(setConfigState)
sendToBackground({
name: "requestDisplayedRules",
}).then((res) => setRules(parseRules(res, true)))
const onStorageChange = (
changes: {
[key: string]: chrome.storage.StorageChange
},
areaName: string,
) => {
if (areaName !== "local" || !changes.config) {
return
}
setConfigState(
_.merge(
{},
defaultConfig,
changes.config.newValue,
) as typeof defaultConfig,
)
}
chrome.storage.onChanged.addListener(onStorageChange)
report({
name: "options-general",
})
return () => {
chrome.storage.onChanged.removeListener(onStorageChange)
}
}, [])
const [count, setCount] = useState(0)
@@ -102,7 +136,7 @@ function General() {
id="notificationsAndReminders"
checked={config.notice.badge}
onCheckedChange={(value) =>
setConfig({
updateConfig({
notice: {
badge: value,
},
@@ -140,7 +174,7 @@ function General() {
id="customRSSHubDomain"
value={config.rsshubDomain}
onChange={(e) =>
setConfig({
updateConfig({
rsshubDomain: e.target.value,
})
}
@@ -162,11 +196,11 @@ function General() {
id="accessKey"
value={config.rsshubAccessControl.accessKey}
onChange={(e) =>
setConfig({
updateConfig({
rsshubAccessControl: {
accessKey: e.target.value,
},
})
} as any)
}
placeholder={chrome.i18n.getMessage(
"configurationRequiredIfAccessKeysEnabled",
@@ -180,13 +214,15 @@ function General() {
<input
type="radio"
id="accessKeyTypeCode"
checked={config.rsshubAccessControl.accessKeyType === "code"}
checked={
config.rsshubAccessControl.accessKeyType === "code"
}
onChange={() =>
setConfig({
updateConfig({
rsshubAccessControl: {
accessKeyType: "code",
},
})
} as any)
}
/>
<label htmlFor="accessKeyTypeCode">code={"{md5}"}</label>
@@ -195,16 +231,20 @@ function General() {
<input
type="radio"
id="accessKeyTypeKey"
checked={config.rsshubAccessControl.accessKeyType === "key"}
checked={
config.rsshubAccessControl.accessKeyType === "key"
}
onChange={() =>
setConfig({
updateConfig({
rsshubAccessControl: {
accessKeyType: "key",
},
})
} as any)
}
/>
<label htmlFor="accessKeyTypeKey">key={"{accessKey}"}</label>
<label htmlFor="accessKeyTypeKey">
key={"{accessKey}"}
</label>
</div>
</div>
</div>
@@ -264,14 +304,17 @@ function General() {
id={quickSubscription.key}
checked={config.submitto[quickSubscription.key]}
onCheckedChange={(value) =>
setConfig({
updateConfig({
submitto: {
[quickSubscription.key]: value,
},
} as any)
}
/>
<Label className="w-28 flex gap-2 items-center" htmlFor={quickSubscription.key}>
<Label
className="w-28 flex gap-2 items-center"
htmlFor={quickSubscription.key}
>
{logoMap.get(quickSubscription.key) && (
<img
src={logoMap.get(quickSubscription.key)}
@@ -293,7 +336,7 @@ function General() {
config.submitto[quickSubscription.subscribeDomainKey]
}
onChange={(e) =>
setConfig({
updateConfig({
submitto: {
[quickSubscription.subscribeDomainKey]:
e.target.value,
+1 -2
View File
@@ -1,8 +1,6 @@
import _ from "lodash"
import { useEffect, useMemo, useState } from "react"
import { sendToBackground } from "@plasmohq/messaging"
import {
Accordion,
AccordionContent,
@@ -19,6 +17,7 @@ import {
PaginationNext,
PaginationPrevious,
} from "~/lib/components/Pagination"
import { sendToBackground } from "~/lib/messaging"
import report from "~/lib/report"
import { getRulesCount, parseRules } from "~/lib/rules"
import type { Rules as IRules } from "~/lib/types"
+29 -15
View File
@@ -1,14 +1,14 @@
import RSSHubIcon from "data-base64:~/assets/icon.png"
import MD5 from "md5.js"
import { useEffect, useState } from "react"
import { useCopyToClipboard } from "usehooks-ts"
import RSSHubIcon from "~/assets/icon.png"
import { Button } from "~/lib/components/Button"
import { defaultConfig, getConfig } from "~/lib/config"
import { quickSubscriptions } from "~/lib/quick-subscriptions"
import { logoMap } from "~/lib/quick-subscriptions-logos"
import report from "~/lib/report"
import type { RSSData } from "~/lib/types"
import { logoMap } from "~/lib/quick-subscriptions-logos"
function RSSItem({
item,
@@ -33,19 +33,20 @@ function RSSItem({
}
}, [copied])
let url = item.url.replace(
"{rsshubDomain}",
config.rsshubDomain.replace(/\/$/, ""),
).replace(/\/$/, "")
let url = item.url
.replace("{rsshubDomain}", config.rsshubDomain.replace(/\/$/, ""))
.replace(/\/$/, "")
if (type === "currentPageRSSHub" && config.rsshubAccessControl.accessKey) {
const urlObj = new URL(url)
if (config.rsshubAccessControl.accessKeyType === "key") {
urlObj.searchParams.append('key', config.rsshubAccessControl.accessKey)
urlObj.searchParams.append("key", config.rsshubAccessControl.accessKey)
} else {
const md5 = new MD5().update(urlObj.pathname + config.rsshubAccessControl.accessKey).digest("hex")
urlObj.searchParams.append('code', md5)
const md5 = new MD5()
.update(urlObj.pathname + config.rsshubAccessControl.accessKey)
.digest("hex")
urlObj.searchParams.append("code", md5)
}
url = urlObj.toString()
@@ -112,10 +113,15 @@ function RSSItem({
image: item.image,
})}`}
>
{logoMap.get(quickSubscription.key) ?
<img className="w-5 h-5 rounded" src={logoMap.get(quickSubscription.key)} /> : (chrome.i18n.getMessage(quickSubscription.name) ||
quickSubscription.name)
}
{logoMap.get(quickSubscription.key) ? (
<img
className="w-5 h-5 rounded"
src={logoMap.get(quickSubscription.key)}
/>
) : (
chrome.i18n.getMessage(quickSubscription.name) ||
quickSubscription.name
)}
</a>
</Button>
)
@@ -149,7 +155,11 @@ function RSSItem({
})
}}
>
<i className={copied ? "i-mingcute-check-line" : "i-mingcute-copy-2-line"}></i>
<i
className={
copied ? "i-mingcute-check-line" : "i-mingcute-copy-2-line"
}
></i>
</Button>
)}
{!item.isDocs && !hidePreview && (
@@ -158,7 +168,11 @@ function RSSItem({
size="sm"
className="text-primary hover:text-primary text-lg"
>
<a target="_blank" href={`/tabs/preview.html?url=${encodedUrl}`} className="flex center">
<a
target="_blank"
href={`/preview.html?url=${encodedUrl}`}
className="flex center"
>
<i className="i-mingcute-eye-line"></i>
</a>
</Button>
+1 -2
View File
@@ -2,9 +2,8 @@ import "~/lib/style.css"
import { useEffect, useState } from "react"
import { sendToBackground } from "@plasmohq/messaging"
import { useDark } from "~/lib/hooks/use-dark"
import { sendToBackground } from "~/lib/messaging"
import report from "~/lib/report"
import RSSList from "./RSSList"
+35 -14
View File
@@ -1,24 +1,45 @@
import { useRef } from "react"
import { useEffect, useRef } from "react"
import { sendToBackground } from "@plasmohq/messaging"
import { sendToBackground } from "~/lib/messaging"
window.addEventListener("message", (event) => {
if (event.data?.name.startsWith("response")) {
chrome.runtime.sendMessage(event.data)
sendToBackground(event.data)
}
})
type OffscreenMessage = {
name: string
body?: unknown
}
function OffscreenPage() {
const iframeRef = useRef<HTMLIFrameElement>(null)
chrome.runtime.onMessage.addListener((msg) => {
iframeRef.current?.contentWindow?.postMessage(msg.data, "*")
})
useEffect(() => {
const onWindowMessage = (
event: MessageEvent<Partial<OffscreenMessage>>,
) => {
if (
typeof event.data?.name === "string" &&
event.data.name.startsWith("response")
) {
chrome.runtime.sendMessage(event.data)
sendToBackground({
name: event.data.name,
body: event.data.body,
})
}
}
return (
<iframe id="sandbox" src="/sandboxes/index.html" ref={iframeRef}></iframe>
)
const onRuntimeMessage = (msg: { data?: unknown }) => {
iframeRef.current?.contentWindow?.postMessage(msg.data, "*")
}
window.addEventListener("message", onWindowMessage)
chrome.runtime.onMessage.addListener(onRuntimeMessage)
return () => {
window.removeEventListener("message", onWindowMessage)
chrome.runtime.onMessage.removeListener(onRuntimeMessage)
}
}, [])
return <iframe id="sandbox" src="/sandbox.html" ref={iframeRef}></iframe>
}
export default OffscreenPage
+1 -1
View File
@@ -3,9 +3,9 @@ import Parser from "rss-parser"
import "~/lib/style.css"
import RSSHubIcon from "data-base64:~/assets/icon.png"
import xss from "xss"
import RSSHubIcon from "~/assets/icon.png"
import {
Card,
CardContent,
+1
View File
@@ -0,0 +1 @@
export * from "~/sandboxes"
+1 -1
View File
@@ -14,7 +14,7 @@ quickSubscriptions.forEach((subscription) => {
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./**/*.tsx"],
content: ["./src/**/*.{ts,tsx,html}"],
safelist,
theme: {
container: {
+6 -7
View File
@@ -1,11 +1,10 @@
{
"extends": "plasmo/templates/tsconfig.base",
"exclude": ["node_modules"],
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"paths": {
"~/*": ["./src/*"],
},
"baseUrl": ".",
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"strict": false,
"noImplicitAny": false
},
"exclude": ["node_modules"]
}
+34
View File
@@ -0,0 +1,34 @@
import { defineConfig } from "wxt"
const iconMap = {
16: "/icon.png",
32: "/icon.png",
48: "/icon.png",
64: "/icon.png",
128: "/icon.png",
}
export default defineConfig({
srcDir: "src",
modules: ["@wxt-dev/module-react"],
manifest: ({ browser }) => ({
name: "RSSHub Radar",
description: "__MSG_extensionDescription__",
default_locale: "en",
permissions: ["tabs", "offscreen", "storage", "alarms"],
host_permissions: ["https://*/*"],
optional_permissions: ["notifications"],
icons: iconMap,
action: {
default_icon: iconMap,
},
browser_specific_settings:
browser === "firefox"
? {
gecko: {
id: "i@diygod.me",
},
}
: undefined,
}),
})