@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 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
@@ -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
 
@@ -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.8039fdb712.js"></script><link href="./static/css/index.c8f13abbf0.css" rel="stylesheet"></head>
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}