@nitronjs/framework 0.1.24 → 0.2.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.
Files changed (37) hide show
  1. package/lib/Build/CssBuilder.js +129 -0
  2. package/lib/Build/FileAnalyzer.js +395 -0
  3. package/lib/Build/HydrationBuilder.js +173 -0
  4. package/lib/Build/Manager.js +290 -943
  5. package/lib/Build/colors.js +10 -0
  6. package/lib/Build/jsxRuntime.js +116 -0
  7. package/lib/Build/plugins.js +264 -0
  8. package/lib/Console/Commands/BuildCommand.js +6 -5
  9. package/lib/Console/Commands/DevCommand.js +151 -311
  10. package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
  11. package/lib/Console/Stubs/page-hydration.tsx +9 -10
  12. package/lib/Console/Stubs/vendor-dev.tsx +50 -0
  13. package/lib/Core/Environment.js +29 -2
  14. package/lib/Core/Paths.js +12 -4
  15. package/lib/Database/Drivers/MySQLDriver.js +5 -4
  16. package/lib/Database/QueryBuilder.js +2 -3
  17. package/lib/Filesystem/Manager.js +32 -7
  18. package/lib/HMR/Server.js +87 -0
  19. package/lib/Http/Server.js +9 -5
  20. package/lib/Logging/Manager.js +68 -18
  21. package/lib/Route/Loader.js +3 -4
  22. package/lib/Route/Manager.js +24 -3
  23. package/lib/Runtime/Entry.js +26 -1
  24. package/lib/Session/File.js +18 -7
  25. package/lib/View/Client/hmr-client.js +166 -0
  26. package/lib/View/Client/spa.js +142 -0
  27. package/lib/View/Layout.js +94 -0
  28. package/lib/View/Manager.js +390 -46
  29. package/lib/index.d.ts +55 -0
  30. package/package.json +2 -1
  31. package/skeleton/.env.example +0 -2
  32. package/skeleton/app/Controllers/HomeController.js +27 -3
  33. package/skeleton/config/app.js +15 -14
  34. package/skeleton/config/session.js +1 -1
  35. package/skeleton/globals.d.ts +3 -63
  36. package/skeleton/resources/views/Site/Home.tsx +274 -50
  37. package/skeleton/tsconfig.json +5 -1
@@ -3,383 +3,223 @@ import dotenv from "dotenv";
3
3
  import { spawn } from "child_process";
4
4
  import chokidar from "chokidar";
5
5
  import path from "path";
6
+ import fs from "fs";
6
7
  import Paths from "../../Core/Paths.js";
8
+ import Environment from "../../Core/Environment.js";
9
+ import Builder from "../../Build/Manager.js";
7
10
 
8
11
  dotenv.config({ quiet: true });
