Skip to content

Commit 21ffea0

Browse files
committed
fix(scan): fix promise/proxy handling and optimize object inspection
1 parent 8f1660d commit 21ffea0

File tree

1 file changed

+171
-78
lines changed

1 file changed

+171
-78
lines changed

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

Lines changed: 171 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,55 @@ const EXPANDED_PATHS = new Set<string>();
1212
const fadeOutTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();
1313
const disabledButtons = new Set<HTMLButtonElement>();
1414

15+
// Utility to check and unwrap proxies
16+
const isProxy = (obj: any) => Object.getPrototypeOf(obj)?.constructor?.name === 'Proxy';
17+
18+
const unwrapProxy = (proxy: any) => {
19+
try {
20+
const descriptors = Object.getOwnPropertyDescriptors(proxy);
21+
const unwrapped = Object.fromEntries(
22+
Object.entries(descriptors).reduce<Array<[string, any]>>((acc, [key, descriptor]) => {
23+
if (key !== 'Symbol(Symbol.iterator)') {
24+
acc.push([key, descriptor.value]);
25+
}
26+
return acc;
27+
}, []),
28+
);
29+
30+
return unwrapped;
31+
} catch (error) {
32+
return proxy;
33+
}
34+
};
35+
36+
37+
38+
const getProxyValue = (proxy: any) => {
39+
try {
40+
if (!proxy || typeof proxy !== 'object') {
41+
return proxy
42+
};
43+
44+
// Handle URLSearchParams-like objects
45+
if (proxy[Symbol.iterator]) {
46+
try {
47+
return Object.fromEntries(proxy);
48+
} catch (err) {
49+
// Silent fail
50+
}
51+
}
52+
53+
// Handle standard proxies
54+
if (isProxy(proxy)) {
55+
return unwrapProxy(proxy);
56+
}
57+
58+
return proxy;
59+
} catch (err) {
60+
return proxy;
61+
}
62+
};
63+
1564
export const renderPropsAndState = (
1665
didRender: boolean,
1766
fiber: any,
@@ -59,14 +108,12 @@ export const renderPropsAndState = (
59108
canEdit
60109
? `
61110
<button class="react-scan-replay-button" title="Replay component">
62-
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="rgb(203, 182, 242)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scan-eye"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="1"/><path d="M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"/></svg>
111+
Replay
63112
</button>
64113
`
65114
: ''
66115
}
67-
<button class="react-scan-close-button" title="Close">
68-
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
69-
</button>
116+
<button class="react-scan-close-button" title="Close">Close</button>
70117
</div>
71118
`;
72119
inspector.appendChild(header);
@@ -427,6 +474,7 @@ export const createPropertyElement = (
427474
if (isExpandable) {
428475
const isExpanded = EXPANDED_PATHS.has(currentPath);
429476

477+
// Check for circular references first
430478
if (typeof value === 'object' && value !== null) {
431479
let paths = objectPathMap.get(value);
432480
if (!paths) {
@@ -439,6 +487,28 @@ export const createPropertyElement = (
439487
paths.add(currentPath);
440488
}
441489

490+
const unwrapped = getProxyValue(value);
491+
const isNonExpandable = (unwrapped === value &&
492+
value &&
493+
Object.getPrototypeOf(value)?.constructor?.name === 'Proxy') ||
494+
value instanceof Promise;
495+
496+
// For non-expandable items, render like a simple property
497+
if (isNonExpandable) {
498+
const preview = document.createElement('div');
499+
preview.className = 'react-scan-preview-line';
500+
preview.dataset.key = key;
501+
preview.dataset.section = section;
502+
preview.innerHTML = `
503+
<span style="width: 8px; display: inline-block"></span>
504+
<span class="react-scan-key">${key}:&nbsp;</span>
505+
<span class="${getValueClassName(value)}">${getValuePreview(value)}</span>
506+
`;
507+
container.appendChild(preview);
508+
return container;
509+
}
510+
511+
// Normal expandable logic for other objects
442512
container.classList.add('react-scan-expandable');
443513
if (isExpanded) {
444514
container.classList.add('react-scan-expanded');
@@ -519,77 +589,79 @@ export const createPropertyElement = (
519589
}
520590
}
521591

522-
arrow.addEventListener('click', (e) => {
523-
e.stopPropagation();
524-
const isExpanding = !container.classList.contains(
525-
'react-scan-expanded',
526-
);
527-
528-
if (isExpanding) {
529-
EXPANDED_PATHS.add(currentPath);
530-
container.classList.add('react-scan-expanded');
531-
content.classList.remove('react-scan-hidden');
532-
533-
if (!content.hasChildNodes()) {
534-
if (Array.isArray(value)) {
535-
const arrayContainer = document.createElement('div');
536-
arrayContainer.className = 'react-scan-array-container';
537-
value.forEach((item, index) => {
538-
const el = createPropertyElement(
539-
componentName,
540-
didRender,
541-
propsContainer,
542-
fiber,
543-
index.toString(),
544-
item,
545-
section,
546-
level + 1,
547-
changedKeys,
548-
currentPath,
549-
new WeakMap(),
550-
);
551-
if (!el) {
552-
return;
553-
}
554-
arrayContainer.appendChild(el);
555-
});
556-
content.appendChild(arrayContainer);
557-
} else {
558-
Object.entries(value).forEach(([k, v]) => {
559-
const el = createPropertyElement(
560-
componentName,
561-
didRender,
562-
propsContainer,
563-
fiber,
564-
k,
565-
v,
566-
section,
567-
level + 1,
568-
changedKeys,
569-
currentPath,
570-
new WeakMap(),
571-
);
572-
if (!el) {
573-
return;
574-
}
575-
content.appendChild(el);
576-
});
592+
if (!isNonExpandable) {
593+
arrow.addEventListener('click', (e) => {
594+
e.stopPropagation();
595+
const isExpanding = !container.classList.contains(
596+
'react-scan-expanded',
597+
);
598+
599+
if (isExpanding) {
600+
EXPANDED_PATHS.add(currentPath);
601+
container.classList.add('react-scan-expanded');
602+
content.classList.remove('react-scan-hidden');
603+
604+
if (!content.hasChildNodes()) {
605+
if (Array.isArray(value)) {
606+
const arrayContainer = document.createElement('div');
607+
arrayContainer.className = 'react-scan-array-container';
608+
value.forEach((item, index) => {
609+
const el = createPropertyElement(
610+
componentName,
611+
didRender,
612+
propsContainer,
613+
fiber,
614+
index.toString(),
615+
item,
616+
section,
617+
level + 1,
618+
changedKeys,
619+
currentPath,
620+
new WeakMap(),
621+
);
622+
if (!el) {
623+
return;
624+
}
625+
arrayContainer.appendChild(el);
626+
});
627+
content.appendChild(arrayContainer);
628+
} else {
629+
Object.entries(value).forEach(([k, v]) => {
630+
const el = createPropertyElement(
631+
componentName,
632+
didRender,
633+
propsContainer,
634+
fiber,
635+
k,
636+
v,
637+
section,
638+
level + 1,
639+
changedKeys,
640+
currentPath,
641+
new WeakMap(),
642+
);
643+
if (!el) {
644+
return;
645+
}
646+
content.appendChild(el);
647+
});
648+
}
577649
}
650+
} else {
651+
EXPANDED_PATHS.delete(currentPath);
652+
container.classList.remove('react-scan-expanded');
653+
content.classList.add('react-scan-hidden');
578654
}
579-
} else {
580-
EXPANDED_PATHS.delete(currentPath);
581-
container.classList.remove('react-scan-expanded');
582-
content.classList.add('react-scan-hidden');
583-
}
584655

585-
requestAnimationFrame(() => {
586-
const inspector = propsContainer.firstElementChild as HTMLElement;
587-
if (inspector) {
588-
const contentHeight = inspector.getBoundingClientRect().height;
589-
propsContainer.style.maxHeight = `${contentHeight}px`;
590-
}
656+
requestAnimationFrame(() => {
657+
const inspector = propsContainer.firstElementChild as HTMLElement;
658+
if (inspector) {
659+
const contentHeight = inspector.getBoundingClientRect().height;
660+
propsContainer.style.maxHeight = `${contentHeight}px`;
661+
}
662+
});
591663
});
592-
});
664+
}
593665
} else {
594666
const preview = document.createElement('div');
595667
preview.className = 'react-scan-preview-line';
@@ -737,14 +809,35 @@ export const getValuePreview = (value: any) => {
737809
case 'boolean':
738810
return value.toString();
739811
case 'object': {
740-
if (value instanceof Promise) {
741-
return 'Promise';
742-
}
743-
const keys = Object.keys(value);
744-
if (keys.length <= 3) {
745-
return `{${keys.join(', ')}}`;
812+
try {
813+
if (value === null) return 'null';
814+
if (value === undefined) return 'undefined';
815+
816+
// Handle special built-in types first
817+
if (value instanceof Promise) return '[Promise]';
818+
if (value instanceof Set) return `Set(${value.size})`;
819+
if (value instanceof Map) return `Map(${value.size})`;
820+
821+
// Try to unwrap proxy values
822+
const proto = Object.getPrototypeOf(value);
823+
if (proto?.constructor?.name === 'Proxy') {
824+
const unwrapped = getProxyValue(value);
825+
if (unwrapped !== value) {
826+
const keys = Object.keys(unwrapped);
827+
return `Proxy{${keys.join(', ')}}`;
828+
}
829+
return '[Next.js Params]';
830+
}
831+
832+
// Handle regular objects
833+
const keys = Object.keys(value);
834+
if (keys.length <= 3) {
835+
return `{${keys.join(', ')}}`;
836+
}
837+
return `{${keys.slice(0, 3).join(', ')}, ...}`;
838+
} catch (error) {
839+
return '{...}';
746840
}
747-
return `{${keys.slice(0, 3).join(', ')}, ...}`;
748841
}
749842
default:
750843
return typeof value;

0 commit comments

Comments
 (0)