Skip to content

Commit 92a3c9d

Browse files
Brian Vaughnfacebook-github-bot
Brian Vaughn
authored andcommitted
React DevTools v4 integration
Summary: This Diff is being posted for discussion purposes. It will not be ready to land until React DevTools v4 has been published to NPM. Update React Native to be compatible with the [new version 4 React DevTools extension](https://github.com/bvaughn/react-devtools-experimental). **Note that this is a breaking change**, as the version 3 and version 4 backends are **not compatible**. Once this update ships (in React Native) users will be required to update their version of the [`react-devtools` NPM package](https://www.npmjs.com/package/react-devtools). The same will be true for IDEs like Nuclide as well as other developer tools like Flipper and [React Native Debugger](https://github.com/jhen0409/react-native-debugger). Related changes also included in this diff are: * Pass an explicit whitelist of style props for the React Native style editor (to improve developer experience when adding new styles). * Update `YellowBox` console patching to coordinate with DevTools own console patching. * Also improved formatting slightly by not calling `stringifySafe` for strings (since this adds visible quotation marks). Regarding the console patching- component stacks will be appended by default when there's no DevTools frontend open. The frontend will provide an option to turn this behavior off though: {F168852162} React DevTools will detect if the new version is used with an older version of React Native, and offer inline upgrade instructions: {F169306863} **Note that the change to the `RCTEnableTurboModule` will not be included in this Diff**. I've just turned those off temporarily so I can use v8+Chrome for debugging. Reviewed By: rickhanlonii Differential Revision: D15973709 fbshipit-source-id: bb9d83fc829af4693e7a10a622acc95a411a48e4
1 parent 5acb364 commit 92a3c9d

File tree

11 files changed

+227
-110
lines changed

11 files changed

+227
-110
lines changed

Libraries/Core/setUpDeveloperTools.js

+5
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,18 @@ if (__DEV__) {
3939
? devServer.url.replace(/https?:\/\//, '').split(':')[0]
4040
: 'localhost';
4141

42+
const viewConfig = require('../Components/View/ReactNativeViewViewConfig.js');
43+
4244
reactDevTools.connectToDevTools({
4345
isAppActive,
4446
host,
4547
// Read the optional global variable for backward compatibility.
4648
// It was added in https://github.com/facebook/react-native/commit/bf2b435322e89d0aeee8792b1c6e04656c2719a0.
4749
port: window.__REACT_DEVTOOLS_PORT__,
4850
resolveRNStyle: require('../StyleSheet/flattenStyle'),
51+
nativeStyleEditorValidAttributes: Object.keys(
52+
viewConfig.validAttributes.style,
53+
),
4954
});
5055
}
5156

Libraries/Inspector/Inspector.js

+60-46
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@ export type ReactRenderer = {
3030
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3131
const renderers = findRenderers();
3232

33-
// required for devtools to be able to edit react native styles
33+
// Required for React DevTools to view/edit React Native styles in Flipper.
34+
// Flipper doesn't inject these values when initializing DevTools.
3435
hook.resolveRNStyle = require('../StyleSheet/flattenStyle');
36+
const viewConfig = require('../Components/View/ReactNativeViewViewConfig.js');
37+
hook.nativeStyleEditorValidAttributes = Object.keys(
38+
viewConfig.validAttributes.style,
39+
);
3540

3641
function findRenderers(): $ReadOnlyArray<ReactRenderer> {
37-
const allRenderers = Object.keys(hook._renderers).map(
38-
key => hook._renderers[key],
39-
);
42+
const allRenderers = Array.from(hook.renderers.values());
4043
invariant(
4144
allRenderers.length >= 1,
4245
'Expected to find at least one React Native renderer on DevTools hook.',
@@ -78,6 +81,7 @@ class Inspector extends React.Component<
7881
networking: boolean,
7982
},
8083
> {
84+
_hideTimeoutID: TimeoutID | null = null;
8185
_subs: ?Array<() => void>;
8286

8387
constructor(props: Object) {
@@ -97,64 +101,78 @@ class Inspector extends React.Component<
97101
}
98102

99103
componentDidMount() {
100-
hook.on('react-devtools', this.attachToDevtools);
104+
hook.on('react-devtools', this._attachToDevtools);
101105
// if devtools is already started
102106
if (hook.reactDevtoolsAgent) {
103-
this.attachToDevtools(hook.reactDevtoolsAgent);
107+
this._attachToDevtools(hook.reactDevtoolsAgent);
104108
}
105109
}
106110

107111
componentWillUnmount() {
108112
if (this._subs) {
109113
this._subs.map(fn => fn());
110114
}
111-
hook.off('react-devtools', this.attachToDevtools);
115+
hook.off('react-devtools', this._attachToDevtools);
112116
}
113117

114118
UNSAFE_componentWillReceiveProps(newProps: Object) {
115119
this.setState({inspectedViewTag: newProps.inspectedViewTag});
116120
}
117121

118-
attachToDevtools: (agent: any) => void = (agent: Object) => {
119-
let _hideWait = null;
120-
const hlSub = agent.sub('highlight', ({node, name, props}) => {
121-
clearTimeout(_hideWait);
122+
_attachToDevtools = (agent: Object) => {
123+
agent.addListener('hideNativeHighlight', this._onAgentHideNativeHighlight);
124+
agent.addListener('showNativeHighlight', this._onAgentShowNativeHighlight);
125+
agent.addListener('shutdown', this._onAgentShutdown);
122126

123-
if (typeof node !== 'number') {
124-
// Fiber
125-
node = ReactNative.findNodeHandle(node);
126-
}
127+
this.setState({
128+
devtoolsAgent: agent,
129+
});
130+
};
127131

128-
UIManager.measure(node, (x, y, width, height, left, top) => {
129-
this.setState({
130-
hierarchy: [],
131-
inspected: {
132-
frame: {left, top, width, height},
133-
style: props ? props.style : {},
134-
},
135-
});
132+
_onAgentHideNativeHighlight = () => {
133+
if (this.state.inspected === null) {
134+
return;
135+
}
136+
// we wait to actually hide in order to avoid flicker
137+
this._hideTimeoutID = setTimeout(() => {
138+
this.setState({
139+
inspected: null,
140+
});
141+
}, 100);
142+
};
143+
144+
_onAgentShowNativeHighlight = node => {
145+
clearTimeout(this._hideTimeoutID);
146+
147+
if (typeof node !== 'number') {
148+
node = ReactNative.findNodeHandle(node);
149+
}
150+
151+
UIManager.measure(node, (x, y, width, height, left, top) => {
152+
this.setState({
153+
hierarchy: [],
154+
inspected: {
155+
frame: {left, top, width, height},
156+
},
136157
});
137158
});
138-
const hideSub = agent.sub('hideHighlight', () => {
139-
if (this.state.inspected === null) {
140-
return;
141-
}
142-
// we wait to actually hide in order to avoid flicker
143-
_hideWait = setTimeout(() => {
144-
this.setState({
145-
inspected: null,
146-
});
147-
}, 100);
148-
});
149-
this._subs = [hlSub, hideSub];
159+
};
160+
161+
_onAgentShutdown = () => {
162+
const agent = this.state.devtoolsAgent;
163+
if (agent != null) {
164+
agent.removeListener(
165+
'hideNativeHighlight',
166+
this._onAgentHideNativeHighlight,
167+
);
168+
agent.removeListener(
169+
'showNativeHighlight',
170+
this._onAgentShowNativeHighlight,
171+
);
172+
agent.removeListener('shutdown', this._onAgentShutdown);
150173

151-
agent.on('shutdown', () => {
152174
this.setState({devtoolsAgent: null});
153-
this._subs = null;
154-
});
155-
this.setState({
156-
devtoolsAgent: agent,
157-
});
175+
}
158176
};
159177

160178
setSelection(i: number) {
@@ -187,11 +205,7 @@ class Inspector extends React.Component<
187205
if (this.state.devtoolsAgent) {
188206
// Skip host leafs
189207
const offsetFromLeaf = hierarchy.length - 1 - selection;
190-
this.state.devtoolsAgent.selectFromDOMNode(
191-
touchedViewTag,
192-
true,
193-
offsetFromLeaf,
194-
);
208+
this.state.devtoolsAgent.selectNode(touchedViewTag);
195209
}
196210

197211
this.setState({

Libraries/YellowBox/Data/YellowBoxCategory.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const YellowBoxCategory = {
6060

6161
if (substitutionIndex < substitutionCount) {
6262
if (substitutionIndex < substitutions.length) {
63-
const substitution = stringifySafe(
64-
substitutions[substitutionIndex],
65-
);
63+
// Don't stringify a string type.
64+
// It adds quotation mark wrappers around the string,
65+
// which causes the yellow box to look odd.
66+
const substitution =
67+
typeof substitutions[substitutionIndex] === 'string'
68+
? substitutions[substitutionIndex]
69+
: stringifySafe(substitutions[substitutionIndex]);
6670
substitutionOffsets.push({
6771
length: substitution.length,
6872
offset: contentString.length,
@@ -88,7 +92,12 @@ const YellowBoxCategory = {
8892
contentParts.push(contentString);
8993
}
9094

91-
const remainingArgs = remaining.map(stringifySafe);
95+
const remainingArgs = remaining.map(arg => {
96+
// Don't stringify a string type.
97+
// It adds quotation mark wrappers around the string,
98+
// which causes the yellow box to look odd.
99+
return typeof arg === 'string' ? arg : stringifySafe(arg);
100+
});
92101
categoryParts.push(...remainingArgs);
93102
contentParts.push(...remainingArgs);
94103

Libraries/YellowBox/Data/YellowBoxWarning.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,28 @@ class YellowBoxWarning {
3434
message: Message,
3535
stack: Stack,
3636
|} {
37+
let mutableArgs: Array<mixed> = [...args];
38+
39+
// This detects a very narrow case of a simple warning string,
40+
// with a component stack appended by React DevTools.
41+
// In this case, we convert the component stack to a substituion,
42+
// because YellowBox formats those pleasantly.
43+
// If there are other subtituations or formatting,
44+
// we bail to avoid potentially corrupting the data.
45+
if (mutableArgs.length === 2) {
46+
const first = mutableArgs[0];
47+
const last = mutableArgs[1];
48+
if (
49+
typeof first === 'string' &&
50+
typeof last === 'string' &&
51+
/^\n {4}in/.exec(last)
52+
) {
53+
mutableArgs[0] = first + '%s';
54+
}
55+
}
56+
3757
return {
38-
...YellowBoxCategory.parse(args),
58+
...YellowBoxCategory.parse(mutableArgs),
3959
stack: createStack({framesToPop: framesToPop + 1}),
4060
};
4161
}

Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js

+25-25
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ describe('YellowBoxCategory', () => {
2626

2727
it('parses strings with arguments', () => {
2828
expect(YellowBoxCategory.parse(['A', 'B', 'C'])).toEqual({
29-
category: 'A "B" "C"',
29+
category: 'A B C',
3030
message: {
31-
content: 'A "B" "C"',
31+
content: 'A B C',
3232
substitutions: [],
3333
},
3434
});
@@ -38,10 +38,10 @@ describe('YellowBoxCategory', () => {
3838
expect(YellowBoxCategory.parse(['%s', 'A'])).toEqual({
3939
category: '\ufeff%s',
4040
message: {
41-
content: '"A"',
41+
content: 'A',
4242
substitutions: [
4343
{
44-
length: 3,
44+
length: 1,
4545
offset: 0,
4646
},
4747
],
@@ -53,15 +53,15 @@ describe('YellowBoxCategory', () => {
5353
expect(YellowBoxCategory.parse(['%s %s', 'A'])).toEqual({
5454
category: '\ufeff%s %s',
5555
message: {
56-
content: '"A" %s',
56+
content: 'A %s',
5757
substitutions: [
5858
{
59-
length: 3,
59+
length: 1,
6060
offset: 0,
6161
},
6262
{
6363
length: 2,
64-
offset: 4,
64+
offset: 2,
6565
},
6666
],
6767
},
@@ -70,12 +70,12 @@ describe('YellowBoxCategory', () => {
7070

7171
it('parses formatted strings with excess arguments', () => {
7272
expect(YellowBoxCategory.parse(['%s', 'A', 'B'])).toEqual({
73-
category: '\ufeff%s "B"',
73+
category: '\ufeff%s B',
7474
message: {
75-
content: '"A" "B"',
75+
content: 'A B',
7676
substitutions: [
7777
{
78-
length: 3,
78+
length: 1,
7979
offset: 0,
8080
},
8181
],
@@ -85,12 +85,12 @@ describe('YellowBoxCategory', () => {
8585

8686
it('treats "%s" in arguments as literals', () => {
8787
expect(YellowBoxCategory.parse(['%s', '%s', 'A'])).toEqual({
88-
category: '\ufeff%s "A"',
88+
category: '\ufeff%s A',
8989
message: {
90-
content: '"%s" "A"',
90+
content: '%s A',
9191
substitutions: [
9292
{
93-
length: 4,
93+
length: 2,
9494
offset: 0,
9595
},
9696
],
@@ -111,10 +111,10 @@ describe('YellowBoxCategory', () => {
111111
expect(
112112
YellowBoxCategory.render(
113113
{
114-
content: '"A"',
114+
content: 'A',
115115
substitutions: [
116116
{
117-
length: 3,
117+
length: 1,
118118
offset: 0,
119119
},
120120
],
@@ -128,19 +128,19 @@ describe('YellowBoxCategory', () => {
128128
expect(
129129
YellowBoxCategory.render(
130130
{
131-
content: '"A" "B" "C"',
131+
content: 'A B C',
132132
substitutions: [
133133
{
134-
length: 3,
134+
length: 1,
135135
offset: 0,
136136
},
137137
{
138-
length: 3,
139-
offset: 4,
138+
length: 1,
139+
offset: 2,
140140
},
141141
{
142-
length: 3,
143-
offset: 8,
142+
length: 1,
143+
offset: 4,
144144
},
145145
],
146146
},
@@ -153,10 +153,10 @@ describe('YellowBoxCategory', () => {
153153
expect(
154154
YellowBoxCategory.render(
155155
{
156-
content: '!"A"',
156+
content: '!A',
157157
substitutions: [
158158
{
159-
length: 3,
159+
length: 1,
160160
offset: 1,
161161
},
162162
],
@@ -170,10 +170,10 @@ describe('YellowBoxCategory', () => {
170170
expect(
171171
YellowBoxCategory.render(
172172
{
173-
content: '"A"!',
173+
content: 'A!',
174174
substitutions: [
175175
{
176-
length: 3,
176+
length: 1,
177177
offset: 0,
178178
},
179179
],

0 commit comments

Comments
 (0)