node-gtk 2.1.0 → 3.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 +52 -152
- package/bin/node-gtk.js +12 -1
- 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/native.js +51 -0
- package/lib/register-class.js +86 -3
- package/lib/styles.d.ts +81 -0
- package/lib/styles.js +428 -0
- package/package.json +15 -1
- package/scripts/windows-bundle-runtime.sh +163 -0
- package/scripts/windows-smoke-test.js +104 -0
- package/src/closure.cc +19 -6
- package/src/closure.h +8 -4
- package/src/function.cc +47 -0
- package/src/gi.cc +10 -0
- package/src/gobject.cc +170 -3
- package/src/gobject.h +3 -0
- 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
package/src/gobject.cc
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
#include <string.h>
|
|
3
3
|
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
4
6
|
#include "boxed.h"
|
|
5
7
|
#include "callback.h"
|
|
6
8
|
#include "closure.h"
|
|
@@ -33,6 +35,11 @@ namespace GNodeJS {
|
|
|
33
35
|
// Our base template for all GObjects
|
|
34
36
|
static Nan::Persistent<FunctionTemplate> baseTemplate;
|
|
35
37
|
|
|
38
|
+
// JS callback (registerClass) invoked to lazily register an unregistered JS
|
|
39
|
+
// subclass the first time it is constructed. Installed via SetLazyClassRegister,
|
|
40
|
+
// this makes registerClass() optional. Empty until JS installs it.
|
|
41
|
+
static Nan::Persistent<Function> lazyClassRegister;
|
|
42
|
+
|
|
36
43
|
|
|
37
44
|
static MaybeLocal<FunctionTemplate> GetClassTemplate(GType gtype);
|
|
38
45
|
static MaybeLocal<Function> GetClass(GType gtype);
|
|
@@ -219,11 +226,34 @@ static void GObjectConstructor(const FunctionCallbackInfo<Value> &info) {
|
|
|
219
226
|
|
|
220
227
|
/* User code calling `new Gtk.Widget({ ... })` */
|
|
221
228
|
|
|
229
|
+
Local<Object> proto = Nan::To<Object>(self->GetPrototype()).ToLocalChecked();
|
|
230
|
+
|
|
231
|
+
/* A JS subclass (`class Foo extends Gtk.Widget {}`) that was never passed to
|
|
232
|
+
* registerClass() owns no GType: `__gtype__` is only *inherited* from its
|
|
233
|
+
* nearest registered ancestor, so constructing it as-is would silently
|
|
234
|
+
* instantiate that ancestor — losing the subtype and any vfunc overrides.
|
|
235
|
+
* Detect the missing *own* property and register the subclass on demand,
|
|
236
|
+
* which is what makes registerClass() optional. The JS callback installs an
|
|
237
|
+
* own `__gtype__` on `proto`, so the lookup below resolves to the
|
|
238
|
+
* freshly-registered subtype. */
|
|
239
|
+
if (!lazyClassRegister.IsEmpty()
|
|
240
|
+
&& !Nan::HasOwnProperty(proto, UTF8("__gtype__")).FromMaybe(true)) {
|
|
241
|
+
Local<Function> registerFn = Nan::New<Function>(lazyClassRegister);
|
|
242
|
+
Local<Value> klass = Nan::Get(proto, UTF8("constructor")).ToLocalChecked();
|
|
243
|
+
Local<Value> argv[] = { klass };
|
|
244
|
+
Nan::TryCatch tryCatch;
|
|
245
|
+
Nan::Call(registerFn, Nan::GetCurrentContext()->Global(), 1, argv);
|
|
246
|
+
if (tryCatch.HasCaught()) {
|
|
247
|
+
tryCatch.ReThrow();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
222
252
|
// FIXME: getting the gtype from the External is faster but doesn't
|
|
223
253
|
// work for dynamically-registered types. Check if we can find something
|
|
224
254
|
// better.
|
|
225
255
|
//gtype = (GType) External::Cast(*info.Data())->Value();
|
|
226
|
-
gtype = GET_OBJECT_GTYPE (
|
|
256
|
+
gtype = GET_OBJECT_GTYPE (proto);
|
|
227
257
|
|
|
228
258
|
gobject = CreateGObjectFromObject (gtype, info[0]);
|
|
229
259
|
|
|
@@ -458,6 +488,61 @@ static void DestroyVFuncs(GType gtype) {
|
|
|
458
488
|
g_type_set_qdata (gtype, GNodeJS::vfuncs_quark(), NULL);
|
|
459
489
|
}
|
|
460
490
|
|
|
491
|
+
/*
|
|
492
|
+
* Signal handlers are stored in a JS array held on the wrapper object itself
|
|
493
|
+
* (via a private symbol), so they are reachable only through the wrapper and
|
|
494
|
+
* the wrapper <-> handler reference loop can be garbage-collected (#375; see
|
|
495
|
+
* doc/signal-handler-gc.md). A Closure keeps only its index into that array.
|
|
496
|
+
*/
|
|
497
|
+
static Local<v8::Private> SignalHandlersKey(v8::Isolate *isolate) {
|
|
498
|
+
return v8::Private::ForApi(isolate, Nan::New("__gnodejs_signal_handlers__").ToLocalChecked());
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Append a handler to the wrapper's handler array, returning its index.
|
|
502
|
+
static guint AddSignalHandler(Local<Object> wrapper, Local<Function> handler) {
|
|
503
|
+
v8::Isolate *isolate = wrapper->GetIsolate();
|
|
504
|
+
Local<v8::Context> context = isolate->GetCurrentContext();
|
|
505
|
+
Local<v8::Private> key = SignalHandlersKey(isolate);
|
|
506
|
+
|
|
507
|
+
Local<Value> existing = wrapper->GetPrivate(context, key).ToLocalChecked();
|
|
508
|
+
Local<Array> handlers;
|
|
509
|
+
if (existing->IsArray()) {
|
|
510
|
+
handlers = existing.As<Array>();
|
|
511
|
+
} else {
|
|
512
|
+
handlers = Nan::New<Array>();
|
|
513
|
+
wrapper->SetPrivate(context, key, handlers).Check();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
guint index = handlers->Length();
|
|
517
|
+
Nan::Set(handlers, index, handler);
|
|
518
|
+
return index;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Look up a handler by the instance it is connected to and its index. Returns
|
|
522
|
+
// an empty handle if the wrapper has been collected or the slot is empty.
|
|
523
|
+
Local<Value> GetSignalHandler(GObject *gobject, guint index) {
|
|
524
|
+
void *data = g_object_get_qdata (gobject, GNodeJS::object_quark());
|
|
525
|
+
if (data == NULL)
|
|
526
|
+
return Local<Value>();
|
|
527
|
+
|
|
528
|
+
auto *wrapper = (GObjectWrapper *) data;
|
|
529
|
+
if (wrapper->collected)
|
|
530
|
+
return Local<Value>();
|
|
531
|
+
|
|
532
|
+
Local<Object> object = Nan::New(wrapper->persistent);
|
|
533
|
+
if (object.IsEmpty())
|
|
534
|
+
return Local<Value>();
|
|
535
|
+
|
|
536
|
+
v8::Isolate *isolate = object->GetIsolate();
|
|
537
|
+
Local<v8::Context> context = isolate->GetCurrentContext();
|
|
538
|
+
Local<Value> handlers =
|
|
539
|
+
object->GetPrivate(context, SignalHandlersKey(isolate)).ToLocalChecked();
|
|
540
|
+
if (!handlers->IsArray())
|
|
541
|
+
return Local<Value>();
|
|
542
|
+
|
|
543
|
+
return Nan::Get(handlers.As<Array>(), index).ToLocalChecked();
|
|
544
|
+
}
|
|
545
|
+
|
|
461
546
|
NAN_METHOD(SignalConnect) {
|
|
462
547
|
bool after = false;
|
|
463
548
|
|
|
@@ -490,9 +575,14 @@ NAN_METHOD(SignalConnect) {
|
|
|
490
575
|
guint signalId;
|
|
491
576
|
GQuark detail;
|
|
492
577
|
GClosure *gclosure;
|
|
578
|
+
guint handlerIndex;
|
|
493
579
|
gulong handler_id;
|
|
494
580
|
|
|
495
|
-
|
|
581
|
+
// Hold the Utf8String for the whole function: `*Nan::Utf8String(...)` alone
|
|
582
|
+
// dangles after the statement, and AddSignalHandler() below allocates in V8,
|
|
583
|
+
// which would clobber the freed buffer before g_signal_connect_closure.
|
|
584
|
+
Nan::Utf8String signalNameValue (TO_STRING (info[0]));
|
|
585
|
+
const char *signalName = *signalNameValue;
|
|
496
586
|
if (!g_signal_parse_name(signalName, gtype, &signalId, &detail, FALSE)) {
|
|
497
587
|
Nan::ThrowTypeError("Signal name is invalid");
|
|
498
588
|
return;
|
|
@@ -506,7 +596,8 @@ NAN_METHOD(SignalConnect) {
|
|
|
506
596
|
}
|
|
507
597
|
}
|
|
508
598
|
|
|
509
|
-
|
|
599
|
+
handlerIndex = AddSignalHandler (TO_OBJECT (info.This ()), callback);
|
|
600
|
+
gclosure = Closure::New (handlerIndex, signal_info, signalId);
|
|
510
601
|
handler_id = g_signal_connect_closure (gobject, signalName, gclosure, after);
|
|
511
602
|
|
|
512
603
|
info.GetReturnValue().Set((double)handler_id);
|
|
@@ -964,6 +1055,10 @@ out:
|
|
|
964
1055
|
}
|
|
965
1056
|
|
|
966
1057
|
|
|
1058
|
+
NAN_METHOD(SetLazyClassRegister) {
|
|
1059
|
+
lazyClassRegister.Reset(info[0].As<Function>());
|
|
1060
|
+
}
|
|
1061
|
+
|
|
967
1062
|
NAN_METHOD(RegisterClass) {
|
|
968
1063
|
auto jsKlassName = Nan::To<String>(info[0]).ToLocalChecked();
|
|
969
1064
|
auto jsKlass = info[1].As<Object>();
|
|
@@ -1034,6 +1129,78 @@ NAN_METHOD(RegisterVFunc) {
|
|
|
1034
1129
|
return;
|
|
1035
1130
|
}
|
|
1036
1131
|
|
|
1132
|
+
/*
|
|
1133
|
+
* Invoke a parent class's implementation of a vfunc — the native half of
|
|
1134
|
+
* `super.<vfunc>(...)`. JS args: (vfuncInfo, implementorGType, instance, argsArray).
|
|
1135
|
+
*
|
|
1136
|
+
* `g_vfunc_info_invoke` resolves the vfunc through `implementorGType`'s class
|
|
1137
|
+
* vtable, so passing the *parent* GType runs the parent's implementation rather
|
|
1138
|
+
* than the overriding subclass's (which is what `super` means). The instance is
|
|
1139
|
+
* passed as in-arg 0, the JS args follow.
|
|
1140
|
+
*
|
|
1141
|
+
* Scope: in-only arguments + (void or simple) return value. Out/inout arguments
|
|
1142
|
+
* are rejected rather than silently mishandled.
|
|
1143
|
+
*/
|
|
1144
|
+
NAN_METHOD(CallVFunc) {
|
|
1145
|
+
auto jsVFuncInfo = info[0].As<Object>();
|
|
1146
|
+
auto jsImplGType = info[1].As<BigInt>();
|
|
1147
|
+
auto jsInstance = info[2];
|
|
1148
|
+
auto jsArgs = info[3].As<Array>();
|
|
1149
|
+
|
|
1150
|
+
BaseInfo vfuncInfo(jsVFuncInfo);
|
|
1151
|
+
GType implementor = jsImplGType->Uint64Value();
|
|
1152
|
+
|
|
1153
|
+
int n_callable = g_callable_info_get_n_args(*vfuncInfo);
|
|
1154
|
+
|
|
1155
|
+
GObject *instance = GObjectFromWrapper(jsInstance);
|
|
1156
|
+
if (instance == NULL) {
|
|
1157
|
+
// The wrapper has no associated GObject yet — e.g. chaining up to a
|
|
1158
|
+
// construction-time vfunc (`constructed`), which fires inside g_object_new
|
|
1159
|
+
// before node-gtk associates the JS wrapper with the GObject. There is no
|
|
1160
|
+
// valid instance to invoke the parent on; fail loudly instead of crashing.
|
|
1161
|
+
Throw::Error("Cannot chain up to parent vfunc '%s': instance has no GObject yet "
|
|
1162
|
+
"(chaining up during construction is unsupported)",
|
|
1163
|
+
g_base_info_get_name(*vfuncInfo));
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
std::vector<GIArgument> in_args(n_callable + 1);
|
|
1168
|
+
in_args[0].v_pointer = instance;
|
|
1169
|
+
|
|
1170
|
+
for (int i = 0; i < n_callable; i++) {
|
|
1171
|
+
GIArgInfo arg_info;
|
|
1172
|
+
GITypeInfo arg_type;
|
|
1173
|
+
g_callable_info_load_arg(*vfuncInfo, i, &arg_info);
|
|
1174
|
+
g_arg_info_load_type(&arg_info, &arg_type);
|
|
1175
|
+
|
|
1176
|
+
if (g_arg_info_get_direction(&arg_info) != GI_DIRECTION_IN) {
|
|
1177
|
+
Throw::Error("Cannot chain up to parent vfunc '%s': out/inout argument %d is unsupported",
|
|
1178
|
+
g_base_info_get_name(*vfuncInfo), i);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
Local<Value> value = Nan::Get(jsArgs, i).ToLocalChecked();
|
|
1183
|
+
bool may_be_null = g_arg_info_may_be_null(&arg_info);
|
|
1184
|
+
V8ToGIArgument(&arg_type, &in_args[i + 1], value, may_be_null);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
GIArgument return_value = {};
|
|
1188
|
+
GError *error = NULL;
|
|
1189
|
+
gboolean ok = g_vfunc_info_invoke(*vfuncInfo, implementor,
|
|
1190
|
+
in_args.data(), n_callable + 1, NULL, 0, &return_value, &error);
|
|
1191
|
+
|
|
1192
|
+
if (!ok) {
|
|
1193
|
+
Throw::GError("Failed to chain up to parent vfunc", error);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
GITypeInfo return_type;
|
|
1198
|
+
g_callable_info_load_return_type(*vfuncInfo, &return_type);
|
|
1199
|
+
if (g_type_info_get_tag(&return_type) != GI_TYPE_TAG_VOID) {
|
|
1200
|
+
info.GetReturnValue().Set(GIArgumentToV8(&return_type, &return_value));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1037
1204
|
};
|
|
1038
1205
|
|
|
1039
1206
|
};
|
package/src/gobject.h
CHANGED
|
@@ -18,14 +18,17 @@ namespace GNodeJS {
|
|
|
18
18
|
MaybeLocal<Function> MakeClass (GIBaseInfo *info);
|
|
19
19
|
Local<Value> WrapperFromGObject (GObject *object);
|
|
20
20
|
GObject * GObjectFromWrapper (Local<Value> value);
|
|
21
|
+
Local<Value> GetSignalHandler (GObject *gobject, guint index);
|
|
21
22
|
Local<FunctionTemplate> GetBaseClassTemplate ();
|
|
22
23
|
MaybeLocal<Value> GetGObjectProperty (GObject * gobject, const char *prop_name);
|
|
23
24
|
MaybeLocal<v8::Boolean> SetGObjectProperty (GObject * gobject, const char *prop_name, Local<Value> value);
|
|
24
25
|
|
|
25
26
|
namespace ObjectClass {
|
|
26
27
|
|
|
28
|
+
NAN_METHOD(SetLazyClassRegister);
|
|
27
29
|
NAN_METHOD(RegisterClass);
|
|
28
30
|
NAN_METHOD(RegisterVFunc);
|
|
31
|
+
NAN_METHOD(CallVFunc);
|
|
29
32
|
|
|
30
33
|
};
|
|
31
34
|
|
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 }
|