This commit is contained in:
2019-08-23 00:12:49 +02:00
committed by GitHub
parent c30e9f27a5
commit fd3a2bc24f
94 changed files with 13855 additions and 9269 deletions

6
packages/lib/.npmignore Normal file
View File

@@ -0,0 +1,6 @@
/babel.config.js
/node_modules
/__mocks__
/test
/src
/jest.config.js

1
packages/lib/.npmrc Normal file
View File

@@ -0,0 +1 @@
@morten-olsen:registry=https://npm.pkg.github.com/

40
packages/lib/CHANGELOG.md Normal file
View File

@@ -0,0 +1,40 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.0.0-alpha.7](https://github.com/morten-olsen/react-native-debugger/compare/v2.0.0-alpha.6...v2.0.0-alpha.7) (2019-07-05)
**Note:** Version bump only for package react-native-debug-console
# [2.0.0-alpha.6](https://github.com/morten-olsen/react-native-debugger/compare/v2.0.0-alpha.5...v2.0.0-alpha.6) (2019-07-05)
**Note:** Version bump only for package react-native-debug-console
# [2.0.0-alpha.5](https://github.com/morten-olsen/react-native-debugger/compare/v2.0.0-alpha.4...v2.0.0-alpha.5) (2019-07-05)
**Note:** Version bump only for package react-native-debug-console
# [2.0.0-alpha.4](https://github.com/morten-olsen/react-native-debugger/compare/v2.0.0-alpha.3...v2.0.0-alpha.4) (2019-07-05)
**Note:** Version bump only for package react-native-debug-console
# [2.0.0-alpha.3](https://github.com/morten-olsen/react-native-debugger/compare/v2.0.0-alpha.2...v2.0.0-alpha.3) (2019-07-05)
**Note:** Version bump only for package react-native-debug-console

2
packages/lib/__mocks__/react.js vendored Normal file
View File

@@ -0,0 +1,2 @@
const React = require('react')
module.exports = { ...React, useEffect: React.useLayoutEffect }

View File

@@ -0,0 +1,3 @@
const config = require('../../babel.config');
module.exports = config;

View File

@@ -0,0 +1,3 @@
const config = require('../../jest.config');
module.exports = config;

25
packages/lib/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "react-native-debug-console",
"version": "2.0.0-alpha.9",
"main": "lib/index.js",
"react-native": "lib/native.js",
"repository": {
"url": "https://github.com/morten-olsen/react-native-debugger"
},
"scripts": {
"test": "jest",
"bundle": "babel --copy-files --extensions '.ts,.tsx,.js' -d lib src",
"prepublish": "yarn run bundle"
},
"license": "MIT",
"dependencies": {
"react-native-json-tree": "^1.2.0",
"styled-components": "^5.0.0-beta.8"
},
"peerDependencies": {
"prop-types": "^15.6.0",
"react": "^16.8.3",
"react-native": "^0.59.8",
"react-native-webview": "^6.9.0"
}
}

View File

@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import {
StyleSheet,
View,
TextInput,
Platform,
} from 'react-native';
import styled from 'styled-components/native';
import { createContext } from '../../../console';
import Icon from '../../base/Icon';
const Button = styled.TouchableOpacity`
padding: 12px 8px;
`;
const styles = StyleSheet.create({
container: {
borderColor: '#ccc',
borderTopWidth: 1,
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 10,
paddingRight: 10,
},
input: {
flex: 1,
fontFamily: 'Menlo-Regular',
borderColor: '#ccc',
borderRadius: 5,
margin: 10,
padding: 5,
},
});
const Input = ({
provider,
context: baseContext,
}) => {
const [text, setText] = useState('');
const [history, setHistory] = useState([]);
const [historyOffset, setHistoryOffset] = useState();
const send = () => {
const newHistory = [...history, text];
const context = createContext({
logProvider: provider,
}, baseContext);
const contextKeys = Object.keys(context);
const contextValues = Object.values(context);
const fn = new Function(...contextKeys, text);
try {
fn(...contextValues);
setText('');
setHistoryOffset(undefined);
setHistory(newHistory);
} catch (err) {
provider.error([err]);
}
};
return (
<View style={styles.container}>
<Button
onPress={() => {
let currentOffset = typeof historyOffset === 'undefined' ? -1 : historyOffset;
currentOffset += 1;
const index = history.length - 1 - currentOffset;
if (history[index]) {
setText(history[index]);
setHistoryOffset(currentOffset);
}
}}
>
<Icon name="left" />
</Button>
<Button
title=">"
onPress={() => {
let currentOffset = typeof historyOffset === 'undefined' ? -1 : historyOffset;
currentOffset -= 1;
const index = history.length - 1 - currentOffset;
if (history[index]) {
setText(history[index]);
setHistoryOffset(currentOffset);
}
}}
>
<Icon name="right" />
</Button>
<TextInput
multiline
placeholder="{your code here}"
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
value={text}
onChangeText={text => setText(text)}
onKeyPress={(evt) => {
global.proxyConsole.log(Platform.OS === 'web' && evt.key === 'Enter' && evt.shiftKey);
if (Platform.OS === 'web' && evt.key === 'Enter' && !evt.shiftKey) {
send();
return false;
}
}}
/>
<Button
onPress={() => send()}
>
<Icon name="play" />
</Button>
</View>
);
};
export default Input;