9
-
10
- // ─────────────────────────────────────────────────────────────────────────────
11
- // Constants
12
- // ─────────────────────────────────────────────────────────────────────────────
13
-
14
- const FRAMEWORK_DIR = Paths.framework;
15
- const PROJECT_DIR = Paths.project;
16
-
17
- const COLORS = {
18
- reset: "\x1b[0m",
19
- bold: "\x1b[1m",
20
- dim: "\x1b[2m",
21
- red: "\x1b[31m",
22
- green: "\x1b[32m",
23
- yellow: "\x1b[33m",
24
- blue: "\x1b[34m",
25
- magenta: "\x1b[35m",
26
- cyan: "\x1b[36m"
12
+ Environment.setDev(true);
13
+
14
+ const C = { r: "\x1b[0m", d: "\x1b[2m", red: "\x1b[31m", g: "\x1b[32m", y: "\x1b[33m", b: "\x1b[34m", m: "\x1b[35m", c: "\x1b[36m" };
15
+ const ICONS = {
16
+ info: `${C.b}ℹ${C.r} `,
17
+ ok: `${C.g}✓${C.r} `,
18
+ err: `${C.red}✗${C.r} `,
19
+ build: `${C.m}⟳${C.r} `,
20
+ hmr: `${C.c}⚡${C.r}`,
21
+ watch: `${C.y}○${C.r} `
27
22
  };
28
23
 
29
- // Watch configuration (relative to PROJECT_DIR)
30
- const WATCH_CONFIG = {
31
- // Files that trigger only a build (no server restart)
32
- // Controllers are hot-reloaded via dynamic import in dev mode
33
- buildOnly: [
34
- "resources/views/**/*.tsx",
35
- "resources/views/**/*.ts",
36
- "resources/css/**/*.css",
37
- "app/Controllers/*.js",
38
- "app/Middlewares/*.js",
39
- "app/Middlewares/**/*.js"
40
- ],
41
-
42
- // Files that trigger a full server restart
43
- fullRestart: [
44
- "config/**/*.js",
45
- "app/Kernel.js",
46
- "app/Models/**/*.js",
47
- "routes/**/*.js",
48
- "app.js"
49
- ],
50
-
51
- // Framework files (relative to FRAMEWORK_DIR) - trigger build only
52
- frameworkBuildOnly: [
53
- "lib/View/Templates/**/*.tsx",
54
- "lib/View/Templates/**/*.ts"
55
- ],
56
-
57
- // Ignored patterns
58
- ignore: [
59
- "**/node_modules/**",
60
- "**/build/**",
61
- "**/storage/app/public/**",
62
- "**/storage/framework/**",
63
- "**/storage/logs/**",
64
- "**/.nitron/**",
65
- "**/.git/**",
66
- "**/package-lock.json"
67
- ]
24
+ const PATTERNS = {
25
+ views: ["resources/views/**/*.tsx"],
26
+ css: ["resources/css/**/*.css"],
27
+ restart: ["config/**/*.js", "app/Kernel.js", "app/Models/**/*.js", "routes/**/*.js", "app.js", "app/Controllers/**/*.js", "app/Middlewares/**/*.js"],
28
+ framework: ["lib/View/Templates/**/*.tsx"]
68
29
  };
69
30
 
70
- // ─────────────────────────────────────────────────────────────────────────────
71
- // Dev Server Class
72
- // ─────────────────────────────────────────────────────────────────────────────
73
-
74
31
  class DevServer {
75
- #serverProcess = null;
76
- #isBuilding = false;
77
- #pendingRestart = false;
78
- #pendingBuild = false;
79
- #buildDebounceTimer = null;
80
- #restartDebounceTimer = null;
81
- #watcherReady = false;
82
-
83
- constructor() {
84
- //
32
+ #proc = null;
33
+ #building = false;
34
+ #builder = new Builder();
35
+ #debounce = { build: null, restart: null };
36
+
37
+ #log(icon, msg, extra) {
38
+ const t = `${C.d}${new Date().toLocaleTimeString()}${C.r}`;
39
+ console.log(`${t} ${ICONS[icon] || ICONS.info} ${msg}${extra ? ` ${C.d}(${extra})${C.r}` : ""}`);
85
40
  }
86
41
 
87
- // ─────────────────────────────────────────────────────────────────────────
88
- // Logging
89
- // ─────────────────────────────────────────────────────────────────────────
42
+ async #build(only = null) {
43
+ if (this.#building) return { success: false, skipped: true };
44
+ this.#building = true;
45
+ this.#log("build", only ? `Building ${only}...` : "Building...");
90
46
 
91
- #log(type, message, details = null) {
92
- const now = new Date();
93
- const time = `${COLORS.dim}${now.toLocaleTimeString()}${COLORS.reset}`;
94
-
95
- const icons = {
96
- info: `${COLORS.blue}[i]${COLORS.reset}`,
97
- success: `${COLORS.green}[✓]${COLORS.reset}`,
98
- warning: `${COLORS.yellow}[!]${COLORS.reset}`,
99
- error: `${COLORS.red}[✗]${COLORS.reset}`,
100
- build: `${COLORS.magenta}[~]${COLORS.reset}`,
101
- reload: `${COLORS.cyan}[↻]${COLORS.reset}`,
102
- watch: `${COLORS.yellow}[○]${COLORS.reset}`
103
- };
47
+ try {
48
+ const result = await this.#builder.run(only, true, true);
49
+ this.#building = false;
104
50
 
105
- const icon = icons[type] || icons.info;
106
- let output = `${time} ${icon} ${message}`;
107
-
108
- if (details) {
109
- output += ` ${COLORS.dim}(${details})${COLORS.reset}`;
51
+ if (result.success) {
52
+ this.#log("ok", "Build completed", `${result.time}ms`);
53
+ } else {
54
+ this.#log("err", "Build failed");
55
+ if (result.error) this.#send("error", { message: result.error });
56
+ }
57
+ return result;
58
+ } catch (e) {
59
+ this.#building = false;
60
+ this.#log("err", `Build error: ${e.message}`);
61
+ this.#send("error", { message: e.message });
62
+ return { success: false, error: e.message };
110
63
  }
111
-
112
- console.log(output);
113
64
  }
114
65
 
115
- // ─────────────────────────────────────────────────────────────────────────
116
- // Build Process
117
- // ─────────────────────────────────────────────────────────────────────────
118
-
119
- async #runBuild(buildArg = null) {
120
- if (this.#isBuilding) {
121
- this.#pendingBuild = true;
122
- return false;
123
- }
124
-
125
- this.#isBuilding = true;
126
- const startTime = Date.now();
127
- this.#log("build", "Building...");
128
-
129
- return new Promise((resolve) => {
130
- const buildScript = path.join(FRAMEWORK_DIR, "lib/Console/Commands/BuildCommand.js");
131
- const args = [buildScript];
132
- if (buildArg) args.push(buildArg);
133
-
134
- const buildProcess = spawn(process.execPath, args, {
135
- cwd: PROJECT_DIR,
136
- stdio: ["inherit", "pipe", "pipe"]
66
+ async #start() {
67
+ return new Promise(resolve => {
68
+ this.#proc = spawn(process.execPath, [path.join(Paths.framework, "lib/Runtime/Entry.js")], {
69
+ cwd: process.cwd(),
70
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
71
+ env: { ...process.env, __NITRON_DEV__: "true" }
137
72
  });
138
-
139
- let output = "";
140
-
141
- buildProcess.stdout.on("data", (data) => {
142
- output += data.toString();
143
- });
144
-
145
- buildProcess.stderr.on("data", (data) => {
146
- output += data.toString();
147
- });
148
-
149
- buildProcess.on("close", (code) => {
150
- this.#isBuilding = false;
151
- const duration = Date.now() - startTime;
152
-
153
- if (code === 0) {
154
- this.#log("success", `Build completed`, `${duration}ms`);
155
-
156
- if (this.#pendingBuild) {
157
- this.#pendingBuild = false;
158
- this.#runBuild().then(resolve);
159
- }
160
- else {
161
- resolve(true);
162
- }
163
- }
164
- else {
165
- this.#log("error", "Build failed");
166
- console.log(output);
167
- resolve(false);
168
- }
73
+ this.#proc.on("error", e => this.#log("err", e.message));
74
+ this.#proc.on("close", code => {
75
+ if (code && code !== 0) this.#log("err", `Exit code ${code}`);
76
+ this.#proc = null;
169
77
  });
78
+ setTimeout(resolve, 500);
170
79
  });
171
80
  }
172
81
 
173
- // ─────────────────────────────────────────────────────────────────────────
174
- // Server Process Management
175
- // ─────────────────────────────────────────────────────────────────────────
176
-
177
- async #startServer() {
178
- return new Promise((resolve) => {
179
- const entryScript = path.join(FRAMEWORK_DIR, "lib/Runtime/Entry.js");
180
-
181
- this.#serverProcess = spawn(process.execPath, [entryScript], {
182
- cwd: PROJECT_DIR,
183
- stdio: ["inherit", "inherit", "inherit"],
184
- env: { ...process.env, APP_DEV: "true" }
185
- });
186
-
187
- this.#serverProcess.on("error", (err) => {
188
- this.#log("error", `Server error: ${err.message}`);
189
- });
190
-
191
- this.#serverProcess.on("close", (code) => {
192
- if (code !== 0 && code !== null && !this.#pendingRestart) {
193
- this.#log("error", `Server exited with code ${code}`);
194
- }
195
- this.#serverProcess = null;
196
- });
197
-
198
- setTimeout(() => resolve(), 500);
199
- });
82
+ #send(type, data) {
83
+ if (this.#proc?.connected) {
84
+ this.#proc.send({ type, ...data });
85
+ }
200
86
  }
