Skip to content

Commit 3d1c3ae

Browse files
committed
fix[devtools]: use native clipboard api instead of clipboard-js
1 parent b14f8da commit 3d1c3ae

File tree

18 files changed

+83
-83
lines changed

18 files changed

+83
-83
lines changed

packages/react-devtools-extensions/firefox/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"applications": {
77
"gecko": {
88
"id": "@react-devtools",
9-
"strict_min_version": "55.0"
9+
"strict_min_version": "63.0"
1010
}
1111
},
1212
"icons": {

packages/react-devtools-shared/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"@babel/traverse": "^7.12.5",
1818
"@reach/menu-button": "^0.16.1",
1919
"@reach/tooltip": "^0.16.0",
20-
"clipboard-js": "^0.3.6",
2120
"compare-versions": "^5.0.3",
2221
"json5": "^2.1.3",
2322
"local-storage-fallback": "^4.1.1",

packages/react-devtools-shared/src/__tests__/setupTests.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,10 @@ if (compactConsole) {
3333
}
3434

3535
beforeEach(() => {
36+
// Storing this mock, so it can be accessed to reset its state
37+
// Used inside inspectElement-test
3638
global.mockClipboardCopy = jest.fn();
37-
38-
// Test environment doesn't support document methods like execCommand()
39-
// Also once the backend components below have been required,
40-
// it's too late for a test to mock the clipboard-js modules.
41-
jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy}));
39+
global.navigator.clipboard = {writeText: global.mockClipboardCopy};
4240

