diff --git a/lib/react/package.json b/lib/react/package.json index 1b9c88d..1cb1aba 100644 --- a/lib/react/package.json +++ b/lib/react/package.json @@ -15,8 +15,11 @@ "./preinit": { "default": "./src/preinit.ts" }, + "./init": { + "default": "./src/init.ts" + }, "./types": { "types": "./src/types.ts" } } -} +} \ No newline at end of file diff --git a/lib/react/src/init.ts b/lib/react/src/init.ts new file mode 100644 index 0000000..1c7c61e --- /dev/null +++ b/lib/react/src/init.ts @@ -0,0 +1 @@ +import './jsx-runtime/patches' diff --git a/lib/react/src/jsx-runtime/_internal.ts b/lib/react/src/jsx-runtime/_internal.ts new file mode 100644 index 0000000..2866f62 --- /dev/null +++ b/lib/react/src/jsx-runtime/_internal.ts @@ -0,0 +1,12 @@ +import type { ElementType } from 'react' +import type { AfterJSXCallback, BeforeJSXCallback, InsteadJSXCallback } from '.' + +export const beforeCallbacks = new Map< + ElementType, + Set> +>() +export const insteadCallbacks = new Map< + ElementType, + Set> +>() +export const afterCallbacks = new Map>() diff --git a/lib/react/src/jsx-runtime/index.ts b/lib/react/src/jsx-runtime/index.ts index 171cf50..d601a9b 100644 --- a/lib/react/src/jsx-runtime/index.ts +++ b/lib/react/src/jsx-runtime/index.ts @@ -1,2 +1,83 @@ -// TODO(react/jsx-runtime): add lib -export {} +import { afterCallbacks, beforeCallbacks, insteadCallbacks } from './_internal' +import type { ComponentProps, ElementType, Key, ReactElement } from 'react' +import type { ReactJSXRuntime } from '..' + +export type AnyJSXFactoryFunction = (typeof ReactJSXRuntime)['jsx' | 'jsxs'] + +export type BeforeJSXCallback< + E extends ElementType = ElementType, + P = ComponentProps, +> = ( + args: [element: E, props: P, key?: Key | undefined], +) => Parameters + +export type InsteadJSXCallback< + E extends ElementType = ElementType, + P = ComponentProps, +> = ( + args: [element: E, props: P, key?: Key | undefined], +) => ReturnType | null + +export type AfterJSXCallback = ( + tree: ReactElement, +) => ReturnType | null + +/** + * Registers a hook to be called after a JSX element with the specified type is created. + * + * @param type The type of the element. + * @param patch The hook. + * @returns A function to unpatch. + */ +export function afterJSX(type: ElementType, patch: AfterJSXCallback) { + let set = afterCallbacks.get(type) + if (!set) { + set = new Set() + afterCallbacks.set(type, set) + } + + set.add(patch as AfterJSXCallback) + return () => set.delete(patch as AfterJSXCallback) +} + +/** + * Registers a hook to be called before a JSX element with the specified type is created. + * + * @param type The type of the element. + * @param patch The hook. + * @returns A function to unpatch. + */ +export function beforeJSX< + E extends ElementType = ElementType, + P = ComponentProps, +>(type: E, patch: BeforeJSXCallback) { + let set = beforeCallbacks.get(type) + if (!set) { + set = new Set() + beforeCallbacks.set(type, set) + } + + set.add(patch as BeforeJSXCallback) + return () => set.delete(patch as BeforeJSXCallback) +} + +/** + * Registers a callback to run instead when a JSX element with the specified type is created. + * + * @param type The type of the element. + * @param patch The hook. + * @returns A function to unpatch. + */ +export function insteadJSX< + E extends ElementType = ElementType, + P = ComponentProps, +>(type: E, patch: InsteadJSXCallback) { + let set = insteadCallbacks.get(type) + if (!set) { + set = new Set() + insteadCallbacks.set(type, set) + } + + set.add(patch as InsteadJSXCallback) + return () => set.delete(patch as InsteadJSXCallback) +} diff --git a/lib/react/src/jsx-runtime/patches.ts b/lib/react/src/jsx-runtime/patches.ts index e69de29..b8bc9db 100644 --- a/lib/react/src/jsx-runtime/patches.ts +++ b/lib/react/src/jsx-runtime/patches.ts @@ -0,0 +1,30 @@ +import { instead } from '@revenge-mod/patcher' +import { ReactJSXRuntime } from '..' +import { afterCallbacks, beforeCallbacks, insteadCallbacks } from './_internal' +import type { ElementType, ReactElement } from 'react' +import type { AnyJSXFactoryFunction } from '.' + +const patch = ( + args: Parameters, + orig: AnyJSXFactoryFunction, +) => { + const Comp = args[0] as any + const type: ElementType = Comp.type ?? Comp + + const before = beforeCallbacks.get(type) + if (before) for (const cb of before) args = cb(args) + + let tree: ReactElement | null | undefined + const instead = insteadCallbacks.get(type) + if (instead) for (const cb of instead) tree = cb(args) + + const after = afterCallbacks.get(type) + if (after) for (const cb of after) tree = cb(tree!) + + if (tree !== undefined) return tree! // have to cast non-nullish even if returning null works + + return Reflect.apply(orig, ReactJSXRuntime, args) +} + +instead(ReactJSXRuntime, 'jsx', patch) +instead(ReactJSXRuntime, 'jsxs', patch) diff --git a/lib/react/src/preinit.ts b/lib/react/src/preinit.ts index cea1efd..12acb30 100644 --- a/lib/react/src/preinit.ts +++ b/lib/react/src/preinit.ts @@ -2,4 +2,3 @@ import '.' import './native/patches' -import './jsx-runtime/patches' diff --git a/src/preinit.ts b/src/preinit.ts index a28729f..e29c94a 100644 --- a/src/preinit.ts +++ b/src/preinit.ts @@ -37,6 +37,7 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() { ) // Initialize init libraries + require('@revenge-mod/react/init') require('@revenge-mod/storage/init') require('@revenge-mod/externals/init') require('@revenge-mod/discord/init')