proto-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +350 -0
- package/README.md +279 -0
- package/components.json +18 -0
- package/package.json +73 -0
- package/scripts/download-mobbin-reference-images.mjs +112 -0
- package/scripts/lib/host-config.example.sh +48 -0
- package/scripts/lib/host-config.sh +25 -0
- package/scripts/lib/resolve-host-root.mjs +18 -0
- package/scripts/lib/source-path.sh +49 -0
- package/scripts/link-source-db.sh +128 -0
- package/scripts/link-source.sh +40 -0
- package/scripts/publish-package.sh +147 -0
- package/scripts/share-prototype.sh +279 -0
- package/scripts/sync-from-source.sh +74 -0
- package/scripts/verify-prototype-component-ids.mjs +151 -0
- package/scripts/verify-prototype-preview-states.mjs +135 -0
- package/src/app.ts +4 -0
- package/src/components/platform-ui/code-block.tsx +21 -0
- package/src/components/platform-ui/controls-panel-options.tsx +71 -0
- package/src/components/platform-ui/controls-panel-select.tsx +438 -0
- package/src/components/platform-ui/empty-state.tsx +15 -0
- package/src/components/platform-ui/icon-button.tsx +18 -0
- package/src/components/platform-ui/panel-select.tsx +123 -0
- package/src/components/platform-ui/panel.tsx +97 -0
- package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
- package/src/components/platform-ui/sidebar.tsx +293 -0
- package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
- package/src/components/platform-ui/toolbar.tsx +37 -0
- package/src/components/prototype-gallery-client.tsx +57 -0
- package/src/components/prototype-provider.tsx +20 -0
- package/src/components/prototypes/mini-pill-label.tsx +37 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
- package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
- package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
- package/src/components/prototypes/prototype-control.tsx +31 -0
- package/src/components/prototypes/prototype-controls.tsx +553 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
- package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
- package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
- package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
- package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
- package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
- package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
- package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
- package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
- package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
- package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
- package/src/components/prototypes/prototype-share-mode.tsx +206 -0
- package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
- package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
- package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
- package/src/components/prototypes/prototype-target.tsx +120 -0
- package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
- package/src/components/prototypes/prototype-tweaks.tsx +48 -0
- package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
- package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
- package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
- package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
- package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
- package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
- package/src/components/shell/prototype-gallery-shell.tsx +79 -0
- package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
- package/src/components/shell/prototype-shell.module.scss +194 -0
- package/src/components/shell/prototype-shell.tsx +160 -0
- package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.module.scss +81 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/option-select.tsx +81 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toggle-group.tsx +80 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/config/create-prototype-gallery-page.tsx +65 -0
- package/src/config/create-prototype-host-app.tsx +121 -0
- package/src/config/create-prototype-page.tsx +41 -0
- package/src/config/create-prototype-site-layout.tsx +28 -0
- package/src/config/define-prototype-config.ts +7 -0
- package/src/config/prototype-icon.tsx +25 -0
- package/src/config/prototype-site-layout-client.tsx +35 -0
- package/src/config/with-prototype.ts +20 -0
- package/src/index.ts +303 -0
- package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
- package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
- package/src/lib/pr-split/pr-split-highlight.ts +6 -0
- package/src/lib/pr-split/pr-split-types.ts +58 -0
- package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
- package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
- package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
- package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
- package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
- package/src/lib/prototype-comments/core/capture.ts +11 -0
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
- package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
- package/src/lib/prototype-comments/core/element-identification.ts +615 -0
- package/src/lib/prototype-comments/core/export.ts +35 -0
- package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
- package/src/lib/prototype-comments/core/identify-element.ts +76 -0
- package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
- package/src/lib/prototype-comments/core/react-detection.ts +704 -0
- package/src/lib/prototype-comments/core/restore.ts +7 -0
- package/src/lib/prototype-comments/core/screenshot.ts +428 -0
- package/src/lib/prototype-comments/core/source-location.ts +906 -0
- package/src/lib/prototype-comments/core/storage.ts +80 -0
- package/src/lib/prototype-comments/core/types.ts +79 -0
- package/src/lib/prototype-comments/core/validation.ts +17 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
- package/src/lib/prototype-comments/index.ts +58 -0
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
- package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
- package/src/lib/prototype-comments/scss.d.ts +10 -0
- package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
- package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
- package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
- package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
- package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
- package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
- package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
- package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
- package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
- package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
- package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
- package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
- package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
- package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
- package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
- package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
- package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
- package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
- package/src/lib/prototypes/changelog-meta.ts +95 -0
- package/src/lib/prototypes/create-prototype-registry.ts +74 -0
- package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
- package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
- package/src/lib/prototypes/design-exploration-default.ts +51 -0
- package/src/lib/prototypes/design-exploration-types.ts +203 -0
- package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
- package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
- package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
- package/src/lib/prototypes/prototype-component-registry.ts +40 -0
- package/src/lib/prototypes/prototype-config-types.ts +34 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
- package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
- package/src/lib/prototypes/prototype-review-context.tsx +905 -0
- package/src/lib/prototypes/prototype-review-url.ts +134 -0
- package/src/lib/prototypes/prototype-share-link.ts +175 -0
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
- package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
- package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
- package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
- package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
- package/src/lib/prototypes/reference-docs.ts +137 -0
- package/src/lib/prototypes/screenshot-manifest.ts +33 -0
- package/src/lib/prototypes/screenshot-src.ts +7 -0
- package/src/lib/prototypes/share-command.ts +8 -0
- package/src/lib/prototypes/state-map-annotation.ts +70 -0
- package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
- package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
- package/src/lib/prototypes/use-prototype-comments.ts +63 -0
- package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
- package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
- package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
- package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
- package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
- package/src/lib/tool-portal.ts +33 -0
- package/src/lib/use-copy-to-clipboard.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
- package/src/server/create-host-api-route.ts +100 -0
- package/src/server/create-prototype-api-routes.ts +428 -0
- package/src/server/gallery/create-gallery-api.ts +56 -0
- package/src/server/gallery/folders-route.ts +156 -0
- package/src/server/gallery/gallery-env.ts +39 -0
- package/src/server/gallery/gallery-route.ts +150 -0
- package/src/server/gallery/gallery-types.ts +15 -0
- package/src/server/gallery/image-route.ts +59 -0
- package/src/server/gallery/item-route.ts +92 -0
- package/src/server/pr-vercel-preview-route.ts +162 -0
- package/src/server/redis/client.ts +55 -0
- package/src/server/redis/prototype-changelog-meta.ts +31 -0
- package/src/server/redis/prototype-comments.ts +92 -0
- package/src/server/redis/prototype-design-exploration-default.ts +32 -0
- package/src/server.ts +51 -0
- package/src/styles/globals.css +624 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Source Location Detection Utilities
|
|
5
|
+
// =============================================================================
|
|
6
|
+
//
|
|
7
|
+
// This module provides utilities for detecting React source file locations from
|
|
8
|
+
// DOM elements. It works by accessing React's internal fiber tree and extracting
|
|
9
|
+
// _debugSource information that's available in development builds.
|
|
10
|
+
//
|
|
11
|
+
// Compatibility:
|
|
12
|
+
// - React 16.8+ (Hooks era)
|
|
13
|
+
// - React 17.x
|
|
14
|
+
// - React 18.x
|
|
15
|
+
// - React 19.x (with fallbacks for changed internals)
|
|
16
|
+
//
|
|
17
|
+
// Limitations:
|
|
18
|
+
// - Only works in development builds (production builds strip _debugSource)
|
|
19
|
+
// - Requires React DevTools-style fiber access
|
|
20
|
+
// - Some bundlers may strip debug info even in development
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Source location information for a React component
|
|
25
|
+
*/
|
|
26
|
+
export interface SourceLocation {
|
|
27
|
+
/** Absolute or relative file path */
|
|
28
|
+
fileName: string;
|
|
29
|
+
/** Line number (1-indexed) */
|
|
30
|
+
lineNumber: number;
|
|
31
|
+
/** Column number (0-indexed, may be undefined) */
|
|
32
|
+
columnNumber?: number;
|
|
33
|
+
/** Component display name if available */
|
|
34
|
+
componentName?: string;
|
|
35
|
+
/** React version detected */
|
|
36
|
+
reactVersion?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Result of source location detection
|
|
41
|
+
*/
|
|
42
|
+
export interface SourceLocationResult {
|
|
43
|
+
/** Whether source location was found */
|
|
44
|
+
found: boolean;
|
|
45
|
+
/** Source location data (if found) */
|
|
46
|
+
source?: SourceLocation;
|
|
47
|
+
/** Reason if not found */
|
|
48
|
+
reason?: SourceLocationNotFoundReason;
|
|
49
|
+
/** Whether the app appears to be a React app */
|
|
50
|
+
isReactApp: boolean;
|
|
51
|
+
/** Whether running in production mode */
|
|
52
|
+
isProduction: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reasons why source location might not be found
|
|
57
|
+
*/
|
|
58
|
+
export type SourceLocationNotFoundReason =
|
|
59
|
+
| "not-react-app"
|
|
60
|
+
| "production-build"
|
|
61
|
+
| "no-fiber"
|
|
62
|
+
| "no-debug-source"
|
|
63
|
+
| "react-19-changed"
|
|
64
|
+
| "element-not-in-react-tree"
|
|
65
|
+
| "unknown";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* React Fiber node structure (partial, for type safety)
|
|
69
|
+
* Based on React's internal FiberNode type
|
|
70
|
+
*/
|
|
71
|
+
interface ReactFiber {
|
|
72
|
+
// Debug source info (only in development)
|
|
73
|
+
_debugSource?: {
|
|
74
|
+
fileName: string;
|
|
75
|
+
lineNumber: number;
|
|
76
|
+
columnNumber?: number;
|
|
77
|
+
};
|
|
78
|
+
// Owner info (React 19 may use this differently)
|
|
79
|
+
_debugOwner?: ReactFiber;
|
|
80
|
+
// Component type
|
|
81
|
+
type?: {
|
|
82
|
+
name?: string;
|
|
83
|
+
displayName?: string;
|
|
84
|
+
// For class components
|
|
85
|
+
prototype?: {
|
|
86
|
+
isReactComponent?: boolean;
|
|
87
|
+
};
|
|
88
|
+
} | string | null;
|
|
89
|
+
// Element type for built-in elements
|
|
90
|
+
elementType?: unknown;
|
|
91
|
+
// Tag indicating fiber type
|
|
92
|
+
tag?: number;
|
|
93
|
+
// Fiber tree navigation
|
|
94
|
+
return?: ReactFiber | null;
|
|
95
|
+
child?: ReactFiber | null;
|
|
96
|
+
sibling?: ReactFiber | null;
|
|
97
|
+
// Memoized props (for context)
|
|
98
|
+
memoizedProps?: Record<string, unknown>;
|
|
99
|
+
// State node for class components
|
|
100
|
+
stateNode?: unknown;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extended HTMLElement with React fiber properties
|
|
105
|
+
*/
|
|
106
|
+
interface ReactDOMElement extends HTMLElement {
|
|
107
|
+
// React 16-17 fiber key
|
|
108
|
+
__reactFiber$?: string;
|
|
109
|
+
// React 18+ fiber key pattern
|
|
110
|
+
__reactFiber?: ReactFiber;
|
|
111
|
+
// React internal instance (older pattern)
|
|
112
|
+
__reactInternalInstance$?: string;
|
|
113
|
+
// Alternative patterns
|
|
114
|
+
_reactRootContainer?: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// React fiber tag constants (for reference)
|
|
118
|
+
const FIBER_TAGS = {
|
|
119
|
+
FunctionComponent: 0,
|
|
120
|
+
ClassComponent: 1,
|
|
121
|
+
IndeterminateComponent: 2,
|
|
122
|
+
HostRoot: 3,
|
|
123
|
+
HostPortal: 4,
|
|
124
|
+
HostComponent: 5,
|
|
125
|
+
HostText: 6,
|
|
126
|
+
Fragment: 7,
|
|
127
|
+
Mode: 8,
|
|
128
|
+
ContextConsumer: 9,
|
|
129
|
+
ContextProvider: 10,
|
|
130
|
+
ForwardRef: 11,
|
|
131
|
+
Profiler: 12,
|
|
132
|
+
SuspenseComponent: 13,
|
|
133
|
+
MemoComponent: 14,
|
|
134
|
+
SimpleMemoComponent: 15,
|
|
135
|
+
LazyComponent: 16,
|
|
136
|
+
} as const;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Checks if the page appears to be running a React application
|
|
140
|
+
*
|
|
141
|
+
* @returns Object with detection results
|
|
142
|
+
*/
|
|
143
|
+
export function detectReactApp(): {
|
|
144
|
+
isReact: boolean;
|
|
145
|
+
version?: string;
|
|
146
|
+
isProduction: boolean;
|
|
147
|
+
} {
|
|
148
|
+
if (typeof window === "undefined") {
|
|
149
|
+
return { isReact: false, isProduction: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for React DevTools hook (most reliable)
|
|
153
|
+
const devToolsHook = (window as unknown as Record<string, unknown>).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
154
|
+
|
|
155
|
+
if (devToolsHook && typeof devToolsHook === "object") {
|
|
156
|
+
const hook = devToolsHook as Record<string, unknown>;
|
|
157
|
+
|
|
158
|
+
// Check for renderers (React 16+)
|
|
159
|
+
const renderers = hook.renderers as Map<number, { version?: string }> | undefined;
|
|
160
|
+
if (renderers && renderers.size > 0) {
|
|
161
|
+
// Get version from first renderer
|
|
162
|
+
const firstRenderer = renderers.values().next().value;
|
|
163
|
+
const version = firstRenderer?.version;
|
|
164
|
+
|
|
165
|
+
// Check for production mode via lack of development tools
|
|
166
|
+
const isProduction = !hook.supportsFiber;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
isReact: true,
|
|
170
|
+
version: version || "unknown",
|
|
171
|
+
isProduction,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Fallback: Check for React root markers on DOM
|
|
177
|
+
const hasReactRoot = document.querySelector("[data-reactroot]") !== null;
|
|
178
|
+
const hasReactContainer =
|
|
179
|
+
(document.getElementById("root") as HTMLElement & { _reactRootContainer?: unknown })
|
|
180
|
+
?._reactRootContainer !== undefined;
|
|
181
|
+
|
|
182
|
+
// Check for fiber keys on body's children
|
|
183
|
+
const bodyChildren = document.body.children;
|
|
184
|
+
let hasFiberKey = false;
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < bodyChildren.length && !hasFiberKey; i++) {
|
|
187
|
+
const child = bodyChildren[i];
|
|
188
|
+
const keys = Object.keys(child);
|
|
189
|
+
hasFiberKey = keys.some(
|
|
190
|
+
(key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (hasReactRoot || hasReactContainer || hasFiberKey) {
|
|
195
|
+
return {
|
|
196
|
+
isReact: true,
|
|
197
|
+
version: "unknown",
|
|
198
|
+
// Assume production if we can't detect dev tools
|
|
199
|
+
isProduction: !devToolsHook,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { isReact: false, isProduction: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Gets the React fiber node associated with a DOM element
|
|
208
|
+
*
|
|
209
|
+
* @param element - DOM element to get fiber for
|
|
210
|
+
* @returns React fiber node or null if not found
|
|
211
|
+
*/
|
|
212
|
+
export function getFiberFromElement(element: HTMLElement): ReactFiber | null {
|
|
213
|
+
if (!element || typeof element !== "object") {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const keys = Object.keys(element);
|
|
218
|
+
|
|
219
|
+
// React 18+ uses __reactFiber$ prefix
|
|
220
|
+
const fiberKey = keys.find((key) => key.startsWith("__reactFiber$"));
|
|
221
|
+
if (fiberKey) {
|
|
222
|
+
return (element as unknown as Record<string, ReactFiber>)[fiberKey] || null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// React 16-17 uses __reactInternalInstance$ prefix
|
|
226
|
+
const instanceKey = keys.find((key) => key.startsWith("__reactInternalInstance$"));
|
|
227
|
+
if (instanceKey) {
|
|
228
|
+
return (element as unknown as Record<string, ReactFiber>)[instanceKey] || null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// React 19 may use different patterns - check for any fiber-like object
|
|
232
|
+
const possibleFiberKey = keys.find((key) => {
|
|
233
|
+
if (!key.startsWith("__react")) return false;
|
|
234
|
+
const value = (element as unknown as Record<string, unknown>)[key];
|
|
235
|
+
return value && typeof value === "object" && "_debugSource" in (value as object);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (possibleFiberKey) {
|
|
239
|
+
return (element as unknown as Record<string, ReactFiber>)[possibleFiberKey] || null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Gets the display name of a React component from its fiber
|
|
247
|
+
*
|
|
248
|
+
* @param fiber - React fiber node
|
|
249
|
+
* @returns Component name or null
|
|
250
|
+
*/
|
|
251
|
+
function getComponentName(fiber: ReactFiber): string | null {
|
|
252
|
+
if (!fiber.type) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// String type means host component (div, span, etc.)
|
|
257
|
+
if (typeof fiber.type === "string") {
|
|
258
|
+
return null; // We want React component names, not HTML tags
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Function/class component
|
|
262
|
+
if (typeof fiber.type === "object" || typeof fiber.type === "function") {
|
|
263
|
+
const type = fiber.type as { displayName?: string; name?: string };
|
|
264
|
+
|
|
265
|
+
// Prefer displayName (set by React DevTools or manually)
|
|
266
|
+
if (type.displayName) {
|
|
267
|
+
return type.displayName;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Fall back to function/class name
|
|
271
|
+
if (type.name) {
|
|
272
|
+
return type.name;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Walks up the fiber tree to find the nearest component with _debugSource
|
|
281
|
+
*
|
|
282
|
+
* @param fiber - Starting fiber node
|
|
283
|
+
* @param maxDepth - Maximum tree depth to traverse (default: 50)
|
|
284
|
+
* @returns Object with source info and component name, or null
|
|
285
|
+
*/
|
|
286
|
+
function findDebugSource(
|
|
287
|
+
fiber: ReactFiber,
|
|
288
|
+
maxDepth = 50
|
|
289
|
+
): { source: ReactFiber["_debugSource"]; componentName: string | null } | null {
|
|
290
|
+
let current: ReactFiber | null | undefined = fiber;
|
|
291
|
+
let depth = 0;
|
|
292
|
+
|
|
293
|
+
while (current && depth < maxDepth) {
|
|
294
|
+
// Check current fiber for debug source
|
|
295
|
+
if (current._debugSource) {
|
|
296
|
+
return {
|
|
297
|
+
source: current._debugSource,
|
|
298
|
+
componentName: getComponentName(current),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check debug owner (for components that wrap the element)
|
|
303
|
+
if (current._debugOwner?._debugSource) {
|
|
304
|
+
return {
|
|
305
|
+
source: current._debugOwner._debugSource,
|
|
306
|
+
componentName: getComponentName(current._debugOwner),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Move up the tree
|
|
311
|
+
current = current.return;
|
|
312
|
+
depth++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Attempts to find source location using React 19's potentially different structure
|
|
320
|
+
*
|
|
321
|
+
* @param fiber - Starting fiber node
|
|
322
|
+
* @returns Source location info or null
|
|
323
|
+
*/
|
|
324
|
+
function findDebugSourceReact19(
|
|
325
|
+
fiber: ReactFiber
|
|
326
|
+
): { source: ReactFiber["_debugSource"]; componentName: string | null } | null {
|
|
327
|
+
// React 19 may store debug info differently
|
|
328
|
+
// This is a forward-compatible attempt based on React 19 RFCs
|
|
329
|
+
|
|
330
|
+
let current: ReactFiber | null | undefined = fiber;
|
|
331
|
+
let depth = 0;
|
|
332
|
+
const maxDepth = 50;
|
|
333
|
+
|
|
334
|
+
while (current && depth < maxDepth) {
|
|
335
|
+
// Check for new React 19 debug patterns
|
|
336
|
+
const anyFiber = current as unknown as Record<string, unknown>;
|
|
337
|
+
|
|
338
|
+
// Possible React 19 locations for debug info
|
|
339
|
+
const possibleSourceKeys = [
|
|
340
|
+
"_debugSource",
|
|
341
|
+
"__source",
|
|
342
|
+
"_source",
|
|
343
|
+
"debugSource",
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
for (const key of possibleSourceKeys) {
|
|
347
|
+
const source = anyFiber[key];
|
|
348
|
+
if (source && typeof source === "object" && "fileName" in source) {
|
|
349
|
+
return {
|
|
350
|
+
source: source as ReactFiber["_debugSource"],
|
|
351
|
+
componentName: getComponentName(current),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Check if debug info is in the element itself
|
|
357
|
+
if (current.memoizedProps) {
|
|
358
|
+
const props = current.memoizedProps as Record<string, unknown>;
|
|
359
|
+
if (props.__source && typeof props.__source === "object") {
|
|
360
|
+
const source = props.__source as { fileName?: string; lineNumber?: number };
|
|
361
|
+
if (source.fileName && source.lineNumber) {
|
|
362
|
+
return {
|
|
363
|
+
source: {
|
|
364
|
+
fileName: source.fileName,
|
|
365
|
+
lineNumber: source.lineNumber,
|
|
366
|
+
columnNumber: (source as { columnNumber?: number }).columnNumber,
|
|
367
|
+
},
|
|
368
|
+
componentName: getComponentName(current),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
current = current.return;
|
|
375
|
+
depth++;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// =============================================================================
|
|
382
|
+
// Stack-Trace Fallback for Source File Detection
|
|
383
|
+
// =============================================================================
|
|
384
|
+
//
|
|
385
|
+
// When _debugSource is unavailable (e.g. Next.js with SWC), we fall back to
|
|
386
|
+
// invoking the component function with a throwing hooks dispatcher, parsing
|
|
387
|
+
// the error stack trace, and stripping bundler URL prefixes. In dev mode,
|
|
388
|
+
// stack frames already contain original source paths.
|
|
389
|
+
// =============================================================================
|
|
390
|
+
|
|
391
|
+
/** Cache: component function → probed SourceLocation (or null if unresolvable) */
|
|
392
|
+
const sourceProbeCache = new Map<Function, SourceLocation | null>();
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Extract the callable function from a fiber, handling wrappers.
|
|
396
|
+
* Returns null for class components, host elements, or unrecognized types.
|
|
397
|
+
*/
|
|
398
|
+
function unwrapComponentType(fiber: ReactFiber): Function | null {
|
|
399
|
+
const tag = fiber.tag;
|
|
400
|
+
const type = fiber.type;
|
|
401
|
+
const elementType = fiber.elementType as Record<string, unknown> | null | undefined;
|
|
402
|
+
|
|
403
|
+
// Host elements (div, span, etc.)
|
|
404
|
+
if (typeof type === "string" || type == null) return null;
|
|
405
|
+
|
|
406
|
+
// Class components — skip (need `new`, different lifecycle)
|
|
407
|
+
if (
|
|
408
|
+
typeof type === "function" &&
|
|
409
|
+
(type as { prototype?: { isReactComponent?: boolean } }).prototype?.isReactComponent
|
|
410
|
+
) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// FunctionComponent / IndeterminateComponent
|
|
415
|
+
if (
|
|
416
|
+
(tag === FIBER_TAGS.FunctionComponent || tag === FIBER_TAGS.IndeterminateComponent) &&
|
|
417
|
+
typeof type === "function"
|
|
418
|
+
) {
|
|
419
|
+
return type as Function;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ForwardRef
|
|
423
|
+
if (tag === FIBER_TAGS.ForwardRef && elementType) {
|
|
424
|
+
const render = elementType.render;
|
|
425
|
+
if (typeof render === "function") return render as Function;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Memo / SimpleMemo
|
|
429
|
+
if (
|
|
430
|
+
(tag === FIBER_TAGS.MemoComponent || tag === FIBER_TAGS.SimpleMemoComponent) &&
|
|
431
|
+
elementType
|
|
432
|
+
) {
|
|
433
|
+
const inner = elementType.type;
|
|
434
|
+
if (typeof inner === "function") return inner as Function;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Generic fallback: if type is a plain function, use it
|
|
438
|
+
if (typeof type === "function") return type as Function;
|
|
439
|
+
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Access the React hooks dispatcher from React's module internals.
|
|
445
|
+
* These are properties on the `react` module export, NOT on `window`.
|
|
446
|
+
* Returns get/set helpers or null if not found.
|
|
447
|
+
*/
|
|
448
|
+
function getReactDispatcher(): {
|
|
449
|
+
get: () => unknown;
|
|
450
|
+
set: (d: unknown) => void;
|
|
451
|
+
} | null {
|
|
452
|
+
// Access React internals from the imported module
|
|
453
|
+
const reactModule = React as unknown as Record<string, unknown>;
|
|
454
|
+
|
|
455
|
+
// React 19: __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H
|
|
456
|
+
const r19 = reactModule.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE as
|
|
457
|
+
| Record<string, unknown>
|
|
458
|
+
| undefined;
|
|
459
|
+
if (r19 && "H" in r19) {
|
|
460
|
+
return {
|
|
461
|
+
get: () => r19.H,
|
|
462
|
+
set: (d: unknown) => { r19.H = d; },
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// React 16-18: __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current
|
|
467
|
+
const r18 = reactModule.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED as
|
|
468
|
+
| Record<string, unknown>
|
|
469
|
+
| undefined;
|
|
470
|
+
if (r18) {
|
|
471
|
+
const dispatcher = r18.ReactCurrentDispatcher as
|
|
472
|
+
| { current: unknown }
|
|
473
|
+
| undefined;
|
|
474
|
+
if (dispatcher && "current" in dispatcher) {
|
|
475
|
+
return {
|
|
476
|
+
get: () => dispatcher.current,
|
|
477
|
+
set: (d: unknown) => { dispatcher.current = d; },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Parse the first non-internal frame from an error stack string.
|
|
487
|
+
*/
|
|
488
|
+
function parseComponentFrame(
|
|
489
|
+
stack: string
|
|
490
|
+
): { fileName: string; line: number; column?: number } | null {
|
|
491
|
+
const lines = stack.split("\n");
|
|
492
|
+
|
|
493
|
+
// Patterns to skip: our own bundle, React internals, node_modules, chunk files
|
|
494
|
+
const skipPatterns = [
|
|
495
|
+
/source-location/,
|
|
496
|
+
/\/dist\/index\./, // Our bundled output (dist/index.mjs, dist/index.js)
|
|
497
|
+
/node_modules\//, // Any package in node_modules
|
|
498
|
+
/react-dom/,
|
|
499
|
+
/react\.development/,
|
|
500
|
+
/react\.production/,
|
|
501
|
+
/chunk-[A-Z0-9]+/i,
|
|
502
|
+
/react-stack-bottom-frame/,
|
|
503
|
+
/react-reconciler/,
|
|
504
|
+
/scheduler/,
|
|
505
|
+
/<anonymous>/, // Proxy handler frames
|
|
506
|
+
];
|
|
507
|
+
|
|
508
|
+
// V8 format: " at FnName (file:line:col)" or " at file:line:col"
|
|
509
|
+
const v8Re = /^\s*at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/;
|
|
510
|
+
// WebKit/Gecko: "FnName@file:line:col" or "@file:line:col"
|
|
511
|
+
const webkitRe = /^[^@]*@(.+?):(\d+):(\d+)$/;
|
|
512
|
+
|
|
513
|
+
for (const line of lines) {
|
|
514
|
+
const trimmed = line.trim();
|
|
515
|
+
if (!trimmed) continue;
|
|
516
|
+
|
|
517
|
+
// Skip frames from internal files
|
|
518
|
+
if (skipPatterns.some((p) => p.test(trimmed))) continue;
|
|
519
|
+
|
|
520
|
+
const match = v8Re.exec(trimmed) || webkitRe.exec(trimmed);
|
|
521
|
+
if (match) {
|
|
522
|
+
return {
|
|
523
|
+
fileName: match[1],
|
|
524
|
+
line: parseInt(match[2], 10),
|
|
525
|
+
column: parseInt(match[3], 10),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Strip bundler URL prefixes from a raw source path.
|
|
535
|
+
*/
|
|
536
|
+
function cleanSourcePath(rawPath: string): string {
|
|
537
|
+
let path = rawPath;
|
|
538
|
+
|
|
539
|
+
// 1. Strip query params and hashes
|
|
540
|
+
path = path.replace(/[?#].*$/, "");
|
|
541
|
+
|
|
542
|
+
// 2. Turbopack project prefix
|
|
543
|
+
path = path.replace(/^turbopack:\/\/\/\[project\]\//, "");
|
|
544
|
+
|
|
545
|
+
// 3. webpack-internal
|
|
546
|
+
path = path.replace(/^webpack-internal:\/\/\/\.\//, "");
|
|
547
|
+
path = path.replace(/^webpack-internal:\/\/\//, "");
|
|
548
|
+
|
|
549
|
+
// 4. webpack
|
|
550
|
+
path = path.replace(/^webpack:\/\/\/\.\//, "");
|
|
551
|
+
path = path.replace(/^webpack:\/\/\//, "");
|
|
552
|
+
|
|
553
|
+
// 5. turbopack generic
|
|
554
|
+
path = path.replace(/^turbopack:\/\/\//, "");
|
|
555
|
+
|
|
556
|
+
// 6. http(s)://host:port/
|
|
557
|
+
path = path.replace(/^https?:\/\/[^/]+\//, "");
|
|
558
|
+
|
|
559
|
+
// 7. file:///
|
|
560
|
+
path = path.replace(/^file:\/\/\//, "/");
|
|
561
|
+
|
|
562
|
+
// 8. Webpack chunk group prefixes like (app-pages-browser)/./
|
|
563
|
+
path = path.replace(/^\([^)]+\)\/\.\//, "");
|
|
564
|
+
|
|
565
|
+
// 9. Leading ./
|
|
566
|
+
path = path.replace(/^\.\//, "");
|
|
567
|
+
|
|
568
|
+
return path;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Probe a single fiber's component function by invoking it with a
|
|
573
|
+
* throwing hooks dispatcher and parsing the resulting error stack.
|
|
574
|
+
*/
|
|
575
|
+
function probeComponentSource(fiber: ReactFiber): SourceLocation | null {
|
|
576
|
+
const fn = unwrapComponentType(fiber);
|
|
577
|
+
if (!fn) return null;
|
|
578
|
+
|
|
579
|
+
// Check cache
|
|
580
|
+
if (sourceProbeCache.has(fn)) {
|
|
581
|
+
return sourceProbeCache.get(fn)!;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const dispatcher = getReactDispatcher();
|
|
585
|
+
if (!dispatcher) {
|
|
586
|
+
sourceProbeCache.set(fn, null);
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const original = dispatcher.get();
|
|
591
|
+
let result: SourceLocation | null = null;
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
// Install a proxy dispatcher that throws an Error (with stack) on any hook access.
|
|
595
|
+
// When the component calls useState/useEffect/etc., the proxy's get trap fires,
|
|
596
|
+
// creating an Error whose stack trace includes the component's source location.
|
|
597
|
+
const stackCapturingDispatcher = new Proxy(
|
|
598
|
+
{},
|
|
599
|
+
{
|
|
600
|
+
get() {
|
|
601
|
+
throw new Error("probe");
|
|
602
|
+
},
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
dispatcher.set(stackCapturingDispatcher);
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
// Invoke the component — it will either:
|
|
609
|
+
// 1. Call a hook → throws Error with stack (ideal case)
|
|
610
|
+
// 2. Have no hooks → runs to completion (harmless, discarded), no stack to parse
|
|
611
|
+
fn({});
|
|
612
|
+
} catch (e) {
|
|
613
|
+
if (e instanceof Error && e.message === "probe" && e.stack) {
|
|
614
|
+
const frame = parseComponentFrame(e.stack);
|
|
615
|
+
if (frame) {
|
|
616
|
+
const cleaned = cleanSourcePath(frame.fileName);
|
|
617
|
+
result = {
|
|
618
|
+
fileName: cleaned,
|
|
619
|
+
lineNumber: frame.line,
|
|
620
|
+
columnNumber: frame.column,
|
|
621
|
+
componentName: getComponentName(fiber) || undefined,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} finally {
|
|
627
|
+
dispatcher.set(original);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
sourceProbeCache.set(fn, result);
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Walk the fiber tree via .return, probing each fiber for source info.
|
|
636
|
+
* Stops at the first success.
|
|
637
|
+
*/
|
|
638
|
+
function probeSourceWalk(
|
|
639
|
+
fiber: ReactFiber,
|
|
640
|
+
maxDepth = 15
|
|
641
|
+
): SourceLocation | null {
|
|
642
|
+
let current: ReactFiber | null | undefined = fiber;
|
|
643
|
+
let depth = 0;
|
|
644
|
+
|
|
645
|
+
while (current && depth < maxDepth) {
|
|
646
|
+
const source = probeComponentSource(current);
|
|
647
|
+
if (source) return source;
|
|
648
|
+
|
|
649
|
+
current = current.return;
|
|
650
|
+
depth++;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Gets the source file location for a DOM element in a React application
|
|
658
|
+
*
|
|
659
|
+
* This function attempts to extract the source file path and line number
|
|
660
|
+
* where a React component is defined. This only works in development mode
|
|
661
|
+
* as production builds strip debug information.
|
|
662
|
+
*
|
|
663
|
+
* @param element - DOM element to get source location for
|
|
664
|
+
* @returns SourceLocationResult with location info or reason for failure
|
|
665
|
+
*
|
|
666
|
+
* @example
|
|
667
|
+
* ```ts
|
|
668
|
+
* const result = getSourceLocation(element);
|
|
669
|
+
* if (result.found && result.source) {
|
|
670
|
+
* console.log(`${result.source.fileName}:${result.source.lineNumber}`);
|
|
671
|
+
* // Output: "/src/components/Button.tsx:42"
|
|
672
|
+
* }
|
|
673
|
+
* ```
|
|
674
|
+
*/
|
|
675
|
+
export function getSourceLocation(element: HTMLElement): SourceLocationResult {
|
|
676
|
+
// Try to get fiber directly from the element (same approach as getReactComponentName)
|
|
677
|
+
// This avoids detectReactApp() whose production heuristic can give false positives
|
|
678
|
+
const fiber = getFiberFromElement(element);
|
|
679
|
+
|
|
680
|
+
if (!fiber) {
|
|
681
|
+
return {
|
|
682
|
+
found: false,
|
|
683
|
+
reason: "no-fiber",
|
|
684
|
+
isReactApp: false,
|
|
685
|
+
isProduction: false,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Try standard React 16-18 debug source finding
|
|
690
|
+
let debugInfo = findDebugSource(fiber);
|
|
691
|
+
|
|
692
|
+
// If not found, try React 19 patterns
|
|
693
|
+
if (!debugInfo) {
|
|
694
|
+
debugInfo = findDebugSourceReact19(fiber);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (debugInfo?.source) {
|
|
698
|
+
return {
|
|
699
|
+
found: true,
|
|
700
|
+
source: {
|
|
701
|
+
fileName: debugInfo.source.fileName,
|
|
702
|
+
lineNumber: debugInfo.source.lineNumber,
|
|
703
|
+
columnNumber: debugInfo.source.columnNumber,
|
|
704
|
+
componentName: debugInfo.componentName || undefined,
|
|
705
|
+
},
|
|
706
|
+
isReactApp: true,
|
|
707
|
+
isProduction: false,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Fallback: probe component via stack trace
|
|
712
|
+
const probed = probeSourceWalk(fiber);
|
|
713
|
+
if (probed) {
|
|
714
|
+
return { found: true, source: probed, isReactApp: true, isProduction: false };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
found: false,
|
|
719
|
+
reason: "no-debug-source",
|
|
720
|
+
isReactApp: true,
|
|
721
|
+
isProduction: false,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Formats a source location as a clickable file path string
|
|
727
|
+
*
|
|
728
|
+
* @param source - Source location object
|
|
729
|
+
* @param format - Output format: "vscode" for VSCode URL, "path" for file:line format
|
|
730
|
+
* @returns Formatted string
|
|
731
|
+
*
|
|
732
|
+
* @example
|
|
733
|
+
* ```ts
|
|
734
|
+
* formatSourceLocation(source, "path")
|
|
735
|
+
* // Returns: "src/components/Button.tsx:42:8"
|
|
736
|
+
*
|
|
737
|
+
* formatSourceLocation(source, "vscode")
|
|
738
|
+
* // Returns: "vscode://file/absolute/path/src/components/Button.tsx:42:8"
|
|
739
|
+
* ```
|
|
740
|
+
*/
|
|
741
|
+
export function formatSourceLocation(
|
|
742
|
+
source: SourceLocation,
|
|
743
|
+
format: "path" | "vscode" = "path"
|
|
744
|
+
): string {
|
|
745
|
+
const { fileName, lineNumber, columnNumber } = source;
|
|
746
|
+
|
|
747
|
+
// Build line:column suffix
|
|
748
|
+
let location = `${fileName}:${lineNumber}`;
|
|
749
|
+
if (columnNumber !== undefined) {
|
|
750
|
+
location += `:${columnNumber}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (format === "vscode") {
|
|
754
|
+
// VSCode can open files via URL protocol
|
|
755
|
+
// Assumes fileName is absolute or can be resolved
|
|
756
|
+
return `vscode://file${fileName.startsWith("/") ? "" : "/"}${location}`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return location;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Gets source locations for multiple elements at once
|
|
764
|
+
*
|
|
765
|
+
* @param elements - Array of DOM elements
|
|
766
|
+
* @returns Array of source location results
|
|
767
|
+
*/
|
|
768
|
+
export function getSourceLocations(elements: HTMLElement[]): SourceLocationResult[] {
|
|
769
|
+
return elements.map((element) => getSourceLocation(element));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Finds the nearest React component ancestor that has source info
|
|
774
|
+
*
|
|
775
|
+
* Useful when clicking on a deeply nested element (like text or an icon)
|
|
776
|
+
* and wanting to find the component that contains it.
|
|
777
|
+
*
|
|
778
|
+
* @param element - Starting DOM element
|
|
779
|
+
* @param maxAncestors - Maximum DOM ancestors to check (default: 10)
|
|
780
|
+
* @returns Source location result
|
|
781
|
+
*/
|
|
782
|
+
export function findNearestComponentSource(
|
|
783
|
+
element: HTMLElement,
|
|
784
|
+
maxAncestors = 10
|
|
785
|
+
): SourceLocationResult {
|
|
786
|
+
let current: HTMLElement | null = element;
|
|
787
|
+
let depth = 0;
|
|
788
|
+
|
|
789
|
+
while (current && depth < maxAncestors) {
|
|
790
|
+
const result = getSourceLocation(current);
|
|
791
|
+
|
|
792
|
+
// Return first successful result
|
|
793
|
+
if (result.found) {
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// If we found fiber but no source, keep looking up DOM
|
|
798
|
+
// (might find a parent component with source info)
|
|
799
|
+
current = current.parentElement;
|
|
800
|
+
depth++;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Return result for original element (will explain why not found)
|
|
804
|
+
return getSourceLocation(element);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Gets all component sources in the ancestor chain
|
|
809
|
+
*
|
|
810
|
+
* Useful for understanding the component hierarchy.
|
|
811
|
+
*
|
|
812
|
+
* @param element - Starting DOM element
|
|
813
|
+
* @returns Array of unique source locations from element up to root
|
|
814
|
+
*/
|
|
815
|
+
export function getComponentHierarchy(element: HTMLElement): SourceLocation[] {
|
|
816
|
+
const fiber = getFiberFromElement(element);
|
|
817
|
+
if (!fiber) {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const sources: SourceLocation[] = [];
|
|
822
|
+
const seenFiles = new Set<string>();
|
|
823
|
+
|
|
824
|
+
let current: ReactFiber | null | undefined = fiber;
|
|
825
|
+
let depth = 0;
|
|
826
|
+
const maxDepth = 100;
|
|
827
|
+
|
|
828
|
+
while (current && depth < maxDepth) {
|
|
829
|
+
if (current._debugSource) {
|
|
830
|
+
const key = `${current._debugSource.fileName}:${current._debugSource.lineNumber}`;
|
|
831
|
+
|
|
832
|
+
// Avoid duplicates
|
|
833
|
+
if (!seenFiles.has(key)) {
|
|
834
|
+
seenFiles.add(key);
|
|
835
|
+
sources.push({
|
|
836
|
+
fileName: current._debugSource.fileName,
|
|
837
|
+
lineNumber: current._debugSource.lineNumber,
|
|
838
|
+
columnNumber: current._debugSource.columnNumber,
|
|
839
|
+
componentName: getComponentName(current) || undefined,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
current = current.return;
|
|
845
|
+
depth++;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return sources;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Checks if source location detection is likely to work in the current environment
|
|
853
|
+
*
|
|
854
|
+
* @returns Object describing support status
|
|
855
|
+
*/
|
|
856
|
+
export function checkSourceLocationSupport(): {
|
|
857
|
+
supported: boolean;
|
|
858
|
+
reason: string;
|
|
859
|
+
suggestions: string[];
|
|
860
|
+
} {
|
|
861
|
+
const reactInfo = detectReactApp();
|
|
862
|
+
|
|
863
|
+
if (!reactInfo.isReact) {
|
|
864
|
+
return {
|
|
865
|
+
supported: false,
|
|
866
|
+
reason: "No React application detected on this page",
|
|
867
|
+
suggestions: [
|
|
868
|
+
"Ensure you're on a page built with React",
|
|
869
|
+
"The page may use a different framework (Vue, Angular, etc.)",
|
|
870
|
+
],
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (reactInfo.isProduction) {
|
|
875
|
+
return {
|
|
876
|
+
supported: false,
|
|
877
|
+
reason: "Production build detected - source info is stripped",
|
|
878
|
+
suggestions: [
|
|
879
|
+
"Run the application in development mode",
|
|
880
|
+
"Set NODE_ENV=development",
|
|
881
|
+
"Ensure your bundler includes source info in development",
|
|
882
|
+
],
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Check for DevTools
|
|
887
|
+
const hasDevTools = typeof window !== "undefined" &&
|
|
888
|
+
!!(window as unknown as Record<string, unknown>).__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
889
|
+
|
|
890
|
+
if (!hasDevTools) {
|
|
891
|
+
return {
|
|
892
|
+
supported: true,
|
|
893
|
+
reason: "Development mode detected, but React DevTools not installed",
|
|
894
|
+
suggestions: [
|
|
895
|
+
"Install React DevTools browser extension for best results",
|
|
896
|
+
"Source detection may still work without it",
|
|
897
|
+
],
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
supported: true,
|
|
903
|
+
reason: `React ${reactInfo.version || "unknown"} development mode detected`,
|
|
904
|
+
suggestions: [],
|
|
905
|
+
};
|
|
906
|
+
}
|