View File

@@ -0,0 +1,133 @@
import React, { Fragment, createRef } from 'react';
import styled from 'styled-components/native';
import JSONTree from 'react-native-json-tree';
import {
ScrollView
} from 'react-native';
import {
Body,
Emphasis,
Fixed,
} from '../../base/text';
const theme = {
scheme: 'bright',
author: 'chris kempson (http://chriskempson.com)',
base00: '#000000',
base01: '#303030',
base02: '#505050',
base03: '#b0b0b0',
base04: '#d0d0d0',
base05: '#e0e0e0',
base06: '#f5f5f5',
base07: '#ffffff',
base08: '#fb0120',
base09: '#fc6d24',
base0A: '#fda331',
base0B: '#a1c659',
base0C: '#76c7b7',
base0D: '#6fb3d2',
base0E: '#d381c3',
base0F: '#be643c'
};
const Wrapper = styled.View``;
export const List = styled.View`
padding-left: 10px;
border-left-width: 10px;
border-color: ${props => props.color || 'black' }
`;
export const Row = styled.View`
margin: 10px;
`;
const getColor = (type) => {
if (type === 'error') {
return 'red';
}
if (type === 'warn') {
return 'yellow';
}
if (type === 'verbose') {
return 'gray';
}
return;
}
const formatData = (data, options) => {
const {
includeStackTrace,
} = options;
if (typeof data === 'undefined') {
return <Fixed>undefined</Fixed>;
}
if (data instanceof Error) {
if (includeStackTrace) {
return (
<JSONTree
theme={theme}
data={{
message: data.toString(),
stackTrace: data.stack ? data.stack.toString() : undefined,
}}
/>
);
} else {
return <Fixed selectable={true}>{data.toString()}</Fixed>;
}
}
if (typeof data === 'object') {
return <JSONTree data={data} />
}
return <Fixed selectable={true}>{data.toString()}</Fixed>;
}
const OutputList = ({
items,
color,
includeStackTrace,
}) => (
<List color={color}>
{items.map((data, i) => (
<Fragment key={i}>
{formatData(data, {
includeStackTrace,
})}
</Fragment>
))}
</List>
)
const Console = ({
logs,
includeStackTrace,
filter = [],
}) => {
// const ref = createRef();
return (
<ScrollView
onContentSizeChange={(contentWidth, contentHeight)=>{
/*if (ref.current) {
ref.current.scrollView.scrollToEnd({animated: true});
}*/
}}
>
<Wrapper>
{logs.filter(l => filter.includes(l.type)).map((log, i) => (
<Row key={i}>
<Emphasis color={getColor(log.type)}>
{log.type}
</Emphasis>
<OutputList
items={log.data}
includeStackTrace={includeStackTrace}
color={getColor(log.type)}
/>
</Row>
))}
</Wrapper>
</ScrollView>
);
};
export default Console;

View File

@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import {
StyleSheet,
View,
} from 'react-native';
import useLog from '../../data/log';
import Toolbar, {
Button,
Selector,
Seperator,
} from '../../base/Toolbar';
import Output from './Output';
import Input from './Input';
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
const initFilters = [
'error',
'warn',
'info',
'debug',
].map(i => ({
name: i,
value: i,
selected: true,
}))
const Console = ({
includeStackTrace,
provider,
context,
}) => {
const logs = useLog(provider);
const [filters, setFilters] = useState(initFilters);
return (
<View style={styles.container}>
<Toolbar>
<Selector
name="Filter"
icon="filter"
options={filters}
multiSelect
onSelect={(selected) => {
setFilters([...selected]);
}}
/>
<Seperator />
<Button
name="Clear"
icon="trash"
onPress={() => provider.clear()}
/>
</Toolbar>
<Output filter={filters.filter(f => f.selected).map(f => f.name)} logs={logs} includeStackTrace={includeStackTrace} />
<Input provider={provider} context={context} />
</View>
);
};
export default Console;

View File

