@bonsae/nrg 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/test/index.js CHANGED
@@ -182,6 +182,7 @@ function createNodeRedNode(options = {}) {
182
182
  flow: flowCtx,
183
183
  global: globalCtx
184
184
  };
185
+ const handlers = /* @__PURE__ */ new Map();
185
186
  return {
186
187
  id: options.id ?? `node-${Math.random().toString(36).slice(2, 10)}`,
187
188
  type: options.type ?? "test-node",
@@ -195,7 +196,15 @@ function createNodeRedNode(options = {}) {
195
196
  log: vi.fn(),
196
197
  warn: vi.fn(),
197
198
  error: vi.fn(),
198
- on: vi.fn(),
199
+ on: vi.fn((event, handler) => {
200
+ if (!handlers.has(event)) handlers.set(event, []);
201
+ handlers.get(event).push(handler);
202
+ }),
203
+ emit: vi.fn(async (event, ...args) => {
204
+ for (const handler of handlers.get(event) ?? []) {
205
+ await handler(...args);
206
+ }
207
+ }),
199
208
  send: vi.fn(),
200
209
  status: vi.fn(),
201
210
  updateWires: vi.fn(),
@@ -385,6 +394,9 @@ function initValidator(RED) {
385
394
  });
386
395
  }
387
396
 
397
+ // src/core/server/nodes/symbols.ts
398
+ var WIRE_HANDLERS = Symbol.for("nrg.wireHandlers");
399
+
388
400
  // src/test/index.ts
