@aaqu/fromcubes-portal-react 0.1.0-alpha.21 → 0.1.0-alpha.22

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.
@@ -66,11 +66,39 @@ module.exports = function (RED) {
66
66
  const endpointOwners = RED.settings.fcEndpointOwners;
67
67
 
68
68
  // Track component name ownership: { compName: nodeId } — prevents duplicate component names
69
+ // Shared namespace with fc-portal-utility nodes so a component and a utility
70
+ // can never share the same name.
69
71
  if (!RED.settings.fcCompNameOwners) {
70
72
  RED.settings.fcCompNameOwners = {};
71
73
  }
72
74
  const compNameOwners = RED.settings.fcCompNameOwners;
73
75
 
76
+ // Utility registry: populated by fc-portal-utility canvas nodes at deploy time.
77
+ // Keyed by node-level utilName (e.g. "mathHelpers"), value { code, error }.
78
+ // Each utility node may declare multiple top-level symbols inside its code block.
79
+ if (!RED.settings.fcPortalUtilities) {
80
+ RED.settings.fcPortalUtilities = {};
81
+ }
82
+ const utilities = RED.settings.fcPortalUtilities;
83
+
84
+ // Per-portal set of utility node names this portal depends on (transitively).
85
+ // Lets utility code changes target only portals that reference at least one
86
+ // symbol declared by the changed utility node.
87
+ if (!RED.settings.fcPortalNeededUtilities) {
88
+ RED.settings.fcPortalNeededUtilities = {};
89
+ }
90
+ const portalNeededUtilities = RED.settings.fcPortalNeededUtilities;
91
+
92
+ // Track owner of each top-level symbol declared inside fc-portal-utility nodes:
93
+ // { symbol: utilName }. Lets us catch collisions upfront (component-name vs
94
+ // utility-symbol, utility-symbol vs utility-symbol from another node) before
95
+ // they reach esbuild, where the diagnostic would surface on the portal node
96
+ // as a confusing transpile error rather than on the offending utility node.
97
+ if (!RED.settings.fcUtilSymbolOwners) {
98
+ RED.settings.fcUtilSymbolOwners = {};
99
+ }
100
+ const utilSymbolOwners = RED.settings.fcUtilSymbolOwners;
101
+
74
102
  // Per-portal config signature — detects no-op Full-deploy reconstructions so unchanged
75
103
  // portals skip rebuild entirely (Node-RED closes/reopens every node on Full deploy).
76
104
  if (!RED.settings.fcPortalSig) {
@@ -133,10 +161,15 @@ module.exports = function (RED) {
133
161
  if (dirty.size > 0) {
134
162
  for (const nodeId of Object.keys(rebuildCallbacks)) {
135
163
  if (targetIds.has(nodeId)) continue;
136
- const used = portalNeeded[nodeId];
164
+ const usedComps = portalNeeded[nodeId];
165
+ const usedUtils = portalNeededUtilities[nodeId];
137
166
  const raw = portalCode[nodeId] || "";
138
167
  for (const name of dirty) {
139
- if ((used && used.has(name)) || raw.includes(name)) {
168
+ if (
169
+ (usedComps && usedComps.has(name)) ||
170
+ (usedUtils && usedUtils.has(name)) ||
171
+ raw.includes(name)
172
+ ) {
140
173
  targetIds.add(nodeId);
141
174
  break;
142
175
  }
@@ -213,6 +246,25 @@ module.exports = function (RED) {
213
246
  });
214
247
  return;
215
248
  }
249
+
250
+ // Cross-namespace collision: compName matches an existing utility's
251
+ // internal top-level symbol → bundle would have two declarations of the
252
+ // same identifier (component IIFE + utility raw decl).
253
+ const utilSymOwner = utilSymbolOwners[compName];
254
+ if (utilSymOwner) {
255
+ node.error(
256
+ `Component name "${compName}" conflicts with a top-level symbol declared in utility "${utilSymOwner}"`,
257
+ );
258
+ node.status({
259
+ fill: "red",
260
+ shape: "ring",
261
+ text: "duplicate symbol: " + compName,
262
+ });
263
+ node.on("close", function (_removed, done) {
264
+ if (done) done();
265
+ });
266
+ return;
267
+ }
216
268
  compNameOwners[compName] = node.id;
217
269
 
218
270
  const newCode = config.compCode || "";
@@ -245,6 +297,145 @@ module.exports = function (RED) {
245
297
  }
246
298
  RED.nodes.registerType("fc-portal-component", PortalComponentNode);
247
299
 
300
+ // ── Canvas node: shared utility (helpers / hooks / constants) ─
301
+
302
+ // Parse top-level symbols from utility code: function/const/let/class declarations.
303
+ // Used for selective inclusion (does user JSX reference any of these symbols?)
304
+ // and for the editor "Utilities" dialog (lists exported identifiers per node).
305
+ function extractUtilitySymbols(code) {
306
+ const names = new Set();
307
+ const re = /^(?:export\s+)?(?:async\s+)?(?:function\s*\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm;
308
+ let m;
309
+ while ((m = re.exec(code || ""))) names.add(m[1]);
310
+ return names;
311
+ }
312
+
313
+ function PortalUtilityNode(config) {
314
+ RED.nodes.createNode(this, config);
315
+ const node = this;
316
+ const utilName = (config.utilName || "").trim();
317
+
318
+ if (!isSafeName(utilName)) {
319
+ node.error("Invalid utility name: " + utilName);
320
+ node.status({ fill: "red", shape: "dot", text: "invalid name" });
321
+ return;
322
+ }
323
+
324
+ // Duplicate name check across components AND utilities (shared namespace)
325
+ const existingOwner = compNameOwners[utilName];
326
+ if (existingOwner && existingOwner !== node.id) {
327
+ node.error(
328
+ `Name "${utilName}" is already used by another component or utility`,
329
+ );
330
+ node.status({
331
+ fill: "red",
332
+ shape: "ring",
333
+ text: "duplicate: " + utilName,
334
+ });
335
+ node.on("close", function (_removed, done) {
336
+ if (done) done();
337
+ });
338
+ return;
339
+ }
340
+ compNameOwners[utilName] = node.id;
341
+
342
+ const newCode = config.utilCode || "";
343
+ const prevCode = utilities[utilName]?.code;
344
+ const prevSyms = extractUtilitySymbols(prevCode || "");
345
+ const newSyms = extractUtilitySymbols(newCode);
346
+
347
+ // Free this node's previously-registered symbols before checking new ones,
348
+ // so a redeploy that simply renames an internal symbol doesn't see itself
349
+ // as a collision.
350
+ for (const s of prevSyms) {
351
+ if (utilSymbolOwners[s] === utilName) delete utilSymbolOwners[s];
352
+ }
353
+
354
+ // Cross-namespace symbol collision check:
355
+ // - vs component names (a component declares `const Name = (() => ...)();`
356
+ // at top level — a utility-internal `function Name` would clash)
357
+ // - vs other utility nodes' internal symbols (raw top-level decls clash)
358
+ const conflicts = [];
359
+ for (const s of newSyms) {
360
+ if (Object.prototype.hasOwnProperty.call(registry, s)) {
361
+ conflicts.push(`${s} (component)`);
362
+ continue;
363
+ }
364
+ const symOwner = utilSymbolOwners[s];
365
+ if (symOwner && symOwner !== utilName) {
366
+ conflicts.push(`${s} (utility ${symOwner})`);
367
+ }
368
+ }
369
+
370
+ const syntaxErr = quickCheckSyntax(newCode);
371
+ const dupErr =
372
+ conflicts.length > 0
373
+ ? "duplicate symbols: " + conflicts.join(", ")
374
+ : null;
375
+ // Syntax error takes precedence (most actionable). If both, both are
376
+ // surfaced in the node.error message but status text shows syntax.
377
+ const combinedErr = syntaxErr || dupErr;
378
+
379
+ utilities[utilName] = { code: newCode, error: combinedErr };
380
+
381
+ if (combinedErr) {
382
+ const msgs = [syntaxErr, dupErr].filter(Boolean).join(" | ");
383
+ node.error(`Utility "${utilName}": ${msgs}`);
384
+ if (syntaxErr) {
385
+ const short = syntaxErr.split("\n")[0].slice(0, 40);
386
+ node.status({ fill: "red", shape: "dot", text: "syntax: " + short });
387
+ } else {
388
+ const firstSym = conflicts[0].split(" ")[0];
389
+ node.status({
390
+ fill: "red",
391
+ shape: "ring",
392
+ text: "duplicate symbol: " + firstSym,
393
+ });
394
+ }
395
+ // Don't register symbols on conflict — leave the namespace clean for
396
+ // whichever node actually owns them. Dependent portals will surface
397
+ // "broken: <utilName>" via the standard utility-error path.
398
+ } else {
399
+ // Register all new symbols as owned by this utility node
400
+ for (const s of newSyms) utilSymbolOwners[s] = utilName;
401
+ node.status({
402
+ fill: "green",
403
+ shape: "dot",
404
+ text: "utility: " + utilName,
405
+ });
406
+ }
407
+
408
+ // Trigger rebuild of portals using this utility (any of its inner symbols).
409
+ // Push BOTH the node-level utilName AND each top-level symbol into the
410
+ // dirty set so portals matching by either are caught by selective rebuild.
411
+ if (prevCode !== newCode) {
412
+ scheduleRebuildUsing(utilName);
413
+ for (const s of newSyms) scheduleRebuildUsing(s);
414
+ // Symbols removed in this edit must also force rebuild for portals that
415
+ // referenced them — those portals will fail to find the symbol and need
416
+ // to surface an error or recompile against the new utility code.
417
+ for (const s of prevSyms) if (!newSyms.has(s)) scheduleRebuildUsing(s);
418
+ }
419
+
420
+ node.on("close", function (removed, done) {
421
+ if (compNameOwners[utilName] === node.id) {
422
+ delete compNameOwners[utilName];
423
+ }
424
+ // Remove all symbols this node currently owns (none if it errored out
425
+ // on dup-check, all of newSyms otherwise — driven by the registration
426
+ // table, not by re-parsing the code).
427
+ for (const s of Object.keys(utilSymbolOwners)) {
428
+ if (utilSymbolOwners[s] === utilName) delete utilSymbolOwners[s];
429
+ }
430
+ const removedSyms = extractUtilitySymbols(utilities[utilName]?.code || "");
431
+ delete utilities[utilName];
432
+ scheduleRebuildUsing(utilName);
433
+ for (const s of removedSyms) scheduleRebuildUsing(s);
434
+ if (done) done();
435
+ });
436
+ }
437
+ RED.nodes.registerType("fc-portal-utility", PortalUtilityNode);
438
+
248
439
  // ── Main node: portal-react ───────────────────────────────────
249
440
 
250
441
  function PortalReactNode(config) {
@@ -410,6 +601,45 @@ module.exports = function (RED) {
410
601
  // can target only affected portals.
411
602
  portalNeeded[nodeId] = new Set(needed);
412
603
 
604
+ // ── Selective utility injection ──
605
+ // Each utility node contributes raw top-level code that may declare
606
+ // multiple symbols. Pull the whole node in if the user JSX OR any
607
+ // included library component references at least one of its symbols.
608
+ const allUtilEntries = Object.entries(utilities);
609
+ const utilSymbols = new Map(); // utilName -> Set of symbol names
610
+ for (const [n, u] of allUtilEntries) {
611
+ utilSymbols.set(n, extractUtilitySymbols(u.code || ""));
612
+ }
613
+ const includedLibraryCode = [...needed]
614
+ .map((n) => registry[n]?.code || "")
615
+ .join("\n");
616
+ const referencesAnySymbol = (text, syms) => {
617
+ for (const s of syms) {
618
+ const re = new RegExp(`\\b${s}\\b`);
619
+ if (re.test(text)) return true;
620
+ }
621
+ return false;
622
+ };
623
+ const neededUtils = new Set();
624
+ function addUtilWithDeps(name) {
625
+ if (neededUtils.has(name)) return;
626
+ const u = utilities[name];
627
+ if (!u) return;
628
+ neededUtils.add(name);
629
+ // Transitively include other utilities referenced by this utility's code
630
+ for (const [other, otherSyms] of utilSymbols) {
631
+ if (other === name) continue;
632
+ if (referencesAnySymbol(u.code || "", otherSyms)) {
633
+ addUtilWithDeps(other);
634
+ }
635
+ }
636
+ }
637
+ const userScanText = componentCode + "\n" + includedLibraryCode;
638
+ for (const [n, syms] of utilSymbols) {
639
+ if (referencesAnySymbol(userScanText, syms)) addUtilWithDeps(n);
640
+ }
641
+ portalNeededUtilities[nodeId] = new Set(neededUtils);
642
+
413
643
  // Topological sort only needed components
414
644
  const entries = allEntries.filter(([n]) => needed.has(n));
415
645
  entries.sort((a, b) => {
@@ -426,17 +656,39 @@ module.exports = function (RED) {
426
656
  )
427
657
  .join("\n\n");
428
658
 
429
- // Extract import statements from library/user code so they appear at top level
659
+ // Build utility block raw top-level concat of needed utility codes.
660
+ // No IIFE wrapper: a single utility node may declare many symbols.
661
+ const utilityJsx = [...neededUtils]
662
+ .map((n) => `// Utility: ${n}\n${utilities[n].code}`)
663
+ .join("\n\n");
664
+
665
+ // Extract import statements from library/utility/user code so they appear at top level
430
666
  const importRe = /^import\s+.+?from\s+['"].+?['"];?\s*$/gm;
431
667
  const libImports = libraryJsx.match(importRe) || [];
432
668
  const userImports = componentCode.match(importRe) || [];
669
+ const utilImports = utilityJsx.match(importRe) || [];
433
670
  const cleanLibJsx = libraryJsx.replace(importRe, "").trim();
434
671
  const cleanCompCode = componentCode.replace(importRe, "").trim();
672
+ const cleanUtilJsx = utilityJsx.replace(importRe, "").trim();
673
+
674
+ // Dedupe imports across all sources (libs may already pull React; user
675
+ // and utility may import the same package).
676
+ const seenImports = new Set();
677
+ const dedupImports = (arr) =>
678
+ arr.filter((s) => {
679
+ const k = s.trim();
680
+ if (seenImports.has(k)) return false;
681
+ seenImports.add(k);
682
+ return true;
683
+ });
684
+ const allLibImports = dedupImports(libImports);
685
+ const allUserImports = dedupImports(userImports);
686
+ const allUtilImports = dedupImports(utilImports);
435
687
 
436
688
  // Warn about import * (prevents tree-shaking)
437
689
  const starRe = /^import\s+\*\s+as\s+(\w+)\s+from\s+['"](.+?)['"];?\s*$/;
438
- const allCode = cleanLibJsx + "\n" + cleanCompCode;
439
- for (const imp of [...libImports, ...userImports]) {
690
+ const allCode = cleanLibJsx + "\n" + cleanUtilJsx + "\n" + cleanCompCode;
691
+ for (const imp of [...allLibImports, ...allUserImports, ...allUtilImports]) {
440
692
  const m = imp.match(starRe);
441
693
  if (!m) continue;
442
694
  const [, localName, modulePath] = m;
@@ -461,8 +713,9 @@ module.exports = function (RED) {
461
713
  'import React from "react";',
462
714
  'import ReactDOM from "react-dom";',
463
715
  'import { createRoot } from "react-dom/client";',
464
- ...libImports,
465
- ...userImports,
716
+ ...allLibImports,
717
+ ...allUtilImports,
718
+ ...allUserImports,
466
719
  "",
467
720
  "// ── React shorthand ──",
468
721
  "Object.keys(React).filter(k => /^use[A-Z]/.test(k)).forEach(k => { window[k] = React[k]; });",
@@ -487,6 +740,9 @@ module.exports = function (RED) {
487
740
  "}",
488
741
  ].join("\n"),
489
742
  "",
743
+ "// ── Utilities (helpers / hooks / constants) ──",
744
+ cleanUtilJsx,
745
+ "",
490
746
  "// ── Library components ──",
491
747
  cleanLibJsx,
492
748
  "",
@@ -499,14 +755,25 @@ module.exports = function (RED) {
499
755
 
500
756
  const jsxHash = hash(fullJsx);
501
757
 
502
- // ── Check: any used component has its own syntax error ──
758
+ // ── Check: any used component or utility has its own syntax error ──
503
759
  let errorSource = null;
760
+ let errorSourceKind = null; // 'component' | 'utility'
504
761
  for (const name of needed) {
505
762
  if (registry[name]?.error) {
506
763
  errorSource = name;
764
+ errorSourceKind = "component";
507
765
  break;
508
766
  }
509
767
  }
768
+ if (!errorSource) {
769
+ for (const name of neededUtils) {
770
+ if (utilities[name]?.error) {
771
+ errorSource = name;
772
+ errorSourceKind = "utility";
773
+ break;
774
+ }
775
+ }
776
+ }
510
777
 
511
778
  // ── Check: missing return in App ──
512
779
  let missingReturn = false;
@@ -529,11 +796,16 @@ module.exports = function (RED) {
529
796
  let cacheHit = false;
530
797
  let errorKind = null; // 'component' | 'missing-return' | 'transpile'
531
798
  if (errorSource) {
799
+ const srcErr =
800
+ errorSourceKind === "utility"
801
+ ? utilities[errorSource].error
802
+ : registry[errorSource].error;
803
+ const label = errorSourceKind === "utility" ? "Utility" : "Component";
532
804
  compiled = {
533
805
  js: null,
534
- error: `Component "${errorSource}" has a syntax error:\n\n${registry[errorSource].error}`,
806
+ error: `${label} "${errorSource}" has a syntax error:\n\n${srcErr}`,
535
807
  };
536
- errorKind = "component";
808
+ errorKind = errorSourceKind;
537
809
  } else if (missingReturn) {
538
810
  compiled = {
539
811
  js: null,
@@ -557,6 +829,8 @@ module.exports = function (RED) {
557
829
  node.error(
558
830
  (errorKind === "component"
559
831
  ? `Component "${errorSource}" syntax error: `
832
+ : errorKind === "utility"
833
+ ? `Utility "${errorSource}" syntax error: `
560
834
  : errorKind === "missing-return"
561
835
  ? "App component has no return statement: "
562
836
  : "JSX transpile error: ") + compiled.error,
@@ -750,7 +1024,7 @@ module.exports = function (RED) {
750
1024
  .set("Cache-Control", "no-store")
751
1025
  .type("text/html")
752
1026
  .send(
753
- `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Building\u2026</title><style>@keyframes __sp{to{transform:rotate(360deg)}}body{font-family:monospace;background:#111;color:#888;margin:0;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center}</style></head><body><div style="font-size:24px;margin-bottom:16px">Building\u2026</div><div style="width:40px;height:40px;border:3px solid #333;border-top-color:#888;border-radius:50%;animation:__sp .8s linear infinite"></div><script>(function(){var r=0;function c(){var p=location.protocol==='https:'?'wss:':'ws:';var ws=new WebSocket(p+'//'+location.host+'${bWsPath}');ws.onmessage=function(e){try{var m=JSON.parse(e.data);if((m.type==='version'&&m.hash)||m.type==='error')location.reload();}catch(_){}};ws.onclose=function(){var d=Math.min(500*Math.pow(2,r),8000);r++;setTimeout(c,d);};ws.onerror=function(){ws.close();};}c();})()</script></body></html>`,
1027
+ `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Building\u2026</title><style>@keyframes __sp{to{transform:rotate(360deg)}}body{font-family:monospace;background:#111;color:#888;margin:0;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center}</style></head><body><div style="font-size:24px;margin-bottom:16px">Building\u2026</div><div style="width:40px;height:40px;border:3px solid #333;border-top-color:#888;border-radius:50%;animation:__sp .8s linear infinite"></div><script>(function(){let r=0;function c(){const p=location.protocol==='https:'?'wss:':'ws:';const ws=new WebSocket(p+'//'+location.host+'${bWsPath}');ws.onmessage=function(e){try{const m=JSON.parse(e.data);if((m.type==='version'&&m.hash)||m.type==='error')location.reload();}catch(_){}};ws.onclose=function(){const d=Math.min(500*Math.pow(2,r),8000);r++;setTimeout(c,d);};ws.onerror=function(){ws.close();};}c();})()</script></body></html>`,
754
1028
  );
755
1029
  return;
756
1030
  }
@@ -1058,6 +1332,7 @@ module.exports = function (RED) {
1058
1332
  // Unregister rebuild callback + selective-rebuild metadata
1059
1333
  delete rebuildCallbacks[nodeId];
1060
1334
  delete portalNeeded[nodeId];
1335
+ delete portalNeededUtilities[nodeId];
1061
1336
  delete portalCode[nodeId];
1062
1337
 
1063
1338
  // Release endpoint ownership
@@ -1191,4 +1466,85 @@ module.exports = function (RED) {
1191
1466
  }
1192
1467
  res.json({ ok: true });
1193
1468
  });
1469
+
1470
+ // ── Admin API for utility registry ────────────────────────────
1471
+
1472
+ RED.httpAdmin.get("/portal-react/utilities", (_req, res) => {
1473
+ // Include parsed top-level symbols so the editor "Utilities" dialog can
1474
+ // list which identifiers each node exports.
1475
+ const out = {};
1476
+ for (const [name, u] of Object.entries(utilities)) {
1477
+ out[name] = {
1478
+ code: u.code,
1479
+ error: u.error || null,
1480
+ symbols: [...extractUtilitySymbols(u.code || "")],
1481
+ };
1482
+ }
1483
+ res.json(out);
1484
+ });
1485
+
1486
+ RED.httpAdmin.post("/portal-react/utilities", (req, res) => {
1487
+ const { name, code } = req.body || {};
1488
+ if (!isSafeName(name))
1489
+ return res.status(400).json({ error: "invalid name" });
1490
+ const newCode = code || "";
1491
+ const prevCode = utilities[name]?.code;
1492
+ const prevSyms = extractUtilitySymbols(prevCode || "");
1493
+ const newSyms = extractUtilitySymbols(newCode);
1494
+
1495
+ // Free previously-owned symbols before conflict check (mirror node ctor)
1496
+ for (const s of prevSyms) {
1497
+ if (utilSymbolOwners[s] === name) delete utilSymbolOwners[s];
1498
+ }
1499
+
1500
+ const conflicts = [];
1501
+ for (const s of newSyms) {
1502
+ if (Object.prototype.hasOwnProperty.call(registry, s)) {
1503
+ conflicts.push(`${s} (component)`);
1504
+ continue;
1505
+ }
1506
+ const symOwner = utilSymbolOwners[s];
1507
+ if (symOwner && symOwner !== name) {
1508
+ conflicts.push(`${s} (utility ${symOwner})`);
1509
+ }
1510
+ }
1511
+
1512
+ const syntaxErr = quickCheckSyntax(newCode);
1513
+ const dupErr =
1514
+ conflicts.length > 0
1515
+ ? "duplicate symbols: " + conflicts.join(", ")
1516
+ : null;
1517
+ const combinedErr = syntaxErr || dupErr;
1518
+
1519
+ utilities[name] = { code: newCode, error: combinedErr };
1520
+
1521
+ if (!combinedErr) {
1522
+ for (const s of newSyms) utilSymbolOwners[s] = name;
1523
+ }
1524
+
1525
+ if (prevCode !== newCode) {
1526
+ scheduleRebuildUsing(name);
1527
+ for (const s of newSyms) scheduleRebuildUsing(s);
1528
+ for (const s of prevSyms) if (!newSyms.has(s)) scheduleRebuildUsing(s);
1529
+ }
1530
+ res.json({ ok: true, error: combinedErr || null });
1531
+ });
1532
+
1533
+ RED.httpAdmin.delete("/portal-react/utilities/:name", (req, res) => {
1534
+ const name = req.params.name;
1535
+ if (!isSafeName(name))
1536
+ return res.status(400).json({ error: "invalid name" });
1537
+ const prev = utilities[name];
1538
+ delete utilities[name];
1539
+ // Release all symbols owned by this utility
1540
+ for (const s of Object.keys(utilSymbolOwners)) {
1541
+ if (utilSymbolOwners[s] === name) delete utilSymbolOwners[s];
1542
+ }
1543
+ if (prev) {
1544
+ const removedSyms = extractUtilitySymbols(prev.code || "");
1545
+ scheduleRebuildUsing(name);
1546
+ for (const s of removedSyms) scheduleRebuildUsing(s);
1547
+ }
1548
+ res.json({ ok: true });
1549
+ });
1194
1550
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaqu/fromcubes-portal-react",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.22",
4
4
  "description": "Fromcubes Portal - React for Node-RED with Tailwind CSS and auto complete",
5
5
  "keywords": [
6
6
  "node-red",