├── LICENSE
├── README.md
├── core.js
├── img.jpg
├── index.html
├── style.css
├── t.png
└── ui.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 weizman
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 |
2 |
3 |
~ The first tool ever for visually exploring the JavaScript prototype chain that's generated live in your browser ~
4 |
5 |
6 |
7 |
8 |
9 |
How does ProtoTree work?
10 |
11 |
12 | * ProtoTree uses [LavaTube 🌋](https://github.com/LavaMoat/LavaTube) to recursively walk through the entire JavaScript prototype chain in real time in your browser
13 | * With every recursive visit, ProtoTree slowely builds a tree representation of the prototype chain
14 | * In order to avoid external polution that the app creates, ProtoTree runs the recursive operation inside a cross origin iframe, to leverage a fresh new realm for the job
15 | * Follow the instructions on the app itself to take full adventage of this tool
16 | * Enjoy and please consider to ⭐ this project
17 |
18 | > *Read further @ https://twitter.com/WeizmanGal/status/1684608574444785664*
19 |
--------------------------------------------------------------------------------
/core.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | function begin() {
3 | function isPrimitive(v) {
4 | if (null === v || undefined === v) {
5 | return true;
6 | }
7 | if (['boolean', 'number', 'string'].includes(typeof v)) {
8 | return true;
9 | }
10 | return false;
11 | }
12 |
13 | // because of adsense
14 | function isFrame(v) {
15 | let i = 0;
16 | let w;
17 | while (w) {
18 | w = top[i];
19 | if (v === w) {
20 | return true;
21 | }
22 | i++;
23 | }
24 | }
25 |
26 | const root = {};
27 | const values = {};
28 |
29 | function cb(path) {
30 | let node = root;
31 | for (let i = 0; i < path.length; i++) {
32 | const val = path[i];
33 | const prop = getProp(val);
34 | if (!node[prop]) {
35 | id += 1;
36 | values['b' + id] = val;
37 | node[prop] = {
38 | _value_reference: 'vf' + id,
39 | _own_names: Object.getOwnPropertyNames(val),
40 | };
41 | }
42 | node = node[prop];
43 | }
44 | }
45 |
46 | function getProp(val) {
47 | switch (val) {
48 | case Symbol.prototype:
49 | return 'Symbol';
50 | case Number.prototype:
51 | return 'Number';
52 | case Array.prototype:
53 | return 'Array';
54 | case Boolean.prototype:
55 | return 'Boolean';
56 | case String.prototype:
57 | return 'String';
58 | default:
59 | let v = '';
60 | try {
61 | if (typeof val === 'symbol') {
62 | v = val.toString();
63 | } else if (typeof val === 'function') {
64 | v = Function.prototype.toString.call(val);
65 | } else {
66 | v += val;
67 | }
68 | if ((v).startsWith('data:text/html;Base64,')) {
69 | return '[location]';
70 | }
71 | return v
72 | .split('[object ').join('')
73 | .split('{ [native code] }').join('')
74 | .split(']').join('')
75 | .split('\n').join('');
76 | } catch (e) {
77 | return ({}).toString.call(val).split('[object ').join('').split(']').join('');
78 | }
79 | }
80 | }
81 |
82 | function main(obj) {
83 | if (isPrimitive(obj) || isFrame(obj)) return;
84 | const protos = [obj];
85 | while (true) {
86 | if (isPrimitive(obj) || isFrame(obj)) break;
87 | const proto = Object.getPrototypeOf(obj);
88 | if (!proto) break;
89 | protos.push(proto);
90 | obj = proto;
91 | }
92 | protos.reverse();
93 | cb(protos);
94 | }
95 |
96 | let id = 0;
97 |
98 | (function () {
99 | const lt = new LavaTube(main, {
100 | onShouldIgnoreError: (p, o, e) => {
101 | // console.error('sync error in LavaTube:', e);
102 | return true;
103 | },
104 | maxRecursionLimit: 9,
105 | });
106 | window.LavaTube = undefined;
107 | window.begin = undefined;
108 | lt.walk(window);
109 | const result = JSON.stringify(root);
110 | setTimeout(() => {
111 | window.values = values;
112 | top.postMessage({result}, '*');
113 | console.log(`%c${decodeURIComponent(escape(window.atob('8J+Xuu+4jw==')))} Map each entry in the full tree below to its real actual object in runtime:`, 'color:deepskyblue');
114 | console.log('', values);
115 | }, 100);
116 | }());
117 | }
118 | document.write(`
119 |
126 | `);
127 | }())
128 |
--------------------------------------------------------------------------------
/img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weizman/ProtoTree/0c0b44a86ebaf3e7cefa8fb50b4113dba505ae00/img.jpg
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | Proto Tree
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
47 |
48 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
This app makes more sense on desktop rather than mobile
58 | devices!
59 |
70 |
71 |
72 |
The Prototype Chain ⛓️
73 |
74 |
75 | Here you can observe the prototype
77 | chain generated in your browser , presented as a tree 🌳
78 |
79 |
80 | The tree can vary depending on:
81 |
82 |
83 | The current browser you use
84 |
85 |
86 | Any extensions and plugins you use
87 |
88 |
89 |
90 |
91 | NOTICE: This list is generated by climbing up the global object (aka
92 | window
) and
93 | therefore is not complete because there are prototypes that are inaccessible via the
94 | global object
95 | and are only accessible as return values from different objects and APIs
96 |
97 |
98 | Suggestions for such are welcome
99 | to enhance this list to cover as much as possible!
100 |
101 |
102 |
103 |
104 |
105 |
106 | Further information is presented in the console, make sure to check that out too
107 |
108 |
109 |
110 |
Filters ⚙️
111 |
112 |
113 | Since the full tree is overwhelming , you can use filters by adding the filters
query
114 | param
115 |
116 |
117 | Multiple filters can be separated by using ","
118 |
119 |
120 | To achieve negative effect ("everything but") use "!"
at the beginning of the
121 | filter
122 |
123 |
124 |
135 |
136 |
137 |
Shortcuts 🎯
138 |
139 |
140 |
141 |
142 |
143 |
149 |
156 |
157 |
160 |
161 |
162 |
163 |
164 | ⏳
165 |
166 |
167 |
168 |
182 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | #btn {
2 | padding: 5px;
3 | border-radius: 20px;
4 | }
5 |
6 | a {
7 | color: green;
8 | }
9 |
10 | a.ofstart {
11 | color: black;
12 | }
13 |
14 | body {
15 | background-color: rgb(231, 255, 212);
16 | }
17 |
18 | .function {
19 | color: green;
20 | }
21 |
22 | code {
23 | color: green;
24 | }
25 |
26 | h2 {
27 | font-size: 22px;
28 | }
29 |
30 | h3 {
31 | font-size: 16px;
32 | }
33 |
34 | ul,li {
35 | font-size: 12px !important;
36 | }
37 |
38 | #menu {
39 | padding-left: 5px;
40 | font-size: 12px !important;
41 | width: 70vh;
42 | }
43 |
44 | #head {
45 | padding: 10px;
46 | margin: 5px;
47 | float: left;
48 | height: 50%;
49 | }
50 |
51 | #head2 {
52 | padding: 20px;
53 | margin: 20px;
54 | overflow: hidden;
55 | }
56 |
57 | #start {
58 | resize: vertical;
59 | overflow: auto;
60 | border-radius: 20px;
61 | background-color: rgb(220, 250, 194);
62 | height: 37%;
63 | border-style: solid;
64 | margin: 20px;
65 | padding: 10px;
66 | }
67 |
--------------------------------------------------------------------------------
/t.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weizman/ProtoTree/0c0b44a86ebaf3e7cefa8fb50b4113dba505ae00/t.png
--------------------------------------------------------------------------------
/ui.js:
--------------------------------------------------------------------------------
1 | const ownNames = {};
2 | function setOwns(span) {
3 | props.innerHTML = 'Own Property Names: ' + ownNames[span.id] + '
';
4 | }
5 |
6 | onmessage = function (data) {
7 | let id = 0;
8 |
9 | function tree(node, obj, opts) {
10 | if (!obj) {
11 | if (!node.parentElement.querySelector('[found="true"]')) {
12 | node.parentElement.remove();
13 | }
14 | }
15 | for (const prop in obj) {
16 | if (prop === '_value_reference' || prop === '_own_names') continue;
17 | const val = obj[prop];
18 | id += 1;
19 | ownNames[val._value_reference] = val._own_names.join(', ');
20 | node.innerHTML += `${prop} `;
21 | tree(node.querySelector('#' + 'id' + id), val, opts);
22 | }
23 | if (!node.parentElement?.querySelector('[found="true"]')) {
24 | node.parentElement?.remove();
25 | }
26 | }
27 |
28 | function opts(queryString) {
29 | const opts = {filters: []};
30 | const urlParams = new URLSearchParams(queryString);
31 | const filters = urlParams.get('filters');
32 | if (!filters) return opts;
33 | for (const filter of filters.split(',')) {
34 | const f = {not: false, filter};
35 | if (filter[0] === '!') {
36 | f.not = true;
37 | f.filter = filter.split('!').join('');
38 | }
39 | opts.filters.push(f);
40 | }
41 | return opts;
42 | }
43 |
44 | function getLayers(start) {
45 | const layers = {
46 | 1: [],
47 | 2: [],
48 | 3: [],
49 | 4: [],
50 | 5: [],
51 | 6: [],
52 | 7: [],
53 | 8: [],
54 | 9: [],
55 | 10: [],
56 | 11: [],
57 | 12: [],
58 | }
59 | const spans = start.querySelectorAll('span');
60 | for (let i = 0; i < spans.length; i++) {
61 | const span = spans[i];
62 | let d = -1, s = span;
63 | while (s !== start) {
64 | d += 1;
65 | s = s.parentElement;
66 | }
67 | layers[d / 2].push(span);
68 | }
69 | return layers;
70 | }
71 |
72 | function prepareMenu(menu, layers, maxLevels = 12, maxItems = 10) {
73 | const div = document.createElement('div');
74 | const x = document.createElement('input');
75 | x.placeholder = 'jump to';
76 | x.onchange = () => shortcut(x.value);
77 | div.appendChild(x);
78 | div.append(' | ');
79 | for (const level in layers) {
80 | const layer = layers[level];
81 | if (!--maxLevels) break;
82 | if (layer.length > maxItems) continue;
83 | for (let i = 0; i < layer.length; i++) {
84 | const span = layer[i];
85 | const a = document.createElement('a');
86 | a.textContent = span.textContent;
87 | a.href = 'javascript:;';
88 | a.onclick = () => span.scrollIntoView({behavior: 'smooth'})
89 | div.appendChild(a);
90 | div.append(' | ');
91 | }
92 | }
93 | div.lastChild?.remove();
94 | menu.appendChild(div);
95 | }
96 |
97 | function includes(opts, prop) {
98 | // ignore filters query param
99 | if (prop.includes('filters=')) {
100 | return false;
101 | }
102 | prop = prop.split('filters=')[0].toLowerCase();
103 | // ignore adsense stuff
104 | if (
105 | prop.startsWith('__') ||
106 | prop.includes('google') ||
107 | prop.includes('amp') ||
108 | prop.includes('ggeac') ||
109 | prop.includes('default_')
110 | ) {
111 | return false;
112 | }
113 | if (!opts.filters.length) return true;
114 | for (const filter of opts.filters) {
115 | let found = false;
116 | if (prop.includes(filter.filter.toLowerCase())) {
117 | found = true;
118 | }
119 | if (filter.not) {
120 | found = !found;
121 | }
122 | if (found) {
123 | return true;
124 | }
125 | }
126 | return false;
127 | }
128 |
129 | function toJson(head, obj) {
130 | for (const child of head.children) {
131 | const span = child.firstChild;
132 | const prop = span?.firstChild?.textContent;
133 | if (prop) {
134 | obj[prop] = {};
135 | toJson(child.lastChild, obj[prop]);
136 | }
137 | }
138 | }
139 |
140 | function getButtonInners() {
141 | return `
142 |
143 |
144 |
145 |
146 |
147 |
148 | `;
149 | }
150 |
151 | function shortcut(text) {
152 | const xpath = `//a[contains(text(),"${text}")][@class="ofstart"]`;
153 | const matchingElement = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
154 | if (matchingElement?.parentElement) {
155 | setTimeout(() =>
156 | matchingElement?.parentElement.scrollIntoView({behavior: 'smooth'}),
157 | 0);
158 | }
159 | }
160 |
161 | if (!data.data.result) return;
162 | const root = top.root = JSON.parse(data.data.result);
163 | const json = top.json = {};
164 | const e = document.createElement('ul');
165 | tree(e, root, opts(window.location.search));
166 | Array.from(e.querySelectorAll('ul')).filter(a=>!a.children.length).forEach(u=>u.remove());
167 | toJson(e, json);
168 | console.log('%c👀 Observe full live tree representing the full prototype chain:', 'color:deepskyblue');
169 | console.log('', root);
170 | console.log('%c👀 Observe the filtered tree as a jsonable object:', 'color:deepskyblue');
171 | console.log('', json);
172 | console.log('%c👀 Observe the filtered tree visually', 'color:deepskyblue');
173 | console.log(treeify.asTree(json));
174 | console.log('%c👆🏻 Begin at the top of the console 👆🏻', 'color:deepskyblue');
175 | const element = start;
176 | element.innerHTML = getButtonInners();
177 | if (!!JSON.parse(localStorage.full_screen)) element.style.height = '80%';
178 | element.appendChild(e);
179 | element.setAttribute('class', 'tf-tree');
180 | e.innerHTML = e.innerHTML
181 | .split('function').join('function ');
182 | const layers = getLayers(element);
183 | prepareMenu(menu, layers, 6, 30);
184 | document.querySelectorAll('.tf-nc').forEach(span => {
185 | const searchParams = new URLSearchParams(window.location.search);
186 | searchParams.set('filters', span.textContent);
187 | const url = location.href.split('?')[0] + '?' + searchParams.toString();
188 | span.innerHTML = `${span.innerHTML} `;
189 | });
190 | };
--------------------------------------------------------------------------------