@bonsae/nrg 0.5.4 → 0.6.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.
@@ -3,6 +3,7 @@ import type { RED } from "../../server/types";
3
3
  import { validator } from "../validator";
4
4
  import { Node } from "./node";
5
5
  import type {
6
+ HexColor,
6
7
  IONodeContext,
7
8
  IONodeContextScope,
8
9
  IONodeStatus,
@@ -19,7 +20,7 @@ abstract class IONode<
19
20
  TSettings = any,
20
21
  > extends Node<TConfig, TCredentials, TSettings> {
21
22
  public static readonly align?: "left" | "right";
22
- public static readonly color: `#${string}`;
23
+ public static readonly color: HexColor;
23
24
  public static readonly labelStyle?:
24
25
  | "node_label"
25
26
  | "node_label_italic"
@@ -0,0 +1,130 @@
1
+ import { type Static, type TSchema } from "@sinclair/typebox";
2
+ import type { RED } from "../../types";
3
+ import type { IONode } from "../io-node";
4
+ import type { ConfigNode } from "../config-node";
5
+ import type { HexColor } from "./io-node";
6
+
7
+ type InferOr<T, Fallback> = T extends TSchema ? Static<T> : Fallback;
8
+
9
+ type InferOutputs<T> = T extends readonly TSchema[]
10
+ ? { [K in keyof T]: T[K] extends TSchema ? Static<T[K]> : never }
11
+ : T extends TSchema
12
+ ? Static<T>
13
+ : any;
14
+
15
+ type BoundIONode<
16
+ TC extends TSchema | undefined,
17
+ TCr extends TSchema | undefined,
18
+ TS extends TSchema | undefined,
19
+ TIn extends TSchema | undefined,
20
+ TOut extends TSchema | readonly TSchema[] | undefined,
21
+ > = IONode<
22
+ InferOr<TC, any>,
23
+ InferOr<TCr, any>,
24
+ InferOr<TIn, any>,
25
+ InferOutputs<TOut>,
26
+ InferOr<TS, any>
27
+ >;
28
+
29
+ type BoundConfigNode<
30
+ TC extends TSchema | undefined,
31
+ TCr extends TSchema | undefined,
32
+ TS extends TSchema | undefined,
33
+ > = ConfigNode<InferOr<TC, any>, InferOr<TCr, any>, InferOr<TS, any>>;
34
+
35
+ interface IONodeDefinition<
36
+ TConfigSchema extends TSchema | undefined = undefined,
37
+ TCredsSchema extends TSchema | undefined = undefined,
38
+ TSettingsSchema extends TSchema | undefined = undefined,
39
+ TInputSchema extends TSchema | undefined = undefined,
40
+ TOutputsSchema extends TSchema | readonly TSchema[] | undefined = undefined,
41
+ > {
42
+ type: string;
43
+ category?: string;
44
+ color?: HexColor;
45
+ inputs?: 0 | 1;
46
+ outputs?: number;
47
+ paletteLabel?: string;
48
+ inputLabels?: string | string[];
49
+ outputLabels?: string | string[];
50
+ align?: "left" | "right";
51
+ labelStyle?: string;
52
+
53
+ configSchema?: TConfigSchema;
54
+ credentialsSchema?: TCredsSchema;
55
+ settingsSchema?: TSettingsSchema;
56
+ inputSchema?: TInputSchema;
57
+ outputsSchema?: TOutputsSchema;
58
+
59
+ validateInput?: boolean;
60
+ validateOutput?: boolean;
61
+
62
+ registered?(RED: RED): void | Promise<void>;
63
+ created?(
64
+ this: BoundIONode<
65
+ TConfigSchema,
66
+ TCredsSchema,
67
+ TSettingsSchema,
68
+ TInputSchema,
69
+ TOutputsSchema
70
+ >,
71
+ ): void | Promise<void>;
72
+ closed?(
73
+ this: BoundIONode<
74
+ TConfigSchema,
75
+ TCredsSchema,
76
+ TSettingsSchema,
77
+ TInputSchema,
78
+ TOutputsSchema
79
+ >,
80
+ removed?: boolean,
81
+ ): void | Promise<void>;
82
+ input?(
83
+ this: BoundIONode<
84
+ TConfigSchema,
85
+ TCredsSchema,
86
+ TSettingsSchema,
87
+ TInputSchema,
88
+ TOutputsSchema
89
+ >,
90
+ msg: InferOr<TInputSchema, any>,
91
+ ): void | Promise<void>;
92
+ }
93
+
94
+ interface ConfigNodeDefinition<
95
+ TConfigSchema extends TSchema | undefined = undefined,
96
+ TCredsSchema extends TSchema | undefined = undefined,
97
+ TSettingsSchema extends TSchema | undefined = undefined,
98
+ > {
99
+ type: string;
100
+
101
+ configSchema?: TConfigSchema;
102
+ credentialsSchema?: TCredsSchema;
103
+ settingsSchema?: TSettingsSchema;
104
+
105
+ registered?(RED: RED): void | Promise<void>;
106
+ created?(
107
+ this: BoundConfigNode<TConfigSchema, TCredsSchema, TSettingsSchema>,
108
+ ): void | Promise<void>;
109
+ closed?(
110
+ this: BoundConfigNode<TConfigSchema, TCredsSchema, TSettingsSchema>,
111
+ removed?: boolean,
112
+ ): void | Promise<void>;
113
+ }
114
+
115
+ // Return types for factory functions — uses structural typing to avoid
116
+ // referencing internal class paths in declaration emit, while preserving
117
+ // the instance type for NodeRef inference.
118
+ interface NodeClassBase {
119
+ readonly type: string;
120
+ readonly category: string;
121
+ new (...args: any[]): any;
122
+ }
123
+
124
+ export type {
125
+ InferOr,
126
+ InferOutputs,
127
+ IONodeDefinition,
128
+ ConfigNodeDefinition,
129
+ NodeClassBase,
130
+ };
@@ -1,3 +1,4 @@
1
1
  export * from "./node";
2
2
  export * from "./io-node";
3
3
  export * from "./config-node";
4
+ export type * from "./factories";
@@ -28,7 +28,10 @@ type IONodeContext = {
28
28
  global: NodeContextStore;
29
29
  };
30
30
 
31
+ type HexColor = `#${string}`;
32
+
31
33
  export {
34
+ HexColor,
32
35
  IONodeConfig,
33
36
  IONodeContext,
34
37
  IONodeContextScope,
@@ -41,6 +41,7 @@ interface NrgFormOptions {
41
41
  icon?: string;
42
42
  typedInputTypes?: string[];
43
43
  editorLanguage?: string;
44
+ toggle?: boolean;
44
45
  }
45
46
 
46
47
  declare module "@sinclair/typebox" {
@@ -161,6 +161,139 @@ function getNodeTypeExports(
161
161
  * Runtime class values are exposed by `cjsDefaultExportPlugin` dynamically
162
162
  * via the `nodes` array on the package function.
163
163
  */
164
+ /**
165
+ * Semantic names for schema properties in both class static properties
166
+ * and factory definition objects.
167
+ */
168
+ const SCHEMA_PROP_SEMANTICS: Record<string, string> = {
169
+ configSchema: "ConfigSchema",
170
+ credentialsSchema: "CredentialsSchema",
171
+ inputSchema: "InputSchema",
172
+ outputsSchema: "OutputsSchema",
173
+ settingsSchema: "SettingsSchema",
174
+ };
175
+
176
+ /**
177
+ * Extract schema references from a node file — works for both class-based
178
+ * and factory-based (defineIONode/defineConfigNode) patterns.
179
+ *
180
+ * Returns an array of { localName, semanticName, importSource } where:
181
+ * - localName: the identifier used in the file (e.g., "ConfigsSchema")
182
+ * - semanticName: the semantic slot name (e.g., "ConfigSchema")
183
+ * - importSource: the relative import path (e.g., "../schemas/my-node")
184
+ */
185
+ function getSchemaReferences(
186
+ filePath: string,
187
+ ): Array<{ localName: string; semanticName: string; importSource: string }> {
188
+ const content = fs.readFileSync(filePath, "utf-8");
189
+ const source = ts.createSourceFile(
190
+ filePath,
191
+ content,
192
+ ts.ScriptTarget.ESNext,
193
+ true,
194
+ );
195
+
196
+ // Build a map of imported identifiers → their source module
197
+ const importMap = new Map<string, string>();
198
+ for (const stmt of source.statements) {
199
+ if (ts.isImportDeclaration(stmt) && stmt.importClause) {
200
+ const moduleSpecifier = (stmt.moduleSpecifier as ts.StringLiteral).text;
201
+ const namedBindings = stmt.importClause.namedBindings;
202
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
203
+ for (const el of namedBindings.elements) {
204
+ importMap.set(el.name.text, moduleSpecifier);
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ // Collect schema property assignments: { propName → identifiers[] }
211
+ const schemaRefs = new Map<string, string[]>();
212
+
213
+ // Extract identifier(s) from an initializer — handles both single identifiers
214
+ // and array literals like [Output1Schema, Output2Schema].
215
+ function extractIdentifiers(node: ts.Expression): string[] {
216
+ if (ts.isIdentifier(node)) return [node.text];
217
+ if (ts.isArrayLiteralExpression(node)) {
218
+ return node.elements.filter(ts.isIdentifier).map((el) => el.text);
219
+ }
220
+ return [];
221
+ }
222
+
223
+ for (const stmt of source.statements) {
224
+ // Class-based: static readonly configSchema = ConfigsSchema
225
+ // static readonly outputsSchema = [Output1Schema, Output2Schema]
226
+ if (ts.isClassDeclaration(stmt)) {
227
+ for (const member of stmt.members) {
228
+ if (
229
+ ts.isPropertyDeclaration(member) &&
230
+ ts.isIdentifier(member.name) &&
231
+ member.name.text in SCHEMA_PROP_SEMANTICS &&
232
+ member.initializer
233
+ ) {
234
+ const ids = extractIdentifiers(member.initializer);
235
+ if (ids.length > 0) {
236
+ schemaRefs.set(member.name.text, ids);
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ // Factory-based: export default defineIONode({ configSchema: X, ... })
243
+ if (
244
+ ts.isExportAssignment(stmt) &&
245
+ stmt.expression &&
246
+ ts.isCallExpression(stmt.expression)
247
+ ) {
248
+ const callee = stmt.expression.expression;
249
+ if (
250
+ ts.isIdentifier(callee) &&
251
+ (callee.text === "defineIONode" || callee.text === "defineConfigNode")
252
+ ) {
253
+ const arg = stmt.expression.arguments[0];
254
+ if (arg && ts.isObjectLiteralExpression(arg)) {
255
+ for (const prop of arg.properties) {
256
+ if (
257
+ ts.isPropertyAssignment(prop) &&
258
+ ts.isIdentifier(prop.name) &&
259
+ prop.name.text in SCHEMA_PROP_SEMANTICS
260
+ ) {
261
+ const ids = extractIdentifiers(prop.initializer);
262
+ if (ids.length > 0) {
263
+ schemaRefs.set(prop.name.text, ids);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ // Build result: match schema references to their import sources
273
+ const result: Array<{
274
+ localName: string;
275
+ semanticName: string;
276
+ importSource: string;
277
+ tupleProp?: string;
278
+ }> = [];
279
+ for (const [propName, identifiers] of schemaRefs) {
280
+ const semanticName = SCHEMA_PROP_SEMANTICS[propName];
281
+ const isArray = identifiers.length > 1;
282
+ for (const identifier of identifiers) {
283
+ const importSource = importMap.get(identifier);
284
+ if (importSource) {
285
+ result.push({
286
+ localName: identifier,
287
+ semanticName: isArray ? identifier : semanticName,
288
+ importSource,
289
+ tupleProp: isArray ? semanticName : undefined,
290
+ });
291
+ }
292
+ }
293
+ }
294
+ return result;
295
+ }
296
+
164
297
  function buildNodeReexports(srcDir: string, entryFile: string): string {
165
298
  const nodesDir = path.join(srcDir, "nodes");
166
299
  const nodeFiles = collectTsFiles(nodesDir);
@@ -175,6 +308,7 @@ function buildNodeReexports(srcDir: string, entryFile: string): string {
175
308
 
176
309
  const lines = [`export { default as ${ns} } from "${specifier}";`];
177
310
 
311
+ // Re-export type aliases from the node file (class-based nodes)
178
312
  const typePairs = getNodeTypeExports(file);
179
313
  if (typePairs.length > 0) {
180
314
  const prefixed = typePairs
@@ -186,6 +320,29 @@ function buildNodeReexports(srcDir: string, entryFile: string): string {
186
320
  lines.push(`export type { ${prefixed} } from "${specifier}";`);
187
321
  }
188
322
 
323
+ // Re-export schemas referenced by the node definition (class or factory)
324
+ const schemaRefs = getSchemaReferences(file);
325
+ // Group by import source for cleaner re-exports
326
+ const bySource = new Map<string, string[]>();
327
+ for (const ref of schemaRefs) {
328
+ const resolvedSource = path
329
+ .relative(
330
+ path.dirname(entryFile),
331
+ path.resolve(path.dirname(file), ref.importSource),
332
+ )
333
+ .replace(/\\/g, "/");
334
+ const sourceSpecifier = resolvedSource.startsWith(".")
335
+ ? resolvedSource
336
+ : `./${resolvedSource}`;
337
+ if (!bySource.has(sourceSpecifier)) bySource.set(sourceSpecifier, []);
338
+ bySource
339
+ .get(sourceSpecifier)!
340
+ .push(`${ref.localName} as ${ns}${ref.semanticName}`);
341
+ }
342
+ for (const [source, names] of bySource) {
343
+ lines.push(`export { ${names.join(", ")} } from "${source}";`);
344
+ }
345
+
189
346
  return lines.join("\n");
190
347
  })
191
348
  .join("\n");