@nitronjs/framework 0.3.3 → 0.3.4

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/lib/HMR/Server.js CHANGED
@@ -1,170 +1,93 @@
1
- import { Server as SocketServer } from "socket.io";
2
- import { createRequire } from "module";
3
- import path from "path";
4
- import fs from "fs";
5
- import { fileURLToPath } from "url";
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
-
9
- /**
10
- * HMR (Hot Module Replacement) server for development mode.
11
- * Manages WebSocket connections and broadcasts file change events to connected clients.
12
- *
13
- * @example
14
- * // In HTTP Server
15
- * HMR.registerRoutes(fastify);
16
- * HMR.setup(httpServer);
17
- *
18
- * // Emit updates
19
- * HMR.emitChange({ changeType: "page", file: "Site/Home.tsx" });
20
- * HMR.emitChange({ changeType: "css", file: "global.css" });
21
- */
22
- class HMRServer {
23
- #io = null;
24
- #connections = 0;
25
- #clientScript = null;
26
-
27
- // Public Methods
28
-
29
- /**
30
- * Registers socket.io client script route in Fastify.
31
- * Must be called before Router.setup() to ensure the route is registered.
32
- * @param {import("fastify").FastifyInstance} fastify
33
- */
34
- registerRoutes(fastify) {
35
- this.#clientScript = this.#findSocketIoClient();
36
- const cachedScript = this.#clientScript ? fs.readFileSync(this.#clientScript, "utf-8") : null;
37
-
38
- fastify.get("/__nitron_client/socket.io.js", (req, reply) => {
39
- if (!cachedScript) {
40
- return reply.code(503).send("// HMR disabled: socket.io client not found");
41
- }
42
-
43
- reply.type("application/javascript").send(cachedScript);
44
- });
45
- }
46
-
47
- /**
48
- * Initializes the WebSocket server.
49
- * @param {import("http").Server} httpServer - Node.js HTTP server instance.
50
- */
51
- setup(httpServer) {
52
- if (this.#io) return;
53
-
54
- this.#io = new SocketServer(httpServer, {
55
- path: "/__nitron_hmr",
56
- transports: ["websocket"],
57
- cors: { origin: "*" },
58
- pingTimeout: 60000,
59
- pingInterval: 25000,
60
- serveClient: false,
61
- allowEIO3: true
62
- });
63
-
64
- this.#io.on("connection", (socket) => {
65
- this.#connections++;
66
- socket.on("disconnect", () => this.#connections--);
67
- });
68
- }
69
-
70
- /**
71
- * Whether the HMR server is ready and accepting connections.
72
- * @returns {boolean}
73
- */
74
- get isReady() {
75
- return this.#io !== null;
76
- }
77
-
78
- /**
79
- * Emits a unified change event for RSC-based hot updates.
80
- * @param {object} data
81
- * @param {string} data.changeType - "page" | "layout" | "css"
82
- * @param {boolean} [data.cssChanged] - Whether CSS also changed (Tailwind rebuild)
83
- * @param {string} [data.file] - Relative path of the changed file (for logging)
84
- */
85
- emitChange(data) {
86
- if (!this.#io) return;
87
-
88
- this.#io.emit("hmr:change", {
89
- changeType: data.changeType,
90
- cssChanged: data.cssChanged || false,
91
- file: data.file || null,
92
- timestamp: Date.now()
93
- });
94
- }
95
-
96
- /**
97
- * Emits a full page reload event.
98
- * @param {string} reason - Reason for the reload (shown in dev tools).
99
- */
100
- emitReload(reason) {
101
- if (!this.#io) return;
102
-
103
- this.#io.emit("hmr:reload", {
104
- reason,
105
- timestamp: Date.now()
106
- });
107
- }
108
-
109
- /**
110
- * Emits a build error event to show error overlay in browser.
111
- * @param {Error|string} error - The error that occurred.
112
- * @param {string} [filePath] - Path to the file that caused the error.
113
- */
114
- emitError(error, filePath) {
115
- if (!this.#io) return;
116
-
117
- this.#io.emit("hmr:error", {
118
- file: filePath,
119
- message: String(error?.message || error),
120
- timestamp: Date.now()
121
- });
122
- }
123
-
124
- /**
125
- * Closes the WebSocket server and cleans up resources.
126
- */
127
- close() {
128
- if (this.#io) {
129
- this.#io.close();
130
- this.#io = null;
131
- }
132
-
133
- this.#connections = 0;
134
- }
135
-
136
- // Private Methods
137
-
138
- /**
139
- * Finds socket.io client script.
140
- * Searches framework node_modules and walks up directory tree for monorepo support.
141
- * @returns {string|null} Path to socket.io.min.js or null if not found
142
- */
143
- #findSocketIoClient() {
144
- const clientFile = "client-dist/socket.io.min.js";
145
-
146
- // 1. Try resolving from framework package location (most common case)
147
- try {
148
- const frameworkRequire = createRequire(import.meta.url);
149
- const socketIoDir = path.dirname(frameworkRequire.resolve("socket.io/package.json"));
150
- const clientPath = path.join(socketIoDir, clientFile);
151
- if (fs.existsSync(clientPath)) return clientPath;
152
- }
153
- catch {}
154
-
155
- // 2. Walk up from framework location for monorepo setups
156
- let currentDir = __dirname;
157
- for (let i = 0; i < 5; i++) {
158
- const clientPath = path.join(currentDir, "node_modules", "socket.io", clientFile);
159
- if (fs.existsSync(clientPath)) return clientPath;
160
-
161
- const parentDir = path.dirname(currentDir);
162
- if (parentDir === currentDir) break;
163
- currentDir = parentDir;
164
- }
165
-
166
- return null;
167
- }
168
- }
169
-
170
- export default new HMRServer();
1
+ import { WebSocketServer } from "ws";
2
+
3
+ class HMRServer {
4
+ #wss = null;
5
+ #clients = new Set();
6
+
7
+ registerRoutes(fastify) {
8
+ // No routes needed — native WebSocket doesn't need a client script endpoint
9
+ }
10
+
11
+ setup(httpServer) {
12
+ if (this.#wss) return;
13
+
14
+ this.#wss = new WebSocketServer({
15
+ server: httpServer,
16
+ path: "/__nitron_hmr"
17
+ });
18
+
19
+ this.#wss.on("connection", (ws) => {
20
+ this.#clients.add(ws);
21
+
22
+ ws.on("close", () => {
23
+ this.#clients.delete(ws);
24
+ });
25
+ });
26
+ }
27
+
28
+ get isReady() {
29
+ return this.#wss !== null;
30
+ }
31
+
32
+ emitChange(data) {
33
+ this.#broadcast({
34
+ type: "change",
35
+ changeType: data.changeType,
36
+ cssChanged: data.cssChanged || false,
37
+ file: data.file || null,
38
+ timestamp: Date.now()
39
+ });
40
+ }
41
+
42
+ emitReload(reason) {
43
+ this.#broadcast({
44
+ type: "reload",
45
+ reason,
46
+ timestamp: Date.now()
47
+ });
48
+ }
49
+
50
+ emitFastRefresh(data) {
51
+ this.#broadcast({
52
+ type: "fast-refresh",
53
+ chunks: data.chunks || [],
54
+ cssChanged: data.cssChanged || false,
55
+ timestamp: data.timestamp || Date.now()
56
+ });
57
+ }
58
+
59
+ emitError(error, filePath) {
60
+ this.#broadcast({
61
+ type: "error",
62
+ file: filePath,
63
+ message: String(error?.message || error),
64
+ timestamp: Date.now()
65
+ });
66
+ }
67
+
68
+ close() {
69
+ if (this.#wss) {
70
+ for (const client of this.#clients) {
71
+ client.close();
72
+ }
73
+
74
+ this.#clients.clear();
75
+ this.#wss.close();
76
+ this.#wss = null;
77
+ }
78
+ }
79
+
80
+ #broadcast(data) {
81
+ if (!this.#wss) return;
82
+
83
+ const message = JSON.stringify(data);
84
+
85
+ for (const client of this.#clients) {
86
+ if (client.readyState === 1) {
87
+ client.send(message);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ export default new HMRServer();
@@ -21,6 +21,9 @@ export async function start() {
21
21
  case "change":
22
22
  HMRServer.emitChange(msg);
23
23
  break;
24
+ case "fast-refresh":
25
+ HMRServer.emitFastRefresh(msg);
26
+ break;
24
27
  case "reload":
25
28
  HMRServer.emitReload(msg.reason);
26
29
  break;