diff --git a/packages/bippy/src/core.ts b/packages/bippy/src/core.ts index 2a8d7d0b..fe6cb7c8 100644 --- a/packages/bippy/src/core.ts +++ b/packages/bippy/src/core.ts @@ -1,4 +1,4 @@ -// React must remain a type-only import because this module loads immediately after the DevTools hook. +// HACK: A runtime React import would load before the DevTools hook can observe the renderer. import type * as React from "react"; @@ -56,11 +56,35 @@ interface FiberSelector { (node: Fiber): boolean | Promise | void; } +interface TraverseFiber { + ( + fiber: Fiber | null, + selector: (node: Fiber) => boolean | void, + ascending?: boolean, + ): Fiber | null; + ( + fiber: Fiber | null, + selector: (node: Fiber) => Promise, + ascending?: boolean, + ): Promise; + ( + fiber: Fiber | null, + selector: FiberSelector, + ascending?: boolean, + ): Fiber | null | Promise; +} + export interface FiberTimings { selfTime: number; totalTime: number; } +export interface FiberTimingNode { + actualDuration?: number; + child?: FiberTimingNode | null; + sibling?: FiberTimingNode | null; +} + export interface RenderHandler { (fiber: Fiber, phase: RenderPhase): unknown; } @@ -70,6 +94,14 @@ interface ValueWrite { value: unknown; } +interface RenderedRootState { + previousFiber: Fiber | null; +} + +export interface RenderedFiberRoot { + current: Fiber | null; +} + /** * Returns `true` if object is a React Element. * @@ -143,8 +175,6 @@ export const isCompositeFiber = (fiber: Fiber): boolean => { */ export const isFiber = (maybeFiber: unknown): maybeFiber is Fiber => { if (!maybeFiber || typeof maybeFiber !== "object") return false; - // this is a fast check. pendingProps will ALWAYS exist in fiber - // `containerInfo` is in FiberRootNode, not FiberNode return "pendingProps" in maybeFiber && !("containerInfo" in maybeFiber); }; @@ -251,7 +281,6 @@ export const didFiberRender = (fiber: Fiber): boolean => { return (flags & ReactFiberFlags.PerformedWork) === ReactFiberFlags.PerformedWork; } default: - // Host nodes (DOM, root, etc.) if (!fiber.alternate) return true; return ( prevProps !== nextProps || @@ -318,11 +347,7 @@ const shouldFilterFiber = (fiber: Fiber): boolean => { const workTags = getReactWorkTagsForFiber(fiber); switch (fiber.tag) { case workTags.DehydratedSuspenseComponent: - // TODO: ideally we would show dehydrated Suspense immediately. - // However, it has some special behavior (like disconnecting - // an alternate and turning into real Suspense) which breaks DevTools. - // For now, ignore it, and only show it once it gets hydrated. - // https://github.com/bvaughn/react-devtools-experimental/issues/197 + // HACK: Showing dehydrated Suspense before it becomes a real Suspense Fiber breaks DevTools. return true; case workTags.Fragment: @@ -332,7 +357,6 @@ const shouldFilterFiber = (fiber: Fiber): boolean => { return true; case workTags.HostRoot: - // It is never valid to filter the root element. return false; default: { @@ -403,26 +427,11 @@ export const getNearestHostFibers = (fiber: Fiber): Fiber[] => { /** * Traverses up or down a {@link Fiber}, return `true` to stop and select a node. */ -export function traverseFiber( - fiber: Fiber | null, - selector: (node: Fiber) => boolean | void, - ascending?: boolean, -): Fiber | null; -export function traverseFiber( - fiber: Fiber | null, - selector: (node: Fiber) => Promise, - ascending?: boolean, -): Promise; -export function traverseFiber( - fiber: Fiber | null, - selector: FiberSelector, - ascending?: boolean, -): Fiber | null | Promise; -export function traverseFiber( +const traverseFiberImplementation = ( fiber: Fiber | null, selector: FiberSelector, ascending = false, -): Fiber | null | Promise { +): Fiber | null | Promise => { if (!fiber) return null; const selection = selector(fiber); @@ -434,7 +443,9 @@ export function traverseFiber( if (selection === true) return fiber; return traverseFiberChildren(fiber, selector, ascending); -} +}; + +export const traverseFiber = traverseFiberImplementation as TraverseFiber; const isPromiseLike = (value: unknown): value is PromiseLike => (typeof value === "object" || typeof value === "function") && @@ -477,14 +488,13 @@ const traverseFiberSiblings = ( * console.log(selfTime, totalTime); * ``` */ -export const getTimings = (fiber?: Fiber | null): FiberTimings => { +export const getTimings = (fiber?: FiberTimingNode | null): FiberTimings => { const totalTime = fiber?.actualDuration ?? 0; let selfTime = totalTime; - // TODO: calculate a DOM time, which is just host component summed up let child = fiber?.child ?? null; while (totalTime > 0 && child !== null) { selfTime -= child.actualDuration ?? 0; - child = child.sibling; + child = child.sibling ?? null; } return { selfTime, totalTime }; }; @@ -599,119 +609,85 @@ const mountFiberRecursively = ( onRender(fiber, "mount"); } - if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) { - const isTimedOut = fiber.memoizedState !== null; - if (isTimedOut) { - // Special case: if Suspense mounts in a timed-out state, - // get the fallback child from the inner fragment and mount - // it as if it was our own child. Updates handle this too. - const primaryChildFragment = fiber.child; - const fallbackChildFragment = primaryChildFragment ? primaryChildFragment.sibling : null; - if (fallbackChildFragment) { - const fallbackChild = fallbackChildFragment.child; - if (fallbackChild !== null) { - mountFiberRecursively(onRender, fallbackChild, false); - } - } - } else { - const primaryChild = fiber.child?.child ?? null; - if (primaryChild !== null) { - mountFiberRecursively(onRender, primaryChild, false); - } - } - } else if (fiber.child !== null) { + const isSuspense = fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent; + if (!isSuspense && fiber.child !== null) { mountFiberRecursively(onRender, fiber.child, true); + } else if (isSuspense) { + const isTimedOut = fiber.memoizedState !== null; + const primaryChild = isTimedOut ? null : (fiber.child?.child ?? null); + const fallbackChild = isTimedOut ? (fiber.child?.sibling?.child ?? null) : null; + const visibleChild = primaryChild ?? fallbackChild; + if (visibleChild !== null) mountFiberRecursively(onRender, visibleChild, false); } fiber = traverseSiblings ? fiber.sibling : null; } }; +const updateFiberChildren = ( + onRender: RenderHandler, + nextFiber: Fiber, + previousFiber: Fiber, +): void => { + if (nextFiber.child === previousFiber.child) return; + + let nextChild = nextFiber.child; + while (nextChild) { + if (nextChild.alternate) { + updateFiberRecursively(onRender, nextChild, nextChild.alternate); + } else { + mountFiberRecursively(onRender, nextChild, false); + } + nextChild = nextChild.sibling; + } +}; + +const updateSuspenseChildren = ( + onRender: RenderHandler, + nextFiber: Fiber, + previousFiber: Fiber, +): void => { + const previousDidTimeOut = previousFiber.memoizedState !== null; + const nextDidTimeOut = nextFiber.memoizedState !== null; + + if (previousDidTimeOut && nextDidTimeOut) { + const nextFallbackChildSet = nextFiber.child?.sibling ?? null; + const previousFallbackChildSet = previousFiber.child?.sibling ?? null; + if (nextFallbackChildSet && previousFallbackChildSet) { + updateFiberRecursively(onRender, nextFallbackChildSet, previousFallbackChildSet); + } + return; + } + + if (previousDidTimeOut) { + if (nextFiber.child) mountFiberRecursively(onRender, nextFiber.child, true); + return; + } + + unmountFiberChildrenRecursively(onRender, previousFiber); + const nextFallbackChildSet = nextFiber.child?.sibling ?? null; + if (nextFallbackChildSet) mountFiberRecursively(onRender, nextFallbackChildSet, true); +}; + const updateFiberRecursively = ( onRender: RenderHandler, nextFiber: Fiber, - prevFiber: Fiber | null, + previousFiber: Fiber | null, ): void => { getFiberId(nextFiber); - if (!prevFiber) return; - getFiberId(prevFiber); - - const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent; + if (!previousFiber) return; + getFiberId(previousFiber); const shouldIncludeInTree = !shouldFilterFiber(nextFiber); if (shouldIncludeInTree && didFiberRender(nextFiber)) { onRender(nextFiber, "update"); } - // The behavior of timed-out Suspense trees is unique. - // Rather than unmount the timed out content (and possibly lose important state), - // React re-parents this content within a hidden Fragment while the fallback is showing. - // This behavior doesn't need to be observable in the DevTools though. - // It might even result in a bad user experience for e.g. node selection in the Elements panel. - // The easiest fix is to strip out the intermediate Fragment fibers, - // so the Elements panel and Profiler don't need to special case them. - // Suspense components only have a non-null memoizedState if they're timed-out. - const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null; - const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null; - - // The logic below is inspired by the code paths in updateSuspenseComponent() - // inside ReactFiberBeginWork in the React source code. - if (prevDidTimeout && nextDidTimeOut) { - // Fallback -> Fallback: - // 1. Reconcile fallback set. - const nextFallbackChildSet = nextFiber.child?.sibling ?? null; - // Note: We can't use nextFiber.child.sibling.alternate - // because the set is special and alternate may not exist. - const prevFallbackChildSet = prevFiber.child?.sibling ?? null; - - if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) { - updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet); - } - } else if (prevDidTimeout && !nextDidTimeOut) { - // Fallback -> Primary: - // 1. Unmount fallback set - // Note: don't emulate fallback unmount because React actually did it. - // 2. Mount primary set - const nextPrimaryChildSet = nextFiber.child; - - if (nextPrimaryChildSet !== null) { - mountFiberRecursively(onRender, nextPrimaryChildSet, true); - } - } else if (!prevDidTimeout && nextDidTimeOut) { - // Primary -> Fallback: - // 1. Hide primary set - // This is not a real unmount, so it won't get reported by React. - // We need to manually walk the previous tree and record unmounts. - unmountFiberChildrenRecursively(onRender, prevFiber); - - // 2. Mount fallback set - const nextFallbackChildSet = nextFiber.child?.sibling ?? null; - - if (nextFallbackChildSet !== null) { - mountFiberRecursively(onRender, nextFallbackChildSet, true); - } - } else if (nextFiber.child !== prevFiber.child) { - // Common case: Primary -> Primary. - // This is the same code path as for non-Suspense fibers. - - // If the first child is different, we need to traverse them. - // Each next child will be either a new child (mount) or an alternate (update). - let nextChild = nextFiber.child; - - while (nextChild) { - // We already know children will be referentially different because - // they are either new mounts or alternates of previous children. - // Schedule updates and mounts depending on whether alternates exist. - // We don't track deletions here because they are reported separately. - if (nextChild.alternate) { - updateFiberRecursively(onRender, nextChild, nextChild.alternate); - } else { - mountFiberRecursively(onRender, nextChild, false); - } + const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent; + const stayedOnPrimaryTree = + !isSuspense || (previousFiber.memoizedState === null && nextFiber.memoizedState === null); + if (stayedOnPrimaryTree) return updateFiberChildren(onRender, nextFiber, previousFiber); - // Try the next child. - nextChild = nextChild.sibling; - } - } + updateSuspenseChildren(onRender, nextFiber, previousFiber); }; const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => { @@ -723,23 +699,15 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => { }; const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => { - // We might meet a nested Suspense on our way. const isTimedOutSuspense = fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null; let child = fiber.child; if (isTimedOutSuspense) { - // If it's showing fallback tree, let's traverse it instead. - const primaryChildFragment = fiber.child; - const fallbackChildFragment = primaryChildFragment?.sibling ?? null; - - // Skip over to the real Fiber child. - child = fallbackChildFragment?.child ?? null; + child = fiber.child?.sibling?.child ?? null; } while (child !== null) { - // Record simulated unmounts children-first. - // We skip nodes without return because those are real unmounts. if (child.return !== null) { unmountFiber(onRender, child); unmountFiberChildrenRecursively(onRender, child); @@ -749,12 +717,45 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): } }; -const rootInstanceMap = new WeakMap< - Fiber | FiberRoot, - { - prevFiber: Fiber | null; +const rootStateMap = new WeakMap(); + +const getRenderedRootState = (root: Fiber | RenderedFiberRoot): RenderedRootState => { + const existingState = rootStateMap.get(root); + if (existingState) return existingState; + const newState: RenderedRootState = { previousFiber: null }; + rootStateMap.set(root, newState); + return newState; +}; + +const isMountedRootFiber = (fiber: Fiber): boolean => + fiber.memoizedState !== null && + fiber.memoizedState.element !== null && + fiber.memoizedState.element !== undefined && + fiber.memoizedState.isDehydrated !== true; + +const traverseRootTransition = ( + currentFiber: Fiber | null, + previousFiber: Fiber | null, + onRender: RenderHandler, +): void => { + if (!currentFiber) { + if (previousFiber) unmountFiber(onRender, previousFiber); + return; } ->(); + if (!previousFiber) return mountFiberRecursively(onRender, currentFiber, true); + + const wasMounted = isMountedRootFiber(previousFiber); + const isMounted = isMountedRootFiber(currentFiber); + if (wasMounted && isMounted) { + updateFiberRecursively(onRender, currentFiber, currentFiber.alternate); + return; + } + if (isMounted) { + mountFiberRecursively(onRender, currentFiber, false); + return; + } + if (wasMounted) unmountFiber(onRender, currentFiber); +}; /** * Creates a fiber visitor function. Must pass a fiber root and a render handler. @@ -763,47 +764,14 @@ const rootInstanceMap = new WeakMap< * console.log(phase) * }) */ -export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => { - const fiber = "current" in root ? root.current : root; - - let rootInstance = rootInstanceMap.get(root); - - if (!rootInstance) { - rootInstance = { prevFiber: null }; - rootInstanceMap.set(root, rootInstance); - } - - const { prevFiber } = rootInstance; - if (!fiber) { - if (prevFiber) { - unmountFiber(onRender, prevFiber); - } - } else if (prevFiber !== null) { - const wasMounted = - prevFiber.memoizedState !== null && - prevFiber.memoizedState.element !== null && - prevFiber.memoizedState.element !== undefined && - // A dehydrated root is not considered mounted - prevFiber.memoizedState.isDehydrated !== true; - const isMounted = - fiber.memoizedState !== null && - fiber.memoizedState.element !== null && - fiber.memoizedState.element !== undefined && - // A dehydrated root is not considered mounted - fiber.memoizedState.isDehydrated !== true; - - if (!wasMounted && isMounted) { - mountFiberRecursively(onRender, fiber, false); - } else if (wasMounted && isMounted) { - updateFiberRecursively(onRender, fiber, fiber.alternate); - } else if (wasMounted && !isMounted) { - unmountFiber(onRender, fiber); - } - } else { - mountFiberRecursively(onRender, fiber, true); - } - - rootInstance.prevFiber = fiber; +export const traverseRenderedFibers = ( + root: Fiber | RenderedFiberRoot, + onRender: RenderHandler, +): void => { + const currentFiber = "current" in root ? root.current : root; + const rootState = getRenderedRootState(root); + traverseRootTransition(currentFiber, rootState.previousFiber, onRender); + rootState.previousFiber = currentFiber; }; const overrideRenderers = new Set(); @@ -945,9 +913,7 @@ export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknow return; } - // production renderers don't expose overrideHookState; dispatching through - // the hook's own queue still works there, but only for whole-value writes - // (a path write through dispatch would replace the entire hook state) + // HACK: Production renderers only support whole-state writes through the hook queue. if (isPOJO(partialValue)) return; const dispatch = getHookStateDispatch(fiber, id); if (!dispatch) return; @@ -1000,11 +966,7 @@ let didSubscribeToHookReplacements = false; const rootRendererIds = new WeakMap(); -// each hook event is dispatched from a single re-installable wrapper. If -// something overwrites the hook method (devtools, direct assignment), the -// next instrument() call installs a fresh wrapper over it; a superseded -// wrapper still forwards the previous chain but skips the listeners so -// they never fire twice. +// HACK: Reinstallable wrappers preserve overwritten DevTools handlers without firing listeners twice. const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => { const dispatchers = hookDispatchers.get(rdtHook) ?? {}; hookDispatchers.set(rdtHook, dispatchers); @@ -1160,7 +1122,6 @@ export const instrument = (options: InstrumentationOptions): Unsubscribe => { }); }; -// React uses per-renderer suffixes for host-instance Fiber keys, so discovered keys are cached. const knownFiberPropertyKeys = new Set(); const isFiberPropertyKey = (key: string): boolean => diff --git a/packages/bippy/src/source/inspect-hooks.ts b/packages/bippy/src/source/inspect-hooks.ts index 314db88f..fd0cd774 100644 --- a/packages/bippy/src/source/inspect-hooks.ts +++ b/packages/bippy/src/source/inspect-hooks.ts @@ -643,7 +643,7 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho } const { primitive } = hook; - const id = NON_ID_HOOK_PRIMITIVES.has(primitive) ? null : nativeHookID++; + const hookId = NON_ID_HOOK_PRIMITIVES.has(primitive) ? null : nativeHookID++; const isStateEditable = primitive === "Reducer" || primitive === "State"; const name = displayName || primitive; @@ -655,7 +655,14 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho fileName: firstStackFrame?.fileName ?? null, }; - levelChildren.push({ id, isStateEditable, name, value: hook.value, subHooks: [], hookSource }); + levelChildren.push({ + id: hookId, + isStateEditable, + name, + value: hook.value, + subHooks: [], + hookSource, + }); } processDebugValues(rootChildren, null); diff --git a/packages/bippy/tests/create-fiber.ts b/packages/bippy/tests/create-fiber.ts new file mode 100644 index 00000000..d98c6c92 --- /dev/null +++ b/packages/bippy/tests/create-fiber.ts @@ -0,0 +1,34 @@ +import type { Fiber } from "../src/react-internals/index.js"; +import { latestReactWorkTags } from "./react-work-tags.js"; + +export const createFiber = (overrides: Record = {}): Fiber => { + const fiber: Fiber = { + alternate: null, + child: null, + childLanes: 0, + deletions: null, + dependencies: null, + elementType: null, + firstEffect: null, + flags: 0, + index: 0, + key: null, + lanes: 0, + lastEffect: null, + memoizedProps: {}, + memoizedState: null, + mode: 0, + nextEffect: null, + pendingProps: {}, + ref: null, + return: null, + sibling: null, + stateNode: null, + subtreeFlags: 0, + tag: latestReactWorkTags.FunctionComponent, + type: null, + updateQueue: null, + }; + + return Object.assign(fiber, overrides); +}; diff --git a/packages/bippy/tests/create-rdt-hook.ts b/packages/bippy/tests/create-rdt-hook.ts new file mode 100644 index 00000000..c2dafd2b --- /dev/null +++ b/packages/bippy/tests/create-rdt-hook.ts @@ -0,0 +1,18 @@ +import type { ReactDevToolsGlobalHook } from "../src/react-internals/index.js"; + +export const createRDTHook = (overrides: Record = {}): ReactDevToolsGlobalHook => { + const rdtHook: ReactDevToolsGlobalHook = { + checkDCE: () => {}, + hasUnsupportedRendererAttached: false, + inject: () => 1, + on: () => {}, + onCommitFiberRoot: () => {}, + onCommitFiberUnmount: () => {}, + onPostCommitFiberRoot: () => {}, + renderers: new Map(), + supportsFiber: true, + supportsFlight: true, + }; + + return Object.assign(rdtHook, overrides); +}; diff --git a/packages/bippy/tests/create-react-renderer.ts b/packages/bippy/tests/create-react-renderer.ts new file mode 100644 index 00000000..9c7e18e9 --- /dev/null +++ b/packages/bippy/tests/create-react-renderer.ts @@ -0,0 +1,11 @@ +import type { ReactRenderer } from "../src/react-internals/index.js"; + +export const createReactRenderer = (overrides: Record = {}): ReactRenderer => { + const renderer: ReactRenderer = { + bundleType: 1, + rendererPackageName: "test-renderer", + version: "19.0.0", + }; + + return Object.assign(renderer, overrides); +}; diff --git a/packages/bippy/tests/did-fiber-commit.test.tsx b/packages/bippy/tests/did-fiber-commit.test.tsx index 0d17d828..55313de4 100644 --- a/packages/bippy/tests/did-fiber-commit.test.tsx +++ b/packages/bippy/tests/did-fiber-commit.test.tsx @@ -1,10 +1,11 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it } from "vite-plus/test"; import React from "react"; -import { didFiberCommit, Fiber, instrument } from "../src/index.js"; +import { didFiberCommit, instrument, type Fiber } from "../src/index.js"; import { render } from "@testing-library/react"; +import { requireFiber } from "./require-fiber.js"; const Example = () => { return
Hello
; @@ -27,7 +28,9 @@ it("should return true for a fiber that has committed", () => { }); render(); expect(maybeRenderedFiber).not.toBeNull(); - expect(didFiberCommit(maybeRenderedFiber as unknown as Fiber)).toBe(true); + expect(didFiberCommit(requireFiber(maybeRenderedFiber, "React DOM did not render a Fiber"))).toBe( + true, + ); }); it("should return false for a fiber that hasn't committed", () => { @@ -39,5 +42,7 @@ it("should return false for a fiber that hasn't committed", () => { }); render(); expect(maybeRenderedFiber).not.toBeNull(); - expect(didFiberCommit(maybeRenderedFiber as unknown as Fiber)).toBe(false); + expect(didFiberCommit(requireFiber(maybeRenderedFiber, "React DOM did not render a Fiber"))).toBe( + false, + ); }); diff --git a/packages/bippy/tests/did-fiber-render.test.tsx b/packages/bippy/tests/did-fiber-render.test.tsx index cf6bf3e5..46a21420 100644 --- a/packages/bippy/tests/did-fiber-render.test.tsx +++ b/packages/bippy/tests/did-fiber-render.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it } from "vite-plus/test"; import React from "react"; @@ -6,6 +6,7 @@ import React from "react"; import { didFiberRender, instrument } from "../src/index.js"; import type { Fiber, WorkTag } from "../src/index.js"; import { ReactFiberFlags } from "../src/react-internals/index.js"; +import { createFiber } from "./create-fiber.js"; import { latestReactWorkTags } from "./react-work-tags.js"; import { render } from "@testing-library/react"; @@ -35,20 +36,7 @@ it("should return true for a fiber that has rendered", () => { }); const createMockFiber = (tag: WorkTag, flags: number | undefined, effectTag?: number): Fiber => - ({ - alternate: null, - child: null, - effectTag, - flags, - memoizedProps: {}, - memoizedState: null, - pendingProps: {}, - return: null, - sibling: null, - stateNode: null, - tag, - type: () => null, - }) as unknown as Fiber; + createFiber({ effectTag, flags, tag, type: () => null }); it("should check the PerformedWork flag for every composite tag", () => { expect( diff --git a/packages/bippy/tests/fiber-id.test.ts b/packages/bippy/tests/fiber-id.test.ts index f364d35d..7af3b88d 100644 --- a/packages/bippy/tests/fiber-id.test.ts +++ b/packages/bippy/tests/fiber-id.test.ts @@ -1,22 +1,13 @@ import { expect, it } from "vite-plus/test"; import { getFiberId, setFiberId } from "../src/index.js"; import type { Fiber } from "../src/react-internals/index.js"; -import { latestReactWorkTags } from "./react-work-tags.js"; +import { createFiber } from "./create-fiber.js"; -const createMockFiber = (alternate: Fiber | null = null): Fiber => - ({ - alternate, - child: null, - flags: 0, - return: null, - sibling: null, - stateNode: null, - tag: latestReactWorkTags.FunctionComponent, - type: null, - }) as unknown as Fiber; +const createFiberWithAlternate = (alternate: Fiber | null = null): Fiber => + createFiber({ alternate }); it("should assign a stable auto-incremented id", () => { - const fiber = createMockFiber(); + const fiber = createFiberWithAlternate(); setFiberId(fiber); const assignedId = getFiberId(fiber); expect(assignedId).toBeTypeOf("number"); @@ -24,22 +15,22 @@ it("should assign a stable auto-incremented id", () => { }); it("should honor an explicitly assigned id", () => { - const fiber = createMockFiber(); + const fiber = createFiberWithAlternate(); setFiberId(fiber, 12_345); expect(getFiberId(fiber)).toBe(12_345); }); it("should advance generated ids past explicitly assigned ids", () => { - const explicitFiber = createMockFiber(); - const generatedFiber = createMockFiber(); + const explicitFiber = createFiberWithAlternate(); + const generatedFiber = createFiberWithAlternate(); setFiberId(explicitFiber, 1_000_000_000); setFiberId(generatedFiber); expect(getFiberId(generatedFiber)).toBeGreaterThan(1_000_000_000); }); it("should reuse the id of the alternate fiber", () => { - const currentFiber = createMockFiber(); + const currentFiber = createFiberWithAlternate(); setFiberId(currentFiber, 0); - const alternateFiber = createMockFiber(currentFiber); + const alternateFiber = createFiberWithAlternate(currentFiber); expect(getFiberId(alternateFiber)).toBe(0); }); diff --git a/packages/bippy/tests/get-display-name-from-source.test.ts b/packages/bippy/tests/get-display-name-from-source.test.ts index 134eaca6..7bcc9678 100644 --- a/packages/bippy/tests/get-display-name-from-source.test.ts +++ b/packages/bippy/tests/get-display-name-from-source.test.ts @@ -2,6 +2,7 @@ import { encode } from "@jridgewell/sourcemap-codec"; import { describe, expect, it } from "vite-plus/test"; import type { Fiber } from "../src/react-internals/index.js"; import { getDisplayNameFromSource } from "../src/source/get-display-name-from-source.js"; +import { createFiber } from "./create-fiber.js"; import { latestReactWorkTags } from "./react-work-tags.js"; import { sourceFetch as failingFetchFn } from "./source-fetch.js"; @@ -18,14 +19,7 @@ const createThrowingComponent = (componentName: string): (() => null) => { return component; }; -const createFakeFiber = (tag: number, type: unknown): Fiber => - ({ - tag, - type, - return: null, - child: null, - sibling: null, - }) as unknown as Fiber; +const createFakeFiber = (tag: number, type: unknown): Fiber => createFiber({ tag, type }); interface FixedPointMapOptions { mappedName?: string; diff --git a/packages/bippy/tests/get-display-name.test.tsx b/packages/bippy/tests/get-display-name.test.tsx index 28722b37..c603f88a 100644 --- a/packages/bippy/tests/get-display-name.test.tsx +++ b/packages/bippy/tests/get-display-name.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it } from "vite-plus/test"; import React, { forwardRef, memo, Component } from "react"; diff --git a/packages/bippy/tests/get-fiber-from-host-instance-no-hook.test.ts b/packages/bippy/tests/get-fiber-from-host-instance-no-hook.test.ts index 911e71cc..f6fdb0bc 100644 --- a/packages/bippy/tests/get-fiber-from-host-instance-no-hook.test.ts +++ b/packages/bippy/tests/get-fiber-from-host-instance-no-hook.test.ts @@ -1,4 +1,4 @@ -// intentionally avoids importing ../index.js so no rdt hook is installed +// HACK: Avoid importing index so the missing-hook path remains observable. import { expect, it } from "vite-plus/test"; import { getFiberFromHostInstance } from "../src/core.js"; diff --git a/packages/bippy/tests/get-fiber-from-host-instance.test.tsx b/packages/bippy/tests/get-fiber-from-host-instance.test.tsx index b4cc49ec..12936c48 100644 --- a/packages/bippy/tests/get-fiber-from-host-instance.test.tsx +++ b/packages/bippy/tests/get-fiber-from-host-instance.test.tsx @@ -1,35 +1,17 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import * as ReactThreeTestRenderer from "@react-three/test-renderer"; import { render, screen } from "@testing-library/react"; import React from "react"; import { expect, it } from "vite-plus/test"; import { getFiberFromHostInstance, getRDTHook, instrument, traverseFiber } from "../src/index.js"; -import type { Fiber, FiberRoot, ReactRenderer, WorkTag } from "../src/react-internals/index.js"; +import type { Fiber, FiberRoot } from "../src/react-internals/index.js"; +import { createFiber } from "./create-fiber.js"; +import { createReactRenderer } from "./create-react-renderer.js"; import { latestReactWorkTags } from "./react-work-tags.js"; -interface MockHostFiber { - child: MockHostFiber | null; - flags: number; - memoizedState?: Record; - pendingProps: Record; - return: MockHostFiber | null; - sibling: MockHostFiber | null; - stateNode: unknown; - tag: WorkTag; - type: string; -} - -const createMockHostFiber = (stateNode: unknown, type = "RCTView"): MockHostFiber => ({ - child: null, - flags: 0, - pendingProps: {}, - return: null, - sibling: null, - stateNode, - tag: latestReactWorkTags.HostComponent, - type, -}); +const createMockHostFiber = (stateNode: unknown, type = "RCTView"): Fiber => + createFiber({ stateNode, tag: latestReactWorkTags.HostComponent, type }); it("should return the fiber from the host instance", () => { render(
HostInstance
); @@ -77,15 +59,15 @@ it("should resolve Fabric public instances from canonical host state", () => { const rootFiber = createMockHostFiber(null, "root"); rootFiber.tag = latestReactWorkTags.HostRoot; rootFiber.child = hostFiber; - rootFiber.memoizedState = { element: {} }; + rootFiber.memoizedState = { element: {}, memoizedState: null, next: null }; hostFiber.return = rootFiber; const fiberRoot = { current: rootFiber }; - getRDTHook().onCommitFiberRoot(999, fiberRoot as unknown as FiberRoot, undefined, false); + getRDTHook().onCommitFiberRoot(999, fiberRoot, undefined, false); try { expect(getFiberFromHostInstance(publicInstance)).toBe(hostFiber); } finally { - rootFiber.memoizedState = { element: null }; - getRDTHook().onCommitFiberRoot(999, fiberRoot as unknown as FiberRoot, undefined, false); + rootFiber.memoizedState = { element: null, memoizedState: null, next: null }; + getRDTHook().onCommitFiberRoot(999, fiberRoot, undefined, false); unsubscribe(); } }); @@ -96,24 +78,24 @@ it("should resolve Paper native tags from host state", () => { const rootFiber = createMockHostFiber(null, "root"); rootFiber.tag = latestReactWorkTags.HostRoot; rootFiber.child = hostFiber; - rootFiber.memoizedState = { element: {} }; + rootFiber.memoizedState = { element: {}, memoizedState: null, next: null }; hostFiber.return = rootFiber; const fiberRoot = { current: rootFiber }; - getRDTHook().onCommitFiberRoot(999, fiberRoot as unknown as FiberRoot, undefined, false); + getRDTHook().onCommitFiberRoot(999, fiberRoot, undefined, false); try { expect(getFiberFromHostInstance(42)).toBe(hostFiber); } finally { - rootFiber.memoizedState = { element: null }; - getRDTHook().onCommitFiberRoot(999, fiberRoot as unknown as FiberRoot, undefined, false); + rootFiber.memoizedState = { element: null, memoizedState: null, next: null }; + getRDTHook().onCommitFiberRoot(999, fiberRoot, undefined, false); unsubscribe(); } }); it("should prefer renderer.findFiberByHostInstance when available", () => { - const mockFiber = { tag: latestReactWorkTags.HostComponent, type: "span" } as unknown as Fiber; + const mockFiber = createFiber({ tag: latestReactWorkTags.HostComponent, type: "span" }); const rdtHook = getRDTHook(); const findFiberByHostInstance = () => mockFiber; - rdtHook.renderers.set(999, { findFiberByHostInstance } as unknown as ReactRenderer); + rdtHook.renderers.set(999, createReactRenderer({ findFiberByHostInstance })); try { expect(getFiberFromHostInstance({})).toBe(mockFiber); } finally { @@ -126,7 +108,7 @@ it("should ignore renderers whose findFiberByHostInstance throws", () => { const findFiberByHostInstance = () => { throw new Error("no fiber"); }; - rdtHook.renderers.set(999, { findFiberByHostInstance } as unknown as ReactRenderer); + rdtHook.renderers.set(999, createReactRenderer({ findFiberByHostInstance })); try { expect(getFiberFromHostInstance({})).toBe(null); } finally { diff --git a/packages/bippy/tests/get-fiber-stack.test.tsx b/packages/bippy/tests/get-fiber-stack.test.tsx index 40aa9482..9d5d19d8 100644 --- a/packages/bippy/tests/get-fiber-stack.test.tsx +++ b/packages/bippy/tests/get-fiber-stack.test.tsx @@ -1,15 +1,19 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; import { expect, it } from "vite-plus/test"; -import { Fiber, getFiberStack, instrument } from "../src/index.js"; +import { getFiberStack, instrument, type Fiber } from "../src/index.js"; + +interface ExampleWithChildrenPropProps { + children: React.ReactNode; +} export const Example = () => { return
Hello
; }; -export const ExampleWithChildrenProp = ({ children }: { children: React.ReactNode }) => { +export const ExampleWithChildrenProp = ({ children }: ExampleWithChildrenPropProps) => { return
{children}
; }; diff --git a/packages/bippy/tests/get-latest-fiber.test.ts b/packages/bippy/tests/get-latest-fiber.test.ts index c506b7db..0c110863 100644 --- a/packages/bippy/tests/get-latest-fiber.test.ts +++ b/packages/bippy/tests/get-latest-fiber.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, it } from "vite-plus/test"; import { _fiberRoots, getLatestFiber } from "../src/index.js"; import type { Fiber } from "../src/react-internals/index.js"; -import { latestReactWorkTags } from "./react-work-tags.js"; +import { createFiber } from "./create-fiber.js"; interface MockFiberOverrides { actualStartTime?: number; @@ -11,21 +11,7 @@ interface MockFiberOverrides { } const createMockFiber = (overrides: MockFiberOverrides = {}): Fiber => - ({ - actualStartTime: 0, - alternate: null, - child: null, - flags: 0, - memoizedProps: {}, - memoizedState: null, - pendingProps: {}, - return: null, - sibling: null, - stateNode: null, - tag: latestReactWorkTags.FunctionComponent, - type: null, - ...overrides, - }) as unknown as Fiber; + createFiber({ actualStartTime: 0, ...overrides }); afterEach(() => { _fiberRoots.clear(); diff --git a/packages/bippy/tests/get-mutated-host-fibers.test.tsx b/packages/bippy/tests/get-mutated-host-fibers.test.tsx index a9a3bd8a..4d5e6dde 100644 --- a/packages/bippy/tests/get-mutated-host-fibers.test.tsx +++ b/packages/bippy/tests/get-mutated-host-fibers.test.tsx @@ -1,10 +1,11 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; import { expect, it } from "vite-plus/test"; import { getMutatedHostFibers, instrument } from "../src/index.js"; import type { Fiber } from "../src/react-internals/index.js"; +import { requireFiber } from "./require-fiber.js"; export const ExampleWithMutation = () => { const [element, setElement] = React.useState(
Hello
); @@ -39,8 +40,12 @@ it("should return all host fibers that have committed and rendered", () => { }, }); render(); - const mutatedHostFibers = getMutatedHostFibers(maybeFiber as unknown as Fiber); - expect(getMutatedHostFibers(maybeFiber as unknown as Fiber)).toHaveLength(1); + const mutatedHostFibers = getMutatedHostFibers( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + ); + expect( + getMutatedHostFibers(requireFiber(maybeFiber, "React DOM did not render a Fiber")), + ).toHaveLength(1); expect(mutatedHostFiber).toBe(mutatedHostFibers[0]); }); @@ -52,5 +57,7 @@ it("should traverse sibling host fibers", () => { }, }); render(); - expect(getMutatedHostFibers(maybeFiber as unknown as Fiber)).toHaveLength(2); + expect( + getMutatedHostFibers(requireFiber(maybeFiber, "React DOM did not render a Fiber")), + ).toHaveLength(2); }); diff --git a/packages/bippy/tests/get-nearest-host-fiber.test.tsx b/packages/bippy/tests/get-nearest-host-fiber.test.tsx index c6f10ec8..97b1c5cc 100644 --- a/packages/bippy/tests/get-nearest-host-fiber.test.tsx +++ b/packages/bippy/tests/get-nearest-host-fiber.test.tsx @@ -1,17 +1,23 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; import { describe, expect, it } from "vite-plus/test"; import { getNearestHostFiber, getNearestHostFibers, instrument } from "../src/index.js"; import type { Fiber } from "../src/index.js"; +import { createFiber } from "./create-fiber.js"; import { latestReactWorkTags } from "./react-work-tags.js"; +import { requireFiber } from "./require-fiber.js"; export const Example = () => { return
Hello
; }; -export const ExampleWithChildrenProp = ({ children }: { children: React.ReactNode }) => { +interface ExampleWithChildrenPropProps { + children: React.ReactNode; +} + +export const ExampleWithChildrenProp = ({ children }: ExampleWithChildrenPropProps) => { return
{children}
; }; @@ -45,10 +51,12 @@ describe("getNearestHostFiber", () => { }, }); render(); - expect(getNearestHostFiber(maybeFiber as unknown as Fiber)).toBe( - (maybeFiber as unknown as Fiber).child, + expect(getNearestHostFiber(requireFiber(maybeFiber, "React DOM did not render a Fiber"))).toBe( + requireFiber(maybeFiber, "React DOM did not render a Fiber").child, + ); + expect(maybeHostFiber).toBe( + getNearestHostFiber(requireFiber(maybeFiber, "React DOM did not render a Fiber")), ); - expect(maybeHostFiber).toBe(getNearestHostFiber(maybeFiber as unknown as Fiber)); }); it("should return null for unmounted fiber", () => { @@ -59,7 +67,9 @@ describe("getNearestHostFiber", () => { }, }); render(); - expect(getNearestHostFiber(maybeFiber as unknown as Fiber)).toBe(null); + expect(getNearestHostFiber(requireFiber(maybeFiber, "React DOM did not render a Fiber"))).toBe( + null, + ); }); }); @@ -81,7 +91,9 @@ describe("getNearestHostFibers", () => { }, }); render(); - expect(getNearestHostFibers(maybeFiber as unknown as Fiber)).toHaveLength(2); + expect( + getNearestHostFibers(requireFiber(maybeFiber, "React DOM did not render a Fiber")), + ).toHaveLength(2); }); it("should return the fiber itself when it is a host fiber", () => { @@ -94,7 +106,9 @@ describe("getNearestHostFibers", () => { }, }); render(); - const hostFibers = getNearestHostFibers(maybeHostFiber as unknown as Fiber); + const hostFibers = getNearestHostFibers( + requireFiber(maybeHostFiber, "React DOM did not render a host Fiber"), + ); expect(hostFibers).toHaveLength(1); expect(hostFibers[0]).toBe(maybeHostFiber); }); @@ -107,38 +121,40 @@ describe("getNearestHostFibers", () => { }, }); render(); - expect(getNearestHostFibers(maybeFiber as unknown as Fiber)).toHaveLength(2); + expect( + getNearestHostFibers(requireFiber(maybeFiber, "React DOM did not render a Fiber")), + ).toHaveLength(2); }); it("should return an empty array for a childless composite fiber", () => { - const childlessCompositeFiber = { + const childlessCompositeFiber = createFiber({ child: null, sibling: null, tag: latestReactWorkTags.FunctionComponent, type: () => null, - } as unknown as Fiber; + }); expect(getNearestHostFibers(childlessCompositeFiber)).toHaveLength(0); }); it("should skip childless composite fibers while traversing", () => { - const hostFiber = { + const hostFiber = createFiber({ child: null, sibling: null, tag: latestReactWorkTags.HostComponent, type: "div", - } as unknown as Fiber; - const childlessCompositeFiber = { + }); + const childlessCompositeFiber = createFiber({ child: null, sibling: hostFiber, tag: latestReactWorkTags.FunctionComponent, type: () => null, - } as unknown as Fiber; - const rootCompositeFiber = { + }); + const rootCompositeFiber = createFiber({ child: childlessCompositeFiber, sibling: null, tag: latestReactWorkTags.FunctionComponent, type: () => null, - } as unknown as Fiber; + }); expect(getNearestHostFibers(rootCompositeFiber)).toEqual([hostFiber]); }); }); diff --git a/packages/bippy/tests/get-source.test.ts b/packages/bippy/tests/get-source.test.ts index b2649a9b..bbc9e2ff 100644 --- a/packages/bippy/tests/get-source.test.ts +++ b/packages/bippy/tests/get-source.test.ts @@ -1,21 +1,14 @@ import { describe, expect, it } from "vite-plus/test"; -import type { Fiber } from "../src/react-internals/index.js"; import { getSource, hasDebugSource, isSourceFile, normalizeFileName, } from "../src/source/get-source.js"; +import { createFiber } from "./create-fiber.js"; -const createFiberWithDebugSource = (debugSource: unknown): Fiber => - ({ - tag: 999, - type: null, - return: null, - child: null, - sibling: null, - _debugSource: debugSource, - }) as unknown as Fiber; +const createFiberWithDebugSource = (debugSource: unknown) => + createFiber({ _debugSource: debugSource, tag: 999 }); const createDebugStackError = (stackLines: string[]): Error => { const error = new Error("react-stack-top-frame"); diff --git a/packages/bippy/tests/get-timings.test.tsx b/packages/bippy/tests/get-timings.test.tsx index d897f83e..4070b11e 100644 --- a/packages/bippy/tests/get-timings.test.tsx +++ b/packages/bippy/tests/get-timings.test.tsx @@ -1,13 +1,14 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; import { expect, it } from "vite-plus/test"; import { getTimings, instrument } from "../src/index.js"; import type { Fiber } from "../src/react-internals/index.js"; +import { requireFiber } from "./require-fiber.js"; const SlowComponent = () => { - for (let i = 0; i < 100; i++) {} // simulate slowdown + for (let iterationIndex = 0; iterationIndex < 100; iterationIndex++) {} return
Hello
; }; @@ -17,8 +18,8 @@ it("should return zero timings when there is no fiber", () => { }); it("should treat children without actualDuration as zero cost", () => { - const childFiber = { actualDuration: undefined, sibling: null } as unknown as Fiber; - const fiber = { actualDuration: 5, child: childFiber } as unknown as Fiber; + const childFiber = { actualDuration: undefined, sibling: null }; + const fiber = { actualDuration: 5, child: childFiber }; expect(getTimings(fiber)).toEqual({ selfTime: 5, totalTime: 5 }); }); @@ -30,7 +31,7 @@ it("should return the timings of the fiber", () => { }, }); render(); - const timings = getTimings(maybeFiber as unknown as Fiber); + const timings = getTimings(requireFiber(maybeFiber, "React DOM did not render a Fiber")); expect(timings.selfTime).toBeGreaterThan(0); expect(timings.totalTime).toBeGreaterThan(0); }); diff --git a/packages/bippy/tests/get-type.test.tsx b/packages/bippy/tests/get-type.test.tsx index 2b91e5b8..df870e3b 100644 --- a/packages/bippy/tests/get-type.test.tsx +++ b/packages/bippy/tests/get-type.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it } from "vite-plus/test"; diff --git a/packages/bippy/tests/has-memo-cache.test.ts b/packages/bippy/tests/has-memo-cache.test.ts index 26bbcec8..37e86d56 100644 --- a/packages/bippy/tests/has-memo-cache.test.ts +++ b/packages/bippy/tests/has-memo-cache.test.ts @@ -1,29 +1,16 @@ import { expect, it } from "vite-plus/test"; import { hasMemoCache } from "../src/index.js"; -import type { Fiber } from "../src/react-internals/index.js"; -import { latestReactWorkTags } from "./react-work-tags.js"; +import { createFiber } from "./create-fiber.js"; -const createMockFiber = (updateQueue: unknown): Fiber => - ({ - alternate: null, - child: null, - flags: 0, - memoizedProps: {}, - memoizedState: null, - pendingProps: {}, - return: null, - sibling: null, - stateNode: null, - tag: latestReactWorkTags.FunctionComponent, - type: null, - updateQueue, - }) as unknown as Fiber; +const createFiberWithUpdateQueue = (updateQueue: unknown) => createFiber({ updateQueue }); it("should return true when the update queue has a memo cache", () => { - expect(hasMemoCache(createMockFiber({ memoCache: { data: [], index: 0 } }))).toBe(true); + expect(hasMemoCache(createFiberWithUpdateQueue({ memoCache: { data: [], index: 0 } }))).toBe( + true, + ); }); it("should return false when there is no memo cache", () => { - expect(hasMemoCache(createMockFiber({}))).toBe(false); - expect(hasMemoCache(createMockFiber(null))).toBe(false); + expect(hasMemoCache(createFiberWithUpdateQueue({}))).toBe(false); + expect(hasMemoCache(createFiberWithUpdateQueue(null))).toBe(false); }); diff --git a/packages/bippy/tests/inspect-hooks-dispatcher-ref.test.ts b/packages/bippy/tests/inspect-hooks-dispatcher-ref.test.ts index 8cdd5355..9c207cda 100644 --- a/packages/bippy/tests/inspect-hooks-dispatcher-ref.test.ts +++ b/packages/bippy/tests/inspect-hooks-dispatcher-ref.test.ts @@ -2,22 +2,14 @@ import { describe, expect, it } from "vite-plus/test"; import type { Fiber } from "../src/react-internals/index.js"; import { _renderers } from "../src/rdt-hook.js"; import { getFiberHooks } from "../src/source/inspect-hooks.js"; -import { latestReactWorkTags } from "./react-work-tags.js"; +import { createFiber } from "./create-fiber.js"; +import { createReactRenderer } from "./create-react-renderer.js"; const createFakeFiber = (type: unknown): Fiber => - ({ - tag: latestReactWorkTags.FunctionComponent, + createFiber({ type, elementType: type, - memoizedState: null, - memoizedProps: {}, - updateQueue: null, - dependencies: null, - ref: null, - child: null, - sibling: null, - return: null, - }) as unknown as Fiber; + }); describe("getFiberHooks dispatcher discovery", () => { it("throws when no react renderer is registered", () => { @@ -29,10 +21,10 @@ describe("getFiberHooks dispatcher discovery", () => { it("supports renderers exposing a dispatcher ref with a current property", () => { const legacyDispatcherRef: { current: unknown } = { current: null }; - const rendererWithoutRef = { currentDispatcherRef: null }; - const legacyRenderer = { currentDispatcherRef: legacyDispatcherRef }; - _renderers.add(rendererWithoutRef as unknown as never); - _renderers.add(legacyRenderer as unknown as never); + const rendererWithoutRef = createReactRenderer({ currentDispatcherRef: null }); + const legacyRenderer = createReactRenderer({ currentDispatcherRef: legacyDispatcherRef }); + _renderers.add(rendererWithoutRef); + _renderers.add(legacyRenderer); try { let capturedState: unknown = null; @@ -51,8 +43,8 @@ describe("getFiberHooks dispatcher discovery", () => { expect(legacyDispatcherRef.current).toBeNull(); expect(hooksTree.length).toBeGreaterThanOrEqual(1); } finally { - _renderers.delete(rendererWithoutRef as unknown as never); - _renderers.delete(legacyRenderer as unknown as never); + _renderers.delete(rendererWithoutRef); + _renderers.delete(legacyRenderer); } }); }); diff --git a/packages/bippy/tests/inspect-hooks-exotic.test.tsx b/packages/bippy/tests/inspect-hooks-exotic.test.tsx index 715e4fe1..dcb200bf 100644 --- a/packages/bippy/tests/inspect-hooks-exotic.test.tsx +++ b/packages/bippy/tests/inspect-hooks-exotic.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. /* eslint-disable unicorn/no-thenable -- Thenables are the behavior under test. */ @@ -6,6 +6,7 @@ import { beforeAll, describe, expect, it } from "vite-plus/test"; import type { Fiber } from "../src/react-internals/index.js"; import { getRDTHook, _renderers } from "../src/index.js"; import { getFiberHooks, type HooksNode } from "../src/source/inspect-hooks.js"; +import { createFiber } from "./create-fiber.js"; import { latestReactWorkTags } from "./react-work-tags.js"; import React from "react"; import { render } from "@testing-library/react"; @@ -42,7 +43,7 @@ const createInspectableFiber = ( shape: FakeFiberShape, dependencyFields: Record = { dependencies: null }, ): Fiber => { - const fiber: Record = { + const fiber = createFiber({ tag: shape.tag ?? latestReactWorkTags.FunctionComponent, type: shape.type, elementType: "elementType" in shape ? shape.elementType : shape.type, @@ -53,9 +54,10 @@ const createInspectableFiber = ( child: null, sibling: null, return: shape.returnFiber ?? null, - }; - Object.assign(fiber, dependencyFields); - return fiber as unknown as Fiber; + ...dependencyFields, + }); + if (!("dependencies" in dependencyFields)) Reflect.deleteProperty(fiber, "dependencies"); + return fiber; }; interface HookChainNode { @@ -81,6 +83,11 @@ const collectValues = (hooksTree: HooksNode[]): unknown[] => { return hookValues; }; +const requireArray = (value: unknown): unknown[] => { + if (!Array.isArray(value)) throw new Error("Dispatcher did not return an array"); + return value; +}; + beforeAll(() => { render(); }); @@ -151,8 +158,7 @@ describe("use() inspection", () => { }); it("rejects context-tagged objects without a current value", () => { - const contextTypeSymbol = (React.createContext("real") as unknown as { $$typeof: symbol }) - .$$typeof; + const contextTypeSymbol = Reflect.get(React.createContext("real"), "$$typeof"); const contextLikeObject = { $$typeof: contextTypeSymbol }; const UseContextLikeComponent = (): null => { React.use(contextLikeObject as unknown as Promise); @@ -359,8 +365,8 @@ describe("dispatcher-only hooks", () => { const MemoCacheComponent = (): null => { const dispatcher = getActiveDispatcher(); capturedCacheData = [ - dispatcher.useMemoCache(2) as unknown[], - dispatcher.useMemoCache(3) as unknown[], + requireArray(dispatcher.useMemoCache(2)), + requireArray(dispatcher.useMemoCache(3)), ]; return null; }; @@ -379,7 +385,7 @@ describe("dispatcher-only hooks", () => { it("returns an empty cache when the update queue has none", () => { let capturedCacheData: unknown[] | null = null; const NoCacheComponent = (): null => { - capturedCacheData = getActiveDispatcher().useMemoCache(4) as unknown[]; + capturedCacheData = requireArray(getActiveDispatcher().useMemoCache(4)); return null; }; const fiber = createInspectableFiber({ type: NoCacheComponent }); diff --git a/packages/bippy/tests/inspect-hooks.test.tsx b/packages/bippy/tests/inspect-hooks.test.tsx index b5782dd9..800fbd08 100644 --- a/packages/bippy/tests/inspect-hooks.test.tsx +++ b/packages/bippy/tests/inspect-hooks.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { describe, expect, it } from "vite-plus/test"; import type { Fiber } from "../src/react-internals/index.js"; diff --git a/packages/bippy/tests/install-hook-non-configurable.test.ts b/packages/bippy/tests/install-hook-non-configurable.test.ts index 719f1b85..588dbd5d 100644 --- a/packages/bippy/tests/install-hook-non-configurable.test.ts +++ b/packages/bippy/tests/install-hook-non-configurable.test.ts @@ -1,8 +1,9 @@ -// intentionally avoids importing ../index.js so this file controls hook installation +// HACK: Avoid importing index so this test controls hook installation. import { expect, it, vi } from "vite-plus/test"; import { ReactBuildType } from "../src/react-internals/index.js"; import { installRDTHook } from "../src/rdt-hook.js"; import type { ReactDevToolsGlobalHook, ReactRenderer } from "../src/react-internals/index.js"; +import { createReactRenderer } from "./create-react-renderer.js"; it("should fall back to patching when the hook property cannot be redefined", () => { const existingInject = vi.fn(() => 42); @@ -30,10 +31,9 @@ it("should fall back to patching when the hook property cannot be redefined", () expect(existingHook.inject).not.toBe(existingInject); expect(onActive).not.toHaveBeenCalled(); - const fakeRenderer = { + const fakeRenderer = createReactRenderer({ bundleType: ReactBuildType.Development, - version: "19.0.0", - } as unknown as ReactRenderer; + }); const rendererId = existingHook.inject(fakeRenderer); expect(rendererId).toBe(42); expect(existingInject).toHaveBeenCalledWith(fakeRenderer); diff --git a/packages/bippy/tests/instrument-unmount.test.tsx b/packages/bippy/tests/instrument-unmount.test.tsx index 9c08de94..6758ee59 100644 --- a/packages/bippy/tests/instrument-unmount.test.tsx +++ b/packages/bippy/tests/instrument-unmount.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; diff --git a/packages/bippy/tests/instrument.test.tsx b/packages/bippy/tests/instrument.test.tsx index a4be3354..cd69b64c 100644 --- a/packages/bippy/tests/instrument.test.tsx +++ b/packages/bippy/tests/instrument.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it, vi } from "vite-plus/test"; import type { FiberRoot, ReactDevToolsGlobalHook } from "../src/react-internals/index.js"; diff --git a/packages/bippy/tests/is-composite-fiber.test.tsx b/packages/bippy/tests/is-composite-fiber.test.tsx index 5930cea9..5f5c8e86 100644 --- a/packages/bippy/tests/is-composite-fiber.test.tsx +++ b/packages/bippy/tests/is-composite-fiber.test.tsx @@ -1,10 +1,12 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; import { expect, it } from "vite-plus/test"; import { instrument, isCompositeFiber } from "../src/index.js"; import type { Fiber } from "../src/react-internals/index.js"; +import { createFiber } from "./create-fiber.js"; +import { requireFiber } from "./require-fiber.js"; export const Example = () => { return
Hello
; @@ -19,22 +21,14 @@ it("should return true for a composite fiber", () => { }); render(); expect(maybeCompositeFiber).not.toBeNull(); - expect(isCompositeFiber(maybeCompositeFiber as unknown as Fiber)).toBe(true); + expect( + isCompositeFiber(requireFiber(maybeCompositeFiber, "React DOM did not render a Fiber")), + ).toBe(true); }); it("should return true for class and forwardRef fiber tags", () => { - const createMockFiber = (tag: number): Fiber => - ({ - child: null, - flags: 0, - return: null, - sibling: null, - stateNode: null, - tag, - type: () => null, - }) as unknown as Fiber; - expect(isCompositeFiber(createMockFiber(1))).toBe(true); - expect(isCompositeFiber(createMockFiber(11))).toBe(true); + expect(isCompositeFiber(createFiber({ tag: 1, type: () => null }))).toBe(true); + expect(isCompositeFiber(createFiber({ tag: 11, type: () => null }))).toBe(true); }); it("should return false for a host fiber", () => { @@ -46,5 +40,7 @@ it("should return false for a host fiber", () => { }); render(
Hello
); expect(maybeCompositeFiber).not.toBeNull(); - expect(isCompositeFiber(maybeCompositeFiber as unknown as Fiber)).toBe(false); + expect( + isCompositeFiber(requireFiber(maybeCompositeFiber, "React DOM did not render a Fiber")), + ).toBe(false); }); diff --git a/packages/bippy/tests/is-fiber.test.tsx b/packages/bippy/tests/is-fiber.test.tsx index b08c9a5a..5897af38 100644 --- a/packages/bippy/tests/is-fiber.test.tsx +++ b/packages/bippy/tests/is-fiber.test.tsx @@ -1,26 +1,26 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; import { expect, it } from "vite-plus/test"; import { isHostFiber, getFiberFromHostInstance, isFiber } from "../src/index.js"; -import type { Fiber } from "../src/react-internals/index.js"; +import { requireFiber } from "./require-fiber.js"; export const Example = () => { return
Hello
; }; -it("should return true for a a fiber", () => { +it("should return true for a fiber", () => { const { container } = render(
Hello
); const hostFiber = getFiberFromHostInstance(container.firstChild); - expect(isFiber(hostFiber as unknown as Fiber)).toBe(true); + expect(isFiber(hostFiber)).toBe(true); }); it("should return true for a composite fiber", () => { const { container } = render(); const hostFiber = getFiberFromHostInstance(container.firstChild); - expect(isHostFiber(hostFiber as unknown as Fiber)).toBe(true); + expect(isHostFiber(requireFiber(hostFiber, "React DOM did not render a host Fiber"))).toBe(true); }); it("should return false for non-object types", () => { diff --git a/packages/bippy/tests/is-host-fiber.test.tsx b/packages/bippy/tests/is-host-fiber.test.tsx index e3b9a59d..7b43723c 100644 --- a/packages/bippy/tests/is-host-fiber.test.tsx +++ b/packages/bippy/tests/is-host-fiber.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; diff --git a/packages/bippy/tests/is-valid-element.test.tsx b/packages/bippy/tests/is-valid-element.test.tsx index d11f90b4..c9f3a106 100644 --- a/packages/bippy/tests/is-valid-element.test.tsx +++ b/packages/bippy/tests/is-valid-element.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import React from "react"; import { expect, it } from "vite-plus/test"; diff --git a/packages/bippy/tests/is-valid-fiber.test.tsx b/packages/bippy/tests/is-valid-fiber.test.tsx index 58cc96ea..b2298182 100644 --- a/packages/bippy/tests/is-valid-fiber.test.tsx +++ b/packages/bippy/tests/is-valid-fiber.test.tsx @@ -1,9 +1,10 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import React from "react"; import { expect, it } from "vite-plus/test"; import { instrument, isValidFiber } from "../src/index.js"; import type { Fiber } from "../src/react-internals/index.js"; +import { requireFiber } from "./require-fiber.js"; import { render } from "@testing-library/react"; export const Example = () => { @@ -18,7 +19,7 @@ it("should return true for a valid fiber", () => { }, }); render(); - expect(isValidFiber(maybeFiber as unknown as Fiber)).toBe(true); + expect(isValidFiber(requireFiber(maybeFiber, "React DOM did not render a Fiber"))).toBe(true); }); it("should return false for a non-fiber", () => { diff --git a/packages/bippy/tests/no-hook-init.test.tsx b/packages/bippy/tests/no-hook-init.test.tsx index e3af0843..5ee7b997 100644 --- a/packages/bippy/tests/no-hook-init.test.tsx +++ b/packages/bippy/tests/no-hook-init.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React from "react"; @@ -8,7 +8,7 @@ import { isInstrumentationActive, isRealReactDevtools, } from "../src/index.js"; -import type { ReactDevToolsGlobalHook } from "../src/react-internals/index.js"; +import { createRDTHook } from "./create-rdt-hook.js"; import { latestReactWorkTags } from "./react-work-tags.js"; const Example = () => { @@ -20,14 +20,11 @@ it("isRealReactDevtools should return false when passed null", () => { }); it("isRealReactDevtools should detect devtools when hook has getFiberRoots", () => { - const mockHookWithDevtools = { + const mockHookWithDevtools = createRDTHook({ getFiberRoots: () => new Set(), - renderers: new Map(), - } as unknown as ReactDevToolsGlobalHook; + }); - const mockHookWithoutDevtools = { - renderers: new Map(), - } as unknown as ReactDevToolsGlobalHook; + const mockHookWithoutDevtools = createRDTHook(); expect(isRealReactDevtools(mockHookWithDevtools)).toBe(true); expect(isRealReactDevtools(mockHookWithoutDevtools)).toBe(false); diff --git a/packages/bippy/tests/override-context-without-props.test.ts b/packages/bippy/tests/override-context-without-props.test.ts index c865d7a2..094b0c81 100644 --- a/packages/bippy/tests/override-context-without-props.test.ts +++ b/packages/bippy/tests/override-context-without-props.test.ts @@ -1,23 +1,21 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it } from "vite-plus/test"; import { overrideContext } from "../src/index.js"; -import type { - Fiber, - ReactDevToolsGlobalHook, - ReactRenderer, -} from "../src/react-internals/index.js"; +import { createFiber } from "./create-fiber.js"; +import { createRDTHook } from "./create-rdt-hook.js"; +import { createReactRenderer } from "./create-react-renderer.js"; it("should do nothing when no renderer exposes overrideProps", () => { - globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { + globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createRDTHook({ _instrumentationSource: "test", - renderers: new Map([[1, {} as unknown as ReactRenderer]]), - } as unknown as ReactDevToolsGlobalHook; + renderers: new Map([[1, createReactRenderer()]]), + }); const contextType = { displayName: "TestContext" }; - const providerFiber = { + const providerFiber = createFiber({ alternate: null, return: null, type: contextType, - } as unknown as Fiber; + }); expect(() => overrideContext(providerFiber, contextType, { theme: "dark" })).not.toThrow(); }); diff --git a/packages/bippy/tests/override-methods.test.ts b/packages/bippy/tests/override-methods.test.ts index 96daa8a7..a3d06737 100644 --- a/packages/bippy/tests/override-methods.test.ts +++ b/packages/bippy/tests/override-methods.test.ts @@ -1,13 +1,11 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it, vi } from "vite-plus/test"; import { overrideContext, overrideHookState, overrideProps } from "../src/index.js"; -import type { - Fiber, - ReactDevToolsGlobalHook, - ReactRenderer, -} from "../src/react-internals/index.js"; -import { latestReactWorkTags } from "./react-work-tags.js"; +import type { Fiber } from "../src/react-internals/index.js"; +import { createFiber } from "./create-fiber.js"; +import { createRDTHook } from "./create-rdt-hook.js"; +import { createReactRenderer } from "./create-react-renderer.js"; interface MockFiberOverrides { alternate?: Fiber | null; @@ -17,33 +15,20 @@ interface MockFiberOverrides { } const createMockFiber = (overrides: MockFiberOverrides = {}): Fiber => - ({ - alternate: null, - child: null, - flags: 0, - memoizedProps: {}, - memoizedState: null, - pendingProps: {}, - return: null, - sibling: null, - stateNode: null, - tag: latestReactWorkTags.FunctionComponent, - type: () => null, - ...overrides, - }) as unknown as Fiber; + createFiber({ type: () => null, ...overrides }); const firstOverrideProps = vi.fn(); const firstOverrideHookState = vi.fn(); const secondOverrideProps = vi.fn(); const secondOverrideHookState = vi.fn(); -const firstRenderer = { +const firstRenderer = createReactRenderer({ overrideHookState: firstOverrideHookState, overrideProps: firstOverrideProps, -} as unknown as ReactRenderer; -const secondRenderer = { +}); +const secondRenderer = createReactRenderer({ overrideHookState: secondOverrideHookState, overrideProps: secondOverrideProps, -} as unknown as ReactRenderer; +}); it("should no-op when no rdt hook exists", () => { delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__; @@ -52,23 +37,23 @@ it("should no-op when no rdt hook exists", () => { }); it("should no-op when the hook has no renderers", () => { - globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { + globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createRDTHook({ _instrumentationSource: "test", - } as unknown as ReactDevToolsGlobalHook; + }); expect(() => overrideProps(createMockFiber(), { count: 1 })).not.toThrow(); expect(firstOverrideProps).not.toHaveBeenCalled(); }); it("should chain override methods from every renderer", () => { - const rendererWithoutOverrides = {} as unknown as ReactRenderer; - globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { + const rendererWithoutOverrides = createReactRenderer(); + globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createRDTHook({ _instrumentationSource: "test", renderers: new Map([ [1, rendererWithoutOverrides], [2, firstRenderer], [3, secondRenderer], ]), - } as unknown as ReactDevToolsGlobalHook; + }); const fiber = createMockFiber(); overrideProps(fiber, { count: 1, nested: { value: 2 } }); diff --git a/packages/bippy/tests/override-renderer-ownership.test.ts b/packages/bippy/tests/override-renderer-ownership.test.ts index 115b5ff5..cc82c748 100644 --- a/packages/bippy/tests/override-renderer-ownership.test.ts +++ b/packages/bippy/tests/override-renderer-ownership.test.ts @@ -1,14 +1,11 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { expect, it, vi } from "vite-plus/test"; import { overrideHookState, overrideProps } from "../src/index.js"; -import type { - Fiber, - FiberRoot, - ReactDevToolsGlobalHook, - ReactRenderer, -} from "../src/react-internals/index.js"; -import { latestReactWorkTags } from "./react-work-tags.js"; +import type { Fiber, FiberRoot } from "../src/react-internals/index.js"; +import { createFiber } from "./create-fiber.js"; +import { createRDTHook } from "./create-rdt-hook.js"; +import { createReactRenderer } from "./create-react-renderer.js"; interface MockFiberOverrides { memoizedState?: unknown; @@ -17,33 +14,20 @@ interface MockFiberOverrides { } const createMockFiber = (overrides: MockFiberOverrides = {}): Fiber => - ({ - alternate: null, - child: null, - flags: 0, - memoizedProps: {}, - memoizedState: null, - pendingProps: {}, - return: null, - sibling: null, - stateNode: null, - tag: latestReactWorkTags.FunctionComponent, - type: () => null, - ...overrides, - }) as unknown as Fiber; + createFiber({ type: () => null, ...overrides }); const firstOverrideProps = vi.fn(); const secondOverrideProps = vi.fn(); -const firstRenderer = { overrideProps: firstOverrideProps } as unknown as ReactRenderer; -const secondRenderer = { overrideProps: secondOverrideProps } as unknown as ReactRenderer; +const firstRenderer = createReactRenderer({ overrideProps: firstOverrideProps }); +const secondRenderer = createReactRenderer({ overrideProps: secondOverrideProps }); -const rdtHook = { +const rdtHook = createRDTHook({ _instrumentationSource: "test", renderers: new Map([ [1, firstRenderer], [2, secondRenderer], ]), -} as unknown as ReactDevToolsGlobalHook; +}); globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = rdtHook; it("should fan out to every renderer when the fiber's root owner is unknown", () => { diff --git a/packages/bippy/tests/owner-stack.test.ts b/packages/bippy/tests/owner-stack.test.ts index 5cf0bdd4..b43d2c50 100644 --- a/packages/bippy/tests/owner-stack.test.ts +++ b/packages/bippy/tests/owner-stack.test.ts @@ -10,18 +10,13 @@ import { getParentStack, hasDebugStack, } from "../src/source/owner-stack.js"; +import { createFiber } from "./create-fiber.js"; +import { createReactRenderer } from "./create-react-renderer.js"; import { latestReactWorkTags } from "./react-work-tags.js"; import { sourceFetch as noopFetchFn } from "./source-fetch.js"; const createFakeFiber = (overrides: Record): Fiber => - ({ - tag: 999, - type: null, - child: null, - sibling: null, - return: null, - ...overrides, - }) as unknown as Fiber; + createFiber({ tag: 999, ...overrides }); const createDebugStackError = (stackLines: string[]): Error => { const error = new Error("react-stack-top-frame"); @@ -333,10 +328,10 @@ describe("describeFiber native component frames", () => { it("uses a dispatcher ref with a current property when available", () => { const legacyDispatcherRef = { current: { placeholder: true } }; - const rendererWithoutRef = { currentDispatcherRef: null }; - const legacyRenderer = { currentDispatcherRef: legacyDispatcherRef }; - _renderers.add(rendererWithoutRef as unknown as never); - _renderers.add(legacyRenderer as unknown as never); + const rendererWithoutRef = createReactRenderer({ currentDispatcherRef: null }); + const legacyRenderer = createReactRenderer({ currentDispatcherRef: legacyDispatcherRef }); + _renderers.add(rendererWithoutRef); + _renderers.add(legacyRenderer); try { const LegacyDispatcherComponent = (): null => { throw new Error("intentional"); @@ -351,8 +346,8 @@ describe("describeFiber native component frames", () => { expect(frame).toContain("LegacyDispatcherComponent"); expect(legacyDispatcherRef.current).toEqual({ placeholder: true }); } finally { - _renderers.delete(rendererWithoutRef as unknown as never); - _renderers.delete(legacyRenderer as unknown as never); + _renderers.delete(rendererWithoutRef); + _renderers.delete(legacyRenderer); } }); @@ -361,12 +356,12 @@ describe("describeFiber native component frames", () => { const modernValue = { renderer: "modern" }; const legacyDispatcherRef = { current: legacyValue }; const modernDispatcherRef = { H: modernValue }; - const legacyRenderer = { currentDispatcherRef: legacyDispatcherRef }; - const modernRenderer = { currentDispatcherRef: modernDispatcherRef }; + const legacyRenderer = createReactRenderer({ currentDispatcherRef: legacyDispatcherRef }); + const modernRenderer = createReactRenderer({ currentDispatcherRef: modernDispatcherRef }); let observedLegacyDispatcher: unknown; let observedModernDispatcher: unknown; - _renderers.add(legacyRenderer as unknown as never); - _renderers.add(modernRenderer as unknown as never); + _renderers.add(legacyRenderer); + _renderers.add(modernRenderer); try { const MixedRendererComponent = (): null => { observedLegacyDispatcher = legacyDispatcherRef.current; @@ -385,8 +380,8 @@ describe("describeFiber native component frames", () => { expect(legacyDispatcherRef.current).toBe(legacyValue); expect(modernDispatcherRef.H).toBe(modernValue); } finally { - _renderers.delete(legacyRenderer as unknown as never); - _renderers.delete(modernRenderer as unknown as never); + _renderers.delete(legacyRenderer); + _renderers.delete(modernRenderer); } }); }); diff --git a/packages/bippy/tests/parse-hook-names.test.tsx b/packages/bippy/tests/parse-hook-names.test.tsx index 832c6aed..99720820 100644 --- a/packages/bippy/tests/parse-hook-names.test.tsx +++ b/packages/bippy/tests/parse-hook-names.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { encode } from "@jridgewell/sourcemap-codec"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; diff --git a/packages/bippy/tests/portal.test.tsx b/packages/bippy/tests/portal.test.tsx index 5f9764ab..c4334e5e 100644 --- a/packages/bippy/tests/portal.test.tsx +++ b/packages/bippy/tests/portal.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { describe, expect, it } from "vite-plus/test"; import type { Fiber } from "../src/react-internals/index.js"; @@ -19,7 +19,11 @@ const PortalChild = () => { return portal content; }; -const PortalExample = ({ container }: { container: HTMLElement }) => { +interface PortalExampleProps { + container: HTMLElement; +} + +const PortalExample = ({ container }: PortalExampleProps) => { return (

main tree

diff --git a/packages/bippy/tests/post-react-devtools.test.tsx b/packages/bippy/tests/post-react-devtools.test.tsx index afc1b204..a7eed7b3 100644 --- a/packages/bippy/tests/post-react-devtools.test.tsx +++ b/packages/bippy/tests/post-react-devtools.test.tsx @@ -1,4 +1,4 @@ -// import bippy, then react devtools +// HACK: Import Bippy before React DevTools to exercise hook replacement. /* eslint-disable @typescript-eslint/no-unsafe-call */ import { expect, it, vi } from "vite-plus/test"; diff --git a/packages/bippy/tests/post-react.test.tsx b/packages/bippy/tests/post-react.test.tsx index ef64109f..2975755e 100644 --- a/packages/bippy/tests/post-react.test.tsx +++ b/packages/bippy/tests/post-react.test.tsx @@ -1,4 +1,4 @@ -// import bippy, then react +// HACK: Import Bippy before React to exercise early hook installation. import { instrument } from "../src/index.js"; import { expect, it, vi } from "vite-plus/test"; diff --git a/packages/bippy/tests/pre-react-devtools.test.tsx b/packages/bippy/tests/pre-react-devtools.test.tsx index f464f602..5e6ef8f5 100644 --- a/packages/bippy/tests/pre-react-devtools.test.tsx +++ b/packages/bippy/tests/pre-react-devtools.test.tsx @@ -1,4 +1,4 @@ -// import react devtools, then bippy +// HACK: Import React DevTools before Bippy to exercise hook patching. /* eslint-disable @typescript-eslint/no-unsafe-call */ // @ts-expect-error - react-devtools-inline types not available diff --git a/packages/bippy/tests/pre-react.test.tsx b/packages/bippy/tests/pre-react.test.tsx index fccfd57b..337ae4e5 100644 --- a/packages/bippy/tests/pre-react.test.tsx +++ b/packages/bippy/tests/pre-react.test.tsx @@ -1,4 +1,4 @@ -// import react, then bippy +// HACK: Import React before Bippy to exercise late hook installation. import React from "react"; import { expect, it, vi } from "vite-plus/test"; diff --git a/packages/bippy/tests/rdt-hook-install.test.ts b/packages/bippy/tests/rdt-hook-install.test.ts index a36af672..90658bf5 100644 --- a/packages/bippy/tests/rdt-hook-install.test.ts +++ b/packages/bippy/tests/rdt-hook-install.test.ts @@ -1,4 +1,4 @@ -// intentionally avoids importing ../index.js so this file controls hook installation +// HACK: Avoid importing index so this test controls hook installation. import { expect, it, vi } from "vite-plus/test"; import { ReactBuildType } from "../src/react-internals/index.js"; import { @@ -9,12 +9,10 @@ import { patchRDTHook, } from "../src/rdt-hook.js"; import type { ReactDevToolsGlobalHook, ReactRenderer } from "../src/react-internals/index.js"; +import { createReactRenderer } from "./create-react-renderer.js"; const createFakeRenderer = (): ReactRenderer => - ({ - bundleType: ReactBuildType.Development, - version: "19.0.0", - }) as unknown as ReactRenderer; + createReactRenderer({ bundleType: ReactBuildType.Development }); it("patchRDTHook should return early when no hook exists", () => { expect(hasRDTHook()).toBe(false); diff --git a/packages/bippy/tests/rdt-hook-setter-empty.test.ts b/packages/bippy/tests/rdt-hook-setter-empty.test.ts index ef5d8f4e..3e18d3d0 100644 --- a/packages/bippy/tests/rdt-hook-setter-empty.test.ts +++ b/packages/bippy/tests/rdt-hook-setter-empty.test.ts @@ -1,4 +1,4 @@ -// intentionally avoids importing ../index.js so this file controls hook installation +// HACK: Avoid importing index so this test controls hook installation. import { expect, it } from "vite-plus/test"; import { getRDTHook } from "../src/rdt-hook.js"; import type { ReactDevToolsGlobalHook, ReactRenderer } from "../src/react-internals/index.js"; diff --git a/packages/bippy/tests/require-fiber.ts b/packages/bippy/tests/require-fiber.ts new file mode 100644 index 00000000..a23eb45d --- /dev/null +++ b/packages/bippy/tests/require-fiber.ts @@ -0,0 +1,6 @@ +import type { Fiber } from "../src/react-internals/index.js"; + +export const requireFiber = (value: Fiber | null | undefined, message: string): Fiber => { + if (value === null || value === undefined) throw new Error(message); + return value; +}; diff --git a/packages/bippy/tests/source.test.tsx b/packages/bippy/tests/source.test.tsx index e8297082..aa853fa2 100644 --- a/packages/bippy/tests/source.test.tsx +++ b/packages/bippy/tests/source.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { render } from "@testing-library/react"; import React, { useState } from "react"; @@ -9,6 +9,7 @@ import { getSource, getOwnerStack, getParentStack, getSourceMap } from "../src/s import { sourceMapCache } from "../src/source/symbolication.js"; import { normalizeFileName } from "../src/source/get-source.js"; import { extractLocation, parseStack, type StackFrame } from "../src/source/parse-stack.js"; +import { requireFiber } from "./require-fiber.js"; const mockFetch = (): Promise => { return Promise.resolve( @@ -46,7 +47,15 @@ const SimpleComponent = () => { return
Hello
; }; -const ComponentWithProps = ({ message }: { message: string }) => { +interface ComponentWithPropsProps { + message: string; +} + +interface ExampleWithChildProps { + children: React.ReactNode; +} + +const ComponentWithProps = ({ message }: ComponentWithPropsProps) => { return
{message}
; }; @@ -55,7 +64,7 @@ const ComponentWithHooks = () => { return
{count}
; }; -const ExampleWithChild = ({ children }: { children: React.ReactNode }) => { +const ExampleWithChild = ({ children }: ExampleWithChildProps) => { return
{children}
; }; @@ -72,7 +81,9 @@ it("getOwnerStack should return stack for simple component", async () => { }); render(); - const result = await getOwnerStack(capturedFiber as unknown as Fiber); + const result = await getOwnerStack( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + ); expect(result).toHaveLength(1); expect(result[0].functionName).toBe("SimpleComponent"); @@ -88,7 +99,9 @@ it("getOwnerStack should return stack for component with props", async () => { }); render(); - const result = await getOwnerStack(capturedFiber as unknown as Fiber); + const result = await getOwnerStack( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + ); const expectedFrame = getComponentThrowFrame(ComponentWithProps); expect(result).toHaveLength(1); @@ -107,7 +120,9 @@ it("getOwnerStack should return stack for component with hooks", async () => { }); render(); - const result = await getOwnerStack(capturedFiber as unknown as Fiber); + const result = await getOwnerStack( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + ); const expectedFrame = getComponentThrowFrame(ComponentWithHooks); expect(result).toHaveLength(1); @@ -157,7 +172,9 @@ it("getOwnerStack should use debug stacks for fibers created during a render", a }); render(); - const result = await getOwnerStack(capturedFiber as unknown as Fiber); + const result = await getOwnerStack( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + ); expect(result.length).toBeGreaterThanOrEqual(2); expect(result[0].functionName).toBe("ComponentWithProps"); @@ -197,7 +214,11 @@ it("getSource should return the usage site from the fiber's own debug stack", as }); render(); - const result = await getSource(capturedFiber as unknown as Fiber, false, mockFetch); + const result = await getSource( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + false, + mockFetch, + ); expect(result).not.toBeNull(); expect(result?.fileName).toContain("source.test.tsx"); @@ -213,7 +234,11 @@ it("getSource should resolve a simple component without props/hooks via an owned }); render(); - const result = await getSource(capturedFiber as unknown as Fiber, false, mockFetch); + const result = await getSource( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + false, + mockFetch, + ); expect(result).not.toBeNull(); expect(result?.fileName).toContain("source.test.tsx"); @@ -229,7 +254,11 @@ it("getSource should work for component with props", async () => { }); render(); - const result = await getSource(capturedFiber as unknown as Fiber, false, mockFetch); + const result = await getSource( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + false, + mockFetch, + ); expect(result?.fileName).toBeTruthy(); expect(typeof result?.fileName).toBe("string"); @@ -244,7 +273,11 @@ it("getSource should work for component with hooks", async () => { }); render(); - const result = await getSource(capturedFiber as unknown as Fiber, false, mockFetch); + const result = await getSource( + requireFiber(capturedFiber, "React DOM did not render a Fiber"), + false, + mockFetch, + ); expect(result?.fileName).toBeTruthy(); expect(typeof result?.fileName).toBe("string"); diff --git a/packages/bippy/tests/traverse-rendered-fibers.test.ts b/packages/bippy/tests/traverse-rendered-fibers.test.ts index 3453d2a4..4ab76e7c 100644 --- a/packages/bippy/tests/traverse-rendered-fibers.test.ts +++ b/packages/bippy/tests/traverse-rendered-fibers.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vite-plus/test"; -import { traverseRenderedFibers } from "../src/index.js"; +import { traverseRenderedFibers, type RenderPhase } from "../src/index.js"; import type { Fiber, FiberRoot } from "../src/react-internals/index.js"; +import { createFiber } from "./create-fiber.js"; import { latestReactWorkTags } from "./react-work-tags.js"; const PERFORMED_WORK_FLAG = 0b1; @@ -17,23 +18,11 @@ interface MockFiberOverrides { } const createMockFiber = (overrides: MockFiberOverrides = {}): Fiber => - ({ - alternate: null, - child: null, - dependencies: null, + createFiber({ flags: PERFORMED_WORK_FLAG, - memoizedProps: {}, - memoizedState: null, - pendingProps: {}, - ref: null, - return: null, - sibling: null, - stateNode: null, - subtreeFlags: 0, - tag: latestReactWorkTags.FunctionComponent, type: () => null, ...overrides, - }) as unknown as Fiber; + }); // root wrappers use flags 0 so only the fibers under test show up in the spy const createMountedRootFiber = (child: Fiber | null, alternate: Fiber | null = null): Fiber => @@ -48,7 +37,7 @@ const createMountedRootFiber = (child: Fiber | null, alternate: Fiber | null = n const commitUpdate = ( nextFiber: Fiber, prevFiber: Fiber, - onRender: (fiber: Fiber, phase: string) => void, + onRender: (fiber: Fiber, phase: RenderPhase) => void, ): void => { if (!nextFiber.alternate) { nextFiber.alternate = prevFiber; @@ -58,11 +47,11 @@ const commitUpdate = ( const root: FiberRoot = { current: prevRootFiber }; traverseRenderedFibers(root, () => {}); root.current = nextRootFiber; - const onRenderWithoutRootWrapper = (fiber: Fiber, phase: string) => { + const onRenderWithoutRootWrapper = (fiber: Fiber, phase: RenderPhase) => { if (fiber === nextRootFiber || fiber === prevRootFiber) return; onRender(fiber, phase); }; - traverseRenderedFibers(root, onRenderWithoutRootWrapper as never); + traverseRenderedFibers(root, onRenderWithoutRootWrapper); }; describe("mount commits", () => { diff --git a/packages/bippy/tests/traverse.test.tsx b/packages/bippy/tests/traverse.test.tsx index a8045e35..b7274dad 100644 --- a/packages/bippy/tests/traverse.test.tsx +++ b/packages/bippy/tests/traverse.test.tsx @@ -1,4 +1,4 @@ -import "../src/index.js"; // KEEP THIS LINE ON TOP +import "../src/index.js"; // HACK: Bippy must initialize before imports that load React. import { describe, expect, it, vi } from "vite-plus/test"; import type { ContextDependency, Fiber } from "../src/react-internals/index.js"; @@ -10,26 +10,11 @@ import { traverseState, } from "../src/index.js"; import { latestReactWorkTags } from "./react-work-tags.js"; +import { createFiber } from "./create-fiber.js"; +import { requireFiber } from "./require-fiber.js"; import React from "react"; import { render } from "@testing-library/react"; -const createMockFiber = (overrides: Record = {}): Fiber => - ({ - alternate: null, - child: null, - dependencies: null, - flags: 0, - memoizedProps: {}, - memoizedState: null, - pendingProps: {}, - return: null, - sibling: null, - stateNode: null, - tag: latestReactWorkTags.FunctionComponent, - type: null, - ...overrides, - }) as unknown as Fiber; - export const Context1 = React.createContext(0); export const Context2 = React.createContext(0); @@ -37,12 +22,12 @@ export const Example = () => { return
Hello
; }; -export const ComplexComponent = ({ - countProp = 0, -}: { +interface ComplexComponentProps { countProp?: number; extraProp?: unknown; -}) => { +} + +export const ComplexComponent = ({ countProp = 0 }: ComplexComponentProps) => { const countContextValue = React.useContext(Context1); // eslint-disable-next-line @typescript-eslint/no-unused-vars const _extraContextValue = React.useContext(Context2); @@ -67,7 +52,7 @@ describe("traverseProps", () => { }); render(); const selector = vi.fn(); - traverseProps(maybeFiber as unknown as Fiber, selector); + traverseProps(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(selector).toHaveBeenCalledWith("countProp", 0, 0); }); @@ -80,7 +65,7 @@ describe("traverseProps", () => { }); render(); const selector = vi.fn(); - traverseProps(maybeFiber as unknown as Fiber, selector); + traverseProps(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(selector).toBeCalledTimes(2); }); @@ -93,13 +78,13 @@ describe("traverseProps", () => { }); render(); const selector = vi.fn(() => true); - traverseProps(maybeFiber as unknown as Fiber, selector); + traverseProps(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(selector).toBeCalledTimes(1); }); it("should visit props that only exist on the previous fiber", () => { - const fiber = createMockFiber({ - alternate: createMockFiber({ memoizedProps: { removedProp: 2, sharedProp: 1 } }), + const fiber = createFiber({ + alternate: createFiber({ memoizedProps: { removedProp: 2, sharedProp: 1 } }), memoizedProps: { sharedProp: 1 }, }); const selector = vi.fn((propName: string) => propName === "removedProp"); @@ -108,8 +93,8 @@ describe("traverseProps", () => { }); it("should return false when no prop is selected", () => { - const fiber = createMockFiber({ - alternate: createMockFiber({ memoizedProps: { removedProp: 2 } }), + const fiber = createFiber({ + alternate: createFiber({ memoizedProps: { removedProp: 2 } }), memoizedProps: { sharedProp: 1 }, }); const selector = vi.fn(() => false); @@ -118,7 +103,7 @@ describe("traverseProps", () => { }); it("should default previous props when there is no alternate", () => { - const fiber = createMockFiber({ memoizedProps: { onlyProp: 1 } }); + const fiber = createFiber({ memoizedProps: { onlyProp: 1 } }); const selector = vi.fn(); expect(traverseProps(fiber, selector)).toBe(false); expect(selector).toHaveBeenCalledWith("onlyProp", 1, undefined); @@ -141,7 +126,7 @@ describe("traverseState", () => { prev: prevState.memoizedState, }); }); - traverseState(maybeFiber as unknown as Fiber, selector); + traverseState(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(states[0].next).toEqual(1); expect(states[0].prev).toEqual(0); expect(states[1].next).toEqual(0); @@ -157,7 +142,7 @@ describe("traverseState", () => { }); render(); const selector = vi.fn(); - traverseState(maybeFiber as unknown as Fiber, selector); + traverseState(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(selector).toBeCalledTimes(3); }); @@ -170,7 +155,7 @@ describe("traverseState", () => { }); render(); const selector = vi.fn(() => true); - traverseState(maybeFiber as unknown as Fiber, selector); + traverseState(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(selector).toBeCalledTimes(1); }); }); @@ -194,7 +179,7 @@ describe("traverseContexts", () => { const selector = vi.fn((context) => { contexts.push(context); }); - traverseContexts(maybeFiber as unknown as Fiber, selector); + traverseContexts(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(contexts).toHaveLength(2); expect(contexts[0].context).toBe(Context1); expect(contexts[0].memoizedValue).toBe(1); @@ -211,13 +196,13 @@ describe("traverseContexts", () => { }); render(); const selector = vi.fn(() => true); - traverseContexts(maybeFiber as unknown as Fiber, selector); + traverseContexts(requireFiber(maybeFiber, "React DOM did not render a Fiber"), selector); expect(selector).toBeCalledTimes(1); }); it("should return false when the fiber has no dependencies", () => { - const fiber = createMockFiber({ - alternate: createMockFiber({ dependencies: { firstContext: null } }), + const fiber = createFiber({ + alternate: createFiber({ dependencies: { firstContext: null } }), dependencies: null, }); const selector = vi.fn(); @@ -226,8 +211,8 @@ describe("traverseContexts", () => { }); it("should return false when dependencies have no firstContext", () => { - const fiber = createMockFiber({ - alternate: createMockFiber({ dependencies: {} }), + const fiber = createFiber({ + alternate: createFiber({ dependencies: {} }), dependencies: {}, }); const selector = vi.fn(); @@ -236,8 +221,8 @@ describe("traverseContexts", () => { }); it("should keep traversing when only the previous fiber has contexts", () => { - const fiber = createMockFiber({ - alternate: createMockFiber({ + const fiber = createFiber({ + alternate: createFiber({ dependencies: { firstContext: { memoizedValue: 1, next: null } }, }), dependencies: { firstContext: null }, @@ -257,9 +242,12 @@ describe("traverseFiber", () => { }, }); render(); - expect(traverseFiber(maybeFiber as unknown as Fiber, (fiber) => fiber.type === "div")).toBe( - (maybeFiber as unknown as Fiber)?.child, - ); + expect( + traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + (fiber) => fiber.type === "div", + ), + ).toBe(requireFiber(maybeFiber, "React DOM did not render a Fiber")?.child); }); it("should call selector only once per node (descending)", () => { @@ -271,7 +259,10 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn((fiber) => fiber.type === "div"); - const result = traverseFiber(maybeFiber as unknown as Fiber, selector); + const result = traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + ); expect(result).toBeTruthy(); const callCounts = new Map(); selector.mock.calls.forEach(([fiber]) => { @@ -291,7 +282,11 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn((fiber) => fiber.tag === latestReactWorkTags.HostRoot); - const result = traverseFiber(maybeFiber as unknown as Fiber, selector, true); + const result = traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + true, + ); expect(result).toBeTruthy(); const callCounts = new Map(); selector.mock.calls.forEach(([fiber]) => { @@ -311,7 +306,10 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn(async (fiber) => fiber.type === "div"); - const result = await traverseFiber(maybeFiber as unknown as Fiber, selector); + const result = await traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + ); expect(result).toBeTruthy(); const callCounts = new Map(); selector.mock.calls.forEach(([fiber]) => { @@ -331,7 +329,11 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn(async (fiber) => fiber.tag === latestReactWorkTags.HostRoot); - const result = await traverseFiber(maybeFiber as unknown as Fiber, selector, true); + const result = await traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + true, + ); expect(result).toBeTruthy(); const callCounts = new Map(); selector.mock.calls.forEach(([fiber]) => { @@ -351,7 +353,10 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn((fiber) => fiber === maybeFiber); - const result = traverseFiber(maybeFiber as unknown as Fiber, selector); + const result = traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + ); expect(result).toBe(maybeFiber); expect(selector).toBeCalledTimes(1); }); @@ -365,7 +370,11 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn((fiber) => fiber === maybeFiber); - const result = traverseFiber(maybeFiber as unknown as Fiber, selector, true); + const result = traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + true, + ); expect(result).toBe(maybeFiber); expect(selector).toBeCalledTimes(1); }); @@ -379,7 +388,10 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn(async (fiber) => fiber === maybeFiber); - const result = await traverseFiber(maybeFiber as unknown as Fiber, selector); + const result = await traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + ); expect(result).toBe(maybeFiber); expect(selector).toBeCalledTimes(1); }); @@ -393,7 +405,11 @@ describe("traverseFiber", () => { }); render(); const selector = vi.fn(async (fiber) => fiber === maybeFiber); - const result = await traverseFiber(maybeFiber as unknown as Fiber, selector, true); + const result = await traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + selector, + true, + ); expect(result).toBe(maybeFiber); expect(selector).toBeCalledTimes(1); }); @@ -411,7 +427,10 @@ describe("traverseFiber", () => { }, }); render(); - const result = await traverseFiber(maybeFiber as unknown as Fiber, async () => false); + const result = await traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + async () => false, + ); expect(result).toBe(null); }); @@ -423,21 +442,25 @@ describe("traverseFiber", () => { }, }); render(); - const result = await traverseFiber(maybeFiber as unknown as Fiber, async () => false, true); + const result = await traverseFiber( + requireFiber(maybeFiber, "React DOM did not render a Fiber"), + async () => false, + true, + ); expect(result).toBe(null); }); it("should traverse siblings when the first async subtree does not match", async () => { - const targetSibling = createMockFiber(); - const firstChild = createMockFiber({ sibling: targetSibling }); - const rootFiber = createMockFiber({ child: firstChild }); + const targetSibling = createFiber(); + const firstChild = createFiber({ sibling: targetSibling }); + const rootFiber = createFiber({ child: firstChild }); const result = await traverseFiber(rootFiber, async (fiber) => fiber === targetSibling); expect(result).toBe(targetSibling); }); it("should await promises returned after synchronous selector results", async () => { - const targetFiber = createMockFiber(); - const rootFiber = createMockFiber({ child: targetFiber }); + const targetFiber = createFiber(); + const rootFiber = createFiber({ child: targetFiber }); const result = traverseFiber(rootFiber, (fiber) => fiber === rootFiber ? false : Promise.resolve(fiber === targetFiber), ); @@ -445,25 +468,25 @@ describe("traverseFiber", () => { }); it("should return null when no node matches (sync descending)", () => { - const targetSibling = createMockFiber(); - const firstChild = createMockFiber({ sibling: targetSibling }); - const rootFiber = createMockFiber({ child: firstChild }); + const targetSibling = createFiber(); + const firstChild = createFiber({ sibling: targetSibling }); + const rootFiber = createFiber({ child: firstChild }); expect(traverseFiber(rootFiber, () => false)).toBe(null); }); it("should traverse nested siblings with an async selector (descending)", async () => { - const targetSibling = createMockFiber(); - const firstGrandchild = createMockFiber({ sibling: targetSibling }); - const childFiber = createMockFiber({ child: firstGrandchild }); - const rootFiber = createMockFiber({ child: childFiber }); + const targetSibling = createFiber(); + const firstGrandchild = createFiber({ sibling: targetSibling }); + const childFiber = createFiber({ child: firstGrandchild }); + const rootFiber = createFiber({ child: childFiber }); const result = await traverseFiber(rootFiber, async (fiber) => fiber === targetSibling); expect(result).toBe(targetSibling); }); it("should return null with an async selector when ascending finds no match", async () => { - const rootFiber = createMockFiber(); - const parentFiber = createMockFiber({ return: rootFiber }); - const childFiber = createMockFiber({ return: parentFiber }); + const rootFiber = createFiber(); + const parentFiber = createFiber({ return: rootFiber }); + const childFiber = createFiber({ return: parentFiber }); const result = await traverseFiber(childFiber, async () => false, true); expect(result).toBe(null); }); diff --git a/packages/e2e/fixtures/expo-app/src/App.tsx b/packages/e2e/fixtures/expo-app/src/expo-test-app.tsx similarity index 97% rename from packages/e2e/fixtures/expo-app/src/App.tsx rename to packages/e2e/fixtures/expo-app/src/expo-test-app.tsx index 368a6cfc..8b1174b7 100644 --- a/packages/e2e/fixtures/expo-app/src/App.tsx +++ b/packages/e2e/fixtures/expo-app/src/expo-test-app.tsx @@ -76,7 +76,20 @@ const getRuntimeBundleUrl = (): string | null => { return typeof scriptUrl === "string" ? scriptUrl : null; }; -const TestChild = ({ name, count }: { name: string; count: number }) => { +interface TestChildProps { + count: number; + name: string; +} + +interface TestMemoChildProps { + value: string; +} + +interface OverridePropsChildProps { + count: number; +} + +const TestChild = ({ name, count }: TestChildProps) => { return ( @@ -86,7 +99,7 @@ const TestChild = ({ name, count }: { name: string; count: number }) => { ); }; -const TestMemoChild = memo(({ value }: { value: string }) => { +const TestMemoChild = memo(({ value }: TestMemoChildProps) => { return ( {value} @@ -158,7 +171,7 @@ const findFibersByDisplayName = (rootFiber: Fiber, displayNames: string[]) => { return fibersByDisplayName; }; -const OverridePropsChild = ({ count }: { count: number }) => ( +const OverridePropsChild = ({ count }: OverridePropsChildProps) => ( override-props {count} @@ -184,8 +197,7 @@ const OverrideContextChild = () => { ); }; -// memoized so the App-level result-row commits bail out here and never -// re-derive the probes' props/state, which would silently undo the overrides +// HACK: Memoization prevents result-row commits from undoing the probe overrides. const OverrideProbes = memo(() => ( @@ -197,7 +209,7 @@ const OverrideProbes = memo(() => ( )); OverrideProbes.displayName = "OverrideProbes"; -const App = () => { +const ExpoTestApp = () => { const [coreResults, setCoreResults] = useState>({}); const [sourceResults, setSourceResults] = useState>({}); const [skiaResults, setSkiaResults] = useState>({}); @@ -655,4 +667,4 @@ const App = () => { ); }; -export default App; +export default ExpoTestApp; diff --git a/packages/e2e/fixtures/expo-app/src/index.ts b/packages/e2e/fixtures/expo-app/src/index.ts index 724f6fb3..f054fae1 100644 --- a/packages/e2e/fixtures/expo-app/src/index.ts +++ b/packages/e2e/fixtures/expo-app/src/index.ts @@ -1,5 +1,5 @@ import "bippy/install-hook-only"; import { registerRootComponent } from "expo"; -import App from "./App"; +import ExpoTestApp from "./expo-test-app"; -registerRootComponent(App); +registerRootComponent(ExpoTestApp); diff --git a/packages/e2e/fixtures/next-app/app/layout.tsx b/packages/e2e/fixtures/next-app/app/layout.tsx index f4a73605..327e4a5a 100644 --- a/packages/e2e/fixtures/next-app/app/layout.tsx +++ b/packages/e2e/fixtures/next-app/app/layout.tsx @@ -2,7 +2,11 @@ export const metadata = { title: "Bippy E2E - Next.js", }; -const RootLayout = ({ children }: { children: React.ReactNode }) => { +interface RootLayoutProps { + children: React.ReactNode; +} + +const RootLayout = ({ children }: RootLayoutProps) => { return ( {children} diff --git a/packages/e2e/fixtures/next-app/app/test-harness.tsx b/packages/e2e/fixtures/next-app/app/test-harness.tsx index ec71ec1c..6664e8f8 100644 --- a/packages/e2e/fixtures/next-app/app/test-harness.tsx +++ b/packages/e2e/fixtures/next-app/app/test-harness.tsx @@ -23,6 +23,15 @@ declare global { const TestContext = createContext("default-context"); +interface TestChildProps { + count: number; + name: string; +} + +interface MemoChildProps { + value: string; +} + export const TestParent = () => { const [count, setCount] = useState(0); const [showConditional, setShowConditional] = useState(true); @@ -59,7 +68,7 @@ export const TestParent = () => { ); }; -export const TestChild = ({ name, count }: { name: string; count: number }) => { +export const TestChild = ({ name, count }: TestChildProps) => { return (
{name} {count} @@ -67,18 +76,18 @@ export const TestChild = ({ name, count }: { name: string; count: number }) => { ); }; -export function MemoChild({ value }: { value: string }) { +export const MemoChild = ({ value }: MemoChildProps) => { return
{value}
; -} +}; const TestMemoChild = memo(MemoChild); -function ForwardRefChild(_: object, ref: React.ForwardedRef) { +const ForwardRefChild = (_props: object, ref: React.ForwardedRef) => { return (
forward-ref
); -} +}; const TestForwardRefChild = forwardRef(ForwardRefChild); const TestContextConsumer = () => { diff --git a/packages/e2e/fixtures/tanstack-app/src/router.tsx b/packages/e2e/fixtures/tanstack-app/src/router.tsx index 1aaeca34..a7e31932 100644 --- a/packages/e2e/fixtures/tanstack-app/src/router.tsx +++ b/packages/e2e/fixtures/tanstack-app/src/router.tsx @@ -2,11 +2,11 @@ import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; -export function getRouter() { +export const getRouter = () => { return createRouter({ routeTree, }); -} +}; declare module "@tanstack/react-router" { interface Register { diff --git a/packages/e2e/fixtures/tanstack-app/src/routes/__root.tsx b/packages/e2e/fixtures/tanstack-app/src/routes/__root.tsx index 03504fb0..6591b881 100644 --- a/packages/e2e/fixtures/tanstack-app/src/routes/__root.tsx +++ b/packages/e2e/fixtures/tanstack-app/src/routes/__root.tsx @@ -1,26 +1,19 @@ import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router"; import type { ReactNode } from "react"; -export const Route = createRootRoute({ - head: () => ({ - meta: [ - { charSet: "utf-8" }, - { name: "viewport", content: "width=device-width, initial-scale=1" }, - { title: "Bippy E2E - TanStack Start" }, - ], - }), - component: RootComponent, -}); +interface RootDocumentProps { + children: ReactNode; +} -function RootComponent() { +const RootComponent = () => { return ( ); -} +}; -function RootDocument({ children }: Readonly<{ children: ReactNode }>) { +const RootDocument = ({ children }: RootDocumentProps) => { return ( @@ -32,4 +25,15 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) { ); -} +}; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { title: "Bippy E2E - TanStack Start" }, + ], + }), + component: RootComponent, +}); diff --git a/packages/e2e/fixtures/tanstack-app/src/test-harness.tsx b/packages/e2e/fixtures/tanstack-app/src/test-harness.tsx index 17a9ea02..7c8d4cc0 100644 --- a/packages/e2e/fixtures/tanstack-app/src/test-harness.tsx +++ b/packages/e2e/fixtures/tanstack-app/src/test-harness.tsx @@ -22,6 +22,15 @@ declare global { const TestContext = createContext("default-context"); +interface TestChildProps { + count: number; + name: string; +} + +interface MemoChildProps { + value: string; +} + export const TestParent = () => { const [count, setCount] = useState(0); const [showConditional, setShowConditional] = useState(true); @@ -58,7 +67,7 @@ export const TestParent = () => { ); }; -export const TestChild = ({ name, count }: { name: string; count: number }) => { +export const TestChild = ({ name, count }: TestChildProps) => { return (
{name} {count} @@ -66,18 +75,18 @@ export const TestChild = ({ name, count }: { name: string; count: number }) => { ); }; -export function MemoChild({ value }: { value: string }) { +export const MemoChild = ({ value }: MemoChildProps) => { return
{value}
; -} +}; const TestMemoChild = memo(MemoChild); -function ForwardRefChild(_: object, ref: React.ForwardedRef) { +const ForwardRefChild = (_props: object, ref: React.ForwardedRef) => { return (
forward-ref
); -} +}; const TestForwardRefChild = forwardRef(ForwardRefChild); const TestContextConsumer = () => { diff --git a/packages/e2e/fixtures/vite-app/src/test-app.tsx b/packages/e2e/fixtures/vite-app/src/test-app.tsx index 634d522d..06e646e1 100644 --- a/packages/e2e/fixtures/vite-app/src/test-app.tsx +++ b/packages/e2e/fixtures/vite-app/src/test-app.tsx @@ -12,6 +12,15 @@ import React, { const TestContext = createContext("default-context"); +interface TestChildProps { + count: number; + name: string; +} + +interface MemoChildProps { + value: string; +} + export const TestParent = () => { const [count, setCount] = useState(0); const [showConditional, setShowConditional] = useState(true); @@ -48,7 +57,7 @@ export const TestParent = () => { ); }; -export const TestChild = ({ name, count }: { name: string; count: number }) => { +export const TestChild = ({ name, count }: TestChildProps) => { return (
{name} {count} @@ -56,18 +65,18 @@ export const TestChild = ({ name, count }: { name: string; count: number }) => { ); }; -export function MemoChild({ value }: { value: string }) { +export const MemoChild = ({ value }: MemoChildProps) => { return
{value}
; -} +}; const TestMemoChild = memo(MemoChild); -function ForwardRefChild(_: object, ref: React.ForwardedRef) { +const ForwardRefChild = (_props: object, ref: React.ForwardedRef) => { return (
forward-ref
); -} +}; const TestForwardRefChild = forwardRef(ForwardRefChild); const TestContextConsumer = () => { diff --git a/packages/e2e/tests/artifacts/metro-source-map.test.mjs b/packages/e2e/tests/artifacts/metro-source-map.test.mjs index 98c59872..6c5750b5 100644 --- a/packages/e2e/tests/artifacts/metro-source-map.test.mjs +++ b/packages/e2e/tests/artifacts/metro-source-map.test.mjs @@ -49,7 +49,7 @@ test("symbolicates a real minified Metro production artifact", async () => { const appPosition = getGeneratedPosition(bundleContent, "test-child"); const appSource = getSourceFromSourceMap(sourceMap, appPosition.line, appPosition.column); assert.ok(appSource); - assert.match(appSource.fileName ?? "", /\/src\/App\.tsx$/); + assert.match(appSource.fileName ?? "", /\/src\/expo-test-app\.tsx$/); assert.match( getSourceContentFromSourceMap(sourceMap, appSource.fileName) ?? "", /const TestChild/, diff --git a/packages/e2e/tests/web/core.spec.ts b/packages/e2e/tests/web/core.spec.ts index 9fd383bd..bf826571 100644 --- a/packages/e2e/tests/web/core.spec.ts +++ b/packages/e2e/tests/web/core.spec.ts @@ -87,8 +87,8 @@ test.describe("instrumentation", () => { test("instrument() fires even when no previous handler exists", async ({ page }) => { const result = await page.evaluate(() => { return new Promise((resolve) => { - const rdtHook = (globalThis as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; - rdtHook.onCommitFiberRoot = undefined; + const rdtHook = window.__BIPPY__.getRDTHook(); + Reflect.set(rdtHook, "onCommitFiberRoot", undefined); window.__BIPPY__.instrument({ onCommitFiberRoot: () => { resolve(true); @@ -188,8 +188,8 @@ test.describe("type guards", () => { object: window.__BIPPY__.isFiber({}), null_: window.__BIPPY__.isFiber(null), undefined_: window.__BIPPY__.isFiber(undefined), - number: window.__BIPPY__.isFiber(42 as any), - string: window.__BIPPY__.isFiber("hello" as any), + number: window.__BIPPY__.isFiber(42), + string: window.__BIPPY__.isFiber("hello"), }; }); expect(result.fiber).toBe(true); @@ -286,7 +286,7 @@ test.describe("display name", () => { null_: window.__BIPPY__.getDisplayName(null), undefined_: window.__BIPPY__.getDisplayName(undefined), boolean: window.__BIPPY__.getDisplayName(true), - number: window.__BIPPY__.getDisplayName(42 as any), + number: window.__BIPPY__.getDisplayName(42), }; }); expect(result.div).toBe("div"); @@ -479,7 +479,7 @@ test.describe("timings", () => { test("getTimings with null returns zeros", async ({ page }) => { const result = await page.evaluate(() => { - const timings = window.__BIPPY__.getTimings(null as any); + const timings = window.__BIPPY__.getTimings(null); return timings; }); expect(result.selfTime).toBe(0); @@ -492,13 +492,13 @@ test.describe("timings", () => { const result = await page.evaluate(() => { const getTimings = window.__BIPPY__.getTimings; return { - childless: getTimings({ actualDuration: 5 } as any), + childless: getTimings({ actualDuration: 5 }), withChildren: getTimings({ actualDuration: 10, child: { actualDuration: 3, sibling: { actualDuration: 2, sibling: null } }, - } as any), - withUntimedChild: getTimings({ actualDuration: 4, child: { sibling: null } } as any), - zeroTotal: getTimings({ actualDuration: 0, child: { actualDuration: 3 } } as any), + }), + withUntimedChild: getTimings({ actualDuration: 4, child: { sibling: null } }), + zeroTotal: getTimings({ actualDuration: 0, child: { actualDuration: 3 } }), }; }); expect(result.childless).toEqual({ selfTime: 5, totalTime: 5 }); diff --git a/packages/e2e/tests/web/rdt-hook.spec.ts b/packages/e2e/tests/web/rdt-hook.spec.ts index b3c61319..163db90e 100644 --- a/packages/e2e/tests/web/rdt-hook.spec.ts +++ b/packages/e2e/tests/web/rdt-hook.spec.ts @@ -12,7 +12,7 @@ test.describe("getRDTHook", () => { const result = await page.evaluate(() => { const rdtHook = window.__BIPPY__.getRDTHook(); return { - isGlobalHook: rdtHook === (globalThis as any).__REACT_DEVTOOLS_GLOBAL_HOOK__, + isGlobalHook: rdtHook === globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__, isActive: rdtHook._instrumentationIsActive === true, isActiveApi: window.__BIPPY__.isInstrumentationActive(), }; @@ -46,7 +46,7 @@ test.describe("patchRDTHook", () => { page, }) => { const result = await page.evaluate(() => { - const rdtHook = (globalThis as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; + const rdtHook = window.__BIPPY__.getRDTHook(); rdtHook._instrumentationSource = undefined; rdtHook._instrumentationIsActive = false; let didFire = false; @@ -55,7 +55,7 @@ test.describe("patchRDTHook", () => { }); return { didFire, - isActive: rdtHook._instrumentationIsActive === true, + isActive: Boolean(rdtHook._instrumentationIsActive), }; }); expect(result.didFire).toBe(true); @@ -103,8 +103,12 @@ test.describe("installRDTHook", () => { rendererCount: installedHook.renderers.size, }; - const fakeRenderer = { version: "19.0.0-e2e" }; - const injectedRendererId = installedHook.inject(fakeRenderer as never); + const fakeRenderer: Parameters[0] = { + bundleType: 1, + rendererPackageName: "e2e-renderer", + version: "19.0.0-e2e", + }; + const injectedRendererId = installedHook.inject(fakeRenderer); const activeAfterInject = installedHook._instrumentationIsActive === true; // the global property is now bippy's accessor: assigning through its diff --git a/packages/e2e/tests/web/source-extras.spec.ts b/packages/e2e/tests/web/source-extras.spec.ts index 9ba4f5ad..205e6b67 100644 --- a/packages/e2e/tests/web/source-extras.spec.ts +++ b/packages/e2e/tests/web/source-extras.spec.ts @@ -72,8 +72,9 @@ test.describe("hook inspection", () => { expect(Array.isArray(result)).toBe(true); return; } - expect(result as string[]).toContain("count"); - expect(result as string[]).toContain("showConditional"); + if (!Array.isArray(result)) throw new Error("Hook names did not resolve to an array"); + expect(result).toContain("count"); + expect(result).toContain("showConditional"); }); }); diff --git a/packages/expo-playground/src/App.tsx b/packages/expo-playground/src/expo-playground-app.tsx similarity index 79% rename from packages/expo-playground/src/App.tsx rename to packages/expo-playground/src/expo-playground-app.tsx index e07335a7..5f7dc7c4 100644 --- a/packages/expo-playground/src/App.tsx +++ b/packages/expo-playground/src/expo-playground-app.tsx @@ -1,7 +1,7 @@ import { Inspector } from "@bippy/next-playground/components/inspector"; import { TodoList } from "@bippy/next-playground/components/todo-list"; -const App = () => { +const ExpoPlaygroundApp = () => { return (
@@ -10,4 +10,4 @@ const App = () => { ); }; -export default App; +export default ExpoPlaygroundApp; diff --git a/packages/expo-playground/src/index.ts b/packages/expo-playground/src/index.ts index 50f85ee0..77fe1d6b 100644 --- a/packages/expo-playground/src/index.ts +++ b/packages/expo-playground/src/index.ts @@ -1,6 +1,6 @@ import "bippy/install-hook-only"; import { registerRootComponent } from "expo"; -import App from "./App"; +import ExpoPlaygroundApp from "./expo-playground-app"; -registerRootComponent(App); +registerRootComponent(ExpoPlaygroundApp); diff --git a/packages/next-playground/app/layout.tsx b/packages/next-playground/app/layout.tsx index e0171879..cf642583 100644 --- a/packages/next-playground/app/layout.tsx +++ b/packages/next-playground/app/layout.tsx @@ -19,14 +19,16 @@ export const metadata: Metadata = { title: "playground", }; -export default function RootLayout({ - children, -}: Readonly<{ +interface RootLayoutProps { children: React.ReactNode; -}>) { +} + +const RootLayout = ({ children }: RootLayoutProps) => { return ( {children} ); -} +}; + +export default RootLayout; diff --git a/packages/next-playground/app/page.tsx b/packages/next-playground/app/page.tsx index 4f314836..fb0e2b5e 100644 --- a/packages/next-playground/app/page.tsx +++ b/packages/next-playground/app/page.tsx @@ -1,11 +1,13 @@ import { Inspector } from "../components/inspector"; import { TodoList } from "../components/todo-list"; -export default function Home() { +const Home = () => { return (
); -} +}; + +export default Home; diff --git a/packages/next-playground/components/class-names.ts b/packages/next-playground/components/class-names.ts new file mode 100644 index 00000000..3b57cc38 --- /dev/null +++ b/packages/next-playground/components/class-names.ts @@ -0,0 +1,4 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export const classNames = (...classValues: ClassValue[]): string => twMerge(clsx(classValues)); diff --git a/packages/next-playground/components/cn.tsx b/packages/next-playground/components/cn.tsx deleted file mode 100644 index a70ebb68..00000000 --- a/packages/next-playground/components/cn.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]): string { - return twMerge(clsx(inputs)); -} diff --git a/packages/next-playground/components/inspector.tsx b/packages/next-playground/components/inspector.tsx index dc632466..bae57e8d 100644 --- a/packages/next-playground/components/inspector.tsx +++ b/packages/next-playground/components/inspector.tsx @@ -5,9 +5,9 @@ import { getFiberFromHostInstance, getLatestFiber } from "bippy"; import { getSource } from "bippy/dist/source"; import { useEffect, useRef, useState } from "react"; -import { cn } from "./cn"; +import { classNames } from "./class-names"; -export function Inspector() { +export const Inspector = () => { const [rect, setRect] = useState(null); const [isEnabled, setIsEnabled] = useState(false); const isEnabledRef = useRef(isEnabled); @@ -51,7 +51,7 @@ export function Inspector() { return (
); -} +}; diff --git a/packages/vite-playground/src/main.tsx b/packages/vite-playground/src/main.tsx index 9640e6ac..c740785c 100644 --- a/packages/vite-playground/src/main.tsx +++ b/packages/vite-playground/src/main.tsx @@ -2,11 +2,11 @@ import "bippy/install-hook-only"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import App from "./App"; +import VitePlaygroundApp from "./vite-playground-app"; import "./index.css"; createRoot(document.getElementById("root")!).render( - + , ); diff --git a/packages/vite-playground/src/source-editor.tsx b/packages/vite-playground/src/source-editor.tsx index 6202c13e..d8f17c9a 100644 --- a/packages/vite-playground/src/source-editor.tsx +++ b/packages/vite-playground/src/source-editor.tsx @@ -14,7 +14,10 @@ import { useState, useEffect, useRef, useCallback } from "react"; import * as JsxDevRuntime from "react/jsx-dev-runtime"; import * as JsxRuntime from "react/jsx-runtime"; -const jsxDevFunction = (JsxDevRuntime as Record).jsxDEV; +const jsxDevFunction = Reflect.get(JsxDevRuntime, "jsxDEV"); + +const isComponentType = (value: unknown): value is React.ComponentType => + typeof value === "function"; const REACT_SCOPE: Record = { React, @@ -58,7 +61,9 @@ const evalComponentSource = ( // eslint-disable-next-line @typescript-eslint/no-implied-eval const factory = new Function(...paramNames, `return (${source})`); - return factory(...paramValues) as React.ComponentType; + const component: unknown = factory(...paramValues); + if (!isComponentType(component)) throw new Error("Edited source must evaluate to a component"); + return component; }; const walkToCompositeFiber = (fiber: Fiber): Fiber | null => { @@ -126,7 +131,7 @@ interface EditorState { componentName: string; } -export function SourceEditor() { +export const SourceEditor = () => { const [isEnabled, setIsEnabled] = useState(false); const [editorState, setEditorState] = useState(null); const [error, setError] = useState(null); @@ -409,4 +414,4 @@ export function SourceEditor() { )} ); -} +}; diff --git a/packages/vite-playground/src/App.tsx b/packages/vite-playground/src/vite-playground-app.tsx similarity index 76% rename from packages/vite-playground/src/App.tsx rename to packages/vite-playground/src/vite-playground-app.tsx index ce58c28d..8e4c0d6d 100644 --- a/packages/vite-playground/src/App.tsx +++ b/packages/vite-playground/src/vite-playground-app.tsx @@ -2,11 +2,13 @@ import { TodoList } from "@bippy/next-playground/components/todo-list"; import { SourceEditor } from "./source-editor"; -export default function App() { +const VitePlaygroundApp = () => { return (
); -} +}; + +export default VitePlaygroundApp;