node-gtk 2.2.0 → 4.0.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/README.md +45 -161
- package/bin/node-gtk.js +12 -1
- package/binding.gyp +21 -0
- package/lib/binding/node-v127-linux-x64/node_gtk.node +0 -0
- package/lib/bootstrap.js +43 -11
- package/lib/esm/hooks.mjs +49 -0
- package/lib/esm/register.mjs +17 -0
- package/lib/index.js +1 -2
- package/lib/index.mjs +25 -0
- package/lib/inspect.js +1 -1
- package/lib/loop.js +5 -0
- package/lib/module.js +8 -2
- package/lib/overrides/Gtk-4.0.js +6 -27
- package/lib/register-class.js +86 -3
- package/lib/styles.d.ts +81 -0
- package/lib/styles.js +428 -0
- package/package.json +15 -2
- package/src/boxed.cc +13 -5
- package/src/closure.cc +19 -6
- package/src/closure.h +8 -4
- package/src/function.cc +59 -5
- package/src/fundamental.cc +451 -0
- package/src/fundamental.h +71 -0
- package/src/gi.cc +21 -1
- package/src/gobject.cc +268 -9
- package/src/gobject.h +5 -0
- package/src/modules/cairo/context.cc +103 -103
- package/src/modules/cairo/font-extents.cc +6 -2
- package/src/modules/cairo/generator.js +1 -1
- package/src/modules/cairo/glyph.cc +6 -2
- package/src/modules/cairo/path.cc +6 -2
- package/src/modules/cairo/rectangle-int.cc +6 -2
- package/src/modules/cairo/rectangle.cc +6 -2
- package/src/modules/cairo/text-cluster.cc +6 -2
- package/src/modules/cairo/text-extents.cc +6 -2
- package/src/modules/system.cc +4 -4
- package/src/util.h +3 -3
- package/src/value.cc +44 -8
- package/src/value.h +2 -2
- package/tools/README.md +52 -2
- package/tools/create-app.js +246 -0
- package/tools/generate-types.js +80 -3
- package/tools/list-libraries.js +125 -0
- package/tools/templates/app/README.md.tmpl +97 -0
- package/tools/templates/app/gitignore.tmpl +10 -0
- package/tools/templates/app/package.json.tmpl +26 -0
- package/tools/templates/app/src/main.ts.tmpl +110 -0
- package/tools/templates/app/src/welcome.ts.tmpl +41 -0
- package/tools/templates/app/style.css.tmpl +19 -0
- package/tools/templates/app/tsconfig.json.tmpl +19 -0
- /package/{COPYING → LICENSE} +0 -0
|
@@ -28,8 +28,12 @@ void FontExtents::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
|
|
|
28
28
|
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
29
29
|
tpl->SetClassName(Nan::New("CairoFontExtents").ToLocalChecked());
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Accessors and indexed handlers must be installed on the instance
|
|
32
|
+
// template, not the prototype: in V8 14 property callbacks expose only
|
|
33
|
+
// HolderV2() (the holder). An accessor on the prototype would hand the
|
|
34
|
+
// callback the prototype object, which has no internal field, and
|
|
35
|
+
// Nan::ObjectWrap::Unwrap would abort. The instance carries the field.
|
|
36
|
+
Local<ObjectTemplate> proto = tpl->InstanceTemplate();
|
|
33
37
|
SetProtoAccessor(proto, UTF8("ascent"), GetAscent, SetAscent, tpl);
|
|
34
38
|
SetProtoAccessor(proto, UTF8("descent"), GetDescent, SetDescent, tpl);
|
|
35
39
|
SetProtoAccessor(proto, UTF8("height"), GetHeight, SetHeight, tpl);
|
|
@@ -133,7 +133,7 @@ function getSource(fn) {
|
|
|
133
133
|
})() : '' }
|
|
134
134
|
NAN_METHOD(${getJSName(fn.name)}) {
|
|
135
135
|
auto self = info.This();
|
|
136
|
-
auto ${selfArgument.name} = (${getTypeName(selfArgument.type)}) self
|
|
136
|
+
auto ${selfArgument.name} = (${getTypeName(selfArgument.type)}) Nan::GetInternalFieldPointer(self, 0);
|
|
137
137
|
${inArguments.length > 0 ? `
|
|
138
138
|
// in-arguments
|
|
139
139
|
${inArguments.map(getInArgumentSource).join('\n ')}
|
|
@@ -28,8 +28,12 @@ void Glyph::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
|
|
|
28
28
|
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
29
29
|
tpl->SetClassName(Nan::New("CairoGlyph").ToLocalChecked());
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Accessors and indexed handlers must be installed on the instance
|
|
32
|
+
// template, not the prototype: in V8 14 property callbacks expose only
|
|
33
|
+
// HolderV2() (the holder). An accessor on the prototype would hand the
|
|
34
|
+
// callback the prototype object, which has no internal field, and
|
|
35
|
+
// Nan::ObjectWrap::Unwrap would abort. The instance carries the field.
|
|
36
|
+
Local<ObjectTemplate> proto = tpl->InstanceTemplate();
|
|
33
37
|
SetProtoAccessor(proto, UTF8("length"), GetLength, NULL, tpl);
|
|
34
38
|
Nan::SetIndexedPropertyHandler(proto, IndexGetter);
|
|
35
39
|
|
|
@@ -28,8 +28,12 @@ void Path::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
|
|
|
28
28
|
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
29
29
|
tpl->SetClassName(Nan::New("CairoPath").ToLocalChecked());
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Accessors and indexed handlers must be installed on the instance
|
|
32
|
+
// template, not the prototype: in V8 14 property callbacks expose only
|
|
33
|
+
// HolderV2() (the holder). An accessor on the prototype would hand the
|
|
34
|
+
// callback the prototype object, which has no internal field, and
|
|
35
|
+
// Nan::ObjectWrap::Unwrap would abort. The instance carries the field.
|
|
36
|
+
Local<ObjectTemplate> proto = tpl->InstanceTemplate();
|
|
33
37
|
SetProtoAccessor(proto, UTF8("status"), GetStatus, NULL, tpl);
|
|
34
38
|
|
|
35
39
|
auto ctor = Nan::GetFunction (tpl).ToLocalChecked();
|
|
@@ -28,8 +28,12 @@ void RectangleInt::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
|
|
|
28
28
|
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
29
29
|
tpl->SetClassName(Nan::New("CairoRectangleInt").ToLocalChecked());
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Accessors and indexed handlers must be installed on the instance
|
|
32
|
+
// template, not the prototype: in V8 14 property callbacks expose only
|
|
33
|
+
// HolderV2() (the holder). An accessor on the prototype would hand the
|
|
34
|
+
// callback the prototype object, which has no internal field, and
|
|
35
|
+
// Nan::ObjectWrap::Unwrap would abort. The instance carries the field.
|
|
36
|
+
Local<ObjectTemplate> proto = tpl->InstanceTemplate();
|
|
33
37
|
SetProtoAccessor(proto, UTF8("x"), GetX, SetX, tpl);
|
|
34
38
|
SetProtoAccessor(proto, UTF8("y"), GetY, SetY, tpl);
|
|
35
39
|
SetProtoAccessor(proto, UTF8("width"), GetWidth, SetWidth, tpl);
|
|
@@ -28,8 +28,12 @@ void Rectangle::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
|
|
|
28
28
|
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
29
29
|
tpl->SetClassName(Nan::New("CairoRectangle").ToLocalChecked());
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Accessors and indexed handlers must be installed on the instance
|
|
32
|
+
// template, not the prototype: in V8 14 property callbacks expose only
|
|
33
|
+
// HolderV2() (the holder). An accessor on the prototype would hand the
|
|
34
|
+
// callback the prototype object, which has no internal field, and
|
|
35
|
+
// Nan::ObjectWrap::Unwrap would abort. The instance carries the field.
|
|
36
|
+
Local<ObjectTemplate> proto = tpl->InstanceTemplate();
|
|
33
37
|
SetProtoAccessor(proto, UTF8("x"), GetX, SetX, tpl);
|
|
34
38
|
SetProtoAccessor(proto, UTF8("y"), GetY, SetY, tpl);
|
|
35
39
|
SetProtoAccessor(proto, UTF8("width"), GetWidth, SetWidth, tpl);
|
|
@@ -28,8 +28,12 @@ void TextCluster::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
|
|
|
28
28
|
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
29
29
|
tpl->SetClassName(Nan::New("CairoTextCluster").ToLocalChecked());
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Accessors and indexed handlers must be installed on the instance
|
|
32
|
+
// template, not the prototype: in V8 14 property callbacks expose only
|
|
33
|
+
// HolderV2() (the holder). An accessor on the prototype would hand the
|
|
34
|
+
// callback the prototype object, which has no internal field, and
|
|
35
|
+
// Nan::ObjectWrap::Unwrap would abort. The instance carries the field.
|
|
36
|
+
Local<ObjectTemplate> proto = tpl->InstanceTemplate();
|
|
33
37
|
SetProtoAccessor(proto, UTF8("length"), GetLength, NULL, tpl);
|
|
34
38
|
SetProtoAccessor(proto, UTF8("flags"), GetFlags, NULL, tpl);
|
|
35
39
|
Nan::SetIndexedPropertyHandler(proto, IndexGetter);
|
|
@@ -28,8 +28,12 @@ void TextExtents::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
|
|
|
28
28
|
tpl->InstanceTemplate()->SetInternalFieldCount(1);
|
|
29
29
|
tpl->SetClassName(Nan::New("CairoTextExtents").ToLocalChecked());
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Accessors and indexed handlers must be installed on the instance
|
|
32
|
+
// template, not the prototype: in V8 14 property callbacks expose only
|
|
33
|
+
// HolderV2() (the holder). An accessor on the prototype would hand the
|
|
34
|
+
// callback the prototype object, which has no internal field, and
|
|
35
|
+
// Nan::ObjectWrap::Unwrap would abort. The instance carries the field.
|
|
36
|
+
Local<ObjectTemplate> proto = tpl->InstanceTemplate();
|
|
33
37
|
SetProtoAccessor(proto, UTF8("xBearing"), GetXBearing, SetXBearing, tpl);
|
|
34
38
|
SetProtoAccessor(proto, UTF8("yBearing"), GetYBearing, SetYBearing, tpl);
|
|
35
39
|
SetProtoAccessor(proto, UTF8("width"), GetWidth, SetWidth, tpl);
|
package/src/modules/system.cc
CHANGED
|
@@ -24,7 +24,7 @@ namespace System {
|
|
|
24
24
|
static gsize GetObjectSize (Local<Object> object) {
|
|
25
25
|
// Boxed
|
|
26
26
|
if (object->InternalFieldCount() == 2) {
|
|
27
|
-
auto box = static_cast<Boxed*>(object
|
|
27
|
+
auto box = static_cast<Boxed*>(Nan::GetInternalFieldPointer(object, 1));
|
|
28
28
|
return GetComplexTypeSize(box->info);
|
|
29
29
|
}
|
|
30
30
|
// GObject
|
|
@@ -38,7 +38,7 @@ static gsize GetObjectSize (Local<Object> object) {
|
|
|
38
38
|
|
|
39
39
|
NAN_METHOD(AddressOf) {
|
|
40
40
|
Local<Object> object = info[0].As<Object>();
|
|
41
|
-
void *pointer = object
|
|
41
|
+
void *pointer = Nan::GetInternalFieldPointer(object, 0);
|
|
42
42
|
RETURN(Nan::New<Number>((uint64_t)pointer));
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -64,7 +64,7 @@ NAN_METHOD(ConvertGValue) {
|
|
|
64
64
|
RETURN(Nan::Undefined());
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
void *ptr = obj
|
|
67
|
+
void *ptr = Nan::GetInternalFieldPointer(obj, 0);
|
|
68
68
|
ResourceOwnership ownership = kCopy;
|
|
69
69
|
RETURN(GValueToV8(reinterpret_cast<GValue *>(ptr), ownership));
|
|
70
70
|
}
|
|
@@ -79,7 +79,7 @@ NAN_METHOD(GetMemoryContent) {
|
|
|
79
79
|
}
|
|
80
80
|
else {
|
|
81
81
|
auto object = info[0].As<Object>();
|
|
82
|
-
address = (uint8_t *) object
|
|
82
|
+
address = (uint8_t *) Nan::GetInternalFieldPointer(object, 0);
|
|
83
83
|
size = GetObjectSize(object);
|
|
84
84
|
}
|
|
85
85
|
|
package/src/util.h
CHANGED
|
@@ -48,14 +48,14 @@ inline void SetProtoAccessor(
|
|
|
48
48
|
Nan::SetterCallback setter,
|
|
49
49
|
v8::Local<v8::FunctionTemplate> ctor
|
|
50
50
|
) {
|
|
51
|
+
// Trailing settings/attribute args are omitted so Nan supplies the right
|
|
52
|
+
// AccessControl default per V8 version (v8::DEFAULT was removed in V8 14).
|
|
51
53
|
Nan::SetAccessor(
|
|
52
54
|
tpl,
|
|
53
55
|
name,
|
|
54
56
|
getter,
|
|
55
57
|
setter,
|
|
56
|
-
v8::Local<v8::Value>()
|
|
57
|
-
v8::DEFAULT,
|
|
58
|
-
v8::None);
|
|
58
|
+
v8::Local<v8::Value>());
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
namespace Util
|
package/src/value.cc
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
#include "error.h"
|
|
8
8
|
#include "boxed.h"
|
|
9
|
+
#include "fundamental.h"
|
|
9
10
|
#include "function.h"
|
|
10
11
|
#include "gi.h"
|
|
11
12
|
#include "gobject.h"
|
|
@@ -51,7 +52,7 @@ static guint64 V8ToUint64 (Local<Value> value) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
Local<Value> GIArgumentToV8(GITypeInfo *type_info, GIArgument *arg, long length, ResourceOwnership ownership) {
|
|
55
|
+
Local<Value> GIArgumentToV8(GITypeInfo *type_info, GIArgument *arg, long length, ResourceOwnership ownership, bool nullable) {
|
|
55
56
|
GITypeTag type_tag = g_type_info_get_tag (type_info);
|
|
56
57
|
|
|
57
58
|
switch (type_tag) {
|
|
@@ -120,6 +121,11 @@ Local<Value> GIArgumentToV8(GITypeInfo *type_info, GIArgument *arg, long length,
|
|
|
120
121
|
case GI_TYPE_TAG_UTF8: {
|
|
121
122
|
if (arg->v_string)
|
|
122
123
|
return New<String>(arg->v_string).ToLocalChecked();
|
|
124
|
+
else if (nullable)
|
|
125
|
+
// A nullable string (e.g. g_data_input_stream_read_line_utf8_finish
|
|
126
|
+
// at EOF) returns NULL: surface it as `null` so it is
|
|
127
|
+
// distinguishable from a legitimately empty string. (#467)
|
|
128
|
+
return Nan::Null();
|
|
123
129
|
else
|
|
124
130
|
return Nan::EmptyString();
|
|
125
131
|
}
|
|
@@ -136,7 +142,9 @@ Local<Value> GIArgumentToV8(GITypeInfo *type_info, GIArgument *arg, long length,
|
|
|
136
142
|
* of a GObject, instead this represent the object type (eg class). A GObject
|
|
137
143
|
* has methods, fields, properties, signals, interfaces, constants and virtual functions. */
|
|
138
144
|
case GI_INFO_TYPE_OBJECT:
|
|
139
|
-
if (
|
|
145
|
+
if (IsFundamentalObjectInfo(interface_info))
|
|
146
|
+
value = WrapperFromFundamental(interface_info, arg->v_pointer, ownership);
|
|
147
|
+
else if (G_IS_PARAM_SPEC(arg->v_pointer))
|
|
140
148
|
value = ParamSpec::FromGParamSpec((GParamSpec *)arg->v_pointer);
|
|
141
149
|
else
|
|
142
150
|
value = WrapperFromGObject((GObject *)arg->v_pointer);
|
|
@@ -144,7 +152,12 @@ Local<Value> GIArgumentToV8(GITypeInfo *type_info, GIArgument *arg, long length,
|
|
|
144
152
|
case GI_INFO_TYPE_BOXED:
|
|
145
153
|
case GI_INFO_TYPE_STRUCT:
|
|
146
154
|
case GI_INFO_TYPE_UNION:
|
|
147
|
-
|
|
155
|
+
// GVariant is a struct-classed fundamental: refcount it via the
|
|
156
|
+
// fundamental wrapper rather than g_boxed_copy (#465).
|
|
157
|
+
if (IsVariantInfo (interface_info))
|
|
158
|
+
value = WrapperFromVariant (arg->v_pointer, ownership);
|
|
159
|
+
else
|
|
160
|
+
value = WrapperFromBoxed (interface_info, arg->v_pointer, ownership);
|
|
148
161
|
break;
|
|
149
162
|
case GI_INFO_TYPE_ENUM:
|
|
150
163
|
value = New<Number>(arg->v_int);
|
|
@@ -166,7 +179,7 @@ Local<Value> GIArgumentToV8(GITypeInfo *type_info, GIArgument *arg, long length,
|
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
case GI_TYPE_TAG_ARRAY:
|
|
169
|
-
return ArrayToV8(type_info, arg->v_pointer, length);
|
|
182
|
+
return ArrayToV8(type_info, arg->v_pointer, length, nullable);
|
|
170
183
|
|
|
171
184
|
case GI_TYPE_TAG_GLIST:
|
|
172
185
|
return GListToV8(type_info, (GList *)arg->v_pointer);
|
|
@@ -323,7 +336,13 @@ static bool IsZeroMemory (const void *ptr, gsize size) {
|
|
|
323
336
|
return true;
|
|
324
337
|
}
|
|
325
338
|
|
|
326
|
-
Local<Value> ArrayToV8 (GITypeInfo *type_info, void* data, long length) {
|
|
339
|
+
Local<Value> ArrayToV8 (GITypeInfo *type_info, void* data, long length, bool nullable) {
|
|
340
|
+
|
|
341
|
+
// A nullable array return (e.g. g_data_input_stream_read_line_finish at
|
|
342
|
+
// EOF) yields a NULL pointer: surface it as `null` so it is
|
|
343
|
+
// distinguishable from a legitimately empty array. (#467)
|
|
344
|
+
if (data == nullptr && nullable)
|
|
345
|
+
return Nan::Null();
|
|
327
346
|
|
|
328
347
|
auto array = New<Array>();
|
|
329
348
|
|
|
@@ -833,6 +852,15 @@ bool V8ToGIArgumentInterface(GIBaseInfo *gi_info, GIArgument *arg, Local<Value>
|
|
|
833
852
|
arg->v_pointer = ParamSpec::FromWrapper(value);
|
|
834
853
|
break;
|
|
835
854
|
}
|
|
855
|
+
|
|
856
|
+
if (IsFundamentalObjectInfo(gi_info)) {
|
|
857
|
+
// A fundamental (non-GObject) instance such as GskRenderNode: take
|
|
858
|
+
// the raw wrapped pointer. GObjectFromWrapper would G_OBJECT()-cast
|
|
859
|
+
// it — silently on Linux (cast checks compiled out) but a fatal
|
|
860
|
+
// `invalid cast ... to GObject` under G_DEBUG on Windows (#468).
|
|
861
|
+
arg->v_pointer = PointerFromWrapper(value);
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
836
864
|
// fallthrough
|
|
837
865
|
}
|
|
838
866
|
case GI_INFO_TYPE_INTERFACE:
|
|
@@ -1726,7 +1754,7 @@ bool CanConvertV8ToGValue(GValue *gvalue, Local<Value> value) {
|
|
|
1726
1754
|
} else if (G_VALUE_HOLDS_POINTER (gvalue)) {
|
|
1727
1755
|
return false;
|
|
1728
1756
|
} else if (G_VALUE_HOLDS_VARIANT (gvalue)) {
|
|
1729
|
-
return
|
|
1757
|
+
return value->IsNullOrUndefined() || ValueIsInstanceOfGType(value, G_TYPE_VARIANT);
|
|
1730
1758
|
}
|
|
1731
1759
|
|
|
1732
1760
|
ERROR("Unhandled GValue type: %s (please report this)",
|
|
@@ -1819,7 +1847,13 @@ bool V8ToGValue(GValue *gvalue, Local<Value> value, ResourceOwnership ownership)
|
|
|
1819
1847
|
} else if (G_VALUE_HOLDS_POINTER (gvalue)) {
|
|
1820
1848
|
ERROR("Unsupported type: pointer");
|
|
1821
1849
|
} else if (G_VALUE_HOLDS_VARIANT (gvalue)) {
|
|
1822
|
-
|
|
1850
|
+
// GVariant is nullable. g_value_set_variant refs a non-NULL variant
|
|
1851
|
+
// (and sinks a floating one), so the GValue holds its own reference
|
|
1852
|
+
// independent of the JS wrapper (#465).
|
|
1853
|
+
g_value_set_variant (gvalue,
|
|
1854
|
+
value->IsNullOrUndefined()
|
|
1855
|
+
? NULL
|
|
1856
|
+
: (GVariant *) PointerFromWrapper (value));
|
|
1823
1857
|
} else {
|
|
1824
1858
|
ERROR("Unhandled GValue type: %s (please report this)",
|
|
1825
1859
|
g_type_name(G_VALUE_TYPE(gvalue)));
|
|
@@ -1896,7 +1930,9 @@ Local<Value> GValueToV8(const GValue *gvalue, ResourceOwnership ownership) {
|
|
|
1896
1930
|
} else if (G_VALUE_HOLDS_POINTER (gvalue)) {
|
|
1897
1931
|
ERROR("Unsuported type: pointer");
|
|
1898
1932
|
} else if (G_VALUE_HOLDS_VARIANT (gvalue)) {
|
|
1899
|
-
|
|
1933
|
+
// GVariant is a struct-classed fundamental; the fundamental wrapper
|
|
1934
|
+
// takes its own reference according to `ownership` (#465).
|
|
1935
|
+
return WrapperFromVariant (g_value_get_variant (gvalue), ownership);
|
|
1900
1936
|
} else {
|
|
1901
1937
|
// Don't abort the whole process on a GValue type we can't convert
|
|
1902
1938
|
// (e.g. GStreamer's GstValueArray / GstValueList, #389). Warn and
|
package/src/value.h
CHANGED
|
@@ -20,9 +20,9 @@ enum ResourceOwnership {
|
|
|
20
20
|
Local<Value> GListToV8 (GITypeInfo *info, GList *glist);
|
|
21
21
|
Local<Value> GSListToV8 (GITypeInfo *info, GSList *glist);
|
|
22
22
|
Local<Value> GHashToV8 (GITypeInfo *info, GHashTable *hash);
|
|
23
|
-
Local<Value> ArrayToV8 (GITypeInfo *info, gpointer data, long length = -1);
|
|
23
|
+
Local<Value> ArrayToV8 (GITypeInfo *info, gpointer data, long length = -1, bool nullable = false);
|
|
24
24
|
Local<Value> GErrorToV8 (GITypeInfo *type_info, GError *err, ResourceOwnership ownership = kNone);
|
|
25
|
-
Local<Value> GIArgumentToV8 (GITypeInfo *type_info, GIArgument *argument, long length = -1, ResourceOwnership ownership = kNone);
|
|
25
|
+
Local<Value> GIArgumentToV8 (GITypeInfo *type_info, GIArgument *argument, long length = -1, ResourceOwnership ownership = kNone, bool nullable = false);
|
|
26
26
|
long GIArgumentToLength(GITypeInfo *type_info, GIArgument *arg, bool is_pointer);
|
|
27
27
|
|
|
28
28
|
bool V8ToGIArgumentInterface (GIBaseInfo *gi_info, GIArgument *argument, Local<Value> value);
|
package/tools/README.md
CHANGED
|
@@ -45,7 +45,7 @@ script so it regenerates after install.
|
|
|
45
45
|
- `bin/node-gtk.js` — CLI entry (`package.json` `"bin"`); dispatches `generate-types`.
|
|
46
46
|
- `tools/generate-types.js` — the generator. `run(argv)` / `generate(roots, outdir)`.
|
|
47
47
|
- `examples/ts-demo/` — `app.ts` (valid, typechecks clean) and `app-errors.ts`
|
|
48
|
-
(
|
|
48
|
+
(5 deliberate mistakes, all caught). Generate types into `.node-gtk-types/` first
|
|
49
49
|
(see that dir's `.gitignore`).
|
|
50
50
|
|
|
51
51
|
## Verify the demo
|
|
@@ -54,7 +54,7 @@ script so it regenerates after install.
|
|
|
54
54
|
node bin/node-gtk.js generate-types Gtk-4.0 --outdir examples/ts-demo/.node-gtk-types
|
|
55
55
|
node_modules/.bin/tsc -p examples/ts-demo/tsconfig.json # passes clean
|
|
56
56
|
sed 's/app.ts/app-errors.ts/' examples/ts-demo/tsconfig.json > examples/ts-demo/tsconfig.errors.json
|
|
57
|
-
node_modules/.bin/tsc -p examples/ts-demo/tsconfig.errors.json #
|
|
57
|
+
node_modules/.bin/tsc -p examples/ts-demo/tsconfig.errors.json # 5 errors caught
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Fidelity
|
|
@@ -68,6 +68,11 @@ type-check with **0 errors even without `skipLibCheck`**. Modelled faithfully:
|
|
|
68
68
|
- 64-bit ints return `bigint` (full precision, #323/#149); params accept
|
|
69
69
|
`number | bigint`.
|
|
70
70
|
- Enum methods and interface constants emitted (declaration-merged).
|
|
71
|
+
- Virtual functions emitted as the `virtual_*` override surface that
|
|
72
|
+
`registerClass` wires into the vtable (`virtual_sizeAllocate` overrides
|
|
73
|
+
`size_allocate`), including invoker-less lifecycle vfuncs (`virtual_dispose`,
|
|
74
|
+
`virtual_constructed`, …) so subclass overrides are type-checked and
|
|
75
|
+
`super.virtual_<name>()` chain-up resolves (issue #457).
|
|
71
76
|
- GObject override conflicts reconciled as overloads, so subclass methods stay
|
|
72
77
|
assignable to inherited ones; multiple-interface signal/method conflicts
|
|
73
78
|
resolved with a unified, assignable-to-all declaration.
|
|
@@ -89,3 +94,48 @@ type-check with **0 errors even without `skipLibCheck`**. Modelled faithfully:
|
|
|
89
94
|
(e.g. a gutter renderer's `activate(iter, …)` vs `GtkWidget.activate()`)
|
|
90
95
|
requires the override to satisfy both signatures — an inherent consequence of
|
|
91
96
|
the GObject API reusing a name, not specific to these types.
|
|
97
|
+
- **`virtual_*` overrides with non-primitive OUT params** are typed with those
|
|
98
|
+
params in the return tuple (the public-method convention). At runtime a vfunc
|
|
99
|
+
implementation receives non-primitive OUT params as objects to mutate rather
|
|
100
|
+
than returning them; the common all-primitive case (e.g. `virtual_measure`)
|
|
101
|
+
matches exactly.
|
|
102
|
+
- **Interface vfuncs are not emitted** (only object/class vfuncs). Emitting
|
|
103
|
+
`virtual_*` members on interfaces collides across multiple-interface diamonds
|
|
104
|
+
(e.g. GTK3's Atk accessibility stack → TS2320). Overriding an interface vfunc
|
|
105
|
+
still works at runtime; it just isn't type-checked.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
# `node-gtk create` — create a new app
|
|
110
|
+
|
|
111
|
+
`node-gtk create <directory>` creates a complete, ready-to-run GTK/Adwaita
|
|
112
|
+
application that uses node-gtk, so a new project is one command away.
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
npx node-gtk create my-app
|
|
116
|
+
cd my-app
|
|
117
|
+
npm run dev
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It generates a TypeScript + ESM project: an idiomatic Adwaita application plus its
|
|
121
|
+
tooling — typed `gi:` imports (with `tsconfig` wired to the generated types) and
|
|
122
|
+
npm scripts to run (`dev`/`start`), build (`build`), and regenerate types
|
|
123
|
+
(`generate-types`, also run on `postinstall`).
|
|
124
|
+
|
|
125
|
+
### Options
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
node-gtk create <directory> [options]
|
|
129
|
+
|
|
130
|
+
--name <name> Human-facing app name (default: derived from <directory>)
|
|
131
|
+
--app-id <id> Reverse-DNS application id (default: com.example.<Name>)
|
|
132
|
+
--no-install Don't run `npm install` after creating the project
|
|
133
|
+
--force Create into <directory> even if it exists and is non-empty
|
|
134
|
+
-h, --help Show this help
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The directory basename drives the defaults: `my-cool-app` →
|
|
138
|
+
name *"My Cool App"*, package `my-cool-app`, id `com.example.MyCoolApp`.
|
|
139
|
+
|
|
140
|
+
The command lives in `tools/create-app.js`; the generated files come from the
|
|
141
|
+
template tree in `tools/templates/app/`.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* create-app.js — create a new GTK/Adwaita application that uses node-gtk.
|
|
3
|
+
*
|
|
4
|
+
* Driven by the CLI: `node-gtk create <directory> [options]`.
|
|
5
|
+
* Copies the template tree in tools/templates/app/, substitutes a few tokens
|
|
6
|
+
* (app name, app id, package name, node-gtk version), and — unless --no-install
|
|
7
|
+
* is passed — runs `npm install` in the new directory (which in turn generates
|
|
8
|
+
* the TypeScript types via the project's postinstall script).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs')
|
|
12
|
+
const path = require('path')
|
|
13
|
+
const child_process = require('child_process')
|
|
14
|
+
|
|
15
|
+
const TEMPLATE_DIR = path.join(__dirname, 'templates', 'app')
|
|
16
|
+
|
|
17
|
+
// template file -> destination path (relative to the new project root).
|
|
18
|
+
// Templates carry a `.tmpl` suffix so npm never rewrites `.gitignore` to
|
|
19
|
+
// `.npmignore` and never treats a nested `package.json` as a real manifest.
|
|
20
|
+
const FILES = [
|
|
21
|
+
['package.json.tmpl', 'package.json'],
|
|
22
|
+
['tsconfig.json.tmpl', 'tsconfig.json'],
|
|
23
|
+
['gitignore.tmpl', '.gitignore'],
|
|
24
|
+
['README.md.tmpl', 'README.md'],
|
|
25
|
+
['style.css.tmpl', 'style.css'],
|
|
26
|
+
['src/main.ts.tmpl', path.join('src', 'main.ts')],
|
|
27
|
+
['src/welcome.ts.tmpl', path.join('src', 'welcome.ts')],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// name derivation
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
// A human-facing title: "my-cool-app" / "my_cool_app" -> "My Cool App".
|
|
35
|
+
function toAppName(base) {
|
|
36
|
+
return base
|
|
37
|
+
.replace(/[-_.\s]+/g, ' ')
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/\b\w/g, (c) => c.toUpperCase()) || 'My App'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A valid npm package name: lowercase, url-safe.
|
|
43
|
+
function toPkgName(base) {
|
|
44
|
+
const name = base
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9-_.]+/g, '-')
|
|
47
|
+
.replace(/^[-_.]+|[-_.]+$/g, '')
|
|
48
|
+
return name || 'gtk-app'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A reverse-DNS application id: "My Cool App" -> "com.example.MyCoolApp".
|
|
52
|
+
function toAppId(appName) {
|
|
53
|
+
const suffix = appName
|
|
54
|
+
.replace(/[^A-Za-z0-9 ]+/g, '')
|
|
55
|
+
.split(/\s+/)
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
58
|
+
.join('') || 'App'
|
|
59
|
+
return `com.example.${suffix}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// GApplication ids must look like reverse-DNS: 2+ dot-separated segments, each
|
|
63
|
+
// starting with a letter, containing only [A-Za-z0-9_-] (and no trailing dot).
|
|
64
|
+
function isValidAppId(id) {
|
|
65
|
+
return /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*)+$/.test(id)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The `node-gtk` dependency to write into the generated package.json.
|
|
69
|
+
//
|
|
70
|
+
// The created app uses the `gi:` import scheme and `node-gtk/register`, which
|
|
71
|
+
// exist only in this node-gtk. When run from a normal install we depend on the
|
|
72
|
+
// matching published version (`^x.y.z`). But when run from a *source checkout*
|
|
73
|
+
// (a contributor testing `node bin/node-gtk.js create`), `^x.y.z` would resolve
|
|
74
|
+
// to the published release — which may predate these features — so we instead
|
|
75
|
+
// point the app at the local checkout via `file:`, so it uses the exact node-gtk
|
|
76
|
+
// it was created with.
|
|
77
|
+
function nodeGtkDependency() {
|
|
78
|
+
const repoRoot = path.resolve(__dirname, '..')
|
|
79
|
+
const version = require('../package.json').version
|
|
80
|
+
const isInstalled = repoRoot.split(path.sep).includes('node_modules')
|
|
81
|
+
return isInstalled ? `^${version}` : `file:${repoRoot}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// project creation (pure: writes files, never installs, never exits the process)
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function createProject(opts) {
|
|
89
|
+
const { dir, appName, appId, pkgName, force = false } = opts
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length > 0 && !force)
|
|
92
|
+
throw new Error(`target directory is not empty: ${dir}\nUse --force to write into it anyway.`)
|
|
93
|
+
|
|
94
|
+
const nodeGtkVersion = opts.nodeGtkVersion || nodeGtkDependency()
|
|
95
|
+
const tokens = {
|
|
96
|
+
__APP_NAME__: appName,
|
|
97
|
+
__APP_ID__: appId,
|
|
98
|
+
__PKG_NAME__: pkgName,
|
|
99
|
+
__NODE_GTK_VERSION__: nodeGtkVersion,
|
|
100
|
+
}
|
|
101
|
+
const substitute = (s) =>
|
|
102
|
+
s.replace(/__APP_NAME__|__APP_ID__|__PKG_NAME__|__NODE_GTK_VERSION__/g, (m) => tokens[m])
|
|
103
|
+
|
|
104
|
+
const written = []
|
|
105
|
+
for (const [src, dest] of FILES) {
|
|
106
|
+
const content = substitute(fs.readFileSync(path.join(TEMPLATE_DIR, src), 'utf8'))
|
|
107
|
+
const destPath = path.join(dir, dest)
|
|
108
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true })
|
|
109
|
+
fs.writeFileSync(destPath, content)
|
|
110
|
+
written.push(dest)
|
|
111
|
+
}
|
|
112
|
+
return written
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// CLI entry
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
const HELP = `node-gtk create — create a GTK/Adwaita app that uses node-gtk
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
node-gtk create <directory> [options]
|
|
123
|
+
|
|
124
|
+
Options:
|
|
125
|
+
--name <name> Human-facing app name (default: derived from <directory>)
|
|
126
|
+
--app-id <id> Reverse-DNS application id (default: com.example.<Name>)
|
|
127
|
+
--no-install Don't run \`npm install\` after creating the project
|
|
128
|
+
--force Create into <directory> even if it exists and is non-empty
|
|
129
|
+
-h, --help Show this help
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
node-gtk create my-app --name "My App" --app-id org.example.MyApp
|
|
133
|
+
`
|
|
134
|
+
|
|
135
|
+
function parseArgs(argv) {
|
|
136
|
+
const opts = { install: true, force: false }
|
|
137
|
+
const positional = []
|
|
138
|
+
for (let i = 0; i < argv.length; i++) {
|
|
139
|
+
const arg = argv[i]
|
|
140
|
+
switch (arg) {
|
|
141
|
+
case '-h': case '--help': opts.help = true; break
|
|
142
|
+
case '--no-install': opts.install = false; break
|
|
143
|
+
case '--force': opts.force = true; break
|
|
144
|
+
case '--name': opts.name = argv[++i]; break
|
|
145
|
+
case '--app-id': opts.appId = argv[++i]; break
|
|
146
|
+
default:
|
|
147
|
+
if (arg.startsWith('--name=')) opts.name = arg.slice('--name='.length)
|
|
148
|
+
else if (arg.startsWith('--app-id=')) opts.appId = arg.slice('--app-id='.length)
|
|
149
|
+
else if (arg.startsWith('-')) { opts.unknown = arg }
|
|
150
|
+
else positional.push(arg)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
opts.dir = positional[0]
|
|
154
|
+
return opts
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Minimal ANSI styling (chalk is only a devDependency, so we can't use it at
|
|
158
|
+
// runtime). No-ops when stdout isn't a TTY or NO_COLOR is set.
|
|
159
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
160
|
+
const ansi = (open, close) => (s) => useColor ? `\x1b[${open}m${s}\x1b[${close}m` : s
|
|
161
|
+
const bold = ansi(1, 22)
|
|
162
|
+
const dim = ansi(2, 22)
|
|
163
|
+
const cyan = ansi(36, 39)
|
|
164
|
+
const green = ansi(32, 39)
|
|
165
|
+
const yellow = ansi(33, 39)
|
|
166
|
+
|
|
167
|
+
function run(argv) {
|
|
168
|
+
const opts = parseArgs(argv)
|
|
169
|
+
|
|
170
|
+
if (opts.help) { process.stdout.write(HELP); return }
|
|
171
|
+
if (opts.unknown) { process.stderr.write(`node-gtk create: unknown option '${opts.unknown}'\n\n${HELP}`); process.exit(1) }
|
|
172
|
+
if (!opts.dir) { process.stderr.write(`node-gtk create: missing <directory>\n\n${HELP}`); process.exit(1) }
|
|
173
|
+
|
|
174
|
+
const dir = path.resolve(opts.dir)
|
|
175
|
+
const base = path.basename(dir)
|
|
176
|
+
const appName = opts.name || toAppName(base)
|
|
177
|
+
const pkgName = toPkgName(base)
|
|
178
|
+
const appId = opts.appId || toAppId(appName)
|
|
179
|
+
|
|
180
|
+
if (!isValidAppId(appId)) {
|
|
181
|
+
process.stderr.write(`node-gtk create: invalid --app-id '${appId}'.\n` +
|
|
182
|
+
`It must be reverse-DNS, e.g. com.example.MyApp (2+ segments, each starting with a letter).\n`)
|
|
183
|
+
process.exit(1)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let written
|
|
187
|
+
try {
|
|
188
|
+
written = createProject({ dir, appName, appId, pkgName, force: opts.force })
|
|
189
|
+
} catch (err) {
|
|
190
|
+
process.stderr.write(`node-gtk create: ${err.message}\n`)
|
|
191
|
+
process.exit(1)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Shortest copy-pasteable path to the new project: the relative path unless it
|
|
195
|
+
// escapes the cwd (e.g. `../../tmp/app`), in which case the absolute path reads
|
|
196
|
+
// better.
|
|
197
|
+
const relPath = path.relative(process.cwd(), dir)
|
|
198
|
+
const rel = (!relPath || relPath.startsWith('..')) ? dir : relPath
|
|
199
|
+
|
|
200
|
+
process.stdout.write(`\n${green('✓')} ${bold(`Created ${appName}`)} ${dim(`(${written.length} files) in ${dir}`)}\n`)
|
|
201
|
+
|
|
202
|
+
if (opts.install) {
|
|
203
|
+
process.stdout.write(`${dim('…')} ${bold('Installing dependencies')}${dim(' (npm install)')}\n`)
|
|
204
|
+
// Capture output and surface it only on failure — keep the happy path quiet.
|
|
205
|
+
const res = child_process.spawnSync('npm', ['install'], { cwd: dir, encoding: 'utf8' })
|
|
206
|
+
if (res.status !== 0) {
|
|
207
|
+
const output = `${res.stdout || ''}${res.stderr || ''}`
|
|
208
|
+
// The one expected, recoverable failure: node-gtk itself installed fine, but
|
|
209
|
+
// the postinstall type generation couldn't find the GTK/GI typelibs because
|
|
210
|
+
// the native libraries aren't installed yet. generate-types reports this as
|
|
211
|
+
// "Typelib file for namespace … not found" — treat it as a warning (the
|
|
212
|
+
// project is created and runnable) and hand back the command to finish up.
|
|
213
|
+
if (/Typelib file for namespace/i.test(output)) {
|
|
214
|
+
const finish = `cd ${rel} && npm run generate-types && npm run dev`
|
|
215
|
+
process.stdout.write(
|
|
216
|
+
`\n${yellow(bold('⚠ Project created, but TypeScript types could not be generated.'))}\n` +
|
|
217
|
+
` The native ${bold('GTK 4 / libadwaita')} libraries don't seem to be installed.\n` +
|
|
218
|
+
` ${dim('They power type generation and the app itself.')}\n\n` +
|
|
219
|
+
` ${bold('1.')} Install them for your platform:\n` +
|
|
220
|
+
` ${cyan('https://github.com/romgrk/node-gtk#installing')}\n\n` +
|
|
221
|
+
` ${bold('2.')} Then finish setup:\n` +
|
|
222
|
+
` ${cyan(finish)}\n\n`)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
// Any other failure is unexpected: surface the raw output, the manual
|
|
226
|
+
// recovery steps, and a link to report it.
|
|
227
|
+
if (res.stdout) process.stderr.write(res.stdout)
|
|
228
|
+
if (res.stderr) process.stderr.write(res.stderr)
|
|
229
|
+
process.stderr.write(
|
|
230
|
+
`\nnpm install did not complete cleanly. Your project is created — ` +
|
|
231
|
+
`finish setup manually:\n\n cd ${rel}\n npm install\n npm run dev\n\n` +
|
|
232
|
+
`If this keeps failing, please report it (include the output above):\n` +
|
|
233
|
+
` https://github.com/romgrk/node-gtk/issues\n`)
|
|
234
|
+
process.exit(res.status || 1)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.stdout.write(`${green('✓')} ${bold('Done!')}\n`)
|
|
239
|
+
|
|
240
|
+
const steps = [`cd ${rel}`].concat(opts.install ? [] : ['npm install']).concat(['npm run dev'])
|
|
241
|
+
process.stdout.write(`\n${bold('Next steps:')}\n\n`)
|
|
242
|
+
for (const s of steps) process.stdout.write(` ${cyan(s)}\n`)
|
|
243
|
+
process.stdout.write('\n')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { run, createProject, nodeGtkDependency, toAppName, toPkgName, toAppId, isValidAppId }
|