react-x11 2.49.0 → 2.51.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 +5 -1
- package/package.json +4 -4
- package/src/ClickToComponent.js +11 -132
- package/src/DevToolsIntegration.js +126 -42
- package/src/Reconciler.js +26 -10
- package/src/cli/dev.js +162 -19
- package/src/cli/errorwindow.js +368 -0
- package/src/cli/index.js +4 -0
- package/src/cocoa/app.js +22 -0
- package/src/cocoa/presenter.js +3 -1
- package/src/cocoa/promotion.js +10 -11
- package/src/cocoa/relaunch.js +114 -15
- package/src/cocoa/window.js +98 -15
- 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/scrollable.js +4 -1
- 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/elements.d.ts +5 -2
- package/src/types/style.d.ts +163 -39
- package/src/types/system.d.ts +4 -1
package/src/cli/dev.js
CHANGED
|
@@ -27,12 +27,26 @@
|
|
|
27
27
|
// missing, names them with the versions the loader is written against, and
|
|
28
28
|
// installs them with the project's own package manager when the developer
|
|
29
29
|
// says yes. Without a terminal to ask on it prints the command instead.
|
|
30
|
+
// - **It turns click-to-component on.** Alt+Click (Option+Click on macOS)
|
|
31
|
+
// opens the JSX line an element came from (docs/click-to-component.md):
|
|
32
|
+
// a development tool, and this is the command a developer runs, so it is
|
|
33
|
+
// on unless REACT_X11_CLICK_TO_COMPONENT=0 says otherwise. Set here, in
|
|
34
|
+
// the environment, because that is where the renderer reads it when the
|
|
35
|
+
// first root is made — and where a toolchain restart inherits it from.
|
|
36
|
+
// - **It connects to React DevTools** whenever `npx react-devtools` is
|
|
37
|
+
// running, before or after the app started, the way a browser with the
|
|
38
|
+
// extension does: on by default where react-devtools-core is installed,
|
|
39
|
+
// unless REACT_X11_DEVTOOLS=0. Where it is not, and a DevTools UI is
|
|
40
|
+
// listening at start, it says what to install. Prepared before React
|
|
41
|
+
// Refresh loads, because Refresh installs a stub hook where it finds none,
|
|
42
|
+
// and the backend installs none where it finds one.
|
|
30
43
|
// - **It runs the project's react-x11.** A second copy of the renderer is a
|
|
31
44
|
// second React root nobody refreshes, so a global or npx-cached
|
|
32
45
|
// `react-x11` hands over to the copy the file itself imports.
|
|
33
46
|
import { spawnSync } from 'node:child_process';
|
|
34
47
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
35
48
|
import * as nodeModule from 'node:module';
|
|
49
|
+
import { connect } from 'node:net';
|
|
36
50
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
37
51
|
import { createInterface } from 'node:readline/promises';
|
|
38
52
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -122,10 +136,22 @@ export async function runDev(file, options = {}) {
|
|
|
122
136
|
return null;
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
process.env.REACT_X11_CLICK_TO_COMPONENT ??= '1';
|
|
140
|
+
if (process.env.REACT_X11_DEVTOOLS === undefined) {
|
|
141
|
+
if (resolves('react-devtools-core')) {
|
|
142
|
+
process.env.REACT_X11_DEVTOOLS = '1';
|
|
143
|
+
} else {
|
|
144
|
+
suggestDevTools({ projectDir, log });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (envFlag(process.env.REACT_X11_DEVTOOLS)) {
|
|
148
|
+
await (await import('../DevToolsIntegration.js')).prepare();
|
|
149
|
+
}
|
|
150
|
+
|
|
125
151
|
// Before anything below loads: a module's source maps are read when it
|
|
126
152
|
// loads, and only once this is on.
|
|
127
153
|
process.setSourceMapsEnabled(true);
|
|
128
|
-
const { registerRefresh } = await import('../refresh/loader.js');
|
|
154
|
+
const { onTransform, registerRefresh } = await import('../refresh/loader.js');
|
|
129
155
|
await registerRefresh();
|
|
130
156
|
// The runtime patches React DevTools' global hook and then hands the
|
|
131
157
|
// renderer to it, which has to happen before the first root commits.
|
|
@@ -135,6 +161,7 @@ export async function runDev(file, options = {}) {
|
|
|
135
161
|
const ReactX11 = await import('../index.js');
|
|
136
162
|
const { NOT_IN_WINDOW } = await import('../errors.js');
|
|
137
163
|
const { liveApps } = await import('../trace-registry.js');
|
|
164
|
+
const { ErrorWindow, describeError } = await import('./errorwindow.js');
|
|
138
165
|
|
|
139
166
|
// What `node file.jsx …args` would hand the app.
|
|
140
167
|
process.argv = [process.argv[0], path, ...args];
|
|
@@ -210,19 +237,55 @@ export async function runDev(file, options = {}) {
|
|
|
210
237
|
const title = `${basename(path)} — react-x11 dev`;
|
|
211
238
|
let wrapped = false;
|
|
212
239
|
let mixedNoted = false;
|
|
240
|
+
// What the error window shows: a render error took the app's tree down,
|
|
241
|
+
// and the app stays down until the next reload tries it again; a compile
|
|
242
|
+
// error is a save that did not compile, beside an app still running the
|
|
243
|
+
// module's last version. Both at once shows the compile error, which is
|
|
244
|
+
// the newer news.
|
|
245
|
+
let renderFailure = null;
|
|
246
|
+
let compileFailure = null;
|
|
247
|
+
// Set when a reload starts while a render error is showing: that reload is
|
|
248
|
+
// the one to try the app again on. Not the reload that threw — react-refresh
|
|
249
|
+
// calls the reload listeners in the same batch as the render that failed,
|
|
250
|
+
// and retrying there would only throw it a second time.
|
|
251
|
+
let retry = false;
|
|
213
252
|
let root = null;
|
|
253
|
+
// Two children, always in these places: an error window opening or
|
|
254
|
+
// closing beside the app is then a sibling coming and going, and the app's
|
|
255
|
+
// state is untouched by it.
|
|
214
256
|
const tree = () => {
|
|
215
257
|
const app = React.createElement(App);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
258
|
+
const failure = compileFailure ?? renderFailure;
|
|
259
|
+
return React.createElement(
|
|
260
|
+
React.Fragment,
|
|
261
|
+
null,
|
|
262
|
+
renderFailure
|
|
263
|
+
? null
|
|
264
|
+
: wrapped
|
|
265
|
+
? React.createElement('window', { ...DEV_WINDOW, title }, app)
|
|
266
|
+
: app,
|
|
267
|
+
failure
|
|
268
|
+
? React.createElement(ErrorWindow, {
|
|
269
|
+
failure,
|
|
270
|
+
title: `${basename(failure.filename ?? path)} — error`,
|
|
271
|
+
// Beside a running app it is a second window, whose closing is
|
|
272
|
+
// ours to answer: it goes until the next error. Standing in for
|
|
273
|
+
// the app it is the only one, and closing it ends the app.
|
|
274
|
+
onCloseRequest: renderFailure
|
|
275
|
+
? undefined
|
|
276
|
+
: () => {
|
|
277
|
+
compileFailure = null;
|
|
278
|
+
render();
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
: null,
|
|
282
|
+
);
|
|
219
283
|
};
|
|
220
284
|
const render = () => root.render(tree());
|
|
221
|
-
const mounted = () => (root.app._rootChildren ?? []).length > 0;
|
|
222
285
|
|
|
223
286
|
root = await (createRoot ?? ReactX11.createRoot)({
|
|
224
287
|
onUncaughtError(error, info) {
|
|
225
|
-
if (error?.code === NOT_IN_WINDOW && !wrapped) {
|
|
288
|
+
if (error?.code === NOT_IN_WINDOW && !wrapped && !renderFailure) {
|
|
226
289
|
wrapped = true;
|
|
227
290
|
log(
|
|
228
291
|
`${name} renders no <window>, so it is shown in one of react-x11 ` +
|
|
@@ -241,27 +304,65 @@ export async function runDev(file, options = {}) {
|
|
|
241
304
|
);
|
|
242
305
|
return;
|
|
243
306
|
}
|
|
244
|
-
console.error(
|
|
245
|
-
|
|
307
|
+
console.error(error);
|
|
308
|
+
const chain = describeError({
|
|
309
|
+
kind: 'render',
|
|
246
310
|
error,
|
|
311
|
+
componentStack: info?.componentStack,
|
|
312
|
+
}).components;
|
|
313
|
+
// Already showing one: this came out of the error window itself, or
|
|
314
|
+
// the tree failed again before a reload — the terminal has it, and a
|
|
315
|
+
// window answering its own failure by mounting itself again would
|
|
316
|
+
// never stop.
|
|
317
|
+
if (renderFailure) return;
|
|
318
|
+
log(
|
|
319
|
+
(chain.length > 0 ? `thrown rendering ${chain.join(' › ')}. ` : '') +
|
|
320
|
+
`Fix it and save: ${name} is mounted again on the next reload.`,
|
|
247
321
|
);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
322
|
+
renderFailure = {
|
|
323
|
+
kind: 'render',
|
|
324
|
+
error,
|
|
325
|
+
componentStack: info?.componentStack,
|
|
326
|
+
};
|
|
327
|
+
setImmediate(render);
|
|
253
328
|
},
|
|
254
329
|
});
|
|
255
330
|
|
|
256
|
-
//
|
|
331
|
+
// A save that does not compile: the reload library prints it and keeps the
|
|
332
|
+
// module's last version, so this is the only place it can be shown. Before
|
|
333
|
+
// the mount, the import of the file itself throws, and the bin prints it.
|
|
334
|
+
//
|
|
335
|
+
// Every reload compiles at least the module it stops at, so this is also
|
|
336
|
+
// where a reload is first seen.
|
|
337
|
+
onTransform(({ filename, error }) => {
|
|
338
|
+
if (error) {
|
|
339
|
+
compileFailure = { kind: 'compile', error, filename };
|
|
340
|
+
setImmediate(render);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (renderFailure) retry = true;
|
|
344
|
+
if (compileFailure?.filename === filename) {
|
|
345
|
+
compileFailure = null;
|
|
346
|
+
setImmediate(render);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// After a reload: try an app an error took down again, and take the dev
|
|
257
351
|
// window away from a tree that now brings its own. The other direction
|
|
258
352
|
// 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.
|
|
353
|
+
// and the handler above wraps it.
|
|
262
354
|
const settle = () => {
|
|
263
|
-
if (
|
|
264
|
-
|
|
355
|
+
if (renderFailure) {
|
|
356
|
+
if (!retry) return;
|
|
357
|
+
retry = false;
|
|
358
|
+
renderFailure = null;
|
|
359
|
+
render();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!wrapped) return;
|
|
363
|
+
const devWindow = (root.app._rootChildren ?? [])[0];
|
|
364
|
+
if (!devWindow) return;
|
|
365
|
+
const { windows, drawn } = rootContent(devWindow);
|
|
265
366
|
if (windows > 0 && drawn === 0) {
|
|
266
367
|
wrapped = false;
|
|
267
368
|
mixedNoted = false;
|
|
@@ -311,6 +412,48 @@ function rootContent(node) {
|
|
|
311
412
|
return { windows, drawn };
|
|
312
413
|
}
|
|
313
414
|
|
|
415
|
+
/** Whether `specifier` resolves from react-x11, which is where it is
|
|
416
|
+
* imported from. */
|
|
417
|
+
function resolves(specifier) {
|
|
418
|
+
try {
|
|
419
|
+
import.meta.resolve(specifier);
|
|
420
|
+
return true;
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The renderer's rule for a switch: set, and not `0`. */
|
|
427
|
+
function envFlag(value) {
|
|
428
|
+
return Boolean(value) && value !== '0';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Where react-devtools-core is missing: if a DevTools UI is listening (one
|
|
433
|
+
* TCP connect, in the background, never holding the process), say what to
|
|
434
|
+
* install for the app to show up in it. Silent otherwise — no DevTools
|
|
435
|
+
* running is the usual case, not one to print about.
|
|
436
|
+
*/
|
|
437
|
+
function suggestDevTools({ projectDir, log }) {
|
|
438
|
+
const host = process.env.REACT_X11_DEVTOOLS_HOST || 'localhost';
|
|
439
|
+
const port = Number(process.env.REACT_X11_DEVTOOLS_PORT) || 8097;
|
|
440
|
+
const socket = connect({ host, port });
|
|
441
|
+
socket.unref();
|
|
442
|
+
socket.setTimeout(1000, () => socket.destroy());
|
|
443
|
+
socket.on('error', () => {});
|
|
444
|
+
socket.on('connect', () => {
|
|
445
|
+
socket.destroy();
|
|
446
|
+
const line = shellLine(
|
|
447
|
+
installCommand(packageManagerFor(projectDir), ['react-devtools-core']),
|
|
448
|
+
);
|
|
449
|
+
log(
|
|
450
|
+
`React DevTools is listening on ${host}:${port}, and this project ` +
|
|
451
|
+
'cannot connect to it without react-devtools-core. Install it, and ' +
|
|
452
|
+
`the next run shows up there:\n\n ${line}\n`,
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
314
457
|
/** The react-x11 package `path` imports, or null when it imports none. */
|
|
315
458
|
export function reactX11For(path) {
|
|
316
459
|
let entry;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// The window `react-x11 dev` shows an error in: what was thrown, the source
|
|
2
|
+
// around where, and the frames of the stack, each of which opens its line in
|
|
3
|
+
// the editor (src/editor.js, the one click-to-component uses).
|
|
4
|
+
//
|
|
5
|
+
// Two kinds of error reach it. A **render** error takes the app's tree down —
|
|
6
|
+
// React unmounts a root whose render threw and nothing caught — so this
|
|
7
|
+
// window stands in for the app until the next save mounts it again. A
|
|
8
|
+
// **compile** error is a save that did not compile: the app keeps running the
|
|
9
|
+
// module's last version, and this window opens beside it until a save
|
|
10
|
+
// compiles.
|
|
11
|
+
//
|
|
12
|
+
// Everything the window says comes out of `describeError`, which is pure, so
|
|
13
|
+
// the tests can read it without rendering anything.
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { relative } from 'node:path';
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { editorName, openInEditor, stackFrames } from '../editor.js';
|
|
18
|
+
|
|
19
|
+
const h = React.createElement;
|
|
20
|
+
|
|
21
|
+
/** Lines of source shown above and below the one that threw. */
|
|
22
|
+
const CONTEXT_ABOVE = 3;
|
|
23
|
+
const CONTEXT_BELOW = 2;
|
|
24
|
+
|
|
25
|
+
function firstLine(text) {
|
|
26
|
+
const at = text.indexOf('\n');
|
|
27
|
+
return at === -1 ? text : text.slice(0, at);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The source lines around `line` (1-based) of `file`: `{ number, text, at }`
|
|
32
|
+
* each, `at` marking the one that threw. Null when the file cannot be read.
|
|
33
|
+
*/
|
|
34
|
+
function excerptOf(file, line) {
|
|
35
|
+
let source;
|
|
36
|
+
try {
|
|
37
|
+
source = readFileSync(file, 'utf8');
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const lines = source.split('\n');
|
|
42
|
+
if (line < 1 || line > lines.length) return null;
|
|
43
|
+
const from = Math.max(1, line - CONTEXT_ABOVE);
|
|
44
|
+
const to = Math.min(lines.length, line + CONTEXT_BELOW);
|
|
45
|
+
const excerpt = [];
|
|
46
|
+
for (let number = from; number <= to; number++) {
|
|
47
|
+
excerpt.push({
|
|
48
|
+
number,
|
|
49
|
+
// a tab is as wide as its font says, and two spaces is what an editor
|
|
50
|
+
// with a two-space indent shows
|
|
51
|
+
text: lines[number - 1].replace(/\t/g, ' ').replace(/\r$/, ''),
|
|
52
|
+
at: number === line,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return excerpt;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Everything the window says about one failure, as data.
|
|
60
|
+
*
|
|
61
|
+
* `failure` is `{ kind, error, filename?, componentStack? }`: `kind` is
|
|
62
|
+
* `'render'` or `'compile'`, `filename` the module that did not compile, and
|
|
63
|
+
* `componentStack` what React handed `onUncaughtError`.
|
|
64
|
+
*/
|
|
65
|
+
export function describeError(failure, cwd = process.cwd()) {
|
|
66
|
+
const { kind, error, filename = null, componentStack = '' } = failure;
|
|
67
|
+
const rel = (file) => relative(cwd, file) || file;
|
|
68
|
+
const message = String(error?.message ?? error);
|
|
69
|
+
const name = error?.name && error.name !== 'Error' ? error.name : 'Error';
|
|
70
|
+
|
|
71
|
+
let heading;
|
|
72
|
+
let detail = '';
|
|
73
|
+
let location = null;
|
|
74
|
+
if (kind === 'compile') {
|
|
75
|
+
// babel says `<file>: <what> (<line>:<column>)` and then draws a frame;
|
|
76
|
+
// the window draws its own from `loc`, when there is one
|
|
77
|
+
let what = firstLine(message);
|
|
78
|
+
if (filename && what.startsWith(`${filename}: `)) {
|
|
79
|
+
what = what.slice(filename.length + 2);
|
|
80
|
+
}
|
|
81
|
+
what = what.replace(/ \(\d+:\d+\)$/, '');
|
|
82
|
+
heading = `${name}: ${what}`;
|
|
83
|
+
if (filename && error?.loc?.line) {
|
|
84
|
+
location = {
|
|
85
|
+
file: filename,
|
|
86
|
+
line: error.loc.line,
|
|
87
|
+
column: (error.loc.column ?? 0) + 1,
|
|
88
|
+
};
|
|
89
|
+
} else {
|
|
90
|
+
detail = message.slice(firstLine(message).length).replace(/^\n+/, '');
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
heading = `${name}: ${firstLine(message)}`;
|
|
94
|
+
detail = message.slice(firstLine(message).length).replace(/^\n+/, '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A compile error's stack is the compiler's: where in babel the parse
|
|
98
|
+
// gave up, which says nothing about the app.
|
|
99
|
+
const all = kind === 'compile' ? [] : stackFrames(error?.stack);
|
|
100
|
+
const shown = all.filter((frame) => !frame.internal);
|
|
101
|
+
if (!location) {
|
|
102
|
+
const own = shown.find((frame) => !frame.installed) ?? shown[0];
|
|
103
|
+
if (own) {
|
|
104
|
+
location = { file: own.file, line: own.line, column: own.column };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// React's component stack, innermost first: `at Broken (…)`. Host
|
|
109
|
+
// elements are in it too; the breadcrumb is the components.
|
|
110
|
+
const components = [];
|
|
111
|
+
for (const text of String(componentStack).split('\n')) {
|
|
112
|
+
const match = text.match(/^\s*at ([A-Z][\w$.]*)/);
|
|
113
|
+
if (match) components.push(match[1]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
kind,
|
|
118
|
+
heading,
|
|
119
|
+
detail,
|
|
120
|
+
location: location && {
|
|
121
|
+
...location,
|
|
122
|
+
where: `${rel(location.file)}:${location.line}:${location.column}`,
|
|
123
|
+
},
|
|
124
|
+
excerpt: location ? excerptOf(location.file, location.line) : null,
|
|
125
|
+
frames: shown.map((frame) => ({
|
|
126
|
+
label: frame.functionName ?? '(anonymous)',
|
|
127
|
+
where: `${rel(frame.file)}:${frame.line}:${frame.column}`,
|
|
128
|
+
file: frame.file,
|
|
129
|
+
line: frame.line,
|
|
130
|
+
column: frame.column,
|
|
131
|
+
own: !frame.installed,
|
|
132
|
+
})),
|
|
133
|
+
hidden: all.length - shown.length,
|
|
134
|
+
components: components.reverse(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const mono = { fontFamily: '$monoFamily', fontSize: 12 };
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A row that opens a source line: drawn pressed on the press and hovered
|
|
142
|
+
* under the pointer, because a click that opens another application takes a
|
|
143
|
+
* moment to show anything of its own.
|
|
144
|
+
*/
|
|
145
|
+
function SourceLink({ target, children }) {
|
|
146
|
+
const open = () => openInEditor(target);
|
|
147
|
+
return h(
|
|
148
|
+
'box',
|
|
149
|
+
{
|
|
150
|
+
focusable: true,
|
|
151
|
+
role: 'link',
|
|
152
|
+
onClick: open,
|
|
153
|
+
onKeyDown: (ev) => {
|
|
154
|
+
if (ev.key === 'Enter') open();
|
|
155
|
+
},
|
|
156
|
+
style: {
|
|
157
|
+
flexDirection: 'row',
|
|
158
|
+
gap: 10,
|
|
159
|
+
paddingTop: 3,
|
|
160
|
+
paddingBottom: 3,
|
|
161
|
+
paddingStart: 6,
|
|
162
|
+
paddingEnd: 6,
|
|
163
|
+
borderRadius: '$radiusSmall',
|
|
164
|
+
cursor: 'pointer',
|
|
165
|
+
':hover': { backgroundColor: '$surfaceHover' },
|
|
166
|
+
':active': { backgroundColor: '$surfaceActive' },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
children,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function Excerpt({ excerpt, column }) {
|
|
174
|
+
const width = String(excerpt[excerpt.length - 1].number).length;
|
|
175
|
+
const rows = [];
|
|
176
|
+
for (const { number, text, at } of excerpt) {
|
|
177
|
+
rows.push(
|
|
178
|
+
h(
|
|
179
|
+
'box',
|
|
180
|
+
{ key: number, style: { flexDirection: 'row' } },
|
|
181
|
+
h(
|
|
182
|
+
'text',
|
|
183
|
+
{
|
|
184
|
+
style: {
|
|
185
|
+
...mono,
|
|
186
|
+
color: at ? '$danger' : '$textMuted',
|
|
187
|
+
textWrap: 'nowrap',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
`${at ? '>' : ' '} ${String(number).padStart(width)} │ `,
|
|
191
|
+
),
|
|
192
|
+
h(
|
|
193
|
+
'text',
|
|
194
|
+
{
|
|
195
|
+
style: {
|
|
196
|
+
...mono,
|
|
197
|
+
color: at ? '$text' : '$textMuted',
|
|
198
|
+
textWrap: 'nowrap',
|
|
199
|
+
flexShrink: 1,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
text || ' ',
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
if (at && column > 0) {
|
|
207
|
+
rows.push(
|
|
208
|
+
h(
|
|
209
|
+
'text',
|
|
210
|
+
{
|
|
211
|
+
key: 'caret',
|
|
212
|
+
style: { ...mono, color: '$danger', textWrap: 'nowrap' },
|
|
213
|
+
},
|
|
214
|
+
`${' '.repeat(width + 5 + column - 1)}^`,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return h(
|
|
220
|
+
'box',
|
|
221
|
+
{
|
|
222
|
+
style: {
|
|
223
|
+
backgroundColor: '$surfaceHover',
|
|
224
|
+
borderRadius: '$radius',
|
|
225
|
+
paddingTop: 10,
|
|
226
|
+
paddingBottom: 10,
|
|
227
|
+
paddingStart: 12,
|
|
228
|
+
paddingEnd: 12,
|
|
229
|
+
overflow: 'hidden',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
rows,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The window itself. `onCloseRequest` is the host's to give: without one,
|
|
238
|
+
* closing the only window there is ends the app, as closing an app's primary
|
|
239
|
+
* window does.
|
|
240
|
+
*/
|
|
241
|
+
export function ErrorWindow({ failure, title, onCloseRequest }) {
|
|
242
|
+
const info = describeError(failure);
|
|
243
|
+
const muted = { color: '$textMuted', fontSize: 12 };
|
|
244
|
+
const editor = editorName();
|
|
245
|
+
const children = [
|
|
246
|
+
h(
|
|
247
|
+
'text',
|
|
248
|
+
{ key: 'kind', style: { ...muted, fontWeight: 600 } },
|
|
249
|
+
info.kind === 'compile'
|
|
250
|
+
? 'This save did not compile. The app keeps running the last version that did.'
|
|
251
|
+
: 'The app threw while rendering, and React took its tree down.',
|
|
252
|
+
),
|
|
253
|
+
h(
|
|
254
|
+
'text',
|
|
255
|
+
{
|
|
256
|
+
key: 'heading',
|
|
257
|
+
style: { color: '$danger', fontSize: 16, fontWeight: 600 },
|
|
258
|
+
},
|
|
259
|
+
info.heading,
|
|
260
|
+
),
|
|
261
|
+
];
|
|
262
|
+
if (info.components.length > 0) {
|
|
263
|
+
children.push(
|
|
264
|
+
h(
|
|
265
|
+
'text',
|
|
266
|
+
{ key: 'components', style: muted },
|
|
267
|
+
info.components.join(' › '),
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (info.detail) {
|
|
272
|
+
children.push(
|
|
273
|
+
h(
|
|
274
|
+
'text',
|
|
275
|
+
{ key: 'detail', style: { ...mono, color: '$text' } },
|
|
276
|
+
info.detail,
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (info.location) {
|
|
281
|
+
children.push(
|
|
282
|
+
h(
|
|
283
|
+
'box',
|
|
284
|
+
{ key: 'source', style: { gap: 6 } },
|
|
285
|
+
h(
|
|
286
|
+
SourceLink,
|
|
287
|
+
{ target: info.location },
|
|
288
|
+
h(
|
|
289
|
+
'text',
|
|
290
|
+
{ style: { ...mono, color: '$link' } },
|
|
291
|
+
info.location.where,
|
|
292
|
+
),
|
|
293
|
+
),
|
|
294
|
+
info.excerpt
|
|
295
|
+
? h(Excerpt, { excerpt: info.excerpt, column: info.location.column })
|
|
296
|
+
: null,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (info.frames.length > 0) {
|
|
301
|
+
children.push(
|
|
302
|
+
h(
|
|
303
|
+
'box',
|
|
304
|
+
{ key: 'frames', style: { gap: 1 } },
|
|
305
|
+
h(
|
|
306
|
+
'text',
|
|
307
|
+
{ style: { ...muted, fontWeight: 600, marginBottom: 4 } },
|
|
308
|
+
'Stack',
|
|
309
|
+
),
|
|
310
|
+
info.frames.map((frame, i) =>
|
|
311
|
+
h(
|
|
312
|
+
SourceLink,
|
|
313
|
+
{ key: i, target: frame },
|
|
314
|
+
h(
|
|
315
|
+
'text',
|
|
316
|
+
{
|
|
317
|
+
style: {
|
|
318
|
+
...mono,
|
|
319
|
+
color: frame.own ? '$text' : '$textMuted',
|
|
320
|
+
fontWeight: frame.own ? 600 : 400,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
frame.label,
|
|
324
|
+
),
|
|
325
|
+
h(
|
|
326
|
+
'text',
|
|
327
|
+
{ style: { ...mono, color: '$textMuted', flexShrink: 1 } },
|
|
328
|
+
frame.where,
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
),
|
|
332
|
+
info.hidden > 0
|
|
333
|
+
? h(
|
|
334
|
+
'text',
|
|
335
|
+
{ style: { ...muted, marginTop: 4, marginLeft: 6 } },
|
|
336
|
+
`${info.hidden} more ${info.hidden === 1 ? 'frame' : 'frames'} in React, Node and react-x11, not shown.`,
|
|
337
|
+
)
|
|
338
|
+
: null,
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
children.push(
|
|
343
|
+
h(
|
|
344
|
+
'text',
|
|
345
|
+
{ key: 'footer', style: { ...muted, marginTop: 4 } },
|
|
346
|
+
(info.kind === 'compile'
|
|
347
|
+
? 'Fix it and save: this window goes once the file compiles. '
|
|
348
|
+
: 'Fix it and save: the app is mounted again on the next reload. ') +
|
|
349
|
+
`Click a line to open it in ${editor}` +
|
|
350
|
+
(process.env.REACT_X11_EDITOR
|
|
351
|
+
? '.'
|
|
352
|
+
: '; REACT_X11_EDITOR names another editor.'),
|
|
353
|
+
),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return h(
|
|
357
|
+
'window',
|
|
358
|
+
{ title, width: 760, minHeight: 240, onCloseRequest },
|
|
359
|
+
h(
|
|
360
|
+
'box',
|
|
361
|
+
{
|
|
362
|
+
selectable: true,
|
|
363
|
+
style: { flexGrow: 1, padding: 20, gap: 14, overflow: 'scroll' },
|
|
364
|
+
},
|
|
365
|
+
children,
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -17,6 +17,10 @@ Commands:
|
|
|
17
17
|
export is mounted, inside a window of its own when it
|
|
18
18
|
renders none; a file that mounts its own root runs as it is.
|
|
19
19
|
Arguments after the file are the app's.
|
|
20
|
+
Alt+Click (Option+Click on macOS) an element to open its
|
|
21
|
+
source; REACT_X11_CLICK_TO_COMPONENT=0 turns that off.
|
|
22
|
+
Connects to React DevTools (npx react-devtools) whenever
|
|
23
|
+
it runs; REACT_X11_DEVTOOLS=0 turns that off.
|
|
20
24
|
|
|
21
25
|
Options:
|
|
22
26
|
-h, --help Show this help.
|
package/src/cocoa/app.js
CHANGED
|
@@ -1447,6 +1447,9 @@ export class CocoaApp {
|
|
|
1447
1447
|
return this._animationEnds.get(ev.id)?.(ev);
|
|
1448
1448
|
case 'window-live-resize':
|
|
1449
1449
|
return this._routeLiveResize(ev);
|
|
1450
|
+
case 'window-fullscreen':
|
|
1451
|
+
case 'window-miniaturize':
|
|
1452
|
+
return this._routeWindowState(ev);
|
|
1450
1453
|
case 'window-created':
|
|
1451
1454
|
// a worker's window, made on the UI thread after the call that
|
|
1452
1455
|
// answered its handle
|
|
@@ -1611,6 +1614,8 @@ export class CocoaApp {
|
|
|
1611
1614
|
// this is the only thing the bridge says about it (`_recheckScreens`).
|
|
1612
1615
|
this._recheckScreens();
|
|
1613
1616
|
wnd._nativeResized(ev);
|
|
1617
|
+
// AppKit says nothing of a zoom but the resize it makes
|
|
1618
|
+
if (ev.type === 'window-resize') wnd._refreshStates();
|
|
1614
1619
|
wnd.emit('resize', {
|
|
1615
1620
|
width: wnd.width,
|
|
1616
1621
|
height: wnd.height,
|
|
@@ -1676,6 +1681,22 @@ export class CocoaApp {
|
|
|
1676
1681
|
wnd.emit('liveresize', { live: wnd.liveResizing });
|
|
1677
1682
|
}
|
|
1678
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* `window-fullscreen { fullscreen, phase }` and `window-miniaturize`
|
|
1686
|
+
* (@windowkit/appkit 0.30.0): the title bar's green button, the Dock, and
|
|
1687
|
+
* the same asked for through `setWmState`. The bridge has published the
|
|
1688
|
+
* state already — a full-screen transition's target from its `begin`,
|
|
1689
|
+
* before the animation, which is the moment an app takes its chrome away
|
|
1690
|
+
* — so the window reads it back and says `statechange` if it moved. A
|
|
1691
|
+
* user's own act, so it is input: its render goes out like a click's.
|
|
1692
|
+
*/
|
|
1693
|
+
_routeWindowState(ev) {
|
|
1694
|
+
const wnd = this._window(ev);
|
|
1695
|
+
if (!wnd || wnd.destroyed) return;
|
|
1696
|
+
wnd._refreshStates();
|
|
1697
|
+
this._afterInput();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1679
1700
|
/**
|
|
1680
1701
|
* `surface-released`: a worker's frame took an IOSurface off a layer and
|
|
1681
1702
|
* the frame that replaced it has committed (windowkit/appkit#52). The
|
|
@@ -1759,6 +1780,7 @@ export class CocoaApp {
|
|
|
1759
1780
|
const wnd = this._window(ev);
|
|
1760
1781
|
if (!wnd) return;
|
|
1761
1782
|
if (name === 'focus') this._lastKeyWindow = wnd;
|
|
1783
|
+
wnd._refreshStates();
|
|
1762
1784
|
wnd.emit(name, { buttons: 0, time: ev.time });
|
|
1763
1785
|
this._afterInput();
|
|
1764
1786
|
}
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -502,7 +502,9 @@ export function propBoxProps(node, app, scale, parentOrigin, order) {
|
|
|
502
502
|
const style = node.style ?? {};
|
|
503
503
|
const border = typeof style.borderWidth === 'number' ? style.borderWidth : 0;
|
|
504
504
|
const colour = (value) =>
|
|
505
|
-
value
|
|
505
|
+
value
|
|
506
|
+
? (app._parseColor(String(node.resolveColor(value))) ?? [0, 0, 0, 0])
|
|
507
|
+
: [0, 0, 0, 0];
|
|
506
508
|
return {
|
|
507
509
|
frame: [
|
|
508
510
|
(abs.x - parentOrigin.x) / scale,
|