react-dev-inspector
Version:
dev-tool for inspect react components and jump to local IDE for component code.
254 lines (212 loc) • 7.56 kB
text/typescript
import type { Fiber, Source } from 'react-reconciler'
import {
isNativeTagFiber,
isReactSymbolFiber,
isForwardRef,
getDirectParentFiber,
getFiberName,
getElementFiberUpward,
} from './fiber'
export interface CodeInfo {
lineNumber: string;
columnNumber: string;
/**
* code source file relative path to dev-server cwd(current working directory)
* need use with `react-dev-inspector/plugins/babel`
*/
relativePath?: string;
/**
* code source file absolute path
* just need use with `@babel/plugin-transform-react-jsx-source` which auto set by most framework
*/
absolutePath?: string;
}
/**
* props that injected into react nodes
*
* like <div data-inspector-line="2" data-inspector-column="3" data-inspector-relative-path="xxx/ooo" />
* this props will be record in fiber
*/
export interface CodeDataAttribute {
'data-inspector-line': string;
'data-inspector-column': string;
'data-inspector-relative-path': string;
}
/**
* react fiber property `_debugSource` created by `@babel/plugin-transform-react-jsx-source`
* https://github.com/babel/babel/blob/v7.16.4/packages/babel-plugin-transform-react-jsx-source/src/index.js
*
* and injected `__source` property used by `React.createElement`, then pass to `ReactElement`
* https://github.com/facebook/react/blob/v18.0.0/packages/react/src/ReactElement.js#L189
* https://github.com/facebook/react/blob/v18.0.0/packages/react/src/ReactElement.js#L389
* https://github.com/facebook/react/blob/v18.0.0/packages/react/src/ReactElement.js#L447
*
* finally, used by `createFiberFromElement` to become a fiber property `_debugSource`.
* https://github.com/facebook/react/blob/v18.0.0/packages/react-reconciler/src/ReactFiber.new.js#L648-L649
*/
export const getCodeInfoFromDebugSource = (fiber?: Fiber): CodeInfo | undefined => {
if (!fiber) return undefined
const debugSource = (
fiber._debugSource
?? fiber._debugOwner?._debugSource
) as Source & { columnNumber?: number }
if (!debugSource) return undefined
const {
fileName,
lineNumber,
columnNumber,
} = debugSource
if (fileName && lineNumber) {
return {
lineNumber: String(lineNumber),
columnNumber: String(columnNumber ?? 1),
/**
* `fileName` in `_debugSource` is absolutely
* ---
*
* compatible with the incorrect `fileName: "</xxx/file>"` by [rspack](https://github.com/web-infra-dev/rspack)
*/
absolutePath: fileName.match(/^<.*>$/)
? fileName.replace(/^<|>$/g, '')
: fileName,
}
}
return undefined
}
/**
* code location data-attribute props inject by `react-dev-inspector/plugins/babel`
*/
export const getCodeInfoFromProps = (fiber?: Fiber): CodeInfo | undefined => {
if (!fiber?.pendingProps) return undefined
const {
'data-inspector-line': lineNumber,
'data-inspector-column': columnNumber,
'data-inspector-relative-path': relativePath,
} = fiber.pendingProps as CodeDataAttribute
if (lineNumber && columnNumber && relativePath) {
return {
lineNumber,
columnNumber,
relativePath,
}
}
return undefined
}
export const getCodeInfoFromFiber = (fiber?: Fiber): CodeInfo | undefined => {
const codeInfos = [
getCodeInfoFromDebugSource(fiber),
getCodeInfoFromProps(fiber),
].filter(Boolean) as CodeInfo[]
if (!codeInfos.length) return undefined
return Object.assign({}, ...codeInfos)
}
/**
* give a `base` dom fiber,
* and will try to get the human friendly react component `reference` fiber from it;
*
* rules and examples see below:
* *******************************************************
*
* if parent is html native tag, `reference` is considered to be as same as `base`
*
* div div
* └─ h1 └─ h1 (<--base) <--reference
* └─ span (<--base) <--reference └─ span
*
* *******************************************************
*
* if parent is NOT html native tag,
* and parent ONLY have one child (the `base` itself),
* then `reference` is considered to be the parent.
*
* Title <--reference Title
* └─ h1 (<--base) └─ h1 (<--base) <--reference
* └─ span └─ span
* └─ div
*
* *******************************************************
*
* while follow the last one,
* "parent" is considered to skip continuous Provider/Customer/ForwardRef components
*
* Title <- reference Title <- reference
* └─ TitleName [ForwardRef] └─ TitleName [ForwardRef]
* └─ Context.Customer └─ Context.Customer
* └─ Context.Customer └─ Context.Customer
* └─ h1 (<- base) └─ h1 (<- base)
* └─ span └─ span
* └─ div
*
* Title
* └─ TitleName [ForwardRef]
* └─ Context.Customer
* └─ Context.Customer
* └─ h1 (<- base) <- reference
* └─ span
* └─ div
*/
export const getReferenceFiber = (baseFiber?: Fiber): Fiber | undefined => {
if (!baseFiber) return undefined
const directParent = getDirectParentFiber(baseFiber)
if (!directParent) return undefined
const isParentNative = isNativeTagFiber(directParent)
const isOnlyOneChild = !directParent.child!.sibling
let referenceFiber = (!isParentNative && isOnlyOneChild)
? directParent
: baseFiber
// fallback for cannot find code-info fiber when traverse to root
const originReferenceFiber = referenceFiber
while (referenceFiber) {
if (getCodeInfoFromFiber(referenceFiber)) return referenceFiber
referenceFiber = referenceFiber.return!
}
return originReferenceFiber
}
export const getElementCodeInfo = (element: HTMLElement): CodeInfo | undefined => {
const fiber: Fiber | undefined = getElementFiberUpward(element)
const referenceFiber = getReferenceFiber(fiber)
return getCodeInfoFromFiber(referenceFiber)
}
export const getNamedFiber = (baseFiber?: Fiber): Fiber | undefined => {
let fiber = baseFiber
// fallback for cannot find code-info fiber when traverse to root
let originNamedFiber: Fiber | undefined
while (fiber) {
let parent = fiber.return ?? undefined
let forwardParent: Fiber | undefined
while (isReactSymbolFiber(parent)) {
if (isForwardRef(parent)) {
forwardParent = parent
}
parent = parent?.return ?? undefined
}
if (forwardParent) {
fiber = forwardParent
}
if (getFiberName(fiber)) {
if (!originNamedFiber) originNamedFiber = fiber
if (getCodeInfoFromFiber(fiber)) return fiber
}
fiber = parent!
}
return originNamedFiber
}
export const getElementInspect = (element: HTMLElement): {
fiber?: Fiber;
name?: string;
title: string;
} => {
const fiber = getElementFiberUpward(element)
const referenceFiber = getReferenceFiber(fiber)
const namedFiber = getNamedFiber(referenceFiber)
const fiberName = getFiberName(namedFiber)
const nodeName = element.nodeName.toLowerCase()
const title = fiberName
? `${nodeName} in <${fiberName}>`
: nodeName
return {
fiber: referenceFiber,
name: fiberName,
title,
}
}