@devscholar/node-with-gjs 0.0.3 → 0.0.5
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 +1 -1
- package/package.json +21 -8
- package/src/index.ts +1 -1
- package/src/ipc.ts +98 -98
- package/start.js +102 -0
- package/AGENTS.md +0 -2
- package/hook.js +0 -44
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ This is a project that brings GNOME's GJS (GObject Introspection JavaScript runt
|
|
|
7
7
|
# Requirements
|
|
8
8
|
|
|
9
9
|
- Linux with GTK4 and WebKitGTK 6.0 installed
|
|
10
|
-
- Node.js
|
|
10
|
+
- Node.js 18+ (LTS version recommended, or Deno/Bun)
|
|
11
11
|
- bash (for Unix pipe IPC)
|
|
12
12
|
|
|
13
13
|
## Installation
|
package/package.json
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devscholar/node-with-gjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Node.js IPC Bridge for GJS",
|
|
5
|
-
"main": "./
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"
|
|
8
|
-
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18.0.0"
|
|
9
10
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
11
|
+
"keywords": [
|
|
12
|
+
"gjs",
|
|
13
|
+
"gnome",
|
|
14
|
+
"gtk",
|
|
15
|
+
"adwaita",
|
|
16
|
+
"gui",
|
|
17
|
+
"webkit",
|
|
18
|
+
"deno",
|
|
19
|
+
"bun"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js"
|
|
12
23
|
},
|
|
24
|
+
"bin": {},
|
|
13
25
|
"scripts": {
|
|
14
26
|
"build": "tsc",
|
|
15
|
-
"test": "
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"start": "node start.js"
|
|
16
29
|
},
|
|
17
30
|
"devDependencies": {
|
|
18
31
|
"@types/node": "^20.0.0",
|
|
19
32
|
"typescript": "^5.0.0",
|
|
20
33
|
"vitest": "^3.0.0"
|
|
21
34
|
}
|
|
22
|
-
}
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import * as cp from 'node:child_process';
|
|
5
5
|
import * as os from 'node:os';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { IpcSync } from './ipc.
|
|
7
|
+
import { IpcSync } from './ipc.js';
|
|
8
8
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
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/AGENTS.md
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
When using TypeScript in Node, you must run TS with the `--experimental-transform-types` flag; other methods like `tsx` are not allowed, and building and transpilation are not allowed, except Vite. The test framework should be Vitest, not Jest. ESM must be used instead of require.
|
|
2
|
-
allowImportingTsExtensions should be true in tsconfig.json. (If this file exists)
|
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
|
-
}
|