diff --git a/plugins/custom-user-tags/src/patches/profile.tsx b/plugins/custom-user-tags/src/patches/profile.tsx
index 66fdfda..caa821e 100644
--- a/plugins/custom-user-tags/src/patches/profile.tsx
+++ b/plugins/custom-user-tags/src/patches/profile.tsx
@@ -1,74 +1,58 @@
-import { find, findByProps } from "@vendetta/metro";
-import { after } from "@vendetta/patcher";
import { findInReactTree } from "@vendetta/utils";
+import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept";
import resolveTag from "../lib/resolveTag";
-
-// Same top-level profile component PronounDB already confirmed works, and the same one Staff Tags
-// now patches for its own profile-screen tag - see that plugin's patches/profile.tsx for the full
-// writeup on why this is a separate component from UserRow (which details.tsx already handles).
-const UserProfile =
- find((m: any) => m?.type?.name === "UserProfileContent") ??
- find((m: any) => m?.type?.name === "UserProfile");
-
-const TagModule = findByProps("getBotLabel");
+import CustomTag from "../ui/Tag";
/**
- * Not independently confirmed against a live render yet - the exact prop shape passed to this
- * component isn't verified on-device, so this pulls from whichever of several plausible field names
- * is actually present. Every step is guarded, so a wrong assumption just means no tag appears here.
+ * Full profile screen tag - see Staff Tags' own `patches/profile.tsx` (same repo pattern, worked
+ * out together in the same live session, see /root/evals-for-rn) for the full writeup of why this
+ * needs `createElement`/`jsx` interception rather than patching `UserProfileContent` and searching
+ * its output: `UserProfilePrimaryInfo` (the component that actually renders the name+tag row) is
+ * only ever created as an implementation detail several layers deep, unreachable by a Metro module
+ * search, and receives `user` directly as its own prop - so `registerTypeDetector` catches the
+ * first render to recover the live reference, and `registerIntercept` swaps in a wrapper for every
+ * later use, no separate context-passing patch needed.
+ *
+ * Unlike Staff Tags, `resolveTag` only needs a `userId` - no guild/permission context - so this
+ * doesn't need `guildId` from props at all.
*/
export default function patchProfile(): () => void {
- if (!UserProfile) return () => {};
+ const cleanups: (() => void)[] = [];
+ patchCreateElement(cleanups);
- return after("type", UserProfile, (args: any[], res: any) => {
- try {
- const props = args?.[0] ?? {};
- const userId: string | undefined = props.userId ?? props.user?.id ?? props.displayProfile?.userId;
- if (!userId) return;
+ registerTypeDetector(
+ "custom-user-tags-profile-primary-info",
+ (type) => typeof type === "function" && type.name === "UserProfilePrimaryInfo",
+ (UserProfilePrimaryInfo: any) => {
+ const PatchedUserProfilePrimaryInfo = (props: any) => {
+ const ret = UserProfilePrimaryInfo(props);
- const tag = resolveTag(userId);
- if (!tag) return;
+ try {
+ const userId: string | undefined = props?.user?.id;
+ if (!userId) return ret;
- const primaryInfo = findInReactTree(res, (c) => c?.type?.name === "PrimaryInfo" || c?.type?.displayName === "PrimaryInfo");
- if (!primaryInfo) return;
+ const tag = resolveTag(userId);
+ if (!tag) return ret;
- const existingTag = findInReactTree(primaryInfo, (c) => c?.type?.Types);
- if (existingTag && existingTag.props?.type !== 0) return;
+ const row = findInReactTree(
+ ret,
+ (c: any) =>
+ Array.isArray(c?.props?.children) &&
+ c.props.children.some((ch: any) => ch?.type?.name === "UserTagAndPronouns")
+ );
+ if (!Array.isArray(row?.props?.children)) return ret;
- if (existingTag) {
- Object.assign(existingTag.props, {
- type: 0,
- text: tag.text,
- textColor: tag.textColor,
- backgroundColor: tag.backgroundColor,
- icon: tag.icon,
- iconColor: tag.iconColor,
- verified: false
- });
- return;
- }
+ row.props.children.push();
+ } catch {
+ // Skip - a broken profile tag beats a crashed profile screen.
+ }
- const nameRow = findInReactTree(
- primaryInfo,
- (c) =>
- Array.isArray(c?.props?.children) &&
- c.props.children.some((ch: any) => typeof ch === "string" || typeof ch?.props?.children === "string")
- );
- if (!Array.isArray(nameRow?.props?.children) || !TagModule) return;
+ return ret;
+ };
- nameRow.props.children.push(
-
- );
- } catch {
- // Skip - a broken profile tag beats a crashed profile screen.
+ registerIntercept(UserProfilePrimaryInfo, PatchedUserProfilePrimaryInfo);
}
- });
+ );
+
+ return () => cleanups.forEach((fn) => fn());
}
diff --git a/plugins/staff-tags/src/patches/profile.tsx b/plugins/staff-tags/src/patches/profile.tsx
index ed1915c..0d78362 100644
--- a/plugins/staff-tags/src/patches/profile.tsx
+++ b/plugins/staff-tags/src/patches/profile.tsx
@@ -1,86 +1,91 @@
-import { find, findByProps, findByStoreName } from "@vendetta/metro";
-import { after } from "@vendetta/patcher";
+import { findByStoreName } from "@vendetta/metro";
import { findInReactTree } from "@vendetta/utils";
+import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept";
import getTag from "../lib/getTag";
+import GradientTag from "../ui/GradientTag";
-// Same top-level profile component PronounDB already confirmed works on current Discord (the
-// findByName-based lookup this repo used to use for it stopped matching after a rename, but this
-// predicate-based find still succeeds). The full profile screen doesn't render through UserRow the
-// way member list rows and the old "profile popout" did - this is a separate component entirely,
-// which is why tags weren't showing up here even though details.tsx's own patch is working fine.
-const UserProfile =
- find((m: any) => m?.type?.name === "UserProfileContent") ??
- find((m: any) => m?.type?.name === "UserProfile");
-
-const TagModule = findByProps("getBotLabel");
const GuildStore = findByStoreName("GuildStore");
-const UserStore = findByStoreName("UserStore");
/**
- * Not independently confirmed against a live render the way most of this repo's fixes are - the
- * exact prop shape passed to this component's render (userId vs. a full user object, whether
- * guildId is even present) isn't verified on-device yet, so this pulls from whichever of several
- * plausible field names is actually present rather than assuming one. Every step is guarded, so a
- * wrong assumption just means no tag appears here, not a crash.
+ * Full profile screen tag, live-verified end to end (see /root/evals-for-rn for the session this
+ * was worked out in, and the Next repo's `staff-tags` `patches/details.tsx` for the exact same
+ * class of bug this replaces one of).
+ *
+ * The full profile doesn't render through `UserRow` (member list/profile popout) at all - it's a
+ * separate tree, `UserProfileContent` -> `PrimaryInfo` -> `UserProfilePrimaryInfo`, and neither
+ * `PrimaryInfo` nor `UserProfileContent` carry a rendered name+tag row worth patching:
+ * `PrimaryInfo` only ever appears as an *unrendered* ``
+ * element inside `UserProfileContent`'s own output (confirmed live it has no `children` at that
+ * point, so searching inside it for anything finds nothing, silently, no matter how good the
+ * search predicate is). `UserProfilePrimaryInfo` is the component that actually renders the name
+ * row and, conveniently, receives `user`/`guildId` directly as its own props - no need to also
+ * patch `UserProfileContent` just to recover context.
+ *
+ * `UserProfilePrimaryInfo` can't be found by a Metro module search the way `UserProfileContent`
+ * is above (confirmed live - it isn't a top-level export findByProps/findByTypeName can reach,
+ * only ever created as an implementation detail inside `PrimaryInfo`'s own render). Patching
+ * `React.createElement`/the `jsx` runtime (`@shared/lib/createElementIntercept`) sidesteps that:
+ * `registerTypeDetector` observes the *first* time anything, anywhere, creates an element with
+ * this name, handing back the live function reference the moment it exists - then
+ * `registerIntercept` swaps in our own wrapper for every future use of that exact reference,
+ * including the one that triggered the detector (both run synchronously inside the same
+ * `createElement`/`jsx` call, so even the very first render is already patched).
+ *
+ * Confirmed live, `UserProfilePrimaryInfo` renders:
+ * `[, [, , ]]`
+ * - `UserTagAndPronouns` is Discord's `@username` handle + pronouns display, not a badge slot
+ * (misleading name); `GuildTag` is Discord's own unrelated "server tag" feature; `ProfileBadgeRows`
+ * is Nitro/HypeSquad-style badges. None of these read custom text/color, so - same lesson as the
+ * member list's `BotTag` - the fix pushes the plugin's own `GradientTag` as a new sibling in that
+ * inner row rather than trying to repurpose any of them.
*/
export default function patchProfile(): () => void {
- if (!UserProfile) return () => {};
+ const cleanups: (() => void)[] = [];
+ patchCreateElement(cleanups);
- return after("type", UserProfile, (args: any[], res: any) => {
- try {
- const props = args?.[0] ?? {};
- const userId: string | undefined = props.userId ?? props.user?.id ?? props.displayProfile?.userId;
- if (!userId) return;
+ registerTypeDetector(
+ "staff-tags-profile-primary-info",
+ (type) => typeof type === "function" && type.name === "UserProfilePrimaryInfo",
+ (UserProfilePrimaryInfo: any) => {
+ const PatchedUserProfilePrimaryInfo = (props: any) => {
+ const ret = UserProfilePrimaryInfo(props);
- const user = props.user ?? UserStore?.getUser?.(userId);
- if (!user) return;
+ try {
+ const { user, guildId } = props ?? {};
+ if (!user) return ret;
- const guildId: string | undefined = props.guildId;
- const guild = guildId ? GuildStore?.getGuild(guildId) : undefined;
+ const guild = guildId ? GuildStore?.getGuild(guildId) : undefined;
+ const tag = getTag(guild, undefined, user);
+ if (!tag) return ret;
- const tag = getTag(guild, undefined, user);
- if (!tag) return;
+ const row = findInReactTree(
+ ret,
+ (c: any) =>
+ Array.isArray(c?.props?.children) &&
+ c.props.children.some((ch: any) => ch?.type?.name === "UserTagAndPronouns")
+ );
+ if (!Array.isArray(row?.props?.children)) return ret;
- const primaryInfo = findInReactTree(res, (c) => c?.type?.name === "PrimaryInfo" || c?.type?.displayName === "PrimaryInfo");
- if (!primaryInfo) return;
+ row.props.children.push(
+
+ );
+ } catch {
+ // Skip - a broken profile tag beats a crashed profile screen.
+ }
- const existingTag = findInReactTree(primaryInfo, (c) => c?.type?.Types);
- if (existingTag && existingTag.props?.type !== 0) return;
+ return ret;
+ };
- if (existingTag) {
- Object.assign(existingTag.props, {
- type: 0,
- text: tag.text,
- textColor: tag.textColor,
- backgroundColor: tag.backgroundColor,
- icon: tag.icon,
- iconColor: tag.iconColor,
- verified: false
- });
- return;
- }
-
- const nameRow = findInReactTree(
- primaryInfo,
- (c) =>
- Array.isArray(c?.props?.children) &&
- c.props.children.some((ch: any) => typeof ch === "string" || typeof ch?.props?.children === "string")
- );
- if (!Array.isArray(nameRow?.props?.children) || !TagModule) return;
-
- nameRow.props.children.push(
-
- );
- } catch {
- // Skip - a broken profile tag beats a crashed profile screen.
+ registerIntercept(UserProfilePrimaryInfo, PatchedUserProfilePrimaryInfo);
}
- });
+ );
+
+ return () => cleanups.forEach((fn) => fn());
}
diff --git a/shared/lib/createElementIntercept.ts b/shared/lib/createElementIntercept.ts
index 9f375b6..5f21f9e 100644
--- a/shared/lib/createElementIntercept.ts
+++ b/shared/lib/createElementIntercept.ts
@@ -1,6 +1,4 @@
import { React } from "@vendetta/metro/common";
-import { rawFind } from "@shared/lib/rawFind";
-import { waitFor } from "@shared/lib/waitFor";
interface Intercept {
replacement: React.ComponentType;
@@ -12,14 +10,24 @@ interface PropsIntercept {
replacement: React.ComponentType | null;
}
+interface PropsTransform {
+ predicate: (props: any) => boolean;
+ transform: (props: any) => any;
+}
+
interface TypeDetector {
+ key: string;
predicate: (type: any) => boolean;
onDetected: (type: any) => void;
+ persistent: boolean;
+ justFired?: boolean;
}
const intercepts = new Map, Intercept>();
const propsIntercepts: PropsIntercept[] = [];
+const propsTransforms: PropsTransform[] = [];
let typeDetectors: TypeDetector[] = [];
+const detectorKeys = new Set();
let isPatched = false;
export function registerIntercept(
@@ -42,26 +50,67 @@ export function registerPropsIntercept(predicate: (props: any) => boolean, repla
}
/**
- * Purely observational, one-shot: the first time an element is created whose `type` matches
- * `predicate`, calls `onDetected(type)` with the real, live type reference and then stops
- * watching - it never alters what actually renders.
+ * Rewrites an element's props in place while keeping the same type - unlike registerPropsIntercept,
+ * this doesn't swap in a different component, it lets the real one render with different input.
+ *
+ * Exists because monkey-patching a hook's exported property (`someModule.useSomeHook = wrapper`)
+ * only affects callers that go through that exact exports-object reference. Discord's own hooks are
+ * often called via an internal, same-chunk closure reference instead - the patched export sits there
+ * unused, and the real, unpatched hook result flows through untouched. A React.createElement/jsx call
+ * has no such ambiguity: it's always the one shared runtime function, called the same way regardless
+ * of which chunk the JSX originated in - so rewriting an element's props here reaches every consumer
+ * exactly as if the underlying data had actually been different.
+ */
+export function registerPropsTransform(predicate: (props: any) => boolean, transform: (props: any) => any) {
+ propsTransforms.push({ predicate, transform });
+}
+
+/**
+ * Purely observational: the first time an element is created whose `type` matches `predicate`,
+ * calls `onDetected(type)` with the real, live type reference - it never alters what actually
+ * renders (pair with registerIntercept/registerPropsIntercept in the callback for that).
*
* This exists for components that can't be found reliably by searching Metro's module registry
* after the fact (findByTypeName/rawFindByTypeName), because that races against whenever the
* component first mounts - if it mounts before the search finds it, and it never mounts again for
- * the rest of the session (true for some React.memo'd chrome components), the search losing that
- * race means the patch never gets a chance to apply at all, permanently. Intercepting element
- * creation instead sidesteps the race entirely: by the time ANYTHING calls createElement/jsx with
- * this component as `type`, that reference already exists and is already the real one about to be
- * used - there's no way to observe the call any earlier than this.
+ * the rest of the session (true for some React.memo'd chrome components, and for inner components
+ * only reachable by rendering an outer one first), the search losing that race means the patch never
+ * gets a chance to apply at all, permanently. Intercepting element creation instead sidesteps the
+ * race entirely: by the time ANYTHING calls createElement/jsx with this component as `type`, that
+ * reference already exists and is already the real one about to be used - there's no way to observe
+ * the call any earlier than this.
+ *
+ * `key` deduplicates registrations - important if the call site that registers a detector can run
+ * more than once (a retry loop, a patch that reapplies on reload), since duplicate detectors mean
+ * duplicate `onDetected` firings and a permanently growing list re-tested against every element
+ * created anywhere in the app.
+ *
+ * `persistent` (default false, one-shot): keeps watching and firing `onDetected` again for every
+ * later match too, instead of stopping after the first. Needed when the component's own type
+ * reference isn't a stable session-long singleton (confirmed live for some chrome components, whose
+ * reference changes on certain navigation paths) - a one-shot detector plus registerIntercept's
+ * identity-keyed map can't follow that: the intercept stays registered for the old, now-abandoned
+ * reference while the new one renders unmodified.
*/
-export function registerTypeDetector(predicate: (type: any) => boolean, onDetected: (type: any) => void) {
- typeDetectors.push({ predicate, onDetected });
+export function registerTypeDetector(
+ key: string,
+ predicate: (type: any) => boolean,
+ onDetected: (type: any) => void,
+ options?: { persistent?: boolean },
+) {
+ if (detectorKeys.has(key)) return;
+ detectorKeys.add(key);
+ typeDetectors.push({ key, predicate, onDetected, persistent: options?.persistent ?? false });
+}
+
+export function hasTypeDetector(key: string): boolean {
+ return detectorKeys.has(key);
}
function runTypeDetectors(type: any) {
if (typeDetectors.length === 0) return;
- const remaining: TypeDetector[] = [];
+
+ let consumed = false;
for (const detector of typeDetectors) {
let matched = false;
try {
@@ -69,28 +118,49 @@ function runTypeDetectors(type: any) {
} catch {
// A bad predicate shouldn't block every other detector or every element from rendering.
}
- if (matched) {
- try {
- detector.onDetected(type);
- } catch {
- // Same - a detector's own handler throwing shouldn't take anything else down.
- }
- } else {
- remaining.push(detector);
+ if (!matched) continue;
+
+ try {
+ detector.onDetected(type);
+ } catch {
+ // Same - a detector's own handler throwing shouldn't take anything else down.
+ }
+ if (!detector.persistent) {
+ detector.justFired = true;
+ consumed = true;
}
}
- typeDetectors = remaining;
+
+ // Only rebuild the list when a one-shot detector actually fired - rebuilding on every single
+ // element creation app-wide would be an allocation per element on the hottest path in the app
+ // whenever any persistent detector is registered (the list is then never empty).
+ if (consumed) {
+ typeDetectors = typeDetectors.filter((d) => d.persistent || !d.justFired);
+ }
}
/** Returns a replacement type if this element should be intercepted, `null` to render nothing, or `undefined` to pass through unchanged. */
-function resolveReplacement(type: any, props: any): { type: any; props: any } | null | undefined {
+function resolveReplacement(type: any, props: any, rest: any[]): { type: any; props: any } | null | undefined {
runTypeDetectors(type);
- if (props) {
+ let effectiveProps = props;
+ if (effectiveProps) {
+ for (const { predicate, transform } of propsTransforms) {
+ try {
+ if (predicate(effectiveProps, type, rest)) {
+ effectiveProps = transform(effectiveProps);
+ }
+ } catch {
+ // A bad transform shouldn't block every other element from rendering.
+ }
+ }
+ }
+
+ if (effectiveProps) {
for (const { predicate, replacement } of propsIntercepts) {
try {
- if (predicate(props)) {
- return replacement ? { type: replacement, props } : null;
+ if (predicate(effectiveProps, type, rest)) {
+ return replacement ? { type: replacement, props: effectiveProps } : null;
}
} catch {
// A bad predicate shouldn't block every other element from rendering.
@@ -100,12 +170,25 @@ function resolveReplacement(type: any, props: any): { type: any; props: any } |
const entry = intercepts.get(type);
if (entry) {
- return { type: entry.replacement, props: entry.extraProps ? { ...props, ...entry.extraProps } : props };
+ return { type: entry.replacement, props: entry.extraProps ? { ...effectiveProps, ...entry.extraProps } : effectiveProps };
+ }
+
+ if (effectiveProps !== props) {
+ return { type, props: effectiveProps };
}
return undefined;
}
+function makeWrapper(orig: Function, thisArg: any) {
+ return function (this: any, type: any, props: any, ...rest: any[]) {
+ const resolved = resolveReplacement(type, props, rest);
+ if (resolved === null) return null;
+ if (resolved) return orig.call(thisArg ?? this, resolved.type, resolved.props ?? null, ...rest);
+ return orig.call(thisArg ?? this, type, props ?? null, ...rest);
+ };
+}
+
export function patchCreateElement(cleanups: (() => void)[]) {
if (isPatched) return;
@@ -115,60 +198,124 @@ export function patchCreateElement(cleanups: (() => void)[]) {
return;
}
- const patchedCreateElement = function (type: any, props: any, ...rest: any[]) {
- const resolved = resolveReplacement(type, props);
- if (resolved === null) return null;
- if (resolved) return origCreateElement.call(React, resolved.type, resolved.props, ...rest);
- return origCreateElement.call(React, type, props, ...rest);
- };
-
+ const patchedCreateElement = makeWrapper(origCreateElement, React);
Object.assign(patchedCreateElement, origCreateElement);
React.createElement = patchedCreateElement as typeof React.createElement;
isPatched = true;
// Modern React/RN builds (Discord's included) compile JSX through the automatic runtime
- // (jsx/jsxs from react/jsx-runtime) instead of React.createElement - patching only
- // createElement misses every one of Discord's own render calls entirely. Found by shape
- // (jsx/jsxs/Fragment together), not by name, for the same reason everything else in this file
- // avoids name-based lookups.
+ // (jsx/jsxs/jsxDEV from react/jsx-runtime) instead of React.createElement - patching only
+ // createElement misses effectively every one of Discord's own render calls. Found by shape
+ // (jsx/jsxs/jsxDEV together), not by name, since runtime function names get mangled/stripped.
//
- // This used to be a single one-shot rawFind() call - if the jsx-runtime module hadn't been
- // required yet at the exact moment this ran (very early in boot, when a plugin's onLoad first
- // calls this), jsx/jsxs would never get patched for the rest of the session at all, silently
- // (only a console.warn, invisible in normal use). Since Discord renders almost everything
- // through jsx/jsxs and not createElement, that made every registerTypeDetector/
- // registerPropsIntercept consumer's patch a near-total no-op whenever that race was lost -
- // plausibly the real reason a component-creation-interception approach could still miss a
- // component that only ever renders once, early in boot. Retrying via waitFor (with the same
- // passive rawFind, so it still never force-requires anything) closes that gap.
+ // ALL matching runtime modules get patched, not just the first one found, and the scan keeps
+ // running instead of stopping at the first success: a bundle can hold more than one copy of the
+ // runtime (different chunks importing different copies), and Metro registers modules lazily, so
+ // a copy belonging to a chunk that hasn't been required yet at boot simply does not exist to be
+ // found at that moment - a one-shot search permanently misses every copy registered later.
const restoreJsx: (() => void)[] = [];
- const jsxHandle = waitFor(
- () => rawFind((m: any) => typeof m?.jsx === "function" && typeof m?.jsxs === "function" && "Fragment" in m),
- (jsxRuntime: any) => {
- for (const key of ["jsx", "jsxs"] as const) {
- const orig = jsxRuntime[key];
- if (typeof orig !== "function") continue;
+ const patchedJsxRuntimes = new WeakSet();
- jsxRuntime[key] = function (type: any, props: any, ...rest: any[]) {
- const resolved = resolveReplacement(type, props);
- if (resolved === null) return null;
- if (resolved) return orig.call(this, resolved.type, resolved.props, ...rest);
- return orig.call(this, type, props, ...rest);
- };
- restoreJsx.push(() => { jsxRuntime[key] = orig; });
+ function isJsxRuntime(m: any): boolean {
+ return typeof m?.jsx === "function" || typeof m?.jsxs === "function" || typeof m?.jsxDEV === "function";
+ }
+
+ function patchJsxObject(runtime: any) {
+ if (patchedJsxRuntimes.has(runtime)) return;
+ patchedJsxRuntimes.add(runtime);
+ for (const key of ["jsx", "jsxs", "jsxDEV"] as const) {
+ const orig = runtime[key];
+ if (typeof orig !== "function") continue;
+ runtime[key] = makeWrapper(orig, runtime);
+ restoreJsx.push(() => { runtime[key] = orig; });
+ }
+ }
+
+ function wrapJsxFunction(runtime: any): any {
+ const wrappedJsx = makeWrapper(runtime, runtime);
+ for (const key of ["jsxs", "jsxDEV"] as const) {
+ const orig = runtime[key];
+ if (typeof orig === "function") {
+ wrappedJsx[key] = makeWrapper(orig, runtime);
}
}
- );
+ if ("Fragment" in runtime) {
+ wrappedJsx.Fragment = runtime.Fragment;
+ }
+ return wrappedJsx;
+ }
+
+ function patchJsxModule(def: any) {
+ if (!def?.publicModule?.exports) return;
+ const exports = def.publicModule.exports;
+
+ try {
+ if (typeof exports === "function" && isJsxRuntime(exports) && !patchedJsxRuntimes.has(exports)) {
+ patchedJsxRuntimes.add(exports);
+ const wrapped = wrapJsxFunction(exports);
+ def.publicModule.exports = wrapped;
+ restoreJsx.push(() => { def.publicModule.exports = exports; });
+ } else if (isJsxRuntime(exports) && !patchedJsxRuntimes.has(exports)) {
+ patchedJsxRuntimes.add(exports);
+ patchJsxObject(exports);
+ }
+
+ const dflt = exports.default;
+ if (dflt != null && typeof dflt === "function" && isJsxRuntime(dflt) && !patchedJsxRuntimes.has(dflt)) {
+ patchedJsxRuntimes.add(dflt);
+ const wrapped = wrapJsxFunction(dflt);
+ exports.default = wrapped;
+ restoreJsx.push(() => { exports.default = dflt; });
+ }
+ } catch {
+ // Ignore one module's bad shape and continue scanning.
+ }
+ }
+
+ function scanAndPatchJsxRuntimes() {
+ const modules = (window as any)?.modules;
+ if (!modules) return;
+ for (const id in modules) {
+ const def = modules[id];
+ if (!def?.isInitialized) continue;
+ patchJsxModule(def);
+ }
+ }
+
+ // Patch any jsx runtimes already loaded before Discord's first render pass starts, synchronously
+ // - an async scan would complete too late for the very first render of an early-mounting target.
+ scanAndPatchJsxRuntimes();
+
+ // Fast while the app is still booting and chunks are being pulled in, then slow, then stop - a
+ // full window.modules walk is not free and there's nothing left to catch once the UI has settled.
+ let ticks = 0;
+ let timer: ReturnType | undefined = setInterval(() => {
+ scanAndPatchJsxRuntimes();
+ if (++ticks === 50 && timer) { // ~5s at 100ms
+ clearInterval(timer);
+ timer = setInterval(() => {
+ scanAndPatchJsxRuntimes();
+ if (++ticks >= 75 && timer) { // + ~25s at 1s
+ clearInterval(timer);
+ timer = undefined;
+ }
+ }, 1000);
+ }
+ }, 100);
cleanups.push(() => {
- jsxHandle.cancel();
+ if (timer) clearInterval(timer);
+ timer = undefined;
if (isPatched) {
React.createElement = origCreateElement;
isPatched = false;
}
restoreJsx.forEach((fn) => fn());
+ restoreJsx.length = 0;
intercepts.clear();
propsIntercepts.length = 0;
+ propsTransforms.length = 0;
typeDetectors = [];
+ detectorKeys.clear();
});
}