201
87
 
202
- async #stopServer() {
203
- if (!this.#serverProcess) return;
204
-
205
- return new Promise((resolve) => {
206
-
207
- const forceKillTimer = setTimeout(() => {
208
- if (this.#serverProcess) {
209
- this.#serverProcess.kill("SIGKILL");
210
- }
211
- resolve();
212
- }, 5000);
213
-
214
- this.#serverProcess.once("close", () => {
215
- clearTimeout(forceKillTimer);
216
- resolve();
217
- });
218
-
219
- this.#serverProcess.kill("SIGTERM");
88
+ async #stop() {
89
+ if (!this.#proc) return;
90
+ return new Promise(resolve => {
91
+ const timeout = setTimeout(() => { this.#proc?.kill("SIGKILL"); resolve(); }, 5000);
92
+ this.#proc.once("close", () => { clearTimeout(timeout); resolve(); });
93
+ this.#proc.kill("SIGTERM");
220
94
  });
221
95
  }
222
96
 
223
- async #restartServer() {
224
- if (this.#pendingRestart) return;
225
-
226
- this.#pendingRestart = true;
227
- await this.#stopServer();
228
- await this.#startServer();
229
- this.#pendingRestart = false;
230
- }
231
-
232
- #getRelativePath(filePath, baseDir = PROJECT_DIR) {
233
- return path.relative(baseDir, filePath).replace(/\\/g, "/");
97
+ #rel(p, base = Paths.project) {
98
+ return path.relative(base, p).replace(/\\/g, "/");
234
99
  }
235
100
 
236
- #matchesPattern(filePath, patterns, baseDir = PROJECT_DIR) {
237
- const relativePath = this.#getRelativePath(filePath, baseDir);
238
-
239
- return patterns.some(pattern => {
240
- let regexPattern = pattern
241
- .replace(/\*\*\//g, "<<ANYDIR>>") // **/ = any directory depth
242
- .replace(/\*\*/g, "<<ANYCHAR>>") // ** = any characters
243
- .replace(/\*/g, "[^/]*") // * = any characters except /
244
- .replace(/\//g, "\\/") // escape forward slashes
245
- .replace(/<<ANYDIR>>/g, "(?:.*\\/)?") // restore **/ placeholder
246
- .replace(/<<ANYCHAR>>/g, ".*"); // restore ** placeholder
247
-
248
- const regex = new RegExp(`^${regexPattern}$`);
249
- return regex.test(relativePath);
101
+ #match(p, patterns, base = Paths.project) {
102
+ const rel = this.#rel(p, base);
103
+ return patterns.some(pat => {
104
+ if (pat.includes("**")) {
105
+ const [prefix, suffix] = pat.split("**");
106
+ const dir = prefix.replace(/\/$/, "");
107
+ const ext = suffix.replace(/^\/?\*/, "");
108
+ return rel.startsWith(dir) && rel.endsWith(ext);
109
+ }
110
+ return rel === pat;
250
111
  });
251
112
  }
252
113
 
253
- async #handleFileChange(filePath, eventType) {
254
- const relativePath = this.#getRelativePath(filePath);
255
- const frameworkRelativePath = this.#getRelativePath(filePath, FRAMEWORK_DIR);
256
-
257
- // Check framework files first (lib/View/Templates)
258
- if (this.#matchesPattern(filePath, WATCH_CONFIG.frameworkBuildOnly, FRAMEWORK_DIR)) {
259
- this.#log("reload", `Framework file changed: ${frameworkRelativePath}`);
260
-
261
- clearTimeout(this.#buildDebounceTimer);
262
- this.#buildDebounceTimer = setTimeout(async () => {
263
- await this.#runBuild();
114
+ async #onChange(filePath) {
115
+ const rel = this.#rel(filePath);
116
+
117
+ if (this.#match(filePath, PATTERNS.framework, Paths.framework)) {
118
+ this.#log("build", `Framework: ${this.#rel(filePath, Paths.framework)}`);
119
+ clearTimeout(this.#debounce.build);
120
+ this.#debounce.build = setTimeout(async () => {
121
+ const r = await this.#build();
122
+ if (r.success) this.#send("reload", { reason: "Framework template changed" });
123
+ }, 100);
124
+ return;
125
+ }
126
+
127
+ if (this.#match(filePath, PATTERNS.css)) {
128
+ this.#log("hmr", rel);
129
+ clearTimeout(this.#debounce.build);
130
+ this.#debounce.build = setTimeout(async () => {
131
+ const r = await this.#build("css");
132
+ if (r.success) this.#send("css", { filePath });
264
133
  }, 100);
265
-
266
134
  return;
267
135
  }
268
-
269
- // Check project build-only files
270
- if (this.#matchesPattern(filePath, WATCH_CONFIG.buildOnly)) {
271
- this.#log("reload", `File changed: ${relativePath}`);
272
- // CSS only needs CSS rebuild
273
- // TSX needs views + CSS (Tailwind scans TSX for classes)
274
- const isCss = filePath.endsWith(".css");
275
- const buildArg = isCss ? "--only=css" : null; // null = full build for TSX
276
-
277
- clearTimeout(this.#buildDebounceTimer);
278
- this.#buildDebounceTimer = setTimeout(async () => {
279
- await this.#runBuild(buildArg);
136
+
137
+ if (this.#match(filePath, PATTERNS.views)) {
138
+ this.#log("hmr", rel);
139
+ clearTimeout(this.#debounce.build);
140
+ this.#debounce.build = setTimeout(async () => {
141
+ const r = await this.#build();
142
+ if (r.success) {
143
+ if (r.cssChanged) this.#send("css", {});
144
+ this.#send("view", { filePath });
145
+ }
280
146
  }, 100);
281
-
282
147
  return;
283
148
  }
284
149
 
285
- if (this.#matchesPattern(filePath, WATCH_CONFIG.fullRestart)) {
286
- this.#log("reload", `File changed: ${relativePath}`, "full restart");
287
-
288
- clearTimeout(this.#restartDebounceTimer);
289
- this.#restartDebounceTimer = setTimeout(async () => {
290
- await this.#runBuild();
291
- await this.#restartServer();
150
+ if (this.#match(filePath, PATTERNS.restart)) {
151
+ this.#log("build", rel, "restart required");
152
+ clearTimeout(this.#debounce.restart);
153
+ this.#debounce.restart = setTimeout(async () => {
154
+ await this.#build();
155
+ await this.#stop();
156
+ await this.#start();
157
+ this.#send("reload", { reason: "Server restarted" });
292
158
  }, 200);
293
-
294
159
  return;
295
160
  }
161
+
162
+ this.#log("info", `Unmatched: ${rel}`);
296
163
  }
297
164
 
298
- #setupWatcher() {
299
- const watchPaths = [
300
- path.join(PROJECT_DIR, "resources/views"),
301
- path.join(PROJECT_DIR, "resources/css"),
302
- path.join(FRAMEWORK_DIR, "lib/View/Templates"),
303
- path.join(PROJECT_DIR, "config"),
304
- path.join(PROJECT_DIR, "app"),
305
- path.join(PROJECT_DIR, "routes"),
306
- path.join(PROJECT_DIR, "app.js")
165
+ #watch() {
166
+ const candidates = [
167
+ path.join(Paths.project, "resources/views"),
168
+ path.join(Paths.project, "resources/css"),
169
+ path.join(Paths.project, "config"),
170
+ path.join(Paths.project, "app"),
171
+ path.join(Paths.project, "routes"),
172
+ path.join(Paths.framework, "lib/View/Templates")
307
173
  ];
308
174
 
175
+ const watchPaths = candidates.filter(p => fs.existsSync(p));
176
+
177
+ if (!watchPaths.length) {
178
+ this.#log("err", "No watch paths found");
179
+ return { close: () => {} };
180
+ }
181
+
309
182
  const watcher = chokidar.watch(watchPaths, {
310
- ignored: [
311
- /node_modules/,
312
- /\.git/,
313
- /build[\\\/]/,
314
- /storage[\\\/]app[\\\/]public/,
315
- /storage[\\\/]framework/,
316
- /storage[\\\/]logs/,
317
- /\.nitron/,
318
- /package-lock\.json$/
319
- ],
183
+ ignored: [/node_modules/, /\.git/, /build[\\/]/, /storage[\\/]/, /\.nitron/],
320
184
  persistent: true,
321
185
  ignoreInitial: true,
322
- usePolling: false,
323
- awaitWriteFinish: {
324
- stabilityThreshold: 100,
325
- pollInterval: 50
326
- }
186
+ usePolling: true,
187
+ interval: 100,
188
+ binaryInterval: 100,
189
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 }
327
190
  });
328
191
 
329
- watcher.on("change", (filePath) => this.#handleFileChange(filePath, "change"));
330
- watcher.on("add", (filePath) => this.#handleFileChange(filePath, "add"));
331
- watcher.on("unlink", (filePath) => this.#handleFileChange(filePath, "unlink"));
332
-
192
+ watcher.on("change", p => this.#onChange(p));
193
+ watcher.on("add", p => this.#onChange(p));
194
+ watcher.on("error", e => this.#log("err", `Watch error: ${e.message}`));
333
195
  watcher.on("ready", () => {
334
- if (!this.#watcherReady) {
335
- this.#watcherReady = true;
336
- this.#log("watch", "Watching for file changes...");
337
- }
338
- });
339
-
340
- watcher.on("error", (error) => {
341
- this.#log("error", `Watcher error: ${error.message}`);
196
+ this.#log("watch", `Watching ${watchPaths.length} directories`);
342
197
  });
343
198
 
344
199
  return watcher;
345
200
  }
346
201
 
347
202
  async start() {
348
- // Initial build
349
- const buildSuccess = await this.#runBuild();
350
- if (!buildSuccess) {
351
- this.#log("error", "Initial build failed. Watching for changes...");
352
- }
353
-
354
- // Start server
355
- await this.#startServer();
356
-
357
- // Setup file watcher
358
- const watcher = this.#setupWatcher();
203
+ await this.#build();
204
+ await this.#start();
205
+ const watcher = this.#watch();
359
206
 
360
- // Graceful shutdown
361
- const cleanup = async () => {
207
+ const exit = async () => {
362
208
  console.log();
363
209
  watcher.close();
364
- await this.#stopServer();
210
+ await this.#stop();
365
211
  process.exit(0);
366
212
  };
367
213
 
368
- process.on("SIGINT", cleanup);
369
- process.on("SIGTERM", cleanup);
214
+ process.on("SIGINT", exit);
215
+ process.on("SIGTERM", exit);
370
216
  }
371
217
  }
372
218
 
373
219
  export default async function Dev() {
374
- const devServer = new DevServer();
375
- await devServer.start();
220
+ await new DevServer().start();
376
221
  }
377
222
 
378
- // Auto-run when called directly
379
- const isMain = process.argv[1]?.endsWith("DevCommand.js");
380
- if (isMain) {
381
- Dev().catch(err => {
382
- console.error(err);
383
- process.exit(1);
384
- });
223
+ if (process.argv[1]?.endsWith("DevCommand.js")) {
224
+ Dev().catch(e => { console.error(e); process.exit(1); });
385
225
  }
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ // __COMPONENT_IMPORTS__
5
+
6
+ const componentManifest: Record<string, React.ComponentType<any>> = {};
7
+
8
+ // __COMPONENT_MANIFEST__
9
+
10
+ const w = window as any;
11
+ const prevRefreshReg = w.$RefreshReg$;
12
+ const prevRefreshSig = w.$RefreshSig$;
13
+
14
+ if (typeof w.$RefreshReg$ === "function") {
15
+ const moduleId = "__NITRON_MODULE_ID__";
16
+ w.$RefreshReg$ = (type: any, id: string) => {
17
+ const fullId = moduleId + "_" + id;
18
+ if (prevRefreshReg) prevRefreshReg(type, fullId);
19
+ };
20
+ }
21
+
22
+ if (typeof w.$RefreshSig$ === "function") {
23
+ w.$RefreshSig$ = prevRefreshSig;
24
+ }
25
+
26
+ // __COMPONENT_REGISTRATIONS__
27
+
28
+ function mount() {
29
+ const props = (window as any).__NITRON_PROPS__ || {};
30
+ if (!(window as any).__NITRON_ROOTS__) (window as any).__NITRON_ROOTS__ = new Map();
31
+
32
+ const islands = document.querySelectorAll<HTMLElement>("[data-cid]");
33
+
34
+ islands.forEach(element => {
35
+ const componentName = element.dataset.island;
36
+ const componentId = element.dataset.cid;
37
+
38
+ if (!componentName || !componentId) return;
39
+
40
+ const Component = componentManifest[componentName];
41
+ if (!Component) return;
42
+
43
+ const componentProps = props[componentId] || {};
44
+
45
+ try {
46
+ const roots = (window as any).__NITRON_ROOTS__ as Map<HTMLElement, any>;
47
+ let root = roots.get(element);
48
+ if (!root) {
49
+ root = createRoot(element);
50
+ roots.set(element, root);
51
+ }
52
+ root.render(React.createElement(Component, componentProps));
53
+ } catch {
54
+ }
55
+ });
56
+
57
+ delete (window as any).__NITRON_PROPS__;
58
+
59
+ if ((window as any).__NITRON_REFRESH__) {
60
+ (window as any).__NITRON_REFRESH__.performReactRefresh();
61
+ }
62
+ }
63
+
64
+ const isHmrUpdate = (window as any).__NITRON_HMR_UPDATE__;
65
+
66
+ if (!isHmrUpdate) {
67
+ if (document.readyState === "loading") {
68
+ document.addEventListener("DOMContentLoaded", mount);
69
+ } else {
70
+ mount();
71
+ }
72
+ }
@@ -6,6 +6,7 @@ import { createRoot } from "react-dom/client";
6
6
  declare global {
7
7
  interface Window {
8
8
  __NITRON_PROPS__?: Record<string, any>;
9
+ __NITRON_ROOTS__?: Map<HTMLElement, any>;
9
10
  }
10
11
  }
11
12
 
@@ -15,6 +16,7 @@ const componentManifest: Record<string, React.ComponentType<any>> = {};
15
16
 
16
17
  function mount() {
17
18
  const props = window.__NITRON_PROPS__ || {};
19
+ if (!window.__NITRON_ROOTS__) window.__NITRON_ROOTS__ = new Map();
18
20
 
19
21
  const islands = document.querySelectorAll<HTMLElement>("[data-cid]");
20
22
 
@@ -22,22 +24,19 @@ function mount() {
22
24
  const componentName = element.dataset.island;
23
25
  const componentId = element.dataset.cid;
24
26
 
25
- if (!componentName || !componentId) {
26
- return;
27
- }
27
+ if (!componentName || !componentId) return;
28
28
 
29
29
  const Component = componentManifest[componentName];
30
-
31
- if (!Component) {
32
- return;
33
- }
30
+ if (!Component) return;
34
31
 
35
32
  const componentProps = props[componentId] || {};
36
33
 
37
34
  try {
38
- // Use createRoot for full client-side rendering
39
- // This ensures animations and effects work correctly
40
- const root = createRoot(element);
35
+ let root = window.__NITRON_ROOTS__.get(element);
36
+ if (!root) {
37
+ root = createRoot(element);
38
+ window.__NITRON_ROOTS__.set(element, root);
39
+ }
41
40
  root.render(React.createElement(Component, componentProps));
42
41
  } catch {
43
42
  }