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
|
@@ -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/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,
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -42,7 +42,7 @@ import { now } from '../nodes/animation.js';
|
|
|
42
42
|
import { BoxNode } from '../nodes/box.js';
|
|
43
43
|
import { addDamageRect, damageToPaint } from '../nodes/damage.js';
|
|
44
44
|
import { intersectRects } from '../nodes/rects.js';
|
|
45
|
-
import { resolveBorderWidths } from '../styles.js';
|
|
45
|
+
import { radiusOf, resolveBorderWidths } from '../styles.js';
|
|
46
46
|
import {
|
|
47
47
|
LayerAnimations,
|
|
48
48
|
RASTER_PAD,
|
|
@@ -841,7 +841,7 @@ export class CocoaPromotion {
|
|
|
841
841
|
cut &&
|
|
842
842
|
!parent.isWindow &&
|
|
843
843
|
parent.clipsChildren?.() &&
|
|
844
|
-
!(parent.style
|
|
844
|
+
!(radiusOf(parent.style, parent.abs) > 0)
|
|
845
845
|
) {
|
|
846
846
|
const inner = insetRect(parent.abs, borderReach(parent));
|
|
847
847
|
// a clip with round corners cut again is no one box's shape: the
|
|
@@ -872,13 +872,15 @@ export class CocoaPromotion {
|
|
|
872
872
|
!cut.clip &&
|
|
873
873
|
!parent.isWindow &&
|
|
874
874
|
parent.clipsChildren?.() &&
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
875
|
+
radiusOf(parent.style, parent.abs) > 0 &&
|
|
876
|
+
!containsRect(
|
|
877
|
+
insetRect(parent.abs, radiusOf(parent.style, parent.abs)),
|
|
878
|
+
bounds,
|
|
879
|
+
)
|
|
878
880
|
) {
|
|
879
881
|
const border = borderReach(parent);
|
|
880
882
|
cut.clip = insetRect(parent.abs, border);
|
|
881
|
-
cut.radius = Math.max(0, parent.style.
|
|
883
|
+
cut.radius = Math.max(0, radiusOf(parent.style, parent.abs) - border);
|
|
882
884
|
roundCut = true;
|
|
883
885
|
bounds = intersectRects(bounds, cut.clip);
|
|
884
886
|
if (!bounds) {
|
|
@@ -906,11 +908,8 @@ export class CocoaPromotion {
|
|
|
906
908
|
return false;
|
|
907
909
|
}
|
|
908
910
|
if (parent.clipsChildren?.() && !roundCut) {
|
|
909
|
-
const radius = parent.style
|
|
910
|
-
const clip =
|
|
911
|
-
typeof radius === 'number' && radius > 0
|
|
912
|
-
? insetRect(parent.abs, radius)
|
|
913
|
-
: parent.abs;
|
|
911
|
+
const radius = radiusOf(parent.style, parent.abs);
|
|
912
|
+
const clip = radius > 0 ? insetRect(parent.abs, radius) : parent.abs;
|
|
914
913
|
if (!containsRect(clip, bounds)) return false;
|
|
915
914
|
}
|
|
916
915
|
}
|
package/src/cocoa/relaunch.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
// name: it runs before the entry is even loaded, and skips the checks.
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import { createRequire } from 'node:module';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
25
27
|
import { isMainThread, SHARE_ENV, Worker } from 'node:worker_threads';
|
|
26
28
|
|
|
27
29
|
import { loadNative } from './native.js';
|
|
@@ -156,6 +158,15 @@ const ENTRY_POINT = new Set([
|
|
|
156
158
|
// ts-node/esm`, `--experimental-loader ./hooks.mjs`.
|
|
157
159
|
const LOADER_FLAG = /^--(?:import|loader|experimental-loader)(?:=|$)/;
|
|
158
160
|
|
|
161
|
+
// The module Bun runs an entry from (`startedRunning`): one it writes
|
|
162
|
+
// itself, which imports the entry (measured, Bun 1.4).
|
|
163
|
+
const BUN_MAIN = 'bun:main';
|
|
164
|
+
|
|
165
|
+
// What Bun parses an entry as, by its extension: a .ts file may hold a
|
|
166
|
+
// `<T>x` cast, which TSX reads as an element, and JSX is allowed in a .js
|
|
167
|
+
// file, where the JSX reader takes any script a plain one would.
|
|
168
|
+
const SCAN_LOADER = { '.tsx': 'tsx', '.ts': 'ts', '.mts': 'ts', '.cts': 'ts' };
|
|
169
|
+
|
|
159
170
|
/**
|
|
160
171
|
* Why this import should not move the app onto a worker, or null when it
|
|
161
172
|
* should. A pure answer over what the process says about itself, so that
|
|
@@ -278,14 +289,14 @@ function compiledExecutable(bun) {
|
|
|
278
289
|
* and later evaluate an `import()` the entry makes before the loop's first
|
|
279
290
|
* turn, so an app that made one moved and ran its entry twice.
|
|
280
291
|
*
|
|
281
|
-
* Bun reports
|
|
282
|
-
*
|
|
283
|
-
*
|
|
292
|
+
* Bun reports no loop there, its `nodeTiming` a stand-in at 1 once
|
|
293
|
+
* `node:perf_hooks` is loaded, and leaves no frame below a module it
|
|
294
|
+
* evaluates, so it is read its own way (`bunStartedRunning`).
|
|
284
295
|
*/
|
|
285
296
|
function startedRunning(bun, moduleUrl, entry) {
|
|
286
297
|
if (!bun) return !evaluatedFromEntryPoint();
|
|
287
298
|
try {
|
|
288
|
-
return
|
|
299
|
+
return bunStartedRunning(bun, moduleUrl, entry);
|
|
289
300
|
} catch {
|
|
290
301
|
return false;
|
|
291
302
|
}
|
|
@@ -293,13 +304,106 @@ function startedRunning(bun, moduleUrl, entry) {
|
|
|
293
304
|
|
|
294
305
|
/** Whether this stack, async frames and all, comes from `ENTRY_POINT`. */
|
|
295
306
|
function evaluatedFromEntryPoint() {
|
|
307
|
+
const sites = callSites();
|
|
308
|
+
if (!sites) return false;
|
|
309
|
+
for (const site of sites) {
|
|
310
|
+
const file = site.getFileName() ?? '';
|
|
311
|
+
if (ENTRY_POINT.has(file)) return true;
|
|
312
|
+
// the app's own code awaits this import, so it has run already
|
|
313
|
+
if (site.isAsync() && !file.startsWith('node:')) return false;
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* `startedRunning` in Bun, read from three things it shows of the entry's
|
|
320
|
+
* run (each measured, Bun 1.4).
|
|
321
|
+
*
|
|
322
|
+
* Its module registry. Bun runs the entry from a module it writes itself,
|
|
323
|
+
* `bun:main`, which imports it, and `require.cache` lists an ES module once
|
|
324
|
+
* it has been evaluated: `bun:main` is there once the entry has finished,
|
|
325
|
+
* top-level awaits and all, and an import made after that is late. Before
|
|
326
|
+
* then the entry's own record says nothing either way. Bun marks a CommonJS
|
|
327
|
+
* module `loaded` before its body runs, which read as late from a CommonJS
|
|
328
|
+
* entry's first line, so it never moved; and an ES module has no record
|
|
329
|
+
* until it is evaluated, which read as not started for an entry awaiting
|
|
330
|
+
* its `import()` of react-x11, so it moved and ran a second time on the
|
|
331
|
+
* worker.
|
|
332
|
+
*
|
|
333
|
+
* The stack. Bun's loader evaluates an ES module's imports with no frame
|
|
334
|
+
* below the module, so a frame below react-x11 is code that loaded it
|
|
335
|
+
* synchronously. A CommonJS entry is in `require.cache` from its first line,
|
|
336
|
+
* and on its first run its own wrapper is the outermost frame. Bun runs the
|
|
337
|
+
* nextTicks and microtasks that first run queues before it evaluates
|
|
338
|
+
* `bun:main`: a callback there in another module reads as late, and one in
|
|
339
|
+
* the entry itself as its first run, unless in strict code it ends by
|
|
340
|
+
* returning its `require()`, which JavaScriptCore makes a tail call that
|
|
341
|
+
* leaves it no frame. For an ES module entry, a frame below react-x11 is a
|
|
342
|
+
* CommonJS module the entry imports, on its first run, or a `require()` in
|
|
343
|
+
* a module's body, and a bundle carries react-x11 inside the entry itself:
|
|
344
|
+
* each of them the start.
|
|
345
|
+
*
|
|
346
|
+
* The entry's static imports. An ES module's body runs once every module it
|
|
347
|
+
* imports statically has been evaluated: while one has not, the entry has
|
|
348
|
+
* not run a line and react-x11 is part of its graph, an `import()` that one
|
|
349
|
+
* of those modules awaits included; once all have, the entry's body is
|
|
350
|
+
* running or waiting on a top-level await, and asked for react-x11 itself.
|
|
351
|
+
* Read with Bun's own scanner and resolver, and what they cannot answer
|
|
352
|
+
* throws, which reads as the start.
|
|
353
|
+
*/
|
|
354
|
+
function bunStartedRunning(bun, moduleUrl, entry) {
|
|
355
|
+
const cache = createRequire(entry).cache;
|
|
356
|
+
// the entry has finished
|
|
357
|
+
if (BUN_MAIN in cache) return true;
|
|
358
|
+
const outer = outermostFile();
|
|
359
|
+
// a CommonJS entry: late unless on its first run
|
|
360
|
+
if (entry in cache) return outer !== undefined && outer !== entry;
|
|
361
|
+
let own = null;
|
|
362
|
+
try {
|
|
363
|
+
own = fileURLToPath(moduleUrl);
|
|
364
|
+
} catch {
|
|
365
|
+
// a CommonJS bundle has no import.meta.url
|
|
366
|
+
}
|
|
367
|
+
// react-x11 inside the entry, or loaded by code below it on the stack
|
|
368
|
+
if (own === entry || (outer !== undefined && outer !== own)) return false;
|
|
369
|
+
// reached by an import: late once the entry's body has started
|
|
370
|
+
const { imports } = new bun.Transpiler({
|
|
371
|
+
loader: SCAN_LOADER[path.extname(entry)] ?? 'jsx',
|
|
372
|
+
}).scan(fs.readFileSync(entry, 'utf8'));
|
|
373
|
+
const dir = path.dirname(entry);
|
|
374
|
+
return imports.every(
|
|
375
|
+
({ kind, path: specifier }) =>
|
|
376
|
+
kind !== 'import-statement' || bun.resolveSync(specifier, dir) in cache,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The file of the outermost frame on this stack that is not native code,
|
|
382
|
+
* or undefined where the stack cannot be read.
|
|
383
|
+
*/
|
|
384
|
+
function outermostFile() {
|
|
385
|
+
const sites = callSites();
|
|
386
|
+
if (!sites) return undefined;
|
|
387
|
+
for (let i = sites.length - 1; i >= 0; i--) {
|
|
388
|
+
if (!sites[i].isNative()) return sites[i].getFileName() ?? undefined;
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* This stack as call sites, every frame of it, or null where it cannot be
|
|
395
|
+
* read as call sites — an `Error` the app has frozen, say. Raises the stack
|
|
396
|
+
* trace limit and sets a formatter for as long as it takes, and puts back
|
|
397
|
+
* the app's own.
|
|
398
|
+
*/
|
|
399
|
+
function callSites() {
|
|
296
400
|
const { prepareStackTrace, stackTraceLimit } = Error;
|
|
297
401
|
let sites;
|
|
298
402
|
try {
|
|
299
403
|
// Reflect.set: an Error the app has frozen is not a reason to throw
|
|
300
404
|
// from its import of react-x11
|
|
301
405
|
Reflect.set(Error, 'stackTraceLimit', Infinity);
|
|
302
|
-
Reflect.set(Error, 'prepareStackTrace', (_,
|
|
406
|
+
Reflect.set(Error, 'prepareStackTrace', (_, frames) => frames);
|
|
303
407
|
const held = {};
|
|
304
408
|
Error.captureStackTrace(held);
|
|
305
409
|
sites = held.stack;
|
|
@@ -307,14 +411,7 @@ function evaluatedFromEntryPoint() {
|
|
|
307
411
|
Reflect.set(Error, 'prepareStackTrace', prepareStackTrace);
|
|
308
412
|
Reflect.set(Error, 'stackTraceLimit', stackTraceLimit);
|
|
309
413
|
}
|
|
310
|
-
|
|
311
|
-
for (const site of sites) {
|
|
312
|
-
const file = site.getFileName() ?? '';
|
|
313
|
-
if (ENTRY_POINT.has(file)) return true;
|
|
314
|
-
// the app's own code awaits this import, so it has run already
|
|
315
|
-
if (site.isAsync() && !file.startsWith('node:')) return false;
|
|
316
|
-
}
|
|
317
|
-
return false;
|
|
414
|
+
return Array.isArray(sites) ? sites : null;
|
|
318
415
|
}
|
|
319
416
|
|
|
320
417
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// CSS's `currentColor`: the colour keyword that is not a colour yet.
|
|
2
|
+
//
|
|
3
|
+
// A leaf, like decorations.js, because every layer that reads a colour asks
|
|
4
|
+
// about it — the grammar (styles.js, shorthands.js, decorations.js) to let
|
|
5
|
+
// it through, and the paint (nodes/boxpaint.js and every backend's reader
|
|
6
|
+
// of a style colour) to resolve it.
|
|
7
|
+
//
|
|
8
|
+
// It stays the keyword in `node.style` — CSS's computed value — and becomes
|
|
9
|
+
// a colour only where something is about to paint it, against the node's
|
|
10
|
+
// resolved ink (`Node.resolveColor`). Resolving it once in the style funnel
|
|
11
|
+
// instead would bake the ink into the style object, which no inherited
|
|
12
|
+
// change reaches: a `:hover { color }` on a row moves the ink of every node
|
|
13
|
+
// under it without restyling any of them.
|
|
14
|
+
|
|
15
|
+
/** `currentColor`, spelled any way CSS accepts — its keywords are ASCII
|
|
16
|
+
* case-insensitive — and with the whitespace a style value may carry. */
|
|
17
|
+
const KEYWORD = /^\s*currentcolor\s*$/i;
|
|
18
|
+
const MENTION = /currentcolor/i;
|
|
19
|
+
|
|
20
|
+
/** Whether `value` is the keyword. Asked per colour per paint, so a value
|
|
21
|
+
* too short to be it is answered without the match. */
|
|
22
|
+
export const isCurrentColor = (value) =>
|
|
23
|
+
typeof value === 'string' && value.length >= 12 && KEYWORD.test(value);
|
|
24
|
+
|
|
25
|
+
/** Whether a value made of several — a gradient, a shadow list — names the
|
|
26
|
+
* keyword anywhere in it. */
|
|
27
|
+
export const mentionsCurrentColor = (value) =>
|
|
28
|
+
typeof value === 'string' && value.length >= 12 && MENTION.test(value);
|