react-x11 0.0.1 → 1.2.0
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/LICENSE +21 -0
- package/README.md +226 -0
- package/package.json +60 -7
- package/src/ClickToComponent.js +179 -0
- package/src/DevToolsIntegration.js +106 -0
- package/src/Reconciler.js +507 -0
- package/src/components/Button.js +63 -0
- package/src/components/Canvas3D.js +28 -0
- package/src/components/Checkbox.js +69 -0
- package/src/components/Dialog.js +138 -0
- package/src/components/Menu.js +644 -0
- package/src/components/ProgressBar.js +48 -0
- package/src/components/Radio.js +95 -0
- package/src/components/Select.js +272 -0
- package/src/components/Slider.js +177 -0
- package/src/components/Switch.js +49 -0
- package/src/components/Tooltip.js +146 -0
- package/src/components/anchor.js +211 -0
- package/src/components/index.js +17 -0
- package/src/components/keys.js +21 -0
- package/src/components/theme.js +66 -0
- package/src/components/typeahead.js +56 -0
- package/src/events.js +584 -0
- package/src/geometry3d.js +223 -0
- package/src/glnodes.js +275 -0
- package/src/index.js +30 -0
- package/src/mat4.js +235 -0
- package/src/nodes.js +1985 -0
- package/src/pointer3d.js +158 -0
- package/src/priority.js +39 -0
- package/src/raycast3d.js +146 -0
- package/src/richnodes.js +436 -0
- package/src/scene3d.js +683 -0
- package/src/styles.js +189 -0
- package/.npmignore +0 -17
- package/combobox.jsx +0 -69
- package/react-x11.js +0 -119
- package/test-canvas.js +0 -98
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Andrey Sidorov
|
|
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.
|
package/README.md
CHANGED
|
@@ -1 +1,227 @@
|
|
|
1
1
|
# react-x11
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sidorares/react-x11/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
React custom rendering where side effects are communication with an [X11
|
|
6
|
+
server](https://www.x.org/wiki/Documentation/): react-like ergonomics on top
|
|
7
|
+
of [ntk](https://github.com/sidorares/ntk). Build small GUI programs for the
|
|
8
|
+
X Window environment (a linux desktop, or macOS +
|
|
9
|
+
[XQuartz](https://www.xquartz.org/)) with your React / React Native
|
|
10
|
+
experience — flexbox layout, components, hooks, synthetic events.
|
|
11
|
+
|
|
12
|
+
Everything is JavaScript all the way down: ntk /
|
|
13
|
+
[node-x11](https://github.com/sidorares/node-x11) implement the X11 protocol
|
|
14
|
+
in pure JS (think xlib rewritten in node.js), layout is
|
|
15
|
+
[yoga-layout](https://www.npmjs.com/package/yoga-layout), text shaping is
|
|
16
|
+
[fontkit](https://github.com/foliojs/fontkit). `npm install` never compiles
|
|
17
|
+
anything — and `npm test` doesn't even need an X server (node-x11 ships an
|
|
18
|
+
in-process pure-JS X server that the tests render into and read pixels back
|
|
19
|
+
from; every screenshot below was rendered that way too, by driving the real
|
|
20
|
+
examples through the real event pipeline).
|
|
21
|
+
|
|
22
|
+
| `examples/dashboard.jsx` — context theming, hooks | `examples/tasks.jsx` — useReducer, textinput, scrollview |
|
|
23
|
+
| ------------------------------------------------- | -------------------------------------------------------- |
|
|
24
|
+
|  |  |
|
|
25
|
+
|
|
26
|
+
| `examples/form.jsx` — textinput + Select | the open Select menu (a real `<popup>` window) |
|
|
27
|
+
| ---------------------------------------- | ---------------------------------------------- |
|
|
28
|
+
|  |  |
|
|
29
|
+
|
|
30
|
+
`examples/three.jsx` — a react-three-fiber-shaped scene over **indirect
|
|
31
|
+
GLX**: `<mesh>`, geometries, materials, lights and a texture, drawn by
|
|
32
|
+
sending the GL protocol over the X connection. No native bindings, no GPU
|
|
33
|
+
driver bindings — the same "JavaScript all the way down" story as the rest.
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install react-x11 react
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import React, { useState } from 'react';
|
|
45
|
+
import { createRoot } from 'react-x11';
|
|
46
|
+
|
|
47
|
+
function Counter() {
|
|
48
|
+
const [n, setN] = useState(0);
|
|
49
|
+
return (
|
|
50
|
+
<window width={240} height={120} title="counter" backgroundColor="#f4f4f4">
|
|
51
|
+
<box flexGrow={1} alignItems="center" justifyContent="center" gap={10}>
|
|
52
|
+
<text fontSize={24}>{String(n)}</text>
|
|
53
|
+
<box
|
|
54
|
+
backgroundColor="#2980b9"
|
|
55
|
+
borderRadius={6}
|
|
56
|
+
padding={8}
|
|
57
|
+
cursor="pointer"
|
|
58
|
+
onClick={() => setN(n + 1)}
|
|
59
|
+
>
|
|
60
|
+
<text color="white">+1</text>
|
|
61
|
+
</box>
|
|
62
|
+
</box>
|
|
63
|
+
</window>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const root = await createRoot(); // connects via $DISPLAY
|
|
68
|
+
root.render(<Counter />);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Run it with `tsx` or any JSX-capable loader — or skip JSX entirely with
|
|
72
|
+
`React.createElement` (see
|
|
73
|
+
[`examples/simple-nojsx.js`](examples/simple-nojsx.js), plain node, no build
|
|
74
|
+
step).
|
|
75
|
+
|
|
76
|
+
## Elements
|
|
77
|
+
|
|
78
|
+
Only `<window>`, `<popup>` and `<glarea>` map to real X11 windows.
|
|
79
|
+
Everything else is laid out by yoga and drawn client-side into the window's
|
|
80
|
+
double-buffered 2d context — see [NEXT_STEPS.md](NEXT_STEPS.md) for the architecture rationale
|
|
81
|
+
and [docs/](docs/README.md) for the full API reference.
|
|
82
|
+
|
|
83
|
+
| element | what it is |
|
|
84
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `<window>` | a real X11 window; the flex, paint and event root |
|
|
86
|
+
| `<popup>` | an override-redirect window at screen coordinates — menus, tooltips, dropdowns |
|
|
87
|
+
| `<box>` | flex container: layout props → yoga, plus backgrounds, borders (solid/dashed, radius), overflow clipping, zIndex |
|
|
88
|
+
| `<scrollview>` | clipped, wheel-scrollable viewport with a drawn scrollbar |
|
|
89
|
+
| `<text>` | shaped, wrapped text (bidi, ligatures, font fallback); nested `<text>` elements are style spans |
|
|
90
|
+
| `<textinput>` | single-line editor: caret/selection via ntk's TextLayout caret API, clipboard (CLIPBOARD + X11 PRIMARY), word select |
|
|
91
|
+
| `<image>` | PNG/JPEG from `src`, natural-size aware |
|
|
92
|
+
| `<canvas>` | escape hatch: `onDraw={(ctx, {width, height}) => …}` with ntk's canvas-like 2d context (XRender-backed) |
|
|
93
|
+
| `<markdown>` | ntk MarkdownView: headings, tables, highlighted fences, math, mermaid; `onLink` |
|
|
94
|
+
| `<html>` | ntk HtmlView: CSS cascade, block/flex layout, images; `onLink` |
|
|
95
|
+
| `<svg>` | static SVG through ntk SvgView, sized like `<image>` |
|
|
96
|
+
| `<tex>` | a KaTeX formula (ntk `layoutTex`), intrinsically sized |
|
|
97
|
+
| `<glarea>` | an OpenGL surface over indirect GLX; the 3D scene below lives inside it |
|
|
98
|
+
|
|
99
|
+
Widget **components** (plain React on top of the primitives, themable via
|
|
100
|
+
`ThemeProvider`): `Button`, `Checkbox`, `Radio`/`RadioGroup`, `Switch`,
|
|
101
|
+
`Slider`, `ProgressBar`, `Select`, `Tooltip`, `MenuBar`/`ContextMenu` and
|
|
102
|
+
`Dialog` — a modal built on `<popup trapFocus>`, which traps Tab and
|
|
103
|
+
restores focus when it closes. See [docs/components.md](docs/components.md).
|
|
104
|
+
|
|
105
|
+
### 3D
|
|
106
|
+
|
|
107
|
+
Inside a `<glarea>` (or the `Canvas3D` component) the children are scene
|
|
108
|
+
elements with react-three-fiber's names:
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
<Canvas3D flexGrow={1} camera={{ position: [0, 2, 6], fov: 45 }}>
|
|
112
|
+
<ambientLight intensity={0.35} />
|
|
113
|
+
<pointLight position={[5, 6, 6]} />
|
|
114
|
+
<mesh rotation={[0.5, 0.4, 0]} onClick={() => pick()}>
|
|
115
|
+
<boxGeometry args={[1.4, 1.4, 1.4]} />
|
|
116
|
+
<meshPhongMaterial color="#2980b9" shininess={60} />
|
|
117
|
+
</mesh>
|
|
118
|
+
</Canvas3D>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`<mesh>`, `<group>`, box/plane/sphere/cylinder/torus geometries,
|
|
122
|
+
`<bufferGeometry>`, basic/Lambert/Phong materials with textures, four light
|
|
123
|
+
types, and pointer events resolved by client-side raycasting. Each geometry
|
|
124
|
+
is compiled into a **server-side display list** once, so a frame costs
|
|
125
|
+
matrices plus one `CallList` per mesh whatever the triangle count. What the
|
|
126
|
+
protocol cannot do — shaders, instancing, post-processing, shadows — throws
|
|
127
|
+
with the reason rather than half-working. See
|
|
128
|
+
[docs/elements.md](docs/elements.md#3d-scene-mesh-group-geometries-materials)
|
|
129
|
+
and [docs/glx-plan.md](docs/glx-plan.md).
|
|
130
|
+
|
|
131
|
+
Events are synthetic with capture/bubble phases and hit testing over the
|
|
132
|
+
drawn tree: `onClick` (with DOM-style `detail` click counting),
|
|
133
|
+
`onMouseDown/Up/Move`, `onMouseEnter/Leave`, `onWheel`, `onKeyDown/Up`,
|
|
134
|
+
`focusable` + `onFocus`/`onBlur` + Tab traversal, and a `cursor` prop.
|
|
135
|
+
User handlers run before element default actions and can
|
|
136
|
+
`ev.preventDefault()` — stopping a `<textinput>` from editing or a
|
|
137
|
+
`<scrollview>` from scrolling, like the DOM.
|
|
138
|
+
|
|
139
|
+
## Examples
|
|
140
|
+
|
|
141
|
+
All need an X server (`DISPLAY` set; XQuartz on macOS, Xvfb for automation):
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
npm run examples:simple # hello world (JSX via tsx)
|
|
145
|
+
npm run examples:simple-nojsx # the same, plain node — no build step
|
|
146
|
+
npm run examples:xeyes # canvas drawing + hooks
|
|
147
|
+
npm run examples:dashboard # context theming, custom hooks, components
|
|
148
|
+
npm run examples:tasks # useReducer, textinput, scrollview
|
|
149
|
+
npm run examples:menu # right-click context menu via <popup>
|
|
150
|
+
npm run examples:form # <textinput> + Select dropdowns
|
|
151
|
+
npm run examples:gl # raw GL in a <glarea> (display-list cube)
|
|
152
|
+
npm run examples:three # <Canvas3D> scene: meshes, lights, textures
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The two GL examples additionally need a server with **indirect GLX**
|
|
156
|
+
enabled (`+iglx` / `AllowIndirectGLX` — off by default on many).
|
|
157
|
+
|
|
158
|
+
### Hot reloading
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
npm run examples:tasks:hot # then edit examples/tasks.jsx while it runs
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Runs the tasks example under
|
|
165
|
+
[hot-module-replacement](https://github.com/sidorares/hot-module-replacement)'s
|
|
166
|
+
ESM hooks (Node ≥ 22.15) with React **Fast Refresh**: saving
|
|
167
|
+
`examples/tasks.jsx` updates the edited components in place. The X11
|
|
168
|
+
connection, the mounted window, and component state — the task list, even
|
|
169
|
+
half-typed text in the input — survive the reload (a component whose hook
|
|
170
|
+
signature changed remounts alone). `examples/hmr-register.mjs` wires up the
|
|
171
|
+
transform half (babel JSX + react-refresh, chained under the HMR hooks),
|
|
172
|
+
`examples/hmr-refresh.js` the runtime half, and `examples/tasks-hot.jsx` is
|
|
173
|
+
the hot entry — the pattern works for any example split the same way.
|
|
174
|
+
|
|
175
|
+
## React DevTools
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
npx react-devtools # 1. start the standalone UI
|
|
179
|
+
REACT_X11_DEVTOOLS=1 npm run examples:dashboard # 2. run any example with the bridge on
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The component tree, props and hook state show up live in the DevTools
|
|
183
|
+
window; selecting a component inspects it, and hovering an element in the
|
|
184
|
+
tree tints its rect in the X11 window (highlight-on-hover).
|
|
185
|
+
`REACT_X11_DEVTOOLS_HOST` / `REACT_X11_DEVTOOLS_PORT` override the default
|
|
186
|
+
`localhost:8097`. See [docs/devtools.md](docs/devtools.md).
|
|
187
|
+
|
|
188
|
+
Two more debugging aids:
|
|
189
|
+
|
|
190
|
+
- `REACT_X11_DEBUG_LAYOUT=1` outlines every laid-out node (color = tree
|
|
191
|
+
depth) — handy when a flexbox doesn't do what you expect.
|
|
192
|
+
- refs give you the retained node (`abs` rect, `scrollTo`, …) for drawn
|
|
193
|
+
elements, or the live [ntk](https://github.com/sidorares/ntk) window for
|
|
194
|
+
`<window>`/`<popup>` — the whole ntk API is a ref away.
|
|
195
|
+
|
|
196
|
+
## Click to component
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
REACT_X11_EDITOR=code npm run examples:tasks
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Alt+Click any rendered element (Option+Click on macOS/XQuartz) to open the
|
|
203
|
+
JSX line that created it in your editor. `REACT_X11_EDITOR` picks the CLI
|
|
204
|
+
and enables the feature in one go; `REACT_X11_CLICK_TO_COMPONENT=1` does
|
|
205
|
+
the same with the default editor
|
|
206
|
+
(`cursor`). See [docs/click-to-component.md](docs/click-to-component.md).
|
|
207
|
+
|
|
208
|
+
## Developing
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
npm test # hermetic: mock smoke tests + in-process X server pixels
|
|
212
|
+
npm run lint # ESLint
|
|
213
|
+
npm run format # Prettier
|
|
214
|
+
npm run screenshots # regenerate docs/img/*.png headlessly (no X server)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`docs/img/three.png` is the exception: the headless path has no GL, so the
|
|
218
|
+
3D shot is captured by hand from `npm run examples:three` on a real server
|
|
219
|
+
with indirect GLX.
|
|
220
|
+
|
|
221
|
+
See [AGENTS.md](AGENTS.md) for architecture notes and contributor/agent
|
|
222
|
+
guidance, [docs/](docs/README.md) for API documentation, and
|
|
223
|
+
[NEXT_STEPS.md](NEXT_STEPS.md) for the roadmap.
|
|
224
|
+
|
|
225
|
+
# See also
|
|
226
|
+
|
|
227
|
+
https://github.com/chentsulin/awesome-react-renderer
|
package/package.json
CHANGED
|
@@ -1,23 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"src"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"format:check": "prettier --check .",
|
|
14
|
+
"examples:simple": "tsx examples/simple.jsx",
|
|
15
|
+
"examples:simple-nojsx": "node examples/simple-nojsx.js",
|
|
16
|
+
"examples:xeyes": "tsx examples/xeyes.jsx",
|
|
17
|
+
"examples:dashboard": "tsx examples/dashboard.jsx",
|
|
18
|
+
"examples:tasks": "tsx examples/tasks.jsx",
|
|
19
|
+
"examples:tasks:hot": "node --enable-source-maps --import ./examples/hmr-register.mjs examples/tasks-hot.jsx",
|
|
20
|
+
"examples:menu": "tsx examples/menu.jsx",
|
|
21
|
+
"examples:form": "tsx examples/form.jsx",
|
|
22
|
+
"examples:richtext": "tsx examples/richtext.jsx",
|
|
23
|
+
"examples:widgets": "tsx examples/widgets.jsx",
|
|
24
|
+
"examples:gl": "tsx examples/gl.jsx",
|
|
25
|
+
"examples:three": "tsx examples/three.jsx",
|
|
26
|
+
"examples:windows": "tsx examples/windows.jsx",
|
|
27
|
+
"screenshots": "tsx scripts/screenshots.jsx",
|
|
28
|
+
"screenshots:framed": "tsx scripts/screenshots-framed.jsx",
|
|
29
|
+
"bench": "tsx scripts/bench/protocol.js"
|
|
30
|
+
},
|
|
6
31
|
"repository": {
|
|
7
32
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/sidorares/react-x11"
|
|
33
|
+
"url": "git+https://github.com/sidorares/react-x11.git"
|
|
9
34
|
},
|
|
10
35
|
"keywords": [
|
|
11
36
|
"react",
|
|
12
37
|
"renderer",
|
|
38
|
+
"desktop",
|
|
13
39
|
"x11",
|
|
14
40
|
"ui"
|
|
15
41
|
],
|
|
16
|
-
"author": "Andrey Sidorov <
|
|
42
|
+
"author": "Andrey Sidorov <andrey.sidorov@gmail.com>",
|
|
17
43
|
"license": "MIT",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20.19"
|
|
46
|
+
},
|
|
18
47
|
"dependencies": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
48
|
+
"ntk": "^3.7.2",
|
|
49
|
+
"react-reconciler": "^0.33.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^19.0.0"
|
|
53
|
+
},
|
|
54
|
+
"overrides": {
|
|
55
|
+
"brace-expansion": "^5.0.8"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@babel/core": "^8.0.1",
|
|
59
|
+
"@babel/plugin-transform-react-jsx": "^8.0.1",
|
|
60
|
+
"@eslint/js": "^9.32.0",
|
|
61
|
+
"eslint": "^9.32.0",
|
|
62
|
+
"eslint-plugin-react": "^7.37.5",
|
|
63
|
+
"globals": "^16.0.0",
|
|
64
|
+
"hot-module-replacement": "^4.0.0",
|
|
65
|
+
"prettier": "^3.6.0",
|
|
66
|
+
"react": "^19.2.8",
|
|
67
|
+
"react-devtools-core": "^7.0.1",
|
|
68
|
+
"react-refresh": "^0.18.0",
|
|
69
|
+
"tsx": "^4.23.1",
|
|
70
|
+
"ws": "^8.21.1"
|
|
71
|
+
},
|
|
72
|
+
"type": "module",
|
|
73
|
+
"exports": {
|
|
74
|
+
".": "./src/index.js"
|
|
22
75
|
}
|
|
23
76
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Click-to-component: Alt+Click a rendered element to open the exact JSX
|
|
2
|
+
// line that created it in your editor. Opt-in via REACT_X11_CLICK_TO_COMPONENT
|
|
3
|
+
// (see docs/click-to-component.md); wired up from Reconciler.js the same way
|
|
4
|
+
// REACT_X11_DEVTOOLS wires up DevToolsIntegration.js.
|
|
5
|
+
//
|
|
6
|
+
// Unlike DOM click-to-component tools this needs no babel/jsx-source plugin
|
|
7
|
+
// and no source-map resolution of its own: React 19 already captures a real
|
|
8
|
+
// `Error` (fiber._debugStack) at every JSX call site in development mode,
|
|
9
|
+
// and Node's `--enable-source-maps` (which tsx and this repo's hot-reload
|
|
10
|
+
// loader both enable) has already rewritten that Error's stack to point at
|
|
11
|
+
// original source — we only have to parse the stack text and skip the
|
|
12
|
+
// frames inside React/the reconciler itself.
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { setClickToComponentHandler } from './events.js';
|
|
16
|
+
|
|
17
|
+
const STACK_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?\s*$/;
|
|
18
|
+
|
|
19
|
+
/** The first stack frame outside React/the reconciler/node internals — the
|
|
20
|
+
* user's own JSX call site for whatever element this Error was captured at. */
|
|
21
|
+
function resolveLocation(debugStack) {
|
|
22
|
+
const stack = debugStack?.stack;
|
|
23
|
+
if (!stack) return null;
|
|
24
|
+
for (const line of stack.split('\n').slice(1)) {
|
|
25
|
+
const match = line.match(STACK_FRAME);
|
|
26
|
+
if (!match) continue;
|
|
27
|
+
const [, functionName, rawFile, lineStr, columnStr] = match;
|
|
28
|
+
if (
|
|
29
|
+
rawFile.includes('/node_modules/') ||
|
|
30
|
+
rawFile.startsWith('node:') ||
|
|
31
|
+
rawFile === '<anonymous>'
|
|
32
|
+
) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const file = rawFile.startsWith('file://')
|
|
36
|
+
? fileURLToPath(rawFile)
|
|
37
|
+
: rawFile;
|
|
38
|
+
return {
|
|
39
|
+
functionName,
|
|
40
|
+
file,
|
|
41
|
+
line: Number(lineStr),
|
|
42
|
+
column: Number(columnStr),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function componentName(fiber) {
|
|
49
|
+
const owner = fiber._debugOwner;
|
|
50
|
+
const type = owner?.type;
|
|
51
|
+
if (type) return type.displayName || type.name || '(anonymous)';
|
|
52
|
+
return typeof fiber.type === 'string' ? `<${fiber.type}>` : '(unknown)';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Alt+Shift+Click: who-rendered-who, from the clicked element up to the
|
|
56
|
+
* root, each with its own source location — handy when the immediate owner
|
|
57
|
+
* isn't the component you meant (a shared wrapper, a list item, ...). */
|
|
58
|
+
function logOwnerChain(fiber) {
|
|
59
|
+
const chain = [];
|
|
60
|
+
for (let owner = fiber._debugOwner; owner; owner = owner._debugOwner) {
|
|
61
|
+
const location = resolveLocation(owner._debugStack);
|
|
62
|
+
const name = owner.type?.displayName || owner.type?.name || '(anonymous)';
|
|
63
|
+
chain.push(
|
|
64
|
+
location
|
|
65
|
+
? `${name} (${location.file}:${location.line}:${location.column})`
|
|
66
|
+
: name,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (chain.length > 0) {
|
|
70
|
+
console.log('[click-to-component] owner chain:\n ' + chain.join('\n '));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// GUI editors: navigate their registered URI scheme (same idea as
|
|
75
|
+
// https://github.com/sidorares/show-component, minus the browser) instead of
|
|
76
|
+
// spawning their CLI shim. The CLI wrapper (e.g. `cursor -g ...`) round-trips
|
|
77
|
+
// through a shell script that re-detects and re-execs the real app, which is
|
|
78
|
+
// visibly slower than the OS handing the URL straight to the already-running
|
|
79
|
+
// instance via Launch Services (macOS) / xdg-open (Linux).
|
|
80
|
+
const EDITOR_SCHEMES = {
|
|
81
|
+
cursor: 'cursor',
|
|
82
|
+
code: 'vscode',
|
|
83
|
+
vscode: 'vscode',
|
|
84
|
+
'code-insiders': 'vscode-insiders',
|
|
85
|
+
'vscode-insiders': 'vscode-insiders',
|
|
86
|
+
windsurf: 'windsurf',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Terminal editors have no URI scheme to navigate — spawn them directly.
|
|
90
|
+
const EDITOR_CLI = {
|
|
91
|
+
vim: (file, line, column) => [
|
|
92
|
+
'vim',
|
|
93
|
+
[`+call cursor(${line},${column})`, file],
|
|
94
|
+
],
|
|
95
|
+
nvim: (file, line, column) => [
|
|
96
|
+
'nvim',
|
|
97
|
+
[`+call cursor(${line},${column})`, file],
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const OPEN_URI_COMMAND = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
102
|
+
|
|
103
|
+
function editorUri(scheme, file, line, column) {
|
|
104
|
+
return `${scheme}://file${encodeURI(file)}:${line}:${column}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function openInEditor({ file, line, column }) {
|
|
108
|
+
const name = process.env.REACT_X11_EDITOR || 'cursor';
|
|
109
|
+
let bin, args;
|
|
110
|
+
if (EDITOR_CLI[name]) {
|
|
111
|
+
[bin, args] = EDITOR_CLI[name](file, line, column);
|
|
112
|
+
} else {
|
|
113
|
+
// an unrecognized name is assumed to be a URI scheme itself, so any
|
|
114
|
+
// editor that registers one works without a matching entry above
|
|
115
|
+
const scheme = EDITOR_SCHEMES[name] || name;
|
|
116
|
+
bin = OPEN_URI_COMMAND;
|
|
117
|
+
args = [editorUri(scheme, file, line, column)];
|
|
118
|
+
}
|
|
119
|
+
const child = spawn(bin, args, { detached: true, stdio: 'ignore' });
|
|
120
|
+
child.on('error', (err) => {
|
|
121
|
+
console.warn(
|
|
122
|
+
`react-x11: click-to-component could not launch "${bin}" (${err.code ?? err.message}). ` +
|
|
123
|
+
'Set REACT_X11_EDITOR to your editor CLI (cursor, code, code-insiders, windsurf, vim, nvim).',
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
child.unref();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Briefly tint the clicked node so there is visual confirmation of what was
|
|
130
|
+
* resolved, reusing the same overlay the DevTools hover highlight paints. */
|
|
131
|
+
function flashHighlight(node) {
|
|
132
|
+
const root = node?.root;
|
|
133
|
+
if (typeof root?.setHighlight !== 'function') return;
|
|
134
|
+
root.setHighlight(node);
|
|
135
|
+
setTimeout(() => root.setHighlight(null), 300);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleClick(node, native) {
|
|
139
|
+
const fiber = node?._reactFiber;
|
|
140
|
+
if (!fiber) {
|
|
141
|
+
console.warn(
|
|
142
|
+
'react-x11: click-to-component — this element has no fiber info.',
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const location = resolveLocation(fiber._debugStack);
|
|
147
|
+
if (!location) {
|
|
148
|
+
console.warn(
|
|
149
|
+
'react-x11: click-to-component — no source location found. This needs ' +
|
|
150
|
+
'React running in development mode (fiber._debugStack).',
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.log(
|
|
155
|
+
`[click-to-component] ${componentName(fiber)} → ` +
|
|
156
|
+
`${location.file}:${location.line}:${location.column}`,
|
|
157
|
+
);
|
|
158
|
+
if (native?.buttons & 1) {
|
|
159
|
+
// Alt+Shift+Click
|
|
160
|
+
logOwnerChain(fiber);
|
|
161
|
+
}
|
|
162
|
+
flashHighlight(node);
|
|
163
|
+
openInEditor(location);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The bit checked in events.js is X11's Mod1Mask either way — this is only
|
|
167
|
+
// about what the user actually has to press. XQuartz maps the Option key to
|
|
168
|
+
// Mod1 by default ("Option keys send Alt_L and Alt_R" in its Input
|
|
169
|
+
// preferences); everywhere else Mod1 is the physical Alt key.
|
|
170
|
+
const MODIFIER_LABEL = process.platform === 'darwin' ? 'Option' : 'Alt';
|
|
171
|
+
|
|
172
|
+
export function install() {
|
|
173
|
+
setClickToComponentHandler(handleClick);
|
|
174
|
+
console.log(
|
|
175
|
+
`react-x11: click-to-component enabled — ${MODIFIER_LABEL}+Click an ` +
|
|
176
|
+
`element to open its source (${MODIFIER_LABEL}+Shift+Click also logs ` +
|
|
177
|
+
'the owner chain).',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Opt-in React DevTools bridge. Enabled by setting REACT_X11_DEVTOOLS=1 in
|
|
2
|
+
// the environment; requires the `react-devtools-core` and `ws` dev
|
|
3
|
+
// dependencies. Start the standalone UI first (`npx react-devtools`), then
|
|
4
|
+
// run the app: REACT_X11_DEVTOOLS=1 npm run examples:dashboard
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
let api = null;
|
|
10
|
+
let connected = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Install the DevTools global hook. Must run before the first React commit
|
|
14
|
+
* so mounted roots are observed — Reconciler.js awaits this at module load
|
|
15
|
+
* when REACT_X11_DEVTOOLS is set.
|
|
16
|
+
*/
|
|
17
|
+
export async function prepare() {
|
|
18
|
+
try {
|
|
19
|
+
// react-devtools-core's backend bundle expects browser-ish globals
|
|
20
|
+
global.self ??= global;
|
|
21
|
+
global.window ??= global;
|
|
22
|
+
if (!global.WebSocket) {
|
|
23
|
+
global.WebSocket = (await import('ws')).default;
|
|
24
|
+
}
|
|
25
|
+
const mod = await import('react-devtools-core');
|
|
26
|
+
// v7 exposes the API on the default export under node ESM interop
|
|
27
|
+
api = mod.default?.initialize ? mod.default : mod;
|
|
28
|
+
api.initialize();
|
|
29
|
+
} catch (err) {
|
|
30
|
+
api = null;
|
|
31
|
+
console.warn(
|
|
32
|
+
'react-x11: REACT_X11_DEVTOOLS is set but devtools could not be loaded. ' +
|
|
33
|
+
'Install react-devtools-core and ws. Original error: ' +
|
|
34
|
+
err.message,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Connect the backend to the standalone DevTools app and register the
|
|
40
|
+
* renderer. Called by Reconciler.js right after the renderer is created. */
|
|
41
|
+
export function connect(renderer) {
|
|
42
|
+
if (!api || connected) return;
|
|
43
|
+
connected = true;
|
|
44
|
+
|
|
45
|
+
api.connectToDevTools({
|
|
46
|
+
isAppActive: () => true,
|
|
47
|
+
host: process.env.REACT_X11_DEVTOOLS_HOST || 'localhost',
|
|
48
|
+
port: Number(process.env.REACT_X11_DEVTOOLS_PORT) || 8097,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
renderer.injectIntoDevTools({
|
|
52
|
+
bundleType: 1,
|
|
53
|
+
version: require('../package.json').version,
|
|
54
|
+
rendererPackageName: 'react-x11',
|
|
55
|
+
findFiberByHostInstance: (instance) => instance._reactFiber,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
watchForAgent();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wire a DevTools backend agent's element-hover events to the renderer:
|
|
63
|
+
* hovering an element in the DevTools tree tints its rect in the window.
|
|
64
|
+
* Exported separately so it can be tested without react-devtools-core.
|
|
65
|
+
*/
|
|
66
|
+
export function attachHighlightAgent(agent) {
|
|
67
|
+
let highlightedRoot = null;
|
|
68
|
+
|
|
69
|
+
const show = (payload) => {
|
|
70
|
+
// newer devtools versions pass an array of public instances
|
|
71
|
+
const target = Array.isArray(payload) ? payload[0] : payload;
|
|
72
|
+
// public instances are ntk windows for <window>/<popup>, nodes otherwise
|
|
73
|
+
const node = target?._reactX11Node ?? target;
|
|
74
|
+
const root = node?.root;
|
|
75
|
+
if (!root || typeof root.setHighlight !== 'function') return;
|
|
76
|
+
if (highlightedRoot && highlightedRoot !== root) {
|
|
77
|
+
highlightedRoot.setHighlight(null);
|
|
78
|
+
}
|
|
79
|
+
highlightedRoot = root;
|
|
80
|
+
root.setHighlight(node);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const hide = () => {
|
|
84
|
+
highlightedRoot?.setHighlight(null);
|
|
85
|
+
highlightedRoot = null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
agent.addListener?.('showNativeHighlight', show);
|
|
89
|
+
agent.addListener?.('hideNativeHighlight', hide);
|
|
90
|
+
agent.addListener?.('shutdown', hide);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function watchForAgent() {
|
|
94
|
+
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
95
|
+
if (!hook) return;
|
|
96
|
+
try {
|
|
97
|
+
if (hook.reactDevtoolsAgent) {
|
|
98
|
+
attachHighlightAgent(hook.reactDevtoolsAgent);
|
|
99
|
+
} else if (typeof hook.on === 'function') {
|
|
100
|
+
// the backend emits 'react-devtools' once the agent is initialized
|
|
101
|
+
hook.on('react-devtools', (agent) => attachHighlightAgent(agent));
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// highlight support is best-effort; the tree view still works without it
|
|
105
|
+
}
|
|
106
|
+
}
|