@gtkx/vitest 0.20.0 → 1.0.0-rc.1

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 CHANGED
@@ -1,106 +1,173 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/eugeniodepalo/gtkx/main/logo.svg" alt="GTKX" width="60" height="60">
2
+ <img src="https://raw.githubusercontent.com/gtkx-org/gtkx/main/logo.svg" alt="GTKX" width="100" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">GTKX</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Build native GTK4 desktop applications with React and TypeScript.</strong>
8
+ The React framework for Linux.<br />
9
+ Build GTK4 and Adwaita apps in TypeScript, with React components and hooks driving GNOME's own widgets. What you ship is a GNOME app.
9
10
  </p>
10
11
 
11
12
  <p align="center">
12
- <a href="https://www.npmjs.com/package/@gtkx/react"><img src="https://img.shields.io/npm/v/@gtkx/react.svg" alt="npm version"></a>
13
- <a href="https://github.com/gtkx-org/gtkx/actions"><img src="https://img.shields.io/github/actions/workflow/status/eugeniodepalo/gtkx/ci.yml" alt="CI"></a>
14
- <a href="https://github.com/gtkx-org/gtkx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue.svg" alt="License"></a>
15
- <a href="https://github.com/gtkx-org/gtkx/discussions"><img src="https://img.shields.io/badge/discussions-GitHub-blue" alt="GitHub Discussions"></a>
13
+ <a href="https://www.npmjs.com/package/create-gtkx"><img src="https://img.shields.io/npm/v/create-gtkx?color=cb3837&logo=npm&label=create-gtkx" alt="npm version" /></a>
14
+ <a href="https://www.npmjs.com/package/create-gtkx"><img src="https://img.shields.io/npm/dm/create-gtkx?color=cb3837&logo=npm&label=downloads" alt="npm downloads" /></a>
15
+ <img src="https://img.shields.io/badge/node-%E2%89%A524-339933?logo=node.js&logoColor=white" alt="Node >= 24" />
16
+ <a href="https://github.com/gtkx-org/gtkx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue.svg" alt="License: MPL-2.0" /></a>
17
+ <a href="https://github.com/gtkx-org/gtkx/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/gtkx-org/gtkx/ci.yml?branch=main&logo=github&label=CI" alt="CI status" /></a>
18
+ <img src="https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white" alt="TypeScript" />
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="https://gtkx.dev">Homepage</a> &middot;
23
+ <a href="https://gtkx.dev/guide/why-gtkx">Documentation</a> &middot;
24
+ <a href="https://github.com/gtkx-org/gtkx/tree/main/examples">Examples</a> &middot;
25
+ <a href="https://github.com/gtkx-org/gtkx/blob/main/CONTRIBUTING.md">Contributing</a>
16
26
  </p>
17
27
 
18
28
  ---
19
29
 
20
- GTKX lets you write Linux desktop applications using React. Your components render as native GTK4 widgets through a Rust FFI bridge—no webviews, no Electron, just native performance with the developer experience you already know.
30
+ GTKX generates fully typed bindings for the entire GTK4 and Adwaita surface directly from GObject-Introspection. On top of those bindings you get the React programming model: components and hooks driving GObject instances, with Fast Refresh while you develop.
21
31
 
22
- ## Quick Start
32
+ <p align="center">
33
+ <img src="https://raw.githubusercontent.com/gtkx-org/gtkx/main/examples/tutorial/assets/screenshot.png" alt="The Tasks app: an Adwaita window with a sidebar of smart views and colored lists on the left, and a boxed task list on the right." />
34
+ </p>
23
35
 
24
- ```bash
25
- npx @gtkx/cli create my-app
26
- cd my-app
27
- npm run dev
28
- ```
36
+ <p align="center">
37
+ <em>The Tasks app you build in the <a href="https://gtkx.dev/tutorial/">tutorial</a>.</em>
38
+ </p>
39
+
40
+ ## Demo
29
41
 
30
- ## Example
42
+ The intrinsic elements in this snippet render GTK4 widgets, and ordinary React hooks and events drive them:
31
43
 
32
44
  ```tsx
33
- import {
34
- GtkApplicationWindow,
35
- GtkBox,
36
- GtkButton,
37
- GtkLabel,
38
- quit,
39
- render,
40
- } from "@gtkx/react";
41
- import * as Gtk from "@gtkx/ffi/gtk";
45
+ import * as Gtk from "@gtkx/gi/gtk";
46
+ import { GtkApplication, GtkApplicationWindow, GtkBox, GtkButton, GtkLabel } from "@gtkx/jsx/gtk";
47
+ import { createRoot, quit } from "@gtkx/react";
42
48
  import { useState } from "react";
43
49
 
44
- const App = () => {
50
+ const Counter = () => {
45
51
  const [count, setCount] = useState(0);
46
52
 
47
53
  return (
48
54
  <GtkApplicationWindow
49
- title="Counter"
50
- defaultWidth={300}
51
- defaultHeight={200}
52
- onClose={quit}
55
+ title="Hello GTKX"
56
+ defaultWidth={400}
57
+ defaultHeight={300}
58
+ onCloseRequest={quit}
53
59
  >
54
60
  <GtkBox
55
61
  orientation={Gtk.Orientation.VERTICAL}
56
62
  spacing={20}
63
+ marginTop={40}
64
+ marginBottom={40}
65
+ marginStart={40}
66
+ marginEnd={40}
57
67
  valign={Gtk.Align.CENTER}
68
+ halign={Gtk.Align.CENTER}
58
69
  >
59
- <GtkLabel label={`Count: ${count}`} cssClasses={["title-1"]} />
60
- <GtkButton label="Increment" onClicked={() => setCount((c) => c + 1)} />
70
+ <GtkLabel cssClasses={["title-1"]}>Welcome to GTKX!</GtkLabel>
71
+ <GtkLabel cssClasses={["title-2"]}>{`Count: ${count}`}</GtkLabel>
72
+ <GtkButton
73
+ label="Increment"
74
+ onClicked={() => setCount((c) => c + 1)}
75
+ cssClasses={["suggested-action", "pill"]}
76
+ />
61
77
  </GtkBox>
62
78
  </GtkApplicationWindow>
63
79
  );
64
80
  };
65
81
 
66
- render(<App />, "com.example.counter");
82
+ const App = () => (
83
+ <GtkApplication>
84
+ <Counter />
85
+ </GtkApplication>
86
+ );
87
+
88
+ createRoot().render(<App />);
67
89
  ```