@@ -0,0 +1,94 @@
var DEFAULT_MAX_DEPTH = 3;
var DEFAULT_ARRAY_MAX_LENGTH = 50;
var seen; // Same variable used for all stringifications
Date.prototype.toPrunedJSON = Date.prototype.toJSON;
String.prototype.toPrunedJSON = String.prototype.toJSON;
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
};
function quote(string) {
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string'
? c
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder, depthDecr, arrayMaxLength) {
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
partial,
value = holder[key];
if (value && typeof value === 'object' && typeof value.toPrunedJSON === 'function') {
value = value.toPrunedJSON(key);
}
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
return String(value);
case 'object':
if (!value) {
return 'null';
}
if (depthDecr<=0 || seen.indexOf(value)!==-1) {
return '"-pruned-"';
}
seen.push(value);
partial = [];
if (Object.prototype.toString.apply(value) === '[object Array]') {
length = Math.min(value.length, arrayMaxLength);
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value, depthDecr-1, arrayMaxLength) || 'null';
}
v = partial.length === 0
? '[]'
: '[' + partial.join(',') + ']';
return v;
}
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
try {
v = str(k, value, depthDecr-1, arrayMaxLength);
if (v) partial.push(quote(k) + ':' + v);
} catch (e) {
// this try/catch due to some "Accessing selectionEnd on an input element that cannot have a selection." on Chrome
}
}
}
v = partial.length === 0
? '{}'
: '{' + partial.join(',') + '}';
return v;
}
}
export default function (value, depthDecr, arrayMaxLength) {
seen = [];
depthDecr = depthDecr || DEFAULT_MAX_DEPTH;
arrayMaxLength = arrayMaxLength || DEFAULT_ARRAY_MAX_LENGTH;
const raw = str('', {'': value}, depthDecr, arrayMaxLength);
// return JSON.stringify(JSON.parse(raw.root), null, ' ');
return raw;
};

View File

@@ -0,0 +1,77 @@
import React, { Component } from 'react';
import {
SafeAreaView,
KeyboardAvoidingView,
} from 'react-native';
import events from '../../events';
import DevTool from './index';
import Modal from '../base/Modal';
class Events extends Component {
constructor() {
super();
this.state = {
visible: false,
};
this.listen = this.listen.bind(this);
}
componentDidMount() {
events.listen(this.listen);
}
componentWillUnmount() {
events.unlisten(this.listen);
}
listen(type, data) {
if (type === 'SHOW_DEVTOOLS') {
return this.setState({
visible: true,
});
}
if (type === 'HIDE_DEVTOOLS') {
return this.setState({
visible: false,
});
}
}
render() {
const {
...others
} = this.props;
return (
<Modal
animationType="slide"
transparent={false}
visible={this.state.visible}
onRequestClose={() => {
}}
>
<SafeAreaView
forceInset={{ top: 'always', vertical: 'always', bottom: 'always' }}
style={{flex: 1}}
>
<KeyboardAvoidingView
style={{flex: 1}}
behavior="padding"
enabled
>
<DevTool
{...others}
onClose={() => {
events.publish('HIDE_DEVTOOLS');
}}
/>
</KeyboardAvoidingView>
</SafeAreaView>
</Modal>
)
}
}
export default Events;

View File

@@ -0,0 +1,149 @@
import React, { Fragment } from 'react';
import {
ScrollView,
View,
} from 'react-native';
import styled from 'styled-components/native';
import { WebView } from 'react-native-webview';
import {
Emphasis,
Fixed,
} from '../../base/text';
import JSONTree from 'react-native-json-tree';
import Cell from '../../base/Cell';
import CellHeader from '../../base/CellHeader';
import Tab from '../Tab';
const theme = {
scheme: 'bright',
author: 'chris kempson (http://chriskempson.com)',
base00: '#000000',
base01: '#303030',
base02: '#505050',
base03: '#b0b0b0',
base04: '#d0d0d0',
base05: '#e0e0e0',
base06: '#f5f5f5',
base07: '#ffffff',
base08: '#fb0120',
base09: '#fc6d24',
base0A: '#fda331',
base0B: '#a1c659',
base0C: '#76c7b7',
base0D: '#6fb3d2',
base0E: '#d381c3',
base0F: '#be643c'
};
const Indented = styled.View`
margin: 0 25px;
`;
const getResponse = (contentType, request) => {
if (!contentType) {
return null;
}
if (request.responseType == 'blob' || request.responseType == 'ArrayBuffer') {
return <Emphasis>🤖 Binary</Emphasis>
}
const contentTypes = contentType.split(';').map(c => c.trim());
if (contentTypes.includes('application/json')) {
const data = JSON.parse(request.responseText);
return <JSONTree theme={theme} data={data} />
}
return <Fixed selectable={true}>{request.responseText}</Fixed>;
}
let i = 0;
const Data = ({
url,
method,
status,
headers,
requestHeaders,
args = [],
}) => {
const headerInfo = Object.keys(headers).map(key => `${key}: ${headers[key]}`).join('\n');
return (
<ScrollView>
<View>
<Cell left="Status" right={status} />
<Cell left="Method" right={method} />
<Cell left="Url" right={url} />
<CellHeader>Response Headers</CellHeader>
<Indented>
<Fixed>
{requestHeaders}
</Fixed>
</Indented>
{headerInfo.length > 0 && (
<Fragment>
<CellHeader>Request Headers</CellHeader>
<Indented>
<Fixed selectable={true}>
{headerInfo}
</Fixed>
</Indented>
</Fragment>
)}
{args[0] && (
<Fragment>
<CellHeader>Request Body</CellHeader>
<Fixed selectable={true}>{args[0].toString()}</Fixed>
</Fragment>
)}
</View>
</ScrollView>
);
};
const Response = ({
contentType,
request,
}) => (
<ScrollView>
<View>
{getResponse(contentType, request)}
</View>
</ScrollView>
);
const getPreview = (contentType, request, url) => {
if (!contentType || request.responseType == 'blob' || request.responseType == 'ArrayBuffer' || !contentType) {
return [];
}
const contentTypes = contentType.split(';').map(c => c.trim());
if (contentTypes.includes('text/html')) {
return [{
name: 'Preview',
view: (
<WebView
source={{
html: request.responseText,
baseUrl: url,
}}
style={{flex: 1}}
/>
),
}]
}
return [];
}
const RequestDetails = (props) => (
<Tab
tabs={[{
name: 'Details',
view: <Data {...props} />
}, {
name: 'Response',
view: <Response {...props} />
}, ...getPreview(props.contentType, props.request, props.url)]}
/>
);
export default RequestDetails;

