react-x11 2.45.3 → 2.46.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 +5 -1
- package/src/cocoa/relaunch.js +98 -12
- package/src/frame/child.d.ts +9 -0
- package/src/frame/index.js +87 -14
- package/src/types/frame.d.ts +30 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.46.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -221,6 +221,10 @@
|
|
|
221
221
|
"./cocoa-main": {
|
|
222
222
|
"types": "./src/cocoa/main.d.ts",
|
|
223
223
|
"default": "./src/cocoa/main.js"
|
|
224
|
+
},
|
|
225
|
+
"./frame/child": {
|
|
226
|
+
"types": "./src/frame/child.d.ts",
|
|
227
|
+
"default": "./src/frame/child.js"
|
|
224
228
|
}
|
|
225
229
|
},
|
|
226
230
|
"types": "./src/index.d.ts"
|
package/src/cocoa/relaunch.js
CHANGED
|
@@ -143,6 +143,19 @@ export const WORKER_START = `(function (process, global) {
|
|
|
143
143
|
// process for ever.
|
|
144
144
|
const WORKER_END_MS = 5000;
|
|
145
145
|
|
|
146
|
+
// Where Node starts an app (`startedRunning`): the script that loads the
|
|
147
|
+
// `--require` preloads and then the entry, and the module that runs the
|
|
148
|
+
// entry, an ES module's graph from an async function.
|
|
149
|
+
const ENTRY_POINT = new Set([
|
|
150
|
+
'node:internal/main/run_main_module',
|
|
151
|
+
'node:internal/modules/run_main',
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// How a process is started under an ES module loader, as a flag of its own
|
|
155
|
+
// or in NODE_OPTIONS: `--import tsx`, `--import=tsx`, `--loader
|
|
156
|
+
// ts-node/esm`, `--experimental-loader ./hooks.mjs`.
|
|
157
|
+
const LOADER_FLAG = /^--(?:import|loader|experimental-loader)(?:=|$)/;
|
|
158
|
+
|
|
146
159
|
/**
|
|
147
160
|
* Why this import should not move the app onto a worker, or null when it
|
|
148
161
|
* should. A pure answer over what the process says about itself, so that
|
|
@@ -152,6 +165,8 @@ export function relaunchVeto({
|
|
|
152
165
|
isMainThread: main,
|
|
153
166
|
platform,
|
|
154
167
|
env,
|
|
168
|
+
execArgv,
|
|
169
|
+
node,
|
|
155
170
|
entry,
|
|
156
171
|
compiled,
|
|
157
172
|
late,
|
|
@@ -184,13 +199,33 @@ export function relaunchVeto({
|
|
|
184
199
|
) {
|
|
185
200
|
return 'a test runner';
|
|
186
201
|
}
|
|
187
|
-
// Imported after the app started running
|
|
188
|
-
//
|
|
189
|
-
// entry has already done on this thread.
|
|
202
|
+
// Imported after the app started running: by an `import()` from a timer,
|
|
203
|
+
// say, or one the entry's body awaits. Relaunching would run again, on
|
|
204
|
+
// the worker, everything the entry has already done on this thread.
|
|
190
205
|
if (late) return 'imported after the app started running';
|
|
206
|
+
// Started under an ES module loader, before Node 24. The worker gets the
|
|
207
|
+
// loader's flags, and whether its hooks follow is the loader's to say:
|
|
208
|
+
// tsx, the one an app is likeliest to run under, keeps them to the main
|
|
209
|
+
// thread on Node 20 and 22 — before 22.22.3, by tsx 4.23's own check —
|
|
210
|
+
// and a worker without them stops on the entry's first .jsx or .ts module
|
|
211
|
+
// (measured, 20.20 and 22.16). No loader can be asked before the move, so
|
|
212
|
+
// any `--import` or `--loader` counts. From 24 an app under one moves,
|
|
213
|
+
// and tsx follows it there from 4.21.1 on 24.11.1 (an older one does not,
|
|
214
|
+
// docs/macos.md); `react-x11/cocoa-main` moves one on 20 or 22 whose
|
|
215
|
+
// loader follows.
|
|
216
|
+
if (node && node < 24 && startedUnderLoader(execArgv, env.NODE_OPTIONS)) {
|
|
217
|
+
return 'an ES module loader before Node 24';
|
|
218
|
+
}
|
|
191
219
|
return null;
|
|
192
220
|
}
|
|
193
221
|
|
|
222
|
+
/** Whether node's flags, its own or NODE_OPTIONS', name an ES module loader. */
|
|
223
|
+
function startedUnderLoader(execArgv = [], nodeOptions = '') {
|
|
224
|
+
return [...execArgv, ...nodeOptions.split(/\s+/)].some((flag) =>
|
|
225
|
+
LOADER_FLAG.test(flag),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
194
229
|
/** What `relaunchVeto` reads, from this process. */
|
|
195
230
|
export function describeProcess(moduleUrl) {
|
|
196
231
|
const bun = globalThis.Bun;
|
|
@@ -199,6 +234,9 @@ export function describeProcess(moduleUrl) {
|
|
|
199
234
|
isMainThread,
|
|
200
235
|
platform: process.platform,
|
|
201
236
|
env: process.env,
|
|
237
|
+
execArgv: process.execArgv,
|
|
238
|
+
// Node's major version; Bun answers with one of its own choosing
|
|
239
|
+
node: bun ? null : Number(process.versions.node.split('.')[0]),
|
|
202
240
|
entry: typeof entry === 'string' && fs.existsSync(entry) ? entry : null,
|
|
203
241
|
compiled: compiledExecutable(bun),
|
|
204
242
|
late: startedRunning(bun, moduleUrl, entry),
|
|
@@ -216,15 +254,36 @@ function compiledExecutable(bun) {
|
|
|
216
254
|
}
|
|
217
255
|
|
|
218
256
|
/**
|
|
219
|
-
* Whether the entry's own code has started
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
257
|
+
* Whether the entry's own code has started: whether this import is one the
|
|
258
|
+
* app made after its start, rather than part of it.
|
|
259
|
+
*
|
|
260
|
+
* Node tells by where the import is evaluated from. It starts an app from
|
|
261
|
+
* two modules of its own (`ENTRY_POINT`): a CommonJS entry, and every
|
|
262
|
+
* `require()` of its first run and of a `--require` preload, evaluate under
|
|
263
|
+
* them synchronously, and an ES module entry's graph — static imports, an
|
|
264
|
+
* `--import` preload's — from an async function there, which V8 keeps on
|
|
265
|
+
* the stack as an async frame. An `import()` is evaluated by a job of its
|
|
266
|
+
* own, whose stack ends in the module loader, or in the app's own code when
|
|
267
|
+
* that awaits it: an `await import()` in the entry's body, an async
|
|
268
|
+
* function of the app's. Measured on Node 20.20, 22.16, 23.2, 24.12, 25.4
|
|
269
|
+
* and 26.0, static imports nested and behind a module with a top-level
|
|
270
|
+
* await, and under tsx, `--experimental-loader`, source maps and a stack
|
|
271
|
+
* trace limit of 0. A Node that moved those modules would read every
|
|
272
|
+
* import as late, which keeps the app where it is rather than running it
|
|
273
|
+
* twice; test/cocoa-relaunch.test.js would say so on any OS.
|
|
274
|
+
*
|
|
275
|
+
* `performance.nodeTiming.loopStart`, -1 until the event loop turns, is the
|
|
276
|
+
* public signal and is wrong both ways: Node 20 and 22 turn the loop while
|
|
277
|
+
* they load an ES module entry, so no such entry moved there, and Node 24
|
|
278
|
+
* and later evaluate an `import()` the entry makes before the loop's first
|
|
279
|
+
* turn, so an app that made one moved and ran its entry twice.
|
|
280
|
+
*
|
|
281
|
+
* Bun reports 1 there from the first line; what tells in Bun is the entry's
|
|
282
|
+
* record in the module cache, which is there once the entry has been
|
|
283
|
+
* evaluated and absent while its imports are (measured, Bun 1.4).
|
|
225
284
|
*/
|
|
226
285
|
function startedRunning(bun, moduleUrl, entry) {
|
|
227
|
-
if (!bun) return
|
|
286
|
+
if (!bun) return !evaluatedFromEntryPoint();
|
|
228
287
|
try {
|
|
229
288
|
return Boolean(createRequire(moduleUrl).cache?.[entry]?.loaded);
|
|
230
289
|
} catch {
|
|
@@ -232,13 +291,40 @@ function startedRunning(bun, moduleUrl, entry) {
|
|
|
232
291
|
}
|
|
233
292
|
}
|
|
234
293
|
|
|
294
|
+
/** Whether this stack, async frames and all, comes from `ENTRY_POINT`. */
|
|
295
|
+
function evaluatedFromEntryPoint() {
|
|
296
|
+
const { prepareStackTrace, stackTraceLimit } = Error;
|
|
297
|
+
let sites;
|
|
298
|
+
try {
|
|
299
|
+
// Reflect.set: an Error the app has frozen is not a reason to throw
|
|
300
|
+
// from its import of react-x11
|
|
301
|
+
Reflect.set(Error, 'stackTraceLimit', Infinity);
|
|
302
|
+
Reflect.set(Error, 'prepareStackTrace', (_, callSites) => callSites);
|
|
303
|
+
const held = {};
|
|
304
|
+
Error.captureStackTrace(held);
|
|
305
|
+
sites = held.stack;
|
|
306
|
+
} finally {
|
|
307
|
+
Reflect.set(Error, 'prepareStackTrace', prepareStackTrace);
|
|
308
|
+
Reflect.set(Error, 'stackTraceLimit', stackTraceLimit);
|
|
309
|
+
}
|
|
310
|
+
if (!Array.isArray(sites)) return false;
|
|
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;
|
|
318
|
+
}
|
|
319
|
+
|
|
235
320
|
/**
|
|
236
321
|
* The import-time move (src/bootstrap.js): relaunch when nothing vetoes it
|
|
237
322
|
* and a bridge that can run AppKit's loop is installed. Returns why it did
|
|
238
323
|
* not, for anyone asking; when it does, it does not return.
|
|
239
324
|
*/
|
|
240
325
|
export function relaunchOnImport(moduleUrl) {
|
|
241
|
-
const
|
|
326
|
+
const facts = describeProcess(moduleUrl);
|
|
327
|
+
const veto = relaunchVeto(facts);
|
|
242
328
|
if (veto) return veto;
|
|
243
329
|
let native;
|
|
244
330
|
try {
|
|
@@ -248,7 +334,7 @@ export function relaunchOnImport(moduleUrl) {
|
|
|
248
334
|
return 'no cocoa bridge';
|
|
249
335
|
}
|
|
250
336
|
if (typeof native.runMain !== 'function') return 'the bridge has no runMain';
|
|
251
|
-
return relaunch(
|
|
337
|
+
return relaunch(facts.entry, native);
|
|
252
338
|
}
|
|
253
339
|
|
|
254
340
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `react-x11/frame/child` — the entry a `<Frame>` forks: a pane process's
|
|
3
|
+
* whole bootstrap, over the IPC channel `fork()` sets up. **Resolved, not
|
|
4
|
+
* imported**: a `transport` that starts the pane its own way — under a
|
|
5
|
+
* sandbox, with a runtime of its own choosing — forks
|
|
6
|
+
* `fileURLToPath(import.meta.resolve('react-x11/frame/child'))`, and
|
|
7
|
+
* importing it anywhere else exits the process. No exports.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/src/frame/index.js
CHANGED
|
@@ -73,21 +73,22 @@ const childPath = () =>
|
|
|
73
73
|
|
|
74
74
|
/** The default transport: fork this package's child entry. `execArgv` is
|
|
75
75
|
* inherited, which is what carries a dev loader (tsx, the refresh loader)
|
|
76
|
-
* into the pane; a bundled app forks plain JS and needs none.
|
|
77
|
-
|
|
76
|
+
* into the pane; a bundled app forks plain JS and needs none. The pane's
|
|
77
|
+
* own flags and environment (`<Frame execArgv env>`) go on top. */
|
|
78
|
+
function forkTransport({ src, display, execArgv = [], env = {} }) {
|
|
78
79
|
const child = fork(childPath(), [], {
|
|
79
80
|
serialization: 'advanced',
|
|
80
81
|
// dev loaders (tsx, the refresh loader) follow into the pane; the test
|
|
81
82
|
// runner's own flags must not — `--test` in execArgv would run the pane
|
|
82
|
-
// entry as a test file
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
// entry as a test file. The pane's come after, so a flag it names is
|
|
84
|
+
// the one in force where the two disagree.
|
|
85
|
+
execArgv: [
|
|
86
|
+
...process.execArgv.filter(
|
|
87
|
+
(a) => a !== '--test' && !a.startsWith('--test-'),
|
|
88
|
+
),
|
|
89
|
+
...execArgv,
|
|
90
|
+
],
|
|
91
|
+
env: paneEnv(env, display),
|
|
91
92
|
});
|
|
92
93
|
void src; // resolved by the child from the hello, not from argv
|
|
93
94
|
return {
|
|
@@ -121,6 +122,58 @@ function forkTransport({ src, display }) {
|
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The environment a pane process starts with: this one's, with what the
|
|
127
|
+
* `<Frame>` names over it — `undefined` takes a variable away — and then
|
|
128
|
+
* the two that are not the app's to set: `REACT_X11_FRAME`, which is how a
|
|
129
|
+
* pane knows it is one (`isFramed`), and `DISPLAY`, which `display` names.
|
|
130
|
+
*/
|
|
131
|
+
function paneEnv(env, display) {
|
|
132
|
+
const out = { ...process.env };
|
|
133
|
+
for (const [name, value] of Object.entries(env)) {
|
|
134
|
+
if (value === undefined) delete out[name];
|
|
135
|
+
else out[name] = value;
|
|
136
|
+
}
|
|
137
|
+
out.REACT_X11_FRAME = '1';
|
|
138
|
+
if (display) out.DISPLAY = display;
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* What a pane's process is started with, checked and as one string: the
|
|
144
|
+
* session's key, so that an `execArgv` array or an `env` object written in
|
|
145
|
+
* place — a new one each render — restarts nothing, and one that says
|
|
146
|
+
* something else restarts the pane. Throws on what a fork cannot take,
|
|
147
|
+
* which the session reports as a `spawn` failure.
|
|
148
|
+
*/
|
|
149
|
+
function launchOf(execArgv, env) {
|
|
150
|
+
if (
|
|
151
|
+
execArgv !== undefined &&
|
|
152
|
+
(!Array.isArray(execArgv) || execArgv.some((a) => typeof a !== 'string'))
|
|
153
|
+
) {
|
|
154
|
+
throw new TypeError(
|
|
155
|
+
'react-x11: <Frame execArgv> is an array of strings — the flags the ' +
|
|
156
|
+
"pane's process is started with.",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (
|
|
160
|
+
env !== undefined &&
|
|
161
|
+
(env === null ||
|
|
162
|
+
typeof env !== 'object' ||
|
|
163
|
+
Object.values(env).some((v) => v !== undefined && typeof v !== 'string'))
|
|
164
|
+
) {
|
|
165
|
+
throw new TypeError(
|
|
166
|
+
"react-x11: <Frame env> maps a variable's name to a string, or to " +
|
|
167
|
+
'undefined to take it away.',
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
// `undefined` is kept apart from a variable left alone, which
|
|
171
|
+
// JSON.stringify would drop
|
|
172
|
+
return JSON.stringify([execArgv ?? [], env ?? {}], (_, v) =>
|
|
173
|
+
v === undefined ? { unset: true } : v,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
124
177
|
function srcString(src) {
|
|
125
178
|
if (src !== null && typeof src === 'object' && typeof src.href === 'string') {
|
|
126
179
|
return src.href;
|
|
@@ -220,6 +273,8 @@ export function Frame({
|
|
|
220
273
|
props = EMPTY_PROPS,
|
|
221
274
|
style,
|
|
222
275
|
display,
|
|
276
|
+
execArgv,
|
|
277
|
+
env: processEnv,
|
|
223
278
|
bridge = true,
|
|
224
279
|
fallback,
|
|
225
280
|
focusable,
|
|
@@ -262,8 +317,20 @@ export function Frame({
|
|
|
262
317
|
const source = srcString(src);
|
|
263
318
|
const makeTransport = transport ?? forkTransport;
|
|
264
319
|
const containerRef = useRef(null);
|
|
320
|
+
// the pane process's own flags and environment, by what they say
|
|
321
|
+
let launch;
|
|
322
|
+
let launchError = null;
|
|
323
|
+
try {
|
|
324
|
+
launch = launchOf(execArgv, processEnv);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
launch = '';
|
|
327
|
+
launchError = err;
|
|
328
|
+
}
|
|
329
|
+
const paneProcess = useRef(null);
|
|
330
|
+
paneProcess.current = { execArgv: execArgv ?? [], env: processEnv ?? {} };
|
|
265
331
|
|
|
266
|
-
// The session: one child process per (src, display,
|
|
332
|
+
// The session: one child process per (src, display, the process's flags
|
|
333
|
+
// and environment, transport, restart).
|
|
267
334
|
// Declared before the update effect so that on the mounting commit the
|
|
268
335
|
// hello goes out first and the update pass sees itself already sent.
|
|
269
336
|
useEffect(() => {
|
|
@@ -297,7 +364,13 @@ export function Frame({
|
|
|
297
364
|
|
|
298
365
|
let t;
|
|
299
366
|
try {
|
|
300
|
-
|
|
367
|
+
if (launchError) throw launchError;
|
|
368
|
+
t = makeTransport({
|
|
369
|
+
src: source,
|
|
370
|
+
display,
|
|
371
|
+
execArgv: paneProcess.current.execArgv,
|
|
372
|
+
env: paneProcess.current.env,
|
|
373
|
+
});
|
|
301
374
|
} catch (err) {
|
|
302
375
|
fail(Object.assign(err, { phase: 'spawn' }));
|
|
303
376
|
return undefined;
|
|
@@ -439,7 +512,7 @@ export function Frame({
|
|
|
439
512
|
// ask, and everything is unref'd so nothing holds the host open
|
|
440
513
|
if (session.current === s) session.current = null;
|
|
441
514
|
};
|
|
442
|
-
}, [source, display, generation, makeTransport, canShowPane]);
|
|
515
|
+
}, [source, display, launch, generation, makeTransport, canShowPane]);
|
|
443
516
|
|
|
444
517
|
// One update per commit that changed the pane's inputs, props and env in
|
|
445
518
|
// the same message — so a theme flip and the state change that caused it
|
package/src/types/frame.d.ts
CHANGED
|
@@ -45,6 +45,22 @@ export interface FrameComponentProps {
|
|
|
45
45
|
style?: StyleProp;
|
|
46
46
|
/** X display for the pane process; defaults to the inherited `$DISPLAY`. */
|
|
47
47
|
display?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Flags for the pane's process, after the ones it inherits from this one
|
|
50
|
+
* (a dev loader's): `['--max-old-space-size=256']` bounds the pane's heap,
|
|
51
|
+
* which nothing inside the pane can. They are handed to the runtime that
|
|
52
|
+
* forks it, so they are that runtime's flags. A change restarts the pane;
|
|
53
|
+
* the same flags in a new array do not.
|
|
54
|
+
*/
|
|
55
|
+
execArgv?: readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* Environment variables for the pane's process, over this one's; a value
|
|
58
|
+
* of `undefined` takes a variable away. `DISPLAY` is `display`'s and
|
|
59
|
+
* `REACT_X11_FRAME` marks the pane (`isFramed`), so neither can be set
|
|
60
|
+
* here. A change restarts the pane; the same variables in a new object do
|
|
61
|
+
* not.
|
|
62
|
+
*/
|
|
63
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
48
64
|
/**
|
|
49
65
|
* Which bridged contexts follow the app into this pane: `true` (default)
|
|
50
66
|
* for every registered one — the theme included — `false` for none, or an
|
|
@@ -75,10 +91,23 @@ export interface FrameComponentProps {
|
|
|
75
91
|
* (a loopback pair into `runFrameChild`), and the door to running a pane
|
|
76
92
|
* somewhere other than a child process. Advanced; see docs/frame.md.
|
|
77
93
|
*/
|
|
78
|
-
transport?(options:
|
|
94
|
+
transport?(options: FrameTransportOptions): FrameTransport;
|
|
79
95
|
ref?: Ref<FrameHandle>;
|
|
80
96
|
}
|
|
81
97
|
|
|
98
|
+
/** What a transport is asked to start: the pane module, and the display,
|
|
99
|
+
* flags and environment `<Frame>` names for its process. The default
|
|
100
|
+
* transport forks `react-x11/frame/child` with them. */
|
|
101
|
+
export interface FrameTransportOptions {
|
|
102
|
+
src: string;
|
|
103
|
+
display?: string;
|
|
104
|
+
/** `<Frame execArgv>`, or none: added to what the pane inherits. */
|
|
105
|
+
execArgv: readonly string[];
|
|
106
|
+
/** `<Frame env>`, or none: over this process's environment, `undefined`
|
|
107
|
+
* taking a variable away. */
|
|
108
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
/** The parent's end of the wire to a pane. */
|
|
83
112
|
export interface FrameTransport {
|
|
84
113
|
/** May throw when the channel is gone or the message will not clone. */
|