@peachy/plugin-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,3 @@
1
+ # @peachy/plugin-react
2
+
3
+ A set of Rolldown plugins that enable React Fast Refresh to work with GTK+React.
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "rolldown";
2
+
3
+ //#region src/index.d.ts
4
+ declare function react(port: number): Plugin[];
5
+ //#endregion
6
+ export { react };
package/dist/index.mjs ADDED
@@ -0,0 +1,105 @@
1
+ import { babel } from "@rollup/plugin-babel";
2
+ import ReactFreshBabelPlugin from "react-refresh/babel";
3
+ import BabelJSX from "@babel/plugin-syntax-jsx";
4
+ import BabelTS from "@babel/plugin-syntax-typescript";
5
+ import { resolve } from "path";
6
+ import { readFileSync } from "fs";
7
+
8
+ //#region src/entrypoint.ts
9
+ function addHmrEntrypoint(input) {
10
+ const PREFIX = "\0virtual:hmr-wrapper:";
11
+ if (typeof input === "string") return PREFIX + input;
12
+ else if (Array.isArray(input)) return input.map((entry) => PREFIX + entry);
13
+ else if (typeof input === "object") {
14
+ const wrapped = {};
15
+ for (const [key, value] of Object.entries(input)) wrapped[key] = PREFIX + value;
16
+ return wrapped;
17
+ }
18
+ return input;
19
+ }
20
+ function createHmrEntrypointPlugin() {
21
+ const PREFIX = "\0virtual:hmr-wrapper:";
22
+ const resolvedPath = resolve(import.meta.dirname, "./templates/hmr-entrypoint.ts");
23
+ return {
24
+ name: "hmr2-entrypoint",
25
+ options(opts) {
26
+ return {
27
+ ...opts,
28
+ input: addHmrEntrypoint(opts.input)
29
+ };
30
+ },
31
+ resolveId(id) {
32
+ if (id.startsWith(PREFIX)) return id;
33
+ return null;
34
+ },
35
+ load(id) {
36
+ if (id.startsWith(PREFIX)) {
37
+ const actualEntry = id.slice(21);
38
+ return `import '${resolvedPath}';\nimport '${resolve(process.cwd(), actualEntry)}';`;
39
+ }
40
+ return null;
41
+ }
42
+ };
43
+ }
44
+
45
+ //#endregion
46
+ //#region src/runtime.ts
47
+ function hmrRuntimePlugin(port) {
48
+ const runtime = resolve(import.meta.dirname, "./templates/hmr-runtime.ts");
49
+ return {
50
+ name: "hmr-runtime-plugin",
51
+ resolveId(id) {
52
+ if (id === "\0virtual:hmr-runtime") return runtime;
53
+ },
54
+ async transform(code, id) {
55
+ if (id === runtime) return { code: code.replace("$port$", port.toString()) };
56
+ }
57
+ };
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/wrapper.ts
62
+ function reactRefreshWrapperPlugin() {
63
+ const template = readFileSync(resolve(import.meta.dirname, "./templates/hmr-wrapper.ts"), "utf-8");
64
+ return {
65
+ name: "react-refresh-wrapper",
66
+ transform(code, id) {
67
+ if (!id.startsWith(process.cwd()) || !id.match(/\.[tj]sx?$/) || id.includes("node_modules") || id.startsWith("gi://*")) return null;
68
+ return {
69
+ code: template.replace("$sourceCode$", code).replaceAll("$hmrId$", JSON.stringify(id)),
70
+ map: null
71
+ };
72
+ }
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ //#region src/index.ts
78
+ function react(port) {
79
+ return [
80
+ babel({
81
+ plugins: [
82
+ BabelJSX,
83
+ [BabelTS, { isTSX: true }],
84
+ ReactFreshBabelPlugin
85
+ ],
86
+ babelHelpers: "bundled",
87
+ ignore: [/node_modules/],
88
+ extensions: [
89
+ ".js",
90
+ ".jsx",
91
+ ".es6",
92
+ ".es",
93
+ ".mjs",
94
+ ".ts",
95
+ ".tsx"
96
+ ]
97
+ }),
98
+ createHmrEntrypointPlugin(),
99
+ reactRefreshWrapperPlugin(),
100
+ hmrRuntimePlugin(port)
101
+ ];
102
+ }
103
+
104
+ //#endregion
105
+ export { react };
@@ -0,0 +1,9 @@
1
+ /// @ts-nocheck
2
+
3
+ import * as RefreshRuntime from "react-refresh";
4
+
5
+ globalThis.$RefreshRuntime$ = RefreshRuntime;
6
+
7
+ globalThis.$RefreshRuntime$.injectIntoGlobalHook(globalThis);
8
+ globalThis.$RefreshReg$ = () => {};
9
+ globalThis.$RefreshSig$ = () => (type) => type;
@@ -0,0 +1,164 @@
1
+ /// @ts-nocheck
2
+
3
+ import Gio from "gi://Gio?version=2.0";
4
+ import GLib from "gi://GLib?version=2.0";
5
+ import Soup from "gi://Soup?version=3.0";
6
+
7
+ Gio._promisify(Soup.Session.prototype, "websocket_connect_async");
8
+
9
+ declare global {
10
+ var __hmr__: {
11
+ contexts: Record<string, ImportMetaHot>;
12
+ };
13
+ }
14
+
15
+ // To prevent the connection being garbage collected
16
+ let connection;
17
+
18
+ if (!globalThis.__hmr__) {
19
+ globalThis.__hmr__ = {
20
+ contexts: {},
21
+ };
22
+
23
+ const socketURL = "ws://localhost:$port$/";
24
+ const session = new Soup.Session({
25
+ idleTimeout: GLib.MAXINT32,
26
+ maxConns: 1,
27
+ timeout: GLib.MAXINT32,
28
+ });
29
+
30
+ session
31
+ .websocket_connect_async(
32
+ Soup.Message.new("GET", socketURL.toString()),
33
+ null,
34
+ null,
35
+ GLib.PRIORITY_DEFAULT,
36
+ null,
37
+ )
38
+ .then((conn) => {
39
+ connection = conn;
40
+ console.log("[HMR] Connected to HMR server at", socketURL);
41
+
42
+ connection.set_keepalive_interval(1);
43
+
44
+ connection.connect("message", async (_, _type, event) => {
45
+ const decoder = new TextDecoder();
46
+ const payload = JSON.parse(
47
+ decoder.decode(event.toArray()),
48
+ ) as HMRMessage;
49
+
50
+ switch (payload?.type) {
51
+ case "reload":
52
+ console.info("[HMR] Reloading...");
53
+ // TODO: handle this
54
+ break;
55
+ case "hmr":
56
+ {
57
+ if (!payload.updates?.length) return;
58
+
59
+ let anyAccepted = false;
60
+ for (const update of payload.updates) {
61
+ if (globalThis.__hmr__.contexts[update.id]) {
62
+ try {
63
+ const module = await import(
64
+ "file://" + update.url + "?t=" + Date.now()
65
+ );
66
+
67
+ const accepted =
68
+ globalThis.__hmr__.contexts[update.id].emit(module);
69
+
70
+ if (accepted) {
71
+ console.info("[HMR] Update accepted by", update.id);
72
+ anyAccepted = true;
73
+ }
74
+ } catch (err) {
75
+ console.debug("[HMR] Error when hot-refreshing", err);
76
+ }
77
+ }
78
+ }
79
+
80
+ if (!anyAccepted) {
81
+ console.info("[HMR] Update rejected, reloading...");
82
+ connection.send_text(JSON.stringify({ type: "reload" }));
83
+ }
84
+ }
85
+ break;
86
+ }
87
+ });
88
+
89
+ connection.connect("closed", () => {
90
+ console.log("[HMR] Connection closed");
91
+ });
92
+ })
93
+ .catch(console.error);
94
+ }
95
+
96
+ export function createHotContext(id: string): ImportMetaHot {
97
+ let callback: undefined | ((mod: ModuleNamespace) => boolean);
98
+ let disposed = false;
99
+
100
+ const hot = {
101
+ accept: (cb) => {
102
+ if (disposed) {
103
+ throw new Error("import.meta.hot.accept() called after dispose()");
104
+ }
105
+ if (callback) {
106
+ throw new Error("import.meta.hot.accept() called multiple times");
107
+ }
108
+ callback = cb;
109
+ },
110
+ dispose: () => {
111
+ disposed = true;
112
+ callback = undefined;
113
+ },
114
+ emit(self: ModuleNamespace) {
115
+ if (disposed) {
116
+ throw new Error("import.meta.hot.emit() called after dispose()");
117
+ }
118
+
119
+ if (callback) {
120
+ return !!callback(self);
121
+ }
122
+
123
+ return false;
124
+ },
125
+ };
126
+
127
+ if (globalThis.__hmr__.contexts[id]) {
128
+ globalThis.__hmr__.contexts[id].dispose();
129
+ globalThis.__hmr__.contexts[id] = undefined;
130
+ }
131
+ globalThis.__hmr__.contexts[id] = hot;
132
+
133
+ return hot;
134
+ }
135
+
136
+ // From: https://github.com/facebook/metro/blob/e232f39fcd82defb421899a8c9b2b3accee90e86/packages/metro-runtime/src/polyfills/require.js#L948
137
+ export function isReactRefreshBoundary(moduleExports) {
138
+ if (globalThis.$RefreshRuntime$.isLikelyComponentType(moduleExports)) {
139
+ return true;
140
+ }
141
+
142
+ if (moduleExports == null || typeof moduleExports !== "object") {
143
+ // Exit if we can't iterate over exports.
144
+ return false;
145
+ }
146
+ let hasExports = false;
147
+ let areAllExportsComponents = true;
148
+ for (const key in moduleExports) {
149
+ hasExports = true;
150
+ if (key === "__esModule") {
151
+ continue;
152
+ }
153
+ const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
154
+ if (desc && desc.get) {
155
+ // Don't invoke getters as they may have side effects.
156
+ return false;
157
+ }
158
+ const exportValue = moduleExports[key];
159
+ if (!globalThis.$RefreshRuntime$.isLikelyComponentType(exportValue)) {
160
+ areAllExportsComponents = false;
161
+ }
162
+ }
163
+ return hasExports && areAllExportsComponents;
164
+ }
@@ -0,0 +1,40 @@
1
+ /// @ts-nocheck
2
+
3
+ import * as __hmr__ from "\0virtual:hmr-runtime";
4
+
5
+ if (import.meta) {
6
+ import.meta.hot = __hmr__.createHotContext($hmrId$);
7
+ }
8
+
9
+ var NO_FAST_REFRESH =
10
+ !globalThis.$RefreshReg$ ||
11
+ !globalThis.$RefreshSig$ ||
12
+ !globalThis.$RefreshRuntime$;
13
+ if (NO_FAST_REFRESH) {
14
+ console.warn(
15
+ "[HMR]: React Refresh is not enabled. Please ensure that you have installed the necessary dependencies and enabled the plugin in your webpack configuration.",
16
+ );
17
+ } else {
18
+ var prevRefreshReg = globalThis.$RefreshReg$;
19
+ var prevRefreshSig = globalThis.$RefreshSig$;
20
+ globalThis.$RefreshReg$ = (type, id) => {
21
+ const fullId = $hmrId$ + "#" + id;
22
+ globalThis.$RefreshRuntime$.register(type, fullId);
23
+ };
24
+ globalThis.$RefreshSig$ =
25
+ globalThis.$RefreshRuntime$.createSignatureFunctionForTransform;
26
+ }
27
+
28
+ $sourceCode$;
29
+
30
+ if (!NO_FAST_REFRESH) {
31
+ globalThis.$RefreshReg$ = prevRefreshReg;
32
+ globalThis.$RefreshSig$ = prevRefreshSig;
33
+ import.meta.hot.accept((moduleExports) => {
34
+ if (__hmr__.isReactRefreshBoundary(moduleExports)) {
35
+ // TODO: debounce this
36
+ globalThis.$RefreshRuntime$.performReactRefresh();
37
+ return true;
38
+ }
39
+ });
40
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@peachy/plugin-react",
3
+ "version": "0.0.1",
4
+ "description": "HMR for react",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "keywords": [],
8
+ "author": "Angelo Verlain <hey@vixalien.com>",
9
+ "license": "MIT",
10
+ "devDependencies": {
11
+ "rolldown": "1.0.0-beta.58",
12
+ "tsdown": "0.20.0-beta.3",
13
+ "typescript": "^5.9.3"
14
+ },
15
+ "dependencies": {
16
+ "@babel/plugin-syntax-jsx": "^7.28.6",
17
+ "@babel/plugin-syntax-typescript": "^7.28.6",
18
+ "@rollup/plugin-babel": "^6.1.0",
19
+ "react-refresh": "^0.18.0"
20
+ },
21
+ "peerDependencies": {
22
+ "rolldown": "1.0.0-beta.58"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsdown src/index.ts --dts && mkdir -p dist/templates && cp src/templates/* dist/templates"
29
+ },
30
+ "types": "./dist/index.d.mts"
31
+ }