@bonsae/nrg 0.18.4 → 0.19.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.
- package/README.md +38 -45
- package/package.json +1 -1
- package/server/index.cjs +96 -10
- package/server/resources/nrg-client.js +2269 -2233
- package/test/client/component/config.js +11 -0
- package/test/client/component/index.js +218 -235
- package/test/client/component/nrg.css +1 -0
- package/test/client/component/setup.js +1549 -140
- package/test/client/e2e/index.js +721 -367
- package/test/client/unit/index.js +204 -16
- package/test/client/unit/setup.js +209 -19
- package/test/server/unit/index.js +25 -4
- package/tsconfig/core/client.json +1 -1
- package/tsconfig/test/client/component.json +1 -1
- package/types/client.d.ts +98 -18
- package/types/server.d.ts +50 -12
- package/types/shims/brands.d.ts +32 -0
- package/types/shims/{form → client/form}/components/node-red-editor-input.vue.d.ts +1 -1
- package/types/shims/{form → client/form}/components/node-red-json-schema-form.vue.d.ts +21 -2
- package/types/shims/{form → client/form}/components/node-red-select-input.vue.d.ts +1 -0
- package/types/shims/{form → client/form}/components/node-red-typed-input.vue.d.ts +1 -0
- package/types/shims/client/types.d.ts +206 -0
- package/types/shims/components.d.ts +8 -8
- package/types/shims/constants.d.ts +4 -0
- package/types/shims/schema-options.d.ts +23 -10
- package/types/shims/typebox.d.ts +2 -2
- package/types/test-client-component.d.ts +170 -55
- package/types/test-client-e2e.d.ts +50 -0
- package/types/test-client-unit.d.ts +86 -22
- package/types/test-server-unit.d.ts +3 -1
- package/types/vite.d.ts +38 -9
- package/vite/index.js +733 -528
- /package/types/shims/{form → client/form}/components/node-red-config-input.vue.d.ts +0 -0
- /package/types/shims/{form → client/form}/components/node-red-input-label.vue.d.ts +0 -0
- /package/types/shims/{form → client/form}/components/node-red-input.vue.d.ts +0 -0
- /package/types/shims/{form → client/form}/components/node-red-toggle.vue.d.ts +0 -0
- /package/types/shims/{globals.d.ts → client/globals.d.ts} +0 -0
package/test/client/e2e/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/test/client/e2e/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs14 from "fs";
|
|
3
|
+
import path13 from "path";
|
|
4
4
|
|
|
5
5
|
// src/test/client/e2e/environment.ts
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import fs13 from "fs";
|
|
7
|
+
import path12 from "path";
|
|
8
8
|
|
|
9
9
|
// src/vite/server/build.ts
|
|
10
10
|
import { build as viteBuild } from "vite";
|
|
@@ -1177,10 +1177,10 @@ function generateHelpDoc(nodeClass, labels, t) {
|
|
|
1177
1177
|
if (inputSection) lines.push(inputSection);
|
|
1178
1178
|
}
|
|
1179
1179
|
if (nodeClass.outputsSchema) {
|
|
1180
|
-
const
|
|
1181
|
-
if (Array.isArray(
|
|
1180
|
+
const os3 = nodeClass.outputsSchema;
|
|
1181
|
+
if (Array.isArray(os3)) {
|
|
1182
1182
|
const portSections = [];
|
|
1183
|
-
|
|
1183
|
+
os3.forEach((schema, i) => {
|
|
1184
1184
|
const title = `${t.sections.port} ${i + 1}`;
|
|
1185
1185
|
const portPropLabels = labels.outputs?.[i];
|
|
1186
1186
|
const section = generateSchemaSection({
|
|
@@ -1199,9 +1199,9 @@ function generateHelpDoc(nodeClass, labels, t) {
|
|
|
1199
1199
|
${portSections.join("\n")}`
|
|
1200
1200
|
);
|
|
1201
1201
|
}
|
|
1202
|
-
} else if (!("type" in
|
|
1202
|
+
} else if (!("type" in os3 || "properties" in os3)) {
|
|
1203
1203
|
const portSections = [];
|
|
1204
|
-
for (const [portName, schema] of Object.entries(
|
|
1204
|
+
for (const [portName, schema] of Object.entries(os3)) {
|
|
1205
1205
|
const portPropLabels = labels.outputs?.[portName];
|
|
1206
1206
|
const section = generateSchemaSection({
|
|
1207
1207
|
title: portName,
|
|
@@ -1223,7 +1223,7 @@ ${portSections.join("\n")}`
|
|
|
1223
1223
|
const outputPropLabels = labels.outputs?.[0];
|
|
1224
1224
|
const section = generateSchemaSection({
|
|
1225
1225
|
title: t.sections.output,
|
|
1226
|
-
schema:
|
|
1226
|
+
schema: os3,
|
|
1227
1227
|
t,
|
|
1228
1228
|
labels: outputPropLabels,
|
|
1229
1229
|
includeDefault: false
|
|
@@ -1375,103 +1375,113 @@ function localesGenerator(options) {
|
|
|
1375
1375
|
];
|
|
1376
1376
|
const frameworkLabels = {
|
|
1377
1377
|
"en-US": {
|
|
1378
|
-
configs: { name: "Name" },
|
|
1378
|
+
configs: { name: "Name", returnProperty: "Return key" },
|
|
1379
1379
|
toggles: {
|
|
1380
1380
|
validateInput: "Validate Input",
|
|
1381
1381
|
validateOutput: "Validate Output",
|
|
1382
1382
|
errorPort: "Error Port",
|
|
1383
1383
|
completePort: "Complete Port",
|
|
1384
|
-
statusPort: "Status Port"
|
|
1384
|
+
statusPort: "Status Port",
|
|
1385
|
+
returnPropertyOverride: "Override return prop key"
|
|
1385
1386
|
}
|
|
1386
1387
|
},
|
|
1387
1388
|
de: {
|
|
1388
|
-
configs: { name: "Name" },
|
|
1389
|
+
configs: { name: "Name", returnProperty: "R\xFCckgabe-Schl\xFCssel" },
|
|
1389
1390
|
toggles: {
|
|
1390
1391
|
validateInput: "Eingabe validieren",
|
|
1391
1392
|
validateOutput: "Ausgabe validieren",
|
|
1392
1393
|
errorPort: "Fehler-Port",
|
|
1393
1394
|
completePort: "Abschluss-Port",
|
|
1394
|
-
statusPort: "Status-Port"
|
|
1395
|
+
statusPort: "Status-Port",
|
|
1396
|
+
returnPropertyOverride: "R\xFCckgabe-Schl\xFCssel \xFCberschreiben"
|
|
1395
1397
|
}
|
|
1396
1398
|
},
|
|
1397
1399
|
"es-ES": {
|
|
1398
|
-
configs: { name: "Nombre" },
|
|
1400
|
+
configs: { name: "Nombre", returnProperty: "Clave de retorno" },
|
|
1399
1401
|
toggles: {
|
|
1400
1402
|
validateInput: "Validar entrada",
|
|
1401
1403
|
validateOutput: "Validar salida",
|
|
1402
1404
|
errorPort: "Puerto de error",
|
|
1403
1405
|
completePort: "Puerto de completado",
|
|
1404
|
-
statusPort: "Puerto de estado"
|
|
1406
|
+
statusPort: "Puerto de estado",
|
|
1407
|
+
returnPropertyOverride: "Sobrescribir clave de retorno"
|
|
1405
1408
|
}
|
|
1406
1409
|
},
|
|
1407
1410
|
fr: {
|
|
1408
|
-
configs: { name: "Nom" },
|
|
1411
|
+
configs: { name: "Nom", returnProperty: "Cl\xE9 de retour" },
|
|
1409
1412
|
toggles: {
|
|
1410
1413
|
validateInput: "Valider l'entr\xE9e",
|
|
1411
1414
|
validateOutput: "Valider la sortie",
|
|
1412
1415
|
errorPort: "Port d'erreur",
|
|
1413
1416
|
completePort: "Port de compl\xE9tion",
|
|
1414
|
-
statusPort: "Port de statut"
|
|
1417
|
+
statusPort: "Port de statut",
|
|
1418
|
+
returnPropertyOverride: "Remplacer la cl\xE9 de retour"
|
|
1415
1419
|
}
|
|
1416
1420
|
},
|
|
1417
1421
|
ko: {
|
|
1418
|
-
configs: { name: "\uC774\uB984" },
|
|
1422
|
+
configs: { name: "\uC774\uB984", returnProperty: "\uBC18\uD658 \uD0A4" },
|
|
1419
1423
|
toggles: {
|
|
1420
1424
|
validateInput: "\uC785\uB825 \uAC80\uC99D",
|
|
1421
1425
|
validateOutput: "\uCD9C\uB825 \uAC80\uC99D",
|
|
1422
1426
|
errorPort: "\uC624\uB958 \uD3EC\uD2B8",
|
|
1423
1427
|
completePort: "\uC644\uB8CC \uD3EC\uD2B8",
|
|
1424
|
-
statusPort: "\uC0C1\uD0DC \uD3EC\uD2B8"
|
|
1428
|
+
statusPort: "\uC0C1\uD0DC \uD3EC\uD2B8",
|
|
1429
|
+
returnPropertyOverride: "\uBC18\uD658 \uD0A4 \uC7AC\uC815\uC758"
|
|
1425
1430
|
}
|
|
1426
1431
|
},
|
|
1427
1432
|
"pt-BR": {
|
|
1428
|
-
configs: { name: "Nome" },
|
|
1433
|
+
configs: { name: "Nome", returnProperty: "Chave de retorno" },
|
|
1429
1434
|
toggles: {
|
|
1430
1435
|
validateInput: "Validar Entrada",
|
|
1431
1436
|
validateOutput: "Validar Sa\xEDda",
|
|
1432
1437
|
errorPort: "Porta de Erro",
|
|
1433
1438
|
completePort: "Porta de Conclus\xE3o",
|
|
1434
|
-
statusPort: "Porta de Status"
|
|
1439
|
+
statusPort: "Porta de Status",
|
|
1440
|
+
returnPropertyOverride: "Sobrescrever chave de retorno"
|
|
1435
1441
|
}
|
|
1436
1442
|
},
|
|
1437
1443
|
ru: {
|
|
1438
|
-
configs: { name: "\u0418\u043C\u044F" },
|
|
1444
|
+
configs: { name: "\u0418\u043C\u044F", returnProperty: "\u041A\u043B\u044E\u0447 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0430" },
|
|
1439
1445
|
toggles: {
|
|
1440
1446
|
validateInput: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u0445\u043E\u0434",
|
|
1441
1447
|
validateOutput: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u044B\u0445\u043E\u0434",
|
|
1442
1448
|
errorPort: "\u041F\u043E\u0440\u0442 \u043E\u0448\u0438\u0431\u043A\u0438",
|
|
1443
1449
|
completePort: "\u041F\u043E\u0440\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F",
|
|
1444
|
-
statusPort: "\u041F\u043E\u0440\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0430"
|
|
1450
|
+
statusPort: "\u041F\u043E\u0440\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0430",
|
|
1451
|
+
returnPropertyOverride: "\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0430"
|
|
1445
1452
|
}
|
|
1446
1453
|
},
|
|
1447
1454
|
ja: {
|
|
1448
|
-
configs: { name: "\u540D\u524D" },
|
|
1455
|
+
configs: { name: "\u540D\u524D", returnProperty: "\u623B\u308A\u30AD\u30FC" },
|
|
1449
1456
|
toggles: {
|
|
1450
1457
|
validateInput: "\u5165\u529B\u691C\u8A3C",
|
|
1451
1458
|
validateOutput: "\u51FA\u529B\u691C\u8A3C",
|
|
1452
1459
|
errorPort: "\u30A8\u30E9\u30FC\u30DD\u30FC\u30C8",
|
|
1453
1460
|
completePort: "\u5B8C\u4E86\u30DD\u30FC\u30C8",
|
|
1454
|
-
statusPort: "\u30B9\u30C6\u30FC\u30BF\u30B9\u30DD\u30FC\u30C8"
|
|
1461
|
+
statusPort: "\u30B9\u30C6\u30FC\u30BF\u30B9\u30DD\u30FC\u30C8",
|
|
1462
|
+
returnPropertyOverride: "\u623B\u308A\u30AD\u30FC\u3092\u4E0A\u66F8\u304D"
|
|
1455
1463
|
}
|
|
1456
1464
|
},
|
|
1457
1465
|
"zh-CN": {
|
|
1458
|
-
configs: { name: "\u540D\u79F0" },
|
|
1466
|
+
configs: { name: "\u540D\u79F0", returnProperty: "\u8FD4\u56DE\u952E" },
|
|
1459
1467
|
toggles: {
|
|
1460
1468
|
validateInput: "\u9A8C\u8BC1\u8F93\u5165",
|
|
1461
1469
|
validateOutput: "\u9A8C\u8BC1\u8F93\u51FA",
|
|
1462
1470
|
errorPort: "\u9519\u8BEF\u7AEF\u53E3",
|
|
1463
1471
|
completePort: "\u5B8C\u6210\u7AEF\u53E3",
|
|
1464
|
-
statusPort: "\u72B6\u6001\u7AEF\u53E3"
|
|
1472
|
+
statusPort: "\u72B6\u6001\u7AEF\u53E3",
|
|
1473
|
+
returnPropertyOverride: "\u8986\u76D6\u8FD4\u56DE\u952E"
|
|
1465
1474
|
}
|
|
1466
1475
|
},
|
|
1467
1476
|
"zh-TW": {
|
|
1468
|
-
configs: { name: "\u540D\u7A31" },
|
|
1477
|
+
configs: { name: "\u540D\u7A31", returnProperty: "\u8FD4\u56DE\u9375" },
|
|
1469
1478
|
toggles: {
|
|
1470
1479
|
validateInput: "\u9A57\u8B49\u8F38\u5165",
|
|
1471
1480
|
validateOutput: "\u9A57\u8B49\u8F38\u51FA",
|
|
1472
1481
|
errorPort: "\u932F\u8AA4\u7AEF\u53E3",
|
|
1473
1482
|
completePort: "\u5B8C\u6210\u7AEF\u53E3",
|
|
1474
|
-
statusPort: "\u72C0\u614B\u7AEF\u53E3"
|
|
1483
|
+
statusPort: "\u72C0\u614B\u7AEF\u53E3",
|
|
1484
|
+
returnPropertyOverride: "\u8986\u84CB\u8FD4\u56DE\u9375"
|
|
1475
1485
|
}
|
|
1476
1486
|
}
|
|
1477
1487
|
};
|
|
@@ -1994,17 +2004,8 @@ async function build2(clientBuildOptions, buildContext) {
|
|
|
1994
2004
|
}
|
|
1995
2005
|
}
|
|
1996
2006
|
|
|
1997
|
-
// src/vite/node-red-launcher.ts
|
|
1998
|
-
import
|
|
1999
|
-
import getPort from "get-port";
|
|
2000
|
-
import detect from "detect-port";
|
|
2001
|
-
import { builtinModules as builtinModules2, createRequire as createRequire3 } from "module";
|
|
2002
|
-
import treeKill from "tree-kill";
|
|
2003
|
-
import fs10 from "fs";
|
|
2004
|
-
import os from "os";
|
|
2005
|
-
import path10 from "path";
|
|
2006
|
-
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
2007
|
-
import { build as esbuild } from "esbuild";
|
|
2007
|
+
// src/vite/node-red-launcher/index.ts
|
|
2008
|
+
import fs12 from "fs";
|
|
2008
2009
|
|
|
2009
2010
|
// src/vite/async-utils.ts
|
|
2010
2011
|
function withTimeout(promise, ms, fallback) {
|
|
@@ -2041,200 +2042,388 @@ async function retry(fn, options = {}) {
|
|
|
2041
2042
|
throw lastError;
|
|
2042
2043
|
}
|
|
2043
2044
|
|
|
2044
|
-
// src/vite/node-red-launcher.ts
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2045
|
+
// src/vite/node-red-launcher/entry-point.ts
|
|
2046
|
+
import { exec } from "child_process";
|
|
2047
|
+
import { randomUUID } from "crypto";
|
|
2048
|
+
import { createRequire as createRequire3 } from "module";
|
|
2049
|
+
import fs10 from "fs";
|
|
2050
|
+
import os from "os";
|
|
2051
|
+
import path10 from "path";
|
|
2052
|
+
function getNodeRedCommand(version) {
|
|
2053
|
+
return version ? `node-red@${version}` : "node-red";
|
|
2054
|
+
}
|
|
2055
|
+
function resolveNodeRedFromLocalNodeModules() {
|
|
2056
|
+
try {
|
|
2057
|
+
const require_ = createRequire3(path10.join(process.cwd(), "package.json"));
|
|
2058
|
+
const pkgJsonPath = require_.resolve("node-red/package.json");
|
|
2059
|
+
const pkgDir = path10.dirname(pkgJsonPath);
|
|
2060
|
+
const pkg = JSON.parse(fs10.readFileSync(pkgJsonPath, "utf-8"));
|
|
2061
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["node-red"];
|
|
2062
|
+
if (!bin) return null;
|
|
2063
|
+
const entry = path10.resolve(pkgDir, bin);
|
|
2064
|
+
return fs10.existsSync(entry) ? entry : null;
|
|
2065
|
+
} catch {
|
|
2066
|
+
return null;
|
|
2061
2067
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2068
|
+
}
|
|
2069
|
+
async function resolveNodeRed(options) {
|
|
2070
|
+
const { version, npxTimeoutMs = 3e5, logger: logger2 } = options;
|
|
2071
|
+
if (version && !/^[\w.^~<>=*-]+$/.test(version)) {
|
|
2072
|
+
throw new NodeRedStartError(
|
|
2073
|
+
new Error(`Invalid node-red version "${version}"`)
|
|
2074
|
+
);
|
|
2064
2075
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2076
|
+
const nodeRedCommand = getNodeRedCommand(version);
|
|
2077
|
+
logger2.info(`Resolving ${nodeRedCommand} entry point...`);
|
|
2078
|
+
const hasExplicitVersion = version !== void 0 && version !== "latest";
|
|
2079
|
+
if (!hasExplicitVersion) {
|
|
2080
|
+
const localEntry = resolveNodeRedFromLocalNodeModules();
|
|
2081
|
+
if (localEntry) {
|
|
2082
|
+
logger2.info(`Resolved from local node_modules: ${localEntry}`);
|
|
2083
|
+
return localEntry;
|
|
2084
|
+
}
|
|
2067
2085
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2086
|
+
logger2.info(
|
|
2087
|
+
hasExplicitVersion ? `Using configured version (${version}), downloading via npx...` : `Not found locally, downloading via npx (this may take a while)...`
|
|
2088
|
+
);
|
|
2089
|
+
const resolverScript = path10.join(
|
|
2090
|
+
os.tmpdir(),
|
|
2091
|
+
`nrg-resolve-node-red-${process.pid}-${randomUUID()}.cjs`
|
|
2092
|
+
);
|
|
2093
|
+
fs10.writeFileSync(
|
|
2094
|
+
resolverScript,
|
|
2095
|
+
`const fs = require("fs");
|
|
2096
|
+
const path = require("path");
|
|
2097
|
+
const isWin = process.platform === "win32";
|
|
2098
|
+
const binName = isWin ? "node-red.cmd" : "node-red";
|
|
2099
|
+
const dirs = process.env.PATH.split(path.delimiter);
|
|
2100
|
+
for (const d of dirs) {
|
|
2101
|
+
const f = path.join(d, binName);
|
|
2102
|
+
if (fs.existsSync(f)) {
|
|
2103
|
+
if (isWin) {
|
|
2104
|
+
const nodeRedDir = path.resolve(d, "..", "node-red");
|
|
2105
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(nodeRedDir, "package.json"), "utf-8"));
|
|
2106
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin["node-red"];
|
|
2107
|
+
process.stdout.write(path.resolve(nodeRedDir, bin));
|
|
2108
|
+
} else {
|
|
2109
|
+
process.stdout.write(fs.realpathSync(f));
|
|
2110
|
+
}
|
|
2111
|
+
break;
|
|
2070
2112
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2113
|
+
}`
|
|
2114
|
+
);
|
|
2115
|
+
try {
|
|
2116
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
2117
|
+
exec(
|
|
2118
|
+
`npx --yes -p ${nodeRedCommand} node "${resolverScript}"`,
|
|
2119
|
+
{ timeout: npxTimeoutMs },
|
|
2120
|
+
(error, stdout2) => {
|
|
2121
|
+
if (error) reject(error);
|
|
2122
|
+
else resolve(stdout2);
|
|
2123
|
+
}
|
|
2124
|
+
);
|
|
2125
|
+
});
|
|
2126
|
+
const entryPoint = stdout.trim();
|
|
2127
|
+
if (!entryPoint || !fs10.existsSync(entryPoint)) {
|
|
2128
|
+
throw new NodeRedStartError(
|
|
2129
|
+
new Error(
|
|
2130
|
+
`Could not resolve node-red entry point: ${entryPoint || "(empty)"}`
|
|
2131
|
+
)
|
|
2132
|
+
);
|
|
2075
2133
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2134
|
+
logger2.info(`Resolved via npx: ${entryPoint}`);
|
|
2135
|
+
return entryPoint;
|
|
2136
|
+
} finally {
|
|
2137
|
+
try {
|
|
2138
|
+
fs10.unlinkSync(resolverScript);
|
|
2139
|
+
} catch {
|
|
2078
2140
|
}
|
|
2079
|
-
return "node-red";
|
|
2080
2141
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/vite/node-red-launcher/settings.ts
|
|
2145
|
+
import { builtinModules as builtinModules2 } from "module";
|
|
2146
|
+
import fs11 from "fs";
|
|
2147
|
+
import os2 from "os";
|
|
2148
|
+
import path11 from "path";
|
|
2149
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
2150
|
+
import { build as esbuild } from "esbuild";
|
|
2151
|
+
function findUserRuntimeSettingsFilepath(settingsFilepath, logger2) {
|
|
2152
|
+
if (settingsFilepath) {
|
|
2153
|
+
const resolved2 = path11.resolve(settingsFilepath);
|
|
2154
|
+
if (fs11.existsSync(resolved2)) {
|
|
2155
|
+
return resolved2;
|
|
2094
2156
|
}
|
|
2157
|
+
logger2.warn(`Settings file not found: ${settingsFilepath}`);
|
|
2095
2158
|
return null;
|
|
2096
2159
|
}
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2160
|
+
const resolved = path11.resolve("node-red.settings.ts");
|
|
2161
|
+
if (fs11.existsSync(resolved)) {
|
|
2162
|
+
return resolved;
|
|
2163
|
+
}
|
|
2164
|
+
return null;
|
|
2165
|
+
}
|
|
2166
|
+
async function compileRuntimeSettingsFile(runtimeSettingsFilepath, port) {
|
|
2167
|
+
const compiledRuntimeSettingsFilepath = path11.join(
|
|
2168
|
+
os2.tmpdir(),
|
|
2169
|
+
`node-red.settings.${process.pid}-${port}.cjs`
|
|
2170
|
+
);
|
|
2171
|
+
const nodeBuiltins2 = [
|
|
2172
|
+
...builtinModules2,
|
|
2173
|
+
...builtinModules2.map((m) => `node:${m}`)
|
|
2174
|
+
];
|
|
2175
|
+
const settingsDir = path11.dirname(runtimeSettingsFilepath).split(path11.sep).join("/");
|
|
2176
|
+
const settingsFile = runtimeSettingsFilepath.split(path11.sep).join("/");
|
|
2177
|
+
await esbuild({
|
|
2178
|
+
entryPoints: [runtimeSettingsFilepath],
|
|
2179
|
+
outfile: compiledRuntimeSettingsFilepath,
|
|
2180
|
+
format: "cjs",
|
|
2181
|
+
platform: "node",
|
|
2182
|
+
target: "node18",
|
|
2183
|
+
bundle: true,
|
|
2184
|
+
define: {
|
|
2185
|
+
"import.meta.dirname": JSON.stringify(settingsDir),
|
|
2186
|
+
"import.meta.filename": JSON.stringify(settingsFile),
|
|
2187
|
+
"import.meta.url": JSON.stringify(pathToFileURL3(settingsFile).href)
|
|
2188
|
+
},
|
|
2189
|
+
external: [...nodeBuiltins2, "node-red", "@node-red/*"]
|
|
2190
|
+
});
|
|
2191
|
+
return compiledRuntimeSettingsFilepath;
|
|
2192
|
+
}
|
|
2193
|
+
async function generateRuntimeSettings(options) {
|
|
2194
|
+
const { outDir, port, settingsFilepath, httpAdminRoot, logger: logger2 } = options;
|
|
2195
|
+
const tempFiles = [];
|
|
2196
|
+
const userRuntimeSettingsFilepath = findUserRuntimeSettingsFilepath(
|
|
2197
|
+
settingsFilepath,
|
|
2198
|
+
logger2
|
|
2199
|
+
);
|
|
2200
|
+
let compiledRuntimeSettingsFilepath = null;
|
|
2201
|
+
if (userRuntimeSettingsFilepath) {
|
|
2202
|
+
compiledRuntimeSettingsFilepath = await compileRuntimeSettingsFile(
|
|
2203
|
+
userRuntimeSettingsFilepath,
|
|
2204
|
+
port
|
|
2101
2205
|
);
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
"import.meta.filename": JSON.stringify(settingsFile),
|
|
2118
|
-
"import.meta.url": JSON.stringify(pathToFileURL3(settingsFile).href)
|
|
2119
|
-
},
|
|
2120
|
-
external: [...nodeBuiltins2, "node-red", "@node-red/*"]
|
|
2121
|
-
});
|
|
2122
|
-
this.compiledRuntimeSettingsFilepath = compiledRuntimeSettingsFilepath;
|
|
2123
|
-
return compiledRuntimeSettingsFilepath;
|
|
2124
|
-
}
|
|
2125
|
-
async generateRuntimeSettingsFile() {
|
|
2126
|
-
const userRuntimeSettingsFilepath = this.findRuntimeSettingsFilepath();
|
|
2127
|
-
let compiledRuntimeSettingsFilepath = null;
|
|
2128
|
-
if (userRuntimeSettingsFilepath) {
|
|
2129
|
-
compiledRuntimeSettingsFilepath = await this.compileRuntimeSettingsFile(
|
|
2130
|
-
userRuntimeSettingsFilepath
|
|
2131
|
-
);
|
|
2132
|
-
}
|
|
2133
|
-
const outDir = path10.resolve(this.outDir).split(path10.sep).join("/");
|
|
2134
|
-
const cwd = process.cwd().split(path10.sep).join("/");
|
|
2135
|
-
const userDir = path10.resolve(cwd, ".node-red").split(path10.sep).join("/");
|
|
2136
|
-
const finalRuntimeSettingsFile = compiledRuntimeSettingsFilepath ? `
|
|
2137
|
-
const compiledRuntimeSettings = require("${compiledRuntimeSettingsFilepath.split(path10.sep).join("/")}");
|
|
2206
|
+
tempFiles.push(compiledRuntimeSettingsFilepath);
|
|
2207
|
+
}
|
|
2208
|
+
const normalizedOutDir = path11.resolve(outDir).split(path11.sep).join("/");
|
|
2209
|
+
const cwd = process.cwd().split(path11.sep).join("/");
|
|
2210
|
+
const userDir = path11.resolve(cwd, ".node-red").split(path11.sep).join("/");
|
|
2211
|
+
const userDirLiteral = JSON.stringify(userDir);
|
|
2212
|
+
const outDirLiteral = JSON.stringify(normalizedOutDir);
|
|
2213
|
+
const httpAdminRootAssignment = httpAdminRoot ? `settings.httpAdminRoot = ${JSON.stringify(httpAdminRoot)};
|
|
2214
|
+
` : "";
|
|
2215
|
+
const httpAdminRootEntry = httpAdminRoot ? `
|
|
2216
|
+
httpAdminRoot: ${JSON.stringify(httpAdminRoot)},` : "";
|
|
2217
|
+
const finalRuntimeSettingsFile = compiledRuntimeSettingsFilepath ? `
|
|
2218
|
+
const compiledRuntimeSettings = require(${JSON.stringify(
|
|
2219
|
+
compiledRuntimeSettingsFilepath.split(path11.sep).join("/")
|
|
2220
|
+
)});
|
|
2138
2221
|
const settings = compiledRuntimeSettings.default || compiledRuntimeSettings;
|
|
2139
|
-
settings.uiPort = ${
|
|
2140
|
-
if(!settings.userDir){
|
|
2141
|
-
settings.userDir =
|
|
2222
|
+
settings.uiPort = ${port};
|
|
2223
|
+
${httpAdminRootAssignment}if(!settings.userDir){
|
|
2224
|
+
settings.userDir = ${userDirLiteral};
|
|
2142
2225
|
}
|
|
2143
2226
|
settings.nodesDir = settings.nodesDir || [];
|
|
2144
|
-
if (!settings.nodesDir.includes(
|
|
2145
|
-
settings.nodesDir.push(
|
|
2227
|
+
if (!settings.nodesDir.includes(${outDirLiteral})) {
|
|
2228
|
+
settings.nodesDir.push(${outDirLiteral});
|
|
2146
2229
|
}
|
|
2147
2230
|
if(!settings.flowFile){
|
|
2148
2231
|
settings.flowFile = "flows.json";
|
|
2149
2232
|
}
|
|
2233
|
+
// the welcome tour overlay intercepts pointer events \u2014 fatal for e2e and
|
|
2234
|
+
// noise for dev; explicit user settings still win
|
|
2235
|
+
settings.editorTheme = settings.editorTheme || {};
|
|
2236
|
+
if (settings.editorTheme.tours === undefined) {
|
|
2237
|
+
settings.editorTheme.tours = false;
|
|
2238
|
+
}
|
|
2150
2239
|
module.exports = settings;
|
|
2151
2240
|
` : `
|
|
2152
2241
|
const settings = {
|
|
2153
|
-
uiPort: ${
|
|
2154
|
-
userDir:
|
|
2242
|
+
uiPort: ${port},
|
|
2243
|
+
userDir: ${userDirLiteral},
|
|
2155
2244
|
flowFile: "flows.json",
|
|
2156
|
-
nodesDir: [
|
|
2245
|
+
nodesDir: [${outDirLiteral}],${httpAdminRootEntry}
|
|
2246
|
+
// the welcome tour overlay intercepts pointer events \u2014 fatal for e2e
|
|
2247
|
+
editorTheme: { tours: false },
|
|
2157
2248
|
};
|
|
2158
2249
|
module.exports = settings;
|
|
2159
2250
|
`;
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2251
|
+
const finalRuntimeSettingsFilepath = path11.join(
|
|
2252
|
+
os2.tmpdir(),
|
|
2253
|
+
`node-red-settings-final-${process.pid}-${port}.cjs`
|
|
2254
|
+
);
|
|
2255
|
+
fs11.writeFileSync(finalRuntimeSettingsFilepath, finalRuntimeSettingsFile);
|
|
2256
|
+
tempFiles.push(finalRuntimeSettingsFilepath);
|
|
2257
|
+
return { filepath: finalRuntimeSettingsFilepath, tempFiles };
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// src/vite/node-red-launcher/process.ts
|
|
2261
|
+
import { spawn } from "child_process";
|
|
2262
|
+
import detect from "detect-port";
|
|
2263
|
+
import getPort from "get-port";
|
|
2264
|
+
import treeKill from "tree-kill";
|
|
2265
|
+
var READY_MARKERS = ["Started flows", "Server now running"];
|
|
2266
|
+
function start(options) {
|
|
2267
|
+
const { entryPoint, settingsPath, args, onLine } = options;
|
|
2268
|
+
const child = spawn(
|
|
2269
|
+
process.execPath,
|
|
2270
|
+
[entryPoint, "-s", settingsPath, ...args],
|
|
2271
|
+
{
|
|
2272
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2180
2273
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2274
|
+
);
|
|
2275
|
+
let isReady = false;
|
|
2276
|
+
let resolveReady;
|
|
2277
|
+
let rejectReady;
|
|
2278
|
+
const ready = new Promise((resolve, reject) => {
|
|
2279
|
+
resolveReady = resolve;
|
|
2280
|
+
rejectReady = reject;
|
|
2281
|
+
});
|
|
2282
|
+
const emitLine = (rawLine, source) => {
|
|
2283
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
2284
|
+
if (!line) return;
|
|
2285
|
+
onLine(line, source, isReady);
|
|
2286
|
+
if (source === "stdout" && READY_MARKERS.some((marker) => line.includes(marker))) {
|
|
2287
|
+
isReady = true;
|
|
2288
|
+
resolveReady();
|
|
2188
2289
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
const
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
fs10.writeFileSync(
|
|
2197
|
-
resolverScript,
|
|
2198
|
-
`const fs = require("fs");
|
|
2199
|
-
const path = require("path");
|
|
2200
|
-
const isWin = process.platform === "win32";
|
|
2201
|
-
const binName = isWin ? "node-red.cmd" : "node-red";
|
|
2202
|
-
const dirs = process.env.PATH.split(path.delimiter);
|
|
2203
|
-
for (const d of dirs) {
|
|
2204
|
-
const f = path.join(d, binName);
|
|
2205
|
-
if (fs.existsSync(f)) {
|
|
2206
|
-
if (isWin) {
|
|
2207
|
-
const nodeRedDir = path.resolve(d, "..", "node-red");
|
|
2208
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(nodeRedDir, "package.json"), "utf-8"));
|
|
2209
|
-
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin["node-red"];
|
|
2210
|
-
process.stdout.write(path.resolve(nodeRedDir, bin));
|
|
2211
|
-
} else {
|
|
2212
|
-
process.stdout.write(fs.realpathSync(f));
|
|
2290
|
+
};
|
|
2291
|
+
const remainders = { stdout: "", stderr: "" };
|
|
2292
|
+
const handleData = (data, source) => {
|
|
2293
|
+
const lines = (remainders[source] + data.toString()).split("\n");
|
|
2294
|
+
remainders[source] = lines.pop() ?? "";
|
|
2295
|
+
for (const line of lines) {
|
|
2296
|
+
emitLine(line, source);
|
|
2213
2297
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
{ encoding: "utf-8", timeout: 3e5 }
|
|
2222
|
-
).trim();
|
|
2223
|
-
if (!entryPoint || !fs10.existsSync(entryPoint)) {
|
|
2224
|
-
throw new NodeRedStartError(
|
|
2225
|
-
new Error(
|
|
2226
|
-
`Could not resolve node-red entry point: ${entryPoint || "(empty)"}`
|
|
2227
|
-
)
|
|
2228
|
-
);
|
|
2298
|
+
};
|
|
2299
|
+
const flushRemainders = () => {
|
|
2300
|
+
for (const source of ["stdout", "stderr"]) {
|
|
2301
|
+
const rest = remainders[source];
|
|
2302
|
+
remainders[source] = "";
|
|
2303
|
+
if (rest) {
|
|
2304
|
+
emitLine(rest, source);
|
|
2229
2305
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2306
|
+
}
|
|
2307
|
+
};
|
|
2308
|
+
child.stdout?.on("data", (data) => handleData(data, "stdout"));
|
|
2309
|
+
child.stderr?.on("data", (data) => handleData(data, "stderr"));
|
|
2310
|
+
child.on("error", (error) => {
|
|
2311
|
+
rejectReady(new NodeRedStartError(error));
|
|
2312
|
+
});
|
|
2313
|
+
child.on("exit", (code) => {
|
|
2314
|
+
flushRemainders();
|
|
2315
|
+
if (!isReady && code !== 0 && code !== null) {
|
|
2316
|
+
rejectReady(
|
|
2317
|
+
new NodeRedStartError(new Error(`Process exited with code ${code}`))
|
|
2318
|
+
);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
resolveReady();
|
|
2322
|
+
});
|
|
2323
|
+
return { child, ready };
|
|
2324
|
+
}
|
|
2325
|
+
function kill(pid) {
|
|
2326
|
+
return new Promise((resolve) => {
|
|
2327
|
+
treeKill(pid, "SIGKILL", () => resolve());
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
async function stop(options) {
|
|
2331
|
+
const { child, pid, gracefulTimeoutMs = 1e4, logger: logger2 } = options;
|
|
2332
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
const exited = new Promise((resolve) => {
|
|
2336
|
+
child.once("exit", () => resolve());
|
|
2337
|
+
treeKill(pid, "SIGTERM", (error) => {
|
|
2338
|
+
if (error) {
|
|
2339
|
+
try {
|
|
2340
|
+
process.kill(pid, "SIGTERM");
|
|
2341
|
+
} catch {
|
|
2342
|
+
resolve();
|
|
2343
|
+
}
|
|
2236
2344
|
}
|
|
2345
|
+
});
|
|
2346
|
+
});
|
|
2347
|
+
try {
|
|
2348
|
+
await withTimeout(exited, gracefulTimeoutMs);
|
|
2349
|
+
} catch {
|
|
2350
|
+
logger2.warn("Graceful shutdown timed out, force killing...");
|
|
2351
|
+
await kill(pid);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
async function acquirePort(options) {
|
|
2355
|
+
const { preferredPort, retryDelay = 2e3, logger: logger2 } = options;
|
|
2356
|
+
const available = await detect(preferredPort);
|
|
2357
|
+
if (available === preferredPort) {
|
|
2358
|
+
return preferredPort;
|
|
2359
|
+
}
|
|
2360
|
+
logger2.warn(`Port ${preferredPort} is still in use, waiting...`);
|
|
2361
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
2362
|
+
const retryAvailable = await detect(preferredPort);
|
|
2363
|
+
if (retryAvailable === preferredPort) {
|
|
2364
|
+
return preferredPort;
|
|
2365
|
+
}
|
|
2366
|
+
const fallbackPort = await getPort({ port: preferredPort });
|
|
2367
|
+
logger2.warn(
|
|
2368
|
+
`Port ${preferredPort} still occupied, using port ${fallbackPort}`
|
|
2369
|
+
);
|
|
2370
|
+
return fallbackPort;
|
|
2371
|
+
}
|
|
2372
|
+
async function waitForPortRelease(port, options = {}) {
|
|
2373
|
+
const { attempts = 10, delay = 300 } = options;
|
|
2374
|
+
const checkPortUsage = async () => {
|
|
2375
|
+
const availablePort = await detect(port);
|
|
2376
|
+
if (availablePort !== port) {
|
|
2377
|
+
throw new Error("Port still in use");
|
|
2237
2378
|
}
|
|
2379
|
+
};
|
|
2380
|
+
try {
|
|
2381
|
+
await retry(checkPortUsage, { attempts, delay });
|
|
2382
|
+
return true;
|
|
2383
|
+
} catch {
|
|
2384
|
+
return false;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// src/vite/node-red-launcher/index.ts
|
|
2389
|
+
var NodeRedLauncher = class {
|
|
2390
|
+
operationQueue = Promise.resolve();
|
|
2391
|
+
process = null;
|
|
2392
|
+
unwatchExit = null;
|
|
2393
|
+
nodeRedEntryPoint = null;
|
|
2394
|
+
tempFiles = [];
|
|
2395
|
+
bufferedLogs = [];
|
|
2396
|
+
port = null;
|
|
2397
|
+
outDir;
|
|
2398
|
+
options;
|
|
2399
|
+
_slug;
|
|
2400
|
+
logger;
|
|
2401
|
+
constructor(outDir, options, slug = "") {
|
|
2402
|
+
this.outDir = outDir;
|
|
2403
|
+
this.options = options;
|
|
2404
|
+
this._slug = slug;
|
|
2405
|
+
this.logger = new Logger({
|
|
2406
|
+
name: "vite-plugin-node-red",
|
|
2407
|
+
prefix: "node-red"
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
get preferredPort() {
|
|
2411
|
+
return this.options.runtime?.port ?? 1880;
|
|
2412
|
+
}
|
|
2413
|
+
get slug() {
|
|
2414
|
+
return this._slug;
|
|
2415
|
+
}
|
|
2416
|
+
get basePath() {
|
|
2417
|
+
return this._slug ? `/${this._slug}/` : "/";
|
|
2418
|
+
}
|
|
2419
|
+
get restartDelay() {
|
|
2420
|
+
return this.options.restartDelay ?? 1e3;
|
|
2421
|
+
}
|
|
2422
|
+
get pid() {
|
|
2423
|
+
return this.process?.child.pid ?? null;
|
|
2424
|
+
}
|
|
2425
|
+
get nodeRedCommand() {
|
|
2426
|
+
return getNodeRedCommand(this.options.runtime?.version);
|
|
2238
2427
|
}
|
|
2239
2428
|
log(line) {
|
|
2240
2429
|
if (line.includes("Server now running at")) {
|
|
@@ -2242,150 +2431,122 @@ for (const d of dirs) {
|
|
|
2242
2431
|
}
|
|
2243
2432
|
this.logger.raw(line);
|
|
2244
2433
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2434
|
+
handleProcessLine(line, source, ready) {
|
|
2435
|
+
if (!ready) {
|
|
2436
|
+
this.bufferedLogs.push(line);
|
|
2437
|
+
} else if (source === "stderr") {
|
|
2438
|
+
this.logger.error(line);
|
|
2249
2439
|
} else {
|
|
2440
|
+
this.log(line);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
async killProcess() {
|
|
2444
|
+
if (!this.process) return;
|
|
2445
|
+
this.stopWatchingExit();
|
|
2446
|
+
const pid = this.process.child.pid;
|
|
2447
|
+
if (pid) {
|
|
2448
|
+
await kill(pid);
|
|
2449
|
+
}
|
|
2450
|
+
this.process = null;
|
|
2451
|
+
}
|
|
2452
|
+
watchForUnexpectedExit(managed) {
|
|
2453
|
+
const onExit = (code, signal) => {
|
|
2454
|
+
this.unwatchExit = null;
|
|
2455
|
+
this.process = null;
|
|
2250
2456
|
this.logger.warn(
|
|
2251
|
-
`
|
|
2457
|
+
`Node-RED exited unexpectedly (${signal ?? `code ${code}`})`
|
|
2252
2458
|
);
|
|
2253
|
-
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
2254
|
-
const retryAvailable = await detect(this.preferredPort);
|
|
2255
|
-
if (retryAvailable === this.preferredPort) {
|
|
2256
|
-
this.port = this.preferredPort;
|
|
2257
|
-
} else {
|
|
2258
|
-
this.logger.warn(
|
|
2259
|
-
`Port ${this.preferredPort} still occupied, using port ${retryAvailable}`
|
|
2260
|
-
);
|
|
2261
|
-
this.port = await getPort({ port: this.preferredPort });
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
const nodeRedEntryPoint = this.resolveNodeRedEntryPoint();
|
|
2265
|
-
const startProcess = () => {
|
|
2266
|
-
return new Promise(async (resolve, reject) => {
|
|
2267
|
-
try {
|
|
2268
|
-
const settingsPath = await this.generateRuntimeSettingsFile();
|
|
2269
|
-
const args = this.options.args ?? [];
|
|
2270
|
-
this.bufferedLogs = [];
|
|
2271
|
-
this.isReady = false;
|
|
2272
|
-
this.process = spawn(
|
|
2273
|
-
process.execPath,
|
|
2274
|
-
[nodeRedEntryPoint, "-s", settingsPath, ...args],
|
|
2275
|
-
{
|
|
2276
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2277
|
-
}
|
|
2278
|
-
);
|
|
2279
|
-
this.process.stdout?.on("data", (data) => {
|
|
2280
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
2281
|
-
for (const line of lines) {
|
|
2282
|
-
if (this.isReady) {
|
|
2283
|
-
this.log(line);
|
|
2284
|
-
} else {
|
|
2285
|
-
this.bufferedLogs.push(line);
|
|
2286
|
-
}
|
|
2287
|
-
if (line.includes("Started flows") || line.includes("Server now running")) {
|
|
2288
|
-
this.isReady = true;
|
|
2289
|
-
resolve(void 0);
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
});
|
|
2293
|
-
this.process.stderr?.on("data", (data) => {
|
|
2294
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
2295
|
-
for (const line of lines) {
|
|
2296
|
-
if (this.isReady) {
|
|
2297
|
-
this.logger.error(`${line}`);
|
|
2298
|
-
} else {
|
|
2299
|
-
this.bufferedLogs.push(line);
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
});
|
|
2303
|
-
this.process.on("error", (error) => {
|
|
2304
|
-
reject(new NodeRedStartError(error));
|
|
2305
|
-
});
|
|
2306
|
-
this.process.on("exit", (code) => {
|
|
2307
|
-
if (!this.isReady && code !== 0 && code !== null) {
|
|
2308
|
-
reject(
|
|
2309
|
-
new NodeRedStartError(
|
|
2310
|
-
new Error(`Process exited with code ${code}`)
|
|
2311
|
-
)
|
|
2312
|
-
);
|
|
2313
|
-
}
|
|
2314
|
-
resolve(void 0);
|
|
2315
|
-
});
|
|
2316
|
-
} catch (error) {
|
|
2317
|
-
reject(new NodeRedStartError(error));
|
|
2318
|
-
}
|
|
2319
|
-
});
|
|
2320
2459
|
};
|
|
2460
|
+
managed.child.once("exit", onExit);
|
|
2461
|
+
this.unwatchExit = () => managed.child.off("exit", onExit);
|
|
2462
|
+
}
|
|
2463
|
+
stopWatchingExit() {
|
|
2464
|
+
this.unwatchExit?.();
|
|
2465
|
+
this.unwatchExit = null;
|
|
2466
|
+
}
|
|
2467
|
+
// start/stop interleaving (e.g. SIGINT while Node-RED is booting) would
|
|
2468
|
+
// race on process/port state and could leak a spawned process, so all
|
|
2469
|
+
// lifecycle operations run strictly one at a time
|
|
2470
|
+
enqueue(operation) {
|
|
2471
|
+
const result = this.operationQueue.then(operation, operation);
|
|
2472
|
+
this.operationQueue = result.catch(() => {
|
|
2473
|
+
});
|
|
2474
|
+
return result;
|
|
2475
|
+
}
|
|
2476
|
+
async start() {
|
|
2477
|
+
return this.enqueue(() => this.doStart());
|
|
2478
|
+
}
|
|
2479
|
+
async stop(skipPortUsageCheck = false) {
|
|
2480
|
+
return this.enqueue(() => this.doStop(skipPortUsageCheck));
|
|
2481
|
+
}
|
|
2482
|
+
async doStart() {
|
|
2321
2483
|
try {
|
|
2484
|
+
this.port = await acquirePort({
|
|
2485
|
+
preferredPort: this.preferredPort,
|
|
2486
|
+
logger: this.logger
|
|
2487
|
+
});
|
|
2488
|
+
if (!this.nodeRedEntryPoint || !fs12.existsSync(this.nodeRedEntryPoint)) {
|
|
2489
|
+
this.nodeRedEntryPoint = await resolveNodeRed({
|
|
2490
|
+
version: this.options.runtime?.version,
|
|
2491
|
+
logger: this.logger
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
const nodeRedEntryPoint = this.nodeRedEntryPoint;
|
|
2495
|
+
const settings = await generateRuntimeSettings({
|
|
2496
|
+
outDir: this.outDir,
|
|
2497
|
+
port: this.port,
|
|
2498
|
+
settingsFilepath: this.options.runtime?.settingsFilepath,
|
|
2499
|
+
httpAdminRoot: this._slug ? this.basePath : void 0,
|
|
2500
|
+
logger: this.logger
|
|
2501
|
+
});
|
|
2502
|
+
for (const file of settings.tempFiles) {
|
|
2503
|
+
if (!this.tempFiles.includes(file)) {
|
|
2504
|
+
this.tempFiles.push(file);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
const startProcess = async () => {
|
|
2508
|
+
await this.killProcess();
|
|
2509
|
+
this.bufferedLogs = [];
|
|
2510
|
+
this.process = start({
|
|
2511
|
+
entryPoint: nodeRedEntryPoint,
|
|
2512
|
+
settingsPath: settings.filepath,
|
|
2513
|
+
args: this.options.args ?? [],
|
|
2514
|
+
onLine: (line, source, ready) => this.handleProcessLine(line, source, ready)
|
|
2515
|
+
});
|
|
2516
|
+
await this.process.ready;
|
|
2517
|
+
};
|
|
2322
2518
|
await retry(startProcess, { attempts: 3, delay: 100 });
|
|
2519
|
+
this.watchForUnexpectedExit(this.process);
|
|
2323
2520
|
return this.port;
|
|
2324
2521
|
} catch (error) {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
if (pid) {
|
|
2328
|
-
treeKill(pid, "SIGKILL");
|
|
2329
|
-
}
|
|
2330
|
-
this.process = null;
|
|
2331
|
-
}
|
|
2332
|
-
throw new NodeRedStartError(error);
|
|
2522
|
+
await this.killProcess();
|
|
2523
|
+
throw error instanceof NodeRedStartError ? error : new NodeRedStartError(error);
|
|
2333
2524
|
}
|
|
2334
2525
|
}
|
|
2335
|
-
async
|
|
2526
|
+
async doStop(skipPortUsageCheck) {
|
|
2336
2527
|
if (!this.process) return;
|
|
2337
|
-
|
|
2528
|
+
this.stopWatchingExit();
|
|
2529
|
+
const pid = this.process.child.pid;
|
|
2338
2530
|
const currentPort = this.port;
|
|
2339
2531
|
if (!pid) {
|
|
2340
2532
|
this.process = null;
|
|
2533
|
+
this.port = null;
|
|
2341
2534
|
return;
|
|
2342
2535
|
}
|
|
2343
|
-
|
|
2344
|
-
this.process.
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
});
|
|
2348
|
-
treeKill(pid, "SIGTERM", (error) => {
|
|
2349
|
-
if (error) {
|
|
2350
|
-
try {
|
|
2351
|
-
process.kill(pid, "SIGTERM");
|
|
2352
|
-
} catch {
|
|
2353
|
-
this.process = null;
|
|
2354
|
-
resolve(void 0);
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
});
|
|
2536
|
+
await stop({
|
|
2537
|
+
child: this.process.child,
|
|
2538
|
+
pid,
|
|
2539
|
+
logger: this.logger
|
|
2358
2540
|
});
|
|
2359
|
-
|
|
2360
|
-
await withTimeout(stopProcess, 1e4);
|
|
2361
|
-
} catch {
|
|
2362
|
-
this.logger.warn("Graceful shutdown timed out, force killing...");
|
|
2363
|
-
await new Promise((resolve) => {
|
|
2364
|
-
treeKill(pid, "SIGKILL", () => {
|
|
2365
|
-
this.process = null;
|
|
2366
|
-
resolve(void 0);
|
|
2367
|
-
});
|
|
2368
|
-
});
|
|
2369
|
-
}
|
|
2541
|
+
this.process = null;
|
|
2370
2542
|
if (!skipPortUsageCheck && currentPort) {
|
|
2371
|
-
const
|
|
2372
|
-
|
|
2373
|
-
if (availablePort !== currentPort) {
|
|
2374
|
-
throw new Error("Port still in use");
|
|
2375
|
-
}
|
|
2376
|
-
};
|
|
2377
|
-
try {
|
|
2378
|
-
await retry(checkPortUsage, { attempts: 10, delay: 300 });
|
|
2379
|
-
} catch {
|
|
2543
|
+
const released = await waitForPortRelease(currentPort);
|
|
2544
|
+
if (!released) {
|
|
2380
2545
|
this.logger.warn(
|
|
2381
2546
|
`Port ${currentPort} still in use after stop. Force killing...`
|
|
2382
2547
|
);
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
treeKill(pid, "SIGKILL", () => resolve());
|
|
2386
|
-
});
|
|
2387
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2388
|
-
}
|
|
2548
|
+
await kill(pid);
|
|
2549
|
+
await waitForPortRelease(currentPort);
|
|
2389
2550
|
}
|
|
2390
2551
|
}
|
|
2391
2552
|
this.port = null;
|
|
@@ -2397,10 +2558,13 @@ for (const d of dirs) {
|
|
|
2397
2558
|
this.bufferedLogs = [];
|
|
2398
2559
|
}
|
|
2399
2560
|
cleanup() {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2561
|
+
for (const file of this.tempFiles) {
|
|
2562
|
+
try {
|
|
2563
|
+
fs12.unlinkSync(file);
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2403
2566
|
}
|
|
2567
|
+
this.tempFiles = [];
|
|
2404
2568
|
}
|
|
2405
2569
|
};
|
|
2406
2570
|
|
|
@@ -2416,10 +2580,10 @@ var NodeRedTestEnvironment = class {
|
|
|
2416
2580
|
options;
|
|
2417
2581
|
constructor(options) {
|
|
2418
2582
|
this.options = options;
|
|
2419
|
-
this.projectDir =
|
|
2420
|
-
this.outDir =
|
|
2421
|
-
this.nodeRedDir =
|
|
2422
|
-
this.installedPkgDir =
|
|
2583
|
+
this.projectDir = path12.resolve(options.projectDir ?? process.cwd());
|
|
2584
|
+
this.outDir = path12.join(this.projectDir, "dist-e2e");
|
|
2585
|
+
this.nodeRedDir = path12.join(this.projectDir, ".node-red");
|
|
2586
|
+
this.installedPkgDir = path12.join(
|
|
2423
2587
|
this.nodeRedDir,
|
|
2424
2588
|
"node_modules",
|
|
2425
2589
|
options.packageName
|
|
@@ -2431,15 +2595,15 @@ var NodeRedTestEnvironment = class {
|
|
|
2431
2595
|
async setup() {
|
|
2432
2596
|
this.originalCwd = process.cwd();
|
|
2433
2597
|
process.chdir(this.projectDir);
|
|
2434
|
-
if (
|
|
2435
|
-
|
|
2598
|
+
if (fs13.existsSync(this.outDir)) fs13.rmSync(this.outDir, { recursive: true });
|
|
2599
|
+
fs13.mkdirSync(this.outDir, { recursive: true });
|
|
2436
2600
|
const buildContext = {
|
|
2437
2601
|
outDir: this.outDir,
|
|
2438
2602
|
packageName: this.options.packageName,
|
|
2439
2603
|
isDev: false
|
|
2440
2604
|
};
|
|
2441
2605
|
const serverOpts = {
|
|
2442
|
-
srcDir:
|
|
2606
|
+
srcDir: path12.join(this.projectDir, "src/server"),
|
|
2443
2607
|
entry: "index.ts",
|
|
2444
2608
|
format: "esm",
|
|
2445
2609
|
bundled: [],
|
|
@@ -2449,7 +2613,7 @@ var NodeRedTestEnvironment = class {
|
|
|
2449
2613
|
};
|
|
2450
2614
|
await build(serverOpts, buildContext);
|
|
2451
2615
|
const clientOpts = {
|
|
2452
|
-
srcDir:
|
|
2616
|
+
srcDir: path12.join(this.projectDir, "src/client"),
|
|
2453
2617
|
entry: "index.ts",
|
|
2454
2618
|
name: this.options.clientName ?? "NodeRedNodes",
|
|
2455
2619
|
format: "es",
|
|
@@ -2458,18 +2622,18 @@ var NodeRedTestEnvironment = class {
|
|
|
2458
2622
|
...this.options.client
|
|
2459
2623
|
};
|
|
2460
2624
|
await build2(clientOpts, buildContext);
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2625
|
+
fs13.mkdirSync(this.installedPkgDir, { recursive: true });
|
|
2626
|
+
fs13.cpSync(this.outDir, this.installedPkgDir, { recursive: true });
|
|
2627
|
+
fs13.mkdirSync(this.nodeRedDir, { recursive: true });
|
|
2628
|
+
fs13.writeFileSync(
|
|
2629
|
+
path12.join(this.nodeRedDir, ".config.runtime.json"),
|
|
2466
2630
|
JSON.stringify({ telemetryEnabled: false })
|
|
2467
2631
|
);
|
|
2468
2632
|
const launcherOpts = {
|
|
2469
2633
|
runtime: { port: this.options.port ?? 1881 }
|
|
2470
2634
|
};
|
|
2471
2635
|
if (this.options.settingsFile) {
|
|
2472
|
-
launcherOpts.runtime.settingsFilepath =
|
|
2636
|
+
launcherOpts.runtime.settingsFilepath = path12.resolve(
|
|
2473
2637
|
this.projectDir,
|
|
2474
2638
|
this.options.settingsFile
|
|
2475
2639
|
);
|
|
@@ -2504,9 +2668,9 @@ var NodeRedTestEnvironment = class {
|
|
|
2504
2668
|
this.launcher.cleanup();
|
|
2505
2669
|
this.launcher = null;
|
|
2506
2670
|
}
|
|
2507
|
-
if (
|
|
2508
|
-
if (
|
|
2509
|
-
|
|
2671
|
+
if (fs13.existsSync(this.outDir)) fs13.rmSync(this.outDir, { recursive: true });
|
|
2672
|
+
if (fs13.existsSync(this.nodeRedDir)) {
|
|
2673
|
+
fs13.rmSync(this.nodeRedDir, { recursive: true });
|
|
2510
2674
|
}
|
|
2511
2675
|
this.port = null;
|
|
2512
2676
|
}
|
|
@@ -2521,7 +2685,7 @@ var defaultConfig = {
|
|
|
2521
2685
|
var _env = null;
|
|
2522
2686
|
async function setup(options) {
|
|
2523
2687
|
const packageName = JSON.parse(
|
|
2524
|
-
|
|
2688
|
+
fs14.readFileSync(path13.join(process.cwd(), "package.json"), "utf-8")
|
|
2525
2689
|
).name;
|
|
2526
2690
|
_env = new NodeRedTestEnvironment({
|
|
2527
2691
|
packageName,
|
|
@@ -2550,8 +2714,8 @@ var NodeRedEditor = class {
|
|
|
2550
2714
|
errors = [];
|
|
2551
2715
|
screenshotDir;
|
|
2552
2716
|
async screenshot(name) {
|
|
2553
|
-
|
|
2554
|
-
const filePath =
|
|
2717
|
+
fs14.mkdirSync(this.screenshotDir, { recursive: true });
|
|
2718
|
+
const filePath = path13.join(this.screenshotDir, `${name}.png`);
|
|
2555
2719
|
await this.page.screenshot({ path: filePath, fullPage: true });
|
|
2556
2720
|
return filePath;
|
|
2557
2721
|
}
|
|
@@ -2602,31 +2766,159 @@ var NodeRedEditor = class {
|
|
|
2602
2766
|
await this.page.waitForTimeout(500);
|
|
2603
2767
|
}
|
|
2604
2768
|
async clickDone() {
|
|
2605
|
-
await this
|
|
2606
|
-
globalThis.document.getElementById("node-dialog-ok").click();
|
|
2607
|
-
});
|
|
2608
|
-
await this.page.waitForSelector(".red-ui-tray", {
|
|
2609
|
-
state: "hidden",
|
|
2610
|
-
timeout: 5e3
|
|
2611
|
-
});
|
|
2769
|
+
await this.#closeTray("node-dialog-ok");
|
|
2612
2770
|
}
|
|
2613
2771
|
async clickCancel() {
|
|
2614
|
-
await this
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2772
|
+
await this.#closeTray("node-dialog-cancel");
|
|
2773
|
+
}
|
|
2774
|
+
// Waits for the tray COUNT to drop rather than for all trays to be hidden,
|
|
2775
|
+
// so a leftover tray from an earlier (failed) test can't poison the wait.
|
|
2776
|
+
async #closeTray(buttonId) {
|
|
2777
|
+
const before = await this.page.locator(".red-ui-tray").count();
|
|
2778
|
+
await this.page.evaluate((id) => {
|
|
2779
|
+
globalThis.document.getElementById(id).click();
|
|
2780
|
+
}, buttonId);
|
|
2781
|
+
await this.page.waitForFunction(
|
|
2782
|
+
(count) => document.querySelectorAll(".red-ui-tray").length < count,
|
|
2783
|
+
before,
|
|
2784
|
+
{ timeout: 5e3 }
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Best-effort close of every open tray — call from afterEach so a failed
|
|
2789
|
+
* test never leaves a tray open for the next one.
|
|
2790
|
+
*/
|
|
2791
|
+
async closeAllTrays() {
|
|
2792
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
2793
|
+
const count = await this.page.locator(".red-ui-tray").count();
|
|
2794
|
+
if (count === 0) return;
|
|
2795
|
+
const cancel = this.page.locator("#node-config-dialog-cancel, #node-dialog-cancel").last();
|
|
2796
|
+
if (await cancel.count() > 0) {
|
|
2797
|
+
await cancel.click({ force: true }).catch(() => {
|
|
2798
|
+
});
|
|
2799
|
+
} else {
|
|
2800
|
+
await this.page.keyboard.press("Escape");
|
|
2801
|
+
}
|
|
2802
|
+
await this.page.waitForTimeout(400);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* Closes the config-node tray (stacked above the node tray) with its own
|
|
2807
|
+
* Done button. The node tray underneath stays open — use clickDone() for it.
|
|
2808
|
+
*/
|
|
2809
|
+
async clickConfigDone() {
|
|
2810
|
+
await this.page.click("#node-config-dialog-ok");
|
|
2811
|
+
await this.page.locator("#node-config-dialog-ok").waitFor({ state: "hidden", timeout: 5e3 });
|
|
2812
|
+
await this.page.waitForTimeout(300);
|
|
2813
|
+
}
|
|
2814
|
+
async clickConfigCancel() {
|
|
2815
|
+
await this.page.click("#node-config-dialog-cancel");
|
|
2816
|
+
await this.page.locator("#node-config-dialog-cancel").waitFor({ state: "hidden", timeout: 5e3 });
|
|
2817
|
+
await this.page.waitForTimeout(300);
|
|
2621
2818
|
}
|
|
2622
2819
|
field(label) {
|
|
2623
2820
|
return new NodeRedField(this.page, label);
|
|
2624
2821
|
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Returns a JSON-safe snapshot of a node in the editor's model — use it to
|
|
2824
|
+
* assert values persisted after clickDone(). Functions, internals
|
|
2825
|
+
* (underscore-prefixed keys like `_def`), config `users` back-references,
|
|
2826
|
+
* and any circular references are stripped.
|
|
2827
|
+
*/
|
|
2828
|
+
async getNode(nodeId) {
|
|
2829
|
+
return this.page.evaluate((id) => {
|
|
2830
|
+
const r = globalThis.RED;
|
|
2831
|
+
const node = r?.nodes?.node(id);
|
|
2832
|
+
if (!node) return null;
|
|
2833
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2834
|
+
return JSON.parse(
|
|
2835
|
+
JSON.stringify(node, (key, value) => {
|
|
2836
|
+
if (typeof value === "function") return void 0;
|
|
2837
|
+
if (key.startsWith("_") || key === "users") return void 0;
|
|
2838
|
+
if (typeof value === "object" && value !== null) {
|
|
2839
|
+
if (seen.has(value)) return void 0;
|
|
2840
|
+
seen.add(value);
|
|
2841
|
+
}
|
|
2842
|
+
return value;
|
|
2843
|
+
})
|
|
2844
|
+
);
|
|
2845
|
+
}, nodeId);
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Clicks the editor's Deploy button and waits until the workspace is clean.
|
|
2849
|
+
* Flows with invalid or unused nodes trigger a confirmation dialog — it is
|
|
2850
|
+
* confirmed automatically.
|
|
2851
|
+
*/
|
|
2852
|
+
async clickDeploy() {
|
|
2853
|
+
await this.page.click("#red-ui-header-button-deploy");
|
|
2854
|
+
const confirm = this.page.locator(".red-ui-notification button.primary").first();
|
|
2855
|
+
const confirmationShown = await confirm.waitFor({ state: "visible", timeout: 2e3 }).then(() => true).catch(() => false);
|
|
2856
|
+
if (confirmationShown) await confirm.click();
|
|
2857
|
+
await this.page.waitForFunction(
|
|
2858
|
+
() => {
|
|
2859
|
+
const r = globalThis.RED;
|
|
2860
|
+
return r?.nodes?.dirty?.() === false;
|
|
2861
|
+
},
|
|
2862
|
+
{ timeout: 15e3 }
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
/** Fetches the currently deployed flow from the runtime (GET /flows). */
|
|
2866
|
+
async getDeployedFlow() {
|
|
2867
|
+
const res = await fetch(`http://localhost:${this.port}/flows`);
|
|
2868
|
+
if (!res.ok) {
|
|
2869
|
+
throw new Error(
|
|
2870
|
+
`Failed to fetch flows: ${res.status} ${await res.text()}`
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
return res.json();
|
|
2874
|
+
}
|
|
2875
|
+
/** Counts the output ports rendered for a node on the canvas. */
|
|
2876
|
+
async getNodePortCount(nodeId) {
|
|
2877
|
+
const group = await this.#nodeGroup(nodeId);
|
|
2878
|
+
return group.locator(".red-ui-flow-port-output").count();
|
|
2879
|
+
}
|
|
2880
|
+
/** Returns the node's label text as rendered on the canvas. */
|
|
2881
|
+
async getNodeLabel(nodeId) {
|
|
2882
|
+
const group = await this.#nodeGroup(nodeId);
|
|
2883
|
+
const label = group.locator(".red-ui-flow-node-label").first();
|
|
2884
|
+
return (await label.textContent() ?? "").trim();
|
|
2885
|
+
}
|
|
2886
|
+
/** Returns the status text rendered under a node, or "" when none is set. */
|
|
2887
|
+
async getNodeStatus(nodeId) {
|
|
2888
|
+
const group = await this.#nodeGroup(nodeId);
|
|
2889
|
+
const status = group.locator(".red-ui-flow-node-status-label").first();
|
|
2890
|
+
if (await status.count() === 0) return "";
|
|
2891
|
+
return (await status.textContent() ?? "").trim();
|
|
2892
|
+
}
|
|
2893
|
+
async #nodeGroup(nodeId) {
|
|
2894
|
+
await this.page.waitForFunction(
|
|
2895
|
+
(id) => {
|
|
2896
|
+
const groups = Array.from(
|
|
2897
|
+
document.querySelectorAll(".red-ui-flow-node-group")
|
|
2898
|
+
);
|
|
2899
|
+
return groups.some((el) => el.__data__?.id === id);
|
|
2900
|
+
},
|
|
2901
|
+
nodeId,
|
|
2902
|
+
{ timeout: 1e4 }
|
|
2903
|
+
);
|
|
2904
|
+
const index = await this.page.evaluate((id) => {
|
|
2905
|
+
const groups = Array.from(
|
|
2906
|
+
document.querySelectorAll(".red-ui-flow-node-group")
|
|
2907
|
+
);
|
|
2908
|
+
return groups.findIndex((el) => el.__data__?.id === id);
|
|
2909
|
+
}, nodeId);
|
|
2910
|
+
return this.page.locator(".red-ui-flow-node-group").nth(index);
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Asserts no uncaught page errors occurred, then clears the collected list
|
|
2914
|
+
* so each test only fails for its own errors — call it from afterEach.
|
|
2915
|
+
*/
|
|
2625
2916
|
expectNoPageErrors() {
|
|
2626
|
-
|
|
2917
|
+
const errors = this.errors.splice(0);
|
|
2918
|
+
if (errors.length > 0) {
|
|
2627
2919
|
throw new Error(
|
|
2628
2920
|
`Page errors detected:
|
|
2629
|
-
${
|
|
2921
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
2630
2922
|
);
|
|
2631
2923
|
}
|
|
2632
2924
|
}
|
|
@@ -2638,7 +2930,7 @@ var NodeRedField = class {
|
|
|
2638
2930
|
constructor(page, label) {
|
|
2639
2931
|
this.page = page;
|
|
2640
2932
|
this.label = label;
|
|
2641
|
-
this.row = page.locator(`.form-row:has(:text("${label}"))`).first();
|
|
2933
|
+
this.row = page.locator(".red-ui-tray").last().locator(`.form-row:has(:text("${label}"))`).first();
|
|
2642
2934
|
}
|
|
2643
2935
|
row;
|
|
2644
2936
|
get input() {
|
|
@@ -2713,6 +3005,18 @@ var NodeRedField = class {
|
|
|
2713
3005
|
get addButton() {
|
|
2714
3006
|
return this.row.locator("a.red-ui-button:has(i.fa-plus)");
|
|
2715
3007
|
}
|
|
3008
|
+
/** Clicks the + button of a config field and waits for the config tray. */
|
|
3009
|
+
async openAddConfig() {
|
|
3010
|
+
await this.addButton.click();
|
|
3011
|
+
await this.page.locator("#node-config-dialog-ok").waitFor({ state: "visible", timeout: 1e4 });
|
|
3012
|
+
await this.page.waitForTimeout(500);
|
|
3013
|
+
}
|
|
3014
|
+
/** Clicks the pencil button of a config field and waits for the config tray. */
|
|
3015
|
+
async openEditConfig() {
|
|
3016
|
+
await this.editButton.click();
|
|
3017
|
+
await this.page.locator("#node-config-dialog-ok").waitFor({ state: "visible", timeout: 1e4 });
|
|
3018
|
+
await this.page.waitForTimeout(500);
|
|
3019
|
+
}
|
|
2716
3020
|
async getSelectedOption() {
|
|
2717
3021
|
return this.select.inputValue();
|
|
2718
3022
|
}
|
|
@@ -2728,6 +3032,56 @@ var NodeRedField = class {
|
|
|
2728
3032
|
get editorWrapper() {
|
|
2729
3033
|
return this.row.locator(".editor-wrapper");
|
|
2730
3034
|
}
|
|
3035
|
+
/** Reads the value of the field's code editor (Monaco, ACE fallback). */
|
|
3036
|
+
async getEditorValue() {
|
|
3037
|
+
return this.editorWrapper.evaluate((el) => {
|
|
3038
|
+
const w = globalThis;
|
|
3039
|
+
const editors = w.monaco?.editor?.getEditors?.() ?? [];
|
|
3040
|
+
const monacoEditor = editors.find(
|
|
3041
|
+
(e) => el.contains(e.getContainerDomNode())
|
|
3042
|
+
);
|
|
3043
|
+
if (monacoEditor) return monacoEditor.getValue();
|
|
3044
|
+
const aceEl = el.querySelector(".ace_editor");
|
|
3045
|
+
if (aceEl && w.ace) return w.ace.edit(aceEl).getValue();
|
|
3046
|
+
throw new Error("No code editor instance found in this field");
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
/** Sets the value of the field's code editor (Monaco, ACE fallback). */
|
|
3050
|
+
async setEditorValue(value) {
|
|
3051
|
+
await this.editorWrapper.evaluate((el, newValue) => {
|
|
3052
|
+
const w = globalThis;
|
|
3053
|
+
const editors = w.monaco?.editor?.getEditors?.() ?? [];
|
|
3054
|
+
const monacoEditor = editors.find(
|
|
3055
|
+
(e) => el.contains(e.getContainerDomNode())
|
|
3056
|
+
);
|
|
3057
|
+
if (monacoEditor) {
|
|
3058
|
+
monacoEditor.setValue(newValue);
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
const aceEl = el.querySelector(".ace_editor");
|
|
3062
|
+
if (aceEl && w.ace) {
|
|
3063
|
+
w.ace.edit(aceEl).setValue(newValue, 1);
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
throw new Error("No code editor instance found in this field");
|
|
3067
|
+
}, value);
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Types into the field and returns the labels of the autocomplete
|
|
3071
|
+
* suggestions that appear (TypedInput types with an `autoComplete` source).
|
|
3072
|
+
*/
|
|
3073
|
+
async getAutoCompleteSuggestions(prefix) {
|
|
3074
|
+
const input = this.row.locator("input:visible").first();
|
|
3075
|
+
await input.fill("", { force: true });
|
|
3076
|
+
await input.pressSequentially(prefix, { delay: 30 });
|
|
3077
|
+
const menu = this.page.locator(".red-ui-autoComplete-container:visible").first();
|
|
3078
|
+
await menu.waitFor({ state: "visible", timeout: 5e3 });
|
|
3079
|
+
const labels = await menu.locator("li").evaluateAll(
|
|
3080
|
+
(els) => els.map((el) => el.textContent?.trim() ?? "").filter(Boolean)
|
|
3081
|
+
);
|
|
3082
|
+
await this.page.keyboard.press("Escape");
|
|
3083
|
+
return labels;
|
|
3084
|
+
}
|
|
2731
3085
|
get expandButton() {
|
|
2732
3086
|
return this.row.locator(".expand-button");
|
|
2733
3087
|
}
|