Skip to content

Commit 9525a7b

Browse files
hoxyqAndyPengc12
authored andcommitted
refactor[devtools]: copy to clipboard only on frontend side (facebook#26604)
Fixes facebook#26500 ## Summary - No more using `clipboard-js` from the backend side, now emitting custom `saveToClipboard` event, also adding corresponding listener in `store.js` - Not migrating to `navigator.clipboard` api yet, there were some issues with using it on Chrome, will add more details to facebook#26539 ## How did you test this change? - Tested on Chrome, Firefox, Edge - Tested on standalone electron app: seems like context menu is not expected to work there (cannot right-click on value, the menu is not appearing), other logic (pressing on copy icon) was not changed
1 parent 6f9ba65 commit 9525a7b

File tree

10 files changed

+73
-70
lines changed

10 files changed

+73
-70
lines changed

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

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,3 @@ if (IS_FIREFOX) {
128128
}
129129
}
130130
}
131-
132-
if (typeof exportFunction === 'function') {
133-
// eslint-disable-next-line no-undef
134-
exportFunction(
135-
text => {
136-
// Call clipboard.writeText from the extension content script
137-
// (as it has the clipboardWrite permission) and return a Promise
138-
// accessible to the webpage js code.
139-
return new window.Promise((resolve, reject) =>
140-
window.navigator.clipboard.writeText(text).then(resolve, reject),
141-
);
142-
},
143-
window.wrappedJSObject.__REACT_DEVTOOLS_GLOBAL_HOOK__,
144-
{defineAs: 'clipboardCopyText'},
145-
);
146-
}

packages/react-devtools-shared/src/__tests__/inspectedElement-test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,7 +1801,7 @@ describe('InspectedElement', () => {
18011801
jest.runOnlyPendingTimers();
18021802
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
18031803
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
1804-
JSON.stringify(nestedObject),
1804+
JSON.stringify(nestedObject, undefined, 2),
18051805
);
18061806

18071807
global.mockClipboardCopy.mockReset();
@@ -1811,7 +1811,7 @@ describe('InspectedElement', () => {
18111811
jest.runOnlyPendingTimers();
18121812
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
18131813
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
1814-
JSON.stringify(nestedObject.a.b),
1814+
JSON.stringify(nestedObject.a.b, undefined, 2),
18151815
);
18161816
});
18171817

@@ -1894,7 +1894,7 @@ describe('InspectedElement', () => {
18941894
jest.runOnlyPendingTimers();
18951895
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
18961896
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
1897-
JSON.stringify('123n'),
1897+
JSON.stringify('123n', undefined, 2),
18981898
);
18991899

19001900
global.mockClipboardCopy.mockReset();
@@ -1904,7 +1904,7 @@ describe('InspectedElement', () => {
19041904
jest.runOnlyPendingTimers();
19051905
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
19061906
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
1907-
JSON.stringify({0: 100, 1: -100, 2: 0}),
1907+
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
19081908
);
19091909
});
19101910

packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('InspectedElementContext', () => {
2626

2727
async function read(
2828
id: number,
29-
path?: Array<string | number> = null,
29+
path: Array<string | number> = null,
3030
): Promise<Object> {
3131
const rendererID = ((store.getRendererIDForElement(id): any): number);
3232
const promise = backendAPI
@@ -826,7 +826,7 @@ describe('InspectedElementContext', () => {
826826
jest.runOnlyPendingTimers();
827827
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
828828
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
829-
JSON.stringify(nestedObject),
829+
JSON.stringify(nestedObject, undefined, 2),
830830
);
831831

832832
global.mockClipboardCopy.mockReset();
@@ -842,7 +842,7 @@ describe('InspectedElementContext', () => {
842842
jest.runOnlyPendingTimers();
843843
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
844844
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
845-
JSON.stringify(nestedObject.a.b),
845+
JSON.stringify(nestedObject.a.b, undefined, 2),
846846
);
847847
});
848848

@@ -932,7 +932,7 @@ describe('InspectedElementContext', () => {
932932
jest.runOnlyPendingTimers();
933933
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
934934
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
935-
JSON.stringify({0: 100, 1: -100, 2: 0}),
935+
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
936936
);
937937
});
938938
});

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,13 @@ export default class Agent extends EventEmitter<{
300300
if (renderer == null) {
301301
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
302302
} else {
303-
renderer.copyElementPath(id, path);
303+
const value = renderer.getSerializedElementValueByPath(id, path);
304+
305+
if (value != null) {
306+
this._bridge.send('saveToClipboard', value);
307+
} else {
308+
console.warn(`Unable to obtain serialized value for element "${id}"`);
309+
}
304310
}
305311
};
306312

