@gtkx/cli 0.11.1 → 0.12.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/dist/cli.js +6 -0
- package/dist/mcp-client.d.ts +30 -0
- package/dist/mcp-client.js +401 -0
- package/dist/refresh-runtime.d.ts +27 -0
- package/dist/refresh-runtime.js +27 -0
- package/package.json +13 -5
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import "./refresh-runtime.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
+
import { events } from "@gtkx/ffi";
|
|
6
7
|
import { render } from "@gtkx/react";
|
|
7
8
|
import { defineCommand, runMain } from "citty";
|
|
8
9
|
import { createApp } from "./create.js";
|
|
9
10
|
import { createDevServer } from "./dev-server.js";
|
|
11
|
+
import { startMcpClient, stopMcpClient } from "./mcp-client.js";
|
|
10
12
|
const require = createRequire(import.meta.url);
|
|
11
13
|
const { version } = require("../package.json");
|
|
12
14
|
const dev = defineCommand({
|
|
@@ -40,6 +42,10 @@ const dev = defineCommand({
|
|
|
40
42
|
}
|
|
41
43
|
console.log(`[gtkx] Rendering app with ID: ${appId}`);
|
|
42
44
|
render(_jsx(App, {}), appId, appFlags);
|
|
45
|
+
await startMcpClient(appId);
|
|
46
|
+
events.on("stop", () => {
|
|
47
|
+
stopMcpClient();
|
|
48
|
+
});
|
|
43
49
|
console.log("[gtkx] HMR enabled - watching for changes...");
|
|
44
50
|
},
|
|
45
51
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type McpClientOptions = {
|
|
2
|
+
socketPath?: string;
|
|
3
|
+
appId: string;
|
|
4
|
+
};
|
|
5
|
+
declare class McpClient {
|
|
6
|
+
private socket;
|
|
7
|
+
private buffer;
|
|
8
|
+
private socketPath;
|
|
9
|
+
private appId;
|
|
10
|
+
private reconnectTimer;
|
|
11
|
+
private hasConnected;
|
|
12
|
+
private isStopping;
|
|
13
|
+
private pendingRequests;
|
|
14
|
+
constructor(options: McpClientOptions);
|
|
15
|
+
connect(): Promise<void>;
|
|
16
|
+
private attemptConnect;
|
|
17
|
+
private scheduleReconnect;
|
|
18
|
+
disconnect(): void;
|
|
19
|
+
private rejectPendingRequests;
|
|
20
|
+
private register;
|
|
21
|
+
private send;
|
|
22
|
+
private sendRequest;
|
|
23
|
+
private handleData;
|
|
24
|
+
private processMessage;
|
|
25
|
+
private handleRequest;
|
|
26
|
+
private executeMethod;
|
|
27
|
+
}
|
|
28
|
+
export declare const startMcpClient: (appId: string) => Promise<McpClient>;
|
|
29
|
+
export declare const stopMcpClient: () => void;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
import { getNativeId, getNativeObject } from "@gtkx/ffi";
|
|
3
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
4
|
+
import { DEFAULT_SOCKET_PATH, IpcRequestSchema, IpcResponseSchema, McpError, McpErrorCode, methodNotFoundError, widgetNotFoundError, } from "@gtkx/mcp";
|
|
5
|
+
import { getApplication } from "@gtkx/react";
|
|
6
|
+
let testingModule = null;
|
|
7
|
+
let testingLoadError = null;
|
|
8
|
+
const loadTestingModule = async () => {
|
|
9
|
+
if (testingModule)
|
|
10
|
+
return testingModule;
|
|
11
|
+
if (testingLoadError)
|
|
12
|
+
throw testingLoadError;
|
|
13
|
+
try {
|
|
14
|
+
testingModule = await import("@gtkx/testing");
|
|
15
|
+
return testingModule;
|
|
16
|
+
}
|
|
17
|
+
catch (cause) {
|
|
18
|
+
testingLoadError = new Error("@gtkx/testing is not installed. Install it to enable MCP widget interactions: pnpm add -D @gtkx/testing", { cause });
|
|
19
|
+
throw testingLoadError;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const RECONNECT_DELAY_MS = 2000;
|
|
23
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
24
|
+
const formatRole = (role) => {
|
|
25
|
+
if (role === undefined)
|
|
26
|
+
return "UNKNOWN";
|
|
27
|
+
return Gtk.AccessibleRole[role] ?? String(role);
|
|
28
|
+
};
|
|
29
|
+
const getWidgetText = (widget) => {
|
|
30
|
+
const role = widget.getAccessibleRole();
|
|
31
|
+
if (role === undefined)
|
|
32
|
+
return null;
|
|
33
|
+
switch (role) {
|
|
34
|
+
case Gtk.AccessibleRole.BUTTON:
|
|
35
|
+
case Gtk.AccessibleRole.LINK:
|
|
36
|
+
case Gtk.AccessibleRole.TAB:
|
|
37
|
+
return widget.getLabel?.() ?? widget.getLabel?.() ?? null;
|
|
38
|
+
case Gtk.AccessibleRole.TOGGLE_BUTTON:
|
|
39
|
+
return widget.getLabel?.() ?? null;
|
|
40
|
+
case Gtk.AccessibleRole.CHECKBOX:
|
|
41
|
+
case Gtk.AccessibleRole.RADIO:
|
|
42
|
+
return widget.getLabel?.() ?? null;
|
|
43
|
+
case Gtk.AccessibleRole.LABEL:
|
|
44
|
+
return widget.getLabel?.() ?? widget.getText?.() ?? null;
|
|
45
|
+
case Gtk.AccessibleRole.TEXT_BOX:
|
|
46
|
+
case Gtk.AccessibleRole.SEARCH_BOX:
|
|
47
|
+
case Gtk.AccessibleRole.SPIN_BUTTON:
|
|
48
|
+
return getNativeObject(widget.handle, Gtk.Editable).getText() ?? null;
|
|
49
|
+
case Gtk.AccessibleRole.GROUP:
|
|
50
|
+
return widget.getLabel?.() ?? null;
|
|
51
|
+
case Gtk.AccessibleRole.WINDOW:
|
|
52
|
+
case Gtk.AccessibleRole.DIALOG:
|
|
53
|
+
case Gtk.AccessibleRole.ALERT_DIALOG:
|
|
54
|
+
return widget.getTitle() ?? null;
|
|
55
|
+
default:
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const serializeWidget = (widget) => {
|
|
60
|
+
const children = [];
|
|
61
|
+
let child = widget.getFirstChild();
|
|
62
|
+
while (child) {
|
|
63
|
+
children.push(serializeWidget(child));
|
|
64
|
+
child = child.getNextSibling();
|
|
65
|
+
}
|
|
66
|
+
const text = getWidgetText(widget);
|
|
67
|
+
return {
|
|
68
|
+
id: String(getNativeId(widget.handle)),
|
|
69
|
+
type: widget.constructor.name,
|
|
70
|
+
role: formatRole(widget.getAccessibleRole()),
|
|
71
|
+
name: widget.getName() || null,
|
|
72
|
+
label: text,
|
|
73
|
+
text,
|
|
74
|
+
sensitive: widget.getSensitive(),
|
|
75
|
+
visible: widget.getVisible(),
|
|
76
|
+
cssClasses: widget.getCssClasses() ?? [],
|
|
77
|
+
children,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const widgetRegistry = new Map();
|
|
81
|
+
const registerWidgets = (widget) => {
|
|
82
|
+
const idStr = String(getNativeId(widget.handle));
|
|
83
|
+
widgetRegistry.set(idStr, widget);
|
|
84
|
+
let child = widget.getFirstChild();
|
|
85
|
+
while (child) {
|
|
86
|
+
registerWidgets(child);
|
|
87
|
+
child = child.getNextSibling();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const getWidgetById = (id) => {
|
|
91
|
+
return widgetRegistry.get(id);
|
|
92
|
+
};
|
|
93
|
+
const refreshWidgetRegistry = () => {
|
|
94
|
+
widgetRegistry.clear();
|
|
95
|
+
const app = getApplication();
|
|
96
|
+
if (!app)
|
|
97
|
+
return;
|
|
98
|
+
const windows = app.getWindows();
|
|
99
|
+
for (const window of windows) {
|
|
100
|
+
registerWidgets(window);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
class McpClient {
|
|
104
|
+
socket = null;
|
|
105
|
+
buffer = "";
|
|
106
|
+
socketPath;
|
|
107
|
+
appId;
|
|
108
|
+
reconnectTimer = null;
|
|
109
|
+
hasConnected = false;
|
|
110
|
+
isStopping = false;
|
|
111
|
+
pendingRequests = new Map();
|
|
112
|
+
constructor(options) {
|
|
113
|
+
this.socketPath = options.socketPath ?? DEFAULT_SOCKET_PATH;
|
|
114
|
+
this.appId = options.appId;
|
|
115
|
+
}
|
|
116
|
+
async connect() {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
this.attemptConnect(resolve, reject);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
attemptConnect(onSuccess, onError) {
|
|
122
|
+
let settled = false;
|
|
123
|
+
const settle = (callback, ...args) => {
|
|
124
|
+
if (settled)
|
|
125
|
+
return;
|
|
126
|
+
settled = true;
|
|
127
|
+
callback?.(...args);
|
|
128
|
+
};
|
|
129
|
+
this.socket = net.createConnection(this.socketPath, () => {
|
|
130
|
+
console.log(`[gtkx] Connected to MCP server at ${this.socketPath}`);
|
|
131
|
+
this.hasConnected = true;
|
|
132
|
+
this.register()
|
|
133
|
+
.then(() => {
|
|
134
|
+
console.log("[gtkx] Registered with MCP server");
|
|
135
|
+
settle(onSuccess);
|
|
136
|
+
})
|
|
137
|
+
.catch((error) => {
|
|
138
|
+
console.error("[gtkx] Failed to register with MCP server:", error.message);
|
|
139
|
+
settle(onError, error instanceof Error ? error : new Error(String(error)));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
this.socket.on("data", (data) => this.handleData(data));
|
|
143
|
+
this.socket.on("close", () => {
|
|
144
|
+
if (this.hasConnected) {
|
|
145
|
+
console.log("[gtkx] Disconnected from MCP server");
|
|
146
|
+
this.hasConnected = false;
|
|
147
|
+
}
|
|
148
|
+
this.socket = null;
|
|
149
|
+
this.rejectPendingRequests(new Error("Connection closed"));
|
|
150
|
+
this.scheduleReconnect();
|
|
151
|
+
});
|
|
152
|
+
this.socket.on("error", (error) => {
|
|
153
|
+
const code = error.code;
|
|
154
|
+
const isDisconnectError = code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE" || code === "ECONNRESET";
|
|
155
|
+
if (isDisconnectError) {
|
|
156
|
+
this.scheduleReconnect();
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.error("[gtkx] Socket error:", error.message);
|
|
160
|
+
}
|
|
161
|
+
settle(onError, error);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
scheduleReconnect() {
|
|
165
|
+
if (this.reconnectTimer || this.isStopping)
|
|
166
|
+
return;
|
|
167
|
+
this.reconnectTimer = setTimeout(() => {
|
|
168
|
+
this.reconnectTimer = null;
|
|
169
|
+
this.attemptConnect();
|
|
170
|
+
}, RECONNECT_DELAY_MS);
|
|
171
|
+
}
|
|
172
|
+
disconnect() {
|
|
173
|
+
this.isStopping = true;
|
|
174
|
+
if (this.reconnectTimer) {
|
|
175
|
+
clearTimeout(this.reconnectTimer);
|
|
176
|
+
this.reconnectTimer = null;
|
|
177
|
+
}
|
|
178
|
+
this.rejectPendingRequests(new Error("Client disconnected"));
|
|
179
|
+
if (this.socket) {
|
|
180
|
+
this.send({ id: crypto.randomUUID(), method: "app.unregister" });
|
|
181
|
+
this.socket.destroy();
|
|
182
|
+
this.socket = null;
|
|
183
|
+
}
|
|
184
|
+
this.hasConnected = false;
|
|
185
|
+
}
|
|
186
|
+
rejectPendingRequests(error) {
|
|
187
|
+
for (const pending of this.pendingRequests.values()) {
|
|
188
|
+
clearTimeout(pending.timeout);
|
|
189
|
+
pending.reject(error);
|
|
190
|
+
}
|
|
191
|
+
this.pendingRequests.clear();
|
|
192
|
+
}
|
|
193
|
+
async register() {
|
|
194
|
+
await this.sendRequest("app.register", {
|
|
195
|
+
appId: this.appId,
|
|
196
|
+
pid: process.pid,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
send(message) {
|
|
200
|
+
if (!this.socket || !this.socket.writable)
|
|
201
|
+
return;
|
|
202
|
+
this.socket.write(`${JSON.stringify(message)}\n`);
|
|
203
|
+
}
|
|
204
|
+
sendRequest(method, params) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
if (!this.socket || !this.socket.writable) {
|
|
207
|
+
reject(new Error("Socket not connected"));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const id = crypto.randomUUID();
|
|
211
|
+
const timeout = setTimeout(() => {
|
|
212
|
+
this.pendingRequests.delete(id);
|
|
213
|
+
reject(new Error(`Request timed out: ${method}`));
|
|
214
|
+
}, REQUEST_TIMEOUT_MS);
|
|
215
|
+
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
216
|
+
this.send({ id, method, params });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
handleData(data) {
|
|
220
|
+
this.buffer += data.toString();
|
|
221
|
+
let newlineIndex = this.buffer.indexOf("\n");
|
|
222
|
+
while (newlineIndex !== -1) {
|
|
223
|
+
const line = this.buffer.slice(0, newlineIndex);
|
|
224
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
225
|
+
if (line.trim()) {
|
|
226
|
+
this.processMessage(line);
|
|
227
|
+
}
|
|
228
|
+
newlineIndex = this.buffer.indexOf("\n");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
processMessage(line) {
|
|
232
|
+
let parsed;
|
|
233
|
+
try {
|
|
234
|
+
parsed = JSON.parse(line);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
console.warn("[gtkx] Received invalid JSON from MCP server");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const responseResult = IpcResponseSchema.safeParse(parsed);
|
|
241
|
+
if (responseResult.success) {
|
|
242
|
+
const response = responseResult.data;
|
|
243
|
+
const pending = this.pendingRequests.get(response.id);
|
|
244
|
+
if (pending) {
|
|
245
|
+
clearTimeout(pending.timeout);
|
|
246
|
+
this.pendingRequests.delete(response.id);
|
|
247
|
+
if (response.error) {
|
|
248
|
+
pending.reject(new Error(response.error.message));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
pending.resolve(response.result);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const requestResult = IpcRequestSchema.safeParse(parsed);
|
|
257
|
+
if (!requestResult.success) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.handleRequest(requestResult.data).catch((error) => {
|
|
261
|
+
console.error("[gtkx] Error handling request:", error);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async handleRequest(request) {
|
|
265
|
+
const { id, method, params } = request;
|
|
266
|
+
try {
|
|
267
|
+
const result = await this.executeMethod(method, params);
|
|
268
|
+
this.send({ id, result });
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (error instanceof McpError) {
|
|
272
|
+
this.send({ id, error: error.toIpcError() });
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
this.send({
|
|
277
|
+
id,
|
|
278
|
+
error: {
|
|
279
|
+
code: McpErrorCode.INTERNAL_ERROR,
|
|
280
|
+
message,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async executeMethod(method, params) {
|
|
287
|
+
const app = getApplication();
|
|
288
|
+
if (!app) {
|
|
289
|
+
throw new Error("Application not initialized");
|
|
290
|
+
}
|
|
291
|
+
refreshWidgetRegistry();
|
|
292
|
+
switch (method) {
|
|
293
|
+
case "widget.getTree": {
|
|
294
|
+
const testing = await loadTestingModule();
|
|
295
|
+
return { tree: testing.prettyWidget(app, { includeIds: true, highlight: false }) };
|
|
296
|
+
}
|
|
297
|
+
case "widget.query": {
|
|
298
|
+
const testing = await loadTestingModule();
|
|
299
|
+
const p = params;
|
|
300
|
+
let widgets = [];
|
|
301
|
+
switch (p.queryType) {
|
|
302
|
+
case "role":
|
|
303
|
+
widgets = await testing.findAllByRole(app, p.value, p.options);
|
|
304
|
+
break;
|
|
305
|
+
case "text":
|
|
306
|
+
widgets = await testing.findAllByText(app, String(p.value), p.options);
|
|
307
|
+
break;
|
|
308
|
+
case "testId":
|
|
309
|
+
widgets = await testing.findAllByTestId(app, String(p.value), p.options);
|
|
310
|
+
break;
|
|
311
|
+
case "labelText":
|
|
312
|
+
widgets = await testing.findAllByLabelText(app, String(p.value), p.options);
|
|
313
|
+
break;
|
|
314
|
+
default:
|
|
315
|
+
throw new Error(`Unknown query type: ${p.queryType}`);
|
|
316
|
+
}
|
|
317
|
+
return { widgets: widgets.map((w) => serializeWidget(w)) };
|
|
318
|
+
}
|
|
319
|
+
case "widget.getProps": {
|
|
320
|
+
const p = params;
|
|
321
|
+
const widget = getWidgetById(p.widgetId);
|
|
322
|
+
if (!widget) {
|
|
323
|
+
throw widgetNotFoundError(p.widgetId);
|
|
324
|
+
}
|
|
325
|
+
return serializeWidget(widget);
|
|
326
|
+
}
|
|
327
|
+
case "widget.click": {
|
|
328
|
+
const testing = await loadTestingModule();
|
|
329
|
+
const p = params;
|
|
330
|
+
const widget = getWidgetById(p.widgetId);
|
|
331
|
+
if (!widget) {
|
|
332
|
+
throw widgetNotFoundError(p.widgetId);
|
|
333
|
+
}
|
|
334
|
+
await testing.userEvent.click(widget);
|
|
335
|
+
return { success: true };
|
|
336
|
+
}
|
|
337
|
+
case "widget.type": {
|
|
338
|
+
const testing = await loadTestingModule();
|
|
339
|
+
const p = params;
|
|
340
|
+
const widget = getWidgetById(p.widgetId);
|
|
341
|
+
if (!widget) {
|
|
342
|
+
throw widgetNotFoundError(p.widgetId);
|
|
343
|
+
}
|
|
344
|
+
if (p.clear) {
|
|
345
|
+
await testing.userEvent.clear(widget);
|
|
346
|
+
}
|
|
347
|
+
await testing.userEvent.type(widget, p.text);
|
|
348
|
+
return { success: true };
|
|
349
|
+
}
|
|
350
|
+
case "widget.fireEvent": {
|
|
351
|
+
const testing = await loadTestingModule();
|
|
352
|
+
const p = params;
|
|
353
|
+
const widget = getWidgetById(p.widgetId);
|
|
354
|
+
if (!widget) {
|
|
355
|
+
throw widgetNotFoundError(p.widgetId);
|
|
356
|
+
}
|
|
357
|
+
const signalArgs = (p.args ?? []);
|
|
358
|
+
await testing.fireEvent(widget, p.signal, ...signalArgs);
|
|
359
|
+
return { success: true };
|
|
360
|
+
}
|
|
361
|
+
case "widget.screenshot": {
|
|
362
|
+
const testing = await loadTestingModule();
|
|
363
|
+
const p = params;
|
|
364
|
+
let targetWindow;
|
|
365
|
+
if (p.windowId) {
|
|
366
|
+
const widget = getWidgetById(p.windowId);
|
|
367
|
+
if (!widget) {
|
|
368
|
+
throw widgetNotFoundError(p.windowId);
|
|
369
|
+
}
|
|
370
|
+
targetWindow = widget;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const windows = app.getWindows();
|
|
374
|
+
if (windows.length === 0) {
|
|
375
|
+
throw new Error("No windows available for screenshot");
|
|
376
|
+
}
|
|
377
|
+
targetWindow = windows[0];
|
|
378
|
+
}
|
|
379
|
+
const result = await testing.screenshot(targetWindow);
|
|
380
|
+
return { data: result.data, mimeType: result.mimeType };
|
|
381
|
+
}
|
|
382
|
+
default:
|
|
383
|
+
throw methodNotFoundError(method);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
let globalClient = null;
|
|
388
|
+
export const startMcpClient = async (appId) => {
|
|
389
|
+
if (globalClient) {
|
|
390
|
+
return globalClient;
|
|
391
|
+
}
|
|
392
|
+
globalClient = new McpClient({ appId });
|
|
393
|
+
await globalClient.connect().catch(() => { });
|
|
394
|
+
return globalClient;
|
|
395
|
+
};
|
|
396
|
+
export const stopMcpClient = () => {
|
|
397
|
+
if (globalClient) {
|
|
398
|
+
globalClient.disconnect();
|
|
399
|
+
globalClient = null;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import RefreshRuntime from "react-refresh/runtime";
|
|
2
2
|
type ComponentType = (...args: unknown[]) => unknown;
|
|
3
|
+
/**
|
|
4
|
+
* Creates registration functions for a module's React components.
|
|
5
|
+
*
|
|
6
|
+
* Used internally by the Vite plugin to register components
|
|
7
|
+
* for React Fast Refresh.
|
|
8
|
+
*
|
|
9
|
+
* @param moduleId - Unique identifier for the module
|
|
10
|
+
* @returns Registration functions for the module
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
3
13
|
export declare function createModuleRegistration(moduleId: string): {
|
|
4
14
|
$RefreshReg$: (type: ComponentType, id: string) => void;
|
|
5
15
|
$RefreshSig$: typeof RefreshRuntime.createSignatureFunctionForTransform;
|
|
6
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a module's exports form a React Refresh boundary.
|
|
19
|
+
*
|
|
20
|
+
* A module is a refresh boundary if all its exports are React components,
|
|
21
|
+
* allowing for fast refresh without full page reload.
|
|
22
|
+
*
|
|
23
|
+
* @param moduleExports - The module's exports object
|
|
24
|
+
* @returns `true` if the module can be fast-refreshed
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
7
27
|
export declare function isReactRefreshBoundary(moduleExports: Record<string, unknown>): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Triggers React Fast Refresh to re-render components.
|
|
30
|
+
*
|
|
31
|
+
* Called after module updates when all exports are React components.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
8
35
|
export declare function performRefresh(): void;
|
|
9
36
|
export {};
|
package/dist/refresh-runtime.js
CHANGED
|
@@ -2,6 +2,16 @@ import RefreshRuntime from "react-refresh/runtime";
|
|
|
2
2
|
RefreshRuntime.injectIntoGlobalHook(globalThis);
|
|
3
3
|
globalThis.$RefreshReg$ = () => { };
|
|
4
4
|
globalThis.$RefreshSig$ = () => (type) => type;
|
|
5
|
+
/**
|
|
6
|
+
* Creates registration functions for a module's React components.
|
|
7
|
+
*
|
|
8
|
+
* Used internally by the Vite plugin to register components
|
|
9
|
+
* for React Fast Refresh.
|
|
10
|
+
*
|
|
11
|
+
* @param moduleId - Unique identifier for the module
|
|
12
|
+
* @returns Registration functions for the module
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
5
15
|
export function createModuleRegistration(moduleId) {
|
|
6
16
|
return {
|
|
7
17
|
$RefreshReg$: (type, id) => {
|
|
@@ -24,6 +34,16 @@ function isLikelyComponentType(value) {
|
|
|
24
34
|
}
|
|
25
35
|
return false;
|
|
26
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a module's exports form a React Refresh boundary.
|
|
39
|
+
*
|
|
40
|
+
* A module is a refresh boundary if all its exports are React components,
|
|
41
|
+
* allowing for fast refresh without full page reload.
|
|
42
|
+
*
|
|
43
|
+
* @param moduleExports - The module's exports object
|
|
44
|
+
* @returns `true` if the module can be fast-refreshed
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
27
47
|
export function isReactRefreshBoundary(moduleExports) {
|
|
28
48
|
if (RefreshRuntime.isLikelyComponentType(moduleExports)) {
|
|
29
49
|
return true;
|
|
@@ -39,6 +59,13 @@ export function isReactRefreshBoundary(moduleExports) {
|
|
|
39
59
|
}
|
|
40
60
|
return Object.keys(moduleExports).filter((k) => k !== "__esModule").length > 0;
|
|
41
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Triggers React Fast Refresh to re-render components.
|
|
64
|
+
*
|
|
65
|
+
* Called after module updates when all exports are React components.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
42
69
|
export function performRefresh() {
|
|
43
70
|
RefreshRuntime.performReactRefresh();
|
|
44
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "CLI for GTKX - create and develop GTK4 React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -58,16 +58,24 @@
|
|
|
58
58
|
"ejs": "^3.1.10",
|
|
59
59
|
"react-refresh": "^0.18.0",
|
|
60
60
|
"vite": "^7.3.0",
|
|
61
|
-
"@gtkx/
|
|
62
|
-
"@gtkx/
|
|
61
|
+
"@gtkx/mcp": "0.12.0",
|
|
62
|
+
"@gtkx/ffi": "0.12.0",
|
|
63
|
+
"@gtkx/react": "0.12.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@types/ejs": "^3.1.5",
|
|
66
67
|
"@types/react-refresh": "^0.14.7",
|
|
67
|
-
"memfs": "^4.51.1"
|
|
68
|
+
"memfs": "^4.51.1",
|
|
69
|
+
"@gtkx/testing": "0.12.0"
|
|
68
70
|
},
|
|
69
71
|
"peerDependencies": {
|
|
70
|
-
"react": "^19"
|
|
72
|
+
"react": "^19",
|
|
73
|
+
"@gtkx/testing": "0.12.0"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"@gtkx/testing": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
71
79
|
},
|
|
72
80
|
"scripts": {
|
|
73
81
|
"build": "tsc -b",
|