@lukekaalim/act-insight 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AttributeTag.module.css +28 -0
- package/AttributeTag.ts +17 -0
- package/CommitViewer.module.css +5 -0
- package/CommitViewer.ts +104 -0
- package/InsightApp.module.css +19 -0
- package/InsightApp.ts +209 -0
- package/MenuBar.module.css +18 -0
- package/MenuBar.ts +39 -0
- package/ThreadControls.ts +22 -0
- package/ThreadViewer.module.css +5 -0
- package/ThreadViewer.ts +185 -0
- package/TreeViewer.module.css +46 -0
- package/TreeViewer.ts +70 -0
- package/debug.ts +80 -0
- package/devtool_page.ts +0 -0
- package/devtool_panel.ts +0 -0
- package/extension_main.ts +0 -0
- package/index.ts +1 -0
- package/insight.test.ts +29 -0
- package/mode.ts +6 -0
- package/package.json +19 -0
- package/public/icons/star.png +0 -0
- package/utils.ts +52 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.commitAttributeTag {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
|
|
4
|
+
gap: 4px;
|
|
5
|
+
margin: auto 4px;
|
|
6
|
+
|
|
7
|
+
color: white;
|
|
8
|
+
|
|
9
|
+
border-radius: 4px;
|
|
10
|
+
padding: 4px;
|
|
11
|
+
|
|
12
|
+
font-family: monospace;
|
|
13
|
+
font-size: 12px;
|
|
14
|
+
|
|
15
|
+
height: 18px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.commitAttributeTagName {
|
|
19
|
+
margin: auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.commitAttributeTagValue {
|
|
23
|
+
margin: auto;
|
|
24
|
+
border-radius: 4px;
|
|
25
|
+
padding: 2px;
|
|
26
|
+
background-color: white;
|
|
27
|
+
color: black;
|
|
28
|
+
}
|
package/AttributeTag.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Component } from "@lukekaalim/act";
|
|
2
|
+
import classes from './AttributeTag.module.css';
|
|
3
|
+
import stringHash from "@sindresorhus/string-hash";
|
|
4
|
+
import { hs } from "@lukekaalim/act-web";
|
|
5
|
+
|
|
6
|
+
export type CommitAttributeTagProps = {
|
|
7
|
+
name: string,
|
|
8
|
+
value: string,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CommitAttributeTag: Component<CommitAttributeTagProps> = ({ name, value }) => {
|
|
12
|
+
const background = `hsl(${stringHash(name) % 360}deg, 50%, 50%)`;
|
|
13
|
+
return hs('span', { className: classes.commitAttributeTag, style: { background } }, [
|
|
14
|
+
hs('span', { className: classes.commitAttributeTagName }, name),
|
|
15
|
+
hs('span', { className: classes.commitAttributeTagValue }, value),
|
|
16
|
+
])
|
|
17
|
+
}
|
package/CommitViewer.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Component, Deps, h, Ref, refSymbol, useState } from "@lukekaalim/act";
|
|
2
|
+
import { Commit, CommitID, CommitTree, Reconciler } from "@lukekaalim/act-recon";
|
|
3
|
+
import { hs } from "@lukekaalim/act-web"
|
|
4
|
+
import { getElementName } from "./utils";
|
|
5
|
+
import { CommitAttributeTag } from "./AttributeTag";
|
|
6
|
+
import classes from './CommitViewer.module.css';
|
|
7
|
+
import { CommitReport, CommitStateReport, ValueReport } from "@lukekaalim/act-debug";
|
|
8
|
+
|
|
9
|
+
export type CommitViewerProps = {
|
|
10
|
+
commit: CommitReport,
|
|
11
|
+
state: CommitStateReport,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const CommitViewer: Component<CommitViewerProps> = ({ commit, state }) => {
|
|
15
|
+
|
|
16
|
+
const isRef = (value: unknown): value is Ref<unknown> => (
|
|
17
|
+
typeof value === 'object' && !!value && (refSymbol in value)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
//const refs = state && [...state.values]
|
|
21
|
+
// .filter(([id, value]) => isRef(value))
|
|
22
|
+
|
|
23
|
+
const [, setRender] = useState(0);
|
|
24
|
+
const rerender = () => {
|
|
25
|
+
setRender(r => r + 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return hs('div', { className: classes.commitViewer }, [
|
|
29
|
+
hs('h3', {}, commit.element.type),
|
|
30
|
+
hs('div', {}, [
|
|
31
|
+
h(CommitAttributeTag, { name: 'Version', value: commit.version.toString() }),
|
|
32
|
+
h(CommitAttributeTag, { name: 'ID', value: commit.id.toString() }),
|
|
33
|
+
//!!state.props.key && [
|
|
34
|
+
// h(CommitAttributeTag, { name: 'Key', value: getValueName(commit.element.props.key) }),
|
|
35
|
+
//]
|
|
36
|
+
]),
|
|
37
|
+
state.props.length > 0 && [
|
|
38
|
+
hs('h4', {}, 'Props'),
|
|
39
|
+
hs('ul', {}, state.props.map(prop =>
|
|
40
|
+
hs('li', {}, [
|
|
41
|
+
h(CommitAttributeTag, { name: 'Key', value: prop.name }),
|
|
42
|
+
h(CommitAttributeTag, { name: 'Value', value: getTextForValue(prop.value) }),
|
|
43
|
+
]))),
|
|
44
|
+
],
|
|
45
|
+
state.values.length && [
|
|
46
|
+
hs('h4', {}, 'useState'),
|
|
47
|
+
hs('ul', {}, state.values.map(value =>
|
|
48
|
+
hs('li', {}, [
|
|
49
|
+
h(CommitAttributeTag, { name: 'Key', value: value.id.toString() }),
|
|
50
|
+
h(CommitAttributeTag, { name: 'Value', value: getTextForValue(value.value) }),
|
|
51
|
+
]))),
|
|
52
|
+
],
|
|
53
|
+
])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ValueViewerProps = {
|
|
57
|
+
value: ValueReport,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const getTextForValue = (value: ValueReport) => {
|
|
61
|
+
switch (value.type) {
|
|
62
|
+
case 'primitive':
|
|
63
|
+
switch (typeof value.value) {
|
|
64
|
+
case 'object':
|
|
65
|
+
return `null`;
|
|
66
|
+
case 'string':
|
|
67
|
+
case 'boolean':
|
|
68
|
+
case 'number':
|
|
69
|
+
return value.value.toString();
|
|
70
|
+
}
|
|
71
|
+
case 'complex':
|
|
72
|
+
return value.name;
|
|
73
|
+
case 'undefined':
|
|
74
|
+
return `undefined`;
|
|
75
|
+
default:
|
|
76
|
+
return `${value.type}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const getValueName = (value: unknown) => {
|
|
81
|
+
switch (typeof value) {
|
|
82
|
+
case 'object':
|
|
83
|
+
if (!value)
|
|
84
|
+
return 'null';
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
return `Array[${value.length}]`;
|
|
87
|
+
if (value.constructor === ({}).constructor)
|
|
88
|
+
return JSON.stringify(value, null, 2);
|
|
89
|
+
|
|
90
|
+
return value.constructor.name;
|
|
91
|
+
case undefined:
|
|
92
|
+
return 'undefined';
|
|
93
|
+
case 'string':
|
|
94
|
+
case 'number':
|
|
95
|
+
case 'boolean':
|
|
96
|
+
case 'symbol':
|
|
97
|
+
return (value as string | number | boolean | symbol).toString();
|
|
98
|
+
case 'function':
|
|
99
|
+
return `${(value as Function).name || 'Function'}()`;
|
|
100
|
+
default:
|
|
101
|
+
console.log(value);
|
|
102
|
+
return typeof value
|
|
103
|
+
}
|
|
104
|
+
}
|
package/InsightApp.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Component, Element, h, useEffect, useMemo, useRef, useState } from '@lukekaalim/act';
|
|
2
|
+
import { Commit, CommitID, CommitTree, DeltaSet, Reconciler, Update, WorkThread } from '@lukekaalim/act-recon';
|
|
3
|
+
import { hs } from '@lukekaalim/act-web';
|
|
4
|
+
|
|
5
|
+
import { CommitPreview, TreeViewer } from './TreeViewer';
|
|
6
|
+
|
|
7
|
+
import { debounce } from 'lodash-es'
|
|
8
|
+
import { getElementName } from './utils';
|
|
9
|
+
import { CommitViewer } from './CommitViewer';
|
|
10
|
+
import classes from './InsightApp.module.css';
|
|
11
|
+
import { InsightMode } from './mode';
|
|
12
|
+
import { MenuBar } from './MenuBar';
|
|
13
|
+
import { ThreadViewer } from './ThreadViewer';
|
|
14
|
+
import { DebuggerServer } from '@lukekaalim/act-debug';
|
|
15
|
+
import { CommitReport, CommitStateReport, TreeReport, updateTreeReport } from '@lukekaalim/act-debug/report';
|
|
16
|
+
|
|
17
|
+
export type InsightAppProps = {
|
|
18
|
+
server: DebuggerServer,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const InsightApp2: Component<InsightAppProps> = ({ server }) => {
|
|
22
|
+
const [tree, setTree] = useState<TreeReport>({ commits: new Map(), roots: [] });
|
|
23
|
+
const [commitId, setCommitId] = useState<CommitID | null>(null);
|
|
24
|
+
const [commitState, setCommitState] = useState<CommitStateReport | null>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
server.subscribe((event) => {
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case 'thread:finish':
|
|
30
|
+
setTree(tree => {
|
|
31
|
+
return updateTreeReport(tree, event.thread);
|
|
32
|
+
});
|
|
33
|
+
break;
|
|
34
|
+
case 'tree:root-update':
|
|
35
|
+
setTree(tree => {
|
|
36
|
+
return { ...tree, roots: event.roots };
|
|
37
|
+
});
|
|
38
|
+
break;
|
|
39
|
+
case 'commit-state:response':
|
|
40
|
+
setCommitState(event.report);
|
|
41
|
+
setCommitId(event.commitId);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
server.ready();
|
|
46
|
+
return;
|
|
47
|
+
}, [server])
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (commitId)
|
|
51
|
+
server.commitState(commitId);
|
|
52
|
+
}, [tree, commitId])
|
|
53
|
+
|
|
54
|
+
const renderCommit = (commit: CommitReport) => {
|
|
55
|
+
const onClick = () => {
|
|
56
|
+
server.commitState(commit.id);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return h(CommitPreview, { commit, renderCommit, tree, onClick });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const selectedCommit = commitId && tree.commits.get(commitId) || null;
|
|
63
|
+
|
|
64
|
+
return h('div', { style: { display: 'flex', flexDirection: 'row' } }, [
|
|
65
|
+
h(TreeViewer, { tree, renderCommit, }),
|
|
66
|
+
commitState && selectedCommit &&
|
|
67
|
+
h(CommitViewer, { commit: selectedCommit, state: commitState }),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const InsightApp: Component<InsightAppProps> = () => {
|
|
72
|
+
throw new Error();
|
|
73
|
+
|
|
74
|
+
const [mode, setMode] = useState<InsightMode>('tree');
|
|
75
|
+
|
|
76
|
+
const [renderReportIndex, setRenderReportIndex] = useState(0);
|
|
77
|
+
const [renderReports, setRenderReports] = useState<WorkThread[]>([]);
|
|
78
|
+
const [tree, setTree] = useState<null | CommitTree>(null);
|
|
79
|
+
|
|
80
|
+
const [currentThread, setCurrentThread] = useState<WorkThread | null>(null);
|
|
81
|
+
|
|
82
|
+
const currentReport = renderReports[renderReportIndex] || null;
|
|
83
|
+
const rootCommits = tree && CommitTree.getRootCommits(tree) || [];
|
|
84
|
+
|
|
85
|
+
const [pendingWork, setPendingWork] = useState(false)
|
|
86
|
+
const [autoWork, setAutoWork] = useState(false);
|
|
87
|
+
|
|
88
|
+
const updateThread = useMemo(() => {
|
|
89
|
+
return debounce(() => {
|
|
90
|
+
console.log('updating thread');
|
|
91
|
+
const currentThread = reconciler.state.thread;
|
|
92
|
+
if (currentThread)
|
|
93
|
+
setCurrentThread(WorkThread.clone(currentThread));
|
|
94
|
+
else
|
|
95
|
+
setCurrentThread(null);
|
|
96
|
+
}, 100, { leading: true, trailing: true, maxWait: 100 });
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
//const [counter, setCounter] = useState(0);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
|
|
103
|
+
const renderSub = reconciler.on('on-thread-complete', (thread) => {
|
|
104
|
+
setRenderReports(rrs => [...rrs, WorkThread.clone(thread)])
|
|
105
|
+
setTree(reconciler.tree);
|
|
106
|
+
updateThread();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
reconciler.on('on-thread-update', () => {
|
|
110
|
+
setTree(reconciler.tree);
|
|
111
|
+
updateThread();
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
onReady();
|
|
116
|
+
return () => {
|
|
117
|
+
renderSub.cancel();
|
|
118
|
+
}
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (autoWork) {
|
|
123
|
+
const id = setInterval(() => {
|
|
124
|
+
scheduler.work();
|
|
125
|
+
}, 5);
|
|
126
|
+
return () => clearInterval(id);
|
|
127
|
+
}
|
|
128
|
+
}, [autoWork, reconciler])
|
|
129
|
+
|
|
130
|
+
const onWorkClick = () => {
|
|
131
|
+
setPendingWork(false);
|
|
132
|
+
scheduler.work();
|
|
133
|
+
updateThread();
|
|
134
|
+
}
|
|
135
|
+
const onToggleAutoWork = (e: Event) => {
|
|
136
|
+
setAutoWork((e.target as HTMLInputElement).checked);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const [selectedCommit, setSelectedCommit] = useState<null | CommitID>(null);
|
|
140
|
+
const onSelectCommit = (id: CommitID) => {
|
|
141
|
+
if (selectedCommit === id)
|
|
142
|
+
setSelectedCommit(null)
|
|
143
|
+
else
|
|
144
|
+
setSelectedCommit(id)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const renderCommit = (depth: number) => (commit: Commit) => {
|
|
148
|
+
if (!tree)
|
|
149
|
+
return null;
|
|
150
|
+
return h(CommitPreview, {
|
|
151
|
+
commit,
|
|
152
|
+
tree,
|
|
153
|
+
depth,
|
|
154
|
+
renderCommit: renderCommit(depth + 1),
|
|
155
|
+
onSelectCommit,
|
|
156
|
+
attributes: [
|
|
157
|
+
['Id', commit.id.toString()]
|
|
158
|
+
],
|
|
159
|
+
selectedCommits: new Set(selectedCommit ? [selectedCommit] : [])
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return h('div', { className: classes.insight }, [
|
|
164
|
+
h(MenuBar, { currentMode: mode, onSelectMode: setMode }),
|
|
165
|
+
mode === 'thread' && hs('div', {}, [
|
|
166
|
+
hs('button', { onClick: onWorkClick }, pendingWork ? 'Do Pending Work' : 'Work'),
|
|
167
|
+
hs('label', {}, [
|
|
168
|
+
hs('span', {}, 'Toggle Auto-Work'),
|
|
169
|
+
hs('input', { type: 'checkbox', onInput: onToggleAutoWork, checked: autoWork }),
|
|
170
|
+
])
|
|
171
|
+
]),
|
|
172
|
+
mode === 'thread' && currentThread && h(ThreadViewer, { thread: currentThread, tree: tree || CommitTree.new() }),
|
|
173
|
+
//hs('pre', {}, counter),
|
|
174
|
+
mode === 'tree' && hs('div', { className: classes.treeExplorer }, [
|
|
175
|
+
tree && [
|
|
176
|
+
h(TreeViewer, { tree, selectedCommits: new Set(selectedCommit ? [selectedCommit] : []), onSelectCommit,
|
|
177
|
+
renderCommit: renderCommit(0)
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
selectedCommit && tree && [
|
|
181
|
+
h(CommitViewer, { tree, commitId: selectedCommit, reconciler })
|
|
182
|
+
]
|
|
183
|
+
])
|
|
184
|
+
]);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const UpdateDesc = ({ update }: { update: Update }) => {
|
|
188
|
+
if (update.prev && update.next)
|
|
189
|
+
return `Update ${update.ref.id} (${getElementName(update.next)})`;
|
|
190
|
+
if (update.prev && !update.next)
|
|
191
|
+
return `Destroy ${update.ref.id} (${getElementName(update.prev.element)})`;
|
|
192
|
+
if (!update.prev && update.next)
|
|
193
|
+
return `Create ${update.ref.id} (${getElementName(update.next)})`;
|
|
194
|
+
return `???`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type CommitTreeLeafProps = {
|
|
198
|
+
commit: Commit,
|
|
199
|
+
tree: CommitTree,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const CommitTreeLeaf: Component<CommitTreeLeafProps> = ({ commit, tree }) => {
|
|
203
|
+
return [
|
|
204
|
+
hs('pre', {}, [getElementName(commit.element), ` id=${commit.id} v=${commit.version}`]),
|
|
205
|
+
hs('ul', {}, commit.children
|
|
206
|
+
.map(ref => tree.commits.get(ref.id) as Commit)
|
|
207
|
+
.map(commit => h(CommitTreeLeaf, { commit, tree })))
|
|
208
|
+
];
|
|
209
|
+
}
|
package/MenuBar.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Component, h } from "@lukekaalim/act";
|
|
2
|
+
import { InsightMode } from "./mode";
|
|
3
|
+
import { hs } from "@lukekaalim/act-web";
|
|
4
|
+
|
|
5
|
+
import classes from './MenuBar.module.css';
|
|
6
|
+
|
|
7
|
+
export type MenuBarProps = {
|
|
8
|
+
currentMode: InsightMode,
|
|
9
|
+
onSelectMode: (mode: InsightMode) => unknown,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MenuBar: Component<MenuBarProps> = ({ currentMode, onSelectMode }) => {
|
|
13
|
+
return hs('menu', { className: classes.menuBar }, [
|
|
14
|
+
hs('li', { className: classes.menuBarEntry },
|
|
15
|
+
h(ModeButton, { onSelectMode, currentMode, mode: 'tree' }, 'Tree')),
|
|
16
|
+
hs('li', { className: classes.menuBarEntry },
|
|
17
|
+
h(ModeButton, { onSelectMode, currentMode, mode: 'thread' }, 'Thread')),
|
|
18
|
+
hs('li', { className: classes.menuBarEntry },
|
|
19
|
+
h(ModeButton, { onSelectMode, currentMode, mode: 'reports' }, 'Reports')),
|
|
20
|
+
])
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ModeButtonProps = {
|
|
24
|
+
mode: InsightMode,
|
|
25
|
+
currentMode: InsightMode,
|
|
26
|
+
onSelectMode: (mode: InsightMode) => unknown,
|
|
27
|
+
}
|
|
28
|
+
const ModeButton: Component<ModeButtonProps> = ({ currentMode, mode, children, onSelectMode }) => {
|
|
29
|
+
const selected = mode === currentMode;
|
|
30
|
+
const onClick = () => {
|
|
31
|
+
onSelectMode(mode);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return hs('button', {
|
|
35
|
+
className: [classes.modeButton, selected && classes.selected].join(' '),
|
|
36
|
+
onClick,
|
|
37
|
+
disabled: selected
|
|
38
|
+
}, children);
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { h } from "@lukekaalim/act";
|
|
2
|
+
|
|
3
|
+
export type ThreadControlsProps = {
|
|
4
|
+
onPlay: () => void,
|
|
5
|
+
onPause: () => void,
|
|
6
|
+
onStep: () => void,
|
|
7
|
+
onPlay: () => void,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ThreadControls = () => {
|
|
11
|
+
return [
|
|
12
|
+
h('button', {}, 'Play'),
|
|
13
|
+
h('button', {}, 'Pause'),
|
|
14
|
+
h('button', {}, 'Step'),
|
|
15
|
+
h('button', {}, 'Play To End'),
|
|
16
|
+
h(PendingWorkNotifier),
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PendingWorkNotifier = () => {
|
|
21
|
+
return 'pending';
|
|
22
|
+
}
|
package/ThreadViewer.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Component, h, useMemo } from "@lukekaalim/act"
|
|
2
|
+
import { Commit, CommitID, CommitPath, CommitRef, CommitTree, DeltaSet, Update, WorkReason, WorkThread } from "@lukekaalim/act-recon"
|
|
3
|
+
import { hs } from "@lukekaalim/act-web"
|
|
4
|
+
import { findCommonAncestor, getElementName } from "./utils"
|
|
5
|
+
import { CommitAttributeTag } from "./AttributeTag"
|
|
6
|
+
import { CommitPreview, TreeViewer } from "./TreeViewer"
|
|
7
|
+
|
|
8
|
+
import classes from './ThreadViewer.module.css';
|
|
9
|
+
|
|
10
|
+
export type ThreadViewerProps = {
|
|
11
|
+
thread: WorkThread,
|
|
12
|
+
tree: CommitTree,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ThreadViewer: Component<ThreadViewerProps> = ({ thread, tree }) => {
|
|
16
|
+
const appliedTree = useMemo(() => {
|
|
17
|
+
const tempTree = CommitTree.clone(tree);
|
|
18
|
+
DeltaSet.apply(thread.deltas, tempTree)
|
|
19
|
+
return tempTree;
|
|
20
|
+
}, [tree, thread]);
|
|
21
|
+
|
|
22
|
+
const fiberCommits = new Set([...thread.pendingUpdates].map(u => u.ref.id));
|
|
23
|
+
|
|
24
|
+
const nextUpdate = thread.pendingUpdates[thread.pendingUpdates.length - 1];
|
|
25
|
+
|
|
26
|
+
const getCommitColor = (commit: Commit) => {
|
|
27
|
+
const isUpdating = fiberCommits.has(commit.id);
|
|
28
|
+
const mustVisit = thread.mustVisit.has(commit.id);
|
|
29
|
+
const mustRender = thread.mustRender.has(commit.id);
|
|
30
|
+
const visited = thread.visited.has(commit.id);
|
|
31
|
+
|
|
32
|
+
if (visited)
|
|
33
|
+
return '#7efb8c';
|
|
34
|
+
if (isUpdating)
|
|
35
|
+
return 'blue'
|
|
36
|
+
if (mustRender)
|
|
37
|
+
return 'red';
|
|
38
|
+
if (mustVisit)
|
|
39
|
+
return 'orange';
|
|
40
|
+
|
|
41
|
+
return 'white';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const renderCommit = (depth: number) => (commit: Commit) => {
|
|
46
|
+
const color = getCommitColor(commit);
|
|
47
|
+
console.log(color);
|
|
48
|
+
const commitNode = h(CommitPreview, {
|
|
49
|
+
commit,
|
|
50
|
+
tree: appliedTree,
|
|
51
|
+
depth,
|
|
52
|
+
renderCommit: renderCommit(depth + 1),
|
|
53
|
+
onSelectCommit: _ => {},
|
|
54
|
+
selectedCommits: new Set<CommitID>(),
|
|
55
|
+
color,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return commitNode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
h('h3', {}, 'Next Update'),
|
|
63
|
+
nextUpdate ? h(UpdateViewer, { update: nextUpdate, tree, thread }) : 'Send to Renderer',
|
|
64
|
+
thread.reasons.map(reason => {
|
|
65
|
+
const commit = appliedTree.commits.get(reason.ref.id);
|
|
66
|
+
if (!commit)
|
|
67
|
+
return null;
|
|
68
|
+
return h(TreeViewer, { tree: appliedTree, roots: [commit], selectedCommits: new Set<CommitID>(), renderCommit: renderCommit(0) });
|
|
69
|
+
}),
|
|
70
|
+
hs('h3', {}, 'Fibers'),
|
|
71
|
+
hs('ul', {}, [...thread.pendingUpdates].map((update) => hs('li', {}, h(UpdateViewer, { update, tree })))),
|
|
72
|
+
hs('h3', {}, 'Visited'),
|
|
73
|
+
hs('ul', {}, [...thread.visited].map(([id, ref]) => hs('li', {}, id))),
|
|
74
|
+
hs('h3', {}, 'Must Visit'),
|
|
75
|
+
hs('ul', {}, [...thread.mustVisit].map(([id, ref]) => hs('li', {}, id))),
|
|
76
|
+
hs('h3', {}, 'Must Render'),
|
|
77
|
+
hs('ul', {}, [...thread.mustRender].map(([id, ref]) => hs('li', {}, id))),
|
|
78
|
+
hs('h3', {}, 'Created'),
|
|
79
|
+
hs('ul', {}, thread.deltas.created.map(delta => hs('li', {}, [delta.next.id,' ', getElementName(delta.next.element)]))),
|
|
80
|
+
hs('h3', {}, 'Removed'),
|
|
81
|
+
hs('ul', {}, thread.deltas.removed.map(delta => hs('li', {}, [delta.ref.id,' ', getElementName(delta.prev.element)]))),
|
|
82
|
+
hs('h3', {}, 'Updated'),
|
|
83
|
+
hs('ul', {}, thread.deltas.updated.map(delta => hs('li', {}, [delta.next.id,' ', getElementName(delta.next.element)]))),
|
|
84
|
+
hs('h3', {}, 'Reasons'),
|
|
85
|
+
h(ReasonsListViewer, { reasons: thread.reasons, tree }),
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type DeltaViewerProps = {
|
|
90
|
+
deltas: DeltaSet,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DeltaViewer = () => {
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type UpdateViewerProps = {
|
|
98
|
+
tree: CommitTree,
|
|
99
|
+
thread: WorkThread,
|
|
100
|
+
update: Update,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const UpdateViewer: Component<UpdateViewerProps> = ({ update, tree, thread }) => {
|
|
104
|
+
const commit = tree.commits.get(update.ref.id);
|
|
105
|
+
|
|
106
|
+
const isEqual = update.next && update.prev && update.next.id === update.prev.element.id;
|
|
107
|
+
|
|
108
|
+
return hs('div', { className: classes.updateViewer }, [
|
|
109
|
+
!isEqual && update.next && typeof update.next.type === 'function' && [
|
|
110
|
+
h('span', {}, `Calling render function for component: `),
|
|
111
|
+
h('pre', { style: { display: 'inline' } }, `"${update.next.type.name}"`),
|
|
112
|
+
],
|
|
113
|
+
isEqual && [
|
|
114
|
+
thread.mustVisit.has(update.ref.id) ? [
|
|
115
|
+
thread.mustRender.has(update.ref.id) ?
|
|
116
|
+
h('span', {}, `Marked for rendering`) :
|
|
117
|
+
h('span', {}, `Visiting but not rendering`),
|
|
118
|
+
] : [
|
|
119
|
+
h('span', {}, `Skipping render and visit - fiber ends here`)
|
|
120
|
+
]
|
|
121
|
+
],
|
|
122
|
+
h(CommitAttributeTag, { name: 'Type', value: getUpdateType(update) }),
|
|
123
|
+
h(CommitAttributeTag, { name: 'ID', value: update.ref.id.toString() }),
|
|
124
|
+
update.prev && h(CommitAttributeTag, { name: 'Prev ID', value: (update.prev && update.prev.id.toString()) || 'null' }),
|
|
125
|
+
update.next && h(CommitAttributeTag, { name: 'Next Element', value: (update.next && getElementName(update.next)) || 'null' }),
|
|
126
|
+
])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const getUpdateType = (update: Update) => {
|
|
130
|
+
if (update.prev && !update.next)
|
|
131
|
+
return 'Remove';
|
|
132
|
+
if (update.prev && update.next)
|
|
133
|
+
if (update.moved)
|
|
134
|
+
return "Move & Update"
|
|
135
|
+
else
|
|
136
|
+
return "Update"
|
|
137
|
+
if (!update.prev && update.next)
|
|
138
|
+
return "Create";
|
|
139
|
+
return "???"
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type ReasonsListViewerProps = {
|
|
143
|
+
reasons: WorkReason[],
|
|
144
|
+
tree: CommitTree,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const ReasonsListViewer: Component<ReasonsListViewerProps> = ({ reasons, tree }) => {
|
|
148
|
+
const firstReason = reasons[0];
|
|
149
|
+
if (!firstReason)
|
|
150
|
+
return null;
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
h('p', {}, `Started rendering because:`),
|
|
154
|
+
h('p', {}, h(ReasonViewer, { reason: firstReason, tree })),
|
|
155
|
+
reasons.length > 1 && [
|
|
156
|
+
h('p', {}, `The following updates were also batched with this render:`),
|
|
157
|
+
h('ol', {}, reasons.slice(1).map(reason => {
|
|
158
|
+
return h('li', {}, h(ReasonViewer, { reason, tree }));
|
|
159
|
+
}))
|
|
160
|
+
]
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type ReasonsViewerProps = {
|
|
165
|
+
reason: WorkReason,
|
|
166
|
+
tree: CommitTree,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const ReasonViewer: Component<ReasonsViewerProps> = ({ reason, tree }) => {
|
|
170
|
+
switch (reason.type) {
|
|
171
|
+
case 'mount':
|
|
172
|
+
return [
|
|
173
|
+
`Mounting a new tree using:`,
|
|
174
|
+
h('pre', { style: { display: 'inline' } }, getElementName(reason.element)),
|
|
175
|
+
];
|
|
176
|
+
case 'target':
|
|
177
|
+
const targetCommit = tree.commits.get(reason.ref.id) as Commit;
|
|
178
|
+
return [
|
|
179
|
+
h(CommitAttributeTag, { name: 'CommitID', value: targetCommit.id.toString() }),
|
|
180
|
+
` `,
|
|
181
|
+
h('pre', { style: { display: 'inline' } }, getElementName(targetCommit.element)),
|
|
182
|
+
` requested a re-render`
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.elementBar {
|
|
2
|
+
overflow: visible;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: row;
|
|
5
|
+
flex-shrink: 0;
|
|
6
|
+
width: max-content;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.elementName {
|
|
10
|
+
font-family: monospace;
|
|
11
|
+
display: inline;
|
|
12
|
+
|
|
13
|
+
padding: 2px;
|
|
14
|
+
border-radius: 8px;
|
|
15
|
+
|
|
16
|
+
margin: 2px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.commit {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
overflow: visible;
|
|
23
|
+
white-space: pre;
|
|
24
|
+
width: max-content;
|
|
25
|
+
flex: 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.elementBar.selected .elementName {
|
|
29
|
+
font-weight: bold;
|
|
30
|
+
text-decoration: underline;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.commitList {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
list-style: none;
|
|
37
|
+
overflow: visible;
|
|
38
|
+
width: max-content;
|
|
39
|
+
gap: 9px;
|
|
40
|
+
}
|
|
41
|
+
.commitList.top {
|
|
42
|
+
margin: 0;
|
|
43
|
+
padding: 0;
|
|
44
|
+
|
|
45
|
+
overflow: auto;
|
|
46
|
+
}
|
package/TreeViewer.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Component, h, Node } from "@lukekaalim/act";
|
|
2
|
+
import { Commit, CommitID, CommitTree } from "@lukekaalim/act-recon";
|
|
3
|
+
import { hs } from "@lukekaalim/act-web";
|
|
4
|
+
import { getElementName } from "./utils";
|
|
5
|
+
import stringHash from '@sindresorhus/string-hash';
|
|
6
|
+
|
|
7
|
+
import classes from './TreeViewer.module.css';
|
|
8
|
+
import { CommitAttributeTag } from "./AttributeTag";
|
|
9
|
+
import { CommitReport, TreeReport } from "@lukekaalim/act-debug";
|
|
10
|
+
|
|
11
|
+
export type TreeViewerProps = {
|
|
12
|
+
tree: TreeReport,
|
|
13
|
+
|
|
14
|
+
renderCommit: (commit: CommitReport) => Node,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const TreeViewer: Component<TreeViewerProps> = ({
|
|
18
|
+
tree, renderCommit
|
|
19
|
+
}) => {
|
|
20
|
+
const rootCommits = tree.roots
|
|
21
|
+
.map(root => tree.commits.get(root.id))
|
|
22
|
+
.filter(Boolean) as CommitReport[];
|
|
23
|
+
|
|
24
|
+
const className = [classes.commitList, classes.top].join(' ')
|
|
25
|
+
|
|
26
|
+
return h('ol', { className }, rootCommits.map(root =>
|
|
27
|
+
h('li', {}, renderCommit(root))));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CommitPreviewProps = {
|
|
31
|
+
commit: CommitReport,
|
|
32
|
+
tree: TreeReport,
|
|
33
|
+
|
|
34
|
+
attributes?: [string, string][],
|
|
35
|
+
|
|
36
|
+
color?: string,
|
|
37
|
+
|
|
38
|
+
depth?: number,
|
|
39
|
+
|
|
40
|
+
renderCommit: (commit: CommitReport) => Node,
|
|
41
|
+
onClick?: () => void,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const CommitPreview: Component<CommitPreviewProps> = ({
|
|
45
|
+
commit, tree, depth = 0,
|
|
46
|
+
attributes = [],
|
|
47
|
+
renderCommit,
|
|
48
|
+
color,
|
|
49
|
+
onClick,
|
|
50
|
+
}) => {
|
|
51
|
+
const children = commit.children
|
|
52
|
+
.map(childRef => tree.commits.get(childRef.id)).filter(c => !!c);
|
|
53
|
+
|
|
54
|
+
const background = `hsl(${(depth * 22.3) % 360}deg, 50%, 80%)`;
|
|
55
|
+
const elementBackground = color || `hsl(${stringHash(commit.element.type) % 360}deg, 60%, 80%)`;
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
return hs('div', { className: classes.commit, style: { background } }, [
|
|
59
|
+
hs('div', { className: [classes.elementBar].join(' ') }, [
|
|
60
|
+
hs('button', { onClick, className: classes.elementName, style: { background: elementBackground } },
|
|
61
|
+
commit.element.type),
|
|
62
|
+
//h(CommitAttributeTag, { name: 'Id', value: commit.id.toString() }),
|
|
63
|
+
attributes.map(([name, value]) => h(CommitAttributeTag, { name, value }))
|
|
64
|
+
//h(CommitAttributeTag, { name: 'Version', value: commit.version.toString() }),
|
|
65
|
+
]),
|
|
66
|
+
hs('ol', { className: classes.commitList }, children.map(child => h('li', {}, renderCommit(child)))),
|
|
67
|
+
])
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// h(CommitPreview, { commit: child, tree, depth: depth + 1, selectedCommits, onSelectCommit })))
|
package/debug.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Commit, CommitTree, createReconciler, Scheduler, WorkID, WorkThread } from "@lukekaalim/act-recon";
|
|
2
|
+
import { createId, h, Node } from "@lukekaalim/act";
|
|
3
|
+
import { createRenderFunction, RenderFunction, RenderSpace } from "@lukekaalim/act-backstage";
|
|
4
|
+
import { createDOMScheduler, createWebSpace, HTML, render } from "@lukekaalim/act-web";
|
|
5
|
+
import { InsightApp } from "./InsightApp";
|
|
6
|
+
|
|
7
|
+
export type DebugScheduler = {
|
|
8
|
+
isWorkPending(): boolean,
|
|
9
|
+
work(): void,
|
|
10
|
+
|
|
11
|
+
inner: Scheduler,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const createDebugScheduler = (): DebugScheduler => {
|
|
15
|
+
const pending = new Map<WorkID, () => void>();
|
|
16
|
+
|
|
17
|
+
const inner: Scheduler = {
|
|
18
|
+
requestWork(callback) {
|
|
19
|
+
const id = createId<'WorkID'>();
|
|
20
|
+
pending.set(id, callback);
|
|
21
|
+
return id;
|
|
22
|
+
},
|
|
23
|
+
cancelWork(workId) {
|
|
24
|
+
pending.delete(workId)
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
isWorkPending() {
|
|
30
|
+
return pending.size > 0;
|
|
31
|
+
},
|
|
32
|
+
work() {
|
|
33
|
+
const pendingWork = [...pending];
|
|
34
|
+
pending.clear();
|
|
35
|
+
for (const [,callback] of pendingWork)
|
|
36
|
+
callback();
|
|
37
|
+
},
|
|
38
|
+
inner,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const renderDebug = (node: Node, createSpace: (tree: CommitTree) => RenderSpace) => {
|
|
43
|
+
const debugScheduler = createDebugScheduler();
|
|
44
|
+
|
|
45
|
+
const reconciler = createReconciler(debugScheduler.inner);
|
|
46
|
+
|
|
47
|
+
const threadCompleteSub = reconciler.on('on-thread-complete', (thread: WorkThread) => {
|
|
48
|
+
space.create(thread.deltas).configure();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const space = createSpace(reconciler.tree);
|
|
52
|
+
|
|
53
|
+
const onReady = () => {
|
|
54
|
+
reconciler.mount(node);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const debugWindow = window.open('', 'debug', 'popup');
|
|
58
|
+
if (debugWindow) {
|
|
59
|
+
const node = h(InsightApp, { reconciler, onReady, scheduler: debugScheduler });
|
|
60
|
+
const root = debugWindow.document.body;
|
|
61
|
+
for (const child of [...debugWindow.document.body.children, ...debugWindow.document.head.children])
|
|
62
|
+
child.remove()
|
|
63
|
+
|
|
64
|
+
for (const child of document.head.children) {
|
|
65
|
+
if (child instanceof HTMLStyleElement) {
|
|
66
|
+
debugWindow.document.head.append(child.cloneNode(true));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const renderWeb = createRenderFunction<HTMLElement>(
|
|
70
|
+
createDOMScheduler(),
|
|
71
|
+
(tree, root) => createWebSpace(tree, root, debugWindow)
|
|
72
|
+
)(h(HTML, {}, node), root);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
stop() {
|
|
77
|
+
threadCompleteSub.cancel();
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
};
|
package/devtool_page.ts
ADDED
|
File without changes
|
package/devtool_panel.ts
ADDED
|
File without changes
|
|
File without changes
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './InsightApp';
|
package/insight.test.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import '@types/node';
|
|
2
|
+
|
|
3
|
+
import { OpaqueID } from '@lukekaalim/act'
|
|
4
|
+
import { CommitRef } from '@lukekaalim/act-recon'
|
|
5
|
+
import { describe, it } from 'node:test'
|
|
6
|
+
import { deepEqual } from 'node:assert/strict'
|
|
7
|
+
import { findCommonAncestor } from './utils'
|
|
8
|
+
|
|
9
|
+
describe('@lukekaalim/act-insight', () => {
|
|
10
|
+
describe('findCommonAncestor', () => {
|
|
11
|
+
it ('should find a common ancestor', () => {
|
|
12
|
+
const a: OpaqueID<'CommitID'> = 'A' as any;
|
|
13
|
+
const b: OpaqueID<'CommitID'> = 'B' as any;
|
|
14
|
+
const c: OpaqueID<'CommitID'> = 'C' as any;
|
|
15
|
+
const d: OpaqueID<'CommitID'> = 'D' as any;
|
|
16
|
+
const e: OpaqueID<'CommitID'> = 'E' as any;
|
|
17
|
+
const f: OpaqueID<'CommitID'> = 'F' as any;
|
|
18
|
+
|
|
19
|
+
const refZ = CommitRef.from([a]);
|
|
20
|
+
const refA = CommitRef.from([a, b]);
|
|
21
|
+
const refB = CommitRef.from([a, c]);
|
|
22
|
+
const refC = CommitRef.from([a, c, d, e]);
|
|
23
|
+
const refD = CommitRef.from([a, c, f]);
|
|
24
|
+
|
|
25
|
+
deepEqual(findCommonAncestor([refA, refB]), refZ)
|
|
26
|
+
deepEqual(findCommonAncestor([refB, refC, refD]), refB)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
})
|
package/mode.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lukekaalim/act-insight",
|
|
3
|
+
"main": "index.ts",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"scripts": {},
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@lukekaalim/act": "*",
|
|
8
|
+
"@lukekaalim/act-recon": "*",
|
|
9
|
+
"@lukekaalim/act-web": "*",
|
|
10
|
+
"@lukekaalim/act-debug": "*",
|
|
11
|
+
"@sindresorhus/string-hash": "^2.0.0",
|
|
12
|
+
"@types/firefox-webext-browser": "^120.0.4",
|
|
13
|
+
"lodash-es": "^4.17.21"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/firefox": "^0.0.34",
|
|
17
|
+
"@types/lodash-es": "^4.17.12"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
Binary file
|
package/utils.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Element, errorBoundaryType, primitiveNodeTypes, providerNodeType, renderNodeType } from "@lukekaalim/act";
|
|
2
|
+
import { CommitPath, CommitRef } from "@lukekaalim/act-recon";
|
|
3
|
+
|
|
4
|
+
export const getElementName = (element: Element) => {
|
|
5
|
+
if (typeof element.type === 'function')
|
|
6
|
+
return `<component(${element.type.name})>`;
|
|
7
|
+
if (typeof element.type === 'symbol')
|
|
8
|
+
switch (element.type) {
|
|
9
|
+
case primitiveNodeTypes.number:
|
|
10
|
+
return `<number value={${element.props.value}}>`
|
|
11
|
+
case primitiveNodeTypes.string:
|
|
12
|
+
return `<string value="${element.props.value}">`
|
|
13
|
+
case primitiveNodeTypes.boolean:
|
|
14
|
+
return `<boolean value="${element.props.value}">`
|
|
15
|
+
case primitiveNodeTypes.array:
|
|
16
|
+
return `<array>`
|
|
17
|
+
case primitiveNodeTypes.null:
|
|
18
|
+
return `<null>`
|
|
19
|
+
case renderNodeType:
|
|
20
|
+
return `<render type="${element.props.type}">`;
|
|
21
|
+
case providerNodeType:
|
|
22
|
+
return `<context id="${element.props.id}">`;
|
|
23
|
+
case errorBoundaryType:
|
|
24
|
+
return `<boundary>`;
|
|
25
|
+
default:
|
|
26
|
+
return `<symbol>`
|
|
27
|
+
}
|
|
28
|
+
if (element.type)
|
|
29
|
+
return `<${element.type}>`;
|
|
30
|
+
return '<none>';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
export const findCommonAncestor = (commitRefs: CommitRef[]) => {
|
|
35
|
+
let commonAncestorPath: CommitPath | null = null;
|
|
36
|
+
for (const ref of commitRefs) {
|
|
37
|
+
if (!commonAncestorPath)
|
|
38
|
+
commonAncestorPath = ref.path
|
|
39
|
+
else {
|
|
40
|
+
for (const id of [...ref.path].reverse()) {
|
|
41
|
+
const index = commonAncestorPath.indexOf(id);
|
|
42
|
+
if (index !== -1) {
|
|
43
|
+
commonAncestorPath = commonAncestorPath.slice(0, index + 1)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
if (commonAncestorPath)
|
|
50
|
+
return CommitRef.from(commonAncestorPath);
|
|
51
|
+
return null;
|
|
52
|
+
}
|