Skip to content

Commit 24926e0

Browse files
committed
fix: improve proxy and type handling in inspect element
- Remove unreliable proxy detection and instanceof checks - Add safer URLSearchParams handling to prevent Next.js errors - Use capability checks instead of type detection - Improve error handling for property access This addresses issues with proxy detection and type checking in the element inspector, making it more reliable across different environments while preventing Next.js-specific errors when handling search params.
1 parent c9efa9f commit 24926e0

File tree

1 file changed

+175
-79
lines changed

1 file changed

+175
-79
lines changed

packages/scan/src/core/web/inspect-element/view-state.ts

Lines changed: 175 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,45 @@ export const cumulativeChanges = {
1717
context: new Map<string, number>(),
1818
};
1919

20+
const getProxyValue = (proxy: any) => {
21+
try {
22+
if (!proxy || typeof proxy !== 'object') {
23+
return proxy;
24+
}
25+
26+
// Handle URLSearchParams-like objects
27+
if (typeof proxy.entries === 'function' && typeof proxy.get === 'function') {
28+
try {
29+
const entries = Array.from(proxy.entries() as Iterable<[string, any]>);
30+
return Object.fromEntries(entries);
31+
} catch (err) {
32+
return proxy;
33+
}
34+
}
35+
36+
// Handle regular objects more efficiently
37+
try {
38+
const descriptors = Object.getOwnPropertyDescriptors(proxy);
39+
const result: Record<string, any> = {};
40+
41+
// Using for...in is faster than reduce for this case
42+
for (const key in descriptors) {
43+
const descriptor = descriptors[key];
44+
if (typeof key === 'string' && 'value' in descriptor) {
45+
result[key] = descriptor.value;
46+
}
47+
}
48+
49+
return result;
50+
} catch (err) {
51+
return proxy;
52+
}
53+
54+
} catch (err) {
55+
return proxy;
56+
}
57+
};
58+
2059
export const renderPropsAndState = (didRender: boolean, fiber: any) => {
2160
const propContainer = Store.inspectState.value.propContainer;
2261

@@ -380,6 +419,7 @@ export const createPropertyElement = (
380419
if (isExpandable) {
381420
const isExpanded = EXPANDED_PATHS.has(currentPath);
382421

422+
// Check for circular references first
383423
if (typeof value === 'object' && value !== null) {
384424
let paths = objectPathMap.get(value);
385425
if (!paths) {
@@ -392,6 +432,28 @@ export const createPropertyElement = (
392432
paths.add(currentPath);
393433
}
394434

435+
const unwrapped = getProxyValue(value);
436+
const isNonExpandable = (unwrapped === value &&
437+
value &&
438+
Object.getPrototypeOf(value)?.constructor?.name === 'Proxy') ||
439+
value instanceof Promise;
440+
441+
// For non-expandable items, render like a simple property
442+
if (isNonExpandable) {
443+
const preview = document.createElement('div');
444+
preview.className = 'react-scan-preview-line';
445+
preview.dataset.key = key;
446+
preview.dataset.section = section;
447+
preview.innerHTML = `
448+
<span style="width: 8px; display: inline-block"></span>
449+
<span class="react-scan-key">${key}:&nbsp;</span>
450+
<span class="${getValueClassName(value)}">${getValuePreview(value)}</span>
451+
`;
452+
container.appendChild(preview);
453+
return container;
454+
}
455+
456+
// Normal expandable logic for other objects
395457
container.classList.add('react-scan-expandable');
396458
if (isExpanded) {
397459
container.classList.add('react-scan-expanded');
@@ -469,67 +531,77 @@ export const createPropertyElement = (
469531
}
470532
}
471533

472-
arrow.addEventListener('click', (e) => {
473-
e.stopPropagation();
474-
475-
const isExpanding = !container.classList.contains(
476-
'react-scan-expanded',
477-
);
478-
479-
if (isExpanding) {
480-
EXPANDED_PATHS.add(currentPath);
481-
container.classList.add('react-scan-expanded');
482-
content.classList.remove('react-scan-hidden');
483-
484-
if (!content.hasChildNodes()) {
485-
if (Array.isArray(value)) {
486-
value.forEach((item, index) => {
487-
const el = createPropertyElement(
488-
componentName,
489-
didRender,
490-
propsContainer,
491-
fiber,
492-
index.toString(),
493-
item,
494-
section,
495-
level + 1,
496-
changedKeys,
497-
currentPath,
498-
new WeakMap(),
499-
);
500-
if (!el) {
501-
return;
502-
}
503-
content.appendChild(el);
504-
});
505-
} else {
506-
Object.entries(value).forEach(([k, v]) => {
507-
const el = createPropertyElement(
508-
componentName,
509-
didRender,
510-
propsContainer,
511-
fiber,
512-
k,
513-
v,
514-
section,
515-
level + 1,
516-
changedKeys,
517-
currentPath,
518-
new WeakMap(),
519-
);
520-
if (!el) {
521-
return;
522-
}
523-
content.appendChild(el);
524-
});
534+
if (!isNonExpandable) {
535+
arrow.addEventListener('click', (e) => {
536+
e.stopPropagation();
537+
const isExpanding = !container.classList.contains('react-scan-expanded');
538+
539+
if (isExpanding) {
540+
EXPANDED_PATHS.add(currentPath);
541+
container.classList.add('react-scan-expanded');
542+
content.classList.remove('react-scan-hidden');
543+
544+
if (!content.hasChildNodes()) {
545+
if (Array.isArray(value)) {
546+
const arrayContainer = document.createElement('div');
547+
arrayContainer.className = 'react-scan-array-container';
548+
value.forEach((item, index) => {
549+
const el = createPropertyElement(
550+
componentName,
551+
didRender,
552+
propsContainer,
553+
fiber,
554+
index.toString(),
555+
item,
556+
section,
557+
level + 1,
558+
changedKeys,
559+
currentPath,
560+
new WeakMap(),
561+
);
562+
if (!el) {
563+
return;
564+
}
565+
arrayContainer.appendChild(el);
566+
});
567+
content.appendChild(arrayContainer);
568+
} else {
569+
Object.entries(value).forEach(([k, v]) => {
570+
const el = createPropertyElement(
571+
componentName,
572+
didRender,
573+
propsContainer,
574+
fiber,
575+
k,
576+
v,
577+
section,
578+
level + 1,
579+
changedKeys,
580+
currentPath,
581+
new WeakMap(),
582+
);
583+
if (!el) {
584+
return;
585+
}
586+
content.appendChild(el);
587+
});
588+
}
525589
}
590+
} else {
591+
EXPANDED_PATHS.delete(currentPath);
592+
container.classList.remove('react-scan-expanded');
593+
content.classList.add('react-scan-hidden');
526594
}
527-
} else {
528-
EXPANDED_PATHS.delete(currentPath);
529-
container.classList.remove('react-scan-expanded');
530-
content.classList.add('react-scan-hidden');
531-
}
532-
});
595+
596+
requestAnimationFrame(() => {
597+
const inspector = propsContainer.firstElementChild as HTMLElement;
598+
if (inspector) {
599+
const contentHeight = inspector.getBoundingClientRect().height;
600+
propsContainer.style.maxHeight = `${contentHeight}px`;
601+
}
602+
});
603+
});
604+
}
533605
} else {
534606
const preview = document.createElement('div');
535607
preview.className = 'react-scan-preview-line';
@@ -663,30 +735,54 @@ export const getValueClassName = (value: any) => {
663735
};
664736

665737
export const getValuePreview = (value: any) => {
666-
if (Array.isArray(value)) {
667-
return `Array(${value.length})`;
668-
}
669738
if (value === null) return 'null';
670739
if (value === undefined) return 'undefined';
671-
switch (typeof value) {
672-
case 'string':
673-
return `&quot;${value}&quot;`;
674-
case 'number':
675-
return value.toString();
676-
case 'boolean':
677-
return value.toString();
678-
case 'object': {
679-
if (value instanceof Promise) {
680-
return 'Promise';
681-
}
682-
const keys = Object.keys(value);
683-
if (keys.length <= 3) {
684-
return `{${keys.join(', ')}}`;
740+
741+
try {
742+
if (Array.isArray(value)) {
743+
return `Array(${value.length})`;
744+
}
745+
746+
switch (typeof value) {
747+
case 'string':
748+
return `"${value}"`;
749+
case 'number':
750+
case 'boolean':
751+
return String(value);
752+
case 'object': {
753+
// Get constructor once for all checks
754+
const constructor = value.constructor;
755+
756+
// Fast constructor checks
757+
if (constructor === Promise) {
758+
return '[Promise]';
759+
}
760+
if (constructor === Set) {
761+
return `Set(${value.size ?? '?'})`;
762+
}
763+
if (constructor === Map) {
764+
return `Map(${value.size ?? '?'})`;
765+
}
766+
if (constructor === URLSearchParams) {
767+
return '[URLSearchParams]';
768+
}
769+
770+
// Handle regular objects
771+
try {
772+
const keys = Object.keys(value);
773+
if (keys.length <= 3) {
774+
return `{${keys.join(', ')}}`;
775+
}
776+
return `{${keys.slice(0, 3).join(', ')}, ...}`;
777+
} catch {
778+
return '{...}';
779+
}
685780
}
686-
return `{${keys.slice(0, 8).join(', ')}, ...}`;
781+
default:
782+
return typeof value;
687783
}
688-
default:
689-
return typeof value;
784+
} catch {
785+
return String(value);
690786
}
691787
};
692788

0 commit comments

Comments
 (0)