@axonpack/react-native-devtools-tab 0.1.0
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +359 -0
- package/dist/metro/index.cjs +435 -0
- package/dist/renderer/index.html +19 -0
- package/dist/renderer/static/css/index.c8f13abbf0.css +1 -0
- package/dist/renderer/static/js/index.8039fdb712.js +2 -0
- package/dist/renderer/static/js/index.8039fdb712.js.LICENSE.txt +39 -0
- package/package.json +73 -0
- package/src/core/constants/devtools.const.ts +24 -0
- package/src/core/constants/message.const.ts +28 -0
- package/src/core/constants/remote-op.const.ts +37 -0
- package/src/core/services/message-channel.service.ts +87 -0
- package/src/core/services/panel-channel.service.ts +29 -0
- package/src/core/services/tab-channel.service.ts +33 -0
- package/src/device/components/tab-frame.component.tsx +139 -0
- package/src/device/react-reconciler.d.ts +34 -0
- package/src/device/services/__tests__/remote-sender.test.ts +461 -0
- package/src/device/services/fusebox-transport.service.ts +106 -0
- package/src/device/services/remote-sender.service.ts +413 -0
- package/src/index.ts +175 -0
- package/src/renderer/services/__tests__/remote-receiver.test.ts +126 -0
- package/src/renderer/services/remote-receiver.service.ts +150 -0
- package/src/renderer/start-renderer.ts +80 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
"use strict";
|
|
3
|
+
// The require scope
|
|
4
|
+
var __webpack_require__ = {};
|
|
5
|
+
|
|
6
|
+
// webpack/runtime/compat_get_default_export
|
|
7
|
+
(() => {
|
|
8
|
+
// getDefaultExport function for compatibility with non-ESM modules
|
|
9
|
+
__webpack_require__.n = (module) => {
|
|
10
|
+
var getter = module && module.__esModule ?
|
|
11
|
+
() => (module['default']) :
|
|
12
|
+
() => (module);
|
|
13
|
+
__webpack_require__.d(getter, { a: getter });
|
|
14
|
+
return getter;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
})();
|
|
18
|
+
// webpack/runtime/define_property_getters
|
|
19
|
+
(() => {
|
|
20
|
+
__webpack_require__.d = (exports, getters, values) => {
|
|
21
|
+
var define = (defs, kind) => {
|
|
22
|
+
for(var key in defs) {
|
|
23
|
+
if(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) {
|
|
24
|
+
Object.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
define(getters, "get");
|
|
29
|
+
define(values, "value");
|
|
30
|
+
};
|
|
31
|
+
})();
|
|
32
|
+
// webpack/runtime/has_own_property
|
|
33
|
+
(() => {
|
|
34
|
+
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
|
35
|
+
})();
|
|
36
|
+
// webpack/runtime/make_namespace_object
|
|
37
|
+
(() => {
|
|
38
|
+
// define __esModule on exports
|
|
39
|
+
__webpack_require__.r = (exports) => {
|
|
40
|
+
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
41
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
42
|
+
}
|
|
43
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __webpack_exports__ = {};
|
|
47
|
+
// ESM COMPAT FLAG
|
|
48
|
+
__webpack_require__.r(__webpack_exports__);
|
|
49
|
+
|
|
50
|
+
// EXPORTS
|
|
51
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
52
|
+
withReactNativeDevtoolsTab: () => (/* binding */ withReactNativeDevtoolsTab)
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
;// CONCATENATED MODULE: external "node:crypto"
|
|
56
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
57
|
+
var external_node_crypto_default = /*#__PURE__*/__webpack_require__.n(external_node_crypto_namespaceObject);
|
|
58
|
+
;// CONCATENATED MODULE: external "node:fs"
|
|
59
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
60
|
+
var external_node_fs_default = /*#__PURE__*/__webpack_require__.n(external_node_fs_namespaceObject);
|
|
61
|
+
;// CONCATENATED MODULE: external "node:module"
|
|
62
|
+
const external_node_module_namespaceObject = require("node:module");
|
|
63
|
+
;// CONCATENATED MODULE: external "node:path"
|
|
64
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
65
|
+
var external_node_path_default = /*#__PURE__*/__webpack_require__.n(external_node_path_namespaceObject);
|
|
66
|
+
;// CONCATENATED MODULE: ./src/core/constants/devtools.const.ts
|
|
67
|
+
/**
|
|
68
|
+
* The one name the app's side and the dev server's side have to agree on.
|
|
69
|
+
*
|
|
70
|
+
* It names the HTTP route the panel is served from, and the domain messages are tagged with on the
|
|
71
|
+
* debugger connection. React Native's own integration uses `react-devtools` on that same channel, so
|
|
72
|
+
* having a name at all is what keeps the two apart.
|
|
73
|
+
*
|
|
74
|
+
* A constant rather than an option, because nothing makes it vary: the dispatcher is per JS runtime
|
|
75
|
+
* and each dev server has its own port, so two apps never share either. Only two independent
|
|
76
|
+
* devtools instances *inside one app* would need to differ, and there is no such case.
|
|
77
|
+
*/ const DEVTOOLS_ID = "axonpack-rn-devtools";
|
|
78
|
+
const DEVTOOLS_ROUTE = `/${DEVTOOLS_ID}-tab`;
|
|
79
|
+
/**
|
|
80
|
+
* Where the app keeps its tab list, for the frontend to read.
|
|
81
|
+
*
|
|
82
|
+
* A panel is built from what is on here, not from a message, so opening or reloading React Native
|
|
83
|
+
* DevTools finds the tabs that are already registered instead of having to be told about them again.
|
|
84
|
+
* The app has no way of knowing a frontend reloaded, and nothing re-announces, so a push alone left
|
|
85
|
+
* the tab strip empty until the app itself restarted.
|
|
86
|
+
*/ const DEVTOOLS_TABS = `__${DEVTOOLS_ID}_tabs__`;
|
|
87
|
+
|
|
88
|
+
;// CONCATENATED MODULE: ./src/metro/index.ts
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Puts a tab of your own in React Native DevTools, by serving the DevTools frontend from a route of
|
|
96
|
+
* ours and injecting one script into its entry page. Nothing is forked and nothing is patched on
|
|
97
|
+
* disk.
|
|
98
|
+
*
|
|
99
|
+
* ```js
|
|
100
|
+
* // metro.config.js
|
|
101
|
+
* const { getDefaultConfig } = require('expo/metro-config');
|
|
102
|
+
* const {
|
|
103
|
+
* withReactNativeDevtoolsTab,
|
|
104
|
+
* } = require('@axonpack/react-native-devtools-tab/metro');
|
|
105
|
+
*
|
|
106
|
+
* module.exports = withReactNativeDevtoolsTab(getDefaultConfig(__dirname));
|
|
107
|
+
* ```
|
|
108
|
+
*/ // This file has no relative imports, and must not grow any. It is the one thing here that Node
|
|
109
|
+
// loads directly, and Node's ESM resolver demands an extension on every relative specifier, which
|
|
110
|
+
// Metro cannot follow back to a `.ts`. Keeping it self-contained is what lets every other file use
|
|
111
|
+
// plain extensionless imports that Metro reads straight from source.
|
|
112
|
+
//
|
|
113
|
+
// `tsc` emits this file as ESM and Node loads it as ESM, where there is no ambient `require`. Every
|
|
114
|
+
// lookup below is for a package belonging to the running dev server, so they have to be resolved at
|
|
115
|
+
// runtime rather than imported.
|
|
116
|
+
//
|
|
117
|
+
// A CommonJS loader (Jest, and some Metro setups) leaves `import.meta.url` null, where the working
|
|
118
|
+
// directory is a fine base: nothing resolved through it lives outside the project.
|
|
119
|
+
const metro_require = (0,external_node_module_namespaceObject.createRequire)(__filename);
|
|
120
|
+
function hostScript() {
|
|
121
|
+
const ROUTE = DEVTOOLS_ROUTE;
|
|
122
|
+
const id = DEVTOOLS_ID;
|
|
123
|
+
const tabs = DEVTOOLS_TABS;
|
|
124
|
+
return `
|
|
125
|
+
import * as UI from '${ROUTE}/ui/legacy/legacy.js';
|
|
126
|
+
import * as SDK from '${ROUTE}/core/sdk/sdk.js';
|
|
127
|
+
|
|
128
|
+
const DOMAIN = ${JSON.stringify(id)};
|
|
129
|
+
const TABS = ${JSON.stringify(tabs)};
|
|
130
|
+
const DISPATCHER = '__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__';
|
|
131
|
+
|
|
132
|
+
// The frontend builds itself after DOMContentLoaded, and the tab strip is the last thing to appear.
|
|
133
|
+
// There is no event for "ready", so this waits for the element that means it.
|
|
134
|
+
const waitForFrontend = () =>
|
|
135
|
+
new Promise((resolve) => {
|
|
136
|
+
const check = () => document.querySelector('.main-tabbed-pane');
|
|
137
|
+
if (check()) return resolve();
|
|
138
|
+
const observer = new MutationObserver(() => {
|
|
139
|
+
if (check()) {
|
|
140
|
+
observer.disconnect();
|
|
141
|
+
resolve();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const getRuntime = () => {
|
|
148
|
+
const targets = SDK.TargetManager.TargetManager.instance();
|
|
149
|
+
const target = targets.primaryPageTarget() ?? targets.rootTarget();
|
|
150
|
+
return target?.model(SDK.RuntimeModel.RuntimeModel) ?? null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The app answers on a binding the dispatcher names itself, and the frontend has to ask for that
|
|
155
|
+
* binding before anything arrives. Polls because the dispatcher is installed by the app's own
|
|
156
|
+
* bundle, which may not have run yet when the panel opens.
|
|
157
|
+
*/
|
|
158
|
+
const connectToApp = async (onMessage) => {
|
|
159
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
160
|
+
const runtime = getRuntime();
|
|
161
|
+
if (runtime) {
|
|
162
|
+
const probe = await runtime.agent.invoke_evaluate({
|
|
163
|
+
expression: 'globalThis.' + DISPATCHER + ' != undefined',
|
|
164
|
+
returnByValue: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (probe.result?.value === true) {
|
|
168
|
+
const named = await runtime.agent.invoke_evaluate({
|
|
169
|
+
expression: DISPATCHER + '.BINDING_NAME',
|
|
170
|
+
});
|
|
171
|
+
const binding = named.result?.value;
|
|
172
|
+
if (binding) {
|
|
173
|
+
runtime.addEventListener('BindingCalled', (event) => {
|
|
174
|
+
if (event.data.name !== binding) return;
|
|
175
|
+
// Every domain shares this one binding, React DevTools included, and its messages carry
|
|
176
|
+
// whole component trees. Checking the raw string first skips parsing those.
|
|
177
|
+
if (!event.data.payload.includes(DOMAIN)) return;
|
|
178
|
+
const parsed = JSON.parse(event.data.payload);
|
|
179
|
+
if (parsed.domain === DOMAIN) onMessage(parsed.message);
|
|
180
|
+
});
|
|
181
|
+
await runtime.agent.invoke_addBinding({ name: binding });
|
|
182
|
+
await runtime.agent.invoke_evaluate({
|
|
183
|
+
expression: 'void ' + DISPATCHER + '.initializeDomain(' + JSON.stringify(DOMAIN) + ')',
|
|
184
|
+
});
|
|
185
|
+
// Read rather than waited for. The app announces a tab when it registers one, but a
|
|
186
|
+
// frontend that reloads has missed every announcement and nothing re-sends them, so the
|
|
187
|
+
// tab strip came back empty. Asking is also what puts the tabs up without a round trip.
|
|
188
|
+
const listed = await runtime.agent.invoke_evaluate({
|
|
189
|
+
expression: 'JSON.stringify(globalThis[' + JSON.stringify(TABS) + '] || [])',
|
|
190
|
+
returnByValue: true,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
registered: JSON.parse(listed.result?.value || '[]'),
|
|
195
|
+
send: (message) => {
|
|
196
|
+
const serialized = JSON.stringify(JSON.stringify(message));
|
|
197
|
+
void runtime.agent.invoke_evaluate({
|
|
198
|
+
expression:
|
|
199
|
+
DISPATCHER + '.sendMessage(' + JSON.stringify(DOMAIN) + ', ' + serialized + ')',
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
class TabPanel extends UI.View.SimpleView {
|
|
212
|
+
constructor(tab) {
|
|
213
|
+
// The symbol is part of the title, and written as an escape so no tool can mangle it. The
|
|
214
|
+
// frontend's own icon slots take an element, and every way of putting one there loses its
|
|
215
|
+
// drawing: the suffix slot re-renders with a shallow cloneNode(), and the leading slot only
|
|
216
|
+
// accepts a name from DevTools' own image set. Text has none of that.
|
|
217
|
+
super(tab.name + ' ' + (tab.icon || '\u235A'), true, tab.id);
|
|
218
|
+
// Keeps the iframe alive when another tab is selected, so a tab does not lose everything it has
|
|
219
|
+
// been sent every time somebody looks at Console.
|
|
220
|
+
this.setHideOnDetach();
|
|
221
|
+
|
|
222
|
+
const iframe = document.createElement('iframe');
|
|
223
|
+
iframe.src = '${ROUTE}/panel/index.html?tab=' + encodeURIComponent(tab.id);
|
|
224
|
+
iframe.style.cssText = 'width:100%;height:100%;border:0';
|
|
225
|
+
this.contentElement.appendChild(iframe);
|
|
226
|
+
this.iframe = iframe;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const main = async () => {
|
|
231
|
+
await waitForFrontend();
|
|
232
|
+
|
|
233
|
+
const inspector = UI.InspectorView.InspectorView.instance();
|
|
234
|
+
const panels = new Map();
|
|
235
|
+
|
|
236
|
+
const addPanel = (tab) => {
|
|
237
|
+
if (!tab?.id || panels.has(tab.id)) return;
|
|
238
|
+
const panel = new TabPanel(tab);
|
|
239
|
+
panels.set(tab.id, panel);
|
|
240
|
+
inspector.addPanel(panel);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const connected = await connectToApp((message) => {
|
|
244
|
+
const body = message?.data;
|
|
245
|
+
|
|
246
|
+
// Every message the app sends names the tab it is for, so this is a lookup rather than a
|
|
247
|
+
// fan-out. Broadcasting meant an app with five tabs posted five copies of every render and four
|
|
248
|
+
// of them were dropped on arrival. A message with no id has no addressee, so it still goes to
|
|
249
|
+
// everyone.
|
|
250
|
+
if (body?.id != null) {
|
|
251
|
+
panels.get(body.id)?.iframe.contentWindow?.postMessage(message, '*');
|
|
252
|
+
} else {
|
|
253
|
+
for (const panel of panels.values()) {
|
|
254
|
+
panel.iframe.contentWindow?.postMessage(message, '*');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// A tab registered after this frontend connected, so it was not in the list that was read.
|
|
259
|
+
if (message?.type === 'tab:register') addPanel(body);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!connected) {
|
|
263
|
+
console.warn('[devtools] the app never installed its devtools dispatcher');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The tabs the app already had. Each page asks for its own registration once it loads, so nothing
|
|
268
|
+
// has to be replayed at them here.
|
|
269
|
+
for (const tab of connected.registered) addPanel(tab);
|
|
270
|
+
const send = connected.send;
|
|
271
|
+
|
|
272
|
+
window.addEventListener('message', (event) => {
|
|
273
|
+
for (const panel of panels.values()) {
|
|
274
|
+
if (event.source === panel.iframe.contentWindow) {
|
|
275
|
+
send(event.data);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
main().catch((error) => console.error('[devtools] panel failed to start', error));
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
const TYPES = {
|
|
286
|
+
".html": "text/html",
|
|
287
|
+
".js": "application/javascript",
|
|
288
|
+
".mjs": "application/javascript",
|
|
289
|
+
".css": "text/css",
|
|
290
|
+
".json": "application/json",
|
|
291
|
+
".svg": "image/svg+xml",
|
|
292
|
+
".png": "image/png",
|
|
293
|
+
".woff2": "font/woff2",
|
|
294
|
+
".map": "application/json"
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Where to look for the debugger's own packages, in the order that matters.
|
|
298
|
+
*
|
|
299
|
+
* Finding *a* copy is not enough: the frontend has to be the one the dev server serves, and the
|
|
300
|
+
* middleware has to be the one it will call, or the shortcut is rewritten on an object nobody
|
|
301
|
+
* loads and DevTools opens without the tab script in it. An install can easily hold three copies,
|
|
302
|
+
* which is what happens the moment a project updates React Native and something else stays behind.
|
|
303
|
+
*
|
|
304
|
+
* So the search starts at whatever is serving: `@expo/cli` for an Expo project, the community CLI
|
|
305
|
+
* plugin for a bare one. They are what `require` these at runtime, so their answer is the right
|
|
306
|
+
* answer. The rest are fallbacks for a layout where neither is installed.
|
|
307
|
+
*/ function projectRoots(projectRoot) {
|
|
308
|
+
const roots = [
|
|
309
|
+
projectRoot
|
|
310
|
+
];
|
|
311
|
+
const found = new Map();
|
|
312
|
+
// Resolved against what has been found so far, because `@expo/cli` is usually a dependency of
|
|
313
|
+
// `expo` rather than of the app.
|
|
314
|
+
for (const from of [
|
|
315
|
+
"expo",
|
|
316
|
+
"react-native",
|
|
317
|
+
"@expo/cli",
|
|
318
|
+
"@react-native/community-cli-plugin"
|
|
319
|
+
]){
|
|
320
|
+
try {
|
|
321
|
+
const dir = external_node_path_default().dirname(metro_require.resolve(`${from}/package.json`, {
|
|
322
|
+
paths: roots
|
|
323
|
+
}));
|
|
324
|
+
roots.push(dir);
|
|
325
|
+
found.set(from, dir);
|
|
326
|
+
} catch {
|
|
327
|
+
// Not every project has every one of these. The next candidate may still resolve.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const serving = [
|
|
331
|
+
"@expo/cli",
|
|
332
|
+
"@react-native/community-cli-plugin",
|
|
333
|
+
"react-native",
|
|
334
|
+
"expo"
|
|
335
|
+
].map((name)=>found.get(name)).filter((dir)=>dir !== undefined);
|
|
336
|
+
return [
|
|
337
|
+
...serving,
|
|
338
|
+
projectRoot
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
/** Its entry exports the directory its files live in, several levels below the package root. */ function findFrontend(roots) {
|
|
342
|
+
try {
|
|
343
|
+
const exported = metro_require(metro_require.resolve("@react-native/debugger-frontend", {
|
|
344
|
+
paths: roots
|
|
345
|
+
}));
|
|
346
|
+
return typeof exported === "string" ? exported : null;
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function pointDebuggerAt(roots) {
|
|
352
|
+
try {
|
|
353
|
+
const middleware = external_node_path_default().dirname(metro_require.resolve("@react-native/dev-middleware/package.json", {
|
|
354
|
+
paths: roots
|
|
355
|
+
}));
|
|
356
|
+
const loaded = metro_require(external_node_path_default().join(middleware, "dist/utils/getDevToolsFrontendUrl"));
|
|
357
|
+
const original = loaded.default;
|
|
358
|
+
if (typeof original !== "function") return;
|
|
359
|
+
if (original.patchedBy) return;
|
|
360
|
+
const patched = (...args)=>String(original(...args)).replace("/debugger-frontend/", `${DEVTOOLS_ROUTE}/`);
|
|
361
|
+
patched.patchedBy = DEVTOOLS_ID;
|
|
362
|
+
loaded.default = patched;
|
|
363
|
+
} catch {
|
|
364
|
+
console.warn("[devtools] could not point the debugger shortcut at the tab.");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** The page ships a strict CSP, so an injected script needs a nonce allowed by it. */ function injectHost(html, base) {
|
|
368
|
+
const nonce = external_node_crypto_default().randomUUID();
|
|
369
|
+
const withNonce = html.replace(/(<meta[^>]*http-equiv="Content-Security-Policy"[^>]*content=")([^"]*)"/, (_match, head, policy)=>`${head}${policy.replace(/script-src ([^;]+)/, `script-src $1 'nonce-${nonce}'`)}"`);
|
|
370
|
+
return withNonce.replace(/<body[^>]*>/, (body)=>`<script type="module" nonce="${nonce}" src="${base}/host.js"></script>${body}`);
|
|
371
|
+
}
|
|
372
|
+
function serve(response, body, type) {
|
|
373
|
+
response.setHeader("Content-Type", type);
|
|
374
|
+
response.end(body);
|
|
375
|
+
}
|
|
376
|
+
/** Answers with a file from `root`. Checked, so a crafted path cannot read outside it. */ function sendFile(root, relative, response, transform) {
|
|
377
|
+
const file = external_node_path_default().join(root, external_node_path_default().normalize(relative));
|
|
378
|
+
if (!file.startsWith(root)) {
|
|
379
|
+
response.statusCode = 403;
|
|
380
|
+
response.end("no");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
external_node_fs_default().readFile(file, (error, contents)=>{
|
|
384
|
+
if (error) {
|
|
385
|
+
response.statusCode = 404;
|
|
386
|
+
response.end("not found");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
serve(response, transform ? transform(contents.toString("utf8")) : contents, TYPES[external_node_path_default().extname(file)] ?? "application/octet-stream");
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function withReactNativeDevtoolsTab(config, options = {}) {
|
|
393
|
+
const base = DEVTOOLS_ROUTE;
|
|
394
|
+
const projectRoot = config.projectRoot ?? process.cwd();
|
|
395
|
+
const roots = projectRoots(projectRoot);
|
|
396
|
+
const frontend = options.frontendPath ?? findFrontend(roots);
|
|
397
|
+
if (!frontend) {
|
|
398
|
+
console.warn("[devtools] React Native DevTools was not found, so no tab is served.");
|
|
399
|
+
return config;
|
|
400
|
+
}
|
|
401
|
+
// The built page shown in the tab, published with this package so it is found the same way from a
|
|
402
|
+
// checkout or from `node_modules`.
|
|
403
|
+
const renderer = external_node_path_default().join(external_node_path_default().dirname(metro_require.resolve("@axonpack/react-native-devtools-tab/package.json")), "dist/renderer");
|
|
404
|
+
pointDebuggerAt(roots);
|
|
405
|
+
/** Answers one of our routes, or reports that the request was not ours. */ const handle = (request, response)=>{
|
|
406
|
+
const url = (request.url ?? "").split("?")[0];
|
|
407
|
+
if (!url.startsWith(base)) return false;
|
|
408
|
+
const rest = url.slice(base.length) || "/";
|
|
409
|
+
if (rest === "/host.js") {
|
|
410
|
+
serve(response, hostScript(), "application/javascript");
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
// `/panel/...` is the page this package ships, everything else is the real DevTools frontend.
|
|
414
|
+
const inPanel = rest.startsWith("/panel/");
|
|
415
|
+
sendFile(inPanel ? renderer : frontend, inPanel ? rest.slice("/panel".length) : rest, response, rest === "/rn_fusebox.html" ? (html)=>injectHost(html, base) : undefined);
|
|
416
|
+
return true;
|
|
417
|
+
};
|
|
418
|
+
const previous = config.server?.enhanceMiddleware;
|
|
419
|
+
return {
|
|
420
|
+
...config,
|
|
421
|
+
server: {
|
|
422
|
+
...config.server,
|
|
423
|
+
enhanceMiddleware: (middleware, server)=>{
|
|
424
|
+
const next = previous ? previous(middleware, server) : middleware;
|
|
425
|
+
return (request, response, done)=>{
|
|
426
|
+
if (!handle(request, response)) next(request, response, done);
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = __webpack_exports__;
|
|
434
|
+
})()
|
|
435
|
+
;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
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>
|
|
7
|
+
<body>
|
|
8
|
+
<div id="root"></div>
|
|
9
|
+
<script>
|
|
10
|
+
// A panel that fails renders as a blank tab, and its errors go to the iframe's own console,
|
|
11
|
+
// which nobody thinks to open. Put them on the page instead.
|
|
12
|
+
window.addEventListener("error", function (event) {
|
|
13
|
+
document.body.textContent = String(
|
|
14
|
+
(event.error && event.error.stack) || event.message,
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -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: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)}}
|