react-x11 2.49.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/package.json +3 -3
- package/src/ClickToComponent.js +11 -132
- package/src/cli/dev.js +94 -19
- package/src/cli/errorwindow.js +368 -0
- package/src/cocoa/presenter.js +3 -1
- package/src/cocoa/promotion.js +10 -11
- package/src/cocoa/relaunch.js +110 -13
- package/src/currentcolor.js +28 -0
- package/src/decorations.js +21 -9
- package/src/editor.js +151 -0
- package/src/foreignnodes.js +8 -1
- package/src/gloverlay.js +2 -1
- package/src/node.d.ts +5 -0
- package/src/nodes/animation.js +31 -3
- package/src/nodes/boxpaint.js +34 -8
- package/src/nodes/cascade.js +50 -3
- package/src/nodes/damage.js +2 -0
- package/src/nodes/layout.js +9 -17
- package/src/nodes/moveblit.js +5 -5
- package/src/nodes/node.js +8 -0
- package/src/nodes/paint.js +4 -2
- package/src/nodes/queries.js +46 -34
- package/src/nodes/scrollblit.js +10 -7
- package/src/nodes/styling.js +176 -21
- package/src/nodes/window/capabilities.js +1 -1
- package/src/nodes/window/floors.js +63 -18
- package/src/nodes/window/size.js +94 -19
- package/src/refresh/loader.d.ts +16 -0
- package/src/refresh/loader.js +58 -8
- package/src/shorthands.js +1266 -0
- package/src/styles.js +370 -131
- package/src/types/style.d.ts +163 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.50.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"peerDependencies": {
|
|
122
122
|
"@babel/core": "^8.0.0",
|
|
123
123
|
"@babel/plugin-transform-react-jsx": "^8.0.0",
|
|
124
|
-
"hot-module-replacement": "^4.0.
|
|
124
|
+
"hot-module-replacement": "^4.0.2",
|
|
125
125
|
"react": "^19.3.0",
|
|
126
126
|
"react-refresh": "^0.19.0"
|
|
127
127
|
},
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"eslint": "^9.32.0",
|
|
154
154
|
"eslint-plugin-react": "^7.37.5",
|
|
155
155
|
"globals": "^17.8.0",
|
|
156
|
-
"hot-module-replacement": "^4.0.
|
|
156
|
+
"hot-module-replacement": "^4.0.2",
|
|
157
157
|
"katex": "^0.19.0",
|
|
158
158
|
"prettier": "^3.6.0",
|
|
159
159
|
"react": "^19.3.0",
|
package/src/ClickToComponent.js
CHANGED
|
@@ -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
|
|
56
|
-
if (!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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/cli/dev.js
CHANGED
|
@@ -125,7 +125,7 @@ export async function runDev(file, options = {}) {
|
|
|
125
125
|
// Before anything below loads: a module's source maps are read when it
|
|
126
126
|
// loads, and only once this is on.
|
|
127
127
|
process.setSourceMapsEnabled(true);
|
|
128
|
-
const { registerRefresh } = await import('../refresh/loader.js');
|
|
128
|
+
const { onTransform, registerRefresh } = await import('../refresh/loader.js');
|
|
129
129
|
await registerRefresh();
|
|
130
130
|
// The runtime patches React DevTools' global hook and then hands the
|
|
131
131
|
// renderer to it, which has to happen before the first root commits.
|
|
@@ -135,6 +135,7 @@ export async function runDev(file, options = {}) {
|
|
|
135
135
|
const ReactX11 = await import('../index.js');
|
|
136
136
|
const { NOT_IN_WINDOW } = await import('../errors.js');
|
|
137
137
|
const { liveApps } = await import('../trace-registry.js');
|
|
138
|
+
const { ErrorWindow, describeError } = await import('./errorwindow.js');
|
|
138
139
|
|
|
139
140
|
// What `node file.jsx …args` would hand the app.
|
|
140
141
|
process.argv = [process.argv[0], path, ...args];
|
|
@@ -210,19 +211,55 @@ export async function runDev(file, options = {}) {
|
|
|
210
211
|
const title = `${basename(path)} — react-x11 dev`;
|
|
211
212
|
let wrapped = false;
|
|
212
213
|
let mixedNoted = false;
|
|
214
|
+
// What the error window shows: a render error took the app's tree down,
|
|
215
|
+
// and the app stays down until the next reload tries it again; a compile
|
|
216
|
+
// error is a save that did not compile, beside an app still running the
|
|
217
|
+
// module's last version. Both at once shows the compile error, which is
|
|
218
|
+
// the newer news.
|
|
219
|
+
let renderFailure = null;
|
|
220
|
+
let compileFailure = null;
|
|
221
|
+
// Set when a reload starts while a render error is showing: that reload is
|
|
222
|
+
// the one to try the app again on. Not the reload that threw — react-refresh
|
|
223
|
+
// calls the reload listeners in the same batch as the render that failed,
|
|
224
|
+
// and retrying there would only throw it a second time.
|
|
225
|
+
let retry = false;
|
|
213
226
|
let root = null;
|
|
227
|
+
// Two children, always in these places: an error window opening or
|
|
228
|
+
// closing beside the app is then a sibling coming and going, and the app's
|
|
229
|
+
// state is untouched by it.
|
|
214
230
|
const tree = () => {
|
|
215
231
|
const app = React.createElement(App);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
const failure = compileFailure ?? renderFailure;
|
|
233
|
+
return React.createElement(
|
|
234
|
+
React.Fragment,
|
|
235
|
+
null,
|
|
236
|
+
renderFailure
|
|
237
|
+
? null
|
|
238
|
+
: wrapped
|
|
239
|
+
? React.createElement('window', { ...DEV_WINDOW, title }, app)
|
|
240
|
+
: app,
|
|
241
|
+
failure
|
|
242
|
+
? React.createElement(ErrorWindow, {
|
|
243
|
+
failure,
|
|
244
|
+
title: `${basename(failure.filename ?? path)} — error`,
|
|
245
|
+
// Beside a running app it is a second window, whose closing is
|
|
246
|
+
// ours to answer: it goes until the next error. Standing in for
|
|
247
|
+
// the app it is the only one, and closing it ends the app.
|
|
248
|
+
onCloseRequest: renderFailure
|
|
249
|
+
? undefined
|
|
250
|
+
: () => {
|
|
251
|
+
compileFailure = null;
|
|
252
|
+
render();
|
|
253
|
+
},
|
|
254
|
+
})
|
|
255
|
+
: null,
|
|
256
|
+
);
|
|
219
257
|
};
|
|
220
258
|
const render = () => root.render(tree());
|
|
221
|
-
const mounted = () => (root.app._rootChildren ?? []).length > 0;
|
|
222
259
|
|
|
223
260
|
root = await (createRoot ?? ReactX11.createRoot)({
|
|
224
261
|
onUncaughtError(error, info) {
|
|
225
|
-
if (error?.code === NOT_IN_WINDOW && !wrapped) {
|
|
262
|
+
if (error?.code === NOT_IN_WINDOW && !wrapped && !renderFailure) {
|
|
226
263
|
wrapped = true;
|
|
227
264
|
log(
|
|
228
265
|
`${name} renders no <window>, so it is shown in one of react-x11 ` +
|
|
@@ -241,27 +278,65 @@ export async function runDev(file, options = {}) {
|
|
|
241
278
|
);
|
|
242
279
|
return;
|
|
243
280
|
}
|
|
244
|
-
console.error(
|
|
245
|
-
|
|
281
|
+
console.error(error);
|
|
282
|
+
const chain = describeError({
|
|
283
|
+
kind: 'render',
|
|
246
284
|
error,
|
|
285
|
+
componentStack: info?.componentStack,
|
|
286
|
+
}).components;
|
|
287
|
+
// Already showing one: this came out of the error window itself, or
|
|
288
|
+
// the tree failed again before a reload — the terminal has it, and a
|
|
289
|
+
// window answering its own failure by mounting itself again would
|
|
290
|
+
// never stop.
|
|
291
|
+
if (renderFailure) return;
|
|
292
|
+
log(
|
|
293
|
+
(chain.length > 0 ? `thrown rendering ${chain.join(' › ')}. ` : '') +
|
|
294
|
+
`Fix it and save: ${name} is mounted again on the next reload.`,
|
|
247
295
|
);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
296
|
+
renderFailure = {
|
|
297
|
+
kind: 'render',
|
|
298
|
+
error,
|
|
299
|
+
componentStack: info?.componentStack,
|
|
300
|
+
};
|
|
301
|
+
setImmediate(render);
|
|
253
302
|
},
|
|
254
303
|
});
|
|
255
304
|
|
|
256
|
-
//
|
|
305
|
+
// A save that does not compile: the reload library prints it and keeps the
|
|
306
|
+
// module's last version, so this is the only place it can be shown. Before
|
|
307
|
+
// the mount, the import of the file itself throws, and the bin prints it.
|
|
308
|
+
//
|
|
309
|
+
// Every reload compiles at least the module it stops at, so this is also
|
|
310
|
+
// where a reload is first seen.
|
|
311
|
+
onTransform(({ filename, error }) => {
|
|
312
|
+
if (error) {
|
|
313
|
+
compileFailure = { kind: 'compile', error, filename };
|
|
314
|
+
setImmediate(render);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (renderFailure) retry = true;
|
|
318
|
+
if (compileFailure?.filename === filename) {
|
|
319
|
+
compileFailure = null;
|
|
320
|
+
setImmediate(render);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// After a reload: try an app an error took down again, and take the dev
|
|
257
325
|
// window away from a tree that now brings its own. The other direction
|
|
258
326
|
// needs nothing here — a tree that lost its window throws NOT_IN_WINDOW,
|
|
259
|
-
// and the handler above wraps it
|
|
260
|
-
// down: react-refresh keeps the roots that failed, and renders them again
|
|
261
|
-
// on the next reload itself.
|
|
327
|
+
// and the handler above wraps it.
|
|
262
328
|
const settle = () => {
|
|
263
|
-
if (
|
|
264
|
-
|
|
329
|
+
if (renderFailure) {
|
|
330
|
+
if (!retry) return;
|
|
331
|
+
retry = false;
|
|
332
|
+
renderFailure = null;
|
|
333
|
+
render();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (!wrapped) return;
|
|
337
|
+
const devWindow = (root.app._rootChildren ?? [])[0];
|
|
338
|
+
if (!devWindow) return;
|
|
339
|
+
const { windows, drawn } = rootContent(devWindow);
|
|
265
340
|
if (windows > 0 && drawn === 0) {
|
|
266
341
|
wrapped = false;
|
|
267
342
|
mixedNoted = false;
|