@axonpack/react-native-devtools-tab 0.1.0 → 0.1.3-canary-7fd4757199c4f00422a2928cce37df21e99c6fac
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +35 -0
- package/dist/metro/index.cjs +52 -1
- package/dist/renderer/index.html +1 -1
- package/dist/renderer/static/css/index.8ca2a34c1f.css +1 -0
- package/dist/renderer/static/js/index.04c85e4111.js +2 -0
- package/package.json +2 -1
- package/src/core/constants/devtools.const.ts +21 -0
- package/src/core/constants/message.const.ts +3 -0
- package/src/device/components/tab-frame.component.tsx +22 -44
- package/src/device/services/__tests__/remote-sender.test.ts +8 -1
- package/src/index.ts +26 -3
- package/dist/renderer/static/css/index.c8f13abbf0.css +0 -1
- package/dist/renderer/static/js/index.8039fdb712.js +0 -2
- /package/dist/renderer/static/js/{index.8039fdb712.js.LICENSE.txt → index.04c85e4111.js.LICENSE.txt} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @axonpack/react-native-devtools-tab
|
|
2
2
|
|
|
3
|
+
## 0.1.3-canary-7fd4757199c4f00422a2928cce37df21e99c6fac
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7b21efb: - **Docs on axonpack.dev**: the links in the DevTools tab and the npm homepage go to axonpack.dev
|
|
8
|
+
|
|
9
|
+
## 0.1.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 8bf976d: - **Accents and emoji in a tab's fields**: text typed into a field that holds an é or an emoji is no longer lost
|
|
14
|
+
|
|
15
|
+
## 0.1.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 99f49a5: - **Open DevTools on your tab**: `registerTab` returns `focus()`, and the next DevTools window lands on that tab
|
|
20
|
+
- **Switch an open window to your tab**: `focus()` also brings the tab forward in a window that is already open
|
|
21
|
+
- **Copy to the computer's clipboard**: give an element `COPY_ATTRIBUTE` with the text, and a click copies it there instead of on the device
|
|
22
|
+
- **Your own right-click menu**: a tab that handles a right-click no longer gets the browser's menu on top of it
|
|
23
|
+
- **A clearer about card**: hover the mark by a tab's name for what it is and links to the docs, changelog, other libraries and GitHub
|
|
24
|
+
|
|
3
25
|
## 0.1.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -247,6 +247,41 @@ event itself, with `preventDefault` and `stopPropagation` there but doing nothin
|
|
|
247
247
|
Everything else a devtools tab usually wants needs none of that: reading the app's state, calling
|
|
248
248
|
into it, drawing a table, filtering a list.
|
|
249
249
|
|
|
250
|
+
Copying is the one thing a handler cannot do for you, since it runs in the app and lands on the
|
|
251
|
+
device's clipboard. Give the element `COPY_ATTRIBUTE` with the text instead, and the panel copies it
|
|
252
|
+
to the computer's clipboard on the click:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
import { COPY_ATTRIBUTE } from "@axonpack/react-native-devtools-tab";
|
|
256
|
+
|
|
257
|
+
<button {...{ [COPY_ATTRIBUTE]: curl }}>Copy as cURL</button>;
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A download needs nothing special: an `<a href="data:…" download="name">` is a real link in the
|
|
261
|
+
panel, and the browser saves it.
|
|
262
|
+
|
|
263
|
+
## Opening DevTools on your tab
|
|
264
|
+
|
|
265
|
+
`registerTab` returns the tab, with one method. `focus()` shows the tab in a DevTools window that
|
|
266
|
+
is already open, and in the next one that connects, so calling it just before the app opens
|
|
267
|
+
DevTools lands the window on it:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { TurboModuleRegistry } from "react-native";
|
|
271
|
+
|
|
272
|
+
const tab = ReactNativeDevtoolsPanel.registerTab({
|
|
273
|
+
name: "Session",
|
|
274
|
+
component: Session,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Later, from a button in the app, in a debug build:
|
|
278
|
+
tab.focus();
|
|
279
|
+
// React Native's own DevSettings: openDebugger is what the dev menu's Open DevTools calls.
|
|
280
|
+
TurboModuleRegistry.get<{ openDebugger?: () => void }>(
|
|
281
|
+
"DevSettings",
|
|
282
|
+
)?.openDebugger?.();
|
|
283
|
+
```
|
|
284
|
+
|
|
250
285
|
## Several tabs
|
|
251
286
|
|
|
252
287
|
Call `registerTab` once per tab. Each gets its own React root, so one tab re-rendering does not
|
package/dist/metro/index.cjs
CHANGED
|
@@ -84,6 +84,23 @@ const DEVTOOLS_ROUTE = `/${DEVTOOLS_ID}-tab`;
|
|
|
84
84
|
* The app has no way of knowing a frontend reloaded, and nothing re-announces, so a push alone left
|
|
85
85
|
* the tab strip empty until the app itself restarted.
|
|
86
86
|
*/ const DEVTOOLS_TABS = `__${DEVTOOLS_ID}_tabs__`;
|
|
87
|
+
/**
|
|
88
|
+
* The tab the next DevTools window should open on, left here by `focus()`.
|
|
89
|
+
*
|
|
90
|
+
* A note rather than a message, because the window it is for does not exist yet. `openDebugger` asks
|
|
91
|
+
* Metro for a window and cannot say which panel to show on iOS, and the frontend's own `panel`
|
|
92
|
+
* parameter is read at startup, before any tab of ours has been added. So the window reads this when
|
|
93
|
+
* it connects, the same way it reads the tab list, and clears it so only one window acts on it.
|
|
94
|
+
*/ const DEVTOOLS_FOCUS = `__${DEVTOOLS_ID}_focus__`;
|
|
95
|
+
/**
|
|
96
|
+
* The one attribute the panel acts on itself: a click on an element that has it copies its value to
|
|
97
|
+
* the clipboard of the computer DevTools runs on.
|
|
98
|
+
*
|
|
99
|
+
* A handler runs in the app, so a copy written the ordinary way lands on the device's clipboard,
|
|
100
|
+
* and a phone does not share that with the computer. The clipboard that matters is the browser's,
|
|
101
|
+
* and so is the click it needs, so the text comes as a prop and the panel does the copying. Any
|
|
102
|
+
* `onClick` on the element still runs, after the copy.
|
|
103
|
+
*/ const COPY_ATTRIBUTE = "data-devtools-copy";
|
|
87
104
|
|
|
88
105
|
;// CONCATENATED MODULE: ./src/metro/index.ts
|
|
89
106
|
|
|
@@ -121,12 +138,14 @@ function hostScript() {
|
|
|
121
138
|
const ROUTE = DEVTOOLS_ROUTE;
|
|
122
139
|
const id = DEVTOOLS_ID;
|
|
123
140
|
const tabs = DEVTOOLS_TABS;
|
|
141
|
+
const focus = DEVTOOLS_FOCUS;
|
|
124
142
|
return `
|
|
125
143
|
import * as UI from '${ROUTE}/ui/legacy/legacy.js';
|
|
126
144
|
import * as SDK from '${ROUTE}/core/sdk/sdk.js';
|
|
127
145
|
|
|
128
146
|
const DOMAIN = ${JSON.stringify(id)};
|
|
129
147
|
const TABS = ${JSON.stringify(tabs)};
|
|
148
|
+
const FOCUS = ${JSON.stringify(focus)};
|
|
130
149
|
const DISPATCHER = '__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__';
|
|
131
150
|
|
|
132
151
|
// The frontend builds itself after DOMContentLoaded, and the tab strip is the last thing to appear.
|
|
@@ -144,6 +163,14 @@ const waitForFrontend = () =>
|
|
|
144
163
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
145
164
|
});
|
|
146
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Every character past ASCII as a \\u escape, which a string literal reads back as the same text.
|
|
168
|
+
* A message goes to the app as source for Runtime.evaluate, and source holding an é or an emoji did
|
|
169
|
+
* not arrive: a keystroke in a field that held one was lost, and so was every event after it.
|
|
170
|
+
*/
|
|
171
|
+
const asciiOnly = (source) =>
|
|
172
|
+
source.replace(/[\\u007f-\\uffff]/g, (char) => '\\\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
173
|
+
|
|
147
174
|
const getRuntime = () => {
|
|
148
175
|
const targets = SDK.TargetManager.TargetManager.instance();
|
|
149
176
|
const target = targets.primaryPageTarget() ?? targets.rootTarget();
|
|
@@ -190,10 +217,19 @@ const connectToApp = async (onMessage) => {
|
|
|
190
217
|
returnByValue: true,
|
|
191
218
|
});
|
|
192
219
|
|
|
220
|
+
// Taken rather than read, so a second window opening later starts where it always does.
|
|
221
|
+
const focused = await runtime.agent.invoke_evaluate({
|
|
222
|
+
expression:
|
|
223
|
+
'(() => { const id = globalThis[' + JSON.stringify(FOCUS) + ']; delete globalThis[' +
|
|
224
|
+
JSON.stringify(FOCUS) + ']; return id ?? null; })()',
|
|
225
|
+
returnByValue: true,
|
|
226
|
+
});
|
|
227
|
+
|
|
193
228
|
return {
|
|
194
229
|
registered: JSON.parse(listed.result?.value || '[]'),
|
|
230
|
+
focus: focused.result?.value ?? null,
|
|
195
231
|
send: (message) => {
|
|
196
|
-
const serialized = JSON.stringify(JSON.stringify(message));
|
|
232
|
+
const serialized = asciiOnly(JSON.stringify(JSON.stringify(message)));
|
|
197
233
|
void runtime.agent.invoke_evaluate({
|
|
198
234
|
expression:
|
|
199
235
|
DISPATCHER + '.sendMessage(' + JSON.stringify(DOMAIN) + ', ' + serialized + ')',
|
|
@@ -222,6 +258,8 @@ class TabPanel extends UI.View.SimpleView {
|
|
|
222
258
|
const iframe = document.createElement('iframe');
|
|
223
259
|
iframe.src = '${ROUTE}/panel/index.html?tab=' + encodeURIComponent(tab.id);
|
|
224
260
|
iframe.style.cssText = 'width:100%;height:100%;border:0';
|
|
261
|
+
// So a tab can copy to this computer's clipboard. See COPY_ATTRIBUTE in the renderer.
|
|
262
|
+
iframe.allow = 'clipboard-write';
|
|
225
263
|
this.contentElement.appendChild(iframe);
|
|
226
264
|
this.iframe = iframe;
|
|
227
265
|
}
|
|
@@ -232,12 +270,23 @@ const main = async () => {
|
|
|
232
270
|
|
|
233
271
|
const inspector = UI.InspectorView.InspectorView.instance();
|
|
234
272
|
const panels = new Map();
|
|
273
|
+
// Held until the tab exists, since the app may ask for one it registers after this connects.
|
|
274
|
+
let wanted = null;
|
|
275
|
+
|
|
276
|
+
// The tab strip itself, not \`showPanel\`: that looks the id up among DevTools' own registered
|
|
277
|
+
// views, and ours are added at run time, so it only logs that it found nothing.
|
|
278
|
+
const select = (id) => {
|
|
279
|
+
if (!panels.has(id)) return false;
|
|
280
|
+
inspector.tabbedPane.selectTab(id, true);
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
235
283
|
|
|
236
284
|
const addPanel = (tab) => {
|
|
237
285
|
if (!tab?.id || panels.has(tab.id)) return;
|
|
238
286
|
const panel = new TabPanel(tab);
|
|
239
287
|
panels.set(tab.id, panel);
|
|
240
288
|
inspector.addPanel(panel);
|
|
289
|
+
if (wanted === tab.id && select(tab.id)) wanted = null;
|
|
241
290
|
};
|
|
242
291
|
|
|
243
292
|
const connected = await connectToApp((message) => {
|
|
@@ -257,6 +306,7 @@ const main = async () => {
|
|
|
257
306
|
|
|
258
307
|
// A tab registered after this frontend connected, so it was not in the list that was read.
|
|
259
308
|
if (message?.type === 'tab:register') addPanel(body);
|
|
309
|
+
if (message?.type === 'tab:focus' && !select(body?.id)) wanted = body?.id ?? null;
|
|
260
310
|
});
|
|
261
311
|
|
|
262
312
|
if (!connected) {
|
|
@@ -266,6 +316,7 @@ const main = async () => {
|
|
|
266
316
|
|
|
267
317
|
// The tabs the app already had. Each page asks for its own registration once it loads, so nothing
|
|
268
318
|
// has to be replayed at them here.
|
|
319
|
+
if (connected.focus) wanted = connected.focus;
|
|
269
320
|
for (const tab of connected.registered) addPanel(tab);
|
|
270
321
|
const send = connected.send;
|
|
271
322
|
|
package/dist/renderer/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<title>DevTools tab</title>
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="./static/js/index.
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"><script defer src="./static/js/index.04c85e4111.js"></script><link href="./static/css/index.8ca2a34c1f.css" rel="stylesheet"></head>
|
|
7
7
|
<body>
|
|
8
8
|
<div id="root"></div>
|
|
9
9
|
<script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bar-height:26px;--bg:#1e1e1e;--fg:#ddd;--bar:#282828;--line:#3c3c3c;--hover:#ffffff1a;--pop:#2f2f2f;--muted:#9aa0a6;--link:#6ba5ff;--logo:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 400 400%27%3E%3Cg fill=%27none%27 stroke-width=%2760%27 stroke-linecap=%27round%27%3E%3Cpath d=%27M199 112 95 287%27 stroke=%27%230c63e9%27/%3E%3Cpath d=%27M243 165 304 287%27 stroke=%27%23f4f6f8%27/%3E%3C/g%3E%3Ccircle cx=%27201%27 cy=%27264%27 r=%2729%27 fill=%27%230c63e9%27/%3E%3C/svg%3E");--refresh:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Cpath d=%27M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z%27/%3E%3C/svg%3E")}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}@media (prefers-color-scheme:light){:root{--bg:#fff;--fg:#202020;--bar:#ececec;--line:#d0d0d0;--hover:#0000000d;--pop:#fff;--muted:#5f6368;--link:#0c63e9;--logo:url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 400 400%27%3E%3Cg fill=%27none%27 stroke-width=%2760%27 stroke-linecap=%27round%27%3E%3Cpath d=%27M199 112 95 287%27 stroke=%27%230c63e9%27/%3E%3Cpath d=%27M243 165 304 287%27 stroke=%27%23000000%27/%3E%3C/g%3E%3Ccircle cx=%27201%27 cy=%27264%27 r=%2729%27 fill=%27%230c63e9%27/%3E%3C/svg%3E")}}html,body,#root{height:100%}#root{flex-direction:column;display:flex}.axonpack-tab-body{flex:1;min-height:0;overflow:auto}body{background:var(--bg);color:var(--fg);margin:0;font:12px ui-monospace,SFMono-Regular,Menlo,monospace}.axonpack-tab-bar{height:var(--bar-height);background:var(--bar);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:8px;padding:0 4px 0 8px;font:500 12px system-ui,sans-serif;display:flex}.axonpack-tab-name{align-items:center;gap:6px;display:flex}.axonpack-tab-brand{background:var(--logo) center / contain no-repeat;width:14px;height:14px;position:relative}.axonpack-tab-about{z-index:1000;border:1px solid var(--line);background:var(--pop);width:264px;color:var(--muted);opacity:0;visibility:hidden;border-radius:4px;justify-items:start;gap:8px;padding:12px 13px;font-size:12px;font-weight:400;line-height:1.5;transition:opacity .12s,transform .12s,visibility 0s .12s;display:grid;position:absolute;top:calc(100% + 5px);left:-6px;transform:translateY(-3px);box-shadow:0 2px 8px #0006}.axonpack-tab-about:before{content:"";position:absolute;inset:-6px 0 100%}.axonpack-tab-brand:hover .axonpack-tab-about,.axonpack-tab-brand:focus-within .axonpack-tab-about{opacity:1;visibility:visible;transition-delay:0s;transform:none}.axonpack-tab-about b{color:var(--fg);font-size:12px;font-weight:600}.axonpack-tab-card{box-sizing:border-box;border:1px solid var(--line);border-radius:4px;grid-template-columns:22px 1fr;align-items:center;gap:1px 9px;width:100%;padding:9px 10px;text-decoration:none;display:grid}.axonpack-tab-card:hover{background:var(--hover)}.axonpack-tab-card-mark{background:var(--logo) center / contain no-repeat;grid-row:span 2;width:22px;height:22px}.axonpack-tab-card-slogan{color:var(--muted);grid-column:2}.axonpack-tab-links{border-top:1px solid var(--line);flex-wrap:wrap;gap:4px 14px;width:100%;padding-top:9px;display:flex}.axonpack-tab-links a{color:var(--link);font-weight:500;text-decoration:none}.axonpack-tab-links a:after{content:" ↗"}.axonpack-tab-links a:hover{text-decoration:underline}.axonpack-tab-bar button{width:20px;height:20px;color:inherit;cursor:pointer;background:0 0;border:0;border-radius:4px;place-items:center;padding:0;display:grid}.axonpack-tab-bar button:hover{background:var(--hover)}.axonpack-tab-bar button:before{content:"";width:14px;height:14px;-webkit-mask:var(--refresh) center / contain no-repeat;-webkit-mask:var(--refresh) center / contain no-repeat;-webkit-mask:var(--refresh) center / contain no-repeat;mask:var(--refresh) center / contain no-repeat;background:currentColor}.axonpack-tab-loading{height:100%;color:var(--muted);place-content:center;justify-items:center;gap:12px;display:grid}.axonpack-tab-loading-glyph{border:2px solid var(--line);border-top-color:var(--link);border-radius:50%;width:24px;height:24px;animation:.7s linear infinite axonpack-tab-spin}@keyframes axonpack-tab-spin{to{transform:rotate(1turn)}}.axonpack-tab-toast{z-index:1001;border:1px solid var(--line);background:var(--pop);max-width:calc(100% - 24px);color:var(--fg);pointer-events:none;border-radius:4px;padding:6px 12px;font:12px system-ui,sans-serif;position:fixed;bottom:12px;left:50%;translate:-50%;box-shadow:0 2px 8px #0006}.axonpack-tab-toast[data-failed]{color:#f28b82}
|