packages/react-devtools-shared/src/backend/legacy/renderer.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import {
1717
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
1818
import {
1919
cleanForBridge,
20-
copyToClipboard,
2120
copyWithDelete,
2221
copyWithRename,
2322
copyWithSet,
23+
serializeToString,
2424
} from '../utils';
2525
import {
2626
deletePathInObject,
@@ -701,10 +701,15 @@ export function attach(
701701
}
702702
}
703703

704-
function copyElementPath(id: number, path: Array<string | number>): void {
704+
function getSerializedElementValueByPath(
705+
id: number,
706+
path: Array<string | number>,
707+
): ?string {
705708
const inspectedElement = inspectElementRaw(id);
706709
if (inspectedElement !== null) {
707-
copyToClipboard(getInObject(inspectedElement, path));
710+
const valueToCopy = getInObject(inspectedElement, path);
711+
712+
return serializeToString(valueToCopy);
708713
}
709714
}
710715

@@ -1105,7 +1110,7 @@ export function attach(
11051110
clearErrorsForFiberID,
11061111
clearWarningsForFiberID,
11071112
cleanup,
1108-
copyElementPath,
1113+
getSerializedElementValueByPath,
11091114
deletePath,
11101115
flushInitialOperations,
11111116
getBestMatchForTrackedPath,

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ import {
3838
utfEncodeString,
3939
} from 'react-devtools-shared/src/utils';
4040
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
41-
import {gt, gte} from 'react-devtools-shared/src/backend/utils';
41+
import {
42+
gt,
43+
gte,
44+
serializeToString,
45+
} from 'react-devtools-shared/src/backend/utils';
4246
import {
4347
cleanForBridge,
44-
copyToClipboard,
4548
copyWithDelete,
4649
copyWithRename,
4750
copyWithSet,
@@ -809,7 +812,7 @@ export function attach(
809812
name: string,
810813
fiber: Fiber,
811814
parentFiber: ?Fiber,
812-
extraString?: string = '',
815+
extraString: string = '',
813816
): void => {
814817
if (__DEBUG__) {
815818
const displayName =
@@ -3544,14 +3547,17 @@ export function attach(
35443547
}
35453548
}
35463549

3547-
function copyElementPath(id: number, path: Array<string | number>): void {
3550+
function getSerializedElementValueByPath(
3551+
id: number,
3552+
path: Array<string | number>,
3553+
): ?string {
35483554
if (isMostRecentlyInspectedElement(id)) {
3549-
copyToClipboard(
3550-
getInObject(
3551-
((mostRecentlyInspectedElement: any): InspectedElement),
3552-
path,
3553-
),
3555+
const valueToCopy = getInObject(
3556+
((mostRecentlyInspectedElement: any): InspectedElement),
3557+
path,
35543558
);
3559+
3560+
return serializeToString(valueToCopy);
35553561
}
35563562
}
35573563

@@ -4494,7 +4500,7 @@ export function attach(
44944500
clearErrorsAndWarnings,
44954501
clearErrorsForFiberID,
44964502
clearWarningsForFiberID,
4497-
copyElementPath,
4503+
getSerializedElementValueByPath,
44984504
deletePath,
44994505
findNativeNodesForFiberID,
45004506
flushInitialOperations,

packages/react-devtools-shared/src/backend/types.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,6 @@ export type RendererInterface = {
350350
clearErrorsAndWarnings: () => void,
351351
clearErrorsForFiberID: (id: number) => void,
352352
clearWarningsForFiberID: (id: number) => void,
353-
copyElementPath: (id: number, path: Array<string | number>) => void,
354353
deletePath: (
355354
type: Type,
356355
id: number,
@@ -367,6 +366,10 @@ export type RendererInterface = {
367366
getProfilingData(): ProfilingDataBackend,
368367
getOwnersList: (id: number) => Array<SerializedElement> | null,
369368
getPathForElement: (id: number) => Array<PathFrame> | null,
369+
getSerializedElementValueByPath: (
370+
id: number,
371+
path: Array<string | number>,
372+
) => ?string,
370373
handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
371374
handleCommitFiberUnmount: (fiber: Object) => void,
372375
handlePostCommitFiberRoot: (fiber: Object) => void,

packages/react-devtools-shared/src/backend/utils.js

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
* @flow
99
*/
1010

11-
import {copy} from 'clipboard-js';
1211
import {compareVersions} from 'compare-versions';
1312
import {dehydrate} from '../hydration';
1413
import isArray from 'shared/isArray';
@@ -18,7 +17,7 @@ import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Comp
1817
export function cleanForBridge(
1918
data: Object | null,
2019
isPathAllowed: (path: Array<string | number>) => boolean,
21-
path?: Array<string | number> = [],
20+
path: Array<string | number> = [],
2221
): DehydratedData | null {
2322
if (data !== null) {
2423
const cleanedPaths: Array<Array<string | number>> = [];
@@ -41,23 +40,6 @@ export function cleanForBridge(
4140
}
4241
}
4342

44-
export function copyToClipboard(value: any): void {
45-
const safeToCopy = serializeToString(value);
46-
const text = safeToCopy === undefined ? 'undefined' : safeToCopy;
47-
const {clipboardCopyText} = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
48-
49-
// On Firefox navigator.clipboard.writeText has to be called from
50-
// the content script js code (because it requires the clipboardWrite
51-
// permission to be allowed out of a "user handling" callback),
52-
// clipboardCopyText is an helper injected into the page from.
53-
// injectGlobalHook.
54-
if (typeof clipboardCopyText === 'function') {
55-
clipboardCopyText(text).catch(err => {});
56-
} else {
57-
copy(text);
58-
}
59-
}
60-
6143
export function copyWithDelete(
6244
obj: Object | Array<any>,
6345
path: Array<string | number>,
@@ -144,20 +126,28 @@ export function getEffectDurations(root: Object): {
144126
}
145127

146128
export function serializeToString(data: any): string {
129+
if (data === undefined) {
130+
return 'undefined';
131+
}
132+
147133
const cache = new Set<mixed>();
148134
// Use a custom replacer function to protect against circular references.
149-
return JSON.stringify(data, (key, value) => {
150-
if (typeof value === 'object' && value !== null) {
151-
if (cache.has(value)) {
152-
return;
135+
return JSON.stringify(
136+
data,
137+
(key, value) => {
138+
if (typeof value === 'object' && value !== null) {
139+
if (cache.has(value)) {
140+
return;
141+
}
142+
cache.add(value);
153143
}
154-
cache.add(value);
155-
}
156-
if (typeof value === 'bigint') {
157-
return value.toString() + 'n';
158-
}
159-
return value;
160-
});
144+
if (typeof value === 'bigint') {
145+
return value.toString() + 'n';
146+
}
147+
return value;
148+
},
149+
2,
150+
);
161151
}
162152

163153
// Formats an array of args with a style for console methods, using

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export type BackendEvents = {
194194
profilingData: [ProfilingDataBackend],
195195
profilingStatus: [boolean],
196196
reloadAppForProfiling: [],
197+
saveToClipboard: [string],
197198
selectFiber: [number],
198199
shutdown: [],
199200
stopInspectingNative: [boolean],

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import {copy} from 'clipboard-js';
1011
import EventEmitter from '../events';
1112
import {inspect} from 'util';
1213
import {
@@ -272,6 +273,8 @@ export default class Store extends EventEmitter<{
272273

273274
bridge.addListener('backendVersion', this.onBridgeBackendVersion);
274275
bridge.send('getBackendVersion');
276+
277+
bridge.addListener('saveToClipboard', this.onSaveToClipboard);
275278
}
276279

277280
// This is only used in tests to avoid memory leaks.
@@ -1362,6 +1365,7 @@ export default class Store extends EventEmitter<{
13621365
);
13631366
bridge.removeListener('backendVersion', this.onBridgeBackendVersion);
13641367
bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
1368+
bridge.removeListener('saveToClipboard', this.onSaveToClipboard);
13651369

13661370
if (this._onBridgeProtocolTimeoutID !== null) {
13671371
clearTimeout(this._onBridgeProtocolTimeoutID);
@@ -1422,6 +1426,10 @@ export default class Store extends EventEmitter<{
14221426
this.emit('unsupportedBridgeProtocolDetected');
14231427
};
14241428

1429+
onSaveToClipboard: (text: string) => void = text => {
1430+
copy(text);
1431+
};
1432+
14251433
// The Store should never throw an Error without also emitting an event.
14261434
// Otherwise Store errors will be invisible to users,
14271435
// but the downstream errors they cause will be reported as bugs.

0 commit comments

Comments
 (0)