@bonsae/nrg 0.18.5 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/server/index.cjs +86 -9
  4. package/server/resources/nrg-client.js +2020 -1987
  5. package/test/client/component/config.js +11 -0
  6. package/test/client/component/index.js +218 -235
  7. package/test/client/component/nrg.css +1 -0
  8. package/test/client/component/setup.js +1549 -140
  9. package/test/client/e2e/index.js +706 -368
  10. package/test/client/unit/index.js +204 -16
  11. package/test/client/unit/setup.js +209 -19
  12. package/test/server/unit/index.js +25 -4
  13. package/tsconfig/core/client.json +1 -1
  14. package/tsconfig/test/client/component.json +1 -1
  15. package/types/client.d.ts +98 -18
  16. package/types/server.d.ts +50 -12
  17. package/types/shims/brands.d.ts +32 -0
  18. package/types/shims/{form → client/form}/components/node-red-editor-input.vue.d.ts +1 -1
  19. package/types/shims/{form → client/form}/components/node-red-json-schema-form.vue.d.ts +21 -2
  20. package/types/shims/{form → client/form}/components/node-red-select-input.vue.d.ts +1 -0
  21. package/types/shims/{form → client/form}/components/node-red-typed-input.vue.d.ts +1 -0
  22. package/types/shims/client/types.d.ts +206 -0
  23. package/types/shims/components.d.ts +8 -8
  24. package/types/shims/constants.d.ts +4 -0
  25. package/types/shims/schema-options.d.ts +23 -10
  26. package/types/shims/typebox.d.ts +2 -2
  27. package/types/test-client-component.d.ts +170 -55
  28. package/types/test-client-e2e.d.ts +50 -0
  29. package/types/test-client-unit.d.ts +86 -22
  30. package/types/test-server-unit.d.ts +3 -1
  31. package/types/vite.d.ts +25 -9
  32. package/vite/index.js +648 -499
  33. /package/types/shims/{form → client/form}/components/node-red-config-input.vue.d.ts +0 -0
  34. /package/types/shims/{form → client/form}/components/node-red-input-label.vue.d.ts +0 -0
  35. /package/types/shims/{form → client/form}/components/node-red-input.vue.d.ts +0 -0
  36. /package/types/shims/{form → client/form}/components/node-red-toggle.vue.d.ts +0 -0
  37. /package/types/shims/{globals.d.ts → client/globals.d.ts} +0 -0
package/vite/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/vite/plugin.ts
2
- import path13 from "path";
2
+ import path14 from "path";
3
3
 
4
4
  // src/vite/defaults.ts
5
5
  var DEFAULT_OUTPUT_DIR = "./dist";
@@ -106,17 +106,8 @@ function mergeOptions(defaults, overrides) {
106
106
  return result;
107
107
  }
108
108
 
109
- // src/vite/node-red-launcher.ts
110
- import { spawn, execSync } from "child_process";
111
- import getPort from "get-port";
112
- import detect from "detect-port";
113
- import { builtinModules, createRequire } from "module";
114
- import treeKill from "tree-kill";
115
- import fs2 from "fs";
116
- import os from "os";
117
- import path2 from "path";
118
- import { pathToFileURL } from "url";
119
- import { build as esbuild } from "esbuild";
109
+ // src/vite/node-red-launcher/index.ts
110
+ import fs4 from "fs";
120
111
 
121
112
  // src/vite/async-utils.ts
