@bonsae/nrg 0.16.0 → 0.18.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 +112 -19
- package/package.json +38 -6
- package/server/index.cjs +38 -24
- package/server/resources/nrg-client.js +3667 -3521
- package/test/client/component/index.js +244 -0
- package/test/client/component/setup.js +201 -0
- package/test/client/e2e/index.js +2698 -0
- package/test/client/unit/index.js +200 -0
- package/test/client/unit/setup.js +199 -0
- package/test/{index.js → server/unit/index.js} +23 -8
- package/tsconfig/core/client.json +11 -0
- package/tsconfig/{server.json → core/server.json} +1 -1
- package/tsconfig/test/client/component.json +11 -0
- package/tsconfig/test/client/e2e.json +6 -0
- package/tsconfig/test/client/unit.json +6 -0
- package/tsconfig/test/server/unit.json +6 -0
- package/types/client.d.ts +66 -0
- package/types/server.d.ts +74 -58
- package/types/shims/form/components/node-red-config-input.vue.d.ts +15 -2
- package/types/shims/form/components/node-red-editor-input.vue.d.ts +18 -3
- package/types/shims/form/components/node-red-input.vue.d.ts +17 -5
- package/types/shims/form/components/node-red-json-schema-form.vue.d.ts +117 -19
- package/types/shims/form/components/node-red-select-input.vue.d.ts +15 -1
- package/types/shims/form/components/node-red-typed-input.vue.d.ts +52 -8
- package/types/test-client-component.d.ts +70 -0
- package/types/test-client-e2e.d.ts +152 -0
- package/types/test-client-unit.d.ts +52 -0
- package/types/{test.d.ts → test-server-unit.d.ts} +12 -2
- package/vite/index.js +15 -7
- package/tsconfig/client.json +0 -11
|
@@ -0,0 +1,2698 @@
|
|
|
1
|
+
// src/test/client/e2e/index.ts
|
|
2
|
+
import fs12 from "fs";
|
|
3
|
+
import path12 from "path";
|
|
4
|
+
|
|
5
|
+
// src/test/client/e2e/environment.ts
|
|
6
|
+
import fs11 from "fs";
|
|
7
|
+
import path11 from "path";
|
|
8
|
+
|
|
9
|
+
// src/vite/server/build.ts
|
|
10
|
+
import { build as viteBuild } from "vite";
|
|
11
|
+
import fs3 from "fs";
|
|
12
|
+
import path3 from "path";
|
|
13
|
+
|
|
14
|
+
// src/vite/errors.ts
|
|
15
|
+
var PluginError = class extends Error {
|
|
16
|
+
constructor(message, code, cause) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
this.name = "PluginError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var NodeRedStartError = class extends PluginError {
|
|
24
|
+
constructor(cause) {
|
|
25
|
+
super("Failed to start Node-RED", "NODE_RED_START_FAILED", cause);
|
|
26
|
+
this.name = "NodeRedStartError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var BuildError = class extends PluginError {
|
|
30
|
+
constructor(phase, cause) {
|
|
31
|
+
super(
|
|
32
|
+
`Failed to build ${phase}`,
|
|
33
|
+
`BUILD_${phase.toUpperCase()}_FAILED`,
|
|
34
|
+
cause
|
|
35
|
+
);
|
|
36
|
+
this.name = "BuildError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/vite/logger.ts
|
|
41
|
+
import * as clackLogger from "@clack/prompts";
|
|
42
|
+
var color = {
|
|
43
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
44
|
+
cyan: (s) => `\x1B[36m${s}\x1B[0m`
|
|
45
|
+
};
|
|
46
|
+
var Logger = class _Logger {
|
|
47
|
+
name;
|
|
48
|
+
prefix;
|
|
49
|
+
spinner = clackLogger.spinner();
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.name = options.name;
|
|
52
|
+
this.prefix = options.prefix;
|
|
53
|
+
}
|
|
54
|
+
format(message) {
|
|
55
|
+
return this.prefix ? `[${this.prefix}] ${message}` : message;
|
|
56
|
+
}
|
|
57
|
+
intro(message) {
|
|
58
|
+
clackLogger.intro(message ?? this.name);
|
|
59
|
+
}
|
|
60
|
+
outro(message) {
|
|
61
|
+
clackLogger.outro(this.format(message));
|
|
62
|
+
}
|
|
63
|
+
step(message) {
|
|
64
|
+
clackLogger.log.step(this.format(message));
|
|
65
|
+
}
|
|
66
|
+
success(message) {
|
|
67
|
+
clackLogger.log.success(this.format(message));
|
|
68
|
+
}
|
|
69
|
+
warn(message) {
|
|
70
|
+
clackLogger.log.warn(this.format(message));
|
|
71
|
+
}
|
|
72
|
+
error(message, cause) {
|
|
73
|
+
clackLogger.log.error(this.format(message));
|
|
74
|
+
if (cause) {
|
|
75
|
+
console.error(cause);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
info(message) {
|
|
79
|
+
clackLogger.log.info(this.format(message));
|
|
80
|
+
}
|
|
81
|
+
message(message) {
|
|
82
|
+
clackLogger.log.message(this.format(message));
|
|
83
|
+
}
|
|
84
|
+
raw(message) {
|
|
85
|
+
const prefix = this.prefix ? color.dim(`[${this.prefix}]`) : "";
|
|
86
|
+
console.log(`\u2502 ${prefix} ${message}`);
|
|
87
|
+
}
|
|
88
|
+
startGroup(title) {
|
|
89
|
+
if (title) {
|
|
90
|
+
console.log(`\u2502`);
|
|
91
|
+
console.log(`\u251C\u2500 ${color.cyan(title)}`);
|
|
92
|
+
}
|
|
93
|
+
console.log(`\u2502`);
|
|
94
|
+
}
|
|
95
|
+
endGroup(message) {
|
|
96
|
+
console.log(`\u2502`);
|
|
97
|
+
console.log(`\u2514\u2500 ${message ?? "Done"}`);
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
startSpinner(message) {
|
|
101
|
+
this.spinner.start(this.format(message));
|
|
102
|
+
}
|
|
103
|
+
stopSpinner(message) {
|
|
104
|
+
this.spinner.stop(this.format(message));
|
|
105
|
+
}
|
|
106
|
+
child(prefix) {
|
|
107
|
+
const newPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix;
|
|
108
|
+
return new _Logger({ name: this.name, prefix: newPrefix });
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var logger = new Logger({ name: "vite-plugin-node-red" });
|
|
112
|
+
|
|
113
|
+
// src/vite/server/plugins/type-generator.ts
|
|
114
|
+
import dts from "vite-plugin-dts";
|
|
115
|
+
import fs from "fs";
|
|
116
|
+
import path from "path";
|
|
117
|
+
import ts from "typescript";
|
|
118
|
+
function collectTsFiles(dir) {
|
|
119
|
+
if (!fs.existsSync(dir)) return [];
|
|
120
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((dirent) => {
|
|
121
|
+
const full = path.join(dir, dirent.name);
|
|
122
|
+
if (dirent.isDirectory()) return collectTsFiles(full);
|
|
123
|
+
if (dirent.isFile() && dirent.name.endsWith(".ts") && !dirent.name.endsWith(".d.ts"))
|
|
124
|
+
return [full];
|
|
125
|
+
return [];
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function toPascalCase(name) {
|
|
129
|
+
return name.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
130
|
+
}
|
|
131
|
+
var BASE_CLASS_SLOTS = {
|
|
132
|
+
IONode: ["Config", "Credentials", "Input", "Output", "Settings"],
|
|
133
|
+
ConfigNode: ["Config", "Credentials", "Settings"]
|
|
134
|
+
};
|
|
135
|
+
function getNodeTypeExports(filePath) {
|
|
136
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
137
|
+
const source = ts.createSourceFile(
|
|
138
|
+
filePath,
|
|
139
|
+
content,
|
|
140
|
+
ts.ScriptTarget.ESNext,
|
|
141
|
+
/* setParentNodes */
|
|
142
|
+
true
|
|
143
|
+
);
|
|
144
|
+
const hasModifier = (node, kind) => (node.modifiers ?? []).some((m) => m.kind === kind);
|
|
145
|
+
const exportedTypeNames = /* @__PURE__ */ new Set();
|
|
146
|
+
for (const stmt of source.statements) {
|
|
147
|
+
if (ts.isTypeAliasDeclaration(stmt) && hasModifier(stmt, ts.SyntaxKind.ExportKeyword)) {
|
|
148
|
+
exportedTypeNames.add(stmt.name.text);
|
|
149
|
+
}
|
|
150
|
+
if (ts.isInterfaceDeclaration(stmt) && hasModifier(stmt, ts.SyntaxKind.ExportKeyword)) {
|
|
151
|
+
exportedTypeNames.add(stmt.name.text);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (exportedTypeNames.size === 0) return [];
|
|
155
|
+
let baseClassName = "";
|
|
156
|
+
const genericArgNames = [];
|
|
157
|
+
for (const stmt of source.statements) {
|
|
158
|
+
if (!ts.isClassDeclaration(stmt) || !hasModifier(stmt, ts.SyntaxKind.DefaultKeyword)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
for (const clause of stmt.heritageClauses ?? []) {
|
|
162
|
+
if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
163
|
+
for (const type of clause.types) {
|
|
164
|
+
if (ts.isIdentifier(type.expression)) {
|
|
165
|
+
baseClassName = type.expression.text;
|
|
166
|
+
}
|
|
167
|
+
for (const arg of type.typeArguments ?? []) {
|
|
168
|
+
if (ts.isTypeReferenceNode(arg) && ts.isIdentifier(arg.typeName)) {
|
|
169
|
+
genericArgNames.push(arg.typeName.text);
|
|
170
|
+
} else {
|
|
171
|
+
genericArgNames.push("");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
const slots = BASE_CLASS_SLOTS[baseClassName];
|
|
179
|
+
if (!slots) return [];
|
|
180
|
+
const result = [];
|
|
181
|
+
for (let i = 0; i < genericArgNames.length; i++) {
|
|
182
|
+
const localName = genericArgNames[i];
|
|
183
|
+
const semanticName = slots[i];
|
|
184
|
+
if (localName && semanticName && exportedTypeNames.has(localName)) {
|
|
185
|
+
result.push({ localName, semanticName });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
var SCHEMA_PROP_SEMANTICS = {
|
|
191
|
+
configSchema: "ConfigSchema",
|
|
192
|
+
credentialsSchema: "CredentialsSchema",
|
|
193
|
+
inputSchema: "InputSchema",
|
|
194
|
+
outputsSchema: "OutputsSchema",
|
|
195
|
+
settingsSchema: "SettingsSchema"
|
|
196
|
+
};
|
|
197
|
+
function getSchemaReferences(filePath) {
|
|
198
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
199
|
+
const source = ts.createSourceFile(
|
|
200
|
+
filePath,
|
|
201
|
+
content,
|
|
202
|
+
ts.ScriptTarget.ESNext,
|
|
203
|
+
true
|
|
204
|
+
);
|
|
205
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const stmt of source.statements) {
|
|
207
|
+
if (ts.isImportDeclaration(stmt) && stmt.importClause) {
|
|
208
|
+
const moduleSpecifier = stmt.moduleSpecifier.text;
|
|
209
|
+
const namedBindings = stmt.importClause.namedBindings;
|
|
210
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
211
|
+
for (const el of namedBindings.elements) {
|
|
212
|
+
importMap.set(el.name.text, moduleSpecifier);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const schemaRefs = /* @__PURE__ */ new Map();
|
|
218
|
+
const recordPortNames = /* @__PURE__ */ new Map();
|
|
219
|
+
let outputsSchemaIsRecord = false;
|
|
220
|
+
function extractIdentifiers(node, propName) {
|
|
221
|
+
if (ts.isIdentifier(node)) return [node.text];
|
|
222
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
223
|
+
return node.elements.filter(ts.isIdentifier).map((el) => el.text);
|
|
224
|
+
}
|
|
225
|
+
if (ts.isObjectLiteralExpression(node) && propName === "outputsSchema") {
|
|
226
|
+
const ids = [];
|
|
227
|
+
outputsSchemaIsRecord = true;
|
|
228
|
+
for (const prop of node.properties) {
|
|
229
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer)) {
|
|
230
|
+
const portName = ts.isIdentifier(prop.name) ? prop.name.text : ts.isStringLiteral(prop.name) ? prop.name.text : void 0;
|
|
231
|
+
if (portName) {
|
|
232
|
+
ids.push(prop.initializer.text);
|
|
233
|
+
recordPortNames.set(prop.initializer.text, portName);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return ids;
|
|
238
|
+
}
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
for (const stmt of source.statements) {
|
|
242
|
+
if (ts.isClassDeclaration(stmt)) {
|
|
243
|
+
for (const member of stmt.members) {
|
|
244
|
+
if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text in SCHEMA_PROP_SEMANTICS && member.initializer) {
|
|
245
|
+
const ids = extractIdentifiers(member.initializer, member.name.text);
|
|
246
|
+
if (ids.length > 0) {
|
|
247
|
+
schemaRefs.set(member.name.text, ids);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (ts.isExportAssignment(stmt) && stmt.expression && ts.isCallExpression(stmt.expression)) {
|
|
253
|
+
const callee = stmt.expression.expression;
|
|
254
|
+
if (ts.isIdentifier(callee) && (callee.text === "defineIONode" || callee.text === "defineConfigNode")) {
|
|
255
|
+
const arg = stmt.expression.arguments[0];
|
|
256
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
257
|
+
for (const prop of arg.properties) {
|
|
258
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text in SCHEMA_PROP_SEMANTICS) {
|
|
259
|
+
const ids = extractIdentifiers(prop.initializer, prop.name.text);
|
|
260
|
+
if (ids.length > 0) {
|
|
261
|
+
schemaRefs.set(prop.name.text, ids);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const result = [];
|
|
270
|
+
for (const [propName, identifiers] of schemaRefs) {
|
|
271
|
+
const semanticName = SCHEMA_PROP_SEMANTICS[propName];
|
|
272
|
+
const isArray = identifiers.length > 1 && !outputsSchemaIsRecord;
|
|
273
|
+
const isRecord = propName === "outputsSchema" && outputsSchemaIsRecord;
|
|
274
|
+
for (const identifier of identifiers) {
|
|
275
|
+
const importSource = importMap.get(identifier);
|
|
276
|
+
if (importSource) {
|
|
277
|
+
result.push({
|
|
278
|
+
localName: identifier,
|
|
279
|
+
semanticName: isArray || isRecord ? identifier : semanticName,
|
|
280
|
+
importSource,
|
|
281
|
+
tupleProp: isArray || isRecord ? semanticName : void 0,
|
|
282
|
+
recordPortName: isRecord ? recordPortNames.get(identifier) : void 0
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
function getFactoryInfo(filePath) {
|
|
290
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
291
|
+
const source = ts.createSourceFile(
|
|
292
|
+
filePath,
|
|
293
|
+
content,
|
|
294
|
+
ts.ScriptTarget.ESNext,
|
|
295
|
+
true
|
|
296
|
+
);
|
|
297
|
+
for (const stmt of source.statements) {
|
|
298
|
+
if (ts.isExportAssignment(stmt) && stmt.expression && ts.isCallExpression(stmt.expression)) {
|
|
299
|
+
const callee = stmt.expression.expression;
|
|
300
|
+
if (ts.isIdentifier(callee)) {
|
|
301
|
+
if (callee.text === "defineIONode" || callee.text === "defineConfigNode") {
|
|
302
|
+
return { factoryName: callee.text };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
function buildTypeArg(schemaMap, semanticName, portNameMap) {
|
|
310
|
+
const names = schemaMap.get(semanticName);
|
|
311
|
+
if (!names || names.length === 0) return "any";
|
|
312
|
+
if (names.length === 1 && !portNameMap?.has(names[0]))
|
|
313
|
+
return `Infer<typeof ${names[0]}>`;
|
|
314
|
+
if (portNameMap && names.some((n) => portNameMap.has(n))) {
|
|
315
|
+
const entries = names.map((n) => {
|
|
316
|
+
const portName = portNameMap.get(n);
|
|
317
|
+
return portName ? `${portName}: Infer<typeof ${n}>` : null;
|
|
318
|
+
}).filter(Boolean);
|
|
319
|
+
return `{ ${entries.join(", ")} }`;
|
|
320
|
+
}
|
|
321
|
+
return `[${names.map((n) => `Infer<typeof ${n}>`).join(", ")}]`;
|
|
322
|
+
}
|
|
323
|
+
function buildNodeReexports(srcDir, entryFile) {
|
|
324
|
+
const nodesDir = path.join(srcDir, "nodes");
|
|
325
|
+
const nodeFiles = collectTsFiles(nodesDir);
|
|
326
|
+
return nodeFiles.map((file) => {
|
|
327
|
+
const rel = path.relative(path.dirname(entryFile), file).replace(/\\/g, "/");
|
|
328
|
+
const relPath = rel.startsWith(".") ? rel : `./${rel}`;
|
|
329
|
+
const specifier = relPath.replace(/\.ts$/, "");
|
|
330
|
+
const ns = toPascalCase(path.basename(file, ".ts"));
|
|
331
|
+
const schemaRefs = getSchemaReferences(file);
|
|
332
|
+
const factoryInfo = getFactoryInfo(file);
|
|
333
|
+
const lines = [];
|
|
334
|
+
if (factoryInfo) {
|
|
335
|
+
const interfaceName = factoryInfo.factoryName === "defineIONode" ? "IIONode" : "IConfigNode";
|
|
336
|
+
lines.push(`import _${ns} from "${specifier}";`);
|
|
337
|
+
const schemaMap = /* @__PURE__ */ new Map();
|
|
338
|
+
const portNameMap = /* @__PURE__ */ new Map();
|
|
339
|
+
for (const ref of schemaRefs) {
|
|
340
|
+
const key = ref.tupleProp ?? ref.semanticName;
|
|
341
|
+
if (!schemaMap.has(key)) schemaMap.set(key, []);
|
|
342
|
+
schemaMap.get(key).push(ref.localName);
|
|
343
|
+
if (ref.recordPortName) {
|
|
344
|
+
portNameMap.set(ref.localName, ref.recordPortName);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const hasSchemas = schemaMap.size > 0;
|
|
348
|
+
const hasRecordOutputs = portNameMap.size > 0;
|
|
349
|
+
const nrgImports = ["NodeConstructor", interfaceName];
|
|
350
|
+
if (hasSchemas) nrgImports.push("Infer");
|
|
351
|
+
lines.push(
|
|
352
|
+
`import type { ${nrgImports.join(", ")} } from "@bonsae/nrg/server";`
|
|
353
|
+
);
|
|
354
|
+
if (hasSchemas) {
|
|
355
|
+
const bySource2 = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const ref of schemaRefs) {
|
|
357
|
+
const resolvedSource = path.relative(
|
|
358
|
+
path.dirname(entryFile),
|
|
359
|
+
path.resolve(path.dirname(file), ref.importSource)
|
|
360
|
+
).replace(/\\/g, "/");
|
|
361
|
+
const sourceSpecifier = resolvedSource.startsWith(".") ? resolvedSource : `./${resolvedSource}`;
|
|
362
|
+
if (!bySource2.has(sourceSpecifier))
|
|
363
|
+
bySource2.set(sourceSpecifier, []);
|
|
364
|
+
const list = bySource2.get(sourceSpecifier);
|
|
365
|
+
if (!list.includes(ref.localName)) list.push(ref.localName);
|
|
366
|
+
}
|
|
367
|
+
for (const [source, names] of bySource2) {
|
|
368
|
+
lines.push(`import type { ${names.join(", ")} } from "${source}";`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
let typeArgs;
|
|
372
|
+
if (factoryInfo.factoryName === "defineIONode") {
|
|
373
|
+
const outputsArg = buildTypeArg(
|
|
374
|
+
schemaMap,
|
|
375
|
+
"OutputsSchema",
|
|
376
|
+
portNameMap.size > 0 ? portNameMap : void 0
|
|
377
|
+
);
|
|
378
|
+
typeArgs = [
|
|
379
|
+
buildTypeArg(schemaMap, "ConfigSchema"),
|
|
380
|
+
buildTypeArg(schemaMap, "CredentialsSchema"),
|
|
381
|
+
buildTypeArg(schemaMap, "InputSchema"),
|
|
382
|
+
outputsArg
|
|
383
|
+
].join(", ");
|
|
384
|
+
} else {
|
|
385
|
+
typeArgs = [
|
|
386
|
+
buildTypeArg(schemaMap, "ConfigSchema"),
|
|
387
|
+
buildTypeArg(schemaMap, "CredentialsSchema")
|
|
388
|
+
].join(", ");
|
|
389
|
+
}
|
|
390
|
+
lines.push(
|
|
391
|
+
`export const ${ns}: NodeConstructor<${interfaceName}<${typeArgs}>> = _${ns};`
|
|
392
|
+
);
|
|
393
|
+
} else {
|
|
394
|
+
lines.push(`export { default as ${ns} } from "${specifier}";`);
|
|
395
|
+
const typePairs = getNodeTypeExports(file);
|
|
396
|
+
if (typePairs.length > 0) {
|
|
397
|
+
const prefixed = typePairs.map(
|
|
398
|
+
({ localName, semanticName }) => `${localName} as ${ns}${semanticName}`
|
|
399
|
+
).join(", ");
|
|
400
|
+
lines.push(`export type { ${prefixed} } from "${specifier}";`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
404
|
+
for (const ref of schemaRefs) {
|
|
405
|
+
const resolvedSource = path.relative(
|
|
406
|
+
path.dirname(entryFile),
|
|
407
|
+
path.resolve(path.dirname(file), ref.importSource)
|
|
408
|
+
).replace(/\\/g, "/");
|
|
409
|
+
const sourceSpecifier = resolvedSource.startsWith(".") ? resolvedSource : `./${resolvedSource}`;
|
|
410
|
+
if (!bySource.has(sourceSpecifier)) bySource.set(sourceSpecifier, []);
|
|
411
|
+
bySource.get(sourceSpecifier).push(`${ref.localName} as ${ns}${ref.semanticName}`);
|
|
412
|
+
}
|
|
413
|
+
for (const [source, names] of bySource) {
|
|
414
|
+
lines.push(`export { ${names.join(", ")} } from "${source}";`);
|
|
415
|
+
}
|
|
416
|
+
return lines.join("\n");
|
|
417
|
+
}).join("\n");
|
|
418
|
+
}
|
|
419
|
+
function typeGenerator(options) {
|
|
420
|
+
const { srcDir, outDir, entryFiles } = options;
|
|
421
|
+
const serverTsconfig = path.resolve(srcDir, "tsconfig.json");
|
|
422
|
+
const rootTsconfig = path.resolve("tsconfig.json");
|
|
423
|
+
const userTsconfig = fs.existsSync(serverTsconfig) ? serverTsconfig : rootTsconfig;
|
|
424
|
+
const clientDir = path.relative(
|
|
425
|
+
process.cwd(),
|
|
426
|
+
path.join(path.dirname(srcDir), "client")
|
|
427
|
+
);
|
|
428
|
+
const augmentedContents = /* @__PURE__ */ new Map();
|
|
429
|
+
let origReadFile = null;
|
|
430
|
+
const nodeTypeExporter = {
|
|
431
|
+
name: "vite-plugin-node-red:server:type-exporter",
|
|
432
|
+
enforce: "pre",
|
|
433
|
+
buildStart() {
|
|
434
|
+
augmentedContents.clear();
|
|
435
|
+
for (const entryFile of entryFiles) {
|
|
436
|
+
const reexports = buildNodeReexports(srcDir, entryFile);
|
|
437
|
+
if (!reexports) continue;
|
|
438
|
+
const original = fs.readFileSync(entryFile, "utf-8");
|
|
439
|
+
augmentedContents.set(
|
|
440
|
+
path.resolve(entryFile),
|
|
441
|
+
`${original}
|
|
442
|
+
${reexports}
|
|
443
|
+
`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (augmentedContents.size === 0) return;
|
|
447
|
+
origReadFile = ts.sys.readFile.bind(ts.sys);
|
|
448
|
+
ts.sys.readFile = (fileName, encoding) => {
|
|
449
|
+
const resolved = path.resolve(fileName);
|
|
450
|
+
return augmentedContents.get(resolved) ?? origReadFile(fileName, encoding);
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
closeBundle() {
|
|
454
|
+
if (origReadFile) {
|
|
455
|
+
ts.sys.readFile = origReadFile;
|
|
456
|
+
origReadFile = null;
|
|
457
|
+
}
|
|
458
|
+
augmentedContents.clear();
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
return [
|
|
462
|
+
nodeTypeExporter,
|
|
463
|
+
dts({
|
|
464
|
+
rollupTypes: true,
|
|
465
|
+
rollupOptions: {
|
|
466
|
+
messageCallback(message) {
|
|
467
|
+
if (message.messageId === "console-preamble") {
|
|
468
|
+
message.logLevel = "none";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
outDir,
|
|
473
|
+
...fs.existsSync(userTsconfig) && { tsconfigPath: userTsconfig },
|
|
474
|
+
compilerOptions: {
|
|
475
|
+
noEmit: false,
|
|
476
|
+
declaration: true,
|
|
477
|
+
emitDeclarationOnly: true,
|
|
478
|
+
noCheck: true
|
|
479
|
+
},
|
|
480
|
+
exclude: [
|
|
481
|
+
`${clientDir}/**`,
|
|
482
|
+
"**/client/**",
|
|
483
|
+
"vite/**",
|
|
484
|
+
"node_modules/**",
|
|
485
|
+
"dist/**"
|
|
486
|
+
]
|
|
487
|
+
})
|
|
488
|
+
].flat();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/vite/server/plugins/output-wrapper.ts
|
|
492
|
+
function cjsWrapper() {
|
|
493
|
+
return {
|
|
494
|
+
name: "vite-plugin-node-red:server:cjs-wrapper",
|
|
495
|
+
renderChunk(code, chunk, outputOptions) {
|
|
496
|
+
if (!chunk.isEntry || outputOptions.format !== "cjs") return null;
|
|
497
|
+
const footer = `(function(){var _exp=module.exports&&module.exports.__esModule?module.exports.default:module.exports;if(_exp&&typeof _exp==="object"&&Array.isArray(_exp.nodes)){var _nrg=require("@bonsae/nrg/server");module.exports=_nrg.registerTypes(_exp.nodes);}else if(typeof _exp==="function"&&Array.isArray(_exp.nodes)){module.exports=_exp;_exp.nodes.forEach(function(cls){if(cls&&cls.type){_exp[cls.type.replace(/(?:^|[-_])(\\w)/g,function(_,c){return c.toUpperCase();})] = cls;}});}})();`;
|
|
498
|
+
return { code: `${code}
|
|
499
|
+
${footer}`, map: null };
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function esmWrapper() {
|
|
504
|
+
return {
|
|
505
|
+
name: "vite-plugin-node-red:server:esm-wrapper",
|
|
506
|
+
renderChunk(code, chunk, outputOptions) {
|
|
507
|
+
if (!chunk.isEntry || outputOptions.format !== "es") return null;
|
|
508
|
+
const reNamed = /export\s*\{\s*(\w+)\s+as\s+default\s*\}\s*;?/;
|
|
509
|
+
const reDefault = /export\s+default\s+(\w+)\s*;?/;
|
|
510
|
+
let match = code.match(reNamed);
|
|
511
|
+
let varName;
|
|
512
|
+
let matchStr;
|
|
513
|
+
if (match) {
|
|
514
|
+
varName = match[1];
|
|
515
|
+
matchStr = match[0];
|
|
516
|
+
} else {
|
|
517
|
+
match = code.match(reDefault);
|
|
518
|
+
if (match) {
|
|
519
|
+
varName = match[1];
|
|
520
|
+
matchStr = match[0];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (!varName || !matchStr) return null;
|
|
524
|
+
const header = [
|
|
525
|
+
`import { fileURLToPath as __nrgFileURLToPath } from "url";`,
|
|
526
|
+
`import { dirname as __nrgDirname } from "path";`,
|
|
527
|
+
`var __filename = __nrgFileURLToPath(import.meta.url);`,
|
|
528
|
+
`var __dirname = __nrgDirname(__filename);`,
|
|
529
|
+
`import { registerTypes as __nrgRegisterTypes } from "@bonsae/nrg/server";`,
|
|
530
|
+
``
|
|
531
|
+
].join("\n");
|
|
532
|
+
const replacement = [
|
|
533
|
+
`var __nrgExport = ${varName};`,
|
|
534
|
+
`if (__nrgExport && typeof __nrgExport === "object" && Array.isArray(__nrgExport.nodes)) {`,
|
|
535
|
+
` __nrgExport = __nrgRegisterTypes(__nrgExport.nodes);`,
|
|
536
|
+
`}`,
|
|
537
|
+
`export { __nrgExport as default };`
|
|
538
|
+
].join("\n");
|
|
539
|
+
const newCode = header + code.replace(matchStr, replacement);
|
|
540
|
+
return { code: newCode, map: null };
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/vite/server/plugins/package-json-generator.ts
|
|
546
|
+
import fs2 from "fs";
|
|
547
|
+
import path2 from "path";
|
|
548
|
+
import { builtinModules } from "node:module";
|
|
549
|
+
var nodeBuiltins = /* @__PURE__ */ new Set([
|
|
550
|
+
...builtinModules,
|
|
551
|
+
...builtinModules.map((m) => `node:${m}`)
|
|
552
|
+
]);
|
|
553
|
+
function buildTypesPath(entryName) {
|
|
554
|
+
return `./${entryName}.d.ts`;
|
|
555
|
+
}
|
|
556
|
+
function buildOutputPath(entryName) {
|
|
557
|
+
return `./${entryName}.js`;
|
|
558
|
+
}
|
|
559
|
+
function buildExportKey(entryName) {
|
|
560
|
+
return entryName === "index" ? "." : `./${entryName}`;
|
|
561
|
+
}
|
|
562
|
+
function buildEsmOutputPath(entryName) {
|
|
563
|
+
return `./${entryName}.mjs`;
|
|
564
|
+
}
|
|
565
|
+
function generateExports(entryNames, format = "cjs") {
|
|
566
|
+
const exports = {};
|
|
567
|
+
for (const name of entryNames) {
|
|
568
|
+
const key = buildExportKey(name);
|
|
569
|
+
if (format === "esm") {
|
|
570
|
+
exports[key] = {
|
|
571
|
+
types: buildTypesPath(name),
|
|
572
|
+
import: buildEsmOutputPath(name),
|
|
573
|
+
require: buildOutputPath(name),
|
|
574
|
+
default: buildOutputPath(name)
|
|
575
|
+
};
|
|
576
|
+
} else {
|
|
577
|
+
exports[key] = {
|
|
578
|
+
types: buildTypesPath(name),
|
|
579
|
+
require: buildOutputPath(name),
|
|
580
|
+
default: buildOutputPath(name)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return exports;
|
|
585
|
+
}
|
|
586
|
+
function patchExportsWithTypes(existingExports, entryNames) {
|
|
587
|
+
const patched = { ...existingExports };
|
|
588
|
+
for (const [key, value] of Object.entries(patched)) {
|
|
589
|
+
const entryName = key === "." ? "index" : key.replace(/^\.\//, "");
|
|
590
|
+
if (!entryNames.includes(entryName)) continue;
|
|
591
|
+
const typesPath = buildTypesPath(entryName);
|
|
592
|
+
if (typeof value === "string") {
|
|
593
|
+
patched[key] = { types: typesPath, require: value, default: value };
|
|
594
|
+
} else if (typeof value === "object" && value !== null) {
|
|
595
|
+
const condition = value;
|
|
596
|
+
if (!condition.types) {
|
|
597
|
+
patched[key] = { types: typesPath, ...condition };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return patched;
|
|
602
|
+
}
|
|
603
|
+
function packageJsonGenerator(options) {
|
|
604
|
+
const {
|
|
605
|
+
outDir,
|
|
606
|
+
bundled = [],
|
|
607
|
+
types = false,
|
|
608
|
+
entryNames = [],
|
|
609
|
+
format = "cjs"
|
|
610
|
+
} = options;
|
|
611
|
+
const trackedDependencies = /* @__PURE__ */ new Set();
|
|
612
|
+
return {
|
|
613
|
+
name: "vite-plugin-node-red:server:package-json-generator",
|
|
614
|
+
enforce: "pre",
|
|
615
|
+
buildStart() {
|
|
616
|
+
trackedDependencies.clear();
|
|
617
|
+
},
|
|
618
|
+
resolveId: {
|
|
619
|
+
order: "pre",
|
|
620
|
+
handler(source, importer) {
|
|
621
|
+
if (!importer || source.startsWith(".") || source.startsWith("/")) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
if (nodeBuiltins.has(source)) {
|
|
625
|
+
return { id: source, external: true };
|
|
626
|
+
}
|
|
627
|
+
const packageName = source.startsWith("@") ? source.split("/").slice(0, 2).join("/") : source.split("/")[0];
|
|
628
|
+
if (bundled.includes(packageName)) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
trackedDependencies.add(packageName);
|
|
632
|
+
return { id: source, external: true };
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
closeBundle() {
|
|
636
|
+
const rootPackageJsonPath = path2.resolve("./package.json");
|
|
637
|
+
if (!fs2.existsSync(rootPackageJsonPath)) {
|
|
638
|
+
logger.warn(`package.json not found: ${rootPackageJsonPath}`);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const rootPackageJson = JSON.parse(
|
|
642
|
+
fs2.readFileSync(rootPackageJsonPath, "utf-8")
|
|
643
|
+
);
|
|
644
|
+
const sourceDeps = rootPackageJson.dependencies ?? {};
|
|
645
|
+
const peerDeps = rootPackageJson.peerDependencies ?? {};
|
|
646
|
+
let distDependencies = {};
|
|
647
|
+
for (const dep of trackedDependencies) {
|
|
648
|
+
if (peerDeps[dep]) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (sourceDeps[dep]) {
|
|
652
|
+
distDependencies[dep] = sourceDeps[dep];
|
|
653
|
+
} else {
|
|
654
|
+
const dependencyPackageJsonPath = path2.resolve(
|
|
655
|
+
`./node_modules/${dep}/package.json`
|
|
656
|
+
);
|
|
657
|
+
if (fs2.existsSync(dependencyPackageJsonPath)) {
|
|
658
|
+
const dependencyPackageJson = JSON.parse(
|
|
659
|
+
fs2.readFileSync(dependencyPackageJsonPath, "utf-8")
|
|
660
|
+
);
|
|
661
|
+
distDependencies[dep] = `^${dependencyPackageJson.version}`;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (Object.keys(distDependencies).length === 0) {
|
|
666
|
+
distDependencies = void 0;
|
|
667
|
+
}
|
|
668
|
+
const distPackageJson = {
|
|
669
|
+
...rootPackageJson,
|
|
670
|
+
main: "index.js",
|
|
671
|
+
type: "commonjs",
|
|
672
|
+
devDependencies: void 0,
|
|
673
|
+
scripts: void 0,
|
|
674
|
+
dependencies: distDependencies,
|
|
675
|
+
keywords: [
|
|
676
|
+
.../* @__PURE__ */ new Set([...rootPackageJson.keywords ?? [], "node-red"])
|
|
677
|
+
],
|
|
678
|
+
"node-red": {
|
|
679
|
+
nodes: { nodes: "index.js" }
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
if (types && entryNames.length > 0) {
|
|
683
|
+
const userExports = rootPackageJson.exports;
|
|
684
|
+
if (userExports && Object.keys(userExports).length > 0) {
|
|
685
|
+
distPackageJson.exports = patchExportsWithTypes(
|
|
686
|
+
userExports,
|
|
687
|
+
entryNames
|
|
688
|
+
);
|
|
689
|
+
} else {
|
|
690
|
+
distPackageJson.exports = generateExports(entryNames, format);
|
|
691
|
+
if (entryNames.includes("index")) {
|
|
692
|
+
distPackageJson.types = "index.d.ts";
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (!fs2.existsSync(outDir)) {
|
|
697
|
+
fs2.mkdirSync(outDir, { recursive: true });
|
|
698
|
+
}
|
|
699
|
+
fs2.writeFileSync(
|
|
700
|
+
path2.join(outDir, "package.json"),
|
|
701
|
+
JSON.stringify(distPackageJson, null, 2)
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/vite/server/build.ts
|
|
708
|
+
async function build(serverOpts, buildContext) {
|
|
709
|
+
const {
|
|
710
|
+
srcDir = "./server",
|
|
711
|
+
entry = "index.ts",
|
|
712
|
+
format = "esm",
|
|
713
|
+
external = [],
|
|
714
|
+
bundled = [],
|
|
715
|
+
types = true,
|
|
716
|
+
nodeTarget = "node22"
|
|
717
|
+
} = serverOpts;
|
|
718
|
+
const entries = Array.isArray(entry) ? entry : [entry];
|
|
719
|
+
const resolvedSrcDir = path3.resolve(srcDir);
|
|
720
|
+
const entryPoints = {};
|
|
721
|
+
for (const entry2 of entries) {
|
|
722
|
+
const entryFilePath = path3.join(resolvedSrcDir, entry2);
|
|
723
|
+
if (fs3.existsSync(entryFilePath)) {
|
|
724
|
+
const fileName = entry2.replace(/\.ts$/, "");
|
|
725
|
+
entryPoints[fileName] = entryFilePath;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (Object.keys(entryPoints).length === 0) {
|
|
729
|
+
logger.warn("No server entry points found");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const isEsm = format === "esm";
|
|
733
|
+
const plugins = [
|
|
734
|
+
packageJsonGenerator({
|
|
735
|
+
outDir: buildContext.outDir,
|
|
736
|
+
bundled,
|
|
737
|
+
types: types && !buildContext.isDev,
|
|
738
|
+
entryNames: Object.keys(entryPoints),
|
|
739
|
+
format
|
|
740
|
+
}),
|
|
741
|
+
isEsm ? esmWrapper() : cjsWrapper()
|
|
742
|
+
];
|
|
743
|
+
if (types && !buildContext.isDev) {
|
|
744
|
+
plugins.push(
|
|
745
|
+
...typeGenerator({
|
|
746
|
+
srcDir: resolvedSrcDir,
|
|
747
|
+
outDir: buildContext.outDir,
|
|
748
|
+
entryFiles: Object.values(entryPoints)
|
|
749
|
+
})
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const config = {
|
|
753
|
+
configFile: false,
|
|
754
|
+
logLevel: "warn",
|
|
755
|
+
plugins,
|
|
756
|
+
build: {
|
|
757
|
+
outDir: buildContext.outDir,
|
|
758
|
+
emptyOutDir: false,
|
|
759
|
+
sourcemap: buildContext.isDev ? "inline" : true,
|
|
760
|
+
minify: !buildContext.isDev,
|
|
761
|
+
lib: {
|
|
762
|
+
entry: entryPoints,
|
|
763
|
+
formats: [isEsm ? "es" : "cjs"]
|
|
764
|
+
},
|
|
765
|
+
rollupOptions: {
|
|
766
|
+
external,
|
|
767
|
+
output: {
|
|
768
|
+
entryFileNames: isEsm ? "[name].mjs" : "[name].js",
|
|
769
|
+
exports: isEsm ? void 0 : "auto",
|
|
770
|
+
preserveModules: false
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
esbuild: {
|
|
775
|
+
platform: "node",
|
|
776
|
+
target: nodeTarget,
|
|
777
|
+
keepNames: true,
|
|
778
|
+
tsconfigRaw: "{}"
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
try {
|
|
782
|
+
await viteBuild(config);
|
|
783
|
+
if (isEsm) {
|
|
784
|
+
const bridgeCode = `'use strict';
|
|
785
|
+
// CJS bridge \u2014 auto-generated by @bonsae/nrg/vite
|
|
786
|
+
// Node-RED uses require() to load packages. This bridge delegates to the ESM bundle.
|
|
787
|
+
module.exports = function (RED) {
|
|
788
|
+
(async () => {
|
|
789
|
+
const mod = await import("./index.mjs");
|
|
790
|
+
if (typeof mod.default !== 'function') {
|
|
791
|
+
throw new Error('ESM bundle must export a default function(RED)');
|
|
792
|
+
}
|
|
793
|
+
await mod.default(RED);
|
|
794
|
+
})().catch(function (err) {
|
|
795
|
+
RED.log.error('Failed to load ESM bundle: ' + err.message);
|
|
796
|
+
});
|
|
797
|
+
};
|
|
798
|
+
`;
|
|
799
|
+
fs3.writeFileSync(path3.join(buildContext.outDir, "index.js"), bridgeCode);
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
throw new BuildError("server", error);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/vite/client/build.ts
|
|
807
|
+
import { build as viteBuild2 } from "vite";
|
|
808
|
+
import vue from "@vitejs/plugin-vue";
|
|
809
|
+
import fs9 from "fs";
|
|
810
|
+
import path9 from "path";
|
|
811
|
+
|
|
812
|
+
// src/vite/client/plugins/help-generator.ts
|
|
813
|
+
import fs4 from "fs";
|
|
814
|
+
import path4 from "path";
|
|
815
|
+
import { pathToFileURL } from "url";
|
|
816
|
+
import { createRequire } from "module";
|
|
817
|
+
|
|
818
|
+
// src/vite/client/plugins/help-i18n.ts
|
|
819
|
+
var translations = {
|
|
820
|
+
"en-US": {
|
|
821
|
+
sections: {
|
|
822
|
+
properties: "Properties",
|
|
823
|
+
credentials: "Credentials",
|
|
824
|
+
input: "Input",
|
|
825
|
+
output: "Output",
|
|
826
|
+
outputs: "Outputs",
|
|
827
|
+
port: "Port"
|
|
828
|
+
},
|
|
829
|
+
columns: {
|
|
830
|
+
property: "Property",
|
|
831
|
+
label: "Label",
|
|
832
|
+
type: "Type",
|
|
833
|
+
required: "Required",
|
|
834
|
+
default: "Default",
|
|
835
|
+
description: "Description"
|
|
836
|
+
},
|
|
837
|
+
values: {
|
|
838
|
+
yes: "Yes",
|
|
839
|
+
no: "No"
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
de: {
|
|
843
|
+
sections: {
|
|
844
|
+
properties: "Eigenschaften",
|
|
845
|
+
credentials: "Zugangsdaten",
|
|
846
|
+
input: "Eingang",
|
|
847
|
+
output: "Ausgang",
|
|
848
|
+
outputs: "Ausg\xE4nge",
|
|
849
|
+
port: "Port"
|
|
850
|
+
},
|
|
851
|
+
columns: {
|
|
852
|
+
property: "Eigenschaft",
|
|
853
|
+
label: "Bezeichnung",
|
|
854
|
+
type: "Typ",
|
|
855
|
+
required: "Erforderlich",
|
|
856
|
+
default: "Standard",
|
|
857
|
+
description: "Beschreibung"
|
|
858
|
+
},
|
|
859
|
+
values: {
|
|
860
|
+
yes: "Ja",
|
|
861
|
+
no: "Nein"
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
"es-ES": {
|
|
865
|
+
sections: {
|
|
866
|
+
properties: "Propiedades",
|
|
867
|
+
credentials: "Credenciales",
|
|
868
|
+
input: "Entrada",
|
|
869
|
+
output: "Salida",
|
|
870
|
+
outputs: "Salidas",
|
|
871
|
+
port: "Puerto"
|
|
872
|
+
},
|
|
873
|
+
columns: {
|
|
874
|
+
property: "Propiedad",
|
|
875
|
+
label: "Etiqueta",
|
|
876
|
+
type: "Tipo",
|
|
877
|
+
required: "Requerido",
|
|
878
|
+
default: "Predeterminado",
|
|
879
|
+
description: "Descripci\xF3n"
|
|
880
|
+
},
|
|
881
|
+
values: {
|
|
882
|
+
yes: "S\xED",
|
|
883
|
+
no: "No"
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
fr: {
|
|
887
|
+
sections: {
|
|
888
|
+
properties: "Propri\xE9t\xE9s",
|
|
889
|
+
credentials: "Identifiants",
|
|
890
|
+
input: "Entr\xE9e",
|
|
891
|
+
output: "Sortie",
|
|
892
|
+
outputs: "Sorties",
|
|
893
|
+
port: "Port"
|
|
894
|
+
},
|
|
895
|
+
columns: {
|
|
896
|
+
property: "Propri\xE9t\xE9",
|
|
897
|
+
label: "Libell\xE9",
|
|
898
|
+
type: "Type",
|
|
899
|
+
required: "Requis",
|
|
900
|
+
default: "Par d\xE9faut",
|
|
901
|
+
description: "Description"
|
|
902
|
+
},
|
|
903
|
+
values: {
|
|
904
|
+
yes: "Oui",
|
|
905
|
+
no: "Non"
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
ko: {
|
|
909
|
+
sections: {
|
|
910
|
+
properties: "\uC18D\uC131",
|
|
911
|
+
credentials: "\uC790\uACA9 \uC99D\uBA85",
|
|
912
|
+
input: "\uC785\uB825",
|
|
913
|
+
output: "\uCD9C\uB825",
|
|
914
|
+
outputs: "\uCD9C\uB825",
|
|
915
|
+
port: "\uD3EC\uD2B8"
|
|
916
|
+
},
|
|
917
|
+
columns: {
|
|
918
|
+
property: "\uC18D\uC131",
|
|
919
|
+
label: "\uB77C\uBCA8",
|
|
920
|
+
type: "\uC720\uD615",
|
|
921
|
+
required: "\uD544\uC218",
|
|
922
|
+
default: "\uAE30\uBCF8\uAC12",
|
|
923
|
+
description: "\uC124\uBA85"
|
|
924
|
+
},
|
|
925
|
+
values: {
|
|
926
|
+
yes: "\uC608",
|
|
927
|
+
no: "\uC544\uB2C8\uC624"
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
"pt-BR": {
|
|
931
|
+
sections: {
|
|
932
|
+
properties: "Propriedades",
|
|
933
|
+
credentials: "Credenciais",
|
|
934
|
+
input: "Entrada",
|
|
935
|
+
output: "Sa\xEDda",
|
|
936
|
+
outputs: "Sa\xEDdas",
|
|
937
|
+
port: "Porta"
|
|
938
|
+
},
|
|
939
|
+
columns: {
|
|
940
|
+
property: "Propriedade",
|
|
941
|
+
label: "R\xF3tulo",
|
|
942
|
+
type: "Tipo",
|
|
943
|
+
required: "Obrigat\xF3rio",
|
|
944
|
+
default: "Padr\xE3o",
|
|
945
|
+
description: "Descri\xE7\xE3o"
|
|
946
|
+
},
|
|
947
|
+
values: {
|
|
948
|
+
yes: "Sim",
|
|
949
|
+
no: "N\xE3o"
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
ru: {
|
|
953
|
+
sections: {
|
|
954
|
+
properties: "\u0421\u0432\u043E\u0439\u0441\u0442\u0432\u0430",
|
|
955
|
+
credentials: "\u0423\u0447\u0451\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435",
|
|
956
|
+
input: "\u0412\u0445\u043E\u0434",
|
|
957
|
+
output: "\u0412\u044B\u0445\u043E\u0434",
|
|
958
|
+
outputs: "\u0412\u044B\u0445\u043E\u0434\u044B",
|
|
959
|
+
port: "\u041F\u043E\u0440\u0442"
|
|
960
|
+
},
|
|
961
|
+
columns: {
|
|
962
|
+
property: "\u0421\u0432\u043E\u0439\u0441\u0442\u0432\u043E",
|
|
963
|
+
label: "\u041C\u0435\u0442\u043A\u0430",
|
|
964
|
+
type: "\u0422\u0438\u043F",
|
|
965
|
+
required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E",
|
|
966
|
+
default: "\u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E",
|
|
967
|
+
description: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435"
|
|
968
|
+
},
|
|
969
|
+
values: {
|
|
970
|
+
yes: "\u0414\u0430",
|
|
971
|
+
no: "\u041D\u0435\u0442"
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
ja: {
|
|
975
|
+
sections: {
|
|
976
|
+
properties: "\u30D7\u30ED\u30D1\u30C6\u30A3",
|
|
977
|
+
credentials: "\u8A8D\u8A3C\u60C5\u5831",
|
|
978
|
+
input: "\u5165\u529B",
|
|
979
|
+
output: "\u51FA\u529B",
|
|
980
|
+
outputs: "\u51FA\u529B",
|
|
981
|
+
port: "\u30DD\u30FC\u30C8"
|
|
982
|
+
},
|
|
983
|
+
columns: {
|
|
984
|
+
property: "\u30D7\u30ED\u30D1\u30C6\u30A3",
|
|
985
|
+
label: "\u30E9\u30D9\u30EB",
|
|
986
|
+
type: "\u578B",
|
|
987
|
+
required: "\u5FC5\u9808",
|
|
988
|
+
default: "\u30C7\u30D5\u30A9\u30EB\u30C8",
|
|
989
|
+
description: "\u8AAC\u660E"
|
|
990
|
+
},
|
|
991
|
+
values: {
|
|
992
|
+
yes: "\u306F\u3044",
|
|
993
|
+
no: "\u3044\u3044\u3048"
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
"zh-CN": {
|
|
997
|
+
sections: {
|
|
998
|
+
properties: "\u5C5E\u6027",
|
|
999
|
+
credentials: "\u51ED\u8BC1",
|
|
1000
|
+
input: "\u8F93\u5165",
|
|
1001
|
+
output: "\u8F93\u51FA",
|
|
1002
|
+
outputs: "\u8F93\u51FA",
|
|
1003
|
+
port: "\u7AEF\u53E3"
|
|
1004
|
+
},
|
|
1005
|
+
columns: {
|
|
1006
|
+
property: "\u5C5E\u6027",
|
|
1007
|
+
label: "\u6807\u7B7E",
|
|
1008
|
+
type: "\u7C7B\u578B",
|
|
1009
|
+
required: "\u5FC5\u586B",
|
|
1010
|
+
default: "\u9ED8\u8BA4\u503C",
|
|
1011
|
+
description: "\u63CF\u8FF0"
|
|
1012
|
+
},
|
|
1013
|
+
values: {
|
|
1014
|
+
yes: "\u662F",
|
|
1015
|
+
no: "\u5426"
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
"zh-TW": {
|
|
1019
|
+
sections: {
|
|
1020
|
+
properties: "\u5C6C\u6027",
|
|
1021
|
+
credentials: "\u6191\u8B49",
|
|
1022
|
+
input: "\u8F38\u5165",
|
|
1023
|
+
output: "\u8F38\u51FA",
|
|
1024
|
+
outputs: "\u8F38\u51FA",
|
|
1025
|
+
port: "\u57E0"
|
|
1026
|
+
},
|
|
1027
|
+
columns: {
|
|
1028
|
+
property: "\u5C6C\u6027",
|
|
1029
|
+
label: "\u6A19\u7C64",
|
|
1030
|
+
type: "\u985E\u578B",
|
|
1031
|
+
required: "\u5FC5\u586B",
|
|
1032
|
+
default: "\u9810\u8A2D\u503C",
|
|
1033
|
+
description: "\u63CF\u8FF0"
|
|
1034
|
+
},
|
|
1035
|
+
values: {
|
|
1036
|
+
yes: "\u662F",
|
|
1037
|
+
no: "\u5426"
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
function getHelpTranslations(lang) {
|
|
1042
|
+
return translations[lang] ?? translations["en-US"];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/vite/client/plugins/help-generator.ts
|
|
1046
|
+
function buildPropertyRow(name, schema, required, label) {
|
|
1047
|
+
let type = "";
|
|
1048
|
+
if (schema["x-nrg-node-type"]) {
|
|
1049
|
+
type = `NodeRef \u2192 ${schema["x-nrg-node-type"]}`;
|
|
1050
|
+
} else if (schema["x-nrg-typed-input"]) {
|
|
1051
|
+
type = "TypedInput";
|
|
1052
|
+
} else if (schema.type) {
|
|
1053
|
+
type = String(schema.type);
|
|
1054
|
+
}
|
|
1055
|
+
if (schema.enum) type += ` (${schema.enum.join(", ")})`;
|
|
1056
|
+
const constraints = [];
|
|
1057
|
+
if (schema.minLength !== void 0)
|
|
1058
|
+
constraints.push(`min: ${schema.minLength}`);
|
|
1059
|
+
if (schema.maxLength !== void 0)
|
|
1060
|
+
constraints.push(`max: ${schema.maxLength}`);
|
|
1061
|
+
if (schema.minimum !== void 0) constraints.push(`min: ${schema.minimum}`);
|
|
1062
|
+
if (schema.maximum !== void 0) constraints.push(`max: ${schema.maximum}`);
|
|
1063
|
+
if (schema.pattern) constraints.push(`pattern: \`${schema.pattern}\``);
|
|
1064
|
+
if (schema.format && schema.format !== "password")
|
|
1065
|
+
constraints.push(`format: ${schema.format}`);
|
|
1066
|
+
if (constraints.length) type += ` [${constraints.join(", ")}]`;
|
|
1067
|
+
const defaultVal = schema.default !== void 0 ? JSON.stringify(schema.default) : "";
|
|
1068
|
+
const description = schema.description ?? "";
|
|
1069
|
+
return { name, label: label ?? "", type, required, defaultVal, description };
|
|
1070
|
+
}
|
|
1071
|
+
var SKIP_FIELDS = /* @__PURE__ */ new Set([
|
|
1072
|
+
"id",
|
|
1073
|
+
"type",
|
|
1074
|
+
"z",
|
|
1075
|
+
"name",
|
|
1076
|
+
"wires",
|
|
1077
|
+
"x",
|
|
1078
|
+
"y",
|
|
1079
|
+
"g",
|
|
1080
|
+
"_users"
|
|
1081
|
+
]);
|
|
1082
|
+
function generateSchemaSection(options) {
|
|
1083
|
+
const {
|
|
1084
|
+
title,
|
|
1085
|
+
schema,
|
|
1086
|
+
t,
|
|
1087
|
+
labels,
|
|
1088
|
+
heading = "###",
|
|
1089
|
+
includeDefault = true
|
|
1090
|
+
} = options;
|
|
1091
|
+
if (!schema?.properties) return "";
|
|
1092
|
+
const required = new Set(schema.required ?? []);
|
|
1093
|
+
const rows = Object.entries(schema.properties).filter(([key]) => !SKIP_FIELDS.has(key)).map(
|
|
1094
|
+
([key, propSchema]) => buildPropertyRow(
|
|
1095
|
+
key,
|
|
1096
|
+
propSchema,
|
|
1097
|
+
required.has(key),
|
|
1098
|
+
labels?.[key]
|
|
1099
|
+
)
|
|
1100
|
+
);
|
|
1101
|
+
if (rows.length === 0) return "";
|
|
1102
|
+
const hasLabels = rows.some((r) => r.label);
|
|
1103
|
+
const c = t.columns;
|
|
1104
|
+
const v = t.values;
|
|
1105
|
+
let headerCells;
|
|
1106
|
+
let rowFn;
|
|
1107
|
+
if (hasLabels && includeDefault) {
|
|
1108
|
+
headerCells = `<th>${c.label}</th><th>${c.property}</th><th>${c.type}</th><th>${c.required}</th><th>${c.default}</th><th style="width:35%">${c.description}</th>`;
|
|
1109
|
+
rowFn = (r) => `<tr><td>${r.label}</td><td>${r.name}</td><td>${r.type}</td><td>${r.required ? v.yes : v.no}</td><td>${r.defaultVal ? `<code>${r.defaultVal}</code>` : ""}</td><td>${r.description}</td></tr>`;
|
|
1110
|
+
} else if (hasLabels) {
|
|
1111
|
+
headerCells = `<th>${c.label}</th><th>${c.property}</th><th>${c.type}</th><th>${c.required}</th><th style="width:35%">${c.description}</th>`;
|
|
1112
|
+
rowFn = (r) => `<tr><td>${r.label}</td><td>${r.name}</td><td>${r.type}</td><td>${r.required ? v.yes : v.no}</td><td>${r.description}</td></tr>`;
|
|
1113
|
+
} else if (includeDefault) {
|
|
1114
|
+
headerCells = `<th>${c.property}</th><th>${c.type}</th><th>${c.required}</th><th>${c.default}</th><th style="width:40%">${c.description}</th>`;
|
|
1115
|
+
rowFn = (r) => `<tr><td>${r.name}</td><td>${r.type}</td><td>${r.required ? v.yes : v.no}</td><td>${r.defaultVal ? `<code>${r.defaultVal}</code>` : ""}</td><td>${r.description}</td></tr>`;
|
|
1116
|
+
} else {
|
|
1117
|
+
headerCells = `<th>${c.property}</th><th>${c.type}</th><th>${c.required}</th><th style="width:40%">${c.description}</th>`;
|
|
1118
|
+
rowFn = (r) => `<tr><td>${r.name}</td><td>${r.type}</td><td>${r.required ? v.yes : v.no}</td><td>${r.description}</td></tr>`;
|
|
1119
|
+
}
|
|
1120
|
+
const tableRows = rows.map(rowFn).join("\n");
|
|
1121
|
+
const table = `<div style="overflow-x:auto">
|
|
1122
|
+
<table width="100%" style="min-width:500px">
|
|
1123
|
+
<thead><tr>${headerCells}</tr></thead>
|
|
1124
|
+
<tbody>
|
|
1125
|
+
${tableRows}
|
|
1126
|
+
</tbody>
|
|
1127
|
+
</table>
|
|
1128
|
+
</div>`;
|
|
1129
|
+
const headingLevel = heading.length;
|
|
1130
|
+
const tag = `h${headingLevel}`;
|
|
1131
|
+
return `<${tag}>${title}</${tag}>
|
|
1132
|
+
${table}
|
|
1133
|
+
`;
|
|
1134
|
+
}
|
|
1135
|
+
function loadNodeLabels(labelPath) {
|
|
1136
|
+
if (!fs4.existsSync(labelPath)) return {};
|
|
1137
|
+
try {
|
|
1138
|
+
const raw = JSON.parse(fs4.readFileSync(labelPath, "utf-8"));
|
|
1139
|
+
return {
|
|
1140
|
+
description: raw.description,
|
|
1141
|
+
configs: raw.configs,
|
|
1142
|
+
credentials: raw.credentials,
|
|
1143
|
+
input: raw.input,
|
|
1144
|
+
outputs: raw.outputs
|
|
1145
|
+
};
|
|
1146
|
+
} catch {
|
|
1147
|
+
return {};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function generateHelpDoc(nodeClass, labels, t) {
|
|
1151
|
+
const lines = [];
|
|
1152
|
+
if (labels.description) {
|
|
1153
|
+
lines.push(`<p>${labels.description}</p>`);
|
|
1154
|
+
}
|
|
1155
|
+
const configSection = generateSchemaSection({
|
|
1156
|
+
title: t.sections.properties,
|
|
1157
|
+
schema: nodeClass.configSchema,
|
|
1158
|
+
t,
|
|
1159
|
+
labels: labels.configs
|
|
1160
|
+
});
|
|
1161
|
+
if (configSection) lines.push(configSection);
|
|
1162
|
+
const credsSection = generateSchemaSection({
|
|
1163
|
+
title: t.sections.credentials,
|
|
1164
|
+
schema: nodeClass.credentialsSchema,
|
|
1165
|
+
t,
|
|
1166
|
+
labels: labels.credentials
|
|
1167
|
+
});
|
|
1168
|
+
if (credsSection) lines.push(credsSection);
|
|
1169
|
+
if (nodeClass.inputSchema) {
|
|
1170
|
+
const inputSection = generateSchemaSection({
|
|
1171
|
+
title: t.sections.input,
|
|
1172
|
+
schema: nodeClass.inputSchema,
|
|
1173
|
+
t,
|
|
1174
|
+
labels: labels.input,
|
|
1175
|
+
includeDefault: false
|
|
1176
|
+
});
|
|
1177
|
+
if (inputSection) lines.push(inputSection);
|
|
1178
|
+
}
|
|
1179
|
+
if (nodeClass.outputsSchema) {
|
|
1180
|
+
const os2 = nodeClass.outputsSchema;
|
|
1181
|
+
if (Array.isArray(os2)) {
|
|
1182
|
+
const portSections = [];
|
|
1183
|
+
os2.forEach((schema, i) => {
|
|
1184
|
+
const title = `${t.sections.port} ${i + 1}`;
|
|
1185
|
+
const portPropLabels = labels.outputs?.[i];
|
|
1186
|
+
const section = generateSchemaSection({
|
|
1187
|
+
title,
|
|
1188
|
+
schema,
|
|
1189
|
+
t,
|
|
1190
|
+
labels: portPropLabels,
|
|
1191
|
+
heading: "####",
|
|
1192
|
+
includeDefault: false
|
|
1193
|
+
});
|
|
1194
|
+
if (section) portSections.push(section);
|
|
1195
|
+
});
|
|
1196
|
+
if (portSections.length) {
|
|
1197
|
+
lines.push(
|
|
1198
|
+
`<h3>${t.sections.outputs}</h3>
|
|
1199
|
+
${portSections.join("\n")}`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
} else if (!("type" in os2 || "properties" in os2)) {
|
|
1203
|
+
const portSections = [];
|
|
1204
|
+
for (const [portName, schema] of Object.entries(os2)) {
|
|
1205
|
+
const portPropLabels = labels.outputs?.[portName];
|
|
1206
|
+
const section = generateSchemaSection({
|
|
1207
|
+
title: portName,
|
|
1208
|
+
schema,
|
|
1209
|
+
t,
|
|
1210
|
+
labels: portPropLabels,
|
|
1211
|
+
heading: "####",
|
|
1212
|
+
includeDefault: false
|
|
1213
|
+
});
|
|
1214
|
+
if (section) portSections.push(section);
|
|
1215
|
+
}
|
|
1216
|
+
if (portSections.length) {
|
|
1217
|
+
lines.push(
|
|
1218
|
+
`<h3>${t.sections.outputs}</h3>
|
|
1219
|
+
${portSections.join("\n")}`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
} else {
|
|
1223
|
+
const outputPropLabels = labels.outputs?.[0];
|
|
1224
|
+
const section = generateSchemaSection({
|
|
1225
|
+
title: t.sections.output,
|
|
1226
|
+
schema: os2,
|
|
1227
|
+
t,
|
|
1228
|
+
labels: outputPropLabels,
|
|
1229
|
+
includeDefault: false
|
|
1230
|
+
});
|
|
1231
|
+
if (section) lines.push(section);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return lines.join("\n").trim();
|
|
1235
|
+
}
|
|
1236
|
+
function discoverLanguages(labelsDir, nodeType) {
|
|
1237
|
+
const nodeLabelsDir = path4.join(labelsDir, nodeType);
|
|
1238
|
+
if (!fs4.existsSync(nodeLabelsDir)) return [];
|
|
1239
|
+
return fs4.readdirSync(nodeLabelsDir).filter((f) => f.endsWith(".json")).map((f) => path4.basename(f, ".json"));
|
|
1240
|
+
}
|
|
1241
|
+
function helpGenerator(options) {
|
|
1242
|
+
const { outDir, localesOutDir, docsDir, labelsDir } = options;
|
|
1243
|
+
return {
|
|
1244
|
+
name: "vite-plugin-node-red:client:help-generator",
|
|
1245
|
+
apply: "build",
|
|
1246
|
+
enforce: "post",
|
|
1247
|
+
async closeBundle() {
|
|
1248
|
+
const esmPath = path4.resolve(outDir, "index.mjs");
|
|
1249
|
+
const cjsPath = path4.resolve(outDir, "index.js");
|
|
1250
|
+
let packageFn;
|
|
1251
|
+
try {
|
|
1252
|
+
if (fs4.existsSync(esmPath)) {
|
|
1253
|
+
const fileUrl = pathToFileURL(esmPath).href + `?t=${Date.now()}`;
|
|
1254
|
+
const mod = await import(fileUrl);
|
|
1255
|
+
packageFn = mod?.default ?? mod;
|
|
1256
|
+
} else if (fs4.existsSync(cjsPath)) {
|
|
1257
|
+
const require2 = createRequire(import.meta.url);
|
|
1258
|
+
delete require2.cache[cjsPath];
|
|
1259
|
+
const rawMod = require2(cjsPath);
|
|
1260
|
+
packageFn = rawMod?.default ?? rawMod;
|
|
1261
|
+
}
|
|
1262
|
+
} catch {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const nodeClasses = packageFn?.nodes ?? [];
|
|
1266
|
+
const helpByLang = /* @__PURE__ */ new Map();
|
|
1267
|
+
for (const NodeClass of nodeClasses) {
|
|
1268
|
+
const type = NodeClass.type;
|
|
1269
|
+
if (!type) continue;
|
|
1270
|
+
const languages = discoverLanguages(labelsDir, type);
|
|
1271
|
+
if (!languages.includes("en-US")) languages.push("en-US");
|
|
1272
|
+
for (const lang of languages) {
|
|
1273
|
+
const manualMd = path4.join(docsDir, type, `${lang}.md`);
|
|
1274
|
+
const manualHtml = path4.join(docsDir, type, `${lang}.html`);
|
|
1275
|
+
if (fs4.existsSync(manualMd) || fs4.existsSync(manualHtml)) continue;
|
|
1276
|
+
const labelPath = path4.join(labelsDir, type, `${lang}.json`);
|
|
1277
|
+
const labels = loadNodeLabels(labelPath);
|
|
1278
|
+
const t = getHelpTranslations(lang);
|
|
1279
|
+
const content = generateHelpDoc(NodeClass, labels, t);
|
|
1280
|
+
if (!content) continue;
|
|
1281
|
+
if (!helpByLang.has(lang)) helpByLang.set(lang, []);
|
|
1282
|
+
helpByLang.get(lang).push(
|
|
1283
|
+
`<script type="text/html" data-help-name="${type}">
|
|
1284
|
+
${content}
|
|
1285
|
+
</script>`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
for (const [lang, scripts] of helpByLang) {
|
|
1290
|
+
const langDir = path4.join(localesOutDir, lang);
|
|
1291
|
+
fs4.mkdirSync(langDir, { recursive: true });
|
|
1292
|
+
const indexPath = path4.join(langDir, "index.html");
|
|
1293
|
+
const existing = fs4.existsSync(indexPath) ? fs4.readFileSync(indexPath, "utf-8") : "";
|
|
1294
|
+
fs4.writeFileSync(
|
|
1295
|
+
indexPath,
|
|
1296
|
+
existing + (existing ? "\n" : "") + scripts.join("\n"),
|
|
1297
|
+
"utf-8"
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/vite/client/plugins/html-generator.ts
|
|
1305
|
+
import mime from "mime-types";
|
|
1306
|
+
import fs5 from "fs";
|
|
1307
|
+
import path5 from "path";
|
|
1308
|
+
function htmlGenerator(options) {
|
|
1309
|
+
const { packageName, licensePath } = options;
|
|
1310
|
+
return {
|
|
1311
|
+
name: "vite-plugin-node-red:client:html-generator",
|
|
1312
|
+
apply: "build",
|
|
1313
|
+
enforce: "post",
|
|
1314
|
+
generateBundle(_, bundle) {
|
|
1315
|
+
const resourcesTags = Object.keys(bundle).map((fileName) => {
|
|
1316
|
+
const asset = bundle[fileName];
|
|
1317
|
+
const srcPath = path5.join(
|
|
1318
|
+
"resources",
|
|
1319
|
+
packageName,
|
|
1320
|
+
fileName.replace(/^resources\/?/, "")
|
|
1321
|
+
);
|
|
1322
|
+
const content = asset.type === "asset" ? asset.source : asset.type === "chunk" ? asset.code : null;
|
|
1323
|
+
if (typeof content !== "string" && !(content instanceof Uint8Array))
|
|
1324
|
+
return null;
|
|
1325
|
+
const mimeType = mime.lookup(fileName);
|
|
1326
|
+
switch (mimeType) {
|
|
1327
|
+
case "application/javascript":
|
|
1328
|
+
case "text/javascript":
|
|
1329
|
+
return `<script type="module" src="${srcPath}" defer></script>`;
|
|
1330
|
+
case "text/css":
|
|
1331
|
+
return `<link rel="stylesheet" href="${srcPath}">`;
|
|
1332
|
+
case "font/woff":
|
|
1333
|
+
case "font/woff2":
|
|
1334
|
+
case "application/font-woff":
|
|
1335
|
+
case "application/font-woff2":
|
|
1336
|
+
case "application/x-font-ttf":
|
|
1337
|
+
case "application/x-font-opentype":
|
|
1338
|
+
case "font/ttf":
|
|
1339
|
+
case "font/otf":
|
|
1340
|
+
return `<link rel="preload" as="font" href="${srcPath}" type="${mimeType}">`;
|
|
1341
|
+
default:
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
}).filter(Boolean).join("\n");
|
|
1345
|
+
const licenseBanner = licensePath && fs5.existsSync(licensePath) ? `<!--
|
|
1346
|
+
${fs5.readFileSync(licensePath, "utf-8")}
|
|
1347
|
+
-->` : "";
|
|
1348
|
+
this.emitFile({
|
|
1349
|
+
type: "asset",
|
|
1350
|
+
fileName: "index.html",
|
|
1351
|
+
source: `${licenseBanner}
|
|
1352
|
+
${resourcesTags}`
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/vite/client/plugins/locales-generator.ts
|
|
1359
|
+
import fs6 from "fs";
|
|
1360
|
+
import path6 from "path";
|
|
1361
|
+
import { merge } from "es-toolkit";
|
|
1362
|
+
function localesGenerator(options) {
|
|
1363
|
+
const { outDir, docsDir, labelsDir } = options;
|
|
1364
|
+
const languages = [
|
|
1365
|
+
"en-US",
|
|
1366
|
+
"de",
|
|
1367
|
+
"es-ES",
|
|
1368
|
+
"fr",
|
|
1369
|
+
"ko",
|
|
1370
|
+
"pt-BR",
|
|
1371
|
+
"ru",
|
|
1372
|
+
"ja",
|
|
1373
|
+
"zh-CN",
|
|
1374
|
+
"zh-TW"
|
|
1375
|
+
];
|
|
1376
|
+
const frameworkLabels = {
|
|
1377
|
+
"en-US": {
|
|
1378
|
+
configs: { name: "Name" },
|
|
1379
|
+
toggles: {
|
|
1380
|
+
validateInput: "Validate Input",
|
|
1381
|
+
validateOutput: "Validate Output",
|
|
1382
|
+
errorPort: "Error Port",
|
|
1383
|
+
completePort: "Complete Port",
|
|
1384
|
+
statusPort: "Status Port"
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
de: {
|
|
1388
|
+
configs: { name: "Name" },
|
|
1389
|
+
toggles: {
|
|
1390
|
+
validateInput: "Eingabe validieren",
|
|
1391
|
+
validateOutput: "Ausgabe validieren",
|
|
1392
|
+
errorPort: "Fehler-Port",
|
|
1393
|
+
completePort: "Abschluss-Port",
|
|
1394
|
+
statusPort: "Status-Port"
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
"es-ES": {
|
|
1398
|
+
configs: { name: "Nombre" },
|
|
1399
|
+
toggles: {
|
|
1400
|
+
validateInput: "Validar entrada",
|
|
1401
|
+
validateOutput: "Validar salida",
|
|
1402
|
+
errorPort: "Puerto de error",
|
|
1403
|
+
completePort: "Puerto de completado",
|
|
1404
|
+
statusPort: "Puerto de estado"
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
1407
|
+
fr: {
|
|
1408
|
+
configs: { name: "Nom" },
|
|
1409
|
+
toggles: {
|
|
1410
|
+
validateInput: "Valider l'entr\xE9e",
|
|
1411
|
+
validateOutput: "Valider la sortie",
|
|
1412
|
+
errorPort: "Port d'erreur",
|
|
1413
|
+
completePort: "Port de compl\xE9tion",
|
|
1414
|
+
statusPort: "Port de statut"
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
ko: {
|
|
1418
|
+
configs: { name: "\uC774\uB984" },
|
|
1419
|
+
toggles: {
|
|
1420
|
+
validateInput: "\uC785\uB825 \uAC80\uC99D",
|
|
1421
|
+
validateOutput: "\uCD9C\uB825 \uAC80\uC99D",
|
|
1422
|
+
errorPort: "\uC624\uB958 \uD3EC\uD2B8",
|
|
1423
|
+
completePort: "\uC644\uB8CC \uD3EC\uD2B8",
|
|
1424
|
+
statusPort: "\uC0C1\uD0DC \uD3EC\uD2B8"
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
"pt-BR": {
|
|
1428
|
+
configs: { name: "Nome" },
|
|
1429
|
+
toggles: {
|
|
1430
|
+
validateInput: "Validar Entrada",
|
|
1431
|
+
validateOutput: "Validar Sa\xEDda",
|
|
1432
|
+
errorPort: "Porta de Erro",
|
|
1433
|
+
completePort: "Porta de Conclus\xE3o",
|
|
1434
|
+
statusPort: "Porta de Status"
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
ru: {
|
|
1438
|
+
configs: { name: "\u0418\u043C\u044F" },
|
|
1439
|
+
toggles: {
|
|
1440
|
+
validateInput: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u0445\u043E\u0434",
|
|
1441
|
+
validateOutput: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u044B\u0445\u043E\u0434",
|
|
1442
|
+
errorPort: "\u041F\u043E\u0440\u0442 \u043E\u0448\u0438\u0431\u043A\u0438",
|
|
1443
|
+
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"
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
ja: {
|
|
1448
|
+
configs: { name: "\u540D\u524D" },
|
|
1449
|
+
toggles: {
|
|
1450
|
+
validateInput: "\u5165\u529B\u691C\u8A3C",
|
|
1451
|
+
validateOutput: "\u51FA\u529B\u691C\u8A3C",
|
|
1452
|
+
errorPort: "\u30A8\u30E9\u30FC\u30DD\u30FC\u30C8",
|
|
1453
|
+
completePort: "\u5B8C\u4E86\u30DD\u30FC\u30C8",
|
|
1454
|
+
statusPort: "\u30B9\u30C6\u30FC\u30BF\u30B9\u30DD\u30FC\u30C8"
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
"zh-CN": {
|
|
1458
|
+
configs: { name: "\u540D\u79F0" },
|
|
1459
|
+
toggles: {
|
|
1460
|
+
validateInput: "\u9A8C\u8BC1\u8F93\u5165",
|
|
1461
|
+
validateOutput: "\u9A8C\u8BC1\u8F93\u51FA",
|
|
1462
|
+
errorPort: "\u9519\u8BEF\u7AEF\u53E3",
|
|
1463
|
+
completePort: "\u5B8C\u6210\u7AEF\u53E3",
|
|
1464
|
+
statusPort: "\u72B6\u6001\u7AEF\u53E3"
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
"zh-TW": {
|
|
1468
|
+
configs: { name: "\u540D\u7A31" },
|
|
1469
|
+
toggles: {
|
|
1470
|
+
validateInput: "\u9A57\u8B49\u8F38\u5165",
|
|
1471
|
+
validateOutput: "\u9A57\u8B49\u8F38\u51FA",
|
|
1472
|
+
errorPort: "\u932F\u8AA4\u7AEF\u53E3",
|
|
1473
|
+
completePort: "\u5B8C\u6210\u7AEF\u53E3",
|
|
1474
|
+
statusPort: "\u72C0\u614B\u7AEF\u53E3"
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
return {
|
|
1479
|
+
name: "vite-plugin-node-red:client:locales-generator",
|
|
1480
|
+
apply: "build",
|
|
1481
|
+
enforce: "post",
|
|
1482
|
+
closeBundle() {
|
|
1483
|
+
function validateLanguage(lang, filePath) {
|
|
1484
|
+
if (!languages.includes(lang)) {
|
|
1485
|
+
throw new Error(
|
|
1486
|
+
`[locales] Invalid language "${lang}" in "${filePath}".
|
|
1487
|
+
Supported: ${languages.join(", ")}`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function forEachFile(baseDir, fileExtensions, processFile) {
|
|
1492
|
+
const langMap = /* @__PURE__ */ new Map();
|
|
1493
|
+
if (!fs6.existsSync(baseDir)) return langMap;
|
|
1494
|
+
const nodeDirs = fs6.readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
1495
|
+
for (const nodeDir of nodeDirs) {
|
|
1496
|
+
const nodeType = nodeDir.name;
|
|
1497
|
+
const nodePath = path6.join(baseDir, nodeType);
|
|
1498
|
+
const files = fs6.readdirSync(nodePath);
|
|
1499
|
+
for (const file of files) {
|
|
1500
|
+
const ext = path6.extname(file);
|
|
1501
|
+
if (!fileExtensions.includes(ext)) continue;
|
|
1502
|
+
const lang = path6.basename(file, ext);
|
|
1503
|
+
const filePath = path6.join(nodePath, file);
|
|
1504
|
+
validateLanguage(lang, filePath);
|
|
1505
|
+
const value = processFile({ ext, filePath, nodeType });
|
|
1506
|
+
if (value == null) continue;
|
|
1507
|
+
if (!langMap.has(lang)) {
|
|
1508
|
+
langMap.set(lang, Array.isArray(value) ? [] : {});
|
|
1509
|
+
}
|
|
1510
|
+
if (Array.isArray(value)) {
|
|
1511
|
+
langMap.get(lang).push(...value);
|
|
1512
|
+
} else {
|
|
1513
|
+
langMap.get(lang)[nodeType] = value;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return langMap;
|
|
1518
|
+
}
|
|
1519
|
+
function writeOutput(langMap, fileName, serialize) {
|
|
1520
|
+
for (const [lang, data] of langMap.entries()) {
|
|
1521
|
+
const langOutDir = path6.join(outDir, lang);
|
|
1522
|
+
fs6.mkdirSync(langOutDir, { recursive: true });
|
|
1523
|
+
fs6.writeFileSync(
|
|
1524
|
+
path6.join(langOutDir, fileName),
|
|
1525
|
+
serialize(data),
|
|
1526
|
+
"utf-8"
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const docLangs = forEachFile(
|
|
1531
|
+
docsDir,
|
|
1532
|
+
[".html", ".md"],
|
|
1533
|
+
({ ext, filePath, nodeType }) => {
|
|
1534
|
+
const type = ext === ".html" ? "text/html" : ext === ".md" ? "text/markdown" : null;
|
|
1535
|
+
if (!type) return null;
|
|
1536
|
+
const content = fs6.readFileSync(filePath, "utf-8");
|
|
1537
|
+
return [
|
|
1538
|
+
`<script type="${type}" data-help-name="${nodeType}">
|
|
1539
|
+
${content}
|
|
1540
|
+
</script>`
|
|
1541
|
+
];
|
|
1542
|
+
}
|
|
1543
|
+
);
|
|
1544
|
+
writeOutput(
|
|
1545
|
+
docLangs,
|
|
1546
|
+
"index.html",
|
|
1547
|
+
(value) => value.join("\n")
|
|
1548
|
+
);
|
|
1549
|
+
const labelLangs = forEachFile(
|
|
1550
|
+
labelsDir,
|
|
1551
|
+
[".json"],
|
|
1552
|
+
({ filePath, nodeType }) => {
|
|
1553
|
+
const parsed = JSON.parse(fs6.readFileSync(filePath, "utf-8"));
|
|
1554
|
+
if (parsed[nodeType] && typeof parsed[nodeType] === "object") {
|
|
1555
|
+
console.warn(
|
|
1556
|
+
`[locales] Warning: "${filePath}" uses nested format (root key "${nodeType}"). Label files should be flat \u2014 the node type is added automatically. See https://bonsaedev.github.io/nrg/guide/building-and-running`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
return parsed;
|
|
1560
|
+
}
|
|
1561
|
+
);
|
|
1562
|
+
for (const [lang, nodeTypes] of labelLangs.entries()) {
|
|
1563
|
+
const defaults = frameworkLabels[lang] ?? frameworkLabels["en-US"];
|
|
1564
|
+
for (const nodeType of Object.keys(nodeTypes)) {
|
|
1565
|
+
nodeTypes[nodeType] = merge(
|
|
1566
|
+
structuredClone(defaults),
|
|
1567
|
+
nodeTypes[nodeType]
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
writeOutput(
|
|
1572
|
+
labelLangs,
|
|
1573
|
+
"index.json",
|
|
1574
|
+
(value) => JSON.stringify(value, null, 2)
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/vite/client/plugins/minifier.ts
|
|
1581
|
+
import { transform } from "esbuild";
|
|
1582
|
+
function minifier() {
|
|
1583
|
+
return {
|
|
1584
|
+
name: "vite-plugin-node-red:client:minifier",
|
|
1585
|
+
apply: "build",
|
|
1586
|
+
async generateBundle(_options, bundle) {
|
|
1587
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
1588
|
+
if (chunk.type === "chunk" && fileName.endsWith(".js")) {
|
|
1589
|
+
const result = await transform(chunk.code, {
|
|
1590
|
+
minify: true
|
|
1591
|
+
});
|
|
1592
|
+
chunk.code = result.code;
|
|
1593
|
+
chunk.map = null;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/vite/client/plugins/node-definitions-inliner.ts
|
|
1601
|
+
import { createRequire as createRequire2 } from "module";
|
|
1602
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1603
|
+
import path7 from "path";
|
|
1604
|
+
import fs7 from "fs";
|
|
1605
|
+
import mime2 from "mime-types";
|
|
1606
|
+
var VIRTUAL_ID = "virtual:nrg/node-definitions";
|
|
1607
|
+
var RESOLVED_ID = "\0" + VIRTUAL_ID;
|
|
1608
|
+
var SKIP_DEFAULTS = /* @__PURE__ */ new Set(["x", "y", "z", "g", "wires", "type", "id"]);
|
|
1609
|
+
function getDefaultsFromSchema(schema) {
|
|
1610
|
+
if (!schema?.properties) return void 0;
|
|
1611
|
+
const result = {};
|
|
1612
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1613
|
+
if (SKIP_DEFAULTS.has(key)) continue;
|
|
1614
|
+
result[key] = {
|
|
1615
|
+
required: false,
|
|
1616
|
+
value: prop.default ?? void 0,
|
|
1617
|
+
type: prop["x-nrg-node-type"]
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
return result;
|
|
1621
|
+
}
|
|
1622
|
+
function getCredentialsFromSchema(schema) {
|
|
1623
|
+
if (!schema?.properties) return void 0;
|
|
1624
|
+
const result = {};
|
|
1625
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
1626
|
+
result[key] = {
|
|
1627
|
+
required: false,
|
|
1628
|
+
type: prop.format === "password" ? "password" : "text",
|
|
1629
|
+
value: prop.default ?? void 0
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
return result;
|
|
1633
|
+
}
|
|
1634
|
+
function resolveIcon(iconsDir, type) {
|
|
1635
|
+
if (!fs7.existsSync(iconsDir)) return void 0;
|
|
1636
|
+
return fs7.readdirSync(iconsDir).find((f) => {
|
|
1637
|
+
if (path7.basename(f, path7.extname(f)) !== type) return false;
|
|
1638
|
+
const mimeType = mime2.lookup(f);
|
|
1639
|
+
return mimeType !== false && mimeType.startsWith("image/");
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir, nodesDir, hasUserEntry = true) {
|
|
1643
|
+
let _nodeTypes = [];
|
|
1644
|
+
let _definitions = {};
|
|
1645
|
+
const cacheDir = path7.resolve("node_modules", ".nrg", "client");
|
|
1646
|
+
return {
|
|
1647
|
+
name: "vite-plugin-node-red:client:node-definitions-inliner",
|
|
1648
|
+
enforce: "pre",
|
|
1649
|
+
// Load the server bundle in buildStart so _nodeTypes is populated
|
|
1650
|
+
// before any load/transform hooks run.
|
|
1651
|
+
async buildStart() {
|
|
1652
|
+
_nodeTypes = [];
|
|
1653
|
+
_definitions = {};
|
|
1654
|
+
const esmEntryPath = path7.resolve(serverOutDir, "index.mjs");
|
|
1655
|
+
const cjsEntryPath = path7.resolve(serverOutDir, "index.js");
|
|
1656
|
+
let packageFn;
|
|
1657
|
+
if (fs7.existsSync(esmEntryPath)) {
|
|
1658
|
+
const fileUrl = pathToFileURL2(esmEntryPath).href + `?t=${Date.now()}`;
|
|
1659
|
+
const mod = await import(fileUrl);
|
|
1660
|
+
packageFn = mod?.default ?? mod;
|
|
1661
|
+
} else if (fs7.existsSync(cjsEntryPath)) {
|
|
1662
|
+
const require2 = createRequire2(import.meta.url);
|
|
1663
|
+
delete require2.cache[cjsEntryPath];
|
|
1664
|
+
const rawMod = require2(cjsEntryPath);
|
|
1665
|
+
packageFn = rawMod?.default ?? rawMod;
|
|
1666
|
+
}
|
|
1667
|
+
const nodeClasses = packageFn?.nodes ?? [];
|
|
1668
|
+
for (const NodeClass of nodeClasses) {
|
|
1669
|
+
const type = NodeClass.type;
|
|
1670
|
+
if (!type) continue;
|
|
1671
|
+
_nodeTypes.push(type);
|
|
1672
|
+
const configSchema = NodeClass.configSchema ?? null;
|
|
1673
|
+
const credentialsSchema = NodeClass.credentialsSchema ?? null;
|
|
1674
|
+
const inputSchema = NodeClass.inputSchema ?? null;
|
|
1675
|
+
const outputsSchema = NodeClass.outputsSchema ?? null;
|
|
1676
|
+
const defaults = getDefaultsFromSchema(configSchema);
|
|
1677
|
+
if (defaults && inputSchema) {
|
|
1678
|
+
defaults.validateInput = { required: false, value: false };
|
|
1679
|
+
}
|
|
1680
|
+
if (defaults && outputsSchema) {
|
|
1681
|
+
defaults.validateOutput = { required: false, value: false };
|
|
1682
|
+
}
|
|
1683
|
+
const credentials = getCredentialsFromSchema(credentialsSchema);
|
|
1684
|
+
_definitions[type] = {
|
|
1685
|
+
type,
|
|
1686
|
+
category: NodeClass.category,
|
|
1687
|
+
configSchema,
|
|
1688
|
+
credentialsSchema,
|
|
1689
|
+
settingsSchema: NodeClass.settingsSchema ?? null,
|
|
1690
|
+
defaults: defaults ?? void 0,
|
|
1691
|
+
credentials: credentials ?? void 0,
|
|
1692
|
+
align: NodeClass.align,
|
|
1693
|
+
color: NodeClass.color,
|
|
1694
|
+
icon: iconsDir ? resolveIcon(iconsDir, type) : void 0,
|
|
1695
|
+
inputs: NodeClass.inputs,
|
|
1696
|
+
outputs: NodeClass.outputs,
|
|
1697
|
+
inputSchema,
|
|
1698
|
+
outputsSchema
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
if (!hasUserEntry) {
|
|
1702
|
+
const nodesCache = path7.resolve(cacheDir, "nodes");
|
|
1703
|
+
if (fs7.existsSync(nodesCache)) {
|
|
1704
|
+
fs7.rmSync(nodesCache, { recursive: true });
|
|
1705
|
+
}
|
|
1706
|
+
fs7.mkdirSync(nodesCache, { recursive: true });
|
|
1707
|
+
for (const type of _nodeTypes) {
|
|
1708
|
+
const userTsPath = nodesDir ? path7.resolve(nodesDir, `${type}.ts`) : null;
|
|
1709
|
+
if (userTsPath && fs7.existsSync(userTsPath)) continue;
|
|
1710
|
+
const content = [
|
|
1711
|
+
`// auto-generated by nrg`,
|
|
1712
|
+
`import { defineNode } from "@bonsae/nrg/client";`,
|
|
1713
|
+
``,
|
|
1714
|
+
`export default defineNode({`,
|
|
1715
|
+
` type: ${JSON.stringify(type)},`,
|
|
1716
|
+
`});`,
|
|
1717
|
+
``
|
|
1718
|
+
].join("\n");
|
|
1719
|
+
fs7.writeFileSync(path7.resolve(nodesCache, `${type}.ts`), content);
|
|
1720
|
+
}
|
|
1721
|
+
const entryContent = generateEntryCode("");
|
|
1722
|
+
fs7.mkdirSync(path7.dirname(path7.resolve(cacheDir, "index.ts")), {
|
|
1723
|
+
recursive: true
|
|
1724
|
+
});
|
|
1725
|
+
fs7.writeFileSync(path7.resolve(cacheDir, "index.ts"), entryContent);
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
resolveId(id) {
|
|
1729
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
1730
|
+
},
|
|
1731
|
+
load(id) {
|
|
1732
|
+
if (id === RESOLVED_ID)
|
|
1733
|
+
return `export default ${JSON.stringify(_definitions)};`;
|
|
1734
|
+
if (!hasUserEntry && id === entryPath) {
|
|
1735
|
+
return generateEntryCode("");
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
transform(code, id) {
|
|
1739
|
+
if (id !== entryPath) return;
|
|
1740
|
+
if (!hasUserEntry) return;
|
|
1741
|
+
return { code: generateEntryCode(code), map: null };
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
function generateEntryCode(userCode) {
|
|
1745
|
+
const nrgImports = /* @__PURE__ */ new Set(["__setSchemas"]);
|
|
1746
|
+
const lines = [`import __nrgSchemas from "${VIRTUAL_ID}";`];
|
|
1747
|
+
const postLines = [`__setSchemas(__nrgSchemas);`];
|
|
1748
|
+
if (componentsDir && fs7.existsSync(componentsDir)) {
|
|
1749
|
+
const formImports = [];
|
|
1750
|
+
const formEntries = [];
|
|
1751
|
+
for (const type of _nodeTypes) {
|
|
1752
|
+
const componentPath = path7.resolve(componentsDir, `${type}.vue`);
|
|
1753
|
+
if (fs7.existsSync(componentPath)) {
|
|
1754
|
+
const varName = `__nrgForm_${type.replace(/-/g, "_")}`;
|
|
1755
|
+
formImports.push(
|
|
1756
|
+
`import ${varName} from ${JSON.stringify(componentPath)};`
|
|
1757
|
+
);
|
|
1758
|
+
formEntries.push(`${JSON.stringify(type)}: ${varName}`);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (formImports.length > 0) {
|
|
1762
|
+
lines.push(...formImports);
|
|
1763
|
+
nrgImports.add("__setForms");
|
|
1764
|
+
postLines.push(`__setForms({ ${formEntries.join(", ")} });`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (!hasUserEntry) {
|
|
1768
|
+
const nodesCache = path7.resolve(cacheDir, "nodes");
|
|
1769
|
+
const defVarNames = [];
|
|
1770
|
+
for (const type of _nodeTypes) {
|
|
1771
|
+
const varName = `__nrgNodeDef_${type.replace(/-/g, "_")}`;
|
|
1772
|
+
const userTsPath = nodesDir ? path7.resolve(nodesDir, `${type}.ts`) : null;
|
|
1773
|
+
const tsPath = userTsPath && fs7.existsSync(userTsPath) ? userTsPath : path7.resolve(nodesCache, `${type}.ts`);
|
|
1774
|
+
lines.push(`import ${varName} from ${JSON.stringify(tsPath)};`);
|
|
1775
|
+
defVarNames.push(varName);
|
|
1776
|
+
}
|
|
1777
|
+
if (defVarNames.length > 0) {
|
|
1778
|
+
nrgImports.add("registerTypes");
|
|
1779
|
+
postLines.push(`registerTypes([${defVarNames.join(", ")}]);`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const importLine = `import { ${[...nrgImports].join(", ")} } from "@bonsae/nrg/client";`;
|
|
1783
|
+
lines.splice(1, 0, importLine);
|
|
1784
|
+
lines.push(...postLines);
|
|
1785
|
+
lines.push("");
|
|
1786
|
+
return lines.join("\n") + userCode;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/vite/client/plugins/static-copy.ts
|
|
1791
|
+
import fs8 from "fs";
|
|
1792
|
+
import path8 from "path";
|
|
1793
|
+
function staticCopy(options) {
|
|
1794
|
+
const { targets } = options;
|
|
1795
|
+
return {
|
|
1796
|
+
name: "vite-plugin-node-red:client:static-copy",
|
|
1797
|
+
apply: "build",
|
|
1798
|
+
enforce: "post",
|
|
1799
|
+
closeBundle() {
|
|
1800
|
+
for (const { src, dest } of targets) {
|
|
1801
|
+
if (!fs8.existsSync(src)) continue;
|
|
1802
|
+
fs8.mkdirSync(dest, { recursive: true });
|
|
1803
|
+
const stat = fs8.statSync(src);
|
|
1804
|
+
if (stat.isDirectory()) {
|
|
1805
|
+
const files = fs8.readdirSync(src);
|
|
1806
|
+
for (const file of files) {
|
|
1807
|
+
const srcFile = path8.join(src, file);
|
|
1808
|
+
const destFile = path8.join(dest, file);
|
|
1809
|
+
const fileStat = fs8.statSync(srcFile);
|
|
1810
|
+
if (fileStat.isDirectory()) {
|
|
1811
|
+
fs8.cpSync(srcFile, destFile, { recursive: true });
|
|
1812
|
+
} else {
|
|
1813
|
+
fs8.copyFileSync(srcFile, destFile);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
} else {
|
|
1817
|
+
fs8.copyFileSync(src, dest);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/vite/client/build.ts
|
|
1825
|
+
async function build2(clientBuildOptions, buildContext) {
|
|
1826
|
+
const {
|
|
1827
|
+
srcDir = "./client",
|
|
1828
|
+
entry = "index.ts",
|
|
1829
|
+
name = "NodeRedNodes",
|
|
1830
|
+
format = "es",
|
|
1831
|
+
licensePath = "./LICENSE",
|
|
1832
|
+
locales,
|
|
1833
|
+
staticDirs = {},
|
|
1834
|
+
external = [],
|
|
1835
|
+
globals = {},
|
|
1836
|
+
manualChunks
|
|
1837
|
+
} = clientBuildOptions;
|
|
1838
|
+
const physicalEntryPath = path9.resolve(srcDir, entry);
|
|
1839
|
+
let entryPath;
|
|
1840
|
+
let generatedEntry = false;
|
|
1841
|
+
if (fs9.existsSync(physicalEntryPath)) {
|
|
1842
|
+
entryPath = physicalEntryPath;
|
|
1843
|
+
} else {
|
|
1844
|
+
const cacheDir = path9.resolve("node_modules", ".nrg", "client");
|
|
1845
|
+
const cachedEntryPath = path9.resolve(cacheDir, entry);
|
|
1846
|
+
if (!fs9.existsSync(cacheDir)) {
|
|
1847
|
+
fs9.mkdirSync(cacheDir, { recursive: true });
|
|
1848
|
+
}
|
|
1849
|
+
fs9.writeFileSync(cachedEntryPath, "// auto-generated entry\n");
|
|
1850
|
+
entryPath = cachedEntryPath;
|
|
1851
|
+
generatedEntry = true;
|
|
1852
|
+
}
|
|
1853
|
+
const iconsDir = path9.resolve(
|
|
1854
|
+
staticDirs.icons ?? path9.join(path9.dirname(path9.resolve(srcDir)), "icons")
|
|
1855
|
+
);
|
|
1856
|
+
const plugins = [
|
|
1857
|
+
vue(),
|
|
1858
|
+
nodeDefinitionsInliner(
|
|
1859
|
+
buildContext.outDir,
|
|
1860
|
+
entryPath,
|
|
1861
|
+
fs9.existsSync(iconsDir) ? iconsDir : void 0,
|
|
1862
|
+
path9.resolve(srcDir, "components"),
|
|
1863
|
+
path9.resolve(srcDir, "nodes"),
|
|
1864
|
+
!generatedEntry
|
|
1865
|
+
)
|
|
1866
|
+
];
|
|
1867
|
+
plugins.push(
|
|
1868
|
+
htmlGenerator({
|
|
1869
|
+
packageName: buildContext.packageName,
|
|
1870
|
+
licensePath: licensePath ? path9.resolve(licensePath) : void 0
|
|
1871
|
+
})
|
|
1872
|
+
);
|
|
1873
|
+
if (locales) {
|
|
1874
|
+
const { docsDir = "./locales/docs", labelsDir = "./locales/labels" } = locales;
|
|
1875
|
+
const localesOutDir = path9.join(buildContext.outDir, "locales");
|
|
1876
|
+
plugins.push(
|
|
1877
|
+
localesGenerator({
|
|
1878
|
+
outDir: localesOutDir,
|
|
1879
|
+
docsDir: path9.resolve(docsDir),
|
|
1880
|
+
labelsDir: path9.resolve(labelsDir)
|
|
1881
|
+
})
|
|
1882
|
+
);
|
|
1883
|
+
plugins.push(
|
|
1884
|
+
helpGenerator({
|
|
1885
|
+
outDir: buildContext.outDir,
|
|
1886
|
+
localesOutDir,
|
|
1887
|
+
docsDir: path9.resolve(docsDir),
|
|
1888
|
+
labelsDir: path9.resolve(labelsDir)
|
|
1889
|
+
})
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
const copyTargets = [];
|
|
1893
|
+
const publicDir = path9.resolve(
|
|
1894
|
+
staticDirs.public ?? path9.join(srcDir, "public")
|
|
1895
|
+
);
|
|
1896
|
+
if (fs9.existsSync(publicDir)) {
|
|
1897
|
+
copyTargets.push({
|
|
1898
|
+
src: publicDir,
|
|
1899
|
+
dest: path9.join(buildContext.outDir, "resources")
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
if (fs9.existsSync(iconsDir)) {
|
|
1903
|
+
copyTargets.push({
|
|
1904
|
+
src: iconsDir,
|
|
1905
|
+
dest: path9.join(buildContext.outDir, "icons")
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
if (copyTargets.length > 0) {
|
|
1909
|
+
plugins.push(staticCopy({ targets: copyTargets }));
|
|
1910
|
+
}
|
|
1911
|
+
if (!buildContext.isDev && format === "es") {
|
|
1912
|
+
plugins.push(minifier());
|
|
1913
|
+
}
|
|
1914
|
+
plugins.unshift({
|
|
1915
|
+
name: "nrg-client-external",
|
|
1916
|
+
enforce: "pre",
|
|
1917
|
+
resolveId(id) {
|
|
1918
|
+
if (id === "@bonsae/nrg/client")
|
|
1919
|
+
return { id: "@bonsae/nrg/client", external: true };
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
const defaultManualChunks = (id) => {
|
|
1923
|
+
if (!id.includes("node_modules")) return void 0;
|
|
1924
|
+
const parts = id.substring(id.lastIndexOf("node_modules/") + "node_modules/".length).split("/");
|
|
1925
|
+
const pkgName = parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
1926
|
+
if (["jsonpointer", "es-toolkit"].includes(pkgName)) return "vendor-utils";
|
|
1927
|
+
return "vendor";
|
|
1928
|
+
};
|
|
1929
|
+
const config = {
|
|
1930
|
+
configFile: false,
|
|
1931
|
+
logLevel: "warn",
|
|
1932
|
+
base: `/resources/${buildContext.packageName}`,
|
|
1933
|
+
publicDir: path9.resolve(srcDir, "public"),
|
|
1934
|
+
resolve: {
|
|
1935
|
+
alias: {
|
|
1936
|
+
"@": path9.resolve(srcDir)
|
|
1937
|
+
}
|
|
1938
|
+
},
|
|
1939
|
+
plugins,
|
|
1940
|
+
esbuild: {
|
|
1941
|
+
tsconfigRaw: "{}"
|
|
1942
|
+
},
|
|
1943
|
+
css: {
|
|
1944
|
+
devSourcemap: buildContext.isDev
|
|
1945
|
+
},
|
|
1946
|
+
build: {
|
|
1947
|
+
outDir: buildContext.outDir,
|
|
1948
|
+
emptyOutDir: false,
|
|
1949
|
+
sourcemap: buildContext.isDev ? "inline" : false,
|
|
1950
|
+
minify: !buildContext.isDev && format !== "es",
|
|
1951
|
+
copyPublicDir: false,
|
|
1952
|
+
lib: {
|
|
1953
|
+
entry: entryPath,
|
|
1954
|
+
name,
|
|
1955
|
+
fileName: "index",
|
|
1956
|
+
formats: [format]
|
|
1957
|
+
},
|
|
1958
|
+
rollupOptions: {
|
|
1959
|
+
external,
|
|
1960
|
+
treeshake: false,
|
|
1961
|
+
output: {
|
|
1962
|
+
entryFileNames: "resources/index.[hash].js",
|
|
1963
|
+
chunkFileNames: "resources/vendor.[hash].js",
|
|
1964
|
+
assetFileNames: "resources/[name].[hash].[ext]",
|
|
1965
|
+
globals,
|
|
1966
|
+
paths: {
|
|
1967
|
+
vue: "/nrg/assets/vue.esm-browser.prod.js",
|
|
1968
|
+
"@bonsae/nrg/client": "/nrg/assets/nrg-client.js"
|
|
1969
|
+
},
|
|
1970
|
+
sourcemapPathTransform: (relativeSourcePath) => {
|
|
1971
|
+
return relativeSourcePath.replace(/\/client\//g, "/");
|
|
1972
|
+
},
|
|
1973
|
+
manualChunks: manualChunks ?? defaultManualChunks
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
1977
|
+
define: {
|
|
1978
|
+
"process.env.NODE_ENV": JSON.stringify(
|
|
1979
|
+
buildContext.isDev ? "development" : "production"
|
|
1980
|
+
),
|
|
1981
|
+
"process.env": {}
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
try {
|
|
1985
|
+
await viteBuild2(config);
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
throw new BuildError("client", error);
|
|
1988
|
+
} finally {
|
|
1989
|
+
if (generatedEntry) {
|
|
1990
|
+
if (fs9.existsSync(entryPath)) {
|
|
1991
|
+
fs9.unlinkSync(entryPath);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/vite/node-red-launcher.ts
|
|
1998
|
+
import { spawn } from "child_process";
|
|
1999
|
+
import getPort from "get-port";
|
|
2000
|
+
import detect from "detect-port";
|
|
2001
|
+
import { builtinModules as builtinModules2 } 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 { build as esbuild } from "esbuild";
|
|
2007
|
+
|
|
2008
|
+
// src/vite/async-utils.ts
|
|
2009
|
+
function withTimeout(promise, ms, fallback) {
|
|
2010
|
+
return new Promise((resolve, reject) => {
|
|
2011
|
+
const timeout = setTimeout(() => {
|
|
2012
|
+
if (fallback !== void 0) {
|
|
2013
|
+
resolve(fallback);
|
|
2014
|
+
} else {
|
|
2015
|
+
reject(new Error(`Timeout after ${ms}ms`));
|
|
2016
|
+
}
|
|
2017
|
+
}, ms);
|
|
2018
|
+
promise.then((result) => {
|
|
2019
|
+
clearTimeout(timeout);
|
|
2020
|
+
resolve(result);
|
|
2021
|
+
}).catch((error) => {
|
|
2022
|
+
clearTimeout(timeout);
|
|
2023
|
+
reject(error);
|
|
2024
|
+
});
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
async function retry(fn, options = {}) {
|
|
2028
|
+
const { attempts = 3, delay = 1e3 } = options;
|
|
2029
|
+
let lastError;
|
|
2030
|
+
for (let i = 0; i < attempts; i++) {
|
|
2031
|
+
try {
|
|
2032
|
+
return await fn();
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
lastError = error;
|
|
2035
|
+
if (i < attempts - 1) {
|
|
2036
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
throw lastError;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/vite/node-red-launcher.ts
|
|
2044
|
+
var NodeRedLauncher = class {
|
|
2045
|
+
compiledRuntimeSettingsFilepath = null;
|
|
2046
|
+
process = null;
|
|
2047
|
+
bufferedLogs = [];
|
|
2048
|
+
isReady = false;
|
|
2049
|
+
port = null;
|
|
2050
|
+
outDir;
|
|
2051
|
+
options;
|
|
2052
|
+
logger;
|
|
2053
|
+
constructor(outDir, options) {
|
|
2054
|
+
this.outDir = outDir;
|
|
2055
|
+
this.options = options;
|
|
2056
|
+
this.logger = new Logger({
|
|
2057
|
+
name: "vite-plugin-node-red",
|
|
2058
|
+
prefix: "node-red"
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
get preferredPort() {
|
|
2062
|
+
return this.options.runtime?.port ?? 1880;
|
|
2063
|
+
}
|
|
2064
|
+
get restartDelay() {
|
|
2065
|
+
return this.options.restartDelay ?? 1e3;
|
|
2066
|
+
}
|
|
2067
|
+
get pid() {
|
|
2068
|
+
return this.process?.pid ?? null;
|
|
2069
|
+
}
|
|
2070
|
+
get nodeRedCommand() {
|
|
2071
|
+
const version = this.options.runtime?.version;
|
|
2072
|
+
if (version === "latest") {
|
|
2073
|
+
return "node-red@latest";
|
|
2074
|
+
}
|
|
2075
|
+
if (version) {
|
|
2076
|
+
return `node-red@${version}`;
|
|
2077
|
+
}
|
|
2078
|
+
return "node-red";
|
|
2079
|
+
}
|
|
2080
|
+
findRuntimeSettingsFilepath() {
|
|
2081
|
+
const runtimeSettingsFilepath = this.options.runtime?.settingsFilepath;
|
|
2082
|
+
if (runtimeSettingsFilepath) {
|
|
2083
|
+
const resolved2 = path10.resolve(runtimeSettingsFilepath);
|
|
2084
|
+
if (fs10.existsSync(resolved2)) {
|
|
2085
|
+
return resolved2;
|
|
2086
|
+
}
|
|
2087
|
+
this.logger.warn(`Settings file not found: ${runtimeSettingsFilepath}`);
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
const resolved = path10.resolve("node-red.settings.ts");
|
|
2091
|
+
if (fs10.existsSync(resolved)) {
|
|
2092
|
+
return resolved;
|
|
2093
|
+
}
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
async compileRuntimeSettingsFile(runtimeSettingsFilepath) {
|
|
2097
|
+
const compiledRuntimeSettingsFilepath = path10.join(
|
|
2098
|
+
os.tmpdir(),
|
|
2099
|
+
`node-red.settings.${process.pid}.cjs`
|
|
2100
|
+
);
|
|
2101
|
+
const nodeBuiltins2 = [
|
|
2102
|
+
...builtinModules2,
|
|
2103
|
+
...builtinModules2.map((m) => `node:${m}`)
|
|
2104
|
+
];
|
|
2105
|
+
const settingsDir = path10.dirname(runtimeSettingsFilepath).split(path10.sep).join("/");
|
|
2106
|
+
const settingsFile = runtimeSettingsFilepath.split(path10.sep).join("/");
|
|
2107
|
+
await esbuild({
|
|
2108
|
+
entryPoints: [runtimeSettingsFilepath],
|
|
2109
|
+
outfile: compiledRuntimeSettingsFilepath,
|
|
2110
|
+
format: "cjs",
|
|
2111
|
+
platform: "node",
|
|
2112
|
+
target: "node18",
|
|
2113
|
+
bundle: true,
|
|
2114
|
+
define: {
|
|
2115
|
+
"import.meta.dirname": JSON.stringify(settingsDir),
|
|
2116
|
+
"import.meta.filename": JSON.stringify(settingsFile),
|
|
2117
|
+
"import.meta.url": JSON.stringify(`file://${settingsFile}`)
|
|
2118
|
+
},
|
|
2119
|
+
external: [...nodeBuiltins2, "node-red", "@node-red/*"]
|
|
2120
|
+
});
|
|
2121
|
+
this.compiledRuntimeSettingsFilepath = compiledRuntimeSettingsFilepath;
|
|
2122
|
+
return compiledRuntimeSettingsFilepath;
|
|
2123
|
+
}
|
|
2124
|
+
async generateRuntimeSettingsFile() {
|
|
2125
|
+
const userRuntimeSettingsFilepath = this.findRuntimeSettingsFilepath();
|
|
2126
|
+
let compiledRuntimeSettingsFilepath = null;
|
|
2127
|
+
if (userRuntimeSettingsFilepath) {
|
|
2128
|
+
compiledRuntimeSettingsFilepath = await this.compileRuntimeSettingsFile(
|
|
2129
|
+
userRuntimeSettingsFilepath
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
const outDir = path10.resolve(this.outDir).split(path10.sep).join("/");
|
|
2133
|
+
const cwd = process.cwd().split(path10.sep).join("/");
|
|
2134
|
+
const userDir = path10.resolve(cwd, ".node-red");
|
|
2135
|
+
const finalRuntimeSettingsFile = compiledRuntimeSettingsFilepath ? `
|
|
2136
|
+
const compiledRuntimeSettings = require("${compiledRuntimeSettingsFilepath.split(path10.sep).join("/")}");
|
|
2137
|
+
const settings = compiledRuntimeSettings.default || compiledRuntimeSettings;
|
|
2138
|
+
settings.uiPort = ${this.port};
|
|
2139
|
+
if(!settings.userDir){
|
|
2140
|
+
settings.userDir = "${userDir}";
|
|
2141
|
+
}
|
|
2142
|
+
settings.nodesDir = settings.nodesDir || [];
|
|
2143
|
+
if (!settings.nodesDir.includes("${outDir}")) {
|
|
2144
|
+
settings.nodesDir.push("${outDir}");
|
|
2145
|
+
}
|
|
2146
|
+
if(!settings.flowFile){
|
|
2147
|
+
settings.flowFile = "flows.json";
|
|
2148
|
+
}
|
|
2149
|
+
module.exports = settings;
|
|
2150
|
+
` : `
|
|
2151
|
+
const settings = {
|
|
2152
|
+
uiPort: ${this.port},
|
|
2153
|
+
userDir: "${userDir}",
|
|
2154
|
+
flowFile: "flows.json",
|
|
2155
|
+
nodesDir: ["${outDir}"],
|
|
2156
|
+
};
|
|
2157
|
+
module.exports = settings;
|
|
2158
|
+
`;
|
|
2159
|
+
const finalRuntimeSettingsFilepath = path10.join(
|
|
2160
|
+
os.tmpdir(),
|
|
2161
|
+
`node-red-settings-final-${process.pid}.cjs`
|
|
2162
|
+
);
|
|
2163
|
+
fs10.writeFileSync(finalRuntimeSettingsFilepath, finalRuntimeSettingsFile);
|
|
2164
|
+
this.compiledRuntimeSettingsFilepath = finalRuntimeSettingsFilepath;
|
|
2165
|
+
return finalRuntimeSettingsFilepath;
|
|
2166
|
+
}
|
|
2167
|
+
log(line) {
|
|
2168
|
+
if (line.includes("Server now running at")) {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
this.logger.raw(line);
|
|
2172
|
+
}
|
|
2173
|
+
async start() {
|
|
2174
|
+
const available = await detect(this.preferredPort);
|
|
2175
|
+
if (available === this.preferredPort) {
|
|
2176
|
+
this.port = this.preferredPort;
|
|
2177
|
+
} else {
|
|
2178
|
+
this.logger.warn(
|
|
2179
|
+
`Port ${this.preferredPort} is still in use, waiting...`
|
|
2180
|
+
);
|
|
2181
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
2182
|
+
const retryAvailable = await detect(this.preferredPort);
|
|
2183
|
+
if (retryAvailable === this.preferredPort) {
|
|
2184
|
+
this.port = this.preferredPort;
|
|
2185
|
+
} else {
|
|
2186
|
+
this.logger.warn(
|
|
2187
|
+
`Port ${this.preferredPort} still occupied, using port ${retryAvailable}`
|
|
2188
|
+
);
|
|
2189
|
+
this.port = await getPort({ port: this.preferredPort });
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
const startProcess = () => {
|
|
2193
|
+
return new Promise(async (resolve, reject) => {
|
|
2194
|
+
try {
|
|
2195
|
+
const settingsPath = await this.generateRuntimeSettingsFile();
|
|
2196
|
+
const args = this.options.args ?? [];
|
|
2197
|
+
this.bufferedLogs = [];
|
|
2198
|
+
this.isReady = false;
|
|
2199
|
+
this.process = spawn(
|
|
2200
|
+
"npx",
|
|
2201
|
+
[this.nodeRedCommand, "-s", settingsPath, ...args],
|
|
2202
|
+
{
|
|
2203
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2204
|
+
shell: true
|
|
2205
|
+
}
|
|
2206
|
+
);
|
|
2207
|
+
this.process.stdout?.on("data", (data) => {
|
|
2208
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
2209
|
+
for (const line of lines) {
|
|
2210
|
+
if (this.isReady) {
|
|
2211
|
+
this.log(line);
|
|
2212
|
+
} else {
|
|
2213
|
+
this.bufferedLogs.push(line);
|
|
2214
|
+
}
|
|
2215
|
+
if (line.includes("Started flows") || line.includes("Server now running")) {
|
|
2216
|
+
this.isReady = true;
|
|
2217
|
+
resolve(void 0);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
this.process.stderr?.on("data", (data) => {
|
|
2222
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
2223
|
+
for (const line of lines) {
|
|
2224
|
+
if (this.isReady) {
|
|
2225
|
+
this.logger.error(`${line}`);
|
|
2226
|
+
} else {
|
|
2227
|
+
this.bufferedLogs.push(line);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
this.process.on("error", (error) => {
|
|
2232
|
+
reject(new NodeRedStartError(error));
|
|
2233
|
+
});
|
|
2234
|
+
this.process.on("exit", (code) => {
|
|
2235
|
+
if (!this.isReady && code !== 0 && code !== null) {
|
|
2236
|
+
reject(
|
|
2237
|
+
new NodeRedStartError(
|
|
2238
|
+
new Error(`Process exited with code ${code}`)
|
|
2239
|
+
)
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
resolve(void 0);
|
|
2243
|
+
});
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
reject(new NodeRedStartError(error));
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
};
|
|
2249
|
+
try {
|
|
2250
|
+
await retry(startProcess, { attempts: 3, delay: 100 });
|
|
2251
|
+
return this.port;
|
|
2252
|
+
} catch (error) {
|
|
2253
|
+
if (this.process) {
|
|
2254
|
+
const pid = this.process.pid;
|
|
2255
|
+
if (pid) {
|
|
2256
|
+
treeKill(pid, "SIGKILL");
|
|
2257
|
+
}
|
|
2258
|
+
this.process = null;
|
|
2259
|
+
}
|
|
2260
|
+
throw new NodeRedStartError(error);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
async stop(skipPortUsageCheck = false) {
|
|
2264
|
+
if (!this.process) return;
|
|
2265
|
+
const pid = this.process.pid;
|
|
2266
|
+
const currentPort = this.port;
|
|
2267
|
+
if (!pid) {
|
|
2268
|
+
this.process = null;
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
const stopProcess = new Promise((resolve) => {
|
|
2272
|
+
this.process.once("exit", () => {
|
|
2273
|
+
this.process = null;
|
|
2274
|
+
resolve(void 0);
|
|
2275
|
+
});
|
|
2276
|
+
treeKill(pid, "SIGTERM", (error) => {
|
|
2277
|
+
if (error) {
|
|
2278
|
+
try {
|
|
2279
|
+
process.kill(pid, "SIGTERM");
|
|
2280
|
+
} catch {
|
|
2281
|
+
this.process = null;
|
|
2282
|
+
resolve(void 0);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
});
|
|
2287
|
+
try {
|
|
2288
|
+
await withTimeout(stopProcess, 1e4);
|
|
2289
|
+
} catch {
|
|
2290
|
+
this.logger.warn("Graceful shutdown timed out, force killing...");
|
|
2291
|
+
await new Promise((resolve) => {
|
|
2292
|
+
treeKill(pid, "SIGKILL", () => {
|
|
2293
|
+
this.process = null;
|
|
2294
|
+
resolve(void 0);
|
|
2295
|
+
});
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
if (!skipPortUsageCheck && currentPort) {
|
|
2299
|
+
const checkPortUsage = async () => {
|
|
2300
|
+
const availablePort = await detect(currentPort);
|
|
2301
|
+
if (availablePort !== currentPort) {
|
|
2302
|
+
throw new Error("Port still in use");
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
try {
|
|
2306
|
+
await retry(checkPortUsage, { attempts: 10, delay: 300 });
|
|
2307
|
+
} catch {
|
|
2308
|
+
this.logger.warn(
|
|
2309
|
+
`Port ${currentPort} still in use after stop. Force killing...`
|
|
2310
|
+
);
|
|
2311
|
+
if (pid) {
|
|
2312
|
+
await new Promise((resolve) => {
|
|
2313
|
+
treeKill(pid, "SIGKILL", () => resolve());
|
|
2314
|
+
});
|
|
2315
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
this.port = null;
|
|
2320
|
+
}
|
|
2321
|
+
flushLogs() {
|
|
2322
|
+
for (const line of this.bufferedLogs) {
|
|
2323
|
+
this.log(line);
|
|
2324
|
+
}
|
|
2325
|
+
this.bufferedLogs = [];
|
|
2326
|
+
}
|
|
2327
|
+
cleanup() {
|
|
2328
|
+
if (this.compiledRuntimeSettingsFilepath && fs10.existsSync(this.compiledRuntimeSettingsFilepath)) {
|
|
2329
|
+
fs10.unlinkSync(this.compiledRuntimeSettingsFilepath);
|
|
2330
|
+
this.compiledRuntimeSettingsFilepath = null;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
// src/test/client/e2e/environment.ts
|
|
2336
|
+
var NodeRedTestEnvironment = class {
|
|
2337
|
+
port = null;
|
|
2338
|
+
launcher = null;
|
|
2339
|
+
originalCwd = null;
|
|
2340
|
+
projectDir;
|
|
2341
|
+
outDir;
|
|
2342
|
+
nodeRedDir;
|
|
2343
|
+
installedPkgDir;
|
|
2344
|
+
options;
|
|
2345
|
+
constructor(options) {
|
|
2346
|
+
this.options = options;
|
|
2347
|
+
this.projectDir = path11.resolve(options.projectDir ?? process.cwd());
|
|
2348
|
+
this.outDir = path11.join(this.projectDir, "dist-e2e");
|
|
2349
|
+
this.nodeRedDir = path11.join(this.projectDir, ".node-red");
|
|
2350
|
+
this.installedPkgDir = path11.join(
|
|
2351
|
+
this.nodeRedDir,
|
|
2352
|
+
"node_modules",
|
|
2353
|
+
options.packageName
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
get nodeRedPort() {
|
|
2357
|
+
return this.port;
|
|
2358
|
+
}
|
|
2359
|
+
async setup() {
|
|
2360
|
+
this.originalCwd = process.cwd();
|
|
2361
|
+
process.chdir(this.projectDir);
|
|
2362
|
+
if (fs11.existsSync(this.outDir)) fs11.rmSync(this.outDir, { recursive: true });
|
|
2363
|
+
fs11.mkdirSync(this.outDir, { recursive: true });
|
|
2364
|
+
const buildContext = {
|
|
2365
|
+
outDir: this.outDir,
|
|
2366
|
+
packageName: this.options.packageName,
|
|
2367
|
+
isDev: false
|
|
2368
|
+
};
|
|
2369
|
+
const serverOpts = {
|
|
2370
|
+
srcDir: path11.join(this.projectDir, "src/server"),
|
|
2371
|
+
entry: "index.ts",
|
|
2372
|
+
format: "esm",
|
|
2373
|
+
bundled: [],
|
|
2374
|
+
types: false,
|
|
2375
|
+
nodeTarget: "node22",
|
|
2376
|
+
...this.options.server
|
|
2377
|
+
};
|
|
2378
|
+
await build(serverOpts, buildContext);
|
|
2379
|
+
const clientOpts = {
|
|
2380
|
+
srcDir: path11.join(this.projectDir, "src/client"),
|
|
2381
|
+
entry: "index.ts",
|
|
2382
|
+
name: this.options.clientName ?? "NodeRedNodes",
|
|
2383
|
+
format: "es",
|
|
2384
|
+
external: ["jquery", "node-red", "vue", "@bonsae/nrg/client"],
|
|
2385
|
+
globals: { jquery: "$", "node-red": "RED", vue: "Vue" },
|
|
2386
|
+
...this.options.client
|
|
2387
|
+
};
|
|
2388
|
+
await build2(clientOpts, buildContext);
|
|
2389
|
+
fs11.mkdirSync(this.installedPkgDir, { recursive: true });
|
|
2390
|
+
fs11.cpSync(this.outDir, this.installedPkgDir, { recursive: true });
|
|
2391
|
+
fs11.mkdirSync(this.nodeRedDir, { recursive: true });
|
|
2392
|
+
fs11.writeFileSync(
|
|
2393
|
+
path11.join(this.nodeRedDir, ".config.runtime.json"),
|
|
2394
|
+
JSON.stringify({ telemetryEnabled: false })
|
|
2395
|
+
);
|
|
2396
|
+
const launcherOpts = {
|
|
2397
|
+
runtime: { port: this.options.port ?? 1881 }
|
|
2398
|
+
};
|
|
2399
|
+
if (this.options.settingsFile) {
|
|
2400
|
+
launcherOpts.runtime.settingsFilepath = path11.resolve(
|
|
2401
|
+
this.projectDir,
|
|
2402
|
+
this.options.settingsFile
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
this.launcher = new NodeRedLauncher(this.installedPkgDir, launcherOpts);
|
|
2406
|
+
this.port = await this.launcher.start();
|
|
2407
|
+
this.launcher.flushLogs();
|
|
2408
|
+
process.chdir(this.originalCwd);
|
|
2409
|
+
return this.port;
|
|
2410
|
+
}
|
|
2411
|
+
async deployFlow(flow) {
|
|
2412
|
+
if (!this.port) {
|
|
2413
|
+
throw new Error("Environment not started. Call setup() first.");
|
|
2414
|
+
}
|
|
2415
|
+
const res = await fetch(`http://localhost:${this.port}/flows`, {
|
|
2416
|
+
method: "POST",
|
|
2417
|
+
headers: {
|
|
2418
|
+
"Content-Type": "application/json",
|
|
2419
|
+
"Node-RED-Deployment-Type": "full"
|
|
2420
|
+
},
|
|
2421
|
+
body: JSON.stringify(flow)
|
|
2422
|
+
});
|
|
2423
|
+
if (!res.ok) {
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
`Failed to deploy flow: ${res.status} ${await res.text()}`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
async teardown() {
|
|
2430
|
+
if (this.launcher) {
|
|
2431
|
+
await this.launcher.stop();
|
|
2432
|
+
this.launcher.cleanup();
|
|
2433
|
+
this.launcher = null;
|
|
2434
|
+
}
|
|
2435
|
+
if (fs11.existsSync(this.outDir)) fs11.rmSync(this.outDir, { recursive: true });
|
|
2436
|
+
if (fs11.existsSync(this.nodeRedDir)) {
|
|
2437
|
+
fs11.rmSync(this.nodeRedDir, { recursive: true });
|
|
2438
|
+
}
|
|
2439
|
+
this.port = null;
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
// src/test/client/e2e/index.ts
|
|
2444
|
+
var defaultConfig = {
|
|
2445
|
+
testTimeout: 6e4,
|
|
2446
|
+
hookTimeout: 12e4,
|
|
2447
|
+
globalSetup: ["@bonsae/nrg/test/client/e2e"]
|
|
2448
|
+
};
|
|
2449
|
+
var _env = null;
|
|
2450
|
+
async function setup(options) {
|
|
2451
|
+
const packageName = JSON.parse(
|
|
2452
|
+
fs12.readFileSync(path12.join(process.cwd(), "package.json"), "utf-8")
|
|
2453
|
+
).name;
|
|
2454
|
+
_env = new NodeRedTestEnvironment({
|
|
2455
|
+
packageName,
|
|
2456
|
+
settingsFile: options?.settingsFile
|
|
2457
|
+
});
|
|
2458
|
+
const port = await _env.setup();
|
|
2459
|
+
process.env.NODE_RED_PORT = String(port);
|
|
2460
|
+
if (options?.flow) {
|
|
2461
|
+
await _env.deployFlow(options.flow);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
async function teardown() {
|
|
2465
|
+
if (_env) {
|
|
2466
|
+
await _env.teardown();
|
|
2467
|
+
_env = null;
|
|
2468
|
+
}
|
|
2469
|
+
delete process.env.NODE_RED_PORT;
|
|
2470
|
+
}
|
|
2471
|
+
var NodeRedEditor = class {
|
|
2472
|
+
constructor(page, port, options) {
|
|
2473
|
+
this.page = page;
|
|
2474
|
+
this.port = port;
|
|
2475
|
+
this.screenshotDir = options?.screenshotDir ?? "test-results/screenshots";
|
|
2476
|
+
page.on("pageerror", (err) => this.errors.push(err.message));
|
|
2477
|
+
}
|
|
2478
|
+
errors = [];
|
|
2479
|
+
screenshotDir;
|
|
2480
|
+
async screenshot(name) {
|
|
2481
|
+
fs12.mkdirSync(this.screenshotDir, { recursive: true });
|
|
2482
|
+
const filePath = path12.join(this.screenshotDir, `${name}.png`);
|
|
2483
|
+
await this.page.screenshot({ path: filePath, fullPage: true });
|
|
2484
|
+
return filePath;
|
|
2485
|
+
}
|
|
2486
|
+
async open() {
|
|
2487
|
+
await this.page.goto(`http://localhost:${this.port}`);
|
|
2488
|
+
await this.page.waitForSelector("#red-ui-workspace", { timeout: 3e4 });
|
|
2489
|
+
await this.page.waitForFunction(
|
|
2490
|
+
() => {
|
|
2491
|
+
const r = globalThis.RED;
|
|
2492
|
+
return r && typeof r.editor?.edit === "function";
|
|
2493
|
+
},
|
|
2494
|
+
{ timeout: 15e3 }
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
async deployFlow(flow) {
|
|
2498
|
+
const res = await fetch(`http://localhost:${this.port}/flows`, {
|
|
2499
|
+
method: "POST",
|
|
2500
|
+
headers: {
|
|
2501
|
+
"Content-Type": "application/json",
|
|
2502
|
+
"Node-RED-Deployment-Type": "full"
|
|
2503
|
+
},
|
|
2504
|
+
body: JSON.stringify(flow)
|
|
2505
|
+
});
|
|
2506
|
+
if (!res.ok) {
|
|
2507
|
+
throw new Error(
|
|
2508
|
+
`Failed to deploy flow: ${res.status} ${await res.text()}`
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
await this.page.reload();
|
|
2512
|
+
await this.page.waitForSelector("#red-ui-workspace", { timeout: 15e3 });
|
|
2513
|
+
}
|
|
2514
|
+
async editNode(nodeId) {
|
|
2515
|
+
await this.page.waitForFunction(
|
|
2516
|
+
(id) => {
|
|
2517
|
+
const r = globalThis.RED;
|
|
2518
|
+
const node = r?.nodes?.node(id);
|
|
2519
|
+
if (!node) return false;
|
|
2520
|
+
return r.nodes.getType(node.type) !== null;
|
|
2521
|
+
},
|
|
2522
|
+
nodeId,
|
|
2523
|
+
{ timeout: 15e3 }
|
|
2524
|
+
);
|
|
2525
|
+
await this.page.evaluate((id) => {
|
|
2526
|
+
const r = globalThis.RED;
|
|
2527
|
+
r.editor.edit(r.nodes.node(id));
|
|
2528
|
+
}, nodeId);
|
|
2529
|
+
await this.page.waitForSelector(".red-ui-tray", { timeout: 1e4 });
|
|
2530
|
+
await this.page.waitForTimeout(500);
|
|
2531
|
+
}
|
|
2532
|
+
async clickDone() {
|
|
2533
|
+
await this.page.evaluate(() => {
|
|
2534
|
+
globalThis.document.getElementById("node-dialog-ok").click();
|
|
2535
|
+
});
|
|
2536
|
+
await this.page.waitForSelector(".red-ui-tray", {
|
|
2537
|
+
state: "hidden",
|
|
2538
|
+
timeout: 5e3
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
async clickCancel() {
|
|
2542
|
+
await this.page.evaluate(() => {
|
|
2543
|
+
globalThis.document.getElementById("node-dialog-cancel").click();
|
|
2544
|
+
});
|
|
2545
|
+
await this.page.waitForSelector(".red-ui-tray", {
|
|
2546
|
+
state: "hidden",
|
|
2547
|
+
timeout: 5e3
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
field(label) {
|
|
2551
|
+
return new NodeRedField(this.page, label);
|
|
2552
|
+
}
|
|
2553
|
+
expectNoPageErrors() {
|
|
2554
|
+
if (this.errors.length > 0) {
|
|
2555
|
+
throw new Error(
|
|
2556
|
+
`Page errors detected:
|
|
2557
|
+
${this.errors.map((e) => ` - ${e}`).join("\n")}`
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
get tray() {
|
|
2562
|
+
return this.page.locator(".red-ui-tray-body-wrapper");
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
var NodeRedField = class {
|
|
2566
|
+
constructor(page, label) {
|
|
2567
|
+
this.page = page;
|
|
2568
|
+
this.label = label;
|
|
2569
|
+
this.row = page.locator(`.form-row:has(:text("${label}"))`).first();
|
|
2570
|
+
}
|
|
2571
|
+
row;
|
|
2572
|
+
get input() {
|
|
2573
|
+
return this.row.locator("input").first();
|
|
2574
|
+
}
|
|
2575
|
+
async fill(value) {
|
|
2576
|
+
await this.input.fill(value, { force: true });
|
|
2577
|
+
}
|
|
2578
|
+
async clear() {
|
|
2579
|
+
await this.input.fill("", { force: true });
|
|
2580
|
+
}
|
|
2581
|
+
async getValue() {
|
|
2582
|
+
return this.input.inputValue();
|
|
2583
|
+
}
|
|
2584
|
+
async getInputType() {
|
|
2585
|
+
return this.input.getAttribute("type");
|
|
2586
|
+
}
|
|
2587
|
+
get toggleSlider() {
|
|
2588
|
+
return this.row.locator(".nrg-toggle__slider");
|
|
2589
|
+
}
|
|
2590
|
+
async toggle() {
|
|
2591
|
+
await this.toggleSlider.click();
|
|
2592
|
+
}
|
|
2593
|
+
get checkbox() {
|
|
2594
|
+
return this.row.locator('input[type="checkbox"]');
|
|
2595
|
+
}
|
|
2596
|
+
get typedInputContainer() {
|
|
2597
|
+
return this.row.locator(".red-ui-typedInput-container");
|
|
2598
|
+
}
|
|
2599
|
+
async getSelectedType() {
|
|
2600
|
+
return this.typedInputContainer.locator("input").first().evaluate((el) => globalThis.$(el).typedInput("type"));
|
|
2601
|
+
}
|
|
2602
|
+
async getSelectedValue() {
|
|
2603
|
+
return this.typedInputContainer.locator("input").first().evaluate(
|
|
2604
|
+
(el) => globalThis.$(el).typedInput("value")
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
async openTypeMenu() {
|
|
2608
|
+
await this.typedInputContainer.locator(".red-ui-typedInput-type-select").click();
|
|
2609
|
+
const menu = this.page.locator(".red-ui-typedInput-options:visible").first();
|
|
2610
|
+
await menu.waitFor({ state: "visible", timeout: 5e3 });
|
|
2611
|
+
return menu;
|
|
2612
|
+
}
|
|
2613
|
+
async getTypeMenuValues() {
|
|
2614
|
+
const menu = await this.openTypeMenu();
|
|
2615
|
+
const values = await menu.locator("a").evaluateAll((els) => els.map((el) => el.getAttribute("value") ?? ""));
|
|
2616
|
+
await this.page.keyboard.press("Escape");
|
|
2617
|
+
return values;
|
|
2618
|
+
}
|
|
2619
|
+
async selectType(type) {
|
|
2620
|
+
const menu = await this.openTypeMenu();
|
|
2621
|
+
await menu.locator(`a[value="${type}"]`).click();
|
|
2622
|
+
}
|
|
2623
|
+
async openOptionMenu() {
|
|
2624
|
+
await this.typedInputContainer.locator(".red-ui-typedInput-option-trigger").click();
|
|
2625
|
+
const menu = this.page.locator(".red-ui-typedInput-options:visible").first();
|
|
2626
|
+
await menu.waitFor({ state: "visible", timeout: 5e3 });
|
|
2627
|
+
return menu;
|
|
2628
|
+
}
|
|
2629
|
+
async getOptionMenuLabels() {
|
|
2630
|
+
const menu = await this.openOptionMenu();
|
|
2631
|
+
const labels = await menu.locator("a").evaluateAll((els) => els.map((el) => el.textContent?.trim() ?? ""));
|
|
2632
|
+
await this.page.keyboard.press("Escape");
|
|
2633
|
+
return labels;
|
|
2634
|
+
}
|
|
2635
|
+
get select() {
|
|
2636
|
+
return this.row.locator("select");
|
|
2637
|
+
}
|
|
2638
|
+
get editButton() {
|
|
2639
|
+
return this.row.locator("a.red-ui-button:has(i.fa-pencil)");
|
|
2640
|
+
}
|
|
2641
|
+
get addButton() {
|
|
2642
|
+
return this.row.locator("a.red-ui-button:has(i.fa-plus)");
|
|
2643
|
+
}
|
|
2644
|
+
async getSelectedOption() {
|
|
2645
|
+
return this.select.inputValue();
|
|
2646
|
+
}
|
|
2647
|
+
async getSelectedOptionLabel() {
|
|
2648
|
+
const value = await this.getSelectedOption();
|
|
2649
|
+
return this.select.locator(`option[value="${value}"]`).textContent().then((t) => t?.trim() ?? "");
|
|
2650
|
+
}
|
|
2651
|
+
async getOptions() {
|
|
2652
|
+
return this.select.locator("option").evaluateAll(
|
|
2653
|
+
(els) => els.map((el) => el.textContent?.trim() ?? "").filter((t) => !t.startsWith("Add new ") && t !== "")
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
get editorWrapper() {
|
|
2657
|
+
return this.row.locator(".editor-wrapper");
|
|
2658
|
+
}
|
|
2659
|
+
get expandButton() {
|
|
2660
|
+
return this.row.locator(".expand-button");
|
|
2661
|
+
}
|
|
2662
|
+
get textarea() {
|
|
2663
|
+
return this.row.locator("textarea");
|
|
2664
|
+
}
|
|
2665
|
+
get requiredIndicator() {
|
|
2666
|
+
return this.row.locator(".nrg-required");
|
|
2667
|
+
}
|
|
2668
|
+
get errorMessage() {
|
|
2669
|
+
return this.row.locator(".node-red-vue-input-error-message");
|
|
2670
|
+
}
|
|
2671
|
+
async expectError(containing) {
|
|
2672
|
+
if (containing) {
|
|
2673
|
+
await this.errorMessage.filter({ hasText: containing }).waitFor({ state: "visible", timeout: 5e3 });
|
|
2674
|
+
} else {
|
|
2675
|
+
await this.errorMessage.waitFor({ state: "visible", timeout: 5e3 });
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
async expectNoError() {
|
|
2679
|
+
await this.errorMessage.waitFor({ state: "hidden", timeout: 5e3 });
|
|
2680
|
+
}
|
|
2681
|
+
async scrollIntoView() {
|
|
2682
|
+
await this.row.scrollIntoViewIfNeeded();
|
|
2683
|
+
}
|
|
2684
|
+
async expectVisible() {
|
|
2685
|
+
await this.row.waitFor({ state: "visible", timeout: 5e3 });
|
|
2686
|
+
}
|
|
2687
|
+
async expectHidden() {
|
|
2688
|
+
await this.row.waitFor({ state: "hidden", timeout: 5e3 });
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
export {
|
|
2692
|
+
NodeRedEditor,
|
|
2693
|
+
NodeRedField,
|
|
2694
|
+
NodeRedTestEnvironment,
|
|
2695
|
+
defaultConfig,
|
|
2696
|
+
setup,
|
|
2697
|
+
teardown
|
|
2698
|
+
};
|