4341
// These files should be required (and re-required) before each test,
4442
// rather than imported at the head of the module.

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,22 @@ import {
1414
ElementTypeHostComponent,
1515
ElementTypeOtherOrUnknown,
1616
} from 'react-devtools-shared/src/types';
17-
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
1817
import {
1918
cleanForBridge,
20-
copyToClipboard,
2119
copyWithDelete,
2220
copyWithRename,
2321
copyWithSet,
24-
} from '../utils';
22+
} from 'react-devtools-shared/src/backend/utils';
2523
import {
2624
deletePathInObject,
2725
getDisplayName,
2826
getInObject,
29-
renamePathInObject,
27+
serializeAndCopyToClipboard,
3028
setInObject,
29+
renamePathInObject,
30+
getUID,
31+
utfEncodeString,
32+
printOperationsArray,
3133
} from 'react-devtools-shared/src/utils';
3234
import {
3335
__DEBUG__,
@@ -704,7 +706,7 @@ export function attach(
704706
function copyElementPath(id: number, path: Array<string | number>): void {
705707
const inspectedElement = inspectElementRaw(id);
706708
if (inspectedElement !== null) {
707-
copyToClipboard(getInObject(inspectedElement, path));
709+
serializeAndCopyToClipboard(getInObject(inspectedElement, path));
708710
}
709711
}
710712

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,20 @@ import {
3434
getInObject,
3535
getUID,
3636
renamePathInObject,
37+
serializeAndCopyToClipboard,
3738
setInObject,
3839
utfEncodeString,
3940
} from 'react-devtools-shared/src/utils';
4041
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
41-
import {gt, gte} from 'react-devtools-shared/src/backend/utils';
4242
import {
4343
cleanForBridge,
44-
copyToClipboard,
4544
copyWithDelete,
4645
copyWithRename,
4746
copyWithSet,
47+
gt,
48+
gte,
4849
getEffectDurations,
49-
} from './utils';
50+
} from 'react-devtools-shared/src/backend/utils';
5051
import {
5152
__DEBUG__,
5253
PROFILING_FLAG_BASIC_SUPPORT,
@@ -3540,7 +3541,7 @@ export function attach(
35403541

35413542
function copyElementPath(id: number, path: Array<string | number>): void {
35423543
if (isMostRecentlyInspectedElement(id)) {
3543-
copyToClipboard(
3544+
serializeAndCopyToClipboard(
35443545
getInObject(
35453546
((mostRecentlyInspectedElement: any): InspectedElement),
35463547
path,

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

Lines changed: 0 additions & 35 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';
@@ -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>,
@@ -143,23 +125,6 @@ export function getEffectDurations(root: Object): {
143125
return {effectDuration, passiveEffectDuration};
144126
}
145127

146-
export function serializeToString(data: any): string {
147-
const cache = new Set<mixed>();
148-
// 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;
153-
}
154-
cache.add(value);
155-
}
156-
if (typeof value === 'bigint') {
157-
return value.toString() + 'n';
158-
}
159-
return value;
160-
});
161-
}
162-
163128
// Formats an array of args with a style for console methods, using
164129
// the following algorithm:
165130
// 1. The first param is a string that contains %c

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js

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

10-
import {copy} from 'clipboard-js';
1110
import * as React from 'react';
1211
import Button from '../Button';
1312
import ButtonIcon from '../ButtonIcon';
@@ -19,6 +18,7 @@ import {
1918
ElementTypeClass,
2019
ElementTypeFunction,
2120
} from 'react-devtools-shared/src/types';
21+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
2222

2323
import type {InspectedElement} from './types';
2424
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -48,7 +48,7 @@ export default function InspectedElementContextTree({
4848

4949
const isEmpty = entries === null || entries.length === 0;
5050

51-
const handleCopy = () => copy(serializeDataForCopy(((context: any): Object)));
51+
const handleCopy = () => copyToClipboard(serializeDataForCopy(context));
5252

5353
// We add an object with a "value" key as a wrapper around Context data
5454
// so that we can use the shared <KeyValue> component to display it.

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js

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

10-
import {copy} from 'clipboard-js';
1110
import * as React from 'react';
1211
import {useCallback, useContext, useRef, useState} from 'react';
1312
import {BridgeContext, StoreContext} from '../context';
@@ -28,6 +27,7 @@ import {
2827
} from 'react-devtools-feature-flags';
2928
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
3029
import isArray from 'react-devtools-shared/src/isArray';
30+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
3131

3232
import type {InspectedElement} from './types';
3333
import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
@@ -79,7 +79,7 @@ export function InspectedElementHooksTree({
7979
toggleTitle = 'Parse hook names (may be slow)';
8080
}
8181

82-
const handleCopy = () => copy(serializeHooksForCopy(hooks));
82+
const handleCopy = () => copyToClipboard(serializeHooksForCopy(hooks));
8383

8484
if (hooks === null) {
8585
return null;

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js

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

10-
import {copy} from 'clipboard-js';
1110
import * as React from 'react';
1211
import {OptionsContext} from '../context';
1312
import Button from '../Button';
@@ -18,6 +17,7 @@ import {alphaSortEntries, serializeDataForCopy} from '../utils';
1817
import Store from '../../store';
1918
import styles from './InspectedElementSharedStyles.css';
2019
import {ElementTypeClass} from 'react-devtools-shared/src/types';
20+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
2121

2222
import type {InspectedElement} from './types';
2323
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -59,8 +59,7 @@ export default function InspectedElementPropsTree({
5959
}
6060

6161
const isEmpty = entries === null || entries.length === 0;
62-
63-
const handleCopy = () => copy(serializeDataForCopy(((props: any): Object)));
62+
const handleCopy = () => copyToClipboard(serializeDataForCopy(props));
6463

6564
return (
6665
<div

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js

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

10-
import {copy} from 'clipboard-js';
1110
import * as React from 'react';
1211
import {ElementTypeHostComponent} from 'react-devtools-shared/src/types';
1312
import Button from '../Button';
@@ -16,6 +15,7 @@ import KeyValue from './KeyValue';
1615
import {alphaSortEntries, serializeDataForCopy} from '../utils';
1716
import Store from '../../store';
1817
import styles from './InspectedElementSharedStyles.css';
18+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
1919

2020
import type {InspectedElement} from './types';
2121
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -50,7 +50,7 @@ export default function InspectedElementStateTree({
5050
entries.sort(alphaSortEntries);
5151
}
5252

53-
const handleCopy = () => copy(serializeDataForCopy(((state: any): Object)));
53+
const handleCopy = () => copyToClipboard(serializeDataForCopy(state));
5454

5555
return (
5656
<div className={styles.InspectedElementTree}>

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js

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

10-
import {copy} from 'clipboard-js';
1110
import * as React from 'react';
1211
import {Fragment, useCallback, useContext} from 'react';
1312
import {TreeDispatcherContext} from './TreeContext';
@@ -34,6 +33,7 @@ import {
3433
} from 'react-devtools-shared/src/backendAPI';
3534
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
3635
import {logEvent} from 'react-devtools-shared/src/Logger';
36+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
3737

3838
import styles from './InspectedElementView.css';
3939

@@ -260,7 +260,7 @@ type SourceProps = {
260260
};
261261

262262
function Source({fileName, lineNumber}: SourceProps) {
263-
const handleCopy = () => copy(`${fileName}:${lineNumber}`);
263+
const handleCopy = () => copyToClipboard(`${fileName}:${lineNumber}`);
264264
return (
265265
<div className={styles.Source} data-testname="InspectedElementView-Source">
266266
<div className={styles.SourceHeaderRow}>

packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
import * as React from 'react';
1111
import {useContext, useMemo, useRef, useState} from 'react';
1212
import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
13-
import {copy} from 'clipboard-js';
1413
import {
1514
BridgeContext,
1615
StoreContext,
1716
} from 'react-devtools-shared/src/devtools/views/context';
17+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
1818
import Button from '../../Button';
1919
import ButtonIcon from '../../ButtonIcon';
2020
import {serializeDataForCopy} from '../../utils';
@@ -63,7 +63,7 @@ export default function StyleEditor({id, style}: Props): React.Node {
6363

6464
const keys = useMemo(() => Array.from(Object.keys(style)), [style]);
6565

66-
const handleCopy = () => copy(serializeDataForCopy(style));
66+
const handleCopy = () => copyToClipboard(serializeDataForCopy(style));
6767

6868
return (
6969
<div className={styles.StyleEditor}>

packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
getSchedulingEventLabel,
2222
} from 'react-devtools-timeline/src/utils/formatting';
2323
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
24-
import {copy} from 'clipboard-js';
24+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
2525

2626
import styles from './SidebarEventInfo.css';
2727

@@ -58,7 +58,7 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
5858
<div className={styles.Row}>
5959
<label className={styles.Label}>Rendered by</label>
6060
<Button
61-
onClick={() => copy(componentStack)}
61+
onClick={() => copyToClipboard(componentStack)}
6262
title="Copy component stack to clipboard">
6363
<ButtonIcon type="copy" />
6464
</Button>

packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {StoreContext} from './context';
1414
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
1515
import Button from './Button';
1616
import ButtonIcon from './ButtonIcon';
17-
import {copy} from 'clipboard-js';
1817
import styles from './UnsupportedBridgeProtocolDialog.css';
18+
import {copyToClipboard} from 'react-devtools-shared/src/utils';
1919

2020
import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
2121

@@ -82,7 +82,7 @@ function DialogContent({
8282
<pre className={styles.NpmCommand}>
8383
{upgradeInstructions}
8484
<Button
85-
onClick={() => copy(upgradeInstructions)}
85+
onClick={() => copyToClipboard(upgradeInstructions)}
8686
title="Copy upgrade command to clipboard">
8787
<ButtonIcon type="copy" />
8888
</Button>
@@ -99,7 +99,7 @@ function DialogContent({
9999
<pre className={styles.NpmCommand}>
100100
{downgradeInstructions}
101101
<Button
102-
onClick={() => copy(downgradeInstructions)}
102+
onClick={() => copyToClipboard(downgradeInstructions)}
103103
title="Copy downgrade command to clipboard">
104104
<ButtonIcon type="copy" />
105105
</Button>

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,3 +890,41 @@ export const isPlainObject = (object: Object): boolean => {
890890
const objectParentPrototype = Object.getPrototypeOf(objectPrototype);
891891
return !objectParentPrototype;
892892
};
893+
894+
export function serializeToString(data: any): string {
895+
const cache = new Set<mixed>();
896+
// Use a custom replacer function to protect against circular references.
897+
return JSON.stringify(data, (key, value) => {
898+
if (typeof value === 'object' && value !== null) {
899+
if (cache.has(value)) {
900+
return;
901+
}
902+
cache.add(value);
903+
}
904+
if (typeof value === 'bigint') {
905+
return value.toString() + 'n';
906+
}
907+
return value;
908+
});
909+
}
910+
911+
export function copyToClipboard(text: string): void {
912+
const {clipboardCopyText} = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
913+
914+
// On Firefox navigator.clipboard.writeText has to be called from
915+
// the content script js code (because it requires the clipboardWrite
916+
// permission to be allowed out of a "user handling" callback),
917+
// clipboardCopyText is a helper injected into the page from injectGlobalHook.
918+
if (typeof clipboardCopyText === 'function') {
919+
clipboardCopyText(text).catch(err => {});
920+
} else {
921+
navigator.clipboard.writeText(text);
922+
}
923+
}
924+
925+
export function serializeAndCopyToClipboard(value: any): void {
926+
const serializedValue = serializeToString(value);
927+
const text = serializedValue === undefined ? 'undefined' : serializedValue;
928+
929+
copyToClipboard(text);
930+
}

0 commit comments

Comments
 (0)