From 64473cdadf3da91d83260433ea40105ce2472b48 Mon Sep 17 00:00:00 2001 From: amsyarasyiq <82711525+amsyarasyiq@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:20:17 +0800 Subject: [PATCH] feat!: totally bunnify plugins --- src/core/polyfills/vendettaObject.tsx | 11 +-- src/core/ui/settings/pages/Plugins/index.tsx | 4 +- src/global.d.ts | 2 +- src/lib/managers/plugins.ts | 93 +++++++++----------- src/lib/utils/index.ts | 1 - src/lib/utils/without.ts | 5 -- tsconfig.json | 2 +- 7 files changed, 54 insertions(+), 64 deletions(-) delete mode 100644 src/lib/utils/without.ts diff --git a/src/core/polyfills/vendettaObject.tsx b/src/core/polyfills/vendettaObject.tsx index 1c04509..30d5cc7 100644 --- a/src/core/polyfills/vendettaObject.tsx +++ b/src/core/polyfills/vendettaObject.tsx @@ -19,6 +19,7 @@ import * as color from "@ui/color"; import * as components from "@ui/components"; import { createThemedStyleSheet } from "@ui/styles"; import * as toasts from "@ui/toasts"; +import { omit } from "es-toolkit"; import { createElement, useEffect } from "react"; import { View } from "react-native"; @@ -132,7 +133,7 @@ export const initVendettaObject = (): any => { findInTree: (tree: any, filter: any, options: any) => utils.findInTree(tree, filter, options), safeFetch: (input: RequestInfo | URL, options?: RequestInit | undefined, timeout?: number | undefined) => utils.safeFetch(input, options, timeout), unfreeze: (obj: object) => Object.isFrozen(obj) ? ({ ...obj }) : obj, - without: (object: any, ...keys: any) => utils.without(object, ...keys) + without: (object: any, ...keys: any) => omit(object, keys) }, debug: { connectToDebugger: (url: string) => debug.connectToDebugger(url), @@ -173,10 +174,10 @@ export const initVendettaObject = (): any => { plugins: plugins.sourceStore, fetchPlugin: (source: string) => plugins.fetchAndStorePlugin(source), installPlugin: (source: string, enabled = true) => plugins.installPlugin(source, enabled), - startPlugin: (id: string) => plugins.startPlugin(plugins.sourceStore[id]!.pluginId), - stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceStore[id]!.pluginId, disable), - removePlugin: (id: string) => plugins.removePlugin(plugins.sourceStore[id]!.pluginId), - getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceStore[id]!.pluginId) + startPlugin: (id: string) => plugins.startPlugin(plugins.sourceStore[id]!.id), + stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceStore[id]!.id, disable), + removePlugin: (id: string) => plugins.removePlugin(plugins.sourceStore[id]!.id), + getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceStore[id]!.id) }, themes: { themes: themes.themes, diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index e804669..0ce115d 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -2,7 +2,7 @@ import { Strings } from "@core/i18n"; import AddonPage from "@core/ui/components/AddonPage"; import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard"; import { useProxy } from "@lib/api/storage"; -import { BunnyPlugin, installPlugin, selectedSources, sourceStore } from "@lib/managers/plugins"; +import { BunnyPlugin, installPlugin, preferredSourceStore, sourceStore } from "@lib/managers/plugins"; import { settings } from "@lib/settings"; import { Author } from "@lib/utils/types"; @@ -17,7 +17,7 @@ export default function Plugins() { "manifest.description", p => p.manifest.authors?.map((a: Author) => a.name).join() ]} - items={selectedSources} + items={preferredSourceStore} resolveItem={source => sourceStore[source]} fetchFunction={installPlugin} safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS} diff --git a/src/global.d.ts b/src/global.d.ts index 4b43d15..2aa5bef 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,5 +1,5 @@ declare global { - var React: typeof import("react"); + type React = typeof import("react"); var SkiaApi: typeof import("@shopify/react-native-skia").Skia; interface Window { diff --git a/src/lib/managers/plugins.ts b/src/lib/managers/plugins.ts index d7d0934..1448fe1 100644 --- a/src/lib/managers/plugins.ts +++ b/src/lib/managers/plugins.ts @@ -1,12 +1,13 @@ +import { allSettled } from "@core/polyfills/allSettled"; import { createVdPluginObject } from "@core/polyfills/vendettaObject"; import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage"; import { settings } from "@lib/settings"; import { safeFetch } from "@lib/utils"; import { BUNNY_PROXY_PREFIX, OLD_BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants"; import invariant from "@lib/utils/invariant"; -import { proxyLazy } from "@lib/utils/lazy"; import { logger } from "@lib/utils/logger"; import { Author } from "@lib/utils/types"; +import { isNotNil, uniqWith } from "es-toolkit"; type EvaledPlugin = { onLoad?(): void; @@ -15,6 +16,7 @@ type EvaledPlugin = { }; interface PluginManifest { + id: string; name: string; description: string; authors: Author[]; @@ -28,8 +30,8 @@ interface PluginManifest { } export interface BunnyPlugin { + id: string; source: string; - pluginId: string; manifest: PluginManifest; enabled: boolean; update: boolean; @@ -40,43 +42,27 @@ export interface BunnyPlugin { // TODO: Use fs to avoid unnecessary memory usage js: string; - - /** - * @deprecated use `source` (for URL) or `pluginId` (plugin's unique ID) - * */ - id: string; } -const pluginsEnabled = () => !settings.safeMode?.enabled; +const arePluginsEnabled = () => !settings.safeMode?.enabled; const _pluginInstances: Record = {}; -export const selectedSources = wrapSync(createStorage>(createMMKVBackend("SELECTED_PLUGIN_SOURCES"))); -export const sourceStore = proxyLazy(() => { - type StorageInterface = Record; - const storagePromise = createStorage(createMMKVBackend("VENDETTA_PLUGINS")); - - storagePromise.then(st => { - for (const plugin of Object.values(st)) if (plugin) { - plugin.pluginId ??= `${plugin.manifest.name}-${plugin.manifest.authors?.[0]?.name}`.toLowerCase(); - plugin.source ??= plugin.id; - } - }); - - return wrapSync(storagePromise); -}); +export const sourceStore = wrapSync(createStorage<{ [id in string]?: BunnyPlugin }>(createMMKVBackend("PLUGIN_SOURCES_STORE"))); +export const preferredSourceStore = wrapSync(createStorage<{ [id in string]?: string }>(createMMKVBackend("PREFERRED_PLUGIN_SOURCE"))); export function getPluginById(id: string) { - if (!selectedSources[id]) { + if (!id) return undefined; + + if (!preferredSourceStore[id]) { for (const plugin of Object.values(sourceStore)) { - if (plugin?.pluginId === id) { - selectedSources[id] = plugin.source; - break; + if (plugin?.id === id) { + return plugin; } } } - return selectedSources[id] ? sourceStore[selectedSources[id]!] : undefined; + return sourceStore[preferredSourceStore[id]!]; } export async function fetchAndStorePlugin(source: string) { @@ -98,21 +84,22 @@ export async function fetchAndStorePlugin(source: string) { throw new Error(`Failed to fetch manifest for ${source}`); } + for (const f of ["id", "main", "hash", "bunny"] as const) + invariant(pluginManifest[f], `Plugin manifest does not contain mandatory field: '${f}'`); + let pluginJs: string | undefined; if (existingPlugin?.manifest.hash !== pluginManifest.hash) { try { - // by polymanifest spec, plugins should always specify their main file, but just in case - pluginJs = await (await fetch(source + (pluginManifest.main || "index.js"))).text(); + pluginJs = await (await fetch(source + pluginManifest.main)).text(); } catch { } // Empty catch, checked below } invariant(pluginJs || existingPlugin, `Failed to fetch JS from ${source}`); return sourceStore[source] = { - id: source, + id: pluginManifest.id, source: source, - pluginId: pluginManifest.name, manifest: pluginManifest, enabled: existingPlugin?.enabled ?? false, update: existingPlugin?.update ?? true, @@ -126,7 +113,7 @@ export async function installPlugin(source: string, enabled = true) { invariant(!(source in sourceStore), "Source was already installed"); const plugin = await fetchAndStorePlugin(source); - if (enabled) await startPlugin(plugin.pluginId); + if (enabled) await startPlugin(plugin.id); } /** @@ -146,7 +133,7 @@ export async function startPlugin(id: string) { invariant(plugin, "Attempted to start non-existent plugin"); try { - if (pluginsEnabled()) { + if (arePluginsEnabled()) { const pluginRet: EvaledPlugin = await evalPlugin(plugin); _pluginInstances[id] = pluginRet; pluginRet.onLoad?.(); @@ -174,7 +161,7 @@ export function stopPlugin(id: string, disable = true) { const pluginInstance = _pluginInstances[id]; invariant(plugin, "Attempted to stop non-existent plugin"); - if (pluginsEnabled()) { + if (arePluginsEnabled()) { try { pluginInstance?.onUnload?.(); } catch (e) { @@ -199,30 +186,38 @@ export async function removePlugin(id: string) { * @internal */ export async function initPlugins() { - await awaitStorage(sourceStore, selectedSources, settings); + await awaitStorage(sourceStore, preferredSourceStore, settings); - // TODO: Optimize this - if (pluginsEnabled()) { - const allIds = [...new Set(Object.values(sourceStore).map(s => s!.pluginId))]; + if (arePluginsEnabled()) { + const plugins = uniqWith( + Object.values(sourceStore) + .map(s => getPluginById(s!.id)) + .filter(isNotNil), + (a, b) => a?.id === b?.id + ); - // Loop over enabled plugins, update and start them - const enabledPlugins = allIds.filter(id => getPluginById(id)?.enabled); - await Promise.all(enabledPlugins.map(async pl => { - if (getPluginById(pl)!.update) { + const updatePromise: Promise[] = []; + const updateAndStart = async (plugin: BunnyPlugin) => { + if (plugin.update) { try { - await fetchAndStorePlugin(pl); + await fetchAndStorePlugin(plugin.id); } catch (e) { logger.error(e); } } - await startPlugin(pl); - })); + await startPlugin(plugin.id); + }; - // Loop over disabled plugins that are allowed to update - allIds - .filter(id => !getPluginById(id)?.enabled && getPluginById(id)!.update) - .forEach(id => fetchAndStorePlugin(id)); + for (const plugin of plugins) { + if (plugin.enabled) { + updatePromise.push(updateAndStart(plugin)); + } else if (plugin.update) { + fetchAndStorePlugin(plugin.id); + } + } + + await allSettled(updatePromise); } return () => Object.keys(_pluginInstances).forEach(p => stopPlugin(p, false)); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index d996960..6b7931a 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -7,4 +7,3 @@ export { findInTree } from "@lib/utils/findInTree"; export * as logger from "@lib/utils/logger"; export { safeFetch } from "@lib/utils/safeFetch"; export * as types from "@lib/utils/types"; -export { without } from "@lib/utils/without"; diff --git a/src/lib/utils/without.ts b/src/lib/utils/without.ts deleted file mode 100644 index 1703a2c..0000000 --- a/src/lib/utils/without.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function without(object: O, ...keys: K): Omit { - const cloned = { ...object }; - keys.forEach(k => delete cloned[k]); - return cloned; -} diff --git a/tsconfig.json b/tsconfig.json index dee7a45..a573d67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,7 @@ "noEmit": true, "allowJs": true, "checkJs": true, - "jsx": "react", + "jsx": "react-jsx", "target": "esnext" } } \ No newline at end of file