@devscholar/node-with-gjs 0.0.0 → 0.0.2
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 +5 -75
- package/__tests__/basic.test.ts +41 -0
- package/__tests__/glib.test.ts +72 -0
- package/__tests__/gtk4.test.ts +73 -0
- package/dist/index.js +211 -0
- package/dist/ipc.js +103 -0
- package/docs/testing.md +100 -0
- package/hook.js +44 -42
- package/jest.config.js +32 -0
- package/package.json +23 -14
- package/scripts/host.js +3 -1
- package/src/index.ts +40 -2
- package/tsconfig.json +22 -11
- package/types/index.d.ts +4 -0
- package/types/ipc.d.ts +10 -0
- package/examples/adwaita/counter/counter.ts +0 -66
- package/examples/console/await-delay/await-delay.ts +0 -11
- package/examples/console/console-input/console-input.ts +0 -27
- package/examples/gtk/counter/counter.ts +0 -62
- package/examples/gtk/drag-box/drag-box.ts +0 -77
- package/examples/gtk-webkit/counter/counter.html +0 -47
- package/examples/gtk-webkit/counter/counter.ts +0 -101
- package/gi-loader.ts +0 -13
- package/start.js +0 -105
package/README.md
CHANGED
|
@@ -31,87 +31,17 @@ pacman -S gtk4 webkitgtk-6.0 gjs
|
|
|
31
31
|
|
|
32
32
|
# Usage
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
For more examples and details, see the [node-with-gjs-examples README](https://github.com/devscholar/node-with-gjs-examples).
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|---------|-------------------|--------------|
|
|
38
|
-
| Node.js | ✅ Supported | ✅ `--experimental-loader` |
|
|
39
|
-
| Bun | ❌ Not supported | ❌ No hooks mechanism |
|
|
40
|
-
| Deno | ❌ Not supported | ❌ No hooks mechanism |
|
|
36
|
+
# Tests
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Node.js supports the `gi://` URL syntax, which is consistent with GJS:
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
import Gtk from 'gi://Gtk?version=4.0';
|
|
48
|
-
import WebKit from 'gi://WebKit?version=6.0';
|
|
49
|
-
|
|
50
|
-
const app = new Gtk.Application({ application_id: 'org.example.app' });
|
|
51
|
-
// ...
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Bun / Deno
|
|
55
|
-
|
|
56
|
-
Bun and Deno do not support Node.js loader hooks, so you need to use the `loadGi` function instead:
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
import { loadGi } from './gi-loader.ts';
|
|
60
|
-
|
|
61
|
-
const Gtk = loadGi('Gtk', '4.0');
|
|
62
|
-
const WebKit = loadGi('WebKit', '6.0');
|
|
63
|
-
|
|
64
|
-
const app = new Gtk.Application({ application_id: 'org.example.app' });
|
|
65
|
-
// ...
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Note:** The `loadGi` function also works with Node.js, useful for older Node.js versions or when not using experimental loader flags.
|
|
69
|
-
|
|
70
|
-
# Examples
|
|
71
|
-
|
|
72
|
-
## Console Apps
|
|
73
|
-
|
|
74
|
-
### Console Input App
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
node start.js examples/console/console-input/console-input.ts
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Await Delay App
|
|
38
|
+
Run all tests:
|
|
81
39
|
|
|
82
40
|
```bash
|
|
83
|
-
|
|
41
|
+
npm test
|
|
84
42
|
```
|
|
85
43
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
### GTK4 Counter App
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
node start.js examples/gtk/counter/counter.ts
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### GTK4 Drag Box App
|
|
95
|
-
|
|
96
|
-
A drag box example that demonstrates high frequency IPC.
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
node start.js examples/gtk/drag-box/drag-box.ts
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### GTK4 WebKit Counter App
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
node start.js examples/gtk-webkit/counter/counter.ts
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Adwaita Counter App (libadwaita)
|
|
109
|
-
|
|
110
|
-
A counter example based on [libadwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/), demonstrating how to use Adwaita-specific components like `Adw.ApplicationWindow` and `Adw.Clamp`.
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
node start.js examples/adwaita/counter/counter.ts
|
|
114
|
-
```
|
|
44
|
+
For detailed testing documentation, see [docs/testing.md](docs/testing.md).
|
|
115
45
|
|
|
116
46
|
# License
|
|
117
47
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
const isLinux = process.platform === 'linux' || process.platform === 'darwin';
|
|
4
|
+
|
|
5
|
+
(isLinux ? describe : describe.skip)('Basic Module Tests', () => {
|
|
6
|
+
let gjs: any;
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
try {
|
|
10
|
+
gjs = await import('../src/index.js');
|
|
11
|
+
gjs.init();
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.log('Skipping basic tests - load failed:', e);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should export init function', () => {
|
|
21
|
+
expect(gjs.init).toBeDefined();
|
|
22
|
+
expect(typeof gjs.init).toBe('function');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should export imports object', () => {
|
|
26
|
+
expect(gjs.imports).toBeDefined();
|
|
27
|
+
expect(gjs.imports.gi).toBeDefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should have gi proxy', () => {
|
|
31
|
+
const gi = gjs.imports.gi;
|
|
32
|
+
expect(gi).toBeDefined();
|
|
33
|
+
expect(typeof gi).toBe('object');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should support gi.versions', () => {
|
|
37
|
+
const gi = gjs.imports.gi;
|
|
38
|
+
expect(gi.versions).toBeDefined();
|
|
39
|
+
expect(typeof gi.versions).toBe('object');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
const isLinux = process.platform === 'linux' || process.platform === 'darwin';
|
|
4
|
+
|
|
5
|
+
(isLinux ? describe : describe.skip)('GLib Tests', () => {
|
|
6
|
+
let gjs: any;
|
|
7
|
+
let GLib: any;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
try {
|
|
11
|
+
gjs = await import('../src/index.js');
|
|
12
|
+
gjs.init();
|
|
13
|
+
const gi = gjs.imports.gi;
|
|
14
|
+
GLib = gi.GLib;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.log('Skipping GLib tests - load failed:', e);
|
|
17
|
+
}
|
|
18
|
+
}, 60000);
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should load GLib namespace', () => {
|
|
24
|
+
expect(GLib).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should get user name', () => {
|
|
28
|
+
const userName = GLib.get_user_name();
|
|
29
|
+
expect(userName).toBeDefined();
|
|
30
|
+
expect(typeof userName).toBe('string');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should get home dir', () => {
|
|
34
|
+
const home = GLib.get_home_dir();
|
|
35
|
+
expect(home).toBeDefined();
|
|
36
|
+
expect(typeof home).toBe('string');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should get tmp dir', () => {
|
|
40
|
+
const tmp = GLib.get_tmp_dir();
|
|
41
|
+
expect(tmp).toBeDefined();
|
|
42
|
+
expect(typeof tmp).toBe('string');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
(isLinux ? describe : describe.skip)('GObject Tests', () => {
|
|
47
|
+
let gjs: any;
|
|
48
|
+
let GObject: any;
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
try {
|
|
52
|
+
gjs = await import('../src/index.js');
|
|
53
|
+
gjs.init();
|
|
54
|
+
const gi = gjs.imports.gi;
|
|
55
|
+
GObject = gi.GObject;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.log('Skipping GObject tests - load failed:', e);
|
|
58
|
+
}
|
|
59
|
+
}, 60000);
|
|
60
|
+
|
|
61
|
+
afterAll(() => {
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should load GObject namespace', () => {
|
|
65
|
+
expect(GObject).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should have type system', () => {
|
|
69
|
+
expect(GObject.type_from_name).toBeDefined();
|
|
70
|
+
expect(GObject.type_register_fundamental).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
const isLinux = process.platform === 'linux' || process.platform === 'darwin';
|
|
4
|
+
|
|
5
|
+
(isLinux ? describe : describe.skip)('GTK4 GUI Tests', () => {
|
|
6
|
+
let gjs: any;
|
|
7
|
+
let Gtk: any;
|
|
8
|
+
let GLib: any;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
try {
|
|
12
|
+
gjs = await import('../src/index.js');
|
|
13
|
+
gjs.init();
|
|
14
|
+
const gi = gjs.imports.gi;
|
|
15
|
+
gi.versions.Gtk = '4.0';
|
|
16
|
+
Gtk = gi.Gtk;
|
|
17
|
+
GLib = gi.GLib;
|
|
18
|
+
Gtk.init();
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.log('Skipping GTK4 tests - load failed:', e);
|
|
21
|
+
}
|
|
22
|
+
}, 60000);
|
|
23
|
+
|
|
24
|
+
afterAll(() => {
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should load Gtk namespace', () => {
|
|
28
|
+
expect(Gtk).toBeDefined();
|
|
29
|
+
expect(Gtk.Application).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should load GLib namespace', () => {
|
|
33
|
+
expect(GLib).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should create an Application instance', () => {
|
|
37
|
+
const app = new Gtk.Application({ application_id: 'org.test.app' });
|
|
38
|
+
expect(app).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should create a Box container', () => {
|
|
42
|
+
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 10 });
|
|
43
|
+
expect(box).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should create a Label', () => {
|
|
47
|
+
const label = new Gtk.Label({ label: 'Test Label' });
|
|
48
|
+
expect(label).toBeDefined();
|
|
49
|
+
expect(label.get_label()).toBe('Test Label');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should create a Button', () => {
|
|
53
|
+
const button = new Gtk.Button({ label: 'Click Me' });
|
|
54
|
+
expect(button).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should set Label text', () => {
|
|
58
|
+
const label = new Gtk.Label({ label: 'Initial' });
|
|
59
|
+
label.set_label('Updated');
|
|
60
|
+
expect(label.get_label()).toBe('Updated');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should use Orientation enum', () => {
|
|
64
|
+
expect(Gtk.Orientation.VERTICAL).toBeDefined();
|
|
65
|
+
expect(Gtk.Orientation.HORIZONTAL).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should use Align enum', () => {
|
|
69
|
+
expect(Gtk.Align.CENTER).toBeDefined();
|
|
70
|
+
expect(Gtk.Align.START).toBeDefined();
|
|
71
|
+
expect(Gtk.Align.END).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as cp from 'node:child_process';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { IpcSync } from './ipc.js';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const gcRegistry = new FinalizationRegistry((id) => {
|
|
11
|
+
try {
|
|
12
|
+
if (ipc)
|
|
13
|
+
ipc.send({ action: 'Release', targetId: id });
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
});
|
|
17
|
+
const callbackRegistry = new Map();
|
|
18
|
+
let ipc = null;
|
|
19
|
+
let proc = null;
|
|
20
|
+
let initialized = false;
|
|
21
|
+
let reqPath = '';
|
|
22
|
+
let resPath = '';
|
|
23
|
+
function cleanup() {
|
|
24
|
+
if (!initialized)
|
|
25
|
+
return;
|
|
26
|
+
initialized = false;
|
|
27
|
+
if (ipc)
|
|
28
|
+
try {
|
|
29
|
+
ipc.close();
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
if (proc && !proc.killed)
|
|
33
|
+
try {
|
|
34
|
+
proc.kill('SIGKILL');
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
if (fs.existsSync(reqPath))
|
|
38
|
+
try {
|
|
39
|
+
fs.unlinkSync(reqPath);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
if (fs.existsSync(resPath))
|
|
43
|
+
try {
|
|
44
|
+
fs.unlinkSync(resPath);
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
proc = null;
|
|
48
|
+
ipc = null;
|
|
49
|
+
}
|
|
50
|
+
function findGjsPath() {
|
|
51
|
+
try {
|
|
52
|
+
const result = cp.execSync('which gjs', { encoding: 'utf-8' }).trim();
|
|
53
|
+
return result || 'gjs';
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return 'gjs';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function initialize() {
|
|
60
|
+
if (initialized)
|
|
61
|
+
return;
|
|
62
|
+
const token = `${process.pid}-${Date.now()}`;
|
|
63
|
+
reqPath = path.join(os.tmpdir(), `gjs-req-${token}.pipe`);
|
|
64
|
+
resPath = path.join(os.tmpdir(), `gjs-res-${token}.pipe`);
|
|
65
|
+
try {
|
|
66
|
+
cp.execSync(`mkfifo "${reqPath}"`);
|
|
67
|
+
cp.execSync(`mkfifo "${resPath}"`);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
console.error("Failed to create Unix FIFOs");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const scriptPath = path.join(__dirname, '..', 'scripts', 'host.js');
|
|
74
|
+
const gjsPath = findGjsPath();
|
|
75
|
+
proc = cp.spawn('bash', ['-c', `exec "${gjsPath}" -m "${scriptPath}" 3<"${reqPath}" 4>"${resPath}"`], {
|
|
76
|
+
stdio: 'inherit',
|
|
77
|
+
env: process.env
|
|
78
|
+
});
|
|
79
|
+
proc.unref();
|
|
80
|
+
process.on('beforeExit', () => { cleanup(); process.exit(0); });
|
|
81
|
+
process.on('exit', cleanup);
|
|
82
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
83
|
+
process.on('uncaughtException', (err) => {
|
|
84
|
+
console.error('Node.js Exception:', err);
|
|
85
|
+
cleanup();
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
88
|
+
const fdWrite = fs.openSync(reqPath, 'w');
|
|
89
|
+
const fdRead = fs.openSync(resPath, 'r');
|
|
90
|
+
ipc = new IpcSync(fdRead, fdWrite, (res) => {
|
|
91
|
+
const cb = callbackRegistry.get(res.callbackId);
|
|
92
|
+
if (cb) {
|
|
93
|
+
const wrappedArgs = (res.args || []).map((arg) => createProxy(arg));
|
|
94
|
+
return cb(...wrappedArgs);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
});
|
|
98
|
+
globalThis.print = (...args) => {
|
|
99
|
+
ipc.send({ action: 'Print', args: args.map(wrapArg) });
|
|
100
|
+
};
|
|
101
|
+
initialized = true;
|
|
102
|
+
}
|
|
103
|
+
function wrapArg(arg) {
|
|
104
|
+
if (arg === null || arg === undefined)
|
|
105
|
+
return { type: 'null' };
|
|
106
|
+
if (arg.__ref)
|
|
107
|
+
return { type: 'ref', id: arg.__ref };
|
|
108
|
+
if (arg instanceof Uint8Array) {
|
|
109
|
+
return { type: 'uint8array', value: Array.from(arg) };
|
|
110
|
+
}
|
|
111
|
+
if (typeof arg === 'function') {
|
|
112
|
+
const cbId = `cb_${Date.now()}_${Math.random()}`;
|
|
113
|
+
callbackRegistry.set(cbId, arg);
|
|
114
|
+
return { type: 'callback', callbackId: cbId };
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(arg))
|
|
117
|
+
return { type: 'array', value: arg.map(wrapArg) };
|
|
118
|
+
if (typeof arg === 'object') {
|
|
119
|
+
const plainObj = {};
|
|
120
|
+
for (let k in arg)
|
|
121
|
+
plainObj[k] = wrapArg(arg[k]);
|
|
122
|
+
return { type: 'object', value: plainObj };
|
|
123
|
+
}
|
|
124
|
+
return { type: 'primitive', value: arg };
|
|
125
|
+
}
|
|
126
|
+
function createProxy(meta) {
|
|
127
|
+
if (meta.type === 'primitive' || meta.type === 'null')
|
|
128
|
+
return meta.value;
|
|
129
|
+
if (meta.type === 'array')
|
|
130
|
+
return meta.value.map((item) => createProxy(item));
|
|
131
|
+
if (meta.type !== 'ref')
|
|
132
|
+
return undefined;
|
|
133
|
+
const id = meta.id;
|
|
134
|
+
const stub = function () { };
|
|
135
|
+
const proxy = new Proxy(stub, {
|
|
136
|
+
get: (target, prop) => {
|
|
137
|
+
if (prop === '__ref')
|
|
138
|
+
return id;
|
|
139
|
+
if (typeof prop !== 'string')
|
|
140
|
+
return undefined;
|
|
141
|
+
const val = ipc.send({ action: 'Get', targetId: id, property: prop });
|
|
142
|
+
if (val && val.type === 'function') {
|
|
143
|
+
return new Proxy(function () { }, {
|
|
144
|
+
apply: (t, thisArg, args) => {
|
|
145
|
+
const netArgs = args.map(wrapArg);
|
|
146
|
+
const res = ipc.send({ action: 'Invoke', targetId: id, methodName: prop, args: netArgs });
|
|
147
|
+
return createProxy(res);
|
|
148
|
+
},
|
|
149
|
+
construct: (t, args) => {
|
|
150
|
+
const netArgs = args.map(wrapArg);
|
|
151
|
+
const res = ipc.send({ action: 'NewProp', targetId: id, property: prop, args: netArgs });
|
|
152
|
+
return createProxy(res);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return createProxy(val);
|
|
157
|
+
},
|
|
158
|
+
set: (target, prop, value) => {
|
|
159
|
+
if (typeof prop !== 'string')
|
|
160
|
+
return false;
|
|
161
|
+
ipc.send({ action: 'Set', targetId: id, property: prop, value: wrapArg(value) });
|
|
162
|
+
return true;
|
|
163
|
+
},
|
|
164
|
+
construct: (target, args) => {
|
|
165
|
+
const netArgs = args.map(wrapArg);
|
|
166
|
+
const res = ipc.send({ action: 'New', typeId: id, args: netArgs });
|
|
167
|
+
return createProxy(res);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
gcRegistry.register(proxy, id);
|
|
171
|
+
return proxy;
|
|
172
|
+
}
|
|
173
|
+
export function init() {
|
|
174
|
+
initialize();
|
|
175
|
+
}
|
|
176
|
+
// Internal function - not exposed to users
|
|
177
|
+
function loadGiNamespace(namespace, version) {
|
|
178
|
+
initialize();
|
|
179
|
+
const res = ipc.send({ action: 'LoadNamespace', namespace, version });
|
|
180
|
+
return createProxy(res);
|
|
181
|
+
}
|
|
182
|
+
// Namespace cache to avoid creating multiple proxies for the same namespace
|
|
183
|
+
const namespaceCache = new Map();
|
|
184
|
+
// GI namespace versions
|
|
185
|
+
const giVersions = {};
|
|
186
|
+
// Create the gi proxy with lazy loading and caching
|
|
187
|
+
const giProxy = new Proxy({}, {
|
|
188
|
+
get(_, namespace) {
|
|
189
|
+
if (namespace === 'versions') {
|
|
190
|
+
return new Proxy(giVersions, {
|
|
191
|
+
set(target, prop, value) {
|
|
192
|
+
target[prop] = value;
|
|
193
|
+
// Clear cache for this namespace when version changes
|
|
194
|
+
const cacheKey = `${prop}@default`;
|
|
195
|
+
namespaceCache.delete(cacheKey);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const version = giVersions[namespace];
|
|
201
|
+
const cacheKey = `${namespace}@${version || 'default'}`;
|
|
202
|
+
if (!namespaceCache.has(cacheKey)) {
|
|
203
|
+
namespaceCache.set(cacheKey, loadGiNamespace(namespace, version));
|
|
204
|
+
}
|
|
205
|
+
return namespaceCache.get(cacheKey);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// The main exports object - compatible with GJS imports
|
|
209
|
+
export const imports = {
|
|
210
|
+
gi: giProxy
|
|
211
|
+
};
|
package/dist/ipc.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/ipc.ts
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
// Use a global/shared read buffer to handle redundant data across calls
|
|
4
|
+
let readBuffer = Buffer.alloc(0);
|
|
5
|
+
export function readLineSync(fd) {
|
|
6
|
+
while (true) {
|
|
7
|
+
// 1. If the buffer already contains a complete line, extract and return it with minimal overhead
|
|
8
|
+
const newlineIdx = readBuffer.indexOf(10); // 10 is the ASCII code for \n
|
|
9
|
+
if (newlineIdx !== -1) {
|
|
10
|
+
const line = readBuffer.subarray(0, newlineIdx).toString('utf8');
|
|
11
|
+
readBuffer = readBuffer.subarray(newlineIdx + 1);
|
|
12
|
+
return line;
|
|
13
|
+
}
|
|
14
|
+
// 2. Otherwise, try to read a large chunk from the pipe
|
|
15
|
+
const chunk = Buffer.alloc(8192); // Attempt to read 8KB each time
|
|
16
|
+
let bytesRead = 0;
|
|
17
|
+
try {
|
|
18
|
+
bytesRead = fs.readSync(fd, chunk, 0, 8192, null);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (bytesRead === 0) {
|
|
24
|
+
if (readBuffer.length === 0)
|
|
25
|
+
return null;
|
|
26
|
+
const line = readBuffer.toString('utf8');
|
|
27
|
+
readBuffer = Buffer.alloc(0);
|
|
28
|
+
return line;
|
|
29
|
+
}
|
|
30
|
+
// 3. Append the newly read data to the buffer
|
|
31
|
+
readBuffer = Buffer.concat([readBuffer, chunk.subarray(0, bytesRead)]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class IpcSync {
|
|
35
|
+
fdRead;
|
|
36
|
+
fdWrite;
|
|
37
|
+
onEvent;
|
|
38
|
+
exited = false;
|
|
39
|
+
constructor(fdRead, fdWrite, onEvent) {
|
|
40
|
+
this.fdRead = fdRead;
|
|
41
|
+
this.fdWrite = fdWrite;
|
|
42
|
+
this.onEvent = onEvent;
|
|
43
|
+
}
|
|
44
|
+
send(cmd) {
|
|
45
|
+
if (this.exited)
|
|
46
|
+
return { type: 'exit' };
|
|
47
|
+
try {
|
|
48
|
+
fs.writeSync(this.fdWrite, JSON.stringify(cmd) + '\n');
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
throw new Error("Pipe closed (Write failed)");
|
|
52
|
+
}
|
|
53
|
+
while (true) {
|
|
54
|
+
const line = readLineSync(this.fdRead);
|
|
55
|
+
if (line === null)
|
|
56
|
+
throw new Error("Pipe closed (Read EOF)");
|
|
57
|
+
if (!line.trim())
|
|
58
|
+
continue;
|
|
59
|
+
let res;
|
|
60
|
+
try {
|
|
61
|
+
res = JSON.parse(line);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
throw new Error(`Invalid JSON from host: ${line}`);
|
|
65
|
+
}
|
|
66
|
+
if (res.type === 'event') {
|
|
67
|
+
let result = null;
|
|
68
|
+
try {
|
|
69
|
+
result = this.onEvent(res);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.error("Callback Error:", e);
|
|
73
|
+
}
|
|
74
|
+
const reply = { type: 'reply', result: result };
|
|
75
|
+
try {
|
|
76
|
+
fs.writeSync(this.fdWrite, JSON.stringify(reply) + '\n');
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (res.type === 'error')
|
|
82
|
+
throw new Error(`GJS Host Error: ${res.message}`);
|
|
83
|
+
if (res.type === 'exit') {
|
|
84
|
+
this.exited = true;
|
|
85
|
+
return res;
|
|
86
|
+
}
|
|
87
|
+
return res;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
close() {
|
|
91
|
+
this.exited = true;
|
|
92
|
+
if (this.fdRead)
|
|
93
|
+
try {
|
|
94
|
+
fs.closeSync(this.fdRead);
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
if (this.fdWrite)
|
|
98
|
+
try {
|
|
99
|
+
fs.closeSync(this.fdWrite);
|
|
100
|
+
}
|
|
101
|
+
catch { }
|
|
102
|
+
}
|
|
103
|
+
}
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
This document describes how to run tests for the `node-with-gjs` project.
|
|
4
|
+
|
|
5
|
+
## Running Tests
|
|
6
|
+
|
|
7
|
+
### Run All Tests
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm test
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Run Specific Test Files
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm test -- --testPathPatterns=gtk4
|
|
17
|
+
npm test -- --testPathPatterns=glib
|
|
18
|
+
npm test -- --testPathPatterns=basic
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Test Configuration
|
|
22
|
+
|
|
23
|
+
Tests are configured to run **serially** (`maxWorkers: 1`) because the module uses a singleton pattern with global state for the IPC connection to GJS. Running tests in parallel would cause conflicts between multiple GJS processes.
|
|
24
|
+
|
|
25
|
+
If you need to run tests in parallel, you would need to refactor the module to support multiple instances (see [jest.config.js](../jest.config.js)).
|
|
26
|
+
|
|
27
|
+
## Test Files
|
|
28
|
+
|
|
29
|
+
Test files are located in the `__tests__` directory:
|
|
30
|
+
|
|
31
|
+
| File | Description |
|
|
32
|
+
|------|-------------|
|
|
33
|
+
| `basic.test.ts` | Basic module functionality tests (init, imports.gi) |
|
|
34
|
+
| `gtk4.test.ts` | GTK4 GUI component tests (Application, Box, Label, Button, enums) |
|
|
35
|
+
| `glib.test.ts` | GLib and GObject tests (get_user_name, get_home_dir, etc.) |
|
|
36
|
+
|
|
37
|
+
## Platform Support
|
|
38
|
+
|
|
39
|
+
| Platform | Support |
|
|
40
|
+
|----------|---------|
|
|
41
|
+
| Linux | All tests run normally |
|
|
42
|
+
| macOS | Tests are skipped (GJS is Linux-only) |
|
|
43
|
+
| Windows | Tests are skipped (GJS is Linux-only) |
|
|
44
|
+
|
|
45
|
+
Tests automatically detect the platform and skip on non-Linux systems.
|
|
46
|
+
|
|
47
|
+
## GUI Testing
|
|
48
|
+
|
|
49
|
+
GUI tests create GTK objects in memory without displaying windows. This allows testing without interfering with the desktop environment. The tests verify:
|
|
50
|
+
|
|
51
|
+
- Object creation (Application, Window, Box, Label, Button)
|
|
52
|
+
- Property getters/setters
|
|
53
|
+
- Method calls
|
|
54
|
+
- Enum values
|
|
55
|
+
|
|
56
|
+
For testing with actual window display and event handling, see the [node-with-gjs-examples](https://github.com/devscholar/node-with-gjs-examples) repository.
|
|
57
|
+
|
|
58
|
+
## Writing New Tests
|
|
59
|
+
|
|
60
|
+
When writing new tests, keep in mind:
|
|
61
|
+
|
|
62
|
+
1. **Initialize the module**: Always call `gjs.init()` in `beforeAll`
|
|
63
|
+
2. **Set GTK version**: Use `gi.versions.Gtk = '4.0'` before accessing Gtk
|
|
64
|
+
3. **Initialize GTK**: Call `Gtk.init()` before creating GTK objects
|
|
65
|
+
4. **Platform check**: Tests automatically skip on non-Linux platforms via `const isLinux = process.platform === 'linux' || process.platform === 'darwin'`
|
|
66
|
+
|
|
67
|
+
Example test structure:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { jest } from '@jest/globals';
|
|
71
|
+
|
|
72
|
+
const isLinux = process.platform === 'linux' || process.platform === 'darwin';
|
|
73
|
+
|
|
74
|
+
(isLinux ? describe : describe.skip)('My Tests', () => {
|
|
75
|
+
let gjs: any;
|
|
76
|
+
let Gtk: any;
|
|
77
|
+
|
|
78
|
+
beforeAll(async () => {
|
|
79
|
+
try {
|
|
80
|
+
gjs = await import('../src/index.js');
|
|
81
|
+
gjs.init();
|
|
82
|
+
const gi = gjs.imports.gi;
|
|
83
|
+
gi.versions.Gtk = '4.0';
|
|
84
|
+
Gtk = gi.Gtk;
|
|
85
|
+
Gtk.init();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.log('Skipping tests - load failed:', e);
|
|
88
|
+
}
|
|
89
|
+
}, 60000);
|
|
90
|
+
|
|
91
|
+
afterAll(() => {
|
|
92
|
+
// Cleanup if needed
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should create a widget', () => {
|
|
96
|
+
const widget = new Gtk.Label({ label: 'Test' });
|
|
97
|
+
expect(widget).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
```
|