View File

@@ -0,0 +1,54 @@
import React, { Fragment } from 'react';
import styled from 'styled-components/native';
import Status from '../../base/Status';
import Row from '../../base/Row';
import {
Body,
} from '../../base/text';
const ScrollView = styled.ScrollView`
flex: 1;
border-bottom-width: 1px;
border-color: #ccc;
`;
const Wrapper = styled.View`
flex: 1;
`;
const TouchableOpacity = styled.TouchableOpacity`
`;
const RequestDetails = ({
requests,
onSelect,
selected,
}) => (
<ScrollView>
<Wrapper>
{requests.map(({
id,
status,
method,
url,
}, i) => (
<TouchableOpacity
key={i}
onPress={() => onSelect(i)}
>
<Row
selected={selected === id}
left={<Body>{method}</Body>}
right={(
<Status code={status} />
)}
>
<Body>{url}</Body>
</Row>
</TouchableOpacity>
))}
</Wrapper>
</ScrollView>
);
export default RequestDetails;

View File

@@ -0,0 +1,55 @@
import React, { useState } from 'react';
import {
StyleSheet,
View,
} from 'react-native';
import network from '../../../network';
import useRequests from '../../data/requests';
import Toolbar, {
Button,
Seperator,
} from '../../base/Toolbar';
import Details from './Details';
import List from './List';
const styles = StyleSheet.create({
container: {
flex: 1,
},
list: {
flex: 1,
borderColor: '#ccc',
borderBottomWidth: 1,
},
details: {
flex: 1,
},
});
let i = 0;
const Console = ({
provider,
}) => {
const requests = useRequests(provider);
const [active, setActive] = useState();
const selected = active >= 0 ? requests[active] : undefined;
return (
<View style={styles.container}>
<Toolbar>
<Seperator />
<Button
name="Clear"
icon="trash"
onPress={() => network.clear()}
/>
</Toolbar>
<List
selected={selected ? selected.id : undefined}
requests={requests}
onSelect={(i) => setActive(i)}
/>
{selected && <Details {...selected} />}
</View>
);
}
export default Console;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import styled from 'styled-components/native';
import Row from '../../base/Row';
import {
Body,
} from '../../base/text';
const Scroll = styled.ScrollView`
flex: 1;
`;
const Wrapper = styled.View`
`;
const Button = styled.TouchableOpacity`
`;
const Keys = ({
keys,
selected,
onSelect,
}) => (
<Scroll>
<Wrapper>
{keys.map(key => (
<Button
key={key || '[unknown]'}
onPress={() => onSelect(key)}
>
<Row
selected={selected === key}
>
<Body>{key}</Body>
</Row>
</Button>
))}
</Wrapper>
</Scroll>
)
export default Keys;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import styled from 'styled-components/native';
import {
Fixed,
} from '../../base/text';
const Scroll = styled.ScrollView`
flex: 1;
border-top-width: 1px;
border-color: #ccc;
`;
const Wrapper = styled.View`
padding: 8px 16px;
`;
const Value = ({
value,
}) => (
<Scroll>
<Wrapper>
<Fixed selectable={true}>{value}</Fixed>
</Wrapper>
</Scroll>
)
export default Value;

View File

@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import styled from 'styled-components/native';
import useStorage from '../../data/storage';
import Toolbar, {
Button,
Seperator,
} from '../../base/Toolbar';
import Keys from './Keys';
import Value from './Value';
const Wrapper = styled.View`
flex: 1;
`;
const StorageView = ({
provider,
}) => {
const [selected, setSelected] = useState();
const {
data,
update,
removeItem,
clear,
} = useStorage(provider);
return (
<Wrapper>
<Toolbar>
<Seperator />
<Button
name="Refresh"
icon="reload"
onPress={update}
/>
<Button
name="Clear"
icon="trash"
onPress={clear}
/>
<Button
name="Delete"
icon="remove"
disabled={!selected}
onPress={() => removeItem(selected)}
/>
</Toolbar>
<Keys
selected={selected}
onSelect={(key) => {
setSelected(key);
}}
keys={Object.keys(data)}
/>
{selected && data[selected] && (
<Value value={data[selected]} />
)}
</Wrapper>
);
};
export default StorageView;

