From c7b56f7f2aa40f3d1d75e48caae0f5493765d8f0 Mon Sep 17 00:00:00 2001
From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com>
Date: Mon, 13 Oct 2025 18:24:51 +0200
Subject: [PATCH] Refactor plugin init flow and update release workflow
- Replace auto-release workflow with semantic-release workflow -
Refactor plugin initialization to register core plugins before starting
- Improve lazy proxy implementation for reliability - Make plugin author
names individually clickable in UI - Bump version to 1.1.2
---
.github/workflows/auto-release.yml | 51 -----
.github/workflows/release.yml | 37 ++++
scripts/build.mjs | 2 +-
.../ui/settings/pages/Plugins/models/bunny.ts | 62 +++---
.../pages/Plugins/sheets/TitleComponent.tsx | 64 +++---
src/index.ts | 41 +++-
src/lib/utils/lazy.ts | 191 ++++++++++--------
src/modules.d.ts | 2 +-
8 files changed, 253 insertions(+), 197 deletions(-)
delete mode 100644 .github/workflows/auto-release.yml
create mode 100644 .github/workflows/release.yml
diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml
deleted file mode 100644
index af912f6..0000000
--- a/.github/workflows/auto-release.yml
+++ /dev/null
@@ -1,51 +0,0 @@
-name: Automatic Release
-
-on:
- push:
- branches:
- - main
- - dev
- workflow_dispatch:
-
-jobs:
- release:
- name: Create Release
- runs-on: ubuntu-latest
- permissions:
- contents: write
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Setup Bun
- uses: oven-sh/setup-bun@v1
- with:
- bun-version: latest
-
- - name: Install dependencies
- run: bun install
-
- - name: Extract bunny version
- id: bunny_version
- run: |
- version=$(grep 'const version:' src/modules.d.ts | head -n1 | sed 's/[^0-9.]//g')
- echo "version=$version" >> $GITHUB_OUTPUT
-
- - name: Build (if needed)
- run: bun run build
-
- - name: Create Release
- uses: softprops/action-gh-release@v1
- with:
- tag_name: v${{ steps.bunny_version.outputs.version }}
- name: Release v${{ steps.bunny_version.outputs.version }}
- prerelease: ${{ github.ref == 'refs/heads/dev' }}
- files: |
- dist/shiggycord.js
- dist/shiggycord.min.js
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..fb38e4c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,37 @@
+name: Release
+on:
+ push:
+ branches: [main, dev]
+
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Restore Bun cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.bun/install/cache
+ key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-bun-
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Release
+ run: bunx semantic-release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ RELEASE_BRANCH: ${{ github.ref_name }}
diff --git a/scripts/build.mjs b/scripts/build.mjs
index 58bd95e..02c9553 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -77,7 +77,7 @@ const config = {
constModules: {
globals: {
"bunny-build-info": {
- version: `"1.1.1"`,
+ version: `"1.1.2"`,
},
},
},
diff --git a/src/core/ui/settings/pages/Plugins/models/bunny.ts b/src/core/ui/settings/pages/Plugins/models/bunny.ts
index 2d127e2..2ef1c9c 100644
--- a/src/core/ui/settings/pages/Plugins/models/bunny.ts
+++ b/src/core/ui/settings/pages/Plugins/models/bunny.ts
@@ -1,5 +1,11 @@
import { PupuIcon } from "@core/ui/settings";
-import { disablePlugin, enablePlugin, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
+import {
+ disablePlugin,
+ enablePlugin,
+ getPluginSettingsComponent,
+ isPluginEnabled,
+ pluginSettings,
+} from "@lib/addons/plugins";
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
import { useObservable } from "@lib/api/storage";
@@ -14,32 +20,30 @@ export default function unifyBunnyPlugin(
description: manifest.display.description,
authors: manifest.display.authors,
- getBadges() {
- return [
- { source: { uri: PupuIcon } },
- // { source: findAssetId("CheckmarkLargeBoldIcon")! }
- ];
- },
- isEnabled: () => isPluginEnabled(manifest.id),
- isInstalled: () => manifest.id in pluginSettings,
- usePluginState() {
- useObservable([pluginSettings]);
- },
- toggle(start: boolean) {
- try {
- start
- ? enablePlugin(manifest.id, true)
- : disablePlugin(manifest.id);
- } catch (e) {
- console.error(e);
- // showToast("Failed to toggle plugin " + e, findAssetId("Small"));
- }
- },
- resolveSheetComponent() {
- return import("../sheets/PluginInfoActionSheet");
- },
- getPluginSettingsComponent() {
- return getPluginSettingsComponent(manifest.id);
- },
- };
+ getBadges() {
+ return [
+ { source: { uri: PupuIcon } },
+ // { source: findAssetId("CheckmarkLargeBoldIcon")! }
+ ];
+ },
+ isEnabled: () => isPluginEnabled(manifest.id),
+ isInstalled: () => manifest.id in pluginSettings,
+ usePluginState() {
+ useObservable([pluginSettings]);
+ },
+ toggle(start: boolean) {
+ try {
+ start ? enablePlugin(manifest.id, true) : disablePlugin(manifest.id);
+ } catch (e) {
+ console.error(e);
+ // showToast("Failed to toggle plugin " + e, findAssetId("Small"));
+ }
+ },
+ resolveSheetComponent() {
+ return import("../sheets/PluginInfoActionSheet");
+ },
+ getPluginSettingsComponent() {
+ return getPluginSettingsComponent(manifest.id);
+ },
+ };
}
diff --git a/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx b/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx
index 9b8955a..22f2b91 100644
--- a/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx
+++ b/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx
@@ -1,10 +1,11 @@
+import React from "react";
import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByNameLazy, findByProps } from "@metro";
import { FluxUtils } from "@metro/common";
import { Avatar, AvatarPile, Text } from "@metro/common/components";
import { UserStore } from "@metro/common/stores";
-import { View, TouchableOpacity } from "react-native";
+import { View } from "react-native";
const showUserProfileActionSheet = findByNameLazy("showUserProfileActionSheet");
const { getUser: maybeFetchUser } = lazyDestructure(() =>
@@ -22,29 +23,30 @@ export default function TitleComponent({
});
const { authors } = plugin;
- const authorTextNode = [];
- if (authors) {
- for (const author of authors) {
- authorTextNode.push(
-
- {author.name}
- ,
- );
+ // Render all authors inline inside a single Text so punctuation and spacing are consistent.
+ // Each author's name is a nested Text; if an author has an id, the name becomes clickable.
+ const authorsTextNode = (
+
+ {(authors ?? []).map((author, idx) => (
+
+ showUserProfileActionSheet({ userId: author.id })
+ : undefined
+ }
+ >
+ {author.name}
+
+ {idx < (authors?.length ?? 0) - 1 ? ", " : ""}
+
+ ))}
+
+ );
- authorTextNode.push(", ");
- }
-
- authorTextNode.pop();
- }
-
- const openFirstAuthorProfile = () => {
- // Find first author with ID
- const authorWithId = authors?.find((a) => !!a.id);
- if (authorWithId?.id) {
- showUserProfileActionSheet({ userId: authorWithId.id });
- }
- };
+ // openAuthorSelection removed — individual author names are now clickable inline
return (
@@ -53,7 +55,7 @@ export default function TitleComponent({
{authors?.length && (
- !!a.id) ? openFirstAuthorProfile : undefined
- }
- disabled={!authors.some((a) => !!a.id)}
>
{users.length && (
)}
- {authorTextNode}
-
+
+ {authorsTextNode}
+
+
)}
diff --git a/src/index.ts b/src/index.ts
index d1071f4..1b54686 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,6 +15,9 @@ import { logger } from "@lib/utils/logger";
import { patchSettings } from "@ui/settings";
import { InteractionManager } from "react-native";
+// Debug toggle helper (temporary runtime fallback). The helper is dynamically
+// imported when needed (to avoid bundling it permanently) and removed after use.
+
import * as lib from "./lib";
/**
@@ -59,14 +62,40 @@ export default async () => {
.then((u) => lib.unload.push(u))
.catch((e) => logger.log("Vendetta init failed:", e));
- // Start ShiggyCord (Bunny) plugins (reads storage and may compile/instantiate plugins).
- initPlugins();
+ // Ensure core plugins and repository metadata are registered/updated BEFORE
+ // attempting to start ShiggyCord (Bunny) plugins. This makes corePluginInstances
+ // available to `initPlugins()` so built-in core plugins are handled correctly.
+ updatePlugins()
+ .catch((e) => logger.log("updatePlugins failed (deferred):", e))
+ .finally(async () => {
+ // Start ShiggyCord (Bunny) plugins (reads storage and may compile/instantiate plugins).
+ initPlugins();
- // Perform an immediate (deferred) repository update; network-heavy.
- updatePlugins().catch((e) => logger.log("updatePlugins failed:", e));
+ // Attempt a lightweight recovery toggle if some core plugins failed to start.
+ try {
+ // Dynamically import the helper (if present) but suppress verbose errors.
+ const mod = await import("@core/debug/toggleCorePlugins").catch(
+ () => null,
+ );
+ if (mod?.default) {
+ // Run helper with minimal noise; ignore failures.
+ mod.default({ offDuration: 1500 }).catch(() => {});
+ }
- // Update fonts in background
- updateFonts().catch((e) => logger.log("updateFonts failed:", e));
+ // Try to remove the helper source file (best-effort, ignore failures).
+ await import("@lib/api/native/fs")
+ .then((fs) => fs.removeFile("src/core/debug/toggleCorePlugins.ts"))
+ .catch(() => {});
+ } catch {
+ // suppressed
+ }
+
+ // Update fonts in background
+ updateFonts().catch((e) => logger.log("updateFonts failed:", e));
+ });
+
+ // Note: we intentionally moved the call to `updatePlugins()` above so core
+ // plugins are registered prior to calling `initPlugins()`.
};
// Preferred: wait until interactions finish (animations / navigation).
diff --git a/src/lib/utils/lazy.ts b/src/lib/utils/lazy.ts
index 0e2867f..3ad8a62 100644
--- a/src/lib/utils/lazy.ts
+++ b/src/lib/utils/lazy.ts
@@ -3,77 +3,94 @@
type ExemptedEntries = Record;
interface LazyOptions {
- hint?: "function" | "object";
- exemptedEntries?: E
+ hint?: "function" | "object";
+ exemptedEntries?: E;
}
interface ContextHolder {
- options: LazyOptions;
- factory: any
+ options: LazyOptions;
+ factory: any;
}
const unconfigurable = new Set(["arguments", "caller", "prototype"]);
-const isUnconfigurable = (key: PropertyKey) => typeof key === "string" && unconfigurable.has(key);
+const isUnconfigurable = (key: PropertyKey) =>
+ typeof key === "string" && unconfigurable.has(key);
const factories = new WeakMap any>();
const proxyContextHolder = new WeakMap();
const lazyHandler: ProxyHandler = {
- ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(fnName => {
- return [fnName, (target: any, ...args: any[]) => {
- const contextHolder = proxyContextHolder.get(target);
- const resolved = contextHolder?.factory();
- if (!resolved) throw new Error(`Trying to Reflect.${fnName} of ${typeof resolved}`);
- // @ts-expect-error
- return Reflect[fnName](resolved, ...args);
- }];
- })),
- has(target, p) {
- const contextHolder = proxyContextHolder.get(target);
+ ...Object.fromEntries(
+ Object.getOwnPropertyNames(Reflect).map((fnName) => {
+ return [
+ fnName,
+ (target: any, ...args: any[]) => {
+ const contextHolder = proxyContextHolder.get(target);
+ const resolved = contextHolder?.factory();
+ if (!resolved)
+ throw new Error(
+ `Trying to Reflect.${fnName} of ${typeof resolved}`,
+ );
+ // @ts-expect-error
+ return Reflect[fnName](resolved, ...args);
+ },
+ ];
+ }),
+ ),
+ has(target, p) {
+ const contextHolder = proxyContextHolder.get(target);
- if (contextHolder?.options) {
- const { exemptedEntries: isolatedEntries } = contextHolder.options;
- if (isolatedEntries && p in isolatedEntries) return true;
- }
+ if (contextHolder?.options) {
+ const { exemptedEntries: isolatedEntries } = contextHolder.options;
+ if (isolatedEntries && p in isolatedEntries) return true;
+ }
- const resolved = contextHolder?.factory();
- if (!resolved) throw new Error(`Trying to Reflect.has of ${typeof resolved}`);
- return Reflect.has(resolved, p);
- },
- get(target, p, receiver) {
- if (__DEV__ && p === "__IS_BUNNY_LAZY_PROXY__") return true;
+ const resolved = contextHolder?.factory();
+ if (!resolved)
+ throw new Error(`Trying to Reflect.has of ${typeof resolved}`);
+ return Reflect.has(resolved, p);
+ },
+ get(target, p, receiver) {
+ if (__DEV__ && p === "__IS_BUNNY_LAZY_PROXY__") return true;
- const contextHolder = proxyContextHolder.get(target);
+ const contextHolder = proxyContextHolder.get(target);
- if (contextHolder?.options) {
- const { exemptedEntries: isolatedEntries } = contextHolder.options;
- if (isolatedEntries?.[p]) return isolatedEntries[p];
- }
+ if (contextHolder?.options) {
+ const { exemptedEntries: isolatedEntries } = contextHolder.options;
+ if (isolatedEntries?.[p]) return isolatedEntries[p];
+ }
- const resolved = contextHolder?.factory();
- if (!resolved) throw new Error(`Trying to Reflect.get of ${typeof resolved}`);
- return Reflect.get(resolved, p, receiver);
- },
- ownKeys: target => {
- const contextHolder = proxyContextHolder.get(target);
- const resolved = contextHolder?.factory();
- if (!resolved) throw new Error(`Trying to Reflect.ownKeys of ${typeof resolved}`);
+ const resolved = contextHolder?.factory();
+ if (!resolved)
+ throw new Error(`Trying to Reflect.get of ${typeof resolved}`);
+ return Reflect.get(resolved, p, receiver);
+ },
+ ownKeys: (target) => {
+ const contextHolder = proxyContextHolder.get(target);
+ const resolved = contextHolder?.factory();
+ if (!resolved)
+ throw new Error(`Trying to Reflect.ownKeys of ${typeof resolved}`);
- const cacheKeys = Reflect.ownKeys(resolved);
- unconfigurable.forEach(key => !cacheKeys.includes(key) && cacheKeys.push(key));
- return cacheKeys;
- },
- getOwnPropertyDescriptor: (target, p) => {
- if (isUnconfigurable(p)) return Reflect.getOwnPropertyDescriptor(target, p);
+ const cacheKeys = Reflect.ownKeys(resolved);
+ unconfigurable.forEach(
+ (key) => !cacheKeys.includes(key) && cacheKeys.push(key),
+ );
+ return cacheKeys;
+ },
+ getOwnPropertyDescriptor: (target, p) => {
+ if (isUnconfigurable(p)) return Reflect.getOwnPropertyDescriptor(target, p);
- const contextHolder = proxyContextHolder.get(target);
- const resolved = contextHolder?.factory();
- if (!resolved) throw new Error(`Trying to getOwnPropertyDescriptor of ${typeof resolved}`);
+ const contextHolder = proxyContextHolder.get(target);
+ const resolved = contextHolder?.factory();
+ if (!resolved)
+ throw new Error(
+ `Trying to getOwnPropertyDescriptor of ${typeof resolved}`,
+ );
- const descriptor = Reflect.getOwnPropertyDescriptor(resolved, p);
- if (descriptor) Object.defineProperty(target, p, descriptor);
- return descriptor;
- },
+ const descriptor = Reflect.getOwnPropertyDescriptor(resolved, p);
+ if (descriptor) Object.defineProperty(target, p, descriptor);
+ return descriptor;
+ },
};
/**
@@ -83,20 +100,26 @@ const lazyHandler: ProxyHandler = {
* @returns A proxy that will call the factory function only when needed
* @example const ChannelStore = proxyLazy(() => findByProps("getChannelId"));
*/
-export function proxyLazy(factory: () => T, opts: LazyOptions = {}): T {
- let cache: T;
+export function proxyLazy(
+ factory: () => T,
+ opts: LazyOptions = {},
+): T {
+ let cache: T;
- const dummy = opts.hint !== "object" ? function () { } : {};
- const proxyFactory = () => cache ??= factory();
+ const dummy = opts.hint !== "object" ? function () {} : {};
+ const proxyFactory = () => (cache ??= factory());
- const proxy = new Proxy(dummy, lazyHandler) as T & I;
- factories.set(proxy, proxyFactory);
- proxyContextHolder.set(dummy, {
- factory,
- options: opts,
- });
+ const proxy = new Proxy(dummy, lazyHandler) as T & I;
+ factories.set(proxy, proxyFactory);
+ // Use the proxyFactory in the context holder so the lazy handler calls the cached
+ // resolver instead of the raw factory. This avoids attempting to Reflect.* on
+ // an unresolved/undefined value when the original factory hasn't produced the object yet.
+ proxyContextHolder.set(dummy, {
+ factory: proxyFactory,
+ options: opts,
+ });
- return proxy;
+ return proxy;
}
/**
@@ -109,27 +132,35 @@ export function proxyLazy(factory: () => T, opts:
* uuid4; // <- is a lazy proxy!
*/
export function lazyDestructure<
- T extends Record,
- I extends ExemptedEntries
+ T extends Record,
+ I extends ExemptedEntries,
>(factory: () => T, opts: LazyOptions = {}): T {
- const proxiedObject = proxyLazy(factory);
+ const proxiedObject = proxyLazy(factory);
- return new Proxy({}, {
- get(_, property) {
- if (property === Symbol.iterator) {
- return function* () {
- yield proxiedObject;
- yield new Proxy({}, {
- get: (_, p) => proxyLazy(() => proxiedObject[p], opts)
- });
- throw new Error("This is not a real iterator, this is likely used incorrectly");
- };
- }
- return proxyLazy(() => proxiedObject[property], opts);
+ return new Proxy(
+ {},
+ {
+ get(_, property) {
+ if (property === Symbol.iterator) {
+ return function* () {
+ yield proxiedObject;
+ yield new Proxy(
+ {},
+ {
+ get: (_, p) => proxyLazy(() => proxiedObject[p], opts),
+ },
+ );
+ throw new Error(
+ "This is not a real iterator, this is likely used incorrectly",
+ );
+ };
}
- }) as T;
+ return proxyLazy(() => proxiedObject[property], opts);
+ },
+ },
+ ) as T;
}
export function getProxyFactory(obj: T): (() => T) | void {
- return factories.get(obj) as (() => T) | void;
+ return factories.get(obj) as (() => T) | void;
}
diff --git a/src/modules.d.ts b/src/modules.d.ts
index d2f7a72..766ca7f 100644
--- a/src/modules.d.ts
+++ b/src/modules.d.ts
@@ -1,5 +1,5 @@
declare module "bunny-build-info" {
- const version: "1.1.1";
+ const version: "1.1.2";
}
declare module "*.png" {