diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a6b152..e2116ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,8 @@ "/plugins/ui/src/helpers", "/plugins/lib/src/helpers", "/plugins/lib/src/classes", - "/plugins/lib/src/redux" + "/plugins/lib/src/redux", + "/plugins/ui/src/react" ], "discord.enabled": false } diff --git a/plugins/lib/src/redux/intercept.ts b/plugins/lib/src/redux/intercept.ts index 7649ee4..80be398 100644 --- a/plugins/lib/src/redux/intercept.ts +++ b/plugins/lib/src/redux/intercept.ts @@ -19,7 +19,7 @@ import { interceptors, type LunaUnload, type LunaUnloads } from "@luna/core"; * Intercept a Redux action based on its `type` * @param actionType The ActionKey to intercept * @param cb Called when action is intercepted with action args, if returning true action is not dispatched (cancelled) - * @param unloads Set of unload functions to add this to, can be null but only pass if you know what your doing + * @param unloads Set of unload functions to add this to, can be nullish but only if you know what your doing * @param once If set true only intercepts once * @returns Function to call to unload/cancel the intercept */ @@ -34,26 +34,25 @@ export function intercept( const actionTypeArray: ActionType[] = Array.isArray(actionTypes) ? actionTypes : [actionTypes]; // If once is true then call unIntercept immediately to only run once - const intercept = once - ? (payload: InterceptPayload, type: ActionType) => { - unIntercept(); - return cb(payload, type); - } - : cb; + if (once) + cb = (payload: InterceptPayload, type: ActionType) => { + unIntercept(); + return cb(payload, type); + }; // Wrap removing the callback from the interceptors in a unload function and return it const unIntercept = () => { for (const actionType of actionTypeArray) { // ?. so that it doesn't throw if the interceptor was already removed - interceptors[actionType]?.delete(intercept); + interceptors[actionType]?.delete(cb); if (interceptors[actionType]?.size === 0) delete interceptors[actionType]; } }; - unIntercept.source = `intercept${JSON.stringify(actionTypeArray)}`; + unIntercept.source = `intercept::${JSON.stringify(actionTypeArray)}`; for (const actionType of actionTypeArray) { interceptors[actionType] ??= new Set>(); - interceptors[actionType].add(intercept); + interceptors[actionType].add(cb); } unloads?.add(unIntercept); diff --git a/plugins/ui/src/index.tsx b/plugins/ui/src/index.tsx index 28d0bab..1f411c1 100644 --- a/plugins/ui/src/index.tsx +++ b/plugins/ui/src/index.tsx @@ -19,6 +19,7 @@ import { unloads } from "./index.safe"; export * from "./classes"; export * from "./components"; export * from "./helpers"; +export * from "./react"; export { lunaMuiTheme, unloads }; diff --git a/plugins/ui/src/react/index.ts b/plugins/ui/src/react/index.ts new file mode 100644 index 0000000..d65d607 --- /dev/null +++ b/plugins/ui/src/react/index.ts @@ -0,0 +1 @@ +export * from "./intercept"; diff --git a/plugins/ui/src/react/intercept.tsx b/plugins/ui/src/react/intercept.tsx new file mode 100644 index 0000000..c44b950 --- /dev/null +++ b/plugins/ui/src/react/intercept.tsx @@ -0,0 +1,99 @@ +import React from "react"; + +import type { UnknownRecord } from "@inrixia/helpers"; +import type { LunaUnload, LunaUnloads } from "@luna/core"; +import jsxRuntime, { type JSX } from "react/jsx-runtime"; +import { unloads } from "../index.safe"; + +export const renderJSX = jsxRuntime.jsx; +export const renderJSXS = jsxRuntime.jsxs; +unloads.add(() => { + jsxRuntime.jsx = renderJSX; + jsxRuntime.jsxs = renderJSXS; +}); + +export type RenderJSX = typeof renderJSX; +export type RenderJSXS = typeof renderJSXS; +export type JSXProps

= P & { children?: React.ReactNode }; +export type JSXSProps

= P & { children?: React.ReactNode[] }; +export type JSXElementType = keyof JSX.IntrinsicElements; + +type JSXRenderArgs = + | [isJSXS: true, elementType: JSXElementType, props: JSXSProps, key?: React.Key] + | [isJSXS: false, elementType: JSXElementType, props: JSXProps, key?: React.Key]; +/** + * @param elementType The React HTMLElementType + * @param props The React element props + * @param key The React element key + * @param isJSXS Indicates if calling render was from JSXS or JSX. JSXS children is an array, JSXS is not + * @returns `undefined` to continue, `ReactElement` to render returned element immediately or `null` to cancel. + */ +export type JSXRender = (...args: JSXRenderArgs) => undefined | React.ReactElement | null; +export const renderInterceptors: Partial>> = {}; + +jsxRuntime.jsx = function (type, props, key) { + if (typeof type === "string") return interceptJSX(false, type, props, key)!; + return renderJSX(type, props, key); +}; +jsxRuntime.jsxs = function (type, props, key) { + if (typeof type === "string") return interceptJSX(true, type, props, key)!; + return renderJSXS(type, props, key); +}; +const interceptJSX = (isJSXS: boolean, type: JSXElementType, props: any, key?: React.Key) => { + if (type in renderInterceptors) { + // Run interceptors for JSXElementType + for (const interceptor of renderInterceptors[type]!) { + const res = interceptor(isJSXS, type, props, key); + // If res is null or ReactElement immediately return it. + if (res !== undefined) return res; + } + } +}; + +interceptRender("div", unloads, (isJSXS, elementType, props, key?) => { + if (props["data-test"] !== "footer-player") return; + + const children = isJSXS ? (props.children ?? []) : [props.children]; + children.push(<>Hello Inside); + props.children = children; + + return ( + <> + Hello Above + {renderJSXS(elementType, props, key)} + + ); +}); + +/** + * Intercept a React Componoent Render based on its `ElementType` + * + * **WARNING!** `cb` is called on every render for `ElementType`, only use this if you know what you are doing. This is performance critical code. + * @param elementType The React HTMLElementType to intercept + * @param cb Called when render is intercepted with props, if returning false element is not rendered + * @param unloads Set of unload functions to add this to, can be nullish but only if you know what your doing + * @param once If set true only intercepts once + * @returns Function to call to unload/cancel the intercept + */ +export function interceptRender(elementType: React.HTMLElementType, unloads: LunaUnloads, cb: JSXRender, once?: boolean): LunaUnload { + // If once is true then call unIntercept immediately to only run once + if (once) + cb = (isJSXS, type, props: any, key?) => { + unIntercept(); + return cb(isJSXS, type, props, key); + }; + + // Wrap removing the callback from the interceptors in a unload function and return it + const unIntercept = () => { + // ?. so that it doesn't throw if the interceptor was already removed + renderInterceptors[elementType]?.delete(cb); + if (renderInterceptors[elementType]?.size === 0) delete renderInterceptors[elementType]; + }; + unIntercept.source = `intercept::${elementType}`; + + renderInterceptors[elementType] ??= new Set(); + renderInterceptors[elementType].add(cb); + + unloads?.add(unIntercept); + return unIntercept; +}