@peachy/react 0.0.1

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 ADDED
@@ -0,0 +1,90 @@
1
+ # @peachy/react
2
+
3
+ This package contains a React renderer for GTK.
4
+
5
+ It implements a custom react-reconciler, which is responsible for rendering React components into the GTK "DOM".
6
+
7
+ ## Installation
8
+
9
+ You can install this package with npm:
10
+
11
+ ```bash
12
+ npm install @peachy/react @peachy/core
13
+ ```
14
+
15
+ For typescript, you will need to install the GTK types:
16
+
17
+ ```bash
18
+ npm install @peachy/types
19
+ ```
20
+
21
+ Then setup your config file
22
+
23
+ ```json
24
+ // tsconfig.json
25
+ {
26
+ "extends": "@peachy/react/tsconfig",
27
+ "include": ["@peachy/types"]
28
+ }
29
+
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Here is a basic example that creates a GTK application window with a label:
35
+
36
+ ```tsx
37
+ import Gtk from "gi://Gtk?version=4.0";
38
+ import { render } from "@peachy/react";
39
+
40
+ const app = Gtk.Application.new(
41
+ "com.hello.world",
42
+ Gio.ApplicationFlags.DEFAULT_FLAGS,
43
+ );
44
+
45
+ app.connect("activate", () => {
46
+ const app_window = Gtk.ApplicationWindow.new(app);
47
+
48
+ render(<Gtk.Label label="Hello, World!" />, app_window);
49
+
50
+ app_window.present();
51
+ });
52
+
53
+ app.run([]);
54
+ ```
55
+
56
+ You can also create reusable components using React's component system. For example, you can create a custom button component:
57
+
58
+ ```tsx
59
+ import Gtk from "gi://Gtk?version=4.0";
60
+ import { render } from "@peachy/react";
61
+
62
+ const Button = ({ label, onClick }) => (
63
+ <Gtk.Button label={label} onClick={onClick} />
64
+ );
65
+
66
+ const app = Gtk.Application.new(
67
+ "com.hello.world",
68
+ Gio.ApplicationFlags.DEFAULT_FLAGS,
69
+ );
70
+
71
+ app.connect("activate", () => {
72
+ const app_window = Gtk.ApplicationWindow.new(app);
73
+
74
+ render(
75
+ <Button label="Click me!" onClick={() => console.log("Button clicked")} />,
76
+ app_window
77
+ );
78
+
79
+ app_window.present();
80
+ });
81
+
82
+ app.run([]);
83
+ ```
84
+
85
+ ## References
86
+
87
+ - https://github.com/VisualElectric/pixi-react
88
+ - react-reconciler
89
+ - https://github.com/samuelscheit/react-native-skia-list
90
+ - https://github.com/jiayihu/react-tiny-dom
@@ -0,0 +1,11 @@
1
+ import Gtk from "gi://Gtk?version=4.0";
2
+
3
+ //#region src/extra.d.ts
4
+ interface ReactionExtraProps<T extends Gtk.Widget> {
5
+ appendChild?(parentInstance: T, child: Gtk.Widget): void;
6
+ insertBefore?(parentInstance: T, child: Gtk.Widget, before: Gtk.Widget | null): void;
7
+ removeChild?(parentInstance: T, child: Gtk.Widget): void;
8
+ }
9
+ declare const extraMap: Map<GObject.GType, ReactionExtraProps<Gtk.Widget>>;
10
+ //#endregion
11
+ export { ReactionExtraProps, extraMap };
package/dist/extra.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import { getMetadata } from "./utilities/metadata.mjs";
2
+ import Gtk from "gi://Gtk?version=4.0";
3
+
4
+ //#region src/extra.ts
5
+ let builder;
6
+ const extraMap = new Map([[Gtk.Buildable.$gtype, { appendChild: (parentInstance, child) => {
7
+ builder ??= Gtk.Builder.new();
8
+ const metadata = getMetadata(child);
9
+ parentInstance.vfunc_add_child(builder, child, metadata?.childType ?? null);
10
+ } }], [Gtk.Window.$gtype, {
11
+ appendChild: (parentInstance, child) => {
12
+ parentInstance.set_child(child);
13
+ },
14
+ removeChild: (parentInstance) => {
15
+ parentInstance.set_child(null);
16
+ }
17
+ }]]);
18
+
19
+ //#endregion
20
+ export { extraMap };
@@ -0,0 +1,36 @@
1
+ import { Key, Ref } from "react";
2
+
3
+ //#region src/global.d.ts
4
+ declare global {
5
+ namespace JSX {
6
+ interface IntrinsicAttributes {
7
+ childType?: string;
8
+ ref?: Ref<any>;
9
+ key?: Key | null | undefined;
10
+ }
11
+ }
12
+ }
13
+ declare module "gi://Gtk?version=4.0" {
14
+ namespace Gtk {
15
+ namespace Widget {
16
+ interface ConstructorProps {
17
+ /**
18
+ * React children
19
+ */
20
+ children: any;
21
+ }
22
+ }
23
+ }
24
+ }
25
+ declare module "gi://Gtk?version=3.0" {
26
+ namespace Gtk {
27
+ namespace Widget {
28
+ interface ConstructorProps {
29
+ /**
30
+ * React children
31
+ */
32
+ children: any;
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ import "./global.mjs";
2
+ import Gtk from "gi://Gtk?version=4.0";
3
+ import "reflect-metadata";
4
+
5
+ //#region src/index.d.ts
6
+ declare const render: (jsx: React.ReactNode, root: Gtk.Widget, callback?: () => void) => void;
7
+ //#endregion
8
+ export { render };
package/dist/index.mjs ADDED
@@ -0,0 +1,123 @@
1
+ import { setMetadataFromProps } from "./utilities/metadata.mjs";
2
+ import "./global.d.mts";
3
+ import { typeMap } from "./type-map.mjs";
4
+ import { appendChild, insertBefore, removeChild } from "./utilities/children.mjs";
5
+ import { getEventListeners, getEventName, getProperties, updateWidget } from "./utilities/diff.mjs";
6
+ import Gtk from "gi://Gtk?version=4.0";
7
+ import "reflect-metadata";
8
+ import GLib from "gi://GLib?version=2.0";
9
+ import Reconciler from "react-reconciler";
10
+ import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants";
11
+
12
+ //#region src/index.ts
13
+ const emptyObject = {};
14
+ let updatePriority = NoEventPriority;
15
+ const reconciler = Reconciler({
16
+ appendInitialChild(parentInstance, child) {
17
+ appendChild(parentInstance, child);
18
+ },
19
+ createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
20
+ const klass = typeMap.get(type);
21
+ if (!klass) throw new Error(`Unknown type: ${type}`);
22
+ return new klass(getProperties(props));
23
+ },
24
+ createTextInstance(text, rootContainerInstance, internalInstanceHandle) {
25
+ return Gtk.Label.new(text);
26
+ },
27
+ finalizeInitialChildren(widget, type, props) {
28
+ const events = getEventListeners(props);
29
+ Object.keys(events).forEach((name) => {
30
+ const eventType = getEventName(name);
31
+ widget.connect(eventType, events[name]);
32
+ });
33
+ setMetadataFromProps(widget, props);
34
+ return false;
35
+ },
36
+ getPublicInstance(inst) {
37
+ return inst;
38
+ },
39
+ prepareForCommit() {
40
+ return null;
41
+ },
42
+ resetAfterCommit() {},
43
+ getRootHostContext(rootInstance) {
44
+ return emptyObject;
45
+ },
46
+ getChildHostContext(parentHostContext, type) {
47
+ return emptyObject;
48
+ },
49
+ shouldSetTextContent(type, props) {
50
+ return false;
51
+ },
52
+ supportsMutation: true,
53
+ supportsHydration: false,
54
+ supportsPersistence: false,
55
+ supportsMicrotasks: true,
56
+ scheduleTimeout: (fn, delay) => {
57
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => {
58
+ fn();
59
+ return GLib.SOURCE_REMOVE;
60
+ });
61
+ },
62
+ scheduleMicrotask: (fn) => {
63
+ GLib.idle_add(GLib.PRIORITY_HIGH_IDLE, () => {
64
+ fn();
65
+ return GLib.SOURCE_REMOVE;
66
+ });
67
+ },
68
+ removeChild(parentInstance, child) {
69
+ removeChild(parentInstance, child);
70
+ },
71
+ removeChildFromContainer(parentInstance, child) {
72
+ removeChild(parentInstance, child);
73
+ },
74
+ insertBefore(parentInstance, child, beforeChild) {
75
+ insertBefore(parentInstance, child, beforeChild);
76
+ },
77
+ insertInContainerBefore(parentInstance, child, beforeChild) {
78
+ insertBefore(parentInstance, child, beforeChild);
79
+ },
80
+ clearContainer(container) {},
81
+ appendChild(parentInstance, child) {
82
+ appendChild(parentInstance, child);
83
+ },
84
+ appendChildToContainer(container, child) {
85
+ appendChild(container, child);
86
+ },
87
+ detachDeletedInstance(node) {
88
+ node.unparent();
89
+ },
90
+ commitUpdate(widget, type, oldProps, newProps, internalInstanceHandle) {
91
+ updateWidget(widget, oldProps, newProps);
92
+ },
93
+ commitTextUpdate(textInstance, oldText, newText) {},
94
+ resolveUpdatePriority: function() {
95
+ if (updatePriority !== NoEventPriority) return updatePriority;
96
+ return DefaultEventPriority;
97
+ },
98
+ setCurrentUpdatePriority: function(newPriority) {
99
+ updatePriority = newPriority;
100
+ },
101
+ getCurrentUpdatePriority: function() {
102
+ return updatePriority;
103
+ },
104
+ resolveEventTimeStamp() {
105
+ return -1.1;
106
+ },
107
+ resolveEventType() {
108
+ return null;
109
+ },
110
+ trackSchedulerEvent() {}
111
+ });
112
+ const render = (jsx, root, callback) => {
113
+ const container = reconciler.createContainer(root, 0, null, false, null, "peachy", console.error, console.error, console.error, () => {}, null);
114
+ reconciler.updateContainer(jsx, container, null, callback);
115
+ };
116
+ reconciler.injectIntoDevTools({
117
+ bundleType: process.env.NODE_ENV === "development" ? 1 : 0,
118
+ version: "0.0.1",
119
+ rendererPackageName: "peachy"
120
+ });
121
+
122
+ //#endregion
123
+ export { render };
@@ -0,0 +1,6 @@
1
+ import * as React from "react/jsx-dev-runtime";
2
+
3
+ //#region src/jsx-dev-runtime.d.ts
4
+ declare const jsxDEV: typeof React.jsxDEV;
5
+ //#endregion
6
+ export { jsxDEV };
@@ -0,0 +1,11 @@
1
+ import { transformType } from "./jsx-utils.mjs";
2
+ import * as React from "react/jsx-dev-runtime";
3
+
4
+ //#region src/jsx-dev-runtime.ts
5
+ const jsxDEV = (type, ...args) => {
6
+ const transformedType = transformType(type);
7
+ return React.jsxDEV(transformedType, ...args);
8
+ };
9
+
10
+ //#endregion
11
+ export { jsxDEV };
@@ -0,0 +1,9 @@
1
+ import * as react0 from "react";
2
+ import * as React from "react/jsx-runtime";
3
+
4
+ //#region src/jsx-runtime.d.ts
5
+ declare const jsx: typeof React.jsx;
6
+ declare const jsxs: typeof React.jsxs;
7
+ declare const Fragment: react0.ExoticComponent<react0.FragmentProps>;
8
+ //#endregion
9
+ export { Fragment, jsx, jsxs };
@@ -0,0 +1,16 @@
1
+ import { transformType } from "./jsx-utils.mjs";
2
+ import * as React from "react/jsx-runtime";
3
+
4
+ //#region src/jsx-runtime.ts
5
+ const jsx = (type, ...args) => {
6
+ const transformedType = transformType(type);
7
+ return React.jsx(transformedType, ...args);
8
+ };
9
+ const jsxs = (type, ...args) => {
10
+ const transformedType = transformType(type);
11
+ return React.jsxs(transformedType, ...args);
12
+ };
13
+ const Fragment = React.Fragment;
14
+
15
+ //#endregion
16
+ export { Fragment, jsx, jsxs };
@@ -0,0 +1,4 @@
1
+ //#region src/jsx-utils.d.ts
2
+ declare const transformType: <T>(type: T) => T;
3
+ //#endregion
4
+ export { transformType };
@@ -0,0 +1,18 @@
1
+ import { typeMap } from "./type-map.mjs";
2
+ import GObject from "gi://GObject?version=2.0";
3
+
4
+ //#region src/jsx-utils.ts
5
+ function isGtkWidgetClass(type) {
6
+ return typeof type === "function" && type.prototype instanceof GObject.Object;
7
+ }
8
+ const transformType = (type) => {
9
+ if (isGtkWidgetClass(type)) {
10
+ const type_name = GObject.type_name(type.$gtype);
11
+ if (!typeMap.has(type_name)) typeMap.set(type_name, type);
12
+ return type_name;
13
+ }
14
+ return type;
15
+ };
16
+
17
+ //#endregion
18
+ export { transformType };
@@ -0,0 +1,4 @@
1
+ //#region src/type-map.d.ts
2
+ declare const typeMap: Map<string, any>;
3
+ //#endregion
4
+ export { typeMap };
@@ -0,0 +1,5 @@
1
+ //#region src/type-map.ts
2
+ const typeMap = /* @__PURE__ */ new Map();
3
+
4
+ //#endregion
5
+ export { typeMap };
@@ -0,0 +1,8 @@
1
+ //#region src/types.d.ts
2
+ type Props = Record<string, unknown>;
3
+ type Listener = (...args: any[]) => any;
4
+ interface ReactionMetadata {
5
+ childType?: string;
6
+ }
7
+ //#endregion
8
+ export { Listener, Props, ReactionMetadata };
@@ -0,0 +1,53 @@
1
+ import { extraMap } from "../extra.mjs";
2
+ import { getGtype } from "./type.mjs";
3
+ import GObject from "gi://GObject?version=2.0";
4
+
5
+ //#region src/utilities/children.ts
6
+ function _appendChildIfPossible(parentInstance, child) {
7
+ const gtype = getGtype(parentInstance);
8
+ for (const [type, props] of extraMap) if (GObject.type_is_a(gtype, type)) {
9
+ if (props.appendChild) {
10
+ props.appendChild(parentInstance, child);
11
+ return true;
12
+ }
13
+ }
14
+ if ("child" in parentInstance) {
15
+ parentInstance.child = child;
16
+ return true;
17
+ }
18
+ if ("content" in parentInstance) {
19
+ parentInstance.content = child;
20
+ return true;
21
+ }
22
+ return false;
23
+ }
24
+ function appendChild(parentInstance, child) {
25
+ if (_appendChildIfPossible(parentInstance, child)) return;
26
+ child.set_parent(parentInstance);
27
+ }
28
+ function insertBefore(parentInstance, child, sibling) {
29
+ const gtype = getGtype(parentInstance);
30
+ for (const [type, props] of extraMap) if (GObject.type_is_a(gtype, type)) {
31
+ if (props.insertBefore) return props.insertBefore(parentInstance, child, sibling);
32
+ }
33
+ if (_appendChildIfPossible(parentInstance, child)) return;
34
+ child.insert_before(parentInstance, sibling);
35
+ }
36
+ function removeChild(parentInstance, child) {
37
+ const gtype = getGtype(parentInstance);
38
+ for (const [type, props] of extraMap) if (GObject.type_is_a(gtype, type)) {
39
+ if (props.removeChild) return props.removeChild(parentInstance, child);
40
+ }
41
+ if ("child" in parentInstance) {
42
+ parentInstance.child = void 0;
43
+ return;
44
+ }
45
+ if ("content" in parentInstance) {
46
+ parentInstance.content = void 0;
47
+ return;
48
+ }
49
+ child.unparent();
50
+ }
51
+
52
+ //#endregion
53
+ export { appendChild, insertBefore, removeChild };
@@ -0,0 +1,45 @@
1
+ import { setMetadataFromProps } from "./metadata.mjs";
2
+ import GObject from "gi://GObject?version=2.0";
3
+
4
+ //#region src/utilities/diff.ts
5
+ const INTERNAL_PROP_NAMES = [
6
+ "childType",
7
+ "ref",
8
+ "key"
9
+ ];
10
+ const getEventName = (event) => event.substring(2).replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "").replace(/:-/, "::");
11
+ const isEvent = (key) => key.startsWith("on");
12
+ const isProperty = (key) => key !== "children" && !isEvent(key) && !INTERNAL_PROP_NAMES.includes(key);
13
+ const isNew = (prev, next) => (key) => prev[key] !== next[key];
14
+ const isGone = (prev, next) => (key) => !(key in next);
15
+ function updateWidget(widget, prev, next) {
16
+ Object.keys(prev).filter(isEvent).filter((key) => !(key in next) || isNew(prev, next)(key)).forEach((name) => {
17
+ if (GObject.signal_handlers_disconnect_by_func(widget, prev[name]) === 0) console.warn(`No signal handler found for ${name}`);
18
+ });
19
+ Object.keys(prev).filter(isProperty).filter(isGone(prev, next)).forEach((name) => {
20
+ widget[name] = void 0;
21
+ });
22
+ Object.keys(next).filter(isProperty).filter(isNew(prev, next)).forEach((name) => {
23
+ widget[name] = next[name];
24
+ });
25
+ Object.keys(next).filter(isEvent).filter(isNew(prev, next)).forEach((name) => {
26
+ const eventType = getEventName(name);
27
+ widget.connect(eventType, next[name]);
28
+ });
29
+ setMetadataFromProps(widget, next);
30
+ }
31
+ function getProperties(props) {
32
+ return Object.keys(props).filter(isProperty).reduce((acc, key) => {
33
+ acc[key] = props[key];
34
+ return acc;
35
+ }, {});
36
+ }
37
+ function getEventListeners(props) {
38
+ return Object.keys(props).filter(isEvent).reduce((acc, key) => {
39
+ acc[key] = props[key];
40
+ return acc;
41
+ }, {});
42
+ }
43
+
44
+ //#endregion
45
+ export { getEventListeners, getEventName, getProperties, updateWidget };
@@ -0,0 +1,17 @@
1
+ //#region src/utilities/metadata.ts
2
+ const METADATA_KEY = "rx:metadata";
3
+ function parseMetadataFromProps(props) {
4
+ return { childType: props.childType };
5
+ }
6
+ function getMetadata(object) {
7
+ return Reflect.getMetadata(METADATA_KEY, object) ?? null;
8
+ }
9
+ function setMetadata(object, value) {
10
+ Reflect.defineMetadata(METADATA_KEY, value, object);
11
+ }
12
+ function setMetadataFromProps(object, props) {
13
+ setMetadata(object, parseMetadataFromProps(props));
14
+ }
15
+
16
+ //#endregion
17
+ export { getMetadata, setMetadataFromProps };
@@ -0,0 +1,7 @@
1
+ //#region src/utilities/type.ts
2
+ function getGtype(obj) {
3
+ return obj.constructor.$gtype;
4
+ }
5
+
6
+ //#endregion
7
+ export { getGtype };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@peachy/react",
3
+ "version": "0.0.1",
4
+ "description": "Run GJS applications with react",
5
+ "main": "./dist/index.mjs",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.mjs",
9
+ "types": "./dist/index.d.mts"
10
+ },
11
+ "./*": {
12
+ "import": "./dist/*.mjs",
13
+ "types": "./dist/*.d.mts"
14
+ },
15
+ "./tsconfig": "./tsconfig.json"
16
+ },
17
+ "author": "Angelo Verlain <hey@vixalien.com>",
18
+ "dependencies": {
19
+ "@peachy/types": "^2025.1.18",
20
+ "react": "^19.2.0",
21
+ "react-reconciler": "^0.33.0",
22
+ "reflect-metadata": "^0.2.2",
23
+ "@peachy/core": "0.0.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^19.2.7",
27
+ "@types/react-reconciler": "^0.32.3",
28
+ "tsdown": "0.20.0-beta.3",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "tsconfig.json"
34
+ ],
35
+ "license": "MIT",
36
+ "scripts": {
37
+ "build": "tsdown"
38
+ },
39
+ "types": "./dist/index.d.mts"
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@peachy/core/tsconfig",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "jsxImportSource": "@peachy/react",
6
+ },
7
+ "include": [
8
+ "./node_modules/@peachy/types/types/index.d.ts",
9
+ "${configDir}/src/**/*",
10
+ ],
11
+ }