@bonsae/nrg 0.18.5 → 0.19.1
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 -2
- package/package.json +1 -1
- package/server/index.cjs +86 -9
- package/server/resources/nrg-client.js +2020 -1987
- 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 +706 -368
- 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 +25 -9
- package/vite/index.js +648 -499
- /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,203 +2042,376 @@ 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, 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
|
-
target: "node18",
|
|
2114
|
-
bundle: true,
|
|
2115
|
-
define: {
|
|
2116
|
-
"import.meta.dirname": JSON.stringify(settingsDir),
|
|
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 finalRuntimeSettingsFile = compiledRuntimeSettingsFilepath ? `
|
|
2214
|
+
const compiledRuntimeSettings = require(${JSON.stringify(
|
|
2215
|
+
compiledRuntimeSettingsFilepath.split(path11.sep).join("/")
|
|
2216
|
+
)});
|
|
2138
2217
|
const settings = compiledRuntimeSettings.default || compiledRuntimeSettings;
|
|
2139
|
-
settings.uiPort = ${
|
|
2218
|
+
settings.uiPort = ${port};
|
|
2140
2219
|
if(!settings.userDir){
|
|
2141
|
-
settings.userDir =
|
|
2220
|
+
settings.userDir = ${userDirLiteral};
|
|
2142
2221
|
}
|
|
2143
2222
|
settings.nodesDir = settings.nodesDir || [];
|
|
2144
|
-
if (!settings.nodesDir.includes(
|
|
2145
|
-
settings.nodesDir.push(
|
|
2223
|
+
if (!settings.nodesDir.includes(${outDirLiteral})) {
|
|
2224
|
+
settings.nodesDir.push(${outDirLiteral});
|
|
2146
2225
|
}
|
|
2147
2226
|
if(!settings.flowFile){
|
|
2148
2227
|
settings.flowFile = "flows.json";
|
|
2149
2228
|
}
|
|
2229
|
+
// the welcome tour overlay intercepts pointer events \u2014 fatal for e2e and
|
|
2230
|
+
// noise for dev; explicit user settings still win
|
|
2231
|
+
settings.editorTheme = settings.editorTheme || {};
|
|
2232
|
+
if (settings.editorTheme.tours === undefined) {
|
|
2233
|
+
settings.editorTheme.tours = false;
|
|
2234
|
+
}
|
|
2150
2235
|
module.exports = settings;
|
|
2151
2236
|
` : `
|
|
2152
2237
|
const settings = {
|
|
2153
|
-
uiPort: ${
|
|
2154
|
-
userDir:
|
|
2238
|
+
uiPort: ${port},
|
|
2239
|
+
userDir: ${userDirLiteral},
|
|
2155
2240
|
flowFile: "flows.json",
|
|
2156
|
-
nodesDir: [
|
|
2241
|
+
nodesDir: [${outDirLiteral}],
|
|
2242
|
+
// the welcome tour overlay intercepts pointer events \u2014 fatal for e2e
|
|
2243
|
+
editorTheme: { tours: false },
|
|
2157
2244
|
};
|
|
2158
2245
|
module.exports = settings;
|
|
2159
2246
|
`;
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2247
|
+
const finalRuntimeSettingsFilepath = path11.join(
|
|
2248
|
+
os2.tmpdir(),
|
|
2249
|
+
`node-red-settings-final-${process.pid}-${port}.cjs`
|
|
2250
|
+
);
|
|
2251
|
+
fs11.writeFileSync(finalRuntimeSettingsFilepath, finalRuntimeSettingsFile);
|
|
2252
|
+
tempFiles.push(finalRuntimeSettingsFilepath);
|
|
2253
|
+
return { filepath: finalRuntimeSettingsFilepath, tempFiles };
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// src/vite/node-red-launcher/process.ts
|
|
2257
|
+
import { spawn } from "child_process";
|
|
2258
|
+
import detect from "detect-port";
|
|
2259
|
+
import getPort from "get-port";
|
|
2260
|
+
import treeKill from "tree-kill";
|
|
2261
|
+
var READY_MARKERS = ["Started flows", "Server now running"];
|
|
2262
|
+
function start(options) {
|
|
2263
|
+
const { entryPoint, settingsPath, args, onLine } = options;
|
|
2264
|
+
const child = spawn(
|
|
2265
|
+
process.execPath,
|
|
2266
|
+
[entryPoint, "-s", settingsPath, ...args],
|
|
2267
|
+
{
|
|
2268
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2180
2269
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2270
|
+
);
|
|
2271
|
+
let isReady = false;
|
|
2272
|
+
let resolveReady;
|
|
2273
|
+
let rejectReady;
|
|
2274
|
+
const ready = new Promise((resolve, reject) => {
|
|
2275
|
+
resolveReady = resolve;
|
|
2276
|
+
rejectReady = reject;
|
|
2277
|
+
});
|
|
2278
|
+
const emitLine = (rawLine, source) => {
|
|
2279
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
2280
|
+
if (!line) return;
|
|
2281
|
+
onLine(line, source, isReady);
|
|
2282
|
+
if (source === "stdout" && READY_MARKERS.some((marker) => line.includes(marker))) {
|
|
2283
|
+
isReady = true;
|
|
2284
|
+
resolveReady();
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
const remainders = { stdout: "", stderr: "" };
|
|
2288
|
+
const handleData = (data, source) => {
|
|
2289
|
+
const lines = (remainders[source] + data.toString()).split("\n");
|
|
2290
|
+
remainders[source] = lines.pop() ?? "";
|
|
2291
|
+
for (const line of lines) {
|
|
2292
|
+
emitLine(line, source);
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
const flushRemainders = () => {
|
|
2296
|
+
for (const source of ["stdout", "stderr"]) {
|
|
2297
|
+
const rest = remainders[source];
|
|
2298
|
+
remainders[source] = "";
|
|
2299
|
+
if (rest) {
|
|
2300
|
+
emitLine(rest, source);
|
|
2190
2301
|
}
|
|
2191
2302
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
const dirs = process.env.PATH.split(path.delimiter);
|
|
2206
|
-
for (const d of dirs) {
|
|
2207
|
-
const f = path.join(d, binName);
|
|
2208
|
-
if (fs.existsSync(f)) {
|
|
2209
|
-
if (isWin) {
|
|
2210
|
-
const nodeRedDir = path.resolve(d, "..", "node-red");
|
|
2211
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(nodeRedDir, "package.json"), "utf-8"));
|
|
2212
|
-
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin["node-red"];
|
|
2213
|
-
process.stdout.write(path.resolve(nodeRedDir, bin));
|
|
2214
|
-
} else {
|
|
2215
|
-
process.stdout.write(fs.realpathSync(f));
|
|
2303
|
+
};
|
|
2304
|
+
child.stdout?.on("data", (data) => handleData(data, "stdout"));
|
|
2305
|
+
child.stderr?.on("data", (data) => handleData(data, "stderr"));
|
|
2306
|
+
child.on("error", (error) => {
|
|
2307
|
+
rejectReady(new NodeRedStartError(error));
|
|
2308
|
+
});
|
|
2309
|
+
child.on("exit", (code) => {
|
|
2310
|
+
flushRemainders();
|
|
2311
|
+
if (!isReady && code !== 0 && code !== null) {
|
|
2312
|
+
rejectReady(
|
|
2313
|
+
new NodeRedStartError(new Error(`Process exited with code ${code}`))
|
|
2314
|
+
);
|
|
2315
|
+
return;
|
|
2216
2316
|
}
|
|
2217
|
-
|
|
2317
|
+
resolveReady();
|
|
2318
|
+
});
|
|
2319
|
+
return { child, ready };
|
|
2320
|
+
}
|
|
2321
|
+
function kill(pid) {
|
|
2322
|
+
return new Promise((resolve) => {
|
|
2323
|
+
treeKill(pid, "SIGKILL", () => resolve());
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
async function stop(options) {
|
|
2327
|
+
const { child, pid, gracefulTimeoutMs = 1e4, logger: logger2 } = options;
|
|
2328
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
2329
|
+
return;
|
|
2218
2330
|
}
|
|
2219
|
-
|
|
2220
|
-
);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
new Error(
|
|
2229
|
-
`Could not resolve node-red entry point: ${entryPoint || "(empty)"}`
|
|
2230
|
-
)
|
|
2231
|
-
);
|
|
2232
|
-
}
|
|
2233
|
-
this.logger.info(`Resolved via npx: ${entryPoint}`);
|
|
2234
|
-
return entryPoint;
|
|
2235
|
-
} finally {
|
|
2236
|
-
try {
|
|
2237
|
-
fs10.unlinkSync(resolverScript);
|
|
2238
|
-
} catch {
|
|
2331
|
+
const exited = new Promise((resolve) => {
|
|
2332
|
+
child.once("exit", () => resolve());
|
|
2333
|
+
treeKill(pid, "SIGTERM", (error) => {
|
|
2334
|
+
if (error) {
|
|
2335
|
+
try {
|
|
2336
|
+
process.kill(pid, "SIGTERM");
|
|
2337
|
+
} catch {
|
|
2338
|
+
resolve();
|
|
2339
|
+
}
|
|
2239
2340
|
}
|
|
2341
|
+
});
|
|
2342
|
+
});
|
|
2343
|
+
try {
|
|
2344
|
+
await withTimeout(exited, gracefulTimeoutMs);
|
|
2345
|
+
} catch {
|
|
2346
|
+
logger2.warn("Graceful shutdown timed out, force killing...");
|
|
2347
|
+
await kill(pid);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
async function acquirePort(options) {
|
|
2351
|
+
const { preferredPort, retryDelay = 2e3, logger: logger2 } = options;
|
|
2352
|
+
const available = await detect(preferredPort);
|
|
2353
|
+
if (available === preferredPort) {
|
|
2354
|
+
return preferredPort;
|
|
2355
|
+
}
|
|
2356
|
+
logger2.warn(`Port ${preferredPort} is still in use, waiting...`);
|
|
2357
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
2358
|
+
const retryAvailable = await detect(preferredPort);
|
|
2359
|
+
if (retryAvailable === preferredPort) {
|
|
2360
|
+
return preferredPort;
|
|
2361
|
+
}
|
|
2362
|
+
const fallbackPort = await getPort({ port: preferredPort });
|
|
2363
|
+
logger2.warn(
|
|
2364
|
+
`Port ${preferredPort} still occupied, using port ${fallbackPort}`
|
|
2365
|
+
);
|
|
2366
|
+
return fallbackPort;
|
|
2367
|
+
}
|
|
2368
|
+
async function waitForPortRelease(port, options = {}) {
|
|
2369
|
+
const { attempts = 10, delay = 300 } = options;
|
|
2370
|
+
const checkPortUsage = async () => {
|
|
2371
|
+
const availablePort = await detect(port);
|
|
2372
|
+
if (availablePort !== port) {
|
|
2373
|
+
throw new Error("Port still in use");
|
|
2240
2374
|
}
|
|
2375
|
+
};
|
|
2376
|
+
try {
|
|
2377
|
+
await retry(checkPortUsage, { attempts, delay });
|
|
2378
|
+
return true;
|
|
2379
|
+
} catch {
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// src/vite/node-red-launcher/index.ts
|
|
2385
|
+
var NodeRedLauncher = class {
|
|
2386
|
+
operationQueue = Promise.resolve();
|
|
2387
|
+
process = null;
|
|
2388
|
+
unwatchExit = null;
|
|
2389
|
+
nodeRedEntryPoint = null;
|
|
2390
|
+
tempFiles = [];
|
|
2391
|
+
bufferedLogs = [];
|
|
2392
|
+
port = null;
|
|
2393
|
+
outDir;
|
|
2394
|
+
options;
|
|
2395
|
+
logger;
|
|
2396
|
+
constructor(outDir, options) {
|
|
2397
|
+
this.outDir = outDir;
|
|
2398
|
+
this.options = options;
|
|
2399
|
+
this.logger = new Logger({
|
|
2400
|
+
name: "vite-plugin-node-red",
|
|
2401
|
+
prefix: "node-red"
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
get preferredPort() {
|
|
2405
|
+
return this.options.runtime?.port ?? 1880;
|
|
2406
|
+
}
|
|
2407
|
+
get restartDelay() {
|
|
2408
|
+
return this.options.restartDelay ?? 1e3;
|
|
2409
|
+
}
|
|
2410
|
+
get pid() {
|
|
2411
|
+
return this.process?.child.pid ?? null;
|
|
2412
|
+
}
|
|
2413
|
+
get nodeRedCommand() {
|
|
2414
|
+
return getNodeRedCommand(this.options.runtime?.version);
|
|
2241
2415
|
}
|
|
2242
2416
|
log(line) {
|
|
2243
2417
|
if (line.includes("Server now running at")) {
|
|
@@ -2245,150 +2419,121 @@ for (const d of dirs) {
|
|
|
2245
2419
|
}
|
|
2246
2420
|
this.logger.raw(line);
|
|
2247
2421
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2422
|
+
handleProcessLine(line, source, ready) {
|
|
2423
|
+
if (!ready) {
|
|
2424
|
+
this.bufferedLogs.push(line);
|
|
2425
|
+
} else if (source === "stderr") {
|
|
2426
|
+
this.logger.error(line);
|
|
2252
2427
|
} else {
|
|
2428
|
+
this.log(line);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
async killProcess() {
|
|
2432
|
+
if (!this.process) return;
|
|
2433
|
+
this.stopWatchingExit();
|
|
2434
|
+
const pid = this.process.child.pid;
|
|
2435
|
+
if (pid) {
|
|
2436
|
+
await kill(pid);
|
|
2437
|
+
}
|
|
2438
|
+
this.process = null;
|
|
2439
|
+
}
|
|
2440
|
+
watchForUnexpectedExit(managed) {
|
|
2441
|
+
const onExit = (code, signal) => {
|
|
2442
|
+
this.unwatchExit = null;
|
|
2443
|
+
this.process = null;
|
|
2253
2444
|
this.logger.warn(
|
|
2254
|
-
`
|
|
2445
|
+
`Node-RED exited unexpectedly (${signal ?? `code ${code}`})`
|
|
2255
2446
|
);
|
|
2256
|
-
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
2257
|
-
const retryAvailable = await detect(this.preferredPort);
|
|
2258
|
-
if (retryAvailable === this.preferredPort) {
|
|
2259
|
-
this.port = this.preferredPort;
|
|
2260
|
-
} else {
|
|
2261
|
-
this.logger.warn(
|
|
2262
|
-
`Port ${this.preferredPort} still occupied, using port ${retryAvailable}`
|
|
2263
|
-
);
|
|
2264
|
-
this.port = await getPort({ port: this.preferredPort });
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
const nodeRedEntryPoint = this.resolveNodeRedEntryPoint();
|
|
2268
|
-
const startProcess = () => {
|
|
2269
|
-
return new Promise(async (resolve, reject) => {
|
|
2270
|
-
try {
|
|
2271
|
-
const settingsPath = await this.generateRuntimeSettingsFile();
|
|
2272
|
-
const args = this.options.args ?? [];
|
|
2273
|
-
this.bufferedLogs = [];
|
|
2274
|
-
this.isReady = false;
|
|
2275
|
-
this.process = spawn(
|
|
2276
|
-
process.execPath,
|
|
2277
|
-
[nodeRedEntryPoint, "-s", settingsPath, ...args],
|
|
2278
|
-
{
|
|
2279
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2280
|
-
}
|
|
2281
|
-
);
|
|
2282
|
-
this.process.stdout?.on("data", (data) => {
|
|
2283
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
2284
|
-
for (const line of lines) {
|
|
2285
|
-
if (this.isReady) {
|
|
2286
|
-
this.log(line);
|
|
2287
|
-
} else {
|
|
2288
|
-
this.bufferedLogs.push(line);
|
|
2289
|
-
}
|
|
2290
|
-
if (line.includes("Started flows") || line.includes("Server now running")) {
|
|
2291
|
-
this.isReady = true;
|
|
2292
|
-
resolve(void 0);
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
});
|
|
2296
|
-
this.process.stderr?.on("data", (data) => {
|
|
2297
|
-
const lines = data.toString().split("\n").filter(Boolean);
|
|
2298
|
-
for (const line of lines) {
|
|
2299
|
-
if (this.isReady) {
|
|
2300
|
-
this.logger.error(`${line}`);
|
|
2301
|
-
} else {
|
|
2302
|
-
this.bufferedLogs.push(line);
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
});
|
|
2306
|
-
this.process.on("error", (error) => {
|
|
2307
|
-
reject(new NodeRedStartError(error));
|
|
2308
|
-
});
|
|
2309
|
-
this.process.on("exit", (code) => {
|
|
2310
|
-
if (!this.isReady && code !== 0 && code !== null) {
|
|
2311
|
-
reject(
|
|
2312
|
-
new NodeRedStartError(
|
|
2313
|
-
new Error(`Process exited with code ${code}`)
|
|
2314
|
-
)
|
|
2315
|
-
);
|
|
2316
|
-
}
|
|
2317
|
-
resolve(void 0);
|
|
2318
|
-
});
|
|
2319
|
-
} catch (error) {
|
|
2320
|
-
reject(new NodeRedStartError(error));
|
|
2321
|
-
}
|
|
2322
|
-
});
|
|
2323
2447
|
};
|
|
2448
|
+
managed.child.once("exit", onExit);
|
|
2449
|
+
this.unwatchExit = () => managed.child.off("exit", onExit);
|
|
2450
|
+
}
|
|
2451
|
+
stopWatchingExit() {
|
|
2452
|
+
this.unwatchExit?.();
|
|
2453
|
+
this.unwatchExit = null;
|
|
2454
|
+
}
|
|
2455
|
+
// start/stop interleaving (e.g. SIGINT while Node-RED is booting) would
|
|
2456
|
+
// race on process/port state and could leak a spawned process, so all
|
|
2457
|
+
// lifecycle operations run strictly one at a time
|
|
2458
|
+
enqueue(operation) {
|
|
2459
|
+
const result = this.operationQueue.then(operation, operation);
|
|
2460
|
+
this.operationQueue = result.catch(() => {
|
|
2461
|
+
});
|
|
2462
|
+
return result;
|
|
2463
|
+
}
|
|
2464
|
+
async start() {
|
|
2465
|
+
return this.enqueue(() => this.doStart());
|
|
2466
|
+
}
|
|
2467
|
+
async stop(skipPortUsageCheck = false) {
|
|
2468
|
+
return this.enqueue(() => this.doStop(skipPortUsageCheck));
|
|
2469
|
+
}
|
|
2470
|
+
async doStart() {
|
|
2324
2471
|
try {
|
|
2472
|
+
this.port = await acquirePort({
|
|
2473
|
+
preferredPort: this.preferredPort,
|
|
2474
|
+
logger: this.logger
|
|
2475
|
+
});
|
|
2476
|
+
if (!this.nodeRedEntryPoint || !fs12.existsSync(this.nodeRedEntryPoint)) {
|
|
2477
|
+
this.nodeRedEntryPoint = await resolveNodeRed({
|
|
2478
|
+
version: this.options.runtime?.version,
|
|
2479
|
+
logger: this.logger
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
const nodeRedEntryPoint = this.nodeRedEntryPoint;
|
|
2483
|
+
const settings = await generateRuntimeSettings({
|
|
2484
|
+
outDir: this.outDir,
|
|
2485
|
+
port: this.port,
|
|
2486
|
+
settingsFilepath: this.options.runtime?.settingsFilepath,
|
|
2487
|
+
logger: this.logger
|
|
2488
|
+
});
|
|
2489
|
+
for (const file of settings.tempFiles) {
|
|
2490
|
+
if (!this.tempFiles.includes(file)) {
|
|
2491
|
+
this.tempFiles.push(file);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
const startProcess = async () => {
|
|
2495
|
+
await this.killProcess();
|
|
2496
|
+
this.bufferedLogs = [];
|
|
2497
|
+
this.process = start({
|
|
2498
|
+
entryPoint: nodeRedEntryPoint,
|
|
2499
|
+
settingsPath: settings.filepath,
|
|
2500
|
+
args: this.options.args ?? [],
|
|
2501
|
+
onLine: (line, source, ready) => this.handleProcessLine(line, source, ready)
|
|
2502
|
+
});
|
|
2503
|
+
await this.process.ready;
|
|
2504
|
+
};
|
|
2325
2505
|
await retry(startProcess, { attempts: 3, delay: 100 });
|
|
2506
|
+
this.watchForUnexpectedExit(this.process);
|
|
2326
2507
|
return this.port;
|
|
2327
2508
|
} catch (error) {
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
if (pid) {
|
|
2331
|
-
treeKill(pid, "SIGKILL");
|
|
2332
|
-
}
|
|
2333
|
-
this.process = null;
|
|
2334
|
-
}
|
|
2335
|
-
throw new NodeRedStartError(error);
|
|
2509
|
+
await this.killProcess();
|
|
2510
|
+
throw error instanceof NodeRedStartError ? error : new NodeRedStartError(error);
|
|
2336
2511
|
}
|
|
2337
2512
|
}
|
|
2338
|
-
async
|
|
2513
|
+
async doStop(skipPortUsageCheck) {
|
|
2339
2514
|
if (!this.process) return;
|
|
2340
|
-
|
|
2515
|
+
this.stopWatchingExit();
|
|
2516
|
+
const pid = this.process.child.pid;
|
|
2341
2517
|
const currentPort = this.port;
|
|
2342
2518
|
if (!pid) {
|
|
2343
2519
|
this.process = null;
|
|
2520
|
+
this.port = null;
|
|
2344
2521
|
return;
|
|
2345
2522
|
}
|
|
2346
|
-
|
|
2347
|
-
this.process.
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
});
|
|
2351
|
-
treeKill(pid, "SIGTERM", (error) => {
|
|
2352
|
-
if (error) {
|
|
2353
|
-
try {
|
|
2354
|
-
process.kill(pid, "SIGTERM");
|
|
2355
|
-
} catch {
|
|
2356
|
-
this.process = null;
|
|
2357
|
-
resolve(void 0);
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
});
|
|
2523
|
+
await stop({
|
|
2524
|
+
child: this.process.child,
|
|
2525
|
+
pid,
|
|
2526
|
+
logger: this.logger
|
|
2361
2527
|
});
|
|
2362
|
-
|
|
2363
|
-
await withTimeout(stopProcess, 1e4);
|
|
2364
|
-
} catch {
|
|
2365
|
-
this.logger.warn("Graceful shutdown timed out, force killing...");
|
|
2366
|
-
await new Promise((resolve) => {
|
|
2367
|
-
treeKill(pid, "SIGKILL", () => {
|
|
2368
|
-
this.process = null;
|
|
2369
|
-
resolve(void 0);
|
|
2370
|
-
});
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2528
|
+
this.process = null;
|
|
2373
2529
|
if (!skipPortUsageCheck && currentPort) {
|
|
2374
|
-
const
|
|
2375
|
-
|
|
2376
|
-
if (availablePort !== currentPort) {
|
|
2377
|
-
throw new Error("Port still in use");
|
|
2378
|
-
}
|
|
2379
|
-
};
|
|
2380
|
-
try {
|
|
2381
|
-
await retry(checkPortUsage, { attempts: 10, delay: 300 });
|
|
2382
|
-
} catch {
|
|
2530
|
+
const released = await waitForPortRelease(currentPort);
|
|
2531
|
+
if (!released) {
|
|
2383
2532
|
this.logger.warn(
|
|
2384
2533
|
`Port ${currentPort} still in use after stop. Force killing...`
|
|
2385
2534
|
);
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
treeKill(pid, "SIGKILL", () => resolve());
|
|
2389
|
-
});
|
|
2390
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2391
|
-
}
|
|
2535
|
+
await kill(pid);
|
|
2536
|
+
await waitForPortRelease(currentPort);
|
|
2392
2537
|
}
|
|
2393
2538
|
}
|
|
2394
2539
|
this.port = null;
|
|
@@ -2400,10 +2545,13 @@ for (const d of dirs) {
|
|
|
2400
2545
|
this.bufferedLogs = [];
|
|
2401
2546
|
}
|
|
2402
2547
|
cleanup() {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2548
|
+
for (const file of this.tempFiles) {
|
|
2549
|
+
try {
|
|
2550
|
+
fs12.unlinkSync(file);
|
|
2551
|
+
} catch {
|
|
2552
|
+
}
|
|
2406
2553
|
}
|
|
2554
|
+
this.tempFiles = [];
|
|
2407
2555
|
}
|
|
2408
2556
|
};
|
|
2409
2557
|
|
|
@@ -2419,10 +2567,10 @@ var NodeRedTestEnvironment = class {
|
|
|
2419
2567
|
options;
|
|
2420
2568
|
constructor(options) {
|
|
2421
2569
|
this.options = options;
|
|
2422
|
-
this.projectDir =
|
|
2423
|
-
this.outDir =
|
|
2424
|
-
this.nodeRedDir =
|
|
2425
|
-
this.installedPkgDir =
|
|
2570
|
+
this.projectDir = path12.resolve(options.projectDir ?? process.cwd());
|
|
2571
|
+
this.outDir = path12.join(this.projectDir, "dist-e2e");
|
|
2572
|
+
this.nodeRedDir = path12.join(this.projectDir, ".node-red");
|
|
2573
|
+
this.installedPkgDir = path12.join(
|
|
2426
2574
|
this.nodeRedDir,
|
|
2427
2575
|
"node_modules",
|
|
2428
2576
|
options.packageName
|
|
@@ -2434,15 +2582,15 @@ var NodeRedTestEnvironment = class {
|
|
|
2434
2582
|
async setup() {
|
|
2435
2583
|
this.originalCwd = process.cwd();
|
|
2436
2584
|
process.chdir(this.projectDir);
|
|
2437
|
-
if (
|
|
2438
|
-
|
|
2585
|
+
if (fs13.existsSync(this.outDir)) fs13.rmSync(this.outDir, { recursive: true });
|
|
2586
|
+
fs13.mkdirSync(this.outDir, { recursive: true });
|
|
2439
2587
|
const buildContext = {
|
|
2440
2588
|
outDir: this.outDir,
|
|
2441
2589
|
packageName: this.options.packageName,
|
|
2442
2590
|
isDev: false
|
|
2443
2591
|
};
|
|
2444
2592
|
const serverOpts = {
|
|
2445
|
-
srcDir:
|
|
2593
|
+
srcDir: path12.join(this.projectDir, "src/server"),
|
|
2446
2594
|
entry: "index.ts",
|
|
2447
2595
|
format: "esm",
|
|
2448
2596
|
bundled: [],
|
|
@@ -2452,7 +2600,7 @@ var NodeRedTestEnvironment = class {
|
|
|
2452
2600
|
};
|
|
2453
2601
|
await build(serverOpts, buildContext);
|
|
2454
2602
|
const clientOpts = {
|
|
2455
|
-
srcDir:
|
|
2603
|
+
srcDir: path12.join(this.projectDir, "src/client"),
|
|
2456
2604
|
entry: "index.ts",
|
|
2457
2605
|
name: this.options.clientName ?? "NodeRedNodes",
|
|
2458
2606
|
format: "es",
|
|
@@ -2461,18 +2609,18 @@ var NodeRedTestEnvironment = class {
|
|
|
2461
2609
|
...this.options.client
|
|
2462
2610
|
};
|
|
2463
2611
|
await build2(clientOpts, buildContext);
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2612
|
+
fs13.mkdirSync(this.installedPkgDir, { recursive: true });
|
|
2613
|
+
fs13.cpSync(this.outDir, this.installedPkgDir, { recursive: true });
|
|
2614
|
+
fs13.mkdirSync(this.nodeRedDir, { recursive: true });
|
|
2615
|
+
fs13.writeFileSync(
|
|
2616
|
+
path12.join(this.nodeRedDir, ".config.runtime.json"),
|
|
2469
2617
|
JSON.stringify({ telemetryEnabled: false })
|
|
2470
2618
|
);
|
|
2471
2619
|
const launcherOpts = {
|
|
2472
2620
|
runtime: { port: this.options.port ?? 1881 }
|
|
2473
2621
|
};
|
|
2474
2622
|
if (this.options.settingsFile) {
|
|
2475
|
-
launcherOpts.runtime.settingsFilepath =
|
|
2623
|
+
launcherOpts.runtime.settingsFilepath = path12.resolve(
|
|
2476
2624
|
this.projectDir,
|
|
2477
2625
|
this.options.settingsFile
|
|
2478
2626
|
);
|
|
@@ -2507,9 +2655,9 @@ var NodeRedTestEnvironment = class {
|
|
|
2507
2655
|
this.launcher.cleanup();
|
|
2508
2656
|
this.launcher = null;
|
|
2509
2657
|
}
|
|
2510
|
-
if (
|
|
2511
|
-
if (
|
|
2512
|
-
|
|
2658
|
+
if (fs13.existsSync(this.outDir)) fs13.rmSync(this.outDir, { recursive: true });
|
|
2659
|
+
if (fs13.existsSync(this.nodeRedDir)) {
|
|
2660
|
+
fs13.rmSync(this.nodeRedDir, { recursive: true });
|
|
2513
2661
|
}
|
|
2514
2662
|
this.port = null;
|
|
2515
2663
|
}
|
|
@@ -2524,7 +2672,7 @@ var defaultConfig = {
|
|
|
2524
2672
|
var _env = null;
|
|
2525
2673
|
async function setup(options) {
|
|
2526
2674
|
const packageName = JSON.parse(
|
|
2527
|
-
|
|
2675
|
+
fs14.readFileSync(path13.join(process.cwd(), "package.json"), "utf-8")
|
|
2528
2676
|
).name;
|
|
2529
2677
|
_env = new NodeRedTestEnvironment({
|
|
2530
2678
|
packageName,
|
|
@@ -2553,8 +2701,8 @@ var NodeRedEditor = class {
|
|
|
2553
2701
|
errors = [];
|
|
2554
2702
|
screenshotDir;
|
|
2555
2703
|
async screenshot(name) {
|
|
2556
|
-
|
|
2557
|
-
const filePath =
|
|
2704
|
+
fs14.mkdirSync(this.screenshotDir, { recursive: true });
|
|
2705
|
+
const filePath = path13.join(this.screenshotDir, `${name}.png`);
|
|
2558
2706
|
await this.page.screenshot({ path: filePath, fullPage: true });
|
|
2559
2707
|
return filePath;
|
|
2560
2708
|
}
|
|
@@ -2605,31 +2753,159 @@ var NodeRedEditor = class {
|
|
|
2605
2753
|
await this.page.waitForTimeout(500);
|
|
2606
2754
|
}
|
|
2607
2755
|
async clickDone() {
|
|
2608
|
-
await this
|
|
2609
|
-
globalThis.document.getElementById("node-dialog-ok").click();
|
|
2610
|
-
});
|
|
2611
|
-
await this.page.waitForSelector(".red-ui-tray", {
|
|
2612
|
-
state: "hidden",
|
|
2613
|
-
timeout: 5e3
|
|
2614
|
-
});
|
|
2756
|
+
await this.#closeTray("node-dialog-ok");
|
|
2615
2757
|
}
|
|
2616
2758
|
async clickCancel() {
|
|
2617
|
-
await this
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2759
|
+
await this.#closeTray("node-dialog-cancel");
|
|
2760
|
+
}
|
|
2761
|
+
// Waits for the tray COUNT to drop rather than for all trays to be hidden,
|
|
2762
|
+
// so a leftover tray from an earlier (failed) test can't poison the wait.
|
|
2763
|
+
async #closeTray(buttonId) {
|
|
2764
|
+
const before = await this.page.locator(".red-ui-tray").count();
|
|
2765
|
+
await this.page.evaluate((id) => {
|
|
2766
|
+
globalThis.document.getElementById(id).click();
|
|
2767
|
+
}, buttonId);
|
|
2768
|
+
await this.page.waitForFunction(
|
|
2769
|
+
(count) => document.querySelectorAll(".red-ui-tray").length < count,
|
|
2770
|
+
before,
|
|
2771
|
+
{ timeout: 5e3 }
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Best-effort close of every open tray — call from afterEach so a failed
|
|
2776
|
+
* test never leaves a tray open for the next one.
|
|
2777
|
+
*/
|
|
2778
|
+
async closeAllTrays() {
|
|
2779
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
2780
|
+
const count = await this.page.locator(".red-ui-tray").count();
|
|
2781
|
+
if (count === 0) return;
|
|
2782
|
+
const cancel = this.page.locator("#node-config-dialog-cancel, #node-dialog-cancel").last();
|
|
2783
|
+
if (await cancel.count() > 0) {
|
|
2784
|
+
await cancel.click({ force: true }).catch(() => {
|
|
2785
|
+
});
|
|
2786
|
+
} else {
|
|
2787
|
+
await this.page.keyboard.press("Escape");
|
|
2788
|
+
}
|
|
2789
|
+
await this.page.waitForTimeout(400);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Closes the config-node tray (stacked above the node tray) with its own
|
|
2794
|
+
* Done button. The node tray underneath stays open — use clickDone() for it.
|
|
2795
|
+
*/
|
|
2796
|
+
async clickConfigDone() {
|
|
2797
|
+
await this.page.click("#node-config-dialog-ok");
|
|
2798
|
+
await this.page.locator("#node-config-dialog-ok").waitFor({ state: "hidden", timeout: 5e3 });
|
|
2799
|
+
await this.page.waitForTimeout(300);
|
|
2800
|
+
}
|
|
2801
|
+
async clickConfigCancel() {
|
|
2802
|
+
await this.page.click("#node-config-dialog-cancel");
|
|
2803
|
+
await this.page.locator("#node-config-dialog-cancel").waitFor({ state: "hidden", timeout: 5e3 });
|
|
2804
|
+
await this.page.waitForTimeout(300);
|
|
2624
2805
|
}
|
|
2625
2806
|
field(label) {
|
|
2626
2807
|
return new NodeRedField(this.page, label);
|
|
2627
2808
|
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Returns a JSON-safe snapshot of a node in the editor's model — use it to
|
|
2811
|
+
* assert values persisted after clickDone(). Functions, internals
|
|
2812
|
+
* (underscore-prefixed keys like `_def`), config `users` back-references,
|
|
2813
|
+
* and any circular references are stripped.
|
|
2814
|
+
*/
|
|
2815
|
+
async getNode(nodeId) {
|
|
2816
|
+
return this.page.evaluate((id) => {
|
|
2817
|
+
const r = globalThis.RED;
|
|
2818
|
+
const node = r?.nodes?.node(id);
|
|
2819
|
+
if (!node) return null;
|
|
2820
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2821
|
+
return JSON.parse(
|
|
2822
|
+
JSON.stringify(node, (key, value) => {
|
|
2823
|
+
if (typeof value === "function") return void 0;
|
|
2824
|
+
if (key.startsWith("_") || key === "users") return void 0;
|
|
2825
|
+
if (typeof value === "object" && value !== null) {
|
|
2826
|
+
if (seen.has(value)) return void 0;
|
|
2827
|
+
seen.add(value);
|
|
2828
|
+
}
|
|
2829
|
+
return value;
|
|
2830
|
+
})
|
|
2831
|
+
);
|
|
2832
|
+
}, nodeId);
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Clicks the editor's Deploy button and waits until the workspace is clean.
|
|
2836
|
+
* Flows with invalid or unused nodes trigger a confirmation dialog — it is
|
|
2837
|
+
* confirmed automatically.
|
|
2838
|
+
*/
|
|
2839
|
+
async clickDeploy() {
|
|
2840
|
+
await this.page.click("#red-ui-header-button-deploy");
|
|
2841
|
+
const confirm = this.page.locator(".red-ui-notification button.primary").first();
|
|
2842
|
+
const confirmationShown = await confirm.waitFor({ state: "visible", timeout: 2e3 }).then(() => true).catch(() => false);
|
|
2843
|
+
if (confirmationShown) await confirm.click();
|
|
2844
|
+
await this.page.waitForFunction(
|
|
2845
|
+
() => {
|
|
2846
|
+
const r = globalThis.RED;
|
|
2847
|
+
return r?.nodes?.dirty?.() === false;
|
|
2848
|
+
},
|
|
2849
|
+
{ timeout: 15e3 }
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
/** Fetches the currently deployed flow from the runtime (GET /flows). */
|
|
2853
|
+
async getDeployedFlow() {
|
|
2854
|
+
const res = await fetch(`http://localhost:${this.port}/flows`);
|
|
2855
|
+
if (!res.ok) {
|
|
2856
|
+
throw new Error(
|
|
2857
|
+
`Failed to fetch flows: ${res.status} ${await res.text()}`
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
return res.json();
|
|
2861
|
+
}
|
|
2862
|
+
/** Counts the output ports rendered for a node on the canvas. */
|
|
2863
|
+
async getNodePortCount(nodeId) {
|
|
2864
|
+
const group = await this.#nodeGroup(nodeId);
|
|
2865
|
+
return group.locator(".red-ui-flow-port-output").count();
|
|
2866
|
+
}
|
|
2867
|
+
/** Returns the node's label text as rendered on the canvas. */
|
|
2868
|
+
async getNodeLabel(nodeId) {
|
|
2869
|
+
const group = await this.#nodeGroup(nodeId);
|
|
2870
|
+
const label = group.locator(".red-ui-flow-node-label").first();
|
|
2871
|
+
return (await label.textContent() ?? "").trim();
|
|
2872
|
+
}
|
|
2873
|
+
/** Returns the status text rendered under a node, or "" when none is set. */
|
|
2874
|
+
async getNodeStatus(nodeId) {
|
|
2875
|
+
const group = await this.#nodeGroup(nodeId);
|
|
2876
|
+
const status = group.locator(".red-ui-flow-node-status-label").first();
|
|
2877
|
+
if (await status.count() === 0) return "";
|
|
2878
|
+
return (await status.textContent() ?? "").trim();
|
|
2879
|
+
}
|
|
2880
|
+
async #nodeGroup(nodeId) {
|
|
2881
|
+
await this.page.waitForFunction(
|
|
2882
|
+
(id) => {
|
|
2883
|
+
const groups = Array.from(
|
|
2884
|
+
document.querySelectorAll(".red-ui-flow-node-group")
|
|
2885
|
+
);
|
|
2886
|
+
return groups.some((el) => el.__data__?.id === id);
|
|
2887
|
+
},
|
|
2888
|
+
nodeId,
|
|
2889
|
+
{ timeout: 1e4 }
|
|
2890
|
+
);
|
|
2891
|
+
const index = await this.page.evaluate((id) => {
|
|
2892
|
+
const groups = Array.from(
|
|
2893
|
+
document.querySelectorAll(".red-ui-flow-node-group")
|
|
2894
|
+
);
|
|
2895
|
+
return groups.findIndex((el) => el.__data__?.id === id);
|
|
2896
|
+
}, nodeId);
|
|
2897
|
+
return this.page.locator(".red-ui-flow-node-group").nth(index);
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Asserts no uncaught page errors occurred, then clears the collected list
|
|
2901
|
+
* so each test only fails for its own errors — call it from afterEach.
|
|
2902
|
+
*/
|
|
2628
2903
|
expectNoPageErrors() {
|
|
2629
|
-
|
|
2904
|
+
const errors = this.errors.splice(0);
|
|
2905
|
+
if (errors.length > 0) {
|
|
2630
2906
|
throw new Error(
|
|
2631
2907
|
`Page errors detected:
|
|
2632
|
-
${
|
|
2908
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
2633
2909
|
);
|
|
2634
2910
|
}
|
|
2635
2911
|
}
|
|
@@ -2641,7 +2917,7 @@ var NodeRedField = class {
|
|
|
2641
2917
|
constructor(page, label) {
|
|
2642
2918
|
this.page = page;
|
|
2643
2919
|
this.label = label;
|
|
2644
|
-
this.row = page.locator(`.form-row:has(:text("${label}"))`).first();
|
|
2920
|
+
this.row = page.locator(".red-ui-tray").last().locator(`.form-row:has(:text("${label}"))`).first();
|
|
2645
2921
|
}
|
|
2646
2922
|
row;
|
|
2647
2923
|
get input() {
|
|
@@ -2716,6 +2992,18 @@ var NodeRedField = class {
|
|
|
2716
2992
|
get addButton() {
|
|
2717
2993
|
return this.row.locator("a.red-ui-button:has(i.fa-plus)");
|
|
2718
2994
|
}
|
|
2995
|
+
/** Clicks the + button of a config field and waits for the config tray. */
|
|
2996
|
+
async openAddConfig() {
|
|
2997
|
+
await this.addButton.click();
|
|
2998
|
+
await this.page.locator("#node-config-dialog-ok").waitFor({ state: "visible", timeout: 1e4 });
|
|
2999
|
+
await this.page.waitForTimeout(500);
|
|
3000
|
+
}
|
|
3001
|
+
/** Clicks the pencil button of a config field and waits for the config tray. */
|
|
3002
|
+
async openEditConfig() {
|
|
3003
|
+
await this.editButton.click();
|
|
3004
|
+
await this.page.locator("#node-config-dialog-ok").waitFor({ state: "visible", timeout: 1e4 });
|
|
3005
|
+
await this.page.waitForTimeout(500);
|
|
3006
|
+
}
|
|
2719
3007
|
async getSelectedOption() {
|
|
2720
3008
|
return this.select.inputValue();
|
|
2721
3009
|
}
|
|
@@ -2731,6 +3019,56 @@ var NodeRedField = class {
|
|
|
2731
3019
|
get editorWrapper() {
|
|
2732
3020
|
return this.row.locator(".editor-wrapper");
|
|
2733
3021
|
}
|
|
3022
|
+
/** Reads the value of the field's code editor (Monaco, ACE fallback). */
|
|
3023
|
+
async getEditorValue() {
|
|
3024
|
+
return this.editorWrapper.evaluate((el) => {
|
|
3025
|
+
const w = globalThis;
|
|
3026
|
+
const editors = w.monaco?.editor?.getEditors?.() ?? [];
|
|
3027
|
+
const monacoEditor = editors.find(
|
|
3028
|
+
(e) => el.contains(e.getContainerDomNode())
|
|
3029
|
+
);
|
|
3030
|
+
if (monacoEditor) return monacoEditor.getValue();
|
|
3031
|
+
const aceEl = el.querySelector(".ace_editor");
|
|
3032
|
+
if (aceEl && w.ace) return w.ace.edit(aceEl).getValue();
|
|
3033
|
+
throw new Error("No code editor instance found in this field");
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
/** Sets the value of the field's code editor (Monaco, ACE fallback). */
|
|
3037
|
+
async setEditorValue(value) {
|
|
3038
|
+
await this.editorWrapper.evaluate((el, newValue) => {
|
|
3039
|
+
const w = globalThis;
|
|
3040
|
+
const editors = w.monaco?.editor?.getEditors?.() ?? [];
|
|
3041
|
+
const monacoEditor = editors.find(
|
|
3042
|
+
(e) => el.contains(e.getContainerDomNode())
|
|
3043
|
+
);
|
|
3044
|
+
if (monacoEditor) {
|
|
3045
|
+
monacoEditor.setValue(newValue);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
const aceEl = el.querySelector(".ace_editor");
|
|
3049
|
+
if (aceEl && w.ace) {
|
|
3050
|
+
w.ace.edit(aceEl).setValue(newValue, 1);
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
throw new Error("No code editor instance found in this field");
|
|
3054
|
+
}, value);
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Types into the field and returns the labels of the autocomplete
|
|
3058
|
+
* suggestions that appear (TypedInput types with an `autoComplete` source).
|
|
3059
|
+
*/
|
|
3060
|
+
async getAutoCompleteSuggestions(prefix) {
|
|
3061
|
+
const input = this.row.locator("input:visible").first();
|
|
3062
|
+
await input.fill("", { force: true });
|
|
3063
|
+
await input.pressSequentially(prefix, { delay: 30 });
|
|
3064
|
+
const menu = this.page.locator(".red-ui-autoComplete-container:visible").first();
|
|
3065
|
+
await menu.waitFor({ state: "visible", timeout: 5e3 });
|
|
3066
|
+
const labels = await menu.locator("li").evaluateAll(
|
|
3067
|
+
(els) => els.map((el) => el.textContent?.trim() ?? "").filter(Boolean)
|
|
3068
|
+
);
|
|
3069
|
+
await this.page.keyboard.press("Escape");
|
|
3070
|
+
return labels;
|
|
3071
|
+
}
|
|
2734
3072
|
get expandButton() {
|
|
2735
3073
|
return this.row.locator(".expand-button");
|
|
2736
3074
|
}
|