View File

@@ -0,0 +1,118 @@
import React, { Fragment, useState } from 'react';
import styled from 'styled-components/native';
import PropTypes from 'prop-types';
import {
Text,
StyleSheet,
View,
} from 'react-native';
import Icon from '../base/Icon';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
tabs: {
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
borderColor: '#ccc',
},
tabInactive: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 10,
},
tabActive: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderBottomWidth: 4,
borderColor: '#2980b9',
},
});
const Button = styled.TouchableOpacity`
padding: 10px 20px 10px 0;
`;
const TabScroll = styled.ScrollView`
flex: 1;
border-right-width: 1px;
border-color: #ccc;
margin-right: 20px;
`;
const TabWrapper = styled.View`
flex-direction: row;
`;
const Header = styled.View`
flex-direction: row;
`;
const Tab = styled.TouchableOpacity`
align-items: center;
justify-content: center;
padding: 10px 20px;
flex: 1;
`
const Console = ({
tabs,
onClose,
onDownload,
}) => {
const [active, setActive] = useState(0);
return (
<View style={styles.container}>
<Header>
<TabScroll horizontal>
<TabWrapper>
{tabs.map(({ name }, i) => (
<Tab
key={name}
style={active === i ? styles.tabActive : styles.tabInactive}
onPress={() => {
setActive(i);
}}
>
<Text>{name}</Text>
</Tab>
))}
</TabWrapper>
</TabScroll>
{onDownload && (
<Button
onPress={onDownload}
>
<Icon name="download" />
</Button>
)}
{onClose && (
<Button
onPress={onClose}
>
<Icon name="close" />
</Button>
)}
</Header>
{tabs[active] && tabs[active].view}
</View>
);
};
Console.propTypes = {
tabs: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
})),
};
Console.defaultProps = {
tabs: [],
};
export default Console;

View File

@@ -0,0 +1,93 @@
import React from 'react';
import {
StyleSheet,
View,
Alert,
Clipboard,
} from 'react-native';
import Tab from './Tab';
import Console from './Console';
import Requests from './Requests';
import Storage from './Storage';
import log from '../../log';
import network from '../../network';
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
const DevTool = ({
style,
includeStackTrace,
onClose,
logProvider = log,
storageProvider,
context = {},
requestProvider = network,
additionalTools = [],
}) => {
const views = [];
if (logProvider) {
views.push({
name: 'Console',
view: (
<Console
includeStackTrace={includeStackTrace}
provider={logProvider}
context={context}
/>
),
getData: log.get,
});
}
if (requestProvider) {
views.push({
name: 'Network',
view: <Requests provider={requestProvider} />,
getData: network.get,
});
}
if (storageProvider) {
views.push({
name: 'Storage',
view: <Storage provider={storageProvider} />,
});
}
additionalTools.forEach(tool => {
views.push(tool);
});
const getData = async () => {
const result = {};
for (let i = 0; i < views.length; i++) {
const view = views[i];
if (view.getData) {
result[view.name] = await view.getData();
}
};
Clipboard.setString(JSON.stringify(result, null, ' '));
Alert.alert(
'Copied to clipboard',
);
}
return (
<View style={style || styles.container}>
<Tab
tabs={views}
onClose={onClose}
onDownload={getData}
/>
</View>
);
};
export default DevTool;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import styled from 'styled-components/native';
import {
Body,
Emphasis,
} from './text';
const Wrapper = styled.View`
flex-direction: row;
padding: 10px;
`;
const Left = styled.View`
width: 100px;
`;
const Row = ({
left,
right,
}) => (
<Wrapper>
<Left>
<Emphasis>{left}:</Emphasis>
</Left>
<Body selectable={true}>{right}</Body>
</Wrapper>
)
export default Row;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import styled from 'styled-components/native';
import {
Emphasis,
} from './text';
const Wrapper = styled.View`
flex-direction: row;
padding: 10px;
`;
const Row = ({
children,
}) => (
<Wrapper>
<Emphasis>{children}:</Emphasis>
</Wrapper>
)
export default Row;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1,47 @@
import React from 'react';
import styled from 'styled-components/native';
import reload from './reload.png';
import trash from './trash.png';
import remove from './return.png';
import right from './right.png';
import left from './left.png';
import play from './play.png';
import download from './download.png';
import close from './close.png';
import filter from './filter.png';
import square from './square.png';
import check from './check.png';
const icons = {
reload,
trash,
remove,
right,
left,
play,
download,
close,
filter,
square,
check,
}
const Image = styled.Image`
height: ${({ height }) => height || '16'}px;
width: ${({ width }) => width || '16'}px;
`;
const Icon = ({
name,
width,
height
}) => (
<Image
width={width}
height={height}
source={icons[name]}
/>
);
export default Icon;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1,5 @@
import {
Modal
} from 'react-native';
export default Modal;

