Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
19 changes: 9 additions & 10 deletions plugins/lib/src/redux/intercept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -34,26 +34,25 @@ export function intercept<T extends ActionType | ActionType[]>(
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<T>, type: ActionType) => {
unIntercept();
return cb(payload, type);
}
: cb;
if (once)
cb = (payload: InterceptPayload<T>, 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<InterceptCallback<T>>();
interceptors[actionType].add(intercept);
interceptors[actionType].add(cb);
}

unloads?.add(unIntercept);
Expand Down
1 change: 1 addition & 0 deletions plugins/ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { unloads } from "./index.safe";
export * from "./classes";
export * from "./components";
export * from "./helpers";
export * from "./react";

export { lunaMuiTheme, unloads };

Expand Down
1 change: 1 addition & 0 deletions plugins/ui/src/react/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./intercept";
99 changes: 99 additions & 0 deletions plugins/ui/src/react/intercept.tsx
Original file line number Diff line number Diff line change
@@ -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 = UnknownRecord> = P & { children?: React.ReactNode };
export type JSXSProps<P = UnknownRecord> = 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<Record<JSXElementType, Set<JSXRender>>> = {};

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);
};
Comment on lines +34 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When type is a string but no interceptor is registered for that element type, interceptJSX returns undefined. The ! non-null assertion hides this, but the element won't be rendered at all.

Should it fallback to renderJSX/renderJSXS when interceptJSX returns undefined?

jsxRuntime.jsx = function (type, props, key) {
      if (typeof type === "string") {
            const result = interceptJSX(false, type, props, key);
            if (result !== undefined) return result;
      }
      return renderJSX(type, props, key);
};

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning undefined cancels the render, or renders nothing. But jsx doesn't like undefined as a return type.

Using ! was easy. Maybe I'll try ?? null if it doesn't complain

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 (
<>
<span>Hello Above</span>
{renderJSXS(elementType, props, key)}
</>
);
});

/**
* Intercept a React Componoent Render based on its `ElementType`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "Componoent" → "Component"

*
* **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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: "your doing" → "you're 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<JSXRender>();
renderInterceptors[elementType].add(cb);

unloads?.add(unIntercept);
return unIntercept;
}