@windowkit/appkit 0.1.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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2026 by Andrey Sidorov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @windowkit/appkit
2
+
3
+ A **retained-mode AppKit backend for Node.js**: Core Animation (CALayer)
4
+ layer trees, CoreText layout and drawing, IOSurface presentation, NSMenu and
5
+ native control bezels. It is the macOS half of [react-x11][react-x11].
6
+ Instead of immediate-mode draw calls, you build a persistent tree of layers, mutate their
7
+ properties, and let the macOS WindowServer composite on the GPU — with implicit animations
8
+ and correct retina handling for free.
9
+
10
+ Built as a drawing-backend experiment for [react-x11]-style reconcilers: React elements map
11
+ 1:1 to layers, and `commitUpdate` becomes `layer.set(props)`.
12
+
13
+ ```bash
14
+ npm install # builds the native addon (macOS only, needs Xcode CLT)
15
+ npm run demo # hover/click the cards; press "q" to quit
16
+ ```
17
+
18
+ ## What the demo shows
19
+
20
+ - **CALayer tree** — frame/bounds/position, backgroundColor, cornerRadius, borderWidth,
21
+ shadows, opacity, zPosition, `masksToBounds` clipping
22
+ - **Implicit animations** — hover/click a card: plain `layer.set({...})` property changes
23
+ animate at 0.25s automatically
24
+ - **CATransaction** — the orange ball tweens over 1.1s with easeInEaseOut just by grouping
25
+ a `position` change in a transaction
26
+ - **Explicit CABasicAnimation** — the spinner runs two infinite animations
27
+ (`transform.rotation.z` + `strokeEnd`) entirely in the render server; they stay smooth
28
+ even if the JS thread stalls
29
+ - **CATextLayer** — retina-crisp text composited by the WindowServer
30
+ - **CoreText** — glyphs measured (`CTLine`) and rasterized (`CTFramesetter` → `CGImage`)
31
+ then set as `layer.contents`, i.e. the glyph-atlas path
32
+ - **CAGradientLayer + layer.mask** — the footer is a gradient masked by a text layer
33
+ - **CAShapeLayer** — CGPath commands, stroke/fill, dash patterns, animatable `strokeEnd`
34
+ - **Hit testing** — native `-[CALayer hitTest:]` mapped back to JS wrapper objects
35
+ - **Events** — mouse/keyboard from the NSApp event pump delivered to a JS callback
36
+ - **Native controls** — push buttons (incl. accent-filled default), checkboxes, radios,
37
+ popup buttons, sliders, and switches rendered by AppKit itself and composited as layer
38
+ contents; fully interactive (pressed states, toggles, slider drag) and re-renderable in
39
+ dark/light appearance (the "Light / Dark" button flips all of them live)
40
+
41
+ ## How it runs
42
+
43
+ Node's main thread *is* the process main thread on macOS, so the addon owns
44
+ `NSApplication` directly. Nobody calls `[NSApp run]`; instead JS drives an event pump
45
+ (`nextEventMatchingMask:` with `distantPast`) off a `setInterval`. Core Animation
46
+ animations execute in the render server, so their smoothness is independent of the pump
47
+ cadence — the JS timer only affects input latency.
48
+
49
+ The window uses a **layer-hosting** `NSView` (we own the whole CALayer tree) with
50
+ `isFlipped = YES`, which makes AppKit give the hosted layer a top-left origin
51
+ (`geometryFlipped`) — coordinates match what a UI toolkit expects. Two flip gotchas are
52
+ handled in native code: `hitTest:` still takes bottom-up points, and
53
+ `renderInContext:` ignores `geometryFlipped` entirely (snapshots therefore capture the
54
+ window's real composited pixels via `CGWindowListCreateImage`, which needs no
55
+ screen-recording permission for the process's own windows).
56
+
57
+ ## API sketch
58
+
59
+ ```js
60
+ const ca = require('@windowkit/appkit');
61
+ const { app, Window, Layer, TextLayer, GradientLayer, ShapeLayer,
62
+ transaction, withoutAnimations } = ca;
63
+
64
+ const win = new Window({ width: 800, height: 560, title: 'hi' });
65
+
66
+ const card = new Layer();
67
+ card.set({
68
+ frame: [32, 108, 228, 128], // top-left origin, points (not pixels)
69
+ backgroundColor: [0.98, 0.42, 0.36, 1],
70
+ cornerRadius: 14,
71
+ shadowOpacity: 0.5, shadowRadius: 12, shadowOffset: [0, 6],
72
+ });
73
+ win.root.add(card);
74
+
75
+ // implicit animation: just set the property
76
+ card.set({ backgroundColor: [0.36, 0.65, 0.98, 1] });
77
+
78
+ // batched, with custom duration/curve
79
+ transaction(() => card.set({ position: [400, 300] }),
80
+ { duration: 1.1, timing: 'easeInEaseOut' });
81
+
82
+ // no animation (e.g. initial tree construction, reconciler commits)
83
+ withoutAnimations(() => card.set({ opacity: 0.5 }));
84
+
85
+ // explicit animation on any animatable keyPath
86
+ card.animate('transform.rotation.z',
87
+ { from: 0, to: Math.PI * 2, duration: 1, repeat: Infinity, timing: 'linear' });
88
+
89
+ // text, two ways
90
+ const label = new TextLayer();
91
+ label.set({ frame: [0, 14, 228, 22], contentsScale: win.scale })
92
+ .text({ string: 'hello', fontName: 'HelveticaNeue', fontSize: 15,
93
+ color: [1, 1, 1, 1], align: 'center' });
94
+ card.add(label);
95
+
96
+ const glyphs = ca.text.render({ text: 'CoreText', fontName: 'Menlo',
97
+ fontSize: 13, color: [1, 1, 1, 1], scale: win.scale });
98
+ new Layer().set({ frame: [10, 10, glyphs.width, glyphs.height] }).setImage(glyphs);
99
+ ca.text.measure({ text: 'CoreText', fontName: 'Menlo', fontSize: 13 });
100
+ // -> { width, ascent, descent, leading }
101
+
102
+ // masks, gradients, shapes
103
+ const g = new GradientLayer();
104
+ g.gradient({ colors: [[1, 0, 0, 1], [0, 0, 1, 1]], startPoint: [0, 0.5], endPoint: [1, 0.5] });
105
+ g.set({ mask: someTextLayer });
106
+
107
+ const shape = new ShapeLayer();
108
+ shape.shape({ path: [['move', 0, 0], ['line', 50, 80], ['arc', 25, 25, 20, 0, Math.PI, false]],
109
+ strokeColor: [1, 1, 1, 1], lineWidth: 4, lineCap: 'round', fillColor: null });
110
+
111
+ // input + hit testing
112
+ app.onEvent((ev) => { // mousedown/up/move/drag, wheel, keydown/up
113
+ const layer = win.hitTest(ev.x, ev.y); // deepest Layer wrapper or null
114
+ });
115
+
116
+ app.run({ onTick: () => { if (!win.visible) process.exit(0); } });
117
+
118
+ win.snapshot('/tmp/out.png'); // real composited pixels of the window
119
+ ```
120
+
121
+ ## Native controls
122
+
123
+ There is no WindowServer API for control drawing — AppKit draws controls in-process via
124
+ the **NSCell** architecture, and cells happily draw offscreen (the technique WebKit's
125
+ `RenderThemeMac` and Firefox's `nsNativeThemeCocoa` use for native form controls).
126
+ `ca.controls.render()` rasterizes a cell at retina scale under a chosen `NSAppearance`
127
+ and returns a `CGImage` for `layer.contents`:
128
+
129
+ ```js
130
+ const img = ca.controls.render({
131
+ kind: 'push', // 'push' | 'checkbox' | 'radio' | 'popup' | 'slider' | 'switch'
132
+ title: 'Click me',
133
+ pressed: false, // drive this from your own mouse events
134
+ state: 1, // on/off for checkbox/radio/switch
135
+ isDefault: true, // push: accent-filled default button
136
+ value: 0.5, // slider position
137
+ controlSize: 'regular', // 'mini' | 'small' | 'regular' | 'large'
138
+ appearance: 'dark', // 'system' | 'dark' | 'light'
139
+ scale: win.scale,
140
+ }); // -> { image, width, height, scale } (natural cellSize if
141
+ // width/height omitted)
142
+ new Layer().set({ frame: [x, y, img.width, img.height] }).setImage(img);
143
+ ```
144
+
145
+ Two render paths inside `drawControl`:
146
+
147
+ - **Cell path** (`NSButtonCell`, `NSPopUpButtonCell`): `drawWithFrame:inView:` into a
148
+ bitmap `NSGraphicsContext`, wrapped in `performAsCurrentDrawingAppearance:` so dark
149
+ mode and the user's accent color apply.
150
+ - **Offscreen-view path** (`NSSlider`, `NSSwitch`): modern `NSSliderCell` no longer
151
+ draws offscreen (it defers to the view's layer machinery), and `NSSwitch` has no cell
152
+ at all, so these render a real unparented `NSControl` via
153
+ `displayRectIgnoringOpacity:inContext:`.
154
+
155
+ The demo re-renders a control's image on each state change; a real renderer would cache
156
+ per `(kind, size, state, appearance)` and nine-slice-stretch bezels with
157
+ `layer.contentsCenter`. Menus/popovers are deliberately *not* painted — their
158
+ vibrancy materials need private API to reproduce; expose real `NSMenu` instead.
159
+
160
+ `native.postMouseEvent(win, 'down'|'up'|'move'|'drag', x, y)` synthesizes events through
161
+ the real pump — used by the demo's self-test (`CAL_CLICKS="x,y;x,y" npm run demo`).
162
+
163
+ ## Mapping to a React reconciler
164
+
165
+ The shape of a host config on top of this:
166
+
167
+ | Reconciler op | @windowkit/appkit |
168
+ | -------------------- | -------------------------------------------------------- |
169
+ | `createInstance` | `new Layer()` / `new TextLayer()` / ... per element type |
170
+ | `appendChild` | `parent.add(child)` |
171
+ | `removeChild` | `child.remove()` |
172
+ | `commitUpdate` | `layer.set(diffedProps)` |
173
+ | commit batch | wrap in `withoutAnimations()` (or a `transaction()` to get animated updates for free) |
174
+ | `getPublicInstance` | the `Layer` wrapper (hit-testing gives it back for events) |
175
+
176
+ Because the tree is retained and properties are mutable, the reconciler diff maps directly
177
+ onto layer mutations — no repaint pass, no damage rects; the WindowServer recomposites
178
+ only what changed.
179
+
180
+ ## Caveats (POC)
181
+
182
+ - The pump-on-a-timer model means live window resizing/dragging runs AppKit's internal
183
+ modal loops; input during those is choppy (Core Animation itself is unaffected).
184
+ - No `NSWindowDelegate` wiring yet — window resize is observable only by polling
185
+ `win.size`; sublayers don't autolayout (by design — the reconciler owns layout).
186
+ - One shared event callback for all windows; per-window routing would need the window
187
+ handle in the event payload.
188
+ - Layer handles are released on GC via External finalizers; native side keeps its own
189
+ retains through the layer tree, so lifetime is safe but not tuned.
190
+ - `x64`/`arm64` follows whatever node arch you build with; no prebuilds.
191
+
192
+ [react-x11]: https://github.com/sidorares/react-x11
package/binding.gyp ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "calayers",
5
+ "sources": ["src/addon.mm", "src/backend.mm"],
6
+ "include_dirs": [
7
+ "<!@(node -p \"require('node-addon-api').include\")"
8
+ ],
9
+ "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"],
10
+ "xcode_settings": {
11
+ "OTHER_CPLUSPLUSFLAGS": ["-fobjc-arc", "-std=c++17"],
12
+ "MACOSX_DEPLOYMENT_TARGET": "11.0",
13
+ "CLANG_ENABLE_OBJC_ARC": "YES"
14
+ },
15
+ "link_settings": {
16
+ "libraries": [
17
+ "-framework Cocoa",
18
+ "-framework QuartzCore",
19
+ "-framework CoreText",
20
+ "-framework CoreGraphics",
21
+ "-framework ImageIO",
22
+ "-framework IOSurface"
23
+ ]
24
+ }
25
+ }
26
+ ]
27
+ }
package/index.js ADDED
@@ -0,0 +1,184 @@
1
+ 'use strict';
2
+
3
+ if (process.platform !== 'darwin') {
4
+ throw new Error('@windowkit/appkit is macOS-only (Core Animation backend)');
5
+ }
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ // A local build wins (dev iteration), then the prebuilt binary bundled in
11
+ // the npm tarball for this platform/arch (see scripts/install.js — the
12
+ // package works even when install scripts are disabled), then a clear error.
13
+ function loadNative() {
14
+ const candidates = [
15
+ 'build/Release/calayers.node',
16
+ 'build/Debug/calayers.node',
17
+ `prebuilds/${process.platform}-${process.arch}/calayers.node`,
18
+ ];
19
+ const errors = [];
20
+ for (const rel of candidates) {
21
+ const abs = path.join(__dirname, rel);
22
+ if (!fs.existsSync(abs)) continue;
23
+ try {
24
+ return require(abs);
25
+ } catch (e) {
26
+ errors.push(` ${rel}: ${e.message}`);
27
+ }
28
+ }
29
+ throw new Error(
30
+ `@windowkit/appkit: no loadable native binary for ${process.platform}-${process.arch}\n` +
31
+ (errors.length ? `tried:\n${errors.join('\n')}\n` : '') +
32
+ 'rebuild with: npm rebuild @windowkit/appkit --build-from-source (needs the Xcode command-line tools)',
33
+ );
34
+ }
35
+
36
+ const native = loadNative();
37
+
38
+ // name -> wrapper, so native hitTest results map back to JS objects
39
+ const layersByName = new Map();
40
+ let seq = 0;
41
+
42
+ class Layer {
43
+ constructor(handle) {
44
+ this._h = handle || native.createLayer();
45
+ this._name = 'layer:' + ++seq;
46
+ native.setLayerProps(this._h, { name: this._name });
47
+ layersByName.set(this._name, this);
48
+ this.parent = null;
49
+ this.children = [];
50
+ }
51
+
52
+ // Retained-mode property update. Changes to position/bounds/backgroundColor/
53
+ // opacity/cornerRadius/transform/... on layers already in a tree get implicit
54
+ // 0.25s animations from Core Animation unless wrapped in withoutAnimations().
55
+ set(props) {
56
+ if (props.mask instanceof Layer) props = { ...props, mask: props.mask._h };
57
+ native.setLayerProps(this._h, props);
58
+ return this;
59
+ }
60
+
61
+ add(child) {
62
+ native.addSublayer(this._h, child._h);
63
+ child.parent = this;
64
+ this.children.push(child);
65
+ return child;
66
+ }
67
+
68
+ remove() {
69
+ native.removeFromSuperlayer(this._h);
70
+ if (this.parent) {
71
+ const i = this.parent.children.indexOf(this);
72
+ if (i >= 0) this.parent.children.splice(i, 1);
73
+ this.parent = null;
74
+ }
75
+ }
76
+
77
+ // Explicit CABasicAnimation on any animatable keyPath, e.g.
78
+ // 'transform.rotation.z', 'position', 'opacity', 'strokeEnd', 'backgroundColor'.
79
+ animate(keyPath, opts = {}, key = keyPath) {
80
+ native.addAnimation(this._h, keyPath, opts, key);
81
+ return this;
82
+ }
83
+
84
+ removeAnimation(key) { native.removeAnimation(this._h, key); }
85
+ removeAllAnimations() { native.removeAllAnimations(this._h); }
86
+
87
+ // img is the result of text.render() (or any {image, scale})
88
+ setImage(img, scale) {
89
+ native.setContentsImage(this._h, img.image, scale ?? img.scale);
90
+ return this;
91
+ }
92
+ }
93
+
94
+ class TextLayer extends Layer {
95
+ constructor() { super(native.createTextLayer()); }
96
+ // {string, fontName, fontSize, color, align, wrapped, truncation}
97
+ text(props) { native.setTextProps(this._h, props); return this; }
98
+ }
99
+
100
+ class GradientLayer extends Layer {
101
+ constructor() { super(native.createGradientLayer()); }
102
+ // {colors: [[r,g,b,a],...], locations, startPoint, endPoint, type}
103
+ gradient(props) { native.setGradientProps(this._h, props); return this; }
104
+ }
105
+
106
+ class ShapeLayer extends Layer {
107
+ constructor() { super(native.createShapeLayer()); }
108
+ // {path: [['move',x,y],['line',x,y],['arc',cx,cy,r,a0,a1,cw],...],
109
+ // fillColor, strokeColor, lineWidth, strokeStart, strokeEnd, lineCap, ...}
110
+ shape(props) { native.setShapeProps(this._h, props); return this; }
111
+ }
112
+
113
+ class Window {
114
+ constructor({ width = 640, height = 480, title = '' } = {}) {
115
+ native.initApp();
116
+ this._h = native.createWindow(width, height, title);
117
+ this.root = new Layer(native.windowRootLayer(this._h));
118
+ this.scale = native.windowScale(this._h); // backing scale (2 on retina)
119
+ }
120
+ get size() {
121
+ const s = native.windowContentSize(this._h);
122
+ return { width: s[0], height: s[1] };
123
+ }
124
+ get visible() { return native.windowIsVisible(this._h); }
125
+ hitTest(x, y) {
126
+ const name = native.hitTest(this.root._h, x, y);
127
+ return (name && layersByName.get(name)) || null;
128
+ }
129
+ close() { native.closeWindow(this._h); }
130
+ snapshot(file) { return native.snapshotWindow(this._h, file); } // renderInContext -> PNG
131
+ }
132
+
133
+ // Group property changes into one CATransaction (shared animation duration/timing).
134
+ function transaction(fn, opts = {}) {
135
+ native.txBegin(opts);
136
+ try { fn(); } finally { native.txCommit(); }
137
+ }
138
+
139
+ // Suppress implicit animations for this batch of changes.
140
+ function withoutAnimations(fn) {
141
+ transaction(fn, { disableActions: true });
142
+ }
143
+
144
+ let timer = null;
145
+ const app = {
146
+ onEvent(fn) { native.setEventCallback(fn); },
147
+ // Drives the NSApplication event pump off node's event loop. Core Animation
148
+ // itself animates in the render server, independent of this cadence.
149
+ run({ fps = 60, onTick } = {}) {
150
+ if (timer) return;
151
+ native.initApp();
152
+ timer = setInterval(() => {
153
+ native.pump();
154
+ if (onTick) onTick();
155
+ }, Math.max(4, Math.floor(1000 / fps)));
156
+ },
157
+ stop() {
158
+ if (timer) { clearInterval(timer); timer = null; }
159
+ },
160
+ pump: () => native.pump(),
161
+ };
162
+
163
+ const text = {
164
+ // CoreText-rendered glyphs -> CGImage, for glyph-atlas style text.
165
+ // {text, fontName, fontSize, color, maxWidth, scale} -> {image, width, height, scale}
166
+ render: (opts) => native.createTextImage(opts),
167
+ // {text, fontName, fontSize} -> {width, ascent, descent, leading}
168
+ measure: (opts) => native.measureText(opts),
169
+ };
170
+
171
+ const controls = {
172
+ // Native control bezels via offscreen NSCell drawing (WebKit/Firefox technique).
173
+ // {kind: 'push'|'checkbox'|'radio'|'popup'|'slider', title, state, pressed,
174
+ // enabled, isDefault, value, controlSize, appearance: 'system'|'dark'|'light',
175
+ // width, height, scale} -> {image, width, height, scale}
176
+ // width/height default to the cell's natural size (sliders must pass them).
177
+ render: (opts) => native.drawControl(opts),
178
+ isDark: () => native.appearanceIsDark(),
179
+ };
180
+
181
+ module.exports = {
182
+ app, Window, Layer, TextLayer, GradientLayer, ShapeLayer,
183
+ transaction, withoutAnimations, text, controls, native,
184
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@windowkit/appkit",
3
+ "version": "0.1.0",
4
+ "description": "Retained-mode AppKit backend for Node.js: Core Animation (CALayer) layer trees, CoreText layout and drawing, IOSurface presentation, NSMenu, and native control bezels — the macOS half of react-x11.",
5
+ "author": "Andrey Sidorov <andrey.sidorov@gmail.com>",
6
+ "license": "MIT",
7
+ "main": "index.js",
8
+ "homepage": "https://github.com/windowkit/appkit",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/windowkit/appkit.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/windowkit/appkit/issues"
15
+ },
16
+ "keywords": [
17
+ "macos",
18
+ "appkit",
19
+ "core-animation",
20
+ "calayer",
21
+ "coretext",
22
+ "iosurface",
23
+ "nsmenu",
24
+ "native-controls",
25
+ "react-x11",
26
+ "windowkit"
27
+ ],
28
+ "os": [
29
+ "darwin"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "files": [
35
+ "LICENSE",
36
+ "index.js",
37
+ "binding.gyp",
38
+ "src/",
39
+ "scripts/",
40
+ "prebuilds/"
41
+ ],
42
+ "scripts": {
43
+ "install": "node scripts/install.js",
44
+ "build": "node-gyp rebuild",
45
+ "demo": "node demo/demo.js"
46
+ },
47
+ "dependencies": {
48
+ "node-addon-api": "^8.3.0"
49
+ },
50
+ "devDependencies": {
51
+ "node-gyp": "^11.0.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "gypfile": true
57
+ }
@@ -0,0 +1,54 @@
1
+ // Install-time dispatch: use the bundled prebuilt binary when one matches
2
+ // this platform/arch and actually loads; otherwise compile with node-gyp.
3
+ // A Mac with no Xcode command-line tools therefore installs from the
4
+ // tarball alone. Force a source build with: npm install --build-from-source
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { spawnSync } = require('child_process');
10
+
11
+ // darwin-only package (AppKit / Core Animation). On any other platform the
12
+ // package is inert; there is nothing to build, and `os` in package.json
13
+ // already warns npm — do not fail the install of a cross-platform consumer.
14
+ if (process.platform !== 'darwin') {
15
+ console.log('@windowkit/appkit: not macOS — skipping native build');
16
+ process.exit(0);
17
+ }
18
+
19
+ const root = path.join(__dirname, '..');
20
+ const target = `${process.platform}-${process.arch}`;
21
+ const prebuilt = path.join(root, 'prebuilds', target, 'calayers.node');
22
+
23
+ if (
24
+ process.env.npm_config_build_from_source !== 'true' &&
25
+ fs.existsSync(prebuilt)
26
+ ) {
27
+ try {
28
+ require(prebuilt); // loading registers exports and touches nothing else
29
+ console.log(`@windowkit/appkit: using bundled prebuilt binary (${target})`);
30
+ process.exit(0);
31
+ } catch (e) {
32
+ console.warn(
33
+ `@windowkit/appkit: bundled prebuild did not load (${e.message}); compiling instead`,
34
+ );
35
+ }
36
+ }
37
+
38
+ // npm points npm_config_node_gyp at its own copy; fall back to PATH
39
+ const gyp = process.env.npm_config_node_gyp;
40
+ const result = gyp
41
+ ? spawnSync(process.execPath, [gyp, 'rebuild'], { cwd: root, stdio: 'inherit' })
42
+ : spawnSync('node-gyp', ['rebuild'], { cwd: root, stdio: 'inherit' });
43
+ if (result.error) {
44
+ console.error(
45
+ `@windowkit/appkit: could not run node-gyp (${result.error.message})`,
46
+ );
47
+ console.error(
48
+ '@windowkit/appkit: no prebuilt binary matches this system and no ' +
49
+ 'toolchain is available. Install the Xcode command-line tools ' +
50
+ '(xcode-select --install) and reinstall.',
51
+ );
52
+ process.exit(1);
53
+ }
54
+ process.exit(result.status === null ? 1 : result.status);