@devscholar/node-with-gjs 0.0.2 → 0.0.4
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/__tests__/basic.test.ts +2 -4
- package/__tests__/glib.test.ts +3 -5
- package/__tests__/gtk4.test.ts +2 -4
- package/package.json +21 -9
- package/src/ipc.ts +98 -98
- package/start.js +102 -0
- package/dist/index.js +0 -211
- package/dist/ipc.js +0 -103
- package/hook.js +0 -44
- package/jest.config.js +0 -32
- package/types/index.d.ts +0 -4
- package/types/ipc.d.ts +0 -10
package/__tests__/basic.test.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
(isLinux ? describe : describe.skip)('Basic Module Tests', () => {
|
|
3
|
+
describe('Basic Module Tests', () => {
|
|
6
4
|
let gjs: any;
|
|
7
5
|
|
|
8
6
|
beforeAll(async () => {
|
package/__tests__/glib.test.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
(isLinux ? describe : describe.skip)('GLib Tests', () => {
|
|
3
|
+
describe('GLib Tests', () => {
|
|
6
4
|
let gjs: any;
|
|
7
5
|
let GLib: any;
|
|
8
6
|
|
|
@@ -43,7 +41,7 @@ const isLinux = process.platform === 'linux' || process.platform === 'darwin';
|
|
|
43
41
|
});
|
|
44
42
|
});
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
describe('GObject Tests', () => {
|
|
47
45
|
let gjs: any;
|
|
48
46
|
let GObject: any;
|
|
49
47
|
|
package/__tests__/gtk4.test.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
(isLinux ? describe : describe.skip)('GTK4 GUI Tests', () => {
|
|
3
|
+
describe('GTK4 GUI Tests', () => {
|
|
6
4
|
let gjs: any;
|
|
7
5
|
let Gtk: any;
|
|
8
6
|
let GLib: any;
|
package/package.json
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devscholar/node-with-gjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Node.js IPC Bridge for GJS",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.22.0"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"gjs",
|
|
13
|
+
"gnome",
|
|
14
|
+
"gtk",
|
|
15
|
+
"adwaita",
|
|
16
|
+
"gui",
|
|
17
|
+
"webkit",
|
|
18
|
+
"deno",
|
|
19
|
+
"bun"
|
|
20
|
+
],
|
|
7
21
|
"exports": {
|
|
8
22
|
".": "./dist/index.js"
|
|
9
23
|
},
|
|
10
|
-
"bin": {
|
|
11
|
-
|
|
12
|
-
},
|
|
24
|
+
"bin": {},
|
|
13
25
|
"scripts": {
|
|
14
26
|
"build": "tsc",
|
|
15
|
-
"test": "node --experimental-
|
|
27
|
+
"test": "node --experimental-transform-types node_modules/vitest/vitest.mjs run",
|
|
28
|
+
"start": "node start.js"
|
|
16
29
|
},
|
|
17
30
|
"devDependencies": {
|
|
18
31
|
"@types/node": "^20.0.0",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"typescript": "^5.0.0"
|
|
32
|
+
"typescript": "^5.0.0",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
22
34
|
}
|
|
23
|
-
}
|
|
35
|
+
}
|
package/src/ipc.ts
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
// src/ipc.ts
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
|
|
4
|
-
// Use a global/shared read buffer to handle redundant data across calls
|
|
5
|
-
let readBuffer = Buffer.alloc(0);
|
|
6
|
-
|
|
7
|
-
export function readLineSync(fd: number): string | null {
|
|
8
|
-
while (true) {
|
|
9
|
-
// 1. If the buffer already contains a complete line, extract and return it with minimal overhead
|
|
10
|
-
const newlineIdx = readBuffer.indexOf(10); // 10 is the ASCII code for \n
|
|
11
|
-
if (newlineIdx !== -1) {
|
|
12
|
-
const line = readBuffer.subarray(0, newlineIdx).toString('utf8');
|
|
13
|
-
readBuffer = readBuffer.subarray(newlineIdx + 1);
|
|
14
|
-
return line;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// 2. Otherwise, try to read a large chunk from the pipe
|
|
18
|
-
const chunk = Buffer.alloc(8192); // Attempt to read 8KB each time
|
|
19
|
-
let bytesRead = 0;
|
|
20
|
-
try {
|
|
21
|
-
bytesRead = fs.readSync(fd, chunk, 0, 8192, null);
|
|
22
|
-
} catch (e) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (bytesRead === 0) {
|
|
27
|
-
if (readBuffer.length === 0) return null;
|
|
28
|
-
const line = readBuffer.toString('utf8');
|
|
29
|
-
readBuffer = Buffer.alloc(0);
|
|
30
|
-
return line;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 3. Append the newly read data to the buffer
|
|
34
|
-
readBuffer = Buffer.concat([readBuffer, chunk.subarray(0, bytesRead)]);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class IpcSync {
|
|
39
|
-
private exited: boolean = false;
|
|
40
|
-
|
|
41
|
-
constructor(
|
|
42
|
-
private fdRead: number,
|
|
43
|
-
private fdWrite: number,
|
|
44
|
-
private onEvent: (msg: any) => any
|
|
45
|
-
) {}
|
|
46
|
-
|
|
47
|
-
send(cmd: any): any {
|
|
48
|
-
if (this.exited) return { type: 'exit' };
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
fs.writeSync(this.fdWrite, JSON.stringify(cmd) + '\n');
|
|
52
|
-
} catch (e) {
|
|
53
|
-
throw new Error("Pipe closed (Write failed)");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
while (true) {
|
|
57
|
-
const line = readLineSync(this.fdRead);
|
|
58
|
-
if (line === null) throw new Error("Pipe closed (Read EOF)");
|
|
59
|
-
if (!line.trim()) continue;
|
|
60
|
-
|
|
61
|
-
let res: any;
|
|
62
|
-
try {
|
|
63
|
-
res = JSON.parse(line);
|
|
64
|
-
} catch (e) {
|
|
65
|
-
throw new Error(`Invalid JSON from host: ${line}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (res.type === 'event') {
|
|
69
|
-
let result = null;
|
|
70
|
-
try {
|
|
71
|
-
result = this.onEvent(res);
|
|
72
|
-
} catch (e) {
|
|
73
|
-
console.error("Callback Error:", e);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const reply = { type: 'reply', result: result };
|
|
77
|
-
try {
|
|
78
|
-
fs.writeSync(this.fdWrite, JSON.stringify(reply) + '\n');
|
|
79
|
-
} catch {}
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (res.type === 'error') throw new Error(`GJS Host Error: ${res.message}`);
|
|
84
|
-
|
|
85
|
-
if (res.type === 'exit') {
|
|
86
|
-
this.exited = true;
|
|
87
|
-
return res;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return res;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
close() {
|
|
95
|
-
this.exited = true;
|
|
96
|
-
if (this.fdRead) try { fs.closeSync(this.fdRead); } catch {}
|
|
97
|
-
if (this.fdWrite) try { fs.closeSync(this.fdWrite); } catch {}
|
|
98
|
-
}
|
|
1
|
+
// src/ipc.ts
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Use a global/shared read buffer to handle redundant data across calls
|
|
5
|
+
let readBuffer = Buffer.alloc(0);
|
|
6
|
+
|
|
7
|
+
export function readLineSync(fd: number): string | null {
|
|
8
|
+
while (true) {
|
|
9
|
+
// 1. If the buffer already contains a complete line, extract and return it with minimal overhead
|
|
10
|
+
const newlineIdx = readBuffer.indexOf(10); // 10 is the ASCII code for \n
|
|
11
|
+
if (newlineIdx !== -1) {
|
|
12
|
+
const line = readBuffer.subarray(0, newlineIdx).toString('utf8');
|
|
13
|
+
readBuffer = readBuffer.subarray(newlineIdx + 1);
|
|
14
|
+
return line;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 2. Otherwise, try to read a large chunk from the pipe
|
|
18
|
+
const chunk = Buffer.alloc(8192); // Attempt to read 8KB each time
|
|
19
|
+
let bytesRead = 0;
|
|
20
|
+
try {
|
|
21
|
+
bytesRead = fs.readSync(fd, chunk, 0, 8192, null);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (bytesRead === 0) {
|
|
27
|
+
if (readBuffer.length === 0) return null;
|
|
28
|
+
const line = readBuffer.toString('utf8');
|
|
29
|
+
readBuffer = Buffer.alloc(0);
|
|
30
|
+
return line;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Append the newly read data to the buffer
|
|
34
|
+
readBuffer = Buffer.concat([readBuffer, chunk.subarray(0, bytesRead)]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class IpcSync {
|
|
39
|
+
private exited: boolean = false;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private fdRead: number,
|
|
43
|
+
private fdWrite: number,
|
|
44
|
+
private onEvent: (msg: any) => any
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
send(cmd: any): any {
|
|
48
|
+
if (this.exited) return { type: 'exit' };
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
fs.writeSync(this.fdWrite, JSON.stringify(cmd) + '\n');
|
|
52
|
+
} catch (e) {
|
|
53
|
+
throw new Error("Pipe closed (Write failed)");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
while (true) {
|
|
57
|
+
const line = readLineSync(this.fdRead);
|
|
58
|
+
if (line === null) throw new Error("Pipe closed (Read EOF)");
|
|
59
|
+
if (!line.trim()) continue;
|
|
60
|
+
|
|
61
|
+
let res: any;
|
|
62
|
+
try {
|
|
63
|
+
res = JSON.parse(line);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
throw new Error(`Invalid JSON from host: ${line}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (res.type === 'event') {
|
|
69
|
+
let result = null;
|
|
70
|
+
try {
|
|
71
|
+
result = this.onEvent(res);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error("Callback Error:", e);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const reply = { type: 'reply', result: result };
|
|
77
|
+
try {
|
|
78
|
+
fs.writeSync(this.fdWrite, JSON.stringify(reply) + '\n');
|
|
79
|
+
} catch {}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (res.type === 'error') throw new Error(`GJS Host Error: ${res.message}`);
|
|
84
|
+
|
|
85
|
+
if (res.type === 'exit') {
|
|
86
|
+
this.exited = true;
|
|
87
|
+
return res;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
close() {
|
|
95
|
+
this.exited = true;
|
|
96
|
+
if (this.fdRead) try { fs.closeSync(this.fdRead); } catch {}
|
|
97
|
+
if (this.fdWrite) try { fs.closeSync(this.fdWrite); } catch {}
|
|
98
|
+
}
|
|
99
99
|
}
|
package/start.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// start.js - Build and run TypeScript files
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
let runtime = 'node';
|
|
12
|
+
let tsFile = null;
|
|
13
|
+
let extraArgs = [];
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
const arg = args[i];
|
|
17
|
+
if (arg.startsWith('--runtime=')) {
|
|
18
|
+
runtime = arg.split('=')[1];
|
|
19
|
+
} else if (arg.startsWith('-r=')) {
|
|
20
|
+
runtime = arg.split('=')[1];
|
|
21
|
+
} else if (arg.endsWith('.ts') || arg.endsWith('.js')) {
|
|
22
|
+
tsFile = arg;
|
|
23
|
+
} else {
|
|
24
|
+
extraArgs.push(arg);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!tsFile) {
|
|
29
|
+
console.error('Usage: node start.js <ts-file> [--runtime=node|bun|deno] [args...]');
|
|
30
|
+
console.error('Example: node start.js src/gtk/counter/counter.ts');
|
|
31
|
+
console.error('Example: node start.js app.ts --runtime=deno');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const filePath = path.resolve(tsFile);
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const runtimeFlags = {
|
|
43
|
+
node: [],
|
|
44
|
+
bun: [],
|
|
45
|
+
deno: ['run', '--allow-all', '--unstable']
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function buildAndRun() {
|
|
49
|
+
console.log('Building TypeScript...');
|
|
50
|
+
|
|
51
|
+
const tscProc = spawn('npx', ['tsc'], {
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
cwd: __dirname,
|
|
54
|
+
shell: true
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
tscProc.on('exit', (code) => {
|
|
59
|
+
if (code !== 0) {
|
|
60
|
+
console.error('Build failed with code:', code);
|
|
61
|
+
reject(new Error(`tsc exited with code ${code}`));
|
|
62
|
+
} else {
|
|
63
|
+
resolve();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
tscProc.on('error', reject);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log('Build complete. Running with', runtime, ':', tsFile);
|
|
70
|
+
|
|
71
|
+
const ext = path.extname(tsFile);
|
|
72
|
+
const baseName = path.basename(tsFile, ext);
|
|
73
|
+
const relativePath = path.relative(path.join(__dirname, 'src'), filePath);
|
|
74
|
+
const jsFile = path.join(__dirname, 'dist', relativePath.replace(/\.ts$/, '.js'));
|
|
75
|
+
|
|
76
|
+
const runtimeArgs = runtimeFlags[runtime] || [];
|
|
77
|
+
const runtimeCmd = runtime;
|
|
78
|
+
|
|
79
|
+
const finalArgs = runtime === 'deno'
|
|
80
|
+
? [...runtimeArgs, jsFile, ...extraArgs]
|
|
81
|
+
: [...runtimeArgs, jsFile, ...extraArgs];
|
|
82
|
+
|
|
83
|
+
const proc = spawn(runtimeCmd, finalArgs, {
|
|
84
|
+
stdio: 'inherit',
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
shell: true
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
proc.on('exit', (code) => {
|
|
90
|
+
process.exit(code);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
proc.on('error', (err) => {
|
|
94
|
+
console.error(`Failed to start ${runtime}:`, err.message);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
buildAndRun().catch((err) => {
|
|
100
|
+
console.error('Error:', err.message);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
package/dist/index.js
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
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/hook.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// hook.js - Node.js module loader hook for gi:// protocol
|
|
2
|
-
export async function resolve(specifier, context, nextResolve) {
|
|
3
|
-
if (specifier.startsWith('gi://')) {
|
|
4
|
-
return {
|
|
5
|
-
url: specifier,
|
|
6
|
-
shortCircuit: true,
|
|
7
|
-
format: 'module'
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
return nextResolve(specifier, context, nextResolve);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function load(url, context, nextLoad) {
|
|
14
|
-
if (url.startsWith('gi://')) {
|
|
15
|
-
// Safely parse 'gi://Gtk?version=4.0'
|
|
16
|
-
const bareUrl = url.replace('gi://', '');
|
|
17
|
-
const [namespacePart, queryPart] = bareUrl.split('?');
|
|
18
|
-
|
|
19
|
-
const namespace = namespacePart;
|
|
20
|
-
let version = '';
|
|
21
|
-
|
|
22
|
-
if (queryPart) {
|
|
23
|
-
const params = new URLSearchParams(queryPart);
|
|
24
|
-
version = params.get('version') || '';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const coreUrl = new URL('./dist/index.js', import.meta.url).href;
|
|
28
|
-
|
|
29
|
-
const source = `
|
|
30
|
-
import { init, imports } from '${coreUrl}';
|
|
31
|
-
init();
|
|
32
|
-
imports.gi.versions['${namespace}'] = '${version}';
|
|
33
|
-
const ns = imports.gi['${namespace}'];
|
|
34
|
-
export default ns;
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
format: 'module',
|
|
39
|
-
shortCircuit: true,
|
|
40
|
-
source: source
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return nextLoad(url, context, nextLoad);
|
|
44
|
-
}
|
package/jest.config.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
preset: 'ts-jest/presets/default-esm',
|
|
3
|
-
testEnvironment: 'node',
|
|
4
|
-
extensionsToTreatAsEsm: ['.ts'],
|
|
5
|
-
moduleNameMapper: {
|
|
6
|
-
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
7
|
-
},
|
|
8
|
-
transform: {
|
|
9
|
-
'^.+\\.ts$': [
|
|
10
|
-
'ts-jest',
|
|
11
|
-
{
|
|
12
|
-
useESM: true,
|
|
13
|
-
tsconfig: {
|
|
14
|
-
module: 'ESNext',
|
|
15
|
-
moduleResolution: 'bundler',
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
},
|
|
20
|
-
testMatch: [
|
|
21
|
-
'**/__tests__/**/*.ts',
|
|
22
|
-
'**/?(*.)+(spec|test).ts',
|
|
23
|
-
],
|
|
24
|
-
collectCoverageFrom: [
|
|
25
|
-
'src/**/*.ts',
|
|
26
|
-
'!src/**/*.d.ts',
|
|
27
|
-
],
|
|
28
|
-
coverageDirectory: 'coverage',
|
|
29
|
-
coverageReporters: ['text', 'lcov', 'html'],
|
|
30
|
-
testTimeout: 60000,
|
|
31
|
-
maxWorkers: 1,
|
|
32
|
-
};
|
package/types/index.d.ts
DELETED
package/types/ipc.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export declare function readLineSync(fd: number): string | null;
|
|
2
|
-
export declare class IpcSync {
|
|
3
|
-
private fdRead;
|
|
4
|
-
private fdWrite;
|
|
5
|
-
private onEvent;
|
|
6
|
-
private exited;
|
|
7
|
-
constructor(fdRead: number, fdWrite: number, onEvent: (msg: any) => any);
|
|
8
|
-
send(cmd: any): any;
|
|
9
|
-
close(): void;
|
|
10
|
-
}
|