@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/hook.js CHANGED
@@ -1,42 +1,44 @@
1
- // hook.js
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);
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('./src/index.ts', import.meta.url).href;
28
-
29
- const source = `
30
- import { init, loadGiNamespace } from '${coreUrl}';
31
- init();
32
- export default loadGiNamespace('${namespace}', '${version}');
33
- `;
34
-
35
- return {
36
- format: 'module',
37
- shortCircuit: true,
38
- source: source
39
- };
40
- }
41
- return nextLoad(url, context);
42
- }
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 ADDED
@@ -0,0 +1,32 @@
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/package.json CHANGED
@@ -1,14 +1,23 @@
1
- {
2
- "name": "@devscholar/node-with-gjs",
3
- "version": "0.0.0",
4
- "description": "Node.js IPC Bridge for GJS",
5
- "main": "start.js",
6
- "type": "module",
7
- "scripts": {
8
-
9
- },
10
- "devDependencies": {
11
- "@types/node": "^20.0.0",
12
- "typescript": "^5.0.0"
13
- }
14
- }
1
+ {
2
+ "name": "@devscholar/node-with-gjs",
3
+ "version": "0.0.2",
4
+ "description": "Node.js IPC Bridge for GJS",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "bin": {
11
+
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.0.0",
19
+ "jest": "^29.7.0",
20
+ "ts-jest": "^29.1.0",
21
+ "typescript": "^5.0.0"
22
+ }
23
+ }
package/scripts/host.js CHANGED
@@ -83,7 +83,9 @@ function ResolveArg(arg) {
83
83
 
84
84
  function executeCommand(cmd) {
85
85
  if (cmd.action === 'LoadNamespace') {
86
- imports.gi.versions[cmd.namespace] = cmd.version;
86
+ if (cmd.version) {
87
+ imports.gi.versions[cmd.namespace] = cmd.version;
88
+ }
87
89
  const ns = imports.gi[cmd.namespace];
88
90
  return ConvertToProtocol(ns);
89
91
  }
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.ts';
7
+ import { IpcSync } from './ipc.js';
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
@@ -173,8 +173,46 @@ export function init() {
173
173
  initialize();
174
174
  }
175
175
 
176
- export function loadGiNamespace(namespace: string, version: string) {
176
+ // Internal function - not exposed to users
177
+ function loadGiNamespace(namespace: string, version: string | undefined) {
177
178
  initialize();
178
179
  const res = ipc!.send({ action: 'LoadNamespace', namespace, version });
179
180
  return createProxy(res);
180
181
  }
182
+
183
+ // Namespace cache to avoid creating multiple proxies for the same namespace
184
+ const namespaceCache = new Map<string, any>();
185
+
186
+ // GI namespace versions
187
+ const giVersions: Record<string, string> = {};
188
+
189
+ // Create the gi proxy with lazy loading and caching
190
+ const giProxy = new Proxy({} as any, {
191
+ get(_, namespace: string) {
192
+ if (namespace === 'versions') {
193
+ return new Proxy(giVersions, {
194
+ set(target, prop, value) {
195
+ target[prop as string] = value;
196
+ // Clear cache for this namespace when version changes
197
+ const cacheKey = `${prop as string}@default`;
198
+ namespaceCache.delete(cacheKey);
199
+ return true;
200
+ }
201
+ });
202
+ }
203
+
204
+ const version = giVersions[namespace];
205
+ const cacheKey = `${namespace}@${version || 'default'}`;
206
+
207
+ if (!namespaceCache.has(cacheKey)) {
208
+ namespaceCache.set(cacheKey, loadGiNamespace(namespace, version));
209
+ }
210
+
211
+ return namespaceCache.get(cacheKey);
212
+ }
213
+ });
214
+
215
+ // The main exports object - compatible with GJS imports
216
+ export const imports = {
217
+ gi: giProxy
218
+ };
package/tsconfig.json CHANGED
@@ -1,11 +1,22 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true
10
- }
11
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "bundler",
7
+ "declaration": true,
8
+ "declarationDir": "./types",
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": false,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "noEmit": false,
18
+ "allowSyntheticDefaultImports": true
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }
@@ -0,0 +1,4 @@
1
+ export declare function init(): void;
2
+ export declare const imports: {
3
+ gi: any;
4
+ };
package/types/ipc.d.ts ADDED
@@ -0,0 +1,10 @@
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
+ }
@@ -1,66 +0,0 @@
1
- // Run: node start.js examples/adwaita/counter/counter.ts
2
- import { loadGi } from '../../../gi-loader.ts';
3
-
4
- const Gtk = loadGi('Gtk', '4.0');
5
- const Adw = loadGi('Adw', '1');
6
-
7
- let clickCount = 0;
8
-
9
- console.log("--- Adwaita Counter ---");
10
-
11
- const app = new Adw.Application({ application_id: 'org.adwaita.counter' });
12
-
13
- app.connect('activate', () => {
14
- const window = new Adw.ApplicationWindow({
15
- application: app,
16
- title: 'Adwaita Counter App',
17
- default_width: 400,
18
- default_height: 300
19
- });
20
-
21
- const toolbarView = new Adw.ToolbarView();
22
-
23
- const headerBar = new Adw.HeaderBar({
24
- title_widget: new Gtk.Label({ label: 'Adwaita Counter App' })
25
- });
26
- toolbarView.add_top_bar(headerBar);
27
-
28
- const box = new Gtk.Box({
29
- orientation: Gtk.Orientation.VERTICAL,
30
- spacing: 10,
31
- halign: Gtk.Align.CENTER,
32
- valign: Gtk.Align.CENTER
33
- });
34
- box.set_margin_start(20);
35
- box.set_margin_end(20);
36
- box.set_margin_top(20);
37
- box.set_margin_bottom(20);
38
-
39
- const label = new Gtk.Label({
40
- label: 'Clicks: 0',
41
- css_classes: ['title-1']
42
- });
43
-
44
- const button = new Gtk.Button({
45
- label: 'Click to Add',
46
- css_classes: ['suggested-action']
47
- });
48
-
49
- button.connect('clicked', () => {
50
- clickCount++;
51
- const message = `Clicked ${clickCount} times`;
52
- label.set_label(message);
53
- console.log(message);
54
- });
55
-
56
- box.append(label);
57
- box.append(button);
58
-
59
- toolbarView.set_content(box);
60
- window.set_content(toolbarView);
61
- window.present();
62
-
63
- console.log("Click the button to increase the counter...");
64
- });
65
-
66
- app.run([]);
@@ -1,11 +0,0 @@
1
- // Run: node start.js examples/console/await-delay/await-delay.ts
2
- import { loadGi } from '../../../gi-loader.ts';
3
-
4
- loadGi('GLib', '2.0');
5
-
6
- print('0s');
7
- await new Promise(resolve => setTimeout(resolve, 1000));
8
- print("1s");
9
- await new Promise(resolve => setTimeout(resolve, 1000));
10
- print("2s");
11
- await new Promise(resolve => setTimeout(resolve, 1000));
@@ -1,27 +0,0 @@
1
- // Run: node start.js examples/console/console-input/console-input.ts
2
- import { loadGi } from '../../../gi-loader.ts';
3
-
4
- const Gio = loadGi('Gio', '2.0');
5
-
6
- let GioUnix;
7
- try {
8
- GioUnix = loadGi('GioUnix', '2.0');
9
- } catch {
10
- GioUnix = Gio;
11
- }
12
-
13
- print("=== Greeting Program ===");
14
- print("Please enter your name: ");
15
-
16
- const stdin = new GioUnix.InputStream({ fd: 0, close_fd: false });
17
- const dataInput = new Gio.DataInputStream({ base_stream: stdin });
18
-
19
- const [name] = dataInput.read_line_utf8(null);
20
-
21
- if (name && name.trim() !== "") {
22
- print(`Hello, ${name}! Welcome to this program!`);
23
- } else {
24
- print("Hello, friend! Welcome to this program!");
25
- }
26
-
27
- print("Program ended.");
@@ -1,62 +0,0 @@
1
- // Run: node start.js examples/gtk/counter/counter.ts
2
- import { loadGi } from '../../../gi-loader.ts';
3
-
4
- const Gtk = loadGi('Gtk', '4.0');
5
-
6
- let clickCount = 0;
7
-
8
- console.log("--- GTK4 Counter ---");
9
-
10
- const app = new Gtk.Application({ application_id: 'org.gtk.counter' });
11
-
12
- app.connect('activate', () => {
13
- const window = new Gtk.ApplicationWindow({
14
- application: app,
15
- title: 'GTK Counter App',
16
- default_width: 400,
17
- default_height: 300
18
- });
19
-
20
- window.connect('close-request', () => {
21
- app.quit();
22
- return false;
23
- });
24
-
25
- const box = new Gtk.Box({
26
- orientation: Gtk.Orientation.VERTICAL,
27
- spacing: 10,
28
- halign: Gtk.Align.CENTER,
29
- valign: Gtk.Align.CENTER
30
- });
31
- box.set_margin_start(20);
32
- box.set_margin_end(20);
33
- box.set_margin_top(20);
34
- box.set_margin_bottom(20);
35
-
36
- const label = new Gtk.Label({
37
- label: 'Clicks: 0',
38
- css_classes: ['title-1']
39
- });
40
-
41
- const button = new Gtk.Button({
42
- label: 'Click to Add',
43
- css_classes: ['suggested-action']
44
- });
45
-
46
- button.connect('clicked', () => {
47
- clickCount++;
48
- const message = `Clicked ${clickCount} times`;
49
- label.set_label(message);
50
- console.log(message);
51
- });
52
-
53
- box.append(label);
54
- box.append(button);
55
-
56
- window.set_child(box);
57
- window.present();
58
-
59
- console.log("Click the button to increase the counter...");
60
- });
61
-
62
- app.run([]);
@@ -1,77 +0,0 @@
1
- // Run: node start.js examples/gtk/drag-box/drag-box.ts
2
- import { loadGi } from '../../../gi-loader.ts';
3
-
4
- const Gtk = loadGi('Gtk', '4.0');
5
-
6
- console.log("--- GTK4 Flicker-free Draggable Square (Cairo) ---");
7
-
8
- const app = new Gtk.Application({ application_id: 'org.gtk.dragbox' });
9
-
10
- app.connect('activate', () => {
11
- const window = new Gtk.ApplicationWindow({
12
- application: app,
13
- title: 'Drag Example (High Frequency IPC) ',
14
- default_width: 600,
15
- default_height: 400
16
- });
17
-
18
- const fixed = new Gtk.Fixed();
19
- fixed.set_hexpand(true);
20
- fixed.set_vexpand(true);
21
-
22
- const squareSize = 80;
23
- const drawingArea = new Gtk.DrawingArea();
24
- drawingArea.set_size_request(squareSize, squareSize);
25
-
26
- const drawFunction = (area: any, cr: any, width: number, height: number) => {
27
- cr.setSourceRGB(1.0, 0.2, 0.2);
28
- cr.rectangle(0, 0, width, height);
29
- cr.fill();
30
- };
31
-
32
- drawingArea.set_draw_func(drawFunction);
33
-
34
- let currentX = 260;
35
- let currentY = 160;
36
- fixed.put(drawingArea, currentX, currentY);
37
-
38
- const drag = new Gtk.GestureDrag();
39
-
40
- let isDragging = false;
41
- let dragStartX = 0;
42
- let dragStartY = 0;
43
-
44
- drag.connect('drag-begin', (gesture: any, startX: number, startY: number) => {
45
- if (startX >= currentX && startX <= currentX + squareSize &&
46
- startY >= currentY && startY <= currentY + squareSize) {
47
- isDragging = true;
48
- dragStartX = currentX;
49
- dragStartY = currentY;
50
- console.log(`✅ Drag started at: (${startX}, ${startY})`);
51
- }
52
- });
53
-
54
- drag.connect('drag-update', (gesture: any, offsetX: number, offsetY: number) => {
55
- if (!isDragging) return;
56
- const newX = dragStartX + offsetX;
57
- const newY = dragStartY + offsetY;
58
- fixed.move(drawingArea, newX, newY);
59
- });
60
-
61
- drag.connect('drag-end', (gesture: any, offsetX: number, offsetY: number) => {
62
- if (!isDragging) return;
63
- isDragging = false;
64
- currentX = dragStartX + offsetX;
65
- currentY = dragStartY + offsetY;
66
- console.log(`🛑 Drag ended at position: (${currentX}, ${currentY})`);
67
- });
68
-
69
- fixed.add_controller(drag);
70
-
71
- window.set_child(fixed);
72
- window.present();
73
-
74
- console.log("Window loaded. Try dragging the red square smoothly!");
75
- });
76
-
77
- app.run([]);
@@ -1,47 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Counter</title>
5
- <meta charset="UTF-8">
6
- <style>
7
- body { font-family: Arial, sans-serif; padding: 20px; text-align: center; }
8
- h1 { color: #333; }
9
- #display { font-size: 24px; margin: 20px 0; }
10
- button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
11
- </style>
12
- <script>
13
- (function() {
14
- const originalConsole = window.console;
15
- window.console = {
16
- log: function(...args) {
17
- window.webkit.messageHandlers.console.postMessage(args.join(' '));
18
- },
19
- error: function(...args) {
20
- window.webkit.messageHandlers.console.postMessage('[ERROR] ' + args.join(' '));
21
- },
22
- warn: function(...args) {
23
- window.webkit.messageHandlers.console.postMessage('[WARN] ' + args.join(' '));
24
- }
25
- };
26
- })();
27
- </script>
28
- </head>
29
- <body>
30
- <h1>Counter App (WebKit)</h1>
31
- <p id="display">Clicks: 0</p>
32
- <button id="btn">Click to Add</button>
33
-
34
- <script>
35
- let clickCount = 0;
36
- const display = document.getElementById('display');
37
- const button = document.getElementById('btn');
38
-
39
- button.addEventListener('click', function() {
40
- clickCount++;
41
- const message = 'Button clicked ' + clickCount + ' times';
42
- display.textContent = message;
43
- console.log(message);
44
- });
45
- </script>
46
- </body>
47
- </html>
@@ -1,101 +0,0 @@
1
- // Run: node start.js examples/gtk-webkit/counter/counter.ts
2
- import { loadGi } from '../../../gi-loader.ts';
3
- import * as path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
-
6
- const Gtk = loadGi('Gtk', '4.0');
7
- const WebKit = loadGi('WebKit', '6.0');
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- console.log('--- GTK4 WebKit Counter ---');
13
-
14
- const app = new Gtk.Application({ application_id: 'org.gtk.webkitcounter' });
15
-
16
- app.connect('activate', () => {
17
- const window = new Gtk.ApplicationWindow({
18
- application: app,
19
- title: 'WebKit Counter App',
20
- default_width: 500,
21
- default_height: 400
22
- });
23
-
24
- const box = new Gtk.Box({
25
- orientation: Gtk.Orientation.VERTICAL,
26
- spacing: 0
27
- });
28
-
29
- const toolbar = new Gtk.Box({
30
- orientation: Gtk.Orientation.HORIZONTAL,
31
- spacing: 5
32
- });
33
- toolbar.set_margin_start(5);
34
- toolbar.set_margin_end(5);
35
- toolbar.set_margin_top(5);
36
- toolbar.set_margin_bottom(5);
37
-
38
- const htmlPath = path.join(__dirname, 'counter.html');
39
- const htmlUri = 'file://' + htmlPath;
40
-
41
- const backButton = new Gtk.Button({ label: '← Back' });
42
- const forwardButton = new Gtk.Button({ label: 'Forward →' });
43
- const urlEntry = new Gtk.Entry({
44
- placeholder_text: 'Enter URL or use default',
45
- text: htmlUri,
46
- hexpand: true
47
- });
48
-
49
- toolbar.append(backButton);
50
- toolbar.append(forwardButton);
51
- toolbar.append(urlEntry);
52
-
53
- const contentManager = new WebKit.UserContentManager();
54
-
55
- contentManager.connect('script-message-received', (manager, value) => {
56
- const message = value.to_string();
57
- if (message) print(`[WebView] ${message}`);
58
- });
59
- contentManager.register_script_message_handler('console', null);
60
-
61
- const webView = new WebKit.WebView({
62
- vexpand: true,
63
- hexpand: true,
64
- user_content_manager: contentManager
65
- });
66
-
67
- webView.load_uri(htmlUri);
68
-
69
- webView.connect('load-changed', (webview, loadEvent) => {
70
- if (loadEvent === WebKit.LoadEvent.FINISHED) {
71
- console.log('Page Loaded Successfully');
72
- urlEntry.set_text(webview.get_uri() || '');
73
- }
74
- });
75
-
76
- backButton.connect('clicked', () => {
77
- if (webView.can_go_back()) webView.go_back();
78
- });
79
-
80
- forwardButton.connect('clicked', () => {
81
- if (webView.can_go_forward()) webView.go_forward();
82
- });
83
-
84
- urlEntry.connect('activate', () => {
85
- let uri = urlEntry.get_text();
86
- if (!uri.startsWith('http://') && !uri.startsWith('https://') && !uri.startsWith('file://')) {
87
- uri = 'https://' + uri;
88
- }
89
- webView.load_uri(uri);
90
- });
91
-
92
- box.append(toolbar);
93
- box.append(webView);
94
-
95
- window.set_child(box);
96
- window.present();
97
-
98
- console.log("Click the button in the web view to increase the counter...");
99
- });
100
-
101
- app.run([]);