Skip to content

Commit f98e479

Browse files
committed
[wip] dev-tools package
1 parent 63fbc9c commit f98e479

File tree

17 files changed

+977
-69
lines changed

17 files changed

+977
-69
lines changed

.prettierrc.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ overrides:
66
- files: "**/*.tsx"
77
options:
88
parser: alloy-ts
9+
- files: "packages/dev-tools/**/*.tsx"
10+
options:
11+
parser: typescript

packages/core/src/render.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ const nodesToContext = new WeakMap<RenderTextTree, Context>();
167167
export function getContextForRenderNode(node: RenderTextTree) {
168168
return nodesToContext.get(node);
169169
}
170+
170171
export type RenderStructure = {};
171172

172173
export type RenderTextTree = (string | RenderTextTree)[];

packages/dev-tools/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@alloy-js/dev-tools",
3+
"version": "0.0.1",
4+
"description": "Development tools for Alloy code generation",
5+
"main": "./dist/src/index.js",
6+
"scripts": {
7+
"build": "tsc -p .",
8+
"test": "vitest run",
9+
"test:watch": "vitest -w"
10+
},
11+
"keywords": [],
12+
"author": "brian.terlson@microsoft.com",
13+
"license": "MIT",
14+
"dependencies": {
15+
"@alloy-js/core": "workspace:~",
16+
"@uiw/react-json-view": "2.0.0-alpha.30",
17+
"esbuild": "^0.19.8",
18+
"flatted": "^3.3.1",
19+
"react": "^18.2.0",
20+
"react-dom": "^18.2.0"
21+
},
22+
"devDependencies": {
23+
"@types/node": "^22.9.0",
24+
"@types/react": "^18.3.12",
25+
"@types/react-dom": "^18.3.1",
26+
"typescript": "catalog:",
27+
"vitest": "catalog:"
28+
},
29+
"type": "module"
30+
}

