Skip to content

Commit 204ec50

Browse files
committed
improve how source files are resolved
also compute relativePath and absolutePath
1 parent fde948b commit 204ec50

17 files changed

Lines changed: 120 additions & 153 deletions

File tree

.changeset/stale-buses-cover.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@react-trace/plugin-copy-to-clipboard": patch
3+
"@react-trace/plugin-open-editor": patch
4+
"@react-trace/plugin-comments": patch
5+
"@react-trace/plugin-preview": patch
6+
"@react-trace/core": patch
7+
---
8+
9+
Improve how source location is resolved and compute relative and absolute paths

apps/website/docs/extending/plugin-api.mdx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,17 @@ Returns a callback that clears the current selection. Use this when a plugin flo
9191

9292
### `useSelectedSource()`
9393

94-
Returns the currently selected `ComponentSource | null`. This is a convenience for accessing just the source location.
94+
Returns the currently selected `ComponentSource | null`. Each source includes source-mapped file coordinates and pre-computed project-relative and absolute paths.
9595

9696
```ts
9797
interface ComponentSource {
9898
fileName: string
9999
lineNumber: number
100100
columnNumber: number
101+
/** Path relative to the project root, e.g. "src/App.tsx" */
102+
relativePath: string
103+
/** Absolute filesystem path, e.g. "/Users/you/project/src/App.tsx" */
104+
absolutePath: string
101105
}
102106
```
103107

