@axonpack/react-native-devtools-tab 0.1.0 → 0.1.2
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 +16 -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 +21 -43
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axonpack/react-native-devtools-tab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Put your own tab in React Native DevTools, with a two-way channel to the running app",
|
|
5
5
|
"author": "Md Asadujjaman <abappi2019@gmail.com> (https://github.com/abappi19)",
|
|
6
6
|
"keywords": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"debugging"
|
|
12
12
|
],
|
|
13
13
|
"license": "MIT",
|
|
14
|
+
"homepage": "https://axonpack.github.io/docs/react-native-devtools-tab/",
|
|
14
15
|
"repository": {
|
|
15
16
|
"type": "git",
|
|
16
17
|
"url": "https://github.com/axonpack/axonpack.git",
|
|
@@ -22,3 +22,24 @@ export const DEVTOOLS_ROUTE = `/${DEVTOOLS_ID}-tab`;
|
|
|
22
22
|
* the tab strip empty until the app itself restarted.
|
|
23
23
|
*/
|
|
24
24
|
export const DEVTOOLS_TABS = `__${DEVTOOLS_ID}_tabs__`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The tab the next DevTools window should open on, left here by `focus()`.
|
|
28
|
+
*
|
|
29
|
+
* A note rather than a message, because the window it is for does not exist yet. `openDebugger` asks
|
|
30
|
+
* Metro for a window and cannot say which panel to show on iOS, and the frontend's own `panel`
|
|
31
|
+
* parameter is read at startup, before any tab of ours has been added. So the window reads this when
|
|
32
|
+
* it connects, the same way it reads the tab list, and clears it so only one window acts on it.
|
|
33
|
+
*/
|
|
34
|
+
export const DEVTOOLS_FOCUS = `__${DEVTOOLS_ID}_focus__`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The one attribute the panel acts on itself: a click on an element that has it copies its value to
|
|
38
|
+
* the clipboard of the computer DevTools runs on.
|
|
39
|
+
*
|
|
40
|
+
* A handler runs in the app, so a copy written the ordinary way lands on the device's clipboard,
|
|
41
|
+
* and a phone does not share that with the computer. The clipboard that matters is the browser's,
|
|
42
|
+
* and so is the click it needs, so the text comes as a prop and the panel does the copying. Any
|
|
43
|
+
* `onClick` on the element still runs, after the copy.
|
|
44
|
+
*/
|
|
45
|
+
export const COPY_ATTRIBUTE = "data-devtools-copy";
|
|
@@ -14,6 +14,9 @@ export const REGISTER = "tab:register";
|
|
|
14
14
|
/** App to panel, on every render: what React just changed. */
|
|
15
15
|
export const MUTATE = "tab:mutate";
|
|
16
16
|
|
|
17
|
+
/** App to panel: show this tab, for a DevTools window that is already open. */
|
|
18
|
+
export const FOCUS = "tab:focus";
|
|
19
|
+
|
|
17
20
|
/** Panel to app: something in the tab was clicked, typed in, or otherwise acted on. */
|
|
18
21
|
export const ACTION = "tab:action";
|
|
19
22
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createElement,
|
|
3
|
-
useEffect,
|
|
4
3
|
useReducer,
|
|
5
|
-
useState,
|
|
6
4
|
useTransition,
|
|
7
5
|
type ComponentType,
|
|
8
6
|
type ReactNode,
|
|
@@ -22,9 +20,15 @@ import {
|
|
|
22
20
|
* Styled by class, with the rules in `renderer/renderer.css`, so the bar follows the panel's light
|
|
23
21
|
* and dark instead of carrying colours picked in the app.
|
|
24
22
|
*/
|
|
25
|
-
const DOCS = "https://axonpack.github.io/docs";
|
|
26
23
|
const HOME = "https://axonpack.github.io";
|
|
27
|
-
const
|
|
24
|
+
const GUIDE = "https://axonpack.github.io/docs/react-native-devtools-tab";
|
|
25
|
+
|
|
26
|
+
const LINKS = [
|
|
27
|
+
{ label: "Read the docs", href: GUIDE },
|
|
28
|
+
{ label: "Changelog", href: `${GUIDE}/changelog` },
|
|
29
|
+
{ label: "Other libraries", href: "https://axonpack.github.io/docs" },
|
|
30
|
+
{ label: "GitHub", href: "https://github.com/axonpack/axonpack" },
|
|
31
|
+
];
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* How long a refresh waits before the tab is drawn again.
|
|
@@ -35,35 +39,6 @@ const REPO = "https://api.github.com/repos/axonpack/axonpack";
|
|
|
35
39
|
*/
|
|
36
40
|
const RENDER_DELAY = 600;
|
|
37
41
|
|
|
38
|
-
/**
|
|
39
|
-
* The star count, or null until it arrives and for good if it never does.
|
|
40
|
-
*
|
|
41
|
-
* The request is kept at module level rather than per component: every tab's bar draws this card, so
|
|
42
|
-
* an app with four tabs would otherwise ask GitHub four times for the same number, against a limit
|
|
43
|
-
* of sixty an hour for an unauthenticated caller. It runs in the app, which is where a `fetch` is,
|
|
44
|
-
* and only once a panel has asked for the tab, so an app nobody is debugging never makes it.
|
|
45
|
-
*/
|
|
46
|
-
let counted: Promise<number | null> | undefined;
|
|
47
|
-
|
|
48
|
-
function useStars(): number | null {
|
|
49
|
-
const [stars, setStars] = useState<number | null>(null);
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
counted ??= fetch(REPO)
|
|
53
|
-
.then((response) => response.json())
|
|
54
|
-
.then((repo: { stargazers_count?: number }) =>
|
|
55
|
-
typeof repo.stargazers_count === "number"
|
|
56
|
-
? repo.stargazers_count
|
|
57
|
-
: null,
|
|
58
|
-
)
|
|
59
|
-
.catch(() => null);
|
|
60
|
-
|
|
61
|
-
void counted.then(setStars);
|
|
62
|
-
}, []);
|
|
63
|
-
|
|
64
|
-
return stars;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
42
|
export function TabFrame({
|
|
68
43
|
name,
|
|
69
44
|
component,
|
|
@@ -73,7 +48,6 @@ export function TabFrame({
|
|
|
73
48
|
}): ReactNode {
|
|
74
49
|
const [, refresh] = useReducer((n: number) => n + 1, 0);
|
|
75
50
|
const [isLoading, startTransition] = useTransition();
|
|
76
|
-
const stars = useStars();
|
|
77
51
|
|
|
78
52
|
const handleRefresh = () => {
|
|
79
53
|
startTransition(async () => {
|
|
@@ -91,7 +65,8 @@ export function TabFrame({
|
|
|
91
65
|
<span className="axonpack-tab-about">
|
|
92
66
|
<b>{name}</b>
|
|
93
67
|
<span>
|
|
94
|
-
|
|
68
|
+
Drawn by your app, not by DevTools. It runs inside the app, so
|
|
69
|
+
what it shows is the app's live state.
|
|
95
70
|
</span>
|
|
96
71
|
<a
|
|
97
72
|
className="axonpack-tab-card"
|
|
@@ -101,18 +76,21 @@ export function TabFrame({
|
|
|
101
76
|
>
|
|
102
77
|
<span className="axonpack-tab-card-mark" />
|
|
103
78
|
<b>Axonpack</b>
|
|
104
|
-
<span className="axonpack-tab-card-stars">
|
|
105
|
-
{stars === null ? "" : `\u2605 ${stars}`}
|
|
106
|
-
</span>
|
|
107
79
|
<span className="axonpack-tab-card-slogan">
|
|
108
|
-
Free, open source
|
|
109
|
-
Expo.
|
|
80
|
+
Free, open source libraries for React Native and Expo.
|
|
110
81
|
</span>
|
|
111
82
|
</a>
|
|
112
83
|
<span className="axonpack-tab-links">
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
84
|
+
{LINKS.map((link) => (
|
|
85
|
+
<a
|
|
86
|
+
key={link.href}
|
|
87
|
+
href={link.href}
|
|
88
|
+
target="_blank"
|
|
89
|
+
rel="noreferrer"
|
|
90
|
+
>
|
|
91
|
+
{link.label}
|
|
92
|
+
</a>
|
|
93
|
+
))}
|
|
116
94
|
</span>
|
|
117
95
|
</span>
|
|
118
96
|
</span>
|
|
@@ -80,7 +80,14 @@ function mount() {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// Turns of the event loop, not a clock. Everything waited on here is queued on this same loop, so
|
|
84
|
+
// a turn always runs behind the hop it waits for. A timer does not: on a starved CI runner it fired
|
|
85
|
+
// before the chain had finished.
|
|
86
|
+
const settle = async () => {
|
|
87
|
+
for (let turn = 0; turn < 50; turn++) {
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
84
91
|
|
|
85
92
|
test("a component's hooks run in the app and its DOM appears in the panel", async () => {
|
|
86
93
|
const { container, sender, pressed, click } = mount();
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { createElement, type ComponentType } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DEVTOOLS_FOCUS,
|
|
5
|
+
DEVTOOLS_ID,
|
|
6
|
+
DEVTOOLS_TABS,
|
|
7
|
+
} from "./core/constants/devtools.const";
|
|
4
8
|
import {
|
|
5
9
|
ACTION,
|
|
10
|
+
FOCUS,
|
|
6
11
|
HELLO,
|
|
7
12
|
MUTATE,
|
|
8
13
|
REGISTER,
|
|
@@ -55,6 +60,8 @@ function idFor(name: string): string {
|
|
|
55
60
|
return id;
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
export { COPY_ATTRIBUTE } from "./core/constants/devtools.const";
|
|
64
|
+
|
|
58
65
|
export type TabOptions = {
|
|
59
66
|
/** The label in the DevTools tab strip, and what the tab's id is built from. */
|
|
60
67
|
name: string;
|
|
@@ -118,10 +125,19 @@ export type TabOptions = {
|
|
|
118
125
|
*/
|
|
119
126
|
export type ReactNativeDevtoolsPanel = {
|
|
120
127
|
/** Adds a tab to React Native DevTools. Call it once per tab, as many times as you have tabs. */
|
|
121
|
-
registerTab: (options: TabOptions) =>
|
|
128
|
+
registerTab: (options: TabOptions) => RegisteredTab;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type RegisteredTab = {
|
|
132
|
+
/**
|
|
133
|
+
* Shows this tab in React Native DevTools: now, in a window that is already open, and in the next
|
|
134
|
+
* window that connects. Call it just before opening DevTools from the app, for example with
|
|
135
|
+
* `DevSettings`' `openDebugger`, so the window lands here instead of on Console.
|
|
136
|
+
*/
|
|
137
|
+
focus: () => void;
|
|
122
138
|
};
|
|
123
139
|
|
|
124
|
-
function registerTab(options: TabOptions):
|
|
140
|
+
function registerTab(options: TabOptions): RegisteredTab {
|
|
125
141
|
const id = idFor(options.name);
|
|
126
142
|
const tab = createTabChannel(channel, id);
|
|
127
143
|
let sender: RemoteSender | null = null;
|
|
@@ -168,6 +184,13 @@ function registerTab(options: TabOptions): void {
|
|
|
168
184
|
const event = payload as TabAction;
|
|
169
185
|
sender?.dispatch(event.action, event.payload);
|
|
170
186
|
});
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
focus: () => {
|
|
190
|
+
(globalThis as Record<string, unknown>)[DEVTOOLS_FOCUS] = id;
|
|
191
|
+
tab.send(FOCUS);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
171
194
|
}
|
|
172
195
|
|
|
173
196
|
export const ReactNativeDevtoolsPanel: ReactNativeDevtoolsPanel = {
|
|
@@ -1 +0,0 @@
|
|
|
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:1;border:1px solid var(--line);background:var(--pop);width:264px;color:var(--muted);opacity:0;visibility:hidden;border-radius:8px;justify-items:start;gap:8px;padding:12px 13px;font-size:11px;font-weight:400;line-height:1.55;transition:opacity .12s,transform .12s,visibility 0s .12s;display:grid;position:absolute;top:calc(100% + 5px);left:-6px;transform:translateY(-3px);box-shadow:0 10px 28px #00000059}.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:6px;grid-template-columns:22px 1fr auto;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-stars{color:var(--muted)}.axonpack-tab-card-slogan{color:var(--muted);grid-column:2/span 2}.axonpack-tab-links{border-top:1px solid var(--line);gap: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)}}
|