├── .circleci
└── config.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── .prettierrc
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── demo
├── index.html
├── public
│ └── iframe.html
├── src
│ ├── index.tsx
│ └── main.tsx
└── vite.config.ts
├── package.json
├── scripts
└── test.js
├── src
├── Component
│ ├── Message.tsx
│ ├── __tests__
│ │ └── Console.spec.tsx
│ ├── devtools-parser
│ │ ├── format-message.ts
│ │ ├── index.ts
│ │ └── string-utils.ts
│ ├── elements.tsx
│ ├── index.tsx
│ ├── message-parsers
│ │ ├── Error.tsx
│ │ ├── Formatted.tsx
│ │ └── Object.tsx
│ ├── react-inspector
│ │ ├── elements.tsx
│ │ ├── index.tsx
│ │ └── util.ts
│ └── theme
│ │ ├── default.ts
│ │ └── index.ts
├── Hook
│ ├── __tests__
│ │ ├── Hook.spec.tsx
│ │ ├── Log.tsx
│ │ ├── __snapshots__
│ │ │ └── Hook.spec.tsx.snap
│ │ └── console.ts
│ ├── construct.ts
│ ├── index.ts
│ ├── parse
│ │ ├── GUID.ts
│ │ ├── __tests__
│ │ │ ├── Parse.spec.tsx
│ │ │ └── __snapshots__
│ │ │ │ └── Parse.spec.tsx.snap
│ │ ├── index.ts
│ │ └── methods
│ │ │ ├── assert.ts
│ │ │ ├── count.ts
│ │ │ └── timing.ts
│ └── store
│ │ ├── actions.ts
│ │ ├── dispatch.ts
│ │ ├── reducer.ts
│ │ └── state.ts
├── Transform
│ ├── BigInt.ts
│ ├── Function.ts
│ ├── HTML.ts
│ ├── Map.ts
│ ├── arithmetic.ts
│ ├── index.ts
│ └── replicator
│ │ └── index.ts
├── Unhook
│ └── index.ts
├── definitions
│ ├── Component.d.ts
│ ├── ComponentOverrides.d.ts
│ ├── Console.d.ts
│ ├── Methods.ts
│ ├── Payload.d.ts
│ ├── Store.d.ts
│ └── Styles.d.ts
└── index.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/node:10.13.0
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/mongo:3.4.4
16 |
17 | working_directory: ~/console-feed
18 |
19 | steps:
20 | - checkout
21 |
22 | # Download and cache dependencies
23 | - restore_cache:
24 | keys:
25 | - v1-dependencies-{{ checksum "package.json" }}
26 | # fallback to using the latest cache if no exact match is found
27 | - v1-dependencies-
28 |
29 | - run: yarn install
30 |
31 | - save_cache:
32 | paths:
33 | - node_modules
34 | key: v1-dependencies-{{ checksum "package.json" }}
35 |
36 | # run tests!
37 | - run: yarn test
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /demo/dist
3 | /es
4 | /lib
5 | /node_modules
6 | /umd
7 | /.idea
8 | npm-debug.log*
9 | .DS_Store
10 | yarn-error.log
11 | .envrc
12 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .circleci
2 | .gitignore
3 | .prettierrc
4 | .vscode
5 | demo
6 | scripts
7 | src
8 | tsconfig.build.json
9 | tsconfig.json
10 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 10.13.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "semi": false,
5 | "singleQuote": true,
6 | "bracketSpacing": true
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "name": "vscode-jest-tests",
10 | "request": "launch",
11 | "args": [
12 | "--runInBand"
13 | ],
14 | "cwd": "${workspaceFolder}",
15 | "console": "integratedTerminal",
16 | "internalConsoleOptions": "neverOpen",
17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Facebook, Inc. and its affiliates.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # console-feed [](https://github.com/sponsors/samdenty)
2 |
3 | [Sponsor this project](https://github.com/sponsors/samdenty)
4 |
5 | [](https://www.npmjs.com/package/console-feed)
6 | [](https://www.npmjs.com/package/console-feed)
7 | [](https://stackblitz.com/github/samdenty/console-feed?file=demo%2Fpublic%2Fiframe.html)
8 |
9 | A React component that displays console logs from the current page, an iframe or transported across a server.
10 |
11 | 
12 |
13 | ## Alternative to `console-feed`
14 |
15 | https://github.com/liriliri/chii supports the embedding the entire Chrome devtools.
16 |
17 | https://github.com/tachibana-shin/vue-console-feed is a fork for Vue.JS
18 |
19 | ## Who's using it
20 |
21 | - [Firebase studio](https://studio.firebase.google.com)
22 | - [Tesla](https://github.com/teslamotors/informed)
23 | - [CodeSandbox.io](https://codesandbox.io)
24 | - [Framer](https://www.framer.com)
25 | - [Plunker](https://plnkr.co)
26 | - [P5.js Editor](https://editor.p5js.org)
27 | - [Builder.io](https://builder.io)
28 | - [Utopia](https://utopia.app/project)
29 | - [facebook/flipper](https://github.com/facebook/flipper)
30 | - [Effector playground](https://share.effector.dev/)
31 |
32 | ## Features
33 |
34 | - **Console formatting** - [style and give your logs color](https://stackoverflow.com/questions/22155879/how-do-i-create-formatted-javascript-console-log-messages), and makes links clickable
35 | - **DOM nodes** - easily inspect & expand HTML elements, with syntax highlighting
36 | - **`console.table`** - view your logs in a table format
37 | - **Other console methods**:
38 | - `console.time` - view the time in milliseconds it takes to complete events
39 | - `console.assert` - assert that a statement is truthy
40 | - `console.count` - count how many times something occurs
41 | - **Inbuilt JSON serialization** - Objects, Functions & DOM elements can be encoded / decoded to and from JSON
42 |
43 | ## Install
44 |
45 | ```sh
46 | yarn add console-feed
47 | # or
48 | npm install console-feed
49 | ```
50 |
51 | ## Basic usage
52 |
53 | [StackBlitz](https://stackblitz.com/github/samdenty/console-feed?file=demo%2Fpublic%2Fiframe.html)
54 |
55 | ```js
56 | import React from 'react'
57 | import { Hook, Console, Decode } from 'console-feed'
58 |
59 | class App extends React.Component {
60 | state = {
61 | logs: [],
62 | }
63 |
64 | componentDidMount() {
65 | Hook(window.console, (log) => {
66 | this.setState(({ logs }) => ({ logs: [...logs, Decode(log)] }))
67 | })
68 |
69 | console.log(`Hello world!`)
70 | }
71 |
72 | render() {
73 | return (
74 |
75 |
76 |
77 | )
78 | }
79 | }
80 | ```
81 |
82 | OR with hooks:
83 |
84 | ```js
85 | import React, { useState, useEffect } from 'react'
86 | import { Console, Hook, Unhook } from 'console-feed'
87 |
88 | const LogsContainer = () => {
89 | const [logs, setLogs] = useState([])
90 |
91 | // run once!
92 | useEffect(() => {
93 | const hookedConsole = Hook(
94 | window.console,
95 | (log) => setLogs((currLogs) => [...currLogs, log]),
96 | false
97 | )
98 | return () => Unhook(hookedConsole)
99 | }, [])
100 |
101 | return
102 | }
103 |
104 | export { LogsContainer }
105 | ```
106 |
107 | ## Props for `` component
108 |
109 | ### `logs: Log[]`
110 |
111 | An array consisting of Log objects. Required
112 |
113 | ### `filter?: Methods[]`
114 |
115 | Filter the logs, only displaying messages of certain methods.
116 |
117 | ### `variant?: 'light' | 'dark'`
118 |
119 | Sets the font color for the component. Default - `light`
120 |
121 | ### `styles?: Styles`
122 |
123 | Defines the custom styles to use on the component - see [`Styles.d.ts`](https://github.com/samdenty/console-feed/blob/master/src/definitions/Styles.d.ts)
124 |
125 | ### `searchKeywords?: string`
126 |
127 | A string value to filter logs
128 |
129 | ### `logFilter?: Function`
130 |
131 | If you want to use a custom log filter function, you can provide your own implementation
132 |
133 | ### `components?: ComponentOverrides`
134 |
135 | To fully customize layout and rendering of the console feed, you can provide your own React
136 | components. Currently, only the `Message` component is customizable.
137 |
138 | ## Log methods
139 |
140 | Each log has a method assigned to it. The method is used to determine the style of the message and for the `filter` prop.
141 |
142 | ```ts
143 | type Methods =
144 | | 'log'
145 | | 'debug'
146 | | 'info'
147 | | 'warn'
148 | | 'error'
149 | | 'table'
150 | | 'clear'
151 | | 'time'
152 | | 'timeEnd'
153 | | 'count'
154 | | 'assert'
155 | ```
156 |
157 | ## `Log` object
158 |
159 | A log object consists of the following:
160 |
161 | ```ts
162 | type Logs = Log[]
163 |
164 | interface Log {
165 | // The log method
166 | method: Methods
167 | // The arguments passed to console API
168 | data: any[]
169 | }
170 | ```
171 |
172 | ## Serialization
173 |
174 | By default when you use the `Hook()` API, logs are serialized so that they will safely work with `JSON.stringify`. In order to restore a log back to format compatible with the `` component, you need to call the `Decode()` method.
175 |
176 | ### Disabling serialization
177 |
178 | If the `Hook` function and the `` component are on the same origin, you can disable serialization to increase performance.
179 |
180 | ```js
181 | Hook(
182 | window.console,
183 | (log) => {
184 | this.setState(({ logs }) => ({ logs: [...logs, log] }))
185 | },
186 | false
187 | )
188 | ```
189 |
190 | ### Limiting serialization
191 |
192 | You can limit the number of keys/elements included when serializing objects/arrays.
193 |
194 | ```js
195 | Hook(
196 | window.console,
197 | (log) => {
198 | this.setState(({ logs }) => ({ logs: [...logs, log] }))
199 | },
200 | true,
201 | 100 // limit to 100 keys/elements
202 | )
203 | ```
204 |
205 | ---
206 |
207 | ## Developing
208 |
209 | To run `console-feed` locally, simply run:
210 |
211 | ```bash
212 | yarn
213 | yarn start
214 | yarn test:watch
215 | ```
216 |
217 | Head over to `http://localhost:3000` in your browser, and you'll see the demo page come up. After you make changes you'll need to reload, but the jest tests will automatically restart.
218 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite + React + TS
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/public/iframe.html:
--------------------------------------------------------------------------------
1 |
2 | demo iframe
3 |
95 |
96 |
--------------------------------------------------------------------------------
/demo/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import update from 'immutability-helper'
3 | import { Hook, Console, Decode } from '../../src'
4 | import { Message } from '../../src/definitions/Component'
5 |
6 | const iframe = document.createElement('iframe')
7 | iframe.src = './iframe.html'
8 | document.body.appendChild(iframe)
9 |
10 | export class App extends React.Component {
11 | state = {
12 | baseFontSize: 12,
13 | isDarkMode: true,
14 | logs: [
15 | {
16 | method: 'result',
17 | data: ['Result'],
18 | timestamp: this.getTimestamp(),
19 | },
20 | {
21 | method: 'command',
22 | data: ['Command'],
23 | timestamp: this.getTimestamp(),
24 | },
25 | ] as Message[],
26 | filter: [],
27 | searchKeywords: '',
28 | }
29 |
30 | getNumberStringWithWidth(num: Number, width: number) {
31 | const str = num.toString()
32 | if (width > str.length) return '0'.repeat(width - str.length) + str
33 | return str.substr(0, width)
34 | }
35 |
36 | getTimestamp() {
37 | const date = new Date()
38 | const h = this.getNumberStringWithWidth(date.getHours(), 2)
39 | const min = this.getNumberStringWithWidth(date.getMinutes(), 2)
40 | const sec = this.getNumberStringWithWidth(date.getSeconds(), 2)
41 | const ms = this.getNumberStringWithWidth(date.getMilliseconds(), 3)
42 | return `${h}:${min}:${sec}.${ms}`
43 | }
44 |
45 | componentDidMount() {
46 | Hook(
47 | (iframe.contentWindow as any).console,
48 | (log) => {
49 | const decoded = Decode(log)
50 | decoded.timestamp = this.getTimestamp()
51 | this.setState((state) => update(state, { logs: { $push: [decoded] } }))
52 | },
53 | true,
54 | 100
55 | )
56 | }
57 |
58 | switch = () => {
59 | const filter = this.state.filter.length === 0 ? ['log'] : []
60 | this.setState({
61 | filter,
62 | })
63 | }
64 |
65 | handleKeywordsChange = ({ target: { value: searchKeywords } }) => {
66 | this.setState({ searchKeywords })
67 | }
68 |
69 | render() {
70 | const { isDarkMode } = this.state
71 | return (
72 |
123 | )
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/demo/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { render } from 'react-dom'
2 | import { App } from './index'
3 |
4 | render(, document.getElementById('root'))
5 |
--------------------------------------------------------------------------------
/demo/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react'
2 | import { defineConfig } from 'vite'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "console-feed",
3 | "version": "3.6.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "description": "A React component that displays console logs from the current page, an iframe or transported across a server",
7 | "scripts": {
8 | "prepack": "yarn build",
9 | "build": "tsc -p ./tsconfig.build.json --declaration && for file in src/**/*.d.ts; do cp $file ${file/src/lib}; done",
10 | "start": "vite demo",
11 | "release": "yarn test && yarn build && yarn publish",
12 | "precommit": "pretty-quick --staged",
13 | "test": "jest --verbose",
14 | "test:coverage": "jest --coverage",
15 | "test:watch": "jest --watch"
16 | },
17 | "dependencies": {
18 | "@emotion/core": "^10.0.10",
19 | "@emotion/styled": "^10.0.12",
20 | "emotion-theming": "^10.0.10",
21 | "linkifyjs": "^2.1.6",
22 | "react-inline-center": "1.0.1",
23 | "react-inspector": "^5.1.0"
24 | },
25 | "devDependencies": {
26 | "@types/enzyme": "^3.1.9",
27 | "@types/jest": "^22.2.3",
28 | "@types/linkifyjs": "2.1.3",
29 | "@types/react": "^16.9.50",
30 | "@types/react-dom": "^16.9.8",
31 | "@vitejs/plugin-react": "^4.2.1",
32 | "enzyme": "^3.3.0",
33 | "enzyme-adapter-react-16": "^1.1.1",
34 | "husky": "^0.14.3",
35 | "immutability-helper": "^2.6.6",
36 | "jest": "^22.4.3",
37 | "lodash": "^4.17.5",
38 | "prettier": "^2.1.2",
39 | "pretty-quick": "^1.6.0",
40 | "react": "^16.14.0",
41 | "react-dom": "^16.14.0",
42 | "ts-jest": "^22.4.2",
43 | "typescript": "^5.4.3",
44 | "vite": "^5.2.7"
45 | },
46 | "jest": {
47 | "setupTestFrameworkScriptFile": "./scripts/test.js",
48 | "moduleFileExtensions": [
49 | "ts",
50 | "tsx",
51 | "js"
52 | ],
53 | "transform": {
54 | "^.+\\.tsx?$": "ts-jest"
55 | },
56 | "testMatch": [
57 | "**/__tests__/*.spec.(ts|tsx|js)"
58 | ]
59 | },
60 | "peerDependencies": {
61 | "react": "^15.x || ^16.x || ^17.x || ^18.x || ^19.x"
62 | },
63 | "files": [
64 | "es",
65 | "lib",
66 | "umd"
67 | ],
68 | "keywords": [
69 | "devtools",
70 | "inspector",
71 | "object",
72 | "object-inspector",
73 | "react",
74 | "react-component",
75 | "reactjs",
76 | "table",
77 | "table-inspector",
78 | "table-view",
79 | "tableview",
80 | "tree",
81 | "tree-view",
82 | "treeview",
83 | "ui",
84 | "view"
85 | ],
86 | "homepage": "https://stackblitz.com/github/samdenty/console-feed?file=demo%2Fpublic%2Fiframe.html",
87 | "repository": {
88 | "type": "git",
89 | "url": "https://github.com/samdenty/console-feed.git"
90 | },
91 | "bugs": {
92 | "url": "https://github.com/samdenty/console-feed/issues"
93 | },
94 | "author": "Sam Denty (http://github.com/samdenty)",
95 | "license": "MIT"
96 | }
97 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Defines the React 16 Adapter for Enzyme.
3 | *
4 | * @link http://airbnb.io/enzyme/docs/installation/#working-with-react-16
5 | * @copyright 2017 Airbnb, Inc.
6 | */
7 | const enzyme = require('enzyme')
8 | const Adapter = require('enzyme-adapter-react-16')
9 |
10 | enzyme.configure({ adapter: new Adapter() })
11 |
--------------------------------------------------------------------------------
/src/Component/Message.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { MessageProps, Theme } from '../definitions/Component'
3 | import { ThemeProvider } from 'emotion-theming'
4 | import InlineCenter from 'react-inline-center'
5 |
6 | import {
7 | Message,
8 | IconContainer,
9 | Icon,
10 | Content,
11 | AmountIcon,
12 | Timestamp,
13 | } from './elements'
14 |
15 | import Formatted from './message-parsers/Formatted'
16 | import ObjectTree from './message-parsers/Object'
17 | import ErrorPanel from './message-parsers/Error'
18 |
19 | // https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
20 | const reSubstitutions = /(%[coOs])|(%(([0-9]*[.])?[0-9]+)?[dif])/g
21 |
22 | class ConsoleMessage extends React.Component {
23 | shouldComponentUpdate(nextProps) {
24 | return this.props.log.amount !== nextProps.log.amount
25 | }
26 |
27 | theme = (theme: Theme) => ({
28 | ...theme,
29 | method: this.props.log.method,
30 | })
31 |
32 | render() {
33 | const { log, components } = this.props
34 | const node = this.getNode()
35 | const MessageComponent = components?.Message || Message
36 |
37 | return (
38 |
39 |
40 |
41 | {/* Align icon to adjacent text, and let the icon can be different size than the text */}
42 |
43 | {log.amount > 1 ? (
44 | {log.amount}
45 | ) : (
46 |
47 | )}
48 |
49 |
50 | {log.timestamp ? {log.timestamp} : }
51 | {node}
52 |
53 |
54 | )
55 | }
56 |
57 | getNode() {
58 | const { log } = this.props
59 |
60 | // Error handling
61 | const error = this.typeCheck(log)
62 | if (error) return error
63 |
64 | // Chrome formatting
65 | if (log.data.length > 0 && typeof log.data[0] === 'string') {
66 | const matchLength = log.data[0].match(reSubstitutions)?.length
67 | if (matchLength) {
68 | const restData = log.data.slice(1 + matchLength)
69 | return (
70 | <>
71 |
72 | {restData.length > 0 && (
73 |
78 | )}
79 | >
80 | )
81 | }
82 | }
83 |
84 | // Error panel
85 | if (
86 | log.data.every((message) => typeof message === 'string') &&
87 | log.method === 'error'
88 | ) {
89 | return
90 | }
91 |
92 | // Normal inspector
93 | const quoted = typeof log.data[0] !== 'string'
94 | return (
95 |
100 | )
101 | }
102 |
103 | typeCheck(log: any) {
104 | if (!log) {
105 | return (
106 |
114 | )
115 | } else if (!(log.data instanceof Array)) {
116 | return (
117 |
125 | )
126 | }
127 | return false
128 | }
129 | }
130 |
131 | export default ConsoleMessage
132 |
--------------------------------------------------------------------------------
/src/Component/__tests__/Console.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { shallow } from 'enzyme'
3 |
4 | import Console from '..'
5 |
6 | it('renders', () => {
7 | const result = shallow(
8 |
17 | )
18 |
19 | expect(result.html()).toContain('my-log')
20 | })
21 |
22 | it('formats messages', () => {
23 | const result = shallow(
24 |
38 | )
39 |
40 | const html = result.html()
41 | expect(html).toContain('test')
42 | expect(html).toContain('foo')
43 | expect(html).toContain('[2]')
44 | })
45 |
46 | it('various data types', () => {
47 | const result = shallow(
48 |
66 | )
67 |
68 | const html = result.html()
69 | expect(html).toContain('test')
70 | expect(html).toContain('foo:')
71 | expect(html).toContain(
72 | '"bar"'
73 | )
74 | })
75 |
76 | it('skips non-existent substitution', () => {
77 | const result = shallow(
78 |
87 | )
88 |
89 | const html = result.html()
90 | expect(html).toContain('%u')
91 | expect(html).toContain('foo')
92 | })
93 |
94 | it('displays object names', () => {
95 | const result = shallow(
96 |
105 | )
106 |
107 | expect(result.html()).toContain(
108 | 'MyObject {}'
109 | )
110 | })
111 |
112 | it('linkify object', () => {
113 | const result = shallow(
114 |
123 | )
124 |
125 | expect(result.html()).toContain(
126 | 'https://example.com'
127 | )
128 | })
129 |
130 | it('linkify object and pass options', () => {
131 | const result = shallow(
132 | (type === 'url' ? { rel: 'nofollow' } : {}),
142 | }}
143 | />
144 | )
145 |
146 | expect(result.html()).toContain(
147 | 'https://example.com'
148 | )
149 | })
150 |
151 | it('allows all types methods', () => {
152 | expect(() =>
153 | shallow(
154 |
171 | )
172 | ).not.toThrowError()
173 | })
174 |
175 | it('displays limited arrays correctly', () => {
176 | const result = shallow(
177 |
191 | )
192 |
193 | expect(result.html()).toContain('(99999)')
194 | expect(result.html()).toContain('…]')
195 | })
196 |
197 | it('displays nested limited arrays correctly', () => {
198 | const result = shallow(
199 |
216 | )
217 |
218 | expect(result.html()).toContain('Array(99999)')
219 | })
220 |
--------------------------------------------------------------------------------
/src/Component/devtools-parser/format-message.ts:
--------------------------------------------------------------------------------
1 | import { String as StringUtils } from './string-utils'
2 |
3 | function createAppend(s: string) {
4 | const container = document.createDocumentFragment()
5 | container.appendChild(document.createTextNode(s))
6 |
7 | return container
8 | }
9 |
10 | /**
11 | * @param {string} format
12 | * @param {!Array.} parameters
13 | * @param {!Element} formattedResult
14 | */
15 | export default function formatWithSubstitutionString(
16 | format: any,
17 | parameters: any,
18 | formattedResult: any
19 | ) {
20 | const formatters: any = {}
21 |
22 | function stringFormatter(obj: any) {
23 | if (typeof obj !== 'string') {
24 | return ''
25 | }
26 |
27 | return String(obj)
28 | }
29 |
30 | function floatFormatter(obj: any) {
31 | if (typeof obj !== 'number') return 'NaN'
32 | return obj
33 | }
34 |
35 | function integerFormatter(obj: any) {
36 | if (typeof obj !== 'number') return 'NaN'
37 | return Math.floor(obj)
38 | }
39 |
40 | let currentStyle: any = null
41 | function styleFormatter(obj: any) {
42 | currentStyle = {}
43 | const buffer = document.createElement('span')
44 | buffer.setAttribute('style', obj)
45 | for (let i = 0; i < buffer.style.length; i++) {
46 | const property = buffer.style[i]
47 | if (isWhitelistedProperty(property))
48 | currentStyle[property] = buffer.style[property]
49 | }
50 | }
51 |
52 | function isWhitelistedProperty(property: string) {
53 | const prefixes = [
54 | 'background',
55 | 'border',
56 | 'color',
57 | 'font',
58 | 'line',
59 | 'margin',
60 | 'padding',
61 | 'text',
62 | '-webkit-background',
63 | '-webkit-border',
64 | '-webkit-font',
65 | '-webkit-margin',
66 | '-webkit-padding',
67 | '-webkit-text'
68 | ]
69 | for (let i = 0; i < prefixes.length; i++) {
70 | if (property.startsWith(prefixes[i])) return true
71 | }
72 | return false
73 | }
74 |
75 | formatters.s = stringFormatter
76 | formatters.f = floatFormatter
77 | // Firebug allows both %i and %d for formatting integers.
78 | formatters.i = integerFormatter
79 | formatters.d = integerFormatter
80 |
81 | // Firebug uses %c for styling the message.
82 | formatters.c = styleFormatter
83 |
84 | function append(a: any, b: any) {
85 | if (b instanceof Node) {
86 | a.appendChild(b)
87 | } else if (typeof b !== 'undefined') {
88 | let toAppend: any = createAppend(String(b))
89 |
90 | if (currentStyle) {
91 | let wrapper = document.createElement('span')
92 | wrapper.appendChild(toAppend)
93 | applyCurrentStyle(wrapper)
94 | for (let i = 0; i < wrapper.children.length; ++i)
95 | applyCurrentStyle(wrapper.children[i])
96 | toAppend = wrapper
97 | }
98 | a.appendChild(toAppend)
99 | }
100 | return a
101 | }
102 |
103 | /**
104 | * @param {!Element} element
105 | */
106 | function applyCurrentStyle(element: any) {
107 | for (var key in currentStyle) element.style[key] = currentStyle[key]
108 | }
109 |
110 | // String.format does treat formattedResult like a Builder, result is an object.
111 | return StringUtils.format(
112 | format,
113 | parameters,
114 | formatters,
115 | formattedResult,
116 | append
117 | )
118 | }
119 |
--------------------------------------------------------------------------------
/src/Component/devtools-parser/index.ts:
--------------------------------------------------------------------------------
1 | import Linkify from 'linkifyjs/html'
2 | import formatMessageString from './format-message'
3 |
4 | /**
5 | * Formats a console log message using the Devtools parser and returns HTML
6 | * @param args The arguments passed to the console method
7 | */
8 | function formatMessage(args: any[]): string {
9 | const formattedResult = document.createElement('span')
10 |
11 | formatMessageString(args[0], args.slice(1), formattedResult)
12 |
13 | return Linkify(formattedResult.outerHTML.replace(/(?:\r\n|\r|\n)/g, '
'))
14 | }
15 |
16 | export default formatMessage
17 |
--------------------------------------------------------------------------------
/src/Component/devtools-parser/string-utils.ts:
--------------------------------------------------------------------------------
1 | // Taken from the source of chrome devtools:
2 | // https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/platform/utilities.js#L805-L1006
3 |
4 | // Copyright 2014 The Chromium Authors. All rights reserved.
5 | //
6 | // Redistribution and use in source and binary forms, with or without
7 | // modification, are permitted provided that the following conditions are
8 | // met:
9 | //
10 | // * Redistributions of source code must retain the above copyright
11 | // notice, this list of conditions and the following disclaimer.
12 | // * Redistributions in binary form must reproduce the above
13 | // copyright notice, this list of conditions and the following disclaimer
14 | // in the documentation and/or other materials provided with the
15 | // distribution.
16 | // * Neither the name of Google Inc. nor the names of its
17 | // contributors may be used to endorse or promote products derived from
18 | // this software without specific prior written permission.
19 | //
20 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
32 | export namespace String {
33 | /**
34 | * @param {string} string
35 | * @param {number} index
36 | * @return {boolean}
37 | */
38 | function isDigitAt(string: any, index: any) {
39 | var c = string.charCodeAt(index)
40 | return 48 <= c && c <= 57
41 | }
42 |
43 | /**
44 | * @param {string} format
45 | * @param {!Object.} formatters
46 | * @return {!Array.}
47 | */
48 | function tokenizeFormatString(format: any, formatters: any) {
49 | var tokens: any = []
50 | var substitutionIndex = 0
51 |
52 | function addStringToken(str: any) {
53 | if (tokens.length && tokens[tokens.length - 1].type === 'string')
54 | tokens[tokens.length - 1].value += str
55 | else tokens.push({ type: 'string', value: str })
56 | }
57 |
58 | function addSpecifierToken(specifier: any, precision: any, substitutionIndex: any) {
59 | tokens.push({
60 | type: 'specifier',
61 | specifier: specifier,
62 | precision: precision,
63 | substitutionIndex: substitutionIndex
64 | })
65 | }
66 |
67 | var index = 0
68 | for (
69 | var precentIndex = format.indexOf('%', index);
70 | precentIndex !== -1;
71 | precentIndex = format.indexOf('%', index)
72 | ) {
73 | if (format.length === index)
74 | // unescaped % sign at the end of the format string.
75 | break
76 | addStringToken(format.substring(index, precentIndex))
77 | index = precentIndex + 1
78 |
79 | if (format[index] === '%') {
80 | // %% escape sequence.
81 | addStringToken('%')
82 | ++index
83 | continue
84 | }
85 |
86 | if (isDigitAt(format, index)) {
87 | // The first character is a number, it might be a substitution index.
88 | var number = parseInt(format.substring(index), 10)
89 | while (isDigitAt(format, index)) ++index
90 |
91 | // If the number is greater than zero and ends with a "$",
92 | // then this is a substitution index.
93 | if (number > 0 && format[index] === '$') {
94 | substitutionIndex = number - 1
95 | ++index
96 | }
97 | }
98 |
99 | var precision = -1
100 | if (format[index] === '.') {
101 | // This is a precision specifier. If no digit follows the ".",
102 | // then the precision should be zero.
103 | ++index
104 | precision = parseInt(format.substring(index), 10)
105 | if (isNaN(precision)) precision = 0
106 |
107 | while (isDigitAt(format, index)) ++index
108 | }
109 |
110 | if (!(format[index] in formatters)) {
111 | addStringToken(format.substring(precentIndex, index + 1))
112 | ++index
113 | continue
114 | }
115 |
116 | addSpecifierToken(format[index], precision, substitutionIndex)
117 |
118 | ++substitutionIndex
119 | ++index
120 | }
121 |
122 | addStringToken(format.substring(index))
123 |
124 | return tokens
125 | }
126 |
127 |
128 | /**
129 | * @param {string} format
130 | * @param {?ArrayLike} substitutions
131 | * @param {!Object.} formatters
132 | * @param {!T} initialValue
133 | * @param {function(T, Q): T|undefined} append
134 | * @param {!Array.=} tokenizedFormat
135 | * @return {!{formattedResult: T, unusedSubstitutions: ?ArrayLike}};
136 | * @template T, Q
137 | */
138 | export function format(
139 | format?: any,
140 | substitutions?: any,
141 | formatters?: any,
142 | initialValue?: any,
143 | append?: any,
144 | tokenizedFormat?: any
145 | ) {
146 | if (!format || !substitutions || !substitutions.length)
147 | return {
148 | formattedResult: append(initialValue, format),
149 | unusedSubstitutions: substitutions
150 | }
151 |
152 | function prettyFunctionName() {
153 | return (
154 | 'String.format("' +
155 | format +
156 | '", "' +
157 | Array.prototype.join.call(substitutions, '", "') +
158 | '")'
159 | )
160 | }
161 |
162 | function warn(msg: any) {
163 | console.warn(prettyFunctionName() + ': ' + msg)
164 | }
165 |
166 | function error(msg: any) {
167 | console.error(prettyFunctionName() + ': ' + msg)
168 | }
169 |
170 | var result = initialValue
171 | var tokens =
172 | tokenizedFormat || tokenizeFormatString(format, formatters)
173 | var usedSubstitutionIndexes = {}
174 |
175 | for (var i = 0; i < tokens.length; ++i) {
176 | var token = tokens[i]
177 |
178 | if (token.type === 'string') {
179 | result = append(result, token.value)
180 | continue
181 | }
182 |
183 | if (token.type !== 'specifier') {
184 | error('Unknown token type "' + token.type + '" found.')
185 | continue
186 | }
187 |
188 | if (token.substitutionIndex >= substitutions.length) {
189 | // If there are not enough substitutions for the current substitutionIndex
190 | // just output the format specifier literally and move on.
191 | error(
192 | 'not enough substitution arguments. Had ' +
193 | substitutions.length +
194 | ' but needed ' +
195 | (token.substitutionIndex + 1) +
196 | ', so substitution was skipped.'
197 | )
198 | result = append(
199 | result,
200 | '%' + (token.precision > -1 ? token.precision : '') + token.specifier
201 | )
202 | continue
203 | }
204 |
205 | usedSubstitutionIndexes[token.substitutionIndex] = true
206 |
207 | if (!(token.specifier in formatters)) {
208 | // Encountered an unsupported format character, treat as a string.
209 | warn(
210 | 'unsupported format character \u201C' +
211 | token.specifier +
212 | '\u201D. Treating as a string.'
213 | )
214 | result = append(result, substitutions[token.substitutionIndex])
215 | continue
216 | }
217 |
218 | result = append(
219 | result,
220 | formatters[token.specifier](
221 | substitutions[token.substitutionIndex],
222 | token
223 | )
224 | )
225 | }
226 |
227 | var unusedSubstitutions = [] as any
228 | for (var i = 0; i < substitutions.length; ++i) {
229 | if (i in usedSubstitutionIndexes) continue
230 | unusedSubstitutions.push(substitutions[i])
231 | }
232 |
233 | return { formattedResult: result, unusedSubstitutions: unusedSubstitutions }
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/Component/elements.tsx:
--------------------------------------------------------------------------------
1 | import styled from './theme'
2 |
3 | /**
4 | * Return themed log-method style
5 | * @param style The style
6 | * @param type The method
7 | */
8 | const Themed = (
9 | style: string,
10 | method: string,
11 | styles: { [name: string]: string }
12 | ) =>
13 | styles[`LOG_${method.toUpperCase()}_${style.toUpperCase()}`] ||
14 | styles[`LOG_${style.toUpperCase()}`]
15 |
16 | /**
17 | * console-feed
18 | */
19 | export const Root = styled('div')({
20 | wordBreak: 'break-word',
21 | width: '100%',
22 | })
23 |
24 | /**
25 | * console-message
26 | */
27 | export const Message = styled('div')(({ theme: { styles, method } }) => ({
28 | position: 'relative',
29 | display: 'flex',
30 | color: Themed('color', method, styles),
31 | backgroundColor: Themed('background', method, styles),
32 | borderTop: `1px solid ${Themed('border', method, styles)}`,
33 | borderBottom: `1px solid ${Themed('border', method, styles)}`,
34 | marginTop: -1,
35 | marginBottom: +/^warn|error$/.test(method),
36 | padding: styles.PADDING,
37 | boxSizing: 'border-box',
38 | '& *': {
39 | boxSizing: 'border-box',
40 | fontFamily: styles.BASE_FONT_FAMILY,
41 | whiteSpace: 'pre-wrap',
42 | fontSize: styles.BASE_FONT_SIZE,
43 | },
44 | '& a': {
45 | color: styles.LOG_LINK_COLOR,
46 | },
47 | }))
48 |
49 | /**
50 | * Icon container
51 | */
52 | export const IconContainer = styled('div')(() => ({
53 | paddingLeft: 10,
54 | }))
55 |
56 | /**
57 | * message-icon
58 | */
59 | export const Icon = styled('div')(({ theme: { styles, method } }) => ({
60 | width: styles.LOG_ICON_WIDTH,
61 | height: styles.LOG_ICON_HEIGHT,
62 | backgroundImage: Themed('icon', method, styles),
63 | backgroundRepeat: 'no-repeat',
64 | backgroundSize: styles.LOG_ICON_BACKGROUND_SIZE,
65 | backgroundPosition: 'center',
66 | }))
67 |
68 | /**
69 | * message-amount
70 | */
71 | export const AmountIcon = styled('div')(({ theme: { styles, method } }) => ({
72 | // make it a circle if the amount is one digit
73 | minWidth: `${16 / 12}em`,
74 | height: `${16 / 12}em`,
75 | margin: '1px 0',
76 | whiteSpace: 'nowrap',
77 | fontSize: `${10 / 12}em!important`,
78 | padding: '0px 3px',
79 | background: Themed('amount_background', method, styles),
80 | color: Themed('amount_color', method, styles),
81 | borderRadius: '9999px',
82 | display: 'flex',
83 | alignItems: 'center',
84 | justifyContent: 'center',
85 | }))
86 |
87 | /**
88 | * timestamp
89 | */
90 | export const Timestamp = styled('div')(({ theme: { styles, method } }) => ({
91 | marginLeft: 5,
92 | color: 'dimgray',
93 | }))
94 |
95 | /**
96 | * console-content
97 | */
98 | export const Content = styled('div')(({ theme: { styles } }) => ({
99 | clear: 'right',
100 | position: 'relative',
101 | marginLeft: 15,
102 | flex: 1,
103 | }))
104 |
--------------------------------------------------------------------------------
/src/Component/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { ThemeProvider } from 'emotion-theming'
3 | import { Props } from '../definitions/Component'
4 | import Styles from './theme/default'
5 |
6 | import { Root } from './elements'
7 | import Message from './Message'
8 |
9 | // https://stackoverflow.com/a/48254637/4089357
10 | const customStringify = function (v) {
11 | const cache = new Set()
12 | return JSON.stringify(v, function (key, value) {
13 | if (typeof value === 'object' && value !== null) {
14 | if (cache.has(value)) {
15 | // Circular reference found, discard key
16 | return
17 | }
18 | // Store value in our set
19 | cache.add(value)
20 | }
21 | return value
22 | })
23 | }
24 |
25 | const getTheme = (props: Props) => ({
26 | variant: props.variant || 'light',
27 | styles: {
28 | ...Styles(props),
29 | ...props.styles,
30 | },
31 | })
32 |
33 | class Console extends React.PureComponent {
34 | state = {
35 | theme: getTheme(this.props),
36 | prevStyles: this.props.styles,
37 | prevVariant: this.props.variant,
38 | }
39 |
40 | static getDerivedStateFromProps(props, state) {
41 | if (
42 | props.variant !== state.prevVariant ||
43 | JSON.stringify(props.styles) !== JSON.stringify(props.prevStyles)
44 | ) {
45 | return {
46 | theme: getTheme(props),
47 | prevStyles: props.styles,
48 | prevVariant: props.variant,
49 | }
50 | }
51 | return null
52 | }
53 |
54 | render() {
55 | let {
56 | filter = [],
57 | logs = [],
58 | searchKeywords,
59 | logFilter,
60 | logGrouping = true,
61 | } = this.props
62 |
63 | if (searchKeywords) {
64 | const regex = new RegExp(searchKeywords)
65 |
66 | const filterFun = logFilter
67 | ? logFilter
68 | : (log) => {
69 | try {
70 | return regex.test(customStringify(log))
71 | } catch (e) {
72 | return true
73 | }
74 | }
75 |
76 | // @ts-ignore
77 | logs = logs.filter(filterFun)
78 | }
79 |
80 | if (logGrouping) {
81 | // @ts-ignore
82 | logs = logs.reduce((acc, log) => {
83 | const prevLog = acc[acc.length - 1]
84 |
85 | if (
86 | prevLog &&
87 | prevLog.amount &&
88 | prevLog.method === log.method &&
89 | prevLog.data.length === log.data.length &&
90 | prevLog.data.every((value, i) => log.data[i] === value)
91 | ) {
92 | prevLog.amount += 1
93 |
94 | return acc
95 | }
96 |
97 | acc.push({ ...log, amount: 1 })
98 |
99 | return acc
100 | }, [])
101 | }
102 |
103 | return (
104 |
105 |
106 | {logs.map((log, i) => {
107 | // If the filter is defined and doesn't include the method
108 | const filtered =
109 | filter.length !== 0 &&
110 | log.method &&
111 | filter.indexOf(log.method) === -1
112 |
113 | return filtered ? null : (
114 |
120 | )
121 | })}
122 |
123 |
124 | )
125 | }
126 | }
127 |
128 | export default Console
129 |
--------------------------------------------------------------------------------
/src/Component/message-parsers/Error.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Linkify from 'linkifyjs/react'
3 |
4 | function splitMessage(message: string): string {
5 | const breakIndex = message.indexOf('\n')
6 | // consider that there can be line without a break
7 | if (breakIndex === -1) {
8 | return message
9 | }
10 | return message.substr(0, breakIndex)
11 | }
12 |
13 | function ErrorPanel({ error }: { error: string }) {
14 | /* This checks for error logTypes and shortens the message in the console by wrapping
15 | it a tag and putting the first line in a tag and the other lines
16 | follow after that. This creates a nice collapsible error message */
17 | let otherErrorLines
18 | const firstLine = splitMessage(error)
19 | const msgArray = error.split('\n')
20 | if (msgArray.length > 1) {
21 | otherErrorLines = msgArray.slice(1)
22 | }
23 |
24 | if (!otherErrorLines) {
25 | return {error}
26 | }
27 |
28 | return (
29 |
30 |
31 | {firstLine}
32 |
33 | {otherErrorLines.join('\n\r')}
34 |
35 | )
36 | }
37 |
38 | export default ErrorPanel
39 |
--------------------------------------------------------------------------------
/src/Component/message-parsers/Formatted.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Root } from '../react-inspector/elements'
3 |
4 | import Format from '../devtools-parser'
5 |
6 | interface Props {
7 | data: any[]
8 | }
9 |
10 | class Formatted extends React.PureComponent {
11 | render() {
12 | return (
13 |
19 | )
20 | }
21 | }
22 |
23 | export default Formatted
24 |
--------------------------------------------------------------------------------
/src/Component/message-parsers/Object.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Theme } from '../../definitions/Component'
3 | import { withTheme } from 'emotion-theming'
4 | import { Root } from '../react-inspector/elements'
5 |
6 | import Linkify from 'linkifyjs/react'
7 | import type { Options } from 'linkifyjs'
8 | import { Message } from '../../definitions/Component'
9 | import Inspector from '../react-inspector'
10 |
11 | interface Props {
12 | log: Message
13 | quoted: boolean
14 | theme?: Theme
15 | linkifyOptions?: Options
16 | }
17 |
18 | class ObjectTree extends React.PureComponent {
19 | render() {
20 | const { theme, quoted, log } = this.props
21 |
22 | return log.data.map((message: any, i: number) => {
23 | if (typeof message === 'string') {
24 | const string =
25 | !quoted && message.length ? (
26 | `${message} `
27 | ) : (
28 |
29 | "
30 |
35 | {message}
36 |
37 | "
38 |
39 | )
40 |
41 | return (
42 |
43 | {string}
44 |
45 | )
46 | }
47 |
48 | return
49 | })
50 | }
51 | }
52 |
53 | export default withTheme(ObjectTree)
54 |
--------------------------------------------------------------------------------
/src/Component/react-inspector/elements.tsx:
--------------------------------------------------------------------------------
1 | import styled from '../theme'
2 |
3 | /**
4 | * Object root
5 | */
6 | export const Root = styled('div')({
7 | display: 'inline-block',
8 | wordBreak: 'break-all',
9 | '&::after': {
10 | content: `' '`,
11 | display: 'inline-block',
12 | },
13 | '& > li, & > ol, & > details': {
14 | backgroundColor: 'transparent !important',
15 | display: 'inline-block',
16 | },
17 | '& ol:empty': {
18 | paddingLeft: '0 !important',
19 | },
20 | })
21 |
22 | /**
23 | * Table
24 | */
25 | export const Table = styled('span')({
26 | '& > li': {
27 | display: 'inline-block',
28 | marginTop: 5,
29 | },
30 | // override react-inspector/TableInspectorHeaderContainer.base
31 | '& div[style*="height: 17px"]': {
32 | height: `${17 / 12}em!important`,
33 | },
34 | // override react-inspector/TableInspectorDataContainer.td
35 | '& td[style*="height: 16px"]': {
36 | height: `${16 / 12}em!important`,
37 | lineHeight: `1!important`,
38 | verticalAlign: 'middle!important',
39 | },
40 | '& table[style*="background-size: 128px 32px"]': {
41 | // = td's fontSize * 2
42 | backgroundSize: `128px ${(16 / 12) * 2}em!important`,
43 | },
44 | })
45 |
46 | /**
47 | * HTML
48 | */
49 | export const HTML = styled('span')({
50 | display: 'inline-block',
51 | '& div:hover': {
52 | backgroundColor: 'rgba(255, 220, 158, .05) !important',
53 | borderRadius: '2px',
54 | },
55 | })
56 |
57 | /**
58 | * Object constructor
59 | */
60 | export const Constructor = styled('span')({
61 | '& > span > span:nth-child(1)': {
62 | opacity: 0.6,
63 | },
64 | })
65 |
--------------------------------------------------------------------------------
/src/Component/react-inspector/index.tsx:
--------------------------------------------------------------------------------
1 | import { withTheme } from 'emotion-theming'
2 | import * as React from 'react'
3 | import {
4 | DOMInspector,
5 | Inspector,
6 | ObjectLabel,
7 | ObjectName,
8 | ObjectValue,
9 | ObjectPreview,
10 | } from 'react-inspector'
11 |
12 | import { Context } from '../../definitions/Component'
13 | import ErrorPanel from '../message-parsers/Error'
14 | import { Constructor, HTML, Root, Table } from './elements'
15 |
16 | interface Props {
17 | theme?: Context
18 | data: any
19 | }
20 |
21 | const REMAINING_KEY = '__console_feed_remaining__'
22 |
23 | // copied from react-inspector
24 | function intersperse(arr, sep) {
25 | if (arr.length === 0) {
26 | return []
27 | }
28 |
29 | return arr.slice(1).reduce((xs, x) => xs.concat([sep, x]), [arr[0]])
30 | }
31 |
32 | const getArrayLength = (array: Array) => {
33 | if (!array || array.length < 1) {
34 | return 0
35 | }
36 |
37 | const remainingKeyCount = array[array.length - 1]
38 | .toString()
39 | .split(REMAINING_KEY)
40 |
41 | if (remainingKeyCount[1] === undefined) {
42 | return array.length
43 | } else {
44 | const remaining = parseInt(
45 | array[array.length - 1].toString().split(REMAINING_KEY)[1]
46 | )
47 |
48 | return array.length - 1 + remaining
49 | }
50 | }
51 |
52 | const CustomObjectRootLabel = ({ name, data }) => {
53 | let rootData = data
54 | if (typeof data === 'object' && !Array.isArray(data) && data !== null) {
55 | const object = {}
56 | for (const propertyName in data) {
57 | if (data.hasOwnProperty(propertyName)) {
58 | let propertyValue = data[propertyName]
59 | if (Array.isArray(propertyValue)) {
60 | const arrayLength = getArrayLength(propertyValue)
61 | object[propertyName] = new Array(arrayLength)
62 | } else {
63 | object[propertyName] = propertyValue
64 | }
65 | }
66 | }
67 | rootData = Object.assign(Object.create(Object.getPrototypeOf(data)), object)
68 | }
69 | if (typeof name === 'string') {
70 | return (
71 |
72 |
73 | :
74 |
75 |
76 | )
77 | } else {
78 | return
79 | }
80 | }
81 |
82 | const CustomObjectLabel = ({ name, data, isNonenumerable = false }) =>
83 | name === REMAINING_KEY ? (
84 | data > 0 ? (
85 | {data} more...
86 | ) : null
87 | ) : (
88 |
89 | {typeof name === 'string' ? (
90 |
91 | ) : (
92 |
93 | )}
94 | :
95 |
96 |
97 |
98 | )
99 |
100 | class CustomInspector extends React.PureComponent {
101 | render() {
102 | const { data, theme } = this.props
103 | const { styles, method } = theme
104 |
105 | const dom = data instanceof HTMLElement
106 | const table = method === 'table'
107 |
108 | return (
109 |
110 | {table ? (
111 |
119 | ) : dom ? (
120 |
121 |
122 |
123 | ) : (
124 |
129 | )}
130 |
131 | )
132 | }
133 |
134 | getCustomNode(data: any) {
135 | const { styles } = this.props.theme
136 | const constructor = data?.constructor?.name
137 |
138 | if (constructor === 'Function')
139 | return (
140 |
141 |
142 | {` {`}
143 | {data.body}
144 | {`}`}
145 |
146 | )
147 |
148 | if (data instanceof Error && typeof data.stack === 'string') {
149 | return
150 | }
151 |
152 | if (constructor === 'Promise')
153 | return (
154 |
155 | Promise {`{`}
156 | {``}
157 | {`}`}
158 |
159 | )
160 |
161 | if (data instanceof HTMLElement)
162 | return (
163 |
164 |
165 |
166 | )
167 |
168 | if (Array.isArray(data)) {
169 | const arrayLength = getArrayLength(data)
170 | const maxProperties = styles.OBJECT_PREVIEW_ARRAY_MAX_PROPERTIES
171 |
172 | if (
173 | typeof data[data.length - 1] === 'string' &&
174 | data[data.length - 1].includes(REMAINING_KEY)
175 | ) {
176 | data = data.slice(0, -1)
177 | }
178 |
179 | const previewArray = data
180 | .slice(0, maxProperties)
181 | .map((element, index) => {
182 | if (Array.isArray(element)) {
183 | return (
184 |
188 | )
189 | } else {
190 | return
191 | }
192 | })
193 | if (arrayLength > maxProperties) {
194 | previewArray.push(…)
195 | }
196 | return (
197 |
198 |
199 | {arrayLength === 0 ? `` : `(${arrayLength})\xa0`}
200 |
201 |
202 | [{intersperse(previewArray, ', ')}
203 | {}]
204 |
205 |
206 | )
207 | }
208 |
209 | return null
210 | }
211 |
212 | nodeRenderer(props: any) {
213 | let { depth, name, data, isNonenumerable } = props
214 |
215 | // Root
216 | if (depth === 0) {
217 | const customNode = this.getCustomNode(data)
218 | return customNode ||
219 | }
220 |
221 | if (typeof data === 'string' && data.includes(REMAINING_KEY)) {
222 | name = REMAINING_KEY
223 | data = data.split(REMAINING_KEY)[1]
224 | }
225 |
226 | if (name === 'constructor')
227 | return (
228 |
229 |
234 |
235 | )
236 |
237 | const customNode = this.getCustomNode(data)
238 |
239 | return customNode ? (
240 |
241 |
242 | :
243 | {customNode}
244 |
245 | ) : (
246 |
251 | )
252 | }
253 | }
254 |
255 | export default withTheme(CustomInspector)
256 |
--------------------------------------------------------------------------------
/src/Component/react-inspector/util.ts:
--------------------------------------------------------------------------------
1 | export const isMinusZero = value => 1 / value === -Infinity
2 |
--------------------------------------------------------------------------------
/src/Component/theme/default.ts:
--------------------------------------------------------------------------------
1 | import { chromeDark, chromeLight } from 'react-inspector'
2 | import { Styles } from '../../definitions/Styles'
3 | import { Props } from '../../definitions/Component'
4 |
5 | const styles = (props: Props) => {
6 | const isLight = (props.variant || 'light') === 'light'
7 | const chrome = isLight ? chromeLight : chromeDark
8 |
9 | return {
10 | ...chrome,
11 | /**
12 | * General
13 | */
14 | PADDING: '3px 22px 2px 0',
15 |
16 | /**
17 | * Default log styles
18 | */
19 | LOG_COLOR: chrome.BASE_COLOR,
20 | LOG_BACKGROUND: 'transparent',
21 | LOG_BORDER: isLight ? 'rgb(236,236,236)' : 'rgb(44,44,44)',
22 | LOG_ICON_WIDTH: `${10 / 12}em`,
23 | LOG_ICON_HEIGHT: `${10 / 12}em`,
24 | LOG_ICON_BACKGROUND_SIZE: 'contain',
25 | LOG_ICON: 'none',
26 | LOG_AMOUNT_BACKGROUND: '#42597f',
27 | LOG_AMOUNT_COLOR: '#8d8f91',
28 | LOG_LINK_COLOR: isLight ? 'rgb(66, 66, 66)' : 'rgb(177, 177, 177)',
29 |
30 | /**
31 | * Log types
32 | */
33 | LOG_WARN_ICON: `url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACkSURBVChTbY7BCoJQFERn0Q/3BX1JuxQjsSCXiV8gtCgxhCIrKIRIqKDVzXl5w5cNHBjm6eGinXiAXu5inY2xYm/mbpIh+vcFhLA3sx0athNUhymEsP+10lAEEA17x8o/9wFuNGnYuVlWve0SQl7P0sBu3aq2R1Q/1JzSkYGd29eqNv2wjdnUuvNRciC/N+qe+7gidbA8zyHkOINsvA/sumcOkjcabcBmw2+mMgAAAABJRU5ErkJggg==)`,
34 | LOG_WARN_BACKGROUND: isLight ? 'rgb(255,250,220)' : '#332b00',
35 | LOG_WARN_COLOR: isLight ? 'rgb(73,45,2)' : '#ffdc9e',
36 | LOG_WARN_BORDER: isLight ? 'rgb(255,244,181)' : '#650',
37 | LOG_WARN_AMOUNT_BACKGROUND: '#ffbb17',
38 | LOG_WARN_AMOUNT_COLOR: '#8d8f91',
39 |
40 | LOG_ERROR_ICON: `url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADESURBVChTY4CB7ZI8tmfU5E6e01b+DMIgNkgMKg0BR9Vkux6YWPx/bemIgkFiIDmwogOaqrYPzazAEm8DwuGKYGyQHEgNw0VT05Mwib9v3v7/kJEHxiA2TDFIDcNNU4vPMFPACj58/P/v40cwGyYOUsNwy8IZRSFIEUgxskKQGoZrzp4ErQapYbgYHG371M4dLACTQGaD5EBqwD6/FpzQ9dTBE64IhkFiIDmwIhi4mlJqey8o4eR9r8jPIAxig8QgsgwMAFZz1YtGPXgjAAAAAElFTkSuQmCC)`,
41 | LOG_ERROR_BACKGROUND: isLight ? 'rgb(255,235,235)' : '#290000',
42 | LOG_ERROR_BORDER: isLight ? 'rgb(253,204,205)' : '#5b0000',
43 | LOG_ERROR_COLOR: isLight ? 'rgb(252,0,5)' : '#ff8080',
44 | LOG_ERROR_AMOUNT_BACKGROUND: '#dc2727',
45 | LOG_ERROR_AMOUNT_COLOR: '#8d8f91',
46 |
47 | LOG_DEBUG_ICON: `url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 459 459'%3e%3cpath fill='%234D88FF' d='M433.5 127.5h-71.4a177.7 177.7 0 0 0-45.9-51L357 35.7 321.3 0l-56.1 56.1c-10.2-2.6-23-5.1-35.7-5.1s-25.5 2.5-35.7 5.1L137.7 0 102 35.7l40.8 40.8a177.7 177.7 0 0 0-45.9 51H25.5v51H79c-2.5 7.7-2.5 17.9-2.5 25.5v25.5h-51v51h51V306a88 88 0 0 0 2.5 25.5H25.5v51h71.4A152.2 152.2 0 0 0 229.5 459c56.1 0 107.1-30.6 132.6-76.5h71.4v-51H380c2.5-7.7 2.5-17.9 2.5-25.5v-25.5h51v-51h-51V204c0-7.7 0-17.9-2.5-25.5h53.5v-51zm-153 204h-102v-51h102v51zm0-102h-102v-51h102v51z'/%3e%3c/svg%3e")`,
48 | LOG_DEBUG_BACKGROUND: '',
49 | LOG_DEBUG_BORDER: '',
50 | LOG_DEBUG_COLOR: '#4D88FF',
51 |
52 | LOG_COMMAND_ICON: `url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABaSURBVChTY6AtmDx5cvnUqVP1oFzsoL+/XwCo8DEQv584caIVVBg7mDBhghxQ4Y2+vr6vU6ZM8YAKYwdA00SB+CxQ8S+g4jCoMCYgSiFRVpPkGaAiHMHDwAAA5Ko+F4/l6+MAAAAASUVORK5CYII=)`,
53 | LOG_RESULT_ICON: `url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABpSURBVChTY6A92LNnj96uXbvKoVzsYMeOHVbbt29/D1T4eP/+/QJQYVSwe/duD6CCr0B8A8iWgwqjAqBk2NatW38B6bPbtm0TBYkBFbsA+c9ANFgRCBCtEASAAoSthgGiPAMD2IOHgQEA521bM7uG52wAAAAASUVORK5CYII=)`,
54 | LOG_INFO_ICON: `url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADISURBVChTY4ABp/AztmZBZ07qe538rO114rOa8+GTskYHbKHSEOARd6nLIOTsf61gIA46U6kVePYQiK3uc/K/hPG+LrCi8IyrtkZh5yCKgk/80w46ba0RdGYGhH/2v6rXyf88qtttGVwSLp2ECQLxeiAu1wo6uwpJ7L+o2f6TDA6xZz8jCyqFnuHXCj4djywmZXHoM/EK0azGqhBsNYpngL6VCTnGqRF4xgKo+D5IDO4ZEEAKnjcQBafvqwWf/YoSPDCAP8AZGAC7mLM81zgOTQAAAABJRU5ErkJggg==)`,
55 |
56 | /**
57 | * Fonts
58 | */
59 | BASE_FONT_FAMILY: 'Consolas, Lucida Console, Courier New, monospace',
60 | BASE_FONT_SIZE: '12px',
61 |
62 | /**
63 | * Other
64 | */
65 | ARROW_FONT_SIZE: `${10 / 12}em`,
66 | OBJECT_VALUE_STRING_COLOR: 'rgb(233,63,59)',
67 | } as Styles
68 | }
69 |
70 | export default styles
71 |
--------------------------------------------------------------------------------
/src/Component/theme/index.ts:
--------------------------------------------------------------------------------
1 | import styled, { CreateStyled } from '@emotion/styled'
2 | import { Context } from '../../definitions/Component'
3 |
4 | export default styled as CreateStyled
5 |
--------------------------------------------------------------------------------
/src/Hook/__tests__/Hook.spec.tsx:
--------------------------------------------------------------------------------
1 | import Hook from '..'
2 | import console from './console'
3 | import Log from './Log'
4 | import { Decode } from '../..'
5 |
6 | it('hooks the console', () => {
7 | Hook(console, (log) => {
8 | console.logs.push(log)
9 | })
10 | expect(console.feed).toBeTruthy()
11 | })
12 |
13 | it('forwards log events', async () => {
14 | const result = await Log('log', 'test')
15 | expect(result).toBeTruthy()
16 | })
17 |
18 | it('decodes messages', () => {
19 | const decoded = Decode(console.logs[0])
20 | expect(decoded.method).toEqual('log')
21 | expect(decoded.data).toMatchSnapshot()
22 | })
23 |
24 | it('correctly encodes a `bigint`', async () => {
25 | const result = await Log('warn', BigInt(1))
26 | expect(result).toBeTruthy()
27 |
28 | const decoded = Decode(result)
29 | expect(decoded.method).toEqual('warn')
30 | expect(decoded.data).toMatchSnapshot()
31 | })
32 |
33 | it('correctly encodes a HTMLElement', async () => {
34 | const result = await Log('warn', document.documentElement)
35 | expect(result).toBeTruthy()
36 |
37 | const decoded = Decode(result)
38 | expect(decoded.method).toEqual('warn')
39 | expect(decoded.data).toMatchSnapshot()
40 | })
41 |
42 | it('correctly encodes Functions', async () => {
43 | // prettier-ignore
44 | const result = await Log('error', function myFunc() { /* body */ })
45 |
46 | const decoded = Decode(result)
47 | expect(decoded.method).toEqual('error')
48 | expect(decoded.data).toMatchSnapshot()
49 | })
50 |
51 | it('correctly encodes nested values', async () => {
52 | const input = {
53 | function: function myFunc() {},
54 | document: document.documentElement,
55 | nested: [[[new Promise(() => {})]]],
56 | recursive: null,
57 | }
58 | input.recursive = input
59 |
60 | const result = await Log('debug', input)
61 |
62 | const decoded = Decode(result)
63 | expect(decoded.method).toEqual('debug')
64 | expect(decoded.data).toMatchSnapshot()
65 | })
66 |
67 | it('disables encoding with a flag', async () => {
68 | Hook(
69 | console,
70 | (log) => {
71 | console.logs.push(log)
72 | },
73 | false
74 | )
75 | const input = {
76 | function: function myFunc() {},
77 | document: document.documentElement,
78 | nested: [[[new Promise(() => {})]]],
79 | recursive: null,
80 | }
81 | input.recursive = input
82 |
83 | const result: any = await Log('debug', input)
84 |
85 | expect(result.data).toMatchSnapshot()
86 | })
87 |
88 | it('correctly limits a long array', async () => {
89 | Hook(
90 | console,
91 | (log) => {
92 | console.logs.push(log)
93 | },
94 | true,
95 | 100
96 | )
97 | const result = await Log('log', Array.from(Array(99999).keys()))
98 | expect(result[0].data[0].length).toEqual(101)
99 | expect(result[0].data[0].pop()).toEqual('__console_feed_remaining__99899')
100 | })
101 |
102 | it('correctly limits a long object', async () => {
103 | Hook(
104 | console,
105 | (log) => {
106 | console.logs.push(log)
107 | },
108 | true,
109 | 100
110 | )
111 | const result = await Log('log', { ...Array.from(Array(99999).keys()) })
112 | expect(Object.keys(result[0].data[0]).length).toEqual(101)
113 | expect(result[0].data[0]['__console_feed_remaining__']).toEqual(99899)
114 | })
115 |
--------------------------------------------------------------------------------
/src/Hook/__tests__/Log.tsx:
--------------------------------------------------------------------------------
1 | import console from './console'
2 | import { Message } from '../../definitions/Console'
3 |
4 | function Log(type: string, ...data: any[]): Promise {
5 | return new Promise((resolve, reject) => {
6 | const length = console.logs.length
7 | console[type](...data)
8 |
9 | setTimeout(() => {
10 | if (console.logs.length !== length) {
11 | resolve(console.logs[console.logs.length - 1])
12 | }
13 | reject()
14 | })
15 | })
16 | }
17 |
18 | export default Log
19 |
--------------------------------------------------------------------------------
/src/Hook/__tests__/__snapshots__/Hook.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`correctly encodes Functions 1`] = `
4 | Array [
5 | [Function],
6 | ]
7 | `;
8 |
9 | exports[`correctly encodes a \`bigint\` 1`] = `
10 | Array [
11 | BigInt {},
12 | ]
13 | `;
14 |
15 | exports[`correctly encodes a HTMLElement 1`] = `
16 | Array [
17 |
18 |
19 |
20 | ,
21 | ]
22 | `;
23 |
24 | exports[`correctly encodes nested values 1`] = `
25 | Array [
26 | Object {
27 | "document":
28 |
29 |
30 | ,
31 | "function": [Function],
32 | "nested": Array [
33 | Array [
34 | Array [
35 | Object {
36 | "constructor": Object {
37 | "name": "Promise",
38 | },
39 | },
40 | "__console_feed_remaining__0",
41 | ],
42 | "__console_feed_remaining__0",
43 | ],
44 | "__console_feed_remaining__0",
45 | ],
46 | "recursive": [Circular],
47 | },
48 | ]
49 | `;
50 |
51 | exports[`decodes messages 1`] = `
52 | Array [
53 | "test",
54 | ]
55 | `;
56 |
57 | exports[`disables encoding with a flag 1`] = `
58 | Array [
59 | Object {
60 | "document":
61 |
62 |
63 | ,
64 | "function": [Function],
65 | "nested": Array [
66 | Array [
67 | Array [
68 | Promise {},
69 | ],
70 | ],
71 | ],
72 | "recursive": [Circular],
73 | },
74 | ]
75 | `;
76 |
--------------------------------------------------------------------------------
/src/Hook/__tests__/console.ts:
--------------------------------------------------------------------------------
1 | import { HookedConsole, Message } from '../../definitions/Console'
2 |
3 | interface Console extends HookedConsole {
4 | logs: Message[]
5 | $log: Function
6 | }
7 |
8 | declare const console: Console
9 | console.logs = []
10 | ;['log', 'warn', 'info', 'error', 'debug', 'assert', 'time', 'timeEnd'].forEach(
11 | method => {
12 | console[`$${method}`] = console[method]
13 | console[method] = () => {}
14 | }
15 | )
16 |
17 | export default console
18 |
--------------------------------------------------------------------------------
/src/Hook/construct.ts:
--------------------------------------------------------------------------------
1 | const construct = (name: string): any => ({
2 | constructor: {
3 | name
4 | }
5 | })
6 |
7 | export default construct
8 |
--------------------------------------------------------------------------------
/src/Hook/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | HookedConsole,
3 | Callback,
4 | Storage,
5 | Methods as ConsoleMethods,
6 | Message,
7 | } from '../definitions/Console'
8 | import Methods from '../definitions/Methods'
9 |
10 | import Parse from './parse'
11 | import Unhook from '../Unhook'
12 | import { Encode } from '../Transform'
13 | // import Construct from './construct'
14 |
15 | /**
16 | * Hook a console constructor and forward messages to a callback
17 | * @argument console The Console constructor to Hook
18 | * @argument callback The callback to be called once a message is logged
19 | */
20 | export default function Hook(
21 | console: Console,
22 | callback: Callback,
23 | encode = true,
24 | limit?: number
25 | ) {
26 | const TargetConsole = console as HookedConsole
27 | const Storage: Storage = {
28 | pointers: {},
29 | src: {
30 | npm: 'https://npmjs.com/package/console-feed',
31 | github: 'https://github.com/samdenty/console-feed',
32 | },
33 | }
34 |
35 | // Override console methods
36 | for (let method of Methods) {
37 | const NativeMethod = TargetConsole[method]
38 |
39 | // Override
40 | TargetConsole[method] = function () {
41 | // Pass back to native method
42 | NativeMethod.apply(this, arguments)
43 |
44 | // Parse arguments and send to transport
45 | const args = [].slice.call(arguments)
46 |
47 | // setTimeout to prevent lag
48 | setTimeout(() => {
49 | const parsed = Parse(method as ConsoleMethods, args)
50 | if (parsed) {
51 | let encoded: Message = parsed as Message
52 | if (encode) {
53 | encoded = Encode(parsed, limit) as Message
54 | }
55 | callback(encoded, parsed)
56 | }
57 | })
58 | }
59 |
60 | // Store native methods
61 | Storage.pointers[method] = NativeMethod
62 | }
63 |
64 | TargetConsole.feed = Storage
65 |
66 | return TargetConsole
67 | }
68 |
--------------------------------------------------------------------------------
/src/Hook/parse/GUID.ts:
--------------------------------------------------------------------------------
1 | export default function guidGenerator(): string {
2 | let S4 = function() {
3 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
4 | }
5 | return (
6 | S4() +
7 | S4() +
8 | '-' +
9 | S4() +
10 | '-' +
11 | S4() +
12 | '-' +
13 | S4() +
14 | '-' +
15 | S4() +
16 | '-' +
17 | Date.now()
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/Hook/parse/__tests__/Parse.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash'
2 | import Parse from '..'
3 |
4 | it('asserts values', () => {
5 | expect(Parse('assert', [2 > 1], 'assert-true')).toBe(false)
6 | expect(Parse('assert', [1 > 2], 'assert-false')).toMatchSnapshot(
7 | 'assertion failed'
8 | )
9 | })
10 |
11 | describe('count', () => {
12 | it('counts with label', () => {
13 | let final
14 |
15 | _.times(10, () => {
16 | final = Parse('count', ['count-10'])
17 | })
18 |
19 | expect(final && final.data[0]).toBe('count-10: 10')
20 | })
21 |
22 | it('counts with default label', () => {
23 | let final
24 |
25 | _.times(10, () => {
26 | final = Parse('count', [])
27 | })
28 |
29 | expect(final && final.data[0]).toBe('default: 10')
30 | })
31 | })
32 |
33 | describe('time', () => {
34 | it('profile time with label', () => {
35 | Parse('time', ['timer-test'])
36 |
37 | setTimeout(() => {
38 | const result = Parse('timeEnd', ['timer-test'], 'timer-result')
39 | expect(
40 | result && +result.data[0].replace(/[^0-9]/g, '') > 100
41 | ).toBeTruthy()
42 | }, 100)
43 | })
44 |
45 | it('non existent label', () => {
46 | Parse('time', ['timer-test'])
47 |
48 | const failure = Parse('timeEnd', ['nonExistent'], 'timer-fail')
49 | expect(failure).toMatchSnapshot('non existent timer')
50 | })
51 |
52 | it('profile time with default label', () => {
53 | Parse('time', [])
54 |
55 | const result = Parse('timeEnd', [], 'timer-result')
56 | expect(result && result.data[0].match(/^default: \d+\.\d+ms$/)).toBeTruthy()
57 | })
58 | })
59 |
60 | it('records errors', () => {
61 | const result = Parse('error', [new Error('one')], 'errors')
62 |
63 | expect(result && result.data[0]).toContain('Error: one')
64 | })
65 |
--------------------------------------------------------------------------------
/src/Hook/parse/__tests__/__snapshots__/Parse.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`asserts values: assertion failed 1`] = `
4 | Object {
5 | "data": Array [
6 | "Assertion failed:",
7 | "console.assert",
8 | ],
9 | "id": "assert-false",
10 | "method": "error",
11 | }
12 | `;
13 |
14 | exports[`time non existent label: non existent timer 1`] = `
15 | Object {
16 | "data": Array [
17 | "Timer 'nonExistent' does not exist",
18 | ],
19 | "id": "timer-fail",
20 | "method": "warn",
21 | }
22 | `;
23 |
--------------------------------------------------------------------------------
/src/Hook/parse/index.ts:
--------------------------------------------------------------------------------
1 | import { Methods } from '../../definitions/Console'
2 | import { Payload } from '../../definitions/Payload'
3 | import GUID from './GUID'
4 |
5 | import * as Timing from './methods/timing'
6 | import * as Count from './methods/count'
7 | import * as Assert from './methods/assert'
8 |
9 | /**
10 | * Parses a console log and converts it to a special Log object
11 | * @argument method The console method to parse
12 | * @argument data The arguments passed to the console method
13 | */
14 | function Parse(
15 | method: Methods,
16 | data: any[],
17 | staticID?: string
18 | ): Payload | false {
19 | // Create an ID
20 | const id = staticID || GUID()
21 |
22 | // Parse the methods
23 | switch (method) {
24 | case 'clear': {
25 | return {
26 | method,
27 | id
28 | }
29 | }
30 |
31 | case 'count': {
32 | const label = typeof data[0] === 'string' ? data[0] : 'default'
33 | if (!label) return false
34 |
35 | return {
36 | ...Count.increment(label),
37 | id
38 | }
39 | }
40 |
41 | case 'time':
42 | case 'timeEnd': {
43 | const label = typeof data[0] === 'string' ? data[0] : 'default'
44 | if (!label) return false
45 |
46 | if (method === 'time') {
47 | Timing.start(label)
48 | return false
49 | }
50 |
51 | return {
52 | ...Timing.stop(label),
53 | id
54 | }
55 | }
56 |
57 | case 'assert': {
58 | const valid = data.length !== 0
59 |
60 | if (valid) {
61 | const assertion = Assert.test(data[0], ...data.slice(1))
62 | if (assertion) {
63 | return {
64 | ...assertion,
65 | id
66 | }
67 | }
68 | }
69 |
70 | return false
71 | }
72 |
73 | case 'error': {
74 | const errors = data.map(error => {
75 | try {
76 | return error.stack || error
77 | } catch (e) {
78 | return error
79 | }
80 | })
81 |
82 | return {
83 | method,
84 | id,
85 | data: errors
86 | }
87 | }
88 |
89 | default: {
90 | return {
91 | method,
92 | id,
93 | data
94 | }
95 | }
96 | }
97 | }
98 |
99 | export default Parse
100 |
--------------------------------------------------------------------------------
/src/Hook/parse/methods/assert.ts:
--------------------------------------------------------------------------------
1 | export function test(expression: any, ...messages: any[]): any {
2 | if (expression) return false
3 |
4 | // Default message
5 | if (messages.length === 0) messages.push('console.assert')
6 |
7 | return {
8 | method: 'error',
9 | data: [`Assertion failed:`, ...messages]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Hook/parse/methods/count.ts:
--------------------------------------------------------------------------------
1 | import { state } from '../../store/state'
2 | import dispatch from '../../store/dispatch'
3 | import { count } from '../../store/actions'
4 |
5 | export function increment(label: string): any {
6 | dispatch(count(label))
7 | const times = state.count[label]
8 |
9 | return {
10 | method: 'log',
11 | data: [`${label}: ${times}`]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Hook/parse/methods/timing.ts:
--------------------------------------------------------------------------------
1 | import { state } from '../../store/state'
2 | import dispatch from '../../store/dispatch'
3 | import { timeStart, timeEnd } from '../../store/actions'
4 |
5 | export function start(label: string) {
6 | dispatch(timeStart(label))
7 | }
8 |
9 | export function stop(label: string): any {
10 | const timing = state?.timings[label]
11 | if (timing && !timing.end) {
12 | dispatch(timeEnd(label))
13 | const { time } = state.timings[label]
14 |
15 | return {
16 | method: 'log',
17 | data: [`${label}: ${time}ms`],
18 | }
19 | }
20 | return {
21 | method: 'warn',
22 | data: [`Timer '${label}' does not exist`],
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Hook/store/actions.ts:
--------------------------------------------------------------------------------
1 | export function count(name: string) {
2 | return {
3 | type: 'COUNT',
4 | name
5 | }
6 | }
7 |
8 | export function timeStart(name: string) {
9 | return {
10 | type: 'TIME_START',
11 | name
12 | }
13 | }
14 |
15 | export function timeEnd(name: string) {
16 | return {
17 | type: 'TIME_END',
18 | name
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Hook/store/dispatch.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../../definitions/Store'
2 | import reduce from './reducer'
3 | import { state, update } from './state'
4 |
5 | function dispatch(action: Action) {
6 | update(reduce(state, action))
7 | }
8 |
9 | export default dispatch
10 |
--------------------------------------------------------------------------------
/src/Hook/store/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../../definitions/Store'
2 |
3 | export const initialState = {
4 | timings: {},
5 | count: {},
6 | }
7 |
8 | const now = () => {
9 | return typeof performance !== 'undefined' && performance.now
10 | ? performance.now()
11 | : Date.now()
12 | }
13 |
14 | export default (state = initialState, action: Action) => {
15 | switch (action.type) {
16 | case 'COUNT': {
17 | const times = state.count[action.name] || 0
18 |
19 | return {
20 | ...state,
21 | count: {
22 | ...state.count,
23 | [action.name]: times + 1,
24 | },
25 | }
26 | }
27 |
28 | case 'TIME_START': {
29 | return {
30 | ...state,
31 | timings: {
32 | ...state.timings,
33 | [action.name]: {
34 | start: now(),
35 | },
36 | },
37 | }
38 | }
39 |
40 | case 'TIME_END': {
41 | const timing = state.timings[action.name]
42 |
43 | const end = now()
44 | const { start } = timing
45 |
46 | const time = end - start
47 |
48 | return {
49 | ...state,
50 | timings: {
51 | ...state.timings,
52 | [action.name]: {
53 | ...timing,
54 | end,
55 | time,
56 | },
57 | },
58 | }
59 | }
60 |
61 | default: {
62 | return state
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Hook/store/state.ts:
--------------------------------------------------------------------------------
1 | import { initialState } from './reducer'
2 | export let state: typeof initialState
3 |
4 | export function update(newState: any) {
5 | state = newState
6 | }
7 |
--------------------------------------------------------------------------------
/src/Transform/BigInt.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Serialize a `bigint` to a string
3 | */
4 | export default {
5 | type: 'BigInt',
6 | shouldTransform(_type: any, obj: any) {
7 | return typeof obj === 'bigint'
8 | },
9 | toSerializable(value: bigint) {
10 | return `${value}n`
11 | },
12 | fromSerializable(data: string) {
13 | return BigInt(data.slice(0, -1))
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/src/Transform/Function.ts:
--------------------------------------------------------------------------------
1 | interface Storage {
2 | name: string
3 | body: string
4 | proto: string
5 | }
6 |
7 | /**
8 | * Serialize a function into JSON
9 | */
10 | export default {
11 | type: 'Function',
12 | lookup: Function,
13 | shouldTransform(type: any, obj: any) {
14 | return typeof obj === 'function'
15 | },
16 | toSerializable(func: Function): Storage {
17 | let body = ''
18 | try {
19 | body = func
20 | .toString()
21 | .substring(body.indexOf('{') + 1, body.lastIndexOf('}'))
22 | } catch (e) {}
23 |
24 | return {
25 | name: func.name,
26 | body,
27 | proto: Object.getPrototypeOf(func).constructor.name,
28 | }
29 | },
30 | fromSerializable(data: Storage) {
31 | try {
32 | const tempFunc = function () {}
33 |
34 | if (typeof data.name === 'string') {
35 | Object.defineProperty(tempFunc, 'name', {
36 | value: data.name,
37 | writable: false,
38 | })
39 | }
40 |
41 | if (typeof data.body === 'string') {
42 | Object.defineProperty(tempFunc, 'body', {
43 | value: data.body,
44 | writable: false,
45 | })
46 | }
47 |
48 | if (typeof data.proto === 'string') {
49 | // @ts-ignore
50 | tempFunc.constructor = {
51 | name: data.proto,
52 | }
53 | }
54 |
55 | return tempFunc
56 | } catch (e) {
57 | return data
58 | }
59 | },
60 | }
61 |
--------------------------------------------------------------------------------
/src/Transform/HTML.ts:
--------------------------------------------------------------------------------
1 | // Sandbox HTML elements
2 | let sandbox: Document
3 | function getSandbox() {
4 | return (sandbox ||= document.implementation.createHTMLDocument('sandbox'))
5 | }
6 |
7 | interface Storage {
8 | tagName: string
9 | attributes: {
10 | [attribute: string]: string
11 | }
12 | innerHTML: string
13 | }
14 |
15 | function objectifyAttributes(element: any) {
16 | const data = {}
17 | for (let attribute of element.attributes) {
18 | data[attribute.name] = attribute.value
19 | }
20 | return data
21 | }
22 |
23 | /**
24 | * Serialize a HTML element into JSON
25 | */
26 | export default {
27 | type: 'HTMLElement',
28 | shouldTransform(type: any, obj: any) {
29 | return (
30 | obj &&
31 | obj.children &&
32 | typeof obj.innerHTML === 'string' &&
33 | typeof obj.tagName === 'string'
34 | )
35 | },
36 | toSerializable(element: HTMLElement) {
37 | return {
38 | tagName: element.tagName.toLowerCase(),
39 | attributes: objectifyAttributes(element),
40 | innerHTML: element.innerHTML,
41 | } as Storage
42 | },
43 | fromSerializable(data: Storage) {
44 | try {
45 | const element = getSandbox().createElement(data.tagName)
46 | element.innerHTML = data.innerHTML
47 | for (let attribute of Object.keys(data.attributes)) {
48 | try {
49 | element.setAttribute(attribute, data.attributes[attribute])
50 | } catch (e) {}
51 | }
52 | return element
53 | } catch (e) {
54 | return data
55 | }
56 | },
57 | }
58 |
--------------------------------------------------------------------------------
/src/Transform/Map.ts:
--------------------------------------------------------------------------------
1 | interface Storage {
2 | name: string
3 | body: object
4 | proto: string
5 | }
6 |
7 | /**
8 | * Serialize a Map into JSON
9 | */
10 | export default {
11 | type: 'Map',
12 | lookup: Map,
13 | shouldTransform(type: any, obj: any) {
14 | return obj && obj.constructor && obj.constructor.name === 'Map'
15 | },
16 | toSerializable(map: any): Storage {
17 | let body = {}
18 |
19 | map.forEach(function (value, key) {
20 | const k = typeof key == 'object' ? JSON.stringify(key) : key
21 | body[k] = value
22 | })
23 |
24 | return {
25 | name: 'Map',
26 | body,
27 | proto: Object.getPrototypeOf(map).constructor.name,
28 | }
29 | },
30 | fromSerializable(data: Storage) {
31 | const { body } = data
32 | let obj = { ...body }
33 |
34 | if (typeof data.proto === 'string') {
35 | // @ts-ignore
36 | obj.constructor = {
37 | name: data.proto,
38 | }
39 | }
40 |
41 | return obj
42 | },
43 | }
44 |
--------------------------------------------------------------------------------
/src/Transform/arithmetic.ts:
--------------------------------------------------------------------------------
1 | enum Arithmetic {
2 | infinity,
3 | minusInfinity,
4 | minusZero,
5 | }
6 |
7 | function isMinusZero(value) {
8 | return 1 / value === -Infinity
9 | }
10 |
11 | export default {
12 | type: 'Arithmetic',
13 | lookup: Number,
14 | shouldTransform(type: any, value: any) {
15 | return (
16 | type === 'number' &&
17 | (value === Infinity || value === -Infinity || isMinusZero(value))
18 | )
19 | },
20 | toSerializable(value): Arithmetic {
21 | return value === Infinity
22 | ? Arithmetic.infinity
23 | : value === -Infinity
24 | ? Arithmetic.minusInfinity
25 | : Arithmetic.minusZero
26 | },
27 | fromSerializable(data: Arithmetic) {
28 | if (data === Arithmetic.infinity) return Infinity
29 | if (data === Arithmetic.minusInfinity) return -Infinity
30 | if (data === Arithmetic.minusZero) return -0
31 |
32 | return data
33 | },
34 | }
35 |
--------------------------------------------------------------------------------
/src/Transform/index.ts:
--------------------------------------------------------------------------------
1 | import { Message } from '../definitions/Console'
2 | import Arithmetic from './arithmetic'
3 | import BigInt from './BigInt'
4 | import Function from './Function'
5 | import HTML from './HTML'
6 | import Map from './Map'
7 |
8 | import Replicator from './replicator'
9 |
10 | const transforms = [HTML, Function, Arithmetic, Map, BigInt]
11 |
12 | const replicator = new Replicator()
13 | replicator.addTransforms(transforms)
14 |
15 | export function Encode(data: any, limit?: number): T {
16 | return JSON.parse(replicator.encode(data, limit))
17 | }
18 |
19 | export function Decode(data: any): Message {
20 | const decoded = replicator.decode(JSON.stringify(data))
21 | // remove __console_feed_remaining__
22 | decoded.data.pop()
23 | return decoded
24 | }
25 |
--------------------------------------------------------------------------------
/src/Transform/replicator/index.ts:
--------------------------------------------------------------------------------
1 | // Const
2 | const TRANSFORMED_TYPE_KEY = '@t'
3 | const CIRCULAR_REF_KEY = '@r'
4 | const KEY_REQUIRE_ESCAPING_RE = /^#*@(t|r)$/
5 |
6 | const REMAINING_KEY = '__console_feed_remaining__'
7 |
8 | const GLOBAL = (function getGlobal() {
9 | // NOTE: see http://www.ecma-international.org/ecma-262/6.0/index.html#sec-performeval step 10
10 | const savedEval = eval
11 |
12 | return savedEval('this')
13 | })()
14 |
15 | const ARRAY_BUFFER_SUPPORTED = typeof ArrayBuffer === 'function'
16 | const MAP_SUPPORTED = typeof Map === 'function'
17 | const SET_SUPPORTED = typeof Set === 'function'
18 |
19 | const TYPED_ARRAY_CTORS = [
20 | 'Int8Array',
21 | 'Uint8Array',
22 | 'Uint8ClampedArray',
23 | 'Int16Array',
24 | 'Uint16Array',
25 | 'Int32Array',
26 | 'Uint32Array',
27 | 'Float32Array',
28 | 'Float64Array',
29 | ]
30 |
31 | // Saved proto functions
32 | const arrSlice = Array.prototype.slice
33 |
34 | // Default serializer
35 | const JSONSerializer = {
36 | serialize(val: any) {
37 | return JSON.stringify(val)
38 | },
39 |
40 | deserialize(val: any) {
41 | return JSON.parse(val)
42 | },
43 | }
44 |
45 | // EncodingTransformer
46 | class EncodingTransformer {
47 | references: any
48 | transforms: any
49 | transformsMap: any
50 | circularCandidates: any
51 | circularCandidatesDescrs: any
52 | circularRefCount: any
53 | limit: number
54 |
55 | constructor(val: any, transforms: any, limit?: number) {
56 | this.references = val
57 | this.transforms = transforms
58 | this.transformsMap = this._makeTransformsMap()
59 | this.circularCandidates = []
60 | this.circularCandidatesDescrs = []
61 | this.circularRefCount = 0
62 | this.limit = limit ?? Infinity
63 | }
64 |
65 | static _createRefMark(idx: any) {
66 | const obj = Object.create(null)
67 |
68 | obj[CIRCULAR_REF_KEY] = idx
69 |
70 | return obj
71 | }
72 |
73 | _createCircularCandidate(val: any, parent: any, key: any) {
74 | this.circularCandidates.push(val)
75 | this.circularCandidatesDescrs.push({ parent, key, refIdx: -1 })
76 | }
77 |
78 | _applyTransform(val: any, parent: any, key: any, transform: any) {
79 | const result = Object.create(null)
80 | const serializableVal = transform.toSerializable(val)
81 |
82 | if (typeof serializableVal === 'object')
83 | this._createCircularCandidate(val, parent, key)
84 |
85 | result[TRANSFORMED_TYPE_KEY] = transform.type
86 | result.data = this._handleValue(() => serializableVal, parent, key)
87 |
88 | return result
89 | }
90 |
91 | _handleArray(arr: any): any {
92 | const result = [] as any
93 | const arrayLimit = Math.min(arr.length, this.limit)
94 | const remaining = arr.length - arrayLimit
95 |
96 | for (let i = 0; i < arrayLimit; i++)
97 | result[i] = this._handleValue(() => arr[i], result, i)
98 |
99 | result[arrayLimit] = REMAINING_KEY + remaining
100 |
101 | return result
102 | }
103 |
104 | _handlePlainObject(obj: any) {
105 | const result = Object.create(null)
106 | let counter = 0
107 | let total = 0
108 | for (const key in obj) {
109 | if (Reflect.has(obj, key)) {
110 | if (counter >= this.limit) {
111 | total++
112 | continue
113 | }
114 | const resultKey = KEY_REQUIRE_ESCAPING_RE.test(key) ? `#${key}` : key
115 |
116 | result[resultKey] = this._handleValue(() => obj[key], result, resultKey)
117 | counter++
118 | total++
119 | }
120 | }
121 |
122 | const remaining = total - counter
123 |
124 | const name = obj?.__proto__?.constructor?.name
125 | if (name && name !== 'Object') {
126 | result.constructor = { name }
127 | }
128 |
129 | if (remaining) {
130 | result[REMAINING_KEY] = remaining
131 | }
132 |
133 | return result
134 | }
135 |
136 | _handleObject(obj: any, parent: any, key: any) {
137 | this._createCircularCandidate(obj, parent, key)
138 |
139 | return Array.isArray(obj)
140 | ? this._handleArray(obj)
141 | : this._handlePlainObject(obj)
142 | }
143 |
144 | _ensureCircularReference(obj: any) {
145 | const circularCandidateIdx = this.circularCandidates.indexOf(obj)
146 |
147 | if (circularCandidateIdx > -1) {
148 | const descr = this.circularCandidatesDescrs[circularCandidateIdx]
149 |
150 | if (descr.refIdx === -1)
151 | descr.refIdx = descr.parent ? ++this.circularRefCount : 0
152 |
153 | return EncodingTransformer._createRefMark(descr.refIdx)
154 | }
155 |
156 | return null
157 | }
158 |
159 | _handleValue(getVal: () => any, parent: any, key: any) {
160 | try {
161 | const val = getVal()
162 | const type = typeof val
163 | const isObject = type === 'object' && val !== null
164 |
165 | if (isObject) {
166 | const refMark = this._ensureCircularReference(val)
167 |
168 | if (refMark) return refMark
169 | }
170 |
171 | const transform = this._findTransform(type, val)
172 |
173 | if (transform) {
174 | return this._applyTransform(val, parent, key, transform)
175 | }
176 |
177 | if (isObject) return this._handleObject(val, parent, key)
178 |
179 | return val
180 | } catch (e) {
181 | try {
182 | return this._handleValue(
183 | () => (e instanceof Error ? e : new Error(e)),
184 | parent,
185 | key
186 | )
187 | } catch {
188 | return null
189 | }
190 | }
191 | }
192 |
193 | _makeTransformsMap() {
194 | if (!MAP_SUPPORTED) {
195 | return
196 | }
197 |
198 | const map = new Map()
199 | this.transforms.forEach((transform) => {
200 | if (transform.lookup) {
201 | map.set(transform.lookup, transform)
202 | }
203 | })
204 | return map
205 | }
206 |
207 | _findTransform(type: string, val: any) {
208 | if (MAP_SUPPORTED) {
209 | if (val && val.constructor) {
210 | const transform = this.transformsMap.get(val.constructor)
211 |
212 | if (transform?.shouldTransform(type, val)) return transform
213 | }
214 | }
215 |
216 | for (const transform of this.transforms) {
217 | if (transform.shouldTransform(type, val)) return transform
218 | }
219 | }
220 |
221 | transform() {
222 | const references = [this._handleValue(() => this.references, null, null)]
223 |
224 | for (const descr of this.circularCandidatesDescrs) {
225 | if (descr.refIdx > 0) {
226 | references[descr.refIdx] = descr.parent[descr.key]
227 | descr.parent[descr.key] = EncodingTransformer._createRefMark(
228 | descr.refIdx
229 | )
230 | }
231 | }
232 |
233 | return references
234 | }
235 | }
236 |
237 | // DecodingTransform
238 | class DecodingTransformer {
239 | references: any
240 | transformMap: any
241 | activeTransformsStack = [] as any
242 | visitedRefs = Object.create(null)
243 |
244 | constructor(references: any, transformsMap: any) {
245 | this.references = references
246 | this.transformMap = transformsMap
247 | }
248 |
249 | _handlePlainObject(obj: any) {
250 | const unescaped = Object.create(null)
251 |
252 | if ('constructor' in obj) {
253 | if (!obj.constructor || typeof obj.constructor.name !== 'string') {
254 | obj.constructor = {
255 | name: 'Object',
256 | }
257 | }
258 | }
259 |
260 | for (const key in obj) {
261 | if (obj.hasOwnProperty(key)) {
262 | this._handleValue(obj[key], obj, key)
263 |
264 | if (KEY_REQUIRE_ESCAPING_RE.test(key)) {
265 | // NOTE: use intermediate object to avoid unescaped and escaped keys interference
266 | // E.g. unescaped "##@t" will be "#@t" which can overwrite escaped "#@t".
267 | unescaped[key.substring(1)] = obj[key]
268 | delete obj[key]
269 | }
270 | }
271 | }
272 |
273 | for (const unsecapedKey in unescaped)
274 | obj[unsecapedKey] = unescaped[unsecapedKey]
275 | }
276 |
277 | _handleTransformedObject(obj: any, parent: any, key: any) {
278 | const transformType = obj[TRANSFORMED_TYPE_KEY]
279 | const transform = this.transformMap[transformType]
280 |
281 | if (!transform)
282 | throw new Error(`Can't find transform for "${transformType}" type.`)
283 |
284 | this.activeTransformsStack.push(obj)
285 | this._handleValue(obj.data, obj, 'data')
286 | this.activeTransformsStack.pop()
287 |
288 | parent[key] = transform.fromSerializable(obj.data)
289 | }
290 |
291 | _handleCircularSelfRefDuringTransform(refIdx: any, parent: any, key: any) {
292 | // NOTE: we've hit a hard case: object reference itself during transformation.
293 | // We can't dereference it since we don't have resulting object yet. And we'll
294 | // not be able to restore reference lately because we will need to traverse
295 | // transformed object again and reference might be unreachable or new object contain
296 | // new circular references. As a workaround we create getter, so once transformation
297 | // complete, dereferenced property will point to correct transformed object.
298 | const references = this.references
299 |
300 | Object.defineProperty(parent, key, {
301 | // @ts-ignore
302 | val: void 0,
303 | configurable: true,
304 | enumerable: true,
305 |
306 | get() {
307 | if (this.val === void 0) this.val = references[refIdx]
308 |
309 | return (this).val
310 | },
311 |
312 | set(value) {
313 | this.val = value
314 | },
315 | })
316 | }
317 |
318 | _handleCircularRef(refIdx: any, parent: any, key: any) {
319 | if (this.activeTransformsStack.includes(this.references[refIdx]))
320 | this._handleCircularSelfRefDuringTransform(refIdx, parent, key)
321 | else {
322 | if (!this.visitedRefs[refIdx]) {
323 | this.visitedRefs[refIdx] = true
324 | this._handleValue(this.references[refIdx], this.references, refIdx)
325 | }
326 |
327 | parent[key] = this.references[refIdx]
328 | }
329 | }
330 |
331 | _handleValue(val: any, parent: any, key: any) {
332 | if (typeof val !== 'object' || val === null) return
333 |
334 | const refIdx = val[CIRCULAR_REF_KEY]
335 |
336 | if (refIdx !== void 0) this._handleCircularRef(refIdx, parent, key)
337 | else if (val[TRANSFORMED_TYPE_KEY])
338 | this._handleTransformedObject(val, parent, key)
339 | else if (Array.isArray(val)) {
340 | for (let i = 0; i < val.length; i++) this._handleValue(val[i], val, i)
341 | } else this._handlePlainObject(val)
342 | }
343 |
344 | transform() {
345 | this.visitedRefs[0] = true
346 | this._handleValue(this.references[0], this.references, 0)
347 |
348 | return this.references[0]
349 | }
350 | }
351 |
352 | // Transforms
353 | const builtInTransforms = [
354 | {
355 | type: '[[NaN]]',
356 |
357 | shouldTransform(type: any, val: any) {
358 | return type === 'number' && isNaN(val)
359 | },
360 |
361 | toSerializable() {
362 | return ''
363 | },
364 |
365 | fromSerializable() {
366 | return NaN
367 | },
368 | },
369 |
370 | {
371 | type: '[[undefined]]',
372 |
373 | shouldTransform(type: any) {
374 | return type === 'undefined'
375 | },
376 |
377 | toSerializable() {
378 | return ''
379 | },
380 |
381 | fromSerializable() {
382 | return void 0
383 | },
384 | },
385 | {
386 | type: '[[Date]]',
387 |
388 | lookup: Date,
389 |
390 | shouldTransform(type: any, val: any) {
391 | return val instanceof Date
392 | },
393 |
394 | toSerializable(date: any) {
395 | return date.getTime()
396 | },
397 |
398 | fromSerializable(val: any) {
399 | const date = new Date()
400 |
401 | date.setTime(val)
402 | return date
403 | },
404 | },
405 | {
406 | type: '[[RegExp]]',
407 |
408 | lookup: RegExp,
409 |
410 | shouldTransform(type: any, val: any) {
411 | return val instanceof RegExp
412 | },
413 |
414 | toSerializable(re: any) {
415 | const result = {
416 | src: re.source,
417 | flags: '',
418 | }
419 |
420 | if (re.global) result.flags += 'g'
421 |
422 | if (re.ignoreCase) result.flags += 'i'
423 |
424 | if (re.multiline) result.flags += 'm'
425 |
426 | return result
427 | },
428 |
429 | fromSerializable(val: any) {
430 | return new RegExp(val.src, val.flags)
431 | },
432 | },
433 |
434 | {
435 | type: '[[Error]]',
436 |
437 | lookup: Error,
438 |
439 | shouldTransform(type: any, val: any) {
440 | return val instanceof Error
441 | },
442 |
443 | toSerializable(err: any) {
444 | if (!err.stack) {
445 | ;(Error as any).captureStackTrace?.(err)
446 | }
447 |
448 | return {
449 | name: err.name,
450 | message: err.message,
451 | stack: err.stack,
452 | }
453 | },
454 |
455 | fromSerializable(val: any) {
456 | const Ctor = GLOBAL[val.name] || Error
457 | const err = new Ctor(val.message)
458 |
459 | err.stack = val.stack
460 | return err
461 | },
462 | },
463 |
464 | {
465 | type: '[[ArrayBuffer]]',
466 |
467 | lookup: ARRAY_BUFFER_SUPPORTED && ArrayBuffer,
468 |
469 | shouldTransform(type: any, val: any) {
470 | return ARRAY_BUFFER_SUPPORTED && val instanceof ArrayBuffer
471 | },
472 |
473 | toSerializable(buffer: any) {
474 | const view = new Int8Array(buffer)
475 |
476 | return arrSlice.call(view)
477 | },
478 |
479 | fromSerializable(val: any) {
480 | if (ARRAY_BUFFER_SUPPORTED) {
481 | const buffer = new ArrayBuffer(val.length)
482 | const view = new Int8Array(buffer)
483 |
484 | view.set(val)
485 |
486 | return buffer
487 | }
488 |
489 | return val
490 | },
491 | },
492 |
493 | {
494 | type: '[[TypedArray]]',
495 |
496 | shouldTransform(type: any, val: any) {
497 | if (ARRAY_BUFFER_SUPPORTED) {
498 | return ArrayBuffer.isView(val) && !(val instanceof DataView)
499 | }
500 |
501 | for (const ctorName of TYPED_ARRAY_CTORS) {
502 | if (
503 | typeof GLOBAL[ctorName] === 'function' &&
504 | val instanceof GLOBAL[ctorName]
505 | )
506 | return true
507 | }
508 |
509 | return false
510 | },
511 |
512 | toSerializable(arr: any) {
513 | return {
514 | ctorName: arr.constructor.name,
515 | arr: arrSlice.call(arr),
516 | }
517 | },
518 |
519 | fromSerializable(val: any) {
520 | return typeof GLOBAL[val.ctorName] === 'function'
521 | ? new GLOBAL[val.ctorName](val.arr)
522 | : val.arr
523 | },
524 | },
525 |
526 | {
527 | type: '[[Map]]',
528 |
529 | lookup: MAP_SUPPORTED && Map,
530 |
531 | shouldTransform(type: any, val: any) {
532 | return MAP_SUPPORTED && val instanceof Map
533 | },
534 |
535 | toSerializable(map: any) {
536 | const flattenedKVArr: any = []
537 |
538 | map.forEach((val: any, key: any) => {
539 | flattenedKVArr.push(key)
540 | flattenedKVArr.push(val)
541 | })
542 |
543 | return flattenedKVArr
544 | },
545 |
546 | fromSerializable(val: any) {
547 | if (MAP_SUPPORTED) {
548 | // NOTE: new Map(iterable) is not supported by all browsers
549 | const map = new Map()
550 |
551 | for (var i = 0; i < val.length; i += 2) map.set(val[i], val[i + 1])
552 |
553 | return map
554 | }
555 |
556 | const kvArr = []
557 |
558 | // @ts-ignore
559 | for (let j = 0; j < val.length; j += 2) kvArr.push([val[i], val[i + 1]])
560 |
561 | return kvArr
562 | },
563 | },
564 |
565 | {
566 | type: '[[Set]]',
567 |
568 | lookup: SET_SUPPORTED && Set,
569 |
570 | shouldTransform(type: any, val: any) {
571 | return SET_SUPPORTED && val instanceof Set
572 | },
573 |
574 | toSerializable(set: any) {
575 | const arr: any = []
576 |
577 | set.forEach((val: any) => {
578 | arr.push(val)
579 | })
580 |
581 | return arr
582 | },
583 |
584 | fromSerializable(val: any) {
585 | if (SET_SUPPORTED) {
586 | // NOTE: new Set(iterable) is not supported by all browsers
587 | const set = new Set()
588 |
589 | for (let i = 0; i < val.length; i++) set.add(val[i])
590 |
591 | return set
592 | }
593 |
594 | return val
595 | },
596 | },
597 | ]
598 |
599 | // Replicator
600 | class Replicator {
601 | transforms = [] as any
602 | transformsMap = Object.create(null)
603 | serializer: any
604 |
605 | constructor(serializer?: any) {
606 | this.serializer = serializer || JSONSerializer
607 |
608 | this.addTransforms(builtInTransforms)
609 | }
610 |
611 | addTransforms(transforms: any) {
612 | transforms = Array.isArray(transforms) ? transforms : [transforms]
613 |
614 | for (const transform of transforms) {
615 | if (this.transformsMap[transform.type])
616 | throw new Error(
617 | `Transform with type "${transform.type}" was already added.`
618 | )
619 |
620 | this.transforms.push(transform)
621 | this.transformsMap[transform.type] = transform
622 | }
623 |
624 | return this
625 | }
626 |
627 | removeTransforms(transforms: any) {
628 | transforms = Array.isArray(transforms) ? transforms : [transforms]
629 |
630 | for (const transform of transforms) {
631 | const idx = this.transforms.indexOf(transform)
632 |
633 | if (idx > -1) this.transforms.splice(idx, 1)
634 |
635 | delete this.transformsMap[transform.type]
636 | }
637 |
638 | return this
639 | }
640 |
641 | encode(val: any, limit?: number) {
642 | const transformer = new EncodingTransformer(val, this.transforms, limit)
643 | const references = transformer.transform()
644 |
645 | return this.serializer.serialize(references)
646 | }
647 |
648 | decode(val: any) {
649 | const references = this.serializer.deserialize(val)
650 | const transformer = new DecodingTransformer(references, this.transformsMap)
651 |
652 | return transformer.transform()
653 | }
654 | }
655 |
656 | export default Replicator
657 |
--------------------------------------------------------------------------------
/src/Unhook/index.ts:
--------------------------------------------------------------------------------
1 | import { HookedConsole } from '../definitions/Console'
2 |
3 | /**
4 | * Unhook a console constructor and restore back the Native methods
5 | * @argument console The Console constructor to Hook
6 | */
7 | function Unhook(console: HookedConsole): boolean {
8 | if (console.feed) {
9 | for (const method of Object.keys(console.feed.pointers)) {
10 | console[method] = console.feed.pointers[method]
11 | }
12 | return delete console.feed
13 | } else {
14 | return false
15 | }
16 | }
17 |
18 | export default Unhook
19 |
--------------------------------------------------------------------------------
/src/definitions/Component.d.ts:
--------------------------------------------------------------------------------
1 | import { Payload } from './Payload'
2 | import { Styles } from './Styles'
3 | import { Methods } from './Methods'
4 | import type { Options } from 'linkifyjs'
5 | import { ComponentOverrides } from './ComponentOverrides'
6 |
7 | export type Variants = 'light' | 'dark'
8 |
9 | export interface Theme {
10 | variant: Variants
11 | styles: Styles
12 | }
13 |
14 | export interface Context extends Theme {
15 | method: Methods
16 | }
17 |
18 | export interface Message extends Payload {
19 | data: any[]
20 | amount?: number
21 | }
22 |
23 | export interface Props {
24 | logs: Message[]
25 | variant?: Variants
26 | styles?: Styles
27 | filter?: Methods[]
28 | searchKeywords?: string
29 | logFilter?: Function
30 | logGrouping?: Boolean
31 | linkifyOptions?: Options
32 | components?: ComponentOverrides
33 | }
34 |
35 | export interface MessageProps {
36 | log: Message
37 | linkifyOptions?: Options
38 | components?: ComponentOverrides
39 | }
40 |
--------------------------------------------------------------------------------
/src/definitions/ComponentOverrides.d.ts:
--------------------------------------------------------------------------------
1 | import { ComponentType } from 'react'
2 | import { Message } from './Component'
3 |
4 | export interface ComponentOverrides {
5 | Message?: ComponentType<{
6 | node?: JSX.Element
7 | log?: Message
8 | children: React.ReactNode
9 | }>
10 | }
11 |
--------------------------------------------------------------------------------
/src/definitions/Console.d.ts:
--------------------------------------------------------------------------------
1 | import { Methods as _Methods } from './Methods'
2 | import { Payload } from './Payload'
3 |
4 | export interface Storage {
5 | pointers: {
6 | [name: string]: Function
7 | }
8 | src: any
9 | }
10 |
11 | export interface HookedConsole extends Console {
12 | feed: Storage
13 | }
14 |
15 | export type Methods = _Methods
16 |
17 | export interface Message {
18 | method: Methods
19 | data?: any[]
20 | timestamp?: string
21 | }
22 |
23 | export type Callback = (encoded: Message, message: Payload) => void
24 |
--------------------------------------------------------------------------------
/src/definitions/Methods.ts:
--------------------------------------------------------------------------------
1 | const methods = [
2 | 'log',
3 | 'debug',
4 | 'info',
5 | 'warn',
6 | 'error',
7 | 'table',
8 | 'clear',
9 | 'time',
10 | 'timeEnd',
11 | 'count',
12 | 'assert',
13 | 'command',
14 | 'result',
15 | 'dir',
16 | ]
17 |
18 | export default methods
19 |
20 | export type Methods =
21 | | 'log'
22 | | 'debug'
23 | | 'info'
24 | | 'warn'
25 | | 'error'
26 | | 'table'
27 | | 'clear'
28 | | 'time'
29 | | 'timeEnd'
30 | | 'count'
31 | | 'assert'
32 | | 'command'
33 | | 'result'
34 | | 'dir'
35 |
--------------------------------------------------------------------------------
/src/definitions/Payload.d.ts:
--------------------------------------------------------------------------------
1 | import { Message } from './Console'
2 |
3 | export interface Payload extends Message {
4 | id: string
5 | }
6 |
--------------------------------------------------------------------------------
/src/definitions/Store.d.ts:
--------------------------------------------------------------------------------
1 | export interface Action {
2 | type: string
3 | [key: string]: any
4 | }
5 |
--------------------------------------------------------------------------------
/src/definitions/Styles.d.ts:
--------------------------------------------------------------------------------
1 | export interface Styles {
2 | // Log icons
3 | LOG_ICON_WIDTH?: string | number
4 | LOG_ICON_HEIGHT?: string | number
5 |
6 | // Log colors
7 | // LOG_ICON => CSS background-image property
8 | LOG_COLOR?: string
9 | LOG_ICON?: string
10 | LOG_BACKGROUND?: string
11 | LOG_ICON_BACKGROUND_SIZE?: string
12 | LOG_BORDER?: string
13 |
14 | LOG_INFO_COLOR?: string
15 | LOG_INFO_ICON?: string
16 | LOG_INFO_BACKGROUND?: string
17 | LOG_INFO_BORDER?: string
18 |
19 | LOG_COMMAND_COLOR?: string
20 | LOG_COMMAND_ICON?: string
21 | LOG_COMMAND_BACKGROUND?: string
22 | LOG_COMMAND_BORDER?: string
23 |
24 | LOG_RESULT_COLOR?: string
25 | LOG_RESULT_ICON?: string
26 | LOG_RESULT_BACKGROUND?: string
27 | LOG_RESULT_BORDER?: string
28 |
29 | LOG_WARN_COLOR?: string
30 | LOG_WARN_ICON?: string
31 | LOG_WARN_BACKGROUND?: string
32 | LOG_WARN_BORDER?: string
33 |
34 | LOG_ERROR_COLOR?: string
35 | LOG_ERROR_ICON?: string
36 | LOG_ERROR_BACKGROUND?: string
37 | LOG_ERROR_BORDER?: string
38 |
39 | // Fonts
40 | BASE_FONT_FAMILY?: any
41 | BASE_FONT_SIZE?: any
42 | BASE_LINE_HEIGHT?: any
43 |
44 | // Spacing
45 | PADDING?: string
46 |
47 | // react-inspector
48 | BASE_BACKGROUND_COLOR?: any
49 | BASE_COLOR?: any
50 |
51 | OBJECT_NAME_COLOR?: any
52 | OBJECT_VALUE_NULL_COLOR?: any
53 | OBJECT_VALUE_UNDEFINED_COLOR?: any
54 | OBJECT_VALUE_REGEXP_COLOR?: any
55 | OBJECT_VALUE_STRING_COLOR?: any
56 | OBJECT_VALUE_SYMBOL_COLOR?: any
57 | OBJECT_VALUE_NUMBER_COLOR?: any
58 | OBJECT_VALUE_BOOLEAN_COLOR?: any
59 | OBJECT_VALUE_FUNCTION_KEYWORD_COLOR?: any
60 |
61 | HTML_TAG_COLOR?: any
62 | HTML_TAGNAME_COLOR?: any
63 | HTML_TAGNAME_TEXT_TRANSFORM?: any
64 | HTML_ATTRIBUTE_NAME_COLOR?: any
65 | HTML_ATTRIBUTE_VALUE_COLOR?: any
66 | HTML_COMMENT_COLOR?: any
67 | HTML_DOCTYPE_COLOR?: any
68 |
69 | ARROW_COLOR?: any
70 | ARROW_MARGIN_RIGHT?: any
71 | ARROW_FONT_SIZE?: any
72 |
73 | TREENODE_FONT_FAMILY?: any
74 | TREENODE_FONT_SIZE?: any
75 | TREENODE_LINE_HEIGHT?: any
76 | TREENODE_PADDING_LEFT?: any
77 |
78 | TABLE_BORDER_COLOR?: any
79 | TABLE_TH_BACKGROUND_COLOR?: any
80 | TABLE_TH_HOVER_COLOR?: any
81 | TABLE_SORT_ICON_COLOR?: any
82 | TABLE_DATA_BACKGROUND_IMAGE?: any
83 | TABLE_DATA_BACKGROUND_SIZE?: any
84 |
85 | [style: string]: any
86 | }
87 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Console } from './Component'
2 | export { default as Hook } from './Hook'
3 | export { default as Unhook } from './Unhook'
4 |
5 | import { ComponentOverrides as _ComponentOverrides } from './definitions/ComponentOverrides'
6 | export type ComponentOverrides = _ComponentOverrides
7 |
8 | export { Decode } from './Transform'
9 | export { Encode } from './Transform'
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src"
5 | },
6 | "exclude": [
7 | "./node_modules",
8 | "./lib",
9 | "./demo",
10 | "./scripts",
11 | "./src/**/__tests__/*"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "lib",
4 | "module": "commonjs",
5 | "target": "es3",
6 | "ignoreDeprecations": "5.0",
7 | "lib": ["es6", "dom", "ES2020.BigInt"],
8 | "inlineSourceMap": true,
9 | "skipDefaultLibCheck": true,
10 | "skipLibCheck": true,
11 | "jsx": "react",
12 | "moduleResolution": "node",
13 | "rootDir": ".",
14 | "forceConsistentCasingInFileNames": true,
15 | "types": ["jest"],
16 | "allowSyntheticDefaultImports": true,
17 | "esModuleInterop": true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------