@@ -121,26 +125,23 @@ function MyToolbarButton() {
121125

122126
## Utilities and constants
123127

124-
| Export | Description |
125-
| --------------------------------- | ---------------------------------------------------------------------- |
126-
| `resolveSource(source)` | Resolves URL-based source locations to original source-map positions. |
127-
| `toAbsolutePath(fileName, root?)` | Converts a source filename or Vite URL to an absolute filesystem path. |
128-
| `toRelativePath(fileName, root?)` | Converts a source filename or Vite URL to a project-relative path. |
129-
| `settingsPluginAtom(pluginKey)` | Returns a writable Jotai atom for a section of `TraceSettings`. |
130-
| `IS_MAC` | `true` on macOS/iOS user agents. |
131-
| `MOD_KEY` | Platform-specific modifier key label (`` on Mac, `Ctrl` elsewhere). |
128+
| Export | Description |
129+
| ------------------------------- | -------------------------------------------------------------------- |
130+
| `settingsPluginAtom(pluginKey)` | Returns a writable Jotai atom for a section of `TraceSettings`. |
131+
| `IS_MAC` | `true` on macOS/iOS user agents. |
132+
| `MOD_KEY` | Platform-specific modifier key label (`` on Mac, `Ctrl` elsewhere). |
132133

133134
## Types
134135

135136
All types are exported from `@react-trace/core`:
136137

137-
| Type | Description |
138-
| ------------------ | ----------------------------------------------------------------------------------- |
139-
| `ComponentContext` | Full context of an inspected component (element, name, breadcrumb, props, sources). |
140-
| `ComponentSource` | Source file location (fileName, lineNumber, columnNumber). |
141-
| `TracePlugin` | The plugin contract (name + optional toolbar/actionPanel/settings components). |
142-
| `TraceProps` | Props accepted by the `Trace` component. |
143-
| `TraceSettings` | Shape of the widget settings state. |
138+
| Type | Description |
139+
| ------------------ | -------------------------------------------------------------------------------------- |
140+
| `ComponentContext` | Full context of an inspected component (element, name, breadcrumb, props, sources). |
141+
| `ComponentSource` | Source file location (fileName, lineNumber, columnNumber, relativePath, absolutePath). |
142+
| `TracePlugin` | The plugin contract (name + optional toolbar/actionPanel/settings components). |
143+
| `TraceProps` | Props accepted by the `Trace` component. |
144+
| `TraceSettings` | Shape of the widget settings state. |
144145

145146
## Production stubs
146147

packages/core/README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,14 @@ export function AppShell() {
117117

118118
## Exported utilities and constants
119119

120-
- `resolveSource(source)` — resolves URL-based source locations back to original source-map positions when possible
121-
- `toAbsolutePath(fileName, root?)` — converts a source filename or Vite URL to an absolute filesystem path
122-
- `toRelativePath(fileName, root?)` — converts a source filename or Vite URL to a path relative to the project root when possible
123120
- `settingsPluginAtom(pluginKey)` — returns a writable Jotai atom for a section of `TraceSettings`
124121
- `IS_MAC``true` on macOS/iOS user agents
125122
- `MOD_KEY` — platform-specific modifier key label (`` or `Ctrl`)
126123

127124
## Exported types
128125

129-
- `ComponentContext`
130-
- `ComponentSource`
126+
- `ComponentContext` — full context of an inspected component (element, name, breadcrumb, props, sources)
127+
- `ComponentSource` — source file location (`fileName`, `lineNumber`, `columnNumber`, `relativePath`, `absolutePath`)
131128
- `TracePlugin`
132129
- `TraceProps`
133130
- `TraceSettings`

packages/core/src/components/ActionPanel.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { useCallback } from 'react'
1111

1212
import {
1313
portalContainerAtom,
14+
projectRootAtom,
1415
selectedContextAtom,
1516
selectedSourceAtom,
1617
} from '../store'
1718
import type { ComponentContext, ComponentSource, TracePlugin } from '../types'
18-
import { toRelativePath } from '../utils/path'
1919
import { ErrorBoundary } from './ErrorBoundary'
2020

2121
interface ActionPanelProps {
@@ -30,14 +30,18 @@ type ChainGroup =
3030
| { kind: 'entry'; names: string[]; source: ComponentSource; index: number }
3131
| { kind: 'third-party'; names: string[][]; count: number }
3232

33-
function groupChain(all: ComponentContext['all']): ChainGroup[] {
33+
function groupChain(root: string, all: ComponentContext['all']): ChainGroup[] {
3434
const result: ChainGroup[] = []
3535
let tpCount = 0
3636
let names: string[][] = []
3737

3838
for (let i = 0; i < all.length; i++) {
3939
const entry = all[i]!
40-
if (!entry.source) {
40+
if (
41+
!entry.source ||
42+
!entry.source.absolutePath.startsWith(root) ||
43+
entry.source.relativePath.startsWith('node_modules/')
44+
) {
4145
tpCount++
4246
names.push(entry.names)
4347
} else {
@@ -64,8 +68,7 @@ function groupChain(all: ComponentContext['all']): ChainGroup[] {
6468
// ---------------------------------------------------------------------------
6569

6670
function SourceLabel({ source }: { source: ComponentSource }) {
67-
const rel = toRelativePath(source.fileName)
68-
const short = rel.split('/').slice(-2).join('/')
71+
const short = source.relativePath.split('/').slice(-2).join('/')
6972

7073
return (
7174
<span
@@ -74,7 +77,7 @@ function SourceLabel({ source }: { source: ComponentSource }) {
7477
fontFamily: 'ui-monospace, monospace',
7578
color: '#97979b',
7679
}}
77-
title={`${rel}:${source.lineNumber}`}
80+
title={`${source.relativePath}:${source.lineNumber}`}
7881
>
7982
{short}:{source.lineNumber}
8083
</span>
@@ -108,9 +111,12 @@ function entryStyle(active: boolean): React.CSSProperties {
108111
// ---------------------------------------------------------------------------
109112

110113
export function ActionPanel({ plugins }: ActionPanelProps) {
114+
const projectRoot = useAtomValue(projectRootAtom)
111115
const [selectedContext, setSelectedContext] = useAtom(selectedContextAtom)
112116
const portalContainer = useAtomValue(portalContainerAtom)
113-
const groups = selectedContext ? groupChain(selectedContext.all) : []
117+
const groups = selectedContext
118+
? groupChain(projectRoot, selectedContext.all)
119+
: []
114120

115121
const onClose = useCallback(
116122
() => setSelectedContext(null),

packages/core/src/hooks/useInspectorBehavior.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
44
import {
55
inspectorActiveAtom,
66
portalContainerAtom,
7+
projectRootAtom,
78
selectedContextAtom,
89
} from '../store'
910
import type { ComponentContext } from '../types'
@@ -22,6 +23,7 @@ import { useEffectEvent } from './useEffectEvent'
2223
export function useInspectorBehavior() {
2324
const portalContainer = useAtomValue(portalContainerAtom)
2425
const inspectorActive = useAtomValue(inspectorActiveAtom)
26+
const root = useAtomValue(projectRootAtom)
2527
const setInspectorActive = useSetAtom(inspectorActiveAtom)
2628
const [hoveredContext, setHoveredContext] = useState<ComponentContext | null>(
2729
null,
@@ -60,7 +62,7 @@ export function useInspectorBehavior() {
6062
lastHoveredElement = target
6163

6264
try {
63-
const ctx = await getComponentContext(target)
65+
const ctx = await getComponentContext(target, root)
6466
if (lastHoveredElement !== target) return
6567
setHoveredContext(ctx)
6668
} catch {}

packages/core/src/index.prod.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import type { WritableAtom } from 'jotai'
77

8-
import type { ComponentSource, TraceSettings } from './types'
8+
import type { TraceSettings } from './types'
99

1010
const NOOP = () => {}
1111

@@ -19,10 +19,6 @@ export const useClearSelectedContext = () => NOOP
1919
export const useSelectedSource = () => null
2020
export const useWidgetPortalContainer = () => null
2121

22-
export const resolveSource = async (source: ComponentSource) => source
23-
export const toAbsolutePath = (path: string) => path
24-
export const toRelativePath = (path: string) => path
25-
2622
export { IS_MAC, MOD_KEY } from './utils/platform'
2723

2824
export function settingsPluginAtom<K extends keyof TraceSettings>(

packages/core/src/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ export {
88
useSelectedSource,
99
useWidgetPortalContainer,
1010
} from './hooks'
11-
export { resolveSource } from './utils/fiber'
12-
export { toAbsolutePath, toRelativePath } from './utils/path'
1311
export { IS_MAC, MOD_KEY } from './utils/platform'
1412

1513
export { settingsPluginAtom } from './store'

packages/core/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ export interface ComponentSource {
44
fileName: string
55
lineNumber: number
66
columnNumber: number
7+
/** Path relative to the project root, e.g. "src/App.tsx" */
8+
relativePath: string
9+
/** Absolute filesystem path, e.g. "/Users/you/project/src/App.tsx" */
10+
absolutePath: string
711
}
812

913
export interface ComponentContext {

packages/core/src/utils/fiber.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'
22
import type { EncodedSourceMap } from '@jridgewell/trace-mapping'
33

44
import type { ComponentContext, ComponentSource } from '../types'
5+
import { toAbsolutePath, toRelativePath } from './path'
6+
7+
/**
8+
* Raw source location extracted from React fibers before path enrichment.
9+
* Missing the `relativePath` and `absolutePath` fields that are computed
10+
* in `getComponentContext` once the project root is known.
11+
*/
12+
type RawSource = Omit<ComponentSource, 'relativePath' | 'absolutePath'>
513

614
// ---------------------------------------------------------------------------
715
// Source map resolution
@@ -67,9 +75,7 @@ function loadTraceMap(fileUrl: string): Promise<TraceMap | null> {
6775
* so the UI updates the line number once the source map resolves (usually <50ms
6876
* after the first hover on a given file, instant on subsequent hovers).
6977
*/
70-
export async function resolveSource(
71-
source: ComponentSource,
72-
): Promise<ComponentSource> {
78+
async function resolveSource(source: RawSource): Promise<RawSource | null> {
7379
// Only attempt resolution for URL-form fileNames
7480
try {
7581
new URL(source.fileName)
@@ -86,7 +92,7 @@ export async function resolveSource(
8692
column: source.columnNumber - 1,
8793
})
8894

89-
if (original.source == null || original.line == null) return source
95+
if (original.source == null || original.line == null) return null
9096

9197
return {
9298
fileName: original.source,
@@ -149,7 +155,7 @@ interface ReactFiber {
149155
/** React 19: Error object captured at JSX creation time, stack contains the callsite */
150156
_debugStack: Error | null
151157
/** React 18: structured source object injected by Babel/SWC's JSX source transform */
152-
_debugSource: ComponentSource | null
158+
_debugSource: RawSource | null
153159
_debugOwner: ReactFiber | null
154160
return: ReactFiber | null
155161
stateNode?: HTMLElement | null
@@ -197,9 +203,7 @@ function getDisplayName(type: ComponentType): string {
197203
* The fileName may be a full URL in Vite dev mode (e.g. "http://localhost:5173/src/Button.tsx").
198204
* URL → absolute path normalization is deferred to the FileSystemService.
199205
*/
200-
function parseComponentSource(
201-
debugStack: Error | null,
202-
): ComponentSource | null {
206+
function parseComponentSource(debugStack: Error | null): RawSource | null {
203207
if (!debugStack?.stack) return null
204208

205209
let stack = debugStack.stack
@@ -254,7 +258,7 @@ function parseComponentSource(
254258
* React 18 paths are absolute filesystem paths (/abs/path/to/File.tsx) so
255259
* resolveSource() will no-op on them — they're already at original positions.
256260
*/
257-
function getSource(fiber: ReactFiber | null): ComponentSource | null {
261+
function getSource(fiber: ReactFiber | null): RawSource | null {
258262
if (!fiber) return null
259263
if (fiber._debugStack) return parseComponentSource(fiber._debugStack)
260264
if (fiber._debugSource) return fiber._debugSource
@@ -285,6 +289,7 @@ function getSource(fiber: ReactFiber | null): ComponentSource | null {
285289
*/
286290
export async function getComponentContext(
287291
element: HTMLElement,
292+
root: string,
288293
): Promise<ComponentContext | null> {
289294
const domFiber = findFiber(element)
290295
if (!domFiber) return null
@@ -295,7 +300,7 @@ export async function getComponentContext(
295300
// raw → ['code', 'p', 'Card']
296301
// display → ['Card', 'p', 'code']
297302
let fiber: ReactFiber | null = domFiber
298-
const parts: Array<{ source: ComponentSource | null; names: string[] }> = [
303+
const parts: Array<{ source: RawSource | null; names: string[] }> = [
299304
{ source: getSource(fiber), names: [] },
300305
]
301306
let i = 0
@@ -320,17 +325,28 @@ export async function getComponentContext(
320325
parts.map((part) => (part.source ? resolveSource(part.source) : null)),
321326
)
322327

328+
/** Enrich a raw source with computed relative and absolute paths. */
329+
function enrichSource(raw: RawSource): ComponentSource {
330+
return {
331+
...raw,
332+
relativePath: toRelativePath(raw.fileName, root),
333+
absolutePath: toAbsolutePath(raw.fileName, root) ?? raw.fileName,
334+
}
335+
}
336+
323337
// parts: [hovered, …ancestors, Component] — reverse for display order
324-
const all = parts.map((part, i) => ({
325-
source: sources[i] ?? part.source,
326-
names: part.names.reverse(),
327-
}))
338+
const all = parts.map((part, idx) => {
339+
const resolved = sources[idx]
340+
return {
341+
source: resolved ? enrichSource(resolved) : null,
342+
names: part.names.reverse(),
343+
}
344+
})
328345

329-
// deduplicate same files
346+
// remove files without a source
330347
const files = all.reduce<typeof all>((acc, part) => {
331348
if (!part.source) return acc
332-
const file = part.source.fileName
333-
if (!acc.some((p) => p.source?.fileName === file)) acc.push(part)
349+
acc.push(part)
334350
return acc
335351
}, [])
336352

packages/plugin-comments/src/index.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type { TracePlugin } from '@react-trace/core'
22
import {
3-
resolveSource,
4-
toRelativePath,
53
useClearSelectedContext,
64
useDeactivateInspector,
7-
useProjectRoot,
85
useSelectedContext,
96
useSelectedSource,
107
useWidgetPortalContainer,
@@ -113,31 +110,24 @@ function CommentsToolbar() {
113110
function CommentsActionPanel() {
114111
const selectedContext = useSelectedContext()
115112
const selectedSource = useSelectedSource()
116-
const root = useProjectRoot()
117113
const { setPending } = useCommentsActions()
118114
const clearSelectedContext = useClearSelectedContext()
119115
const deactivateInspector = useDeactivateInspector()
120116

121117
if (!selectedContext || !selectedSource) return null
122118

123-
const handleAddComment = async () => {
124-
const { fileName, lineNumber } = await resolveSource(selectedSource)
125-
const filePath = toRelativePath(fileName, root)
126-
119+
const handleAddComment = () => {
127120
setPending({
128-
filePath,
129-
lineNumber,
121+
filePath: selectedSource.relativePath,
122+
lineNumber: selectedSource.lineNumber,
130123
anchorEl: selectedContext.element,
131124
})
132125
clearSelectedContext()
133126
deactivateInspector()
134127
}
135128

136129
return (
137-
<DropdownMenu.Item
138-
closeOnClick
139-
onClick={() => handleAddComment().catch(() => {})}
140-
>
130+
<DropdownMenu.Item closeOnClick onClick={handleAddComment}>
141131
<span style={{ display: 'flex', alignItems: 'center' }}>
142132
<ChatBubbleIcon />
143133
</span>

0 commit comments

Comments
 (0)