View File

@@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
const Wrapper = styled.div`
position: fixed;
background: #fff;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
`;
const Modal = ({
visible,
children
}) => {
const [root, setRoot] = useState();
useEffect(() => {
const elm = document.createElement('div');
document.body.appendChild(elm);
setRoot(elm);
}, []);
const elm = visible ? (
<Wrapper>{children}</Wrapper>
) : null;
if (!root) {
return null;
}
return createPortal(
elm,
root,
);
};
export default Modal;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import styled from 'styled-components/native';
const Wrapper = styled.View`
flex-direction: row;
padding: 10px;
border-left-width: ${props => props.selected ? '10px' : '0'};
border-left-color: #2980b9;
border-bottom-width: 1px;
border-bottom-color: #efefef;
`;
const Left = styled.View`
`;
const Right = styled.View`
`;
const Center = styled.View`
flex: 1;
margin: 0 10px;
`;
const Row = ({
left,
right,
children,
selected,
}) => (
<Wrapper selected={selected}>
{left && <Left>{left}</Left>}
<Center>{children}</Center>
{right && <Right>{right}</Right>}
</Wrapper>
)
export default Row;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import styled from 'styled-components/native';
import {
Body,
} from '../base/text';
const getColor = (code) => {
if (code === 'Error') {
return '#c0392b';
}
if (code >= 500) {
return '#c0392b';
}
if (code >= 400) {
return '#f1c40f';
}
if (code >= 300) {
return '#2980b9';
}
return '#2ecc71';
}
const Wrapper = styled.View`
flex-direction: row;
`;
const Icon = styled.View`
width: 16px;
height: 16px;
border-radius: 8px;
background: ${props => getColor(props.code)};
margin-left: 5px;
`;
const Status = ({
code,
}) => (
<Wrapper>
<Body>{code}</Body>
{code !== 'Waiting' && <Icon code={code} />}
</Wrapper>
)
export default Status;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import styled from 'styled-components/native';
import Icon from '../Icon';
import {
Body,
} from '../text';
const Item = styled.TouchableOpacity`
padding: 10px 10px;
opacity: ${({ disabled }) => disabled ? 0.3 : 1};
`;
const Button = ({
name,
icon,
onPress,
disabled,
}) => (
<Item
onPress={disabled ? undefined : onPress}
disabled={disabled}
>
{icon ? (
<Icon name={icon} />
) : (
<Body color={disabled ? '#ccc' : undefined}>{name}</Body>
)}
</Item>
);
export default Button;

View File

@@ -0,0 +1,74 @@
import React, { Fragment, useState } from 'react';
import {
SafeAreaView,
TouchableOpacity,
} from 'react-native';
import Button from './Button';
import Row from '../Row';
import Icon from '../Icon';
import Modal from '../Modal';
import {
Body,
} from '../text';
const Selector = ({
multiSelect = false,
onSelect,
options = [],
...others
}) => {
const [open, setOpen] = useState(false);
return (
<Fragment>
<Button
{...others}
onPress={() => {
setOpen(true);
}}
/>
<Modal
animationType="slide"
transparent={false}
visible={!!open}
onRequestClose={() => {
setOpen(false);
}}
>
<SafeAreaView
forceInset={{ top: 'always', vertical: 'always' }}
style={{flex: 1}}
>
<Button
name="Close"
icon="close"
onPress={() => {
setOpen(false);
}}
/>
{options.map((option) => (
<TouchableOpacity
key={option.name}
onPress={() => {
if (!multiSelect) {
options.forEach(o => o.selected = false);
}
option.selected = !option.selected
onSelect(options);
}}
>
<Row
left={(
<Icon name={option.selected ? 'check' : 'square'} />
)}
>
<Body>{option.name}</Body>
</Row>
</TouchableOpacity>
))}
</SafeAreaView>
</Modal>
</Fragment>
);
};
export default Selector;

View File

@@ -0,0 +1,7 @@
import styled from 'styled-components/native';
const Seperator = styled.View`
flex: 1;
`;
export default Seperator;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import styled from 'styled-components/native';
import Button from './Button';
import Seperator from './Seperator';
import Selector from './Selector';
export {
Button,
Seperator,
Selector,
}
const Wrapper = styled.View`
flex-direction: row;
align-items: center;
border-bottom-width: 1px;
border-color: #ccc;
padding: 0 10px;
`;
const Toolbar = ({
children,
}) => (
<Wrapper>
{children}
</Wrapper>
)
export default Toolbar;

View File

@@ -0,0 +1,14 @@
import styled from 'styled-components/native';
export const Body = styled.Text`
color: ${props => props.color || 'black'};
`;
export const Emphasis = styled.Text`
font-weight: bold;
color: ${props => props.color || 'black'};
`;
export const Fixed = styled.Text`
font-family: Menlo-Regular;
`;

View File

