Merge 922de88ed6 into sapling-pr-archive-poteto

This commit is contained in:
lauren
2025-07-29 11:54:35 -04:00
committed by GitHub
40 changed files with 1093 additions and 920 deletions
+1
View File
@@ -608,6 +608,7 @@ module.exports = {
symbol: 'readonly',
SyntheticEvent: 'readonly',
SyntheticMouseEvent: 'readonly',
SyntheticPointerEvent: 'readonly',
Thenable: 'readonly',
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
@@ -1324,6 +1324,34 @@ const allTests = {
`,
errors: [asyncComponentHookError('use')],
},
{
code: normalizeIndent`
function App({p1, p2}) {
try {
use(p1);
} catch (error) {
console.error(error);
}
use(p2);
return <div>App</div>;
}
`,
errors: [tryCatchUseError('use')],
},
{
code: normalizeIndent`
function App({p1, p2}) {
try {
doSomething();
} catch {
use(p1);
}
use(p2);
return <div>App</div>;
}
`,
errors: [tryCatchUseError('use')],
},
],
};
@@ -1383,7 +1411,7 @@ if (__EXPERIMENTAL__) {
const onEvent = useEffectEvent((text) => {
console.log(text);
});
useEffect(() => {
onEvent('Hello world');
});
@@ -1421,7 +1449,7 @@ if (__EXPERIMENTAL__) {
});
return <Child onClick={() => onClick()} />
}
// The useEffectEvent function shares an identifier name with the above
function MyLastComponent({theme}) {
const onClick = useEffectEvent(() => {
@@ -1573,6 +1601,12 @@ function asyncComponentHookError(fn) {
};
}
function tryCatchUseError(fn) {
return {
message: `React Hook "${fn}" cannot be called in a try/catch block.`,
};
}
// For easier local testing
if (!process.env.CI) {
let only = [];
@@ -111,6 +111,16 @@ function isInsideDoWhileLoop(node: Node | undefined): node is DoWhileStatement {
return false;
}
function isInsideTryCatch(node: Node | undefined): boolean {
while (node) {
if (node.type === 'TryStatement' || node.type === 'CatchClause') {
return true;
}
node = node.parent;
}
return false;
}
function isUseEffectEventIdentifier(node: Node): boolean {
if (__EXPERIMENTAL__) {
return node.type === 'Identifier' && node.name === 'useEffectEvent';
@@ -532,6 +542,16 @@ const rule = {
continue;
}
// Report an error if use() is called inside try/catch/finally.
if (isUseIdentifier(hook) && isInsideTryCatch(hook)) {
context.report({
node: hook,
message: `React Hook "${getSourceCode().getText(
hook,
)}" cannot be called in a try/catch/finally block.`,
});
}
// Report an error if a hook may be called more then once.
// `use(...)` can be called in loops.
if (
+2 -64
View File
@@ -22,6 +22,8 @@ import {
addObjectToProperties,
} from 'shared/ReactPerformanceTrackProperties';
import {getIODescription} from 'shared/ReactIODescription';
const supportsUserTiming =
enableProfilerTimer &&
typeof console !== 'undefined' &&
@@ -300,70 +302,6 @@ function getIOColor(
}
}
function getIODescription(value: any): string {
if (!__DEV__) {
return '';
}
try {
switch (typeof value) {
case 'object':
// Test the object for a bunch of common property names that are useful identifiers.
// While we only have the return value here, it should ideally be a name that
// describes the arguments requested.
if (value === null) {
return '';
} else if (value instanceof Error) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.message);
} else if (typeof value.url === 'string') {
return value.url;
} else if (typeof value.command === 'string') {
return value.command;
} else if (
typeof value.request === 'object' &&
typeof value.request.url === 'string'
) {
return value.request.url;
} else if (
typeof value.response === 'object' &&
typeof value.response.url === 'string'
) {
return value.response.url;
} else if (
typeof value.id === 'string' ||
typeof value.id === 'number' ||
typeof value.id === 'bigint'
) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.id);
} else if (typeof value.name === 'string') {
return value.name;
} else {
const str = value.toString();
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
// This is probably not a useful description.
return '';
}
return str;
}
case 'string':
if (value.length < 5 || value.length > 500) {
return '';
}
return value;
case 'number':
case 'bigint':
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value);
default:
// Not useful descriptors.
return '';
}
} catch (x) {
return '';
}
}
function getIOLongName(
ioInfo: ReactIOInfo,
description: string,
@@ -857,7 +857,7 @@ describe('Timeline profiler', () => {
{
"batchUID": 0,
"depth": 0,
"duration": 0.014,
"duration": 0.012,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.008,
"type": "render-idle",
@@ -873,25 +873,17 @@ describe('Timeline profiler', () => {
{
"batchUID": 0,
"depth": 0,
"duration": 0.010,
"duration": 0.008,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.012,
"type": "commit",
},
{
"batchUID": 0,
"depth": 1,
"duration": 0.001,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.02,
"type": "layout-effects",
},
{
"batchUID": 0,
"depth": 0,
"duration": 0.004,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.023,
"timestamp": 0.021,
"type": "passive-effects",
},
],
@@ -899,9 +891,9 @@ describe('Timeline profiler', () => {
{
"batchUID": 1,
"depth": 0,
"duration": 0.014,
"duration": 0.012,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.028,
"timestamp": 0.026,
"type": "render-idle",
},
{
@@ -909,31 +901,23 @@ describe('Timeline profiler', () => {
"depth": 0,
"duration": 0.003,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.028,
"timestamp": 0.026,
"type": "render",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0.010,
"duration": 0.008,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.032,
"timestamp": 0.03,
"type": "commit",
},
{
"batchUID": 1,
"depth": 1,
"duration": 0.001,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.04,
"type": "layout-effects",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0.003,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.043,
"timestamp": 0.039,
"type": "passive-effects",
},
],
@@ -949,26 +933,26 @@ describe('Timeline profiler', () => {
{
"componentName": "App",
"duration": 0.002,
"timestamp": 0.024,
"timestamp": 0.022,
"type": "passive-effect-mount",
"warning": null,
},
{
"componentName": "App",
"duration": 0.001,
"timestamp": 0.029,
"timestamp": 0.027,
"type": "render",
"warning": null,
},
{
"componentName": "App",
"duration": 0.001,
"timestamp": 0.044,
"timestamp": 0.04,
"type": "passive-effect-mount",
"warning": null,
},
],
"duration": 0.046,
"duration": 0.042,
"flamechart": [],
"internalModuleSourceToRanges": Map {
undefined => [
@@ -1031,7 +1015,7 @@ describe('Timeline profiler', () => {
{
"batchUID": 0,
"depth": 0,
"duration": 0.014,
"duration": 0.012,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.008,
"type": "render-idle",
@@ -1047,33 +1031,25 @@ describe('Timeline profiler', () => {
{
"batchUID": 0,
"depth": 0,
"duration": 0.010,
"duration": 0.008,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.012,
"type": "commit",
},
{
"batchUID": 0,
"depth": 1,
"duration": 0.001,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.02,
"type": "layout-effects",
},
{
"batchUID": 0,
"depth": 0,
"duration": 0.004,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.023,
"timestamp": 0.021,
"type": "passive-effects",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0.014,
"duration": 0.012,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.028,
"timestamp": 0.026,
"type": "render-idle",
},
{
@@ -1081,31 +1057,23 @@ describe('Timeline profiler', () => {
"depth": 0,
"duration": 0.003,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.028,
"timestamp": 0.026,
"type": "render",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0.010,
"duration": 0.008,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.032,
"timestamp": 0.03,
"type": "commit",
},
{
"batchUID": 1,
"depth": 1,
"duration": 0.001,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.04,
"type": "layout-effects",
},
{
"batchUID": 1,
"depth": 0,
"duration": 0.003,
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.043,
"timestamp": 0.039,
"type": "passive-effects",
},
],
@@ -1149,7 +1117,7 @@ describe('Timeline profiler', () => {
{
"componentName": "App",
"lanes": "0b0000000000000000000000000000101",
"timestamp": 0.025,
"timestamp": 0.023,
"type": "schedule-state-update",
"warning": null,
},
@@ -1254,6 +1222,15 @@ describe('Timeline profiler', () => {
let promise = null;
let resolvedValue = null;
function readValue(value) {
if (React.use) {
if (promise === null) {
promise = Promise.resolve(true).then(() => {
return value;
});
promise.displayName = 'Testing displayName';
}
return React.use(promise);
}
if (resolvedValue !== null) {
return resolvedValue;
} else if (promise === null) {
@@ -1273,7 +1250,7 @@ describe('Timeline profiler', () => {
const testMarks = [creactCpuProfilerSample()];
const root = ReactDOMClient.createRoot(document.createElement('div'));
utils.act(() =>
await utils.actAsync(() =>
root.render(
<React.Suspense fallback="Loading...">
<Component />
@@ -1823,6 +1800,14 @@ describe('Timeline profiler', () => {
let promise = null;
let resolvedValue = null;
function readValue(value) {
if (React.use) {
if (promise === null) {
promise = Promise.resolve(true).then(() => {
return value;
});
}
return React.use(promise);
}
if (resolvedValue !== null) {
return resolvedValue;
} else if (promise === null) {
@@ -1881,6 +1866,14 @@ describe('Timeline profiler', () => {
let promise = null;
let resolvedValue = null;
function readValue(value) {
if (React.use) {
if (promise === null) {
promise = Promise.resolve(true).then(() => {
return value;
});
}
return React.use(promise);
}
if (resolvedValue !== null) {
return resolvedValue;
} else if (promise === null) {
@@ -2192,14 +2185,6 @@ describe('Timeline profiler', () => {
"timestamp": 10,
"type": "commit",
},
{
"batchUID": 1,
"depth": 1,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "layout-effects",
},
{
"batchUID": 1,
"depth": 0,
@@ -2234,14 +2219,6 @@ describe('Timeline profiler', () => {
"timestamp": 10,
"type": "commit",
},
{
"batchUID": 2,
"depth": 1,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "layout-effects",
},
{
"batchUID": 2,
"depth": 0,
@@ -2292,8 +2269,8 @@ describe('Timeline profiler', () => {
8 => "InputContinuous",
16 => "DefaultHydration",
32 => "Default",
64 => "TransitionHydration",
128 => "Transition",
64 => undefined,
128 => "TransitionHydration",
256 => "Transition",
512 => "Transition",
1024 => "Transition",
@@ -2349,14 +2326,6 @@ describe('Timeline profiler', () => {
"timestamp": 10,
"type": "commit",
},
{
"batchUID": 1,
"depth": 1,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "layout-effects",
},
{
"batchUID": 1,
"depth": 0,
@@ -2389,14 +2358,6 @@ describe('Timeline profiler', () => {
"timestamp": 10,
"type": "commit",
},
{
"batchUID": 2,
"depth": 1,
"duration": 0,
"lanes": "0b0000000000000000000000000100000",
"timestamp": 10,
"type": "layout-effects",
},
{
"batchUID": 2,
"depth": 0,
@@ -215,7 +215,11 @@ describe('ProfilerStore', () => {
it('should not throw while initializing context values for Fibers within a not-yet-mounted subtree', () => {
const promise = new Promise(resolve => {});
const SuspendingView = () => {
throw promise;
if (React.use) {
React.use(promise);
} else {
throw promise;
}
};
const App = () => {
@@ -682,6 +682,14 @@ describe('ProfilingCache', () => {
it('should calculate durations correctly for suspended views', async () => {
let data;
const getData = () => {
if (React.use) {
if (!data) {
data = new Promise(resolve => {
resolve('abc');
});
}
return React.use(data);
}
if (data) {
return data;
} else {
File diff suppressed because it is too large Load Diff
@@ -509,7 +509,11 @@ describe('Store component filters', () => {
const Component = ({shouldSuspend}) => {
if (shouldSuspend) {
throw promise;
if (React.use) {
React.use(promise);
} else {
throw promise;
}
}
return null;
};
@@ -522,7 +522,11 @@ describe('StoreStress (Legacy Mode)', () => {
];
const Never = () => {
throw new Promise(() => {});
if (React.use) {
React.use(new Promise(() => {}));
} else {
throw new Promise(() => {});
}
};
const Root = ({children}) => {
@@ -1144,7 +1148,11 @@ describe('StoreStress (Legacy Mode)', () => {
];
const Never = () => {
throw new Promise(() => {});
if (React.use) {
React.use(new Promise(() => {}));
} else {
throw new Promise(() => {});
}
};
const MaybeSuspend = ({children, suspend}) => {
@@ -38,7 +38,7 @@ describe('StoreStressConcurrent', () => {
// This is a stress test for the tree mount/update/unmount traversal.
// It renders different trees that should produce the same output.
// @reactVersion >= 18.0
it('should handle a stress test with different tree operations (Concurrent Mode)', () => {
it('should handle a stress test with different tree operations (Concurrent Mode)', async () => {
let setShowX;
const A = () => 'a';
const B = () => 'b';
@@ -151,26 +151,26 @@ describe('StoreStressConcurrent', () => {
root = ReactDOMClient.createRoot(container);
// Verify mounting 'abcde'.
act(() => root.render(<Parent>{cases[i]}</Parent>));
await act(() => root.render(<Parent>{cases[i]}</Parent>));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
await act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
await act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// Clean up.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -180,19 +180,19 @@ describe('StoreStressConcurrent', () => {
root = ReactDOMClient.createRoot(container);
for (let i = 0; i < cases.length; i++) {
// Verify mounting 'abcde'.
act(() => root.render(<Parent>{cases[i]}</Parent>));
await act(() => root.render(<Parent>{cases[i]}</Parent>));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
await act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
await act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
@@ -204,7 +204,7 @@ describe('StoreStressConcurrent', () => {
});
// @reactVersion >= 18.0
it('should handle stress test with reordering (Concurrent Mode)', () => {
it('should handle stress test with reordering (Concurrent Mode)', async () => {
const A = () => 'a';
const B = () => 'b';
const C = () => 'c';
@@ -245,10 +245,10 @@ describe('StoreStressConcurrent', () => {
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
const root = ReactDOMClient.createRoot(container);
act(() => root.render(<Root>{steps[i]}</Root>));
await act(() => root.render(<Root>{steps[i]}</Root>));
// We snapshot each step once so it doesn't regress.
snapshots.push(print(store));
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -316,13 +316,13 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() => root.render(<Root>{steps[i]}</Root>));
await act(() => root.render(<Root>{steps[i]}</Root>));
expect(print(store)).toMatch(snapshots[i]);
act(() => root.render(<Root>{steps[j]}</Root>));
await act(() => root.render(<Root>{steps[j]}</Root>));
expect(print(store)).toMatch(snapshots[j]);
act(() => root.render(<Root>{steps[i]}</Root>));
await act(() => root.render(<Root>{steps[i]}</Root>));
expect(print(store)).toMatch(snapshots[i]);
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -332,7 +332,7 @@ describe('StoreStressConcurrent', () => {
for (let j = 0; j < steps.length; j++) {
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<div>{steps[i]}</div>
@@ -340,7 +340,7 @@ describe('StoreStressConcurrent', () => {
),
);
expect(print(store)).toMatch(snapshots[i]);
act(() =>
await act(() =>
root.render(
<Root>
<div>{steps[j]}</div>
@@ -348,7 +348,7 @@ describe('StoreStressConcurrent', () => {
),
);
expect(print(store)).toMatch(snapshots[j]);
act(() =>
await act(() =>
root.render(
<Root>
<div>{steps[i]}</div>
@@ -356,7 +356,7 @@ describe('StoreStressConcurrent', () => {
),
);
expect(print(store)).toMatch(snapshots[i]);
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -392,7 +392,11 @@ describe('StoreStressConcurrent', () => {
];
const Never = () => {
throw new Promise(() => {});
if (React.use) {
React.use(new Promise(() => {}));
} else {
throw new Promise(() => {});
}
};
const Root = ({children}) => {
@@ -405,7 +409,7 @@ describe('StoreStressConcurrent', () => {
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -416,7 +420,7 @@ describe('StoreStressConcurrent', () => {
);
// We snapshot each step once so it doesn't regress.d
snapshots.push(print(store));
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -507,7 +511,7 @@ describe('StoreStressConcurrent', () => {
// 2. Verify check Suspense can render same steps as initial fallback content.
for (let i = 0; i < steps.length; i++) {
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -521,7 +525,7 @@ describe('StoreStressConcurrent', () => {
),
);
expect(print(store)).toEqual(snapshots[i]);
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -531,7 +535,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -542,7 +546,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -554,7 +558,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -565,7 +569,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -576,7 +580,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -591,7 +595,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -607,7 +611,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -622,7 +626,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -633,7 +637,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -644,7 +648,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -660,7 +664,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -671,7 +675,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -682,7 +686,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -697,7 +701,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -709,7 +713,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -724,7 +728,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -735,7 +739,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -772,7 +776,7 @@ describe('StoreStressConcurrent', () => {
expect(print(store)).toEqual(snapshots[i]);
// Trigger actual fallback.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -788,7 +792,7 @@ describe('StoreStressConcurrent', () => {
expect(print(store)).toEqual(snapshots[j]);
// Force fallback while we're in fallback mode.
act(() => {
await act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
@@ -799,7 +803,7 @@ describe('StoreStressConcurrent', () => {
expect(print(store)).toEqual(snapshots[j]);
// Switch to primary mode.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -859,7 +863,11 @@ describe('StoreStressConcurrent', () => {
];
const Never = () => {
throw new Promise(() => {});
if (React.use) {
React.use(new Promise(() => {}));
} else {
throw new Promise(() => {});
}
};
const MaybeSuspend = ({children, suspend}) => {
@@ -890,7 +898,7 @@ describe('StoreStressConcurrent', () => {
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -903,7 +911,7 @@ describe('StoreStressConcurrent', () => {
);
// We snapshot each step once so it doesn't regress.
snapshots.push(print(store));
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -913,7 +921,7 @@ describe('StoreStressConcurrent', () => {
const fallbackSnapshots = [];
for (let i = 0; i < steps.length; i++) {
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -928,7 +936,7 @@ describe('StoreStressConcurrent', () => {
);
// We snapshot each step once so it doesn't regress.
fallbackSnapshots.push(print(store));
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -1046,7 +1054,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1059,7 +1067,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1073,7 +1081,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1086,7 +1094,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -1097,7 +1105,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1115,7 +1123,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1134,7 +1142,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1152,7 +1160,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -1163,7 +1171,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1176,7 +1184,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1190,7 +1198,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1203,7 +1211,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -1214,7 +1222,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1227,7 +1235,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1241,7 +1249,7 @@ describe('StoreStressConcurrent', () => {
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1254,7 +1262,7 @@ describe('StoreStressConcurrent', () => {
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -1265,7 +1273,7 @@ describe('StoreStressConcurrent', () => {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1304,7 +1312,7 @@ describe('StoreStressConcurrent', () => {
expect(print(store)).toEqual(snapshots[i]);
// Trigger actual fallback.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1318,7 +1326,7 @@ describe('StoreStressConcurrent', () => {
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Force fallback while we're in fallback mode.
act(() => {
await act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
@@ -1329,7 +1337,7 @@ describe('StoreStressConcurrent', () => {
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Switch to primary mode.
act(() =>
await act(() =>
root.render(
<Root>
<X />
@@ -1355,7 +1363,7 @@ describe('StoreStressConcurrent', () => {
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
await act(() => root.unmount());
expect(print(store)).toBe('');
}
}
@@ -105,6 +105,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
import is from 'shared/objectIs';
import hasOwnProperty from 'shared/hasOwnProperty';
import {getIODescription} from 'shared/ReactIODescription';
import {
getStackByFiberInDevAndProd,
getOwnerStackByFiberInDev,
@@ -4116,9 +4118,26 @@ export function attach(
parentInstance,
asyncInfo.owner,
);
const value: any = ioInfo.value;
let resolvedValue = undefined;
if (
typeof value === 'object' &&
value !== null &&
typeof value.then === 'function'
) {
switch (value.status) {
case 'fulfilled':
resolvedValue = value.value;
break;
case 'rejected':
resolvedValue = value.reason;
break;
}
}
return {
awaited: {
name: ioInfo.name,
description: getIODescription(resolvedValue),
start: ioInfo.start,
end: ioInfo.end,
value: ioInfo.value == null ? null : ioInfo.value,
+1
View File
@@ -235,6 +235,7 @@ export type PathMatch = {
// Serialized version of ReactIOInfo
export type SerializedIOInfo = {
name: string,
description: string,
start: number,
end: number,
value: null | Promise<mixed>,
+1
View File
@@ -218,6 +218,7 @@ function backendToFrontendSerializedAsyncInfo(
return {
awaited: {
name: ioInfo.name,
description: ioInfo.description,
start: ioInfo.start,
end: ioInfo.end,
value: ioInfo.value,
+29 -57
View File
@@ -7,7 +7,12 @@
* @flow
*/
import type {ReactContext, Thenable} from 'shared/ReactTypes';
import type {
ReactContext,
Thenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
import * as React from 'react';
import {createContext} from 'react';
@@ -26,27 +31,6 @@ import {createContext} from 'react';
export type {Thenable};
interface Suspender {
then(resolve: () => mixed, reject: () => mixed): mixed;
}
type PendingResult = {
status: 0,
value: Suspender,
};
type ResolvedResult<Value> = {
status: 1,
value: Value,
};
type RejectedResult = {
status: 2,
value: mixed,
};
type Result<Value> = PendingResult | ResolvedResult<Value> | RejectedResult;
export type Resource<Input, Key, Value> = {
clear(): void,
invalidate(Key): void,
@@ -55,10 +39,6 @@ export type Resource<Input, Key, Value> = {
write(Key, Value): void,
};
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
let readContext;
if (typeof React.use === 'function') {
readContext = function (Context: ReactContext<null>) {
@@ -115,33 +95,25 @@ function accessResult<Input, Key, Value>(
fetch: Input => Thenable<Value>,
input: Input,
key: Key,
): Result<Value> {
): Thenable<Value> {
const entriesForResource = getEntriesForResource(resource);
const entry = entriesForResource.get(key);
if (entry === undefined) {
const thenable = fetch(input);
thenable.then(
value => {
if (newResult.status === Pending) {
const resolvedResult: ResolvedResult<Value> = (newResult: any);
resolvedResult.status = Resolved;
resolvedResult.value = value;
}
const fulfilledThenable: FulfilledThenable<Value> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = value;
},
error => {
if (newResult.status === Pending) {
const rejectedResult: RejectedResult = (newResult: any);
rejectedResult.status = Rejected;
rejectedResult.value = error;
}
const rejectedThenable: RejectedThenable<Value> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
},
);
const newResult: PendingResult = {
status: Pending,
value: thenable,
};
entriesForResource.set(key, newResult);
return newResult;
entriesForResource.set(key, thenable);
return thenable;
} else {
return entry;
}
@@ -167,23 +139,22 @@ export function createResource<Input, Key, Value>(
readContext(CacheContext);
const key = hashInput(input);
const result: Result<Value> = accessResult(resource, fetch, input, key);
const result: Thenable<Value> = accessResult(resource, fetch, input, key);
if (typeof React.use === 'function') {
return React.use(result);
}
switch (result.status) {
case Pending: {
const suspender = result.value;
throw suspender;
}
case Resolved: {
case 'fulfilled': {
const value = result.value;
return value;
}
case Rejected: {
const error = result.value;
case 'rejected': {
const error = result.reason;
throw error;
}
default:
// Should be unreachable
return (undefined: any);
throw result;
}
},
@@ -198,12 +169,13 @@ export function createResource<Input, Key, Value>(
write(key: Key, value: Value): void {
const entriesForResource = getEntriesForResource(resource);
const resolvedResult: ResolvedResult<Value> = {
status: Resolved,
const fulfilledThenable: FulfilledThenable<Value> = (Promise.resolve(
value,
};
): any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = value;
entriesForResource.set(key, resolvedResult);
entriesForResource.set(key, fulfilledThenable);
},
};
@@ -16,7 +16,6 @@
.TreeWrapper {
flex: 0 0 var(--horizontal-resize-percentage);
overflow: auto;
}
.InspectedElementWrapper {
@@ -32,13 +31,20 @@
.ResizeBar {
position: absolute;
left: -2px;
/*
* moving the bar out of its bounding box might cause its hitbox to overlap
* with another scrollbar creating disorienting UX where you both resize and scroll
* at the same time.
* If you adjust this value, double check that starting resize right on this edge
* doesn't also cause scroll
*/
left: 1px;
width: 5px;
height: 100%;
cursor: ew-resize;
}
@media screen and (max-width: 600px) {
@container devtools (width < 600px) {
.Components {
flex-direction: column;
}
@@ -52,7 +58,7 @@
}
.ResizeBar {
top: -2px;
top: 1px;
left: 0;
width: 100%;
height: 5px;
@@ -29,7 +29,6 @@ type Orientation = 'horizontal' | 'vertical';
type ResizeActionType =
| 'ACTION_SET_DID_MOUNT'
| 'ACTION_SET_IS_RESIZING'
| 'ACTION_SET_HORIZONTAL_PERCENTAGE'
| 'ACTION_SET_VERTICAL_PERCENTAGE';
@@ -40,7 +39,6 @@ type ResizeAction = {
type ResizeState = {
horizontalPercentage: number,
isResizing: boolean,
verticalPercentage: number,
};
@@ -81,82 +79,81 @@ function Components(_: {}) {
return () => clearTimeout(timeoutID);
}, [horizontalPercentage, verticalPercentage]);
const {isResizing} = state;
const onResizeStart = (event: SyntheticPointerEvent<HTMLElement>) => {
const element = event.currentTarget;
element.setPointerCapture(event.pointerId);
};
const onResizeStart = () =>
dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true});
const onResizeEnd = (event: SyntheticPointerEvent<HTMLElement>) => {
const element = event.currentTarget;
element.releasePointerCapture(event.pointerId);
};
let onResize;
let onResizeEnd;
if (isResizing) {
onResizeEnd = () =>
dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false});
const onResize = (event: SyntheticPointerEvent<HTMLElement>) => {
const element = event.currentTarget;
const isResizing = element.hasPointerCapture(event.pointerId);
if (!isResizing) {
return;
}
// $FlowFixMe[missing-local-annot]
onResize = event => {
const resizeElement = resizeElementRef.current;
const wrapperElement = wrapperElementRef.current;
const resizeElement = resizeElementRef.current;
const wrapperElement = wrapperElementRef.current;
if (!isResizing || wrapperElement === null || resizeElement === null) {
return;
}
if (wrapperElement === null || resizeElement === null) {
return;
}
event.preventDefault();
event.preventDefault();
const orientation = getOrientation(wrapperElement);
const orientation = getOrientation(wrapperElement);
const {height, width, left, top} = wrapperElement.getBoundingClientRect();
const {height, width, left, top} = wrapperElement.getBoundingClientRect();
const currentMousePosition =
const currentMousePosition =
orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
const boundaryMin = MINIMUM_SIZE;
const boundaryMax =
orientation === 'horizontal'
? width - MINIMUM_SIZE
: height - MINIMUM_SIZE;
const isMousePositionInBounds =
currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
if (isMousePositionInBounds) {
const resizedElementDimension =
orientation === 'horizontal' ? width : height;
const actionType =
orientation === 'horizontal'
? event.clientX - left
: event.clientY - top;
? 'ACTION_SET_HORIZONTAL_PERCENTAGE'
: 'ACTION_SET_VERTICAL_PERCENTAGE';
const percentage = (currentMousePosition / resizedElementDimension) * 100;
const boundaryMin = MINIMUM_SIZE;
const boundaryMax =
orientation === 'horizontal'
? width - MINIMUM_SIZE
: height - MINIMUM_SIZE;
setResizeCSSVariable(resizeElement, orientation, percentage);
const isMousePositionInBounds =
currentMousePosition > boundaryMin &&
currentMousePosition < boundaryMax;
if (isMousePositionInBounds) {
const resizedElementDimension =
orientation === 'horizontal' ? width : height;
const actionType =
orientation === 'horizontal'
? 'ACTION_SET_HORIZONTAL_PERCENTAGE'
: 'ACTION_SET_VERTICAL_PERCENTAGE';
const percentage =
(currentMousePosition / resizedElementDimension) * 100;
setResizeCSSVariable(resizeElement, orientation, percentage);
dispatch({
type: actionType,
payload: currentMousePosition / resizedElementDimension,
});
}
};
}
dispatch({
type: actionType,
payload: currentMousePosition / resizedElementDimension,
});
}
};
return (
<SettingsModalContextController>
<OwnersListContextController>
<div
ref={wrapperElementRef}
className={styles.Components}
onMouseMove={onResize}
onMouseLeave={onResizeEnd}
onMouseUp={onResizeEnd}>
<div ref={wrapperElementRef} className={styles.Components}>
<Fragment>
<div ref={resizeElementRef} className={styles.TreeWrapper}>
<Tree />
</div>
<div className={styles.ResizeBarWrapper}>
<div onMouseDown={onResizeStart} className={styles.ResizeBar} />
<div
onPointerDown={onResizeStart}
onPointerMove={onResize}
onPointerUp={onResizeEnd}
className={styles.ResizeBar}
/>
</div>
<div className={styles.InspectedElementWrapper}>
<NativeStyleContextController>
@@ -193,18 +190,12 @@ function initResizeState(): ResizeState {
return {
horizontalPercentage,
isResizing: false,
verticalPercentage,
};
}
function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState {
switch (action.type) {
case 'ACTION_SET_IS_RESIZING':
return {
...state,
isResizing: action.payload,
};
case 'ACTION_SET_HORIZONTAL_PERCENTAGE':
return {
...state,
@@ -20,7 +20,7 @@ import {getMetaValueLabel, serializeHooksForCopy} from '../utils';
import Store from '../../store';
import styles from './InspectedElementHooksTree.css';
import {meta} from '../../../hydration';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import isArray from 'react-devtools-shared/src/isArray';
@@ -75,11 +75,25 @@
color: var(--color-expand-collapse-toggle);
}
.CollapsableHeaderTitle {
flex: 1 1 auto;
.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
text-align: left;
white-space: nowrap;
}
.CollapsableHeaderTitle {
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}
.CollapsableHeaderSeparator {
flex: 0 0 auto;
white-space: pre;
}
.CollapsableHeaderFiller {
flex: 1 0 0;
}
.CollapsableContent {
@@ -108,4 +122,4 @@
.TimeBarSpanErrored {
background-color: var(--color-timespan-background-errored);
}
}
@@ -38,6 +38,37 @@ type RowProps = {
maxTime: number,
};
function getShortDescription(name: string, description: string): string {
const descMaxLength = 30 - name.length;
if (descMaxLength > 1) {
const l = description.length;
if (l > 0 && l <= descMaxLength) {
// We can fit the full description
return description;
} else if (
description.startsWith('http://') ||
description.startsWith('https://') ||
description.startsWith('/')
) {
// Looks like a URL. Let's see if we can extract something shorter.
// We don't have to do a full parse so let's try something cheaper.
let queryIdx = description.indexOf('?');
if (queryIdx === -1) {
queryIdx = description.length;
}
if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) {
// Ends with slash. Look before that.
queryIdx--;
}
const slashIdx = description.lastIndexOf('/', queryIdx - 1);
// This may now be either the file name or the host.
// Include the slash to make it more obvious what we trimmed.
return '…' + description.slice(slashIdx, queryIdx);
}
}
return '';
}
function SuspendedByRow({
bridge,
element,
@@ -50,6 +81,9 @@ function SuspendedByRow({
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const name = asyncInfo.awaited.name;
const description = asyncInfo.awaited.description;
const longName = description === '' ? name : name + ' (' + description + ')';
const shortDescription = getShortDescription(name, description);
let stack;
let owner;
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
@@ -83,12 +117,22 @@ function SuspendedByRow({
<Button
className={styles.CollapsableHeader}
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
title={name + ' — ' + (end - start).toFixed(2) + ' ms'}>
title={longName + ' — ' + (end - start).toFixed(2) + ' ms'}>
<ButtonIcon
className={styles.CollapsableHeaderIcon}
type={isOpen ? 'expanded' : 'collapsed'}
/>
<span className={styles.CollapsableHeaderTitle}>{name}</span>
{shortDescription === '' ? null : (
<>
<span className={styles.CollapsableHeaderSeparator}>{' ('}</span>
<span className={styles.CollapsableHeaderTitle}>
{shortDescription}
</span>
<span className={styles.CollapsableHeaderSeparator}>{') '}</span>
</>
)}
<div className={styles.CollapsableHeaderFiller} />
<div className={styles.TimeBarContainer}>
<div
className={
@@ -38,6 +38,7 @@
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
line-height: var(--line-height-data);
user-select: none;
}
.VRule {
@@ -5,6 +5,8 @@
flex-direction: column;
background-color: var(--color-background);
color: var(--color-text);
container-name: devtools;
container-type: inline-size;
}
.TabBar {
@@ -7,54 +7,46 @@
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
import type {
Thenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
import type {GitHubIssue} from './githubAPI';
import * as React from 'react';
import {unstable_getCacheForType as getCacheForType} from 'react';
import {searchGitHubIssues} from './githubAPI';
const API_TIMEOUT = 3000;
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingRecord = {
status: 0,
value: Wakeable,
};
type ResolvedRecord<T> = {
status: 1,
value: T,
};
type RejectedRecord = {
status: 2,
value: null,
};
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
if (record.status === Resolved) {
// This is just a type refinement.
return record;
} else if (record.status === Rejected) {
// This is just a type refinement.
return record;
function readRecord<T>(record: Thenable<T>): T | null {
if (typeof React.use === 'function') {
try {
return React.use(record);
} catch (x) {
if (x === null) {
return null;
}
throw x;
}
}
if (record.status === 'fulfilled') {
return record.value;
} else if (record.status === 'rejected') {
return null;
} else {
throw record.value;
throw record;
}
}
type GitHubIssueMap = Map<string, Record<GitHubIssue>>;
type GitHubIssueMap = Map<string, Thenable<GitHubIssue>>;
function createMap(): GitHubIssueMap {
return new Map();
}
function getRecordMap(): Map<string, Record<GitHubIssue>> {
function getRecordMap(): Map<string, Thenable<GitHubIssue>> {
return getCacheForType(createMap);
}
@@ -65,10 +57,15 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
let record = map.get(errorMessage);
if (!record) {
const callbacks = new Set<() => mixed>();
const wakeable: Wakeable = {
then(callback: () => mixed) {
const callbacks = new Set<(value: any) => mixed>();
const rejectCallbacks = new Set<(reason: mixed) => mixed>();
const thenable: Thenable<GitHubIssue> = {
status: 'pending',
value: null,
reason: null,
then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
callbacks.add(callback);
rejectCallbacks.add(reject);
},
// Optional property used by Timeline:
@@ -76,13 +73,17 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
};
const wake = () => {
// This assumes they won't throw.
callbacks.forEach(callback => callback());
callbacks.forEach(callback => callback((thenable: any).value));
callbacks.clear();
rejectCallbacks.clear();
};
const wakeRejections = () => {
// This assumes they won't throw.
rejectCallbacks.forEach(callback => callback((thenable: any).reason));
rejectCallbacks.clear();
callbacks.clear();
};
const newRecord: Record<GitHubIssue> = (record = {
status: Pending,
value: wakeable,
});
record = thenable;
let didTimeout = false;
@@ -93,41 +94,40 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
}
if (maybeItem) {
const resolvedRecord =
((newRecord: any): ResolvedRecord<GitHubIssue>);
resolvedRecord.status = Resolved;
resolvedRecord.value = maybeItem;
const fulfilledThenable: FulfilledThenable<GitHubIssue> =
(thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = maybeItem;
wake();
} else {
const notFoundRecord = ((newRecord: any): RejectedRecord);
notFoundRecord.status = Rejected;
notFoundRecord.value = null;
const notFoundThenable: RejectedThenable<GitHubIssue> =
(thenable: any);
notFoundThenable.status = 'rejected';
notFoundThenable.reason = null;
wakeRejections();
}
wake();
})
.catch(error => {
const thrownRecord = ((newRecord: any): RejectedRecord);
thrownRecord.status = Rejected;
thrownRecord.value = null;
wake();
const rejectedThenable: RejectedThenable<GitHubIssue> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = null;
wakeRejections();
});
// Only wait a little while for GitHub results before showing a fallback.
setTimeout(() => {
didTimeout = true;
const timedoutRecord = ((newRecord: any): RejectedRecord);
timedoutRecord.status = Rejected;
timedoutRecord.value = null;
wake();
const timedoutThenable: RejectedThenable<GitHubIssue> = (thenable: any);
timedoutThenable.status = 'rejected';
timedoutThenable.reason = null;
wakeRejections();
}, API_TIMEOUT);
map.set(errorMessage, record);
}
const response = readRecord(record).value;
const response = readRecord(record);
return response;
}
@@ -21,10 +21,8 @@ import ButtonIcon from '../ButtonIcon';
import {InspectedElementContext} from '../Components/InspectedElementContext';
import {StoreContext} from '../context';
import {
getAlreadyLoadedHookNames,
getHookSourceLocationKey,
} from 'react-devtools-shared/src/hookNamesCache';
import {getAlreadyLoadedHookNames} from 'react-devtools-shared/src/hookNamesCache';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
import Toggle from '../Toggle';
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
import type {ChangeDescription} from './types';
@@ -36,7 +36,12 @@ export default function portaledContent(
<ThemeProvider>
<div
data-react-devtools-portal-root={true}
style={{width: '100vw', height: '100vh'}}>
style={{
width: '100vw',
height: '100vh',
containerName: 'devtools',
containerType: 'inline-size',
}}>
{children}
</div>
</ThemeProvider>
+56 -50
View File
@@ -9,31 +9,16 @@
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import type {Thenable, Wakeable} from 'shared/ReactTypes';
import type {
Thenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
import * as React from 'react';
const TIMEOUT = 30000;
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingRecord = {
status: 0,
value: Wakeable,
};
type ResolvedRecord<T> = {
status: 1,
value: T,
};
type RejectedRecord = {
status: 2,
value: null,
};
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
type Module = any;
type ModuleLoaderFunction = () => Thenable<Module>;
@@ -42,16 +27,23 @@ type ModuleLoaderFunction = () => Thenable<Module>;
// Modules are static anyway.
const moduleLoaderFunctionToModuleMap: Map<ModuleLoaderFunction, Module> =
new Map();
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
if (record.status === Resolved) {
// This is just a type refinement.
return record;
} else if (record.status === Rejected) {
// This is just a type refinement.
return record;
function readRecord<T>(record: Thenable<T>): T | null {
if (typeof React.use === 'function') {
try {
return React.use(record);
} catch (x) {
if (x === null) {
return null;
}
throw x;
}
}
if (record.status === 'fulfilled') {
return record.value;
} else if (record.status === 'rejected') {
return null;
} else {
throw record.value;
throw record;
}
}
@@ -66,10 +58,15 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
}
if (!record) {
const callbacks = new Set<() => mixed>();
const wakeable: Wakeable = {
then(callback: () => mixed) {
const callbacks = new Set<(value: any) => mixed>();
const rejectCallbacks = new Set<(reason: mixed) => mixed>();
const thenable: Thenable<Module> = {
status: 'pending',
value: null,
reason: null,
then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
callbacks.add(callback);
rejectCallbacks.add(reject);
},
// Optional property used by Timeline:
@@ -85,12 +82,21 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
// This assumes they won't throw.
callbacks.forEach(callback => callback());
callbacks.clear();
rejectCallbacks.clear();
};
const wakeRejections = () => {
if (timeoutID) {
clearTimeout(timeoutID);
timeoutID = null;
}
// This assumes they won't throw.
rejectCallbacks.forEach(callback => callback((thenable: any).reason));
rejectCallbacks.clear();
callbacks.clear();
};
const newRecord: Record<Module> = (record = {
status: Pending,
value: wakeable,
});
record = thenable;
let didTimeout = false;
@@ -106,9 +112,9 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
return;
}
const resolvedRecord = ((newRecord: any): ResolvedRecord<Module>);
resolvedRecord.status = Resolved;
resolvedRecord.value = module;
const fulfilledThenable: FulfilledThenable<Module> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = module;
wake();
},
@@ -125,11 +131,11 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
console.log(error);
const thrownRecord = ((newRecord: any): RejectedRecord);
thrownRecord.status = Rejected;
thrownRecord.value = null;
const rejectedThenable: RejectedThenable<Module> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
wake();
wakeRejections();
},
);
@@ -145,17 +151,17 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
didTimeout = true;
const timedoutRecord = ((newRecord: any): RejectedRecord);
timedoutRecord.status = Rejected;
timedoutRecord.value = null;
const rejectedThenable: RejectedThenable<Module> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = null;
wake();
wakeRejections();
}, TIMEOUT);
moduleLoaderFunctionToModuleMap.set(moduleLoaderFunction, record);
}
// $FlowFixMe[underconstrained-implicit-instantiation]
const response = readRecord(record).value;
const response = readRecord(record);
return response;
}
+1
View File
@@ -187,6 +187,7 @@ export type Element = {
// Serialized version of ReactIOInfo
export type SerializedIOInfo = {
name: string,
description: string,
start: number,
end: number,
value: null | Promise<mixed>,
+74 -76
View File
@@ -10,49 +10,40 @@
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
import type {Thenable, Wakeable} from 'shared/ReactTypes';
import type {
Thenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
import type {
Element,
HookNames,
HookSourceLocationKey,
} from 'react-devtools-shared/src/frontend/types';
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
import * as React from 'react';
import {withCallbackPerfMeasurements} from './PerformanceLoggingUtils';
import {logEvent} from './Logger';
const TIMEOUT = 30000;
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingRecord = {
status: 0,
value: Wakeable,
};
type ResolvedRecord<T> = {
status: 1,
value: T,
};
type RejectedRecord = {
status: 2,
value: null,
};
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
if (record.status === Resolved) {
// This is just a type refinement.
return record;
} else if (record.status === Rejected) {
// This is just a type refinement.
return record;
function readRecord<T>(record: Thenable<T>): T | null {
if (typeof React.use === 'function') {
try {
return React.use(record);
} catch (x) {
if (record.status === 'rejected') {
return null;
}
throw x;
}
}
if (record.status === 'fulfilled') {
return record.value;
} else if (record.status === 'rejected') {
return null;
} else {
throw record.value;
throw record;
}
}
@@ -65,16 +56,16 @@ type LoadHookNamesFunction = (
// Otherwise, refreshing the inspected element cache would also clear this cache.
// TODO Rethink this if the React API constraints change.
// See https://github.com/reactwg/react-18/discussions/25#discussioncomment-980435
let map: WeakMap<Element, Record<HookNames>> = new WeakMap();
let map: WeakMap<Element, Thenable<HookNames>> = new WeakMap();
export function hasAlreadyLoadedHookNames(element: Element): boolean {
const record = map.get(element);
return record != null && record.status === Resolved;
return record != null && record.status === 'fulfilled';
}
export function getAlreadyLoadedHookNames(element: Element): HookNames | null {
const record = map.get(element);
if (record != null && record.status === Resolved) {
if (record != null && record.status === 'fulfilled') {
return record.value;
}
return null;
@@ -95,10 +86,15 @@ export function loadHookNames(
}
if (!record) {
const callbacks = new Set<() => mixed>();
const wakeable: Wakeable = {
then(callback: () => mixed) {
const callbacks = new Set<(value: any) => mixed>();
const rejectCallbacks = new Set<(reason: mixed) => mixed>();
const thenable: Thenable<HookNames> = {
status: 'pending',
value: null,
reason: null,
then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
callbacks.add(callback);
rejectCallbacks.add(reject);
},
// Optional property used by Timeline:
@@ -117,7 +113,18 @@ export function loadHookNames(
}
// This assumes they won't throw.
callbacks.forEach(callback => callback());
callbacks.forEach(callback => callback((thenable: any).value));
callbacks.clear();
rejectCallbacks.clear();
};
const wakeRejections = () => {
if (timeoutID) {
clearTimeout(timeoutID);
timeoutID = null;
}
// This assumes they won't throw.
rejectCallbacks.forEach(callback => callback((thenable: any).reason));
rejectCallbacks.clear();
callbacks.clear();
};
@@ -132,10 +139,7 @@ export function loadHookNames(
});
};
const newRecord: Record<HookNames> = (record = {
status: Pending,
value: wakeable,
});
record = thenable;
withCallbackPerfMeasurements(
'loadHookNames',
@@ -151,20 +155,24 @@ export function loadHookNames(
}
if (hookNames) {
const resolvedRecord =
((newRecord: any): ResolvedRecord<HookNames>);
resolvedRecord.status = Resolved;
resolvedRecord.value = hookNames;
const fulfilledThenable: FulfilledThenable<HookNames> =
(thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = hookNames;
status = 'success';
resolvedHookNames = hookNames;
done();
wake();
} else {
const notFoundRecord = ((newRecord: any): RejectedRecord);
notFoundRecord.status = Rejected;
notFoundRecord.value = null;
const notFoundThenable: RejectedThenable<HookNames> =
(thenable: any);
notFoundThenable.status = 'rejected';
notFoundThenable.reason = null;
status = 'error';
resolvedHookNames = hookNames;
done();
wakeRejections();
}
status = 'success';
resolvedHookNames = hookNames;
done();
wake();
},
function onError(error) {
if (didTimeout) {
@@ -177,13 +185,14 @@ export function loadHookNames(
console.error(error);
const thrownRecord = ((newRecord: any): RejectedRecord);
thrownRecord.status = Rejected;
thrownRecord.value = null;
const rejectedThenable: RejectedThenable<HookNames> =
(thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = null;
status = 'error';
done();
wake();
wakeRejections();
},
);
@@ -197,13 +206,13 @@ export function loadHookNames(
didTimeout = true;
const timedoutRecord = ((newRecord: any): RejectedRecord);
timedoutRecord.status = Rejected;
timedoutRecord.value = null;
const timedoutThenable: RejectedThenable<HookNames> = (thenable: any);
timedoutThenable.status = 'rejected';
timedoutThenable.reason = null;
status = 'timeout';
done();
wake();
wakeRejections();
}, TIMEOUT);
},
handleLoadComplete,
@@ -211,21 +220,10 @@ export function loadHookNames(
map.set(element, record);
}
const response = readRecord(record).value;
const response = readRecord(record);
return response;
}
export function getHookSourceLocationKey({
fileName,
lineNumber,
columnNumber,
}: HookSource): HookSourceLocationKey {
if (fileName == null || lineNumber == null || columnNumber == null) {
throw Error('Hook source code location not found.');
}
return `${fileName}:${lineNumber}:${columnNumber}`;
}
export function clearHookNamesCache(): void {
map = new WeakMap();
}
@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {HookSourceLocationKey} from 'react-devtools-shared/src/frontend/types';
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
export function getHookSourceLocationKey({
fileName,
lineNumber,
columnNumber,
}: HookSource): HookSourceLocationKey {
if (fileName == null || lineNumber == null || columnNumber == null) {
throw Error('Hook source code location not found.');
}
return `${fileName}:${lineNumber}:${columnNumber}`;
}
@@ -12,6 +12,8 @@ import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
import type {HookNames} from 'react-devtools-shared/src/frontend/types';
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
import 'react';
import {withAsyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
import WorkerizedParseSourceAndMetadata from './parseSourceAndMetadata.worker';
import typeof * as ParseSourceAndMetadataModule from './parseSourceAndMetadata';
@@ -46,7 +46,7 @@
// and there is no need to convert runtime code to the original source.
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
import {sourceMapIncludesSource} from '../SourceMapUtils';
import {
withAsyncPerfMeasurements,
@@ -15,7 +15,7 @@ import LRU from 'lru-cache';
import {getHookName} from '../astUtils';
import {areSourceMapsAppliedToErrors} from '../ErrorTester';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
import {SourceMapMetadataConsumer} from '../SourceMapMetadataConsumer';
import {
withAsyncPerfMeasurements,
+55 -53
View File
@@ -7,6 +7,8 @@
* @flow
*/
import * as React from 'react';
import {
unstable_getCacheForType as getCacheForType,
startTransition,
@@ -16,7 +18,11 @@ import {inspectElement as inspectElementMutableSource} from 'react-devtools-shar
import ElementPollingCancellationError from 'react-devtools-shared/src//errors/ElementPollingCancellationError';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {Wakeable} from 'shared/ReactTypes';
import type {
Thenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
import type {
Element,
InspectedElement as InspectedElementFrontend,
@@ -24,44 +30,27 @@ import type {
InspectedElementPath,
} from 'react-devtools-shared/src/frontend/types';
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingRecord = {
status: 0,
value: Wakeable,
};
type ResolvedRecord<T> = {
status: 1,
value: T,
};
type RejectedRecord = {
status: 2,
value: Error | string,
};
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
if (record.status === Resolved) {
// This is just a type refinement.
return record;
function readRecord<T>(record: Thenable<T>): T {
if (typeof React.use === 'function') {
return React.use(record);
}
if (record.status === 'fulfilled') {
return record.value;
} else if (record.status === 'rejected') {
throw record.reason;
} else {
throw record.value;
throw record;
}
}
type InspectedElementMap = WeakMap<Element, Record<InspectedElementFrontend>>;
type InspectedElementMap = WeakMap<Element, Thenable<InspectedElementFrontend>>;
type CacheSeedKey = () => InspectedElementMap;
function createMap(): InspectedElementMap {
return new WeakMap();
}
function getRecordMap(): WeakMap<Element, Record<InspectedElementFrontend>> {
function getRecordMap(): WeakMap<Element, Thenable<InspectedElementFrontend>> {
return getCacheForType(createMap);
}
@@ -69,12 +58,15 @@ function createCacheSeed(
element: Element,
inspectedElement: InspectedElementFrontend,
): [CacheSeedKey, InspectedElementMap] {
const newRecord: Record<InspectedElementFrontend> = {
status: Resolved,
const thenable: FulfilledThenable<InspectedElementFrontend> = {
then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
callback(thenable.value);
},
status: 'fulfilled',
value: inspectedElement,
};
const map = createMap();
map.set(element, newRecord);
map.set(element, thenable);
return [createMap, map];
}
@@ -91,10 +83,15 @@ export function inspectElement(
const map = getRecordMap();
let record = map.get(element);
if (!record) {
const callbacks = new Set<() => mixed>();
const wakeable: Wakeable = {
then(callback: () => mixed) {
const callbacks = new Set<(value: any) => mixed>();
const rejectCallbacks = new Set<(reason: mixed) => mixed>();
const thenable: Thenable<InspectedElementFrontend> = {
status: 'pending',
value: null,
reason: null,
then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
callbacks.add(callback);
rejectCallbacks.add(reject);
},
// Optional property used by Timeline:
@@ -103,19 +100,24 @@ export function inspectElement(
const wake = () => {
// This assumes they won't throw.
callbacks.forEach(callback => callback());
callbacks.forEach(callback => callback((thenable: any).value));
callbacks.clear();
rejectCallbacks.clear();
};
const wakeRejections = () => {
// This assumes they won't throw.
rejectCallbacks.forEach(callback => callback((thenable: any).reason));
rejectCallbacks.clear();
callbacks.clear();
};
const newRecord: Record<InspectedElementFrontend> = (record = {
status: Pending,
value: wakeable,
});
record = thenable;
const rendererID = store.getRendererIDForElement(element.id);
if (rendererID == null) {
const rejectedRecord = ((newRecord: any): RejectedRecord);
rejectedRecord.status = Rejected;
rejectedRecord.value = new Error(
const rejectedThenable: RejectedThenable<InspectedElementFrontend> =
(thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = new Error(
`Could not inspect element with id "${element.id}". No renderer found.`,
);
@@ -129,29 +131,29 @@ export function inspectElement(
InspectedElementFrontend,
InspectedElementResponseType,
]) => {
const resolvedRecord =
((newRecord: any): ResolvedRecord<InspectedElementFrontend>);
resolvedRecord.status = Resolved;
resolvedRecord.value = inspectedElement;
const fulfilledThenable: FulfilledThenable<InspectedElementFrontend> =
(thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = inspectedElement;
wake();
},
error => {
console.error(error);
const rejectedRecord = ((newRecord: any): RejectedRecord);
rejectedRecord.status = Rejected;
rejectedRecord.value = error;
const rejectedThenable: RejectedThenable<InspectedElementFrontend> =
(thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
wake();
wakeRejections();
},
);
map.set(element, record);
}
const response = readRecord(record).value;
const response = readRecord(record);
return response;
}
+25 -1
View File
@@ -47,7 +47,31 @@ const E2E_APP_BUILD_DIR = process.env.REACT_VERSION
const makeConfig = (entry, alias) => ({
mode: __DEV__ ? 'development' : 'production',
devtool: __DEV__ ? 'cheap-source-map' : 'source-map',
stats: 'normal',
stats: {
preset: 'normal',
warningsFilter: [
warning => {
const message = warning.message;
// We use ReactDOM legacy APIs conditionally based on the React version.
// react-native-web also accesses legacy APIs statically but we don't end
// up using them at runtime.
return (
message.startsWith(
`export 'findDOMNode' (imported as 'findDOMNode') was not found in 'react-dom'`,
) ||
message.startsWith(
`export 'hydrate' (reexported as 'hydrate') was not found in 'react-dom'`,
) ||
message.startsWith(
`export 'render' (imported as 'render') was not found in 'react-dom'`,
) ||
message.startsWith(
`export 'unmountComponentAtNode' (imported as 'unmountComponentAtNode') was not found in 'react-dom'`,
)
);
},
],
},
entry,
output: {
publicPath: '/dist/',
+55 -49
View File
@@ -7,46 +7,42 @@
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
import type {
Thenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
import type {TimelineData} from './types';
import * as React from 'react';
import {importFile as importFileWorker} from './import-worker';
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingRecord = {
status: 0,
value: Wakeable,
};
type ResolvedRecord<T> = {
status: 1,
value: T,
};
type RejectedRecord = {
status: 2,
value: Error,
};
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
// This is intentionally a module-level Map, rather than a React-managed one.
// Otherwise, refreshing the inspected element cache would also clear this cache.
// Profiler file contents are static anyway.
const fileNameToProfilerDataMap: Map<string, Record<TimelineData>> = new Map();
const fileNameToProfilerDataMap: Map<
string,
Thenable<TimelineData>,
> = new Map();
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
if (record.status === Resolved) {
// This is just a type refinement.
return record;
} else if (record.status === Rejected) {
// This is just a type refinement.
return record;
function readRecord<T>(record: Thenable<T>): T | Error {
if (typeof React.use === 'function') {
try {
return React.use(record);
} catch (x) {
if (record.status === 'rejected') {
return (record.reason: any);
}
throw x;
}
}
if (record.status === 'fulfilled') {
return record.value;
} else if (record.status === 'rejected') {
return (record.reason: any);
} else {
throw record.value;
throw record;
}
}
@@ -55,10 +51,15 @@ export function importFile(file: File): TimelineData | Error {
let record = fileNameToProfilerDataMap.get(fileName);
if (!record) {
const callbacks = new Set<() => mixed>();
const wakeable: Wakeable = {
then(callback: () => mixed) {
const callbacks = new Set<(value: any) => mixed>();
const rejectCallbacks = new Set<(reason: mixed) => mixed>();
const thenable: Thenable<TimelineData> = {
status: 'pending',
value: null,
reason: null,
then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
callbacks.add(callback);
rejectCallbacks.add(reject);
},
// Optional property used by Timeline:
@@ -67,37 +68,42 @@ export function importFile(file: File): TimelineData | Error {
const wake = () => {
// This assumes they won't throw.
callbacks.forEach(callback => callback());
callbacks.forEach(callback => callback((thenable: any).value));
callbacks.clear();
rejectCallbacks.clear();
};
const wakeRejections = () => {
// This assumes they won't throw.
rejectCallbacks.forEach(callback => callback((thenable: any).reason));
rejectCallbacks.clear();
callbacks.clear();
};
const newRecord: Record<TimelineData> = (record = {
status: Pending,
value: wakeable,
});
record = thenable;
importFileWorker(file).then(data => {
switch (data.status) {
case 'SUCCESS':
const resolvedRecord =
((newRecord: any): ResolvedRecord<TimelineData>);
resolvedRecord.status = Resolved;
resolvedRecord.value = data.processedData;
const fulfilledThenable: FulfilledThenable<TimelineData> =
(thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = data.processedData;
wake();
break;
case 'INVALID_PROFILE_ERROR':
case 'UNEXPECTED_ERROR':
const thrownRecord = ((newRecord: any): RejectedRecord);
thrownRecord.status = Rejected;
thrownRecord.value = data.error;
const rejectedThenable: RejectedThenable<TimelineData> =
(thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = data.error;
wakeRejections();
break;
}
wake();
});
fileNameToProfilerDataMap.set(fileName, record);
}
const response = readRecord(record).value;
const response = readRecord(record);
return response;
}
+6 -50
View File
@@ -68,9 +68,9 @@ import {
SuspenseActionException,
createThenableState,
trackUsedThenable,
resolveLazy,
} from './ReactFiberThenable';
import {readContextDuringReconciliation} from './ReactFiberNewContext';
import {callLazyInitInDEV} from './ReactFiberCallUserSpace';
import {runWithFiberInDEV} from './ReactCurrentFiber';
@@ -364,15 +364,6 @@ function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
}
}
function resolveLazy(lazyType: any) {
if (__DEV__) {
return callLazyInitInDEV(lazyType);
}
const payload = lazyType._payload;
const init = lazyType._init;
return init(payload);
}
type ChildReconciler = (
returnFiber: Fiber,
currentFirstChild: Fiber | null,
@@ -698,14 +689,7 @@ function createChildReconciler(
}
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((newChild: any));
const created = createChild(returnFiber, resolvedChild, lanes);
currentDebugInfo = prevDebugInfo;
return created;
@@ -830,14 +814,7 @@ function createChildReconciler(
}
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((newChild: any));
const updated = updateSlot(
returnFiber,
oldFiber,
@@ -962,14 +939,7 @@ function createChildReconciler(
}
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((newChild: any));
const updated = updateFromMap(
existingChildren,
returnFiber,
@@ -1086,14 +1056,7 @@ function createChildReconciler(
});
break;
case REACT_LAZY_TYPE: {
let resolvedChild;
if (__DEV__) {
resolvedChild = callLazyInitInDEV((child: any));
} else {
const payload = child._payload;
const init = (child._init: any);
resolvedChild = init(payload);
}
const resolvedChild = resolveLazy((child: any));
warnOnInvalidKey(
returnFiber,
workInProgress,
@@ -1809,14 +1772,7 @@ function createChildReconciler(
);
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
let result;
if (__DEV__) {
result = callLazyInitInDEV(newChild);
} else {
const payload = newChild._payload;
const init = newChild._init;
result = init(payload);
}
const result = resolveLazy((newChild: any));
const firstChild = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
+3 -13
View File
@@ -302,11 +302,8 @@ import {
pushRootMarkerInstance,
TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent';
import {
callLazyInitInDEV,
callComponentInDEV,
callRenderInDEV,
} from './ReactFiberCallUserSpace';
import {callComponentInDEV, callRenderInDEV} from './ReactFiberCallUserSpace';
import {resolveLazy} from './ReactFiberThenable';
// A special exception that's used to unwind the stack when an update flows
// into a dehydrated boundary.
@@ -2020,14 +2017,7 @@ function mountLazyComponent(
const props = workInProgress.pendingProps;
const lazyComponent: LazyComponentType<any, any> = elementType;
let Component;
if (__DEV__) {
Component = callLazyInitInDEV(lazyComponent);
} else {
const payload = lazyComponent._payload;
const init = lazyComponent._init;
Component = init(payload);
}
let Component = resolveLazy(lazyComponent);
// Store the unwrapped component in the type.
workInProgress.type = Component;
+25
View File
@@ -14,6 +14,10 @@ import type {
RejectedThenable,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import {callLazyInitInDEV} from './ReactFiberCallUserSpace';
import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -260,6 +264,27 @@ export function suspendCommit(): void {
throw SuspenseyCommitException;
}
export function resolveLazy<T>(lazyType: LazyComponentType<T, any>): T {
try {
if (__DEV__) {
return callLazyInitInDEV(lazyType);
}
const payload = lazyType._payload;
const init = lazyType._init;
return init(payload);
} catch (x) {
if (x !== null && typeof x === 'object' && typeof x.then === 'function') {
// This lazy Suspended. Treat this as if we called use() to unwrap it.
suspendedThenable = x;
if (__DEV__) {
needsToResetSuspendedThenableDEV = true;
}
throw SuspenseException;
}
throw x;
}
}
// This is used to track the actual thenable that suspended so it can be
// passed to the rest of the Suspense implementation — which, for historical
// reasons, expects to receive a thenable.
@@ -198,10 +198,7 @@ describe('ReactLazy', () => {
await resolveFakeImport(Foo);
await waitForAll([
'Foo',
...(gate('alwaysThrottleRetries') ? [] : ['Foo']),
]);
await waitForAll(['Foo']);
expect(root).not.toMatchRenderedOutput('FooBar');
await act(() => resolveFakeImport(Bar));
@@ -1329,11 +1326,7 @@ describe('ReactLazy', () => {
expect(ref.current).toBe(null);
await act(() => resolveFakeImport(Foo));
assertLog([
'Foo',
// pre-warming
'Foo',
]);
assertLog(['Foo']);
await act(() => resolveFakeImport(ForwardRefBar));
assertLog(['Foo', 'forwardRef', 'Bar']);
@@ -1493,11 +1486,7 @@ describe('ReactLazy', () => {
expect(root).not.toMatchRenderedOutput('AB');
await act(() => resolveFakeImport(ChildA));
assertLog([
'A',
// pre-warming
'A',
]);
assertLog(['A']);
await act(() => resolveFakeImport(ChildB));
assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export function getIODescription(value: any): string {
if (!__DEV__) {
return '';
}
try {
switch (typeof value) {
case 'object':
// Test the object for a bunch of common property names that are useful identifiers.
// While we only have the return value here, it should ideally be a name that
// describes the arguments requested.
if (value === null) {
return '';
} else if (value instanceof Error) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.message);
} else if (typeof value.url === 'string') {
return value.url;
} else if (typeof value.command === 'string') {
return value.command;
} else if (
typeof value.request === 'object' &&
typeof value.request.url === 'string'
) {
return value.request.url;
} else if (
typeof value.response === 'object' &&
typeof value.response.url === 'string'
) {
return value.response.url;
} else if (
typeof value.id === 'string' ||
typeof value.id === 'number' ||
typeof value.id === 'bigint'
) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.id);
} else if (typeof value.name === 'string') {
return value.name;
} else {
const str = value.toString();
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
// This is probably not a useful description.
return '';
}
return str;
}
case 'string':
if (value.length < 5 || value.length > 500) {
return '';
}
return value;
case 'number':
case 'bigint':
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value);
default:
// Not useful descriptors.
return '';
}
} catch (x) {
return '';
}
}