@gjsify/create-app 0.1.10 → 0.1.12
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/dist-templates/adw-canvas2d/package.json +27 -0
- package/dist-templates/adw-canvas2d/src/draw.ts +27 -0
- package/dist-templates/adw-canvas2d/src/index.ts +19 -0
- package/dist-templates/adw-canvas2d/src/main-window.blp +19 -0
- package/dist-templates/adw-canvas2d/src/main-window.ts +37 -0
- package/dist-templates/adw-canvas2d/tsconfig.json +20 -0
- package/dist-templates/adw-game/package.json +29 -0
- package/dist-templates/adw-game/src/game.ts +25 -0
- package/dist-templates/adw-game/src/index.ts +19 -0
- package/dist-templates/adw-game/src/main-window.blp +19 -0
- package/dist-templates/adw-game/src/main-window.ts +55 -0
- package/dist-templates/adw-game/tsconfig.json +20 -0
- package/dist-templates/adw-webgl/package.json +29 -0
- package/dist-templates/adw-webgl/src/index.ts +19 -0
- package/dist-templates/adw-webgl/src/main-window.blp +19 -0
- package/dist-templates/adw-webgl/src/main-window.ts +37 -0
- package/dist-templates/adw-webgl/src/scene.ts +35 -0
- package/dist-templates/adw-webgl/tsconfig.json +20 -0
- package/{templates → dist-templates/cli}/package.json +6 -2
- package/dist-templates/cli/src/index.ts +37 -0
- package/{templates → dist-templates/cli}/tsconfig.json +1 -1
- package/dist-templates/gtk-minimal/package.json +23 -0
- package/dist-templates/gtk-minimal/src/index.ts +40 -0
- package/dist-templates/gtk-minimal/tsconfig.json +14 -0
- package/dist-templates/web-server-express/package.json +24 -0
- package/dist-templates/web-server-express/src/index.ts +27 -0
- package/dist-templates/web-server-express/tsconfig.json +14 -0
- package/dist-templates/web-server-hono/package.json +24 -0
- package/dist-templates/web-server-hono/src/index.ts +21 -0
- package/dist-templates/web-server-hono/tsconfig.json +14 -0
- package/lib/create.d.ts +14 -1
- package/lib/create.js +91 -24
- package/lib/discover-templates.d.ts +14 -0
- package/lib/discover-templates.js +45 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +42 -4
- package/lib/prompt-template.d.ts +6 -0
- package/lib/prompt-template.js +73 -0
- package/package.json +6 -2
- package/templates/src/index.ts +0 -64
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "new-gjsify-app",
|
|
3
|
+
"description": "Adwaita app with HTML Canvas 2D rendering (Blueprint UI).",
|
|
4
|
+
"version": "0.1.10",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check": "tsc --noEmit",
|
|
9
|
+
"build": "gjsify build src/index.ts --outfile dist/index.js",
|
|
10
|
+
"start": "gjsify run dist/index.js",
|
|
11
|
+
"dev": "gjsify build src/index.ts --outfile dist/index.js && gjsify run dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@girs/adw-1": "^1.10.0-4.0.0-rc.3",
|
|
15
|
+
"@girs/gio-2.0": "^2.88.0-4.0.0-rc.3",
|
|
16
|
+
"@girs/gjs": "^4.0.0-rc.3",
|
|
17
|
+
"@girs/gobject-2.0": "^2.88.0-4.0.0-rc.3",
|
|
18
|
+
"@girs/gtk-4.0": "^4.23.0-4.0.0-rc.3",
|
|
19
|
+
"@gjsify/cli": "^0.1.12",
|
|
20
|
+
"@gjsify/esbuild-plugin-blueprint": "^0.1.12",
|
|
21
|
+
"@types/node": "^25.6.0",
|
|
22
|
+
"typescript": "^6.0.2"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@gjsify/canvas2d": "^0.1.12"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function startAnimation(canvas: HTMLCanvasElement): void {
|
|
2
|
+
const ctx = canvas.getContext('2d');
|
|
3
|
+
if (!ctx) return;
|
|
4
|
+
|
|
5
|
+
let t = 0;
|
|
6
|
+
const loop = () => {
|
|
7
|
+
const { width, height } = canvas;
|
|
8
|
+
ctx.fillStyle = '#1e1e2e';
|
|
9
|
+
ctx.fillRect(0, 0, width, height);
|
|
10
|
+
|
|
11
|
+
const cx = width / 2 + Math.cos(t * 0.02) * (width / 3);
|
|
12
|
+
const cy = height / 2 + Math.sin(t * 0.03) * (height / 3);
|
|
13
|
+
|
|
14
|
+
const grad = ctx.createRadialGradient(cx, cy, 10, cx, cy, 120);
|
|
15
|
+
grad.addColorStop(0, '#f5c2e7');
|
|
16
|
+
grad.addColorStop(1, 'rgba(245, 194, 231, 0)');
|
|
17
|
+
ctx.fillStyle = grad;
|
|
18
|
+
ctx.beginPath();
|
|
19
|
+
ctx.arc(cx, cy, 120, 0, Math.PI * 2);
|
|
20
|
+
ctx.fill();
|
|
21
|
+
|
|
22
|
+
t++;
|
|
23
|
+
requestAnimationFrame(loop);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
requestAnimationFrame(loop);
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '@girs/gjs';
|
|
2
|
+
import '@girs/gtk-4.0';
|
|
3
|
+
|
|
4
|
+
import Adw from 'gi://Adw?version=1';
|
|
5
|
+
import Gio from 'gi://Gio?version=2.0';
|
|
6
|
+
import { MainWindow } from './main-window.js';
|
|
7
|
+
|
|
8
|
+
const app = new Adw.Application({
|
|
9
|
+
application_id: 'org.gjsify.example',
|
|
10
|
+
flags: Gio.ApplicationFlags.FLAGS_NONE,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.connect('activate', () => {
|
|
14
|
+
let win = app.get_active_window();
|
|
15
|
+
if (!win) win = new MainWindow(app);
|
|
16
|
+
win.present();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.run([]);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
using Gtk 4.0;
|
|
2
|
+
using Adw 1;
|
|
3
|
+
|
|
4
|
+
template $MainWindow: Adw.ApplicationWindow {
|
|
5
|
+
default-width: 800;
|
|
6
|
+
default-height: 600;
|
|
7
|
+
title: "new-gjsify-app — Canvas 2D";
|
|
8
|
+
|
|
9
|
+
content: Gtk.Box {
|
|
10
|
+
orientation: vertical;
|
|
11
|
+
|
|
12
|
+
Adw.HeaderBar {}
|
|
13
|
+
|
|
14
|
+
Gtk.Box canvasContainer {
|
|
15
|
+
hexpand: true;
|
|
16
|
+
vexpand: true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import GObject from 'gi://GObject?version=2.0';
|
|
2
|
+
import type Gtk from 'gi://Gtk?version=4.0';
|
|
3
|
+
import Adw from 'gi://Adw?version=1';
|
|
4
|
+
import { Canvas2DWidget } from '@gjsify/canvas2d';
|
|
5
|
+
import { startAnimation } from './draw.js';
|
|
6
|
+
import Template from './main-window.blp';
|
|
7
|
+
|
|
8
|
+
export class MainWindow extends Adw.ApplicationWindow {
|
|
9
|
+
declare private _canvasContainer: Gtk.Box;
|
|
10
|
+
|
|
11
|
+
static {
|
|
12
|
+
GObject.registerClass(
|
|
13
|
+
{
|
|
14
|
+
GTypeName: 'MainWindow',
|
|
15
|
+
Template,
|
|
16
|
+
InternalChildren: ['canvasContainer'],
|
|
17
|
+
},
|
|
18
|
+
this,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor(application: Adw.Application) {
|
|
23
|
+
super({ application });
|
|
24
|
+
|
|
25
|
+
const canvasWidget = new Canvas2DWidget();
|
|
26
|
+
canvasWidget.set_hexpand(true);
|
|
27
|
+
canvasWidget.set_vexpand(true);
|
|
28
|
+
canvasWidget.installGlobals();
|
|
29
|
+
this._canvasContainer.append(canvasWidget);
|
|
30
|
+
|
|
31
|
+
canvasWidget.onReady((canvas) => {
|
|
32
|
+
canvas.width = canvasWidget.get_allocated_width();
|
|
33
|
+
canvas.height = canvasWidget.get_allocated_height();
|
|
34
|
+
startAnimation(canvas);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "NodeNext",
|
|
8
|
+
"types": [
|
|
9
|
+
"node",
|
|
10
|
+
"@girs/gjs",
|
|
11
|
+
"@girs/gtk-4.0",
|
|
12
|
+
"@girs/adw-1",
|
|
13
|
+
"@gjsify/esbuild-plugin-blueprint/types"
|
|
14
|
+
],
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "new-gjsify-app",
|
|
3
|
+
"description": "Adwaita game shell using Excalibur.js, WebGL → Canvas2D fallback.",
|
|
4
|
+
"version": "0.1.10",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check": "tsc --noEmit",
|
|
9
|
+
"build": "gjsify build src/index.ts --outfile dist/index.js --globals auto,dom",
|
|
10
|
+
"start": "gjsify run dist/index.js",
|
|
11
|
+
"dev": "gjsify build src/index.ts --outfile dist/index.js --globals auto,dom && gjsify run dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@girs/adw-1": "^1.10.0-4.0.0-rc.3",
|
|
15
|
+
"@girs/gio-2.0": "^2.88.0-4.0.0-rc.3",
|
|
16
|
+
"@girs/gjs": "^4.0.0-rc.3",
|
|
17
|
+
"@girs/gobject-2.0": "^2.88.0-4.0.0-rc.3",
|
|
18
|
+
"@girs/gtk-4.0": "^4.23.0-4.0.0-rc.3",
|
|
19
|
+
"@gjsify/canvas2d": "^0.1.12",
|
|
20
|
+
"@gjsify/cli": "^0.1.12",
|
|
21
|
+
"@gjsify/esbuild-plugin-blueprint": "^0.1.12",
|
|
22
|
+
"@types/node": "^25.6.0",
|
|
23
|
+
"typescript": "^6.0.2"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@gjsify/webgl": "^0.1.12",
|
|
27
|
+
"excalibur": "0.32.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as ex from 'excalibur';
|
|
2
|
+
|
|
3
|
+
export async function startGame(canvas: HTMLCanvasElement): Promise<ex.Engine> {
|
|
4
|
+
const engine = new ex.Engine({
|
|
5
|
+
canvasElement: canvas as unknown as HTMLCanvasElement,
|
|
6
|
+
width: canvas.width,
|
|
7
|
+
height: canvas.height,
|
|
8
|
+
backgroundColor: ex.Color.fromHex('#1e1e2e'),
|
|
9
|
+
suppressPlayButton: true,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const player = new ex.Actor({
|
|
13
|
+
pos: ex.vec(canvas.width / 2, canvas.height / 2),
|
|
14
|
+
width: 60,
|
|
15
|
+
height: 60,
|
|
16
|
+
color: ex.Color.fromHex('#89b4fa'),
|
|
17
|
+
});
|
|
18
|
+
player.actions.repeatForever((ctx) => {
|
|
19
|
+
ctx.rotateBy(Math.PI, 1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
engine.add(player);
|
|
23
|
+
await engine.start();
|
|
24
|
+
return engine;
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '@girs/gjs';
|
|
2
|
+
import '@girs/gtk-4.0';
|
|
3
|
+
|
|
4
|
+
import Adw from 'gi://Adw?version=1';
|
|
5
|
+
import Gio from 'gi://Gio?version=2.0';
|
|
6
|
+
import { MainWindow } from './main-window.js';
|
|
7
|
+
|
|
8
|
+
const app = new Adw.Application({
|
|
9
|
+
application_id: 'org.gjsify.example',
|
|
10
|
+
flags: Gio.ApplicationFlags.FLAGS_NONE,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.connect('activate', () => {
|
|
14
|
+
let win = app.get_active_window();
|
|
15
|
+
if (!win) win = new MainWindow(app);
|
|
16
|
+
win.present();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.run([]);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
using Gtk 4.0;
|
|
2
|
+
using Adw 1;
|
|
3
|
+
|
|
4
|
+
template $MainWindow: Adw.ApplicationWindow {
|
|
5
|
+
default-width: 800;
|
|
6
|
+
default-height: 600;
|
|
7
|
+
title: "new-gjsify-app — Game";
|
|
8
|
+
|
|
9
|
+
content: Gtk.Box {
|
|
10
|
+
orientation: vertical;
|
|
11
|
+
|
|
12
|
+
Adw.HeaderBar {}
|
|
13
|
+
|
|
14
|
+
Gtk.Box canvasContainer {
|
|
15
|
+
hexpand: true;
|
|
16
|
+
vexpand: true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import GObject from 'gi://GObject?version=2.0';
|
|
2
|
+
import type Gtk from 'gi://Gtk?version=4.0';
|
|
3
|
+
import Adw from 'gi://Adw?version=1';
|
|
4
|
+
import { CanvasWebGLWidget } from '@gjsify/webgl';
|
|
5
|
+
import { Canvas2DWidget } from '@gjsify/canvas2d';
|
|
6
|
+
import { startGame } from './game.js';
|
|
7
|
+
import Template from './main-window.blp';
|
|
8
|
+
|
|
9
|
+
export class MainWindow extends Adw.ApplicationWindow {
|
|
10
|
+
declare private _canvasContainer: Gtk.Box;
|
|
11
|
+
|
|
12
|
+
static {
|
|
13
|
+
GObject.registerClass(
|
|
14
|
+
{
|
|
15
|
+
GTypeName: 'MainWindow',
|
|
16
|
+
Template,
|
|
17
|
+
InternalChildren: ['canvasContainer'],
|
|
18
|
+
},
|
|
19
|
+
this,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
constructor(application: Adw.Application) {
|
|
24
|
+
super({ application });
|
|
25
|
+
this._startWithWidget(false);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private _startWithWidget(useFallback: boolean): void {
|
|
29
|
+
let child = this._canvasContainer.get_first_child();
|
|
30
|
+
while (child) {
|
|
31
|
+
this._canvasContainer.remove(child);
|
|
32
|
+
child = this._canvasContainer.get_first_child();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const widget = useFallback ? new Canvas2DWidget() : new CanvasWebGLWidget();
|
|
36
|
+
widget.set_hexpand(true);
|
|
37
|
+
widget.set_vexpand(true);
|
|
38
|
+
widget.installGlobals();
|
|
39
|
+
this._canvasContainer.append(widget);
|
|
40
|
+
|
|
41
|
+
widget.onReady((canvas) => {
|
|
42
|
+
widget.grab_focus();
|
|
43
|
+
canvas.width = widget.get_allocated_width();
|
|
44
|
+
canvas.height = widget.get_allocated_height();
|
|
45
|
+
startGame(canvas).catch((err: Error) => {
|
|
46
|
+
if (useFallback) {
|
|
47
|
+
console.error('Canvas 2D fallback also failed:', err.message);
|
|
48
|
+
} else {
|
|
49
|
+
console.error('WebGL start failed, trying Canvas 2D fallback:', err.message);
|
|
50
|
+
this._startWithWidget(true);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "NodeNext",
|
|
8
|
+
"types": [
|
|
9
|
+
"node",
|
|
10
|
+
"@girs/gjs",
|
|
11
|
+
"@girs/gtk-4.0",
|
|
12
|
+
"@girs/adw-1",
|
|
13
|
+
"@gjsify/esbuild-plugin-blueprint/types"
|
|
14
|
+
],
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "new-gjsify-app",
|
|
3
|
+
"description": "Adwaita app with WebGL + three.js (Blueprint UI).",
|
|
4
|
+
"version": "0.1.10",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check": "tsc --noEmit",
|
|
9
|
+
"build": "gjsify build src/index.ts --outfile dist/index.js",
|
|
10
|
+
"start": "gjsify run dist/index.js",
|
|
11
|
+
"dev": "gjsify build src/index.ts --outfile dist/index.js && gjsify run dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@girs/adw-1": "^1.10.0-4.0.0-rc.3",
|
|
15
|
+
"@girs/gio-2.0": "^2.88.0-4.0.0-rc.3",
|
|
16
|
+
"@girs/gjs": "^4.0.0-rc.3",
|
|
17
|
+
"@girs/gobject-2.0": "^2.88.0-4.0.0-rc.3",
|
|
18
|
+
"@girs/gtk-4.0": "^4.23.0-4.0.0-rc.3",
|
|
19
|
+
"@gjsify/cli": "^0.1.12",
|
|
20
|
+
"@gjsify/esbuild-plugin-blueprint": "^0.1.12",
|
|
21
|
+
"@types/node": "^25.6.0",
|
|
22
|
+
"@types/three": "^0.183.1",
|
|
23
|
+
"typescript": "^6.0.2"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@gjsify/webgl": "^0.1.12",
|
|
27
|
+
"three": "0.183.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '@girs/gjs';
|
|
2
|
+
import '@girs/gtk-4.0';
|
|
3
|
+
|
|
4
|
+
import Adw from 'gi://Adw?version=1';
|
|
5
|
+
import Gio from 'gi://Gio?version=2.0';
|
|
6
|
+
import { MainWindow } from './main-window.js';
|
|
7
|
+
|
|
8
|
+
const app = new Adw.Application({
|
|
9
|
+
application_id: 'org.gjsify.example',
|
|
10
|
+
flags: Gio.ApplicationFlags.FLAGS_NONE,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.connect('activate', () => {
|
|
14
|
+
let win = app.get_active_window();
|
|
15
|
+
if (!win) win = new MainWindow(app);
|
|
16
|
+
win.present();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.run([]);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
using Gtk 4.0;
|
|
2
|
+
using Adw 1;
|
|
3
|
+
|
|
4
|
+
template $MainWindow: Adw.ApplicationWindow {
|
|
5
|
+
default-width: 800;
|
|
6
|
+
default-height: 600;
|
|
7
|
+
title: "new-gjsify-app — WebGL";
|
|
8
|
+
|
|
9
|
+
content: Gtk.Box {
|
|
10
|
+
orientation: vertical;
|
|
11
|
+
|
|
12
|
+
Adw.HeaderBar {}
|
|
13
|
+
|
|
14
|
+
Gtk.Box canvasContainer {
|
|
15
|
+
hexpand: true;
|
|
16
|
+
vexpand: true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import GObject from 'gi://GObject?version=2.0';
|
|
2
|
+
import type Gtk from 'gi://Gtk?version=4.0';
|
|
3
|
+
import Adw from 'gi://Adw?version=1';
|
|
4
|
+
import { CanvasWebGLWidget } from '@gjsify/webgl';
|
|
5
|
+
import { startScene } from './scene.js';
|
|
6
|
+
import Template from './main-window.blp';
|
|
7
|
+
|
|
8
|
+
export class MainWindow extends Adw.ApplicationWindow {
|
|
9
|
+
declare private _canvasContainer: Gtk.Box;
|
|
10
|
+
|
|
11
|
+
static {
|
|
12
|
+
GObject.registerClass(
|
|
13
|
+
{
|
|
14
|
+
GTypeName: 'MainWindow',
|
|
15
|
+
Template,
|
|
16
|
+
InternalChildren: ['canvasContainer'],
|
|
17
|
+
},
|
|
18
|
+
this,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor(application: Adw.Application) {
|
|
23
|
+
super({ application });
|
|
24
|
+
|
|
25
|
+
const glArea = new CanvasWebGLWidget();
|
|
26
|
+
glArea.set_hexpand(true);
|
|
27
|
+
glArea.set_vexpand(true);
|
|
28
|
+
glArea.installGlobals();
|
|
29
|
+
this._canvasContainer.append(glArea);
|
|
30
|
+
|
|
31
|
+
glArea.onReady((canvas) => {
|
|
32
|
+
canvas.width = glArea.get_allocated_width();
|
|
33
|
+
canvas.height = glArea.get_allocated_height();
|
|
34
|
+
startScene(canvas);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
export function startScene(canvas: HTMLCanvasElement): void {
|
|
4
|
+
// Three.js r163+ requires WebGL2.
|
|
5
|
+
const gl2 = canvas.getContext('webgl2') as WebGL2RenderingContext | null;
|
|
6
|
+
if (!gl2) throw new Error('WebGL2 context unavailable');
|
|
7
|
+
|
|
8
|
+
const renderer = new THREE.WebGLRenderer({ canvas, context: gl2 });
|
|
9
|
+
renderer.setSize(canvas.width, canvas.height, false);
|
|
10
|
+
renderer.setClearColor(0x1e1e2e);
|
|
11
|
+
|
|
12
|
+
const scene = new THREE.Scene();
|
|
13
|
+
const camera = new THREE.PerspectiveCamera(60, canvas.width / canvas.height, 0.1, 100);
|
|
14
|
+
camera.position.z = 3;
|
|
15
|
+
|
|
16
|
+
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
17
|
+
const material = new THREE.MeshNormalMaterial();
|
|
18
|
+
const cube = new THREE.Mesh(geometry, material);
|
|
19
|
+
scene.add(cube);
|
|
20
|
+
|
|
21
|
+
canvas.addEventListener('resize', () => {
|
|
22
|
+
renderer.setSize(canvas.width, canvas.height, false);
|
|
23
|
+
camera.aspect = canvas.width / canvas.height;
|
|
24
|
+
camera.updateProjectionMatrix();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const loop = () => {
|
|
28
|
+
cube.rotation.x += 0.01;
|
|
29
|
+
cube.rotation.y += 0.013;
|
|
30
|
+
renderer.render(scene, camera);
|
|
31
|
+
requestAnimationFrame(loop);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
requestAnimationFrame(loop);
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "NodeNext",
|
|
8
|
+
"types": [
|
|
9
|
+
"node",
|
|
10
|
+
"@girs/gjs",
|
|
11
|
+
"@girs/gtk-4.0",
|
|
12
|
+
"@girs/adw-1",
|
|
13
|
+
"@gjsify/esbuild-plugin-blueprint/types"
|
|
14
|
+
],
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"]
|
|
20
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "new-gjsify-app",
|
|
3
|
+
"description": "Command-line tool using yargs (Node.js + GJS).",
|
|
3
4
|
"version": "0.1.10",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"private": true,
|
|
@@ -10,11 +11,14 @@
|
|
|
10
11
|
"dev": "gjsify build src/index.ts --outfile dist/index.js && gjsify run dist/index.js"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
13
|
-
"@gjsify/cli": "^0.1.
|
|
14
|
+
"@gjsify/cli": "^0.1.12",
|
|
15
|
+
"@gjsify/node-globals": "^0.1.12",
|
|
16
|
+
"@gjsify/runtime": "^0.1.12",
|
|
14
17
|
"@types/node": "^25.6.0",
|
|
18
|
+
"@types/yargs": "^17.0.35",
|
|
15
19
|
"typescript": "^6.0.2"
|
|
16
20
|
},
|
|
17
21
|
"dependencies": {
|
|
18
|
-
"
|
|
22
|
+
"yargs": "^18.0.0"
|
|
19
23
|
}
|
|
20
24
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runtimeName } from '@gjsify/runtime';
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
|
+
|
|
5
|
+
const c = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
bold: '\x1b[1m',
|
|
8
|
+
dim: '\x1b[2m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
yargs(hideBin(process.argv))
|
|
14
|
+
.scriptName('new-gjsify-app')
|
|
15
|
+
.usage(`\n${c.bold}new-gjsify-app${c.reset} — a gjsify CLI starter`)
|
|
16
|
+
.command(
|
|
17
|
+
'greet [name]',
|
|
18
|
+
'Print a greeting',
|
|
19
|
+
(y) => y.positional('name', { type: 'string', default: 'world' }),
|
|
20
|
+
(args) => {
|
|
21
|
+
console.log(`${c.cyan}Hello, ${c.bold}${args.name}${c.reset}${c.cyan}!${c.reset}`);
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
.command(
|
|
25
|
+
'info',
|
|
26
|
+
'Show runtime info',
|
|
27
|
+
() => {},
|
|
28
|
+
() => {
|
|
29
|
+
console.log(`${c.dim}runtime :${c.reset} ${c.green}${runtimeName}${c.reset}`);
|
|
30
|
+
console.log(`${c.dim}platform:${c.reset} ${process.platform}`);
|
|
31
|
+
console.log(`${c.dim}pid :${c.reset} ${process.pid}`);
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
.demandCommand(1, 'Pass --help to see available commands.')
|
|
35
|
+
.strict()
|
|
36
|
+
.help()
|
|
37
|
+
.parse();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "new-gjsify-app",
|
|
3
|
+
"description": "Minimal GTK4 app — Gtk.Window + Gtk.Label (no Adwaita, no Blueprint).",
|
|
4
|
+
"version": "0.1.10",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check": "tsc --noEmit",
|
|
9
|
+
"build": "gjsify build src/index.ts --outfile dist/index.js",
|
|
10
|
+
"start": "gjsify run dist/index.js",
|
|
11
|
+
"dev": "gjsify build src/index.ts --outfile dist/index.js && gjsify run dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@gjsify/cli": "^0.1.12",
|
|
15
|
+
"@types/node": "^25.6.0",
|
|
16
|
+
"typescript": "^6.0.2"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@girs/gio-2.0": "^2.88.0-4.0.0-rc.3",
|
|
20
|
+
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.3",
|
|
21
|
+
"@girs/gtk-4.0": "^4.23.0-4.0.0-rc.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Gtk from 'gi://Gtk?version=4.0';
|
|
2
|
+
import Gio from 'gi://Gio?version=2.0';
|
|
3
|
+
|
|
4
|
+
const app = new Gtk.Application({
|
|
5
|
+
applicationId: 'org.gjsify.example',
|
|
6
|
+
flags: Gio.ApplicationFlags.FLAGS_NONE,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
app.connect('activate', () => {
|
|
10
|
+
const window = new Gtk.ApplicationWindow({
|
|
11
|
+
application: app,
|
|
12
|
+
title: 'new-gjsify-app',
|
|
13
|
+
defaultWidth: 480,
|
|
14
|
+
defaultHeight: 280,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const box = new Gtk.Box({
|
|
18
|
+
orientation: Gtk.Orientation.VERTICAL,
|
|
19
|
+
spacing: 12,
|
|
20
|
+
marginTop: 24,
|
|
21
|
+
marginBottom: 24,
|
|
22
|
+
marginStart: 24,
|
|
23
|
+
marginEnd: 24,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const title = new Gtk.Label({ label: 'Hello from gjsify!' });
|
|
27
|
+
title.add_css_class('title-2');
|
|
28
|
+
|
|
29
|
+
const hint = new Gtk.Label({
|
|
30
|
+
label: `Running on ${process.platform} · PID ${process.pid}`,
|
|
31
|
+
xalign: 0.5,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
box.append(title);
|
|
35
|
+
box.append(hint);
|
|
36
|
+
window.set_child(box);
|
|
37
|
+
window.present();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
app.run([]);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "NodeNext",
|
|
8
|
+
"types": ["node", "@girs/gtk-4.0", "@girs/gio-2.0", "@girs/glib-2.0"],
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "new-gjsify-app",
|
|
3
|
+
"description": "HTTP server using Express (familiar Node.js stack).",
|
|
4
|
+
"version": "0.1.10",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check": "tsc --noEmit",
|
|
9
|
+
"build": "gjsify build src/index.ts --outfile dist/index.js",
|
|
10
|
+
"start": "gjsify run dist/index.js",
|
|
11
|
+
"dev": "gjsify build src/index.ts --outfile dist/index.js && gjsify run dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@gjsify/cli": "^0.1.12",
|
|
15
|
+
"@gjsify/node-globals": "^0.1.12",
|
|
16
|
+
"@gjsify/runtime": "^0.1.12",
|
|
17
|
+
"@types/express": "^5.0.6",
|
|
18
|
+
"@types/node": "^25.6.0",
|
|
19
|
+
"typescript": "^6.0.2"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"express": "^5.2.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { runtimeName } from '@gjsify/runtime';
|
|
2
|
+
import express, { type Request, type Response } from 'express';
|
|
3
|
+
|
|
4
|
+
const app = express();
|
|
5
|
+
app.use(express.json());
|
|
6
|
+
|
|
7
|
+
app.get('/', (_req: Request, res: Response) => {
|
|
8
|
+
res.type('html').send(`<!doctype html>
|
|
9
|
+
<html>
|
|
10
|
+
<head><meta charset="utf-8"><title>new-gjsify-app</title></head>
|
|
11
|
+
<body>
|
|
12
|
+
<h1>new-gjsify-app</h1>
|
|
13
|
+
<p>Runtime: <strong>${runtimeName}</strong></p>
|
|
14
|
+
<p>Try <a href="/api/ping">GET /api/ping</a></p>
|
|
15
|
+
</body>
|
|
16
|
+
</html>`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.get('/api/ping', (_req: Request, res: Response) => {
|
|
20
|
+
res.json({ ok: true, runtime: runtimeName, time: new Date().toISOString() });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const PORT = parseInt(process.env.PORT ?? '3000', 10);
|
|
24
|
+
app.listen(PORT, () => {
|
|
25
|
+
console.log(`Express server running at http://localhost:${PORT}`);
|
|
26
|
+
console.log(`Runtime: ${runtimeName}`);
|
|
27
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "NodeNext",
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "new-gjsify-app",
|
|
3
|
+
"description": "HTTP server using Hono (Web-standard fetch-style API).",
|
|
4
|
+
"version": "0.1.10",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"check": "tsc --noEmit",
|
|
9
|
+
"build": "gjsify build src/index.ts --outfile dist/index.js",
|
|
10
|
+
"start": "gjsify run dist/index.js",
|
|
11
|
+
"dev": "gjsify build src/index.ts --outfile dist/index.js && gjsify run dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@gjsify/cli": "^0.1.12",
|
|
15
|
+
"@gjsify/node-globals": "^0.1.12",
|
|
16
|
+
"@gjsify/runtime": "^0.1.12",
|
|
17
|
+
"@types/node": "^25.6.0",
|
|
18
|
+
"typescript": "^6.0.2"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@hono/node-server": "^1.19.14",
|
|
22
|
+
"hono": "^4.12.14"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runtimeName } from '@gjsify/runtime';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { serve } from '@hono/node-server';
|
|
4
|
+
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
|
|
7
|
+
app.get('/', (c) =>
|
|
8
|
+
c.json({
|
|
9
|
+
name: 'new-gjsify-app',
|
|
10
|
+
runtime: runtimeName,
|
|
11
|
+
endpoints: ['GET /', 'GET /api/ping'],
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
app.get('/api/ping', (c) => c.json({ ok: true, time: new Date().toISOString() }));
|
|
16
|
+
|
|
17
|
+
const PORT = parseInt(process.env.PORT ?? '3000', 10);
|
|
18
|
+
serve({ fetch: app.fetch, port: PORT }, (info) => {
|
|
19
|
+
console.log(`Hono server running at http://localhost:${info.port}`);
|
|
20
|
+
console.log(`Runtime: ${runtimeName}`);
|
|
21
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "NodeNext",
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
package/lib/create.d.ts
CHANGED
|
@@ -1 +1,14 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { discoverTemplates, findTemplate } from './discover-templates.js';
|
|
2
|
+
export type { TemplateInfo } from './discover-templates.js';
|
|
3
|
+
export interface CreateProjectOptions {
|
|
4
|
+
projectName: string;
|
|
5
|
+
/** Template short name, e.g. "gtk-minimal". If omitted, the caller is responsible for providing one via prompt. */
|
|
6
|
+
template: string;
|
|
7
|
+
/** Allow scaffolding into an existing non-empty directory. */
|
|
8
|
+
force?: boolean;
|
|
9
|
+
/** Run `npm install` in the scaffolded directory after writing files. */
|
|
10
|
+
install?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** npm package names: lowercase, digits, -, _, .; no leading . or _. */
|
|
13
|
+
export declare function sanitizeProjectName(raw: string): string;
|
|
14
|
+
export declare function createProject(options: CreateProjectOptions): Promise<void>;
|
package/lib/create.js
CHANGED
|
@@ -1,33 +1,100 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, cpSync } from 'node:fs';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { discoverTemplates, findTemplate } from './discover-templates.js';
|
|
5
|
+
export { discoverTemplates, findTemplate } from './discover-templates.js';
|
|
6
|
+
/** Sentinel replaced by the user's project name in every text file under the template. */
|
|
7
|
+
const PROJECT_NAME_SENTINEL = 'new-gjsify-app';
|
|
8
|
+
/** File extensions we treat as text and scan for the sentinel. */
|
|
9
|
+
const TEXT_FILE_EXT = new Set([
|
|
10
|
+
'.json', '.md', '.ts', '.tsx', '.js', '.mjs', '.cjs',
|
|
11
|
+
'.blp', '.html', '.css', '.scss', '.xml', '.ui', '.txt',
|
|
12
|
+
]);
|
|
13
|
+
/** npm package names: lowercase, digits, -, _, .; no leading . or _. */
|
|
14
|
+
export function sanitizeProjectName(raw) {
|
|
15
|
+
const trimmed = raw.trim();
|
|
16
|
+
if (!trimmed)
|
|
17
|
+
throw new Error('Project name cannot be empty.');
|
|
18
|
+
const cleaned = trimmed
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
21
|
+
.replace(/^[._-]+/, '')
|
|
22
|
+
.replace(/[._-]+$/, '');
|
|
23
|
+
if (!cleaned)
|
|
24
|
+
throw new Error(`"${raw}" is not a valid npm package name.`);
|
|
25
|
+
return cleaned;
|
|
26
|
+
}
|
|
27
|
+
function isDirEmpty(path) {
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return true;
|
|
30
|
+
return readdirSync(path).length === 0;
|
|
31
|
+
}
|
|
32
|
+
export async function createProject(options) {
|
|
33
|
+
const projectName = sanitizeProjectName(options.projectName);
|
|
34
|
+
const { template, force = false, install = false } = options;
|
|
35
|
+
const info = findTemplate(template);
|
|
36
|
+
if (!info) {
|
|
37
|
+
const available = discoverTemplates().map((t) => t.name).join(', ');
|
|
38
|
+
throw new Error(`Unknown template "${template}". Available templates: ${available || '(none — run "yarn build" first)'}`);
|
|
39
|
+
}
|
|
6
40
|
const targetDir = resolve(process.cwd(), projectName);
|
|
7
|
-
if (existsSync(targetDir)) {
|
|
8
|
-
console.error(`Error: Directory "${projectName}"
|
|
41
|
+
if (existsSync(targetDir) && !isDirEmpty(targetDir) && !force) {
|
|
42
|
+
console.error(`Error: Directory "${projectName}" exists and is not empty. Use --force to scaffold into it anyway.`);
|
|
9
43
|
process.exit(1);
|
|
10
44
|
}
|
|
11
|
-
console.log(`Creating new Gjsify project in ${targetDir}...`);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
console.log(`Creating new Gjsify project in ${targetDir} (template: ${info.name})...`);
|
|
46
|
+
mkdirSync(targetDir, { recursive: true });
|
|
47
|
+
cpSync(info.path, targetDir, { recursive: true });
|
|
48
|
+
substituteProjectName(targetDir, projectName);
|
|
49
|
+
if (install) {
|
|
50
|
+
console.log('Running npm install...');
|
|
51
|
+
const result = spawnSync('npm', ['install', '--no-audit', '--no-fund'], {
|
|
52
|
+
cwd: targetDir,
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
});
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
console.warn('npm install failed; re-run it manually in the project directory.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
printNextSteps(projectName, info, install);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Walk the scaffolded tree and replace the sentinel in every text file.
|
|
63
|
+
* Skips node_modules / dist / lib and non-text files by extension.
|
|
64
|
+
*/
|
|
65
|
+
function substituteProjectName(rootDir, projectName) {
|
|
66
|
+
const skipDirs = new Set(['node_modules', 'dist', 'lib']);
|
|
67
|
+
const stack = [rootDir];
|
|
68
|
+
while (stack.length > 0) {
|
|
69
|
+
const dir = stack.pop();
|
|
70
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
71
|
+
const full = join(dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
if (!skipDirs.has(entry.name))
|
|
74
|
+
stack.push(full);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!entry.isFile())
|
|
78
|
+
continue;
|
|
79
|
+
const dot = entry.name.lastIndexOf('.');
|
|
80
|
+
const ext = dot >= 0 ? entry.name.slice(dot) : '';
|
|
81
|
+
if (!TEXT_FILE_EXT.has(ext))
|
|
82
|
+
continue;
|
|
83
|
+
const content = readFileSync(full, 'utf-8');
|
|
84
|
+
if (!content.includes(PROJECT_NAME_SENTINEL))
|
|
85
|
+
continue;
|
|
86
|
+
writeFileSync(full, content.replaceAll(PROJECT_NAME_SENTINEL, projectName));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function printNextSteps(projectName, template, installed) {
|
|
24
91
|
console.log('');
|
|
25
|
-
console.log(
|
|
92
|
+
console.log(`Project created from template "${template.name}".`);
|
|
26
93
|
console.log('');
|
|
27
94
|
console.log('Next steps:');
|
|
28
95
|
console.log(` cd ${projectName}`);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.log(' npm
|
|
96
|
+
if (!installed)
|
|
97
|
+
console.log(' npm install');
|
|
98
|
+
console.log(' npm run dev');
|
|
32
99
|
console.log('');
|
|
33
100
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TemplateInfo {
|
|
2
|
+
/** Short name, e.g. "gtk-minimal" */
|
|
3
|
+
name: string;
|
|
4
|
+
/** One-line description (from template package.json `description` or a built-in fallback) */
|
|
5
|
+
description: string;
|
|
6
|
+
/** Absolute path to the template directory inside dist-templates/ */
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Discover all shipped templates by scanning dist-templates/ next to this file.
|
|
11
|
+
* Returns templates sorted alphabetically by name.
|
|
12
|
+
*/
|
|
13
|
+
export declare function discoverTemplates(): TemplateInfo[];
|
|
14
|
+
export declare function findTemplate(name: string): TemplateInfo | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const FALLBACK_DESCRIPTIONS = {
|
|
5
|
+
'gtk-minimal': 'Minimal GTK4 app — Gtk.Window + Gtk.Label (no Adwaita, no Blueprint).',
|
|
6
|
+
'cli': 'Command-line tool using yargs (Node.js + GJS).',
|
|
7
|
+
'adw-canvas2d': 'Adwaita app with HTML Canvas 2D rendering (Blueprint UI).',
|
|
8
|
+
'adw-webgl': 'Adwaita app with WebGL + three.js (Blueprint UI).',
|
|
9
|
+
'adw-game': 'Adwaita game shell using Excalibur.js, WebGL → Canvas2D fallback.',
|
|
10
|
+
'web-server-hono': 'HTTP server using Hono (Web-standard fetch-style API).',
|
|
11
|
+
'web-server-express': 'HTTP server using Express (familiar Node.js stack).',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Discover all shipped templates by scanning dist-templates/ next to this file.
|
|
15
|
+
* Returns templates sorted alphabetically by name.
|
|
16
|
+
*/
|
|
17
|
+
export function discoverTemplates() {
|
|
18
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const templatesRoot = join(pkgRoot, 'dist-templates');
|
|
20
|
+
if (!existsSync(templatesRoot))
|
|
21
|
+
return [];
|
|
22
|
+
const templates = [];
|
|
23
|
+
for (const name of readdirSync(templatesRoot)) {
|
|
24
|
+
const path = join(templatesRoot, name);
|
|
25
|
+
const pkgJsonPath = join(path, 'package.json');
|
|
26
|
+
if (!statSync(path).isDirectory() || !existsSync(pkgJsonPath))
|
|
27
|
+
continue;
|
|
28
|
+
let description = FALLBACK_DESCRIPTIONS[name] ?? '';
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
31
|
+
if (typeof pkg.description === 'string' && pkg.description.trim()) {
|
|
32
|
+
description = pkg.description;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Fall back to built-in description
|
|
37
|
+
}
|
|
38
|
+
templates.push({ name, description, path });
|
|
39
|
+
}
|
|
40
|
+
templates.sort((a, b) => a.name.localeCompare(b.name));
|
|
41
|
+
return templates;
|
|
42
|
+
}
|
|
43
|
+
export function findTemplate(name) {
|
|
44
|
+
return discoverTemplates().find((t) => t.name === name);
|
|
45
|
+
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -2,18 +2,56 @@
|
|
|
2
2
|
import yargs from 'yargs';
|
|
3
3
|
import { hideBin } from 'yargs/helpers';
|
|
4
4
|
import { createProject } from './create.js';
|
|
5
|
+
import { discoverTemplates } from './discover-templates.js';
|
|
6
|
+
import { promptTemplate } from './prompt-template.js';
|
|
7
|
+
const templates = discoverTemplates();
|
|
8
|
+
const templateChoices = templates.map((t) => t.name);
|
|
5
9
|
void yargs(hideBin(process.argv))
|
|
6
10
|
.scriptName('@gjsify/create-app')
|
|
7
11
|
.usage('$0 [project-name]', 'Create a new Gjsify project', (yargs) => {
|
|
8
|
-
return yargs
|
|
12
|
+
return yargs
|
|
13
|
+
.positional('project-name', {
|
|
9
14
|
describe: 'Name of the project directory to create',
|
|
10
15
|
type: 'string',
|
|
11
|
-
default: 'my-gjs-app'
|
|
16
|
+
default: 'my-gjs-app',
|
|
17
|
+
})
|
|
18
|
+
.option('template', {
|
|
19
|
+
alias: 't',
|
|
20
|
+
describe: 'Template to scaffold from',
|
|
21
|
+
type: 'string',
|
|
22
|
+
choices: templateChoices.length > 0 ? templateChoices : undefined,
|
|
23
|
+
})
|
|
24
|
+
.option('force', {
|
|
25
|
+
alias: 'f',
|
|
26
|
+
describe: 'Scaffold into a non-empty directory',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: false,
|
|
29
|
+
})
|
|
30
|
+
.option('install', {
|
|
31
|
+
describe: 'Run npm install after scaffolding',
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: false,
|
|
12
34
|
});
|
|
13
35
|
}, async (argv) => {
|
|
14
36
|
const projectName = argv['project-name'];
|
|
15
|
-
|
|
37
|
+
let template = argv['template'];
|
|
38
|
+
if (!template) {
|
|
39
|
+
if (!process.stdin.isTTY) {
|
|
40
|
+
const list = templateChoices.join(', ');
|
|
41
|
+
console.error(`Error: --template is required in non-interactive mode. Available templates: ${list || '(none)'}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const picked = await promptTemplate(templates);
|
|
45
|
+
template = picked.name;
|
|
46
|
+
}
|
|
47
|
+
await createProject({
|
|
48
|
+
projectName,
|
|
49
|
+
template,
|
|
50
|
+
force: argv['force'],
|
|
51
|
+
install: argv['install'],
|
|
52
|
+
});
|
|
16
53
|
})
|
|
17
54
|
.help()
|
|
18
55
|
.argv;
|
|
19
|
-
export { createProject } from './create.js';
|
|
56
|
+
export { createProject, sanitizeProjectName } from './create.js';
|
|
57
|
+
export { discoverTemplates, findTemplate } from './discover-templates.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TemplateInfo } from './discover-templates.js';
|
|
2
|
+
/**
|
|
3
|
+
* Interactive template picker. Returns the selected TemplateInfo.
|
|
4
|
+
* Throws if stdin is not a TTY — caller should check process.stdin.isTTY first.
|
|
5
|
+
*/
|
|
6
|
+
export declare function promptTemplate(templates: TemplateInfo[]): Promise<TemplateInfo>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { stdin, stdout } from 'node:process';
|
|
2
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
3
|
+
const c = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
bold: '\x1b[1m',
|
|
6
|
+
dim: '\x1b[2m',
|
|
7
|
+
cyan: '\x1b[36m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Render a templates list with one row highlighted.
|
|
12
|
+
* First call: just draws. Subsequent calls move the cursor back up first.
|
|
13
|
+
*/
|
|
14
|
+
function render(templates, selected, firstRender) {
|
|
15
|
+
if (!firstRender) {
|
|
16
|
+
// Move cursor up over the previously drawn rows
|
|
17
|
+
stdout.write(`\x1b[${templates.length}A`);
|
|
18
|
+
}
|
|
19
|
+
for (let i = 0; i < templates.length; i++) {
|
|
20
|
+
const t = templates[i];
|
|
21
|
+
const marker = i === selected ? `${c.cyan}❯${c.reset}` : ' ';
|
|
22
|
+
const label = i === selected ? `${c.bold}${c.green}${t.name}${c.reset}` : t.name;
|
|
23
|
+
// Clear line, then write content
|
|
24
|
+
stdout.write(`\x1b[2K\r ${marker} ${label} ${c.dim}${t.description}${c.reset}\n`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Interactive template picker. Returns the selected TemplateInfo.
|
|
29
|
+
* Throws if stdin is not a TTY — caller should check process.stdin.isTTY first.
|
|
30
|
+
*/
|
|
31
|
+
export function promptTemplate(templates) {
|
|
32
|
+
if (templates.length === 0) {
|
|
33
|
+
return Promise.reject(new Error('No templates available.'));
|
|
34
|
+
}
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let selected = 0;
|
|
37
|
+
stdout.write(`${c.bold}Select a template${c.reset} ${c.dim}(↑/↓ to navigate, Enter to confirm, Ctrl+C to cancel)${c.reset}\n`);
|
|
38
|
+
render(templates, selected, true);
|
|
39
|
+
emitKeypressEvents(stdin);
|
|
40
|
+
if (stdin.isTTY)
|
|
41
|
+
stdin.setRawMode(true);
|
|
42
|
+
stdin.resume();
|
|
43
|
+
const cleanup = () => {
|
|
44
|
+
stdin.removeListener('keypress', onKeypress);
|
|
45
|
+
if (stdin.isTTY)
|
|
46
|
+
stdin.setRawMode(false);
|
|
47
|
+
stdin.pause();
|
|
48
|
+
};
|
|
49
|
+
const onKeypress = (_str, key) => {
|
|
50
|
+
if (!key)
|
|
51
|
+
return;
|
|
52
|
+
if (key.ctrl && key.name === 'c') {
|
|
53
|
+
cleanup();
|
|
54
|
+
stdout.write('\n');
|
|
55
|
+
reject(new Error('Cancelled by user'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
59
|
+
selected = (selected - 1 + templates.length) % templates.length;
|
|
60
|
+
render(templates, selected, false);
|
|
61
|
+
}
|
|
62
|
+
else if (key.name === 'down' || key.name === 'j') {
|
|
63
|
+
selected = (selected + 1) % templates.length;
|
|
64
|
+
render(templates, selected, false);
|
|
65
|
+
}
|
|
66
|
+
else if (key.name === 'return' || key.name === 'enter') {
|
|
67
|
+
cleanup();
|
|
68
|
+
resolve(templates[selected]);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
stdin.on('keypress', onKeypress);
|
|
72
|
+
});
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/create-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Create a new Gjsify project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/create.js",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./lib/create.d.ts",
|
|
11
11
|
"import": "./lib/create.js"
|
|
12
12
|
},
|
|
13
|
+
"./prompt": {
|
|
14
|
+
"types": "./lib/prompt-template.d.ts",
|
|
15
|
+
"import": "./lib/prompt-template.js"
|
|
16
|
+
},
|
|
13
17
|
"./package.json": "./package.json"
|
|
14
18
|
},
|
|
15
19
|
"bin": "./lib/index.js",
|
|
@@ -28,7 +32,7 @@
|
|
|
28
32
|
],
|
|
29
33
|
"files": [
|
|
30
34
|
"lib",
|
|
31
|
-
"templates"
|
|
35
|
+
"dist-templates"
|
|
32
36
|
],
|
|
33
37
|
"dependencies": {
|
|
34
38
|
"yargs": "^18.0.0"
|
package/templates/src/index.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import Gtk from 'gi://Gtk?version=4.0'
|
|
2
|
-
|
|
3
|
-
// Globals (process, crypto, Buffer, etc.) are automatically detected
|
|
4
|
-
// and injected by `gjsify build` via --globals auto (the default).
|
|
5
|
-
|
|
6
|
-
const app = new Gtk.Application({
|
|
7
|
-
applicationId: 'org.gjsify.example',
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
app.connect('activate', () => {
|
|
11
|
-
const window = new Gtk.ApplicationWindow({
|
|
12
|
-
application: app,
|
|
13
|
-
title: 'new-gjsify-app',
|
|
14
|
-
defaultWidth: 480,
|
|
15
|
-
defaultHeight: 280,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const box = new Gtk.Box({
|
|
19
|
-
orientation: Gtk.Orientation.VERTICAL,
|
|
20
|
-
spacing: 16,
|
|
21
|
-
margin_top: 24,
|
|
22
|
-
margin_bottom: 24,
|
|
23
|
-
margin_start: 24,
|
|
24
|
-
margin_end: 24,
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
// Node.js — process API
|
|
28
|
-
const platformLabel = new Gtk.Label({
|
|
29
|
-
label: `Platform: ${process.platform} | PID: ${process.pid}`,
|
|
30
|
-
xalign: 0,
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// Web Crypto API — UUID
|
|
34
|
-
const uuidLabel = new Gtk.Label({
|
|
35
|
-
label: 'UUID: —',
|
|
36
|
-
xalign: 0,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// Node.js — Buffer API
|
|
40
|
-
const base64Label = new Gtk.Label({
|
|
41
|
-
label: 'Base64: —',
|
|
42
|
-
xalign: 0,
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// Button wires everything together
|
|
46
|
-
const button = new Gtk.Button({ label: '↺ Generate UUID' })
|
|
47
|
-
button.connect('clicked', () => {
|
|
48
|
-
const uuid = crypto.randomUUID() // Web Crypto
|
|
49
|
-
const base64 = Buffer.from(uuid).toString('base64') // Node.js Buffer
|
|
50
|
-
uuidLabel.set_label(`UUID: ${uuid}`)
|
|
51
|
-
base64Label.set_label(`Base64: ${base64}`)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
box.append(platformLabel)
|
|
55
|
-
box.append(new Gtk.Separator({ orientation: Gtk.Orientation.HORIZONTAL }))
|
|
56
|
-
box.append(button)
|
|
57
|
-
box.append(uuidLabel)
|
|
58
|
-
box.append(base64Label)
|
|
59
|
-
|
|
60
|
-
window.set_child(box)
|
|
61
|
-
window.present()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
app.run([])
|