@@ -0,0 +1,18 @@
import { useEffect, useState } from 'react';
const useLog = (provider) => {
const [logs, setLogs] = useState([]);
useEffect(() => {
const update = (newLogs) => {
setLogs([...newLogs]);
}
provider.listen(update);
return () => {
provider.unlisten(update);
}
}, []);
return logs;
};
export default useLog;

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
const useRequests = (provider) => {
const [requests, setRequests] = useState([]);
useEffect(() => {
const update = (newRequests) => {
setRequests(newRequests);
};
provider.listen(update);
return () => {
provider.unlisten(update);
};
}, []);
return requests;
};
export default useRequests;

View File

@@ -0,0 +1,40 @@
import { useState, useEffect } from 'react';
const useStorage = (provider) => {
const [data, setData] = useState({});
const update = async () => {
const keys = await provider.getAllKeys();
const values = await Promise.all(keys.map(key => provider.getItem(key)));
const data = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = values[i];
data[key] = value;
}
setData(data);
};
const removeItem = async (name) => {
await provider.removeItem(name);
await update();
}
const clear = async () => {
await provider.clear();
await update();
}
useEffect(() => {
update().catch(err => console.error(err));
}, []);
return {
data,
update,
removeItem,
clear,
}
}
export default useStorage;

View File

@@ -0,0 +1,9 @@
export const createContext = ({
logProvider,
}, baseContext = {}) => ({
log: (...args) => logProvider.log(...args),
clear: logProvider.clear,
window: baseContext,
global: baseContext,
context: baseContext,
});

View File

@@ -0,0 +1,21 @@
class Events {
constructor() {
this.listeners = [];
}
listen(fn) {
this.listeners.push(fn);
}
unlisten(fn) {
this.listeners = this.listeners(l => l !== fn);
}
publish(type, data) {
this.listeners.forEach(l => l(type, data));
}
}
const events = new Events();
export default events;

17
packages/lib/src/index.js Normal file
View File

@@ -0,0 +1,17 @@
import DevTool from './components/DevTool/index';
import DevToolModal from './components/DevTool/Modal';
import log from './log';
import network from './network';
import events from './events';
const show = () => events.publish('SHOW_DEVTOOLS');
const hide = () => events.publish('HIDE_DEVTOOLS');
export {
DevTool,
DevToolModal,
log,
network,
show,
hide,
};

104
packages/lib/src/log.js Normal file
View File

@@ -0,0 +1,104 @@
export const proxyConsole = global.console;
class Log {
constructor() {
this.logs = [];
this.listeners = [];
this.clear = this.clear.bind(this);
this.get = this.get.bind(this);
this.handleError = this.handleError.bind(this);
}
listen(fn) {
this.listeners.push(fn);
fn(this.logs);
}
unlisten(fn) {
this.listeners = this.listeners.filter(l => l !== fn);
}
clear() {
this.logs = [];
this.listeners.forEach(l => l(this.logs));
}
get() {
return this.logs;
}
log(type, data, keep) {
if (data && data[0] && data[0].indexOf && data[0].indexOf('Warning: Using the "className" prop on') === 0) {
return;
}
const entry = {
type,
data,
};
this.logs.push(entry);
this.listeners.forEach(l => l(this.logs));
if (keep) {
proxyConsole[type](...data);
}
}
info(data, keep) {
this.log('info', data, keep);
}
error(data, keep) {
this.log('error', data, keep);
}
warn(data, keep) {
this.log('warn', data, keep);
}
debug(data, keep) {
this.log('debug', data, keep);
}
handleError(err) {
if (err && err.error) {
this.error([err.error]);
}
}
attach(keep) {
const redirected = Object.keys(proxyConsole).reduce((output, key) => ({
...output,
[key]: keep ? (...args) => proxyConsole[key](...args) : () => {},
}), {});
global.console = {
...redirected,
error: (...data) => this.error(data, keep),
warn: (...data) => this.warn(data, keep),
info: (...data) => this.info(data, keep),
log: (...data) => this.info(data, keep),
debug: (...data) => this.debug(data, keep),
verbose: (...data) => this.debug(data, keep),
};
/*if (global.ErrorUtils) {
global.ErrorUtils.setGlobalHandler((err, fatal) => {
this.error([err], keep);
});
}*/
if (global.addEventListener) {
global.addEventListener('error', this.handleError);
}
}
detach() {
overrides.forEach((key) => {
window.console[key] = proxies[key];
});
if (global.removeEventListener) {
global.removeEventListener('error', this.handleError);
}
}
}
const log = new Log();
export default log;

View File

@@ -0,0 +1,17 @@
import DevTool from './components/DevTool/index';
import DevToolModal from './components/DevTool/Modal';
import log from './log';
import network from './network';
import events from './events';
const show = () => events.publish('SHOW_DEVTOOLS');
const hide = () => events.publish('HIDE_DEVTOOLS');
export {
DevTool,
DevToolModal,
log,
network,
show,
hide,
};

109
packages/lib/src/network.js Normal file
View File

