Skip to content

Commit 3e319a9

Browse files
authored
[DevTools] Apply component filters on initial load (facebook#35587)
1 parent 2c30ebc commit 3e319a9

File tree

15 files changed

+127
-106
lines changed

15 files changed

+127
-106
lines changed

packages/react-devtools-core/README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,30 @@ if (process.env.NODE_ENV !== 'production') {
2525
> **NOTE** that this API (`connectToDevTools`) must be (1) run in the same context as React and (2) must be called before React packages are imported (e.g. `react`, `react-dom`, `react-native`).
2626
2727
### `initialize` arguments
28-
| Argument | Description |
29-
|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
30-
| `settings` | Optional. If not specified, or received as null, then default settings are used. Can be plain object or a Promise that resolves with the [plain settings object](#Settings). If Promise rejects, the console will not be patched and some console features from React DevTools will not work. |
28+
| Argument | Description |
29+
|---------------------------|-------------|
30+
| `settings` | Optional. If not specified, or received as null, then default settings are used. Can be plain object or a Promise that resolves with the [plain settings object](#Settings). If Promise rejects, the console will not be patched and some console features from React DevTools will not work. |
31+
| `shouldStartProfilingNow` | Optional. Whether to start profiling immediately after installing the hook. Defaults to `false`. |
32+
| `profilingSettings` | Optional. Profiling settings used when `shouldStartProfilingNow` is `true`. Defaults to `{ recordChangeDescriptions: false, recordTimeline: false }`. |
33+
| `componentFilters` | Optional. Array or Promise that resolves to an array of component filters to apply before DevTools connects. Defaults to the built-in host component filter. See [Component filters](#component-filters) for the full spec. |
3134

3235
#### `Settings`
3336
| Spec | Default value |
3437
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
3538
| <pre>{<br> appendComponentStack: boolean,<br> breakOnConsoleErrors: boolean,<br> showInlineWarningsAndErrors: boolean,<br> hideConsoleLogsInStrictMode: boolean,<br> disableSecondConsoleLogDimmingInStrictMode: boolean<br>}</pre> | <pre>{<br> appendComponentStack: true,<br> breakOnConsoleErrors: false,<br> showInlineWarningsAndErrors: true,<br> hideConsoleLogsInStrictMode: false,<br> disableSecondConsoleLogDimmingInStrictMode: false<br>}</pre> |
3639

40+
#### Component filters
41+
Each filter object must include `type` and `isEnabled`. Some filters also require `value` or `isValid`.
42+
43+
| Type | Required fields | Description |
44+
|------|-----------------|-------------|
45+
| `ComponentFilterElementType` (`1`) | `type`, `isEnabled`, `value: ElementType` | Hides elements of the given element type. DevTools defaults to hiding host components. |
46+
| `ComponentFilterDisplayName` (`2`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose display name matches the provided RegExp string. |
47+
| `ComponentFilterLocation` (`3`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose source location matches the provided RegExp string. |
48+
| `ComponentFilterHOC` (`4`) | `type`, `isEnabled`, `isValid` | Hides higher-order components. |
49+
| `ComponentFilterEnvironmentName` (`5`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose environment name matches the provided string. |
50+
| `ComponentFilterActivitySlice` (`6`) | `type`, `isEnabled`, `isValid`, `activityID`, `rendererID` | Filters activity slices; usually managed by DevTools rather than user code. |
51+
3752
### `connectToDevTools` options
3853
| Prop | Default | Description |
3954
|------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------|

packages/react-devtools-core/src/backend.js

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,17 @@ export function initialize(
6666
| Promise<DevToolsHookSettings>,
6767
shouldStartProfilingNow: boolean = false,
6868
profilingSettings?: ProfilingSettings,
69+
maybeComponentFiltersOrComponentFiltersPromise?:
70+
| Array<ComponentFilter>
71+
| Promise<Array<ComponentFilter>>,
6972
) {
73+
const componentFiltersOrComponentFiltersPromise =
74+
maybeComponentFiltersOrComponentFiltersPromise
75+
? maybeComponentFiltersOrComponentFiltersPromise
76+
: savedComponentFilters;
7077
installHook(
7178
window,
79+
componentFiltersOrComponentFiltersPromise,
7280
maybeSettingsOrSettingsPromise,
7381
shouldStartProfilingNow,
7482
profilingSettings,
@@ -174,19 +182,6 @@ export function connectToDevTools(options: ?ConnectOptions) {
174182
},
175183
);
176184

177-
// The renderer interface doesn't read saved component filters directly,
178-
// because they are generally stored in localStorage within the context of the extension.
179-
// Because of this it relies on the extension to pass filters.
180-
// In the case of the standalone DevTools being used with a website,
181-
// saved filters are injected along with the backend script tag so we shouldn't override them here.
182-
// This injection strategy doesn't work for React Native though.
183-
// Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution.
184-
// So for now we just fall back to using the default filters...
185-
if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
186-
// $FlowFixMe[incompatible-use] found when upgrading Flow
187-
bridge.send('overrideComponentFilters', savedComponentFilters);
188-
}
189-
190185
// TODO (npm-packages) Warn if "isBackendStorageAPISupported"
191186
// $FlowFixMe[incompatible-call] found when upgrading Flow
192187
const agent = new Agent(bridge, isProfiling, onReloadAndProfile);
@@ -381,10 +376,6 @@ export function connectWithCustomMessagingProtocol({
381376
},
382377
);
383378

384-
if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
385-
bridge.send('overrideComponentFilters', savedComponentFilters);
386-
}
387-
388379
const agent = new Agent(bridge, isProfiling, onReloadAndProfile);
389380
if (typeof onReloadAndProfileFlagsReset === 'function') {
390381
onReloadAndProfileFlagsReset();

packages/react-devtools-core/src/standalone.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -356,17 +356,12 @@ function startServer(
356356
// because they are generally stored in localStorage within the context of the extension.
357357
// Because of this it relies on the extension to pass filters, so include them wth the response here.
358358
// This will ensure that saved filters are shared across different web pages.
359-
const savedPreferencesString = `
360-
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
361-
getSavedComponentFilters(),
362-
)};`;
359+
const componentFiltersString = JSON.stringify(getSavedComponentFilters());
363360

364361
response.end(
365-
savedPreferencesString +
362+
backendFile.toString() +
366363
'\n;' +
367-
backendFile.toString() +
368-
'\n;' +
369-
'ReactDevToolsBackend.initialize();' +
364+
`ReactDevToolsBackend.initialize(undefined, undefined, undefined, ${componentFiltersString});` +
370365
'\n' +
371366
`ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${
372367
useHttps ? 'true' : 'false'

packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
// This is the only purpose of this script - to send persisted settings to installHook.js content script
66

77
import type {UnknownMessageEvent} from './messages';
8-
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
8+
import type {
9+
DevToolsHookSettings,
10+
DevToolsSettings,
11+
} from 'react-devtools-shared/src/backend/types';
12+
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
913
import {postMessage} from './messages';
1014

15+
import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
16+
1117
async function messageListener(event: UnknownMessageEvent) {
1218
if (event.source !== window) {
1319
return;
1420
}
1521

1622
if (event.data.source === 'react-devtools-hook-installer') {
1723
if (event.data.payload.handshake) {
18-
const settings: Partial<DevToolsHookSettings> =
24+
const settings: Partial<DevToolsSettings> =
1925
await chrome.storage.local.get();
2026
// If storage was empty (first installation), define default settings
2127
const hookSettings: DevToolsHookSettings = {
@@ -41,10 +47,15 @@ async function messageListener(event: UnknownMessageEvent) {
4147
? settings.disableSecondConsoleLogDimmingInStrictMode
4248
: false,
4349
};
50+
const componentFilters: Array<ComponentFilter> = Array.isArray(
51+
settings.componentFilters,
52+
)
53+
? settings.componentFilters
54+
: getDefaultComponentFilters();
4455

4556
postMessage({
46-
source: 'react-devtools-hook-settings-injector',
47-
payload: {settings: hookSettings},
57+
source: 'react-devtools-settings-injector',
58+
payload: {hookSettings, componentFilters},
4859
});
4960

5061
window.removeEventListener('message', messageListener);
@@ -54,6 +65,6 @@ async function messageListener(event: UnknownMessageEvent) {
5465

5566
window.addEventListener('message', messageListener);
5667
postMessage({
57-
source: 'react-devtools-hook-settings-injector',
68+
source: 'react-devtools-settings-injector',
5869
payload: {handshake: true},
5970
});

packages/react-devtools-extensions/src/contentScripts/installHook.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type {UnknownMessageEvent} from './messages';
44
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
5+
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
56

67
import {installHook} from 'react-devtools-shared/src/hook';
78
import {
@@ -11,13 +12,14 @@ import {
1112
import {postMessage} from './messages';
1213

1314
let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void;
15+
let resolveComponentFiltersInjection: (filters: Array<ComponentFilter>) => void;
1416

1517
function messageListener(event: UnknownMessageEvent) {
1618
if (event.source !== window) {
1719
return;
1820
}
1921

20-
if (event.data.source === 'react-devtools-hook-settings-injector') {
22+
if (event.data.source === 'react-devtools-settings-injector') {
2123
const payload = event.data.payload;
2224
// In case handshake message was sent prior to hookSettingsInjector execution
2325
// We can't guarantee order
@@ -26,9 +28,10 @@ function messageListener(event: UnknownMessageEvent) {
2628
source: 'react-devtools-hook-installer',
2729
payload: {handshake: true},
2830
});
29-
} else if (payload.settings) {
31+
} else if (payload.hookSettings) {
3032
window.removeEventListener('message', messageListener);
31-
resolveHookSettingsInjection(payload.settings);
33+
resolveHookSettingsInjection(payload.hookSettings);
34+
resolveComponentFiltersInjection(payload.componentFilters);
3235
}
3336
}
3437
}
@@ -38,6 +41,11 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
3841
const hookSettingsPromise = new Promise<DevToolsHookSettings>(resolve => {
3942
resolveHookSettingsInjection = resolve;
4043
});
44+
const componentFiltersPromise = new Promise<Array<ComponentFilter>>(
45+
resolve => {
46+
resolveComponentFiltersInjection = resolve;
47+
},
48+
);
4149

4250
window.addEventListener('message', messageListener);
4351
postMessage({
@@ -50,6 +58,7 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
5058
// Can't delay hook installation, inject settings lazily
5159
installHook(
5260
window,
61+
componentFiltersPromise,
5362
hookSettingsPromise,
5463
shouldStartProfiling,
5564
profilingSettings,
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @flow */
22

33
import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
4+
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
45

56
export function postMessage(event: UnknownMessageEventData): void {
67
window.postMessage(event);
@@ -10,7 +11,7 @@ export interface UnknownMessageEvent
1011
extends MessageEvent<UnknownMessageEventData> {}
1112

1213
export type UnknownMessageEventData =
13-
| HookSettingsInjectorEventData
14+
| SettingsInjectorEventData
1415
| HookInstallerEventData;
1516

1617
export type HookInstallerEventData = {
@@ -24,19 +25,20 @@ export type HookInstallerEventPayloadHandshake = {
2425
handshake: true,
2526
};
2627

27-
export type HookSettingsInjectorEventData = {
28-
source: 'react-devtools-hook-settings-injector',
29-
payload: HookSettingsInjectorEventPayload,
28+
export type SettingsInjectorEventData = {
29+
source: 'react-devtools-settings-injector',
30+
payload: SettingsInjectorEventPayload,
3031
};
3132

32-
export type HookSettingsInjectorEventPayload =
33-
| HookSettingsInjectorEventPayloadHandshake
34-
| HookSettingsInjectorEventPayloadSettings;
33+
export type SettingsInjectorEventPayload =
34+
| SettingsInjectorEventPayloadHandshake
35+
| SettingsInjectorEventPayloadSettings;
3536

36-
export type HookSettingsInjectorEventPayloadHandshake = {
37+
export type SettingsInjectorEventPayloadHandshake = {
3738
handshake: true,
3839
};
3940

40-
export type HookSettingsInjectorEventPayloadSettings = {
41-
settings: DevToolsHookSettings,
41+
export type SettingsInjectorEventPayloadSettings = {
42+
hookSettings: DevToolsHookSettings,
43+
componentFilters: Array<ComponentFilter>,
4244
};

packages/react-devtools-extensions/src/main/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ function createBridgeAndStore() {
171171
createSuspensePanel();
172172
});
173173

174-
store.addListener('settingsUpdated', settings => {
175-
chrome.storage.local.set(settings);
174+
store.addListener('settingsUpdated', (hookSettings, componentFilters) => {
175+
chrome.storage.local.set({...hookSettings, componentFilters});
176176
});
177177

178178
if (!isProfiling) {

packages/react-devtools-inline/src/backend.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,22 @@ import type {
1010
BackendBridge,
1111
SavedPreferencesParams,
1212
} from 'react-devtools-shared/src/bridge';
13-
import type {Wall} from 'react-devtools-shared/src/frontend/types';
13+
import type {
14+
ComponentFilter,
15+
Wall,
16+
} from 'react-devtools-shared/src/frontend/types';
1417
import {
1518
getIfReloadedAndProfiling,
1619
getIsReloadAndProfileSupported,
1720
onReloadAndProfile,
1821
onReloadAndProfileFlagsReset,
1922
} from 'react-devtools-shared/src/utils';
2023

24+
let resolveComponentFiltersInjection: (filters: Array<ComponentFilter>) => void;
25+
const componentFiltersPromise = new Promise<Array<ComponentFilter>>(resolve => {
26+
resolveComponentFiltersInjection = resolve;
27+
});
28+
2129
function startActivation(contentWindow: any, bridge: BackendBridge) {
2230
const onSavedPreferences = (data: SavedPreferencesParams) => {
2331
// This is the only message we're listening for,
@@ -26,21 +34,13 @@ function startActivation(contentWindow: any, bridge: BackendBridge) {
2634

2735
const {componentFilters} = data;
2836

29-
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
30-
31-
// TRICKY
32-
// The backend entry point may be required in the context of an iframe or the parent window.
33-
// If it's required within the parent window, store the saved values on it as well,
34-
// since the injected renderer interface will read from window.
35-
// Technically we don't need to store them on the contentWindow in this case,
36-
// but it doesn't really hurt anything to store them there too.
37-
if (contentWindow !== window) {
38-
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
39-
}
40-
41-
finishActivation(contentWindow, bridge);
37+
resolveComponentFiltersInjection(componentFilters);
4238
};
4339

40+
componentFiltersPromise.then(
41+
finishActivation.bind(null, contentWindow, bridge),
42+
);
43+
4444
bridge.addListener('savedPreferences', onSavedPreferences);
4545

4646
// The backend may be unable to read saved preferences directly,
@@ -113,5 +113,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge {
113113
}
114114

115115
export function initialize(contentWindow: any): void {
116-
installHook(contentWindow);
116+
installHook(contentWindow, componentFiltersPromise);
117117
}

packages/react-devtools-shared/src/__tests__/setupTests.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,8 @@ beforeEach(() => {
238238

239239
// Initialize filters to a known good state.
240240
setSavedComponentFilters(getDefaultComponentFilters());
241-
global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters();
242241

243-
installHook(global, {
242+
installHook(global, getDefaultComponentFilters(), {
244243
appendComponentStack: true,
245244
breakOnConsoleErrors: false,
246245
showInlineWarningsAndErrors: true,

packages/react-devtools-shared/src/attachRenderer.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
RendererID,
1515
ProfilingSettings,
1616
} from 'react-devtools-shared/src/backend/types';
17+
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
1718

1819
import {attach as attachFlight} from 'react-devtools-shared/src/backend/flight/renderer';
1920
import {attach as attachFiber} from 'react-devtools-shared/src/backend/fiber/renderer';
@@ -32,6 +33,9 @@ export default function attachRenderer(
3233
global: Object,
3334
shouldStartProfilingNow: boolean,
3435
profilingSettings: ProfilingSettings,
36+
componentFiltersOrComponentFiltersPromise:
37+
| Array<ComponentFilter>
38+
| Promise<Array<ComponentFilter>>,
3539
): RendererInterface | void {
3640
// only attach if the renderer is compatible with the current version of the backend
3741
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
@@ -58,6 +62,7 @@ export default function attachRenderer(
5862
global,
5963
shouldStartProfilingNow,
6064
profilingSettings,
65+
componentFiltersOrComponentFiltersPromise,
6166
);
6267
} else if (renderer.ComponentTree) {
6368
// react-dom v15

0 commit comments

Comments
 (0)