Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ module.exports = {
$AsyncIterator: 'readonly',
Iterator: 'readonly',
AsyncIterator: 'readonly',
IntervalID: 'readonly',
IteratorResult: 'readonly',
JSONValue: 'readonly',
JSResourceReference: 'readonly',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const contentScriptsToInject = [
js: ['build/proxy.js'],
matches: ['<all_urls>'],
persistAcrossSessions: true,
runAt: 'document_end',
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.ISOLATED,
},
{
Expand Down
28 changes: 26 additions & 2 deletions packages/react-devtools-extensions/src/background/executeScript.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
/**
* 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
*/
/* global chrome */

export function executeScriptInIsolatedWorld({target, files}) {
export function executeScriptInIsolatedWorld({
target,
files,
}: {
files: any,
target: any,
}): Promise<void> {
return chrome.scripting.executeScript({
target,
files,
world: chrome.scripting.ExecutionWorld.ISOLATED,
});
}

export function executeScriptInMainWorld({target, files}) {
export function executeScriptInMainWorld({
target,
files,
injectImmediately,
}: {
files: any,
target: any,
// It's nice to have this required to make active choices.
injectImmediately: boolean,
}): Promise<void> {
return chrome.scripting.executeScript({
target,
files,
injectImmediately,
world: chrome.scripting.ExecutionWorld.MAIN,
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* global chrome */

import {__DEBUG__} from 'react-devtools-shared/src/constants';
import setExtensionIconAndPopup from './setExtensionIconAndPopup';
import {executeScriptInMainWorld} from './executeScript';

Expand All @@ -25,6 +26,7 @@ export function handleBackendManagerMessage(message, sender) {
payload.versions.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
executeScriptInMainWorld({
injectImmediately: true,
target: {tabId: sender.tab.id},
files: [`/build/react_devtools_backend_${version}.js`],
});
Expand Down Expand Up @@ -79,9 +81,19 @@ export function handleDevToolsPageMessage(message) {
}

executeScriptInMainWorld({
injectImmediately: true,
target: {tabId},
files: ['/build/backendManager.js'],
});
}).then(
() => {
if (__DEBUG__) {
console.log('Successfully injected backend manager');
}
},
reason => {
console.error('Failed to inject backend manager:', reason);
},
);

break;
}
Expand Down
25 changes: 20 additions & 5 deletions packages/react-devtools-extensions/src/contentScripts/proxy.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
/**
* 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
*/
/* global chrome */

'use strict';

window.addEventListener('pageshow', function ({target}) {
function injectProxy({target}: {target: any}) {
// Firefox's behaviour for injecting this content script can be unpredictable
// While navigating the history, some content scripts might not be re-injected and still be alive
if (!window.__REACT_DEVTOOLS_PROXY_INJECTED__) {
Expand All @@ -14,15 +22,19 @@ window.addEventListener('pageshow', function ({target}) {
// The backend waits to install the global hook until notified by the content script.
// In the event of a page reload, the content script might be loaded before the backend manager is injected.
// Because of this we need to poll the backend manager until it has been initialized.
const intervalID = setInterval(() => {
const intervalID: IntervalID = setInterval(() => {
if (backendInitialized) {
clearInterval(intervalID);
} else {
sayHelloToBackendManager();
}
}, 500);
}
});
}

window.addEventListener('pagereveal', injectProxy);
// For backwards compat with browsers not implementing `pagereveal` which is a fairly new event.
window.addEventListener('pageshow', injectProxy);

window.addEventListener('pagehide', function ({target}) {
if (target !== window.document) {
Expand All @@ -45,7 +57,7 @@ function sayHelloToBackendManager() {
);
}

function handleMessageFromDevtools(message) {
function handleMessageFromDevtools(message: any) {
window.postMessage(
{
source: 'react-devtools-content-script',
Expand All @@ -55,7 +67,7 @@ function handleMessageFromDevtools(message) {
);
}

function handleMessageFromPage(event) {
function handleMessageFromPage(event: any) {
if (event.source !== window || !event.data) {
return;
}
Expand All @@ -65,6 +77,7 @@ function handleMessageFromPage(event) {
case 'react-devtools-bridge': {
backendInitialized = true;

// $FlowFixMe[incompatible-use]
port.postMessage(event.data.payload);
break;
}
Expand Down Expand Up @@ -99,6 +112,8 @@ function connectPort() {

window.addEventListener('message', handleMessageFromPage);

// $FlowFixMe[incompatible-use]
port.onMessage.addListener(handleMessageFromDevtools);
// $FlowFixMe[incompatible-use]
port.onDisconnect.addListener(handleDisconnect);
}
28 changes: 28 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,34 @@ describe('Store', () => {
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
`);

await actAsync(() => {
agent.overrideSuspenseMilestone({
rendererID,
rootID,
suspendedSet: [],
});
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Component key="Outside">
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense name="one">
<Component key="Suspense 1 Content">
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense name="three">
<Component key="Suspense 3 Content">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});

it('should display a partially rendered SuspenseList', async () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7519,6 +7519,9 @@ export function attach(
}

// TODO: Allow overriding the timeline for the specified root.
forceFallbackForFibers.forEach(fiber => {
scheduleUpdate(fiber);
});
forceFallbackForFibers.clear();

for (let i = 0; i < suspendedSet.length; ++i) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,26 +126,28 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
}

function handleChange(event: SyntheticEvent) {
const pendingValue = +event.currentTarget.value;
for (let i = 0; i < timeline.length; i++) {
const forceFallback = i > pendingValue;
const suspense = timeline[i];
const elementID = suspense.id;
const rendererID = store.getRendererIDForElement(elementID);
if (rendererID === null) {
// TODO: Handle disconnected elements.
console.warn(
`No renderer ID found for element ${elementID} in suspense timeline.`,
);
} else {
bridge.send('overrideSuspense', {
id: elementID,
rendererID,
forceFallback,
});
}
if (rootID === undefined) {
return;
}
const rendererID = store.getRendererIDForElement(rootID);
if (rendererID === null) {
console.error(
`No renderer ID found for root element ${rootID} in suspense timeline.`,
);
return;
}

const pendingValue = +event.currentTarget.value;
const suspendedSet = timeline
.slice(pendingValue + 1)
.map(suspense => suspense.id);

bridge.send('overrideSuspenseMilestone', {
rendererID,
rootID,
suspendedSet,
});

const suspense = timeline[pendingValue];
const elementID = suspense.id;
highlightHostInstance(elementID);
Expand Down
Loading