Files
react/packages/react-devtools-core/src/standalone.js
T
Brian Vaughn 1e8aa8105a Re-enable "view source" button for standalone shell
But only do this if we can verify the element file path. This hopefully avoids the case where clicking the button does nothing because of an invalid/incomplete path.
2019-07-25 09:28:45 -07:00

308 lines
7.7 KiB
JavaScript

// @flow
import { createElement } from 'react';
import {
// $FlowFixMe Flow does not yet know about flushSync()
flushSync,
// $FlowFixMe Flow does not yet know about createRoot()
unstable_createRoot as createRoot,
} from 'react-dom';
import Bridge from 'src/bridge';
import Store from 'src/devtools/store';
import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
import { Server } from 'ws';
import { existsSync, readFileSync } from 'fs';
import { installHook } from 'src/hook';
import DevTools from 'src/devtools/views/DevTools';
import { doesFilePathExist, launchEditor } from './editor';
import { __DEBUG__ } from 'src/constants';
import type { FrontendBridge } from 'src/bridge';
import type { InspectedElement } from 'src/devtools/views/Components/types';
installHook(window);
export type StatusListener = (message: string) => void;
let node: HTMLElement = ((null: any): HTMLElement);
let nodeWaitingToConnectHTML: string = '';
let projectRoots: Array<string> = [];
let statusListener: StatusListener = (message: string) => {};
function setContentDOMNode(value: HTMLElement) {
node = value;
// Save so we can restore the exact waiting message between sessions.
nodeWaitingToConnectHTML = node.innerHTML;
return DevtoolsUI;
}
function setProjectRoots(value: Array<string>) {
projectRoots = value;
}
function setStatusListener(value: StatusListener) {
statusListener = value;
return DevtoolsUI;
}
let bridge: FrontendBridge | null = null;
let store: Store | null = null;
let root = null;
const log = (...args) => console.log('[React DevTools]', ...args);
log.warn = (...args) => console.warn('[React DevTools]', ...args);
log.error = (...args) => console.error('[React DevTools]', ...args);
function debug(methodName: string, ...args) {
if (__DEBUG__) {
console.log(
`%c[core/standalone] %c${methodName}`,
'color: teal; font-weight: bold;',
'font-weight: bold;',
...args
);
}
}
function safeUnmount() {
flushSync(() => {
if (root !== null) {
root.unmount();
}
});
root = null;
}
function reload() {
safeUnmount();
node.innerHTML = '';
setTimeout(() => {
root = createRoot(node);
root.render(
createElement(DevTools, {
bridge: ((bridge: any): FrontendBridge),
canViewElementSourceFunction,
showTabBar: true,
store: ((store: any): Store),
warnIfLegacyBackendDetected: true,
viewElementSourceFunction,
})
);
}, 100);
}
function canViewElementSourceFunction(
inspectedElement: InspectedElement
): boolean {
if (
inspectedElement.canViewSource === false ||
inspectedElement.source === null
) {
return false;
}
const { source } = inspectedElement;
return doesFilePathExist(source.fileName, projectRoots);
}
function viewElementSourceFunction(
id: number,
inspectedElement: InspectedElement
): void {
const { source } = inspectedElement;
if (source !== null) {
launchEditor(source.fileName, source.lineNumber, projectRoots);
} else {
log.error('Cannot inspect element', id);
}
}
function onDisconnected() {
safeUnmount();
node.innerHTML = nodeWaitingToConnectHTML;
}
function onError({ code, message }) {
safeUnmount();
if (code === 'EADDRINUSE') {
node.innerHTML = `<div id="waiting"><h2>Another instance of DevTools is running</h2></div>`;
} else {
node.innerHTML = `<div id="waiting"><h2>Unknown error (${message})</h2></div>`;
}
}
function initialize(socket: WebSocket) {
const listeners = [];
socket.onmessage = event => {
let data;
try {
if (typeof event.data === 'string') {
data = JSON.parse(event.data);
if (__DEBUG__) {
debug('WebSocket.onmessage', data);
}
} else {
throw Error();
}
} catch (e) {
log.error('Failed to parse JSON', event.data);
return;
}
listeners.forEach(fn => {
try {
fn(data);
} catch (error) {
log.error('Error calling listener', data);
throw error;
}
});
};
bridge = new Bridge({
listen(fn) {
listeners.push(fn);
return () => {
const index = listeners.indexOf(fn);
if (index >= 0) {
listeners.splice(index, 1);
}
};
},
send(event: string, payload: any, transferable?: Array<any>) {
if (socket.readyState === socket.OPEN) {
socket.send(JSON.stringify({ event, payload }));
}
},
});
((bridge: any): FrontendBridge).addListener('shutdown', () => {
socket.close();
});
store = new Store(bridge, { supportsNativeInspection: false });
log('Connected');
reload();
}
let startServerTimeoutID: TimeoutID | null = null;
function connectToSocket(socket: WebSocket) {
socket.onerror = err => {
onDisconnected();
log.error('Error with websocket connection', err);
};
socket.onclose = () => {
onDisconnected();
log('Connection to RN closed');
};
initialize(socket);
return {
close: function() {
onDisconnected();
},
};
}
function startServer(port?: number = 8097) {
const httpServer = require('http').createServer();
const server = new Server({ server: httpServer });
let connected: WebSocket | null = null;
server.on('connection', (socket: WebSocket) => {
if (connected !== null) {
connected.close();
log.warn(
'Only one connection allowed at a time.',
'Closing the previous connection'
);
}
connected = socket;
socket.onerror = error => {
connected = null;
onDisconnected();
log.error('Error with websocket connection', error);
};
socket.onclose = () => {
connected = null;
onDisconnected();
log('Connection to RN closed');
};
initialize(socket);
});
server.on('error', event => {
onError(event);
log.error('Failed to start the DevTools server', event);
startServerTimeoutID = setTimeout(() => startServer(port), 1000);
});
httpServer.on('request', (request, response) => {
// NPM installs should read from node_modules,
// But local dev mode needs to use a relative path.
const basePath = existsSync('./node_modules/react-devtools-core')
? 'node_modules/react-devtools-core'
: '../react-devtools-core';
// Serve a file that immediately sets up the connection.
const backendFile = readFileSync(`${basePath}/dist/backend.js`);
// The renderer interface doesn't read saved component filters directly,
// because they are generally stored in localStorage within the context of the extension.
// Because of this it relies on the extension to pass filters, so include them wth the response here.
// This will ensure that saved filters are shared across different web pages.
const savedPreferencesString = `
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
getSavedComponentFilters()
)};
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
getAppendComponentStack()
)};`;
response.end(
savedPreferencesString +
'\n;' +
backendFile.toString() +
'\n;' +
'ReactDevToolsBackend.connectToDevTools();'
);
});
httpServer.on('error', event => {
onError(event);
statusListener('Failed to start the server.');
startServerTimeoutID = setTimeout(() => startServer(port), 1000);
});
httpServer.listen(port, () => {
statusListener('The server is listening on the port ' + port + '.');
});
return {
close: function() {
connected = null;
onDisconnected();
clearTimeout(startServerTimeoutID);
server.close();
httpServer.close();
},
};
}
const DevtoolsUI = {
connectToSocket,
setContentDOMNode,
setProjectRoots,
setStatusListener,
startServer,
};
export default DevtoolsUI;