@aaqu/fromcubes-portal-react 0.1.0-alpha.7 → 0.1.0-alpha.9

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,113 @@ 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}.default || __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
+ logOverride: { "import-is-undefined": "silent" },
140
+ nodePaths: [path.join(userDir, "node_modules")],
141
+ });
142
+ const js = buildResult.outputFiles[0].text;
143
+ const h = hash(js);
144
+ return { js, hash: h };
145
+ }
146
+
147
+ function getVendorBundle(libs) {
148
+ // Build cache key from actual installed versions
149
+ const keyParts = ["react@" + getInstalledVersion("react")];
150
+ for (const lib of libs) {
151
+ keyParts.push(lib.module + "@" + getInstalledVersion(getPackageName(lib.module)));
152
+ }
153
+ keyParts.sort();
154
+ const cacheKey = hash(JSON.stringify(keyParts));
155
+
156
+ if (vendorCache[cacheKey]) return vendorCache[cacheKey];
157
+
158
+ const bundle = buildVendorBundle(libs);
159
+ vendorCache[cacheKey] = bundle;
160
+ return bundle;
161
+ }
162
+
163
+ function transpile(jsx, libs) {
164
+ const externalList = ["react", "react-dom", "react-dom/client"];
165
+ if (libs) {
166
+ libs.forEach((lib) => {
167
+ if (!externalList.includes(lib.module)) {
168
+ externalList.push(lib.module);
169
+ }
170
+ });
171
+ }
172
+
173
+ // Auto-detect require() calls in JSX and add them as externals
174
+ const requireRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
175
+ let m;
176
+ while ((m = requireRe.exec(jsx)) !== null) {
177
+ if (!externalList.includes(m[1])) {
178
+ externalList.push(m[1]);
179
+ }
180
+ }
181
+
182
+ const requireShim = [
183
+ "var require = (function() {",
184
+ ' var _r = {"react":window.React, "react-dom":window.ReactDOM, "react-dom/client":window.ReactDOM};',
185
+ " return function(m) {",
186
+ " if (_r[m]) return _r[m];",
187
+ " if (window.__pkg && window.__pkg[m]) return window.__pkg[m];",
188
+ ' throw new Error("Module not found: " + m);',
189
+ " };",
190
+ "})();",
191
+ ].join("\n");
192
+
96
193
  try {
97
194
  const buildResult = esbuild.buildSync({
98
195
  stdin: {
99
196
  contents: jsx,
100
- resolveDir: path.join(__dirname, "../../.."),
197
+ resolveDir,
101
198
  loader: "jsx",
102
199
  },
103
200
  bundle: true,
@@ -107,8 +204,9 @@ module.exports = function (RED) {
107
204
  jsx: "transform",
108
205
  jsxFactory: "React.createElement",
109
206
  jsxFragment: "React.Fragment",
110
- external: ["react", "react-dom"],
207
+ external: externalList,
111
208
  define: { "process.env.NODE_ENV": '"production"' },
209
+ banner: { js: requireShim },
112
210
  });
113
211
  return { js: buildResult.outputFiles[0].text, error: null };
114
212
  } catch (e) {
@@ -137,10 +235,14 @@ module.exports = function (RED) {
137
235
  function extractPortalUser(headers) {
138
236
  const user = {};
139
237
  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"];
238
+ if (headers["x-portal-user-name"])
239
+ user.userName = headers["x-portal-user-name"];
240
+ if (headers["x-portal-user-username"])
241
+ user.username = headers["x-portal-user-username"];
242
+ if (headers["x-portal-user-email"])
243
+ user.email = headers["x-portal-user-email"];
244
+ if (headers["x-portal-user-role"])
245
+ user.role = headers["x-portal-user-role"];
144
246
  if (headers["x-portal-user-groups"]) {
145
247
  try {
146
248
  user.groups = JSON.parse(headers["x-portal-user-groups"]);
@@ -209,11 +311,13 @@ module.exports = function (RED) {
209
311
  const nodeId = node.id;
210
312
 
211
313
  // Config
212
- const endpoint = (config.endpoint || "/portal").replace(/\/+$/, "");
314
+ const endpoint = (config.endpoint || "/fromcubes").replace(/\/+$/, "");
213
315
  const componentCode = config.componentCode || "";
214
316
  const pageTitle = config.pageTitle || "Portal";
215
317
  const customHead = config.customHead || "";
216
318
  const portalAuth = config.portalAuth === true;
319
+ const showWsStatus = config.showWsStatus === true;
320
+ const libs = config.libs || [];
217
321
 
218
322
  // State
219
323
  const clients = new Set();
@@ -226,19 +330,51 @@ module.exports = function (RED) {
226
330
  // ── Rebuild: transpile JSX + update page state ────────────
227
331
 
228
332
  function rebuild() {
229
- // Topological sort: components used by others come first
230
- const entries = Object.entries(registry);
231
- const names = entries.map(([n]) => n);
333
+ // Build or get cached vendor bundle
334
+ let vendorBundle;
335
+ try {
336
+ vendorBundle = getVendorBundle(libs);
337
+ } catch (e) {
338
+ node.error("Vendor bundle failed: " + e.message);
339
+ node.status({ fill: "red", shape: "dot", text: "vendor build error" });
340
+ return;
341
+ }
342
+
343
+ // Selective injection: only include components referenced in user code (+ transitive deps)
344
+ const allEntries = Object.entries(registry);
345
+ const needed = new Set();
346
+
347
+ function addWithDeps(name) {
348
+ if (needed.has(name)) return;
349
+ const entry = registry[name];
350
+ if (!entry) return;
351
+ needed.add(name);
352
+ for (const [other] of allEntries) {
353
+ if (other !== name && entry.code.includes(other)) {
354
+ addWithDeps(other);
355
+ }
356
+ }
357
+ }
358
+
359
+ for (const [name] of allEntries) {
360
+ if (componentCode.includes(name)) {
361
+ addWithDeps(name);
362
+ }
363
+ }
364
+
365
+ // Topological sort only needed components
366
+ const entries = allEntries.filter(([n]) => needed.has(n));
232
367
  entries.sort((a, b) => {
233
368
  const aUsesB = a[1].code.includes(b[0]);
234
369
  const bUsesA = b[1].code.includes(a[0]);
235
- if (aUsesB && !bUsesA) return 1; // a depends on b → b first
370
+ if (aUsesB && !bUsesA) return 1; // a depends on b → b first
236
371
  if (bUsesA && !aUsesB) return -1; // b depends on a → a first
237
372
  return 0;
238
373
  });
239
374
  const libraryJsx = entries
240
- .map(([name, c]) =>
241
- `// Library: ${name}\nconst ${name} = (() => {\n${c.code}\nreturn ${name};\n})();`
375
+ .map(
376
+ ([name, c]) =>
377
+ `// Library: ${name}\nconst ${name} = (() => {\n${c.code}\nreturn ${name};\n})();`,
242
378
  )
243
379
  .join("\n\n");
244
380
 
@@ -248,17 +384,19 @@ module.exports = function (RED) {
248
384
  "const { createContext, memo, forwardRef, Fragment } = React;",
249
385
  "",
250
386
  "// ── 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
- }`,
387
+ [
388
+ "function useNodeRed() {",
389
+ " const [data, setData] = React.useState(window.__NR._lastData);",
390
+ " React.useEffect(() => {",
391
+ " return window.__NR.subscribe(setData);",
392
+ " }, []);",
393
+ " const send = React.useCallback((payload, topic) => {",
394
+ " window.__NR.send(payload, topic);",
395
+ " }, []);",
396
+ " const user = window.__NR._user || null;",
397
+ " return { data, send, user };",
398
+ "}",
399
+ ].join("\n"),
262
400
  "",
263
401
  "// ── Library components ──",
264
402
  libraryJsx,
@@ -270,7 +408,7 @@ module.exports = function (RED) {
270
408
  "ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));",
271
409
  ].join("\n");
272
410
 
273
- const compiled = transpile(fullJsx);
411
+ const compiled = transpile(fullJsx, libs);
274
412
 
275
413
  if (compiled.error) {
276
414
  node.error("JSX transpile error: " + compiled.error);
@@ -301,6 +439,8 @@ module.exports = function (RED) {
301
439
  wsPath,
302
440
  customHead,
303
441
  portalAuth,
442
+ showWsStatus,
443
+ vendorHash: vendorBundle.hash,
304
444
  };
305
445
  }
306
446
 
@@ -328,7 +468,9 @@ module.exports = function (RED) {
328
468
  return;
329
469
  }
330
470
  const cssHash = await state.cssHashReady;
331
- const user = state.portalAuth ? extractPortalUser(_req.headers) : null;
471
+ const user = state.portalAuth
472
+ ? extractPortalUser(_req.headers)
473
+ : null;
332
474
  res
333
475
  .type("text/html")
334
476
  .send(
@@ -339,6 +481,8 @@ module.exports = function (RED) {
339
481
  state.customHead,
340
482
  cssHash,
341
483
  user,
484
+ state.showWsStatus,
485
+ state.vendorHash,
342
486
  ),
343
487
  );
344
488
  });
@@ -500,7 +644,9 @@ module.exports = function (RED) {
500
644
  }); // end setImmediate
501
645
  }
502
646
 
503
- RED.nodes.registerType("portal-react", PortalReactNode);
647
+ RED.nodes.registerType("portal-react", PortalReactNode, {
648
+ dynamicModuleList: "libs",
649
+ });
504
650
 
505
651
  // ── Serve Monaco editor files locally ────────────────────────
506
652
  const express = require("express");
@@ -536,14 +682,21 @@ module.exports = function (RED) {
536
682
  res.send(css);
537
683
  });
538
684
 
539
- // ── Vendor React bundle endpoint ────────────────────────────
540
- RED.httpAdmin.get("/portal-react/vendor/react.min.js", (_req, res) => {
685
+ // ── Vendor bundle endpoint (dynamic, hash-based) ───────────
686
+ RED.httpAdmin.get("/portal-react/vendor/:hash.js", (req, res) => {
687
+ const entry = Object.values(vendorCache).find(
688
+ (v) => v.hash === req.params.hash,
689
+ );
690
+ if (!entry) {
691
+ res.status(404).send("Not found");
692
+ return;
693
+ }
541
694
  res.set({
542
695
  "Content-Type": "application/javascript",
543
696
  "Cache-Control": "public, max-age=31536000, immutable",
544
- ETag: `"${reactHash}"`,
697
+ ETag: `"${req.params.hash}"`,
545
698
  });
546
- res.send(reactBundle);
699
+ res.send(entry.js);
547
700
  });
548
701
 
549
702
  // ── Admin API for component registry ──────────────────────────
@@ -570,95 +723,116 @@ module.exports = function (RED) {
570
723
 
571
724
  // ── Page builders ─────────────────────────────────────────────
572
725
 
573
- function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user) {
726
+ function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user, showWsStatus, vendorHash) {
574
727
  return `<!DOCTYPE html>
575
- <html lang="en">
576
- <head>
577
- <meta charset="UTF-8">
578
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
579
- <title>${esc(title)}</title>
580
- <script src="${adminRoot}/portal-react/vendor/react.min.js?v=${reactHash}"><\/script>
581
- ${cssHash ? `<link rel="stylesheet" href="${adminRoot}/portal-react/css/${cssHash}.css">` : ""}
582
- ${escScript(customHead)}
583
- <style>
584
- @layer base{
585
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
586
- body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#e0e0e0}
587
- #root{min-height:100vh}
588
- }
589
- #__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}
590
- #__cs:hover{opacity:1}
591
- #__cs.ok{color:#4ade80}
592
- #__cs.err{color:#f87171}
593
- </style>
594
- </head>
595
- <body>
596
- <div id="root"></div>
597
- <div id="__cs" class="err">disconnected</div>
598
- <script>
599
- window.__NR={
600
- _ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,_version:null,_user:${user ? escScript(JSON.stringify(user)) : 'null'},
601
- connect(){
602
- const p=location.protocol==='https:'?'wss:':'ws:';
603
- const ws=new WebSocket(p+'//'+location.host+'${wsPath}');
604
- this._ws=ws;
605
- const s=document.getElementById('__cs');
606
- ws.onopen=()=>{
607
- if(this._wasConnected){s.textContent='connected';s.className='ok';this._retries=0;return;}
608
- s.textContent='connected';s.className='ok';this._retries=0;this._wasConnected=true;
609
- };
610
- ws.onmessage=(e)=>{
611
- try{const m=JSON.parse(e.data);
612
- if(m.type==='version'){if(this._version&&this._version!==m.hash){location.reload();return;}this._version=m.hash;}
613
- if(m.type==='data'){this._lastData=m.payload;this._listeners.forEach(fn=>fn(m.payload));}
614
- }catch(err){console.error('WS parse',err);}
615
- };
616
- ws.onclose=(e)=>{
617
- s.textContent='disconnected';s.className='err';
618
- this._ws=null;
619
- const delay=Math.min(500*Math.pow(2,this._retries),8000);
620
- this._retries++;
621
- setTimeout(()=>this.connect(),delay);
622
- };
623
- ws.onerror=()=>ws.close();
624
- },
625
- subscribe(fn){
626
- this._listeners.add(fn);
627
- if(this._lastData!==null)fn(this._lastData);
628
- return()=>this._listeners.delete(fn);
629
- },
630
- send(payload,topic){
631
- if(this._ws&&this._ws.readyState===1)
632
- this._ws.send(JSON.stringify({type:'output',payload,topic:topic||''}));
633
- }
634
- };
635
- window.__NR.connect();
636
- <\/script>
637
- <script>
638
- ${escScript(transpiledJs)}
639
- <\/script>
640
- </body>
641
- </html>`;
728
+ <html lang="en">
729
+ <head>
730
+ <meta charset="UTF-8">
731
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
732
+ <title>${esc(title)}</title>
733
+ <script src="${adminRoot}/portal-react/vendor/${vendorHash}.js"><\/script>
734
+ ${cssHash ? `<link rel="stylesheet" href="${adminRoot}/portal-react/css/${cssHash}.css">` : ""}
735
+ ${escScript(customHead)}
736
+ ${showWsStatus ? `<style>
737
+ #__cs {
738
+ position: fixed; bottom: 6px; right: 6px;
739
+ padding: 3px 8px; font-size: 10px; border-radius: 3px;
740
+ z-index: 99999; background: #111; border: 1px solid #333;
741
+ opacity: .7; transition: opacity .2s;
742
+ }
743
+ #__cs:hover { opacity: 1 }
744
+ #__cs.ok { color: #4ade80 }
745
+ #__cs.err { color: #f87171 }
746
+ </style>` : ""}
747
+ </head>
748
+ <body>
749
+ <div id="root"></div>
750
+ ${showWsStatus ? `<div id="__cs" class="err">fromcubes</div>` : ""}
751
+ <script>
752
+ window.__NR = {
753
+ _ws: null,
754
+ _listeners: new Set(),
755
+ _lastData: null,
756
+ _retries: 0,
757
+ _wasConnected: false,
758
+ _version: null,
759
+ _user: ${user ? escScript(JSON.stringify(user)) : "null"},
760
+
761
+ connect() {
762
+ const p = location.protocol === 'https:' ? 'wss:' : 'ws:';
763
+ const ws = new WebSocket(p + '//' + location.host + '${wsPath}');
764
+ this._ws = ws;
765
+ const s = document.getElementById('__cs');
766
+
767
+ ws.onopen = () => {
768
+ if (s) { s.textContent = 'fromcubes \u2022 connected'; s.className = 'ok'; }
769
+ this._retries = 0;
770
+ this._wasConnected = true;
771
+ };
772
+
773
+ ws.onmessage = (e) => {
774
+ try {
775
+ const m = JSON.parse(e.data);
776
+ if (m.type === 'version') {
777
+ if (this._version && this._version !== m.hash) { location.reload(); return; }
778
+ this._version = m.hash;
779
+ }
780
+ if (m.type === 'data') {
781
+ this._lastData = m.payload;
782
+ this._listeners.forEach(fn => fn(m.payload));
783
+ }
784
+ } catch (err) { console.error('WS parse', err); }
785
+ };
786
+
787
+ ws.onclose = () => {
788
+ if (s) { s.textContent = 'fromcubes \u2022 disconnected'; s.className = 'err'; }
789
+ this._ws = null;
790
+ const delay = Math.min(500 * Math.pow(2, this._retries), 8000);
791
+ this._retries++;
792
+ setTimeout(() => this.connect(), delay);
793
+ };
794
+
795
+ ws.onerror = () => ws.close();
796
+ },
797
+
798
+ subscribe(fn) {
799
+ this._listeners.add(fn);
800
+ if (this._lastData !== null) fn(this._lastData);
801
+ return () => this._listeners.delete(fn);
802
+ },
803
+
804
+ send(payload, topic) {
805
+ if (this._ws && this._ws.readyState === 1)
806
+ this._ws.send(JSON.stringify({ type: 'output', payload, topic: topic || '' }));
807
+ }
808
+ };
809
+ window.__NR.connect();
810
+ <\/script>
811
+ <script>
812
+ ${escScript(transpiledJs)}
813
+ <\/script>
814
+ </body>
815
+ </html>`;
642
816
  }
643
817
 
644
818
  function buildErrorPage(title, error) {
645
819
  return `<!DOCTYPE html>
646
- <html lang="en">
647
- <head>
648
- <meta charset="UTF-8">
649
- <title>${esc(title)} — Error</title>
650
- <style>
651
- body{font-family:monospace;background:#1a0000;color:#f87171;padding:40px;line-height:1.6}
652
- h1{color:#ff4444;margin-bottom:16px}
653
- pre{background:#0a0a0a;border:1px solid #ff4444;border-radius:8px;padding:20px;overflow-x:auto;color:#fca5a5}
654
- </style>
655
- </head>
656
- <body>
657
- <h1>JSX Transpile Error</h1>
658
- <p>Fix the component code in Node-RED and deploy again.</p>
659
- <pre>${esc(error)}</pre>
660
- </body>
661
- </html>`;
820
+ <html lang="en">
821
+ <head>
822
+ <meta charset="UTF-8">
823
+ <title>${esc(title)} — Error</title>
824
+ <style>
825
+ body { font-family: monospace; background: #1a0000; color: #f87171; padding: 40px; line-height: 1.6 }
826
+ h1 { color: #ff4444; margin-bottom: 16px }
827
+ pre { background: #0a0a0a; border: 1px solid #ff4444; border-radius: 8px; padding: 20px; overflow-x: auto; color: #fca5a5 }
828
+ </style>
829
+ </head>
830
+ <body>
831
+ <h1>JSX Transpile Error</h1>
832
+ <p>Fix the component code in Node-RED and deploy again.</p>
833
+ <pre>${esc(error)}</pre>
834
+ </body>
835
+ </html>`;
662
836
  }
663
837
 
664
838
  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.7",
3
+ "version": "0.1.0-alpha.9",
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",