@gjsify/node-gi 0.64.11 → 0.64.13
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/gettext.d.ts +8 -0
- package/gettext.js +8 -0
- package/gi.js +22 -4
- package/gtk-runtime.d.ts +0 -2
- package/gtk-runtime.js +0 -21
- package/host.nativescript.js +124 -1
- package/host.node.js +15 -1
- package/index.d.ts +8 -0
- package/index.js +2 -0
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-arm64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/addon.cc +1 -0
- package/src/calls.cc +1 -1
- package/src/class.cc +18 -3
- package/src/common.h +35 -0
- package/src/loop.cc +1 -1
- package/src/object.cc +49 -3
- package/src/private.cc +2 -2
- package/src/repo.cc +1 -0
- package/src/signals.cc +1 -1
package/gettext.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// @gjsify/node-gi/gettext — types for the GJS `Gettext` module on Node.
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Re-derive the process's message languages from the platform. A no-op off Android; on Android
|
|
6
|
+
* call it after the user switches the app locale. Returns the `LANGUAGE` value set, or null.
|
|
7
|
+
*/
|
|
8
|
+
export function refreshLocale(): string | null;
|
|
4
9
|
/** Look up `msgid` in the default domain. */
|
|
5
10
|
export function gettext(msgid: string): string;
|
|
6
11
|
/** Look up `msgid` in `domain`. */
|
|
@@ -63,3 +68,6 @@ export interface GettextModule {
|
|
|
63
68
|
|
|
64
69
|
declare const Gettext: GettextModule;
|
|
65
70
|
export default Gettext;
|
|
71
|
+
|
|
72
|
+
/** Re-derive message languages from the platform (Android); null elsewhere. */
|
|
73
|
+
export function refreshLocale(): string | null;
|
package/gettext.js
CHANGED
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
import * as native from './index.js';
|
|
26
26
|
import { ensureNative } from './index.js';
|
|
27
27
|
import { requireGi } from './gi.js';
|
|
28
|
+
import { refreshLocale } from '#host';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Re-derive the process's message languages from the platform. A no-op off Android; on
|
|
32
|
+
* Android call it after the user switches the app locale (LANGUAGE is re-read per lookup).
|
|
33
|
+
* @returns {string | null}
|
|
34
|
+
*/
|
|
35
|
+
export { refreshLocale };
|
|
28
36
|
|
|
29
37
|
// GLib is resolved on first lookup, not at import: loading a namespace walks the
|
|
30
38
|
// typelib, and a module imported for its LocaleCategory alone should not pay for
|
package/gi.js
CHANGED
|
@@ -1190,8 +1190,11 @@ function runJsPropertySetter(handle, propertyName) {
|
|
|
1190
1190
|
const up = instance[USER_PROTO];
|
|
1191
1191
|
if (up === undefined) return;
|
|
1192
1192
|
const set = findPropertySetter(up, propertyName);
|
|
1193
|
-
|
|
1193
|
+
// No JS accessor: the store is the single backing store. The `false` tells the engine
|
|
1194
|
+
// so, and it stops crossing for this (class, property) — see NodeGiEnvData::noJsSetter.
|
|
1195
|
+
if (set === undefined) return false;
|
|
1194
1196
|
set.call(instance, wrapReturn(native.getProperty(handle, propertyName)));
|
|
1197
|
+
return true;
|
|
1195
1198
|
}
|
|
1196
1199
|
// Guarded because the addon paired with this JS may PREDATE it, and that pairing is
|
|
1197
1200
|
// deliberate rather than hypothetical: gtk-os-suites.yml's shipped-closure legs run
|
|
@@ -1704,6 +1707,15 @@ function wrapInstance(handle, userProto) {
|
|
|
1704
1707
|
// Built fresh, every `obj.m(…)` in a loop allocated a closure and re-asked for
|
|
1705
1708
|
// the type name across the N-API boundary. Null prototype: `constructor` and
|
|
1706
1709
|
// friends must not read as a cached thunk.
|
|
1710
|
+
// Positive answers only: a GObject class never loses a property, so a hit is final
|
|
1711
|
+
// and saves one boundary crossing per get/set; a miss stays a live lookup.
|
|
1712
|
+
const knownProps = new Set();
|
|
1713
|
+
const hasProp = (name) => {
|
|
1714
|
+
if (knownProps.has(name)) return true;
|
|
1715
|
+
if (!native.hasProperty(handle, name)) return false;
|
|
1716
|
+
knownProps.add(name);
|
|
1717
|
+
return true;
|
|
1718
|
+
};
|
|
1707
1719
|
const giMethodThunks = Object.create(null);
|
|
1708
1720
|
const giMethodThunk = (prop) => {
|
|
1709
1721
|
let thunk = giMethodThunks[prop];
|
|
@@ -1815,7 +1827,7 @@ function wrapInstance(handle, userProto) {
|
|
|
1815
1827
|
const shim = objectPrototypeShim(prop);
|
|
1816
1828
|
if (shim !== undefined) return (...args) => shim(handle, args);
|
|
1817
1829
|
const propName = toKebab(prop);
|
|
1818
|
-
if (
|
|
1830
|
+
if (hasProp(propName)) {
|
|
1819
1831
|
// A READ materialises a custom property's value, exactly as GJS's generated
|
|
1820
1832
|
// getter writes the default into its private field on first look-up — which
|
|
1821
1833
|
// is what makes a LATER assignment of that same default silent. See the
|
|
@@ -1864,7 +1876,7 @@ function wrapInstance(handle, userProto) {
|
|
|
1864
1876
|
}
|
|
1865
1877
|
}
|
|
1866
1878
|
const propName = toKebab(prop);
|
|
1867
|
-
if (
|
|
1879
|
+
if (hasProp(propName)) {
|
|
1868
1880
|
// Equal-value guard, GJS's `_generateAccessors` setter verbatim:
|
|
1869
1881
|
// `if (value !== this[privateName]) { …; this.notify(name) }`
|
|
1870
1882
|
// (refs/gjs/modules/core/_common.js). GJS puts it in the accessor it
|
|
@@ -1886,6 +1898,12 @@ function wrapInstance(handle, userProto) {
|
|
|
1886
1898
|
// all three spellings onto it.
|
|
1887
1899
|
const custom = up !== undefined && customPropertiesByProto.get(up)?.has(propName) === true;
|
|
1888
1900
|
if (custom && materialise(propName)) {
|
|
1901
|
+
if (typeof native.setPropertyIfChanged === 'function') {
|
|
1902
|
+
// One crossing: the engine compares and stores. 2 = a value type it
|
|
1903
|
+
// does not decide (objects, enums…), so take the two-call path.
|
|
1904
|
+
const done = native.setPropertyIfChanged(handle, propName, unwrapArg(value));
|
|
1905
|
+
if (done !== 2) return true;
|
|
1906
|
+
}
|
|
1889
1907
|
if (wrapReturn(native.getProperty(handle, propName)) === value) return true;
|
|
1890
1908
|
}
|
|
1891
1909
|
native.setProperty(handle, propName, unwrapArg(value));
|
|
@@ -1903,7 +1921,7 @@ function wrapInstance(handle, userProto) {
|
|
|
1903
1921
|
// GI methods are resolved dynamically and are intentionally not reported.)
|
|
1904
1922
|
const up = t[USER_PROTO];
|
|
1905
1923
|
if (up !== undefined && findProtoDescriptor(up, prop) !== undefined) return true;
|
|
1906
|
-
if (
|
|
1924
|
+
if (hasProp(toKebab(prop))) return true;
|
|
1907
1925
|
}
|
|
1908
1926
|
return false;
|
|
1909
1927
|
},
|
package/gtk-runtime.d.ts
CHANGED
|
@@ -13,8 +13,6 @@ export function maybeReexecForGtkRuntime(): void;
|
|
|
13
13
|
export function maybePrependGtkRuntimeDllPath(): void;
|
|
14
14
|
/** Windows: wire the env for a full-windowing bundle's runtime data — GSettings schemas, gdk-pixbuf loaders, icon themes, fontconfig (no-op off win32 / for a display-free bundle). */
|
|
15
15
|
export function maybeWireGtkWindowingEnv(): void;
|
|
16
|
-
/** darwin: point libgda at the bundle's SQLite provider via `GDA_TOP_BUILD_DIR` (only when unset; no-op for a bundle without `lib/libgda-6.0/providers`). */
|
|
17
|
-
export function wireGdaProviders(bundle: { dir: string }): void;
|
|
18
16
|
/** Activate the bundled GTK runtime for the native engine, if one is present. */
|
|
19
17
|
export function activateBundledGtkRuntime(native: {
|
|
20
18
|
prependSearchPath: (p: string) => void;
|
package/gtk-runtime.js
CHANGED
|
@@ -641,26 +641,6 @@ export function maybeWireGtkWindowingEnv() {
|
|
|
641
641
|
if (existsSync(gioModules)) setIfUnset('GIO_MODULE_DIR', gioModules);
|
|
642
642
|
}
|
|
643
643
|
|
|
644
|
-
/**
|
|
645
|
-
* Point libgda at the bundle's database providers (darwin; `@gjsify/sqlite` / `node:sqlite`).
|
|
646
|
-
*
|
|
647
|
-
* libgda loads a provider as a GModule from a directory COMPILED IN as the build prefix —
|
|
648
|
-
* `<keg>/lib/libgda-6.0/providers` — and, with that keg absent, `Gda.Connection.new_from_string
|
|
649
|
-
* ('SQLite', …)` throws `No provider 'SQLite' installed`. With the keg PRESENT it is worse: brew's
|
|
650
|
-
* provider loads against brew's libgda and glib beside the bundle's own. The one override libgda
|
|
651
|
-
* has is `GDA_TOP_BUILD_DIR`, read as `<dir>/providers` at the first provider scan, which is long
|
|
652
|
-
* after this runs. In the JS and not in a launcher for the reason `GJSIFY_GI_LIBRARY_PATH` is:
|
|
653
|
-
* a signed app loses `DYLD_*`, never `process.env`.
|
|
654
|
-
* Set only when unset, so an operator pointing libgda elsewhere wins; a no-op for a bundle
|
|
655
|
-
* that carries no provider dir.
|
|
656
|
-
* @param {{ dir: string }} bundle
|
|
657
|
-
*/
|
|
658
|
-
export function wireGdaProviders(bundle) {
|
|
659
|
-
const gdaDir = join(bundle.dir, 'lib', 'libgda-6.0');
|
|
660
|
-
if (!existsSync(join(gdaDir, 'providers'))) return;
|
|
661
|
-
if (!process.env.GDA_TOP_BUILD_DIR) process.env.GDA_TOP_BUILD_DIR = gdaDir;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
644
|
/** The plugin scanner's leaf name — the bundles ship the same tree on both OSes. */
|
|
665
645
|
function gstScannerLeaf() {
|
|
666
646
|
return process.platform === 'win32' ? 'gst-plugin-scanner.exe' : 'gst-plugin-scanner';
|
|
@@ -826,7 +806,6 @@ export function activateBundledGtkRuntime(native) {
|
|
|
826
806
|
// variable REPLACES rather than extends.
|
|
827
807
|
process.env.DYLD_FALLBACK_LIBRARY_PATH = composeDyldFallback([bundle.libDir]);
|
|
828
808
|
}
|
|
829
|
-
wireGdaProviders(bundle);
|
|
830
809
|
}
|
|
831
810
|
|
|
832
811
|
activated = bundle;
|
package/host.nativescript.js
CHANGED
|
@@ -116,9 +116,130 @@ function deriveAppDir() {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// A Vite-built app exposes only the `android.*` packages its own code touched, and nothing
|
|
120
|
+
// touches `android.system` — so `android.system.Os` is undefined there and every setenv
|
|
121
|
+
// silently did nothing (LANGUAGE and XDG_* never reached the native process). Reflection
|
|
122
|
+
// reaches the class whatever the bundle referenced.
|
|
123
|
+
function androidSetenv(name, value) {
|
|
124
|
+
const os = globalThis.android?.system?.Os;
|
|
125
|
+
if (os) {
|
|
126
|
+
os.setenv(name, value, true);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const java = globalThis.java;
|
|
130
|
+
if (!java?.lang?.Class) return;
|
|
131
|
+
try {
|
|
132
|
+
const types = java.lang.reflect.Array.newInstance(java.lang.Class.class, 3);
|
|
133
|
+
java.lang.reflect.Array.set(types, 0, java.lang.String.class);
|
|
134
|
+
java.lang.reflect.Array.set(types, 1, java.lang.String.class);
|
|
135
|
+
java.lang.reflect.Array.set(types, 2, java.lang.Boolean.TYPE);
|
|
136
|
+
const args = java.lang.reflect.Array.newInstance(java.lang.Object.class, 3);
|
|
137
|
+
java.lang.reflect.Array.set(args, 0, name);
|
|
138
|
+
java.lang.reflect.Array.set(args, 1, value);
|
|
139
|
+
java.lang.reflect.Array.set(args, 2, java.lang.Boolean.TRUE);
|
|
140
|
+
java.lang.Class.forName('android.system.Os').getMethod('setenv', types).invoke(null, args);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
globalThis.console?.warn?.(`node-gi: setenv(${name}) failed: ${e}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
119
146
|
function setNativeEnv(name, value) {
|
|
120
147
|
env[name] = value;
|
|
121
|
-
|
|
148
|
+
androidSetenv(name, value);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The LANGUAGE value for a list of BCP-47 tags: `de-DE` → `de_DE:de`, `zh-Hans-CN` → `zh_CN:zh`,
|
|
153
|
+
* de-duplicated, cut after the first English tag, with `C` last as GLib orders it. Bionic's setlocale accepts only C/C.UTF-8
|
|
154
|
+
* and Android sets no LANG, so GLib's language list (and the libintl in gi-runtime-android,
|
|
155
|
+
* which reads LANGUAGE on every lookup) has nothing else to go on. Pure — no device needed.
|
|
156
|
+
* @param {string[]} tags
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
export function languageFromBcp47(tags) {
|
|
160
|
+
const out = [];
|
|
161
|
+
const add = (name) => {
|
|
162
|
+
if (!out.includes(name)) out.push(name);
|
|
163
|
+
};
|
|
164
|
+
for (const tag of tags) {
|
|
165
|
+
const parts = String(tag).split(/[-_]/).filter(Boolean);
|
|
166
|
+
if (parts.length === 0 || !/^[A-Za-z]{2,3}$/.test(parts[0])) continue;
|
|
167
|
+
const lang = parts[0].toLowerCase();
|
|
168
|
+
const region = parts.slice(1).find((p) => /^([A-Za-z]{2}|\d{3})$/.test(p));
|
|
169
|
+
if (region) add(`${lang}_${region.toUpperCase()}`);
|
|
170
|
+
add(lang);
|
|
171
|
+
// English is the msgid language, so no catalog exists for it and gettext would go on to
|
|
172
|
+
// the NEXT entry: with the app switched to en-US over a German system the list is
|
|
173
|
+
// [en-US, de-DE], and every string came back German. Nothing after English may apply.
|
|
174
|
+
if (lang === 'en') break;
|
|
175
|
+
}
|
|
176
|
+
add('C');
|
|
177
|
+
return out.join(':');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function platformLanguageTags() {
|
|
181
|
+
const locales = globalThis.android?.content?.res?.Resources?.getSystem?.()?.getConfiguration?.()?.getLocales?.();
|
|
182
|
+
const tags = [];
|
|
183
|
+
if (locales) {
|
|
184
|
+
for (let i = 0; i < locales.size(); i++) tags.push(String(locales.get(i).toLanguageTag()));
|
|
185
|
+
}
|
|
186
|
+
if (tags.length === 0) {
|
|
187
|
+
const one = globalThis.java?.util?.Locale?.getDefault?.();
|
|
188
|
+
if (one) tags.push(String(one.toLanguageTag()));
|
|
189
|
+
}
|
|
190
|
+
return tags;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Point LANGUAGE at the platform's current locale list. Call it again when the user
|
|
195
|
+
* switches the app's language: libintl and GLib re-read the variable, nothing is cached.
|
|
196
|
+
* The explicit __NODE_GI_LANGUAGE global wins, for a host that manages its own.
|
|
197
|
+
* @returns {string | null} the value set, or null when the platform gave nothing
|
|
198
|
+
*/
|
|
199
|
+
export function refreshLocale() {
|
|
200
|
+
let value = globalThis.__NODE_GI_LANGUAGE;
|
|
201
|
+
if (typeof value !== 'string' || value === '') {
|
|
202
|
+
try {
|
|
203
|
+
const tags = platformLanguageTags();
|
|
204
|
+
value = tags.length > 0 ? languageFromBcp47(tags) : null;
|
|
205
|
+
} catch {
|
|
206
|
+
value = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (value === null || value === undefined) return null;
|
|
210
|
+
setNativeEnv('LANGUAGE', value);
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let followingSystemLocale = false;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Re-run `refreshLocale()` whenever the system configuration changes — a language switch is
|
|
218
|
+
* one. The Application-level ComponentCallbacks2 fires whether or not the activity declares
|
|
219
|
+
* `locale` in `configChanges`. Registered once, from the host that owns the LANGUAGE bridge,
|
|
220
|
+
* so no consumer has to know the variable exists.
|
|
221
|
+
* @returns {boolean} whether a callback is registered
|
|
222
|
+
*/
|
|
223
|
+
export function followSystemLocale() {
|
|
224
|
+
if (followingSystemLocale) return true;
|
|
225
|
+
try {
|
|
226
|
+
const app = globalThis.com?.tns?.NativeScriptApplication?.getInstance();
|
|
227
|
+
const ComponentCallbacks2 = globalThis.android?.content?.ComponentCallbacks2;
|
|
228
|
+
if (!app || !ComponentCallbacks2) return false;
|
|
229
|
+
app.registerComponentCallbacks(
|
|
230
|
+
new ComponentCallbacks2({
|
|
231
|
+
onConfigurationChanged: () => {
|
|
232
|
+
refreshLocale();
|
|
233
|
+
},
|
|
234
|
+
onLowMemory: () => {},
|
|
235
|
+
onTrimMemory: () => {},
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
followingSystemLocale = true;
|
|
239
|
+
} catch {
|
|
240
|
+
// A callback missing only costs the live switch; the locale at start is still set.
|
|
241
|
+
}
|
|
242
|
+
return followingSystemLocale;
|
|
122
243
|
}
|
|
123
244
|
|
|
124
245
|
function appDirectory() {
|
|
@@ -165,6 +286,8 @@ function ensureJavaVm(native) {
|
|
|
165
286
|
}
|
|
166
287
|
|
|
167
288
|
function configureEnvironment() {
|
|
289
|
+
refreshLocale();
|
|
290
|
+
followSystemLocale();
|
|
168
291
|
const systemDir = globalThis.__NODE_GI_SYSTEM_DIR;
|
|
169
292
|
if (typeof systemDir === 'string' && systemDir !== '') {
|
|
170
293
|
setNativeEnv('XDG_DATA_DIRS', `${systemDir}/share`);
|
package/host.node.js
CHANGED
|
@@ -32,7 +32,21 @@ import { setImmediate, setInterval, clearInterval } from 'node:timers';
|
|
|
32
32
|
// loop-liveness hooks in gi.js would silently never fire.
|
|
33
33
|
import runtimeProcess from 'node:process';
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// Only Android derives the locale from the platform (host.nativescript.js); a desktop
|
|
36
|
+
// process already has LANG, so there is nothing to refresh.
|
|
37
|
+
function refreshLocale() {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
setImmediate,
|
|
43
|
+
setInterval,
|
|
44
|
+
clearInterval,
|
|
45
|
+
runtimeProcess,
|
|
46
|
+
windowingEnvWrites,
|
|
47
|
+
bundledLocaleDir,
|
|
48
|
+
refreshLocale,
|
|
49
|
+
};
|
|
36
50
|
|
|
37
51
|
const require = createRequire(import.meta.url);
|
|
38
52
|
const here = packageRoot;
|
package/index.d.ts
CHANGED
|
@@ -307,6 +307,13 @@ export function getProperty(handle: GObjectHandle, name: string): unknown;
|
|
|
307
307
|
|
|
308
308
|
/** Write a GObject property. */
|
|
309
309
|
export function setProperty(handle: GObjectHandle, name: string, value: unknown): void;
|
|
310
|
+
/**
|
|
311
|
+
* `setProperty` with gi.js's equal-value guard folded in, so a guarded write is one crossing.
|
|
312
|
+
* Returns 0 when the value was stored, 1 when it equals the current one (nothing stored, no
|
|
313
|
+
* notify), 2 when the property's type is not decided here (objects, enums, …; nothing done).
|
|
314
|
+
* Absent from an addon older than this JS.
|
|
315
|
+
*/
|
|
316
|
+
export function setPropertyIfChanged(handle: GObjectHandle, name: string, value: unknown): 0 | 1 | 2;
|
|
310
317
|
|
|
311
318
|
/**
|
|
312
319
|
* Whether the instance's type has a GObject property by this name (kebab- or
|
|
@@ -632,6 +639,7 @@ declare const native: {
|
|
|
632
639
|
getTemplateChild: typeof getTemplateChild;
|
|
633
640
|
getProperty: typeof getProperty;
|
|
634
641
|
setProperty: typeof setProperty;
|
|
642
|
+
setPropertyIfChanged?: typeof setPropertyIfChanged;
|
|
635
643
|
hasProperty: typeof hasProperty;
|
|
636
644
|
getTypeName: typeof getTypeName;
|
|
637
645
|
classInfoForTypeName: typeof classInfoForTypeName;
|
package/index.js
CHANGED
|
@@ -67,6 +67,7 @@ export function ensureNative() {
|
|
|
67
67
|
getTemplateChild = addon.getTemplateChild;
|
|
68
68
|
getProperty = addon.getProperty;
|
|
69
69
|
setProperty = addon.setProperty;
|
|
70
|
+
setPropertyIfChanged = addon.setPropertyIfChanged;
|
|
70
71
|
hasProperty = addon.hasProperty;
|
|
71
72
|
getTypeName = addon.getTypeName;
|
|
72
73
|
classInfoForTypeName = addon.classInfoForTypeName;
|
|
@@ -177,6 +178,7 @@ export let callClassVfunc;
|
|
|
177
178
|
export let getTemplateChild;
|
|
178
179
|
export let getProperty;
|
|
179
180
|
export let setProperty;
|
|
181
|
+
export let setPropertyIfChanged;
|
|
180
182
|
export let hasProperty;
|
|
181
183
|
export let getTypeName;
|
|
182
184
|
export let classInfoForTypeName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.13",
|
|
4
4
|
"description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"imports": {
|
|
11
11
|
"#host": {
|
|
12
12
|
"nativescript": "./host.nativescript.js",
|
|
13
|
+
"wasm": "./host.wasm.js",
|
|
13
14
|
"default": "./host.node.js"
|
|
14
15
|
}
|
|
15
16
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -109,6 +109,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
109
109
|
exports.Set("getTemplateChild", Napi::Function::New(env, GetTemplateChild));
|
|
110
110
|
exports.Set("getProperty", Napi::Function::New(env, GetProperty));
|
|
111
111
|
exports.Set("setProperty", Napi::Function::New(env, SetProperty));
|
|
112
|
+
exports.Set("setPropertyIfChanged", Napi::Function::New(env, SetPropertyIfChanged));
|
|
112
113
|
exports.Set("hasProperty", Napi::Function::New(env, HasProperty));
|
|
113
114
|
exports.Set("getTypeName", Napi::Function::New(env, GetTypeName));
|
|
114
115
|
exports.Set("classInfoForTypeName", Napi::Function::New(env, ClassInfoForTypeName));
|
package/src/calls.cc
CHANGED
|
@@ -250,7 +250,7 @@ static void NodeGiCallbackTrampoline(ffi_cif* /*cif*/, void* result, void** args
|
|
|
250
250
|
// napi_make_callback drains nextTick/microtasks around the call (Node; on
|
|
251
251
|
// Bun/Deno the checkpoint is run by NodeGiMaybeDrainMicrotasks below).
|
|
252
252
|
g_loopDispatchDepth++;
|
|
253
|
-
napi_status st =
|
|
253
|
+
napi_status st = NodeGiCallIntoJs(env, global, fn, jsArgs.size(), jsArgs.data(), &ret);
|
|
254
254
|
g_loopDispatchDepth--;
|
|
255
255
|
if (st != napi_ok) ret = nullptr;
|
|
256
256
|
}
|
package/src/class.cc
CHANGED
|
@@ -264,7 +264,7 @@ static void NodeGiVFuncTrampoline(ffi_cif* /*cif*/, void* result, void** args,
|
|
|
264
264
|
// wrapped instance is the receiver (`this`).
|
|
265
265
|
g_loopDispatchDepth++;
|
|
266
266
|
napi_status st =
|
|
267
|
-
|
|
267
|
+
NodeGiCallIntoJs(env, recv, fn, jsArgs.size(), jsArgs.data(), &ret);
|
|
268
268
|
g_loopDispatchDepth--;
|
|
269
269
|
if (st != napi_ok) ret = nullptr;
|
|
270
270
|
}
|
|
@@ -387,6 +387,10 @@ static void MaybeRunJsPropertySetter(NodeGiClassData* cd, GObject* obj, const ch
|
|
|
387
387
|
// Never enter JS during GC / env teardown (same gate as the vfunc trampoline and
|
|
388
388
|
// RunJsConstructorForCObject); the property is already stored either way.
|
|
389
389
|
if (!NodeGiJsAvailable(env)) return;
|
|
390
|
+
NodeGiEnvData* d = EnvData(env);
|
|
391
|
+
GParamSpec* pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(obj), name);
|
|
392
|
+
const std::pair<GType, GParamSpec*> memoKey(G_OBJECT_TYPE(obj), pspec);
|
|
393
|
+
if (d != nullptr && pspec != nullptr && d->noJsSetter.count(memoKey) != 0) return;
|
|
390
394
|
Napi::Env napiEnv(env);
|
|
391
395
|
Napi::HandleScope scope(napiEnv);
|
|
392
396
|
NodeGiPumpJsDispatchScope pumpWindow;
|
|
@@ -394,7 +398,6 @@ static void MaybeRunJsPropertySetter(NodeGiClassData* cd, GObject* obj, const ch
|
|
|
394
398
|
Napi::Value handle = PeekGObjectHandle(napiEnv, obj);
|
|
395
399
|
if (handle.IsEmpty()) return; // construction has not reached the wrap yet
|
|
396
400
|
|
|
397
|
-
NodeGiEnvData* d = EnvData(env);
|
|
398
401
|
napi_value cb = nullptr;
|
|
399
402
|
if (d == nullptr || d->propertySetCallback == nullptr ||
|
|
400
403
|
napi_get_reference_value(env, d->propertySetCallback, &cb) != napi_ok || cb == nullptr) {
|
|
@@ -410,7 +413,19 @@ static void MaybeRunJsPropertySetter(NodeGiClassData* cd, GObject* obj, const ch
|
|
|
410
413
|
// own frame and must not get a microtask checkpoint in the middle of it — the same
|
|
411
414
|
// reasoning as RunJsConstructorForCObject, and gjs's JS_SetProperty takes no
|
|
412
415
|
// checkpoint either.
|
|
413
|
-
|
|
416
|
+
napi_status callStatus = napi_call_function(env, undef, cb, 2, args, &ret);
|
|
417
|
+
if (callStatus == napi_ok && pspec != nullptr && ret != nullptr) {
|
|
418
|
+
// gi.js answers a literal `false` only once the instance's prototype chain has been
|
|
419
|
+
// searched and holds no accessor; any other answer (no wrapper yet, a setter ran)
|
|
420
|
+
// keeps the crossing.
|
|
421
|
+
bool isBool = false;
|
|
422
|
+
napi_valuetype vt = napi_undefined;
|
|
423
|
+
if (napi_typeof(env, ret, &vt) == napi_ok && vt == napi_boolean &&
|
|
424
|
+
napi_get_value_bool(env, ret, &isBool) == napi_ok && !isBool) {
|
|
425
|
+
d->noJsSetter.insert(memoKey);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (callStatus != napi_ok) {
|
|
414
429
|
// The setter threw. A pending JS exception must never cross back into GObject
|
|
415
430
|
// (g_object_set has no way to report one), so fold it into a g_warning — gjs logs
|
|
416
431
|
// the uncaught exception at the same point (gjs_log_exception_uncaught).
|
package/src/common.h
CHANGED
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
#include <map>
|
|
54
54
|
#include <memory>
|
|
55
55
|
#include <mutex>
|
|
56
|
+
#include <set>
|
|
57
|
+
#include <unordered_map>
|
|
56
58
|
#include <string>
|
|
57
59
|
#include <thread>
|
|
58
60
|
#include <vector>
|
|
@@ -114,6 +116,17 @@ struct NodeGiEnvData {
|
|
|
114
116
|
// setter from its set_property vfunc (refs/gjs/gi/gobject.cpp jsobj_set_gproperty).
|
|
115
117
|
// Per-env for the same reason as errorBuilder (a napi_ref is env-specific).
|
|
116
118
|
napi_ref propertySetCallback = nullptr;
|
|
119
|
+
// (instance GType, pspec) pairs for which gi.js answered "this class chain declares
|
|
120
|
+
// no JS accessor" — the store is then the whole write and NodeGiSetProperty skips the
|
|
121
|
+
// JS crossing. Keyed by the INSTANCE's GType because a subclass may add an accessor
|
|
122
|
+
// its parent lacks; mirrors gi.js's per-prototype memo, so the answer is as final as
|
|
123
|
+
// it is there.
|
|
124
|
+
std::set<std::pair<GType, GParamSpec*>> noJsSetter;
|
|
125
|
+
// One JS External per GParamSpec handed out by GValueToJs (a `notify` emission passes
|
|
126
|
+
// the pspec every time). Minting a finalizable External per emission is the dominant
|
|
127
|
+
// cost of a notify on Wasm; the ref (count 1) keeps the External, which keeps the
|
|
128
|
+
// pspec ref, until env teardown.
|
|
129
|
+
std::unordered_map<GParamSpec*, napi_ref> paramSpecHandles;
|
|
117
130
|
};
|
|
118
131
|
|
|
119
132
|
void NodeGiEnvDataFinalize(napi_env env, void* data, void* hint);
|
|
@@ -702,6 +715,27 @@ extern int g_syncEmitDepth;
|
|
|
702
715
|
// InternalCallbackScopes and GJS's "drain when the last JS frame exits" do.
|
|
703
716
|
extern int g_loopDispatchDepth;
|
|
704
717
|
|
|
718
|
+
// Enter JS at a loop-dispatched callback boundary (signals.cc / calls.cc /
|
|
719
|
+
// class.cc / loop.cc / private.cc). On a real Node-API host this is
|
|
720
|
+
// napi_make_callback, which runs the nextTick + microtask checkpoint at the
|
|
721
|
+
// boundary. Under Emscripten the Node-API host is emnapi, whose
|
|
722
|
+
// napi_make_callback is a thin forward to @emnapi/node-binding and returns
|
|
723
|
+
// napi_generic_failure whenever that binding is absent — which it always is
|
|
724
|
+
// outside Node, i.e. in every browser. The call then fails SILENTLY: the GI
|
|
725
|
+
// callback returns FALSE, the GSource is removed, and no handler ever runs.
|
|
726
|
+
// napi_call_function is the portable entry; the checkpoint it skips is the one
|
|
727
|
+
// NodeGiMaybeDrainMicrotasks already performs for Bun/Deno, and the wasm host
|
|
728
|
+
// registers the same drain (host.wasm.js).
|
|
729
|
+
static inline napi_status NodeGiCallIntoJs(napi_env env, napi_value recv, napi_value fn,
|
|
730
|
+
size_t argc, const napi_value* argv,
|
|
731
|
+
napi_value* result) {
|
|
732
|
+
#ifdef __EMSCRIPTEN__
|
|
733
|
+
return napi_call_function(env, recv, fn, argc, argv, result);
|
|
734
|
+
#else
|
|
735
|
+
return napi_make_callback(env, nullptr, recv, fn, argc, argv, result);
|
|
736
|
+
#endif
|
|
737
|
+
}
|
|
738
|
+
|
|
705
739
|
// Cross-runtime microtask checkpoint (Bun/Deno — see loop.cc). Invokes the
|
|
706
740
|
// runtime-native drain registered via setMicrotaskDrain after an outermost
|
|
707
741
|
// loop-dispatched callback returns; a no-op on Node (never registered), with a
|
|
@@ -883,6 +917,7 @@ Napi::Value IsFundamentalHandle(const Napi::CallbackInfo& info);
|
|
|
883
917
|
Napi::Value NewObject(const Napi::CallbackInfo& info);
|
|
884
918
|
Napi::Value GetProperty(const Napi::CallbackInfo& info);
|
|
885
919
|
Napi::Value SetProperty(const Napi::CallbackInfo& info);
|
|
920
|
+
Napi::Value SetPropertyIfChanged(const Napi::CallbackInfo& info);
|
|
886
921
|
Napi::Value HasProperty(const Napi::CallbackInfo& info);
|
|
887
922
|
Napi::Value GetTypeName(const Napi::CallbackInfo& info);
|
|
888
923
|
Napi::Value ClassInfoForTypeName(const Napi::CallbackInfo& info);
|
package/src/loop.cc
CHANGED
|
@@ -107,7 +107,7 @@ static void DrainMicrotasks() {
|
|
|
107
107
|
if (napi_get_reference_value(env, g_process_ref, &process_v) == napi_ok &&
|
|
108
108
|
napi_get_reference_value(env, g_tick_callback_ref, &tick) == napi_ok &&
|
|
109
109
|
process_v != nullptr && tick != nullptr) {
|
|
110
|
-
|
|
110
|
+
NodeGiCallIntoJs(env, process_v, tick, 0, nullptr, &result);
|
|
111
111
|
}
|
|
112
112
|
napi_close_handle_scope(env, scope);
|
|
113
113
|
}
|
package/src/object.cc
CHANGED
|
@@ -304,7 +304,24 @@ Napi::Value GValueToJs(Napi::Env env, const GValue* v) {
|
|
|
304
304
|
// .name/.get_name()/.value_type/.nick/.blurb/.flags/.owner_type — matching
|
|
305
305
|
// gjs. g_value_get_param BORROWS (transfer none).
|
|
306
306
|
GParamSpec* p = g_value_get_param(v);
|
|
307
|
-
|
|
307
|
+
if (p == nullptr) return env.Null();
|
|
308
|
+
NodeGiEnvData* d = EnvData(env);
|
|
309
|
+
if (d == nullptr) return MakeParamSpecHandle(env, p, GI_TRANSFER_NOTHING);
|
|
310
|
+
auto hit = d->paramSpecHandles.find(p);
|
|
311
|
+
if (hit != d->paramSpecHandles.end()) {
|
|
312
|
+
napi_value cached = nullptr;
|
|
313
|
+
if (napi_get_reference_value(env, hit->second, &cached) == napi_ok && cached != nullptr) {
|
|
314
|
+
return Napi::Value(env, cached);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
Napi::Value fresh = MakeParamSpecHandle(env, p, GI_TRANSFER_NOTHING);
|
|
318
|
+
if (!fresh.IsEmpty() && fresh.IsExternal()) {
|
|
319
|
+
napi_ref ref = nullptr;
|
|
320
|
+
if (napi_create_reference(env, fresh, 1, &ref) == napi_ok && ref != nullptr) {
|
|
321
|
+
d->paramSpecHandles[p] = ref;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return fresh;
|
|
308
325
|
}
|
|
309
326
|
default:
|
|
310
327
|
Napi::TypeError::New(env, std::string("Unsupported property GType ") +
|
|
@@ -765,7 +782,11 @@ Napi::Value GetProperty(const Napi::CallbackInfo& info) {
|
|
|
765
782
|
}
|
|
766
783
|
|
|
767
784
|
// setProperty(handle, name, value) -> void
|
|
768
|
-
|
|
785
|
+
// setPropertyIfChanged(handle, name, value) -> 0 stored | 1 equal, nothing done | 2 undecided,
|
|
786
|
+
// nothing done. Folds gi.js's equal-value guard (`wrapReturn(getProperty) === value`) into the
|
|
787
|
+
// store so a guarded set is ONE crossing. Only the types whose JS form compares by value are
|
|
788
|
+
// decided here; anything else (objects, boxed, enums…) answers 2 and gi.js takes the two-call path.
|
|
789
|
+
static Napi::Value SetPropertyImpl(const Napi::CallbackInfo& info, bool guarded) {
|
|
769
790
|
Napi::Env env = info.Env();
|
|
770
791
|
if (info.Length() < 3 || !info[1].IsString()) {
|
|
771
792
|
Napi::TypeError::New(env, "setProperty(handle, name: string, value)").ThrowAsJavaScriptException();
|
|
@@ -782,6 +803,26 @@ Napi::Value SetProperty(const Napi::CallbackInfo& info) {
|
|
|
782
803
|
// gjs refuses an out-of-range value for these property types on assignment
|
|
783
804
|
// (object.cpp, with its own text); JsToGValue below now refuses them too, with the
|
|
784
805
|
// gjs_value_to_g_value text.
|
|
806
|
+
if (guarded) {
|
|
807
|
+
switch (G_TYPE_FUNDAMENTAL(pspec->value_type)) {
|
|
808
|
+
case G_TYPE_BOOLEAN: case G_TYPE_CHAR: case G_TYPE_UCHAR: case G_TYPE_INT:
|
|
809
|
+
case G_TYPE_UINT: case G_TYPE_LONG: case G_TYPE_ULONG: case G_TYPE_INT64:
|
|
810
|
+
case G_TYPE_UINT64: case G_TYPE_FLOAT: case G_TYPE_DOUBLE: case G_TYPE_STRING:
|
|
811
|
+
break;
|
|
812
|
+
default:
|
|
813
|
+
return Napi::Number::New(env, 2);
|
|
814
|
+
}
|
|
815
|
+
GValue cur = G_VALUE_INIT;
|
|
816
|
+
g_value_init(&cur, pspec->value_type);
|
|
817
|
+
g_object_get_property(obj, name.c_str(), &cur);
|
|
818
|
+
Napi::Value curJs = GValueToJs(env, &cur);
|
|
819
|
+
g_value_unset(&cur);
|
|
820
|
+
if (env.IsExceptionPending()) return env.Undefined();
|
|
821
|
+
bool same = false;
|
|
822
|
+
if (!curJs.IsEmpty() && napi_strict_equals(env, curJs, info[2], &same) == napi_ok && same) {
|
|
823
|
+
return Napi::Number::New(env, 1);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
785
826
|
GITypeTag intTag = GI_TYPE_TAG_VOID;
|
|
786
827
|
switch (G_TYPE_FUNDAMENTAL(pspec->value_type)) {
|
|
787
828
|
case G_TYPE_CHAR: intTag = GI_TYPE_TAG_INT8; break;
|
|
@@ -809,7 +850,12 @@ Napi::Value SetProperty(const Napi::CallbackInfo& info) {
|
|
|
809
850
|
g_object_set_property(obj, name.c_str(), &v);
|
|
810
851
|
}
|
|
811
852
|
g_value_unset(&v);
|
|
812
|
-
return env.Undefined();
|
|
853
|
+
return guarded ? Napi::Number::New(env, 0).As<Napi::Value>() : env.Undefined();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
Napi::Value SetProperty(const Napi::CallbackInfo& info) { return SetPropertyImpl(info, false); }
|
|
857
|
+
Napi::Value SetPropertyIfChanged(const Napi::CallbackInfo& info) {
|
|
858
|
+
return SetPropertyImpl(info, true);
|
|
813
859
|
}
|
|
814
860
|
|
|
815
861
|
// getTypeName(handle) -> string (the runtime GType name of an instance)
|
package/src/private.cc
CHANGED
|
@@ -109,7 +109,7 @@ static GLogWriterOutput NodeGiLogWriterWrapper(GLogLevelFlags log_level, const G
|
|
|
109
109
|
napi_get_global(env, &global);
|
|
110
110
|
napi_value args[2] = {Napi::Number::New(napiEnv, static_cast<double>(log_level)), obj};
|
|
111
111
|
napi_value ret = nullptr;
|
|
112
|
-
napi_status st =
|
|
112
|
+
napi_status st = NodeGiCallIntoJs(env, global, fn, 2, args, &ret);
|
|
113
113
|
if (st != napi_ok || napiEnv.IsExceptionPending()) {
|
|
114
114
|
// A throwing writer must never wedge the logging path (nor leave a pending
|
|
115
115
|
// exception across the C boundary): surface + clear, then default-write.
|
|
@@ -209,7 +209,7 @@ static gboolean NodeGiBindingTransformInvoke(GBinding* binding, const GValue* fr
|
|
|
209
209
|
napi_value global = nullptr;
|
|
210
210
|
napi_get_global(env, &global);
|
|
211
211
|
napi_value ret = nullptr;
|
|
212
|
-
napi_status st =
|
|
212
|
+
napi_status st = NodeGiCallIntoJs(env, global, fn, 2, args, &ret);
|
|
213
213
|
if (st != napi_ok || napiEnv.IsExceptionPending()) {
|
|
214
214
|
SurfacePendingException(env, "property binding transform");
|
|
215
215
|
return FALSE;
|
package/src/repo.cc
CHANGED
|
@@ -19,6 +19,7 @@ void NodeGiEnvDataFinalize(napi_env env, void* data, void* /*hint*/) {
|
|
|
19
19
|
if (d->microtaskDrain != nullptr) napi_delete_reference(env, d->microtaskDrain);
|
|
20
20
|
if (d->constructCallback != nullptr) napi_delete_reference(env, d->constructCallback);
|
|
21
21
|
if (d->propertySetCallback != nullptr) napi_delete_reference(env, d->propertySetCallback);
|
|
22
|
+
for (auto& kv : d->paramSpecHandles) napi_delete_reference(env, kv.second);
|
|
22
23
|
delete d;
|
|
23
24
|
}
|
|
24
25
|
|
package/src/signals.cc
CHANGED
|
@@ -142,7 +142,7 @@ static void JsClosureMarshalImpl(GClosure* closure, GValue* return_value, guint
|
|
|
142
142
|
napi_value global = nullptr;
|
|
143
143
|
napi_get_global(jc->env, &global);
|
|
144
144
|
g_loopDispatchDepth++;
|
|
145
|
-
st =
|
|
145
|
+
st = NodeGiCallIntoJs(jc->env, global, cbv, args.size(), args.data(), &result);
|
|
146
146
|
g_loopDispatchDepth--;
|
|
147
147
|
}
|
|
148
148
|
if (st != napi_ok) {
|