122
113
  function debounce(fn, delay) {
@@ -259,203 +250,376 @@ var Logger = class _Logger {
259
250
  };
260
251
  var logger = new Logger({ name: "vite-plugin-node-red" });
261
252
 
262
- // src/vite/node-red-launcher.ts
263
- var NodeRedLauncher = class {
264
- compiledRuntimeSettingsFilepath = null;
265
- process = null;
266
- bufferedLogs = [];
267
- isReady = false;
268
- port = null;
269
- outDir;
270
- options;
271
- logger;
272
- constructor(outDir, options) {
273
- this.outDir = outDir;
274
- this.options = options;
275
- this.logger = new Logger({
276
- name: "vite-plugin-node-red",
277
- prefix: "node-red"
278
- });
253
+ // src/vite/node-red-launcher/entry-point.ts
254
+ import { exec } from "child_process";
255
+ import { randomUUID } from "crypto";
256
+ import { createRequire } from "module";
257
+ import fs2 from "fs";
258
+ import os from "os";
259
+ import path2 from "path";
260
+ function getNodeRedCommand(version) {
261
+ return version ? `node-red@${version}` : "node-red";
262
+ }
263
+ function resolveNodeRedFromLocalNodeModules() {
264
+ try {
265
+ const require_ = createRequire(path2.join(process.cwd(), "package.json"));
266
+ const pkgJsonPath = require_.resolve("node-red/package.json");
267
+ const pkgDir = path2.dirname(pkgJsonPath);
268
+ const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
269
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["node-red"];
270
+ if (!bin) return null;
271
+ const entry = path2.resolve(pkgDir, bin);
272
+ return fs2.existsSync(entry) ? entry : null;
273
+ } catch {
274
+ return null;
279
275
  }
280
- get preferredPort() {
281
- return this.options.runtime?.port ?? 1880;
276
+ }
277
+ async function resolveNodeRed(options) {
278
+ const { version, npxTimeoutMs = 3e5, logger: logger2 } = options;
279
+ if (version && !/^[\w.^~<>=*-]+$/.test(version)) {
280
+ throw new NodeRedStartError(
281
+ new Error(`Invalid node-red version "${version}"`)
282
+ );
282
283
  }
283
- get restartDelay() {
284
- return this.options.restartDelay ?? 1e3;
284
+ const nodeRedCommand = getNodeRedCommand(version);
285
+ logger2.info(`Resolving ${nodeRedCommand} entry point...`);
286
+ const hasExplicitVersion = version !== void 0 && version !== "latest";
287
+ if (!hasExplicitVersion) {
288
+ const localEntry = resolveNodeRedFromLocalNodeModules();
289
+ if (localEntry) {
290
+ logger2.info(`Resolved from local node_modules: ${localEntry}`);
291
+ return localEntry;
292
+ }
285
293
  }
286
- get pid() {
287
- return this.process?.pid ?? null;
294
+ logger2.info(
295
+ hasExplicitVersion ? `Using configured version (${version}), downloading via npx...` : `Not found locally, downloading via npx (this may take a while)...`
296
+ );
297
+ const resolverScript = path2.join(
298
+ os.tmpdir(),
299
+ `nrg-resolve-node-red-${process.pid}-${randomUUID()}.cjs`
300
+ );
301
+ fs2.writeFileSync(
302
+ resolverScript,
303
+ `const fs = require("fs");
304
+ const path = require("path");
305
+ const isWin = process.platform === "win32";
306
+ const binName = isWin ? "node-red.cmd" : "node-red";
307
+ const dirs = process.env.PATH.split(path.delimiter);
308
+ for (const d of dirs) {
309
+ const f = path.join(d, binName);
310
+ if (fs.existsSync(f)) {
311
+ if (isWin) {
312
+ const nodeRedDir = path.resolve(d, "..", "node-red");
313
+ const pkg = JSON.parse(fs.readFileSync(path.join(nodeRedDir, "package.json"), "utf-8"));
314
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin["node-red"];
315
+ process.stdout.write(path.resolve(nodeRedDir, bin));
316
+ } else {
317
+ process.stdout.write(fs.realpathSync(f));
318
+ }
319
+ break;
288
320
  }
289
- get nodeRedCommand() {
290
- const version = this.options.runtime?.version;
291
- if (version === "latest") {
292
- return "node-red@latest";
321
+ }`
322
+ );
323
+ try {
324
+ const stdout = await new Promise((resolve, reject) => {
325
+ exec(
326
+ `npx --yes -p ${nodeRedCommand} node "${resolverScript}"`,
327
+ { timeout: npxTimeoutMs },
328
+ (error, stdout2) => {
329
+ if (error) reject(error);
330
+ else resolve(stdout2);
331
+ }
332
+ );
333
+ });
334
+ const entryPoint = stdout.trim();
335
+ if (!entryPoint || !fs2.existsSync(entryPoint)) {
336
+ throw new NodeRedStartError(
337
+ new Error(
338
+ `Could not resolve node-red entry point: ${entryPoint || "(empty)"}`
339
+ )
340
+ );
293
341
  }
294
- if (version) {
295
- return `node-red@${version}`;
342
+ logger2.info(`Resolved via npx: ${entryPoint}`);
343
+ return entryPoint;
344
+ } finally {
345
+ try {
346
+ fs2.unlinkSync(resolverScript);
347
+ } catch {
296
348
  }
297
- return "node-red";
298
349
  }
299
- findRuntimeSettingsFilepath() {
300
- const runtimeSettingsFilepath = this.options.runtime?.settingsFilepath;
301
- if (runtimeSettingsFilepath) {
302
- const resolved2 = path2.resolve(runtimeSettingsFilepath);
303
- if (fs2.existsSync(resolved2)) {
304
- return resolved2;
305
- }
306
- this.logger.warn(`Settings file not found: ${runtimeSettingsFilepath}`);
307
- return null;
308
- }
309
- const resolved = path2.resolve("node-red.settings.ts");
310
- if (fs2.existsSync(resolved)) {
311
- return resolved;
350
+ }
351
+
352
+ // src/vite/node-red-launcher/settings.ts
353
+ import { builtinModules } from "module";
354
+ import fs3 from "fs";
355
+ import os2 from "os";
356
+ import path3 from "path";
357
+ import { pathToFileURL } from "url";
358
+ import { build as esbuild } from "esbuild";
359
+ function findUserRuntimeSettingsFilepath(settingsFilepath, logger2) {
360
+ if (settingsFilepath) {
361
+ const resolved2 = path3.resolve(settingsFilepath);
362
+ if (fs3.existsSync(resolved2)) {
363
+ return resolved2;
312
364
  }
365
+ logger2.warn(`Settings file not found: ${settingsFilepath}`);
313
366
  return null;
314
367
  }
315
- async compileRuntimeSettingsFile(runtimeSettingsFilepath) {
316
- const compiledRuntimeSettingsFilepath = path2.join(
317
- os.tmpdir(),
318
- `node-red.settings.${process.pid}.cjs`
368
+ const resolved = path3.resolve("node-red.settings.ts");
369
+ if (fs3.existsSync(resolved)) {
370
+ return resolved;
371
+ }
372
+ return null;
373
+ }
374
+ async function compileRuntimeSettingsFile(runtimeSettingsFilepath, port) {
375
+ const compiledRuntimeSettingsFilepath = path3.join(
376
+ os2.tmpdir(),
377
+ `node-red.settings.${process.pid}-${port}.cjs`
378
+ );
379
+ const nodeBuiltins2 = [
380
+ ...builtinModules,
381
+ ...builtinModules.map((m) => `node:${m}`)
382
+ ];
383
+ const settingsDir = path3.dirname(runtimeSettingsFilepath).split(path3.sep).join("/");
384
+ const settingsFile = runtimeSettingsFilepath.split(path3.sep).join("/");
385
+ await esbuild({
386
+ entryPoints: [runtimeSettingsFilepath],
387
+ outfile: compiledRuntimeSettingsFilepath,
388
+ format: "cjs",
389
+ platform: "node",
390
+ target: "node18",
391
+ bundle: true,
392
+ define: {
393
+ "import.meta.dirname": JSON.stringify(settingsDir),
394
+ "import.meta.filename": JSON.stringify(settingsFile),
395
+ "import.meta.url": JSON.stringify(pathToFileURL(settingsFile).href)
396
+ },
397
+ external: [...nodeBuiltins2, "node-red", "@node-red/*"]
398
+ });
399
+ return compiledRuntimeSettingsFilepath;
400
+ }
401
+ async function generateRuntimeSettings(options) {
402
+ const { outDir, port, settingsFilepath, logger: logger2 } = options;
403
+ const tempFiles = [];
404
+ const userRuntimeSettingsFilepath = findUserRuntimeSettingsFilepath(
405
+ settingsFilepath,
406
+ logger2
407
+ );
408
+ let compiledRuntimeSettingsFilepath = null;
409
+ if (userRuntimeSettingsFilepath) {
410
+ compiledRuntimeSettingsFilepath = await compileRuntimeSettingsFile(
411
+ userRuntimeSettingsFilepath,
412
+ port
319
413
  );
320
- const nodeBuiltins2 = [
321
- ...builtinModules,
322
- ...builtinModules.map((m) => `node:${m}`)
323
- ];
324
- const settingsDir = path2.dirname(runtimeSettingsFilepath).split(path2.sep).join("/");
325
- const settingsFile = runtimeSettingsFilepath.split(path2.sep).join("/");
326
- await esbuild({
327
- entryPoints: [runtimeSettingsFilepath],
328
- outfile: compiledRuntimeSettingsFilepath,
329
- format: "cjs",
330
- platform: "node",
331
- target: "node18",
332
- bundle: true,
333
- define: {
334
- "import.meta.dirname": JSON.stringify(settingsDir),
335
- "import.meta.filename": JSON.stringify(settingsFile),
336
- "import.meta.url": JSON.stringify(pathToFileURL(settingsFile).href)
337
- },
338
- external: [...nodeBuiltins2, "node-red", "@node-red/*"]
339
- });
340
- this.compiledRuntimeSettingsFilepath = compiledRuntimeSettingsFilepath;
341
- return compiledRuntimeSettingsFilepath;
342
- }
343
- async generateRuntimeSettingsFile() {
344
- const userRuntimeSettingsFilepath = this.findRuntimeSettingsFilepath();
345
- let compiledRuntimeSettingsFilepath = null;
346
- if (userRuntimeSettingsFilepath) {
347
- compiledRuntimeSettingsFilepath = await this.compileRuntimeSettingsFile(
348
- userRuntimeSettingsFilepath
349
- );
350
- }
351
- const outDir = path2.resolve(this.outDir).split(path2.sep).join("/");
352
- const cwd = process.cwd().split(path2.sep).join("/");
353
- const userDir = path2.resolve(cwd, ".node-red").split(path2.sep).join("/");
354
- const finalRuntimeSettingsFile = compiledRuntimeSettingsFilepath ? `
355
- const compiledRuntimeSettings = require("${compiledRuntimeSettingsFilepath.split(path2.sep).join("/")}");
414
+ tempFiles.push(compiledRuntimeSettingsFilepath);
415
+ }
416
+ const normalizedOutDir = path3.resolve(outDir).split(path3.sep).join("/");
417
+ const cwd = process.cwd().split(path3.sep).join("/");
418
+ const userDir = path3.resolve(cwd, ".node-red").split(path3.sep).join("/");
419
+ const userDirLiteral = JSON.stringify(userDir);
420
+ const outDirLiteral = JSON.stringify(normalizedOutDir);
421
+ const finalRuntimeSettingsFile = compiledRuntimeSettingsFilepath ? `
422
+ const compiledRuntimeSettings = require(${JSON.stringify(
423
+ compiledRuntimeSettingsFilepath.split(path3.sep).join("/")
424
+ )});
356
425
  const settings = compiledRuntimeSettings.default || compiledRuntimeSettings;
357
- settings.uiPort = ${this.port};
426
+ settings.uiPort = ${port};
358
427
  if(!settings.userDir){
359
- settings.userDir = "${userDir}";
428
+ settings.userDir = ${userDirLiteral};
360
429
  }
361
430
  settings.nodesDir = settings.nodesDir || [];
362
- if (!settings.nodesDir.includes("${outDir}")) {
363
- settings.nodesDir.push("${outDir}");
431
+ if (!settings.nodesDir.includes(${outDirLiteral})) {
432
+ settings.nodesDir.push(${outDirLiteral});
364
433
  }
365
434
  if(!settings.flowFile){
366
435
  settings.flowFile = "flows.json";
367
436
  }
437
+ // the welcome tour overlay intercepts pointer events \u2014 fatal for e2e and
438
+ // noise for dev; explicit user settings still win
439
+ settings.editorTheme = settings.editorTheme || {};
440
+ if (settings.editorTheme.tours === undefined) {
441
+ settings.editorTheme.tours = false;
442
+ }
368
443
  module.exports = settings;
369
444
  ` : `
370
445
  const settings = {
371
- uiPort: ${this.port},
372
- userDir: "${userDir}",
446
+ uiPort: ${port},
447
+ userDir: ${userDirLiteral},
373
448
  flowFile: "flows.json",
374
- nodesDir: ["${outDir}"],
449
+ nodesDir: [${outDirLiteral}],
450
+ // the welcome tour overlay intercepts pointer events \u2014 fatal for e2e
451
+ editorTheme: { tours: false },
375
452
  };
376
453
  module.exports = settings;
377
454
  `;
378
- const finalRuntimeSettingsFilepath = path2.join(
379
- os.tmpdir(),
380
- `node-red-settings-final-${process.pid}.cjs`
381
- );
382
- fs2.writeFileSync(finalRuntimeSettingsFilepath, finalRuntimeSettingsFile);
383
- this.compiledRuntimeSettingsFilepath = finalRuntimeSettingsFilepath;
384
- return finalRuntimeSettingsFilepath;
385
- }
386
- resolveFromLocalNodeModules() {
387
- try {
388
- const require_ = createRequire(path2.join(process.cwd(), "package.json"));
389
- const pkgJsonPath = require_.resolve("node-red/package.json");
390
- const pkgDir = path2.dirname(pkgJsonPath);
391
- const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
392
- const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["node-red"];
393
- if (!bin) return null;
394
- const entry = path2.resolve(pkgDir, bin);
395
- return fs2.existsSync(entry) ? entry : null;
396
- } catch {
397
- return null;
455
+ const finalRuntimeSettingsFilepath = path3.join(
456
+ os2.tmpdir(),
457
+ `node-red-settings-final-${process.pid}-${port}.cjs`
458
+ );
459
+ fs3.writeFileSync(finalRuntimeSettingsFilepath, finalRuntimeSettingsFile);
460
+ tempFiles.push(finalRuntimeSettingsFilepath);
461
+ return { filepath: finalRuntimeSettingsFilepath, tempFiles };
462
+ }
463
+
464
+ // src/vite/node-red-launcher/process.ts
465
+ import { spawn } from "child_process";
466
+ import detect from "detect-port";
467
+ import getPort from "get-port";
468
+ import treeKill from "tree-kill";
469
+ var READY_MARKERS = ["Started flows", "Server now running"];
470
+ function start(options) {
471
+ const { entryPoint, settingsPath, args, onLine } = options;
472
+ const child = spawn(
473
+ process.execPath,
474
+ [entryPoint, "-s", settingsPath, ...args],
475
+ {
476
+ stdio: ["ignore", "pipe", "pipe"]
398
477
  }
399
- }
400
- resolveNodeRedEntryPoint() {
401
- this.logger.info(`Resolving ${this.nodeRedCommand} entry point...`);
402
- const hasExplicitVersion = this.options.runtime?.version !== void 0 && this.options.runtime.version !== "latest";
403
- if (!hasExplicitVersion) {
404
- const localEntry = this.resolveFromLocalNodeModules();
405
- if (localEntry) {
406
- this.logger.info(`Resolved from local node_modules: ${localEntry}`);
407
- return localEntry;
478
+ );
479
+ let isReady = false;
480
+ let resolveReady;
481
+ let rejectReady;
482
+ const ready = new Promise((resolve, reject) => {
483
+ resolveReady = resolve;
484
+ rejectReady = reject;
485
+ });
486
+ const emitLine = (rawLine, source) => {
487
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
488
+ if (!line) return;
489
+ onLine(line, source, isReady);
490
+ if (source === "stdout" && READY_MARKERS.some((marker) => line.includes(marker))) {
491
+ isReady = true;
492
+ resolveReady();
493
+ }
494
+ };
495
+ const remainders = { stdout: "", stderr: "" };
496
+ const handleData = (data, source) => {
497
+ const lines = (remainders[source] + data.toString()).split("\n");
498
+ remainders[source] = lines.pop() ?? "";
499
+ for (const line of lines) {
500
+ emitLine(line, source);
501
+ }
502
+ };
503
+ const flushRemainders = () => {
504
+ for (const source of ["stdout", "stderr"]) {
505
+ const rest = remainders[source];
506
+ remainders[source] = "";
507
+ if (rest) {
508
+ emitLine(rest, source);
408
509
  }
409
510
  }
410
- this.logger.info(
411
- hasExplicitVersion ? `Using configured version (${this.options.runtime.version}), downloading via npx...` : `Not found locally, downloading via npx (this may take a while)...`
412
- );
413
- const resolverScript = path2.join(
414
- os.tmpdir(),
415
- `nrg-resolve-node-red-${process.pid}.cjs`
416
- );
417
- fs2.writeFileSync(
418
- resolverScript,
419
- `const fs = require("fs");
420
- const path = require("path");
421
- const isWin = process.platform === "win32";
422
- const binName = isWin ? "node-red.cmd" : "node-red";
423
- const dirs = process.env.PATH.split(path.delimiter);
424
- for (const d of dirs) {
425
- const f = path.join(d, binName);
426
- if (fs.existsSync(f)) {
427
- if (isWin) {
428
- const nodeRedDir = path.resolve(d, "..", "node-red");
429
- const pkg = JSON.parse(fs.readFileSync(path.join(nodeRedDir, "package.json"), "utf-8"));
430
- const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin["node-red"];
431
- process.stdout.write(path.resolve(nodeRedDir, bin));
432
- } else {
433
- process.stdout.write(fs.realpathSync(f));
511
+ };
512
+ child.stdout?.on("data", (data) => handleData(data, "stdout"));
513
+ child.stderr?.on("data", (data) => handleData(data, "stderr"));
514
+ child.on("error", (error) => {
515
+ rejectReady(new NodeRedStartError(error));
516
+ });
517
+ child.on("exit", (code) => {
518
+ flushRemainders();
519
+ if (!isReady && code !== 0 && code !== null) {
520
+ rejectReady(
521
+ new NodeRedStartError(new Error(`Process exited with code ${code}`))
522
+ );
523
+ return;
434
524
  }
435
- break;
525
+ resolveReady();
526
+ });
527
+ return { child, ready };
528
+ }
529
+ function kill(pid) {
530
+ return new Promise((resolve) => {
531
+ treeKill(pid, "SIGKILL", () => resolve());
532
+ });
533
+ }
534
+ async function stop(options) {
535
+ const { child, pid, gracefulTimeoutMs = 1e4, logger: logger2 } = options;
536
+ if (child.exitCode !== null || child.signalCode !== null) {
537
+ return;
436
538
  }
437
- }`
438
- );
439
- try {
440
- const entryPoint = execSync(
441
- `npx --yes -p ${this.nodeRedCommand} -c "node ${resolverScript}"`,
442
- { encoding: "utf-8", timeout: 3e5 }
443
- ).trim();
444
- if (!entryPoint || !fs2.existsSync(entryPoint)) {
445
- throw new NodeRedStartError(
446
- new Error(
447
- `Could not resolve node-red entry point: ${entryPoint || "(empty)"}`
448
- )
449
- );
450
- }
451
- this.logger.info(`Resolved via npx: ${entryPoint}`);
452
- return entryPoint;
453
- } finally {
454
- try {
455
- fs2.unlinkSync(resolverScript);
456
- } catch {
539
+ const exited = new Promise((resolve) => {
540
+ child.once("exit", () => resolve());
541
+ treeKill(pid, "SIGTERM", (error) => {
542
+ if (error) {
543
+ try {
544
+ process.kill(pid, "SIGTERM");
545
+ } catch {
546
+ resolve();
547
+ }
457
548
  }
549
+ });
550
+ });
551
+ try {
552
+ await withTimeout(exited, gracefulTimeoutMs);
553
+ } catch {
554
+ logger2.warn("Graceful shutdown timed out, force killing...");
555
+ await kill(pid);
556
+ }
557
+ }
558
+ async function acquirePort(options) {
559
+ const { preferredPort, retryDelay = 2e3, logger: logger2 } = options;
560
+ const available = await detect(preferredPort);
561
+ if (available === preferredPort) {
562
+ return preferredPort;
563
+ }
564
+ logger2.warn(`Port ${preferredPort} is still in use, waiting...`);
565
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
566
+ const retryAvailable = await detect(preferredPort);
567
+ if (retryAvailable === preferredPort) {
568
+ return preferredPort;
569
+ }
570
+ const fallbackPort = await getPort({ port: preferredPort });
571
+ logger2.warn(
572
+ `Port ${preferredPort} still occupied, using port ${fallbackPort}`
573
+ );
574
+ return fallbackPort;
575
+ }
576
+ async function waitForPortRelease(port, options = {}) {
577
+ const { attempts = 10, delay = 300 } = options;
578
+ const checkPortUsage = async () => {
579
+ const availablePort = await detect(port);
580
+ if (availablePort !== port) {
581
+ throw new Error("Port still in use");
458
582
  }
583
+ };
584
+ try {
585
+ await retry(checkPortUsage, { attempts, delay });
586
+ return true;
587
+ } catch {
588
+ return false;
589
+ }
590
+ }
591
+
592
+ // src/vite/node-red-launcher/index.ts
593
+ var NodeRedLauncher = class {
594
+ operationQueue = Promise.resolve();
595
+ process = null;
596
+ unwatchExit = null;
597
+ nodeRedEntryPoint = null;
598
+ tempFiles = [];
599
+ bufferedLogs = [];
600
+ port = null;
601
+ outDir;
602
+ options;
603
+ logger;
604
+ constructor(outDir, options) {
605
+ this.outDir = outDir;
606
+ this.options = options;
607
+ this.logger = new Logger({
608
+ name: "vite-plugin-node-red",
609
+ prefix: "node-red"
610
+ });
611
+ }
612
+ get preferredPort() {
613
+ return this.options.runtime?.port ?? 1880;
614
+ }
615
+ get restartDelay() {
616
+ return this.options.restartDelay ?? 1e3;
617
+ }
618
+ get pid() {
619
+ return this.process?.child.pid ?? null;
620
+ }
621
+ get nodeRedCommand() {
622
+ return getNodeRedCommand(this.options.runtime?.version);
459
623
  }
460
624
  log(line) {
461
625
  if (line.includes("Server now running at")) {
@@ -463,150 +627,121 @@ for (const d of dirs) {
463
627
  }
464
628
  this.logger.raw(line);
465
629
  }
466
- async start() {
467
- const available = await detect(this.preferredPort);
468
- if (available === this.preferredPort) {
469
- this.port = this.preferredPort;
630
+ handleProcessLine(line, source, ready) {
631
+ if (!ready) {
632
+ this.bufferedLogs.push(line);
633
+ } else if (source === "stderr") {
634
+ this.logger.error(line);
470
635
  } else {
636
+ this.log(line);
637
+ }
638
+ }
639
+ async killProcess() {
640
+ if (!this.process) return;
641
+ this.stopWatchingExit();
642
+ const pid = this.process.child.pid;
643
+ if (pid) {
644
+ await kill(pid);
645
+ }
646
+ this.process = null;
647
+ }
648
+ watchForUnexpectedExit(managed) {
649
+ const onExit = (code, signal) => {
650
+ this.unwatchExit = null;
651
+ this.process = null;
471
652
  this.logger.warn(
472
- `Port ${this.preferredPort} is still in use, waiting...`
653
+ `Node-RED exited unexpectedly (${signal ?? `code ${code}`})`
473
654
  );
474
- await new Promise((resolve) => setTimeout(resolve, 2e3));
475
- const retryAvailable = await detect(this.preferredPort);
476
- if (retryAvailable === this.preferredPort) {
477
- this.port = this.preferredPort;
478
- } else {
479
- this.logger.warn(
480
- `Port ${this.preferredPort} still occupied, using port ${retryAvailable}`
481
- );
482
- this.port = await getPort({ port: this.preferredPort });
483
- }
484
- }
485
- const nodeRedEntryPoint = this.resolveNodeRedEntryPoint();
486
- const startProcess = () => {
487
- return new Promise(async (resolve, reject) => {
488
- try {
489
- const settingsPath = await this.generateRuntimeSettingsFile();
490
- const args = this.options.args ?? [];
491
- this.bufferedLogs = [];
492
- this.isReady = false;
493
- this.process = spawn(
494
- process.execPath,
495
- [nodeRedEntryPoint, "-s", settingsPath, ...args],
496
- {
497
- stdio: ["ignore", "pipe", "pipe"]
498
- }
499
- );
500
- this.process.stdout?.on("data", (data) => {
501
- const lines = data.toString().split("\n").filter(Boolean);
502
- for (const line of lines) {
503
- if (this.isReady) {
504
- this.log(line);
505
- } else {
506
- this.bufferedLogs.push(line);
507
- }
508
- if (line.includes("Started flows") || line.includes("Server now running")) {
509
- this.isReady = true;
510
- resolve(void 0);
511
- }
512
- }
513
- });
514
- this.process.stderr?.on("data", (data) => {
515
- const lines = data.toString().split("\n").filter(Boolean);
516
- for (const line of lines) {
517
- if (this.isReady) {
518
- this.logger.error(`${line}`);
519
- } else {
520
- this.bufferedLogs.push(line);
521
- }
522
- }
523
- });
524
- this.process.on("error", (error) => {
525
- reject(new NodeRedStartError(error));
526
- });
527
- this.process.on("exit", (code) => {
528
- if (!this.isReady && code !== 0 && code !== null) {
529
- reject(
530
- new NodeRedStartError(
531
- new Error(`Process exited with code ${code}`)
532
- )
533
- );
534
- }
535
- resolve(void 0);
536
- });
537
- } catch (error) {
538
- reject(new NodeRedStartError(error));
539
- }
540
- });
541
655
  };
656
+ managed.child.once("exit", onExit);
657
+ this.unwatchExit = () => managed.child.off("exit", onExit);
658
+ }
659
+ stopWatchingExit() {
660
+ this.unwatchExit?.();
661
+ this.unwatchExit = null;
662
+ }
663
+ // start/stop interleaving (e.g. SIGINT while Node-RED is booting) would
664
+ // race on process/port state and could leak a spawned process, so all
665
+ // lifecycle operations run strictly one at a time
666
+ enqueue(operation) {
667
+ const result = this.operationQueue.then(operation, operation);
668
+ this.operationQueue = result.catch(() => {
669
+ });
670
+ return result;
671
+ }
672
+ async start() {
673
+ return this.enqueue(() => this.doStart());
674
+ }
675
+ async stop(skipPortUsageCheck = false) {
676
+ return this.enqueue(() => this.doStop(skipPortUsageCheck));
677
+ }
678
+ async doStart() {
542
679
  try {
680
+ this.port = await acquirePort({
681
+ preferredPort: this.preferredPort,
682
+ logger: this.logger
683
+ });
684
+ if (!this.nodeRedEntryPoint || !fs4.existsSync(this.nodeRedEntryPoint)) {
685
+ this.nodeRedEntryPoint = await resolveNodeRed({
686
+ version: this.options.runtime?.version,
687
+ logger: this.logger
688
+ });
689
+ }
690
+ const nodeRedEntryPoint = this.nodeRedEntryPoint;
691
+ const settings = await generateRuntimeSettings({
692
+ outDir: this.outDir,
693
+ port: this.port,
694
+ settingsFilepath: this.options.runtime?.settingsFilepath,
695
+ logger: this.logger
696
+ });
697
+ for (const file of settings.tempFiles) {
698
+ if (!this.tempFiles.includes(file)) {
699
+ this.tempFiles.push(file);
700
+ }
701
+ }
702
+ const startProcess = async () => {
703
+ await this.killProcess();
704
+ this.bufferedLogs = [];
705
+ this.process = start({
706
+ entryPoint: nodeRedEntryPoint,
707
+ settingsPath: settings.filepath,
708
+ args: this.options.args ?? [],
709
+ onLine: (line, source, ready) => this.handleProcessLine(line, source, ready)
710
+ });
711
+ await this.process.ready;
712
+ };
543
713
  await retry(startProcess, { attempts: 3, delay: 100 });
714
+ this.watchForUnexpectedExit(this.process);
544
715
  return this.port;
545
716
  } catch (error) {
546
- if (this.process) {
547
- const pid = this.process.pid;
548
- if (pid) {
549
- treeKill(pid, "SIGKILL");
550
- }
551
- this.process = null;
552
- }
553
- throw new NodeRedStartError(error);
717
+ await this.killProcess();
718
+ throw error instanceof NodeRedStartError ? error : new NodeRedStartError(error);
554
719
  }
555
720
  }
556
- async stop(skipPortUsageCheck = false) {
721
+ async doStop(skipPortUsageCheck) {
557
722
  if (!this.process) return;
558
- const pid = this.process.pid;
723
+ this.stopWatchingExit();
724
+ const pid = this.process.child.pid;
559
725
  const currentPort = this.port;
560
726
  if (!pid) {
561
727
  this.process = null;
728
+ this.port = null;
562
729
  return;
563
730
  }
564
- const stopProcess = new Promise((resolve) => {
565
- this.process.once("exit", () => {
566
- this.process = null;
567
- resolve(void 0);
568
- });
569
- treeKill(pid, "SIGTERM", (error) => {
570
- if (error) {
571
- try {
572
- process.kill(pid, "SIGTERM");
573
- } catch {
574
- this.process = null;
575
- resolve(void 0);
576
- }
577
- }
578
- });
731
+ await stop({
732
+ child: this.process.child,
733
+ pid,
734
+ logger: this.logger
579
735
  });
580
- try {
581
- await withTimeout(stopProcess, 1e4);
582
- } catch {
583
- this.logger.warn("Graceful shutdown timed out, force killing...");
584
- await new Promise((resolve) => {
585
- treeKill(pid, "SIGKILL", () => {
586
- this.process = null;
587
- resolve(void 0);
588
- });
589
- });
590
- }
736
+ this.process = null;
591
737
  if (!skipPortUsageCheck && currentPort) {
592
- const checkPortUsage = async () => {
593
- const availablePort = await detect(currentPort);
594
- if (availablePort !== currentPort) {
595
- throw new Error("Port still in use");
596
- }
597
- };
598
- try {
599
- await retry(checkPortUsage, { attempts: 10, delay: 300 });
600
- } catch {
738
+ const released = await waitForPortRelease(currentPort);
739
+ if (!released) {
601
740
  this.logger.warn(
602
741
  `Port ${currentPort} still in use after stop. Force killing...`
603
742
  );
604
- if (pid) {
605
- await new Promise((resolve) => {
606
- treeKill(pid, "SIGKILL", () => resolve());
607
- });
608
- await new Promise((resolve) => setTimeout(resolve, 1e3));
609
- }
743
+ await kill(pid);
744
+ await waitForPortRelease(currentPort);
610
745
  }
611
746
  }
612
747
  this.port = null;
@@ -618,32 +753,35 @@ for (const d of dirs) {
618
753
  this.bufferedLogs = [];
619
754
  }
620
755
  cleanup() {
621
- if (this.compiledRuntimeSettingsFilepath && fs2.existsSync(this.compiledRuntimeSettingsFilepath)) {
622
- fs2.unlinkSync(this.compiledRuntimeSettingsFilepath);
623
- this.compiledRuntimeSettingsFilepath = null;
756
+ for (const file of this.tempFiles) {
757
+ try {
758
+ fs4.unlinkSync(file);
759
+ } catch {
760
+ }
624
761
  }
762
+ this.tempFiles = [];
625
763
  }
626
764
  };
627
765
 
628
766
  // src/vite/plugins/server.ts
629
767
  import chokidar from "chokidar";
630
768
  import treeKill2 from "tree-kill";
631
- import path12 from "path";
769
+ import path13 from "path";
632
770
 
633
771
  // src/vite/server/build.ts
634
772
  import { build as viteBuild } from "vite";
635
- import fs5 from "fs";
636
- import path5 from "path";
773
+ import fs7 from "fs";
774
+ import path6 from "path";
637
775
 
638
776
  // src/vite/server/plugins/type-generator.ts
639
777
  import dts from "vite-plugin-dts";
640
- import fs3 from "fs";
641
- import path3 from "path";
778
+ import fs5 from "fs";
779
+ import path4 from "path";
642
780
  import ts from "typescript";
643
781
  function collectTsFiles(dir) {
644
- if (!fs3.existsSync(dir)) return [];
645
- return fs3.readdirSync(dir, { withFileTypes: true }).flatMap((dirent) => {
646
- const full = path3.join(dir, dirent.name);
782
+ if (!fs5.existsSync(dir)) return [];
783
+ return fs5.readdirSync(dir, { withFileTypes: true }).flatMap((dirent) => {
784
+ const full = path4.join(dir, dirent.name);
647
785
  if (dirent.isDirectory()) return collectTsFiles(full);
648
786
  if (dirent.isFile() && dirent.name.endsWith(".ts") && !dirent.name.endsWith(".d.ts"))
649
787
  return [full];
@@ -658,7 +796,7 @@ var BASE_CLASS_SLOTS = {
658
796
  ConfigNode: ["Config", "Credentials", "Settings"]
659
797
  };
660
798
  function getNodeTypeExports(filePath) {
661
- const content = fs3.readFileSync(filePath, "utf-8");
799
+ const content = fs5.readFileSync(filePath, "utf-8");
662
800
  const source = ts.createSourceFile(
663
801
  filePath,
664
802
  content,
@@ -720,7 +858,7 @@ var SCHEMA_PROP_SEMANTICS = {
720
858
  settingsSchema: "SettingsSchema"
721
859
  };
722
860
  function getSchemaReferences(filePath) {
723
- const content = fs3.readFileSync(filePath, "utf-8");
861
+ const content = fs5.readFileSync(filePath, "utf-8");
724
862
  const source = ts.createSourceFile(
725
863
  filePath,
726
864
  content,
@@ -812,7 +950,7 @@ function getSchemaReferences(filePath) {
812
950
  return result;
813
951
  }
814
952
  function getFactoryInfo(filePath) {
815
- const content = fs3.readFileSync(filePath, "utf-8");
953
+ const content = fs5.readFileSync(filePath, "utf-8");
816
954
  const source = ts.createSourceFile(
817
955
  filePath,
818
956
  content,
@@ -846,13 +984,13 @@ function buildTypeArg(schemaMap, semanticName, portNameMap) {
846
984
  return `[${names.map((n) => `Infer<typeof ${n}>`).join(", ")}]`;
847
985
  }
848
986
  function buildNodeReexports(srcDir, entryFile) {
849
- const nodesDir = path3.join(srcDir, "nodes");
987
+ const nodesDir = path4.join(srcDir, "nodes");
850
988
  const nodeFiles = collectTsFiles(nodesDir);
851
989
  return nodeFiles.map((file) => {
852
- const rel = path3.relative(path3.dirname(entryFile), file).replace(/\\/g, "/");
990
+ const rel = path4.relative(path4.dirname(entryFile), file).replace(/\\/g, "/");
853
991
  const relPath = rel.startsWith(".") ? rel : `./${rel}`;
854
992
  const specifier = relPath.replace(/\.ts$/, "");
855
- const ns = toPascalCase(path3.basename(file, ".ts"));
993
+ const ns = toPascalCase(path4.basename(file, ".ts"));
856
994
  const schemaRefs = getSchemaReferences(file);
857
995
  const factoryInfo = getFactoryInfo(file);
858
996
  const lines = [];
@@ -879,9 +1017,9 @@ function buildNodeReexports(srcDir, entryFile) {
879
1017
  if (hasSchemas) {
880
1018
  const bySource2 = /* @__PURE__ */ new Map();
881
1019
  for (const ref of schemaRefs) {
882
- const resolvedSource = path3.relative(
883
- path3.dirname(entryFile),
884
- path3.resolve(path3.dirname(file), ref.importSource)
1020
+ const resolvedSource = path4.relative(
1021
+ path4.dirname(entryFile),
1022
+ path4.resolve(path4.dirname(file), ref.importSource)
885
1023
  ).replace(/\\/g, "/");
886
1024
  const sourceSpecifier = resolvedSource.startsWith(".") ? resolvedSource : `./${resolvedSource}`;
887
1025
  if (!bySource2.has(sourceSpecifier))
@@ -927,9 +1065,9 @@ function buildNodeReexports(srcDir, entryFile) {
927
1065
  }
928
1066
  const bySource = /* @__PURE__ */ new Map();
929
1067
  for (const ref of schemaRefs) {
930
- const resolvedSource = path3.relative(
931
- path3.dirname(entryFile),
932
- path3.resolve(path3.dirname(file), ref.importSource)
1068
+ const resolvedSource = path4.relative(
1069
+ path4.dirname(entryFile),
1070
+ path4.resolve(path4.dirname(file), ref.importSource)
933
1071
  ).replace(/\\/g, "/");
934
1072
  const sourceSpecifier = resolvedSource.startsWith(".") ? resolvedSource : `./${resolvedSource}`;
935
1073
  if (!bySource.has(sourceSpecifier)) bySource.set(sourceSpecifier, []);
@@ -943,12 +1081,12 @@ function buildNodeReexports(srcDir, entryFile) {
943
1081
  }
944
1082
  function typeGenerator(options) {
945
1083
  const { srcDir, outDir, entryFiles } = options;
946
- const serverTsconfig = path3.resolve(srcDir, "tsconfig.json");
947
- const rootTsconfig = path3.resolve("tsconfig.json");
948
- const userTsconfig = fs3.existsSync(serverTsconfig) ? serverTsconfig : rootTsconfig;
949
- const clientDir = path3.relative(
1084
+ const serverTsconfig = path4.resolve(srcDir, "tsconfig.json");
1085
+ const rootTsconfig = path4.resolve("tsconfig.json");
1086
+ const userTsconfig = fs5.existsSync(serverTsconfig) ? serverTsconfig : rootTsconfig;
1087
+ const clientDir = path4.relative(
950
1088
  process.cwd(),
951
- path3.join(path3.dirname(srcDir), "client")
1089
+ path4.join(path4.dirname(srcDir), "client")
952
1090
  );
953
1091
  const augmentedContents = /* @__PURE__ */ new Map();
954
1092
  let origReadFile = null;
@@ -960,9 +1098,9 @@ function typeGenerator(options) {
960
1098
  for (const entryFile of entryFiles) {
961
1099
  const reexports = buildNodeReexports(srcDir, entryFile);
962
1100
  if (!reexports) continue;
963
- const original = fs3.readFileSync(entryFile, "utf-8");
1101
+ const original = fs5.readFileSync(entryFile, "utf-8");
964
1102
  augmentedContents.set(
965
- path3.resolve(entryFile),
1103
+ path4.resolve(entryFile),
966
1104
  `${original}
967
1105
  ${reexports}
968
1106
  `
@@ -971,7 +1109,7 @@ ${reexports}
971
1109
  if (augmentedContents.size === 0) return;
972
1110
  origReadFile = ts.sys.readFile.bind(ts.sys);
973
1111
  ts.sys.readFile = (fileName, encoding) => {
974
- const resolved = path3.resolve(fileName);
1112
+ const resolved = path4.resolve(fileName);
975
1113
  return augmentedContents.get(resolved) ?? origReadFile(fileName, encoding);
976
1114
  };
977
1115
  },
@@ -995,7 +1133,7 @@ ${reexports}
995
1133
  }
996
1134
  },
997
1135
  outDir,
998
- ...fs3.existsSync(userTsconfig) && { tsconfigPath: userTsconfig },
1136
+ ...fs5.existsSync(userTsconfig) && { tsconfigPath: userTsconfig },
999
1137
  compilerOptions: {
1000
1138
  noEmit: false,
1001
1139
  declaration: true,
@@ -1068,8 +1206,8 @@ function esmWrapper() {
1068
1206
  }
1069
1207
 
1070
1208
  // src/vite/server/plugins/package-json-generator.ts
1071
- import fs4 from "fs";
1072
- import path4 from "path";
1209
+ import fs6 from "fs";
1210
+ import path5 from "path";
1073
1211
  import { builtinModules as builtinModules2 } from "node:module";
1074
1212
  var nodeBuiltins = /* @__PURE__ */ new Set([
1075
1213
  ...builtinModules2,
@@ -1158,13 +1296,13 @@ function packageJsonGenerator(options) {
1158
1296
  }
1159
1297
  },
1160
1298
  closeBundle() {
1161
- const rootPackageJsonPath = path4.resolve("./package.json");
1162
- if (!fs4.existsSync(rootPackageJsonPath)) {
1299
+ const rootPackageJsonPath = path5.resolve("./package.json");
1300
+ if (!fs6.existsSync(rootPackageJsonPath)) {
1163
1301
  logger.warn(`package.json not found: ${rootPackageJsonPath}`);
1164
1302
  return;
1165
1303
  }
1166
1304
  const rootPackageJson = JSON.parse(
1167
- fs4.readFileSync(rootPackageJsonPath, "utf-8")
1305
+ fs6.readFileSync(rootPackageJsonPath, "utf-8")
1168
1306
  );
1169
1307
  const sourceDeps = rootPackageJson.dependencies ?? {};
1170
1308
  const peerDeps = rootPackageJson.peerDependencies ?? {};
@@ -1176,12 +1314,12 @@ function packageJsonGenerator(options) {
1176
1314
  if (sourceDeps[dep]) {
1177
1315
  distDependencies[dep] = sourceDeps[dep];
1178
1316
  } else {
1179
- const dependencyPackageJsonPath = path4.resolve(
1317
+ const dependencyPackageJsonPath = path5.resolve(
1180
1318
  `./node_modules/${dep}/package.json`
1181
1319
  );
1182
- if (fs4.existsSync(dependencyPackageJsonPath)) {
1320
+ if (fs6.existsSync(dependencyPackageJsonPath)) {
1183
1321
  const dependencyPackageJson = JSON.parse(
1184
- fs4.readFileSync(dependencyPackageJsonPath, "utf-8")
1322
+ fs6.readFileSync(dependencyPackageJsonPath, "utf-8")
1185
1323
  );
1186
1324
  distDependencies[dep] = `^${dependencyPackageJson.version}`;
1187
1325
  }
@@ -1218,11 +1356,11 @@ function packageJsonGenerator(options) {
1218
1356
  }
1219
1357
  }
1220
1358
  }
1221
- if (!fs4.existsSync(outDir)) {
1222
- fs4.mkdirSync(outDir, { recursive: true });
1359
+ if (!fs6.existsSync(outDir)) {
1360
+ fs6.mkdirSync(outDir, { recursive: true });
1223
1361
  }
1224
- fs4.writeFileSync(
1225
- path4.join(outDir, "package.json"),
1362
+ fs6.writeFileSync(
1363
+ path5.join(outDir, "package.json"),
1226
1364
  JSON.stringify(distPackageJson, null, 2)
1227
1365
  );
1228
1366
  }
@@ -1241,11 +1379,11 @@ async function build(serverOpts, buildContext) {
1241
1379
  nodeTarget = "node22"
1242
1380
  } = serverOpts;
1243
1381
  const entries = Array.isArray(entry) ? entry : [entry];
1244
- const resolvedSrcDir = path5.resolve(srcDir);
1382
+ const resolvedSrcDir = path6.resolve(srcDir);
1245
1383
  const entryPoints = {};
1246
1384
  for (const entry2 of entries) {
1247
- const entryFilePath = path5.join(resolvedSrcDir, entry2);
1248
- if (fs5.existsSync(entryFilePath)) {
1385
+ const entryFilePath = path6.join(resolvedSrcDir, entry2);
1386
+ if (fs7.existsSync(entryFilePath)) {
1249
1387
  const fileName = entry2.replace(/\.ts$/, "");
1250
1388
  entryPoints[fileName] = entryFilePath;
1251
1389
  }
@@ -1321,7 +1459,7 @@ module.exports = function (RED) {
1321
1459
  });
1322
1460
  };
1323
1461
  `;
1324
- fs5.writeFileSync(path5.join(buildContext.outDir, "index.js"), bridgeCode);
1462
+ fs7.writeFileSync(path6.join(buildContext.outDir, "index.js"), bridgeCode);
1325
1463
  }
1326
1464
  } catch (error) {
1327
1465
  throw new BuildError("server", error);
@@ -1331,12 +1469,12 @@ module.exports = function (RED) {
1331
1469
  // src/vite/client/build.ts
1332
1470
  import { build as viteBuild2 } from "vite";
1333
1471
  import vue from "@vitejs/plugin-vue";
1334
- import fs11 from "fs";
1335
- import path11 from "path";
1472
+ import fs13 from "fs";
1473
+ import path12 from "path";
1336
1474
 
1337
1475
  // src/vite/client/plugins/help-generator.ts
1338
- import fs6 from "fs";
1339
- import path6 from "path";
1476
+ import fs8 from "fs";
1477
+ import path7 from "path";
1340
1478
  import { pathToFileURL as pathToFileURL2 } from "url";
1341
1479
  import { createRequire as createRequire2 } from "module";
1342
1480
 
@@ -1658,9 +1796,9 @@ ${table}
1658
1796
  `;
1659
1797
  }
1660
1798
  function loadNodeLabels(labelPath) {
1661
- if (!fs6.existsSync(labelPath)) return {};
1799
+ if (!fs8.existsSync(labelPath)) return {};
1662
1800
  try {
1663
- const raw = JSON.parse(fs6.readFileSync(labelPath, "utf-8"));
1801
+ const raw = JSON.parse(fs8.readFileSync(labelPath, "utf-8"));
1664
1802
  return {
1665
1803
  description: raw.description,
1666
1804
  configs: raw.configs,
@@ -1702,10 +1840,10 @@ function generateHelpDoc(nodeClass, labels, t) {
1702
1840
  if (inputSection) lines.push(inputSection);
1703
1841
  }
1704
1842
  if (nodeClass.outputsSchema) {
1705
- const os2 = nodeClass.outputsSchema;
1706
- if (Array.isArray(os2)) {
1843
+ const os3 = nodeClass.outputsSchema;
1844
+ if (Array.isArray(os3)) {
1707
1845
  const portSections = [];
1708
- os2.forEach((schema, i) => {
1846
+ os3.forEach((schema, i) => {
1709
1847
  const title = `${t.sections.port} ${i + 1}`;
1710
1848
  const portPropLabels = labels.outputs?.[i];
1711
1849
  const section = generateSchemaSection({
@@ -1724,9 +1862,9 @@ function generateHelpDoc(nodeClass, labels, t) {
1724
1862
  ${portSections.join("\n")}`
1725
1863
  );
1726
1864
  }
1727
- } else if (!("type" in os2 || "properties" in os2)) {
1865
+ } else if (!("type" in os3 || "properties" in os3)) {
1728
1866
  const portSections = [];
1729
- for (const [portName, schema] of Object.entries(os2)) {
1867
+ for (const [portName, schema] of Object.entries(os3)) {
1730
1868
  const portPropLabels = labels.outputs?.[portName];
1731
1869
  const section = generateSchemaSection({
1732
1870
  title: portName,
@@ -1748,7 +1886,7 @@ ${portSections.join("\n")}`
1748
1886
  const outputPropLabels = labels.outputs?.[0];
1749
1887
  const section = generateSchemaSection({
1750
1888
  title: t.sections.output,
1751
- schema: os2,
1889
+ schema: os3,
1752
1890
  t,
1753
1891
  labels: outputPropLabels,
1754
1892
  includeDefault: false
@@ -1759,9 +1897,9 @@ ${portSections.join("\n")}`
1759
1897
  return lines.join("\n").trim();
1760
1898
  }
1761
1899
  function discoverLanguages(labelsDir, nodeType) {
1762
- const nodeLabelsDir = path6.join(labelsDir, nodeType);
1763
- if (!fs6.existsSync(nodeLabelsDir)) return [];
1764
- return fs6.readdirSync(nodeLabelsDir).filter((f) => f.endsWith(".json")).map((f) => path6.basename(f, ".json"));
1900
+ const nodeLabelsDir = path7.join(labelsDir, nodeType);
1901
+ if (!fs8.existsSync(nodeLabelsDir)) return [];
1902
+ return fs8.readdirSync(nodeLabelsDir).filter((f) => f.endsWith(".json")).map((f) => path7.basename(f, ".json"));
1765
1903
  }
1766
1904
  function helpGenerator(options) {
1767
1905
  const { outDir, localesOutDir, docsDir, labelsDir } = options;
@@ -1770,15 +1908,15 @@ function helpGenerator(options) {
1770
1908
  apply: "build",
1771
1909
  enforce: "post",
1772
1910
  async closeBundle() {
1773
- const esmPath = path6.resolve(outDir, "index.mjs");
1774
- const cjsPath = path6.resolve(outDir, "index.js");
1911
+ const esmPath = path7.resolve(outDir, "index.mjs");
1912
+ const cjsPath = path7.resolve(outDir, "index.js");
1775
1913
  let packageFn;
1776
1914
  try {
1777
- if (fs6.existsSync(esmPath)) {
1915
+ if (fs8.existsSync(esmPath)) {
1778
1916
  const fileUrl = pathToFileURL2(esmPath).href + `?t=${Date.now()}`;
1779
1917
  const mod = await import(fileUrl);
1780
1918
  packageFn = mod?.default ?? mod;
1781
- } else if (fs6.existsSync(cjsPath)) {
1919
+ } else if (fs8.existsSync(cjsPath)) {
1782
1920
  const require2 = createRequire2(import.meta.url);
1783
1921
  delete require2.cache[cjsPath];
1784
1922
  const rawMod = require2(cjsPath);
@@ -1795,10 +1933,10 @@ function helpGenerator(options) {
1795
1933
  const languages = discoverLanguages(labelsDir, type);
1796
1934
  if (!languages.includes("en-US")) languages.push("en-US");
1797
1935
  for (const lang of languages) {
1798
- const manualMd = path6.join(docsDir, type, `${lang}.md`);
1799
- const manualHtml = path6.join(docsDir, type, `${lang}.html`);
1800
- if (fs6.existsSync(manualMd) || fs6.existsSync(manualHtml)) continue;
1801
- const labelPath = path6.join(labelsDir, type, `${lang}.json`);
1936
+ const manualMd = path7.join(docsDir, type, `${lang}.md`);
1937
+ const manualHtml = path7.join(docsDir, type, `${lang}.html`);
1938
+ if (fs8.existsSync(manualMd) || fs8.existsSync(manualHtml)) continue;
1939
+ const labelPath = path7.join(labelsDir, type, `${lang}.json`);
1802
1940
  const labels = loadNodeLabels(labelPath);
1803
1941
  const t = getHelpTranslations(lang);
1804
1942
  const content = generateHelpDoc(NodeClass, labels, t);
@@ -1812,11 +1950,11 @@ ${content}
1812
1950
  }
1813
1951
  }
1814
1952
  for (const [lang, scripts] of helpByLang) {
1815
- const langDir = path6.join(localesOutDir, lang);
1816
- fs6.mkdirSync(langDir, { recursive: true });
1817
- const indexPath = path6.join(langDir, "index.html");
1818
- const existing = fs6.existsSync(indexPath) ? fs6.readFileSync(indexPath, "utf-8") : "";
1819
- fs6.writeFileSync(
1953
+ const langDir = path7.join(localesOutDir, lang);
1954
+ fs8.mkdirSync(langDir, { recursive: true });
1955
+ const indexPath = path7.join(langDir, "index.html");
1956
+ const existing = fs8.existsSync(indexPath) ? fs8.readFileSync(indexPath, "utf-8") : "";
1957
+ fs8.writeFileSync(
1820
1958
  indexPath,
1821
1959
  existing + (existing ? "\n" : "") + scripts.join("\n"),
1822
1960
  "utf-8"
@@ -1828,8 +1966,8 @@ ${content}
1828
1966
 
1829
1967
  // src/vite/client/plugins/html-generator.ts
1830
1968
  import mime from "mime-types";
1831
- import fs7 from "fs";
1832
- import path7 from "path";
1969
+ import fs9 from "fs";
1970
+ import path8 from "path";
1833
1971
  function htmlGenerator(options) {
1834
1972
  const { packageName, licensePath } = options;
1835
1973
  return {
@@ -1839,7 +1977,7 @@ function htmlGenerator(options) {
1839
1977
  generateBundle(_, bundle) {
1840
1978
  const resourcesTags = Object.keys(bundle).map((fileName) => {
1841
1979
  const asset = bundle[fileName];
1842
- const srcPath = path7.join(
1980
+ const srcPath = path8.join(
1843
1981
  "resources",
1844
1982
  packageName,
1845
1983
  fileName.replace(/^resources\/?/, "")
@@ -1867,8 +2005,8 @@ function htmlGenerator(options) {
1867
2005
  return null;
1868
2006
  }
1869
2007
  }).filter(Boolean).join("\n");
1870
- const licenseBanner = licensePath && fs7.existsSync(licensePath) ? `<!--
1871
- ${fs7.readFileSync(licensePath, "utf-8")}
2008
+ const licenseBanner = licensePath && fs9.existsSync(licensePath) ? `<!--
2009
+ ${fs9.readFileSync(licensePath, "utf-8")}
1872
2010
  -->` : "";
1873
2011
  this.emitFile({
1874
2012
  type: "asset",
@@ -1881,8 +2019,8 @@ ${resourcesTags}`
1881
2019
  }
1882
2020
 
1883
2021
  // src/vite/client/plugins/locales-generator.ts
1884
- import fs8 from "fs";
1885
- import path8 from "path";
2022
+ import fs10 from "fs";
2023
+ import path9 from "path";
1886
2024
  import { merge } from "es-toolkit";
1887
2025
  function localesGenerator(options) {
1888
2026
  const { outDir, docsDir, labelsDir } = options;
@@ -1900,103 +2038,113 @@ function localesGenerator(options) {
1900
2038
  ];
1901
2039
  const frameworkLabels = {
1902
2040
  "en-US": {
1903
- configs: { name: "Name" },
2041
+ configs: { name: "Name", returnProperty: "Return key" },
1904
2042
  toggles: {
1905
2043
  validateInput: "Validate Input",
1906
2044
  validateOutput: "Validate Output",
1907
2045
  errorPort: "Error Port",
1908
2046
  completePort: "Complete Port",
1909
- statusPort: "Status Port"
2047
+ statusPort: "Status Port",
2048
+ returnPropertyOverride: "Override return prop key"
1910
2049
  }
1911
2050
  },
1912
2051
  de: {
1913
- configs: { name: "Name" },
2052
+ configs: { name: "Name", returnProperty: "R\xFCckgabe-Schl\xFCssel" },
1914
2053
  toggles: {
1915
2054
  validateInput: "Eingabe validieren",
1916
2055
  validateOutput: "Ausgabe validieren",
1917
2056
  errorPort: "Fehler-Port",
1918
2057
  completePort: "Abschluss-Port",
1919
- statusPort: "Status-Port"
2058
+ statusPort: "Status-Port",
2059
+ returnPropertyOverride: "R\xFCckgabe-Schl\xFCssel \xFCberschreiben"
1920
2060
  }
1921
2061
  },
1922
2062
  "es-ES": {
1923
- configs: { name: "Nombre" },
2063
+ configs: { name: "Nombre", returnProperty: "Clave de retorno" },
1924
2064
  toggles: {
1925
2065
  validateInput: "Validar entrada",
1926
2066
  validateOutput: "Validar salida",
1927
2067
  errorPort: "Puerto de error",
1928
2068
  completePort: "Puerto de completado",
1929
- statusPort: "Puerto de estado"
2069
+ statusPort: "Puerto de estado",
2070
+ returnPropertyOverride: "Sobrescribir clave de retorno"
1930
2071
  }
1931
2072
  },
1932
2073
  fr: {
1933
- configs: { name: "Nom" },
2074
+ configs: { name: "Nom", returnProperty: "Cl\xE9 de retour" },
1934
2075
  toggles: {
1935
2076
  validateInput: "Valider l'entr\xE9e",
1936
2077
  validateOutput: "Valider la sortie",
1937
2078
  errorPort: "Port d'erreur",
1938
2079
  completePort: "Port de compl\xE9tion",
1939
- statusPort: "Port de statut"
2080
+ statusPort: "Port de statut",
2081
+ returnPropertyOverride: "Remplacer la cl\xE9 de retour"
1940
2082
  }
1941
2083
  },
1942
2084
  ko: {
1943
- configs: { name: "\uC774\uB984" },
2085
+ configs: { name: "\uC774\uB984", returnProperty: "\uBC18\uD658 \uD0A4" },
1944
2086
  toggles: {
1945
2087
  validateInput: "\uC785\uB825 \uAC80\uC99D",
1946
2088
  validateOutput: "\uCD9C\uB825 \uAC80\uC99D",
1947
2089
  errorPort: "\uC624\uB958 \uD3EC\uD2B8",
1948
2090
  completePort: "\uC644\uB8CC \uD3EC\uD2B8",
1949
- statusPort: "\uC0C1\uD0DC \uD3EC\uD2B8"
2091
+ statusPort: "\uC0C1\uD0DC \uD3EC\uD2B8",
2092
+ returnPropertyOverride: "\uBC18\uD658 \uD0A4 \uC7AC\uC815\uC758"
1950
2093
  }
1951
2094
  },
1952
2095
  "pt-BR": {
1953
- configs: { name: "Nome" },
2096
+ configs: { name: "Nome", returnProperty: "Chave de retorno" },
1954
2097
  toggles: {
1955
2098
  validateInput: "Validar Entrada",
1956
2099
  validateOutput: "Validar Sa\xEDda",
1957
2100
  errorPort: "Porta de Erro",
1958
2101
  completePort: "Porta de Conclus\xE3o",
1959
- statusPort: "Porta de Status"
2102
+ statusPort: "Porta de Status",
2103
+ returnPropertyOverride: "Sobrescrever chave de retorno"
1960
2104
  }
1961
2105
  },
1962
2106
  ru: {
1963
- configs: { name: "\u0418\u043C\u044F" },
2107
+ configs: { name: "\u0418\u043C\u044F", returnProperty: "\u041A\u043B\u044E\u0447 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0430" },
1964
2108
  toggles: {
1965
2109
  validateInput: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u0445\u043E\u0434",
1966
2110
  validateOutput: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u044B\u0445\u043E\u0434",
1967
2111
  errorPort: "\u041F\u043E\u0440\u0442 \u043E\u0448\u0438\u0431\u043A\u0438",
1968
2112
  completePort: "\u041F\u043E\u0440\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F",
1969
- statusPort: "\u041F\u043E\u0440\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0430"
2113
+ statusPort: "\u041F\u043E\u0440\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0430",
2114
+ returnPropertyOverride: "\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0430"
1970
2115
  }
1971
2116
  },
1972
2117
  ja: {
1973
- configs: { name: "\u540D\u524D" },
2118
+ configs: { name: "\u540D\u524D", returnProperty: "\u623B\u308A\u30AD\u30FC" },
1974
2119
  toggles: {
1975
2120
  validateInput: "\u5165\u529B\u691C\u8A3C",
1976
2121
  validateOutput: "\u51FA\u529B\u691C\u8A3C",
1977
2122
  errorPort: "\u30A8\u30E9\u30FC\u30DD\u30FC\u30C8",
1978
2123
  completePort: "\u5B8C\u4E86\u30DD\u30FC\u30C8",
1979
- statusPort: "\u30B9\u30C6\u30FC\u30BF\u30B9\u30DD\u30FC\u30C8"
2124
+ statusPort: "\u30B9\u30C6\u30FC\u30BF\u30B9\u30DD\u30FC\u30C8",
2125
+ returnPropertyOverride: "\u623B\u308A\u30AD\u30FC\u3092\u4E0A\u66F8\u304D"
1980
2126
  }
1981
2127
  },
1982
2128
  "zh-CN": {
1983
- configs: { name: "\u540D\u79F0" },
2129
+ configs: { name: "\u540D\u79F0", returnProperty: "\u8FD4\u56DE\u952E" },
1984
2130
  toggles: {
1985
2131
  validateInput: "\u9A8C\u8BC1\u8F93\u5165",
1986
2132
  validateOutput: "\u9A8C\u8BC1\u8F93\u51FA",
1987
2133
  errorPort: "\u9519\u8BEF\u7AEF\u53E3",
1988
2134
  completePort: "\u5B8C\u6210\u7AEF\u53E3",
1989
- statusPort: "\u72B6\u6001\u7AEF\u53E3"
2135
+ statusPort: "\u72B6\u6001\u7AEF\u53E3",
2136
+ returnPropertyOverride: "\u8986\u76D6\u8FD4\u56DE\u952E"
1990
2137
  }
1991
2138
  },
1992
2139
  "zh-TW": {
1993
- configs: { name: "\u540D\u7A31" },
2140
+ configs: { name: "\u540D\u7A31", returnProperty: "\u8FD4\u56DE\u9375" },
1994
2141
  toggles: {
1995
2142
  validateInput: "\u9A57\u8B49\u8F38\u5165",
1996
2143
  validateOutput: "\u9A57\u8B49\u8F38\u51FA",
1997
2144
  errorPort: "\u932F\u8AA4\u7AEF\u53E3",
1998
2145
  completePort: "\u5B8C\u6210\u7AEF\u53E3",
1999
- statusPort: "\u72C0\u614B\u7AEF\u53E3"
2146
+ statusPort: "\u72C0\u614B\u7AEF\u53E3",
2147
+ returnPropertyOverride: "\u8986\u84CB\u8FD4\u56DE\u9375"
2000
2148
  }
2001
2149
  }
2002
2150
  };
@@ -2015,17 +2163,17 @@ Supported: ${languages.join(", ")}`
2015
2163
  }
2016
2164
  function forEachFile(baseDir, fileExtensions, processFile) {
2017
2165
  const langMap = /* @__PURE__ */ new Map();
2018
- if (!fs8.existsSync(baseDir)) return langMap;
2019
- const nodeDirs = fs8.readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2166
+ if (!fs10.existsSync(baseDir)) return langMap;
2167
+ const nodeDirs = fs10.readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2020
2168
  for (const nodeDir of nodeDirs) {
2021
2169
  const nodeType = nodeDir.name;
2022
- const nodePath = path8.join(baseDir, nodeType);
2023
- const files = fs8.readdirSync(nodePath);
2170
+ const nodePath = path9.join(baseDir, nodeType);
2171
+ const files = fs10.readdirSync(nodePath);
2024
2172
  for (const file of files) {
2025
- const ext = path8.extname(file);
2173
+ const ext = path9.extname(file);
2026
2174
  if (!fileExtensions.includes(ext)) continue;
2027
- const lang = path8.basename(file, ext);
2028
- const filePath = path8.join(nodePath, file);
2175
+ const lang = path9.basename(file, ext);
2176
+ const filePath = path9.join(nodePath, file);
2029
2177
  validateLanguage(lang, filePath);
2030
2178
  const value = processFile({ ext, filePath, nodeType });
2031
2179
  if (value == null) continue;
@@ -2043,10 +2191,10 @@ Supported: ${languages.join(", ")}`
2043
2191
  }
2044
2192
  function writeOutput(langMap, fileName, serialize) {
2045
2193
  for (const [lang, data] of langMap.entries()) {
2046
- const langOutDir = path8.join(outDir, lang);
2047
- fs8.mkdirSync(langOutDir, { recursive: true });
2048
- fs8.writeFileSync(
2049
- path8.join(langOutDir, fileName),
2194
+ const langOutDir = path9.join(outDir, lang);
2195
+ fs10.mkdirSync(langOutDir, { recursive: true });
2196
+ fs10.writeFileSync(
2197
+ path9.join(langOutDir, fileName),
2050
2198
  serialize(data),
2051
2199
  "utf-8"
2052
2200
  );
@@ -2058,7 +2206,7 @@ Supported: ${languages.join(", ")}`
2058
2206
  ({ ext, filePath, nodeType }) => {
2059
2207
  const type = ext === ".html" ? "text/html" : ext === ".md" ? "text/markdown" : null;
2060
2208
  if (!type) return null;
2061
- const content = fs8.readFileSync(filePath, "utf-8");
2209
+ const content = fs10.readFileSync(filePath, "utf-8");
2062
2210
  return [
2063
2211
  `<script type="${type}" data-help-name="${nodeType}">
2064
2212
  ${content}
@@ -2075,7 +2223,7 @@ ${content}
2075
2223
  labelsDir,
2076
2224
  [".json"],
2077
2225
  ({ filePath, nodeType }) => {
2078
- const parsed = JSON.parse(fs8.readFileSync(filePath, "utf-8"));
2226
+ const parsed = JSON.parse(fs10.readFileSync(filePath, "utf-8"));
2079
2227
  if (parsed[nodeType] && typeof parsed[nodeType] === "object") {
2080
2228
  console.warn(
2081
2229
  `[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`
@@ -2125,8 +2273,8 @@ function minifier() {
2125
2273
  // src/vite/client/plugins/node-definitions-inliner.ts
2126
2274
  import { createRequire as createRequire3 } from "module";
2127
2275
  import { pathToFileURL as pathToFileURL3 } from "url";
2128
- import path9 from "path";
2129
- import fs9 from "fs";
2276
+ import path10 from "path";
2277
+ import fs11 from "fs";
2130
2278
  import mime2 from "mime-types";
2131
2279
  var VIRTUAL_ID = "virtual:nrg/node-definitions";
2132
2280
  var RESOLVED_ID = "\0" + VIRTUAL_ID;
@@ -2157,9 +2305,9 @@ function getCredentialsFromSchema(schema) {
2157
2305
  return result;
2158
2306
  }
2159
2307
  function resolveIcon(iconsDir, type) {
2160
- if (!fs9.existsSync(iconsDir)) return void 0;
2161
- return fs9.readdirSync(iconsDir).find((f) => {
2162
- if (path9.basename(f, path9.extname(f)) !== type) return false;
2308
+ if (!fs11.existsSync(iconsDir)) return void 0;
2309
+ return fs11.readdirSync(iconsDir).find((f) => {
2310
+ if (path10.basename(f, path10.extname(f)) !== type) return false;
2163
2311
  const mimeType = mime2.lookup(f);
2164
2312
  return mimeType !== false && mimeType.startsWith("image/");
2165
2313
  });
@@ -2167,7 +2315,7 @@ function resolveIcon(iconsDir, type) {
2167
2315
  function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir, nodesDir, hasUserEntry = true) {
2168
2316
  let _nodeTypes = [];
2169
2317
  let _definitions = {};
2170
- const cacheDir = path9.resolve("node_modules", ".nrg", "client");
2318
+ const cacheDir = path10.resolve("node_modules", ".nrg", "client");
2171
2319
  return {
2172
2320
  name: "vite-plugin-node-red:client:node-definitions-inliner",
2173
2321
  enforce: "pre",
@@ -2176,14 +2324,14 @@ function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir
2176
2324
  async buildStart() {
2177
2325
  _nodeTypes = [];
2178
2326
  _definitions = {};
2179
- const esmEntryPath = path9.resolve(serverOutDir, "index.mjs");
2180
- const cjsEntryPath = path9.resolve(serverOutDir, "index.js");
2327
+ const esmEntryPath = path10.resolve(serverOutDir, "index.mjs");
2328
+ const cjsEntryPath = path10.resolve(serverOutDir, "index.js");
2181
2329
  let packageFn;
2182
- if (fs9.existsSync(esmEntryPath)) {
2330
+ if (fs11.existsSync(esmEntryPath)) {
2183
2331
  const fileUrl = pathToFileURL3(esmEntryPath).href + `?t=${Date.now()}`;
2184
2332
  const mod = await import(fileUrl);
2185
2333
  packageFn = mod?.default ?? mod;
2186
- } else if (fs9.existsSync(cjsEntryPath)) {
2334
+ } else if (fs11.existsSync(cjsEntryPath)) {
2187
2335
  const require2 = createRequire3(import.meta.url);
2188
2336
  delete require2.cache[cjsEntryPath];
2189
2337
  const rawMod = require2(cjsEntryPath);
@@ -2224,14 +2372,14 @@ function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir
2224
2372
  };
2225
2373
  }
2226
2374
  if (!hasUserEntry) {
2227
- const nodesCache = path9.resolve(cacheDir, "nodes");
2228
- if (fs9.existsSync(nodesCache)) {
2229
- fs9.rmSync(nodesCache, { recursive: true });
2375
+ const nodesCache = path10.resolve(cacheDir, "nodes");
2376
+ if (fs11.existsSync(nodesCache)) {
2377
+ fs11.rmSync(nodesCache, { recursive: true });
2230
2378
  }
2231
- fs9.mkdirSync(nodesCache, { recursive: true });
2379
+ fs11.mkdirSync(nodesCache, { recursive: true });
2232
2380
  for (const type of _nodeTypes) {
2233
- const userTsPath = nodesDir ? path9.resolve(nodesDir, `${type}.ts`) : null;
2234
- if (userTsPath && fs9.existsSync(userTsPath)) continue;
2381
+ const userTsPath = nodesDir ? path10.resolve(nodesDir, `${type}.ts`) : null;
2382
+ if (userTsPath && fs11.existsSync(userTsPath)) continue;
2235
2383
  const content = [
2236
2384
  `// auto-generated by nrg`,
2237
2385
  `import { defineNode } from "@bonsae/nrg/client";`,
@@ -2241,13 +2389,13 @@ function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir
2241
2389
  `});`,
2242
2390
  ``
2243
2391
  ].join("\n");
2244
- fs9.writeFileSync(path9.resolve(nodesCache, `${type}.ts`), content);
2392
+ fs11.writeFileSync(path10.resolve(nodesCache, `${type}.ts`), content);
2245
2393
  }
2246
2394
  const entryContent = generateEntryCode("");
2247
- fs9.mkdirSync(path9.dirname(path9.resolve(cacheDir, "index.ts")), {
2395
+ fs11.mkdirSync(path10.dirname(path10.resolve(cacheDir, "index.ts")), {
2248
2396
  recursive: true
2249
2397
  });
2250
- fs9.writeFileSync(path9.resolve(cacheDir, "index.ts"), entryContent);
2398
+ fs11.writeFileSync(path10.resolve(cacheDir, "index.ts"), entryContent);
2251
2399
  }
2252
2400
  },
2253
2401
  resolveId(id) {
@@ -2270,12 +2418,12 @@ function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir
2270
2418
  const nrgImports = /* @__PURE__ */ new Set(["__setSchemas"]);
2271
2419
  const lines = [`import __nrgSchemas from "${VIRTUAL_ID}";`];
2272
2420
  const postLines = [`__setSchemas(__nrgSchemas);`];
2273
- if (componentsDir && fs9.existsSync(componentsDir)) {
2421
+ if (componentsDir && fs11.existsSync(componentsDir)) {
2274
2422
  const formImports = [];
2275
2423
  const formEntries = [];
2276
2424
  for (const type of _nodeTypes) {
2277
- const componentPath = path9.resolve(componentsDir, `${type}.vue`);
2278
- if (fs9.existsSync(componentPath)) {
2425
+ const componentPath = path10.resolve(componentsDir, `${type}.vue`);
2426
+ if (fs11.existsSync(componentPath)) {
2279
2427
  const varName = `__nrgForm_${type.replace(/-/g, "_")}`;
2280
2428
  formImports.push(
2281
2429
  `import ${varName} from ${JSON.stringify(componentPath)};`
@@ -2290,12 +2438,12 @@ function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir
2290
2438
  }
2291
2439
  }
2292
2440
  if (!hasUserEntry) {
2293
- const nodesCache = path9.resolve(cacheDir, "nodes");
2441
+ const nodesCache = path10.resolve(cacheDir, "nodes");
2294
2442
  const defVarNames = [];
2295
2443
  for (const type of _nodeTypes) {
2296
2444
  const varName = `__nrgNodeDef_${type.replace(/-/g, "_")}`;
2297
- const userTsPath = nodesDir ? path9.resolve(nodesDir, `${type}.ts`) : null;
2298
- const tsPath = userTsPath && fs9.existsSync(userTsPath) ? userTsPath : path9.resolve(nodesCache, `${type}.ts`);
2445
+ const userTsPath = nodesDir ? path10.resolve(nodesDir, `${type}.ts`) : null;
2446
+ const tsPath = userTsPath && fs11.existsSync(userTsPath) ? userTsPath : path10.resolve(nodesCache, `${type}.ts`);
2299
2447
  lines.push(`import ${varName} from ${JSON.stringify(tsPath)};`);
2300
2448
  defVarNames.push(varName);
2301
2449
  }
@@ -2313,8 +2461,8 @@ function nodeDefinitionsInliner(serverOutDir, entryPath, iconsDir, componentsDir
2313
2461
  }
2314
2462
 
2315
2463
  // src/vite/client/plugins/static-copy.ts
2316
- import fs10 from "fs";
2317
- import path10 from "path";
2464
+ import fs12 from "fs";
2465
+ import path11 from "path";
2318
2466
  function staticCopy(options) {
2319
2467
  const { targets } = options;
2320
2468
  return {
@@ -2323,23 +2471,23 @@ function staticCopy(options) {
2323
2471
  enforce: "post",
2324
2472
  closeBundle() {
2325
2473
  for (const { src, dest } of targets) {
2326
- if (!fs10.existsSync(src)) continue;
2327
- fs10.mkdirSync(dest, { recursive: true });
2328
- const stat = fs10.statSync(src);
2474
+ if (!fs12.existsSync(src)) continue;
2475
+ fs12.mkdirSync(dest, { recursive: true });
2476
+ const stat = fs12.statSync(src);
2329
2477
  if (stat.isDirectory()) {
2330
- const files = fs10.readdirSync(src);
2478
+ const files = fs12.readdirSync(src);
2331
2479
  for (const file of files) {
2332
- const srcFile = path10.join(src, file);
2333
- const destFile = path10.join(dest, file);
2334
- const fileStat = fs10.statSync(srcFile);
2480
+ const srcFile = path11.join(src, file);
2481
+ const destFile = path11.join(dest, file);
2482
+ const fileStat = fs12.statSync(srcFile);
2335
2483
  if (fileStat.isDirectory()) {
2336
- fs10.cpSync(srcFile, destFile, { recursive: true });
2484
+ fs12.cpSync(srcFile, destFile, { recursive: true });
2337
2485
  } else {
2338
- fs10.copyFileSync(srcFile, destFile);
2486
+ fs12.copyFileSync(srcFile, destFile);
2339
2487
  }
2340
2488
  }
2341
2489
  } else {
2342
- fs10.copyFileSync(src, dest);
2490
+ fs12.copyFileSync(src, dest);
2343
2491
  }
2344
2492
  }
2345
2493
  }
@@ -2360,74 +2508,74 @@ async function build2(clientBuildOptions, buildContext) {
2360
2508
  globals = {},
2361
2509
  manualChunks
2362
2510
  } = clientBuildOptions;
2363
- const physicalEntryPath = path11.resolve(srcDir, entry);
2511
+ const physicalEntryPath = path12.resolve(srcDir, entry);
2364
2512
  let entryPath;
2365
2513
  let generatedEntry = false;
2366
- if (fs11.existsSync(physicalEntryPath)) {
2514
+ if (fs13.existsSync(physicalEntryPath)) {
2367
2515
  entryPath = physicalEntryPath;
2368
2516
  } else {
2369
- const cacheDir = path11.resolve("node_modules", ".nrg", "client");
2370
- const cachedEntryPath = path11.resolve(cacheDir, entry);
2371
- if (!fs11.existsSync(cacheDir)) {
2372
- fs11.mkdirSync(cacheDir, { recursive: true });
2517
+ const cacheDir = path12.resolve("node_modules", ".nrg", "client");
2518
+ const cachedEntryPath = path12.resolve(cacheDir, entry);
2519
+ if (!fs13.existsSync(cacheDir)) {
2520
+ fs13.mkdirSync(cacheDir, { recursive: true });
2373
2521
  }
2374
- fs11.writeFileSync(cachedEntryPath, "// auto-generated entry\n");
2522
+ fs13.writeFileSync(cachedEntryPath, "// auto-generated entry\n");
2375
2523
  entryPath = cachedEntryPath;
2376
2524
  generatedEntry = true;
2377
2525
  }
2378
- const iconsDir = path11.resolve(
2379
- staticDirs.icons ?? path11.join(path11.dirname(path11.resolve(srcDir)), "icons")
2526
+ const iconsDir = path12.resolve(
2527
+ staticDirs.icons ?? path12.join(path12.dirname(path12.resolve(srcDir)), "icons")
2380
2528
  );
2381
2529
  const plugins = [
2382
2530
  vue(),
2383
2531
  nodeDefinitionsInliner(
2384
2532
  buildContext.outDir,
2385
2533
  entryPath,
2386
- fs11.existsSync(iconsDir) ? iconsDir : void 0,
2387
- path11.resolve(srcDir, "components"),
2388
- path11.resolve(srcDir, "nodes"),
2534
+ fs13.existsSync(iconsDir) ? iconsDir : void 0,
2535
+ path12.resolve(srcDir, "components"),
2536
+ path12.resolve(srcDir, "nodes"),
2389
2537
  !generatedEntry
2390
2538
  )
2391
2539
  ];
2392
2540
  plugins.push(
2393
2541
  htmlGenerator({
2394
2542
  packageName: buildContext.packageName,
2395
- licensePath: licensePath ? path11.resolve(licensePath) : void 0
2543
+ licensePath: licensePath ? path12.resolve(licensePath) : void 0
2396
2544
  })
2397
2545
  );
2398
2546
  if (locales) {
2399
2547
  const { docsDir = "./locales/docs", labelsDir = "./locales/labels" } = locales;
2400
- const localesOutDir = path11.join(buildContext.outDir, "locales");
2548
+ const localesOutDir = path12.join(buildContext.outDir, "locales");
2401
2549
  plugins.push(
2402
2550
  localesGenerator({
2403
2551
  outDir: localesOutDir,
2404
- docsDir: path11.resolve(docsDir),
2405
- labelsDir: path11.resolve(labelsDir)
2552
+ docsDir: path12.resolve(docsDir),
2553
+ labelsDir: path12.resolve(labelsDir)
2406
2554
  })
2407
2555
  );
2408
2556
  plugins.push(
2409
2557
  helpGenerator({
2410
2558
  outDir: buildContext.outDir,
2411
2559
  localesOutDir,
2412
- docsDir: path11.resolve(docsDir),
2413
- labelsDir: path11.resolve(labelsDir)
2560
+ docsDir: path12.resolve(docsDir),
2561
+ labelsDir: path12.resolve(labelsDir)
2414
2562
  })
2415
2563
  );
2416
2564
  }
2417
2565
  const copyTargets = [];
2418
- const publicDir = path11.resolve(
2419
- staticDirs.public ?? path11.join(srcDir, "public")
2566
+ const publicDir = path12.resolve(
2567
+ staticDirs.public ?? path12.join(srcDir, "public")
2420
2568
  );
2421
- if (fs11.existsSync(publicDir)) {
2569
+ if (fs13.existsSync(publicDir)) {
2422
2570
  copyTargets.push({
2423
2571
  src: publicDir,
2424
- dest: path11.join(buildContext.outDir, "resources")
2572
+ dest: path12.join(buildContext.outDir, "resources")
2425
2573
  });
2426
2574
  }
2427
- if (fs11.existsSync(iconsDir)) {
2575
+ if (fs13.existsSync(iconsDir)) {
2428
2576
  copyTargets.push({
2429
2577
  src: iconsDir,
2430
- dest: path11.join(buildContext.outDir, "icons")
2578
+ dest: path12.join(buildContext.outDir, "icons")
2431
2579
  });
2432
2580
  }
2433
2581
  if (copyTargets.length > 0) {
@@ -2455,10 +2603,10 @@ async function build2(clientBuildOptions, buildContext) {
2455
2603
  configFile: false,
2456
2604
  logLevel: "warn",
2457
2605
  base: `/resources/${buildContext.packageName}`,
2458
- publicDir: path11.resolve(srcDir, "public"),
2606
+ publicDir: path12.resolve(srcDir, "public"),
2459
2607
  resolve: {
2460
2608
  alias: {
2461
- "@": path11.resolve(srcDir)
2609
+ "@": path12.resolve(srcDir)
2462
2610
  }
2463
2611
  },
2464
2612
  plugins,
@@ -2512,8 +2660,8 @@ async function build2(clientBuildOptions, buildContext) {
2512
2660
  throw new BuildError("client", error);
2513
2661
  } finally {
2514
2662
  if (generatedEntry) {
2515
- if (fs11.existsSync(entryPath)) {
2516
- fs11.unlinkSync(entryPath);
2663
+ if (fs13.existsSync(entryPath)) {
2664
+ fs13.unlinkSync(entryPath);
2517
2665
  }
2518
2666
  }
2519
2667
  }
@@ -2558,7 +2706,7 @@ function serverPlugin(options) {
2558
2706
  logger.stopSpinner("Copied extra files");
2559
2707
  }
2560
2708
  };
2561
- const start = async (clean = false) => {
2709
+ const start2 = async (clean = false) => {
2562
2710
  if (isStarting) {
2563
2711
  pendingStart = true;
2564
2712
  return;
@@ -2591,7 +2739,7 @@ function serverPlugin(options) {
2591
2739
  } finally {
2592
2740
  isStarting = false;
2593
2741
  if (pendingStart) {
2594
- start(false);
2742
+ start2(false);
2595
2743
  }
2596
2744
  }
2597
2745
  };
@@ -2664,7 +2812,7 @@ function serverPlugin(options) {
2664
2812
  async configureServer(viteServer) {
2665
2813
  server = viteServer;
2666
2814
  logger.intro();
2667
- await start(true);
2815
+ await start2(true);
2668
2816
  initialStartDone = true;
2669
2817
  printServerUrls(server.config.server.port ?? 5173, {
2670
2818
  actual: nodeRedPort,
@@ -2672,20 +2820,20 @@ function serverPlugin(options) {
2672
2820
  });
2673
2821
  logger.startGroup("Node-RED");
2674
2822
  nodeRedLauncher.flushLogs();
2675
- const serverSrcDir = path12.resolve(
2823
+ const serverSrcDir = path13.resolve(
2676
2824
  serverBuildOptions.srcDir ?? "./server"
2677
2825
  );
2678
- const clientSrcDir = path12.resolve(
2826
+ const clientSrcDir = path13.resolve(
2679
2827
  clientBuildOptions.srcDir ?? "./client"
2680
2828
  );
2681
- const localesDocsDir = path12.resolve(
2829
+ const localesDocsDir = path13.resolve(
2682
2830
  clientBuildOptions.locales?.docsDir ?? "./locales/docs"
2683
2831
  );
2684
- const localesLabelsDir = path12.resolve(
2832
+ const localesLabelsDir = path13.resolve(
2685
2833
  clientBuildOptions.locales?.labelsDir ?? "./locales/labels"
2686
2834
  );
2687
- const iconsDir = path12.resolve(
2688
- clientBuildOptions.staticDirs?.icons ?? path12.join(path12.dirname(clientSrcDir), "icons")
2835
+ const iconsDir = path13.resolve(
2836
+ clientBuildOptions.staticDirs?.icons ?? path13.join(path13.dirname(clientSrcDir), "icons")
2689
2837
  );
2690
2838
  const watchPaths = [
2691
2839
  serverSrcDir,
@@ -2700,12 +2848,12 @@ function serverPlugin(options) {
2700
2848
  ignored: ["**/node_modules/**", "**/dist/**", "**/.node-red/**"]
2701
2849
  });
2702
2850
  const debounceBeforeStart = debounce(
2703
- () => start(false),
2851
+ () => start2(false),
2704
2852
  nodeRedLauncher.restartDelay ?? 1e3
2705
2853
  );
2706
2854
  const handleFileChange = (file, event) => {
2707
2855
  if (!initialStartDone) return;
2708
- logger.info(`${event}: ${path12.relative(process.cwd(), file)}`);
2856
+ logger.info(`${event}: ${path13.relative(process.cwd(), file)}`);
2709
2857
  debounceBeforeStart();
2710
2858
  };
2711
2859
  watcher.on("change", (file) => handleFileChange(file, "Changed"));
@@ -2783,22 +2931,23 @@ function buildPlugin(options) {
2783
2931
  }
2784
2932
 
2785
2933
  // src/vite/plugin.ts
2786
- function nodeRed(options = {}) {
2787
- const { outDir = DEFAULT_OUTPUT_DIR } = options;
2934
+ function nrg(options = {}) {
2935
+ const { build: build3 = {}, server = {} } = options;
2936
+ const { outDir = DEFAULT_OUTPUT_DIR } = build3;
2788
2937
  const clientBuildOptions = mergeOptions(
2789
2938
  DEFAULT_CLIENT_BUILD_OPTIONS,
2790
- options.clientBuildOptions
2939
+ build3.client
2791
2940
  );
2792
2941
  const serverBuildOptions = mergeOptions(
2793
2942
  DEFAULT_SERVER_BUILD_OPTIONS,
2794
- options.serverBuildOptions
2943
+ build3.server
2795
2944
  );
2796
2945
  const nodeRedLauncherOptions = mergeOptions(
2797
2946
  DEFAULT_NODE_RED_LAUNCHER_OPTIONS,
2798
- options.nodeRedLauncherOptions
2947
+ server.nodeRed
2799
2948
  );
2800
- const extraFilesCopyTargets = options.extraFilesCopyTargets ?? DEFAULT_EXTRA_FILES_COPY_TARGETS;
2801
- const resolvedOutDir = path13.resolve(outDir);
2949
+ const extraFilesCopyTargets = build3.extraFilesCopyTargets ?? DEFAULT_EXTRA_FILES_COPY_TARGETS;
2950
+ const resolvedOutDir = path14.resolve(outDir);
2802
2951
  const buildContext = {
2803
2952
  outDir: resolvedOutDir,
2804
2953
  packageName: getPackageName(),
@@ -2825,5 +2974,5 @@ function nodeRed(options = {}) {
2825
2974
  ];
2826
2975
  }
2827
2976
  export {
2828
- nodeRed
2977
+ nrg
2829
2978
  };