diff --git a/scripts/build.mjs b/scripts/build.mjs index c4d344a..0fd5b07 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -77,7 +77,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"1.1.6-1"`, + version: `"1.1.6-2"`, }, }, }, diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts index e25ab5c..cad430b 100644 --- a/src/core/ui/settings/index.ts +++ b/src/core/ui/settings/index.ts @@ -20,13 +20,6 @@ export default function initSettings() { render: () => import("@core/ui/settings/pages/General"), useTrailing: () => `(${version})`, }, - { - key: "SHIGGYCORD_UPDATER", - title: () => "Updater", - icon: findAssetId("BellIcon"), - render: () => import("@core/ui/settings/pages/Updater"), - }, - { key: "CORE_PLUGINS", title: () => Strings.PLUGINS, @@ -56,7 +49,12 @@ export default function initSettings() { render: () => import("@core/ui/settings/pages/Fonts"), usePredicate: () => isFontSupported(), }, - + { + key: "SHIGGYCORD_UPDATER", + title: () => "Updater", + icon: findAssetId("BellIcon"), + render: () => import("@core/ui/settings/pages/Updater"), + }, { key: "BUNNY_DEVELOPER", title: () => Strings.DEVELOPER, diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 7634947..31a1e0a 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -26,7 +26,7 @@ import { findAssetId } from "@lib/api/assets"; import { settings } from "@lib/api/settings"; import { useObservable } from "@lib/api/storage"; import { showToast } from "@lib/ui/toasts"; -import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants"; +import { VD_PROXY_PREFIX } from "@lib/utils/constants"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByProps } from "@metro"; import { ComponentProps } from "react"; @@ -186,39 +186,6 @@ function InstallButton({ } }; - const continueInstallation = async () => { - if ( - !addon.installUrl.startsWith(VD_PROXY_PREFIX) && - !addon.installUrl.startsWith(BUNNY_PROXY_PREFIX) && - !settings.developerSettings - ) { - openAlert( - "bunny-plugin-unproxied-confirmation", - - {addon.installUrl} - - } - actions={ - - - - - } - />, - ); - } else { - performInstall(); - } - }; - const handleUninstall = () => { VdPluginManager.removePlugin(pluginId); setRefreshTick((prev: number) => prev + 1); @@ -628,12 +595,13 @@ export default function Plugins() { // Keep core plugins registration available if needed elsewhere const corePlugins = [...corePluginInstances.keys()] - .map((id) => registeredPlugins.get(id)!) + .map((id) => registeredPlugins.get(id)) + .filter((m): m is NonNullable => Boolean(m)) .map(unifyBunnyPlugin); // Merge lists: show Vendetta-managed plugins first (recent first), - // then Bunny-installed externals (recent first). - return [...vdPlugins, ...bnPlugins]; + // include core plugins, then Bunny-installed externals (recent first). + return [...vdPlugins, ...corePlugins, ...bnPlugins]; }} ListHeaderComponent={() => null} installAction={{ diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts index 174b7dc..76fab8e 100644 --- a/src/core/vendetta/plugins.ts +++ b/src/core/vendetta/plugins.ts @@ -8,7 +8,7 @@ import { import { Author } from "@lib/addons/types"; import { settings } from "@lib/api/settings"; import { safeFetch } from "@lib/utils"; -import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants"; +import { VD_PROXY_PREFIX } from "@lib/utils/constants"; import { logger, LoggerClass } from "@lib/utils/logger"; type EvaledPlugin = { @@ -192,24 +192,65 @@ export const VdPluginManager = { const allIds = Object.keys(plugins); if (!settings.safeMode?.enabled) { - // Loop over any plugin that is enabled, update it if allowed, then start it. - await Promise.allSettled( - allIds - .filter((pl) => plugins[pl].enabled) - .map( - async (pl) => ( - plugins[pl].update && - (await this.fetchPlugin(pl).catch((e: Error) => - logger.error(e.message), - )), - await this.startPlugin(pl) - ), - ), - ); - // Wait for the above to finish, then update all disabled plugins that are allowed to. - allIds - .filter((pl) => !plugins[pl].enabled && plugins[pl].update) - .forEach((pl) => this.fetchPlugin(pl)); + // Staggered startup: start enabled plugins in small batches to avoid + // CPU / I/O spikes during app launch. Network fetches (which may be slow) + // are deferred to background so the UI can become interactive faster. + const enabledIds = allIds.filter((pl) => plugins[pl].enabled); + const batchSize = 2; + const staggerInterval = 500; + + if (enabledIds.length > 0) { + const sleep = (ms: number) => + new Promise((res) => setTimeout(res, ms)); + + for (let i = 0; i < enabledIds.length; i += batchSize) { + const batch = enabledIds.slice(i, i + batchSize); + + // Start the batch in parallel but avoid letting a single failing plugin + // abort the whole process. + await Promise.allSettled( + batch.map(async (id) => { + try { + if (!plugins[id]) return; + await this.startPlugin(id).catch((e) => + logger.error(`Vendetta plugin ${id} failed to start:`, e), + ); + } catch (err) { + logger.error( + `Unexpected error while starting vendetta plugin ${id}:`, + err, + ); + } + }), + ); + + if (i + batchSize < enabledIds.length) { + await sleep(staggerInterval); + } + } + } + + // Defer network fetches to the background so startup isn't delayed. + // We fetch updates for plugins (both enabled and disabled) but do this + // asynchronously and with a small delay between requests to reduce contention. + (async () => { + const toFetch = allIds.filter((pl) => plugins[pl]?.update); + for (const pl of toFetch) { + try { + // Fire fetch; fetchPlugin may update plugin JS/manifest. Errors are logged. + await this.fetchPlugin(pl).catch((e: Error) => { + logger.error(e.message); + }); + } catch (e) { + logger.error( + `Failed background fetch for vendetta plugin ${pl}:`, + e, + ); + } + // Small pause between network requests to avoid spikes + await new Promise((res) => setTimeout(res, 200)); + } + })(); } return () => this.stopAllPlugins(); diff --git a/src/index.ts b/src/index.ts index 402ff52..b51122d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -119,17 +119,6 @@ export default async () => { setTimeout(runDeferred, 200); } - // Schedule a delayed plugin repository refresh (5 minutes) if the earlier deferred schedule didn't run. - // This ensures we refresh plugin metadata after the app is fully launched but avoid startup contention. - setTimeout( - () => { - updatePlugins().catch((e) => - logger.log("Scheduled updatePlugins (5min) failed:", e), - ); - }, - 5 * 60 * 1000, - ); - // Periodic bundle check: every 3 hours, check GitHub releases (ignore prereleases). // We use the GitHub Releases API to find the latest non-prerelease release and compare its tag. const checkBundle = async () => { diff --git a/src/lib/addons/plugins/index.ts b/src/lib/addons/plugins/index.ts index 917ae45..a407059 100644 --- a/src/lib/addons/plugins/index.ts +++ b/src/lib/addons/plugins/index.ts @@ -33,6 +33,53 @@ export const apiObjects = new Map< ReturnType >(); +/** + * Register core plugins into the runtime registries so they appear in the UI + * as soon as this module is imported. This function is intentionally synchronous + * and idempotent so it is safe to call at module-load time or from initialization + * code. + */ +export function registerCorePlugins() { + try { + const core = getCorePlugins(); + for (const id in core) { + try { + const { default: instance, preenabled } = core[id]; + + // Ensure manifest is registered for UI listing + registeredPlugins.set(id, instance.manifest); + + // Keep a reference to the core plugin instance so isCorePlugin checks work + corePluginInstances.set(id, instance); + + // Ensure pluginSettings has an entry for the core plugin so the UI can + // reflect enabled/disabled state. Do not overwrite existing user settings. + try { + // `pluginSettings` is a storage wrapper; ensure we only set a default if absent. + if (pluginSettings[id] == null) { + pluginSettings[id] = { enabled: preenabled ?? true }; + } + } catch (e) { + // Best-effort: if storage isn't available yet, ignore and continue. + console.error( + "Failed to set default pluginSettings for core plugin", + id, + e, + ); + } + } catch (e) { + console.error("Failed to register core plugin", id, e); + } + } + } catch (e) { + console.error("registerCorePlugins: unexpected error", e); + } +} + +// Eager core plugin registration removed to avoid circular require / timing issues. +// Call `registerCorePlugins()` explicitly from initialization code when appropriate, +// for example during a safe deferred startup step. + export const pluginRepositories = createStorage( "plugins/repositories.json", ); @@ -628,6 +675,16 @@ export async function initPlugins( ) { const { staggerInterval = 100, batchSize = 3 } = options; + // Ensure core plugins are registered prior to any plugin startup logic so + // settings pages and UI that read registered/core plugins show them. + // registerCorePlugins is synchronous and idempotent, but call it here as a + // safety-net in case module-load registration didn't run earlier. + try { + registerCorePlugins(); + } catch { + // ignore + } + await awaitStorage(pluginRepositories, pluginSettings); // Collect enabled plugin ids diff --git a/src/lib/utils/constants.ts b/src/lib/utils/constants.ts index d74c404..d1587b5 100644 --- a/src/lib/utils/constants.ts +++ b/src/lib/utils/constants.ts @@ -5,10 +5,7 @@ export const HTTP_REGEX = export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&\/=]*)/g; -export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy"; - export const NEXPID_PLUGINS_REPO_URL = "https://revenge.nexpid.xyz"; -//export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json"; export const OFFICIAL_PLUGINS_REPO_URL = "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json"; diff --git a/src/metro/internals/modules.ts b/src/metro/internals/modules.ts index 964b116..a02fddb 100644 --- a/src/metro/internals/modules.ts +++ b/src/metro/internals/modules.ts @@ -1,4 +1,8 @@ -import { getMetroCache, indexBlacklistFlag, indexExportsFlags } from "@metro/internals/caches"; +import { + getMetroCache, + indexBlacklistFlag, + indexExportsFlags, +} from "@metro/internals/caches"; import { Metro } from "@metro/types"; import { ModuleFlags, ModulesMapInternal } from "./enums"; @@ -20,88 +24,103 @@ let patchedNativeComponentRegistry = false; let _importingModuleId: number = -1; for (const key in metroModules) { - const id = Number(key); - const metroModule = metroModules[id]; + const id = Number(key); + const metroModule = metroModules[id]; - const cache = getMetroCache().flagsIndex[id]; - if (cache & ModuleFlags.BLACKLISTED) { + const cache = getMetroCache().flagsIndex[id]; + if (cache & ModuleFlags.BLACKLISTED) { + blacklistModule(id); + continue; + } + + if (metroModule!.factory) { + instead("factory", metroModule, (( + args: Parameters, + origFunc: Metro.FactoryFn, + ) => { + const originalImportingId = _importingModuleId; + _importingModuleId = id; + + const { 1: metroRequire, 4: moduleObject } = args; + + args[2 /* metroImportDefault */] = (id) => { + const exps = metroRequire(id); + return exps && exps.__esModule ? exps.default : exps; + }; + + args[3 /* metroImportAll */] = (id) => { + const exps = metroRequire(id); + if (exps && exps.__esModule) return exps; + + const importAll: Record = {}; + if (exps) Object.assign(importAll, exps); + importAll.default = exps; + return importAll; + }; + + origFunc(...args); + if (!isBadExports(moduleObject.exports)) { + onModuleRequire(moduleObject.exports, id); + } else { blacklistModule(id); - continue; - } + } - if (metroModule!.factory) { - instead("factory", metroModule, ((args: Parameters, origFunc: Metro.FactoryFn) => { - const originalImportingId = _importingModuleId; - _importingModuleId = id; - - const { 1: metroRequire, 4: moduleObject } = args; - - args[2 /* metroImportDefault */] = id => { - const exps = metroRequire(id); - return exps && exps.__esModule ? exps.default : exps; - }; - - args[3 /* metroImportAll */] = id => { - const exps = metroRequire(id); - if (exps && exps.__esModule) return exps; - - const importAll: Record = {}; - if (exps) Object.assign(importAll, exps); - importAll.default = exps; - return importAll; - }; - - origFunc(...args); - if (!isBadExports(moduleObject.exports)) { - onModuleRequire(moduleObject.exports, id); - } else { - blacklistModule(id); - } - - _importingModuleId = originalImportingId; - }) as any); // If only spitroast had better types - } + _importingModuleId = originalImportingId; + }) as any); // If only spitroast had better types + } } /** Makes the module associated with the specified ID non-enumberable. */ function blacklistModule(id: number) { - Object.defineProperty(metroModules, id, { enumerable: false }); - blacklistedIds.add(id); - indexBlacklistFlag(Number(id)); + Object.defineProperty(metroModules, id, { enumerable: false }); + blacklistedIds.add(id); + indexBlacklistFlag(Number(id)); } function isBadExports(exports: any) { - return !exports - || exports === window - || exports[""] === null - || (exports.__proto__ === Object.prototype && Reflect.ownKeys(exports).length === 0) - || (exports.default?.[Symbol.toStringTag] === "IntlMessagesProxy"); + return ( + !exports || + exports === window || + exports[""] === + null || + (exports.__proto__ === Object.prototype && + Reflect.ownKeys(exports).length === 0) || + exports.default?.[Symbol.toStringTag] === "IntlMessagesProxy" + ); } function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { - indexExportsFlags(id, moduleExports); + indexExportsFlags(id, moduleExports); - // Temporary - moduleExports.initSentry &&= () => undefined; - if (moduleExports.default?.track && moduleExports.default.trackMaker) - moduleExports.default.track = () => Promise.resolve(); + // Temporary + moduleExports.initSentry &&= () => undefined; + if (moduleExports.default?.track && moduleExports.default.trackMaker) + moduleExports.default.track = () => Promise.resolve(); - if (moduleExports.registerAsset) { - require("@lib/api/assets/patches").patchAssets(moduleExports); - } + if (moduleExports.registerAsset) { + require("@lib/api/assets/patches").patchAssets(moduleExports); + } - // There are modules registering the same native component - if (!patchedNativeComponentRegistry && ["customBubblingEventTypes", "customDirectEventTypes", "register", "get"].every(x => moduleExports[x])) { - instead("register", moduleExports, ([name, cb]: any, origFunc: any) => { - try { - return origFunc(name, cb); - } catch { - return name; - } - }); + // There are modules registering the same native component + if ( + !patchedNativeComponentRegistry && + [ + "customBubblingEventTypes", + "customDirectEventTypes", + "register", + "get", + ].every((x) => moduleExports[x]) + ) { + instead("register", moduleExports, ([name, cb]: any, origFunc: any) => { + try { + return origFunc(name, cb); + } catch { + return name; + } + }); - patchedNativeComponentRegistry = true; - } + patchedNativeComponentRegistry = true; + } // Hook DeveloperExperimentStore @@ -141,100 +160,133 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { if (!metroModules[id + inc]?.isInitialized) { blacklistModule(id + inc); } - } - // Hindi timestamps moment - if (moduleExports.isMoment) { - instead("defineLocale", moduleExports, (args: [string], orig: (lcl: string) => string) => { - const origLocale = moduleExports.locale(); - orig(...args); - moduleExports.locale(origLocale); - }); - } + return Reflect.get(target, property, receiver); + }, + }); + } - const subs = moduleSubscriptions.get(Number(id)); - if (subs) { - subs.forEach(s => s()); - moduleSubscriptions.delete(Number(id)); + if (!patchedImportTracker && moduleExports.fileFinishedImporting) { + before("fileFinishedImporting", moduleExports, ([filePath]: [string]) => { + if (_importingModuleId === -1 || !filePath) return; + metroModules[_importingModuleId]!.__filePath = filePath; + }); + patchedImportTracker = true; + } + + // Funny infinity recursion caused by a race condition + if (!patchedInspectSource && window["__core-js_shared__"]) { + const inspect = (f: unknown) => + typeof f === "function" && functionToString.apply(f, []); + window["__core-js_shared__"].inspectSource = inspect; + patchedInspectSource = true; + } + + // + if (moduleExports.findHostInstance_DEPRECATED) { + const prevExports = metroModules[id - 1]?.publicModule.exports; + const inc = prevExports.default?.reactProfilingEnabled ? 1 : -1; + if (!metroModules[id + inc]?.isInitialized) { + blacklistModule(id + inc); } + } + + // Hindi timestamps moment + if (moduleExports.isMoment) { + instead( + "defineLocale", + moduleExports, + (args: [string], orig: (lcl: string) => string) => { + const origLocale = moduleExports.locale(); + orig(...args); + moduleExports.locale(origLocale); + }, + ); + } + + const subs = moduleSubscriptions.get(Number(id)); + if (subs) { + subs.forEach((s) => s()); + moduleSubscriptions.delete(Number(id)); + } } export function getImportingModuleId() { - return _importingModuleId; + return _importingModuleId; } export function subscribeModule(id: number, cb: () => void): () => void { - const subs = moduleSubscriptions.get(id) ?? new Set(); + const subs = moduleSubscriptions.get(id) ?? new Set(); - subs.add(cb); - moduleSubscriptions.set(id, subs); + subs.add(cb); + moduleSubscriptions.set(id, subs); - return () => subs.delete(cb); + return () => subs.delete(cb); } export function requireModule(id: Metro.ModuleID) { - if (!metroModules[0]?.isInitialized) metroRequire(0); - if (blacklistedIds.has(id)) return undefined; + if (!metroModules[0]?.isInitialized) metroRequire(0); + if (blacklistedIds.has(id)) return undefined; - if (Number(id) === -1) return require("@metro/polyfills/redesign"); + if (Number(id) === -1) return require("@metro/polyfills/redesign"); - if (metroModules[id]?.isInitialized && !metroModules[id]?.hasError) { - return metroRequire(id); - } + if (metroModules[id]?.isInitialized && !metroModules[id]?.hasError) { + return metroRequire(id); + } - // Disable Internal RN error reporting logic - const originalHandler = ErrorUtils.getGlobalHandler(); - ErrorUtils.setGlobalHandler(noopHandler); + // Disable Internal RN error reporting logic + const originalHandler = ErrorUtils.getGlobalHandler(); + ErrorUtils.setGlobalHandler(noopHandler); - let moduleExports; - try { - moduleExports = metroRequire(id); - } catch { - blacklistModule(id); - moduleExports = undefined; - } + let moduleExports; + try { + moduleExports = metroRequire(id); + } catch { + blacklistModule(id); + moduleExports = undefined; + } - // Done initializing! Now, revert our hacks - ErrorUtils.setGlobalHandler(originalHandler); + // Done initializing! Now, revert our hacks + ErrorUtils.setGlobalHandler(originalHandler); - return moduleExports; + return moduleExports; } export function* getModules(uniq: string, all = false) { - yield [-1, require("@metro/polyfills/redesign")]; + yield [-1, require("@metro/polyfills/redesign")]; - let cache = getMetroCache().findIndex[uniq]; - if (all && !cache?.[`_${ModulesMapInternal.FULL_LOOKUP}`]) cache = undefined; - if (cache?.[`_${ModulesMapInternal.NOT_FOUND}`]) return; + let cache = getMetroCache().findIndex[uniq]; + if (all && !cache?.[`_${ModulesMapInternal.FULL_LOOKUP}`]) cache = undefined; + if (cache?.[`_${ModulesMapInternal.NOT_FOUND}`]) return; - for (const id in cache) { - if (id[0] === "_") continue; - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + for (const id in cache) { + if (id[0] === "_") continue; + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; + } - for (const id in metroModules) { - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + for (const id in metroModules) { + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; + } } export function* getCachedPolyfillModules(name: string) { - const cache = getMetroCache().polyfillIndex[name]!; + const cache = getMetroCache().polyfillIndex[name]!; - for (const id in cache) { - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + for (const id in cache) { + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; + } - if (!cache[`_${ModulesMapInternal.FULL_LOOKUP}`]) { - for (const id in metroModules) { - const exports = requireModule(Number(id)); - if (isBadExports(exports)) continue; - yield [id, exports]; - } + if (!cache[`_${ModulesMapInternal.FULL_LOOKUP}`]) { + for (const id in metroModules) { + const exports = requireModule(Number(id)); + if (isBadExports(exports)) continue; + yield [id, exports]; } + } }