packages/dev-tools/src/client.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as Flatted from "flatted";
2+
import ReactDOM from "react-dom";
3+
import { AnnotatedNodeViewer } from "./components/AnnotatedNodeViewer";
4+
5+
declare global {
6+
interface Window {
7+
__INITIAL_DATA__: any;
8+
}
9+
}
10+
11+
const root = document.getElementById("root");
12+
if (root) {
13+
ReactDOM.render(
14+
<AnnotatedNodeViewer node={Flatted.parse(window.__INITIAL_DATA__)} />,
15+
root,
16+
);
17+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React, { createContext, useContext, useState } from "react";
2+
import { AnnotatedNode } from "../types";
3+
4+
interface AnnotatedNodeContextType {
5+
selectedNode: AnnotatedNode | null;
6+
setSelectedNode: (node: AnnotatedNode | null) => void;
7+
hoveredNode: AnnotatedNode | null;
8+
setHoveredNode: (node: AnnotatedNode | null) => void;
9+
}
10+
11+
const AnnotatedNodeContext = createContext<AnnotatedNodeContextType>({
12+
selectedNode: null,
13+
setSelectedNode: () => {},
14+
hoveredNode: null,
15+
setHoveredNode: () => {},
16+
});
17+
18+
export const AnnotatedNodeProvider: React.FC<{ children: React.ReactNode }> = ({
19+
children,
20+
}) => {
21+
const [selectedNode, setSelectedNode] = useState<AnnotatedNode | null>(null);
22+
const [hoveredNode, setHoveredNode] = useState<AnnotatedNode | null>(null);
23+
24+
return (
25+
<AnnotatedNodeContext.Provider
26+
value={{
27+
selectedNode,
28+
setSelectedNode,
29+
hoveredNode,
30+
setHoveredNode,
31+
}}
32+
>
33+
{children}
34+
</AnnotatedNodeContext.Provider>
35+
);
36+
};
37+
38+
export const useAnnotatedNode = () => useContext(AnnotatedNodeContext);
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import JsonView from "@uiw/react-json-view";
2+
import React from "react";
3+
import { AnnotatedNode, OutputDirectory, OutputFile } from "../types";
4+
import { ComponentView } from "./ComponentView";
5+
import { AnnotatedNodeProvider, useAnnotatedNode } from "./AnnotatedNodeContext";
6+
import { FileExplorer } from "./FileExplorer";
7+
8+
interface AnnotatedNodeViewerProps {
9+
node: OutputDirectory;
10+
depth?: number;
11+
}
12+
13+
const DEPTH = [
14+
"#f0f4ff", // Level 1 - Very light blue
15+
"#fff0f4", // Level 2 - Very light pink
16+
"#f3fff0", // Level 3 - Very light green
17+
"#fff0fc", // Level 4 - Very light purple
18+
"#fffaf0", // Level 5 - Very light orange
19+
"#f0fffc", // Level 6 - Very light cyan
20+
"#fff0f0", // Level 7 - Very light red
21+
"#f4f0ff", // Level 8 - Very light violet
22+
"#f0fff4", // Level 9 - Very light mint
23+
];
24+
25+
// TODO: Reactive contexts
26+
// TODO: Source maps (babel transform)
27+
// TODO: Live transform
28+
29+
const NodeContent: React.FC<{
30+
item: AnnotatedNode;
31+
depth: number;
32+
index: number;
33+
}> = React.memo(({ item, depth, index }) => {
34+
const { selectedNode, setSelectedNode, hoveredNode, setHoveredNode } =
35+
useAnnotatedNode();
36+
37+
if (typeof item === "string") {
38+
return item;
39+
}
40+
41+
if (
42+
!item.component ||
43+
item.component === "Provider" ||
44+
item.component === "Indent"
45+
) {
46+
return item.rendered.map((subItem, index) => (
47+
<NodeContent key={index} item={subItem} depth={depth} index={index} />
48+
));
49+
}
50+
51+
const isSelected = item === selectedNode;
52+
const isHovered = item === hoveredNode;
53+
54+
return (
55+
<span
56+
className="content"
57+
style={{
58+
backgroundColor: DEPTH[(depth * (index + 2)) % DEPTH.length],
59+
outline:
60+
isSelected ? "2px solid #007bff"
61+
: isHovered ? "1px solid #ccc"
62+
: "none",
63+
cursor: "pointer",
64+
display: "inline",
65+
}}
66+
onClick={(e) => {
67+
e.stopPropagation();
68+
if (isSelected) {
69+
setSelectedNode(null);
70+
} else {
71+
setSelectedNode(item);
72+
}
73+
}}
74+
onMouseEnter={() => setHoveredNode(item)}
75+
onMouseLeave={() => setHoveredNode(null)}
76+
>
77+
{item.rendered.map((subItem, index) => (
78+
<NodeContent
79+
key={index}
80+
item={subItem}
81+
depth={depth + 1}
82+
index={index}
83+
/>
84+
))}
85+
</span>
86+
);
87+
});
88+
89+
const NodeDetails: React.FC = () => {
90+
const { selectedNode, hoveredNode } = useAnnotatedNode();
91+
const nodeToShow = selectedNode || hoveredNode;
92+
93+
if (!nodeToShow) {
94+
return <div>No element selected</div>;
95+
}
96+
97+
if (typeof nodeToShow === "string") {
98+
return (
99+
<div>
100+
<h3 style={{ fontSize: "1em", fontFamily: "monospace" }}>Text Node</h3>
101+
<pre
102+
style={{
103+
whiteSpace: "pre-wrap",
104+
wordBreak: "break-word",
105+
borderRadius: "4px",
106+
}}
107+
>
108+
{nodeToShow}
109+
</pre>
110+
</div>
111+
);
112+
}
113+
114+
let jsonView = null;
115+
// @ts-expect-error
116+
globalThis.__temp = nodeToShow.props;
117+
try {
118+
JSON.stringify(nodeToShow.props);
119+
jsonView = <JsonView value={nodeToShow.props as any} />;
120+
} catch {
121+
jsonView = "Circular references detected, access __temp in dev tools";
122+
}
123+
124+
return (
125+
<div>
126+
<h3 style={{ fontSize: "1em", fontFamily: "monospace" }}>
127+
{nodeToShow.component}
128+
</h3>
129+
<p className="label">props</p>
130+
{jsonView}
131+
<p className="label">context</p>
132+
<p className="label">source</p>
133+
<pre>{nodeToShow.implementation}</pre>
134+
</div>
135+
);
136+
};
137+
138+
export const AnnotatedNodeViewer: React.FC<AnnotatedNodeViewerProps> = ({
139+
node,
140+
depth = 0,
141+
}) => {
142+
const [selectedFile, setSelectedFile] = React.useState<
143+
OutputFile | undefined
144+
>();
145+
146+
return (
147+
<AnnotatedNodeProvider>
148+
<div
149+
className="element-node-viewer"
150+
style={{
151+
backgroundColor: "white",
152+
display: "flex",
153+
flexDirection: "column",
154+
height: "100vh",
155+
}}
156+
>
157+
<div
158+
style={{
159+
display: "flex",
160+
overflowY: "auto",
161+
flex: "3",
162+
padding: "1rem",
163+
gap: "1rem",
164+
minHeight: "100px", // Ensure minimum height
165+
}}
166+
>
167+
<div
168+
className="fileExplorer"
169+
style={{
170+
margin: 0,
171+
minWidth: 300,
172+
fontFamily: "monospace",
173+
flex: 0,
174+
overflowY: "auto",
175+
}}
176+
>
177+
<FileExplorer
178+
directory={node}
179+
onFileSelect={setSelectedFile}
180+
selectedFile={selectedFile}
181+
/>
182+
</div>
183+
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
184+
{selectedFile && (
185+
<div
186+
style={{
187+
padding: "0.5rem",
188+
borderBottom: "1px solid #ccc",
189+
fontFamily: "monospace",
190+
fontSize: "14px",
191+
color: "#666",
192+
}}
193+
>
194+
📄 {selectedFile.path}
195+
</div>
196+
)}
197+
<pre
198+
style={{
199+
margin: 0,
200+
padding: "0.5rem",
201+
border: "1px solid #ccc",
202+
borderTop: "none",
203+
overflow: "auto",
204+
fontFamily: "monospace",
205+
flex: 1,
206+
}}
207+
>
208+
{selectedFile?.contents.map((item, index) => (
209+
<React.Fragment key={index}>
210+
<NodeContent item={item} depth={depth} index={index} />
211+
</React.Fragment>
212+
)) ?? "Select a file!"}
213+
</pre>
214+
</div>
215+
</div>
216+
<div
217+
className="gutter"
218+
style={{
219+
height: "4px",
220+
backgroundColor: "#ccc",
221+
cursor: "row-resize",
222+
position: "relative",
223+
}}
224+
onMouseDown={(e) => {
225+
const startY = e.clientY;
226+
const bottomPane = e.currentTarget
227+
.nextElementSibling as HTMLElement;
228+
const startHeight = bottomPane.offsetHeight;
229+
230+
const handleMouseMove = (e: MouseEvent) => {
231+
const delta = e.clientY - startY;
232+
const newHeight = Math.max(100, startHeight - delta);
233+
bottomPane.style.height = `${newHeight}px`;
234+
bottomPane.style.flex = "none";
235+
};
236+
237+
const handleMouseUp = () => {
238+
document.removeEventListener("mousemove", handleMouseMove);
239+
document.removeEventListener("mouseup", handleMouseUp);
240+
};
241+
242+
document.addEventListener("mousemove", handleMouseMove);
243+
document.addEventListener("mouseup", handleMouseUp);
244+
}}
245+
/>
246+
<div
247+
style={{
248+
display: "flex",
249+
minHeight: "100px",
250+
height: "300px",
251+
overflowY: "auto",
252+
borderTop: "1px solid #ccc",
253+
}}
254+
>
255+
<div
256+
style={{
257+
display: "flex",
258+
flexDirection: "column",
259+
padding: "1rem",
260+
overflowY: "auto",
261+
borderRight: "1px solid #dee2e6",
262+
flex: "1",
263+
}}
264+
>
265+
{selectedFile?.contents.map((item, index) => (
266+
<ComponentView key={index} item={item} depth={0} index={index} />
267+
))}
268+
</div>
269+
<div
270+
style={{
271+
width: 300,
272+
padding: "1rem",
273+
overflowY: "auto",
274+
}}
275+
>
276+
<NodeDetails />
277+
</div>
278+
</div>
279+
</div>
280+
</AnnotatedNodeProvider>
281+
);
282+
};

0 commit comments

Comments
 (0)