react-x11 2.48.0 → 2.50.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/README.md CHANGED
@@ -446,14 +446,15 @@ To have it manage your real session instead — including replacing
446
446
  npm run examples:tasks:hot # then edit examples/tasks.jsx while it runs
447
447
  ```
448
448
 
449
- Runs the tasks example under the supported hot-reload entry point,
450
- `react-x11/refresh` (`node --import react-x11/refresh/register`, Node ≥
451
- 22.15), with React **Fast Refresh**: saving `examples/tasks.jsx` updates
452
- the edited components in place. The connection, the mounted window, and
453
- component state — the task list, even half-typed text in the input —
454
- survive the reload (a component whose hook signature changed remounts
455
- alone). Any app adopts it the same way — no accept handlers, no loader
456
- files to copy; see
449
+ Runs the tasks example under `react-x11 dev`, with React **Fast Refresh**
450
+ (Node ≥ 22.15): saving `examples/tasks.jsx` updates the edited components
451
+ in place. The connection, the mounted window, and component state — the
452
+ task list, even half-typed text in the input — survive the reload (a
453
+ component whose hook signature changed remounts alone). Any app adopts it
454
+ the same way, with no accept handlers and no entry file to write:
455
+ `npx react-x11 dev app.jsx` mounts the file's default export, in a window
456
+ of its own while the tree renders none, and offers to install the toolchain
457
+ on its first run; see
457
458
  [docs/ecosystem/dev-tooling.md](docs/ecosystem/dev-tooling.md#react-refresh).
458
459
 
459
460
  ## React DevTools
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.48.0",
3
+ "version": "2.50.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
+ "bin": {
7
+ "react-x11": "./src/cli/index.js"
8
+ },
6
9
  "files": [
7
10
  "src"
8
11
  ],
@@ -26,7 +29,7 @@
26
29
  "examples:xeyes": "tsx examples/xeyes.jsx",
27
30
  "examples:dashboard": "tsx examples/dashboard.jsx",
28
31
  "examples:tasks": "tsx examples/tasks.jsx",
29
- "examples:tasks:hot": "node --enable-source-maps --import react-x11/refresh/register examples/tasks-hot.jsx",
32
+ "examples:tasks:hot": "node src/cli/index.js dev examples/tasks.jsx",
30
33
  "examples:menu": "tsx examples/menu.jsx",
31
34
  "examples:notify": "tsx examples/notify.jsx",
32
35
  "examples:monitor": "tsx examples/monitor.jsx",
@@ -118,7 +121,7 @@
118
121
  "peerDependencies": {
119
122
  "@babel/core": "^8.0.0",
120
123
  "@babel/plugin-transform-react-jsx": "^8.0.0",
121
- "hot-module-replacement": "^4.0.1",
124
+ "hot-module-replacement": "^4.0.2",
122
125
  "react": "^19.3.0",
123
126
  "react-refresh": "^0.19.0"
124
127
  },
@@ -150,7 +153,7 @@
150
153
  "eslint": "^9.32.0",
151
154
  "eslint-plugin-react": "^7.37.5",
152
155
  "globals": "^17.8.0",
153
- "hot-module-replacement": "^4.0.1",
156
+ "hot-module-replacement": "^4.0.2",
154
157
  "katex": "^0.19.0",
155
158
  "prettier": "^3.6.0",
156
159
  "react": "^19.3.0",
@@ -10,40 +10,9 @@
10
10
  // loader both enable) has already rewritten that Error's stack to point at
11
11
  // original source — we only have to parse the stack text and skip the
12
12
  // frames inside React/the reconciler itself.
13
- import { spawn } from 'node:child_process';
14
- import { dirname, sep } from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
13
  import { setClickToComponentHandler } from './events.js';
17
14
  import { selectInDevTools } from './DevToolsIntegration.js';
18
-
19
- const STACK_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?\s*$/;
20
-
21
- // The frames to walk past before the JSX call site itself: React captures
22
- // the Error inside `jsxDEV`/`createElement`, and an element created by this
23
- // renderer (or by node internals) is never what a click means. `SELF_DIR` is
24
- // whichever copy of react-x11 is running — the repo's own `src/` for the
25
- // examples and tests, an installed `node_modules/react-x11/src` for an app.
26
- //
27
- // Everything *else* in `node_modules` is deliberately not skipped here: a
28
- // frame inside an installed package is a real call site, it is just not one
29
- // in the application's source, and telling the two apart is what lets
30
- // `resolveOwnedLocation` climb to the owner that is. Skipping every
31
- // `node_modules` frame instead would walk straight past the library
32
- // component and land on whatever application frame happens to be deeper in
33
- // the render stack — usually the `render()` call at startup, which looks
34
- // like an answer and isn't.
35
- const REACT_RUNTIME =
36
- /[\\/]node_modules[\\/](react|react-dom|react-reconciler|scheduler)[\\/]/;
37
- const SELF_DIR = dirname(fileURLToPath(import.meta.url)) + sep;
38
-
39
- function isInternalFrame(file) {
40
- return (
41
- file.startsWith('node:') ||
42
- file === '<anonymous>' ||
43
- file.startsWith(SELF_DIR) ||
44
- REACT_RUNTIME.test(file)
45
- );
46
- }
15
+ import { openInEditor, stackFrames } from './editor.js';
47
16
 
48
17
  /** The first stack frame outside React/the reconciler/node internals — the
49
18
  * JSX call site for whatever element this Error was captured at, wherever it
@@ -52,25 +21,16 @@ function isInternalFrame(file) {
52
21
  * Exported because `react-x11/test`'s `sourceOf` answers the same question
53
22
  * for a test that a click answers for an editor. */
54
23
  export function resolveLocation(debugStack) {
55
- const stack = debugStack?.stack;
56
- if (!stack) return null;
57
- for (const line of stack.split('\n').slice(1)) {
58
- const match = line.match(STACK_FRAME);
59
- if (!match) continue;
60
- const [, functionName, rawFile, lineStr, columnStr] = match;
61
- const file = rawFile.startsWith('file://')
62
- ? fileURLToPath(rawFile)
63
- : rawFile;
64
- if (isInternalFrame(file)) continue;
65
- return {
66
- functionName,
67
- file,
68
- line: Number(lineStr),
69
- column: Number(columnStr),
70
- installed: file.includes(`${sep}node_modules${sep}`),
71
- };
72
- }
73
- return null;
24
+ const frame = stackFrames(debugStack?.stack).find((f) => !f.internal);
25
+ if (!frame) return null;
26
+ const { functionName, file, line, column, installed } = frame;
27
+ return {
28
+ functionName: functionName ?? undefined,
29
+ file,
30
+ line,
31
+ column,
32
+ installed,
33
+ };
74
34
  }
75
35
 
76
36
  /** The nearest source location a click can mean: the clicked element's own
@@ -132,87 +92,6 @@ function logOwnerChain(fiber) {
132
92
  }
133
93
  }
134
94
 
135
- // GUI editors: navigate their registered URI scheme (same idea as
136
- // https://github.com/sidorares/show-component, minus the browser) instead of
137
- // spawning their CLI shim. The CLI wrapper (e.g. `cursor -g ...`) round-trips
138
- // through a shell script that re-detects and re-execs the real app, which is
139
- // visibly slower than the OS handing the URL straight to the already-running
140
- // instance via Launch Services (macOS) / xdg-open (Linux).
141
- const EDITOR_SCHEMES = {
142
- cursor: 'cursor',
143
- code: 'vscode',
144
- vscode: 'vscode',
145
- 'code-insiders': 'vscode-insiders',
146
- 'vscode-insiders': 'vscode-insiders',
147
- windsurf: 'windsurf',
148
- };
149
-
150
- // Terminal editors have no URI scheme to navigate — spawn them directly.
151
- const EDITOR_CLI = {
152
- vim: (file, line, column) => [
153
- 'vim',
154
- [`+call cursor(${line},${column})`, file],
155
- ],
156
- nvim: (file, line, column) => [
157
- 'nvim',
158
- [`+call cursor(${line},${column})`, file],
159
- ],
160
- };
161
-
162
- const OPEN_URI_COMMAND = process.platform === 'darwin' ? 'open' : 'xdg-open';
163
-
164
- function editorUri(scheme, file, line, column) {
165
- return `${scheme}://file${encodeURI(file)}:${line}:${column}`;
166
- }
167
-
168
- function openInEditor({ file, line, column }) {
169
- const name = process.env.REACT_X11_EDITOR || 'cursor';
170
- let bin, args;
171
- if (EDITOR_CLI[name]) {
172
- [bin, args] = EDITOR_CLI[name](file, line, column);
173
- } else {
174
- // an unrecognized name is assumed to be a URI scheme itself, so any
175
- // editor that registers one works without a matching entry above
176
- const scheme = EDITOR_SCHEMES[name] || name;
177
- bin = OPEN_URI_COMMAND;
178
- args = [editorUri(scheme, file, line, column)];
179
- }
180
- // stderr is kept (rather than the whole stdio ignored) for one reason:
181
- // `open`/`xdg-open` exit non-zero and print *there* when nothing claims
182
- // the scheme — an editor that isn't installed, or one whose URL handler
183
- // was never registered. Swallowing that is what makes click-to-component
184
- // look like it only logs: the location resolves, the console line prints,
185
- // and the editor silently never opens.
186
- const child = spawn(bin, args, {
187
- detached: true,
188
- stdio: ['ignore', 'ignore', 'pipe'],
189
- });
190
- let stderr = '';
191
- // ...and unref'd, so keeping it does not keep the process alive: a pipe is
192
- // a ref'd handle, and an editor that outlives the app it was launched from
193
- // is the normal case, not a reason to hold the event loop open.
194
- child.stderr.unref();
195
- child.stderr.on('data', (chunk) => {
196
- stderr += chunk;
197
- });
198
- child.on('error', (err) => {
199
- console.warn(
200
- `react-x11: click-to-component could not launch "${bin}" (${err.code ?? err.message}). ` +
201
- 'Set REACT_X11_EDITOR to your editor CLI (cursor, code, code-insiders, windsurf, vim, nvim).',
202
- );
203
- });
204
- child.on('exit', (code) => {
205
- if (!code) return;
206
- console.warn(
207
- `react-x11: click-to-component — \`${bin} ${args.join(' ')}\` exited ${code}. ` +
208
- (stderr.trim() ? `${stderr.trim()} ` : '') +
209
- `Set REACT_X11_EDITOR to the editor you actually use (currently "${name}"; ` +
210
- 'cursor, code, code-insiders, windsurf, vim, nvim, or any registered URI scheme).',
211
- );
212
- });
213
- child.unref();
214
- }
215
-
216
95
  /** Briefly tint the clicked node so there is visual confirmation of what was
217
96
  * resolved, reusing the same overlay the DevTools hover highlight paints. */
218
97
  function flashHighlight(node) {
package/src/Reconciler.js CHANGED
@@ -44,6 +44,7 @@ import { hasDropProps } from './dnd.js';
44
44
  import { AppProvider } from './appcontext.js';
45
45
  import {
46
46
  defaultRootHandlers,
47
+ NOT_IN_WINDOW,
47
48
  setErrorHandler,
48
49
  STRICT_TOKENS,
49
50
  } from './errors.js';
@@ -138,11 +139,13 @@ const ROOT_TYPES = new Set(['window', 'popup', THEME_SCOPE]);
138
139
  * `<box>` the app never wrote (#584).
139
140
  */
140
141
  function notAtRoot(type) {
141
- return new Error(
142
+ const err = new Error(
142
143
  `react-x11: <${type}> must be inside a <window> — it is at the root of ` +
143
144
  'the tree, which holds only <window> and <popup> elements. Render it ' +
144
145
  `inside one: <window><${type} …/></window>.`,
145
146
  );
147
+ err.code = NOT_IN_WINDOW;
148
+ return err;
146
149
  }
147
150
 
148
151
  const HostConfig = {