mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge 922de88ed6 into sapling-pr-archive-poteto
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
+226
-186
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}) => {
|
||||
|
||||
+78
-70
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
+55
-64
@@ -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,
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+17
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+45
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user