bippy
Version:
hack into react internals
230 lines (212 loc) • 8.05 kB
text/typescript
// IMPORTANT:
// this file is super important to load the __REACT_DEVTOOLS_GLOBAL_HOOK__ object
// without this, we can't stub the React DevTools global hook, we don't have a way to instrument the application
// make sure you import this file first before anything else (particularly React)
import type { ReactDevToolsGlobalHook, ReactRenderer } from "./types.js";
export const version = process.env.VERSION;
export const BIPPY_INSTRUMENTATION_STRING = `bippy-${version}`;
const objectDefineProperty = Object.defineProperty;
// eslint-disable-next-line @typescript-eslint/unbound-method
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
const NO_OP = () => {
/**/
};
const checkDCE = (fn: unknown): void => {
try {
const code = Function.prototype.toString.call(fn);
if (code.indexOf("^_^") > -1) {
setTimeout(() => {
throw new Error(
"React is running in production mode, but dead code " +
"elimination has not been applied. Read how to correctly " +
"configure React for production: " +
"https://reactjs.org/link/perf-use-production-build",
);
});
}
} catch {}
};
export const isRealReactDevtools = (
rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
): boolean => {
return Boolean(rdtHook && "getFiberRoots" in rdtHook);
};
let isReactRefreshOverride = false;
let injectFnStr: string | undefined = undefined;
export const isReactRefresh = (
rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
): boolean => {
if (isReactRefreshOverride) return true;
if (rdtHook && typeof rdtHook.inject === "function") {
injectFnStr = rdtHook.inject.toString();
}
// https://github.com/facebook/react/blob/8f8b336734d7c807f5aa11b0f31540e63302d789/packages/react-refresh/src/ReactFreshRuntime.js#L459
return Boolean(injectFnStr?.includes("(injected)"));
};
const onActiveListeners = new Set<() => unknown>();
export const _renderers = new Set<ReactRenderer>();
export const installRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
const renderers = new Map<number, ReactRenderer>();
let i = 0;
let rdtHook: ReactDevToolsGlobalHook = {
_instrumentationIsActive: false,
_instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
checkDCE,
hasUnsupportedRendererAttached: false,
inject(renderer) {
const nextID = ++i;
renderers.set(nextID, renderer);
_renderers.add(renderer);
if (!rdtHook._instrumentationIsActive) {
rdtHook._instrumentationIsActive = true;
onActiveListeners.forEach((listener) => listener());
}
return nextID;
},
on: NO_OP,
onCommitFiberRoot: NO_OP,
onCommitFiberUnmount: NO_OP,
onPostCommitFiberRoot: NO_OP,
renderers,
supportsFiber: true,
supportsFlight: true,
};
try {
objectDefineProperty(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
configurable: true,
enumerable: true,
get() {
return rdtHook;
},
set(newHook) {
if (newHook && typeof newHook === "object") {
const ourRenderers = rdtHook.renderers;
rdtHook = newHook;
if (ourRenderers.size > 0) {
ourRenderers.forEach((renderer, id) => {
_renderers.add(renderer);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
newHook.renderers.set(id, renderer);
});
patchRDTHook(onActive);
}
}
},
});
// [!] this is a hack for chrome extensions - if we install before React DevTools, we could accidently prevent React DevTools from installing:
// https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-devtools-extensions/src/contentScripts/installHook.js#L30C6-L30C27
// eslint-disable-next-line @typescript-eslint/unbound-method
const originalWindowHasOwnProperty = window.hasOwnProperty;
let hasRanHack = false;
objectDefineProperty(window, "hasOwnProperty", {
configurable: true,
value: function (this: unknown, ...args: [PropertyKey]) {
try {
if (!hasRanHack && args[0] === "__REACT_DEVTOOLS_GLOBAL_HOOK__") {
globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined;
// special falsy value to know that we've already installed before
hasRanHack = true;
return -0;
}
} catch {}
return originalWindowHasOwnProperty.apply(this, args);
},
writable: true,
});
} catch {
patchRDTHook(onActive);
}
return rdtHook;
};
export const patchRDTHook = (onActive?: () => unknown): void => {
if (onActive) {
onActiveListeners.add(onActive);
}
try {
const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (!rdtHook) return;
if (!rdtHook._instrumentationSource) {
rdtHook.checkDCE = checkDCE;
rdtHook.supportsFiber = true;
rdtHook.supportsFlight = true;
rdtHook.hasUnsupportedRendererAttached = false;
rdtHook._instrumentationSource = BIPPY_INSTRUMENTATION_STRING;
rdtHook._instrumentationIsActive = false;
// we need to be careful here (needs to be below _instrumentationSource) else it causes excessive recursion
const isReactDevtools = isRealReactDevtools(rdtHook);
if (!isReactDevtools) {
rdtHook.on = NO_OP;
}
if (rdtHook.renderers.size) {
rdtHook._instrumentationIsActive = true;
onActiveListeners.forEach((listener) => listener());
return;
}
const prevInject = rdtHook.inject;
const isRefresh = isReactRefresh(rdtHook);
if (isRefresh && !isReactDevtools) {
isReactRefreshOverride = true;
// but since the underlying implementation doens't care,
// it's ok: https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
const nextID = rdtHook.inject({
scheduleRefresh() {},
} as unknown as ReactRenderer);
if (nextID) {
rdtHook._instrumentationIsActive = true;
}
}
rdtHook.inject = (renderer) => {
const id = prevInject(renderer);
_renderers.add(renderer);
if (isRefresh) {
// react refresh doesn't inject this properly
// https://github.com/facebook/react/blob/18eaf51bd51fed8dfed661d64c306759101d0bfd/packages/react-refresh/src/ReactFreshRuntime.js#L430
rdtHook.renderers.set(id, renderer);
}
rdtHook._instrumentationIsActive = true;
onActiveListeners.forEach((listener) => listener());
return id;
};
}
if (
rdtHook.renderers.size ||
rdtHook._instrumentationIsActive ||
// depending on this to inject is unsafe, since inject could occur before and we wouldn't know
isReactRefresh()
) {
onActive?.();
}
} catch {}
};
export const hasRDTHook = (): boolean => {
return objectHasOwnProperty.call(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
};
/**
* Returns the current React DevTools global hook.
*/
export const getRDTHook = (onActive?: () => unknown): ReactDevToolsGlobalHook => {
if (!hasRDTHook()) {
return installRDTHook(onActive);
}
patchRDTHook(onActive);
// must exist at this point
return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ as ReactDevToolsGlobalHook;
};
export const isClientEnvironment = (): boolean => {
return Boolean(
typeof window !== "undefined" &&
// eslint-disable-next-line @typescript-eslint/unbound-method
(window.document?.createElement || window.navigator?.product === "ReactNative"),
);
};
/**
* Usually used purely for side effect
*/
export const safelyInstallRDTHook = () => {
try {
// __REACT_DEVTOOLS_GLOBAL_HOOK__ must exist before React is ever executed
if (isClientEnvironment()) {
getRDTHook();
}
} catch {}
};