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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- 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,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();
|
package/src/registry.js
ADDED
|
@@ -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
|
+
}
|