Skip to content

Commit e8927ff

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

File tree

1 file changed

+169
-74
lines changed

1 file changed

+169
-74
lines changed

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

Lines changed: 169 additions & 74 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,
@@ -427,6 +476,7 @@ export const createPropertyElement = (
427476
if (isExpandable) {
428477
const isExpanded = EXPANDED_PATHS.has(currentPath);
429478

479+
// Check for circular references first
430480
if (typeof value === 'object' && value !== null) {
431481
let paths = objectPathMap.get(value);
432482
if (!paths) {
@@ -439,6 +489,28 @@ export const createPropertyElement = (
439489
paths.add(currentPath);
440490
}
441491

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

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-
});
594+
if (!isNonExpandable) {
595+
arrow.addEventListener('click', (e) => {
596+
e.stopPropagation();
597+
const isExpanding = !container.classList.contains(
598+
'react-scan-expanded',
599+
);
600+
601+
if (isExpanding) {
602+
EXPANDED_PATHS.add(currentPath);
603+
container.classList.add('react-scan-expanded');
604+
content.classList.remove('react-scan-hidden');
605+
606+
if (!content.hasChildNodes()) {
607+
if (Array.isArray(value)) {
608+
const arrayContainer = document.createElement('div');
609+
arrayContainer.className = 'react-scan-array-container';
610+
value.forEach((item, index) => {
611+
const el = createPropertyElement(
612+
componentName,
613+
didRender,
614+
propsContainer,
615+
fiber,
616+
index.toString(),
617+
item,
618+
section,
619+
level + 1,
620+
changedKeys,
621+
currentPath,
622+
new WeakMap(),
623+
);
624+
if (!el) {
625+
return;
626+
}
627+
arrayContainer.appendChild(el);
628+
});
629+
content.appendChild(arrayContainer);
630+
} else {
631+
Object.entries(value).forEach(([k, v]) => {
632+
const el = createPropertyElement(
633+
componentName,
634+
didRender,
635+
propsContainer,
636+
fiber,
637+
k,
638+
v,
639+
section,
640+
level + 1,
641+
changedKeys,
642+
currentPath,
643+
new WeakMap(),
644+
);
645+
if (!el) {
646+
return;
647+
}
648+
content.appendChild(el);
649+
});
650+
}
577651
}
652+
} else {
653+
EXPANDED_PATHS.delete(currentPath);
654+
container.classList.remove('react-scan-expanded');
655+
content.classList.add('react-scan-hidden');
578656
}
579-
} else {
580-
EXPANDED_PATHS.delete(currentPath);
581-
container.classList.remove('react-scan-expanded');
582-
content.classList.add('react-scan-hidden');
583-
}
584657

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-
}
658+
requestAnimationFrame(() => {
659+
const inspector = propsContainer.firstElementChild as HTMLElement;
660+
if (inspector) {
661+
const contentHeight = inspector.getBoundingClientRect().height;
662+
propsContainer.style.maxHeight = `${contentHeight}px`;
663+
}
664+
});
591665
});
592-
});
666+
}
593667
} else {
594668
const preview = document.createElement('div');
595669
preview.className = 'react-scan-preview-line';
@@ -737,14 +811,35 @@ export const getValuePreview = (value: any) => {
737811
case 'boolean':
738812
return value.toString();
739813
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(', ')}}`;
814+
try {
815+
if (value === null) return 'null';
816+
if (value === undefined) return 'undefined';
817+
818+
// Handle special built-in types first
819+
if (value instanceof Promise) return '[Promise]';
820+
if (value instanceof Set) return `Set(${value.size})`;
821+
if (value instanceof Map) return `Map(${value.size})`;
822+
823+
// Try to unwrap proxy values
824+
const proto = Object.getPrototypeOf(value);
825+
if (proto?.constructor?.name === 'Proxy') {
826+
const unwrapped = getProxyValue(value);
827+
if (unwrapped !== value) {
828+
const keys = Object.keys(unwrapped);
829+
return `Proxy{${keys.join(', ')}}`;
830+
}
831+
return '[Next.js Params]';
832+
}
833+
834+
// Handle regular objects
835+
const keys = Object.keys(value);
836+
if (keys.length <= 3) {
837+
return `{${keys.join(', ')}}`;
838+
}
839+
return `{${keys.slice(0, 3).join(', ')}, ...}`;
840+
} catch (error) {
841+
return '{...}';
746842
}
747-
return `{${keys.slice(0, 3).join(', ')}, ...}`;
748843
}
749844
default:
750845
return typeof value;

0 commit comments

Comments
 (0)