Itsnotlupus' React Tools

Observe, inspect and perhaps modify a React tree through the React DevTools Hook

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/473998/1246974/Itsnotlupus%27%20React%20Tools.js

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name        Itsnotlupus' React Tools
// @description Observe, inspect and perhaps modify a React tree through the React DevTools Hook
// @namespace   Itsnotlupus Industries
// @author      itsnotlupus
// @license     MIT
// @version     1.1
// ==/UserScript==

// React shenanigans. the code below allows us to:
// - observe every react renders
// - inspect nodes for props (which can expose redux and other useful state)
// - modify props value (which may not stick unless reapplied on every render)
class ReactTools {

  #HOOK = '__REACT_DEVTOOLS_GLOBAL_HOOK__';

  #reactRoots = new Set;
  #reactObservers = new Set;
  #notifyReactObservers(root) { this.#reactObservers.forEach(fn=>fn(root)); }

  // singleton, so we only muck with the React hook once.
  static #instance;
  constructor() {
    if (ReactTools.#instance) return ReactTools.#instance;
    ReactTools.#instance = this;
    
    const win = globalThis.unsafeWindow ?? window;
    const nop = ()=>{};
    const hook = win[this.#HOOK];
    if (hook) {
      const ocfr = hook.onCommitFiberRoot?.bind(hook) ?? nop;
      hook.onCommitFiberRoot = (_, root) => {
        this.#reactRoots.add(root);
        this.#notifyReactObservers(root);
        return ocfr(_, root);
      };
    } else {
      win[this.#HOOK] = {
        onCommitFiberRoot: (_, root) => {
          this.#reactRoots.add(root);
          this.#notifyReactObservers(root);
        },
        onCommitFiberUnmount: nop,
        inject: nop,
        checkDCE: nop,
        supportsFiber: true,
        on: nop,
        sub: nop,
        renderers: [],
        emit: nop
      };
    }
  }

  /** Traversal of React's tree to find nodes that match a props name */
  findNodesWithProp(name, firstOnly = false) {
    const acc = new Set;
    const visited = new Set;
    const getPropFromNode = node => {
      if (!node || visited.has(node)) return;
      visited.add(node);
      const props = node.memoizedProps;
      if (props && typeof props === 'object' && name in props) {
        acc.add(node);
        if (firstOnly) throw 0; // goto end
      }
      getPropFromNode(node.sibling);
      getPropFromNode(node.child);
    };
    try { this.#reactRoots.forEach(root => getPropFromNode(root.current)) } catch {}
    return Array.from(acc);
  }

  /** Magically obtain a prop value from the most top-level React component we can find */
  getProp(name) {
    return this.findNodesWithProp(name, true)[0]?.memoizedProps?.[name];
  }

  /** Forcefully mutate props on a component node in the react tree. */
  updateNodeProps(node, props) {
    Object.assign(node.memoizedProps, props);
    Object.assign(node.pendingProps, props);
    Object.assign(node.stateNode?.props??{}, props);
  }

  /** calls a function whenever react renders */
  observe(fn) {
    this.#reactObservers.add(fn);
    return () => this.#reactObservers.delete(fn);
  };
  
  // Hook into a React tree, and find a redux-shaped store off of one of the components there.
  static withReduxState(fn) {
    const react = new ReactTools();
    const disconnect = react.observe(() => {
      const store = react.getProp('store');
      if (store) {
        fn(store.getState());
        disconnect();
      }
    });
  }
}