mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
refactor[devtools]: copy to clipboard only on frontend side (#26604)
Fixes https://github.com/facebook/react/issues/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 https://github.com/facebook/react/pull/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
This commit is contained in:
@@ -128,19 +128,3 @@ if (IS_FIREFOX) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof exportFunction === 'function') {
|
||||
// eslint-disable-next-line no-undef
|
||||
exportFunction(
|
||||
text => {
|
||||
// Call clipboard.writeText from the extension content script
|
||||
// (as it has the clipboardWrite permission) and return a Promise
|
||||
// accessible to the webpage js code.
|
||||
return new window.Promise((resolve, reject) =>
|
||||
window.navigator.clipboard.writeText(text).then(resolve, reject),
|
||||
);
|
||||
},
|
||||
window.wrappedJSObject.__REACT_DEVTOOLS_GLOBAL_HOOK__,
|
||||
{defineAs: 'clipboardCopyText'},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1801,7 +1801,7 @@ describe('InspectedElement', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject),
|
||||
JSON.stringify(nestedObject, undefined, 2),
|
||||
);
|
||||
|
||||
global.mockClipboardCopy.mockReset();
|
||||
@@ -1811,7 +1811,7 @@ describe('InspectedElement', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject.a.b),
|
||||
JSON.stringify(nestedObject.a.b, undefined, 2),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1894,7 +1894,7 @@ describe('InspectedElement', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify('123n'),
|
||||
JSON.stringify('123n', undefined, 2),
|
||||
);
|
||||
|
||||
global.mockClipboardCopy.mockReset();
|
||||
@@ -1904,7 +1904,7 @@ describe('InspectedElement', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify({0: 100, 1: -100, 2: 0}),
|
||||
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
async function read(
|
||||
id: number,
|
||||
path?: Array<string | number> = null,
|
||||
path: Array<string | number> = null,
|
||||
): Promise<Object> {
|
||||
const rendererID = ((store.getRendererIDForElement(id): any): number);
|
||||
const promise = backendAPI
|
||||
@@ -826,7 +826,7 @@ describe('InspectedElementContext', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject),
|
||||
JSON.stringify(nestedObject, undefined, 2),
|
||||
);
|
||||
|
||||
global.mockClipboardCopy.mockReset();
|
||||
@@ -842,7 +842,7 @@ describe('InspectedElementContext', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify(nestedObject.a.b),
|
||||
JSON.stringify(nestedObject.a.b, undefined, 2),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -932,7 +932,7 @@ describe('InspectedElementContext', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||
JSON.stringify({0: 100, 1: -100, 2: 0}),
|
||||
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,7 +300,13 @@ export default class Agent extends EventEmitter<{
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.copyElementPath(id, path);
|
||||
const value = renderer.getSerializedElementValueByPath(id, path);
|
||||
|
||||
if (value != null) {
|
||||
this._bridge.send('saveToClipboard', value);
|
||||
} else {
|
||||
console.warn(`Unable to obtain serialized value for element "${id}"`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
|
||||
import {
|
||||
cleanForBridge,
|
||||
copyToClipboard,
|
||||
copyWithDelete,
|
||||
copyWithRename,
|
||||
copyWithSet,
|
||||
serializeToString,
|
||||
} from '../utils';
|
||||
import {
|
||||
deletePathInObject,
|
||||
@@ -701,10 +701,15 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function copyElementPath(id: number, path: Array<string | number>): void {
|
||||
function getSerializedElementValueByPath(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
): ?string {
|
||||
const inspectedElement = inspectElementRaw(id);
|
||||
if (inspectedElement !== null) {
|
||||
copyToClipboard(getInObject(inspectedElement, path));
|
||||
const valueToCopy = getInObject(inspectedElement, path);
|
||||
|
||||
return serializeToString(valueToCopy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,7 +1110,7 @@ export function attach(
|
||||
clearErrorsForFiberID,
|
||||
clearWarningsForFiberID,
|
||||
cleanup,
|
||||
copyElementPath,
|
||||
getSerializedElementValueByPath,
|
||||
deletePath,
|
||||
flushInitialOperations,
|
||||
getBestMatchForTrackedPath,
|
||||
|
||||
@@ -38,10 +38,13 @@ import {
|
||||
utfEncodeString,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
|
||||
import {gt, gte} from 'react-devtools-shared/src/backend/utils';
|
||||
import {
|
||||
gt,
|
||||
gte,
|
||||
serializeToString,
|
||||
} from 'react-devtools-shared/src/backend/utils';
|
||||
import {
|
||||
cleanForBridge,
|
||||
copyToClipboard,
|
||||
copyWithDelete,
|
||||
copyWithRename,
|
||||
copyWithSet,
|
||||
@@ -809,7 +812,7 @@ export function attach(
|
||||
name: string,
|
||||
fiber: Fiber,
|
||||
parentFiber: ?Fiber,
|
||||
extraString?: string = '',
|
||||
extraString: string = '',
|
||||
): void => {
|
||||
if (__DEBUG__) {
|
||||
const displayName =
|
||||
@@ -3544,14 +3547,17 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function copyElementPath(id: number, path: Array<string | number>): void {
|
||||
function getSerializedElementValueByPath(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
): ?string {
|
||||
if (isMostRecentlyInspectedElement(id)) {
|
||||
copyToClipboard(
|
||||
getInObject(
|
||||
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||
path,
|
||||
),
|
||||
const valueToCopy = getInObject(
|
||||
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||
path,
|
||||
);
|
||||
|
||||
return serializeToString(valueToCopy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4494,7 +4500,7 @@ export function attach(
|
||||
clearErrorsAndWarnings,
|
||||
clearErrorsForFiberID,
|
||||
clearWarningsForFiberID,
|
||||
copyElementPath,
|
||||
getSerializedElementValueByPath,
|
||||
deletePath,
|
||||
findNativeNodesForFiberID,
|
||||
flushInitialOperations,
|
||||
|
||||
@@ -350,7 +350,6 @@ export type RendererInterface = {
|
||||
clearErrorsAndWarnings: () => void,
|
||||
clearErrorsForFiberID: (id: number) => void,
|
||||
clearWarningsForFiberID: (id: number) => void,
|
||||
copyElementPath: (id: number, path: Array<string | number>) => void,
|
||||
deletePath: (
|
||||
type: Type,
|
||||
id: number,
|
||||
@@ -367,6 +366,10 @@ export type RendererInterface = {
|
||||
getProfilingData(): ProfilingDataBackend,
|
||||
getOwnersList: (id: number) => Array<SerializedElement> | null,
|
||||
getPathForElement: (id: number) => Array<PathFrame> | null,
|
||||
getSerializedElementValueByPath: (
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
) => ?string,
|
||||
handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
|
||||
handleCommitFiberUnmount: (fiber: Object) => void,
|
||||
handlePostCommitFiberRoot: (fiber: Object) => void,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {copy} from 'clipboard-js';
|
||||
import {compareVersions} from 'compare-versions';
|
||||
import {dehydrate} from '../hydration';
|
||||
import isArray from 'shared/isArray';
|
||||
@@ -18,7 +17,7 @@ import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Comp
|
||||
export function cleanForBridge(
|
||||
data: Object | null,
|
||||
isPathAllowed: (path: Array<string | number>) => boolean,
|
||||
path?: Array<string | number> = [],
|
||||
path: Array<string | number> = [],
|
||||
): DehydratedData | null {
|
||||
if (data !== null) {
|
||||
const cleanedPaths: Array<Array<string | number>> = [];
|
||||
@@ -41,23 +40,6 @@ export function cleanForBridge(
|
||||
}
|
||||
}
|
||||
|
||||
export function copyToClipboard(value: any): void {
|
||||
const safeToCopy = serializeToString(value);
|
||||
const text = safeToCopy === undefined ? 'undefined' : safeToCopy;
|
||||
const {clipboardCopyText} = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
|
||||
// On Firefox navigator.clipboard.writeText has to be called from
|
||||
// the content script js code (because it requires the clipboardWrite
|
||||
// permission to be allowed out of a "user handling" callback),
|
||||
// clipboardCopyText is an helper injected into the page from.
|
||||
// injectGlobalHook.
|
||||
if (typeof clipboardCopyText === 'function') {
|
||||
clipboardCopyText(text).catch(err => {});
|
||||
} else {
|
||||
copy(text);
|
||||
}
|
||||
}
|
||||
|
||||
export function copyWithDelete(
|
||||
obj: Object | Array<any>,
|
||||
path: Array<string | number>,
|
||||
@@ -144,20 +126,28 @@ export function getEffectDurations(root: Object): {
|
||||
}
|
||||
|
||||
export function serializeToString(data: any): string {
|
||||
if (data === undefined) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
const cache = new Set<mixed>();
|
||||
// Use a custom replacer function to protect against circular references.
|
||||
return JSON.stringify(data, (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.has(value)) {
|
||||
return;
|
||||
return JSON.stringify(
|
||||
data,
|
||||
(key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.has(value)) {
|
||||
return;
|
||||
}
|
||||
cache.add(value);
|
||||
}
|
||||
cache.add(value);
|
||||
}
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString() + 'n';
|
||||
}
|
||||
return value;
|
||||
});
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString() + 'n';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
// Formats an array of args with a style for console methods, using
|
||||
|
||||
1
packages/react-devtools-shared/src/bridge.js
vendored
1
packages/react-devtools-shared/src/bridge.js
vendored
@@ -194,6 +194,7 @@ export type BackendEvents = {
|
||||
profilingData: [ProfilingDataBackend],
|
||||
profilingStatus: [boolean],
|
||||
reloadAppForProfiling: [],
|
||||
saveToClipboard: [string],
|
||||
selectFiber: [number],
|
||||
shutdown: [],
|
||||
stopInspectingNative: [boolean],
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {copy} from 'clipboard-js';
|
||||
import EventEmitter from '../events';
|
||||
import {inspect} from 'util';
|
||||
import {
|
||||
@@ -272,6 +273,8 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
bridge.addListener('backendVersion', this.onBridgeBackendVersion);
|
||||
bridge.send('getBackendVersion');
|
||||
|
||||
bridge.addListener('saveToClipboard', this.onSaveToClipboard);
|
||||
}
|
||||
|
||||
// This is only used in tests to avoid memory leaks.
|
||||
@@ -1362,6 +1365,7 @@ export default class Store extends EventEmitter<{
|
||||
);
|
||||
bridge.removeListener('backendVersion', this.onBridgeBackendVersion);
|
||||
bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
|
||||
bridge.removeListener('saveToClipboard', this.onSaveToClipboard);
|
||||
|
||||
if (this._onBridgeProtocolTimeoutID !== null) {
|
||||
clearTimeout(this._onBridgeProtocolTimeoutID);
|
||||
@@ -1422,6 +1426,10 @@ export default class Store extends EventEmitter<{
|
||||
this.emit('unsupportedBridgeProtocolDetected');
|
||||
};
|
||||
|
||||
onSaveToClipboard: (text: string) => void = text => {
|
||||
copy(text);
|
||||
};
|
||||
|
||||
// The Store should never throw an Error without also emitting an event.
|
||||
// Otherwise Store errors will be invisible to users,
|
||||
// but the downstream errors they cause will be reported as bugs.
|
||||
|
||||
Reference in New Issue
Block a user