389
401
  function buildConfig(NodeClass, userConfig = {}) {
390
402
  const defaults = {};
@@ -409,16 +421,23 @@ function attachHelpers(node, nodeRedNode) {
409
421
  nodeRedNode.status.mockImplementation((status) => {
410
422
  statusCalls.push(status);
411
423
  });
412
- const nodeRef = node;
413
424
  const helpers = {
414
425
  async receive(msg) {
415
426
  const sendFn = vi2.fn((outMsg) => {
416
427
  nodeRedNode.send(outMsg);
417
428
  });
418
- await nodeRef._input(msg, sendFn);
429
+ const doneFn = vi2.fn();
430
+ await nodeRedNode.emit("input", msg, sendFn, doneFn);
431
+ if (doneFn.mock.calls[0]?.[0] instanceof Error) {
432
+ throw doneFn.mock.calls[0][0];
433
+ }
419
434
  },
420
435
  async close(removed = false) {
421
- await nodeRef._closed(removed);
436
+ const doneFn = vi2.fn();
437
+ await nodeRedNode.emit("close", removed, doneFn);
438
+ if (doneFn.mock.calls[0]?.[0] instanceof Error) {
439
+ throw doneFn.mock.calls[0][0];
440
+ }
422
441
  },
423
442
  reset() {
424
443
  sentMessages.length = 0;
@@ -500,12 +519,13 @@ async function createNode(NodeClass, options = {}) {
500
519
  credentials,
501
520
  ...overrideOpts
502
521
  });
503
- await Promise.resolve(
504
- NodeClass._registered?.(RED) ?? NodeClass.registered?.(RED)
505
- );
522
+ NodeClass.validateSettings(RED);
523
+ await Promise.resolve(NodeClass.registered?.(RED));
506
524
  const node = new NodeClass(RED, nodeRedNode, config, credentials);
507
525
  const augmented = attachHelpers(node, nodeRedNode);
508
- await Promise.resolve(augmented.created?.());
526
+ const createdPromise = Promise.resolve(node.created?.());
527
+ node[WIRE_HANDLERS](nodeRedNode, createdPromise);
528
+ await createdPromise;
509
529
  return { node: augmented, RED };
510
530
  }
511
531
  export {
package/types/server.d.ts CHANGED
@@ -375,8 +375,18 @@ export interface TNodeRef<T = any> extends TSchema {
375
375
  type ResolveNodeRefs<T> = T extends TypedInput<any> ? T : T extends (...args: any[]) => any ? T : T extends Array<infer Item> ? ResolveNodeRefs<Item>[] : T extends object ? {
376
376
  [K in keyof T]: ResolveNodeRefs<T[K]>;
377
377
  } : T;
378
- /** Infers the TypeScript type from a schema, resolving node references to their instance types. */
379
- export type Infer<T extends TSchema> = ResolveNodeRefs<Static<T>>;
378
+ /**
379
+ * Infers the TypeScript type from a schema or a record of schemas.
380
+ *
381
+ * - Single schema: `Infer<typeof MySchema>` → the inferred message type
382
+ * - Record of schemas: `Infer<typeof outputsSchema>` → `{ portName: InferredType }` port map
383
+ *
384
+ * The record form produces a simple mapped type that resolves eagerly,
385
+ * giving `sendToPort()` proper autocomplete in class-based nodes.
386
+ */
387
+ export type Infer<T extends TSchema | Record<string, TSchema>> = T extends TSchema ? ResolveNodeRefs<Static<T>> : {
388
+ [K in keyof T & string]: T[K] extends TSchema ? ResolveNodeRefs<Static<T[K]>> : never;
389
+ };
380
390
  type TypedInputType = (typeof TYPED_INPUT_TYPES)[number];
381
391
  /** Schema type representing a Node-RED TypedInput (value + type pair). */
382
392
  export interface TTypedInput<T = unknown> extends TSchema {
@@ -390,7 +400,9 @@ export type Schema<T extends TProperties = TProperties> = TObject<T>;
390
400
  type InferOr<T, Fallback> = T extends TSchema ? Infer<T> : Fallback;
391
401
  type InferOutputs<T> = T extends readonly TSchema[] ? {
392
402
  [K in keyof T]: T[K] extends TSchema ? Infer<T[K]> : never;
393
- } : T extends TSchema ? Infer<T> : any;
403
+ } : T extends TSchema ? Infer<T> : T extends Record<string, TSchema> ? {
404
+ [K in keyof T & string]: Infer<T[K]>;
405
+ } : any;
394
406
  declare const NodeConfigSchema: import("@sinclair/typebox").TObject<{
395
407
  id: import("@sinclair/typebox").TString;
396
408
  type: import("@sinclair/typebox").TString;
@@ -414,6 +426,43 @@ declare const IONodeConfigSchema: import("@sinclair/typebox").TObject<{
414
426
  name: import("@sinclair/typebox").TString;
415
427
  z: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
416
428
  }>;
429
+ export declare const ErrorPortSchema: import("@sinclair/typebox").TObject<{
430
+ error: import("@sinclair/typebox").TObject<{
431
+ message: import("@sinclair/typebox").TString;
432
+ source: import("@sinclair/typebox").TObject<{
433
+ id: import("@sinclair/typebox").TString;
434
+ type: import("@sinclair/typebox").TString;
435
+ name: import("@sinclair/typebox").TString;
436
+ }>;
437
+ }>;
438
+ }>;
439
+ export declare const CompletePortSchema: import("@sinclair/typebox").TObject<{
440
+ complete: import("@sinclair/typebox").TObject<{
441
+ source: import("@sinclair/typebox").TObject<{
442
+ id: import("@sinclair/typebox").TString;
443
+ type: import("@sinclair/typebox").TString;
444
+ name: import("@sinclair/typebox").TString;
445
+ }>;
446
+ }>;
447
+ }>;
448
+ export declare const StatusPortSchema: import("@sinclair/typebox").TObject<{
449
+ status: import("@sinclair/typebox").TObject<{
450
+ fill: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[
451
+ import("@sinclair/typebox").TLiteral<"red">,
452
+ import("@sinclair/typebox").TLiteral<"green">
453
+ ]>>;
454
+ shape: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[
455
+ import("@sinclair/typebox").TLiteral<"dot">,
456
+ import("@sinclair/typebox").TLiteral<"string">
457
+ ]>>;
458
+ text: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
459
+ }>;
460
+ source: import("@sinclair/typebox").TObject<{
461
+ id: import("@sinclair/typebox").TString;
462
+ type: import("@sinclair/typebox").TString;
463
+ name: import("@sinclair/typebox").TString;
464
+ }>;
465
+ }>;
417
466
  declare function NodeRef<T extends new (...args: any[]) => any>(nodeClass: T, options?: NrgSchemaOptions): TNodeRef<InstanceType<T>>;
418
467
  declare function TypedInput$1<T = unknown>(options?: NrgSchemaOptions): TTypedInput<T>;
419
468
  /**
@@ -445,7 +494,7 @@ interface NodeContextStore {
445
494
  set<T = any>(key: string, value: T): Promise<void>;
446
495
  keys(): Promise<string[]>;
447
496
  }
448
- interface NodeConstructor<T = any, TConfig = any, TCredentials = any> {
497
+ export interface NodeConstructor<T = any, TConfig = any, TCredentials = any> {
449
498
  readonly type: string;
450
499
  readonly category: string;
451
500
  readonly color?: string;
@@ -456,12 +505,13 @@ interface NodeConstructor<T = any, TConfig = any, TCredentials = any> {
456
505
  readonly credentialsSchema?: Schema;
457
506
  readonly settingsSchema?: Schema;
458
507
  readonly inputSchema?: Schema;
459
- readonly outputsSchema?: Schema | Schema[];
508
+ readonly outputsSchema?: Schema | Schema[] | Record<string, Schema>;
460
509
  readonly validateInput?: boolean;
461
510
  readonly validateOutput?: boolean;
462
511
  readonly name: string;
463
512
  registered?(RED: RED): void | Promise<void>;
464
- _registered?(RED: RED): void | Promise<void>;
513
+ register(RED: RED): void | Promise<void>;
514
+ validateSettings(RED: RED): void;
465
515
  new (RED: RED, node: NodeRedNode, config: NodeConfig<TConfig>, credentials: NodeCredentials<TCredentials>): T;
466
516
  }
467
517
  type NodeConfig<TConfig = any> = TConfig & Static<typeof NodeConfigSchema>;
@@ -485,6 +535,7 @@ interface INode<TConfig = any, TCredentials = any, TSettings = any> {
485
535
  created?(): void | Promise<void>;
486
536
  closed?(removed?: boolean): void | Promise<void>;
487
537
  }
538
+ declare const WIRE_HANDLERS: unique symbol;
488
539
  /**
489
540
  * Abstract base class for all NRG nodes. Provides lifecycle hooks, config
490
541
  * validation, logging, timers, i18n, and settings management.
@@ -493,14 +544,19 @@ interface INode<TConfig = any, TCredentials = any, TSettings = any> {
493
544
  * for shared configuration nodes.
494
545
  */
495
546
  declare abstract class Node$1<TConfig = any, TCredentials = any, TSettings = any> implements INode<TConfig, TCredentials, TSettings> {
547
+ #private;
496
548
  static readonly type: string;
497
549
  static readonly category: "config" | string;
498
550
  static readonly configSchema?: Schema;
499
551
  static readonly credentialsSchema?: Schema;
500
552
  static readonly settingsSchema?: Schema;
501
- private static _cachedSettings;
502
553
  static registered?(RED: RED): void | Promise<void>;
503
554
  static validateSettings(RED: RED): void;
555
+ /**
556
+ * Registers this node class with Node-RED. Handles instance creation,
557
+ * event handler wiring, settings validation, and the user's registered() hook.
558
+ */
559
+ static register(RED: RED): Promise<void>;
504
560
  protected readonly RED: RED;
505
561
  protected readonly node: NodeRedNode;
506
562
  protected readonly context: ConfigNodeContext | IONodeContext;
@@ -508,6 +564,7 @@ declare abstract class Node$1<TConfig = any, TCredentials = any, TSettings = any
508
564
  private readonly timers;
509
565
  private readonly intervals;
510
566
  constructor(RED: RED, node: NodeRedNode, config: NodeConfig<TConfig>, credentials: NodeCredentials<TCredentials>);
567
+ [WIRE_HANDLERS](nodeRedNode: NodeRedNode, createdPromise: Promise<void>): void;
511
568
  i18n(key: string, substitutions?: Record<string, string>): string;
512
569
  setTimeout(fn: () => void, ms: number): NodeJS.Timeout;
513
570
  setInterval(fn: () => void, ms: number): NodeJS.Timeout;
@@ -543,29 +600,31 @@ declare abstract class Node$1<TConfig = any, TCredentials = any, TSettings = any
543
600
  * ```
544
601
  */
545
602
  export declare abstract class IONode<TConfig = any, TCredentials = any, TInput = any, TOutput = any, TSettings = any> extends Node$1<TConfig, TCredentials, TSettings> implements IIONode<TConfig, TCredentials, TInput, TOutput, TSettings> {
603
+ #private;
546
604
  static readonly align?: "left" | "right";
547
605
  static readonly color: HexColor;
548
606
  static readonly inputSchema?: Schema;
549
- static readonly outputsSchema?: Schema | Schema[];
607
+ static readonly outputsSchema?: Schema | Schema[] | Record<string, Schema>;
550
608
  static readonly validateInput: boolean;
551
609
  static readonly validateOutput: boolean;
552
610
  static get inputs(): 0 | 1;
553
611
  static get outputs(): number;
554
- private _send;
555
612
  readonly config: IONodeConfig<TConfig>;
556
613
  protected readonly context: IONodeContext;
557
614
  constructor(RED: RED, node: NodeRedNode, config: IONodeConfig<TConfig>, credentials: IONodeCredentials<TCredentials>);
615
+ [WIRE_HANDLERS](nodeRedNode: NodeRedNode, createdPromise: Promise<void>): void;
558
616
  input(msg: TInput): void | Promise<void>;
559
617
  send(msg: TOutput): void;
618
+ get baseOutputs(): number;
619
+ get totalOutputs(): number;
560
620
  /**
561
621
  * Send a message to a specific output port by index or name.
562
- * Named ports: `"error"`, `"complete"`, `"status"` — resolved automatically
563
- * based on the node's emit port configuration.
622
+ * Built-in ports: `"error"`, `"complete"`, `"status"` — resolved automatically
623
+ * based on the node's built-in port configuration.
624
+ * Custom named ports are resolved from `outputsSchema` when it is a record.
564
625
  * Numeric indices refer to the base output ports (0-based).
565
626
  */
566
- sendToPort(port: number | "error" | "complete" | "status", msg: TOutput): void;
567
- private _getEmitPortIndex;
568
- private _nodeSource;
627
+ sendToPort<P extends (keyof TOutput & string) | number | "error" | "complete" | "status">(port: P, msg: P extends keyof TOutput ? TOutput[P] : unknown): void;
569
628
  status(status: IONodeStatus): void;
570
629
  error(message: string, msg?: any): void;
571
630
  updateWires(wires: string[][]): void;
@@ -594,8 +653,9 @@ type IONodeContext = {
594
653
  global: NodeContextStore;
595
654
  };
596
655
  type HexColor = `#${string}`;
597
- type BoundIONode<TC extends TSchema | undefined, TCr extends TSchema | undefined, TS extends TSchema | undefined, TIn extends TSchema | undefined, TOut extends TSchema | readonly TSchema[] | undefined> = IONode<InferOr<TC, any>, InferOr<TCr, any>, InferOr<TIn, any>, InferOutputs<TOut>, InferOr<TS, any>>;
598
- interface IIONode<TConfig = any, TCredentials = any, TInput = any, TOutput = any, TSettings = any> extends INode<TConfig, TCredentials, TSettings> {
656
+ type BoundIONode<TC extends TSchema | undefined, TCr extends TSchema | undefined, TS extends TSchema | undefined, TIn extends TSchema | undefined, TOut extends TSchema | readonly TSchema[] | Record<string, TSchema> | undefined> = IONode<InferOr<TC, any>, InferOr<TCr, any>, InferOr<TIn, any>, InferOutputs<TOut>, InferOr<TS, any>>;
657
+ /** Public instance interface for IO nodes. Implemented by {@link IONode}. */
658
+ export interface IIONode<TConfig = any, TCredentials = any, TInput = any, TOutput = any, TSettings = any> extends INode<TConfig, TCredentials, TSettings> {
599
659
  readonly config: IONodeConfig<TConfig>;
600
660
  readonly credentials: IONodeCredentials<TCredentials> | undefined;
601
661
  readonly x: number;
@@ -607,9 +667,11 @@ interface IIONode<TConfig = any, TCredentials = any, TInput = any, TOutput = any
607
667
  status(status: IONodeStatus): void;
608
668
  updateWires(wires: string[][]): void;
609
669
  receive(msg: TInput): void;
610
- sendToPort(port: number | "error" | "complete" | "status", msg: TOutput): void;
670
+ readonly baseOutputs: number;
671
+ readonly totalOutputs: number;
672
+ sendToPort<P extends (keyof TOutput & string) | number | "error" | "complete" | "status">(port: P, msg: P extends keyof TOutput ? TOutput[P] : unknown): void;
611
673
  }
612
- interface IONodeDefinition<TConfigSchema extends TSchema | undefined = undefined, TCredsSchema extends TSchema | undefined = undefined, TSettingsSchema extends TSchema | undefined = undefined, TInputSchema extends TSchema | undefined = undefined, TOutputsSchema extends TSchema | readonly TSchema[] | undefined = undefined> {
674
+ interface IONodeDefinition<TConfigSchema extends TSchema | undefined = undefined, TCredsSchema extends TSchema | undefined = undefined, TSettingsSchema extends TSchema | undefined = undefined, TInputSchema extends TSchema | undefined = undefined, TOutputsSchema extends TSchema | readonly TSchema[] | Record<string, TSchema> | undefined = undefined> {
613
675
  type: string;
614
676
  category?: string;
615
677
  color?: HexColor;
@@ -656,7 +718,8 @@ type ConfigNodeContext = {
656
718
  global: NodeContextStore;
657
719
  };
658
720
  type BoundConfigNode<TC extends TSchema | undefined, TCr extends TSchema | undefined, TS extends TSchema | undefined> = ConfigNode<InferOr<TC, any>, InferOr<TCr, any>, InferOr<TS, any>>;
659
- interface IConfigNode<TConfig = any, TCredentials = any, TSettings = any> extends INode<TConfig, TCredentials, TSettings> {
721
+ /** Public instance interface for config nodes. Implemented by {@link ConfigNode}. */
722
+ export interface IConfigNode<TConfig = any, TCredentials = any, TSettings = any> extends INode<TConfig, TCredentials, TSettings> {
660
723
  readonly config: ConfigNodeConfig<TConfig>;
661
724
  readonly credentials: ConfigNodeCredentials<TCredentials> | undefined;
662
725
  readonly userIds: string[];
@@ -672,6 +735,27 @@ interface ConfigNodeDefinition<TConfigSchema extends TSchema | undefined = undef
672
735
  created?(this: BoundConfigNode<TConfigSchema, TCredsSchema, TSettingsSchema>): void | Promise<void>;
673
736
  closed?(this: BoundConfigNode<TConfigSchema, TCredsSchema, TSettingsSchema>, removed?: boolean): void | Promise<void>;
674
737
  }
738
+ /**
739
+ * Registers a custom node with Node-RED.
740
+ *
741
+ * @param RED - The Node-RED runtime API object
742
+ * @param NodeClass - A node class extending Node, IONode, or ConfigNode
743
+ * @throws If NodeClass does not extend Node
744
+ * @throws If NodeClass.type is not defined
745
+ */
746
+ export declare function registerType(RED: RED, NodeClass: NodeConstructor): Promise<void>;
747
+ type RegistrationFunction = ((RED: RED) => Promise<void>) & {
748
+ nodes: NodeConstructor[];
749
+ };
750
+ /**
751
+ * Registers multiple node classes with Node-RED.
752
+ *
753
+ * Returns a Node-RED package function that Node-RED calls with the RED
754
+ * runtime object when loading the package.
755
+ *
756
+ * @param nodes - Array of node classes to register
757
+ */
758
+ export declare function registerTypes(nodes: NodeConstructor[]): RegistrationFunction;
675
759
  /**
676
760
  * Creates an IO node class from a definition object. Provides automatic type
677
761
  * inference from schemas, reducing boilerplate compared to the class-based API.
@@ -690,7 +774,7 @@ interface ConfigNodeDefinition<TConfigSchema extends TSchema | undefined = undef
690
774
  * });
691
775
  * ```
692
776
  */
693
- export declare function defineIONode<TConfigSchema extends TSchema | undefined = undefined, TCredsSchema extends TSchema | undefined = undefined, TSettingsSchema extends TSchema | undefined = undefined, TInputSchema extends TSchema | undefined = undefined, TOutputsSchema extends TSchema | readonly TSchema[] | undefined = undefined>(def: IONodeDefinition<TConfigSchema, TCredsSchema, TSettingsSchema, TInputSchema, TOutputsSchema>): NodeConstructor<IIONode<InferOr<TConfigSchema, any>, InferOr<TCredsSchema, any>, InferOr<TInputSchema, any>, InferOutputs<TOutputsSchema>>>;
777
+ export declare function defineIONode<TConfigSchema extends TSchema | undefined = undefined, TCredsSchema extends TSchema | undefined = undefined, TSettingsSchema extends TSchema | undefined = undefined, TInputSchema extends TSchema | undefined = undefined, TOutputsSchema extends TSchema | readonly TSchema[] | Record<string, TSchema> | undefined = undefined>(def: IONodeDefinition<TConfigSchema, TCredsSchema, TSettingsSchema, TInputSchema, TOutputsSchema>): NodeConstructor<IIONode<InferOr<TConfigSchema, any>, InferOr<TCredsSchema, any>, InferOr<TInputSchema, any>, InferOutputs<TOutputsSchema>>>;
694
778
  /**
695
779
  * Creates a config node class from a definition object.
696
780
  *
@@ -708,27 +792,6 @@ export declare function defineConfigNode<TConfigSchema extends TSchema | undefin
708
792
  export declare class NrgError extends Error {
709
793
  constructor(message: string);
710
794
  }
711
- /**
712
- * Registers a custom node with Node-RED.
713
- *
714
- * @param RED - The Node-RED runtime API object
715
- * @param NodeClass - A node class extending Node, IONode, or ConfigNode
716
- * @throws If NodeClass does not extend Node
717
- * @throws If NodeClass.type is not defined
718
- */
719
- export declare function registerType(RED: RED, NodeClass: NodeConstructor): Promise<void>;
720
- type RegistrationFunction = ((RED: RED) => Promise<void>) & {
721
- nodes: NodeConstructor[];
722
- };
723
- /**
724
- * Registers multiple node classes with Node-RED.
725
- *
726
- * Returns a Node-RED package function that Node-RED calls with the RED
727
- * runtime object when loading the package.
728
- *
729
- * @param nodes - Array of node classes to register
730
- */
731
- export declare function registerTypes(nodes: NodeConstructor[]): RegistrationFunction;
732
795
  /** Defines the set of nodes exported by a Node-RED package. */
733
796
  export interface ModuleDefinition {
734
797
  nodes: NodeConstructor[];
package/types/test.d.ts CHANGED
@@ -214,12 +214,13 @@ interface NodeConstructor<T = any, TConfig = any, TCredentials = any> {
214
214
  readonly credentialsSchema?: Schema;
215
215
  readonly settingsSchema?: Schema;
216
216
  readonly inputSchema?: Schema;
217
- readonly outputsSchema?: Schema | Schema[];
217
+ readonly outputsSchema?: Schema | Schema[] | Record<string, Schema>;
218
218
  readonly validateInput?: boolean;
219
219
  readonly validateOutput?: boolean;
220
220
  readonly name: string;
221
221
  registered?(RED: RED): void | Promise<void>;
222
- _registered?(RED: RED): void | Promise<void>;
222
+ register(RED: RED): void | Promise<void>;
223
+ validateSettings(RED: RED): void;
223
224
  new (RED: RED, node: NodeRedNode, config: NodeConfig<TConfig>, credentials: NodeCredentials<TCredentials>): T;
224
225
  }
225
226
  type NodeConfig<TConfig = any> = TConfig & Static<typeof NodeConfigSchema>;
package/vite/index.js CHANGED
@@ -661,18 +661,34 @@ function getSchemaReferences(filePath) {
661
661
  }
662
662
  }
663
663
  const schemaRefs = /* @__PURE__ */ new Map();
664
- function extractIdentifiers(node) {
664
+ const recordPortNames = /* @__PURE__ */ new Map();
665
+ let outputsSchemaIsRecord = false;
666
+ function extractIdentifiers(node, propName) {
665
667
  if (ts.isIdentifier(node)) return [node.text];
666
668
  if (ts.isArrayLiteralExpression(node)) {
667
669
  return node.elements.filter(ts.isIdentifier).map((el) => el.text);
668
670
  }
671
+ if (ts.isObjectLiteralExpression(node) && propName === "outputsSchema") {
672
+ const ids = [];
673
+ outputsSchemaIsRecord = true;
674
+ for (const prop of node.properties) {
675
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer)) {
676
+ const portName = ts.isIdentifier(prop.name) ? prop.name.text : ts.isStringLiteral(prop.name) ? prop.name.text : void 0;
677
+ if (portName) {
678
+ ids.push(prop.initializer.text);
679
+ recordPortNames.set(prop.initializer.text, portName);
680
+ }
681
+ }
682
+ }
683
+ return ids;
684
+ }
669
685
  return [];
670
686
  }
671
687
  for (const stmt of source.statements) {
672
688
  if (ts.isClassDeclaration(stmt)) {
673
689
  for (const member of stmt.members) {
674
690
  if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && member.name.text in SCHEMA_PROP_SEMANTICS && member.initializer) {
675
- const ids = extractIdentifiers(member.initializer);
691
+ const ids = extractIdentifiers(member.initializer, member.name.text);
676
692
  if (ids.length > 0) {
677
693
  schemaRefs.set(member.name.text, ids);
678
694
  }
@@ -686,7 +702,7 @@ function getSchemaReferences(filePath) {
686
702
  if (arg && ts.isObjectLiteralExpression(arg)) {
687
703
  for (const prop of arg.properties) {
688
704
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text in SCHEMA_PROP_SEMANTICS) {
689
- const ids = extractIdentifiers(prop.initializer);
705
+ const ids = extractIdentifiers(prop.initializer, prop.name.text);
690
706
  if (ids.length > 0) {
691
707
  schemaRefs.set(prop.name.text, ids);
692
708
  }
@@ -699,15 +715,17 @@ function getSchemaReferences(filePath) {
699
715
  const result = [];
700
716
  for (const [propName, identifiers] of schemaRefs) {
701
717
  const semanticName = SCHEMA_PROP_SEMANTICS[propName];
702
- const isArray = identifiers.length > 1;
718
+ const isArray = identifiers.length > 1 && !outputsSchemaIsRecord;
719
+ const isRecord = propName === "outputsSchema" && outputsSchemaIsRecord;
703
720
  for (const identifier of identifiers) {
704
721
  const importSource = importMap.get(identifier);
705
722
  if (importSource) {
706
723
  result.push({
707
724
  localName: identifier,
708
- semanticName: isArray ? identifier : semanticName,
725
+ semanticName: isArray || isRecord ? identifier : semanticName,
709
726
  importSource,
710
- tupleProp: isArray ? semanticName : void 0
727
+ tupleProp: isArray || isRecord ? semanticName : void 0,
728
+ recordPortName: isRecord ? recordPortNames.get(identifier) : void 0
711
729
  });
712
730
  }
713
731
  }
@@ -734,10 +752,18 @@ function getFactoryInfo(filePath) {
734
752
  }
735
753
  return null;
736
754
  }
737
- function buildTypeArg(schemaMap, semanticName) {
755
+ function buildTypeArg(schemaMap, semanticName, portNameMap) {
738
756
  const names = schemaMap.get(semanticName);
739
757
  if (!names || names.length === 0) return "any";
740
- if (names.length === 1) return `Infer<typeof ${names[0]}>`;
758
+ if (names.length === 1 && !portNameMap?.has(names[0]))
759
+ return `Infer<typeof ${names[0]}>`;
760
+ if (portNameMap && names.some((n) => portNameMap.has(n))) {
761
+ const entries = names.map((n) => {
762
+ const portName = portNameMap.get(n);
763
+ return portName ? `${portName}: Infer<typeof ${n}>` : null;
764
+ }).filter(Boolean);
765
+ return `{ ${entries.join(", ")} }`;
766
+ }
741
767
  return `[${names.map((n) => `Infer<typeof ${n}>`).join(", ")}]`;
742
768
  }
743
769
  function buildNodeReexports(srcDir, entryFile) {
@@ -755,12 +781,17 @@ function buildNodeReexports(srcDir, entryFile) {
755
781
  const interfaceName = factoryInfo.factoryName === "defineIONode" ? "IIONode" : "IConfigNode";
756
782
  lines.push(`import _${ns} from "${specifier}";`);
757
783
  const schemaMap = /* @__PURE__ */ new Map();
784
+ const portNameMap = /* @__PURE__ */ new Map();
758
785
  for (const ref of schemaRefs) {
759
786
  const key = ref.tupleProp ?? ref.semanticName;
760
787
  if (!schemaMap.has(key)) schemaMap.set(key, []);
761
788
  schemaMap.get(key).push(ref.localName);
789
+ if (ref.recordPortName) {
790
+ portNameMap.set(ref.localName, ref.recordPortName);
791
+ }
762
792
  }
763
793
  const hasSchemas = schemaMap.size > 0;
794
+ const hasRecordOutputs = portNameMap.size > 0;
764
795
  const nrgImports = ["NodeConstructor", interfaceName];
765
796
  if (hasSchemas) nrgImports.push("Infer");
766
797
  lines.push(
@@ -785,11 +816,16 @@ function buildNodeReexports(srcDir, entryFile) {
785
816
  }
786
817
  let typeArgs;
787
818
  if (factoryInfo.factoryName === "defineIONode") {
819
+ const outputsArg = buildTypeArg(
820
+ schemaMap,
821
+ "OutputsSchema",
822
+ portNameMap.size > 0 ? portNameMap : void 0
823
+ );
788
824
  typeArgs = [
789
825
  buildTypeArg(schemaMap, "ConfigSchema"),
790
826
  buildTypeArg(schemaMap, "CredentialsSchema"),
791
827
  buildTypeArg(schemaMap, "InputSchema"),
792
- buildTypeArg(schemaMap, "OutputsSchema")
828
+ outputsArg
793
829
  ].join(", ");
794
830
  } else {
795
831
  typeArgs = [
@@ -1586,9 +1622,10 @@ function generateHelpDoc(nodeClass, labels, t) {
1586
1622
  if (inputSection) lines.push(inputSection);
1587
1623
  }
1588
1624
  if (nodeClass.outputsSchema) {
1589
- if (Array.isArray(nodeClass.outputsSchema)) {
1625
+ const os2 = nodeClass.outputsSchema;
1626
+ if (Array.isArray(os2)) {
1590
1627
  const portSections = [];
1591
- nodeClass.outputsSchema.forEach((schema, i) => {
1628
+ os2.forEach((schema, i) => {
1592
1629
  const title = `${t.sections.port} ${i + 1}`;
1593
1630
  const portPropLabels = labels.outputs?.[i];
1594
1631
  const section = generateSchemaSection({
@@ -1604,6 +1641,26 @@ function generateHelpDoc(nodeClass, labels, t) {
1604
1641
  if (portSections.length) {
1605
1642
  lines.push(
1606
1643
  `<h3>${t.sections.outputs}</h3>
1644
+ ${portSections.join("\n")}`
1645
+ );
1646
+ }
1647
+ } else if (!("type" in os2 || "properties" in os2)) {
1648
+ const portSections = [];
1649
+ for (const [portName, schema] of Object.entries(os2)) {
1650
+ const portPropLabels = labels.outputs?.[portName];
1651
+ const section = generateSchemaSection({
1652
+ title: portName,
1653
+ schema,
1654
+ t,
1655
+ labels: portPropLabels,
1656
+ heading: "####",
1657
+ includeDefault: false
1658
+ });
1659
+ if (section) portSections.push(section);
1660
+ }
1661
+ if (portSections.length) {
1662
+ lines.push(
1663
+ `<h3>${t.sections.outputs}</h3>
1607
1664
  ${portSections.join("\n")}`
1608
1665
  );
1609
1666
  }
@@ -1611,7 +1668,7 @@ ${portSections.join("\n")}`
1611
1668
  const outputPropLabels = labels.outputs?.[0];
1612
1669
  const section = generateSchemaSection({
1613
1670
  title: t.sections.output,
1614
- schema: nodeClass.outputsSchema,
1671
+ schema: os2,
1615
1672
  t,
1616
1673
  labels: outputPropLabels,
1617
1674
  includeDefault: false