@@ -0,0 +1,109 @@
const proxied = global.XMLHttpRequest ? global.XMLHttpRequest.prototype.open : () => {};
let currentId = 0;
class Network {
constructor() {
this.requests = [];
this.listeners = [];
// this.clear = this.clear.bind(this);
this.get = this.get.bind(this);
}
listen(fn) {
this.listeners.push(fn);
fn(this.requests);
}
get() {
return this.requests;
}
unlisten(fn) {
this.listeners = this.listeners.filter(l => l !== fn);
}
clear() {
this.requests = [];
this.listeners.forEach(l => l(this.requests));
}
addRequest(id, request) {
const index = this.requests.findIndex(req => req.id === id);
if (index >= 0) {
this.requests[index] = {
id,
...request,
};
} else {
this.requests.push({
id,
...request,
});
}
this.listeners.forEach(l => l(this.requests));
}
attach() {
const me = this;
const headers = {};
global.XMLHttpRequest.prototype.open = function proxyOpen (...args) {
let sendArgs;
const [
method,
url,
] = args;
const id = currentId++;
this.addEventListener('load', () => {
me.addRequest(id, {
url,
method,
args: sendArgs,
headers,
requestHeaders: this.getAllResponseHeaders(),
contentType: (this.getResponseHeader('content-type') || ''),
request: this,
status: this.status || null,
});
})
this.addEventListener('error', (error) => {
me.addRequest(id, {
url,
method,
error,
args: sendArgs,
headers,
requestHeaders: this.getAllResponseHeaders(),
request: this,
status: this.status || 'Error',
});
})
const proxiedSend = this.send;
const proxiedSetRequestHeader = this.setRequestHeader;
this.send = function proxySend (...sendargs) {
sendArgs = sendargs;
me.addRequest(id, {
url,
method,
args: sendArgs,
headers,
request: this,
status: 'Waiting',
});
return proxiedSend.apply(this, [].slice.call(arguments));
}
this.setRequestHeader = function (name, value) {
headers[name] = value;
return proxiedSetRequestHeader.apply(this, [].slice.call(arguments));
}
return proxied.apply(this, [].slice.call(arguments));
};
}
detach() {
global.XMLHttpRequest.prototype.open = proxied;
}
}
const network = new Network();
export default network;

View File

@@ -0,0 +1,47 @@
import getDevTools from './getDevTools';
import renderer from 'react-test-renderer';
describe('Console', () => {
describe('output', () => {
let devTool;
beforeEach(() => {
devTool = getDevTools();
});
it('should render an emptry list', () => {
expect(devTool.console.output().props.logs).toEqual([
]);
});
it('should render one item', () => {
renderer.act(() => {
devTool.log.add('test');
});
expect(devTool.console.output().props.logs).toEqual([
'test',
]);
});
it('should render multible items item', () => {
renderer.act(() => {
devTool.log.add('1');
devTool.log.add('2');
devTool.log.add('3');
devTool.log.add('4');
devTool.log.add('5');
devTool.log.add('6');
devTool.log.add('7');
});
expect(devTool.console.output().props.logs).toEqual([
'1',
'2',
'3',
'4',
'5',
'6',
'7',
]);
});
});
})

View File

@@ -0,0 +1,61 @@
import React from 'react';
import renderer from 'react-test-renderer';
import {
DevTool,
} from '../src';
import Console from '../src/components/DevTool/Console';
import ConsoleOutput, { Row as OutputRow} from '../src/components/DevTool/Console/Output';
import ConsoleInput from '../src/components/DevTool/Console/Input';
import Requests from '../src/components/DevTool/Requests';
import RequestsDetails from '../src/components/DevTool/Requests/Details';
import RequestsList from '../src/components/DevTool/Requests/List';
import Storage from '../src/components/DevTool/Storage';
import StorageKeys from '../src/components/DevTool/Storage/Keys';
import StorageValues from '../src/components/DevTool/Storage/Value';
const createLog = () => {
const listeners = [];
const logs = [];
return {
listen: (l) => listeners.push(l),
unlisten: (l) => listeners = listeners.filter(nl => nl !== l),
add: (log) => {
logs.push(log);
listeners.forEach(l => l(logs));
}
}
}
const getDevTools = () => {
const log = createLog();
const instance = renderer.create(
<DevTool
logProvider={log}
/>
);
const result = {
instance,
console: {
main: () => instance.root.findByType(Console),
output: () => instance.root.findByType(ConsoleOutput),
rows: () => instance.root.findAllByType(OutputRow),
input: () => instance.root.findByType(ConsoleInput),
},
requests: {
main: () => instance.root.findByType(Requests),
output: () => instance.root.findByType(RequestsDetails),
input: () => instance.root.findByType(RequestsList),
},
storage: {
main: () => instance.root.findByType(Storage),
output: () => instance.root.findByType(StorageKeys),
input: () => instance.root.findByType(StorageValues),
},
log,
};
return result;
};
export default getDevTools;