diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 0b78ab196cb98..fcf2a3fc14e34 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -6,7 +6,7 @@ "applications": { "gecko": { "id": "@react-devtools", - "strict_min_version": "55.0" + "strict_min_version": "63.0" } }, "icons": { diff --git a/packages/react-devtools-shared/package.json b/packages/react-devtools-shared/package.json index b03ce9ea74310..2b7851bbefb3b 100644 --- a/packages/react-devtools-shared/package.json +++ b/packages/react-devtools-shared/package.json @@ -17,7 +17,6 @@ "@babel/traverse": "^7.12.5", "@reach/menu-button": "^0.16.1", "@reach/tooltip": "^0.16.0", - "clipboard-js": "^0.3.6", "compare-versions": "^5.0.3", "json5": "^2.1.3", "local-storage-fallback": "^4.1.1", diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 614c3a8ef88c2..43cc2453b29dc 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -33,12 +33,10 @@ if (compactConsole) { } beforeEach(() => { + // Storing this mock, so it can be accessed to reset its state + // Used inside inspectElement-test global.mockClipboardCopy = jest.fn(); - - // Test environment doesn't support document methods like execCommand() - // Also once the backend components below have been required, - // it's too late for a test to mock the clipboard-js modules. - jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy})); + global.navigator.clipboard = {writeText: global.mockClipboardCopy}; // These files should be required (and re-required) before each test, // rather than imported at the head of the module. diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 92e7d91c37b24..07ab61e90052e 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -14,20 +14,22 @@ import { ElementTypeHostComponent, ElementTypeOtherOrUnknown, } from 'react-devtools-shared/src/types'; -import {getUID, utfEncodeString, printOperationsArray} from '../../utils'; import { cleanForBridge, - copyToClipboard, copyWithDelete, copyWithRename, copyWithSet, -} from '../utils'; +} from 'react-devtools-shared/src/backend/utils'; import { deletePathInObject, getDisplayName, getInObject, - renamePathInObject, + serializeAndCopyToClipboard, setInObject, + renamePathInObject, + getUID, + utfEncodeString, + printOperationsArray, } from 'react-devtools-shared/src/utils'; import { __DEBUG__, @@ -704,7 +706,7 @@ export function attach( function copyElementPath(id: number, path: Array): void { const inspectedElement = inspectElementRaw(id); if (inspectedElement !== null) { - copyToClipboard(getInObject(inspectedElement, path)); + serializeAndCopyToClipboard(getInObject(inspectedElement, path)); } } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index d37497078e43f..a053001d7dd05 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -34,19 +34,20 @@ import { getInObject, getUID, renamePathInObject, + serializeAndCopyToClipboard, setInObject, 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 { cleanForBridge, - copyToClipboard, copyWithDelete, copyWithRename, copyWithSet, + gt, + gte, getEffectDurations, -} from './utils'; +} from 'react-devtools-shared/src/backend/utils'; import { __DEBUG__, PROFILING_FLAG_BASIC_SUPPORT, @@ -3546,7 +3547,7 @@ export function attach( function copyElementPath(id: number, path: Array): void { if (isMostRecentlyInspectedElement(id)) { - copyToClipboard( + serializeAndCopyToClipboard( getInObject( ((mostRecentlyInspectedElement: any): InspectedElement), path, diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils.js index 22bfe1131da5b..cd9ac2052c402 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'; @@ -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, @@ -143,23 +125,6 @@ export function getEffectDurations(root: Object): { return {effectDuration, passiveEffectDuration}; } -export function serializeToString(data: any): string { - 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; - } - cache.add(value); - } - if (typeof value === 'bigint') { - return value.toString() + 'n'; - } - return value; - }); -} - // Formats an array of args with a style for console methods, using // the following algorithm: // 1. The first param is a string that contains %c diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js index eb74c244d9d1a..dd4fa4cb4c956 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js @@ -7,7 +7,6 @@ * @flow */ -import {copy} from 'clipboard-js'; import * as React from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; @@ -19,6 +18,7 @@ import { ElementTypeClass, ElementTypeFunction, } from 'react-devtools-shared/src/types'; +import {copyToClipboard} from 'react-devtools-shared/src/utils'; import type {InspectedElement} from './types'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; @@ -48,7 +48,7 @@ export default function InspectedElementContextTree({ const isEmpty = entries === null || entries.length === 0; - const handleCopy = () => copy(serializeDataForCopy(((context: any): Object))); + const handleCopy = () => copyToClipboard(serializeDataForCopy(context)); // We add an object with a "value" key as a wrapper around Context data // so that we can use the shared component to display it. diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js index 61260d6d05d38..66fc9a2a1b84d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -7,7 +7,6 @@ * @flow */ -import {copy} from 'clipboard-js'; import * as React from 'react'; import {useCallback, useContext, useRef, useState} from 'react'; import {BridgeContext, StoreContext} from '../context'; @@ -28,6 +27,7 @@ import { } from 'react-devtools-feature-flags'; import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import isArray from 'react-devtools-shared/src/isArray'; +import {copyToClipboard} from 'react-devtools-shared/src/utils'; import type {InspectedElement} from './types'; import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; @@ -79,7 +79,7 @@ export function InspectedElementHooksTree({ toggleTitle = 'Parse hook names (may be slow)'; } - const handleCopy = () => copy(serializeHooksForCopy(hooks)); + const handleCopy = () => copyToClipboard(serializeHooksForCopy(hooks)); if (hooks === null) { return null; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js index f3962b3a77290..67fc2c4e860b3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js @@ -7,7 +7,6 @@ * @flow */ -import {copy} from 'clipboard-js'; import * as React from 'react'; import {OptionsContext} from '../context'; import Button from '../Button'; @@ -18,6 +17,7 @@ import {alphaSortEntries, serializeDataForCopy} from '../utils'; import Store from '../../store'; import styles from './InspectedElementSharedStyles.css'; import {ElementTypeClass} from 'react-devtools-shared/src/types'; +import {copyToClipboard} from 'react-devtools-shared/src/utils'; import type {InspectedElement} from './types'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; @@ -59,8 +59,7 @@ export default function InspectedElementPropsTree({ } const isEmpty = entries === null || entries.length === 0; - - const handleCopy = () => copy(serializeDataForCopy(((props: any): Object))); + const handleCopy = () => copyToClipboard(serializeDataForCopy(props)); return (
copy(serializeDataForCopy(((state: any): Object))); + const handleCopy = () => copyToClipboard(serializeDataForCopy(state)); return (
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index 4804662f31007..93121bbb4a713 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -7,7 +7,6 @@ * @flow */ -import {copy} from 'clipboard-js'; import * as React from 'react'; import {Fragment, useCallback, useContext} from 'react'; import {TreeDispatcherContext} from './TreeContext'; @@ -34,6 +33,7 @@ import { } from 'react-devtools-shared/src/backendAPI'; import {enableStyleXFeatures} from 'react-devtools-feature-flags'; import {logEvent} from 'react-devtools-shared/src/Logger'; +import {copyToClipboard} from 'react-devtools-shared/src/utils'; import styles from './InspectedElementView.css'; @@ -260,7 +260,7 @@ type SourceProps = { }; function Source({fileName, lineNumber}: SourceProps) { - const handleCopy = () => copy(`${fileName}:${lineNumber}`); + const handleCopy = () => copyToClipboard(`${fileName}:${lineNumber}`); return (
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js index 8967ea68c913f..46f34f314353f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js @@ -10,11 +10,11 @@ import * as React from 'react'; import {useContext, useMemo, useRef, useState} from 'react'; import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; -import {copy} from 'clipboard-js'; import { BridgeContext, StoreContext, } from 'react-devtools-shared/src/devtools/views/context'; +import {copyToClipboard} from 'react-devtools-shared/src/utils'; import Button from '../../Button'; import ButtonIcon from '../../ButtonIcon'; import {serializeDataForCopy} from '../../utils'; @@ -63,7 +63,7 @@ export default function StyleEditor({id, style}: Props): React.Node { const keys = useMemo(() => Array.from(Object.keys(style)), [style]); - const handleCopy = () => copy(serializeDataForCopy(style)); + const handleCopy = () => copyToClipboard(serializeDataForCopy(style)); return (
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js index d0d8be666ed32..ff861ee4fe777 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js @@ -21,7 +21,7 @@ import { getSchedulingEventLabel, } from 'react-devtools-timeline/src/utils/formatting'; import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils'; -import {copy} from 'clipboard-js'; +import {copyToClipboard} from 'react-devtools-shared/src/utils'; import styles from './SidebarEventInfo.css'; @@ -58,7 +58,7 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js b/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js index 425dfe1d08c13..1c929ded6a053 100644 --- a/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js +++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js @@ -14,8 +14,8 @@ import {StoreContext} from './context'; import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge'; import Button from './Button'; import ButtonIcon from './ButtonIcon'; -import {copy} from 'clipboard-js'; import styles from './UnsupportedBridgeProtocolDialog.css'; +import {copyToClipboard} from 'react-devtools-shared/src/utils'; import type {BridgeProtocol} from 'react-devtools-shared/src/bridge'; @@ -82,7 +82,7 @@ function DialogContent({
           {upgradeInstructions}
           
@@ -99,7 +99,7 @@ function DialogContent({
         
           {downgradeInstructions}
           
diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js
index 58845219d9591..c6e31eafacd8e 100644
--- a/packages/react-devtools-shared/src/utils.js
+++ b/packages/react-devtools-shared/src/utils.js
@@ -890,3 +890,41 @@ export const isPlainObject = (object: Object): boolean => {
   const objectParentPrototype = Object.getPrototypeOf(objectPrototype);
   return !objectParentPrototype;
 };
+
+export function serializeToString(data: any): string {
+  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;
+      }
+      cache.add(value);
+    }
+    if (typeof value === 'bigint') {
+      return value.toString() + 'n';
+    }
+    return value;
+  });
+}
+
+export function copyToClipboard(text: string): void {
+  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 a helper injected into the page from injectGlobalHook.
+  if (typeof clipboardCopyText === 'function') {
+    clipboardCopyText(text).catch(err => {});
+  } else {
+    navigator.clipboard.writeText(text);
+  }
+}
+
+export function serializeAndCopyToClipboard(value: any): void {
+  const serializedValue = serializeToString(value);
+  const text = serializedValue === undefined ? 'undefined' : serializedValue;
+
+  copyToClipboard(text);
+}
diff --git a/packages/react-devtools-timeline/package.json b/packages/react-devtools-timeline/package.json
index 0dfacabdd9153..b48ec39d7be48 100644
--- a/packages/react-devtools-timeline/package.json
+++ b/packages/react-devtools-timeline/package.json
@@ -5,7 +5,6 @@
   "license": "MIT",
   "dependencies": {
     "@elg/speedscope": "1.9.0-a6f84db",
-    "clipboard-js": "^0.3.6",
     "memoize-one": "^5.1.1",
     "nullthrows": "^1.1.1",
     "pretty-ms": "^7.0.0",
diff --git a/packages/react-devtools-timeline/src/CanvasPage.js b/packages/react-devtools-timeline/src/CanvasPage.js
index 5172b7d5d2b05..716f6ebd0294e 100644
--- a/packages/react-devtools-timeline/src/CanvasPage.js
+++ b/packages/react-devtools-timeline/src/CanvasPage.js
@@ -26,7 +26,6 @@ import {
   useCallback,
 } from 'react';
 import AutoSizer from 'react-virtualized-auto-sizer';
-import {copy} from 'clipboard-js';
 import prettyMilliseconds from 'pretty-ms';
 
 import {
@@ -60,6 +59,7 @@ import {RegistryContext} from 'react-devtools-shared/src/devtools/ContextMenu/Co
 import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu';
 import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem';
 import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu';
+import {copyToClipboard} from 'react-devtools-shared/src/utils';
 import {getBatchRange} from './utils/getBatchRange';
 import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
 import {TimelineSearchContext} from './TimelineSearchContext';
@@ -98,7 +98,7 @@ const copySummary = (data: TimelineData, measure: ReactMeasure) => {
 
   const [startTime, stopTime] = getBatchRange(batchUID, data);
 
-  copy(
+  copyToClipboard(
     JSON.stringify({
       type,
       timestamp: prettyMilliseconds(timestamp),
@@ -741,28 +741,30 @@ function AutoSizedCanvas({
             
               {componentMeasure !== null && (
                  copy(componentMeasure.componentName)}
+                  onClick={() =>
+                    copyToClipboard(componentMeasure.componentName)
+                  }
                   title="Copy component name">
                   Copy component name
                 
               )}
               {networkMeasure !== null && (
                  copy(networkMeasure.url)}
+                  onClick={() => copyToClipboard(networkMeasure.url)}
                   title="Copy URL">
                   Copy URL
                 
               )}
               {schedulingEvent !== null && (
                  copy(schedulingEvent.componentName)}
+                  onClick={() => copyToClipboard(schedulingEvent.componentName)}
                   title="Copy component name">
                   Copy component name
                 
               )}
               {suspenseEvent !== null && (
                  copy(suspenseEvent.componentName)}
+                  onClick={() => copyToClipboard(suspenseEvent.componentName)}
                   title="Copy component name">
                   Copy component name
                 
@@ -785,7 +787,9 @@ function AutoSizedCanvas({
               )}
               {flamechartStackFrame !== null && (
                  copy(flamechartStackFrame.scriptUrl)}
+                  onClick={() =>
+                    copyToClipboard(flamechartStackFrame.scriptUrl)
+                  }
                   title="Copy file path">
                   Copy file path
                 
@@ -793,7 +797,7 @@ function AutoSizedCanvas({
               {flamechartStackFrame !== null && (
                 
-                    copy(
+                    copyToClipboard(
                       `line ${
                         flamechartStackFrame.locationLine ?? ''
                       }, column ${flamechartStackFrame.locationColumn ?? ''}`,
diff --git a/yarn.lock b/yarn.lock
index 3c8628af7f140..bf24da5cd76dd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5394,11 +5394,6 @@ cli-width@^2.0.0:
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
   integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
 
-clipboard-js@^0.3.6:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/clipboard-js/-/clipboard-js-0.3.6.tgz#6260add69b5318fde40b80f9d3c8c863efdaf339"
-  integrity sha512-hyrmvbrYCeRBHdiR3KrEz0tmrUTXXEU8HLeGW0Y0icUSwYmAsmc+d6wfE4EDb/TxZmAVJG0eTfMlulCIT+ecfw==
-
 cliui@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"