diff --git a/packages/extension/src/utils/helpers.ts b/packages/extension/src/utils/helpers.ts index b8699853..707b0b74 100644 --- a/packages/extension/src/utils/helpers.ts +++ b/packages/extension/src/utils/helpers.ts @@ -1,13 +1,13 @@ -export const isIframe = window !== window.top; -export const isPopup = window.opener !== null; +const isIframe = window !== window.top; +const isPopup = window.opener !== null; export const canLoadReactScan = !isIframe && !isPopup; -export const IS_CLIENT = typeof window !== 'undefined'; +const IS_CLIENT = typeof window !== "undefined"; export const isInternalUrl = (url: string): boolean => { if (!url) return false; - const allowedProtocols = ['http:', 'https:', 'file:']; + const allowedProtocols = ["http:", "https:", "file:"]; return !allowedProtocols.includes(new URL(url).protocol); }; @@ -26,26 +26,42 @@ const ReactDetection = { limits: { MAX_DEPTH: 10, MAX_ELEMENTS: 30, - ELEMENTS_PER_LEVEL: 5 + ELEMENTS_PER_LEVEL: 5, }, nonVisualTags: new Set([ // Document level - 'HTML', 'HEAD', 'META', 'TITLE', 'BASE', + "HTML", + "HEAD", + "META", + "TITLE", + "BASE", // Scripts and styles - 'SCRIPT', 'STYLE', 'LINK', 'NOSCRIPT', + "SCRIPT", + "STYLE", + "LINK", + "NOSCRIPT", // Media and embeds - 'SOURCE', 'TRACK', 'EMBED', 'OBJECT', 'PARAM', + "SOURCE", + "TRACK", + "EMBED", + "OBJECT", + "PARAM", // Special elements - 'TEMPLATE', 'PORTAL', 'SLOT', + "TEMPLATE", + "PORTAL", + "SLOT", // Others - 'AREA', 'XML', 'DOCTYPE', 'COMMENT' + "AREA", + "XML", + "DOCTYPE", + "COMMENT", ]), reactMarkers: { - root: '_reactRootContainer', - fiber: '__reactFiber', - instance: '__reactInternalInstance$', - container: '__reactContainer$' - } + root: "_reactRootContainer", + fiber: "__reactFiber", + instance: "__reactInternalInstance$", + container: "__reactContainer$", + }, } as const; const childrenCache = new WeakMap(); @@ -81,8 +97,8 @@ export const hasReactFiber = (): boolean => { const rootContainer = elementWithRoot._reactRootContainer; const hasLegacyRoot = rootContainer?._internalRoot?.current?.child != null; - const hasContainerRoot = Object.keys(elementWithRoot).some(key => - key.startsWith(ReactDetection.reactMarkers.container) + const hasContainerRoot = Object.keys(elementWithRoot).some((key) => + key.startsWith(ReactDetection.reactMarkers.container), ); return hasLegacyRoot || hasContainerRoot; @@ -133,59 +149,6 @@ export const saveLocalStorage = (storageKey: string, state: T): void => { } catch {} }; -export const removeLocalStorage = (storageKey: string): void => { - if (!IS_CLIENT) return; - - try { - window.localStorage.removeItem(storageKey); - } catch {} -}; - -export const debounce = Promise>( - fn: T, - wait: number, - options: { leading?: boolean; trailing?: boolean } = {}, -) => { - let timeoutId: number | undefined; - let lastArg: boolean | null | undefined; - let isLeadingInvoked = false; - - const debounced = (enabled: boolean | null) => { - lastArg = enabled; - - if (options.leading && !isLeadingInvoked) { - isLeadingInvoked = true; - fn(enabled); - return; - } - - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - - if (options.trailing !== false) { - timeoutId = setTimeout(() => { - isLeadingInvoked = false; - timeoutId = undefined; - if (lastArg !== undefined) { - fn(lastArg); - } - }, wait); - } - }; - - debounced.cancel = () => { - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - timeoutId = undefined; - isLeadingInvoked = false; - lastArg = undefined; - } - }; - - return debounced; -}; - type EventCallback = (data: T) => void; const eventBus = new Map>(); @@ -220,14 +183,16 @@ export const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); }; -export const storageGetItem = async ( - storageKey: string, - key: string, -): Promise => { +const isStorageRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +export const storageGetItem = async (storageKey: string, key: string): Promise => { try { const result = await chrome.storage.local.get(storageKey); const data = result[storageKey]; - return data?.[key] ?? null; + if (!isStorageRecord(data)) return null; + const value = data[key]; + return value === undefined ? null : (value as T); } catch { return null; } @@ -240,9 +205,11 @@ export const storageSetItem = async ( ): Promise => { try { const result = await chrome.storage.local.get(storageKey); - const data = result[storageKey] || {}; - data[key] = value; - await chrome.storage.local.set({ [storageKey]: data }); - } catch { - } + const data = result[storageKey]; + const updatedData = { + ...(isStorageRecord(data) ? data : {}), + [key]: value, + }; + await chrome.storage.local.set({ [storageKey]: updatedData }); + } catch {} }; diff --git a/packages/scan/package.json b/packages/scan/package.json index f5c3848b..e899003b 100644 --- a/packages/scan/package.json +++ b/packages/scan/package.json @@ -3,41 +3,66 @@ "version": "0.5.7", "description": "Scan your React app for renders", "keywords": [ + "performance", "react", - "react-scan", "react scan", - "render", - "performance" + "react-scan", + "render" ], "homepage": "https://react-scan.million.dev", "bugs": { "url": "https://github.com/aidenybai/react-scan/issues" }, - "repository": { - "type": "git", - "url": "git+https://github.com/aidenybai/react-scan.git" - }, "license": "MIT", "author": { "name": "Aiden Bai", "email": "aiden@million.dev", "url": "https://million.dev" }, - "scripts": { - "build": "pnpm build:css && NODE_ENV=production tsup", - "build:copy": "pnpm build && cat dist/auto.global.js | pbcopy", - "build:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css", - "dev:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css --watch", - "dev:tsup": "NODE_ENV=development tsup --watch", - "dev": "pnpm run --parallel \"/^dev:(css|tsup)/\"", - "pack": "npm version patch && pnpm build && npm pack", - "pack:bump": "node scripts/bump-version.mjs && pnpm run pack && echo $(pwd)/react-scan-$(node -p \"require('./package.json').version\").tgz | pbcopy", - "publint": "publint", - "test": "vp test run", - "test:watch": "vp test", - "lint": "vp lint", - "format": "vp fmt", - "typecheck": "tsc --noEmit" + "repository": { + "type": "git", + "url": "git+https://github.com/aidenybai/react-scan.git" + }, + "bin": "bin/cli.js", + "files": [ + "dist", + "bin", + "package.json", + "README.md", + "LICENSE", + "auto.d.ts" + ], + "main": "dist/index.js", + "module": "dist/index.mjs", + "browser": "dist/auto.global.js", + "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "react-component-name/vite": [ + "./dist/react-component-name/vite.d.ts" + ], + "react-component-name/webpack": [ + "./dist/react-component-name/webpack.d.ts" + ], + "react-component-name/esbuild": [ + "./dist/react-component-name/esbuild.d.ts" + ], + "react-component-name/rspack": [ + "./dist/react-component-name/rspack.d.ts" + ], + "react-component-name/rolldown": [ + "./dist/react-component-name/rolldown.d.ts" + ], + "react-component-name/rollup": [ + "./dist/react-component-name/rollup.d.ts" + ], + "react-component-name/astro": [ + "./dist/react-component-name/astro.d.ts" + ], + "react-component-name/loader": [ + "./dist/react-component-name/loader.d.ts" + ] + } }, "exports": { "./package.json": "./package.json", @@ -164,47 +189,25 @@ "require": "./dist/react-component-name/loader.js" } }, - "main": "dist/index.js", - "module": "dist/index.mjs", - "browser": "dist/auto.global.js", - "types": "dist/index.d.ts", - "typesVersions": { - "*": { - "react-component-name/vite": [ - "./dist/react-component-name/vite.d.ts" - ], - "react-component-name/webpack": [ - "./dist/react-component-name/webpack.d.ts" - ], - "react-component-name/esbuild": [ - "./dist/react-component-name/esbuild.d.ts" - ], - "react-component-name/rspack": [ - "./dist/react-component-name/rspack.d.ts" - ], - "react-component-name/rolldown": [ - "./dist/react-component-name/rolldown.d.ts" - ], - "react-component-name/rollup": [ - "./dist/react-component-name/rollup.d.ts" - ], - "react-component-name/astro": [ - "./dist/react-component-name/astro.d.ts" - ], - "react-component-name/loader": [ - "./dist/react-component-name/loader.d.ts" - ] - } + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "pnpm build:css && NODE_ENV=production tsup", + "build:copy": "pnpm build && cat dist/auto.global.js | pbcopy", + "build:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css", + "dev:css": "postcss ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css --watch", + "dev:tsup": "NODE_ENV=development tsup --watch", + "dev": "pnpm run --parallel \"/^dev:(css|tsup)/\"", + "pack": "npm version patch && pnpm build && npm pack", + "pack:bump": "node scripts/bump-version.mjs && pnpm run pack && echo $(pwd)/react-scan-$(node -p \"require('./package.json').version\").tgz | pbcopy", + "publint": "publint", + "test": "vp test run", + "test:watch": "vp test", + "lint": "vp lint", + "format": "vp fmt", + "typecheck": "tsc --noEmit" }, - "bin": "bin/cli.js", - "files": [ - "dist", - "bin", - "package.json", - "README.md", - "LICENSE", - "auto.d.ts" - ], "dependencies": { "@babel/core": "^7.29.0", "@babel/types": "^7.29.0", @@ -253,8 +256,5 @@ }, "optionalDependencies": { "unplugin": "^3.0.0" - }, - "publishConfig": { - "access": "public" } } diff --git a/packages/scan/src/core/instrumentation.ts b/packages/scan/src/core/instrumentation.ts index 15e0e43e..5fb75788 100644 --- a/packages/scan/src/core/instrumentation.ts +++ b/packages/scan/src/core/instrumentation.ts @@ -1,4 +1,4 @@ -import { type Signal, signal } from '@preact/signals'; +import { type Signal, signal } from "@preact/signals"; import { ClassComponentTag, type Fiber, @@ -19,20 +19,15 @@ import { traverseContexts, traverseProps, traverseRenderedFibers, -} from 'bippy'; -import { isValidElement } from 'preact'; -import { isEqual } from '~core/utils'; +} from "bippy"; +import { isValidElement } from "preact"; +import { isEqual } from "~core/utils"; import { collectContextChanges, collectPropsChanges, collectStateChanges, -} from '~web/views/inspector/timeline/utils'; -import { - type Change, - type ContextChange, - ReactScanInternals, - type StateChange, -} from './index'; +} from "~web/views/inspector/timeline/utils"; +import { type Change, type ContextChange, ReactScanInternals, type StateChange } from "./index"; export enum RenderPhase { Mount = 0b001, @@ -40,7 +35,7 @@ export enum RenderPhase { Unmount = 0b100, } -export const RENDER_PHASE_STRING_TO_ENUM = { +const RENDER_PHASE_STRING_TO_ENUM = { mount: RenderPhase.Mount, update: RenderPhase.Update, unmount: RenderPhase.Unmount, @@ -62,7 +57,7 @@ export interface AggregatedRender { unnecessary: boolean | null; didCommit: boolean; fps: number; - computedKey: import('./index').OutlineKey | null; + computedKey: import("./index").OutlineKey | null; computedCurrent: DOMRect | null; } @@ -92,13 +87,13 @@ export const getFPS = () => { return fps; }; -export const isElementVisible = (el: Element) => { +const isElementVisible = (el: Element) => { const style = window.getComputedStyle(el); return ( - style.display !== 'none' && - style.visibility !== 'hidden' && - style.contentVisibility !== 'hidden' && - style.opacity !== '0' + style.display !== "none" && + style.visibility !== "hidden" && + style.contentVisibility !== "hidden" && + style.opacity !== "0" ); }; @@ -112,10 +107,7 @@ export const isValueUnstable = (prevValue: unknown, nextValue: unknown) => { ); }; -export const isElementInViewport = ( - el: Element, - rect = el.getBoundingClientRect(), -) => { +const isElementInViewport = (el: Element, rect = el.getBoundingClientRect()) => { const isVisible = rect.bottom > 0 && rect.right > 0 && @@ -149,29 +141,29 @@ export interface Render { fps: number; } -const unstableTypes = ['function', 'object']; +const unstableTypes = ["function", "object"]; const cache = new WeakMap(); export function fastSerialize(value: unknown, depth = 0): string { - if (depth < 0) return '…'; + if (depth < 0) return "…"; switch (typeof value) { - case 'function': + case "function": return value.toString(); - case 'string': + case "string": return value; - case 'number': - case 'boolean': - case 'undefined': + case "number": + case "boolean": + case "undefined": return String(value); - case 'object': + case "object": break; default: return String(value); } - if (value === null) return 'null'; + if (value === null) return "null"; if (cache.has(value)) { const cached = cache.get(value); @@ -181,13 +173,13 @@ export function fastSerialize(value: unknown, depth = 0): string { } if (Array.isArray(value)) { - const str = value.length ? `[${value.length}]` : '[]'; + const str = value.length ? `[${value.length}]` : "[]"; cache.set(value, str); return str; } if (isValidElement(value)) { - const type = getDisplayName(value.type) ?? ''; + const type = getDisplayName(value.type) ?? ""; const propCount = value.props ? Object.keys(value.props).length : 0; const str = `<${type} ${propCount}>`; cache.set(value, str); @@ -196,14 +188,13 @@ export function fastSerialize(value: unknown, depth = 0): string { if (Object.getPrototypeOf(value) === Object.prototype) { const keys = Object.keys(value); - const str = keys.length ? `{${keys.length}}` : '{}'; + const str = keys.length ? `{${keys.length}}` : "{}"; cache.set(value, str); return str; } - const ctor = - value && typeof value === 'object' ? value.constructor : undefined; - if (ctor && typeof ctor === 'function' && ctor.name) { + const ctor = value && typeof value === "object" ? value.constructor : undefined; + if (ctor && typeof ctor === "function" && ctor.name) { const str = `${ctor.name}{…}`; cache.set(value, str); return str; @@ -226,8 +217,7 @@ export const getStateChanges = (fiber: Fiber): StateChange[] => { fiber.tag === MemoComponentTag ) { let memoizedState: MemoizedState | null = fiber.memoizedState; - let prevState: MemoizedState | null | undefined = - fiber.alternate?.memoizedState; + let prevState: MemoizedState | null | undefined = fiber.alternate?.memoizedState; let index = 0; while (memoizedState) { @@ -254,7 +244,7 @@ export const getStateChanges = (fiber: Fiber): StateChange[] => { // when we have class component fiber, memoizedState is the component state const change: StateChange = { type: ChangeReason.ClassState, - name: 'state', + name: "state", value: fiber.memoizedState, prevValue: fiber.alternate?.memoizedState, }; @@ -295,8 +285,7 @@ function getContextChangesTraversal( const change: ContextChange = { type: ChangeReason.Context, name: - (nextValue.context as { displayName: string | undefined }).displayName ?? - 'Context.Provider', + (nextValue.context as { displayName: string | undefined }).displayName ?? "Context.Provider", value: nextMemoizedValue, contextType: getContextId(nextValue.context as ContextFiber), @@ -373,16 +362,13 @@ function isRenderUnnecessaryTraversal( prevValue: unknown, nextValue: unknown, ): void { - if ( - !isEqual(prevValue, nextValue) && - !isValueUnstable(prevValue, nextValue) - ) { + if (!isEqual(prevValue, nextValue) && !isValueUnstable(prevValue, nextValue)) { this.isRequiredChange = true; } } // FIXME: calculation is slow -export const isRenderUnnecessary = (fiber: Fiber) => { +const isRenderUnnecessary = (fiber: Fiber) => { if (!didFiberCommit(fiber)) return true; const mutatedHostFibers = getMutatedHostFibers(fiber); @@ -442,7 +428,7 @@ export interface OldRenderData { const RENDER_DEBOUNCE_MS = 16; -export const renderDataMap = new WeakMap>(); +const renderDataMap = new WeakMap>(); function getFiberIdentifier(fiber: Fiber) { return String(getFiberId(fiber)); @@ -485,8 +471,7 @@ const trackRender = ( if ( (hasChanges || hasDomMutations) && (!existingData || - currentTimestamp - (existingData.lastRenderTimestamp || 0) > - RENDER_DEBOUNCE_MS) + currentTimestamp - (existingData.lastRenderTimestamp || 0) > RENDER_DEBOUNCE_MS) ) { const renderData: RenderData = existingData || { selfTime: 0, @@ -504,10 +489,7 @@ const trackRender = ( } }; -export const createInstrumentation = ( - instanceKey: string, - config: InstrumentationConfig, -) => { +export const createInstrumentation = (instanceKey: string, config: InstrumentationConfig) => { const instrumentation: Instrumentation = { // this will typically be false, but in cases where a user provides showToolbar: true, this will be true isPaused: signal(!ReactScanInternals.options.value.enabled), @@ -522,7 +504,7 @@ export const createInstrumentation = ( inited = true; instrument({ - name: 'react-scan', + name: "react-scan", onActive: config.onActive, onCommitFiberRoot(_rendererID, root) { instrumentation.fiberRoots.add(root); @@ -542,7 +524,7 @@ export const createInstrumentation = ( traverseRenderedFibers( root.current, - (fiber: Fiber, phase: 'mount' | 'update' | 'unmount') => { + (fiber: Fiber, phase: "mount" | "update" | "unmount") => { const type = getType(fiber.type); if (!type) return null; @@ -604,8 +586,7 @@ export const createInstrumentation = ( ); } - const { selfTime: fiberSelfTime, totalTime: fiberTotalTime } = - getTimings(fiber); + const { selfTime: fiberSelfTime, totalTime: fiberTotalTime } = getTimings(fiber); const fps = getFPS(); const render: Render = { @@ -617,9 +598,7 @@ export const createInstrumentation = ( forget: hasMemoCache(fiber), // todo: allow this to be toggle-able through toolbar // todo: performance optimization: if the last fiber measure was very off screen, do not run isRenderUnnecessary - unnecessary: TRACK_UNNECESSARY_RENDERS - ? isRenderUnnecessary(fiber) - : null, + unnecessary: TRACK_UNNECESSARY_RENDERS ? isRenderUnnecessary(fiber) : null, didCommit: didFiberCommit(fiber), fps, }; @@ -628,14 +607,8 @@ export const createInstrumentation = ( const hasChanges = changes.length > 0; const hasDomMutations = getMutatedHostFibers(fiber).length > 0; - if (phase === 'update') { - trackRender( - fiber, - fiberSelfTime, - fiberTotalTime, - hasChanges, - hasDomMutations, - ); + if (phase === "update") { + trackRender(fiber, fiberSelfTime, fiberTotalTime, hasChanges, hasDomMutations); } for (let i = 0, len = validInstancesIndicies.length; i < len; i++) { diff --git a/packages/scan/src/core/notifications/event-tracking.ts b/packages/scan/src/core/notifications/event-tracking.ts index 371aed83..d0a05000 100644 --- a/packages/scan/src/core/notifications/event-tracking.ts +++ b/packages/scan/src/core/notifications/event-tracking.ts @@ -1,6 +1,6 @@ -import { useSyncExternalStore } from 'preact/compat'; -import { not_globally_unique_generateId } from '~core/utils'; -import { MAX_INTERACTION_BATCH, interactionStore } from './interaction-store'; +import { useSyncExternalStore } from "preact/compat"; +import { not_globally_unique_generateId } from "~core/utils"; +import { MAX_INTERACTION_BATCH, interactionStore } from "./interaction-store"; import { FiberRenders, PerformanceEntryChannelEvent, @@ -9,15 +9,10 @@ import { listenForRenders, setupDetailedPointerTimingListener, setupPerformancePublisher, -} from './performance'; -import { - MAX_CHANNEL_SIZE, - performanceEntryChannels, -} from './performance-store'; -import { BoundedArray } from './performance-utils'; -import { createStore } from '~web/utils/create-store'; - -let profileListeners: Array<(interaction: FinalInteraction) => void> = []; +} from "./performance"; +import { MAX_CHANNEL_SIZE, performanceEntryChannels } from "./performance-store"; +import { BoundedArray } from "./performance-utils"; +import { createStore } from "~web/utils/create-store"; type FinalInteraction = { detailedTiming: TimeoutStage; @@ -25,68 +20,22 @@ type FinalInteraction = { completedAt: number; }; -export const listenForProfile = ( - listener: (interaction: FinalInteraction) => void, -) => { - profileListeners.push(listener); - - return () => { - profileListeners = profileListeners.filter( - (existingListener) => existingListener !== listener, - ); - }; -}; - -export let interactionStatus: - | { kind: 'started'; startedAt: number } - | { kind: 'completed'; startedAt: number; endedAt: number } - | { kind: 'no-interaction' } = { - kind: 'no-interaction', -}; - -type NewInteractionStoreState = { - /** - * problem definition: we need to store bounds but how do we handle uninitialized bounds - * - * i guess what we said before, we just have one active bounds and that's all that matters chat - */ - - startAt: number; - endAt: number; -}; - -export const interactionStatusStore: { - state: NewInteractionStoreState | null; - listeners: Array<(state: NewInteractionStoreState) => void>; - addListener: (cb: (state: NewInteractionStoreState) => void) => () => void; -} = { - state: null, - addListener: (cb) => { - interactionStatusStore.listeners.push(cb); - return () => { - interactionStatusStore.listeners = - interactionStatusStore.listeners.filter((l) => l !== cb); - }; - }, - listeners: [], -}; - let accumulatedFiberRendersOverTask: null | FiberRenders = null; type InteractionEvent = { - kind: 'interaction'; + kind: "interaction"; data: { startAt: number; endAt: number; meta: { detailedTiming: TimeoutStage; latency: number; - kind: PerformanceEntryChannelEvent['kind']; + kind: PerformanceEntryChannelEvent["kind"]; }; }; }; type LongRenderPipeline = { - kind: 'long-render'; + kind: "long-render"; data: { startAt: number; endAt: number; @@ -113,175 +62,125 @@ type ToolbarEventStoreState = { }; }; -type DebugEvent = { - kind: string; - at: number; - meta?: unknown; -}; -export const debugEventStore = createStore<{ - state: { - events: Array; - }; - actions: { - // oxlint-disable-next-line typescript/no-explicit-any - addEvent: (event: any) => void; - clear: () => void; - }; -}>()((set) => ({ - state: { - events: [], - }, - actions: { - addEvent: (event: DebugEvent) => { - set((store) => ({ - state: { - events: [...store.state.events, event], - }, - })); - }, - clear: () => { - set({ - state: { - events: [], - }, - }); +const EVENT_STORE_CAPACITY = 200; + +export const toolbarEventStore = createStore()((set, get) => { + const listeners = new Set<(event: SlowdownEvent) => void>(); + + return { + state: { + events: new BoundedArray(EVENT_STORE_CAPACITY), }, - }, -})); -const EVENT_STORE_CAPACITY = 200; + actions: { + addEvent: (event: SlowdownEvent) => { + listeners.forEach((listener) => listener(event)); + + const events = [...get().state.events, event]; + const applyOverlapCheckToLongRenderEvent = ( + longRenderEvent: LongRenderPipeline & { id: string }, + onOverlap: (overlapsWith: InteractionEvent & { id: string }) => void, + ) => { + const overlapsWith = events.find((event) => { + if (event.kind === "long-render") { + return; + } -export const toolbarEventStore = createStore()( - (set, get) => { - const listeners = new Set<(event: SlowdownEvent) => void>(); + if (event.id === longRenderEvent.id) { + return; + } - return { - state: { - events: new BoundedArray(EVENT_STORE_CAPACITY), - }, + /** + * |---x-----------x------ (interaction) + * |x-----------x (long-render) + */ - actions: { - addEvent: (event: SlowdownEvent) => { - listeners.forEach((listener) => listener(event)); - - const events = [...get().state.events, event]; - const applyOverlapCheckToLongRenderEvent = ( - longRenderEvent: LongRenderPipeline & { id: string }, - onOverlap: ( - overlapsWith: InteractionEvent & { id: string }, - ) => void, - ) => { - const overlapsWith = events.find((event) => { - if (event.kind === 'long-render') { - return; - } - - if (event.id === longRenderEvent.id) { - return; - } - - /** - * |---x-----------x------ (interaction) - * |x-----------x (long-render) - */ - - if ( - longRenderEvent.data.startAt <= event.data.startAt && - longRenderEvent.data.endAt <= event.data.endAt && - longRenderEvent.data.endAt >= event.data.startAt - ) { - return true; - } - - /** + if ( + longRenderEvent.data.startAt <= event.data.startAt && + longRenderEvent.data.endAt <= event.data.endAt && + longRenderEvent.data.endAt >= event.data.startAt + ) { + return true; + } + + /** * |x-----------x---- (interaction) * |--x------------x (long-render) * */ - if ( - event.data.startAt <= longRenderEvent.data.startAt && - event.data.endAt >= longRenderEvent.data.startAt - ) { - return true; - } - - /** - * - * |--x-------------x (interaction) - * |x------------------x (long-render) - * - */ - - if ( - longRenderEvent.data.startAt <= event.data.startAt && - longRenderEvent.data.endAt >= event.data.endAt - ) { - return true; - } - }) as undefined | (InteractionEvent & { id: string }); // invariant: because we early check the typechecker does not know it must be the case that when it finds something, it will be an interaction it overlaps with - - if (overlapsWith) { - onOverlap(overlapsWith); + if ( + event.data.startAt <= longRenderEvent.data.startAt && + event.data.endAt >= longRenderEvent.data.startAt + ) { + return true; } - }; - const toRemove = new Set(); + /** + * + * |--x-------------x (interaction) + * |x------------------x (long-render) + * + */ - events.forEach((event) => { - if (event.kind === 'interaction') return; - applyOverlapCheckToLongRenderEvent(event, () => { - toRemove.add(event.id); - }); - }); + if ( + longRenderEvent.data.startAt <= event.data.startAt && + longRenderEvent.data.endAt >= event.data.endAt + ) { + return true; + } + }) as undefined | (InteractionEvent & { id: string }); // invariant: because we early check the typechecker does not know it must be the case that when it finds something, it will be an interaction it overlaps with - const withRemovedEvents = events.filter( - (event) => !toRemove.has(event.id), - ); + if (overlapsWith) { + onOverlap(overlapsWith); + } + }; - set(() => ({ - state: { - events: BoundedArray.fromArray( - withRemovedEvents, - EVENT_STORE_CAPACITY, - ), - }, - })); - }, - - addListener: (listener: (event: SlowdownEvent) => void) => { - listeners.add(listener); - return () => { - listeners.delete(listener); - }; - }, - - clear: () => { - set({ - state: { - events: new BoundedArray(EVENT_STORE_CAPACITY), - }, + const toRemove = new Set(); + + events.forEach((event) => { + if (event.kind === "interaction") return; + applyOverlapCheckToLongRenderEvent(event, () => { + toRemove.add(event.id); }); - }, + }); + + const withRemovedEvents = events.filter((event) => !toRemove.has(event.id)); + + set(() => ({ + state: { + events: BoundedArray.fromArray(withRemovedEvents, EVENT_STORE_CAPACITY), + }, + })); }, - }; - }, -); + + addListener: (listener: (event: SlowdownEvent) => void) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + + clear: () => { + set({ + state: { + events: new BoundedArray(EVENT_STORE_CAPACITY), + }, + }); + }, + }, + }; +}); export const useToolbarEventLog = () => { - return useSyncExternalStore( - toolbarEventStore.subscribe, - toolbarEventStore.getState, - ); + return useSyncExternalStore(toolbarEventStore.subscribe, toolbarEventStore.getState); }; let taskDirtyAt: null | number = null; let taskDirtyOrigin: null | number = null; -let previousTrackCurrentMouseOverElementCallback: - | ((e: MouseEvent) => void) - | null = null; +let previousTrackCurrentMouseOverElementCallback: ((e: MouseEvent) => void) | null = null; let overToolbar: boolean | null; @@ -291,33 +190,30 @@ const trackCurrentMouseOverToolbar = () => { .composedPath() .map((path) => (path as Element).id) .filter(Boolean) - .includes('react-scan-toolbar'); + .includes("react-scan-toolbar"); }; - document.addEventListener('mouseover', callback); + document.addEventListener("mouseover", callback); previousTrackCurrentMouseOverElementCallback = callback; return () => { if (previousTrackCurrentMouseOverElementCallback) { - document.removeEventListener( - 'mouseover', - previousTrackCurrentMouseOverElementCallback, - ); + document.removeEventListener("mouseover", previousTrackCurrentMouseOverElementCallback); } }; }; // stops long tasks b/c backgrounded from being reported -export const startDirtyTaskTracking = () => { +const startDirtyTaskTracking = () => { const onVisibilityChange = () => { taskDirtyAt = performance.now(); taskDirtyOrigin = performance.timeOrigin; }; - document.addEventListener('visibilitychange', onVisibilityChange); + document.addEventListener("visibilitychange", onVisibilityChange); return () => { - document.removeEventListener('visibilitychange', onVisibilityChange); + document.removeEventListener("visibilitychange", onVisibilityChange); }; }; @@ -325,7 +221,7 @@ export const HIGH_SEVERITY_FPS_DROP_TIME = 150; let framesDrawnInTheLastSecond: Array = []; -export function startLongPipelineTracking() { +function startLongPipelineTracking() { let rafHandle: number; let timeoutHandle: ReturnType; @@ -361,14 +257,14 @@ export function startLongPipelineTracking() { if ( duration > HIGH_SEVERITY_FPS_DROP_TIME && !taskConsideredDirty && - document.visibilityState === 'visible' && + document.visibilityState === "visible" && !wasTaskInfluencedByToolbar ) { const endAt = endOrigin + endNow; const startAt = startTime + startOrigin; toolbarEventStore.getState().actions.addEvent({ - kind: 'long-render', + kind: "long-render", id: not_globally_unique_generateId(), data: { endAt: endAt, @@ -413,7 +309,7 @@ export const startTimingTracking = () => { event: PerformanceEntryChannelEvent, ) => { toolbarEventStore.getState().actions.addEvent({ - kind: 'interaction', + kind: "interaction", id: not_globally_unique_generateId(), data: { startAt: finalInteraction.detailedTiming.blockingTimeStart, @@ -422,8 +318,7 @@ export const startTimingTracking = () => { }, }); - const existingCompletedInteractions = - performanceEntryChannels.getChannelState('recording'); + const existingCompletedInteractions = performanceEntryChannels.getChannelState("recording"); finalInteraction.detailedTiming.stopListeningForRenders(); @@ -432,34 +327,26 @@ export const startTimingTracking = () => { // it may be possible the performance entry returned before detailed timing. If that's the case we should update // assumptions and deal with mapping the entry back to the detailed timing here performanceEntryChannels.updateChannelState( - 'recording', + "recording", () => new BoundedArray(MAX_CHANNEL_SIZE), ); } }; - const unSubDetailedPointerTiming = setupDetailedPointerTimingListener( - 'pointer', - { - onComplete, - }, - ); - const unSubDetailedKeyboardTiming = setupDetailedPointerTimingListener( - 'keyboard', - { - onComplete, - }, - ); - - const unSubInteractions = listenForPerformanceEntryInteractions( - (completedInteraction) => { - interactionStore.setState( - BoundedArray.fromArray( - interactionStore.getCurrentState().concat(completedInteraction), - MAX_INTERACTION_BATCH, - ), - ); - }, - ); + const unSubDetailedPointerTiming = setupDetailedPointerTimingListener("pointer", { + onComplete, + }); + const unSubDetailedKeyboardTiming = setupDetailedPointerTimingListener("keyboard", { + onComplete, + }); + + const unSubInteractions = listenForPerformanceEntryInteractions((completedInteraction) => { + interactionStore.setState( + BoundedArray.fromArray( + interactionStore.getCurrentState().concat(completedInteraction), + MAX_INTERACTION_BATCH, + ), + ); + }); return () => { unSubMouseOver(); diff --git a/packages/scan/src/core/notifications/interaction-store.ts b/packages/scan/src/core/notifications/interaction-store.ts index 95e940e5..1ef4ab26 100644 --- a/packages/scan/src/core/notifications/interaction-store.ts +++ b/packages/scan/src/core/notifications/interaction-store.ts @@ -3,7 +3,7 @@ import { CompletedInteraction } from "./performance"; type Subscriber = (data: T) => void; -export class Store { +class Store { private subscribers: Set> = new Set(); private currentValue: T; @@ -32,5 +32,5 @@ export class Store { } export const MAX_INTERACTION_BATCH = 150; export const interactionStore = new Store>( - new BoundedArray(MAX_INTERACTION_BATCH) + new BoundedArray(MAX_INTERACTION_BATCH), ); diff --git a/packages/scan/src/core/notifications/outline-overlay.ts b/packages/scan/src/core/notifications/outline-overlay.ts index 7703128c..2eafa099 100644 --- a/packages/scan/src/core/notifications/outline-overlay.ts +++ b/packages/scan/src/core/notifications/outline-overlay.ts @@ -1,13 +1,13 @@ -import { signal } from '@preact/signals'; -import { iife } from './performance-utils'; +import { signal } from "@preact/signals"; +import { iife } from "./performance-utils"; -export let highlightCanvas: HTMLCanvasElement | null = null; -export let highlightCtx: CanvasRenderingContext2D | null = null; +let highlightCanvas: HTMLCanvasElement | null = null; +let highlightCtx: CanvasRenderingContext2D | null = null; let animationFrame: number | null = null; type TransitionHighlightState = { - kind: 'transition'; + kind: "transition"; transitionTo: { name: string; rects: Array; @@ -22,7 +22,7 @@ type TransitionHighlightState = { type HighlightState = | TransitionHighlightState | { - kind: 'move-out'; + kind: "move-out"; current: { name: string; rects: Array; @@ -30,7 +30,7 @@ type HighlightState = }; } | { - kind: 'idle'; + kind: "idle"; current: { name: string; rects: Array; @@ -38,7 +38,7 @@ type HighlightState = }; export const HighlightStore = signal({ - kind: 'idle', + kind: "idle", current: null, }); @@ -65,24 +65,22 @@ export const drawHighlights = () => { highlightCtx.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height); - const color = 'hsl(271, 76%, 53%)'; + const color = "hsl(271, 76%, 53%)"; const state = HighlightStore.value; const { alpha, current } = iife(() => { switch (state.kind) { - case 'transition': { + case "transition": { const current = - state.current?.alpha && state.current.alpha > 0 - ? state.current - : state.transitionTo; + state.current?.alpha && state.current.alpha > 0 ? state.current : state.transitionTo; return { alpha: current ? current.alpha : 0, current, }; } - case 'move-out': { + case "move-out": { return { alpha: state.current?.alpha ?? 0, current: state.current }; } - case 'idle': { + case "idle": { return { alpha: 1, current: state.current }; } } @@ -113,10 +111,10 @@ export const drawHighlights = () => { }); switch (state.kind) { - case 'move-out': { + case "move-out": { if (state.current.alpha === 0) { HighlightStore.value = { - kind: 'idle', + kind: "idle", current: null, }; lastFrameTime = 0; @@ -129,7 +127,7 @@ export const drawHighlights = () => { drawHighlights(); return; } - case 'transition': { + case "transition": { if (state.current && state.current.alpha > 0) { state.current.alpha = Math.max(0, state.current.alpha - step); drawHighlights(); @@ -139,7 +137,7 @@ export const drawHighlights = () => { // invariant, state.current.alpha === 0 if (state.transitionTo.alpha === 1) { HighlightStore.value = { - kind: 'idle', + kind: "idle", current: state.transitionTo, }; lastFrameTime = 0; @@ -150,7 +148,7 @@ export const drawHighlights = () => { drawHighlights(); } - case 'idle': { + case "idle": { // no-op lastFrameTime = 0; return; @@ -161,8 +159,8 @@ export const drawHighlights = () => { let handleResizeListener: (() => void) | null = null; export const createHighlightCanvas = (root: HTMLElement) => { - highlightCanvas = document.createElement('canvas'); - highlightCtx = highlightCanvas.getContext('2d', { alpha: true }); + highlightCanvas = document.createElement("canvas"); + highlightCtx = highlightCanvas.getContext("2d", { alpha: true }); if (!highlightCtx) return null; const dpr = window.devicePixelRatio || 1; @@ -172,18 +170,18 @@ export const createHighlightCanvas = (root: HTMLElement) => { highlightCanvas.style.height = `${innerHeight}px`; highlightCanvas.width = innerWidth * dpr; highlightCanvas.height = innerHeight * dpr; - highlightCanvas.style.position = 'fixed'; - highlightCanvas.style.left = '0'; - highlightCanvas.style.top = '0'; - highlightCanvas.style.pointerEvents = 'none'; - highlightCanvas.style.zIndex = '2147483600'; + highlightCanvas.style.position = "fixed"; + highlightCanvas.style.left = "0"; + highlightCanvas.style.top = "0"; + highlightCanvas.style.pointerEvents = "none"; + highlightCanvas.style.zIndex = "2147483600"; highlightCtx.scale(dpr, dpr); root.appendChild(highlightCanvas); if (handleResizeListener) { - window.removeEventListener('resize', handleResizeListener); + window.removeEventListener("resize", handleResizeListener); } const handleResize = () => { @@ -201,7 +199,7 @@ export const createHighlightCanvas = (root: HTMLElement) => { }; handleResizeListener = handleResize; - window.addEventListener('resize', handleResize); + window.addEventListener("resize", handleResize); HighlightStore.subscribe(() => { requestAnimationFrame(() => { @@ -212,7 +210,7 @@ export const createHighlightCanvas = (root: HTMLElement) => { return cleanup; }; -export function cleanup() { +function cleanup() { if (animationFrame) { cancelAnimationFrame(animationFrame); animationFrame = null; diff --git a/packages/scan/src/core/notifications/performance-utils.ts b/packages/scan/src/core/notifications/performance-utils.ts index 228c5c98..afb5cfc2 100644 --- a/packages/scan/src/core/notifications/performance-utils.ts +++ b/packages/scan/src/core/notifications/performance-utils.ts @@ -1,66 +1,3 @@ -import { Fiber } from 'bippy'; -export const getChildrenFromFiberLL = (fiber: Fiber) => { - const children: Array = []; - - let curr: typeof fiber.child = fiber.child; - - while (curr) { - children.push(curr); - - curr = curr.sibling; - } - - return children; -}; - -type Node = Map< - Fiber, - { - children: Array; - parent: Fiber | null; - isRoot: boolean; - isSVG: boolean; - } ->; - -export const createChildrenAdjacencyList = (root: Fiber, limit: number) => { - const tree: Node = new Map([]); - - const queue: Array<[node: Fiber, parent: Fiber | null]> = []; - const visited = new Set(); - - queue.push([root, root.return]); - let traversed = 1; - - while (queue.length) { - if (traversed >= limit) { - return tree; - } - // oxlint-disable-next-line typescript/no-non-null-assertion - const [node, parent] = queue.pop()!; - const children = getChildrenFromFiberLL(node); - - tree.set(node, { - children: [], - parent, - isRoot: node === root, - isSVG: node.type === 'svg', - }); - - for (const child of children) { - traversed += 1; - // this isn't needed since the fiber tree is a TREE, not a graph, but it makes me feel safer - if (visited.has(child)) { - continue; - } - visited.add(child); - tree.get(node)?.children.push(child); - queue.push([child, node]); - } - } - return tree; -}; - const THROW_INVARIANTS = false; export const invariantError = (message: string | undefined) => { diff --git a/packages/scan/src/core/notifications/performance.ts b/packages/scan/src/core/notifications/performance.ts index 86259e4b..527142e8 100644 --- a/packages/scan/src/core/notifications/performance.ts +++ b/packages/scan/src/core/notifications/performance.ts @@ -1,31 +1,15 @@ -import { - Fiber, - getDisplayName, - getTimings, - hasMemoCache, - isHostFiber, - traverseFiber, -} from 'bippy'; -import { Store } from '../..'; +import { Fiber, getDisplayName, getTimings, hasMemoCache, isHostFiber, traverseFiber } from "bippy"; +import { Store } from "../.."; -import { - BoundedArray, - invariantError, -} from '~core/notifications/performance-utils'; +import { BoundedArray, invariantError } from "~core/notifications/performance-utils"; import { SectionData, collectInspectorDataWithoutCounts, -} from '~web/views/inspector/timeline/utils'; -import { - getFiberFromElement, - getParentCompositeFiber, -} from '~web/views/inspector/utils'; -import { performanceEntryChannels } from './performance-store'; -import type { - PerformanceInteraction, - PerformanceInteractionEntry, -} from './types'; -import { not_globally_unique_generateId } from '~core/utils'; +} from "~web/views/inspector/timeline/utils"; +import { getFiberFromElement, getParentCompositeFiber } from "~web/views/inspector/utils"; +import { performanceEntryChannels } from "./performance-store"; +import type { PerformanceInteraction, PerformanceInteractionEntry } from "./types"; +import { not_globally_unique_generateId } from "~core/utils"; interface PathFilters { skipProviders: boolean; @@ -75,13 +59,7 @@ const shouldIncludeInPath = ( return !patternsToCheck.some((pattern) => pattern.test(name)); }; -const minifiedPatterns = [ - /^[a-z]$/, - /^[a-z][0-9]$/, - /^_+$/, - /^[A-Za-z][_$]$/, - /^[a-z]{1,2}$/, -]; +const minifiedPatterns = [/^[a-z]$/, /^[a-z][0-9]$/, /^_+$/, /^[A-Za-z][_$]$/, /^[a-z]{1,2}$/]; const isMinified = (name: string): boolean => { for (let i = 0; i < minifiedPatterns.length; i++) { @@ -108,11 +86,8 @@ interface FiberType { const getCleanComponentName = (component: FiberType): string => { const name = getDisplayName(component); - if (!name) return ''; - return name.replace( - /^(?:Memo|Forward(?:Ref)?|With.*?)\((?.*?)\)$/, - '$', - ); + if (!name) return ""; + return name.replace(/^(?:Memo|Forward(?:Ref)?|With.*?)\((?.*?)\)$/, "$"); }; const getInteractionPath = ( @@ -128,7 +103,12 @@ const getInteractionPath = ( let fiber = initialFiber; while (fiber.return) { const name = getCleanComponentName(fiber.type); - if (name && !isMinified(name) && shouldIncludeInPath(name, filters) && name.toLowerCase() !== name) { + if ( + name && + !isMinified(name) && + shouldIncludeInPath(name, filters) && + name.toLowerCase() !== name + ) { stack.push(name); } fiber = fiber.return; @@ -140,10 +120,7 @@ const getInteractionPath = ( return fullPath; }; -const getFirstNameFromAncestor = ( - fiber: Fiber, - accept: (name: string) => boolean = () => true, -) => { +const getFirstNameFromAncestor = (fiber: Fiber, accept: (name: string) => boolean = () => true) => { let curr: Fiber | null = fiber; while (curr) { @@ -159,7 +136,7 @@ const getFirstNameFromAncestor = ( let unsubscribeTrackVisibilityChange: (() => void) | undefined; // fixme: compress me if this stays here for bad interaction time checks -let lastVisibilityHiddenAt: number | 'never-hidden' = 'never-hidden'; +let lastVisibilityHiddenAt: number | "never-hidden" = "never-hidden"; const trackVisibilityChange = () => { unsubscribeTrackVisibilityChange?.(); @@ -168,10 +145,10 @@ const trackVisibilityChange = () => { lastVisibilityHiddenAt = Date.now(); } }; - document.addEventListener('visibilitychange', onVisibilityChange); + document.addEventListener("visibilitychange", onVisibilityChange); unsubscribeTrackVisibilityChange = () => { - document.removeEventListener('visibilitychange', onVisibilityChange); + document.removeEventListener("visibilitychange", onVisibilityChange); }; }; export type FiberRenders = Record< @@ -200,8 +177,8 @@ export type FiberRenders = Record< */ type InteractionStartStage = { - kind: 'interaction-start'; - interactionType: 'pointer' | 'keyboard'; + kind: "interaction-start"; + interactionType: "pointer" | "keyboard"; interactionUUID: string; interactionStartDetail: number; blockingTimeStart: number; @@ -215,29 +192,29 @@ type InteractionStartStage = { stopListeningForRenders: () => void; }; -type JSEndStage = Omit & { - kind: 'js-end-stage'; +type JSEndStage = Omit & { + kind: "js-end-stage"; jsEndDetail: number; }; -type RAFStage = Omit & { - kind: 'raf-stage'; +type RAFStage = Omit & { + kind: "raf-stage"; rafStart: number; }; -export type TimeoutStage = Omit & { - kind: 'timeout-stage'; +export type TimeoutStage = Omit & { + kind: "timeout-stage"; commitEnd: number; blockingTimeEnd: number; }; export type PerformanceEntryChannelEvent = | { - kind: 'entry-received'; + kind: "entry-received"; entry: PerformanceInteraction; } | { - kind: 'auto-complete-race'; + kind: "auto-complete-race"; interactionUUID: string; detailedTiming: TimeoutStage; }; @@ -250,61 +227,26 @@ export type CompletedInteraction = { }; type UnInitializedStage = { - kind: 'uninitialized-stage'; + kind: "uninitialized-stage"; // todo: no longer a uuid interactionUUID: string; - interactionType: 'pointer' | 'keyboard'; + interactionType: "pointer" | "keyboard"; }; -type CurrentInteraction = { - kind: 'pointer' | 'keyboard'; - interactionUUID: string; - pointerUpStart: number; - // needed for when inputs that can be clicked and trigger on change (like checkboxes) - clickChangeStart: number | null; - clickHandlerMicroTaskEnd: number | null; - rafStart: number | null; - commmitEnd: number | null; - timeorigin: number; - - // for now i don't trust performance now timing for UTC time... - blockingTimeStart: number; - blockingTimeEnd: number | null; - fiberRenders: Map< - string, - { - renderCount: number; - parents: Set; - selfTime: number; - } - >; - componentPath: Array; - componentName: string; - childrenTree: Record< - string, - { children: Array; firstNamedAncestor: string; isRoot: boolean } - >; -}; - -export let currentInteractions: Array = []; -const getInteractionType = ( - eventName: string, -): 'pointer' | 'keyboard' | null => { +const getInteractionType = (eventName: string): "pointer" | "keyboard" | null => { // todo: track pointer down, but tends to not house expensive logic so not very high priority - if (['pointerup', 'click'].includes(eventName)) { - return 'pointer'; + if (["pointerup", "click"].includes(eventName)) { + return "pointer"; } - if (eventName.includes('key')) { + if (eventName.includes("key")) { } - if (['keydown', 'keyup'].includes(eventName)) { - return 'keyboard'; + if (["keydown", "keyup"].includes(eventName)) { + return "keyboard"; } return null; }; let onEntryAnimationId: number | null = null; -const setupPerformanceListener = ( - onEntry: (interaction: PerformanceInteraction) => void, -) => { +const setupPerformanceListener = (onEntry: (interaction: PerformanceInteraction) => void) => { trackVisibilityChange(); const interactionMap = new Map(); const interactionTargetMap = new Map(); @@ -312,20 +254,13 @@ const setupPerformanceListener = ( const processInteractionEntry = (entry: PerformanceInteractionEntry) => { if (!entry.interactionId) return; - if ( - entry.interactionId && - entry.target && - !interactionTargetMap.has(entry.interactionId) - ) { + if (entry.interactionId && entry.target && !interactionTargetMap.has(entry.interactionId)) { interactionTargetMap.set(entry.interactionId, entry.target); } if (entry.target) { let current: Element | null = entry.target; while (current) { - if ( - current.id === 'react-scan-toolbar-root' || - current.id === 'react-scan-root' - ) { + if (current.id === "react-scan-toolbar-root" || current.id === "react-scan-root") { return; } current = current.parentElement; @@ -363,13 +298,12 @@ const setupPerformanceListener = ( duration: entry.duration, inputDelay: entry.processingStart - entry.startTime, processingDuration: entry.processingEnd - entry.processingStart, - presentationDelay: - entry.duration - (entry.processingEnd - entry.startTime), + presentationDelay: entry.duration - (entry.processingEnd - entry.startTime), // componentPath: timestamp: Date.now(), timeSinceTabInactive: - lastVisibilityHiddenAt === 'never-hidden' - ? 'never-hidden' + lastVisibilityHiddenAt === "never-hidden" + ? "never-hidden" : Date.now() - lastVisibilityHiddenAt, visibilityState: document.visibilityState, timeOrigin: performance.timeOrigin, @@ -409,12 +343,12 @@ const setupPerformanceListener = ( try { po.observe({ - type: 'event', + type: "event", buffered: true, durationThreshold: 16, } as PerformanceObserverInit); po.observe({ - type: 'first-input', + type: "first-input", buffered: true, }); } catch { @@ -428,22 +362,20 @@ export const setupPerformancePublisher = () => { return setupPerformanceListener((entry) => { performanceEntryChannels.publish( { - kind: 'entry-received', + kind: "entry-received", entry, }, - 'recording', + "recording", ); }); }; // we should actually only feed it the information it needs to complete so we can support safari type Task = { - completeInteraction: ( - entry: PerformanceEntryChannelEvent, - ) => CompletedInteraction; + completeInteraction: (entry: PerformanceEntryChannelEvent) => CompletedInteraction; startDateTime: number; endDateTime: number; - type: 'keyboard' | 'pointer'; + type: "keyboard" | "pointer"; interactionUUID: string; }; export const MAX_INTERACTION_TASKS = 25; @@ -481,26 +413,22 @@ export const listenForPerformanceEntryInteractions = ( onComplete: (completedInteraction: CompletedInteraction) => void, ) => { // we make the assumption that the detailed timing will be ready before the performance timing - const unsubscribe = performanceEntryChannels.subscribe( - 'recording', - (event) => { - const associatedDetailedInteraction = - event.kind === 'auto-complete-race' - ? tasks.find((task) => task.interactionUUID === event.interactionUUID) - : getAssociatedDetailedTimingInteraction(event.entry, tasks); - - // REMINDME: this likely means we clicked a non interactable thing but our handler still ran - // so we shouldn't treat this as an invariant, but instead use it to verify if we clicked - // something interactable - if (!associatedDetailedInteraction) { - return; - } + const unsubscribe = performanceEntryChannels.subscribe("recording", (event) => { + const associatedDetailedInteraction = + event.kind === "auto-complete-race" + ? tasks.find((task) => task.interactionUUID === event.interactionUUID) + : getAssociatedDetailedTimingInteraction(event.entry, tasks); + + // REMINDME: this likely means we clicked a non interactable thing but our handler still ran + // so we shouldn't treat this as an invariant, but instead use it to verify if we clicked + // something interactable + if (!associatedDetailedInteraction) { + return; + } - const completedInteraction = - associatedDetailedInteraction.completeInteraction(event); - onComplete(completedInteraction); - }, - ); + const completedInteraction = associatedDetailedInteraction.completeInteraction(event); + onComplete(completedInteraction); + }); return unsubscribe; }; @@ -549,14 +477,10 @@ const getTargetInteractionDetails = (target: Element) => { } // TODO: if element is minified, squash upwards till first non minified ancestor, and set name as ChildOf() - let componentName = associatedFiber - ? getDisplayName(associatedFiber?.type) - : 'N/A'; + let componentName = associatedFiber ? getDisplayName(associatedFiber?.type) : "N/A"; if (!componentName) { - componentName = - getFirstNameFromAncestor(associatedFiber, (name) => name.length > 2) ?? - 'N/A'; + componentName = getFirstNameFromAncestor(associatedFiber, (name) => name.length > 2) ?? "N/A"; } if (!componentName) { @@ -574,13 +498,9 @@ const getTargetInteractionDetails = (target: Element) => { }; type LastInteractionRef = { - current: ( - | InteractionStartStage - | JSEndStage - | RAFStage - | TimeoutStage - | UnInitializedStage - ) & { stageStart: number }; + current: (InteractionStartStage | JSEndStage | RAFStage | TimeoutStage | UnInitializedStage) & { + stageStart: number; + }; }; /** @@ -588,7 +508,7 @@ type LastInteractionRef = { * handles tracking event timings for arbitrarily overlapping handlers with cancel logic */ export const setupDetailedPointerTimingListener = ( - kind: 'pointer' | 'keyboard', + kind: "pointer" | "keyboard", options: { onStart?: (interactionUUID: string) => void; onComplete?: ( @@ -606,35 +526,30 @@ export const setupDetailedPointerTimingListener = ( ) => { let instrumentationIdInControl: string | null = null; - const getEvent = ( - info: { phase: 'start' } | { phase: 'end'; target: Element }, - ) => { + const getEvent = (info: { phase: "start" } | { phase: "end"; target: Element }) => { switch (kind) { - case 'pointer': { - if (info.phase === 'start') { - return 'pointerup'; + case "pointer": { + if (info.phase === "start") { + return "pointerup"; } - if ( - info.target instanceof HTMLInputElement || - info.target instanceof HTMLSelectElement - ) { - return 'change'; + if (info.target instanceof HTMLInputElement || info.target instanceof HTMLSelectElement) { + return "change"; } - return 'click'; + return "click"; } - case 'keyboard': { - if (info.phase === 'start') { - return 'keydown'; + case "keyboard": { + if (info.phase === "start") { + return "keydown"; } - return 'change'; + return "change"; } } }; const lastInteractionRef: LastInteractionRef = { current: { - kind: 'uninitialized-stage', + kind: "uninitialized-stage", interactionUUID: not_globally_unique_generateId(), // the first interaction uses this stageStart: Date.now(), interactionType: kind, @@ -643,23 +558,19 @@ export const setupDetailedPointerTimingListener = ( const onInteractionStart = (e: Event) => { const path = e.composedPath(); - if ( - path.some( - (el) => el instanceof Element && el.id === 'react-scan-toolbar-root', - ) - ) { + if (path.some((el) => el instanceof Element && el.id === "react-scan-toolbar-root")) { return; } if (Date.now() - lastInteractionRef.current.stageStart > 2000) { lastInteractionRef.current = { - kind: 'uninitialized-stage', + kind: "uninitialized-stage", interactionUUID: not_globally_unique_generateId(), stageStart: Date.now(), interactionType: kind, }; } - if (lastInteractionRef.current.kind !== 'uninitialized-stage') { + if (lastInteractionRef.current.kind !== "uninitialized-stage") { return; } @@ -673,7 +584,7 @@ export const setupDetailedPointerTimingListener = ( return; } - const fiberRenders: InteractionStartStage['fiberRenders'] = {}; + const fiberRenders: InteractionStartStage["fiberRenders"] = {}; const stopListeningForRenders = listenForRenders(fiberRenders); lastInteractionRef.current = { ...lastInteractionRef.current, @@ -683,12 +594,12 @@ export const setupDetailedPointerTimingListener = ( componentName: details.componentName, componentPath: details.componentPath, fiberRenders, - kind: 'interaction-start', + kind: "interaction-start", interactionStartDetail: pointerUpStart, stopListeningForRenders, }; - const event = getEvent({ phase: 'end', target: e.target as Element }); + const event = getEvent({ phase: "end", target: e.target as Element }); // oxlint-disable-next-line typescript/no-explicit-any document.addEventListener(event, onLastJS as any, { once: true, @@ -705,7 +616,7 @@ export const setupDetailedPointerTimingListener = ( }; document.addEventListener( - getEvent({ phase: 'start' }), + getEvent({ phase: "start" }), // oxlint-disable-next-line typescript/no-explicit-any onInteractionStart as any, { @@ -717,18 +628,14 @@ export const setupDetailedPointerTimingListener = ( * * TODO: IF WE DETECT RENDERS DURING THIS PERIOD WE CAN INCLUDE THAT IN THE RESULT AND THEN BACK THAT OUT OF COMPUTED STYLE TIME AND ADD IT BACK INTO JS TIME */ - const onLastJS = ( - e: { target: Element }, - instrumentationId: string, - abort: () => boolean, - ) => { + const onLastJS = (e: { target: Element }, instrumentationId: string, abort: () => boolean) => { if ( - lastInteractionRef.current.kind !== 'interaction-start' && + lastInteractionRef.current.kind !== "interaction-start" && instrumentationId === instrumentationIdInControl ) { - if (kind === 'pointer' && e.target instanceof HTMLSelectElement) { + if (kind === "pointer" && e.target instanceof HTMLSelectElement) { lastInteractionRef.current = { - kind: 'uninitialized-stage', + kind: "uninitialized-stage", interactionUUID: not_globally_unique_generateId(), stageStart: Date.now(), interactionType: kind, @@ -738,12 +645,12 @@ export const setupDetailedPointerTimingListener = ( options?.onError?.(lastInteractionRef.current.interactionUUID); lastInteractionRef.current = { - kind: 'uninitialized-stage', + kind: "uninitialized-stage", interactionUUID: not_globally_unique_generateId(), stageStart: Date.now(), interactionType: kind, }; - invariantError('pointer -> click'); + invariantError("pointer -> click"); return; } @@ -752,26 +659,26 @@ export const setupDetailedPointerTimingListener = ( trackDetailedTiming({ abort, onMicroTask: () => { - if (lastInteractionRef.current.kind === 'uninitialized-stage') { + if (lastInteractionRef.current.kind === "uninitialized-stage") { return false; } lastInteractionRef.current = { ...lastInteractionRef.current, - kind: 'js-end-stage', + kind: "js-end-stage", jsEndDetail: performance.now(), }; return true; }, onRAF: () => { if ( - lastInteractionRef.current.kind !== 'js-end-stage' && - lastInteractionRef.current.kind !== 'raf-stage' + lastInteractionRef.current.kind !== "js-end-stage" && + lastInteractionRef.current.kind !== "raf-stage" ) { options?.onError?.(lastInteractionRef.current.interactionUUID); - invariantError('bad transition to raf'); + invariantError("bad transition to raf"); lastInteractionRef.current = { - kind: 'uninitialized-stage', + kind: "uninitialized-stage", interactionUUID: not_globally_unique_generateId(), stageStart: Date.now(), interactionType: kind, @@ -781,34 +688,34 @@ export const setupDetailedPointerTimingListener = ( lastInteractionRef.current = { ...lastInteractionRef.current, - kind: 'raf-stage', + kind: "raf-stage", rafStart: performance.now(), }; return true; }, onTimeout: () => { - if (lastInteractionRef.current.kind !== 'raf-stage') { + if (lastInteractionRef.current.kind !== "raf-stage") { options?.onError?.(lastInteractionRef.current.interactionUUID); lastInteractionRef.current = { - kind: 'uninitialized-stage', + kind: "uninitialized-stage", interactionUUID: not_globally_unique_generateId(), stageStart: Date.now(), interactionType: kind, }; - invariantError('raf->timeout'); + invariantError("raf->timeout"); return; } const now = Date.now(); const timeoutStage: TimeoutStage = Object.freeze({ ...lastInteractionRef.current, - kind: 'timeout-stage', + kind: "timeout-stage", blockingTimeEnd: now, commitEnd: performance.now(), }); lastInteractionRef.current = { - kind: 'uninitialized-stage', + kind: "uninitialized-stage", interactionUUID: not_globally_unique_generateId(), stageStart: now, interactionType: kind, @@ -818,9 +725,8 @@ export const setupDetailedPointerTimingListener = ( completed = true; const latency = - event.kind === 'auto-complete-race' - ? event.detailedTiming.commitEnd - - event.detailedTiming.interactionStartDetail + event.kind === "auto-complete-race" + ? event.detailedTiming.commitEnd - event.detailedTiming.interactionStartDetail : event.entry.latency; const finalInteraction = { detailedTiming: timeoutStage, @@ -829,11 +735,7 @@ export const setupDetailedPointerTimingListener = ( flushNeeded: true, }; - options?.onComplete?.( - timeoutStage.interactionUUID, - finalInteraction, - event, - ); + options?.onComplete?.(timeoutStage.interactionUUID, finalInteraction, event); const newTasks = tasks.filter( (task) => task.interactionUUID !== timeoutStage.interactionUUID, ); @@ -857,7 +759,7 @@ export const setupDetailedPointerTimingListener = ( ); tasks = BoundedArray.fromArray(newTasks, MAX_INTERACTION_TASKS); completeInteraction({ - kind: 'auto-complete-race', + kind: "auto-complete-race", // redundant detailedTiming: timeoutStage, interactionUUID: timeoutStage.interactionUUID, @@ -868,7 +770,7 @@ export const setupDetailedPointerTimingListener = ( return; } completeInteraction({ - kind: 'auto-complete-race', + kind: "auto-complete-race", // redundant detailedTiming: timeoutStage, interactionUUID: timeoutStage.interactionUUID, @@ -889,14 +791,14 @@ export const setupDetailedPointerTimingListener = ( onLastJS(e, id, () => id !== instrumentationIdInControl); }; - if (kind === 'keyboard') { + if (kind === "keyboard") { // oxlint-disable-next-line typescript/no-explicit-any - document.addEventListener('keypress', onKeyPress as any); + document.addEventListener("keypress", onKeyPress as any); } return () => { document.removeEventListener( - getEvent({ phase: 'start' }), + getEvent({ phase: "start" }), // oxlint-disable-next-line typescript/no-explicit-any onInteractionStart as any, { @@ -904,7 +806,7 @@ export const setupDetailedPointerTimingListener = ( }, ); // oxlint-disable-next-line typescript/no-explicit-any - document.removeEventListener('keypress', onKeyPress as any); + document.removeEventListener("keypress", onKeyPress as any); }; }; @@ -918,12 +820,10 @@ const getHostFromFiber = (fiber: Fiber) => { }; const isPerformanceEventAvailable = () => { - return 'PerformanceEventTiming' in globalThis; + return "PerformanceEventTiming" in globalThis; }; -export const listenForRenders = ( - fiberRenders: InteractionStartStage['fiberRenders'], -) => { +export const listenForRenders = (fiberRenders: InteractionStartStage["fiberRenders"]) => { const listener = (fiber: Fiber) => { const displayName = getDisplayName(fiber.type); if (!displayName) { @@ -960,7 +860,7 @@ export const listenForRenders = ( nodeInfo: [ { element: getHostFromFiber(fiber), - name: getDisplayName(fiber.type) ?? 'Unknown', + name: getDisplayName(fiber.type) ?? "Unknown", selfTime: getTimings(fiber).selfTime, }, ], @@ -989,8 +889,7 @@ export const listenForRenders = ( changesCounts: new Map(), }; - existing.wasFiberRenderMount = - existing.wasFiberRenderMount || wasFiberRenderMount(fiber); + existing.wasFiberRenderMount = existing.wasFiberRenderMount || wasFiberRenderMount(fiber); existing.hasMemoCache = existing.hasMemoCache || hasMemoCache(fiber); existing.changes = { fiberProps: mergeSectionData( @@ -1012,7 +911,7 @@ export const listenForRenders = ( existing.totalTime += totalTime; existing.nodeInfo.push({ element: getHostFromFiber(fiber), - name: getDisplayName(fiber.type) ?? 'Unknown', + name: getDisplayName(fiber.type) ?? "Unknown", selfTime: getTimings(fiber).selfTime, }); }; @@ -1025,10 +924,7 @@ export const listenForRenders = ( }; }; -const mergeSectionData = ( - existing: SectionData, - newData: SectionData, -): SectionData => { +const mergeSectionData = (existing: SectionData, newData: SectionData): SectionData => { const mergedSection: SectionData = { current: [...existing.current], changes: new Set(), @@ -1042,7 +938,7 @@ const mergeSectionData = ( } for (const change of newData.changes) { - if (typeof change === 'string' || typeof change === 'number') { + if (typeof change === "string" || typeof change === "number") { mergedSection.changes.add(change); const existingCount = existing.changesCounts.get(change) || 0; const newCount = newData.changesCounts.get(change) || 0; diff --git a/packages/scan/src/core/utils.ts b/packages/scan/src/core/utils.ts index 3a4f11c1..d4ac2e2b 100644 --- a/packages/scan/src/core/utils.ts +++ b/packages/scan/src/core/utils.ts @@ -1,43 +1,6 @@ // @ts-nocheck -import { type Fiber, getType } from 'bippy'; -import { ReactScanInternals } from '~core/index'; -import type { AggregatedChange, AggregatedRender, Render } from './instrumentation'; -import { IS_CLIENT } from '~web/utils/constants'; - -export const aggregateChanges = ( - changes: Array, - prevAggregatedChange?: AggregatedChange, -) => { - const newChange = { - type: prevAggregatedChange?.type ?? 0, - unstable: prevAggregatedChange?.unstable ?? false, - }; - for (const change of changes) { - newChange.type |= change.type; - newChange.unstable = newChange.unstable || (change.unstable ?? false); - } - - return newChange; -}; - -export const aggregateRender = ( - newRender: Render, - prevAggregated: AggregatedRender, -) => { - prevAggregated.changes = aggregateChanges( - newRender.changes, - prevAggregated.changes, - ); - prevAggregated.aggregatedCount += 1; - prevAggregated.didCommit = prevAggregated.didCommit || newRender.didCommit; - prevAggregated.forget = prevAggregated.forget || newRender.forget; - prevAggregated.fps = prevAggregated.fps + newRender.fps; - prevAggregated.phase |= newRender.phase; - prevAggregated.time = (prevAggregated.time ?? 0) + (newRender.time ?? 0); - - prevAggregated.unnecessary = - prevAggregated.unnecessary || newRender.unnecessary; -}; +import type { AggregatedRender, Render } from "./instrumentation"; +import { IS_CLIENT } from "~web/utils/constants"; function descending(a: number, b: number): number { return b - a; @@ -81,10 +44,8 @@ function componentGroupHasForget(group: ComponentData[]): boolean { return false; } -export const getLabelText = ( - groupedAggregatedRenders: Array, -) => { - let labelText = ''; +export const getLabelText = (groupedAggregatedRenders: Array) => { + let labelText = ""; const componentsByCount = new Map< number, @@ -117,7 +78,7 @@ export const getLabelText = ( cumulativeTime += totalTime; if (componentGroup.length > 4) { - text += '…'; + text += "…"; } if (count > 1) { @@ -131,7 +92,7 @@ export const getLabelText = ( parts.push(text); } - labelText = parts.join(', '); + labelText = parts.join(", "); if (!labelText.length) return null; @@ -146,23 +107,6 @@ export const getLabelText = ( return labelText; }; -export const updateFiberRenderData = (fiber: Fiber, renders: Array) => { - ReactScanInternals.options.value.onRender?.(fiber, renders); - const type = getType(fiber.type) || fiber.type; - if (type && (typeof type === 'function' || typeof type === 'object')) { - const renderData = (type.renderData || { - count: 0, - time: 0, - renders: [], - }) as RenderData; - const firstRender = renders[0]; - renderData.count += firstRender.count; - renderData.time += firstRender.time ?? 0; - renderData.renders.push(firstRender); - type.renderData = renderData; - } -}; - export interface RenderData { count: number; time: number; @@ -178,7 +122,7 @@ export function isEqual(a: unknown, b: unknown): boolean { export const not_globally_unique_generateId = () => { if (!IS_CLIENT) { - return '0'; + return "0"; } // @ts-expect-error @@ -198,7 +142,7 @@ export const playNotificationSound = (audioContext: AudioContext) => { gainNode.connect(audioContext.destination); const options = { - type: 'sine' as OscillatorType, + type: "sine" as OscillatorType, freq: [ 392, // 523.25, @@ -213,20 +157,13 @@ export const playNotificationSound = (audioContext: AudioContext) => { const timePerNote = options.duration / frequencies.length; frequencies.forEach((freq, i) => { - oscillator.frequency.setValueAtTime( - freq, - audioContext.currentTime + i * timePerNote, - ); + oscillator.frequency.setValueAtTime(freq, audioContext.currentTime + i * timePerNote); }); oscillator.type = options.type; gainNode.gain.setValueAtTime(options.gain, audioContext.currentTime); - gainNode.gain.setTargetAtTime( - 0, - audioContext.currentTime + options.duration * 0.7, - 0.05, - ); + gainNode.gain.setTargetAtTime(0, audioContext.currentTime + options.duration * 0.7, 0.05); oscillator.start(); oscillator.stop(audioContext.currentTime + options.duration); diff --git a/packages/scan/src/lite/index.ts b/packages/scan/src/lite/index.ts index 6ab91159..e2a02ca1 100644 --- a/packages/scan/src/lite/index.ts +++ b/packages/scan/src/lite/index.ts @@ -3,22 +3,19 @@ import { type ReactRenderer, getRDTHook, isRealReactDevtools, -} from 'bippy'; -import type { SchedulerPriorityLevel } from './types'; -import { - DEFAULT_MAX_FIBERS_PER_COMMIT, - DEFAULT_MIN_FIBER_ACTUAL_DURATION_MS, -} from './constants'; -import { createEmitter } from './create-emitter'; -import { createProfilingHooks } from './create-profiling-hooks'; -import { createLaneLabelTranslator } from './lane-labels'; +} from "bippy"; +import { DEFAULT_MAX_FIBERS_PER_COMMIT, DEFAULT_MIN_FIBER_ACTUAL_DURATION_MS } from "./constants"; +import { createEmitter } from "./create-emitter"; +import { createProfilingHooks } from "./create-profiling-hooks"; +import { createLaneLabelTranslator } from "./lane-labels"; import type { LiteHandle, LiteOptions, ProfilingHooks, ReactRendererWithProfiling, -} from './types'; -import { walkFiber } from './walk-fiber'; + SchedulerPriorityLevel, +} from "./types"; +import { walkFiber } from "./walk-fiber"; const noopHandle: LiteHandle = { stop: () => {}, @@ -28,13 +25,13 @@ const noopHandle: LiteHandle = { interface ProfilingAttachOutcome { available: boolean; - reason?: 'no-inject-method' | 'threw' | 'opted-out'; + reason?: "no-inject-method" | "threw" | "opted-out"; error?: string; } const errorToMessage = (cause: unknown): string => { if (cause instanceof Error) return cause.message; - if (typeof cause === 'string') return cause; + if (typeof cause === "string") return cause; try { return JSON.stringify(cause); } catch { @@ -47,35 +44,33 @@ const tryInjectProfilingHooks = ( profilingHooks: ProfilingHooks, ): ProfilingAttachOutcome => { const rendererWithProfiling = renderer as ReactRendererWithProfiling; - if (typeof rendererWithProfiling.injectProfilingHooks !== 'function') { - return { available: false, reason: 'no-inject-method' }; + if (typeof rendererWithProfiling.injectProfilingHooks !== "function") { + return { available: false, reason: "no-inject-method" }; } try { rendererWithProfiling.injectProfilingHooks(profilingHooks); return { available: true }; } catch (cause) { - return { available: false, reason: 'threw', error: errorToMessage(cause) }; + return { available: false, reason: "threw", error: errorToMessage(cause) }; } }; const isValidEndpointUrl = (candidate: string): boolean => { try { const parsed = new URL(candidate); - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + return parsed.protocol === "http:" || parsed.protocol === "https:"; } catch { return false; } }; export const instrument = (options: LiteOptions = {}): LiteHandle => { - if (typeof window === 'undefined') return noopHandle; + if (typeof window === "undefined") return noopHandle; if (window.__REACT_SCAN_LITE__) return window.__REACT_SCAN_LITE__; if (options.endpoint && !options.sessionId) { // oxlint-disable-next-line no-console - console.warn( - '[react-scan/lite] `endpoint` requires `sessionId`; events will not be POSTed.', - ); + console.warn("[react-scan/lite] `endpoint` requires `sessionId`; events will not be POSTed."); } // Validate the endpoint URL once at instrument() time and DROP it on failure @@ -86,7 +81,7 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { if (effectiveEndpoint && !isValidEndpointUrl(effectiveEndpoint)) { // oxlint-disable-next-line no-console console.error( - '[react-scan/lite] `endpoint` is not a valid http(s) URL; events will not be POSTed.', + "[react-scan/lite] `endpoint` is not a valid http(s) URL; events will not be POSTed.", ); effectiveEndpoint = undefined; } @@ -99,7 +94,7 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { ) { // oxlint-disable-next-line no-console console.warn( - '[react-scan/lite] `includeFiberTree: false` disables per-fiber enrichment options (`recordChangeDescriptions`, `includeFiberSource`, `includeFiberIdentity`). Remove `includeFiberTree: false` to enable them.', + "[react-scan/lite] `includeFiberTree: false` disables per-fiber enrichment options (`recordChangeDescriptions`, `includeFiberSource`, `includeFiberIdentity`). Remove `includeFiberTree: false` to enable them.", ); } @@ -145,11 +140,11 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { attachedRenderers.add(renderer); const outcome: ProfilingAttachOutcome = includeProfilingHooks ? tryInjectProfilingHooks(renderer, profilingHooks) - : { available: false, reason: 'opted-out' }; - emitter.emit('renderer-injected', { + : { available: false, reason: "opted-out" }; + emitter.emit("renderer-injected", { data: { version: renderer.version, bundleType: renderer.bundleType }, }); - emitter.emit('profiling-hooks-status', { + emitter.emit("profiling-hooks-status", { available: outcome.available, reason: outcome.reason, error: outcome.error, @@ -169,7 +164,7 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { if (includeProfilingHooks && isRealReactDevtools(hook)) { // oxlint-disable-next-line no-console console.warn( - '[react-scan/lite] React DevTools is also attached. Calling injectProfilingHooks replaces its profiling channel; the DevTools Timeline Profiler may stop receiving events while this instrumentation is active.', + "[react-scan/lite] React DevTools is also attached. Calling injectProfilingHooks replaces its profiling channel; the DevTools Timeline Profiler may stop receiving events while this instrumentation is active.", ); } @@ -195,12 +190,7 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { didError?: boolean, ) => void; - const ourOnCommitFiberRoot: WidenedOnCommitFiberRoot = ( - rendererId, - root, - priority, - didError, - ) => { + const ourOnCommitFiberRoot: WidenedOnCommitFiberRoot = (rendererId, root, priority, didError) => { if (originalOnCommitFiberRoot) { try { (originalOnCommitFiberRoot as WidenedOnCommitFiberRoot).call( @@ -223,7 +213,7 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { isCancelled: () => isStopped, }) : undefined; - emitter.emit('commit', { + emitter.emit("commit", { rendererId, // HACK: bippy types `priority` as `number | void`. React actually // passes a Scheduler priority (1-5); narrow to `SchedulerPriorityLevel`. @@ -233,35 +223,28 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { }); }; - const ourOnPostCommitFiberRoot: typeof hook.onPostCommitFiberRoot = ( - rendererId, - root, - ) => { + const ourOnPostCommitFiberRoot: typeof hook.onPostCommitFiberRoot = (rendererId, root) => { if (originalOnPostCommitFiberRoot) { try { originalOnPostCommitFiberRoot.call(hook, rendererId, root); } catch {} } if (isStopped) return; - emitter.emit('post-commit', { rendererId }); + emitter.emit("post-commit", { rendererId }); }; - const ourOnCommitFiberUnmount: typeof hook.onCommitFiberUnmount = ( - rendererId, - fiber, - ) => { + const ourOnCommitFiberUnmount: typeof hook.onCommitFiberUnmount = (rendererId, fiber) => { if (originalOnCommitFiberUnmount) { try { originalOnCommitFiberUnmount.call(hook, rendererId, fiber); } catch {} } if (isStopped) return; - emitter.emit('fiber-unmount', { rendererId }); + emitter.emit("fiber-unmount", { rendererId }); }; hook.inject = ourInject; - hook.onCommitFiberRoot = - ourOnCommitFiberRoot as typeof hook.onCommitFiberRoot; + hook.onCommitFiberRoot = ourOnCommitFiberRoot as typeof hook.onCommitFiberRoot; hook.onPostCommitFiberRoot = ourOnPostCommitFiberRoot; hook.onCommitFiberUnmount = ourOnCommitFiberUnmount; @@ -272,9 +255,7 @@ export const instrument = (options: LiteOptions = {}): LiteHandle => { if (isStopped) return; isStopped = true; if (hook.inject === ourInject) hook.inject = originalInject; - if ( - (hook.onCommitFiberRoot as unknown) === (ourOnCommitFiberRoot as unknown) - ) { + if ((hook.onCommitFiberRoot as unknown) === (ourOnCommitFiberRoot as unknown)) { hook.onCommitFiberRoot = originalOnCommitFiberRoot; } if (hook.onPostCommitFiberRoot === ourOnPostCommitFiberRoot) { @@ -309,4 +290,4 @@ export type { LiteOptions, ProfilingHooksUnavailableReason, SchedulerPriorityLevel, -} from './types'; +} from "./types"; diff --git a/packages/scan/src/new-outlines/canvas.ts b/packages/scan/src/new-outlines/canvas.ts index 30d21c28..cab5bda6 100644 --- a/packages/scan/src/new-outlines/canvas.ts +++ b/packages/scan/src/new-outlines/canvas.ts @@ -1,8 +1,7 @@ -import type { ActiveOutline, OutlineData } from './types'; +import type { ActiveOutline, OutlineData } from "./types"; export const OUTLINE_ARRAY_SIZE = 7; -const MONO_FONT = - 'Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace'; +const MONO_FONT = "Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace"; const INTERPOLATION_SPEED = 0.2; const SNAP_THRESHOLD = 0.5; @@ -16,28 +15,26 @@ const MAX_PARTS_LENGTH = 4; const MAX_LABEL_LENGTH = 40; const TOTAL_FRAMES = 45; -const PRIMARY_COLOR = '115,97,230'; +const PRIMARY_COLOR = "115,97,230"; function sortEntry(prev: [number, string[]], next: [number, string[]]): number { return next[0] - prev[0]; } -function getSortedEntries( - countByNames: Map, -): [number, string[]][] { +function getSortedEntries(countByNames: Map): [number, string[]][] { const entries = [...countByNames.entries()]; return entries.sort(sortEntry); } function getLabelTextPart([count, names]: [number, string[]]): string { - let part = `${names.slice(0, MAX_PARTS_LENGTH).join(', ')} ×${count}`; + let part = `${names.slice(0, MAX_PARTS_LENGTH).join(", ")} ×${count}`; if (part.length > MAX_LABEL_LENGTH) { part = `${part.slice(0, MAX_LABEL_LENGTH)}…`; } return part; } -export const getLabelText = (outlines: ActiveOutline[]): string => { +const getLabelText = (outlines: ActiveOutline[]): string => { const nameByCount = new Map(); for (const { name, count } of outlines) { nameByCount.set(name, (nameByCount.get(name) || 0) + count); @@ -57,7 +54,7 @@ export const getLabelText = (outlines: ActiveOutline[]): string => { const partsEntries = getSortedEntries(countByNames); let labelText = getLabelTextPart(partsEntries[0]); for (let i = 1, len = partsEntries.length; i < len; i++) { - labelText += ', ' + getLabelTextPart(partsEntries[i]); + labelText += ", " + getLabelTextPart(partsEntries[i]); } if (labelText.length > MAX_LABEL_LENGTH) { @@ -67,7 +64,7 @@ export const getLabelText = (outlines: ActiveOutline[]): string => { return labelText; }; -export const getAreaFromOutlines = (outlines: ActiveOutline[]) => { +const getAreaFromOutlines = (outlines: ActiveOutline[]) => { let area = 0; for (const outline of outlines) { area += outline.width * outline.height; @@ -125,11 +122,8 @@ export const updateScroll = ( } }; -export const initCanvas = ( - canvas: HTMLCanvasElement | OffscreenCanvas, - dpr: number, -) => { - const ctx = canvas.getContext('2d', { alpha: true }) as +export const initCanvas = (canvas: HTMLCanvasElement | OffscreenCanvas, dpr: number) => { + const ctx = canvas.getContext("2d", { alpha: true }) as | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; if (ctx) { @@ -159,17 +153,7 @@ export const drawCanvas = ( >(); for (const outline of activeOutlines.values()) { - const { - x, - y, - width, - height, - targetX, - targetY, - targetWidth, - targetHeight, - frame, - } = outline; + const { x, y, width, height, targetX, targetY, targetWidth, targetHeight, frame } = outline; if (targetX !== x) { outline.x = lerp(x, targetX); } @@ -242,7 +226,7 @@ export const drawCanvas = ( } >(); - ctx.textRendering = 'optimizeSpeed'; + ctx.textRendering = "optimizeSpeed"; // TODO(Alexis): optimizable? for (const outlines of groupedOutlinesMap.values()) { @@ -276,11 +260,9 @@ export const drawCanvas = ( } // TODO(Alexis): optimize - const sortedLabels = Array.from(labelMap.entries()).sort( - ([_, a], [__, b]) => { - return getAreaFromOutlines(b.outlines) - getAreaFromOutlines(a.outlines); - }, - ); + const sortedLabels = Array.from(labelMap.entries()).sort(([_, a], [__, b]) => { + return getAreaFromOutlines(b.outlines) - getAreaFromOutlines(a.outlines); + }); for (const [labelKey, label] of sortedLabels) { if (!labelMap.has(labelKey)) continue; @@ -289,12 +271,7 @@ export const drawCanvas = ( if (labelKey === otherKey) continue; const { x, y, width, height } = label; - const { - x: otherX, - y: otherY, - width: otherWidth, - height: otherHeight, - } = otherLabel; + const { x: otherX, y: otherY, width: otherWidth, height: otherHeight } = otherLabel; if ( x + width > otherX && diff --git a/packages/scan/src/new-outlines/index.ts b/packages/scan/src/new-outlines/index.ts index 6d1e1630..63df5e57 100644 --- a/packages/scan/src/new-outlines/index.ts +++ b/packages/scan/src/new-outlines/index.ts @@ -7,7 +7,7 @@ import { getTimings, getType, isCompositeFiber, -} from 'bippy'; +} from "bippy"; import { Change, ContextChange, @@ -15,28 +15,22 @@ import { ReactScanInternals, Store, ignoredProps, -} from '~core/index'; +} from "~core/index"; import { ChangeReason, createInstrumentation, getContextChanges, getStateChanges, OldRenderData, -} from '~core/instrumentation'; -import { log, logIntro } from '~web/utils/log'; -import { inspectorUpdateSignal } from '~web/views/inspector/states'; -import { - OUTLINE_ARRAY_SIZE, - drawCanvas, - initCanvas, - updateOutlines, - updateScroll, -} from './canvas'; -import type { ActiveOutline, BlueprintOutline, OutlineData } from './types'; -import { getChangedPropsDetailed } from '~web/views/inspector/utils'; +} from "~core/instrumentation"; +import { log, logIntro } from "~web/utils/log"; +import { inspectorUpdateSignal } from "~web/views/inspector/states"; +import { OUTLINE_ARRAY_SIZE, drawCanvas, initCanvas, updateOutlines, updateScroll } from "./canvas"; +import type { ActiveOutline, BlueprintOutline, OutlineData } from "./types"; +import { getChangedPropsDetailed } from "~web/views/inspector/utils"; // The worker code will be replaced at build time -const workerCode = '__WORKER_CODE__'; +const workerCode = "__WORKER_CODE__"; let worker: Worker | null = null; let canvas: HTMLCanvasElement | null = null; @@ -48,10 +42,9 @@ const activeOutlines = new Map(); const blueprintMap = new Map(); const blueprintMapKeys = new Set(); -export const outlineFiber = (fiber: Fiber) => { +const outlineFiber = (fiber: Fiber) => { if (!isCompositeFiber(fiber)) return; - const name = - typeof fiber.type === 'string' ? fiber.type : getDisplayName(fiber); + const name = typeof fiber.type === "string" ? fiber.type : getDisplayName(fiber); if (!name) return; const blueprint = blueprintMap.get(fiber); const nearestFibers = getNearestHostFibers(fiber); @@ -83,12 +76,8 @@ const mergeRects = (rects: DOMRect[]) => { const rect = rects[i]; minX = minX == null ? rect.x : Math.min(minX, rect.x); minY = minY == null ? rect.y : Math.min(minY, rect.y); - maxX = - maxX == null ? rect.x + rect.width : Math.max(maxX, rect.x + rect.width); - maxY = - maxY == null - ? rect.y + rect.height - : Math.max(maxY, rect.y + rect.height); + maxX = maxX == null ? rect.x + rect.width : Math.max(maxX, rect.x + rect.width); + maxY = maxY == null ? rect.y + rect.height : Math.max(maxY, rect.y + rect.height); } if (minX == null || minY == null || maxX == null || maxY == null) { @@ -150,11 +139,9 @@ export const getBatchedRectMap = async function* ( } while (!state.done) { - const entries = await new Promise( - (resolve) => { - state.resolveNext = resolve; - }, - ); + const entries = await new Promise((resolve) => { + state.resolveNext = resolve; + }); if (entries.length > 0) { yield entries; } @@ -162,9 +149,9 @@ export const getBatchedRectMap = async function* ( }; const SupportedArrayBuffer = - typeof SharedArrayBuffer !== 'undefined' ? SharedArrayBuffer : ArrayBuffer; + typeof SharedArrayBuffer !== "undefined" ? SharedArrayBuffer : ArrayBuffer; -export const flushOutlines = async () => { +const flushOutlines = async () => { const elements: Element[] = []; for (const fiber of blueprintMapKeys) { @@ -215,9 +202,7 @@ export const flushOutlines = async () => { } if (blueprints.length > 0) { - const arrayBuffer = new SupportedArrayBuffer( - blueprints.length * OUTLINE_ARRAY_SIZE * 4, - ); + const arrayBuffer = new SupportedArrayBuffer(blueprints.length * OUTLINE_ARRAY_SIZE * 4); const sharedView = new Float32Array(arrayBuffer); const blueprintNames = new Array(blueprints.length); let outlineData: OutlineData[] | undefined; @@ -255,7 +240,7 @@ export const flushOutlines = async () => { if (worker) { worker.postMessage({ - type: 'draw-outlines', + type: "draw-outlines", data: arrayBuffer, names: blueprintNames, }); @@ -287,25 +272,25 @@ const draw = () => { }; const IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED = - typeof OffscreenCanvas !== 'undefined' && typeof Worker !== 'undefined'; + typeof OffscreenCanvas !== "undefined" && typeof Worker !== "undefined"; const getDpr = () => { return Math.min(window.devicePixelRatio || 1, 2); }; -export const getCanvasEl = () => { +const getCanvasEl = () => { cleanup(); - const host = document.createElement('div'); - host.setAttribute('data-react-scan', 'true'); - const shadowRoot = host.attachShadow({ mode: 'open' }); - - const canvasEl = document.createElement('canvas'); - canvasEl.style.position = 'fixed'; - canvasEl.style.top = '0'; - canvasEl.style.left = '0'; - canvasEl.style.pointerEvents = 'none'; - canvasEl.style.zIndex = '2147483646'; - canvasEl.setAttribute('aria-hidden', 'true'); + const host = document.createElement("div"); + host.setAttribute("data-react-scan", "true"); + const shadowRoot = host.attachShadow({ mode: "open" }); + + const canvasEl = document.createElement("canvas"); + canvasEl.style.position = "fixed"; + canvasEl.style.top = "0"; + canvasEl.style.left = "0"; + canvasEl.style.pointerEvents = "none"; + canvasEl.style.zIndex = "2147483646"; + canvasEl.setAttribute("aria-hidden", "true"); shadowRoot.appendChild(canvasEl); if (!canvasEl) return null; @@ -323,24 +308,19 @@ export const getCanvasEl = () => { // Users on a strict CSP without `worker-src blob:` (see #372) can opt out; // we still render outlines on the main thread via `initCanvas` below. - const workerOptOut = - ReactScanInternals.options.value.useOffscreenCanvasWorker === false; - - if ( - IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED && - !window.__REACT_SCAN_EXTENSION__ && - !workerOptOut - ) { + const workerOptOut = ReactScanInternals.options.value.useOffscreenCanvasWorker === false; + + if (IS_OFFSCREEN_CANVAS_WORKER_SUPPORTED && !window.__REACT_SCAN_EXTENSION__ && !workerOptOut) { try { const blobUrl = URL.createObjectURL( - new Blob([workerCode], { type: 'application/javascript' }), + new Blob([workerCode], { type: "application/javascript" }), ); worker = new Worker(blobUrl); const offscreenCanvas = canvasEl.transferControlToOffscreen(); worker.postMessage( { - type: 'init', + type: "init", canvas: offscreenCanvas, width: canvasEl.width, height: canvasEl.height, @@ -353,9 +333,9 @@ export const getCanvasEl = () => { // The fallback below renders fine on the main thread, so log only when // the user explicitly asked for verbose diagnostics. worker = null; - if (ReactScanInternals.options.value._debug === 'verbose') { + if (ReactScanInternals.options.value._debug === "verbose") { // oxlint-disable-next-line no-console - console.warn('Failed to initialize OffscreenCanvas worker:', error); + console.warn("Failed to initialize OffscreenCanvas worker:", error); } } // The blob URL stays alive until the worker is GC'd. Revoking @@ -369,7 +349,7 @@ export const getCanvasEl = () => { } let isResizeScheduled = false; - window.addEventListener('resize', () => { + window.addEventListener("resize", () => { if (!isResizeScheduled) { isResizeScheduled = true; // TODO(Alexis): bindable @@ -381,7 +361,7 @@ export const getCanvasEl = () => { canvasEl.style.height = `${height}px`; if (worker) { worker.postMessage({ - type: 'resize', + type: "resize", width, height, dpr, @@ -404,7 +384,7 @@ export const getCanvasEl = () => { let prevScrollY = window.scrollY; let isScrollScheduled = false; - window.addEventListener('scroll', () => { + window.addEventListener("scroll", () => { if (!isScrollScheduled) { isScrollScheduled = true; // TODO(Alexis): bindable @@ -416,14 +396,12 @@ export const getCanvasEl = () => { prevScrollY = scrollY; if (worker) { worker.postMessage({ - type: 'scroll', + type: "scroll", deltaX, deltaY, }); } else { - requestAnimationFrame( - updateScroll.bind(null, activeOutlines, deltaX, deltaY), - ); + requestAnimationFrame(updateScroll.bind(null, activeOutlines, deltaX, deltaY)); } isScrollScheduled = false; }, 16 * 2); @@ -440,17 +418,12 @@ export const getCanvasEl = () => { return host; }; -export const hasStopped = () => { +const hasStopped = () => { return globalThis.__REACT_SCAN_STOP__; }; -export const stop = () => { - globalThis.__REACT_SCAN_STOP__ = true; - cleanup(); -}; - -export const cleanup = () => { - const host = document.querySelector('[data-react-scan]'); +const cleanup = () => { + const host = document.querySelector("[data-react-scan]"); if (host) { host.remove(); } @@ -461,7 +434,7 @@ const reportRenderToListeners = (fiber: Fiber) => { // report render has a non trivial cost because it calls Date.now(), so we want to avoid the computation if possible if ( ReactScanInternals.options.value.showToolbar !== false && - Store.inspectState.value.kind === 'focused' + Store.inspectState.value.kind === "focused" ) { const reportFiber = fiber; const { selfTime } = getTimings(fiber); @@ -478,9 +451,7 @@ const reportRenderToListeners = (fiber: Fiber) => { const listeners = Store.changesListeners.get(getFiberId(fiber)); if (listeners?.length) { - const propsChanges: Array = getChangedPropsDetailed( - fiber, - ).map((change) => ({ + const propsChanges: Array = getChangedPropsDetailed(fiber).map((change) => ({ type: ChangeReason.Props, name: change.name, value: change.value, @@ -494,14 +465,12 @@ const reportRenderToListeners = (fiber: Fiber) => { // currently, we say every context change, regardless of the render it happened, is a change. Which requires us to hack change tracking // in the whats-changed toolbar component const fiberContext = getContextChanges(fiber); - const contextChanges: Array = fiberContext.map( - (info) => ({ - name: info.name, - type: ChangeReason.Context, - value: info.value, - contextType: info.contextType, - }), - ); + const contextChanges: Array = fiberContext.map((info) => ({ + name: info.name, + type: ChangeReason.Context, + value: info.value, + contextType: info.contextType, + })); listeners.forEach((listener) => { listener({ @@ -528,7 +497,7 @@ const reportRenderToListeners = (fiber: Fiber) => { let needsReport = false; let reportInterval: ReturnType; -export const startReportInterval = () => { +const startReportInterval = () => { clearInterval(reportInterval); reportInterval = setInterval(() => { if (needsReport) { @@ -538,7 +507,7 @@ export const startReportInterval = () => { }, 50); }; -export const isValidFiber = (fiber: Fiber) => { +const isValidFiber = (fiber: Fiber) => { if (ignoredProps.has(fiber.memoizedProps)) { return false; } @@ -572,7 +541,7 @@ export const initReactScanInstrumentation = (setupToolbar: () => void) => { }); // TODO(Alexis): perhaps a better timing }; - const instrumentation = createInstrumentation('react-scan-devtools-0.1.0', { + const instrumentation = createInstrumentation("react-scan-devtools-0.1.0", { onCommitStart: () => { ReactScanInternals.options.value.onCommitStart?.(); }, @@ -601,11 +570,10 @@ export const initReactScanInstrumentation = (setupToolbar: () => void) => { if (isCompositeFiber(fiber)) { Store.interactionListeningForRenders?.(fiber, renders); } - const isOverlayPaused = - ReactScanInternals.instrumentation?.isPaused.value; + const isOverlayPaused = ReactScanInternals.instrumentation?.isPaused.value; const isInspectorInactive = - Store.inspectState.value.kind === 'inspect-off' || - Store.inspectState.value.kind === 'uninitialized'; + Store.inspectState.value.kind === "inspect-off" || + Store.inspectState.value.kind === "uninitialized"; const shouldFullyAbort = isOverlayPaused && isInspectorInactive; if (shouldFullyAbort) { @@ -619,7 +587,7 @@ export const initReactScanInstrumentation = (setupToolbar: () => void) => { log(renders); } - if (Store.inspectState.value.kind === 'focused') { + if (Store.inspectState.value.kind === "focused") { inspectorUpdateSignal.value = Date.now(); } if (!isInspectorInactive) { diff --git a/packages/scan/src/new-outlines/types.ts b/packages/scan/src/new-outlines/types.ts index 1d54f343..ec7061bd 100644 --- a/packages/scan/src/new-outlines/types.ts +++ b/packages/scan/src/new-outlines/types.ts @@ -9,16 +9,6 @@ export interface OutlineData { didCommit: 0 | 1; } -export type InlineOutlineData = [ - id: number, - count: number, - x: number, - y: number, - width: number, - height: number, - didCommit: 0 | 1, -]; - export interface ActiveOutline { id: number; name: string; @@ -46,7 +36,6 @@ declare global { var __REACT_SCAN_STOP__: boolean; var ReactScan: { hasStopped: () => boolean; - stop: () => void; cleanup: () => void; init: () => void; flushOutlines: () => void; diff --git a/packages/scan/src/react-component-name/core/options.ts b/packages/scan/src/react-component-name/core/options.ts index e1a1171b..a7c43e98 100644 --- a/packages/scan/src/react-component-name/core/options.ts +++ b/packages/scan/src/react-component-name/core/options.ts @@ -1,9 +1,9 @@ -import type { FilterPattern } from '@rollup/pluginutils'; +import type { FilterPattern } from "@rollup/pluginutils"; export interface Options { include?: FilterPattern; exclude?: FilterPattern; - enforce?: 'pre' | 'post' | undefined; + enforce?: "pre" | "post" | undefined; flags?: { noTryCatchDisplayNames?: boolean; noStyledComponents?: boolean; @@ -11,19 +11,3 @@ export interface Options { ignoreComponentSubstrings?: Array; }; } - -type Overwrite = Pick> & U; - -export type OptionsResolved = Overwrite< - Required, - Pick ->; - -export function resolveOptions(options: Options): OptionsResolved { - return { - include: options.include ?? [/\.[cm]?[jt]sx?$/], - exclude: options.exclude ?? [/node_modules/], - enforce: 'enforce' in options ? options.enforce : 'pre', - flags: options.flags ?? {}, - }; -} diff --git a/packages/scan/src/types.ts b/packages/scan/src/types.ts index 4e0dbb29..bac578e7 100644 --- a/packages/scan/src/types.ts +++ b/packages/scan/src/types.ts @@ -19,9 +19,6 @@ declare global { }; var reactScanCleanupListeners: (() => void) | undefined; var reactScan: Scan; - var scheduler: { - postTask: (cb: unknown, options: { priority: string }) => void; - }; type TTimer = NodeJS.Timeout; diff --git a/packages/scan/src/web/state.ts b/packages/scan/src/web/state.ts index c7feb5ea..b09792e0 100644 --- a/packages/scan/src/web/state.ts +++ b/packages/scan/src/web/state.ts @@ -1,21 +1,18 @@ import { signal } from "@preact/signals"; import { LOCALSTORAGE_KEY, + LOCALSTORAGE_COLLAPSED_KEY, MIN_CONTAINER_WIDTH, MIN_SIZE, SAFE_AREA, - LOCALSTORAGE_COLLAPSED_KEY, } from "./constants"; import { IS_CLIENT } from "./utils/constants"; import { readLocalStorage, saveLocalStorage } from "./utils/helpers"; import { getSafeArea } from "./utils/safe-area"; -import type { Corner, WidgetConfig, WidgetSettings } from "./widget/types"; -import type { CollapsedPosition } from "./widget/types"; +import type { CollapsedPosition, Corner, WidgetConfig, WidgetSettings } from "./widget/types"; export const signalIsSettingsOpen = /* @__PURE__ */ signal(false); -export const signalRefWidget = /* @__PURE__ */ signal( - null -); +export const signalRefWidget = /* @__PURE__ */ signal(null); // Use the raw SAFE_AREA constant (not getSafeArea()) here: this runs at // module-init time, before any user has called scan() with options. @@ -45,7 +42,7 @@ export const getDefaultWidgetConfig = (): WidgetConfig => ({ /** @deprecated use {@link getDefaultWidgetConfig} */ export const defaultWidgetConfig: WidgetConfig = getDefaultWidgetConfig(); -export const getInitialWidgetConfig = (): WidgetConfig => { +const getInitialWidgetConfig = (): WidgetConfig => { const defaults = getDefaultWidgetConfig(); const stored = readLocalStorage(LOCALSTORAGE_KEY); if (!stored) { @@ -63,10 +60,7 @@ export const getInitialWidgetConfig = (): WidgetConfig => { corner: stored.corner ?? defaults.corner, dimensions: stored.dimensions ?? defaults.dimensions, - lastDimensions: - stored.lastDimensions ?? - stored.dimensions ?? - defaults.lastDimensions, + lastDimensions: stored.lastDimensions ?? stored.dimensions ?? defaults.lastDimensions, componentsTree: stored.componentsTree ?? defaults.componentsTree, }; }; @@ -92,11 +86,6 @@ export const updateDimensions = (): void => { }; }; -export interface SlowDowns { - slowDowns: number; - hideNotification: boolean; -} - export type WidgetStates = | { view: "none"; @@ -121,8 +110,6 @@ export const signalWidgetViews = signal({ view: "none", }); -const storedCollapsed = readLocalStorage( - LOCALSTORAGE_COLLAPSED_KEY -); +const storedCollapsed = readLocalStorage(LOCALSTORAGE_COLLAPSED_KEY); export const signalWidgetCollapsed = /* @__PURE__ */ signal(storedCollapsed ?? null); diff --git a/packages/scan/src/web/utils/create-store.ts b/packages/scan/src/web/utils/create-store.ts index 8f85a020..759f705d 100644 --- a/packages/scan/src/web/utils/create-store.ts +++ b/packages/scan/src/web/utils/create-store.ts @@ -6,12 +6,9 @@ * Do not modify unless you know what you are doing */ type SetStateInternal = { - _( - partial: T | Partial | { _(state: T): T | Partial }['_'], - replace?: false, - ): void; - _(state: T | { _(state: T): T }['_'], replace: true): void; -}['_']; + _(partial: T | Partial | { _(state: T): T | Partial }["_"], replace?: false): void; + _(state: T | { _(state: T): T }["_"], replace: true): void; +}["_"]; export interface StoreApi { setState: SetStateInternal; @@ -26,11 +23,9 @@ export interface StoreApi { }; } -export type ExtractState = S extends { getState: () => infer T } ? T : never; - type Get = K extends keyof T ? T[K] : F; -export type Mutate = number extends Ms['length' & keyof Ms] +export type Mutate = number extends Ms["length" & keyof Ms] ? S : Ms extends [] ? S @@ -44,8 +39,8 @@ export type StateCreator< Mos extends [StoreMutatorIdentifier, unknown][] = [], U = T, > = (( - setState: Get, Mis>, 'setState', never>, - getState: Get, Mis>, 'getState', never>, + setState: Get, Mis>, "setState", never>, + getState: Get, Mis>, "getState", never>, store: Mutate, Mis>, ) => U) & { $$storeMutators?: Mos }; @@ -63,10 +58,7 @@ type CreateStore = { ) => Mutate, Mos>; }; -type CreateStoreImpl = < - T, - Mos extends [StoreMutatorIdentifier, unknown][] = [], ->( +type CreateStoreImpl = ( initializer: StateCreator, ) => Mutate, Mos>; @@ -76,27 +68,24 @@ const createStoreImpl: CreateStoreImpl = (createState) => { let state: TState; const listeners: Set = new Set(); - const setState: StoreApi['setState'] = (partial, replace) => { + const setState: StoreApi["setState"] = (partial, replace) => { const nextState = - typeof partial === 'function' - ? (partial as (state: TState) => TState)(state) - : partial; + typeof partial === "function" ? (partial as (state: TState) => TState)(state) : partial; if (!Object.is(nextState, state)) { const previousState = state; state = - (replace ?? (typeof nextState !== 'object' || nextState === null)) + (replace ?? (typeof nextState !== "object" || nextState === null)) ? (nextState as TState) : Object.assign({}, state, nextState); listeners.forEach((listener) => listener(state, previousState)); } }; - const getState: StoreApi['getState'] = () => state; + const getState: StoreApi["getState"] = () => state; - const getInitialState: StoreApi['getInitialState'] = () => - initialState; + const getInitialState: StoreApi["getInitialState"] = () => initialState; - const subscribe: StoreApi['subscribe'] = ( + const subscribe: StoreApi["subscribe"] = ( selectorOrListener: | ((state: TState, prevState: TState) => void) // oxlint-disable-next-line typescript/no-explicit-any @@ -116,10 +105,7 @@ const createStoreImpl: CreateStoreImpl = (createState) => { actualListener = listener; } else { // Regular subscription case - actualListener = selectorOrListener as ( - state: TState, - prevState: TState, - ) => void; + actualListener = selectorOrListener as (state: TState, prevState: TState) => void; } let currentSlice = selector ? selector(state) : undefined; diff --git a/packages/scan/src/web/utils/helpers.ts b/packages/scan/src/web/utils/helpers.ts index d0624c3d..2eb44f35 100644 --- a/packages/scan/src/web/utils/helpers.ts +++ b/packages/scan/src/web/utils/helpers.ts @@ -5,35 +5,16 @@ import { SuspenseComponentTag, getDisplayName, hasMemoCache, -} from 'bippy'; -import { type ClassValue, clsx } from 'clsx'; -import { IS_CLIENT } from './constants'; -import { twMerge } from 'tailwind-merge'; +} from "bippy"; +import { type ClassValue, clsx } from "clsx"; +import { IS_CLIENT } from "./constants"; +import { twMerge } from "tailwind-merge"; export const cn = (...inputs: Array): string => { return twMerge(clsx(inputs)); }; -export const isFirefox = - /* @__PURE__ */ typeof navigator !== 'undefined' && - navigator.userAgent.includes('Firefox'); - -export const onIdle = (callback: () => void) => { - if ('scheduler' in globalThis) { - return globalThis.scheduler.postTask(callback, { - priority: 'background', - }); - } - if ('requestIdleCallback' in window) { - return requestIdleCallback(callback); - } - return setTimeout(callback, 0); -}; - -export const throttle = ( - callback: (e?: E) => void, - delay: number, -): ((e?: E) => void) => { +export const throttle = (callback: (e?: E) => void, delay: number): ((e?: E) => void) => { let lastCall = 0; return (e?: E) => { const now = Date.now(); @@ -45,14 +26,6 @@ export const throttle = ( }; }; -export const tryOrElse = (fn: () => T, defaultValue: T): T => { - try { - return fn(); - } catch { - return defaultValue; - } -}; - export const readLocalStorage = (storageKey: string): T | null => { if (!IS_CLIENT) return null; @@ -80,12 +53,12 @@ export const removeLocalStorage = (storageKey: string): void => { }; interface WrapperBadge { - type: 'memo' | 'forwardRef' | 'lazy' | 'suspense' | 'profiler' | 'strict'; + type: "memo" | "forwardRef" | "lazy" | "suspense" | "profiler" | "strict"; title: string; compiler?: boolean; } -export interface ExtendedDisplayName { +interface ExtendedDisplayName { name: string | null; wrappers: Array; wrapperTypes: Array; @@ -98,7 +71,7 @@ const ProfilerTag = 12; export const getExtendedDisplayName = (fiber: Fiber): ExtendedDisplayName => { if (!fiber) { return { - name: 'Unknown', + name: "Unknown", wrappers: [], wrapperTypes: [], }; @@ -113,42 +86,41 @@ export const getExtendedDisplayName = (fiber: Fiber): ExtendedDisplayName => { hasMemoCache(fiber) || tag === SimpleMemoComponentTag || tag === MemoComponentTag || - (type as { $$typeof?: symbol })?.$$typeof === Symbol.for('react.memo') || - (elementType as { $$typeof?: symbol })?.$$typeof === - Symbol.for('react.memo') + (type as { $$typeof?: symbol })?.$$typeof === Symbol.for("react.memo") || + (elementType as { $$typeof?: symbol })?.$$typeof === Symbol.for("react.memo") ) { const compiler = hasMemoCache(fiber); wrapperTypes.push({ - type: 'memo', + type: "memo", title: compiler - ? 'This component has been auto-memoized by the React Compiler.' - : 'Memoized component that skips re-renders if props are the same', + ? "This component has been auto-memoized by the React Compiler." + : "Memoized component that skips re-renders if props are the same", compiler, }); } if (tag === LazyComponentTag) { wrapperTypes.push({ - type: 'lazy', - title: 'Lazily loaded component that supports code splitting', + type: "lazy", + title: "Lazily loaded component that supports code splitting", }); } if (tag === SuspenseComponentTag) { wrapperTypes.push({ - type: 'suspense', - title: 'Component that can suspend while content is loading', + type: "suspense", + title: "Component that can suspend while content is loading", }); } if (tag === ProfilerTag) { wrapperTypes.push({ - type: 'profiler', - title: 'Component that measures rendering performance', + type: "profiler", + title: "Component that measures rendering performance", }); } - if (typeof name === 'string') { + if (typeof name === "string") { const wrapperRegex = /^(\w+)\((.*)\)$/; let currentName = name; while (wrapperRegex.test(currentName)) { @@ -164,7 +136,7 @@ export const getExtendedDisplayName = (fiber: Fiber): ExtendedDisplayName => { } return { - name: name || 'Unknown', + name: name || "Unknown", wrappers, wrapperTypes, }; diff --git a/packages/scan/src/web/utils/log.ts b/packages/scan/src/web/utils/log.ts index d7a4e4bb..5ce0e27b 100644 --- a/packages/scan/src/web/utils/log.ts +++ b/packages/scan/src/web/utils/log.ts @@ -1,6 +1,6 @@ // @ts-nocheck -import { ChangeReason, type Render } from '~core/instrumentation'; -import { getLabelText } from '~core/utils'; +import { ChangeReason, type Render } from "~core/instrumentation"; +import { getLabelText } from "~core/utils"; export const log = (renders: Array) => { const logMap = new Map< @@ -13,7 +13,6 @@ export const log = (renders: Array) => { if (!render.componentName) continue; const changeLog = logMap.get(render.componentName) ?? []; - renders; const labelText = getLabelText([ { aggregatedCount: 1, @@ -38,18 +37,17 @@ export const log = (renders: Array) => { if (render.changes) { for (let i = 0, len = render.changes.length; i < len; i++) { - const { name, prevValue, nextValue, unstable, type } = - render.changes[i]; + const { name, prevValue, nextValue, unstable, type } = render.changes[i]; if (type === ChangeReason.Props) { prevChangedProps ??= {}; nextChangedProps ??= {}; - prevChangedProps[`${unstable ? '⚠️' : ''}${name} (prev)`] = prevValue; - nextChangedProps[`${unstable ? '⚠️' : ''}${name} (next)`] = nextValue; + prevChangedProps[`${unstable ? "⚠️" : ""}${name} (prev)`] = prevValue; + nextChangedProps[`${unstable ? "⚠️" : ""}${name} (next)`] = nextValue; } else { changeLog.push({ prev: prevValue, next: nextValue, - type: type === ChangeReason.Context ? 'context' : 'state', + type: type === ChangeReason.Context ? "context" : "state", unstable: unstable ?? false, }); } @@ -60,7 +58,7 @@ export const log = (renders: Array) => { changeLog.push({ prev: prevChangedProps, next: nextChangedProps, - type: 'props', + type: "props", unstable: false, }); } @@ -69,13 +67,10 @@ export const log = (renders: Array) => { } for (const [name, changeLog] of Array.from(logMap.entries())) { // oxlint-disable-next-line no-console - console.group( - `%c${name}`, - 'background: hsla(0,0%,70%,.3); border-radius:3px; padding: 0 2px;', - ); + console.group(`%c${name}`, "background: hsla(0,0%,70%,.3); border-radius:3px; padding: 0 2px;"); for (const { type, prev, next, unstable } of changeLog) { // oxlint-disable-next-line no-console - console.log(`${type}:`, unstable ? '⚠️' : '', prev, '!==', next); + console.log(`${type}:`, unstable ? "⚠️" : "", prev, "!==", next); } // oxlint-disable-next-line no-console console.groupEnd(); @@ -89,8 +84,8 @@ export const logIntro = () => { } // oxlint-disable-next-line no-console console.log( - '%c[·] %cReact Scan', - 'font-weight:bold;color:#7a68e8;font-size:20px;', - 'font-weight:bold;font-size:14px;', + "%c[·] %cReact Scan", + "font-weight:bold;color:#7a68e8;font-size:20px;", + "font-weight:bold;font-size:14px;", ); }; diff --git a/packages/scan/src/web/views/inspector/components-tree/state.ts b/packages/scan/src/web/views/inspector/components-tree/state.ts index 8bb6f045..ed1d8eae 100644 --- a/packages/scan/src/web/views/inspector/components-tree/state.ts +++ b/packages/scan/src/web/views/inspector/components-tree/state.ts @@ -1,6 +1,6 @@ -import { signal } from '@preact/signals'; -import type { Fiber } from 'bippy'; -import type { RenderData } from '~core/instrumentation'; +import { signal } from "@preact/signals"; +import type { Fiber } from "bippy"; +import type { RenderData } from "~core/instrumentation"; export interface TreeNode { label: string; @@ -23,16 +23,9 @@ export const searchState = signal<{ matches: FlattenedNode[]; currentMatchIndex: number; }>({ - query: '', + query: "", matches: [], currentMatchIndex: -1, }); -export interface TreeItem { - name: string; - depth: number; - element: HTMLElement; - fiber: Fiber; -} - export const signalSkipTreeUpdate = /* @__PURE__ */ signal(false); diff --git a/packages/scan/src/web/views/inspector/overlay/index.tsx b/packages/scan/src/web/views/inspector/overlay/index.tsx index 55191e24..ee354ed3 100644 --- a/packages/scan/src/web/views/inspector/overlay/index.tsx +++ b/packages/scan/src/web/views/inspector/overlay/index.tsx @@ -1,10 +1,10 @@ -import { type Fiber, getDisplayName } from 'bippy'; -import { useEffect, useRef } from 'preact/hooks'; -import { ReactScanInternals, Store } from '~core/index'; +import { type Fiber, getDisplayName } from "bippy"; +import { useEffect, useRef } from "preact/hooks"; +import { ReactScanInternals, Store } from "~core/index"; -import { signalIsSettingsOpen, signalWidgetViews } from '~web/state'; -import { IS_CLIENT } from '~web/utils/constants'; -import { cn, throttle } from '~web/utils/helpers'; +import { signalIsSettingsOpen, signalWidgetViews } from "~web/state"; +import { IS_CLIENT } from "~web/utils/constants"; +import { cn, throttle } from "~web/utils/helpers"; const lerp = (start: number, end: number, t: number) => start + (end - start) * t; import { type States, @@ -12,9 +12,9 @@ import { getAssociatedFiberRect, getCompositeComponentFromElement, nonVisualTags, -} from '../utils'; +} from "../utils"; -type DrawKind = 'locked' | 'inspecting'; +type DrawKind = "locked" | "inspecting"; interface Rect { left: number; @@ -39,11 +39,7 @@ const ANIMATION_CONFIG = { }, } as const; -export const OVERLAY_DPR = IS_CLIENT - ? /* @__PURE__ */ window.devicePixelRatio || 1 - : 1; - -export const currentLockIconRect: LockIconRect | null = null; +const OVERLAY_DPR = IS_CLIENT ? /* @__PURE__ */ window.devicePixelRatio || 1 : 1; export const ScanOverlay = () => { const refCanvas = useRef(null); @@ -53,21 +49,14 @@ export const ScanOverlay = () => { const refLastHoveredElement = useRef(null); const refRafId = useRef(0); const refTimeout = useRef(); - const refCleanupMap = useRef( - new Map void>(), - ); + const refCleanupMap = useRef(new Map void>()); const refIsFadingOut = useRef(false); const refLastFrameTime = useRef(0); - const drawLockIcon = ( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - size: number, - ) => { + const drawLockIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number) => { ctx.save(); - ctx.strokeStyle = 'white'; - ctx.fillStyle = 'white'; + ctx.strokeStyle = "white"; + ctx.fillStyle = "white"; ctx.lineWidth = 1.5; const shackleWidth = size * 0.6; @@ -98,35 +87,33 @@ export const ScanOverlay = () => { const drawStatsPill = ( ctx: CanvasRenderingContext2D, rect: Rect, - kind: 'locked' | 'inspecting', + kind: "locked" | "inspecting", fiber: Fiber | null, ) => { if (!fiber) return; const pillHeight = 24; const pillPadding = 8; - const componentName = - (fiber?.type && getDisplayName(fiber.type)) ?? 'Unknown'; + const componentName = (fiber?.type && getDisplayName(fiber.type)) ?? "Unknown"; const text = componentName; ctx.save(); - ctx.font = '12px system-ui, -apple-system, sans-serif'; + ctx.font = "12px system-ui, -apple-system, sans-serif"; const textMetrics = ctx.measureText(text); const textWidth = textMetrics.width; - const lockIconSize = kind === 'locked' ? 14 : 0; - const lockIconPadding = kind === 'locked' ? 6 : 0; - const pillWidth = - textWidth + pillPadding * 2 + lockIconSize + lockIconPadding; + const lockIconSize = kind === "locked" ? 14 : 0; + const lockIconPadding = kind === "locked" ? 6 : 0; + const pillWidth = textWidth + pillPadding * 2 + lockIconSize + lockIconPadding; const pillX = rect.left; const pillY = rect.top - pillHeight - 4; - ctx.fillStyle = 'rgb(37, 37, 38, .75)'; + ctx.fillStyle = "rgb(37, 37, 38, .75)"; ctx.beginPath(); ctx.roundRect(pillX, pillY, pillWidth, pillHeight, 3); ctx.fill(); - if (kind === 'locked') { + if (kind === "locked") { const lockX = pillX + pillPadding; const lockY = pillY + (pillHeight - lockIconSize) / 2 + 2; drawLockIcon(ctx, lockX, lockY, lockIconSize); @@ -140,12 +127,9 @@ export const ScanOverlay = () => { refCurrentLockIconRect.current = null; } - ctx.fillStyle = 'white'; - ctx.textBaseline = 'middle'; - const textX = - pillX + - pillPadding + - (kind === 'locked' ? lockIconSize + lockIconPadding : 0); + ctx.fillStyle = "white"; + ctx.textBaseline = "middle"; + const textX = pillX + pillPadding + (kind === "locked" ? lockIconSize + lockIconPadding : 0); ctx.fillText(text, textX, pillY + pillHeight / 2); ctx.restore(); }; @@ -160,10 +144,10 @@ export const ScanOverlay = () => { const rect = refCurrentRect.current; ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.strokeStyle = 'rgba(142, 97, 227, 0.5)'; - ctx.fillStyle = 'rgba(173, 97, 230, 0.10)'; + ctx.strokeStyle = "rgba(142, 97, 227, 0.5)"; + ctx.fillStyle = "rgba(173, 97, 230, 0.10)"; - if (kind === 'locked') { + if (kind === "locked") { ctx.setLineDash([]); } else { ctx.setLineDash([4]); @@ -189,10 +173,7 @@ export const ScanOverlay = () => { const t = ANIMATION_CONFIG.speeds[speed] ?? ANIMATION_CONFIG.speeds.off; const animationFrame = (timestamp: number) => { - if ( - timestamp - refLastFrameTime.current < - ANIMATION_CONFIG.frameInterval - ) { + if (timestamp - refLastFrameTime.current < ANIMATION_CONFIG.frameInterval) { refRafId.current = requestAnimationFrame(animationFrame); return; } @@ -270,8 +251,7 @@ export const ScanOverlay = () => { ) => { if (!overlayElement || !canvas || !ctx) return; - const { parentCompositeFiber } = - getCompositeComponentFromElement(overlayElement); + const { parentCompositeFiber } = getCompositeComponentFromElement(overlayElement); const targetRect = await getAssociatedFiberRect(overlayElement); if (!parentCompositeFiber || !targetRect) return; @@ -286,14 +266,14 @@ export const ScanOverlay = () => { }; const cleanupCanvas = (canvas: HTMLCanvasElement) => { - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (ctx) { ctx.clearRect(0, 0, canvas.width, canvas.height); } refCurrentRect.current = null; refCurrentLockIconRect.current = null; refLastHoveredElement.current = null; - canvas.classList.remove('fade-in'); + canvas.classList.remove("fade-in"); refIsFadingOut.current = false; }; @@ -301,47 +281,37 @@ export const ScanOverlay = () => { if (!refCanvas.current || refIsFadingOut.current) return; const handleTransitionEnd = (e: TransitionEvent) => { - if ( - !refCanvas.current || - e.propertyName !== 'opacity' || - !refIsFadingOut.current - ) { + if (!refCanvas.current || e.propertyName !== "opacity" || !refIsFadingOut.current) { return; } - refCanvas.current.removeEventListener( - 'transitionend', - handleTransitionEnd, - ); + refCanvas.current.removeEventListener("transitionend", handleTransitionEnd); cleanupCanvas(refCanvas.current); onComplete?.(); }; - const existingListener = refCleanupMap.current.get('fade-out'); + const existingListener = refCleanupMap.current.get("fade-out"); if (existingListener) { existingListener(); - refCleanupMap.current.delete('fade-out'); + refCleanupMap.current.delete("fade-out"); } - refCanvas.current.addEventListener('transitionend', handleTransitionEnd); - refCleanupMap.current.set('fade-out', () => { - refCanvas.current?.removeEventListener( - 'transitionend', - handleTransitionEnd, - ); + refCanvas.current.addEventListener("transitionend", handleTransitionEnd); + refCleanupMap.current.set("fade-out", () => { + refCanvas.current?.removeEventListener("transitionend", handleTransitionEnd); }); refIsFadingOut.current = true; - refCanvas.current.classList.remove('fade-in'); + refCanvas.current.classList.remove("fade-in"); requestAnimationFrame(() => { - refCanvas.current?.classList.add('fade-out'); + refCanvas.current?.classList.add("fade-out"); }); }; const startFadeIn = () => { if (!refCanvas.current) return; refIsFadingOut.current = false; - refCanvas.current.classList.remove('fade-out'); + refCanvas.current.classList.remove("fade-out"); requestAnimationFrame(() => { - refCanvas.current?.classList.add('fade-in'); + refCanvas.current?.classList.add("fade-in"); }); }; @@ -357,17 +327,13 @@ export const ScanOverlay = () => { } Store.inspectState.value = { - kind: 'inspecting', + kind: "inspecting", hoveredDomElement: componentElement, }; }; const handleNonHoverableArea = () => { - if ( - !refCurrentRect.current || - !refCanvas.current || - refIsFadingOut.current - ) { + if (!refCurrentRect.current || !refCanvas.current || refIsFadingOut.current) { return; } @@ -376,19 +342,17 @@ export const ScanOverlay = () => { const handlePointerMove = throttle((e?: PointerEvent) => { const state = Store.inspectState.peek(); - if (state.kind !== 'inspecting' || !refEventCatcher.current) return; + if (state.kind !== "inspecting" || !refEventCatcher.current) return; - refEventCatcher.current.style.pointerEvents = 'none'; + refEventCatcher.current.style.pointerEvents = "none"; const element = document.elementFromPoint(e?.clientX ?? 0, e?.clientY ?? 0); - refEventCatcher.current.style.removeProperty('pointer-events'); + refEventCatcher.current.style.removeProperty("pointer-events"); clearTimeout(refTimeout.current); if (element && element !== refCanvas.current) { - const { parentCompositeFiber } = getCompositeComponentFromElement( - element as Element, - ); + const { parentCompositeFiber } = getCompositeComponentFromElement(element as Element); if (parentCompositeFiber) { const componentElement = findComponentDOMNode(parentCompositeFiber); if (componentElement) { @@ -422,24 +386,18 @@ export const ScanOverlay = () => { }; const handleLockIconClick = (state: States) => { - if (state.kind === 'focused') { + if (state.kind === "focused") { Store.inspectState.value = { - kind: 'inspecting', + kind: "inspecting", hoveredDomElement: state.focusedDomElement, }; } }; const handleElementClick = (e: MouseEvent) => { - const clickableElements = [ - 'react-scan-inspect-element', - 'react-scan-power', - ]; + const clickableElements = ["react-scan-inspect-element", "react-scan-power"]; // avoid capturing the synthetic event sent back to the toolbar, we don't want to block click events on it ever - if ( - e.target instanceof HTMLElement && - clickableElements.includes(e.target.id) - ) { + if (e.target instanceof HTMLElement && clickableElements.includes(e.target.id)) { return; } @@ -452,25 +410,19 @@ export const ScanOverlay = () => { e.stopPropagation(); const element = - refLastHoveredElement.current ?? - document.elementFromPoint(e.clientX, e.clientY); + refLastHoveredElement.current ?? document.elementFromPoint(e.clientX, e.clientY); if (!element) return; const clickedEl = e.composedPath().at(0); - if ( - clickedEl instanceof HTMLElement && - clickableElements.includes(clickedEl.id) - ) { + if (clickedEl instanceof HTMLElement && clickableElements.includes(clickedEl.id)) { const syntheticEvent = new MouseEvent(e.type, e); // @ts-ignore - this allows to know to not re-process this event when this event handler captures it syntheticEvent.__reactScanSyntheticEvent = true; clickedEl.dispatchEvent(syntheticEvent); return; } - const { parentCompositeFiber } = getCompositeComponentFromElement( - element as Element, - ); + const { parentCompositeFiber } = getCompositeComponentFromElement(element as Element); if (!parentCompositeFiber) return; const componentElement = findComponentDOMNode(parentCompositeFiber); @@ -478,13 +430,13 @@ export const ScanOverlay = () => { if (!componentElement) { refLastHoveredElement.current = null; Store.inspectState.value = { - kind: 'inspect-off', + kind: "inspect-off", }; return; } Store.inspectState.value = { - kind: 'focused', + kind: "focused", focusedDomElement: componentElement, fiber: parentCompositeFiber, }; @@ -507,46 +459,46 @@ export const ScanOverlay = () => { return; } - if (state.kind === 'inspecting') { + if (state.kind === "inspecting") { handleElementClick(e); } }; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key !== 'Escape') return; + if (e.key !== "Escape") return; const state = Store.inspectState.peek(); const canvas = refCanvas.current; if (!canvas) return; - if (document.activeElement?.id === 'react-scan-root') { + if (document.activeElement?.id === "react-scan-root") { return; } signalWidgetViews.value = { - view: 'none', + view: "none", }; - if (state.kind === 'focused' || state.kind === 'inspecting') { + if (state.kind === "focused" || state.kind === "inspecting") { e.preventDefault(); e.stopPropagation(); switch (state.kind) { - case 'focused': { + case "focused": { startFadeIn(); refCurrentRect.current = null; refLastHoveredElement.current = state.focusedDomElement; Store.inspectState.value = { - kind: 'inspecting', + kind: "inspecting", hoveredDomElement: state.focusedDomElement, }; break; } - case 'inspecting': { + case "inspecting": { startFadeOut(() => { signalIsSettingsOpen.value = false; Store.inspectState.value = { - kind: 'inspect-off', + kind: "inspect-off", }; }); break; @@ -563,8 +515,8 @@ export const ScanOverlay = () => { refCleanupMap.current.get(state.kind)?.(); if (refEventCatcher.current) { - if (state.kind !== 'inspecting') { - refEventCatcher.current.style.pointerEvents = 'none'; + if (state.kind !== "inspecting") { + refEventCatcher.current.style.pointerEvents = "none"; } } @@ -575,15 +527,15 @@ export const ScanOverlay = () => { let unsubReport: (() => void) | undefined; switch (state.kind) { - case 'inspect-off': + case "inspect-off": startFadeOut(); return; - case 'inspecting': - drawHoverOverlay(state.hoveredDomElement, canvas, ctx, 'inspecting'); + case "inspecting": + drawHoverOverlay(state.hoveredDomElement, canvas, ctx, "inspecting"); break; - case 'focused': + case "focused": if (!state.focusedDomElement) return; if (refLastHoveredElement.current !== state.focusedDomElement) { @@ -591,10 +543,10 @@ export const ScanOverlay = () => { } signalWidgetViews.value = { - view: 'inspector', + view: "inspector", }; - drawHoverOverlay(state.focusedDomElement, canvas, ctx, 'locked'); + drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); unsubReport = Store.lastReportTime.subscribe(() => { if (refRafId.current && refCurrentRect.current) { @@ -602,7 +554,7 @@ export const ScanOverlay = () => { state.focusedDomElement, ); if (parentCompositeFiber) { - drawHoverOverlay(state.focusedDomElement, canvas, ctx, 'locked'); + drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); } } }); @@ -614,10 +566,7 @@ export const ScanOverlay = () => { } }; - const updateCanvasSize = ( - canvas: HTMLCanvasElement, - ctx: CanvasRenderingContext2D, - ) => { + const updateCanvasSize = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => { const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * OVERLAY_DPR; canvas.height = rect.height * OVERLAY_DPR; @@ -629,7 +578,7 @@ export const ScanOverlay = () => { const state = Store.inspectState.peek(); const canvas = refCanvas.current; if (!canvas) return; - const ctx = canvas?.getContext('2d'); + const ctx = canvas?.getContext("2d"); if (!ctx) return; cancelAnimationFrame(refRafId.current); @@ -638,10 +587,10 @@ export const ScanOverlay = () => { updateCanvasSize(canvas, ctx); refCurrentRect.current = null; - if (state.kind === 'focused' && state.focusedDomElement) { - drawHoverOverlay(state.focusedDomElement, canvas, ctx, 'locked'); - } else if (state.kind === 'inspecting' && state.hoveredDomElement) { - drawHoverOverlay(state.hoveredDomElement, canvas, ctx, 'inspecting'); + if (state.kind === "focused" && state.focusedDomElement) { + drawHoverOverlay(state.focusedDomElement, canvas, ctx, "locked"); + } else if (state.kind === "inspecting" && state.hoveredDomElement) { + drawHoverOverlay(state.hoveredDomElement, canvas, ctx, "inspecting"); } }; @@ -650,10 +599,7 @@ export const ScanOverlay = () => { const canvas = refCanvas.current; if (!canvas) return; - if ( - state.kind === 'inspecting' || - isClickInLockIcon(e as unknown as MouseEvent, canvas) - ) { + if (state.kind === "inspecting" || isClickInLockIcon(e as unknown as MouseEvent, canvas)) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); @@ -664,7 +610,7 @@ export const ScanOverlay = () => { useEffect(() => { const canvas = refCanvas.current; if (!canvas) return; - const ctx = canvas?.getContext('2d'); + const ctx = canvas?.getContext("2d"); if (!ctx) return; updateCanvasSize(canvas, ctx); @@ -673,31 +619,31 @@ export const ScanOverlay = () => { handleStateChange(state, canvas, ctx); }); - window.addEventListener('scroll', handleResizeOrScroll, { passive: true }); - window.addEventListener('resize', handleResizeOrScroll, { passive: true }); - document.addEventListener('pointermove', handlePointerMove, { + window.addEventListener("scroll", handleResizeOrScroll, { passive: true }); + window.addEventListener("resize", handleResizeOrScroll, { passive: true }); + document.addEventListener("pointermove", handlePointerMove, { passive: true, capture: true, }); - document.addEventListener('pointerdown', handlePointerDown, { + document.addEventListener("pointerdown", handlePointerDown, { capture: true, }); - document.addEventListener('click', handleClick, { capture: true }); - document.addEventListener('keydown', handleKeyDown, { capture: true }); + document.addEventListener("click", handleClick, { capture: true }); + document.addEventListener("keydown", handleKeyDown, { capture: true }); return () => { unsubscribeAll(); unSubState(); - window.removeEventListener('scroll', handleResizeOrScroll); - window.removeEventListener('resize', handleResizeOrScroll); - document.removeEventListener('pointermove', handlePointerMove, { + window.removeEventListener("scroll", handleResizeOrScroll); + window.removeEventListener("resize", handleResizeOrScroll); + document.removeEventListener("pointermove", handlePointerMove, { capture: true, }); - document.removeEventListener('click', handleClick, { capture: true }); - document.removeEventListener('pointerdown', handlePointerDown, { + document.removeEventListener("click", handleClick, { capture: true }); + document.removeEventListener("pointerdown", handlePointerDown, { capture: true, }); - document.removeEventListener('keydown', handleKeyDown, { capture: true }); + document.removeEventListener("keydown", handleKeyDown, { capture: true }); if (refRafId.current) { cancelAnimationFrame(refRafId.current); @@ -710,20 +656,20 @@ export const ScanOverlay = () => { <>
diff --git a/packages/scan/src/web/views/inspector/states.ts b/packages/scan/src/web/views/inspector/states.ts index 38ecda97..ad02be7c 100644 --- a/packages/scan/src/web/views/inspector/states.ts +++ b/packages/scan/src/web/views/inspector/states.ts @@ -1,8 +1,7 @@ -import { signal } from '@preact/signals'; -import type { Fiber } from 'bippy'; -import type { ComponentType } from 'preact'; -import { flashManager } from './flash-overlay'; -import { type SectionData, resetTracking } from './timeline/utils'; +import { signal } from "@preact/signals"; +import type { Fiber } from "bippy"; +import type { ComponentType } from "preact"; +import type { SectionData } from "./timeline/utils"; export interface MinimalFiberInfo { id?: string | number; @@ -36,7 +35,7 @@ export interface TimelineState { export const TIMELINE_MAX_UPDATES = 1000; -export const timelineStateDefault: TimelineState = { +const timelineStateDefault: TimelineState = { updates: [], currentFiber: null, totalUpdates: 0, @@ -60,8 +59,7 @@ const batchUpdates = () => { const batchedUpdates = [...pendingUpdates]; - const { updates, totalUpdates, currentIndex, isViewingHistory } = - timelineState.value; + const { updates, totalUpdates, currentIndex, isViewingHistory } = timelineState.value; const newUpdates = [...updates]; let newTotalUpdates = totalUpdates; @@ -132,7 +130,7 @@ export const timelineActions = { }; }, - updatePlaybackSpeed: (speed: TimelineState['playbackSpeed']) => { + updatePlaybackSpeed: (speed: TimelineState["playbackSpeed"]) => { timelineState.value = { ...timelineState.value, playbackSpeed: speed, @@ -166,15 +164,3 @@ export const timelineActions = { timelineState.value = timelineStateDefault; }, }; - -export const globalInspectorState = { - lastRendered: new Map(), - expandedPaths: new Set(), - cleanup: () => { - globalInspectorState.lastRendered.clear(); - globalInspectorState.expandedPaths.clear(); - flashManager.cleanupAll(); - resetTracking(); - timelineState.value = timelineStateDefault; - }, -}; diff --git a/packages/scan/src/web/views/inspector/timeline/utils.ts b/packages/scan/src/web/views/inspector/timeline/utils.ts index a1463c4c..6d47212e 100644 --- a/packages/scan/src/web/views/inspector/timeline/utils.ts +++ b/packages/scan/src/web/views/inspector/timeline/utils.ts @@ -7,9 +7,9 @@ import { MemoComponentTag, type MemoizedState, SimpleMemoComponentTag, -} from 'bippy'; -import { isEqual } from '~core/utils'; -import { getChangedPropsDetailed, isPromise } from '../utils'; +} from "bippy"; +import { isEqual } from "~core/utils"; +import { getChangedPropsDetailed, isPromise } from "../utils"; interface ChangeTrackingInfo { count: number; @@ -26,14 +26,12 @@ const contextTracker = new Map(); let lastComponentType: unknown = null; const STATE_NAME_REGEX = /\[(?\w+),\s*set\w+\]/g; -const PROPS_ORDER_REGEX = /\(\s*{\s*(?[^}]+)\s*}\s*\)/; - export const getStateNames = (fiber: Fiber): Array => { - const componentSource = fiber.type?.toString?.() || ''; + const componentSource = fiber.type?.toString?.() || ""; return componentSource ? Array.from( componentSource.matchAll(STATE_NAME_REGEX), - (m: RegExpMatchArray) => m.groups?.name ?? '', + (m: RegExpMatchArray) => m.groups?.name ?? "", ) : []; }; @@ -45,13 +43,13 @@ export const resetTracking = () => { lastComponentType = null; }; -export const isInitialComponentUpdate = (fiber: Fiber): boolean => { +const isInitialComponentUpdate = (fiber: Fiber): boolean => { const isNewComponent = fiber.type !== lastComponentType; lastComponentType = fiber.type; return isNewComponent; }; -export const trackChange = ( +const trackChange = ( tracker: Map, key: ChangeKey, currentValue: unknown, @@ -104,9 +102,7 @@ export interface InspectorData { fiberContext: SectionData; } -export const getStateFromFiber = ( - fiber: Fiber, -): Record => { +const getStateFromFiber = (fiber: Fiber): Record => { if (!fiber) return {}; if ( @@ -137,20 +133,6 @@ export const getStateFromFiber = ( return {}; }; -/** - * Used to preserve the order of the fiber's props as represented in source code - */ -export const getPropsOrder = (fiber: Fiber): Array => { - const componentSource = fiber.type?.toString?.() || ''; - const match = componentSource.match(PROPS_ORDER_REGEX); - if (!match?.groups?.props) return []; - - return match.groups.props - .split(',') - .map((prop: string) => prop.trim().split(':')[0].split('=')[0].trim()) - .filter(Boolean); -}; - export interface InspectorDataResult { data: InspectorData; shouldUpdate: boolean; @@ -181,9 +163,7 @@ interface CollectorResult { changes: Array; } -export const collectPropsChanges = ( - fiber: Fiber, -): CollectorResult => { +export const collectPropsChanges = (fiber: Fiber): CollectorResult => { const currentProps = fiber.memoizedProps || {}; const prevProps = fiber.alternate?.memoizedProps || {}; @@ -207,9 +187,7 @@ export const collectPropsChanges = ( return { current, prev, changes }; }; -export const collectStateChanges = ( - fiber: Fiber, -): CollectorResult => { +export const collectStateChanges = (fiber: Fiber): CollectorResult => { const current = getStateFromFiber(fiber); const prev = fiber.alternate ? getStateFromFiber(fiber.alternate) : {}; const changes: Array = []; @@ -228,13 +206,9 @@ export const collectStateChanges = ( return { current, prev, changes }; }; -export const collectContextChanges = ( - fiber: Fiber, -): CollectorResult => { +export const collectContextChanges = (fiber: Fiber): CollectorResult => { const currentContexts = getAllFiberContexts(fiber); - const prevContexts = fiber.alternate - ? getAllFiberContexts(fiber.alternate) - : new Map(); + const prevContexts = fiber.alternate ? getAllFiberContexts(fiber.alternate) : new Map(); const current: Record = {}; const prev: Record = {}; @@ -295,9 +269,7 @@ export const collectInspectorData = (fiber: Fiber): InspectorDataResult => { for (const [key, value] of Object.entries(current)) { propsData.current.push({ name: key, - value: isPromise(value) - ? { type: 'promise', displayValue: 'Promise' } - : value, + value: isPromise(value) ? { type: "promise", displayValue: "Promise" } : value, }); } @@ -318,8 +290,7 @@ export const collectInspectorData = (fiber: Fiber): InspectorDataResult => { } const stateData = emptySection(); - const { current: stateCurrent, changes: stateChanges } = - collectStateChanges(fiber); + const { current: stateCurrent, changes: stateChanges } = collectStateChanges(fiber); for (const [index, value] of Object.entries(stateCurrent)) { const stateKey = fiber.tag === ClassComponentTag ? index : Number(index); @@ -342,8 +313,7 @@ export const collectInspectorData = (fiber: Fiber): InspectorDataResult => { } const contextData = emptySection(); - const { current: contextCurrent, changes: contextChanges } = - collectContextChanges(fiber); + const { current: contextCurrent, changes: contextChanges } = collectContextChanges(fiber); for (const [name, value] of Object.entries(contextCurrent)) { contextData.current.push({ name, value }); @@ -397,9 +367,7 @@ interface ContextInfo { // the motivation is this fiber traversal on every rendering fiber is extremely expensive const fiberContextsCache = new WeakMap>(); -export const getAllFiberContexts = ( - fiber: Fiber, -): Map => { +export const getAllFiberContexts = (fiber: Fiber): Map => { if (!fiber) { return new Map(); } @@ -418,8 +386,7 @@ export const getAllFiberContexts = ( const dependencies = currentFiber.dependencies; if (dependencies?.firstContext) { - let contextItem: ContextDependency | null = - dependencies.firstContext; + let contextItem: ContextDependency | null = dependencies.firstContext; while (contextItem) { const memoizedValue = contextItem.memoizedValue; @@ -428,7 +395,7 @@ export const getAllFiberContexts = ( if (!contexts.has(memoizedValue)) { contexts.set(contextItem.context, { value: memoizedValue, - displayName: displayName ?? 'UnnamedContext', + displayName: displayName ?? "UnnamedContext", contextType: null, }); } @@ -474,9 +441,7 @@ export const collectInspectorDataWithoutCounts = (fiber: Fiber) => { for (const [key, value] of Object.entries(current)) { propsData.current.push({ name: key, - value: isPromise(value) - ? { type: 'promise', displayValue: 'Promise' } - : value, + value: isPromise(value) ? { type: "promise", displayValue: "Promise" } : value, }); } @@ -494,9 +459,7 @@ export const collectInspectorDataWithoutCounts = (fiber: Fiber) => { for (const [key, value] of Object.entries(current)) { stateData.current.push({ name: key, - value: isPromise(value) - ? { type: 'promise', displayValue: 'Promise' } - : value, + value: isPromise(value) ? { type: "promise", displayValue: "Promise" } : value, }); } @@ -513,9 +476,7 @@ export const collectInspectorDataWithoutCounts = (fiber: Fiber) => { for (const [key, value] of Object.entries(current)) { contextData.current.push({ name: key, - value: isPromise(value) - ? { type: 'promise', displayValue: 'Promise' } - : value, + value: isPromise(value) ? { type: "promise", displayValue: "Promise" } : value, }); } diff --git a/packages/scan/src/web/views/inspector/utils.ts b/packages/scan/src/web/views/inspector/utils.ts index 6db17bfe..4207bb76 100644 --- a/packages/scan/src/web/views/inspector/utils.ts +++ b/packages/scan/src/web/views/inspector/utils.ts @@ -7,15 +7,15 @@ import { isCompositeFiber, isHostFiber, traverseFiber, -} from 'bippy'; -import { type PropsChange, ReactScanInternals } from '~core/index'; -import { ChangeReason } from '~core/instrumentation'; -import { isEqual } from '~core/utils'; -import { globalInspectorState } from '.'; -import type { ExtendedReactRenderer } from '../../../types'; -import { TIMELINE_MAX_UPDATES } from './states'; -import type { MinimalFiberInfo } from './states'; -import { getAllFiberContexts, getStateNames } from './timeline/utils'; +} from "bippy"; +import { type PropsChange, ReactScanInternals } from "~core/index"; +import { ChangeReason } from "~core/instrumentation"; +import { isEqual } from "~core/utils"; +import { globalInspectorState } from "."; +import type { ExtendedReactRenderer } from "../../../types"; +import { TIMELINE_MAX_UPDATES } from "./states"; +import type { MinimalFiberInfo } from "./states"; +import { getAllFiberContexts, getStateNames } from "./timeline/utils"; interface StateItem { name: string; @@ -25,19 +25,19 @@ interface StateItem { // todo, change this to currently focused fiber export type States = | { - kind: 'inspecting'; + kind: "inspecting"; hoveredDomElement: Element | null; } | { - kind: 'inspect-off'; + kind: "inspect-off"; } | { - kind: 'focused'; + kind: "focused"; focusedDomElement: Element; fiber: Fiber; } | { - kind: 'uninitialized'; + kind: "uninitialized"; }; interface ReactRootContainer { @@ -55,7 +55,7 @@ interface ReactInternalProps { } export const getFiberFromElement = (element: Element): Fiber | null => { - if ('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window) { + if ("__REACT_DEVTOOLS_GLOBAL_HOOK__" in window) { const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (!hook?.renderers) return null; @@ -69,17 +69,14 @@ export const getFiberFromElement = (element: Element): Fiber | null => { } } - if ('_reactRootContainer' in element) { + if ("_reactRootContainer" in element) { const elementWithRoot = element as unknown as ReactRootContainer; const rootContainer = elementWithRoot._reactRootContainer; return rootContainer?._internalRoot?.current?.child ?? null; } for (const key in element) { - if ( - key.startsWith('__reactInternalInstance$') || - key.startsWith('__reactFiber') - ) { + if (key.startsWith("__reactInternalInstance$") || key.startsWith("__reactFiber")) { const elementWithFiber = element as unknown as ReactInternalProps; return elementWithFiber[key]; } @@ -87,7 +84,7 @@ export const getFiberFromElement = (element: Element): Fiber | null => { return null; }; -export const getFirstStateNode = (fiber: Fiber): Element | null => { +const getFirstStateNode = (fiber: Fiber): Element | null => { let current: Fiber | null = fiber; while (current) { if (current.stateNode instanceof Element) { @@ -113,9 +110,7 @@ export const getFirstStateNode = (fiber: Fiber): Element | null => { return null; }; -export const getNearestFiberFromElement = ( - element: Element | null, -): Fiber | null => { +const getNearestFiberFromElement = (element: Element | null): Fiber | null => { if (!element) return null; try { @@ -129,9 +124,7 @@ export const getNearestFiberFromElement = ( } }; -export const getParentCompositeFiber = ( - fiber: Fiber, -): readonly [Fiber, Fiber | null] | null => { +export const getParentCompositeFiber = (fiber: Fiber): readonly [Fiber, Fiber | null] | null => { let current: Fiber | null = fiber; let prevHost: Fiber | null = null; @@ -144,7 +137,6 @@ export const getParentCompositeFiber = ( return null; }; - const isFiberInTree = (fiber: Fiber, root: Fiber): boolean => { { // const root= fiberRootCache.get(fiber) || (fiber.alternate && fiberRootCache.get(fiber.alternate) ) @@ -157,7 +149,7 @@ const isFiberInTree = (fiber: Fiber, root: Fiber): boolean => { } }; -export const isCurrentTree = (fiber: Fiber) => { +const isCurrentTree = (fiber: Fiber) => { let curr: Fiber | null = fiber; let rootFiber: Fiber | null = null; @@ -226,10 +218,7 @@ export const getCompositeComponentFromElement = (element: Element) => { }; }; -export const getCompositeFiberFromElement = ( - element: Element, - knownFiber?: Fiber, -) => { +export const getCompositeFiberFromElement = (element: Element, knownFiber?: Fiber) => { if (!element.isConnected) return {}; let fiber = knownFiber ?? getNearestFiberFromElement(element); @@ -256,9 +245,7 @@ export const getCompositeFiberFromElement = ( if (!rootFiber || !currentRootFiber) return {}; // Get the current associated fiber using cached root - fiber = isFiberInTree(fiber, currentRootFiber) - ? fiber - : (fiber.alternate ?? fiber); + fiber = isFiberInTree(fiber, currentRootFiber) ? fiber : (fiber.alternate ?? fiber); if (!fiber) return {}; if (!getFirstStateNode(fiber)) return {}; @@ -281,7 +268,7 @@ export const getChangedPropsDetailed = (fiber: Fiber): Array => { const changes: Array = []; for (const key in currentProps) { - if (key === 'children') continue; + if (key === "children") continue; const currentValue = currentProps[key]; const prevValue = previousProps[key]; @@ -300,27 +287,21 @@ export const getChangedPropsDetailed = (fiber: Fiber): Array => { }; export interface OverrideMethods { - overrideProps: - | ((fiber: Fiber, path: string[], value: unknown) => void) - | null; - overrideHookState: - | ((fiber: Fiber, id: string, path: string[], value: unknown) => void) - | null; - overrideContext: - | ((fiber: Fiber, contextType: unknown, value: unknown) => void) - | null; + overrideProps: ((fiber: Fiber, path: string[], value: unknown) => void) | null; + overrideHookState: ((fiber: Fiber, id: string, path: string[], value: unknown) => void) | null; + overrideContext: ((fiber: Fiber, contextType: unknown, value: unknown) => void) | null; } const isRecord = (value: unknown): value is Record => { - return value !== null && typeof value === 'object'; + return value !== null && typeof value === "object"; }; -export const getOverrideMethods = (): OverrideMethods => { - let overrideProps: OverrideMethods['overrideProps'] = null; - let overrideHookState: OverrideMethods['overrideHookState'] = null; - let overrideContext: OverrideMethods['overrideContext'] = null; +const getOverrideMethods = (): OverrideMethods => { + let overrideProps: OverrideMethods["overrideProps"] = null; + let overrideHookState: OverrideMethods["overrideHookState"] = null; + let overrideContext: OverrideMethods["overrideContext"] = null; - if ('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window) { + if ("__REACT_DEVTOOLS_GLOBAL_HOOK__" in window) { const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (!hook?.renderers) { return { @@ -336,12 +317,7 @@ export const getOverrideMethods = (): OverrideMethods => { if (overrideHookState) { const prevOverrideHookState = overrideHookState; - overrideHookState = ( - fiber: Fiber, - id: string, - path: string[], - value: unknown, - ) => { + overrideHookState = (fiber: Fiber, id: string, path: string[], value: unknown) => { // Find the hook let current = fiber.memoizedState; for (let i = 0; i < Number(id); i++) { @@ -352,7 +328,7 @@ export const getOverrideMethods = (): OverrideMethods => { if (current?.queue) { // Update through React's queue mechanism const queue = current.queue; - if (isRecord(queue) && 'dispatch' in queue) { + if (isRecord(queue) && "dispatch" in queue) { const dispatch = queue.dispatch as (value: unknown) => void; dispatch(value); return; @@ -370,11 +346,7 @@ export const getOverrideMethods = (): OverrideMethods => { if (overrideProps) { const prevOverrideProps = overrideProps; - overrideProps = ( - fiber: Fiber, - path: Array, - value: unknown, - ) => { + overrideProps = (fiber: Fiber, path: Array, value: unknown) => { // Chain updates through all renderers to maintain consistency prevOverrideProps(fiber, path, value); devToolsRenderer.overrideProps?.(fiber, path, value); @@ -385,11 +357,7 @@ export const getOverrideMethods = (): OverrideMethods => { // For context, we don't need the chaining pattern since we're using overrideProps internally // to update the context provider's value prop, which already handles the chaining - overrideContext = ( - fiber: Fiber, - contextType: unknown, - value: unknown, - ) => { + overrideContext = (fiber: Fiber, contextType: unknown, value: unknown) => { // Find the provider fiber for this context let current: Fiber | null = fiber; while (current) { @@ -397,9 +365,9 @@ export const getOverrideMethods = (): OverrideMethods => { if (type === contextType || type?.Provider === contextType) { // Found the provider, update both current and alternate fibers if (overrideProps) { - overrideProps(current, ['value'], value); + overrideProps(current, ["value"], value); if (current.alternate) { - overrideProps(current.alternate, ['value'], value); + overrideProps(current.alternate, ["value"], value); } } break; @@ -417,35 +385,35 @@ export const getOverrideMethods = (): OverrideMethods => { }; export const nonVisualTags = new Set([ - 'HTML', - 'HEAD', - 'META', - 'TITLE', - 'BASE', - 'SCRIPT', - 'SCRIPT', - 'STYLE', - 'LINK', - 'NOSCRIPT', - 'SOURCE', - 'TRACK', - 'EMBED', - 'OBJECT', - 'PARAM', - 'TEMPLATE', - 'PORTAL', - 'SLOT', - 'AREA', - 'XML', - 'DOCTYPE', - 'COMMENT', + "HTML", + "HEAD", + "META", + "TITLE", + "BASE", + "SCRIPT", + "SCRIPT", + "STYLE", + "LINK", + "NOSCRIPT", + "SOURCE", + "TRACK", + "EMBED", + "OBJECT", + "PARAM", + "TEMPLATE", + "PORTAL", + "SLOT", + "AREA", + "XML", + "DOCTYPE", + "COMMENT", ]); export const findComponentDOMNode = ( fiber: Fiber, excludeNonVisualTags = true, ): HTMLElement | null => { - if (fiber.stateNode && 'nodeType' in fiber.stateNode) { + if (fiber.stateNode && "nodeType" in fiber.stateNode) { const element = fiber.stateNode as HTMLElement; if ( excludeNonVisualTags && @@ -479,9 +447,7 @@ export const getInspectableElements = ( ): Array => { const result: Array = []; - const findInspectableFiber = ( - element: HTMLElement | null, - ): HTMLElement | null => { + const findInspectableFiber = (element: HTMLElement | null): HTMLElement | null => { if (!element) return null; const { parentCompositeFiber } = getCompositeComponentFromElement(element); @@ -494,15 +460,14 @@ export const getInspectableElements = ( const traverse = (element: HTMLElement, depth = 0) => { const inspectable = findInspectableFiber(element); if (inspectable) { - const { parentCompositeFiber } = - getCompositeComponentFromElement(inspectable); + const { parentCompositeFiber } = getCompositeComponentFromElement(inspectable); if (!parentCompositeFiber) return; result.push({ element: inspectable, depth, - name: getDisplayName(parentCompositeFiber.type) ?? 'Unknown', + name: getDisplayName(parentCompositeFiber.type) ?? "Unknown", fiber: parentCompositeFiber, }); } @@ -519,14 +484,10 @@ export const getInspectableElements = ( const fiberMap = new WeakMap(); -export const getInspectableAncestors = ( - element: HTMLElement, -): Array => { +const getInspectableAncestors = (element: HTMLElement): Array => { const result: Array = []; - const findInspectableFiber = ( - element: HTMLElement | null, - ): HTMLElement | null => { + const findInspectableFiber = (element: HTMLElement | null): HTMLElement | null => { if (!element) return null; const { parentCompositeFiber } = getCompositeComponentFromElement(element); if (!parentCompositeFiber) return null; @@ -550,7 +511,7 @@ export const getInspectableAncestors = ( result.unshift({ element: inspectable, depth: 0, - name: getDisplayName(fiber.type) ?? 'Unknown', + name: getDisplayName(fiber.type) ?? "Unknown", fiber, }); } @@ -562,7 +523,7 @@ export const getInspectableAncestors = ( }; type DiffResult = { - type: 'primitive' | 'reference' | 'object'; + type: "primitive" | "reference" | "object"; changes: Array<{ path: string[]; prevValue: unknown; @@ -607,8 +568,8 @@ export type AggregatedChanges = { name: string; }; -export const isExpandable = (value: unknown): value is InspectableValue => { - if (value === null || typeof value !== 'object' || isPromise(value)) { +const isExpandable = (value: unknown): value is InspectableValue => { + if (value === null || typeof value !== "object" || isPromise(value)) { return false; } @@ -635,29 +596,22 @@ export const isExpandable = (value: unknown): value is InspectableValue => { return Object.keys(value).length > 0; }; -export const isEditableValue = ( - value: unknown, - parentPath?: string, -): boolean => { +const isEditableValue = (value: unknown, parentPath?: string): boolean => { if (value == null) return true; if (isPromise(value)) return false; - if (typeof value === 'function') { + if (typeof value === "function") { return false; } if (parentPath) { - const parts = parentPath.split('.'); - let currentPath = ''; + const parts = parentPath.split("."); + let currentPath = ""; for (const part of parts) { currentPath = currentPath ? `${currentPath}.${part}` : part; const obj = globalInspectorState.lastRendered.get(currentPath); - if ( - obj instanceof DataView || - obj instanceof ArrayBuffer || - ArrayBuffer.isView(obj) - ) { + if (obj instanceof DataView || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) { return false; } } @@ -670,10 +624,10 @@ export const isEditableValue = ( return true; default: switch (typeof value) { - case 'string': - case 'number': - case 'boolean': - case 'bigint': + case "string": + case "number": + case "boolean": + case "bigint": return true; default: return false; @@ -681,7 +635,7 @@ export const isEditableValue = ( } }; -export const getPath = ( +const getPath = ( componentName: string, section: string, parentPath: string, @@ -691,53 +645,53 @@ export const getPath = ( return `${componentName}.${parentPath}.${key}`; } - if (section === 'context' && !key.startsWith('context.')) { + if (section === "context" && !key.startsWith("context.")) { return `${componentName}.${section}.context.${key}`; } return `${componentName}.${section}.${key}`; }; -export const sanitizeString = (value: string): string => { +const sanitizeString = (value: string): string => { return value - .replace(/[<>]/g, '') - .replace(/javascript:/gi, '') - .replace(/data:/gi, '') - .replace(/on\w+=/gi, '') + .replace(/[<>]/g, "") + .replace(/javascript:/gi, "") + .replace(/data:/gi, "") + .replace(/on\w+=/gi, "") .slice(0, 50000); }; -export const sanitizeErrorMessage = (error: string): string => { +const sanitizeErrorMessage = (error: string): string => { return error - .replace(/[<>]/g, '') - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\//g, '/'); + .replace(/[<>]/g, "") + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/\//g, "/"); }; -export const formatValue = (value: unknown): string => { +const formatValue = (value: unknown): string => { const metadata = ensureRecord(value); return metadata.displayValue as string; }; export const formatForClipboard = (value: unknown): string => { try { - if (value === null) return 'null'; - if (value === undefined) return 'undefined'; - if (isPromise(value)) return 'Promise'; + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (isPromise(value)) return "Promise"; - if (typeof value === 'function') { + if (typeof value === "function") { const fnStr = value.toString(); try { const formatted = fnStr - .replace(/\s+/g, ' ') // Normalize whitespace - .replace(/{\s+/g, '{\n ') // Add newline after { - .replace(/;\s+/g, ';\n ') // Add newline after ; - .replace(/}\s*$/g, '\n}') // Add newline before final } - .replace(/\(\s+/g, '(') // Remove space after ( - .replace(/\s+\)/g, ')') // Remove space before ) - .replace(/,\s+/g, ', '); // Normalize comma spacing + .replace(/\s+/g, " ") // Normalize whitespace + .replace(/{\s+/g, "{\n ") // Add newline after { + .replace(/;\s+/g, ";\n ") // Add newline after ; + .replace(/}\s*$/g, "\n}") // Add newline before final } + .replace(/\(\s+/g, "(") // Remove space after ( + .replace(/\s+\)/g, ")") // Remove space before ) + .replace(/,\s+/g, ", "); // Normalize comma spacing return formatted; } catch { @@ -757,22 +711,14 @@ export const formatForClipboard = (value: unknown): string => { case value instanceof Set: return JSON.stringify(Array.from(value), null, 2); case value instanceof DataView: - return JSON.stringify( - Array.from(new Uint8Array(value.buffer)), - null, - 2, - ); + return JSON.stringify(Array.from(new Uint8Array(value.buffer)), null, 2); case value instanceof ArrayBuffer: return JSON.stringify(Array.from(new Uint8Array(value)), null, 2); - case ArrayBuffer.isView(value) && 'length' in value: - return JSON.stringify( - Array.from(value as unknown as ArrayLike), - null, - 2, - ); + case ArrayBuffer.isView(value) && "length" in value: + return JSON.stringify(Array.from(value as unknown as ArrayLike), null, 2); case Array.isArray(value): return JSON.stringify(value, null, 2); - case typeof value === 'object': + case typeof value === "object": return JSON.stringify(value, null, 2); default: return String(value); @@ -782,11 +728,11 @@ export const formatForClipboard = (value: unknown): string => { } }; -export const parseArrayValue = (value: string): Array => { - if (value.trim() === '[]') return []; +const parseArrayValue = (value: string): Array => { + if (value.trim() === "[]") return []; const result: Array = []; - let current = ''; + let current = ""; let depth = 0; let inString = false; let escapeNext = false; @@ -800,7 +746,7 @@ export const parseArrayValue = (value: string): Array => { continue; } - if (char === '\\') { + if (char === "\\") { escapeNext = true; } @@ -815,23 +761,23 @@ export const parseArrayValue = (value: string): Array => { continue; } - if (char === '[' || char === '{') { + if (char === "[" || char === "{") { depth++; current += char; continue; } - if (char === ']' || char === '}') { + if (char === "]" || char === "}") { depth--; current += char; continue; } - if (char === ',' && depth === 0) { + if (char === "," && depth === 0) { if (current.trim()) { - result.push(parseValue(current.trim(), '')); + result.push(parseValue(current.trim(), "")); } - current = ''; + current = ""; continue; } @@ -839,26 +785,26 @@ export const parseArrayValue = (value: string): Array => { } if (current.trim()) { - result.push(parseValue(current.trim(), '')); + result.push(parseValue(current.trim(), "")); } return result; }; -export const parseValue = (value: string, currentType: unknown): unknown => { +const parseValue = (value: string, currentType: unknown): unknown => { try { switch (typeof currentType) { - case 'number': + case "number": return Number(value); - case 'string': + case "string": return value; - case 'boolean': - return value === 'true'; - case 'bigint': + case "boolean": + return value === "true"; + case "bigint": return BigInt(value); - case 'undefined': + case "undefined": return undefined; - case 'object': { + case "object": { if (!currentType) { return null; } @@ -882,13 +828,10 @@ export const parseValue = (value: string, currentType: unknown): unknown => { if (currentType instanceof Map) { const entries = value .slice(1, -1) - .split(', ') + .split(", ") .map((entry) => { - const [key, val] = entry.split(' => '); - return [parseValue(key, ''), parseValue(val, '')] as [ - unknown, - unknown, - ]; + const [key, val] = entry.split(" => "); + return [parseValue(key, ""), parseValue(val, "")] as [unknown, unknown]; }); return new Map(entries); } @@ -896,16 +839,16 @@ export const parseValue = (value: string, currentType: unknown): unknown => { if (currentType instanceof Set) { const values = value .slice(1, -1) - .split(', ') - .map((v) => parseValue(v, '')); + .split(", ") + .map((v) => parseValue(v, "")); return new Set(values); } const entries = value .slice(1, -1) - .split(', ') + .split(", ") .map((entry) => { - const [key, val] = entry.split(': '); - return [key, parseValue(val, '')]; + const [key, val] = entry.split(": "); + return [key, parseValue(val, "")]; }); return Object.fromEntries(entries); } @@ -917,48 +860,44 @@ export const parseValue = (value: string, currentType: unknown): unknown => { } }; -export const detectValueType = ( +const detectValueType = ( value: string, ): { - type: 'string' | 'number' | 'undefined' | 'null' | 'boolean'; + type: "string" | "number" | "undefined" | "null" | "boolean"; value: unknown; } => { const trimmed = value.trim(); switch (trimmed) { - case 'undefined': - return { type: 'undefined', value: undefined }; - case 'null': - return { type: 'null', value: null }; - case 'true': - return { type: 'boolean', value: true }; - case 'false': - return { type: 'boolean', value: false }; + case "undefined": + return { type: "undefined", value: undefined }; + case "null": + return { type: "null", value: null }; + case "true": + return { type: "boolean", value: true }; + case "false": + return { type: "boolean", value: false }; } if (/^".*"$/.test(trimmed)) { - return { type: 'string', value: trimmed.slice(1, -1) }; + return { type: "string", value: trimmed.slice(1, -1) }; } if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) { - return { type: 'number', value: Number(trimmed) }; + return { type: "number", value: Number(trimmed) }; } - return { type: 'string', value: `"${trimmed}"` }; + return { type: "string", value: `"${trimmed}"` }; }; -export const formatInitialValue = (value: unknown): string => { - if (value === undefined) return 'undefined'; - if (value === null) return 'null'; - if (typeof value === 'string') return `"${value}"`; +const formatInitialValue = (value: unknown): string => { + if (value === undefined) return "undefined"; + if (value === null) return "null"; + if (typeof value === "string") return `"${value}"`; return String(value); }; -export const updateNestedValue = ( - obj: unknown, - path: Array, - value: unknown, -): unknown => { +const updateNestedValue = (obj: unknown, path: Array, value: unknown): unknown => { try { if (path.length === 0) return value; @@ -967,7 +906,7 @@ export const updateNestedValue = ( // Handle our special array of {name, value} pairs if ( Array.isArray(obj) && - obj.every((item): item is StateItem => 'name' in item && 'value' in item) + obj.every((item): item is StateItem => "name" in item && "value" in item) ) { const index = obj.findIndex((item) => item.name === key); if (index === -1) return obj; @@ -1006,17 +945,13 @@ export const updateNestedValue = ( return newArray; } - if (obj && typeof obj === 'object') { + if (obj && typeof obj === "object") { if (rest.length === 0) { return { ...obj, [key]: value }; } return { ...obj, - [key]: updateNestedValue( - (obj as Record)[key], - rest, - value, - ), + [key]: updateNestedValue((obj as Record)[key], rest, value), }; } @@ -1026,10 +961,10 @@ export const updateNestedValue = ( } }; -export const areFunctionsEqual = (prev: unknown, current: unknown): boolean => { +const areFunctionsEqual = (prev: unknown, current: unknown): boolean => { try { // Check if both values are actually functions - if (typeof prev !== 'function' || typeof current !== 'function') { + if (typeof prev !== "function" || typeof current !== "function") { return false; } @@ -1047,13 +982,13 @@ export const getObjectDiff = ( seen = new WeakSet(), ): DiffResult => { if (prev === current) { - return { type: 'primitive', changes: [], hasDeepChanges: false }; + return { type: "primitive", changes: [], hasDeepChanges: false }; } - if (typeof prev === 'function' && typeof current === 'function') { + if (typeof prev === "function" && typeof current === "function") { const isSameFunction = areFunctionsEqual(prev, current); return { - type: 'primitive', + type: "primitive", changes: [ { path, @@ -1071,11 +1006,11 @@ export const getObjectDiff = ( current === null || prev === undefined || current === undefined || - typeof prev !== 'object' || - typeof current !== 'object' + typeof prev !== "object" || + typeof current !== "object" ) { return { - type: 'primitive', + type: "primitive", changes: [{ path, prevValue: prev, currentValue: current }], hasDeepChanges: true, }; @@ -1083,8 +1018,8 @@ export const getObjectDiff = ( if (seen.has(prev) || seen.has(current)) { return { - type: 'object', - changes: [{ path, prevValue: '[Circular]', currentValue: '[Circular]' }], + type: "object", + changes: [{ path, prevValue: "[Circular]", currentValue: "[Circular]" }], hasDeepChanges: false, }; } @@ -1094,10 +1029,7 @@ export const getObjectDiff = ( const prevObj = prev as Record; const currentObj = current as Record; - const allKeys = new Set([ - ...Object.keys(prevObj), - ...Object.keys(currentObj), - ]); + const allKeys = new Set([...Object.keys(prevObj), ...Object.keys(currentObj)]); const changes: Array = []; let hasDeepChanges = false; @@ -1107,17 +1039,12 @@ export const getObjectDiff = ( if (prevValue !== currentValue) { if ( - typeof prevValue === 'object' && - typeof currentValue === 'object' && + typeof prevValue === "object" && + typeof currentValue === "object" && prevValue !== null && currentValue !== null ) { - const nestedDiff = getObjectDiff( - prevValue, - currentValue, - [...path, key], - seen, - ); + const nestedDiff = getObjectDiff(prevValue, currentValue, [...path, key], seen); changes.push(...nestedDiff.changes); if (nestedDiff.hasDeepChanges) { hasDeepChanges = true; @@ -1134,14 +1061,14 @@ export const getObjectDiff = ( } return { - type: 'object', + type: "object", changes, hasDeepChanges, }; }; export const formatPath = (path: string[]): string => { - if (path.length === 0) return ''; + if (path.length === 0) return ""; return path.reduce((acc, segment, i) => { // Check if segment is a number (array index) @@ -1150,31 +1077,31 @@ export const formatPath = (path: string[]): string => { } // Add dot separator only if not first segment and previous segment wasn't an array index return i === 0 ? segment : `${acc}.${segment}`; - }, ''); + }, ""); }; -export const formatFunctionBody = (body: string): string => { +const formatFunctionBody = (body: string): string => { // Remove newlines and extra spaces - let formatted = body.replace(/\s+/g, ' ').trim(); + let formatted = body.replace(/\s+/g, " ").trim(); // Add newlines after {, ; and before } formatted = formatted - .replace(/{/g, '{\n ') - .replace(/;/g, ';\n ') - .replace(/}/g, '\n}') - .replace(/{\s+}/g, '{ }'); // Clean up empty blocks + .replace(/{/g, "{\n ") + .replace(/;/g, ";\n ") + .replace(/}/g, "\n}") + .replace(/{\s+}/g, "{ }"); // Clean up empty blocks // Clean up arrow functions - formatted = formatted.replace(/=> {\n/g, '=> {').replace(/\n\s*}\s*$/g, ' }'); + formatted = formatted.replace(/=> {\n/g, "=> {").replace(/\n\s*}\s*$/g, " }"); return formatted; }; -export function hackyJsFormatter(code: string) { +function hackyJsFormatter(code: string) { // // 1) Collapse runs of whitespace to single spaces // - const normalizedCode = code.replace(/\s+/g, ' ').trim(); + const normalizedCode = code.replace(/\s+/g, " ").trim(); // // 2) Tokenize @@ -1193,15 +1120,15 @@ export function hackyJsFormatter(code: string) { // We'll also try to combine () or [] or {} or <> if they appear empty. // const rawTokens = []; - let current = ''; + let current = ""; for (let i = 0; i < normalizedCode.length; i++) { const c = normalizedCode[i]; // Detect arrow => - if (c === '=' && normalizedCode[i + 1] === '>') { + if (c === "=" && normalizedCode[i + 1] === ">") { if (current.trim()) rawTokens.push(current.trim()); - rawTokens.push('=>'); - current = ''; + rawTokens.push("=>"); + current = ""; i++; continue; } @@ -1213,13 +1140,13 @@ export function hackyJsFormatter(code: string) { rawTokens.push(current.trim()); } rawTokens.push(c); - current = ''; + current = ""; } else if (/\s/.test(c)) { // whitespace ends the current token if (current.trim()) { rawTokens.push(current.trim()); } - current = ''; + current = ""; } else { current += c; } @@ -1237,10 +1164,10 @@ export function hackyJsFormatter(code: string) { const t = rawTokens[i]; const n = rawTokens[i + 1]; if ( - (t === '(' && n === ')') || - (t === '[' && n === ']') || - (t === '{' && n === '}') || - (t === '<' && n === '>') + (t === "(" && n === ")") || + (t === "[" && n === "]") || + (t === "{" && n === "}") || + (t === "<" && n === ">") ) { merged.push(t + n); // '()', '[]', '{}', '<>' i++; @@ -1262,11 +1189,7 @@ export function hackyJsFormatter(code: string) { const arrowParamSet = new Set(); // indexes inside arrow param lists const genericSet = new Set(); // indexes inside generics <...> - function findMatchingPair( - openTok: string, - closeTok: string, - startIndex: number, - ) { + function findMatchingPair(openTok: string, closeTok: string, startIndex: number) { // e.g. openTok = '(', closeTok = ')' let depth = 0; for (let j = startIndex; j < merged.length; j++) { @@ -1283,9 +1206,9 @@ export function hackyJsFormatter(code: string) { // Detect arrow param sets for (let i = 0; i < merged.length; i++) { const t = merged[i]; - if (t === '(') { - const closeIndex = findMatchingPair('(', ')', i); - if (closeIndex !== -1 && merged[closeIndex + 1] === '=>') { + if (t === "(") { + const closeIndex = findMatchingPair("(", ")", i); + if (closeIndex !== -1 && merged[closeIndex + 1] === "=>") { // Mark all tokens from i..closeIndex as arrow param for (let k = i; k <= closeIndex; k++) { arrowParamSet.add(k); @@ -1301,8 +1224,8 @@ export function hackyJsFormatter(code: string) { const prev = merged[i - 1]; const t = merged[i]; // If prev is an identifier and t is '<', find matching '>' - if (/^[a-zA-Z0-9_$]+$/.test(prev) && t === '<') { - const closeIndex = findMatchingPair('<', '>', i); + if (/^[a-zA-Z0-9_$]+$/.test(prev) && t === "<") { + const closeIndex = findMatchingPair("<", ">", i); if (closeIndex !== -1) { // Mark i..closeIndex as generic for (let k = i; k <= closeIndex; k++) { @@ -1316,15 +1239,15 @@ export function hackyJsFormatter(code: string) { // 5) Build lines with indentation. We maintain a stack for open brackets. // let indentLevel = 0; - const indentStr = ' '; // 2 spaces + const indentStr = " "; // 2 spaces const lines: Array = []; - let line = ''; + let line = ""; function pushLine() { if (line.trim()) { - lines.push(line.replace(/\s+$/, '')); + lines.push(line.replace(/\s+$/, "")); } - line = ''; + line = ""; } function newLine() { pushLine(); @@ -1351,38 +1274,30 @@ export function hackyJsFormatter(code: string) { for (let i = 0; i < merged.length; i++) { const tok = merged[i]; - const next = merged[i + 1] || ''; + const next = merged[i + 1] || ""; // Open brackets - if (['(', '{', '[', '<'].includes(tok)) { + if (["(", "{", "[", "<"].includes(tok)) { placeToken(tok); stack.push(tok); // If '{', definitely newline + indent - if (tok === '{') { + if (tok === "{") { indentLevel++; newLine(); - } else if (tok === '(' || tok === '[' || tok === '<') { + } else if (tok === "(" || tok === "[" || tok === "<") { // If we are in arrowParamSet or genericSet, keep it on one line - if ( - (arrowParamSet.has(i) && tok === '(') || - (genericSet.has(i) && tok === '<') - ) { + if ((arrowParamSet.has(i) && tok === "(") || (genericSet.has(i) && tok === "<")) { // Don't break lines after commas etc. // We won't do multiline logic for these. } else { // If next is not a direct close, go multiline const directClose = { - '(': ')', - '[': ']', - '<': '>', + "(": ")", + "[": "]", + "<": ">", }[tok]; - if ( - next !== directClose && - next !== '()' && - next !== '[]' && - next !== '<>' - ) { + if (next !== directClose && next !== "()" && next !== "[]" && next !== "<>") { indentLevel++; newLine(); } @@ -1391,29 +1306,26 @@ export function hackyJsFormatter(code: string) { } // Close brackets - else if ([')', '}', ']', '>'].includes(tok)) { + else if ([")", "}", "]", ">"].includes(tok)) { // pop stack const opening = stackTop(); if ( - (tok === ')' && opening === '(') || - (tok === ']' && opening === '[') || - (tok === '>' && opening === '<') + (tok === ")" && opening === "(") || + (tok === "]" && opening === "[") || + (tok === ">" && opening === "<") ) { // if not arrowParamSet or genericSet, multiline - if ( - !(arrowParamSet.has(i) && tok === ')') && - !(genericSet.has(i) && tok === '>') - ) { + if (!(arrowParamSet.has(i) && tok === ")") && !(genericSet.has(i) && tok === ">")) { indentLevel = Math.max(indentLevel - 1, 0); newLine(); } - } else if (tok === '}' && opening === '{') { + } else if (tok === "}" && opening === "{") { indentLevel = Math.max(indentLevel - 1, 0); newLine(); } stack.pop(); placeToken(tok); - if (tok === '}') { + if (tok === "}") { // break line after } newLine(); } @@ -1424,26 +1336,23 @@ export function hackyJsFormatter(code: string) { placeToken(tok); // Arrow => - } else if (tok === '=>') { + } else if (tok === "=>") { placeToken(tok); // We'll let the next token (maybe '{') handle line breaks. // Semicolon - } else if (tok === ';') { + } else if (tok === ";") { placeToken(tok, true); newLine(); // Comma - } else if (tok === ',') { + } else if (tok === ",") { placeToken(tok, true); // If inside an arrow param set or generic set, don't break // Otherwise, if top is {, (, [ or <, break line const top = stackTop(); - if ( - !(arrowParamSet.has(i) && top === '(') && - !(genericSet.has(i) && top === '<') - ) { - if (top && ['{', '[', '(', '<'].includes(top)) { + if (!(arrowParamSet.has(i) && top === "(") && !(genericSet.has(i) && top === "<")) { + if (top && ["{", "[", "(", "<"].includes(top)) { newLine(); } } @@ -1458,25 +1367,20 @@ export function hackyJsFormatter(code: string) { // Remove extra blank lines return lines - .join('\n') - .replace(/\n\s*\n+/g, '\n') + .join("\n") + .replace(/\n\s*\n+/g, "\n") .trim(); } // Update the formatFunctionPreview to use the new formatter -export const formatFunctionPreview = ( - fn: { toString(): string }, - expanded = false, -): string => { +export const formatFunctionPreview = (fn: { toString(): string }, expanded = false): string => { try { const fnStr = fn.toString(); - const match = fnStr.match( - /(?:function\s*)?(?:\(([^)]*)\)|([^=>\s]+))\s*=>?/, - ); - if (!match) return 'ƒ'; + const match = fnStr.match(/(?:function\s*)?(?:\(([^)]*)\)|([^=>\s]+))\s*=>?/); + if (!match) return "ƒ"; - const params = match[1] || match[2] || ''; - const cleanParams = params.replace(/\s+/g, ''); + const params = match[1] || match[2] || ""; + const cleanParams = params.replace(/\s+/g, ""); if (!expanded) { return `ƒ (${cleanParams}) => ...`; @@ -1485,51 +1389,48 @@ export const formatFunctionPreview = ( // For expanded view, use the new formatter return hackyJsFormatter(fnStr); } catch { - return 'ƒ'; + return "ƒ"; } }; export const formatValuePreview = (value: unknown): string => { - if (value === null) return 'null'; - if (value === undefined) return 'undefined'; - if (typeof value === 'string') + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return `"${value.length > 150 ? `${value.slice(0, 20)}...` : value}"`; - if (typeof value === 'number' || typeof value === 'boolean') - return String(value); - if (typeof value === 'function') return formatFunctionPreview(value); + if (typeof value === "number" || typeof value === "boolean") return String(value); + if (typeof value === "function") return formatFunctionPreview(value); if (Array.isArray(value)) return `Array(${value.length})`; if (value instanceof Map) return `Map(${value.size})`; if (value instanceof Set) return `Set(${value.size})`; if (value instanceof Date) return value.toISOString(); if (value instanceof RegExp) return value.toString(); if (value instanceof Error) return `${value.name}: ${value.message}`; - if (typeof value === 'object') { + if (typeof value === "object") { const keys = Object.keys(value as object); - return `{${keys.length > 2 ? `${keys.slice(0, 2).join(', ')}, ...` : keys.join(', ')}}`; + return `{${keys.length > 2 ? `${keys.slice(0, 2).join(", ")}, ...` : keys.join(", ")}}`; } return String(value); }; -export const safeGetValue = ( - value: unknown, -): { value: unknown; error?: string } => { +export const safeGetValue = (value: unknown): { value: unknown; error?: string } => { if (value === null || value === undefined) return { value }; - if (typeof value === 'function') return { value }; - if (typeof value !== 'object') return { value }; + if (typeof value === "function") return { value }; + if (typeof value !== "object") return { value }; if (isPromise(value)) { - return { value: 'Promise' }; + return { value: "Promise" }; } try { const proto = Object.getPrototypeOf(value); - if (proto === Promise.prototype || proto?.constructor?.name === 'Promise') { - return { value: 'Promise' }; + if (proto === Promise.prototype || proto?.constructor?.name === "Promise") { + return { value: "Promise" }; } return { value }; } catch { - return { value: null, error: 'Error accessing value' }; + return { value: null, error: "Error accessing value" }; } }; @@ -1541,7 +1442,7 @@ export interface TimelineSliderValues { rightValue: number; } -export const calculateSliderValues = ( +const calculateSliderValues = ( totalUpdates: number, currentIndex: number, ): TimelineSliderValues => { @@ -1579,10 +1480,10 @@ interface ExtendedMemoizedState extends MemoizedState { element?: unknown; } -export const isDirectComponent = (fiber: Fiber): boolean => { +const isDirectComponent = (fiber: Fiber): boolean => { if (!fiber || !fiber.type) return false; - const isFunctionalComponent = typeof fiber.type === 'function'; + const isFunctionalComponent = typeof fiber.type === "function"; const isClassComponent = fiber.type?.prototype?.isReactComponent ?? false; if (!(isFunctionalComponent || isClassComponent)) return false; @@ -1605,36 +1506,33 @@ export const isDirectComponent = (fiber: Fiber): boolean => { }; export const isPromise = (value: unknown): value is Promise => { - return ( - !!value && - (value instanceof Promise || (typeof value === 'object' && 'then' in value)) - ); + return !!value && (value instanceof Promise || (typeof value === "object" && "then" in value)); }; -export const ensureRecord = ( +const ensureRecord = ( value: unknown, maxDepth = 2, seen = new WeakSet(), ): Record => { if (isPromise(value)) { - return { type: 'promise', displayValue: 'Promise' }; + return { type: "promise", displayValue: "Promise" }; } if (value === null) { - return { type: 'null', displayValue: 'null' }; + return { type: "null", displayValue: "null" }; } if (value === undefined) { - return { type: 'undefined', displayValue: 'undefined' }; + return { type: "undefined", displayValue: "undefined" }; } switch (typeof value) { - case 'object': { + case "object": { if (seen.has(value)) { - return { type: 'circular', displayValue: '[Circular Reference]' }; + return { type: "circular", displayValue: "[Circular Reference]" }; } - if (!value) return { type: 'null', displayValue: 'null' }; + if (!value) return { type: "null", displayValue: "null" }; seen.add(value); @@ -1642,14 +1540,14 @@ export const ensureRecord = ( const result: Record = {}; if (value instanceof Element) { - result.type = 'Element'; + result.type = "Element"; result.tagName = value.tagName.toLowerCase(); result.displayValue = value.tagName.toLowerCase(); return result; } if (value instanceof Map) { - result.type = 'Map'; + result.type = "Map"; result.size = value.size; result.displayValue = `Map(${value.size})`; @@ -1662,8 +1560,8 @@ export const ensureRecord = ( entries[String(key)] = ensureRecord(val, maxDepth - 1, seen); } catch { entries[String(index)] = { - type: 'error', - displayValue: 'Error accessing Map entry', + type: "error", + displayValue: "Error accessing Map entry", }; } index++; @@ -1674,7 +1572,7 @@ export const ensureRecord = ( } if (value instanceof Set) { - result.type = 'Set'; + result.type = "Set"; result.size = value.size; result.displayValue = `Set(${value.size})`; @@ -1692,21 +1590,21 @@ export const ensureRecord = ( } if (value instanceof Date) { - result.type = 'Date'; + result.type = "Date"; result.value = value.toISOString(); result.displayValue = value.toLocaleString(); return result; } if (value instanceof RegExp) { - result.type = 'RegExp'; + result.type = "RegExp"; result.value = value.toString(); result.displayValue = value.toString(); return result; } if (value instanceof Error) { - result.type = 'Error'; + result.type = "Error"; result.name = value.name; result.message = value.message; result.displayValue = `${value.name}: ${value.message}`; @@ -1714,14 +1612,14 @@ export const ensureRecord = ( } if (value instanceof ArrayBuffer) { - result.type = 'ArrayBuffer'; + result.type = "ArrayBuffer"; result.byteLength = value.byteLength; result.displayValue = `ArrayBuffer(${value.byteLength})`; return result; } if (value instanceof DataView) { - result.type = 'DataView'; + result.type = "DataView"; result.byteLength = value.byteLength; result.displayValue = `DataView(${value.byteLength})`; return result; @@ -1741,25 +1639,23 @@ export const ensureRecord = ( } if (Array.isArray(value)) { - result.type = 'array'; + result.type = "array"; result.length = value.length; result.displayValue = `Array(${value.length})`; if (maxDepth > 0) { - result.items = value - .slice(0, 50) - .map((item) => ensureRecord(item, maxDepth - 1, seen)); + result.items = value.slice(0, 50).map((item) => ensureRecord(item, maxDepth - 1, seen)); } return result; } const keys = Object.keys(value); - result.type = 'object'; + result.type = "object"; result.size = keys.length; result.displayValue = keys.length <= 5 - ? `{${keys.join(', ')}}` - : `{${keys.slice(0, 5).join(', ')}, ...${keys.length - 5}}`; + ? `{${keys.join(", ")}}` + : `{${keys.slice(0, 5).join(", ")}, ...${keys.length - 5}}`; if (maxDepth > 0) { const entries: Record = {}; @@ -1772,8 +1668,8 @@ export const ensureRecord = ( ); } catch { entries[key] = { - type: 'error', - displayValue: 'Error accessing property', + type: "error", + displayValue: "Error accessing property", }; } } @@ -1784,17 +1680,17 @@ export const ensureRecord = ( seen.delete(value); } } - case 'string': + case "string": return { - type: 'string', + type: "string", value, displayValue: `"${value}"`, }; - case 'function': + case "function": return { - type: 'function', - displayValue: 'ƒ()', - name: value.name || 'anonymous', + type: "function", + displayValue: "ƒ()", + name: value.name || "anonymous", }; default: return { @@ -1805,9 +1701,7 @@ export const ensureRecord = ( } }; -export const getCurrentFiberState = ( - fiber: Fiber, -): Record | null => { +const getCurrentFiberState = (fiber: Fiber): Record | null => { if (fiber.tag !== FunctionComponentTag || !isDirectComponent(fiber)) { return null; } @@ -1825,9 +1719,8 @@ export const getCurrentFiberState = ( return memoizedState; }; -export const replayComponent = async (fiber: Fiber): Promise => { - const { overrideProps, overrideHookState, overrideContext } = - getOverrideMethods(); +const replayComponent = async (fiber: Fiber): Promise => { + const { overrideProps, overrideHookState, overrideContext } = getOverrideMethods(); if (!overrideProps || !overrideHookState || !fiber) return; try { @@ -1835,8 +1728,8 @@ export const replayComponent = async (fiber: Fiber): Promise => { const currentProps = fiber.memoizedProps || {}; const propKeys = Object.keys(currentProps).filter((key) => { const value = currentProps[key]; - if (Array.isArray(value) || typeof value === 'string') { - return !Number.isInteger(Number(key)) && key !== 'length'; + if (Array.isArray(value) || typeof value === "string") { + return !Number.isInteger(Number(key)) && key !== "length"; } return true; }); @@ -1847,7 +1740,7 @@ export const replayComponent = async (fiber: Fiber): Promise => { // For arrays and objects, we need to clone to trigger updates const propValue = Array.isArray(value) ? [...value] - : typeof value === 'object' && value !== null + : typeof value === "object" && value !== null ? { ...value } : value; overrideProps(fiber, [key], propValue); @@ -1868,7 +1761,7 @@ export const replayComponent = async (fiber: Fiber): Promise => { // For arrays and objects, we need to clone to trigger updates const stateValue = Array.isArray(value) ? [...value] - : typeof value === 'object' && value !== null + : typeof value === "object" && value !== null ? { ...value } : value; overrideHookState(fiber, hookId, [], stateValue); @@ -1889,7 +1782,7 @@ export const replayComponent = async (fiber: Fiber): Promise => { // For arrays and objects, we need to clone to trigger updates const stateValue = Array.isArray(value) ? [...value] - : typeof value === 'object' && value !== null + : typeof value === "object" && value !== null ? { ...value } : value; overrideHookState(fiber, hookId, [], stateValue); @@ -1921,9 +1814,9 @@ export const replayComponent = async (fiber: Fiber): Promise => { if (isEqual(currentValue, newValue)) break; // Update the provider's value prop - overrideProps(current, ['value'], newValue); + overrideProps(current, ["value"], newValue); if (current.alternate) { - overrideProps(current.alternate, ['value'], newValue); + overrideProps(current.alternate, ["value"], newValue); } break; } @@ -1946,7 +1839,7 @@ export const replayComponent = async (fiber: Fiber): Promise => { export const extractMinimalFiberInfo = (fiber: Fiber): MinimalFiberInfo => { const timings = getTimings(fiber); return { - displayName: getDisplayName(fiber) || 'Unknown', + displayName: getDisplayName(fiber) || "Unknown", type: fiber.type, key: fiber.key, id: fiber.index, diff --git a/packages/scan/src/web/views/inspector/what-changed.tsx b/packages/scan/src/web/views/inspector/what-changed.tsx index 27d19411..c04b4f14 100644 --- a/packages/scan/src/web/views/inspector/what-changed.tsx +++ b/packages/scan/src/web/views/inspector/what-changed.tsx @@ -1,31 +1,23 @@ -import { type ReactNode, memo } from 'preact/compat'; -import { - type Dispatch, - type StateUpdater, - useEffect, - useRef, - useState, -} from 'preact/hooks'; -import { CopyToClipboard } from '~web/components/copy-to-clipboard'; -import { Icon } from '~web/components/icon'; -import { cn, throttle } from '~web/utils/helpers'; -import { DiffValueView } from './diff-value'; -import { timelineState } from './states'; +import { type ReactNode, memo } from "preact/compat"; +import { useEffect, useRef, useState } from "preact/hooks"; +import { CopyToClipboard } from "~web/components/copy-to-clipboard"; +import { Icon } from "~web/components/icon"; +import { cn, throttle } from "~web/utils/helpers"; +import { DiffValueView } from "./diff-value"; +import { timelineState } from "./states"; import { AggregatedChanges, formatFunctionPreview, formatPath, getObjectDiff, safeGetValue, -} from './utils'; +} from "./utils"; import { calculateTotalChanges, useInspectedFiberChangeStore, -} from './whats-changed/use-change-store'; -import { getDisplayName, getType } from 'bippy'; -import { Store } from '~core/index'; - -export type Setter = Dispatch>; +} from "./whats-changed/use-change-store"; +import { getDisplayName, getType } from "bippy"; +import { Store } from "~core/index"; export const WhatChanged = /* @__PURE__ */ memo(() => { const [isExpanded, setIsExpanded] = useState(true); @@ -47,18 +39,15 @@ export const WhatChanged = /* @__PURE__ */ memo(() => { const initializedContextChanges = new Map( Array.from(aggregatedChanges.contextChanges.entries()) - .filter(([, value]) => value.kind === 'initialized') + .filter(([, value]) => value.kind === "initialized") .map(([key, value]) => [ key, // oxlint-disable-next-line typescript/no-non-null-assertion - value.kind === 'partially-initialized' ? null! : value.changes, + value.kind === "partially-initialized" ? null! : value.changes, ]), ); - const fiber = - Store.inspectState.value.kind === 'focused' - ? Store.inspectState.value.fiber - : null; + const fiber = Store.inspectState.value.kind === "focused" ? Store.inspectState.value.fiber : null; if (!fiber) { // invariant @@ -71,25 +60,18 @@ export const WhatChanged = /* @__PURE__ */ memo(() => {
- Why did{' '} - {getDisplayName(fiber)}{' '} - render? + Why did {getDisplayName(fiber)} render? {!hasAnyChanges && (
No changes detected since selecting
- The props, state, and context changes within your component will - be reported here + The props, state, and context changes within your component will be reported here
)}
-
+
{ />
- renderStateName( - name, - getDisplayName(getType(fiber)) ?? 'Unknown Component', - ) + renderStateName(name, getDisplayName(getType(fiber)) ?? "Unknown Component") } changes={aggregatedChanges.stateChanges} title="Changed State" @@ -127,17 +106,17 @@ const renderStateName = (key: string, componentName: string) => { const lastDigit = num % 10; const lastTwoDigits = num % 100; if (lastTwoDigits >= 11 && lastTwoDigits <= 13) { - return 'th'; + return "th"; } switch (lastDigit) { case 1: - return 'st'; + return "st"; case 2: - return 'nd'; + return "nd"; case 3: - return 'rd'; + return "rd"; default: - return 'th'; + return "th"; } }; @@ -145,9 +124,9 @@ const renderStateName = (key: string, componentName: string) => { {n} - {getOrdinalSuffix(n)} hook{' '} + {getOrdinalSuffix(n)} hook{" "} - + called in {componentName} @@ -172,20 +151,20 @@ const WhatsChangedHeader = memo(() => { useEffect(() => { const flash = throttle(() => { const flashElements = []; - if (refProps.current?.dataset.flash === 'true') { + if (refProps.current?.dataset.flash === "true") { flashElements.push(refProps.current); } - if (refState.current?.dataset.flash === 'true') { + if (refState.current?.dataset.flash === "true") { flashElements.push(refState.current); } - if (refContext.current?.dataset.flash === 'true') { + if (refContext.current?.dataset.flash === "true") { flashElements.push(refContext.current); } for (const element of flashElements) { - element.classList.remove('count-flash-white'); + element.classList.remove("count-flash-white"); void element.offsetWidth; - element.classList.add('count-flash-white'); + element.classList.add("count-flash-white"); } }, 400); @@ -209,17 +188,14 @@ const WhatsChangedHeader = memo(() => { isContextChanged: (currentUpdate.context?.changes?.size ?? 0) > 0, }; - if (refProps.current.dataset.flash !== 'true') { - refProps.current.dataset.flash = - refStats.current.isPropsChanged.toString(); + if (refProps.current.dataset.flash !== "true") { + refProps.current.dataset.flash = refStats.current.isPropsChanged.toString(); } - if (refState.current.dataset.flash !== 'true') { - refState.current.dataset.flash = - refStats.current.isStateChanged.toString(); + if (refState.current.dataset.flash !== "true") { + refState.current.dataset.flash = refStats.current.isStateChanged.toString(); } - if (refContext.current.dataset.flash !== 'true') { - refContext.current.dataset.flash = - refStats.current.isContextChanged.toString(); + if (refContext.current.dataset.flash !== "true") { + refContext.current.dataset.flash = refStats.current.isContextChanged.toString(); } }); @@ -230,23 +206,19 @@ const WhatsChangedHeader = memo(() => { -
-
-
- {prevError || currError ? ( - - ) : diff.changes.length > 0 ? ( - - ) : ( - - )} -
+
+ +
+
+
+ {prevError || currError ? ( + + ) : diff.changes.length > 0 ? ( + + ) : ( + + )}
- ); - })} -
+
+ ); + })}
- ); - }, -); +
+ ); +}); -const AccessError = ({ - prevError, - currError, -}: { - prevError?: string; - currError?: string; -}) => { +const AccessError = ({ prevError, currError }: { prevError?: string; currError?: string }) => { return ( <> {prevError && ( @@ -420,26 +377,20 @@ const DiffChange = ({ setExpandedFns: (updater: (prev: Set) => Set) => void; }) => { return diff.changes.map((diffChange, i) => { - const { value: prevDiffValue, error: prevDiffError } = safeGetValue( - diffChange.prevValue, - ); - const { value: currDiffValue, error: currDiffError } = safeGetValue( - diffChange.currentValue, - ); + const { value: prevDiffValue, error: prevDiffError } = safeGetValue(diffChange.prevValue); + const { value: currDiffValue, error: currDiffError } = safeGetValue(diffChange.currentValue); - const isFunction = - typeof prevDiffValue === 'function' || - typeof currDiffValue === 'function'; + const isFunction = typeof prevDiffValue === "function" || typeof currDiffValue === "function"; let path: string | undefined; - if (title === 'Props') { + if (title === "Props") { path = diffChange.path.length > 0 ? `${renderName(String(change.name))}.${formatPath(diffChange.path)}` : undefined; } - if (title === 'State' && diffChange.path.length > 0) { + if (title === "State" && diffChange.path.length > 0) { path = `state.${formatPath(diffChange.path)}`; } @@ -450,22 +401,19 @@ const DiffChange = ({ return (
{path &&
{path}
}
) : ( { const key = `${formatPath(diffChange.path)}-prev`; setExpandedFns((prev) => { @@ -540,13 +482,13 @@ const DiffChange = ({ -
+
             {getLLMPrompt(activeTab, selectedEvent)}
@@ -511,12 +484,12 @@ export const Optimize = ({
           setTimeout(() => setCopying(false), 1000);
         }}
         className={cn([
-          'mt-4 px-4 py-2 bg-[#18181B] text-[#6E6E77] rounded-sm',
-          'hover:text-white transition-colors duration-200',
-          'flex items-center justify-center gap-x-2 text-xs',
+          "mt-4 px-4 py-2 bg-[#18181B] text-[#6E6E77] rounded-sm",
+          "hover:text-white transition-colors duration-200",
+          "flex items-center justify-center gap-x-2 text-xs",
         ])}
       >
-        {copying ? 'Copied!' : 'Copy Prompt'}
+        {copying ? "Copied!" : "Copy Prompt"}
         
           {copying ? (
             
diff --git a/packages/scan/src/web/views/notifications/other-visualization.tsx b/packages/scan/src/web/views/notifications/other-visualization.tsx
index b20dc751..311329fc 100644
--- a/packages/scan/src/web/views/notifications/other-visualization.tsx
+++ b/packages/scan/src/web/views/notifications/other-visualization.tsx
@@ -1,94 +1,84 @@
-import { ReactNode } from 'preact/compat';
-import { useContext, useEffect, useState } from 'preact/hooks';
-import { getIsProduction } from '~core/index';
-import { iife } from '~core/notifications/performance-utils';
-import { cn } from '~web/utils/helpers';
-import {
-  InteractionEvent,
-  NotificationEvent,
-  getTotalTime,
-  useNotificationsContext,
-} from './data';
-import { getLLMPrompt } from './optimize';
-import { ToolbarElementContext } from '~web/widget';
+import { ReactNode } from "preact/compat";
+import { useContext, useEffect, useState } from "preact/hooks";
+import { getIsProduction } from "~core/index";
+import { iife } from "~core/notifications/performance-utils";
+import { cn } from "~web/utils/helpers";
+import { InteractionEvent, NotificationEvent, getTotalTime, useNotificationsContext } from "./data";
+import { getLLMPrompt } from "./optimize";
+import { ToolbarElementContext } from "~web/widget";
 type BaseTimeDataItem = {
   name: string;
   time: number;
   color: string;
   kind:
-    | 'other-not-javascript'
-    | 'other-javascript'
-    | 'render'
-    | 'other-frame-drop'
-    | 'total-processing-time';
+    | "other-not-javascript"
+    | "other-javascript"
+    | "render"
+    | "other-frame-drop"
+    | "total-processing-time";
 };
 
 type TimeData = Array;
 
-const getTimeData = (
-  selectedEvent: NotificationEvent,
-  isProduction: boolean,
-) => {
+const getTimeData = (selectedEvent: NotificationEvent, isProduction: boolean) => {
   switch (selectedEvent.kind) {
     // todo: push instead of conditional spread
-    case 'dropped-frames': {
+    case "dropped-frames": {
       const timeData: TimeData = [
         ...(isProduction
           ? [
               {
-                name: 'Total Processing Time',
+                name: "Total Processing Time",
                 time: getTotalTime(selectedEvent.timing),
-                color: 'bg-red-500',
-                kind: 'total-processing-time' as const,
+                color: "bg-red-500",
+                kind: "total-processing-time" as const,
               },
             ]
           : [
               {
-                name: 'Renders',
+                name: "Renders",
                 time: selectedEvent.timing.renderTime,
-                color: 'bg-purple-500',
-                kind: 'render' as const,
+                color: "bg-purple-500",
+                kind: "render" as const,
               },
               {
-                name: 'JavaScript, DOM updates, Draw Frame',
+                name: "JavaScript, DOM updates, Draw Frame",
                 time: selectedEvent.timing.otherTime,
-                color: 'bg-[#4b4b4b]',
-                kind: 'other-frame-drop' as const,
+                color: "bg-[#4b4b4b]",
+                kind: "other-frame-drop" as const,
               },
             ]),
       ];
       return timeData;
     }
-    case 'interaction': {
+    case "interaction": {
       const timeData: TimeData = [
         ...(!isProduction
           ? [
               {
-                name: 'Renders',
+                name: "Renders",
                 time: selectedEvent.timing.renderTime,
-                color: 'bg-purple-500',
-                kind: 'render' as const,
+                color: "bg-purple-500",
+                kind: "render" as const,
               },
             ]
           : []),
         {
-          name: isProduction
-            ? 'React Renders, Hooks, Other JavaScript'
-            : 'JavaScript/React Hooks ',
+          name: isProduction ? "React Renders, Hooks, Other JavaScript" : "JavaScript/React Hooks ",
           time: selectedEvent.timing.otherJSTime,
-          color: 'bg-[#EFD81A]',
+          color: "bg-[#EFD81A]",
 
-          kind: 'other-javascript',
+          kind: "other-javascript",
         },
 
         {
-          name: 'Update DOM and Draw New Frame',
+          name: "Update DOM and Draw New Frame",
           time:
             getTotalTime(selectedEvent.timing) -
             selectedEvent.timing.renderTime -
             selectedEvent.timing.otherJSTime,
-          color: 'bg-[#1D3A66]',
-          kind: 'other-not-javascript',
+          color: "bg-[#1D3A66]",
+          kind: "other-not-javascript",
         },
       ];
 
@@ -97,17 +87,11 @@ const getTimeData = (
   }
 };
 
-export const OtherVisualization = ({
-  selectedEvent,
-}: {
-  selectedEvent: NotificationEvent;
-}) => {
+export const OtherVisualization = ({ selectedEvent }: { selectedEvent: NotificationEvent }) => {
   const [isProduction] = useState(getIsProduction() ?? false);
   const { notificationState } = useNotificationsContext();
   const [expandedItems, setExpandedItems] = useState(
-    notificationState.routeMessage?.name
-      ? [notificationState.routeMessage.name]
-      : [],
+    notificationState.routeMessage?.name ? [notificationState.routeMessage.name] : [],
   );
   const timeData = getTimeData(selectedEvent, isProduction);
   const root = useContext(ToolbarElementContext);
@@ -116,7 +100,7 @@ export const OtherVisualization = ({
   // oxlint-disable-next-line react-hooks/exhaustive-deps
   useEffect(() => {
     if (notificationState.routeMessage?.name) {
-      const container = root?.querySelector('#overview-scroll-container');
+      const container = root?.querySelector("#overview-scroll-container");
       const element = root?.querySelector(
         `#react-scan-overview-bar-${notificationState.routeMessage.name}`,
       ) as HTMLElement;
@@ -132,11 +116,9 @@ export const OtherVisualization = ({
 
   // oxlint-disable-next-line react-hooks/exhaustive-deps
   useEffect(() => {
-    if (notificationState.route === 'other-visualization') {
+    if (notificationState.route === "other-visualization") {
       setExpandedItems((prev) =>
-        notificationState.routeMessage?.name
-          ? [notificationState.routeMessage.name]
-          : prev,
+        notificationState.routeMessage?.name ? [notificationState.routeMessage.name] : prev,
       );
     }
   }, [notificationState.route]);
@@ -148,9 +130,7 @@ export const OtherVisualization = ({
       

What was time spent on?

- - Total: {totalTime.toFixed(0)}ms - + Total: {totalTime.toFixed(0)}ms
@@ -172,7 +152,7 @@ export const OtherVisualization = ({
- - {entry.name} - + {entry.name}
- - {entry.time.toFixed(0)}ms - + {entry.time.toFixed(0)}ms
{iife(() => { switch (selectedEvent.kind) { - case 'interaction': { + case "interaction": { switch (entry.kind) { - case 'render': { - return ( - - ); + case "render": { + return ; } - case 'other-javascript': { - return ( - - ); + case "other-javascript": { + return ; } - case 'other-not-javascript': { - return ( - - ); + case "other-not-javascript": { + return ; } } } - case 'dropped-frames': { + case "dropped-frames": { switch (entry.kind) { - case 'total-processing-time': { + case "total-processing-time": { return ( ); } - case 'render': { + case "render": { return ( <> - b.totalTime - a.totalTime, - ) - .slice(0, 3) - .map((render) => ({ - name: render.name, - percentage: - render.totalTime / - getTotalTime( - selectedEvent.timing, - ), - })), + topByTime: selectedEvent.groupedFiberRenders + .toSorted((a, b) => b.totalTime - a.totalTime) + .slice(0, 3) + .map((render) => ({ + name: render.name, + percentage: + render.totalTime / getTotalTime(selectedEvent.timing), + })), }, }} /> ); } - case 'other-frame-drop': { + case "other-frame-drop": { return ( ); @@ -302,29 +259,29 @@ export const OtherVisualization = ({ type OverviewInput = | { - kind: 'js-explanation-base'; + kind: "js-explanation-base"; } | { - kind: 'total-processing'; + kind: "total-processing"; data: { time: number; }; } | { - kind: 'high-render-count-high-js'; + kind: "high-render-count-high-js"; data: { renderCount: number; topByCount: Array<{ name: string; count: number }>; }; } | { - kind: 'low-render-count-high-js'; + kind: "low-render-count-high-js"; data: { renderCount: number; }; } | { - kind: 'high-render-count-update-dom-draw-frame'; + kind: "high-render-count-update-dom-draw-frame"; data: { count: number; percentageOfTotal: number; @@ -332,33 +289,21 @@ type OverviewInput = }; } | { - kind: 'update-dom-draw-frame'; + kind: "update-dom-draw-frame"; data: { copyButton: ReactNode; }; } | { - kind: 'render'; + kind: "render"; data: { topByTime: Array<{ name: string; percentage: number }> }; } | { - kind: 'other'; + kind: "other"; }; -export const getTotalProcessingTimeInput = (event: NotificationEvent) => { - return { - kind: 'total-processing', - data: { - time: getTotalTime(event.timing), - }, - } satisfies OverviewInput; -}; - const getDrawInput = (event: InteractionEvent): OverviewInput => { - const renderCount = event.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.count, - 0, - ); + const renderCount = event.groupedFiberRenders.reduce((prev, curr) => prev + curr.count, 0); const renderTime = event.timing.renderTime; const totalTime = getTotalTime(event.timing); @@ -366,7 +311,7 @@ const getDrawInput = (event: InteractionEvent): OverviewInput => { if (renderCount > 100) { return { - kind: 'high-render-count-update-dom-draw-frame', + kind: "high-render-count-update-dom-draw-frame", data: { count: renderCount, percentageOfTotal: renderPercentage, @@ -376,7 +321,7 @@ const getDrawInput = (event: InteractionEvent): OverviewInput => { } return { - kind: 'update-dom-draw-frame', + kind: "update-dom-draw-frame", data: { copyButton: , }, @@ -395,14 +340,14 @@ const CopyPromptButton = () => { } await navigator.clipboard.writeText( - getLLMPrompt('explanation', notificationState.selectedEvent), + getLLMPrompt("explanation", notificationState.selectedEvent), ); setCopying(true); setTimeout(() => setCopying(false), 1000); }} className="bg-zinc-800 flex hover:bg-zinc-700 text-zinc-200 px-2 py-1 rounded gap-x-3" > - {copying ? 'Copied!' : 'Copy Prompt'} + {copying ? "Copied!" : "Copy Prompt"} { strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - className={cn([ - 'transition-transform duration-200', - copying && 'scale-110', - ])} + className={cn(["transition-transform duration-200", copying && "scale-110"])} > {copying ? ( @@ -434,7 +376,7 @@ const CopyPromptButton = () => { const getRenderInput = (event: InteractionEvent): OverviewInput => { if (event.timing.renderTime / getTotalTime(event.timing) > 0.3) { return { - kind: 'render', + kind: "render", data: { topByTime: event.groupedFiberRenders .toSorted((a, b) => b.totalTime - a.totalTime) @@ -448,18 +390,15 @@ const getRenderInput = (event: InteractionEvent): OverviewInput => { } return { - kind: 'other', + kind: "other", }; }; const getJSInput = (event: InteractionEvent): OverviewInput => { - const renderCount = event.groupedFiberRenders.reduce( - (prev, curr) => prev + curr.count, - 0, - ); + const renderCount = event.groupedFiberRenders.reduce((prev, curr) => prev + curr.count, 0); if (event.timing.otherJSTime / getTotalTime(event.timing) < 0.2) { return { - kind: 'js-explanation-base', + kind: "js-explanation-base", }; } if ( @@ -468,7 +407,7 @@ const getJSInput = (event: InteractionEvent): OverviewInput => { ) { // not sure a great heuristic for picking the render count return { - kind: 'high-render-count-high-js', + kind: "high-render-count-high-js", data: { renderCount, topByCount: event.groupedFiberRenders @@ -481,12 +420,12 @@ const getJSInput = (event: InteractionEvent): OverviewInput => { if (event.timing.otherJSTime / getTotalTime(event.timing) > 0.3) { if (event.timing.renderTime > 0.2) { return { - kind: 'js-explanation-base', + kind: "js-explanation-base", }; } return { - kind: 'low-render-count-high-js', + kind: "low-render-count-high-js", data: { renderCount, }, @@ -494,147 +433,121 @@ const getJSInput = (event: InteractionEvent): OverviewInput => { } return { - kind: 'js-explanation-base', + kind: "js-explanation-base", }; }; const Explanation = ({ input }: { input: OverviewInput }) => { switch (input.kind) { - case 'total-processing': { + case "total-processing": { return ( -
+

- This is the time it took to draw the entire frame that was presented - to the user. To be at 60FPS, this number needs to be {'<=16ms'} + This is the time it took to draw the entire frame that was presented to the user. To be + at 60FPS, this number needs to be {"<=16ms"}

- To debug the issue, check the "Ranked" tab to see if there are - significant component renders + To debug the issue, check the "Ranked" tab to see if there are significant component + renders

- On a production React build, React Scan can't access the time it - took for component to render. To get that information, run React - Scan on a development build + On a production React build, React Scan can't access the time it took for component to + render. To get that information, run React Scan on a development build

- To understand precisely what caused the slowdown while in - production, use the Chrome profiler and analyze the - function call times. + To understand precisely what caused the slowdown while in production, use the{" "} + Chrome profiler and analyze the function call times.

); } - case 'render': { + case "render": { return ( -
+

- This is the time it took React to run components, and internal logic - to handle the output of your component. + This is the time it took React to run components, and internal logic to handle the + output of your component.

-
+

The slowest components for this time period were:

{input.data.topByTime.map((item) => (
- {item.name}:{' '} - {(item.percentage * 100).toFixed(0)}% of total + {item.name}: {(item.percentage * 100).toFixed(0)}% of total
))}

- To view the render times of all your components, and what caused - them to render, go to the "Ranked" tab + To view the render times of all your components, and what caused them to render, go to + the "Ranked" tab

The "Ranked" tab shows the render times of every component.

+

The render times of the same components are grouped together into one bar.

- The render times of the same components are grouped together into - one bar. -

-

- Clicking the component will show you what props, state, or context - caused the component to re-render. + Clicking the component will show you what props, state, or context caused the component + to re-render.

); } - case 'js-explanation-base': { + case "js-explanation-base": { return ( -
+

- This is the period when JavaScript hooks and other JavaScript - outside of React Renders run. + This is the period when JavaScript hooks and other JavaScript outside of React Renders + run.

- The most common culprit for high JS time is expensive hooks, like - expensive callbacks inside of useEffect's or a large - number of useEffect's called, but this can also be JavaScript event - handlers ('onclick', 'onchange') that + The most common culprit for high JS time is expensive hooks, like expensive callbacks + inside of useEffect's or a large number of useEffect's called, but this can + also be JavaScript event handlers ('onclick', 'onchange') that performed expensive computation.

- If you have lots of components rendering that call hooks, like - useEffect, it can add significant overhead even if the callbacks are - not expensive. If this is the case, you can try optimizing the - renders of those components to avoid the hook from having to run. + If you have lots of components rendering that call hooks, like useEffect, it can add + significant overhead even if the callbacks are not expensive. If this is the case, you + can try optimizing the renders of those components to avoid the hook from having to run.

- You should profile your app using the{' '} - Chrome DevTools profiler to learn exactly which - functions took the longest to execute. + You should profile your app using the Chrome DevTools profiler to learn + exactly which functions took the longest to execute.

); } - case 'high-render-count-high-js': { + case "high-render-count-high-js": { return ( -
+

- This is the period when JavaScript hooks and other JavaScript - outside of React Renders run. + This is the period when JavaScript hooks and other JavaScript outside of React Renders + run.

{input.data.renderCount === 0 ? ( <>

- There were no renders, which means nothing related to React - caused this slowdown. The most likely cause of the slowdown is a - slow JavaScript event handler, or code related to a Web API + There were no renders, which means nothing related to React caused this slowdown. + The most likely cause of the slowdown is a slow JavaScript event handler, or code + related to a Web API

- You should try to reproduce the slowdown while profiling your - website with the - Chrome DevTools profiler to see exactly what - functions took the longest to execute. + You should try to reproduce the slowdown while profiling your website with the + Chrome DevTools profiler to see exactly what functions took the + longest to execute.

) : ( <> - {' '} + {" "}

- There were {input.data.renderCount} renders, - which could have contributed to the high JavaScript/Hook time if - they ran lots of hooks, like useEffects. + There were {input.data.renderCount} renders, which could have + contributed to the high JavaScript/Hook time if they ran lots of hooks, like{" "} + useEffects.

-
+

You should try optimizing the renders of:

{input.data.topByCount.map((item) => (
@@ -644,130 +557,107 @@ const Explanation = ({ input }: { input: OverviewInput }) => {
and then checking if the problem still exists.

- You can also try profiling your app using the{' '} - Chrome DevTools profiler to see exactly what - functions took the longest to execute. + You can also try profiling your app using the{" "} + Chrome DevTools profiler to see exactly what functions took the + longest to execute.

)}
); } - case 'low-render-count-high-js': { + case "low-render-count-high-js": { return ( -
+

- This is the period when JavaScript hooks and other JavaScript - outside of React Renders run. + This is the period when JavaScript hooks and other JavaScript outside of React Renders + run.

- There were only {input.data.renderCount} renders - detected, which means either you had very expensive hooks like{' '} - useEffect/useLayoutEffect, or there is - other JavaScript running during this interaction that took up the - majority of the time. + There were only {input.data.renderCount} renders detected, which means + either you had very expensive hooks like useEffect/ + useLayoutEffect, or there is other JavaScript running during this + interaction that took up the majority of the time.

- To understand precisely what caused the slowdown, use the{' '} - Chrome profiler and analyze the function call - times. + To understand precisely what caused the slowdown, use the{" "} + Chrome profiler and analyze the function call times.

); } - case 'high-render-count-update-dom-draw-frame': { + case "high-render-count-update-dom-draw-frame": { return ( -
+

- These are the calculations the browser is forced to do in response - to the JavaScript that ran during the interaction. + These are the calculations the browser is forced to do in response to the JavaScript + that ran during the interaction.

- This can be caused by CSS updates/CSS recalculations, or new DOM - elements/DOM mutations. + This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations.

- During this interaction, there were{' '} - {input.data.count} renders, which was{' '} - {input.data.percentageOfTotal.toFixed(0)}% of the - time spent processing + During this interaction, there were {input.data.count} renders, which + was {input.data.percentageOfTotal.toFixed(0)}% of the time spent + processing

- The work performed as a result of the renders may have forced the - browser to spend a lot of time to draw the next frame. + The work performed as a result of the renders may have forced the browser to spend a lot + of time to draw the next frame.

- You can try optimizing the renders to see if the performance problem - still exists using the "Ranked" tab. + You can try optimizing the renders to see if the performance problem still exists using + the "Ranked" tab.

- If you use an AI-based code editor, you can export the performance - data collected as a prompt. + If you use an AI-based code editor, you can export the performance data collected as a + prompt.

{input.data.copyButton}

- Provide this formatted data to the model and ask it to find, or fix, - what could be causing this performance problem. + Provide this formatted data to the model and ask it to find, or fix, what could be + causing this performance problem.

For a larger selection of prompts, try the "Prompts" tab

); } - case 'update-dom-draw-frame': { + case "update-dom-draw-frame": { return ( -
+

- These are the calculations the browser is forced to do in response - to the JavaScript that ran during the interaction. + These are the calculations the browser is forced to do in response to the JavaScript + that ran during the interaction.

- This can be caused by CSS updates/CSS recalculations, or new DOM - elements/DOM mutations. + This can be caused by CSS updates/CSS recalculations, or new DOM elements/DOM mutations.

- If you use an AI-based code editor, you can export the performance - data collected as a prompt. + If you use an AI-based code editor, you can export the performance data collected as a + prompt.

{input.data.copyButton}

- Provide this formatted data to the model and ask it to find, or fix, - what could be causing this performance problem. + Provide this formatted data to the model and ask it to find, or fix, what could be + causing this performance problem.

For a larger selection of prompts, try the "Prompts" tab

); } - case 'other': { + case "other": { return ( -
+

- This is the time it took to run everything other than React renders. - This can be hooks like useEffect, other JavaScript not - part of React, or work the browser has to do to update the DOM and - draw the next frame. + This is the time it took to run everything other than React renders. This can be hooks + like useEffect, other JavaScript not part of React, or work the browser has + to do to update the DOM and draw the next frame.

- To get a better picture of what happened, profile your app using the{' '} - Chrome profiler when the performance problem - arises. + To get a better picture of what happened, profile your app using the{" "} + Chrome profiler when the performance problem arises.

); diff --git a/packages/scan/src/web/views/notifications/render-bar-chart.tsx b/packages/scan/src/web/views/notifications/render-bar-chart.tsx index 1ec24736..248d804e 100644 --- a/packages/scan/src/web/views/notifications/render-bar-chart.tsx +++ b/packages/scan/src/web/views/notifications/render-bar-chart.tsx @@ -1,35 +1,32 @@ -import { useRef, useState } from 'preact/hooks'; -import { getBatchedRectMap } from 'src/new-outlines'; -import { getIsProduction } from '~core/index'; -import { iife } from '~core/notifications/performance-utils'; -import { cn } from '~web/utils/helpers'; +import { useRef, useState } from "preact/hooks"; +import { getBatchedRectMap } from "src/new-outlines"; +import { getIsProduction } from "~core/index"; +import { iife } from "~core/notifications/performance-utils"; +import { cn } from "~web/utils/helpers"; import { GroupedFiberRender, NotificationEvent, getTotalTime, isRenderMemoizable, useNotificationsContext, -} from './data'; -import { - HighlightStore, - drawHighlights, -} from '~core/notifications/outline-overlay'; -import { ChevronRight } from './icons'; +} from "./data"; +import { HighlightStore, drawHighlights } from "~core/notifications/outline-overlay"; +import { ChevronRight } from "./icons"; // todo: cleanup, convoluted ternaries export const fadeOutHighlights = () => { const curr = HighlightStore.value.current ? HighlightStore.value.current - : HighlightStore.value.kind === 'transition' + : HighlightStore.value.kind === "transition" ? HighlightStore.value.transitionTo : null; if (!curr) { return; } - if (HighlightStore.value.kind === 'transition') { + if (HighlightStore.value.kind === "transition") { HighlightStore.value = { - kind: 'move-out', + kind: "move-out", // because we want to dynamically fade this value current: HighlightStore.value.current?.alpha === 0 @@ -42,7 +39,7 @@ export const fadeOutHighlights = () => { } HighlightStore.value = { - kind: 'move-out', + kind: "move-out", current: { alpha: 0, ...curr, @@ -51,33 +48,29 @@ export const fadeOutHighlights = () => { }; type Bars = Array< - | { kind: 'other-frame-drop'; totalTime: number } - | { kind: 'other-not-javascript'; totalTime: number } - | { kind: 'other-javascript'; totalTime: number } - | { kind: 'render'; event: GroupedFiberRender; totalTime: number } + | { kind: "other-frame-drop"; totalTime: number } + | { kind: "other-not-javascript"; totalTime: number } + | { kind: "other-javascript"; totalTime: number } + | { kind: "render"; event: GroupedFiberRender; totalTime: number } >; -export const NO_PURGE = ['hover:bg-[#0f0f0f]']; - -export const RenderBarChart = ({ - selectedEvent, -}: { selectedEvent: NotificationEvent }) => { +export const RenderBarChart = ({ selectedEvent }: { selectedEvent: NotificationEvent }) => { const totalInteractionTime = getTotalTime(selectedEvent.timing); const nonRender = totalInteractionTime - selectedEvent.timing.renderTime; const [isProduction] = useState(getIsProduction()); const events = selectedEvent.groupedFiberRenders; const bars: Bars = events.map((event) => ({ event, - kind: 'render', + kind: "render", totalTime: isProduction ? event.count : event.totalTime, })); const isShowingExtraInfo = iife(() => { switch (selectedEvent.kind) { - case 'dropped-frames': { + case "dropped-frames": { return selectedEvent.timing.renderTime / totalInteractionTime < 0.1; } - case 'interaction': { + case "interaction": { return ( (selectedEvent.timing.otherJSTime + selectedEvent.timing.renderTime) / totalInteractionTime < @@ -89,17 +82,17 @@ export const RenderBarChart = ({ /** * We don't add the extra bars in production because we can't compare them to the renders, so the bar is useless, user can use overview tab to see times */ - if (selectedEvent.kind === 'interaction' && !isProduction) { + if (selectedEvent.kind === "interaction" && !isProduction) { bars.push({ - kind: 'other-javascript', + kind: "other-javascript", totalTime: selectedEvent.timing.otherJSTime, }); } if (isShowingExtraInfo && !isProduction) { - if (selectedEvent.kind === 'interaction') { + if (selectedEvent.kind === "interaction") { bars.push({ - kind: 'other-not-javascript', + kind: "other-not-javascript", totalTime: getTotalTime(selectedEvent.timing) - selectedEvent.timing.renderTime - @@ -107,7 +100,7 @@ export const RenderBarChart = ({ }); } else { bars.push({ - kind: 'other-frame-drop', + kind: "other-frame-drop", totalTime: nonRender, }); } @@ -124,29 +117,21 @@ export const RenderBarChart = ({ const totalBarTime = bars.reduce((prev, curr) => prev + curr.totalTime, 0); return ( -
+
{iife(() => { if (isProduction && bars.length === 0) { return (
-

- No data available -

-

- No data was collected during this period -

+

No data available

+

No data was collected during this period

); } if (bars.length === 0) { return (
-

- No renders collected -

-

- There were no renders during this period -

+

No renders collected

+

There were no renders during this period

); } @@ -156,7 +141,7 @@ export const RenderBarChart = ({ .toSorted((a, b) => b.totalTime - a.totalTime) .map((bar) => ( { if (!state.current) { - return 'fading-in'; + return "fading-in"; } if (state.current.alpha > 0) { - return 'fading-out' as const; + return "fading-out" as const; } - return 'fading-in' as const; + return "fading-in" as const; }; const RenderBar = ({ @@ -204,41 +189,37 @@ const RenderBar = ({ const { setNotificationState, setRoute } = useNotificationsContext(); const [isExpanded, setIsExpanded] = useState(false); - const isLeaf = bar.kind === 'render' ? bar.event.parents.size === 0 : true; + const isLeaf = bar.kind === "render" ? bar.event.parents.size === 0 : true; const parentBars = bars.filter((otherBar) => - otherBar.kind === 'render' && bar.kind === 'render' - ? bar.event.parents.has(otherBar.event.name) && - otherBar.event.name !== bar.event.name + otherBar.kind === "render" && bar.kind === "render" + ? bar.event.parents.has(otherBar.event.name) && otherBar.event.name !== bar.event.name : false, ); const missingParentNames = - bar.kind === 'render' + bar.kind === "render" ? Array.from(bar.event.parents).filter( - (parentName) => - !bars.some( - (b) => b.kind === 'render' && b.event.name === parentName, - ), + (parentName) => !bars.some((b) => b.kind === "render" && b.event.name === parentName), ) : []; const handleBarClick = () => { - if (bar.kind === 'render') { + if (bar.kind === "render") { setNotificationState((prev) => ({ ...prev, selectedFiber: bar.event, })); setRoute({ - route: 'render-explanation', + route: "render-explanation", routeMessage: null, }); } else { setRoute({ - route: 'other-visualization', + route: "other-visualization", routeMessage: { - kind: 'auto-open-overview-accordion', + kind: "auto-open-overview-accordion", name: bar.kind, }, }); @@ -247,34 +228,33 @@ const RenderBar = ({ return (
-
+