68
90
 
69
- ## Features
91
+ This is the [`hello-world`](https://github.com/gtkx-org/gtkx/tree/main/examples/hello-world) example, with `app.tsx` and `index.tsx` combined into a single snippet. `@gtkx/gi` and `@gtkx/jsx` are per-project bindings generated by the CLI, not packages you install from npm.
70
92
 
71
- - **React 19** — Hooks, concurrent features, and the component model you know
72
- - **Native GTK4 widgets** — Real native controls, not web components in a webview
73
- - **Adwaita support** — Modern GNOME styling with Libadwaita components
74
- - **Declarative animations** — Framer Motion-like API using native Adwaita animations
75
- - **Hot Module Replacement** — Fast refresh during development
76
- - **TypeScript first** — Full type safety with auto-generated bindings
77
- - **CSS-in-JS styling** — Familiar styling patterns adapted for GTK
78
- - **Testing utilities** — Component testing similar to Testing Library
93
+ ## Why GTKX
79
94
 
80
- ## Examples
95
+ ### A declarative layer for the GNOME stack
96
+
97
+ GTK4 is mature, and GtkBuilder XML can lay out a static interface, but nothing re-renders that interface when your application state changes, and nothing hot-reloads it as you work. GTKX adds that missing layer, and the tooling around it, on top of the stack you already know:
98
+
99
+ - a React reconciler that exposes every GObject as a JSX element,
100
+ - a CLI for scaffolding, development, and production builds,
101
+ - a dev server with Fast Refresh that patches your running UI in place,
102
+ - CSS-in-JS styling and high-level list, grid, and dialog components,
103
+ - a Testing Library-style API for querying and driving your widgets in tests,
104
+ - and a Model Context Protocol (MCP) server that exposes your live app to AI agents.
105
+
106
+ ### The full GNOME API surface
107
+
108
+ React Native and similar frameworks hide the native toolkit so one API can run everywhere. GTKX exposes it: GTK4, Adwaita, and any other GObject-Introspection library on your system. Linux-only by design.
109
+
110
+ ### Why Node.js, and why generated bindings
111
+
112
+ GTKX runs on Node.js, which puts native modules, the npm ecosystem, and the tooling built for Node.js APIs within reach. GJS is GNOME's own runtime, built on SpiderMonkey rather than V8; node-gtk runs on Node.js but is lightly maintained, on the older nan/V8 ABI rather than N-API, and still centered on GTK3. The [why-gtkx guide](https://gtkx.dev/guide/why-gtkx) covers the comparison in full.
113
+
114
+ GTKX generates the TypeScript types and the native FFI calls from the same GObject-Introspection data, so the types cannot drift from the calls they back. Codegen covers the whole GTK4 and Adwaita surface.
115
+
116
+ At runtime, the native Rust core calls straight into the system GTK4, Adwaita, and GLib libraries through libffi, without loading libgirepository at all.
81
117
 
82
- Explore complete applications in the [`examples/`](./examples) directory:
118
+ ## Quick start
83
119
 
84
- - **[gtk-demo](./examples/gtk-demo)** Full replica of the official GTK demo app
85
- - **[hello-world](./examples/hello-world)** — Minimal application showing a counter
86
- - **[todo](./examples/todo)** Full-featured todo application with Adwaita styling and testing
87
- - **[x-showcase](./examples/x-showcase)** — Showcase of all x.\* virtual components
88
- - **[browser](./examples/browser)** — Simple browser using WebKitWebView
89
- - **[deploying](./examples/deploying)** — Example of packaging and distributing a GTKX app
120
+ GTKX is Linux-only and needs Node.js 24 or later. See [Requirements](#requirements).
121
+
122
+ Scaffold a new app with the `create-gtkx` initializer:
123
+
124
+ ```sh
125
+ npm create gtkx@rc
126
+ ```
127
+
128
+ The same command works with other package managers: `pnpm create gtkx@rc` or `yarn create gtkx@rc`.
129
+
130
+ Then run your new app:
131
+
132
+ ```sh
133
+ cd my-app
134
+ npm run dev
135
+ ```
136
+
137
+ To go further, follow the [tutorial](https://gtkx.dev/tutorial/).
90
138
 
91
139
  ## Documentation
92
140
 
93
- Visit [https://gtkx.dev](https://gtkx.dev) for the full documentation.
141
+ The documentation at **[gtkx.dev](https://gtkx.dev)** includes a step-by-step tutorial that builds a complete GNOME app, from scaffolding to packaging and shipping, plus guides and a full API reference.
94
142
 
95
- ## Contributing
143
+ **[Read the docs &rarr;](https://gtkx.dev/guide/why-gtkx)**
96
144
 
97
- Contributions are welcome! Please see the [contributing guidelines](./CONTRIBUTING.md) and check out the [good first issues](https://github.com/gtkx-org/gtkx/labels/good%20first%20issue).
145
+ ## Requirements
98
146
 
99
- ## Community
147
+ GTKX is Linux-only. You need:
148
+
149
+ - Linux with the GTK4 (4.20 or later), Adwaita (1.8 or later), and GLib development libraries
150
+ - Node.js 24 or later
151
+
152
+ The `@gtkx/native` addon ships prebuilt for x64 and arm64 glibc Linux; other targets need to build it from the GTKX repository, which requires a Rust toolchain.
153
+
154
+ ## Examples
155
+
156
+ Explore the [example apps](https://github.com/gtkx-org/gtkx/tree/main/examples):
157
+
158
+ - [`hello-world`](https://github.com/gtkx-org/gtkx/tree/main/examples/hello-world): the counter above.
159
+ - [`gtk-demo`](https://github.com/gtkx-org/gtkx/tree/main/examples/gtk-demo): a React port of the official GTK4 widget showcase, covering lists, dialogs, gestures, CSS, and OpenGL.
160
+ - [`browser`](https://github.com/gtkx-org/gtkx/tree/main/examples/browser): a WebKitWebView-based web browser.
161
+ - [`tutorial`](https://github.com/gtkx-org/gtkx/tree/main/examples/tutorial): the Tasks app the documentation builds.
162
+
163
+ ## Status
164
+
165
+ GTKX 1.0 is at the release candidate stage.
166
+
167
+ ## Contributing
100
168
 
101
- - [GitHub Discussions](https://github.com/gtkx-org/gtkx/discussions) Questions, ideas, and general discussion
102
- - [Issue Tracker](https://github.com/gtkx-org/gtkx/issues) — Bug reports and feature requests
169
+ Contributions are welcome. See [CONTRIBUTING.md](https://github.com/gtkx-org/gtkx/blob/main/CONTRIBUTING.md), the [Code of Conduct](https://github.com/gtkx-org/gtkx/blob/main/CODE_OF_CONDUCT.md), and the [security policy](https://github.com/gtkx-org/gtkx/blob/main/SECURITY.md). Building the repo needs Node.js 24 or later, pnpm, and a Rust toolchain.
103
170
 
104
171
  ## License
105
172
 
106
- [MPL-2.0](./LICENSE)
173
+ GTKX is licensed under [MPL-2.0](https://github.com/gtkx-org/gtkx/blob/main/LICENSE).
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Wayland compositors that can back a headless display.
3
+ */
4
+ export type CompositorId = "sway" | "weston";
5
+ export declare const DEFAULT_HEADLESS_SIZE = "1024x768";
6
+ /**
7
+ * Settings for the per-worker headless Wayland display.
8
+ */
9
+ export type HeadlessOptions = {
10
+ /** Output resolution as a "WIDTHxHEIGHT" string, for example "1024x768". */
11
+ size: string;
12
+ /** Wayland compositor used to back the headless display. */
13
+ compositor: CompositorId;
14
+ };
15
+ export declare const resolveHeadlessOptions: (provided: Partial<HeadlessOptions>) => HeadlessOptions;
16
+ export declare const readHeadlessOptions: (params: URLSearchParams) => Partial<HeadlessOptions>;
17
+ export declare const STATIC_HEADLESS_ENV: {
18
+ GDK_BACKEND: string;
19
+ GDK_DISABLE: string;
20
+ GDK_DEBUG: string;
21
+ GSK_RENDERER: string;
22
+ GTK_A11Y: string;
23
+ LIBGL_ALWAYS_SOFTWARE: string;
24
+ GST_GL_WINDOW: string;
25
+ GSETTINGS_BACKEND: string;
26
+ ALSOFT_DRIVERS: string;
27
+ ALSOFT_LOGLEVEL: string;
28
+ };
29
+ export declare const startHeadlessDisplay: (options: HeadlessOptions) => Promise<() => void>;
30
+ //# sourceMappingURL=headless-display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-display.d.ts","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE7C,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC1B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,UAAU,EAAE,YAAY,CAAC;CAC5B,CAAC;AAIF,eAAO,MAAM,sBAAsB,aAAc,OAAO,CAAC,eAAe,CAAC,KAAG,eAG1E,CAAC;AAyFH,eAAO,MAAM,mBAAmB,WAAY,eAAe,KAAG,OAAO,CAAC,eAAe,CAOpF,CAAC;AAkGF,eAAO,MAAM,mBAAmB;IAC5B,WAAW;IACX,WAAW;IACX,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,qBAAqB;IACrB,aAAa;IACb,iBAAiB;IACjB,cAAc;IACd,eAAe;CAClB,CAAC;AAyCF,eAAO,MAAM,oBAAoB,YAAmB,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAsDvF,CAAC"}
@@ -0,0 +1,250 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { chmodSync, createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { Socket } from "node:net";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { startNotificationService } from "./notification-service.js";
7
+ export const DEFAULT_HEADLESS_SIZE = "1024x768";
8
+ const DEFAULT_HEADLESS_COMPOSITOR = "weston";
9
+ export const resolveHeadlessOptions = (provided) => ({
10
+ size: provided.size ?? DEFAULT_HEADLESS_SIZE,
11
+ compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,
12
+ });
13
+ const applyEnv = (snapshot, values) => {
14
+ for (const [name, value] of Object.entries(values)) {
15
+ if (!(name in snapshot))
16
+ snapshot[name] = process.env[name];
17
+ process.env[name] = value;
18
+ }
19
+ };
20
+ const restoreEnv = (snapshot) => {
21
+ for (const [name, previous] of Object.entries(snapshot)) {
22
+ if (previous === undefined)
23
+ delete process.env[name];
24
+ else
25
+ process.env[name] = previous;
26
+ }
27
+ };
28
+ const spawnWithParentDeathSignal = (command, args, stdio) => {
29
+ const child = spawn("setpriv", ["--pdeathsig", "SIGKILL", command, ...args], { stdio });
30
+ child.unref();
31
+ return child;
32
+ };
33
+ let westonFakeSeatSupport;
34
+ const westonSupportsFakeSeat = () => {
35
+ if (westonFakeSeatSupport === undefined) {
36
+ const help = spawnSync("weston", ["--help"], { encoding: "utf8" });
37
+ westonFakeSeatSupport = `${help.stdout}${help.stderr}`.includes("--fake-seat");
38
+ }
39
+ return westonFakeSeatSupport;
40
+ };
41
+ const compositorRegistry = {
42
+ sway: {
43
+ socket: "wayland-1",
44
+ env: {
45
+ WLR_BACKENDS: "headless",
46
+ WLR_RENDERER: "pixman",
47
+ WLR_RENDERER_ALLOW_SOFTWARE: "1",
48
+ WLR_LIBINPUT_NO_DEVICES: "1",
49
+ WLR_HEADLESS_OUTPUTS: "1",
50
+ },
51
+ start: (runtimeDir, width, height) => {
52
+ const configPath = join(runtimeDir, "sway.conf");
53
+ writeFileSync(configPath, [
54
+ "xwayland disable",
55
+ "default_border none",
56
+ "default_floating_border none",
57
+ `output HEADLESS-1 resolution ${width}x${height}`,
58
+ "output HEADLESS-1 bg #000000 solid_color",
59
+ 'for_window [app_id=".*"] floating enable, border none',
60
+ 'for_window [title=".*"] floating enable, border none',
61
+ "",
62
+ ].join("\n"));
63
+ return spawnWithParentDeathSignal("sway", ["-c", configPath], ["ignore", "ignore", "pipe"]);
64
+ },
65
+ },
66
+ weston: {
67
+ socket: "wayland-0",
68
+ env: {},
69
+ start: (_runtimeDir, width, height) => spawnWithParentDeathSignal("weston", [
70
+ "--backend=headless",
71
+ "--renderer=pixman",
72
+ ...(westonSupportsFakeSeat() ? ["--fake-seat"] : []),
73
+ `--width=${width}`,
74
+ `--height=${height}`,
75
+ "--socket=wayland-0",
76
+ ], ["ignore", "ignore", "pipe"]),
77
+ },
78
+ };
79
+ const isCompositorId = (value) => Object.hasOwn(compositorRegistry, value);
80
+ export const readHeadlessOptions = (params) => {
81
+ const options = {};
82
+ const size = params.get("size");
83
+ if (size !== null)
84
+ options.size = size;
85
+ const compositor = params.get("compositor");
86
+ if (compositor !== null && isCompositorId(compositor))
87
+ options.compositor = compositor;
88
+ return options;
89
+ };
90
+ const startCompositor = (runtimeDir, options, env) => {
91
+ const descriptor = compositorRegistry[options.compositor];
92
+ const [width = "", height = ""] = options.size.split("x");
93
+ applyEnv(env, descriptor.env);
94
+ return { child: descriptor.start(runtimeDir, width, height), socket: descriptor.socket };
95
+ };
96
+ const writeBusConfig = (busConfigPath, busSocketPath) => {
97
+ writeFileSync(busConfigPath, `<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
98
+ <busconfig>
99
+ <type>session</type>
100
+ <listen>unix:path=${busSocketPath}</listen>
101
+ <auth>EXTERNAL</auth>
102
+ <policy context="default">
103
+ <allow send_destination="*" eavesdrop="true"/>
104
+ <allow eavesdrop="true"/>
105
+ <allow own="*"/>
106
+ </policy>
107
+ </busconfig>`);
108
+ };
109
+ const waitForSocket = (path, { label, timeout = 15000, child, signal }) => new Promise((resolve, reject) => {
110
+ let log = "";
111
+ const stderr = child?.stderr ?? null;
112
+ stderr?.setEncoding("utf8");
113
+ stderr?.on("data", (chunk) => {
114
+ log += chunk;
115
+ });
116
+ let timer;
117
+ let poll;
118
+ const stopListening = () => {
119
+ clearTimeout(timer);
120
+ clearInterval(poll);
121
+ child?.removeListener("exit", onExit);
122
+ child?.removeListener("error", onError);
123
+ signal?.removeEventListener("abort", onAbort);
124
+ stderr?.removeAllListeners("data");
125
+ stderr?.resume();
126
+ if (stderr instanceof Socket)
127
+ stderr.unref();
128
+ };
129
+ const onExit = (code, terminationSignal) => {
130
+ stopListening();
131
+ reject(new Error(`${label} exited (code ${code ?? "null"}, signal ${terminationSignal ?? "null"}) before ${path} appeared\n${log}`));
132
+ };
133
+ const onError = (cause) => {
134
+ stopListening();
135
+ reject(new Error(`${label} failed to spawn: ${cause.message}\n${log}`));
136
+ };
137
+ const onAbort = () => {
138
+ stopListening();
139
+ reject(new Error(`${label} startup aborted before ${path} appeared`));
140
+ };
141
+ poll = setInterval(() => {
142
+ if (existsSync(path)) {
143
+ stopListening();
144
+ resolve();
145
+ }
146
+ }, 50);
147
+ timer = setTimeout(() => {
148
+ stopListening();
149
+ const suffix = child ? `\n${log}` : "";
150
+ reject(new Error(`${label} did not become available within ${timeout}ms${suffix}`));
151
+ }, timeout);
152
+ child?.on("exit", onExit);
153
+ child?.on("error", onError);
154
+ if (signal) {
155
+ if (signal.aborted) {
156
+ onAbort();
157
+ return;
158
+ }
159
+ signal.addEventListener("abort", onAbort);
160
+ }
161
+ });
162
+ export const STATIC_HEADLESS_ENV = {
163
+ GDK_BACKEND: "wayland",
164
+ GDK_DISABLE: "vulkan",
165
+ GDK_DEBUG: "no-vsync",
166
+ GSK_RENDERER: "cairo",
167
+ GTK_A11Y: "test",
168
+ LIBGL_ALWAYS_SOFTWARE: "1",
169
+ GST_GL_WINDOW: "none",
170
+ GSETTINGS_BACKEND: "memory",
171
+ ALSOFT_DRIVERS: "null",
172
+ ALSOFT_LOGLEVEL: "0",
173
+ };
174
+ const captureCompositorStderr = (child, logPath) => {
175
+ const captured = [];
176
+ const stderr = child.stderr;
177
+ if (stderr !== null) {
178
+ stderr.setEncoding("utf8");
179
+ const logStream = createWriteStream(logPath);
180
+ logStream.on("error", () => { });
181
+ stderr.on("data", (chunk) => {
182
+ captured.push(chunk);
183
+ logStream.write(chunk);
184
+ });
185
+ }
186
+ return captured;
187
+ };
188
+ const reportUnexpectedCompositorExit = (child, capturedStderr) => {
189
+ if (child.exitCode === null && child.signalCode === null)
190
+ return;
191
+ process.stderr.write(`[gtkx] headless compositor died before teardown (code ${child.exitCode ?? "null"}, ` +
192
+ `signal ${child.signalCode ?? "null"}); the worker's Wayland client was severed.\n${capturedStderr.join("")}`);
193
+ };
194
+ const makeTeardown = (compositor, capturedStderr, stopNotifications, removeRuntime) => {
195
+ let torndown = false;
196
+ return () => {
197
+ if (torndown)
198
+ return;
199
+ torndown = true;
200
+ reportUnexpectedCompositorExit(compositor, capturedStderr);
201
+ stopNotifications();
202
+ removeRuntime();
203
+ };
204
+ };
205
+ export const startHeadlessDisplay = async (options) => {
206
+ const env = {};
207
+ const runtimeDir = mkdtempSync(join(tmpdir(), "gtkx-xdg-"));
208
+ chmodSync(runtimeDir, 0o700);
209
+ const spawned = [];
210
+ const removeRuntime = () => {
211
+ restoreEnv(env);
212
+ rmSync(runtimeDir, { recursive: true, force: true });
213
+ };
214
+ try {
215
+ applyEnv(env, { XDG_RUNTIME_DIR: runtimeDir });
216
+ const busConfigPath = join(runtimeDir, "session.conf");
217
+ const busSocketPath = join(runtimeDir, "bus");
218
+ writeBusConfig(busConfigPath, busSocketPath);
219
+ const busChild = spawnWithParentDeathSignal("dbus-daemon", [`--config-file=${busConfigPath}`], ["ignore", "ignore", "pipe"]);
220
+ spawned.push(busChild);
221
+ applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });
222
+ const compositor = startCompositor(runtimeDir, options, env);
223
+ spawned.push(compositor.child);
224
+ applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
225
+ const abort = new AbortController();
226
+ try {
227
+ await Promise.all([
228
+ waitForSocket(join(runtimeDir, compositor.socket), {
229
+ label: "Compositor",
230
+ child: compositor.child,
231
+ signal: abort.signal,
232
+ }),
233
+ waitForSocket(busSocketPath, { label: "D-Bus session bus", child: busChild, signal: abort.signal }),
234
+ ]);
235
+ }
236
+ finally {
237
+ abort.abort();
238
+ }
239
+ const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);
240
+ const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, "weston.stderr.log"));
241
+ return makeTeardown(compositor.child, capturedStderr, stopNotifications, removeRuntime);
242
+ }
243
+ catch (cause) {
244
+ for (const child of spawned)
245
+ child.kill("SIGKILL");
246
+ removeRuntime();
247
+ throw cause;
248
+ }
249
+ };
250
+ //# sourceMappingURL=headless-display.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-display.js","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, type StdioOptions, spawn, spawnSync } from \"node:child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { Socket } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { startNotificationService } from \"./notification-service.js\";\n\n/**\n * Wayland compositors that can back a headless display.\n */\nexport type CompositorId = \"sway\" | \"weston\";\n\nexport const DEFAULT_HEADLESS_SIZE = \"1024x768\";\n\n/**\n * Settings for the per-worker headless Wayland display.\n */\nexport type HeadlessOptions = {\n /** Output resolution as a \"WIDTHxHEIGHT\" string, for example \"1024x768\". */\n size: string;\n /** Wayland compositor used to back the headless display. */\n compositor: CompositorId;\n};\n\nconst DEFAULT_HEADLESS_COMPOSITOR: CompositorId = \"weston\";\n\nexport const resolveHeadlessOptions = (provided: Partial<HeadlessOptions>): HeadlessOptions => ({\n size: provided.size ?? DEFAULT_HEADLESS_SIZE,\n compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,\n});\n\ntype EnvSnapshot = { [name: string]: string | undefined };\n\nconst applyEnv = (snapshot: EnvSnapshot, values: { [name: string]: string }): void => {\n for (const [name, value] of Object.entries(values)) {\n if (!(name in snapshot)) snapshot[name] = process.env[name];\n process.env[name] = value;\n }\n};\n\nconst restoreEnv = (snapshot: EnvSnapshot): void => {\n for (const [name, previous] of Object.entries(snapshot)) {\n if (previous === undefined) delete process.env[name];\n else process.env[name] = previous;\n }\n};\n\nconst spawnWithParentDeathSignal = (command: string, args: string[], stdio: StdioOptions): ChildProcess => {\n const child = spawn(\"setpriv\", [\"--pdeathsig\", \"SIGKILL\", command, ...args], { stdio });\n child.unref();\n return child;\n};\n\nlet westonFakeSeatSupport: boolean | undefined;\n\nconst westonSupportsFakeSeat = (): boolean => {\n if (westonFakeSeatSupport === undefined) {\n const help = spawnSync(\"weston\", [\"--help\"], { encoding: \"utf8\" });\n westonFakeSeatSupport = `${help.stdout}${help.stderr}`.includes(\"--fake-seat\");\n }\n return westonFakeSeatSupport;\n};\n\ntype CompositorDescriptor = {\n socket: string;\n env: { [name: string]: string };\n start: (runtimeDir: string, width: string, height: string) => ChildProcess;\n};\n\nconst compositorRegistry: { [K in CompositorId]: CompositorDescriptor } = {\n sway: {\n socket: \"wayland-1\",\n env: {\n WLR_BACKENDS: \"headless\",\n WLR_RENDERER: \"pixman\",\n WLR_RENDERER_ALLOW_SOFTWARE: \"1\",\n WLR_LIBINPUT_NO_DEVICES: \"1\",\n WLR_HEADLESS_OUTPUTS: \"1\",\n },\n start: (runtimeDir, width, height) => {\n const configPath = join(runtimeDir, \"sway.conf\");\n writeFileSync(\n configPath,\n [\n \"xwayland disable\",\n \"default_border none\",\n \"default_floating_border none\",\n `output HEADLESS-1 resolution ${width}x${height}`,\n \"output HEADLESS-1 bg #000000 solid_color\",\n 'for_window [app_id=\".*\"] floating enable, border none',\n 'for_window [title=\".*\"] floating enable, border none',\n \"\",\n ].join(\"\\n\"),\n );\n return spawnWithParentDeathSignal(\"sway\", [\"-c\", configPath], [\"ignore\", \"ignore\", \"pipe\"]);\n },\n },\n weston: {\n socket: \"wayland-0\",\n env: {},\n start: (_runtimeDir, width, height) =>\n spawnWithParentDeathSignal(\n \"weston\",\n [\n \"--backend=headless\",\n \"--renderer=pixman\",\n ...(westonSupportsFakeSeat() ? [\"--fake-seat\"] : []),\n `--width=${width}`,\n `--height=${height}`,\n \"--socket=wayland-0\",\n ],\n [\"ignore\", \"ignore\", \"pipe\"],\n ),\n },\n};\n\nconst isCompositorId = (value: string): value is CompositorId => Object.hasOwn(compositorRegistry, value);\n\nexport const readHeadlessOptions = (params: URLSearchParams): Partial<HeadlessOptions> => {\n const options: Partial<HeadlessOptions> = {};\n const size = params.get(\"size\");\n if (size !== null) options.size = size;\n const compositor = params.get(\"compositor\");\n if (compositor !== null && isCompositorId(compositor)) options.compositor = compositor;\n return options;\n};\n\ntype SpawnedCompositor = {\n child: ChildProcess;\n socket: string;\n};\n\nconst startCompositor = (runtimeDir: string, options: HeadlessOptions, env: EnvSnapshot): SpawnedCompositor => {\n const descriptor = compositorRegistry[options.compositor];\n\n const [width = \"\", height = \"\"] = options.size.split(\"x\");\n applyEnv(env, descriptor.env);\n\n return { child: descriptor.start(runtimeDir, width, height), socket: descriptor.socket };\n};\n\nconst writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {\n writeFileSync(\n busConfigPath,\n `<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\" \"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">\n<busconfig>\n <type>session</type>\n <listen>unix:path=${busSocketPath}</listen>\n <auth>EXTERNAL</auth>\n <policy context=\"default\">\n <allow send_destination=\"*\" eavesdrop=\"true\"/>\n <allow eavesdrop=\"true\"/>\n <allow own=\"*\"/>\n </policy>\n</busconfig>`,\n );\n};\n\ntype WaitForSocketOptions = {\n label: string;\n timeout?: number;\n child?: ChildProcess;\n signal?: AbortSignal;\n};\n\nconst waitForSocket = (path: string, { label, timeout = 15000, child, signal }: WaitForSocketOptions): Promise<void> =>\n new Promise((resolve, reject) => {\n let log = \"\";\n const stderr = child?.stderr ?? null;\n stderr?.setEncoding(\"utf8\");\n stderr?.on(\"data\", (chunk: string) => {\n log += chunk;\n });\n let timer: ReturnType<typeof setTimeout>;\n let poll: ReturnType<typeof setInterval>;\n const stopListening = (): void => {\n clearTimeout(timer);\n clearInterval(poll);\n child?.removeListener(\"exit\", onExit);\n child?.removeListener(\"error\", onError);\n signal?.removeEventListener(\"abort\", onAbort);\n stderr?.removeAllListeners(\"data\");\n stderr?.resume();\n if (stderr instanceof Socket) stderr.unref();\n };\n const onExit = (code: number | null, terminationSignal: NodeJS.Signals | null): void => {\n stopListening();\n reject(\n new Error(\n `${label} exited (code ${code ?? \"null\"}, signal ${terminationSignal ?? \"null\"}) before ${path} appeared\\n${log}`,\n ),\n );\n };\n const onError = (cause: Error): void => {\n stopListening();\n reject(new Error(`${label} failed to spawn: ${cause.message}\\n${log}`));\n };\n const onAbort = (): void => {\n stopListening();\n reject(new Error(`${label} startup aborted before ${path} appeared`));\n };\n poll = setInterval(() => {\n if (existsSync(path)) {\n stopListening();\n resolve();\n }\n }, 50);\n timer = setTimeout(() => {\n stopListening();\n const suffix = child ? `\\n${log}` : \"\";\n reject(new Error(`${label} did not become available within ${timeout}ms${suffix}`));\n }, timeout);\n child?.on(\"exit\", onExit);\n child?.on(\"error\", onError);\n if (signal) {\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener(\"abort\", onAbort);\n }\n });\n\nexport const STATIC_HEADLESS_ENV = {\n GDK_BACKEND: \"wayland\",\n GDK_DISABLE: \"vulkan\",\n GDK_DEBUG: \"no-vsync\",\n GSK_RENDERER: \"cairo\",\n GTK_A11Y: \"test\",\n LIBGL_ALWAYS_SOFTWARE: \"1\",\n GST_GL_WINDOW: \"none\",\n GSETTINGS_BACKEND: \"memory\",\n ALSOFT_DRIVERS: \"null\",\n ALSOFT_LOGLEVEL: \"0\",\n};\n\nconst captureCompositorStderr = (child: ChildProcess, logPath: string): string[] => {\n const captured: string[] = [];\n const stderr = child.stderr;\n if (stderr !== null) {\n stderr.setEncoding(\"utf8\");\n const logStream = createWriteStream(logPath);\n logStream.on(\"error\", () => {});\n stderr.on(\"data\", (chunk: string) => {\n captured.push(chunk);\n logStream.write(chunk);\n });\n }\n return captured;\n};\n\nconst reportUnexpectedCompositorExit = (child: ChildProcess, capturedStderr: string[]): void => {\n if (child.exitCode === null && child.signalCode === null) return;\n process.stderr.write(\n `[gtkx] headless compositor died before teardown (code ${child.exitCode ?? \"null\"}, ` +\n `signal ${child.signalCode ?? \"null\"}); the worker's Wayland client was severed.\\n${capturedStderr.join(\"\")}`,\n );\n};\n\nconst makeTeardown = (\n compositor: ChildProcess,\n capturedStderr: string[],\n stopNotifications: () => void,\n removeRuntime: () => void,\n): (() => void) => {\n let torndown = false;\n return (): void => {\n if (torndown) return;\n torndown = true;\n reportUnexpectedCompositorExit(compositor, capturedStderr);\n stopNotifications();\n removeRuntime();\n };\n};\n\nexport const startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => void> => {\n const env: EnvSnapshot = {};\n const runtimeDir = mkdtempSync(join(tmpdir(), \"gtkx-xdg-\"));\n chmodSync(runtimeDir, 0o700);\n const spawned: ChildProcess[] = [];\n\n const removeRuntime = (): void => {\n restoreEnv(env);\n rmSync(runtimeDir, { recursive: true, force: true });\n };\n\n try {\n applyEnv(env, { XDG_RUNTIME_DIR: runtimeDir });\n\n const busConfigPath = join(runtimeDir, \"session.conf\");\n const busSocketPath = join(runtimeDir, \"bus\");\n writeBusConfig(busConfigPath, busSocketPath);\n\n const busChild = spawnWithParentDeathSignal(\n \"dbus-daemon\",\n [`--config-file=${busConfigPath}`],\n [\"ignore\", \"ignore\", \"pipe\"],\n );\n spawned.push(busChild);\n applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });\n\n const compositor = startCompositor(runtimeDir, options, env);\n spawned.push(compositor.child);\n\n applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });\n\n const abort = new AbortController();\n try {\n await Promise.all([\n waitForSocket(join(runtimeDir, compositor.socket), {\n label: \"Compositor\",\n child: compositor.child,\n signal: abort.signal,\n }),\n waitForSocket(busSocketPath, { label: \"D-Bus session bus\", child: busChild, signal: abort.signal }),\n ]);\n } finally {\n abort.abort();\n }\n\n const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);\n\n const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, \"weston.stderr.log\"));\n return makeTeardown(compositor.child, capturedStderr, stopNotifications, removeRuntime);\n } catch (cause) {\n for (const child of spawned) child.kill(\"SIGKILL\");\n removeRuntime();\n throw cause;\n }\n};\n"]}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,20 @@
1
- export { default } from "./plugin.js";
1
+ import type { Plugin } from "vitest/config";
2
+ import { type CompositorId, type HeadlessOptions } from "./headless-display.js";
3
+ export type { CompositorId, HeadlessOptions };
4
+ export declare const GTKX_INLINE_DEPS: RegExp[];
5
+ /**
6
+ * Options accepted by the {@link gtkx} Vitest plugin. Every headless display
7
+ * setting is optional and falls back to a built-in default when omitted.
8
+ */
9
+ export type GtkxPluginOptions = Partial<HeadlessOptions>;
10
+ /**
11
+ * Vitest plugin that runs each test worker against its own isolated headless
12
+ * Wayland display. It configures the forks pool, injects the worker preload and
13
+ * setup files, and sets the environment needed for headless GTK4 rendering.
14
+ *
15
+ * @param options Headless display settings (size, compositor) forwarded to each worker.
16
+ * @returns A Vitest config plugin.
17
+ */
18
+ declare const gtkx: (options?: GtkxPluginOptions) => Plugin;
19
+ export default gtkx;
2
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAuB,MAAM,uBAAuB,CAAC;AAErG,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC;AAE9C,eAAO,MAAM,gBAAgB,EAAE,MAAM,EAA8C,CAAC;AAEpF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;AAqBzD;;;;;;;GAOG;AACH,QAAA,MAAM,IAAI,aAAa,iBAAiB,KAAQ,MAqB1C,CAAC;eAEQ,IAAI"}
package/dist/index.js CHANGED
@@ -1,2 +1,54 @@
1
- export { default } from "./plugin.js";
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import createConfigPlugin from "@gtkx/config/vite-plugin";
5
+ import { STATIC_HEADLESS_ENV } from "./headless-display.js";
6
+ export const GTKX_INLINE_DEPS = [/@gtkx\/(?!native)/, /[/\\]\.gtkx[/\\]/];
7
+ const workerPreloadUrl = () => {
8
+ const sibling = join(import.meta.dirname, "worker-preload.js");
9
+ const path = existsSync(sibling) ? sibling : join(import.meta.dirname, "..", "dist", "worker-preload.js");
10
+ return pathToFileURL(path);
11
+ };
12
+ const headlessPreloadSpecifier = (options) => {
13
+ const url = workerPreloadUrl();
14
+ for (const [key, value] of Object.entries(options)) {
15
+ if (value !== undefined)
16
+ url.searchParams.set(key, value);
17
+ }
18
+ return url.href;
19
+ };
20
+ const workerSetupPath = () => {
21
+ const sibling = join(import.meta.dirname, "worker-setup.js");
22
+ return existsSync(sibling) ? sibling : join(import.meta.dirname, "..", "dist", "worker-setup.js");
23
+ };
24
+ /**
25
+ * Vitest plugin that runs each test worker against its own isolated headless
26
+ * Wayland display. It configures the forks pool, injects the worker preload and
27
+ * setup files, and sets the environment needed for headless GTK4 rendering.
28
+ *
29
+ * @param options Headless display settings (size, compositor) forwarded to each worker.
30
+ * @returns A Vitest config plugin.
31
+ */
32
+ const gtkx = (options = {}) => createConfigPlugin({
33
+ name: "gtkx:vitest",
34
+ config() {
35
+ return {
36
+ test: {
37
+ globals: true,
38
+ execArgv: ["--import", headlessPreloadSpecifier(options)],
39
+ setupFiles: [workerSetupPath()],
40
+ testTimeout: 30000,
41
+ hookTimeout: 30000,
42
+ pool: "forks",
43
+ env: STATIC_HEADLESS_ENV,
44
+ server: {
45
+ deps: {
46
+ inline: GTKX_INLINE_DEPS,
47
+ },
48
+ },
49
+ },
50
+ };
51
+ },
52
+ });
53
+ export default gtkx;
2
54
  //# sourceMappingURL=index.js.map