diff --git a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js index 7c136aee58b67..beda841480ee1 100644 --- a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js +++ b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js @@ -120,19 +120,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'}, - ); -} diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index b88c01868b03b..bc50e175e9c3c 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -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), ); }); diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index 6b622af2bfc39..278b82ac1ab75 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -26,7 +26,7 @@ describe('InspectedElementContext', () => { async function read( id: number, - path?: Array = null, + path: Array = null, ): Promise { 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), ); }); }); diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 0b78fce3c22ca..31ed20f51b510 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -297,7 +297,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}"`); + } } }; diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 92e7d91c37b24..e7ef06c5060d2 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -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): void { + function getSerializedElementValueByPath( + id: number, + path: Array, + ): ?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, diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index d37497078e43f..45745684f5841 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -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): void { + function getSerializedElementValueByPath( + id: number, + path: Array, + ): ?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, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 3bf60b8cb5416..e4942d8df14cc 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -342,7 +342,6 @@ export type RendererInterface = { clearErrorsAndWarnings: () => void, clearErrorsForFiberID: (id: number) => void, clearWarningsForFiberID: (id: number) => void, - copyElementPath: (id: number, path: Array) => void, deletePath: ( type: Type, id: number, @@ -359,6 +358,10 @@ export type RendererInterface = { getProfilingData(): ProfilingDataBackend, getOwnersList: (id: number) => Array | null, getPathForElement: (id: number) => Array | null, + getSerializedElementValueByPath: ( + id: number, + path: Array, + ) => ?string, handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void, handleCommitFiberUnmount: (fiber: Object) => void, handlePostCommitFiberRoot: (fiber: Object) => void, diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils.js index 22bfe1131da5b..da70575295e02 100644 --- a/packages/react-devtools-shared/src/backend/utils.js +++ b/packages/react-devtools-shared/src/backend/utils.js @@ -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) => boolean, - path?: Array = [], + path: Array = [], ): DehydratedData | null { if (data !== null) { const cleanedPaths: Array> = []; @@ -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, path: Array, @@ -144,20 +126,28 @@ export function getEffectDurations(root: Object): { } export function serializeToString(data: any): string { + if (data === undefined) { + return 'undefined'; + } + const cache = new Set(); // 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 diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index c2dd07d393ea9..71e527ffd5671 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -194,6 +194,7 @@ export type BackendEvents = { profilingData: [ProfilingDataBackend], profilingStatus: [boolean], reloadAppForProfiling: [], + saveToClipboard: [string], selectFiber: [number], shutdown: [], stopInspectingNative: [boolean], diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index ba7ae80953409..eb5ac7af4f290 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -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.