@aaqu/fromcubes-portal-react 0.1.0-alpha.23 → 0.1.0-alpha.25
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/README.md +2 -1
- package/nodes/lib/helpers.js +42 -0
- package/nodes/lib/page-builder.js +22 -2
- package/nodes/portal-react.js +22 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -227,7 +227,8 @@ Import **Shared Components** first — it provides the UI building blocks (Page,
|
|
|
227
227
|
| Red status "legacy endpoint" on deploy | Flow was saved before the `/fromcubes/` prefix became hardcoded. Open the node, set **Sub-path**, redeploy. Automatic migration is disabled to avoid silent URL changes. |
|
|
228
228
|
| Red status "bad sub-path" | Sub-path is empty or violates the rules (no leading `/`, no whitespace, no `..`, segments must start alphanumerically, `public`/`_ws` reserved). |
|
|
229
229
|
| Yellow status "css-fail" | Tailwind generation failed (usually an invalid class in JSX). Page still loads but unstyled. Fix the class, redeploy — the status clears on the next successful build. |
|
|
230
|
-
|
|
|
230
|
+
| Code editor stays blank / `portal-react/vs/loader.js` 404s | Monaco is served live from the package's own `node_modules` at `/portal-react/vs` (no postinstall copy step). The 404 means `RED.httpAdmin` is mounted under a non-root `httpAdminRoot` the editor didn't account for, or `monaco-editor` failed to install. Reinstall the package and hard-refresh the editor (`Cmd/Ctrl-Shift-R`). |
|
|
231
|
+
| `npm install @aaqu/fromcubes-portal-react` ends with `EACCES` | The Node-RED `userDir/node_modules` install path is not writable by the user running Node-RED. Re-run `npm install` from a shell with the right ownership. |
|
|
231
232
|
| Browser request `/portal-react/css/<hash>.css` returns 404 | The portal's deploy hasn't produced a CSS bundle yet — open the editor, redeploy. If the URL is bookmarked from before a deploy, the hash is stale; reload the portal page itself. |
|
|
232
233
|
| WebSocket reconnects in an endless loop | Reverse-proxy is not forwarding `Upgrade: websocket` on `/fromcubes/<sub-path>/_ws`. Check the proxy config — nginx needs `proxy_set_header Upgrade $http_upgrade`, Traefik needs the `websocket` middleware. |
|
|
233
234
|
| `libs` packages fail to install on deploy | The user-installed npm packages declared in **Libs** install via Node-RED's `dynamicModuleList` mechanism, which needs network access from `userDir` and a writable `node_modules`. Behind a corporate proxy set `npm config set proxy …` for the Node-RED user. |
|
package/nodes/lib/helpers.js
CHANGED
|
@@ -363,6 +363,44 @@ function findMissingComponentRefs(userCode, knownNames) {
|
|
|
363
363
|
return missing;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Resolve the content hash the server may advertise to a browser over the WS
|
|
368
|
+
* `version` frame. The hash MUST only be non-empty when there is a page the
|
|
369
|
+
* GET route can actually serve — otherwise a stale hash makes the served error
|
|
370
|
+
* / building page reload in a tight loop (it reloads on any non-empty
|
|
371
|
+
* `version` hash).
|
|
372
|
+
*
|
|
373
|
+
* States, in order:
|
|
374
|
+
* - no state / nulled `compiled` (post-`close` teardown window) → ""
|
|
375
|
+
* - degraded (build error but a previous good build is kept) → lastGood hash
|
|
376
|
+
* - hard build error with no fallback → ""
|
|
377
|
+
* - real serveable build (`compiled.js` present) → contentHash
|
|
378
|
+
*
|
|
379
|
+
* @param {?PageState} state pageState[endpoint] (may be null/partially torn down).
|
|
380
|
+
* @returns {string} Hash to advertise, or "" when nothing is serveable.
|
|
381
|
+
*/
|
|
382
|
+
function serveableHash(state) {
|
|
383
|
+
if (!state || !state.compiled) return "";
|
|
384
|
+
if (state.compiled.error) {
|
|
385
|
+
return state.lastGood ? state.lastGood.contentHash || "" : "";
|
|
386
|
+
}
|
|
387
|
+
return state.compiled.js ? state.contentHash || "" : "";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* True only when `state` holds a real, serveable build. Used by the no-op
|
|
392
|
+
* redeploy guard to decide whether a rebuild can be skipped. MUST be false
|
|
393
|
+
* after close() nulls `compiled` (`{ ...compiled: null }` teardown window),
|
|
394
|
+
* otherwise the guard treats the destroyed build as valid, skips the rebuild,
|
|
395
|
+
* and the GET route serves the holding page forever (permanent spinner).
|
|
396
|
+
*
|
|
397
|
+
* @param {?PageState} state pageState[endpoint] (may be null/torn down).
|
|
398
|
+
* @returns {boolean}
|
|
399
|
+
*/
|
|
400
|
+
function hasFreshBuild(state) {
|
|
401
|
+
return !!state && !state.building && !!state.compiled && !state.compiled.error;
|
|
402
|
+
}
|
|
403
|
+
|
|
366
404
|
module.exports = function (RED) {
|
|
367
405
|
return createHelpers(RED);
|
|
368
406
|
};
|
|
@@ -373,6 +411,8 @@ module.exports.quickCheckSyntax = quickCheckSyntax;
|
|
|
373
411
|
module.exports.formatEsbuildError = formatEsbuildError;
|
|
374
412
|
module.exports.extractPortalUser = extractPortalUser;
|
|
375
413
|
module.exports.findMissingComponentRefs = findMissingComponentRefs;
|
|
414
|
+
module.exports.serveableHash = serveableHash;
|
|
415
|
+
module.exports.hasFreshBuild = hasFreshBuild;
|
|
376
416
|
module.exports.NAME_MAX_LEN = NAME_MAX_LEN;
|
|
377
417
|
module.exports.MAX_GROUPS_HEADER_BYTES = MAX_GROUPS_HEADER_BYTES;
|
|
378
418
|
|
|
@@ -593,6 +633,8 @@ function createHelpers(RED) {
|
|
|
593
633
|
quickCheckSyntax,
|
|
594
634
|
generateCSS,
|
|
595
635
|
extractPortalUser,
|
|
636
|
+
serveableHash,
|
|
637
|
+
hasFreshBuild,
|
|
596
638
|
removeRoute,
|
|
597
639
|
isSafeName,
|
|
598
640
|
validateSubPath,
|
|
@@ -218,6 +218,16 @@ function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user, showW
|
|
|
218
218
|
// Flushed in onopen so node status can go red even when the
|
|
219
219
|
// exception fires synchronously during initial bundle execution.
|
|
220
220
|
_pendingRuntimeError: null,
|
|
221
|
+
// Page-load timestamp + guarded reload: never reload within 2s of
|
|
222
|
+
// load, so a briefly-inconsistent server (error page served while a
|
|
223
|
+
// "ready" version hash is advertised over WS) cannot drive a tight
|
|
224
|
+
// reload loop. Caps recovery to one reload / 2s.
|
|
225
|
+
_loadT: Date.now(),
|
|
226
|
+
_reload() {
|
|
227
|
+
const wait = 2000 - (Date.now() - this._loadT);
|
|
228
|
+
if (wait > 0) setTimeout(() => location.reload(), wait);
|
|
229
|
+
else location.reload();
|
|
230
|
+
},
|
|
221
231
|
_user: ${user ? escScript(JSON.stringify(user)) : "null"},
|
|
222
232
|
|
|
223
233
|
connect() {
|
|
@@ -253,7 +263,7 @@ function buildPage(title, transpiledJs, wsPath, customHead, cssHash, user, showW
|
|
|
253
263
|
// exception caught locally renders the same overlay node and would
|
|
254
264
|
// otherwise loop reload to runtime-error to reload forever.
|
|
255
265
|
if (this._buildErrorActive || (this._version && this._version !== m.hash)) {
|
|
256
|
-
|
|
266
|
+
this._reload();
|
|
257
267
|
return;
|
|
258
268
|
}
|
|
259
269
|
this._version = m.hash;
|
|
@@ -379,6 +389,16 @@ function buildErrorPage(title, error, wsPath) {
|
|
|
379
389
|
const st = document.getElementById('__err_status');
|
|
380
390
|
const pre = document.querySelector('#__error_overlay pre');
|
|
381
391
|
let retries = 0;
|
|
392
|
+
// Reload guard: never reload within 2s of this page loading. If the
|
|
393
|
+
// server is briefly inconsistent (serves this error page yet
|
|
394
|
+
// advertises a "ready" version hash over WS), an unguarded reload
|
|
395
|
+
// turns into a tight loop. The guard caps it to one reload / 2s.
|
|
396
|
+
const __loadT = Date.now();
|
|
397
|
+
function __reload() {
|
|
398
|
+
const wait = 2000 - (Date.now() - __loadT);
|
|
399
|
+
if (wait > 0) { setTimeout(function(){ location.reload(); }, wait); }
|
|
400
|
+
else { location.reload(); }
|
|
401
|
+
}
|
|
382
402
|
function setStatus(text, ok) {
|
|
383
403
|
if (!st) return;
|
|
384
404
|
st.textContent = text;
|
|
@@ -394,7 +414,7 @@ function buildErrorPage(title, error, wsPath) {
|
|
|
394
414
|
ws.onmessage = function(e) {
|
|
395
415
|
try {
|
|
396
416
|
const m = JSON.parse(e.data);
|
|
397
|
-
if (m.type === 'version' && m.hash)
|
|
417
|
+
if (m.type === 'version' && m.hash) __reload();
|
|
398
418
|
if (m.type === 'error' && pre) pre.textContent = m.message;
|
|
399
419
|
} catch(_) {}
|
|
400
420
|
};
|
package/nodes/portal-react.js
CHANGED
|
@@ -544,6 +544,8 @@ module.exports = function (RED) {
|
|
|
544
544
|
quickCheckSyntax,
|
|
545
545
|
generateCSS,
|
|
546
546
|
extractPortalUser,
|
|
547
|
+
serveableHash,
|
|
548
|
+
hasFreshBuild,
|
|
547
549
|
removeRoute,
|
|
548
550
|
isSafeName,
|
|
549
551
|
validateSubPath,
|
|
@@ -1548,8 +1550,11 @@ module.exports = function (RED) {
|
|
|
1548
1550
|
);
|
|
1549
1551
|
const prevSig = portalSig[nodeId];
|
|
1550
1552
|
const existing = pageState[endpoint];
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
+
// hasFreshBuild also requires `compiled` to be present — close() nulls it on
|
|
1554
|
+
// redeploy, and a guard that ignored that (checking only building/error)
|
|
1555
|
+
// would treat the destroyed build as valid and skip the rebuild, leaving the
|
|
1556
|
+
// GET route serving the holding page forever. See helpers.hasFreshBuild.
|
|
1557
|
+
const hasValidBuild = hasFreshBuild(existing);
|
|
1553
1558
|
portalSig[nodeId] = sig;
|
|
1554
1559
|
|
|
1555
1560
|
if (prevSig !== sig || !hasValidBuild) {
|
|
@@ -1564,7 +1569,11 @@ module.exports = function (RED) {
|
|
|
1564
1569
|
RED.httpNode.get(endpoint, async function (_req, res) {
|
|
1565
1570
|
try {
|
|
1566
1571
|
const state = pageState[endpoint];
|
|
1567
|
-
|
|
1572
|
+
// `!state.compiled` is the teardown window between node close
|
|
1573
|
+
// (which nulls compiled) and the next rebuild. Treat it like
|
|
1574
|
+
// "building" — serve the holding page (200), never fall through to
|
|
1575
|
+
// `state.compiled.error` which would throw and 500 via the catch.
|
|
1576
|
+
if (!state || state.building || !state.compiled) {
|
|
1568
1577
|
const bWsPath = state?.wsPath || wsPath;
|
|
1569
1578
|
res
|
|
1570
1579
|
.set("Cache-Control", "no-store")
|
|
@@ -1721,10 +1730,11 @@ module.exports = function (RED) {
|
|
|
1721
1730
|
// In degraded mode (current build failed but lastGood served), advertise
|
|
1722
1731
|
// the lastGood hash so the freshly reloaded client matches the JS we sent.
|
|
1723
1732
|
const cs = pageState[endpoint];
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1733
|
+
// Only advertise a non-empty hash when there is a page the GET route
|
|
1734
|
+
// can actually serve (real build, or degraded lastGood). Advertising
|
|
1735
|
+
// a stale hash for a nulled/error state drives the served error page
|
|
1736
|
+
// into a reload loop. See helpers.serveableHash.
|
|
1737
|
+
const contentHash = serveableHash(cs);
|
|
1728
1738
|
wsSend(ws, { type: "version", hash: contentHash });
|
|
1729
1739
|
|
|
1730
1740
|
// Send assigned portalClient to browser
|
|
@@ -1964,6 +1974,11 @@ module.exports = function (RED) {
|
|
|
1964
1974
|
st.cssReady = null;
|
|
1965
1975
|
st.compiled = null;
|
|
1966
1976
|
st.css = null;
|
|
1977
|
+
// Clear the advertised hash too. Leaving a stale non-empty
|
|
1978
|
+
// contentHash while compiled is null makes the WS `version` frame
|
|
1979
|
+
// advertise a "ready" page that the GET route can no longer serve,
|
|
1980
|
+
// which drives the served error/building page into a reload loop.
|
|
1981
|
+
st.contentHash = "";
|
|
1967
1982
|
}
|
|
1968
1983
|
|
|
1969
1984
|
// Clean up route only on full removal/disable (not on redeploy).
|