react-x11 2.52.0 → 2.54.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 +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1096 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/imagedecode-worker.js +32 -0
- package/src/imagedecode.js +140 -2
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/ntk.d.ts +12 -0
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
package/docs/packaging.md
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# Packaging and distribution
|
|
2
2
|
|
|
3
3
|
Five ways to ship a react-x11 app, cheapest first. Every recipe here was
|
|
4
|
-
run
|
|
5
|
-
where something does not work, the reason is measured rather than
|
|
6
|
-
guessed.
|
|
4
|
+
run, except the signing steps that need a real identity, which say so.
|
|
7
5
|
|
|
8
6
|
| tier | works | cost |
|
|
9
7
|
| ------------------------------- | -------------------------- | ------------------------------- |
|
|
@@ -16,14 +14,13 @@ guessed.
|
|
|
16
14
|
## Tier 1 — `npm install`
|
|
17
15
|
|
|
18
16
|
The default, and fine for a server-side app, a CI tool, or anything already
|
|
19
|
-
deployed with a package manager.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
are `optionalDependencies` that install from a bundled prebuild where one
|
|
17
|
+
deployed with a package manager. The package is ESM (`"type": "module"`),
|
|
18
|
+
needs Node ≥ 20.19 (`engines` in its `package.json`), and has **no required
|
|
19
|
+
native dependencies** — ntk, node-x11 and yoga-layout are pure JavaScript
|
|
20
|
+
and WASM, so the X11 backend needs no node-gyp and no prebuild matrix. The
|
|
21
|
+
native bridges the other backends drive — `@windowkit/appkit` for Cocoa,
|
|
22
|
+
`@windowkit/win32` for win32, `x11-dri` for Wayland's GPU path — are
|
|
23
|
+
`optionalDependencies` that install from a bundled prebuild where one
|
|
27
24
|
matches the platform, so an install that cannot build one still succeeds.
|
|
28
25
|
|
|
29
26
|
## Tier 2 — one `.mjs` file
|
|
@@ -40,11 +37,10 @@ Three things about that command are load-bearing.
|
|
|
40
37
|
**`--format=esm` is the easy path, not the only one.** It tolerates top-level
|
|
41
38
|
await anywhere in the graph; `cjs` does not, and refuses to build with
|
|
42
39
|
`Top-level await is currently not supported with the "cjs" output format`.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
only about what your own code does at module scope.
|
|
40
|
+
react-x11's own graph has none (the layout engine is loaded during
|
|
41
|
+
`createRoot()`, not imported), so the choice is only about what your own
|
|
42
|
+
code does at module scope. [Tier 3](#tier-3--node-single-executable) needs
|
|
43
|
+
`cjs`.
|
|
48
44
|
|
|
49
45
|
**The `--banner:js` is not optional.** node-x11 is CommonJS and uses dynamic
|
|
50
46
|
`require`, which esbuild cannot resolve statically. Without the banner the
|
|
@@ -54,42 +50,27 @@ bundle builds cleanly and then dies at startup:
|
|
|
54
50
|
Error: Dynamic require of "events" is not supported
|
|
55
51
|
```
|
|
56
52
|
|
|
57
|
-
The banner defines the `require` that shim calls
|
|
58
|
-
node-x11
|
|
59
|
-
it closes, the banner is the fix.
|
|
53
|
+
The banner defines the `require` that esbuild's shim calls; it is needed
|
|
54
|
+
until node-x11 ships ESM sources ([node-x11#246][x11-246]).
|
|
60
55
|
|
|
61
56
|
**The banner's import must be aliased.** The obvious form, `import
|
|
62
57
|
{createRequire} from 'node:module'`, collides with react-x11's own import of
|
|
63
58
|
the same name and the bundle fails to parse with `Identifier 'createRequire'
|
|
64
|
-
has already been declared`. Hence `as __cjsRequire` above.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
node-x11's in-process X server produces a 7 MB `app.mjs` that mounts a tree,
|
|
80
|
-
paints, and reads its own pixels back.
|
|
81
|
-
|
|
82
|
-
If you use react-x11's [DevTools](devtools.md) or
|
|
83
|
-
[click-to-component](click-to-component.md) integrations, note that they are
|
|
84
|
-
dynamically imported behind environment variables. A bundler will pull in
|
|
85
|
-
`ws` and `node:child_process` for them; if you do not want that, stub them
|
|
86
|
-
out with a resolver plugin the way
|
|
87
|
-
`website/scripts/build-demo-bundles.mjs` does.
|
|
88
|
-
|
|
89
|
-
On macOS the bundle moves onto a worker as it starts, the way the source
|
|
90
|
-
does, and still needs nothing beside it: the worker's entry is the bundle
|
|
91
|
-
itself. Verified with a one-window app under Node and Bun; what it costs,
|
|
92
|
-
and how to skip it, is [below](#on-macos-the-app-moves-onto-a-worker).
|
|
59
|
+
has already been declared`. Hence `as __cjsRequire` above.
|
|
60
|
+
|
|
61
|
+
**yoga-layout is bundler-safe**: its WASM is base64-inlined into the
|
|
62
|
+
JavaScript, so nothing is emitted beside the bundle and nothing needs a
|
|
63
|
+
loader rule or a copy step.
|
|
64
|
+
|
|
65
|
+
react-x11's [DevTools](devtools.md) and
|
|
66
|
+
[click-to-component](click-to-component.md) integrations are dynamically
|
|
67
|
+
imported behind environment variables, so a bundler pulls in `ws` and
|
|
68
|
+
`node:child_process` for them; to leave them out, stub them with a resolver
|
|
69
|
+
plugin the way `website/scripts/build-demo-bundles.mjs` does.
|
|
70
|
+
|
|
71
|
+
On macOS the bundle moves onto a worker as it starts, as the source does,
|
|
72
|
+
and still needs nothing beside it: the worker's entry is the bundle itself
|
|
73
|
+
([below](#on-macos-the-app-moves-onto-a-worker)).
|
|
93
74
|
|
|
94
75
|
## Tier 3 — Node single executable
|
|
95
76
|
|
|
@@ -103,83 +84,51 @@ node --build-sea=sea.json # { "main": "app.cjs", "output": "myapp" }
|
|
|
103
84
|
{ "main": "app.cjs", "output": "myapp", "disableExperimentalSEAWarning": true }
|
|
104
85
|
```
|
|
105
86
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
layout engine: `yoga-layout`'s default entry is
|
|
112
|
-
`const Yoga = wrapAssembly(await loadYoga())`, and every app inherited it.
|
|
113
|
-
`src/yoga.js` loads the engine through `yoga-layout/load` instead — enums
|
|
114
|
-
synchronously, WebAssembly during `createRoot()` — and react-x11 moved its
|
|
115
|
-
own three `await import()`s for DevTools, click-to-component and tracing out
|
|
116
|
-
of module scope. Nothing in the graph has a top-level await now, and
|
|
117
|
-
`test/yoga.test.js` fails the build if an import puts one back.
|
|
118
|
-
|
|
119
|
-
Verified: a bundle of react-x11, react, ntk and node-x11's in-process X
|
|
120
|
-
server mounts a 40-row tree and paints it — 157 requests in 7 socket writes —
|
|
121
|
-
from a single 142 MB file, most of which is node itself.
|
|
122
|
-
|
|
123
|
-
What the CommonJS format costs you, in your own code:
|
|
87
|
+
Node's SEA evaluates the embedded main as **CommonJS** — there is no
|
|
88
|
+
`package.json` inside the blob, and a `.mjs` name has no effect on an
|
|
89
|
+
embedded script — so the bundle is `--format=cjs`. Nothing in react-x11's
|
|
90
|
+
graph has a top-level await, so that builds. What the CommonJS format costs
|
|
91
|
+
you, in your own code:
|
|
124
92
|
|
|
125
93
|
- **No top-level `await`.** Put startup in an `async function main()`;
|
|
126
94
|
`const root = await createRoot()` at the top level of your entry is enough
|
|
127
|
-
to fail the build. esbuild names the file and line
|
|
128
|
-
five-second fix rather than a mystery.
|
|
95
|
+
to fail the build. esbuild names the file and line.
|
|
129
96
|
- **`import.meta.url` is `undefined`.** Anything resolving paths through it
|
|
130
|
-
needs `process.execPath` or a literal
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fails when first used rather than at startup: a pane hands its `fallback`
|
|
138
|
-
a `spawn`-phase `ERR_INVALID_URL` (and is an empty box without one), and
|
|
139
|
-
`createRoot()` with `REACT_X11_CLICK_TO_COMPONENT` set rejects with
|
|
140
|
-
`ERR_INVALID_ARG_TYPE`. Both measured on the SEA below.
|
|
97
|
+
needs `process.execPath` or a literal; react-x11 guards its own uses. Two
|
|
98
|
+
features cannot be guarded, because what they resolve has to exist on
|
|
99
|
+
disk, and neither works in a SEA. Each fails when first used rather than
|
|
100
|
+
at startup: `<Frame>` forks a child entry, and a pane hands its `fallback`
|
|
101
|
+
a `spawn`-phase `ERR_INVALID_URL` (and is an empty box without one);
|
|
102
|
+
click-to-component opens your source, and `createRoot()` with
|
|
103
|
+
`REACT_X11_CLICK_TO_COMPONENT` set rejects with `ERR_INVALID_ARG_TYPE`.
|
|
141
104
|
- **Runtime module loading is out.** Inside a SEA, the embedded main's
|
|
142
105
|
`require()` _and_ `import()` resolve **built-in modules only** — a `data:`
|
|
143
106
|
or `file:` URL import fails with `ERR_UNKNOWN_BUILTIN_MODULE`. A bundle has
|
|
144
107
|
nothing left to resolve, so this only bites if you meant to load something
|
|
145
|
-
later
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
CommonJS loader rooted at the executable, and how the cocoa addon gets in.
|
|
108
|
+
later (an ESM bundle carried as a SEA asset cannot be imported either).
|
|
109
|
+
The one door left open is `createRequire(process.execPath)` from
|
|
110
|
+
`node:module` — a full CommonJS loader rooted at the executable.
|
|
149
111
|
- **Assets are not files.** `sea.getAsset()` reads what the config's
|
|
150
112
|
`assets` map embedded; fonts are the usual case, and `StaticFontSource`
|
|
151
113
|
takes bytes directly ([ntk's fonts guide][ntk-fonts]).
|
|
152
114
|
|
|
153
115
|
On macOS the binary must be re-signed before it runs
|
|
154
116
|
(`codesign --remove-signature myapp && codesign --sign - myapp`); on Linux
|
|
155
|
-
nothing extra.
|
|
117
|
+
nothing extra.
|
|
156
118
|
|
|
157
119
|
**The cocoa backend works in a SEA, with the addon beside the binary.** The
|
|
158
|
-
native bridge is a `.node` file
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
[tier 5](#developer-id-or-the-app-store) — this recipe inside a bundle —
|
|
171
|
-
ran without the define it used to need. Earlier releases made that loader
|
|
172
|
-
at import time, from
|
|
173
|
-
`import.meta.url`, and every cocoa SEA died with `ERR_INVALID_ARG_VALUE`
|
|
174
|
-
before the backend could say what it was loading; on a release you cannot
|
|
175
|
-
move off, `--define:import.meta.url='"file:///dev/null"'` at bundle time is
|
|
176
|
-
the workaround, and `test/cocoa-native.test.js` keeps the fix.
|
|
177
|
-
|
|
178
|
-
A SEA runs the cocoa backend in pump mode, on the main thread: the move
|
|
179
|
-
onto a worker (below) starts the worker on the entry file, and a SEA's
|
|
180
|
-
entry is inside the binary, where a worker cannot load it from. So a menu,
|
|
181
|
-
a drag or a live resize stops its JS, as every cocoa app's did before
|
|
182
|
-
threaded mode.
|
|
120
|
+
native bridge is a `.node` file the blob cannot carry, so react-x11 loads it
|
|
121
|
+
through `createRequire(process.execPath)`, which resolves from the
|
|
122
|
+
executable's own directory. Either point `REACT_X11_CALAYERS_PATH` at the
|
|
123
|
+
addon (the `@windowkit/appkit` package directory or its `calayers.node`
|
|
124
|
+
prebuild, the way `examples/form/main.js` names it inside a compiled
|
|
125
|
+
bundle), or ship `node_modules/@windowkit/appkit` next to the executable and
|
|
126
|
+
let the bare name resolve. With neither, the error names both attempts.
|
|
127
|
+
|
|
128
|
+
A SEA runs the cocoa backend in pump mode, on the main thread — its entry
|
|
129
|
+
is inside the binary, where a worker cannot load it from — so a menu, a
|
|
130
|
+
drag or a live resize stops its JS
|
|
131
|
+
([below](#on-macos-the-app-moves-onto-a-worker)).
|
|
183
132
|
|
|
184
133
|
[ntk-fonts]: https://github.com/sidorares/ntk/blob/master/docs/fonts.md
|
|
185
134
|
|
|
@@ -202,15 +151,14 @@ metadata rather than anything about JavaScript.
|
|
|
202
151
|
the size.
|
|
203
152
|
|
|
204
153
|
Nothing here is react-x11-specific — any Node desktop app packages the same
|
|
205
|
-
way
|
|
154
|
+
way.
|
|
206
155
|
|
|
207
156
|
## Tier 5 — a macOS `.app`
|
|
208
157
|
|
|
209
|
-
The cocoa backend's counterpart to tier 4
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
centre first among them ([notifications.md](notifications.md)).
|
|
158
|
+
The cocoa backend's counterpart to tier 4: a wrapper around tier 2 or tier
|
|
159
|
+
3 whose content is metadata. Several integrations are things macOS grants
|
|
160
|
+
**a bundle** and refuses a loose executable — the notification centre first
|
|
161
|
+
among them ([notifications.md](notifications.md)).
|
|
214
162
|
|
|
215
163
|
```sh
|
|
216
164
|
APP=Downloads.app
|
|
@@ -238,14 +186,12 @@ $APP/Contents/MacOS/Downloads app.mjs # or app.cjs from tier 3
|
|
|
238
186
|
|
|
239
187
|
**The executable must be the real binary, in `Contents/MacOS/`.** A shell
|
|
240
188
|
script that execs node from elsewhere does not work: `NSBundle.mainBundle` is
|
|
241
|
-
resolved from the running executable's own path
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
bundle wraps decides where the app's JS runs: a tier 2 bundle beside the
|
|
245
|
-
binary moves onto a worker, a SEA stays on the pump
|
|
189
|
+
resolved from the running executable's own path. Copy the binary in (or ship
|
|
190
|
+
a tier 3 SEA). Which tier the bundle wraps decides where the app's JS runs: a
|
|
191
|
+
tier 2 bundle beside the binary moves onto a worker, a SEA stays on the pump
|
|
246
192
|
([below](#on-macos-the-app-moves-onto-a-worker)).
|
|
247
193
|
|
|
248
|
-
What the bundle changes
|
|
194
|
+
What the bundle changes:
|
|
249
195
|
|
|
250
196
|
| | inside the `.app` | bare `node` |
|
|
251
197
|
| ----------------------------------------- | ----------------------- | ------------------ |
|
|
@@ -264,39 +210,31 @@ Apple Events ([uri-schemes.md](uri-schemes.md)).
|
|
|
264
210
|
An **ad-hoc signature (`--sign -`) is enough to be a bundle, and not enough to
|
|
265
211
|
post a notification.** With one, the centre reports `available: true` and
|
|
266
212
|
every authorization request fails with `UNErrorDomain` code 1
|
|
267
|
-
(`notificationsNotAllowed`)
|
|
268
|
-
`notDetermined
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
of exec'ing the binary, `activationPolicy: 'regular'` with a real window on
|
|
272
|
-
screen, and re-signing with the hardened runtime and node's own entitlements.
|
|
273
|
-
None of them move it.
|
|
213
|
+
(`notificationsNotAllowed`): the prompt never appears and the status stays
|
|
214
|
+
`notDetermined`. Registering with `lsregister -f`, launching through
|
|
215
|
+
`open -a`, `activationPolicy: 'regular'` and the hardened runtime do not
|
|
216
|
+
change that.
|
|
274
217
|
|
|
275
|
-
The
|
|
276
|
-
|
|
218
|
+
The documented requirement is a real Apple Development or Developer ID
|
|
219
|
+
certificate, which a free Apple ID gets you through Xcode (not tested here):
|
|
277
220
|
|
|
278
221
|
```sh
|
|
279
222
|
security find-identity -v -p codesigning # pick one
|
|
280
223
|
codesign --force --sign "Apple Development: you@example.com (TEAMID)" $APP
|
|
281
224
|
```
|
|
282
225
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
through to `osascript`, which delivers real banners attributed to Script
|
|
289
|
-
Editor, and `useNotifier().backend` says `osascript` so an app can tell the
|
|
290
|
-
difference ([notifications.md](notifications.md)). Nothing else about the
|
|
291
|
-
bundle depends on the certificate.
|
|
226
|
+
Without one, `notify()` falls through to `osascript`, which delivers real
|
|
227
|
+
banners attributed to Script Editor, and `useNotifier().backend` says
|
|
228
|
+
`osascript` so an app can tell the difference
|
|
229
|
+
([notifications.md](notifications.md)). Nothing else about the bundle
|
|
230
|
+
depends on the certificate.
|
|
292
231
|
|
|
293
232
|
### Developer ID, or the App Store
|
|
294
233
|
|
|
295
|
-
Past the ad-hoc signature there are two ways
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
everything past it is Apple's documentation, marked as such.
|
|
234
|
+
Past the ad-hoc signature there are two ways to distribute, and they want
|
|
235
|
+
different runtimes. The sandboxed, ad-hoc-signed half below was run (macOS
|
|
236
|
+
15.2, bun 1.4.0, node 26); the steps that need a real identity follow
|
|
237
|
+
Apple's documentation and were not run.
|
|
300
238
|
|
|
301
239
|
| | Developer ID — a download | Mac App Store |
|
|
302
240
|
| ---------------- | -------------------------------------------------- | ---------------------------------------------------- |
|
|
@@ -307,52 +245,23 @@ everything past it is Apple's documentation, marked as such.
|
|
|
307
245
|
| identity | Developer ID Application | Apple Distribution, plus an installer identity |
|
|
308
246
|
| then | `notarytool`, `stapler` | `productbuild`, Transporter |
|
|
309
247
|
|
|
310
|
-
**
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
`
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
- **inside the bundle**: `SIGABRT` in HIServices' `_RegisterApplication`,
|
|
321
|
-
under `-[NSApplication init]`, after the kernel logs
|
|
322
|
-
`Sandbox: Guestbook deny(1) mach-lookup com.apple.coreservices.launchservicesd`.
|
|
323
|
-
|
|
324
|
-
Measured not to be the cause: how it is launched (exec'd from a shell, in
|
|
325
|
-
the foreground under a watchdog, or by `open`), the plist (the example's
|
|
326
|
-
and a minimal one), and where the bundle sits. Upstream:
|
|
327
|
-
[oven-sh/bun#15661][bun-15661], "Cannot run Bun within macOS sandbox", open
|
|
328
|
-
since December 2024 — its "status code 5" is this SIGTRAP.
|
|
329
|
-
|
|
330
|
-
The same example, sandboxed with the same entitlement and the same ad-hoc
|
|
331
|
-
signature, runs as node: as an esbuild `.mjs` (tier 2) under a copied
|
|
332
|
-
`node` binary, and as a `node --build-sea` executable (tier 3). Both stayed
|
|
333
|
-
up with the window on screen and logged no `Sandbox: … deny` line at all;
|
|
334
|
-
the SEA, launched by `open`, registers with Launch Services as itself and
|
|
335
|
-
gets its container:
|
|
336
|
-
|
|
337
|
-
```
|
|
338
|
-
$ open SeaGuestbook.app
|
|
339
|
-
"LSDisplayName"="SeaGuestbook"
|
|
340
|
-
"CFBundleIdentifier"="com.example.seaguestbook"
|
|
341
|
-
container: ~/Library/Containers/com.example.seaguestbook
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
The control: a five-line Swift AppKit app (`NSApplication.shared`, print
|
|
345
|
-
the container id), ad-hoc-signed with the same entitlement, runs and
|
|
346
|
-
reports its container. So an ad-hoc signature plus the entitlement is a
|
|
347
|
-
valid local sandbox test, and no certificate is needed for this part.
|
|
248
|
+
**Bun does not start under the App Sandbox**, which the Mac App Store
|
|
249
|
+
requires (one entitlement, `com.apple.security.app-sandbox`). A
|
|
250
|
+
`bun build --compile` binary signed with it crashes at launch: `SIGTRAP` in
|
|
251
|
+
`_libsecinit_appsandbox` before any JavaScript runs, or, inside a bundle,
|
|
252
|
+
`SIGABRT` in `_RegisterApplication` after a
|
|
253
|
+
`Sandbox: … deny(1) mach-lookup com.apple.coreservices.launchservicesd`
|
|
254
|
+
([oven-sh/bun#15661][bun-15661]). The same app as node — a tier 2 `.mjs`
|
|
255
|
+
under a copied `node`, or a tier 3 SEA — runs sandboxed. An ad-hoc
|
|
256
|
+
signature plus the entitlement is a valid local sandbox test; no
|
|
257
|
+
certificate is needed for it.
|
|
348
258
|
|
|
349
259
|
**The SEA is the store shape.** Launch Services starts a bundle's
|
|
350
260
|
executable with no arguments, so a copied `node` has nothing to run; the
|
|
351
|
-
SEA carries its main.
|
|
352
|
-
|
|
353
|
-
`MODULE_NOT_FOUND
|
|
354
|
-
|
|
355
|
-
one esbuild define that the form example needs today:
|
|
261
|
+
SEA carries its main. The sandbox also moves the working directory into the
|
|
262
|
+
container (`~/Library/Containers/<bundle id>/Data`), so a relative script
|
|
263
|
+
path fails with `MODULE_NOT_FOUND`; a SEA has no path to get wrong. Its
|
|
264
|
+
build is tier 3's, with one esbuild define that the form example needs:
|
|
356
265
|
|
|
357
266
|
```sh
|
|
358
267
|
esbuild sea-main.js --bundle --platform=node --format=cjs --jsx=automatic --outfile=app.cjs \
|
|
@@ -360,46 +269,34 @@ esbuild sea-main.js --bundle --platform=node --format=cjs --jsx=automatic --outf
|
|
|
360
269
|
node --build-sea sea.json # { "main": "app.cjs", "output": "Guestbook", "disableExperimentalSEAWarning": true }
|
|
361
270
|
```
|
|
362
271
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
});
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
- **`import.meta.url` needs no define any more.** This recipe used to
|
|
395
|
-
carry a second one, `--define:import.meta.url='"file:///dev/null"'`,
|
|
396
|
-
because the cocoa backend's addon loader was made from `import.meta.url`
|
|
397
|
-
at import and a SEA has none. The loader is now made from
|
|
398
|
-
`process.execPath` when the URL is missing
|
|
399
|
-
([tier 3](#tier-3--node-single-executable)), and the addon is found
|
|
400
|
-
through `REACT_X11_CALAYERS_PATH` as before. On a react-x11 whose
|
|
401
|
-
`src/cocoa/native.js` still calls `createRequire(import.meta.url)` at
|
|
402
|
-
module scope, put the define back.
|
|
272
|
+
`index.jsx` mounts itself under a `REACT_X11_NO_AUTORUN` guard with a
|
|
273
|
+
top-level `await createRoot()`, which `cjs` cannot express; defined, the
|
|
274
|
+
guard is a constant, the branch is dropped at build time, and the entry
|
|
275
|
+
mounts `App` itself, in an async function:
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
// sea-main.js — main.js's job, as CommonJS
|
|
279
|
+
const path = require('node:path');
|
|
280
|
+
const contents = path.resolve(path.dirname(process.execPath), '..');
|
|
281
|
+
process.env.REACT_X11_CALAYERS_PATH ??= path.join(
|
|
282
|
+
contents,
|
|
283
|
+
'Resources',
|
|
284
|
+
'calayers.node',
|
|
285
|
+
);
|
|
286
|
+
process.env.REACT_X11_BACKEND ??= 'cocoa';
|
|
287
|
+
(async () => {
|
|
288
|
+
const [{ createRoot }, { default: App }, React] = await Promise.all([
|
|
289
|
+
import('react-x11'),
|
|
290
|
+
import('./index.jsx'),
|
|
291
|
+
import('react'),
|
|
292
|
+
]);
|
|
293
|
+
const root = await createRoot();
|
|
294
|
+
root.render(React.createElement(App));
|
|
295
|
+
})().catch((e) => {
|
|
296
|
+
console.error(e);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
});
|
|
299
|
+
```
|
|
403
300
|
|
|
404
301
|
The bundle is then tier 5's — the plist, the SEA at
|
|
405
302
|
`Contents/MacOS/Guestbook`, `calayers.node` in `Contents/Resources` — and
|
|
@@ -411,18 +308,14 @@ codesign --force --sign - --entitlements sandbox.plist Guestbook.app
|
|
|
411
308
|
open Guestbook.app
|
|
412
309
|
```
|
|
413
310
|
|
|
414
|
-
where `sandbox.plist` holds the one key.
|
|
311
|
+
where `sandbox.plist` holds the one key.
|
|
415
312
|
|
|
416
|
-
**Sign inside-out, and never `--deep
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
entitlements on the main executable only: "Do not apply entitlements to
|
|
423
|
-
library code. It doesn't do anything useful and can prevent your code from
|
|
424
|
-
running." `--deep` applies one set of options to every item, which is the
|
|
425
|
-
wrong thing the moment the app has entitlements and the addon must not.
|
|
313
|
+
**Sign inside-out, and never `--deep`** (Apple DTS, [forums
|
|
314
|
+
128166][dts-128166], [701514][dts-701514]). Sign each code item separately,
|
|
315
|
+
dependencies first — the addon before the app, because the app's signature
|
|
316
|
+
seals the addon's — and put entitlements on the main executable only.
|
|
317
|
+
`--deep` applies one set of options to every item, which is wrong the moment
|
|
318
|
+
the app has entitlements and the addon must not.
|
|
426
319
|
|
|
427
320
|
**Developer ID — not run.** For a download outside the store, Gatekeeper
|
|
428
321
|
wants a Developer ID signature with the hardened runtime, then
|
|
@@ -444,21 +337,15 @@ xcrun stapler staple Guestbook.app
|
|
|
444
337
|
`com.apple.security.cs.allow-jit`, `allow-unsigned-executable-memory`,
|
|
445
338
|
`disable-executable-page-protection`, `allow-dyld-environment-variables`
|
|
446
339
|
and `disable-library-validation`. Node's file has a sixth,
|
|
447
|
-
`com.apple.security.get-task-allow` — a debugging entitlement,
|
|
448
|
-
`node` you copy from a release carries it
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
V8 unable to map executable memory — and runs with `allow-jit` alone; a
|
|
457
|
-
`bun build --compile` binary ran with none, with `allow-jit` alone, and
|
|
458
|
-
with all five (whether its JIT was on was not measured). So `allow-jit` is
|
|
459
|
-
the load-bearing one for node, and the other four are what upstream
|
|
460
|
-
ships, not what this page found necessary. The Developer ID identity,
|
|
461
|
-
`notarytool` and `stapler` were not run: the machine has none.
|
|
340
|
+
`com.apple.security.get-task-allow` — a debugging entitlement, which the
|
|
341
|
+
`node` you copy from a release carries. Never ship it; a re-sign replaces
|
|
342
|
+
node's entitlements wholesale, so it only appears if your plist names it.
|
|
343
|
+
|
|
344
|
+
Under the hardened runtime a copied `node` signed with no entitlements
|
|
345
|
+
aborts at startup with
|
|
346
|
+
`Fatal process out of memory: Failed to reserve virtual memory for CodeRange`;
|
|
347
|
+
`allow-jit` alone is enough for it to run. A `bun build --compile` binary
|
|
348
|
+
ran with none. The other four are what upstream ships.
|
|
462
349
|
|
|
463
350
|
**Mac App Store — not run.** Sign the same bundle, inside-out, with the
|
|
464
351
|
store identity, and package it as an installer:
|
|
@@ -475,13 +362,12 @@ xcrun altool --upload-app -f Guestbook.pkg -t macos --apiKey KEY --apiIssuer ISS
|
|
|
475
362
|
`store.plist` is `com.apple.security.app-sandbox` plus what the app
|
|
476
363
|
actually needs — `network.client`, `files.user-selected.read-write` — and
|
|
477
364
|
none of the hardened-runtime keys: the store does not require the hardened
|
|
478
|
-
runtime, and
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
identity, `productbuild --sign` and the upload are Apple's documentation.
|
|
365
|
+
runtime, and `allow-jit` is needed only under it.
|
|
366
|
+
`Contents/embedded.provisionprofile` is needed only for restricted
|
|
367
|
+
entitlements (iCloud, push, app groups) or TestFlight.
|
|
368
|
+
|
|
369
|
+
Design notes — the sandbox crash investigation, what was measured and how:
|
|
370
|
+
[design/packaging.md](../design/packaging.md#developer-id-or-the-app-store).
|
|
485
371
|
|
|
486
372
|
[bun-15661]: https://github.com/oven-sh/bun/issues/15661
|
|
487
373
|
[dts-128166]: https://developer.apple.com/forums/thread/128166
|
|
@@ -492,32 +378,21 @@ identity, `productbuild --sign` and the upload are Apple's documentation.
|
|
|
492
378
|
## On macOS: the app moves onto a worker
|
|
493
379
|
|
|
494
380
|
On macOS the first import of react-x11 moves the app onto a
|
|
495
|
-
`worker_threads` Worker. AppKit keeps the process main thread —
|
|
496
|
-
|
|
497
|
-
|
|
381
|
+
`worker_threads` Worker. AppKit keeps the process main thread — the only
|
|
382
|
+
thread it lets own a window — and an app whose JS ran there would stop
|
|
383
|
+
through every menu, drag, live resize and modal panel
|
|
498
384
|
([macos.md](macos.md#js-on-a-worker-a-ui-thread-of-the-bridges-own)). The
|
|
499
385
|
worker's entry is your entry itself, run from the top: when react-x11 is
|
|
500
386
|
imported the entry has not run a line yet, because its imports finish
|
|
501
387
|
before its body does. The main thread then waits, and launches AppKit only
|
|
502
388
|
when the app's first cocoa `createRoot` asks for it.
|
|
503
389
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
| Node, a tier 2 bundle | 427 ms | 463 ms | — |
|
|
511
|
-
| Bun, the source | 408 ms | 446 ms | 385 ms |
|
|
512
|
-
| Bun, a tier 2 bundle | 401 ms | 426 ms | — |
|
|
513
|
-
| memory, Node / Bun | 170 / 150 MB | 219 / 182 MB | 186 / 159 MB |
|
|
514
|
-
|
|
515
|
-
The move's 25–73 ms and 30–50 MB are the main thread loading the entry's
|
|
516
|
-
imports once, for nothing, before the worker loads them again; a bundle is
|
|
517
|
-
at the low end because one file loads faster than a tree of them. An app
|
|
518
|
-
that imports react-x11 and never makes a cocoa root — an X11 app on
|
|
519
|
-
XQuartz, a script — never launches AppKit, and pays 95–131 ms more to exit
|
|
520
|
-
(Node 251 → 382 ms, Bun 196 → 291 ms).
|
|
390
|
+
The move costs 25–73 ms of startup and 30–50 MB: the main thread loads the
|
|
391
|
+
entry's imports once before the worker loads them again (a tier 2 bundle is
|
|
392
|
+
at the low end). An app that imports react-x11 and never makes a cocoa root
|
|
393
|
+
— an X11 app on XQuartz, a script — never launches AppKit, and takes
|
|
394
|
+
95–131 ms longer to exit. Measurements:
|
|
395
|
+
[design/packaging.md](../design/packaging.md#on-macos-the-app-moves-onto-a-worker).
|
|
521
396
|
|
|
522
397
|
To skip the cost, or the move:
|
|
523
398
|
|
|
@@ -554,20 +429,18 @@ Node 20 and 22, under an ES module loader. By Node version, measured on
|
|
|
554
429
|
| under `--import react-x11/cocoa-main` | moves | moves |
|
|
555
430
|
| importing react-x11 with an `import()` | stays | stays |
|
|
556
431
|
|
|
557
|
-
An app under a loader stays on Node 20 and 22
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
24.11.1, and an older one there stops the app as it starts, on
|
|
566
|
-
`ERR_UNKNOWN_FILE_EXTENSION` (measured, tsx 4.20.6): update tsx, or set
|
|
432
|
+
An app under a loader stays on the main thread on Node 20 and 22: tsx, the
|
|
433
|
+
likeliest loader, keeps its hooks to the main thread there, and a worker
|
|
434
|
+
without them stops on the first `.jsx` or `.ts` file it loads. Any
|
|
435
|
+
`--import` or `--loader`, on the command line or in `NODE_OPTIONS`, counts.
|
|
436
|
+
The launcher moves such an app all the same, for a loader that does follow
|
|
437
|
+
it onto a worker (tsx 4.23 does from Node 22.22.3). On Node 24 and later
|
|
438
|
+
tsx follows from tsx 4.21.1 and Node 24.11.1; an older tsx there stops the
|
|
439
|
+
app as it starts with `ERR_UNKNOWN_FILE_EXTENSION` — update tsx, or set
|
|
567
440
|
`REACT_X11_THREADED=0`. Bun moves an ES module entry and a CommonJS one,
|
|
568
441
|
`bun app.mjs` and `bun app.cjs`, and keeps the main thread for an entry
|
|
569
|
-
that imports react-x11 with an `import()
|
|
570
|
-
|
|
442
|
+
that imports react-x11 with an `import()`; it tells a late import its own
|
|
443
|
+
way ([macos.md](macos.md#what-changes-for-an-app)).
|
|
571
444
|
|
|
572
445
|
## Checklist
|
|
573
446
|
|
|
@@ -590,22 +463,6 @@ late import its own way ([macos.md](macos.md#the-renderers-half)).
|
|
|
590
463
|
never `--deep`.
|
|
591
464
|
- Test on a display you did not develop on. Fonts are the usual surprise:
|
|
592
465
|
family resolution goes through `fc-match`, so `sans-serif` is a different
|
|
593
|
-
face on the target ([remote.md](remote.md#the-other-x-servers)
|
|
594
|
-
|
|
595
|
-
## Upstream
|
|
596
|
-
|
|
597
|
-
Two things would delete the rest of the friction here:
|
|
598
|
-
|
|
599
|
-
- **node-x11: ESM sources** ([node-x11#246][x11-246]). That removes tier 2's
|
|
600
|
-
banner, which is now the only real trap on the page. Note the tempting
|
|
601
|
-
cheaper version does not work: `node:`-prefixed requires change nothing,
|
|
602
|
-
because esbuild wraps a CommonJS module either way and its `require` shim
|
|
603
|
-
throws in ESM output regardless of the specifier — measured, not assumed.
|
|
604
|
-
- **bun: App Sandbox** ([oven-sh/bun#15661][bun-15661]). Until a
|
|
605
|
-
`bun build --compile` binary can start sandboxed, a Mac App Store build is
|
|
606
|
-
node's SEA and bun is a Developer ID runtime only.
|
|
607
|
-
|
|
608
|
-
ESM support for a SEA's embedded main would be welcome in Node, but it is no
|
|
609
|
-
longer load-bearing: tier 3 works as CommonJS.
|
|
466
|
+
face on the target ([remote.md](remote.md#the-other-x-servers)).
|
|
610
467
|
|
|
611
468
|
[x11-246]: https://github.com/sidorares/node-x11/issues/246
|