react-x11 1.2.0 → 2.0.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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
@@ -0,0 +1,401 @@
1
+ // react-x11/refresh — the loader half of state-preserving hot reload.
2
+ // `registerRefresh(options)` is the seam for a tool that needs options;
3
+ // `react-x11/refresh/register` calls it with the defaults so an app adopts
4
+ // the whole thing as `node --import react-x11/refresh/register app.jsx`.
5
+ //
6
+ // Two module hook layers, both sync (module.registerHooks, Node >= 22.15):
7
+ //
8
+ // 1. a JSX + Fast Refresh loader — babel with the classic JSX transform
9
+ // plus react-refresh/babel, which instruments every component so edits
10
+ // update it *in place*, keeping its hook state. retainLines keeps the
11
+ // source's line structure, which the layer above depends on. The
12
+ // injected prelude provides the $RefreshReg$/$RefreshSig$ bindings the
13
+ // instrumentation emits (components register under their canonical
14
+ // module URL, the ?hmr=N cache-buster stripped, so a reloaded module
15
+ // lands in the same component family), and the injected footer hands
16
+ // the module's exports to the runtime, which decides whether the
17
+ // module is a refresh boundary (see ./index.js);
18
+ // 2. hot-module-replacement's ESM hooks, registered second so they run
19
+ // outermost and see plain JS: an acorn transform rewrites static
20
+ // imports into live `let` bindings and wires up import.meta.hot.
21
+ //
22
+ // node_modules and react-x11's own src/ are kept out of the hot graph:
23
+ // React, the renderer and react-refresh/runtime stay singletons, so the
24
+ // X11 connection and the mounted window survive reloads — the hot boundary
25
+ // is the application's own modules.
26
+ import { readFileSync } from 'node:fs';
27
+ import * as nodeModule from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { markHotReloadSession } from '../registry.js';
30
+
31
+ const RUNTIME_URL = new URL('./index.js', import.meta.url).href;
32
+ const SRC_DIR = fileURLToPath(new URL('../', import.meta.url));
33
+
34
+ const TOOLCHAIN = [
35
+ '@babel/core',
36
+ '@babel/plugin-transform-react-jsx',
37
+ 'react-refresh',
38
+ 'hot-module-replacement',
39
+ ];
40
+
41
+ async function loadToolchain() {
42
+ const missing = [];
43
+ const load = async (specifier, name) => {
44
+ try {
45
+ return await import(specifier);
46
+ } catch {
47
+ missing.push(name);
48
+ return null;
49
+ }
50
+ };
51
+ const babel = await load('@babel/core', '@babel/core');
52
+ const jsxTransform = await load(
53
+ '@babel/plugin-transform-react-jsx',
54
+ '@babel/plugin-transform-react-jsx',
55
+ );
56
+ const refreshTransform = await load('react-refresh/babel', 'react-refresh');
57
+ if (missing.length > 0) {
58
+ throw new Error(
59
+ `react-x11/refresh: the hot-reload toolchain is not installed ` +
60
+ `(missing: ${missing.join(', ')}). It is a set of optional peer ` +
61
+ `dependencies — an app that never hot-reloads does not carry them. ` +
62
+ `Install them where the dev loop runs:\n\n` +
63
+ ` npm install --save-dev ${TOOLCHAIN.join(' ')}\n`,
64
+ );
65
+ }
66
+ return {
67
+ babel,
68
+ jsxTransform: jsxTransform.default,
69
+ refreshTransform: refreshTransform.default,
70
+ };
71
+ }
72
+
73
+ function validateOptions(babel, options) {
74
+ const {
75
+ extensions = ['.jsx'],
76
+ jsxRuntime = 'classic',
77
+ prelude = [],
78
+ ignore,
79
+ } = options;
80
+
81
+ if (jsxRuntime !== 'classic') {
82
+ throw new Error(
83
+ `react-x11/refresh: jsxRuntime must be 'classic', got ` +
84
+ `${JSON.stringify(jsxRuntime)}. The automatic runtime appends its ` +
85
+ `react/jsx-runtime import to the last existing import's line, and ` +
86
+ `hot-module-replacement's line-oriented import rewrite then breaks ` +
87
+ `the statement — and the injected import would itself become a ` +
88
+ `named-import binding that is undefined at module scope. The ` +
89
+ `classic runtime compiles to React.createElement, which reaches ` +
90
+ `React through the default import and has neither problem.`,
91
+ );
92
+ }
93
+
94
+ if (
95
+ !Array.isArray(extensions) ||
96
+ extensions.length === 0 ||
97
+ extensions.some((ext) => typeof ext !== 'string' || !ext.startsWith('.'))
98
+ ) {
99
+ throw new Error(
100
+ `react-x11/refresh: extensions must be a non-empty array of ` +
101
+ `'.ext' strings, got ${JSON.stringify(extensions)}.`,
102
+ );
103
+ }
104
+
105
+ if (ignore !== undefined && typeof ignore !== 'function') {
106
+ throw new Error(
107
+ `react-x11/refresh: ignore must be a function (path) => boolean, ` +
108
+ `got ${typeof ignore}.`,
109
+ );
110
+ }
111
+
112
+ if (!Array.isArray(prelude)) {
113
+ throw new Error(
114
+ `react-x11/refresh: prelude must be an array of statements, got ` +
115
+ `${typeof prelude}.`,
116
+ );
117
+ }
118
+ prelude.forEach((statement, i) => {
119
+ const reject = (what) => {
120
+ throw new Error(
121
+ `react-x11/refresh: prelude[${i}] ${what}. Injected prelude lines ` +
122
+ `are joined one statement per line: hot-module-replacement's ` +
123
+ `import rewrite replaces a whole import statement's span, so a ` +
124
+ `second statement sharing its line is silently swallowed or ` +
125
+ `becomes a syntax error. Split it into one array entry per ` +
126
+ `statement.\n prelude[${i}]: ${JSON.stringify(statement)}`,
127
+ );
128
+ };
129
+ if (typeof statement !== 'string' || statement.trim() === '') {
130
+ throw new Error(
131
+ `react-x11/refresh: prelude[${i}] must be a non-empty statement ` +
132
+ `string, got ${JSON.stringify(statement)}.`,
133
+ );
134
+ }
135
+ if (statement.includes('\n')) reject('contains a newline');
136
+ let ast;
137
+ try {
138
+ ast = babel.parseSync(statement, {
139
+ babelrc: false,
140
+ configFile: false,
141
+ sourceType: 'module',
142
+ });
143
+ } catch (err) {
144
+ throw new Error(
145
+ `react-x11/refresh: prelude[${i}] does not parse as a statement: ` +
146
+ `${err.message}\n prelude[${i}]: ${JSON.stringify(statement)}`,
147
+ );
148
+ }
149
+ if (ast.program.body.length !== 1) {
150
+ reject(`holds ${ast.program.body.length} statements`);
151
+ }
152
+ });
153
+
154
+ return { extensions, prelude, ignore };
155
+ }
156
+
157
+ // The two per-module analyses, one babel pass:
158
+ //
159
+ // - the enforced constraint: a *named* import called at module top level.
160
+ // hot-module-replacement rewrites named imports into `let` bindings it
161
+ // initializes in a microtask, so at module scope the value is still
162
+ // undefined — the call throws a bare "x is not a function" at runtime,
163
+ // four frames from anything the developer wrote. Caught here instead,
164
+ // at transform time, with the module and line;
165
+ // - export collection for the boundary decision. Any export the footer
166
+ // cannot name as a local value — a re-export, `export *`, an anonymous
167
+ // default — makes the module opaque: never a boundary, so an edit to it
168
+ // propagates to the nearest boundary above, which is correct just wider.
169
+ function makeGuardPlugin(onModuleInfo) {
170
+ return ({ types: t }) => ({
171
+ name: 'react-x11-refresh-guards',
172
+ visitor: {
173
+ Program: {
174
+ exit(path) {
175
+ for (const binding of Object.values(path.scope.bindings)) {
176
+ if (binding.kind !== 'module') continue;
177
+ if (!t.isImportSpecifier(binding.path.node)) continue;
178
+ for (const ref of binding.referencePaths) {
179
+ if (ref.getFunctionParent()) continue;
180
+ // A class field initializer runs at instantiation, not at
181
+ // module evaluation — a static block does run now, though.
182
+ const classBody = ref.findParent(
183
+ (p) => p.isClassBody() || p.isStaticBlock(),
184
+ );
185
+ if (classBody && classBody.isClassBody()) continue;
186
+ const parent = ref.parentPath;
187
+ const called =
188
+ ((t.isCallExpression(parent.node) ||
189
+ t.isNewExpression(parent.node)) &&
190
+ parent.node.callee === ref.node) ||
191
+ (t.isTaggedTemplateExpression(parent.node) &&
192
+ parent.node.tag === ref.node);
193
+ if (!called) continue;
194
+ const name = ref.node.name;
195
+ throw ref.buildCodeFrameError(
196
+ `react-x11/refresh: \`${name}\` is a named import called at ` +
197
+ `module top level inside a hot module. Named imports here ` +
198
+ `become live bindings that hot-module-replacement ` +
199
+ `initializes in a microtask, so \`${name}\` is still ` +
200
+ `undefined when this line runs. Call it through the ` +
201
+ `default import instead (React.createContext(...), not ` +
202
+ `createContext(...)), or move the call inside a component ` +
203
+ `or function — and keep identity that must survive a ` +
204
+ `reload (contexts, stores) in a module outside the hot ` +
205
+ `graph.`,
206
+ );
207
+ }
208
+ }
209
+
210
+ let opaque = false;
211
+ const names = [];
212
+ for (const node of path.node.body) {
213
+ if (node.type === 'ExportAllDeclaration') {
214
+ opaque = true;
215
+ } else if (node.type === 'ExportDefaultDeclaration') {
216
+ const d = node.declaration;
217
+ if (
218
+ (d.type === 'FunctionDeclaration' ||
219
+ d.type === 'ClassDeclaration') &&
220
+ d.id
221
+ ) {
222
+ names.push(d.id.name);
223
+ } else if (d.type === 'Identifier') {
224
+ names.push(d.name);
225
+ } else {
226
+ opaque = true;
227
+ }
228
+ } else if (node.type === 'ExportNamedDeclaration') {
229
+ if (node.source) {
230
+ opaque = true;
231
+ continue;
232
+ }
233
+ const d = node.declaration;
234
+ if (d && d.type === 'VariableDeclaration') {
235
+ for (const declarator of d.declarations) {
236
+ if (declarator.id.type === 'Identifier') {
237
+ names.push(declarator.id.name);
238
+ } else {
239
+ opaque = true;
240
+ }
241
+ }
242
+ } else if (d && d.id) {
243
+ names.push(d.id.name);
244
+ } else if (d) {
245
+ opaque = true;
246
+ }
247
+ for (const spec of node.specifiers) {
248
+ if (spec.type === 'ExportSpecifier') {
249
+ names.push(spec.local.name);
250
+ } else {
251
+ opaque = true;
252
+ }
253
+ }
254
+ }
255
+ }
256
+ onModuleInfo({ opaque, names });
257
+ },
258
+ },
259
+ },
260
+ });
261
+ }
262
+
263
+ /**
264
+ * The transform alone, for a tool that hosts its own hooks (and for the
265
+ * tests). `matches(pathname)` says whether a file is a hot module;
266
+ * `transform(source, filename)` returns `{ code }` with the refresh
267
+ * prelude, the instrumented body, and the boundary footer.
268
+ */
269
+ export async function createTransformer(options = {}) {
270
+ const toolchain = await loadToolchain();
271
+ return buildTransformer(toolchain, validateOptions(toolchain.babel, options));
272
+ }
273
+
274
+ function buildTransformer({ babel, jsxTransform, refreshTransform }, options) {
275
+ const { extensions, prelude } = options;
276
+
277
+ // One statement per line, `;`-joined — see the prelude constraint in
278
+ // validateOptions. Everything reaches the runtime through its default
279
+ // export because only default bindings initialize synchronously in a
280
+ // hot module (the runtime says the same from its side).
281
+ const preludeLines = [
282
+ `import __ReactX11Refresh from ${JSON.stringify(RUNTIME_URL)}`,
283
+ `const __refreshUrl = import.meta.url.replace(/\\?hmr=\\d+$/, '')`,
284
+ `const $RefreshReg$ = (type, id) => __ReactX11Refresh.register(type, __refreshUrl + ' ' + id)`,
285
+ `const $RefreshSig$ = __ReactX11Refresh.createSignatureFunctionForTransform`,
286
+ ...prelude,
287
+ ];
288
+ // Stacks through hot modules are off by these lines; dev-only.
289
+ const preludeSource = preludeLines.join(';\n') + ';';
290
+
291
+ let lastModuleInfo = null;
292
+ const guardPlugin = makeGuardPlugin((info) => {
293
+ lastModuleInfo = info;
294
+ });
295
+
296
+ return {
297
+ preludeLineCount: preludeLines.length,
298
+ matches(pathname) {
299
+ return extensions.some((ext) => pathname.endsWith(ext));
300
+ },
301
+ transform(source, filename) {
302
+ lastModuleInfo = null;
303
+ const { code } = babel.transformSync(source, {
304
+ filename,
305
+ babelrc: false,
306
+ configFile: false,
307
+ plugins: [
308
+ // classic runtime only — see validateOptions for why
309
+ [jsxTransform, { runtime: 'classic' }],
310
+ [refreshTransform, { skipEnvCheck: true }],
311
+ guardPlugin,
312
+ ],
313
+ retainLines: true,
314
+ sourceMaps: 'inline',
315
+ });
316
+ const { opaque, names } = lastModuleInfo;
317
+ const exportsArg =
318
+ opaque || names.length === 0
319
+ ? 'null'
320
+ : `{ ${names.map((n) => `${JSON.stringify(n)}: ${n}`).join(', ')} }`;
321
+ const footer = `\n;import.meta.hot && __ReactX11Refresh.moduleReady(import.meta.hot, import.meta.url, ${exportsArg});`;
322
+ return { code: `${preludeSource}\n${code}${footer}` };
323
+ },
324
+ };
325
+ }
326
+
327
+ let registered = false;
328
+
329
+ /**
330
+ * Register the hot-reload loader. `react-x11/refresh/register` calls this
331
+ * with no options; a tool that needs the seams writes its own two-line
332
+ * `--import` module and passes them here.
333
+ */
334
+ export async function registerRefresh(options = {}) {
335
+ if (process.env.NODE_ENV === 'production') {
336
+ throw new Error(
337
+ `react-x11/refresh: NODE_ENV is 'production'. Hot reload is a ` +
338
+ `development tool — react-refresh ships a no-op production build, ` +
339
+ `so nothing would reload. Unset NODE_ENV (or set it to ` +
340
+ `'development') for the dev loop, and drop the --import for ` +
341
+ `production runs; there is no production hot path, by design.`,
342
+ );
343
+ }
344
+ if (typeof nodeModule.registerHooks !== 'function') {
345
+ throw new Error(
346
+ `react-x11/refresh needs module.registerHooks, which landed in ` +
347
+ `Node.js 22.15 — this is Node ${process.versions.node}. Upgrade ` +
348
+ `Node to use hot reload; the renderer itself runs on Node >= 20.19.`,
349
+ );
350
+ }
351
+ if (registered) {
352
+ throw new Error(
353
+ `react-x11/refresh: registerRefresh was already called in this ` +
354
+ `process. Register it once — from the app's --import module or ` +
355
+ `from the tool hosting it, not both.`,
356
+ );
357
+ }
358
+
359
+ const toolchain = await loadToolchain();
360
+ const validated = validateOptions(toolchain.babel, options);
361
+ const transformer = buildTransformer(toolchain, validated);
362
+ const { ignore } = validated;
363
+ registered = true;
364
+
365
+ const hotIgnore = (path) =>
366
+ path.includes('/node_modules/') ||
367
+ path.startsWith(SRC_DIR) ||
368
+ (ignore ? ignore(path) === true : false);
369
+
370
+ nodeModule.registerHooks({
371
+ load(url, context, nextLoad) {
372
+ if (!url.startsWith('file:')) {
373
+ return nextLoad(url, context);
374
+ }
375
+ // A ?hmr=N request is a hot re-import about to re-run a module
376
+ // scope. Told to the registry *before* the module evaluates — the
377
+ // first reloaded module may itself call registerElement at top
378
+ // level — and checked on every extension, because a chain reload
379
+ // re-imports plain .js modules this hook does not transform.
380
+ if (/[?&]hmr=\d+/.test(url)) markHotReloadSession();
381
+ const fileUrl = new URL(url);
382
+ fileUrl.search = ''; // hot-module-replacement cache-busts with ?hmr=N
383
+ if (!transformer.matches(fileUrl.pathname)) {
384
+ return nextLoad(url, context);
385
+ }
386
+ const filename = fileURLToPath(fileUrl);
387
+ if (hotIgnore(filename)) {
388
+ return nextLoad(url, context);
389
+ }
390
+ const { code } = transformer.transform(
391
+ readFileSync(filename, 'utf8'),
392
+ filename,
393
+ );
394
+ return { format: 'module', source: code, shortCircuit: true };
395
+ },
396
+ });
397
+
398
+ // Registered second so its hooks run outermost and see plain JS.
399
+ globalThis.__HMR_OPTIONS__ = { ignore: hotIgnore };
400
+ await import('hot-module-replacement/register');
401
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `react-x11/refresh/register` — side-effect entry for `node --import`:
3
+ * registers the hot-reload loader with the default options. No exports.
4
+ */
5
+ export {};
@@ -0,0 +1,13 @@
1
+ // react-x11/refresh/register — hot reload with the defaults, for --import:
2
+ //
3
+ // node --enable-source-maps --import react-x11/refresh/register app.jsx
4
+ //
5
+ // The app's entry needs no changes: the loader injects the runtime wiring
6
+ // into every hot module, edits to components apply in place with hook
7
+ // state intact, and `onReload` (from react-x11/refresh) is there when a
8
+ // tool wants to observe the reloads. A tool that needs the seams —
9
+ // extensions, ignore, an injected prelude — writes its own two-line
10
+ // --import module calling registerRefresh(options) instead.
11
+ import { registerRefresh } from './loader.js';
12
+
13
+ await registerRefresh();
@@ -0,0 +1,232 @@
1
+ // The element registry: how a package that is not react-x11 adds a host
2
+ // element (issue #125).
3
+ //
4
+ // The vocabulary used to be a closed `switch` with a throwing default, so a
5
+ // `<sparkline>`, a tray icon or an XEMBED `<foreign>` meant forking. It is
6
+ // now that switch plus this table, consulted by `createInstance` before it
7
+ // gives up — the built-ins stay a switch because they are not data, and a
8
+ // registered element is data.
9
+ //
10
+ // Two things the table carries besides the constructor, both because
11
+ // getting them wrong fails somewhere far away:
12
+ //
13
+ // - **`drawn`** puts the kind in `DRAWN_KINDS`, which is what `paintOrder`
14
+ // filters on. A node missing from it lays out and never paints, with no
15
+ // error anywhere — the single most confusing way for a custom element to
16
+ // fail.
17
+ // - **`semanticNames`** exempts the element's own prop names from the DEV
18
+ // assertion that catches `<box width={10}>` (a style property written
19
+ // flat). Without it an element whose vocabulary overlaps the style
20
+ // vocabulary — `width`, `stroke`, `opacity` — throws on its own props in
21
+ // development and works in production, which is the worst shape a bug
22
+ // can have.
23
+ //
24
+ // And one that is an optimisation rather than a trap, `selfDamagedProps`,
25
+ // which is the commit half of the damage seam `Node.paintDamage()` opens
26
+ // (issue #301).
27
+ import {
28
+ DRAWN_KINDS,
29
+ CUSTOM_SEMANTIC_NAMES,
30
+ CUSTOM_SELF_DAMAGED,
31
+ Node,
32
+ } from './nodes.js';
33
+
34
+ /** kind -> definition. Insertion-ordered, which is the order errors list. */
35
+ const registry = new Map();
36
+
37
+ const RESERVED = new Set(['textchunk', 'svgchild']);
38
+
39
+ // The re-registration policy for hot reload (issue #318). Module-scope
40
+ // registration is the pattern the docs recommend and tree-shaking forces on
41
+ // component libraries, and a hot re-import runs that module scope again —
42
+ // so under an active reload session a duplicate registration replaces
43
+ // silently instead of throwing. The flag is flipped by react-x11/refresh's
44
+ // loader the moment the first hot re-import is compiled (before it
45
+ // evaluates, so the first reloaded module's own registerElement is already
46
+ // covered), and never outside a hot dev loop — a plain run still hears
47
+ // about two packages claiming one name. It stays one-way on purpose: after
48
+ // the first reload every registration may be a re-run of code that
49
+ // registered before, and there is no later moment at which that stops
50
+ // being true. The setter takes `false` only so tests can restore the
51
+ // default; nothing else should ever pass it.
52
+ let hotReloadSession = false;
53
+
54
+ /** @internal — called by react-x11/refresh; not part of the public API. */
55
+ export function markHotReloadSession(active = true) {
56
+ hotReloadSession = active;
57
+ }
58
+
59
+ function assertNode(node, type) {
60
+ if (!(node instanceof Node)) {
61
+ throw new Error(
62
+ `react-x11: the create() registered for <${type}> returned ` +
63
+ `${node === undefined ? 'undefined' : typeof node} — it must return ` +
64
+ 'a Node (import { Node } from "react-x11/node").',
65
+ );
66
+ }
67
+ if (node.kind !== type) {
68
+ throw new Error(
69
+ `react-x11: the create() registered for <${type}> returned a node of ` +
70
+ `kind "${node.kind}". The kind is what paint order, queries and the ` +
71
+ 'DEV style assertion match on, so it has to be the element name.',
72
+ );
73
+ }
74
+ return node;
75
+ }
76
+
77
+ /**
78
+ * Teach react-x11 a new element.
79
+ *
80
+ * ```js
81
+ * import { registerElement } from 'react-x11/host';
82
+ * import { Node } from 'react-x11/node';
83
+ *
84
+ * class SparklineNode extends Node {
85
+ * constructor(props, app) {
86
+ * super('sparkline', props, app);
87
+ * }
88
+ * paint(ctx) {
89
+ * super.paint(ctx); // background, border, clip
90
+ * // ...draw this.props.data inside this.abs
91
+ * }
92
+ * }
93
+ *
94
+ * registerElement('sparkline', {
95
+ * create: (props, app) => new SparklineNode(props, app),
96
+ * drawn: true,
97
+ * semanticNames: ['data', 'stroke'],
98
+ * });
99
+ * ```
100
+ *
101
+ * @param {string} type element name, as written in JSX
102
+ * @param {object} definition
103
+ * @param {(props, app, hostContext) => Node} definition.create builds the
104
+ * node. `app` is the ntk connection the tree renders through — the second
105
+ * argument every built-in node constructor takes.
106
+ * @param {boolean} [definition.drawn=true] lays out with yoga and paints
107
+ * into the owning window. `false` is for a node that owns a real child X
108
+ * window instead (`GlAreaNode` is the worked example).
109
+ * @param {string[]} [definition.semanticNames] prop names this element owns
110
+ * even though they are also style names.
111
+ * @param {string[]} [definition.selfDamagedProps] prop names whose damage
112
+ * the element's own `applyProps` claims, so a commit that changes one does
113
+ * not also damage the whole node. For an element that draws a scene into
114
+ * one node and invalidates the part of it that moved; an element that
115
+ * answers wrong shows stale pixels, so everything left out stays core's
116
+ * conservative answer (docs/extending.md).
117
+ * @param {boolean} [definition.childrenAllowed=true] reject children in DEV
118
+ * with a message naming the element, rather than laying out something
119
+ * that cannot paint.
120
+ * @param {boolean} [definition.override=false] replace an existing
121
+ * registration. Off by default: two packages claiming one name is a
122
+ * conflict to be told about, not resolved silently.
123
+ */
124
+ export function registerElement(type, definition) {
125
+ if (typeof type !== 'string' || type === '') {
126
+ throw new Error('react-x11: registerElement needs an element name.');
127
+ }
128
+ if (!/^[a-z][a-z0-9-]*$/.test(type)) {
129
+ // Capitalised names are components to JSX, never host elements, so
130
+ // `registerElement('Sparkline')` would register something unreachable.
131
+ throw new Error(
132
+ `react-x11: "${type}" cannot be an element name — JSX only treats ` +
133
+ 'lowercase names as host elements. Use "sparkline", not "Sparkline".',
134
+ );
135
+ }
136
+ if (RESERVED.has(type)) {
137
+ throw new Error(
138
+ `react-x11: <${type}> is internal to react-x11 and cannot be ` +
139
+ 'registered.',
140
+ );
141
+ }
142
+ if (!definition || typeof definition.create !== 'function') {
143
+ throw new Error(
144
+ `react-x11: registerElement("${type}") needs a create(props, app) ` +
145
+ 'function that returns a Node.',
146
+ );
147
+ }
148
+ if (registry.has(type) && !definition.override) {
149
+ // Identical re-registration — the same create() reference — is the
150
+ // same definition arriving twice (a module evaluated twice, a test
151
+ // registering in a loop) and never a conflict; the latest flags win.
152
+ // Everything else is a real collision unless a hot-reload session is
153
+ // re-running module scopes, which is the one loop where "already
154
+ // registered" means "registered by the previous version of yourself".
155
+ // Mounted nodes keep the old prototype until they remount — the same
156
+ // staleness contract React Refresh has for classes.
157
+ const identical = registry.get(type).create === definition.create;
158
+ if (!identical && !hotReloadSession) {
159
+ throw new Error(
160
+ `react-x11: <${type}> is already registered. Pass { override: true } ` +
161
+ 'if replacing it is deliberate — two packages claiming one element ' +
162
+ 'name is usually not. (Under hot reload a module re-registering ' +
163
+ 'its own elements replaces them silently; this error means two ' +
164
+ 'different definitions collided outside any reload.)',
165
+ );
166
+ }
167
+ }
168
+
169
+ const {
170
+ create,
171
+ drawn = true,
172
+ semanticNames = [],
173
+ selfDamagedProps = [],
174
+ childrenAllowed = true,
175
+ } = definition;
176
+
177
+ registry.set(type, { create, drawn, childrenAllowed });
178
+
179
+ // paintOrder() filters on this set, so a drawn element has to join it or
180
+ // it lays out and never paints
181
+ if (drawn) DRAWN_KINDS.add(type);
182
+ else DRAWN_KINDS.delete(type);
183
+
184
+ if (semanticNames.length > 0) {
185
+ CUSTOM_SEMANTIC_NAMES.set(type, new Set(semanticNames));
186
+ } else {
187
+ CUSTOM_SEMANTIC_NAMES.delete(type);
188
+ }
189
+
190
+ // Both maps are keyed on the kind and cleared when the name is not
191
+ // claimed, so re-registering with `override` cannot leave the previous
192
+ // definition's declarations behind for the new one to inherit.
193
+ if (selfDamagedProps.length > 0) {
194
+ CUSTOM_SELF_DAMAGED.set(type, new Set(selfDamagedProps));
195
+ } else {
196
+ CUSTOM_SELF_DAMAGED.delete(type);
197
+ }
198
+ }
199
+
200
+ /** Undo a registration. Mostly for tests, which must not leak an element
201
+ * into the next file's expectations. */
202
+ export function unregisterElement(type) {
203
+ DRAWN_KINDS.delete(type);
204
+ CUSTOM_SEMANTIC_NAMES.delete(type);
205
+ CUSTOM_SELF_DAMAGED.delete(type);
206
+ return registry.delete(type);
207
+ }
208
+
209
+ /** The definition for a registered element, or undefined. */
210
+ export function elementDefinition(type) {
211
+ return registry.get(type);
212
+ }
213
+
214
+ /** Registered element names, in registration order. */
215
+ export function registeredElements() {
216
+ return [...registry.keys()];
217
+ }
218
+
219
+ /**
220
+ * Build a registered element's node, or undefined if the name is not one.
221
+ * Called from `createInstance`'s default branch, so an unregistered name
222
+ * still produces the built-in "unknown element" error.
223
+ */
224
+ export function createRegisteredNode(type, props, app, hostContext) {
225
+ const definition = registry.get(type);
226
+ if (!definition) return undefined;
227
+ const node = assertNode(definition.create(props, app, hostContext), type);
228
+ // read by Node.insertBefore — carried on the instance so nodes.js needs
229
+ // no import from here
230
+ if (!definition.childrenAllowed) node._childrenAllowed = false;
231
+ return node;
232
+ }