@gjsify/node-gi 0.13.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/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gjsify contributors
4
+
5
+ Portions of this package are derived from node-gtk
6
+ (https://github.com/romgrk/node-gtk):
7
+ Copyright (c) romgrk and node-gtk contributors.
8
+ node-gtk is licensed under the MIT License.
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # @gjsify/node-gi
2
+
3
+ **GObject-Introspection runtime for Node.js** — the native engine that lets
4
+ unchanged GJS / GObject-Introspection code run under Node.js, the inverse of
5
+ gjsify's Node/Web/DOM → GJS direction (see the gjsify `AGENTS.md`
6
+ `### Axis 5 active track`).
7
+
8
+ It loads `gi://` namespaces (GLib, GObject, Gio, …) via `libgirepository` and
9
+ exposes them with GJS-compatible semantics, so the same source builds and runs
10
+ on both GJS and Node via `gjsify build --app {gjs,node}`.
11
+
12
+ > **Status: milestone 1 (headless core) — in progress.** The native engine over
13
+ > the modern `girepository-2.0` API now does: resolve the default repository,
14
+ > `require` a namespace, enumerate its infos; call namespace-level functions and
15
+ > **instance methods** (own + implemented-interface methods, up the parent chain)
16
+ > with value marshalling (numbers, booleans, strings, GObjects, enums/flags);
17
+ > construct GObjects and read/write properties (GValue round-trip); connect /
18
+ > emit / disconnect signals (incl. detailed names like `notify::prop`); and
19
+ > **register GObject subclasses** (subtype + construct-by-type, inheriting the
20
+ > parent's properties/methods, plus **custom properties + signals** declared on
21
+ > the subclass). Ownership rides
22
+ > N-API finalizers (no V8-GC reentrancy). On top of the engine, an **L1
23
+ > GJS-compatibility layer** (`@gjsify/node-gi/gi`, `requireGi`) surfaces a
24
+ > GJS-shaped namespace: `new Gio.SimpleAction({ name })`, `action.name` property
25
+ > access, `action.get_name()` methods, `.connect()/.emit()/.disconnect()`, and
26
+ > enums / flags / constants (`Gio.BusType.SESSION`, `GLib.PRIORITY_DEFAULT`);
27
+ > constructor/static methods (`Gio.File.new_for_path(...)`); and both snake_case
28
+ > and camelCase accessors. A **libuv↔GLib mainloop bridge** (`startMainLoop`,
29
+ > auto-attached by `requireGi`) nests Node's libuv loop inside the GLib loop, so a
30
+ > blocking `GLib.MainLoop.run()` keeps Node's timers/I/O alive — including the
31
+ > **boxed/struct slice** that needs (`GLib.MainLoop.new(...)` → a boxed handle →
32
+ > `.run()`/`.quit()`). **JS functions marshal as GI callbacks** via an ffi
33
+ > closure (`GLib.timeout_add`/`idle_add` fire from the loop, the boolean return
34
+ > drives source continuation; the hidden user_data/destroy slots are auto-filled).
35
+ > register GObject subclasses (subtype + construct-by-type, inheriting the
36
+ > parent's properties/methods, plus **custom properties + signals**, plus
37
+ > **vfunc overrides** — a JS function overriding a parent GObject vfunc, hooked
38
+ > into the new type's class vtable). The gjsify `--app node` bundler integration
39
+ > already rewrites `gi://` onto the L1 layer. vfunc **chain-up** to the parent
40
+ > implementation (with the toggle-ref GC bridge) and general struct field access
41
+ > land in subsequent drops — for now a vfunc override fully replaces the inherited
42
+ > implementation.
43
+
44
+ ## Provenance
45
+
46
+ Derived from [node-gtk](https://github.com/romgrk/node-gtk) (romgrk and
47
+ contributors, MIT) — vendored and rewritten under MIT (see `LICENSE`). The
48
+ native binding is **retargeted to `girepository-2.0`** (the GIRepository merged
49
+ into GLib ≥ 2.80); the standalone `libgirepository-1.0` node-gtk linked is no
50
+ longer shipped on modern systems. GJS's own `gi/repo.cpp` is the reference for
51
+ the `girepository-2.0` API surface. node-gtk's own examples and tests are **not**
52
+ vendored as-is — gjsify ships its own dual (GJS + Node) example/test infra.
53
+
54
+ ## Requirements
55
+
56
+ - Node.js ≥ 20
57
+ - A C++ toolchain (`g++`/`clang`, `make`), `node-gyp`
58
+ - GLib ≥ 2.80 development headers exposing `girepository-2.0`
59
+ (Fedora: `glib2-devel gobject-introspection-devel gcc-c++`;
60
+ Debian/Ubuntu: `libglib2.0-dev libgirepository-2.0-dev g++`)
61
+ - At runtime, the target libraries' typelibs must be installed (same as `gi://`
62
+ under GJS).
63
+
64
+ ## Build & test
65
+
66
+ ```bash
67
+ npm install # builds the native addon via node-gyp (install script)
68
+ npm test # node --test (smoke tests)
69
+ # or rebuild explicitly:
70
+ npm run rebuild
71
+ ```
72
+
73
+ ## Usage (milestone 1)
74
+
75
+ ```js
76
+ import {
77
+ requireNamespace, listInfoNames, callFunction,
78
+ newObject, getProperty, setProperty, callMethod,
79
+ connectSignal, emitSignal,
80
+ } from '@gjsify/node-gi';
81
+
82
+ requireNamespace('GLib', '2.0');
83
+ console.log(listInfoNames('GLib').includes('MainLoop')); // true
84
+ console.log(callFunction('GLib', 'get_host_name')); // namespace function
85
+
86
+ requireNamespace('Gio', '2.0');
87
+ const action = newObject('Gio', 'SimpleAction', { name: 'greet', enabled: true });
88
+ console.log(getProperty(action, 'name')); // 'greet' (GValue round-trip)
89
+ console.log(callMethod(action, 'get_name')); // 'greet' (interface method)
90
+ callMethod(action, 'set_enabled', [false]); // method with an IN argument
91
+
92
+ const c = newObject('Gio', 'Cancellable', {});
93
+ connectSignal(c, 'cancelled', () => console.log('cancelled'));
94
+ emitSignal(c, 'cancelled');
95
+ ```
96
+
97
+ Register a GObject subclass and construct it (inherited properties + methods):
98
+
99
+ ```js
100
+ import { registerClass, constructType, callMethod } from '@gjsify/node-gi';
101
+
102
+ const MyAction = registerClass('MyAction', 'Gio', 'SimpleAction');
103
+ const a = constructType(MyAction, { name: 'greet', enabled: true });
104
+ console.log(callMethod(a, 'get_name')); // 'greet' (inherited GAction method)
105
+ ```
106
+
107
+ Declare custom properties and signals on the subclass:
108
+
109
+ ```js
110
+ import { registerClass, constructType, getProperty, setProperty,
111
+ connectSignal, emitSignal } from '@gjsify/node-gi';
112
+
113
+ const Counter = registerClass('Counter', 'GObject', 'Object', {
114
+ properties: [{ name: 'count', type: 'int', default: 0, minimum: 0, maximum: 100 }],
115
+ signals: [{ name: 'changed', paramTypes: ['int'] }],
116
+ });
117
+
118
+ const c = constructType(Counter, { count: 1 });
119
+ connectSignal(c, 'notify::count', (pspec) => console.log('changed:', pspec.name));
120
+ connectSignal(c, 'changed', (n) => console.log('count is now', n));
121
+ setProperty(c, 'count', 5); // fires notify::count
122
+ emitSignal(c, 'changed', [5]);
123
+ console.log(getProperty(c, 'count')); // 5
124
+ ```
125
+
126
+ Override a parent GObject vfunc with a JS function (the override runs as a method
127
+ on the instance — `this` is the GObject handle). Chain-up to the parent vfunc
128
+ lands in a later drop, so an override fully replaces the inherited implementation:
129
+
130
+ ```js
131
+ import { registerClass, constructType, getProperty } from '@gjsify/node-gi';
132
+
133
+ const Greeter = registerClass('Greeter', 'Gio', 'SimpleAction', {
134
+ vfuncs: {
135
+ // GObject's `constructed` vfunc — runs once, after construct properties are
136
+ // set. `name` is a CONSTRUCT_ONLY property already available on `this`.
137
+ constructed() {
138
+ console.log('constructed:', getProperty(this, 'name'));
139
+ },
140
+ },
141
+ });
142
+
143
+ constructType(Greeter, { name: 'greet' }); // logs "constructed: greet"
144
+ ```
145
+
146
+ ### L1 — GJS-shaped surface (`@gjsify/node-gi/gi`)
147
+
148
+ The ergonomic layer the gjsify `--app node` build rewrites `gi://` imports onto.
149
+ This is the same code you would write under GJS:
150
+
151
+ ```js
152
+ import { requireGi } from '@gjsify/node-gi/gi';
153
+
154
+ const GLib = requireGi('GLib', '2.0');
155
+ console.log(GLib.get_host_name());
156
+
157
+ const Gio = requireGi('Gio', '2.0');
158
+ const action = new Gio.SimpleAction({ name: 'greet', enabled: true });
159
+ console.log(action.name); // 'greet' (property accessor)
160
+ console.log(action.get_name()); // 'greet' (method)
161
+ action.enabled = false; // property set → set_property
162
+
163
+ const c = new Gio.Cancellable();
164
+ c.connect('cancelled', () => console.log('cancelled'));
165
+ c.cancel(); // fires the signal
166
+
167
+ // enums, flags and constants (GJS-style UPPER_CASE members)
168
+ console.log(GLib.PRIORITY_DEFAULT); // 0
169
+ console.log(Gio.BusType.SESSION); // 2
170
+ console.log(Gio.ApplicationFlags.HANDLES_OPEN); // 4
171
+
172
+ // constructor/static methods + camelCase aliases
173
+ const file = Gio.File.new_for_path('/usr/bin/gjs');
174
+ console.log(file.get_path()); // '/usr/bin/gjs'
175
+ console.log(file.getBasename()); // 'gjs' (camelCase alias)
176
+
177
+ // mainloop: a blocking GLib loop, with Node's libuv kept alive underneath
178
+ const loop = GLib.MainLoop.new(null, false);
179
+ setTimeout(() => loop.quit(), 100); // a libuv timer that fires during run()
180
+ loop.run(); // blocks like under GJS; returns on quit()
181
+
182
+ // GI callbacks: a JS function passed where a GI callback is expected. The
183
+ // GLib source fires from the loop; returning false (G_SOURCE_REMOVE) stops it.
184
+ const ticker = GLib.MainLoop.new(null, false);
185
+ let n = 0;
186
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
187
+ if (++n >= 3) { ticker.quit(); return false; }
188
+ return true;
189
+ });
190
+ ticker.run();
191
+ ```
192
+
193
+ The mainloop bridge (`startMainLoop`) is auto-attached the first time `requireGi`
194
+ loads a namespace, so `GLib.MainLoop.run()` / `Gio.Application.run()` block as
195
+ they do under GJS while Node's timers, I/O and signal handlers keep running.
196
+
197
+ ### GJS ambient globals (`@gjsify/node-gi/globals`)
198
+
199
+ GJS source relies on globals that exist implicitly under `gjs` — `print`,
200
+ `printerr`, `log`, `logError`, `ARGV`, and the legacy `imports` object. Importing
201
+ `@gjsify/node-gi/globals` (a side effect) installs Node-backed equivalents that
202
+ route through the same backend:
203
+
204
+ ```js
205
+ import '@gjsify/node-gi/globals';
206
+
207
+ print('hello', 1, true); // → stdout, GJS String()-join
208
+ const GLib = imports.gi.GLib; // legacy imports.gi (honours .versions)
209
+ imports.gi.versions.Gtk = '4.0';
210
+ console.log(imports.gettext.gettext('x')); // no-translation passthrough
211
+ ```
212
+
213
+ A follow-up `--app node` build step will inject this automatically for any
214
+ bundle that references those globals (so `const Gtk = imports.gi.Gtk` /
215
+ `print(...)` GJS source runs unmodified on Node); today it is an explicit import.
216
+
217
+ The remaining GJS-compatible surface (`import GLib from 'gi://GLib?version=2.0'`,
218
+ `const GLib = imports.gi.GLib`, the core overrides, `_promisify`, the legacy
219
+ `imports.*` modules) is layered on top of this engine in the gjsify bundler
220
+ integration and subsequent drops.
package/binding.gyp ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "node_gi",
5
+ "sources": [
6
+ "src/addon.cc"
7
+ ],
8
+ "include_dirs": [
9
+ "<!@(node -p \"require('node-addon-api').include\")"
10
+ ],
11
+ "cflags": [
12
+ "<!@(pkg-config --cflags girepository-2.0)"
13
+ ],
14
+ "cflags_cc": [
15
+ "<!@(pkg-config --cflags girepository-2.0)",
16
+ "-std=c++17"
17
+ ],
18
+ "libraries": [
19
+ "<!@(pkg-config --libs girepository-2.0)"
20
+ ],
21
+ "defines": [
22
+ "NAPI_VERSION=8",
23
+ "NAPI_DISABLE_CPP_EXCEPTIONS"
24
+ ],
25
+ "xcode_settings": {
26
+ "OTHER_CFLAGS": [
27
+ "<!@(pkg-config --cflags girepository-2.0)"
28
+ ],
29
+ "CLANG_CXX_LANGUAGE_STANDARD": "c++17",
30
+ "GCC_ENABLE_CPP_EXCEPTIONS": "NO"
31
+ }
32
+ }
33
+ ]
34
+ }
package/gi.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/node-gi/gi — L1 GJS-compatibility layer type surface.
3
+ //
4
+ // The runtime returns a Proxy whose members are resolved dynamically from
5
+ // introspection, so the precise per-namespace shape is not statically known
6
+ // here. Under gjsify's bundler integration the call site
7
+ // `import Ns from 'gi://Ns?version=X'` is typed by the platform-neutral ambient
8
+ // `@girs/*` module declarations; this declaration types the bare runtime entry.
9
+
10
+ /** A GJS-shaped namespace object (members resolved dynamically). */
11
+ export type GiNamespace = Record<string, unknown>;
12
+
13
+ /**
14
+ * Require a GObject-Introspection namespace and return a GJS-shaped namespace
15
+ * object. The Node twin of `import Ns from 'gi://Ns?version=X'` /
16
+ * `imports.gi.Ns`. Members are resolved lazily from introspection: namespace
17
+ * functions become callables, GObject types become constructors whose instances
18
+ * expose `.method(...)`, `.prop` get/set and `.connect()/.emit()/.disconnect()`.
19
+ */
20
+ export function requireGi(namespace: string, version?: string): GiNamespace;
21
+
22
+ /**
23
+ * Extract the raw native GObject handle from a wrapped instance (advanced /
24
+ * interop use). Returns the value unchanged if it is not a wrapped instance.
25
+ */
26
+ export function unwrap(value: unknown): unknown;
27
+
28
+ export default requireGi;
package/gi.js ADDED
@@ -0,0 +1,256 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/node-gi/gi — the L1 GJS-compatibility layer over the native engine.
3
+ //
4
+ // Turns the low-level primitives (requireNamespace / findInfo / callFunction /
5
+ // newObject / callMethod / get|setProperty / connect|emit|disconnectSignal) into
6
+ // a GJS-shaped namespace object: `requireGi('Gio', '2.0')` returns an object
7
+ // where `Gio.SomeFunction(...)` calls a namespace function, `new Gio.SomeClass({
8
+ // prop: value })` constructs a GObject, and the instance exposes `.method(...)`,
9
+ // `.prop` (property get/set) and `.connect()/.emit()/.disconnect()` — the same
10
+ // surface `import Gio from 'gi://Gio?version=2.0'` gives under GJS.
11
+ //
12
+ // This is the seam the bundler's --app node target rewrites `gi://Ns?version=X`
13
+ // onto. Reference: GJS's gi module (gjs/modules/esm/gi.js) for the require shape;
14
+ // node-gtk (romgrk, MIT) for the binding lineage. Hand-authored JS (the package
15
+ // ships no build step). Milestone 1 surfaces functions + GObject classes; enums,
16
+ // flags, constants, structs/boxed, interface static methods and camelCase
17
+ // aliases land in subsequent drops.
18
+ import * as native from './index.js';
19
+
20
+ // Symbol carrying the raw native GObject handle on a wrapped instance, so it can
21
+ // be unwrapped again when passed back into the engine as a GI argument.
22
+ const HANDLE = Symbol('nodeGiHandle');
23
+
24
+ // JS-builtin / interop names that must NEVER be treated as a GI method or
25
+ // property — otherwise awaiting, printing or inspecting a wrapper would call
26
+ // into GObject (e.g. a stray `then` would make a wrapper look thenable).
27
+ const RESERVED = new Set(['then', 'toString', 'valueOf', 'constructor', 'inspect']);
28
+
29
+ // GJS accepts both snake_case and camelCase for methods/properties. Map a JS
30
+ // accessor to the GI method name (snake_case) and to a GObject property name
31
+ // (kebab-case); a name that is already in the target case passes through.
32
+ function camelToSnake(name) {
33
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase();
34
+ }
35
+
36
+ function toKebab(name) {
37
+ return name.replace(/([A-Z])/g, '-$1').replace(/_/g, '-').toLowerCase();
38
+ }
39
+
40
+ function unwrapArg(value) {
41
+ if (value !== null && typeof value === 'object' && value[HANDLE] !== undefined) {
42
+ return value[HANDLE];
43
+ }
44
+ return value;
45
+ }
46
+
47
+ function unwrapArgs(args) {
48
+ return args.map(unwrapArg);
49
+ }
50
+
51
+ function unwrapProps(props) {
52
+ const out = {};
53
+ for (const key of Object.keys(props)) out[key] = unwrapArg(props[key]);
54
+ return out;
55
+ }
56
+
57
+ // Object-typed return values become chainable instance proxies; boxed/struct
58
+ // handles (e.g. GMainLoop) become method-routing proxies; everything else
59
+ // (primitives, strings, null) passes through.
60
+ function wrapReturn(value) {
61
+ if (native.isGObjectHandle(value)) return wrapInstance(value);
62
+ if (native.isBoxedHandle(value)) return wrapBoxed(value);
63
+ return value;
64
+ }
65
+
66
+ // Wrap a boxed/struct handle so its methods are callable GJS-style
67
+ // (`mainLoop.run()`, `mainLoop.quit()`, snake_case or camelCase). Boxed types
68
+ // have no GObject properties/signals, so only method routing is provided.
69
+ function wrapBoxed(handle) {
70
+ const target = { [HANDLE]: handle };
71
+ return new Proxy(target, {
72
+ get(t, prop) {
73
+ if (prop === HANDLE) return handle;
74
+ if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
75
+ return (...args) =>
76
+ wrapReturn(native.callBoxedMethod(handle, camelToSnake(prop), unwrapArgs(args)));
77
+ },
78
+ has(t, prop) {
79
+ return prop === HANDLE || prop in t;
80
+ },
81
+ });
82
+ }
83
+
84
+ function wrapInstance(handle) {
85
+ const target = { [HANDLE]: handle };
86
+ return new Proxy(target, {
87
+ get(t, prop) {
88
+ if (prop === HANDLE) return handle;
89
+ if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
90
+ switch (prop) {
91
+ case 'connect':
92
+ return (signal, cb) => native.connectSignal(handle, signal, cb, false);
93
+ case 'connect_after':
94
+ return (signal, cb) => native.connectSignal(handle, signal, cb, true);
95
+ case 'emit':
96
+ return (signal, ...args) =>
97
+ wrapReturn(native.emitSignal(handle, signal, unwrapArgs(args)));
98
+ case 'disconnect':
99
+ return (id) => native.disconnectSignal(handle, id);
100
+ default:
101
+ break;
102
+ }
103
+ const propName = toKebab(prop);
104
+ if (native.hasProperty(handle, propName)) {
105
+ return wrapReturn(native.getProperty(handle, propName));
106
+ }
107
+ return (...args) => wrapReturn(native.callMethod(handle, camelToSnake(prop), unwrapArgs(args)));
108
+ },
109
+ set(t, prop, value) {
110
+ if (typeof prop === 'string') {
111
+ const propName = toKebab(prop);
112
+ if (native.hasProperty(handle, propName)) {
113
+ native.setProperty(handle, propName, unwrapArg(value));
114
+ return true;
115
+ }
116
+ }
117
+ t[prop] = value;
118
+ return true;
119
+ },
120
+ has(t, prop) {
121
+ return prop === HANDLE || prop in t;
122
+ },
123
+ });
124
+ }
125
+
126
+ function makeClass(namespace, typeName) {
127
+ const ctor = function ctor(props) {
128
+ const handle = native.newObject(namespace, typeName, props ? unwrapProps(props) : {});
129
+ return wrapInstance(handle);
130
+ };
131
+ Object.defineProperty(ctor, 'name', { value: typeName, configurable: true });
132
+ ctor.$gtypeName = `${namespace}.${typeName}`;
133
+ // Expose constructor/static methods lazily: Ns.Class.new(...) /
134
+ // Ns.Class.new_for_path(...) (and the camelCase aliases). `new Ns.Class({...})`
135
+ // still goes through the target's [[Construct]] (the default Proxy behaviour).
136
+ return new Proxy(ctor, {
137
+ get(t, prop) {
138
+ if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
139
+ const giName = camelToSnake(prop);
140
+ return (...args) =>
141
+ wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
142
+ },
143
+ });
144
+ }
145
+
146
+ // Surface a boxed/struct type (e.g. GLib.MainLoop) as a class-like object whose
147
+ // static/constructor methods route through the engine: `GLib.MainLoop.new(...)`
148
+ // and the camelCase alias, plus `new GLib.MainLoop(...)` mapped to the `new`
149
+ // constructor. Returned boxed instances are wrapped by {@link wrapBoxed}.
150
+ function makeStruct(namespace, typeName) {
151
+ const base = function () {};
152
+ Object.defineProperty(base, 'name', { value: typeName, configurable: true });
153
+ base.$gtypeName = `${namespace}.${typeName}`;
154
+ return new Proxy(base, {
155
+ get(t, prop) {
156
+ if (typeof prop !== 'string' || prop in t || RESERVED.has(prop)) return t[prop];
157
+ const giName = camelToSnake(prop);
158
+ return (...args) =>
159
+ wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
160
+ },
161
+ construct(_t, args) {
162
+ return wrapReturn(native.callStaticMethod(namespace, typeName, 'new', unwrapArgs(args)));
163
+ },
164
+ });
165
+ }
166
+
167
+ // Build a frozen enum/flags object keyed GJS-style: member names UPPER_CASED
168
+ // with '-' → '_' (e.g. Gio.BusType.SYSTEM, Gio.ApplicationFlags.HANDLES_OPEN).
169
+ function makeEnum(namespace, typeName) {
170
+ const raw = native.getEnumValues(namespace, typeName);
171
+ const out = {};
172
+ for (const key of Object.keys(raw)) {
173
+ out[key.toUpperCase().replace(/-/g, '_')] = raw[key];
174
+ }
175
+ return Object.freeze(out);
176
+ }
177
+
178
+ function createNamespace(namespace) {
179
+ const cache = new Map();
180
+ return new Proxy(Object.create(null), {
181
+ get(_t, prop) {
182
+ if (typeof prop !== 'string' || RESERVED.has(prop)) return undefined;
183
+ if (cache.has(prop)) return cache.get(prop);
184
+ const info = native.findInfo(namespace, prop);
185
+ let value;
186
+ if (info === null) {
187
+ value = undefined;
188
+ } else if (info.kind === 'function') {
189
+ value = (...args) => wrapReturn(native.callFunction(namespace, prop, unwrapArgs(args)));
190
+ } else if (info.kind === 'object' || info.kind === 'interface') {
191
+ // Interfaces are not constructible (`new` throws via newObject) but
192
+ // carry static/constructor methods, e.g. Gio.File.new_for_path.
193
+ value = makeClass(namespace, prop);
194
+ } else if (info.kind === 'enum' || info.kind === 'flags') {
195
+ value = makeEnum(namespace, prop);
196
+ } else if (info.kind === 'constant') {
197
+ value = native.getConstantValue(namespace, prop);
198
+ } else if (info.kind === 'struct' || info.kind === 'union') {
199
+ // Boxed/struct types: static/constructor methods + boxed instances
200
+ // (GLib.MainLoop, …). Field access lands with the broader structs drop.
201
+ value = makeStruct(namespace, prop);
202
+ } else {
203
+ // interface / callback: surfaced in a later drop.
204
+ value = undefined;
205
+ }
206
+ cache.set(prop, value);
207
+ return value;
208
+ },
209
+ has(_t, prop) {
210
+ return typeof prop === 'string' && native.findInfo(namespace, prop) !== null;
211
+ },
212
+ });
213
+ }
214
+
215
+ const namespaceCache = new Map();
216
+
217
+ // Whether the libuv↔GLib bridge has been attached this process. The native
218
+ // startMainLoop is itself idempotent; this just avoids the extra call.
219
+ let loopAttached = false;
220
+
221
+ /**
222
+ * Require a GObject-Introspection namespace and return a GJS-shaped namespace
223
+ * object. The Node twin of `import Ns from 'gi://Ns?version=X'` /
224
+ * `imports.gi.Ns`.
225
+ * @param {string} namespace e.g. "Gio"
226
+ * @param {string} [version] e.g. "2.0"
227
+ * @returns {Record<string, unknown>}
228
+ */
229
+ export function requireGi(namespace, version) {
230
+ native.requireNamespace(namespace, version);
231
+ // Attach the libuv-in-GLib bridge once, so any later blocking GLib loop
232
+ // (GLib.MainLoop.run / Gio.Application.run) keeps Node's event loop alive.
233
+ if (!loopAttached) {
234
+ native.startMainLoop();
235
+ loopAttached = true;
236
+ }
237
+ const key = version ? `${namespace}@${version}` : namespace;
238
+ let ns = namespaceCache.get(key);
239
+ if (ns === undefined) {
240
+ ns = createNamespace(namespace);
241
+ namespaceCache.set(key, ns);
242
+ }
243
+ return ns;
244
+ }
245
+
246
+ /**
247
+ * Extract the raw native GObject handle from a wrapped instance (advanced /
248
+ * interop use). Returns the value unchanged if it is not a wrapped instance.
249
+ * @param {unknown} value
250
+ * @returns {unknown}
251
+ */
252
+ export function unwrap(value) {
253
+ return unwrapArg(value);
254
+ }
255
+
256
+ export default requireGi;
package/globals.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/node-gi/globals — types for the GJS ambient-globals shim.
3
+
4
+ /** The legacy GJS `imports` object (a minimal Node-backed subset). */
5
+ export interface GjsImports {
6
+ /** `imports.gi.<Ns>` resolves a namespace; `imports.gi.versions.<Ns>` pins a version. */
7
+ gi: {
8
+ versions: Record<string, string | undefined>;
9
+ [namespace: string]: unknown;
10
+ };
11
+ /** `imports.system` — process identity + lifecycle (Node-backed subset). */
12
+ system: {
13
+ exit(code?: number): void;
14
+ gc(): void;
15
+ readonly programInvocationName: string;
16
+ readonly programPath: string | null;
17
+ version: number;
18
+ addressOf(): string;
19
+ refcount(): number;
20
+ breakpoint(): void;
21
+ dumpHeap(): void;
22
+ dumpMemoryInfo(): void;
23
+ };
24
+ /** `imports.gettext` — a no-translation passthrough. */
25
+ gettext: {
26
+ gettext(s: string): string;
27
+ dgettext(domain: string, s: string): string;
28
+ dcgettext(domain: string, s: string): string;
29
+ ngettext(s: string, p: string, n: number): string;
30
+ dngettext(domain: string, s: string, p: string, n: number): string;
31
+ pgettext(ctx: string, s: string): string;
32
+ dpgettext(domain: string, ctx: string, s: string): string;
33
+ domain(domain: string): {
34
+ gettext(s: string): string;
35
+ ngettext(s: string, p: string, n: number): string;
36
+ pgettext(ctx: string, s: string): string;
37
+ };
38
+ setlocale(): null;
39
+ bindtextdomain(): null;
40
+ textdomain(): null;
41
+ bindtextdomainCodeset(): null;
42
+ LocaleCategory: Record<string, number>;
43
+ };
44
+ versions: Record<string, unknown>;
45
+ }
46
+
47
+ declare global {
48
+ // eslint-disable-next-line no-var
49
+ var print: (...args: unknown[]) => void;
50
+ // eslint-disable-next-line no-var
51
+ var printerr: (...args: unknown[]) => void;
52
+ // eslint-disable-next-line no-var
53
+ var log: (...args: unknown[]) => void;
54
+ // eslint-disable-next-line no-var
55
+ var logError: (error: unknown, prefix?: string) => void;
56
+ // eslint-disable-next-line no-var
57
+ var ARGV: string[];
58
+ // eslint-disable-next-line no-var
59
+ var imports: GjsImports;
60
+ }
61
+
62
+ /** Install the GJS ambient globals on `globalThis` (idempotent). */
63
+ export function installGjsGlobals(): GjsImports;
64
+
65
+ declare const _default: typeof installGjsGlobals;
66
+ export default _default;