@eyeglass/inspector 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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * React Fiber Walker - extracts component info from React DevTools internals
3
+ */
4
+ export interface FrameworkInfo {
5
+ name: 'react' | 'vue' | 'svelte' | 'vanilla';
6
+ componentName?: string;
7
+ filePath?: string;
8
+ lineNumber?: number;
9
+ props?: Record<string, unknown>;
10
+ ancestry?: string[];
11
+ }
12
+ /**
13
+ * Extract framework information from a DOM element
14
+ */
15
+ export declare function extractFrameworkInfo(element: Element): FrameworkInfo;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * React Fiber Walker - extracts component info from React DevTools internals
3
+ */
4
+ // Fiber tag constants
5
+ const FunctionComponent = 0;
6
+ const ClassComponent = 1;
7
+ const ForwardRef = 11;
8
+ const MemoComponent = 14;
9
+ const SimpleMemoComponent = 15;
10
+ const COMPONENT_TAGS = new Set([
11
+ FunctionComponent,
12
+ ClassComponent,
13
+ ForwardRef,
14
+ MemoComponent,
15
+ SimpleMemoComponent,
16
+ ]);
17
+ /**
18
+ * Find the React Fiber attached to a DOM element
19
+ */
20
+ function getFiberFromElement(element) {
21
+ const keys = Object.keys(element);
22
+ const fiberKey = keys.find((k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
23
+ if (!fiberKey)
24
+ return null;
25
+ return element[fiberKey];
26
+ }
27
+ /**
28
+ * Check if a fiber is a user-defined component (not built-in React internals)
29
+ */
30
+ function isUserComponent(fiber) {
31
+ if (!COMPONENT_TAGS.has(fiber.tag) || typeof fiber.type !== 'function') {
32
+ return false;
33
+ }
34
+ const name = fiber.type.displayName || fiber.type.name || '';
35
+ // Skip built-in React components (Context.Provider, StrictMode, etc.)
36
+ if (!name || name.startsWith('Context') || name.endsWith('Provider') || name === 'StrictMode') {
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+ /**
42
+ * Get component name from fiber
43
+ */
44
+ function getComponentName(fiber) {
45
+ return fiber.type.displayName || fiber.type.name || undefined;
46
+ }
47
+ /**
48
+ * Walk up the fiber tree to find the nearest user-defined component
49
+ */
50
+ function findComponentFiber(fiber) {
51
+ let current = fiber;
52
+ while (current) {
53
+ if (isUserComponent(current)) {
54
+ return current;
55
+ }
56
+ current = current.return;
57
+ }
58
+ return null;
59
+ }
60
+ /**
61
+ * Collect all parent component names by walking up the fiber tree
62
+ */
63
+ function collectAncestry(fiber) {
64
+ const ancestry = [];
65
+ let current = fiber;
66
+ while (current) {
67
+ if (isUserComponent(current)) {
68
+ const name = getComponentName(current);
69
+ if (name) {
70
+ ancestry.push(name);
71
+ }
72
+ }
73
+ current = current.return;
74
+ }
75
+ return ancestry;
76
+ }
77
+ /**
78
+ * Get safe props (primitives only, no functions/objects)
79
+ */
80
+ function getSafeProps(props) {
81
+ if (!props)
82
+ return undefined;
83
+ const safe = {};
84
+ for (const [key, value] of Object.entries(props)) {
85
+ if (key === 'children')
86
+ continue;
87
+ const type = typeof value;
88
+ if (type === 'string' || type === 'number' || type === 'boolean' || value === null) {
89
+ safe[key] = value;
90
+ }
91
+ }
92
+ return Object.keys(safe).length > 0 ? safe : undefined;
93
+ }
94
+ /**
95
+ * Detect Vue component
96
+ */
97
+ function detectVue(element) {
98
+ // Vue 2
99
+ const vueInstance = element.__vue__;
100
+ if (vueInstance) {
101
+ const componentName = vueInstance.$options?.name || vueInstance.$options?._componentTag;
102
+ return {
103
+ name: 'vue',
104
+ componentName,
105
+ };
106
+ }
107
+ // Vue 3 - check for any __vue* property
108
+ const keys = Object.keys(element);
109
+ const vueKey = keys.find((k) => k.startsWith('__vue'));
110
+ if (vueKey) {
111
+ const vnode = element[vueKey];
112
+ // Try to get component info from vnode
113
+ if (vnode) {
114
+ // Walk up to find component instance
115
+ let instance = vnode;
116
+ let componentName;
117
+ let props;
118
+ // Check various Vue 3 internal structures
119
+ if (instance?.type?.name) {
120
+ componentName = instance.type.name;
121
+ }
122
+ else if (instance?.type?.__name) {
123
+ componentName = instance.type.__name;
124
+ }
125
+ else if (instance?.component?.type?.name) {
126
+ componentName = instance.component.type.name;
127
+ }
128
+ else if (instance?.component?.type?.__name) {
129
+ componentName = instance.component.type.__name;
130
+ }
131
+ // Try to get props
132
+ const rawProps = instance?.props || instance?.component?.props;
133
+ if (rawProps) {
134
+ props = getSafeProps(rawProps);
135
+ }
136
+ // Try to get file info from __file
137
+ let filePath;
138
+ if (instance?.type?.__file) {
139
+ filePath = instance.type.__file;
140
+ }
141
+ else if (instance?.component?.type?.__file) {
142
+ filePath = instance.component.type.__file;
143
+ }
144
+ return {
145
+ name: 'vue',
146
+ componentName,
147
+ filePath,
148
+ props,
149
+ };
150
+ }
151
+ return { name: 'vue' };
152
+ }
153
+ return null;
154
+ }
155
+ /**
156
+ * Detect Svelte component
157
+ */
158
+ function detectSvelte(element) {
159
+ const keys = Object.keys(element);
160
+ const svelteKey = keys.find((k) => k.startsWith('__svelte'));
161
+ if (svelteKey) {
162
+ const svelteData = element[svelteKey];
163
+ // Try to extract component info from Svelte internals
164
+ let componentName;
165
+ // Svelte 5 uses different structure
166
+ if (svelteData?.constructor?.name && svelteData.constructor.name !== 'Object') {
167
+ componentName = svelteData.constructor.name;
168
+ }
169
+ // Check for component context
170
+ if (!componentName && svelteData?.$$?.ctx) {
171
+ // Svelte 4 structure
172
+ const ctx = svelteData.$$.ctx;
173
+ if (ctx?.constructor?.name && ctx.constructor.name !== 'Object') {
174
+ componentName = ctx.constructor.name;
175
+ }
176
+ }
177
+ return {
178
+ name: 'svelte',
179
+ componentName,
180
+ };
181
+ }
182
+ return null;
183
+ }
184
+ /**
185
+ * Extract framework information from a DOM element
186
+ */
187
+ export function extractFrameworkInfo(element) {
188
+ // Try React first
189
+ const fiber = getFiberFromElement(element);
190
+ if (fiber) {
191
+ const componentFiber = findComponentFiber(fiber);
192
+ if (componentFiber) {
193
+ const componentName = getComponentName(componentFiber);
194
+ const debugSource = componentFiber._debugSource;
195
+ const ancestry = collectAncestry(componentFiber);
196
+ return {
197
+ name: 'react',
198
+ componentName,
199
+ filePath: debugSource?.fileName,
200
+ lineNumber: debugSource?.lineNumber,
201
+ props: getSafeProps(componentFiber.memoizedProps),
202
+ ancestry: ancestry.length > 0 ? ancestry : undefined,
203
+ };
204
+ }
205
+ // React element but no user component found (just DOM nodes)
206
+ // Still try to get ancestry from the fiber
207
+ const ancestry = collectAncestry(fiber);
208
+ return {
209
+ name: 'react',
210
+ ancestry: ancestry.length > 0 ? ancestry : undefined,
211
+ };
212
+ }
213
+ // Try Vue
214
+ const vueInfo = detectVue(element);
215
+ if (vueInfo)
216
+ return vueInfo;
217
+ // Try Svelte
218
+ const svelteInfo = detectSvelte(element);
219
+ if (svelteInfo)
220
+ return svelteInfo;
221
+ // Vanilla fallback
222
+ return { name: 'vanilla' };
223
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,252 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { extractFrameworkInfo } from './fiber-walker.js';
3
+ // Helper to create a mock function with a displayName
4
+ function createMockComponent(name) {
5
+ const fn = function () { };
6
+ fn.displayName = name;
7
+ return fn;
8
+ }
9
+ describe('extractFrameworkInfo', () => {
10
+ beforeEach(() => {
11
+ document.body.innerHTML = '';
12
+ });
13
+ describe('vanilla detection', () => {
14
+ it('should return vanilla for plain DOM elements', () => {
15
+ const div = document.createElement('div');
16
+ document.body.appendChild(div);
17
+ const info = extractFrameworkInfo(div);
18
+ expect(info.name).toBe('vanilla');
19
+ expect(info.componentName).toBeUndefined();
20
+ expect(info.filePath).toBeUndefined();
21
+ });
22
+ });
23
+ describe('React detection', () => {
24
+ it('should detect React from __reactFiber$ key', () => {
25
+ const div = document.createElement('div');
26
+ document.body.appendChild(div);
27
+ // Mock React fiber structure
28
+ const mockFiber = {
29
+ tag: 0, // FunctionComponent
30
+ type: createMockComponent('MyComponent'),
31
+ return: null,
32
+ _debugSource: {
33
+ fileName: 'src/components/MyComponent.tsx',
34
+ lineNumber: 42,
35
+ },
36
+ memoizedProps: {
37
+ className: 'test',
38
+ onClick: () => { },
39
+ isActive: true,
40
+ },
41
+ };
42
+ div['__reactFiber$abc123'] = mockFiber;
43
+ const info = extractFrameworkInfo(div);
44
+ expect(info.name).toBe('react');
45
+ expect(info.componentName).toBe('MyComponent');
46
+ expect(info.filePath).toBe('src/components/MyComponent.tsx');
47
+ expect(info.lineNumber).toBe(42);
48
+ });
49
+ it('should detect React from __reactInternalInstance$ key', () => {
50
+ const div = document.createElement('div');
51
+ document.body.appendChild(div);
52
+ const mockFiber = {
53
+ tag: 0,
54
+ type: createMockComponent('Button'),
55
+ return: null,
56
+ };
57
+ div['__reactInternalInstance$xyz789'] = mockFiber;
58
+ const info = extractFrameworkInfo(div);
59
+ expect(info.name).toBe('react');
60
+ expect(info.componentName).toBe('Button');
61
+ });
62
+ it('should extract safe props (primitives only)', () => {
63
+ const div = document.createElement('div');
64
+ document.body.appendChild(div);
65
+ const mockFiber = {
66
+ tag: 0,
67
+ type: createMockComponent('Card'),
68
+ return: null,
69
+ memoizedProps: {
70
+ title: 'Hello',
71
+ count: 42,
72
+ isOpen: true,
73
+ data: null,
74
+ children: ['some', 'children'],
75
+ onClick: () => { },
76
+ config: { nested: 'object' },
77
+ },
78
+ };
79
+ div['__reactFiber$test'] = mockFiber;
80
+ const info = extractFrameworkInfo(div);
81
+ expect(info.props).toEqual({
82
+ title: 'Hello',
83
+ count: 42,
84
+ isOpen: true,
85
+ data: null,
86
+ });
87
+ // Should not include children, functions, or objects
88
+ expect(info.props?.children).toBeUndefined();
89
+ expect(info.props?.onClick).toBeUndefined();
90
+ expect(info.props?.config).toBeUndefined();
91
+ });
92
+ it('should walk up fiber tree to find component', () => {
93
+ const div = document.createElement('div');
94
+ document.body.appendChild(div);
95
+ // Simulate a DOM fiber with component parents
96
+ const parentFiber = {
97
+ tag: 0,
98
+ type: createMockComponent('ParentComponent'),
99
+ return: null,
100
+ };
101
+ const domFiber = {
102
+ tag: 5, // HostComponent (div)
103
+ type: 'div',
104
+ return: parentFiber,
105
+ };
106
+ div['__reactFiber$test'] = domFiber;
107
+ const info = extractFrameworkInfo(div);
108
+ expect(info.name).toBe('react');
109
+ expect(info.componentName).toBe('ParentComponent');
110
+ });
111
+ it('should collect ancestry chain', () => {
112
+ const div = document.createElement('div');
113
+ document.body.appendChild(div);
114
+ const appFiber = {
115
+ tag: 0,
116
+ type: createMockComponent('App'),
117
+ return: null,
118
+ };
119
+ const cardFiber = {
120
+ tag: 0,
121
+ type: createMockComponent('Card'),
122
+ return: appFiber,
123
+ };
124
+ const buttonFiber = {
125
+ tag: 0,
126
+ type: createMockComponent('Button'),
127
+ return: cardFiber,
128
+ };
129
+ div['__reactFiber$test'] = buttonFiber;
130
+ const info = extractFrameworkInfo(div);
131
+ expect(info.ancestry).toEqual(['Button', 'Card', 'App']);
132
+ });
133
+ it('should skip Context.Provider and similar internal components', () => {
134
+ const div = document.createElement('div');
135
+ document.body.appendChild(div);
136
+ const appFiber = {
137
+ tag: 0,
138
+ type: createMockComponent('App'),
139
+ return: null,
140
+ };
141
+ const providerFiber = {
142
+ tag: 0,
143
+ type: createMockComponent('ThemeProvider'),
144
+ return: appFiber,
145
+ };
146
+ const contextFiber = {
147
+ tag: 0,
148
+ type: createMockComponent('Context.Consumer'),
149
+ return: providerFiber,
150
+ };
151
+ const buttonFiber = {
152
+ tag: 0,
153
+ type: createMockComponent('Button'),
154
+ return: contextFiber,
155
+ };
156
+ div['__reactFiber$test'] = buttonFiber;
157
+ const info = extractFrameworkInfo(div);
158
+ // The nearest user component should be Button
159
+ expect(info.componentName).toBe('Button');
160
+ });
161
+ });
162
+ describe('Vue detection', () => {
163
+ it('should detect Vue 2 from __vue__', () => {
164
+ const div = document.createElement('div');
165
+ document.body.appendChild(div);
166
+ div.__vue__ = {
167
+ $options: {
168
+ name: 'MyVueComponent',
169
+ },
170
+ };
171
+ const info = extractFrameworkInfo(div);
172
+ expect(info.name).toBe('vue');
173
+ expect(info.componentName).toBe('MyVueComponent');
174
+ });
175
+ it('should detect Vue 3 from __vueParentComponent', () => {
176
+ const div = document.createElement('div');
177
+ document.body.appendChild(div);
178
+ div.__vueParentComponent = {
179
+ type: {
180
+ name: 'VueThreeComponent',
181
+ __file: 'src/components/VueThreeComponent.vue',
182
+ },
183
+ props: {
184
+ message: 'Hello',
185
+ count: 5,
186
+ },
187
+ };
188
+ const info = extractFrameworkInfo(div);
189
+ expect(info.name).toBe('vue');
190
+ expect(info.componentName).toBe('VueThreeComponent');
191
+ expect(info.filePath).toBe('src/components/VueThreeComponent.vue');
192
+ expect(info.props).toEqual({
193
+ message: 'Hello',
194
+ count: 5,
195
+ });
196
+ });
197
+ it('should detect Vue 3 with __name', () => {
198
+ const div = document.createElement('div');
199
+ document.body.appendChild(div);
200
+ div.__vueParentComponent = {
201
+ type: {
202
+ __name: 'SetupScriptComponent',
203
+ },
204
+ };
205
+ const info = extractFrameworkInfo(div);
206
+ expect(info.name).toBe('vue');
207
+ expect(info.componentName).toBe('SetupScriptComponent');
208
+ });
209
+ });
210
+ describe('Svelte detection', () => {
211
+ it('should detect Svelte from __svelte key', () => {
212
+ const div = document.createElement('div');
213
+ document.body.appendChild(div);
214
+ div.__svelte_component = {
215
+ constructor: {
216
+ name: 'SvelteButton',
217
+ },
218
+ };
219
+ const info = extractFrameworkInfo(div);
220
+ expect(info.name).toBe('svelte');
221
+ });
222
+ it('should extract Svelte component name', () => {
223
+ const div = document.createElement('div');
224
+ document.body.appendChild(div);
225
+ class MySvelteComponent {
226
+ }
227
+ div.__svelte_component = new MySvelteComponent();
228
+ const info = extractFrameworkInfo(div);
229
+ expect(info.name).toBe('svelte');
230
+ expect(info.componentName).toBe('MySvelteComponent');
231
+ });
232
+ });
233
+ describe('framework priority', () => {
234
+ it('should prefer React over Vue if both are present', () => {
235
+ const div = document.createElement('div');
236
+ document.body.appendChild(div);
237
+ // Add both React and Vue markers
238
+ const mockFiber = {
239
+ tag: 0,
240
+ type: createMockComponent('ReactComp'),
241
+ return: null,
242
+ };
243
+ div['__reactFiber$test'] = mockFiber;
244
+ div.__vue__ = {
245
+ $options: { name: 'VueComp' },
246
+ };
247
+ const info = extractFrameworkInfo(div);
248
+ expect(info.name).toBe('react');
249
+ expect(info.componentName).toBe('ReactComp');
250
+ });
251
+ });
252
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @eyeglass/inspector - Browser-side inspection web component
3
+ *
4
+ * Usage:
5
+ * import '@eyeglass/inspector';
6
+ * // Or inject via script tag
7
+ *
8
+ * This automatically registers the <eyeglass-inspector> custom element.
9
+ */
10
+ export { EyeglassInspector } from './inspector.js';
11
+ export { captureSnapshot } from './snapshot.js';
12
+ export { extractFrameworkInfo } from './fiber-walker.js';
13
+ export type { FrameworkInfo } from './fiber-walker.js';
14
+ /**
15
+ * Initialize the inspector by appending it to the document
16
+ */
17
+ export declare function initInspector(): void;
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @eyeglass/inspector - Browser-side inspection web component
3
+ *
4
+ * Usage:
5
+ * import '@eyeglass/inspector';
6
+ * // Or inject via script tag
7
+ *
8
+ * This automatically registers the <eyeglass-inspector> custom element.
9
+ */
10
+ export { EyeglassInspector } from './inspector.js';
11
+ export { captureSnapshot } from './snapshot.js';
12
+ export { extractFrameworkInfo } from './fiber-walker.js';
13
+ /**
14
+ * Initialize the inspector by appending it to the document
15
+ */
16
+ export function initInspector() {
17
+ if (document.querySelector('eyeglass-inspector')) {
18
+ console.warn('[eyeglass] Inspector already initialized');
19
+ return;
20
+ }
21
+ const inspector = document.createElement('eyeglass-inspector');
22
+ document.body.appendChild(inspector);
23
+ console.log('[eyeglass] Inspector initialized. Hover over elements and click to annotate.');
24
+ }
25
+ // Auto-initialize when imported in a browser context
26
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
27
+ if (document.readyState === 'loading') {
28
+ document.addEventListener('DOMContentLoaded', initInspector);
29
+ }
30
+ else {
31
+ initInspector();
32
+ }
33
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Eyeglass Inspector - Glass UI for visual element inspection
3
+ */
4
+ export declare class EyeglassInspector extends HTMLElement {
5
+ private shadow;
6
+ private highlight;
7
+ private panel;
8
+ private toast;
9
+ private hub;
10
+ private currentElement;
11
+ private currentSnapshot;
12
+ private interactionId;
13
+ private frozen;
14
+ private eventSource;
15
+ private throttleTimeout;
16
+ private mode;
17
+ private activityEvents;
18
+ private currentStatus;
19
+ private hubExpanded;
20
+ private inspectorEnabled;
21
+ private history;
22
+ private isDragging;
23
+ private dragOffset;
24
+ private customPanelPosition;
25
+ private multiSelectMode;
26
+ private selectedElements;
27
+ private selectedSnapshots;
28
+ private multiSelectHighlights;
29
+ private submittedSnapshots;
30
+ private static readonly MAX_SELECTION;
31
+ private cursorStyleElement;
32
+ private scrollTimeout;
33
+ constructor();
34
+ connectedCallback(): void;
35
+ private saveSession;
36
+ private restoreSession;
37
+ private showResultToast;
38
+ private hideToast;
39
+ private loadHistory;
40
+ private saveHistory;
41
+ private addToHistory;
42
+ private updateHistoryStatus;
43
+ private renderHub;
44
+ private requestUndo;
45
+ disconnectedCallback(): void;
46
+ private connectSSE;
47
+ private handleActivityEvent;
48
+ private handleMouseMove;
49
+ private handleClick;
50
+ private handleKeyDown;
51
+ private handleScroll;
52
+ private disableHighlightTransitions;
53
+ private enableHighlightTransitions;
54
+ private updateMultiSelectHighlightPositions;
55
+ private handlePanelDragStart;
56
+ private handlePanelDrag;
57
+ private handlePanelDragEnd;
58
+ private showHighlight;
59
+ private hideHighlight;
60
+ private freeze;
61
+ private enterMultiSelectMode;
62
+ private toggleInSelection;
63
+ private removeFromSelection;
64
+ private exitMultiSelectMode;
65
+ private renderMultiSelectHighlights;
66
+ private clearMultiSelectHighlights;
67
+ private unfreeze;
68
+ private renderPanel;
69
+ private renderInputMode;
70
+ private renderActivityMode;
71
+ private renderActivityFeed;
72
+ private renderStatusItem;
73
+ private renderThoughtItem;
74
+ private renderActionItem;
75
+ private renderQuestionItem;
76
+ private getUserNote;
77
+ private getStatusText;
78
+ private hidePanel;
79
+ private submit;
80
+ private submitAnswer;
81
+ private escapeHtml;
82
+ private updateCursor;
83
+ }