@aaqu/fromcubes-portal-react 0.1.0-alpha.6 → 0.1.0-alpha.8

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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @aaqu/fromcubes-portal-react
3
3
  *
4
- * Server-side JSX transpilation & bundling via esbuild.
5
- * Deploy-safe: handles rapid redeploys without leaking WS connections or stale routes.
6
- * Transpiled JS is cached by content hash unchanged code skips recompilation on redeploy.
4
+ * Node-RED node that serves React apps from configurable HTTP endpoints
5
+ * with live WebSocket data binding. JSX is transpiled server-side via esbuild
6
+ * at deploy timebrowsers receive pre-compiled JS.
7
7
  */
8
8
 
9
9
  const crypto = require("crypto");
@@ -11,16 +11,6 @@ const fs = require("fs");
11
11
  const path = require("path");
12
12
  const esbuild = require("esbuild");
13
13
 
14
- const reactBundle = fs.readFileSync(
15
- path.join(__dirname, "vendor", "react-19.production.min.js"),
16
- "utf8",
17
- );
18
- const reactHash = crypto
19
- .createHash("sha256")
20
- .update(reactBundle)
21
- .digest("hex")
22
- .slice(0, 10);
23
-
24
14
  module.exports = function (RED) {
25
15
  // ── Admin root prefix (for correct URLs when httpAdminRoot is set) ──
26
16
  const adminRoot = (RED.settings.httpAdminRoot || "/").replace(/\/$/, "");
@@ -63,6 +53,12 @@ module.exports = function (RED) {
63
53
  }
64
54
  const rebuildCallbacks = RED.settings.fcRebuildCallbacks;
65
55
 
56
+ // Vendor bundle cache: cacheKey → { js, hash }
57
+ if (!RED.settings.fcVendorCache) {
58
+ RED.settings.fcVendorCache = {};
59
+ }
60
+ const vendorCache = RED.settings.fcVendorCache;
61
+
66
62
  // ── Helpers ───────────────────────────────────────────────────
67
63
 
68
64
  function hash(str) {
@@ -92,12 +88,112 @@ module.exports = function (RED) {
92
88
  return twCompiled;
93
89
  }
94
90
 
95
- function transpile(jsx) {
91
+ // Package root — where react/react-dom live (this package's own node_modules)
92
+ const pkgRoot = path.join(__dirname, "..");
93
+ // userDir — where dynamicModuleList installs user packages
94
+ const userDir = RED.settings.userDir || path.join(__dirname, "../../..");
95
+ // esbuild resolveDir: package root (react is here); nodePaths adds userDir for user libs
96
+ const resolveDir = pkgRoot;
97
+
98
+ function getPackageName(moduleSpec) {
99
+ const m = moduleSpec.match(/^((?:@[^/]+\/)?[^/]+)/);
100
+ return m ? m[1] : moduleSpec;
101
+ }
102
+
103
+ function getInstalledVersion(pkgName) {
104
+ try {
105
+ const pkgJson = require(
106
+ require.resolve(pkgName + "/package.json", { paths: [pkgRoot, userDir] }),
107
+ );
108
+ return pkgJson.version;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ function buildVendorBundle(libs) {
115
+ const lines = [
116
+ 'import React from "react";',
117
+ 'import ReactDOM from "react-dom";',
118
+ 'import { createRoot } from "react-dom/client";',
119
+ "window.React = React;",
120
+ "window.ReactDOM = ReactDOM;",
121
+ "window.ReactDOM.createRoot = createRoot;",
122
+ ];
123
+ if (libs.length > 0) {
124
+ lines.push("if(!window.__pkg) window.__pkg = {};");
125
+ libs.forEach((lib, i) => {
126
+ lines.push(`import * as __p${i} from ${JSON.stringify(lib.module)};`);
127
+ lines.push(`window.__pkg[${JSON.stringify(lib.module)}] = __p${i};`);
128
+ });
129
+ }
130
+ const contents = lines.join("\n");
131
+ const buildResult = esbuild.buildSync({
132
+ stdin: { contents, resolveDir, loader: "js" },
133
+ bundle: true,
134
+ format: "iife",
135
+ minify: true,
136
+ write: false,
137
+ target: ["es2020"],
138
+ define: { "process.env.NODE_ENV": '"production"' },
139
+ nodePaths: [path.join(userDir, "node_modules")],
140
+ });
141
+ const js = buildResult.outputFiles[0].text;
142
+ const h = hash(js);
143
+ return { js, hash: h };
144
+ }
145
+
146
+ function getVendorBundle(libs) {
147
+ // Build cache key from actual installed versions
148
+ const keyParts = ["react@" + getInstalledVersion("react")];
149
+ for (const lib of libs) {
150
+ keyParts.push(lib.module + "@" + getInstalledVersion(getPackageName(lib.module)));
151
+ }
152
+ keyParts.sort();
153
+ const cacheKey = hash(JSON.stringify(keyParts));
154
+
155
+ if (vendorCache[cacheKey]) return vendorCache[cacheKey];
156
+
157
+ const bundle = buildVendorBundle(libs);
158
+ vendorCache[cacheKey] = bundle;
159
+ return bundle;
160
+ }
161
+
162
+ function transpile(jsx, libs) {
163
+ const externalList = ["react", "react-dom", "react-dom/client"];
164
+ if (libs) {
165
+ libs.forEach((lib) => {
166
+ if (!externalList.includes(lib.module)) {
167
+ externalList.push(lib.module);
168
+ }
169
+ });
170
+ }
171
+
172
+ // Auto-detect require() calls in JSX and add them as externals
173
+ const requireRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
174
+ let m;
175
+ while ((m = requireRe.exec(jsx)) !== null) {
176
+ if (!externalList.includes(m[1])) {
177
+ externalList.push(m[1]);
178
+ }
179
+ }
180
+
181
+ const requireShim = [
182
+ "var require = (function() {",
183
+ ' var _r = {"react":window.React, "react-dom":window.ReactDOM, "react-dom/client":window.ReactDOM};',
184
+ " return function(m) {",
185
+ " if (_r[m]) return _r[m];",
186
+ " if (window.__pkg && window.__pkg[m]) return window.__pkg[m];",
187
+ ' throw new Error("Module not found: " + m);',
188
+ " };",
189
+ "})();",
190
+ ].join("\n");
191
+
96
192
  try {
97
193
  const buildResult = esbuild.buildSync({
98
194
  stdin: {
99
195
  contents: jsx,
100
- resolveDir: path.join(__dirname, "../../.."),
196
+ resolveDir,
101
197
  loader: "jsx",
102
198
  },
103
199
  bundle: true,
@@ -107,8 +203,9 @@ module.exports = function (RED) {
107
203
  jsx: "transform",
108
204
  jsxFactory: "React.createElement",
109
205
  jsxFragment: "React.Fragment",
110
- external: ["react", "react-dom"],
206
+ external: externalList,
111
207
  define: { "process.env.NODE_ENV": '"production"' },
208
+ banner: { js: requireShim },
112
209
  });
113
210
  return { js: buildResult.outputFiles[0].text, error: null };
114
211
  } catch (e) {
@@ -137,10 +234,14 @@ module.exports = function (RED) {
137
234
  function extractPortalUser(headers) {
138
235
  const user = {};
139
236
  if (headers["x-portal-user-id"]) user.userId = headers["x-portal-user-id"];
140
- if (headers["x-portal-user-name"]) user.userName = headers["x-portal-user-name"];
141
- if (headers["x-portal-user-username"]) user.username = headers["x-portal-user-username"];
142
- if (headers["x-portal-user-email"]) user.email = headers["x-portal-user-email"];
143
- if (headers["x-portal-user-role"]) user.role = headers["x-portal-user-role"];
237
+ if (headers["x-portal-user-name"])
238
+ user.userName = headers["x-portal-user-name"];
239
+ if (headers["x-portal-user-username"])
240
+ user.username = headers["x-portal-user-username"];
241
+ if (headers["x-portal-user-email"])
242
+ user.email = headers["x-portal-user-email"];
243
+ if (headers["x-portal-user-role"])
244
+ user.role = headers["x-portal-user-role"];
144
245
  if (headers["x-portal-user-groups"]) {
145
246
  try {
146
247
  user.groups = JSON.parse(headers["x-portal-user-groups"]);
@@ -209,11 +310,13 @@ module.exports = function (RED) {
209
310
  const nodeId = node.id;
210
311
 
211
312
  // Config
212
- const endpoint = (config.endpoint || "/portal").replace(/\/+$/, "");
313
+ const endpoint = (config.endpoint || "/fromcubes").replace(/\/+$/, "");
213
314
  const componentCode = config.componentCode || "";
214
315
  const pageTitle = config.pageTitle || "Portal";
215
316
  const customHead = config.customHead || "";
216
317
  const portalAuth = config.portalAuth === true;
318
+ const showWsStatus = config.showWsStatus === true;
319
+ const libs = config.libs || [];
217
320
 
218
321
  // State
219
322
  const clients = new Set();
@@ -226,19 +329,51 @@ module.exports = function (RED) {
226
329
  // ── Rebuild: transpile JSX + update page state ────────────
227
330
 
228
331
  function rebuild() {
229
- // Topological sort: components used by others come first
230
- const entries = Object.entries(registry);
231
- const names = entries.map(([n]) => n);
332
+ // Build or get cached vendor bundle
333
+ let vendorBundle;
334
+ try {
335
+ vendorBundle = getVendorBundle(libs);
336
+ } catch (e) {
337
+ node.error("Vendor bundle failed: " + e.message);
338
+ node.status({ fill: "red", shape: "dot", text: "vendor build error" });
339
+ return;
340
+ }
341
+
342
+ // Selective injection: only include components referenced in user code (+ transitive deps)
343
+ const allEntries = Object.entries(registry);
344
+ const needed = new Set();
345
+
346
+ function addWithDeps(name) {
347
+ if (needed.has(name)) return;
348
+ const entry = registry[name];
349
+ if (!entry) return;
350
+ needed.add(name);
351
+ for (const [other] of allEntries) {
352
+ if (other !== name && entry.code.includes(other)) {
353
+ addWithDeps(other);
354
+ }
355
+ }
356
+ }
357
+
358
+ for (const [name] of allEntries) {
359
+ if (componentCode.includes(name)) {
360
+ addWithDeps(name);
361
+ }
362
+ }
363
+
364
+ // Topological sort only needed components
365
+ const entries = allEntries.filter(([n]) => needed.has(n));
232
366
  entries.sort((a, b) => {
233
367
  const aUsesB = a[1].code.includes(b[0]);
234
368
  const bUsesA = b[1].code.includes(a[0]);
235
- if (aUsesB && !bUsesA) return 1; // a depends on b → b first
369
+ if (aUsesB && !bUsesA) return 1; // a depends on b → b first
236
370
  if (bUsesA && !aUsesB) return -1; // b depends on a → a first
237
371
  return 0;
238
372
  });
239
373
  const libraryJsx = entries
240
- .map(([name, c]) =>
241
- `// Library: ${name}\nconst ${name} = (() => {\n${c.code}\nreturn ${name};\n})();`
374
+ .map(
375
+ ([name, c]) =>
376
+ `// Library: ${name}\nconst ${name} = (() => {\n${c.code}\nreturn ${name};\n})();`,
242
377
  )
243
378
  .join("\n\n");
244
379
 
@@ -248,17 +383,19 @@ module.exports = function (RED) {
248
383
  "const { createContext, memo, forwardRef, Fragment } = React;",
249
384
  "",
250
385
  "// ── useNodeRed hook ──",
251
- `function useNodeRed() {
252
- const [data, setData] = React.useState(window.__NR._lastData);
253
- React.useEffect(() => {
254
- return window.__NR.subscribe(setData);
255
- }, []);
256
- const send = React.useCallback((payload, topic) => {
257
- window.__NR.send(payload, topic);
258
- }, []);
259
- const user = window.__NR._user || null;
260
- return { data, send, user };
261
- }`,
386
+ [
387
+ "function useNodeRed() {",
388
+ " const [data, setData] = React.useState(window.__NR._lastData);",
389
+ " React.useEffect(() => {",
390
+ " return window.__NR.subscribe(setData);",
391
+ " }, []);",
392
+ " const send = React.useCallback((payload, topic) => {",
393
+ " window.__NR.send(payload, topic);",
394
+ " }, []);",
395
+ " const user = window.__NR._user || null;",
396
+ " return { data, send, user };",
397
+ "}",
398
+ ].join("\n"),
262
399
  "",
263
400
  "// ── Library components ──",
264
401
  libraryJsx,
@@ -270,7 +407,7 @@ module.exports = function (RED) {
270
407
  "ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));",
271
408
  ].join("\n");
272
409
 
273
- const compiled = transpile(fullJsx);
410
+ const compiled = transpile(fullJsx, libs);
274
411
 
275
412
  if (compiled.error) {
276
413
  node.error("JSX transpile error: " + compiled.error);
@@ -291,13 +428,18 @@ module.exports = function (RED) {
291
428
  })
292
429
  : Promise.resolve("");
293
430
 
431
+ const contentHash = compiled.js ? hash(compiled.js) : "";
432
+
294
433
  pageState[endpoint] = {
295
434
  compiled,
435
+ contentHash,
296
436
  cssHashReady,
297
437
  pageTitle,
298
438
  wsPath,
299
439
  customHead,
300
440
  portalAuth,
441
+ showWsStatus,
442
+ vendorHash: vendorBundle.hash,
301
443
  };
302
444
  }
303
445
 
@@ -325,7 +467,9 @@ module.exports = function (RED) {
325
467
  return;
326
468
  }
327
469
  const cssHash = await state.cssHashReady;
328
- const user = state.portalAuth ? extractPortalUser(_req.headers) : null;
470
+ const user = state.portalAuth
471
+ ? extractPortalUser(_req.headers)
472
+ : null;
329
473
  res
330
474
  .type("text/html")
331
475
  .send(
@@ -336,6 +480,8 @@ module.exports = function (RED) {
336
480
  state.customHead,
337
481
  cssHash,
338
482
  user,
483
+ state.showWsStatus,
484
+ state.vendorHash,
339
485
  ),
340
486
  );
341
487
  });
@@ -389,6 +535,10 @@ module.exports = function (RED) {
389
535
  wsSend(ws, { type: "data", payload: lastPayload });
390
536
  }
391
537
 
538
+ // Send content version for deploy-reload detection
539
+ const contentHash = pageState[endpoint]?.contentHash || "";
540
+ wsSend(ws, { type: "version", hash: contentHash });
541
+
392
542
  ws.on("message", (raw) => {
393
543
  try {
394
544
  const msg = JSON.parse(raw.toString());
@@ -493,7 +643,9 @@ module.exports = function (RED) {
493
643
  }); // end setImmediate
494
644
  }
495
645
 
496
- RED.nodes.registerType("portal-react", PortalReactNode);
646
+ RED.nodes.registerType("portal-react", PortalReactNode, {
647
+ dynamicModuleList: "libs",
648
+ });
497
649
 
498
650
  // ── Serve Monaco editor files locally ────────────────────────
499
651
  const express = require("express");
@@ -529,14 +681,21 @@ module.exports = function (RED) {
529
681
  res.send(css);
530
682
  });
531
683
 
532
- // ── Vendor React bundle endpoint ────────────────────────────
533
- RED.httpAdmin.get("/portal-react/vendor/react.min.js", (_req, res) => {
684
+ // ── Vendor bundle endpoint (dynamic, hash-based) ───────────
685
+ RED.httpAdmin.get("/portal-react/vendor/:hash.js", (req, res) => {
686
+ const entry = Object.values(vendorCache).find(
687
+ (v) => v.hash === req.params.hash,
688
+ );
689
+ if (!entry) {
690
+ res.status(404).send("Not found");
691
+ return;
692
+ }
534
693
  res.set({
535
694
  "Content-Type": "application/javascript",
536
695
  "Cache-Control": "public, max-age=31536000, immutable",
537
- ETag: `"${reactHash}"`,
696
+ ETag: `"${req.params.hash}"`,
538
697
  });
539
- res.send(reactBundle);
698
+ res.send(entry.js);
540
699
  });
541
700
 
542
701
  // ── Admin API for component registry ──────────────────────────
@@ -563,93 +722,116 @@ module.exports = function (RED) {
563
722
 
564
723
  // ── Page builders ─────────────────────────────────────────────
565
724
 
566
- function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user) {
725
+ function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user, showWsStatus, vendorHash) {
567
726
  return `<!DOCTYPE html>
568
- <html lang="en">
569
- <head>
570
- <meta charset="UTF-8">
571
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
572
- <title>${esc(title)}</title>
573
- <script src="${adminRoot}/portal-react/vendor/react.min.js?v=${reactHash}"><\/script>
574
- ${cssHash ? `<link rel="stylesheet" href="${adminRoot}/portal-react/css/${cssHash}.css">` : ""}
575
- ${escScript(customHead)}
576
- <style>
577
- @layer base{
578
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
579
- body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#e0e0e0}
580
- #root{min-height:100vh}
581
- }
582
- #__cs{position:fixed;bottom:6px;right:6px;padding:3px 8px;font-size:10px;border-radius:3px;z-index:99999;background:#111;border:1px solid #333;opacity:.7;transition:opacity .2s}
583
- #__cs:hover{opacity:1}
584
- #__cs.ok{color:#4ade80}
585
- #__cs.err{color:#f87171}
586
- </style>
587
- </head>
588
- <body>
589
- <div id="root"></div>
590
- <div id="__cs" class="err">disconnected</div>
591
- <script>
592
- window.__NR={
593
- _ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,_user:${user ? escScript(JSON.stringify(user)) : 'null'},
594
- connect(){
595
- const p=location.protocol==='https:'?'wss:':'ws:';
596
- const ws=new WebSocket(p+'//'+location.host+'${wsPath}');
597
- this._ws=ws;
598
- const s=document.getElementById('__cs');
599
- ws.onopen=()=>{
600
- if(this._wasConnected){s.textContent='connected';s.className='ok';this._retries=0;return;}
601
- s.textContent='connected';s.className='ok';this._retries=0;this._wasConnected=true;
602
- };
603
- ws.onmessage=(e)=>{
604
- try{const m=JSON.parse(e.data);if(m.type==='data'){this._lastData=m.payload;this._listeners.forEach(fn=>fn(m.payload));}}
605
- catch(err){console.error('WS parse',err);}
606
- };
607
- ws.onclose=(e)=>{
608
- s.textContent='disconnected';s.className='err';
609
- this._ws=null;
610
- const delay=Math.min(500*Math.pow(2,this._retries),8000);
611
- this._retries++;
612
- setTimeout(()=>this.connect(),delay);
613
- };
614
- ws.onerror=()=>ws.close();
615
- },
616
- subscribe(fn){
617
- this._listeners.add(fn);
618
- if(this._lastData!==null)fn(this._lastData);
619
- return()=>this._listeners.delete(fn);
620
- },
621
- send(payload,topic){
622
- if(this._ws&&this._ws.readyState===1)
623
- this._ws.send(JSON.stringify({type:'output',payload,topic:topic||''}));
624
- }
625
- };
626
- window.__NR.connect();
627
- <\/script>
628
- <script>
629
- ${escScript(transpiledJs)}
630
- <\/script>
631
- </body>
632
- </html>`;
727
+ <html lang="en">
728
+ <head>
729
+ <meta charset="UTF-8">
730
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
731
+ <title>${esc(title)}</title>
732
+ <script src="${adminRoot}/portal-react/vendor/${vendorHash}.js"><\/script>
733
+ ${cssHash ? `<link rel="stylesheet" href="${adminRoot}/portal-react/css/${cssHash}.css">` : ""}
734
+ ${escScript(customHead)}
735
+ ${showWsStatus ? `<style>
736
+ #__cs {
737
+ position: fixed; bottom: 6px; right: 6px;
738
+ padding: 3px 8px; font-size: 10px; border-radius: 3px;
739
+ z-index: 99999; background: #111; border: 1px solid #333;
740
+ opacity: .7; transition: opacity .2s;
741
+ }
742
+ #__cs:hover { opacity: 1 }
743
+ #__cs.ok { color: #4ade80 }
744
+ #__cs.err { color: #f87171 }
745
+ </style>` : ""}
746
+ </head>
747
+ <body>
748
+ <div id="root"></div>
749
+ ${showWsStatus ? `<div id="__cs" class="err">fromcubes</div>` : ""}
750
+ <script>
751
+ window.__NR = {
752
+ _ws: null,
753
+ _listeners: new Set(),
754
+ _lastData: null,
755
+ _retries: 0,
756
+ _wasConnected: false,
757
+ _version: null,
758
+ _user: ${user ? escScript(JSON.stringify(user)) : "null"},
759
+
760
+ connect() {
761
+ const p = location.protocol === 'https:' ? 'wss:' : 'ws:';
762
+ const ws = new WebSocket(p + '//' + location.host + '${wsPath}');
763
+ this._ws = ws;
764
+ const s = document.getElementById('__cs');
765
+
766
+ ws.onopen = () => {
767
+ if (s) { s.textContent = 'fromcubes \u2022 connected'; s.className = 'ok'; }
768
+ this._retries = 0;
769
+ this._wasConnected = true;
770
+ };
771
+
772
+ ws.onmessage = (e) => {
773
+ try {
774
+ const m = JSON.parse(e.data);
775
+ if (m.type === 'version') {
776
+ if (this._version && this._version !== m.hash) { location.reload(); return; }
777
+ this._version = m.hash;
778
+ }
779
+ if (m.type === 'data') {
780
+ this._lastData = m.payload;
781
+ this._listeners.forEach(fn => fn(m.payload));
782
+ }
783
+ } catch (err) { console.error('WS parse', err); }
784
+ };
785
+
786
+ ws.onclose = () => {
787
+ if (s) { s.textContent = 'fromcubes \u2022 disconnected'; s.className = 'err'; }
788
+ this._ws = null;
789
+ const delay = Math.min(500 * Math.pow(2, this._retries), 8000);
790
+ this._retries++;
791
+ setTimeout(() => this.connect(), delay);
792
+ };
793
+
794
+ ws.onerror = () => ws.close();
795
+ },
796
+
797
+ subscribe(fn) {
798
+ this._listeners.add(fn);
799
+ if (this._lastData !== null) fn(this._lastData);
800
+ return () => this._listeners.delete(fn);
801
+ },
802
+
803
+ send(payload, topic) {
804
+ if (this._ws && this._ws.readyState === 1)
805
+ this._ws.send(JSON.stringify({ type: 'output', payload, topic: topic || '' }));
806
+ }
807
+ };
808
+ window.__NR.connect();
809
+ <\/script>
810
+ <script>
811
+ ${escScript(transpiledJs)}
812
+ <\/script>
813
+ </body>
814
+ </html>`;
633
815
  }
634
816
 
635
817
  function buildErrorPage(title, error) {
636
818
  return `<!DOCTYPE html>
637
- <html lang="en">
638
- <head>
639
- <meta charset="UTF-8">
640
- <title>${esc(title)} — Error</title>
641
- <style>
642
- body{font-family:monospace;background:#1a0000;color:#f87171;padding:40px;line-height:1.6}
643
- h1{color:#ff4444;margin-bottom:16px}
644
- pre{background:#0a0a0a;border:1px solid #ff4444;border-radius:8px;padding:20px;overflow-x:auto;color:#fca5a5}
645
- </style>
646
- </head>
647
- <body>
648
- <h1>JSX Transpile Error</h1>
649
- <p>Fix the component code in Node-RED and deploy again.</p>
650
- <pre>${esc(error)}</pre>
651
- </body>
652
- </html>`;
819
+ <html lang="en">
820
+ <head>
821
+ <meta charset="UTF-8">
822
+ <title>${esc(title)} — Error</title>
823
+ <style>
824
+ body { font-family: monospace; background: #1a0000; color: #f87171; padding: 40px; line-height: 1.6 }
825
+ h1 { color: #ff4444; margin-bottom: 16px }
826
+ pre { background: #0a0a0a; border: 1px solid #ff4444; border-radius: 8px; padding: 20px; overflow-x: auto; color: #fca5a5 }
827
+ </style>
828
+ </head>
829
+ <body>
830
+ <h1>JSX Transpile Error</h1>
831
+ <p>Fix the component code in Node-RED and deploy again.</p>
832
+ <pre>${esc(error)}</pre>
833
+ </body>
834
+ </html>`;
653
835
  }
654
836
 
655
837
  function esc(s) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaqu/fromcubes-portal-react",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "Fromcubes Portal - React for Node-RED with Tailwind CSS and auto complete",
5
5
  "keywords": [
6
6
  "node-red",
@@ -19,7 +19,6 @@
19
19
  "files": [
20
20
  "nodes/",
21
21
  "examples/",
22
- "scripts/",
23
22
  "README.md",
24
23
  "LICENSE"
25
24
  ],
@@ -27,17 +26,17 @@
27
26
  "node": ">=18"
28
27
  },
29
28
  "dependencies": {
30
- "esbuild": "0.25.0",
29
+ "esbuild": "^0.27.4",
31
30
  "monaco-editor": "^0.55.1",
32
- "tailwindcss": "^4.0.0"
31
+ "react": "^19.2.4",
32
+ "react-dom": "^19.2.4",
33
+ "tailwindcss": "^4.2.1"
33
34
  },
34
35
  "devDependencies": {
35
- "prettier": "^3.8.1",
36
- "react": "^19.0.0",
37
- "react-dom": "^19.0.0"
36
+ "node-red": "next",
37
+ "prettier": "^3.8.1"
38
38
  },
39
39
  "scripts": {
40
- "build": "node scripts/bundle-react.js",
41
40
  "start": "node-red"
42
41
  },
43
42
  "license": "Apache-2.0",