react-x11 2.2.0 → 2.2.1
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 +1 -1
- package/src/Reconciler.js +20 -2
- package/src/errors.js +42 -0
- package/src/nodes.js +58 -4
- package/src/style.d.ts +4 -1
- package/src/styles.js +43 -19
package/package.json
CHANGED
package/src/Reconciler.js
CHANGED
|
@@ -34,7 +34,11 @@ import {
|
|
|
34
34
|
} from './nodes.js';
|
|
35
35
|
import { hasDropProps } from './dnd.js';
|
|
36
36
|
import { AppProvider } from './appcontext.js';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
defaultRootHandlers,
|
|
39
|
+
setErrorHandler,
|
|
40
|
+
STRICT_TOKENS,
|
|
41
|
+
} from './errors.js';
|
|
38
42
|
import {
|
|
39
43
|
registerApp,
|
|
40
44
|
unregisterApp,
|
|
@@ -303,15 +307,29 @@ const HostConfig = {
|
|
|
303
307
|
// and trapFocus need commitMount too — the node has to be in the tree
|
|
304
308
|
// first, so it can find the EventManager that owns focus. Drop targets
|
|
305
309
|
// likewise: registration needs the root, which insertion assigns.
|
|
310
|
+
//
|
|
311
|
+
// Under REACT_X11_STRICT_TOKENS every token-styled node asks for one as
|
|
312
|
+
// well, since a bad token is only *found* once the node is attached —
|
|
313
|
+
// which is after this ran — and commitMount is the first moment React
|
|
314
|
+
// holds that node's own fiber (nodes.js `_tokenProblem`). Gated on the
|
|
315
|
+
// flag so the default mount pays nothing for a debugging mode.
|
|
306
316
|
return (
|
|
307
317
|
type === 'popup' ||
|
|
308
318
|
Boolean(props.autoFocus) ||
|
|
309
319
|
Boolean(props.trapFocus) ||
|
|
310
|
-
hasDropProps(props)
|
|
320
|
+
hasDropProps(props) ||
|
|
321
|
+
(STRICT_TOKENS && instance._usesTokens)
|
|
311
322
|
);
|
|
312
323
|
},
|
|
313
324
|
|
|
314
325
|
commitMount(instance, type, props) {
|
|
326
|
+
// first, and before any of the work below: the tree is on its way out.
|
|
327
|
+
// `false` afterwards marks this instance's one commitMount spent, so a
|
|
328
|
+
// later re-attach throws at once rather than deferring to a call that
|
|
329
|
+
// will never come (nodes.js `_tokenProblem`).
|
|
330
|
+
const tokenError = instance._tokenError;
|
|
331
|
+
instance._tokenError = false;
|
|
332
|
+
if (tokenError) throw tokenError;
|
|
315
333
|
if (type === 'popup') {
|
|
316
334
|
instance.realize(null);
|
|
317
335
|
}
|
package/src/errors.js
CHANGED
|
@@ -59,6 +59,48 @@ export function reportHandlerError(node, handler, error) {
|
|
|
59
59
|
markFailed();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* `REACT_X11_STRICT_TOKENS=1` makes a `$token` the theme does not define
|
|
64
|
+
* fatal again, for a build that would rather stop than paint something
|
|
65
|
+
* wrong. The default reports and carries on — see `reportStyleError`.
|
|
66
|
+
*
|
|
67
|
+
* Guarded rather than a bare `process.env` because the playground bundle
|
|
68
|
+
* runs in a browser, where there is no `process` at all.
|
|
69
|
+
*/
|
|
70
|
+
export const STRICT_TOKENS =
|
|
71
|
+
(typeof process === 'undefined'
|
|
72
|
+
? undefined
|
|
73
|
+
: process.env?.REACT_X11_STRICT_TOKENS) === '1';
|
|
74
|
+
|
|
75
|
+
/** Messages already printed, so a shared misspelled style reports once per
|
|
76
|
+
* (node, message) rather than once per restyle — a theme swap re-resolves
|
|
77
|
+
* the whole subtree and would otherwise print the same line every time. */
|
|
78
|
+
const reportedStyleErrors = new WeakMap();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A style the node cannot resolve — today only an unknown `$token`.
|
|
82
|
+
*
|
|
83
|
+
* Not a throw, and deliberately: the mistake is one property in one style,
|
|
84
|
+
* and the tree it would take down is the whole GUI. The property is dropped
|
|
85
|
+
* (so the widget paints without it, visibly wrong), the message names the
|
|
86
|
+
* token and whose element wore it, and `process.exitCode` is set so a test
|
|
87
|
+
* run or a supervisor still counts this as a failure. `REACT_X11_STRICT_TOKENS=1`
|
|
88
|
+
* restores the throw.
|
|
89
|
+
*/
|
|
90
|
+
export function reportStyleError(node, message) {
|
|
91
|
+
const seen = reportedStyleErrors.get(node);
|
|
92
|
+
if (seen?.has(message)) return;
|
|
93
|
+
if (seen) seen.add(message);
|
|
94
|
+
else reportedStyleErrors.set(node, new Set([message]));
|
|
95
|
+
const owner = ownerName(node);
|
|
96
|
+
console.error(
|
|
97
|
+
`${message}${owner ? ` — in ${owner}` : ''}. ` +
|
|
98
|
+
'The property is dropped and the app carries on; set ' +
|
|
99
|
+
'REACT_X11_STRICT_TOKENS=1 to make this throw instead.',
|
|
100
|
+
);
|
|
101
|
+
markFailed();
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
/** Wrap a call to user code so a throw is reported instead of escaping. */
|
|
63
105
|
export function callHandler(node, handler, fn, ev) {
|
|
64
106
|
try {
|
package/src/nodes.js
CHANGED
|
@@ -109,7 +109,12 @@ import {
|
|
|
109
109
|
windowOrigin,
|
|
110
110
|
} from './anchor.js';
|
|
111
111
|
import { baseTheme } from './palette.js';
|
|
112
|
-
import {
|
|
112
|
+
import {
|
|
113
|
+
callHandler,
|
|
114
|
+
ownerName,
|
|
115
|
+
reportStyleError,
|
|
116
|
+
STRICT_TOKENS,
|
|
117
|
+
} from './errors.js';
|
|
113
118
|
import {
|
|
114
119
|
hooks as a11yHooks,
|
|
115
120
|
isFocusable as a11yFocusable,
|
|
@@ -1557,6 +1562,11 @@ export class Node {
|
|
|
1557
1562
|
// subtree's hit reach, invalidated through _clearHitBounds()
|
|
1558
1563
|
this._paintOrderCache = null;
|
|
1559
1564
|
this._hitBoundsCache = null;
|
|
1565
|
+
// a `$token` the theme does not define, held for `commitMount` to throw
|
|
1566
|
+
// on this node's own fiber — see `_tokenProblem`. Strict mode only.
|
|
1567
|
+
// `null` is "commitMount is still to come", `false` is "it has been and
|
|
1568
|
+
// gone", and an Error is one waiting for it
|
|
1569
|
+
this._tokenError = null;
|
|
1560
1570
|
this._syncStyle(props);
|
|
1561
1571
|
this.yoga = yoga ? createLayoutNode() : null;
|
|
1562
1572
|
if (this.yoga) {
|
|
@@ -1588,7 +1598,7 @@ export class Node {
|
|
|
1588
1598
|
* them. `baseStyle` is the flattened `style` prop; `style` is that with
|
|
1589
1599
|
* the active state blocks overlaid.
|
|
1590
1600
|
*/
|
|
1591
|
-
_syncStyle(props) {
|
|
1601
|
+
_syncStyle(props, mounting = false) {
|
|
1592
1602
|
if (DEV && this.stylable) {
|
|
1593
1603
|
assertNoFlatStyleProps(props, this.kind, this.semanticNames);
|
|
1594
1604
|
validateStyle(flattenStyle(props.style), `<${this.kind} style>`);
|
|
@@ -1597,12 +1607,16 @@ export class Node {
|
|
|
1597
1607
|
this._usesTokens = this.stylable && styleUsesTokens(this._baseStyle);
|
|
1598
1608
|
if (this._usesTokens) {
|
|
1599
1609
|
const theme = this.theme;
|
|
1610
|
+
const strict = this.placed;
|
|
1611
|
+
const problems = strict ? [] : null;
|
|
1600
1612
|
this._baseStyle = resolveTokens(
|
|
1601
1613
|
this._baseStyle,
|
|
1602
1614
|
theme,
|
|
1603
1615
|
`<${this.kind} style>`,
|
|
1604
|
-
|
|
1616
|
+
strict,
|
|
1617
|
+
problems,
|
|
1605
1618
|
);
|
|
1619
|
+
if (problems?.length) this._tokenProblem(problems, mounting);
|
|
1606
1620
|
}
|
|
1607
1621
|
// `disabled` is a prop, not something the pointer does, so it is read
|
|
1608
1622
|
// straight off props rather than driven by the event manager
|
|
@@ -2241,6 +2255,46 @@ export class Node {
|
|
|
2241
2255
|
return owner.isPopup ? owner.parent != null : true;
|
|
2242
2256
|
}
|
|
2243
2257
|
|
|
2258
|
+
/**
|
|
2259
|
+
* A `$token` this node's completed ancestry does not define.
|
|
2260
|
+
*
|
|
2261
|
+
* The default is `reportStyleError`: say so loudly, set `process.exitCode`,
|
|
2262
|
+
* and keep the property dropped. `REACT_X11_STRICT_TOKENS=1` makes it fatal
|
|
2263
|
+
* again, and then *where* the throw lands is the whole question — an error
|
|
2264
|
+
* boundary only catches what React invoked, on the fiber React thinks it
|
|
2265
|
+
* is working on.
|
|
2266
|
+
*
|
|
2267
|
+
* `mounting` is the attach walk, which runs inside `appendInitialChild`
|
|
2268
|
+
* while React is completing the nearest host *ancestor* — the `<window>`,
|
|
2269
|
+
* for a whole tree rendered at once. A throw there is attributed to the
|
|
2270
|
+
* window and sails past every boundary the app wrote inside it, which is
|
|
2271
|
+
* the bug this deferral exists for (#420). Stashed instead, and thrown
|
|
2272
|
+
* from `commitMount` on this node's own fiber, where the walk up finds a
|
|
2273
|
+
* boundary at any depth.
|
|
2274
|
+
*
|
|
2275
|
+
* Every other caller already has the right fiber (`commitUpdate`) or has
|
|
2276
|
+
* no React on the stack at all (`appearanceChanged`, from an X event) —
|
|
2277
|
+
* for those, throwing here is both the earliest and the only option, and
|
|
2278
|
+
* the second is the crash strict mode asked for.
|
|
2279
|
+
*
|
|
2280
|
+
* `commitMount` happens once per instance, so a node re-attached after it
|
|
2281
|
+
* has been and gone has nothing left to defer *to*; stashing there would
|
|
2282
|
+
* swallow the error instead of raising it late. Those throw at once, like
|
|
2283
|
+
* the keyed reorder they resemble.
|
|
2284
|
+
*/
|
|
2285
|
+
_tokenProblem(problems, mounting) {
|
|
2286
|
+
if (!STRICT_TOKENS) {
|
|
2287
|
+
// every one of them: two misspellings in a style are two things to
|
|
2288
|
+
// fix, and a report that named only the first would send someone back
|
|
2289
|
+
// for a second run to find the second
|
|
2290
|
+
for (const message of problems) reportStyleError(this, message);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
const error = new Error(problems[0]);
|
|
2294
|
+
if (mounting && this._tokenError === null) this._tokenError = error;
|
|
2295
|
+
else throw error;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2244
2298
|
/** The owning window resized: re-resolve, since a query block may now
|
|
2245
2299
|
* match that did not, or the other way round. */
|
|
2246
2300
|
_sizeQueriesChanged() {
|
|
@@ -2287,7 +2341,7 @@ export class Node {
|
|
|
2287
2341
|
if (this.isWindow) this._syncWindowBackground();
|
|
2288
2342
|
if (this._usesTokens) {
|
|
2289
2343
|
const before = this.style;
|
|
2290
|
-
this._syncStyle(this.props);
|
|
2344
|
+
this._syncStyle(this.props, mounting);
|
|
2291
2345
|
// a token change reaches the node without React re-rendering it, so
|
|
2292
2346
|
// the invalidation a commit would have done has to happen here too
|
|
2293
2347
|
if (localTextStyleChanged(this.style, before)) {
|
package/src/style.d.ts
CHANGED
|
@@ -43,12 +43,15 @@ export function tokenNames(
|
|
|
43
43
|
style: StyleProperties,
|
|
44
44
|
out?: Set<string>,
|
|
45
45
|
): Set<string>;
|
|
46
|
-
/** Replace `$token` references with values from the theme.
|
|
46
|
+
/** Replace `$token` references with values from the theme. A token the theme
|
|
47
|
+
* does not define is dropped either way; with `strict`, the message naming
|
|
48
|
+
* it is pushed onto `problems` for the caller to report or throw. */
|
|
47
49
|
export function resolveTokens(
|
|
48
50
|
style: StyleProperties,
|
|
49
51
|
theme: Record<string, unknown> | null | undefined,
|
|
50
52
|
where?: string,
|
|
51
53
|
strict?: boolean,
|
|
54
|
+
problems?: string[] | null,
|
|
52
55
|
): StyleProperties;
|
|
53
56
|
|
|
54
57
|
export function styleHasSizeQueries(style: StyleProperties): boolean;
|
package/src/styles.js
CHANGED
|
@@ -1206,16 +1206,37 @@ export function stripTokens(style) {
|
|
|
1206
1206
|
* `strict` says the node's ancestry is complete, so a token that does not
|
|
1207
1207
|
* resolve is a mistake. While a subtree is still being built its nodes can
|
|
1208
1208
|
* see only part of their ancestry — the theme two levels up does not exist
|
|
1209
|
-
* for them yet — so resolution there is provisional
|
|
1210
|
-
*
|
|
1209
|
+
* for them yet — so resolution there is provisional.
|
|
1210
|
+
*
|
|
1211
|
+
* Both cases drop the property, because a value is either resolved or absent
|
|
1212
|
+
* and `'$textMuted1'` is not a colour. What `strict` changes is whether
|
|
1213
|
+
* anyone hears about it: mistakes are pushed onto `problems` and the caller
|
|
1214
|
+
* decides what one costs. Resolving itself never throws — it runs from a
|
|
1215
|
+
* commit and from an X event alike, and only the caller knows whether React
|
|
1216
|
+
* is on the stack to route a throw to a boundary (src/nodes.js).
|
|
1217
|
+
*
|
|
1218
|
+
* A cache hit replays the problems it recorded, so the second node to wear a
|
|
1219
|
+
* misspelled shared style is reported like the first.
|
|
1211
1220
|
*/
|
|
1212
|
-
export function resolveTokens(
|
|
1221
|
+
export function resolveTokens(
|
|
1222
|
+
style,
|
|
1223
|
+
theme,
|
|
1224
|
+
where = 'style',
|
|
1225
|
+
strict = true,
|
|
1226
|
+
problems = null,
|
|
1227
|
+
) {
|
|
1213
1228
|
if (!theme) return stripTokens(style);
|
|
1214
1229
|
let byTheme = strict ? resolvedCache.get(style) : null;
|
|
1215
1230
|
if (strict && !byTheme) resolvedCache.set(style, (byTheme = new WeakMap()));
|
|
1216
1231
|
const hit = byTheme?.get(theme);
|
|
1217
|
-
if (hit)
|
|
1232
|
+
if (hit) {
|
|
1233
|
+
if (problems && hit.problems) problems.push(...hit.problems);
|
|
1234
|
+
return hit.out;
|
|
1235
|
+
}
|
|
1218
1236
|
|
|
1237
|
+
// collected here rather than pushed straight to `problems` so the cache
|
|
1238
|
+
// entry can keep them: the caller that misses is not the only one to hear
|
|
1239
|
+
const found = strict ? [] : null;
|
|
1219
1240
|
const out = {};
|
|
1220
1241
|
for (const key of Object.keys(style)) {
|
|
1221
1242
|
const v = style[key];
|
|
@@ -1225,11 +1246,7 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1225
1246
|
out[key] = theme[name];
|
|
1226
1247
|
continue;
|
|
1227
1248
|
}
|
|
1228
|
-
if (
|
|
1229
|
-
throw new Error(
|
|
1230
|
-
`react-x11: unknown theme token "${v}" in ${where} ` +
|
|
1231
|
-
`(theme has ${Object.keys(theme).join(', ') || 'nothing'})`,
|
|
1232
|
-
);
|
|
1249
|
+
if (strict) found.push(unknownToken(v, theme, where));
|
|
1233
1250
|
} else if (mentionsToken(v)) {
|
|
1234
1251
|
let unknown = null;
|
|
1235
1252
|
const substituted = v.replace(TOKEN_IN_VALUE, (token) => {
|
|
@@ -1245,13 +1262,10 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1245
1262
|
// frame instead of at the style.
|
|
1246
1263
|
if (!unknown) out[key] = substituted;
|
|
1247
1264
|
else if (strict) {
|
|
1248
|
-
|
|
1249
|
-
`react-x11: unknown theme token "${unknown}" in ${where} ${key} ` +
|
|
1250
|
-
`(theme has ${Object.keys(theme).join(', ') || 'nothing'})`,
|
|
1251
|
-
);
|
|
1265
|
+
found.push(unknownToken(unknown, theme, `${where} ${key}`));
|
|
1252
1266
|
}
|
|
1253
1267
|
} else if (key.charCodeAt(0) === 58 && v) {
|
|
1254
|
-
out[key] = resolveTokens(v, theme, `${where} ${key}`, strict);
|
|
1268
|
+
out[key] = resolveTokens(v, theme, `${where} ${key}`, strict, found);
|
|
1255
1269
|
} else if (key === 'animation' && v && typeof v === 'object') {
|
|
1256
1270
|
const loops = {};
|
|
1257
1271
|
let incomplete = false;
|
|
@@ -1266,11 +1280,11 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1266
1280
|
theme,
|
|
1267
1281
|
`${where} animation ${prop}`,
|
|
1268
1282
|
strict,
|
|
1283
|
+
found,
|
|
1269
1284
|
);
|
|
1270
|
-
// A
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
// the ancestry to complete rather than throwing at a half of one.
|
|
1285
|
+
// A loop with one end missing is not a shorter loop, so a
|
|
1286
|
+
// declaration that lost a value is dropped whole rather than run
|
|
1287
|
+
// between a colour and nothing.
|
|
1274
1288
|
if (Object.keys(resolved).length !== Object.keys(entry).length) {
|
|
1275
1289
|
incomplete = true;
|
|
1276
1290
|
}
|
|
@@ -1281,10 +1295,20 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1281
1295
|
out[key] = v;
|
|
1282
1296
|
}
|
|
1283
1297
|
}
|
|
1284
|
-
|
|
1298
|
+
if (found?.length && problems) problems.push(...found);
|
|
1299
|
+
byTheme?.set(theme, { out, problems: found?.length ? found : null });
|
|
1285
1300
|
return out;
|
|
1286
1301
|
}
|
|
1287
1302
|
|
|
1303
|
+
/** The one message, written once: what was named, and what the palette in
|
|
1304
|
+
* force actually has — listing the alternatives is most of the fix. */
|
|
1305
|
+
function unknownToken(token, theme, where) {
|
|
1306
|
+
return (
|
|
1307
|
+
`react-x11: unknown theme token "${token}" in ${where} ` +
|
|
1308
|
+
`(theme has ${Object.keys(theme).join(', ') || 'nothing'})`
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1288
1312
|
export { validateStyle };
|
|
1289
1313
|
|
|
1290
1314
|
/**
|