@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.
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ .commitViewer {
2
+ position: fixed;
3
+ right: 0px;
4
+ width: 40%;
5
+ }
@@ -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
+ }
@@ -0,0 +1,19 @@
1
+ .treeExplorer {
2
+ display: flex;
3
+ flex-direction: row;
4
+
5
+ position: relative;
6
+ }
7
+
8
+ .insight {
9
+ /*
10
+ display: flex;
11
+ flex-direction: column;
12
+ top: 0;
13
+ left: 0;
14
+ right: 0;
15
+ bottom: 0;
16
+
17
+ position: absolute;
18
+ */
19
+ }
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
+ }
@@ -0,0 +1,18 @@
1
+ .modeButton {
2
+ flex: 1;
3
+ }
4
+
5
+ .menuBar {
6
+ margin: 0;
7
+ padding: 0;
8
+
9
+ display: flex;
10
+ flex-direction: row;
11
+
12
+ list-style: none;
13
+ }
14
+
15
+ .menuBarEntry {
16
+ display: flex;
17
+ flex: 1;
18
+ }
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
+ }
@@ -0,0 +1,5 @@
1
+ .updateViewer {
2
+ background-color: rgb(204, 252, 202);
3
+ border-radius: 8px;
4
+ padding: 8px;
5
+ }
@@ -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
+ };
File without changes
File without changes
File without changes
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './InsightApp';
@@ -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
@@ -0,0 +1,6 @@
1
+ export type InsightMode =
2
+ | 'tree'
3
+ | 'thread'
4
+ | 'reports'
5
+
6
+
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
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vite';
2
+ import webExtension from "vite-plugin-web-extension";
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ modulePreload: false
7
+ },
8
+ plugins: [webExtension({
9
+ browser: 'firefox'
10
+ })],
11
+ });