diff --git a/lib/plugins/src/apis/react.ts b/lib/plugins/src/apis/react.ts index fe2ed75..5f4fd4b 100644 --- a/lib/plugins/src/apis/react.ts +++ b/lib/plugins/src/apis/react.ts @@ -1,11 +1,14 @@ import * as PluginApiReact_ from '@revenge-mod/react' +import * as PluginApiReactJsxRuntime from '@revenge-mod/react/jsx-runtime' import * as PluginApiReactNative from '@revenge-mod/react/native' export type PluginApiReact = typeof PluginApiReact_ & { + jsxRuntime: typeof PluginApiReactJsxRuntime native: typeof PluginApiReactNative } export const react: PluginApiReact = { ...PluginApiReact_, + jsxRuntime: PluginApiReactJsxRuntime, native: PluginApiReactNative, } diff --git a/lib/react/package.json b/lib/react/package.json index 1007ba2..d750a06 100644 --- a/lib/react/package.json +++ b/lib/react/package.json @@ -6,12 +6,18 @@ ".": { "default": "./src/index.ts" }, + "./jsx-runtime": { + "default": "./src/jsx-runtime/index.ts" + }, "./native": { "default": "./src/native/index.ts" }, "./preinit": { "default": "./src/preinit.ts" }, + "./init": { + "default": "./src/init.ts" + }, "./types": { "types": "./src/types.ts" } 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..39b7830 --- /dev/null +++ b/lib/react/src/jsx-runtime/_internal.ts @@ -0,0 +1,11 @@ +import type { ElementType } from 'react' +import type { AfterJSXCallback, BeforeJSXCallback, InsteadJSXCallback } from '.' + +export const jPatches = new Map< + ElementType | string, + [ + before?: Set, + after?: Set, + instead?: Set, + ] +>() diff --git a/lib/react/src/jsx-runtime/index.ts b/lib/react/src/jsx-runtime/index.ts new file mode 100644 index 0000000..518efd0 --- /dev/null +++ b/lib/react/src/jsx-runtime/index.ts @@ -0,0 +1,108 @@ +import { jPatches } 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], + jsx: AnyJSXFactoryFunction, +) => ReturnType | null + +export type AfterJSXCallback = ( + fiber: 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 patches = jPatches.get(type) + if (!patches) { + patches = [undefined, new Set(), undefined] + jPatches.set(type, patches) + } + + const set = patches[1]! + + set.add(patch as AfterJSXCallback) + return () => { + const res = set.delete(patch as AfterJSXCallback) + if (res) attemptCleanup(type) + return res + } +} + +/** + * 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 patches = jPatches.get(type) + if (!patches) { + patches = [new Set(), undefined, undefined] + jPatches.set(type, patches) + } + + const set = patches[0]! + + set.add(patch as BeforeJSXCallback) + return () => { + const res = set.delete(patch as BeforeJSXCallback) + if (res) attemptCleanup(type) + return res + } +} + +/** + * 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 patches = jPatches.get(type) + if (!patches) { + patches = [undefined, undefined, new Set()] + jPatches.set(type, patches) + } + + const set = patches[2]! + + set.add(patch as InsteadJSXCallback) + return () => { + const res = set.delete(patch as InsteadJSXCallback) + if (res) attemptCleanup(type) + return res + } +} + +function attemptCleanup(type: ElementType) { + const patches = jPatches.get(type)! + if (patches[0]?.size || patches[1]?.size || patches[2]?.size) return + jPatches.delete(type) +} diff --git a/lib/react/src/jsx-runtime/patches.ts b/lib/react/src/jsx-runtime/patches.ts new file mode 100644 index 0000000..9d3f639 --- /dev/null +++ b/lib/react/src/jsx-runtime/patches.ts @@ -0,0 +1,34 @@ +import { instead } from '@revenge-mod/patcher' +import { ReactJSXRuntime } from '..' +import { jPatches } from './_internal' +import type { ReactElement } from 'react' +import type { AnyJSXFactoryFunction } from '.' + +const jsx = ReactJSXRuntime.jsx + +const patch = ( + args: Parameters, + orig: AnyJSXFactoryFunction, +) => { + const [type] = args + const patches = jPatches.get(type) + if (!patches) return Reflect.apply(orig, ReactJSXRuntime, args) + + const [before, after, instead] = patches + + if (before) for (const cb of before) args = cb(args) + + let fiber: ReactElement | null | undefined + + // If there are instead patches, fiber will always be set by the instead patches + if (instead) for (const cb of instead) fiber = cb(args, jsx) + // If there aren't any instead patches, we compute the fiber normally, and it will be set + else fiber = Reflect.apply(orig, ReactJSXRuntime, args) + + if (after) for (const cb of after) fiber = cb(fiber!) + + return fiber! +} + +instead(ReactJSXRuntime, 'jsx', patch) +instead(ReactJSXRuntime, 'jsxs', patch) diff --git a/src/preinit.ts b/src/preinit.ts index 1b6eb4f..93e08a1 100644 --- a/src/preinit.ts +++ b/src/preinit.ts @@ -39,10 +39,11 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() { // Initialize init libraries require('@revenge-mod/utils/init') + require('@revenge-mod/react/init') require('@revenge-mod/storage/init') + require('@revenge-mod/externals/init') require('@revenge-mod/discord/init') require('@revenge-mod/components/init') - require('@revenge-mod/externals/init') // Run all init plugins require('~/plugins/init')