@bonsae/nrg 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +130 -0
  2. package/build/server/index.cjs +910 -0
  3. package/build/server/resources/nrg-client.js +6530 -0
  4. package/build/server/resources/vue.esm-browser.prod.js +13 -0
  5. package/build/vite/index.js +1893 -0
  6. package/build/vite/utils.js +60 -0
  7. package/package.json +110 -0
  8. package/src/core/client/api/index.ts +17 -0
  9. package/src/core/client/app.vue +201 -0
  10. package/src/core/client/components/node-red-config-input.vue +57 -0
  11. package/src/core/client/components/node-red-editor-input.vue +283 -0
  12. package/src/core/client/components/node-red-input.vue +71 -0
  13. package/src/core/client/components/node-red-json-schema-form.vue +369 -0
  14. package/src/core/client/components/node-red-select-input.vue +86 -0
  15. package/src/core/client/components/node-red-typed-input.vue +130 -0
  16. package/src/core/client/components.d.ts +18 -0
  17. package/src/core/client/globals.d.ts +17 -0
  18. package/src/core/client/index.ts +504 -0
  19. package/src/core/client/shims-vue.d.ts +5 -0
  20. package/src/core/client/tsconfig.json +18 -0
  21. package/src/core/client/virtual.d.ts +5 -0
  22. package/src/core/constants.ts +18 -0
  23. package/src/core/server/index.ts +209 -0
  24. package/src/core/server/nodes/config-node.ts +67 -0
  25. package/src/core/server/nodes/index.ts +4 -0
  26. package/src/core/server/nodes/io-node.ts +178 -0
  27. package/src/core/server/nodes/node.ts +255 -0
  28. package/src/core/server/nodes/types/config-node.ts +28 -0
  29. package/src/core/server/nodes/types/index.ts +3 -0
  30. package/src/core/server/nodes/types/io-node.ts +37 -0
  31. package/src/core/server/nodes/types/node.ts +41 -0
  32. package/src/core/server/nodes/utils.ts +83 -0
  33. package/src/core/server/schemas/base.ts +66 -0
  34. package/src/core/server/schemas/index.ts +3 -0
  35. package/src/core/server/schemas/type.ts +95 -0
  36. package/src/core/server/schemas/types/index.ts +73 -0
  37. package/src/core/server/tsconfig.json +17 -0
  38. package/src/core/server/types/index.ts +73 -0
  39. package/src/core/server/utils.ts +56 -0
  40. package/src/core/server/validator.ts +32 -0
  41. package/src/core/validator.ts +222 -0
  42. package/src/tsconfig/base.json +23 -0
  43. package/src/tsconfig/client.json +11 -0
  44. package/src/tsconfig/server.json +6 -0
  45. package/src/vite/async-utils.ts +61 -0
  46. package/src/vite/client/build.ts +223 -0
  47. package/src/vite/client/index.ts +1 -0
  48. package/src/vite/client/plugins/html-generator.ts +75 -0
  49. package/src/vite/client/plugins/index.ts +5 -0
  50. package/src/vite/client/plugins/locales-generator.ts +126 -0
  51. package/src/vite/client/plugins/minifier.ts +22 -0
  52. package/src/vite/client/plugins/node-definitions-inliner.ts +224 -0
  53. package/src/vite/client/plugins/static-copy.ts +43 -0
  54. package/src/vite/defaults.ts +77 -0
  55. package/src/vite/errors.ts +37 -0
  56. package/src/vite/index.ts +3 -0
  57. package/src/vite/logger.ts +94 -0
  58. package/src/vite/node-red-launcher.ts +344 -0
  59. package/src/vite/plugin.ts +61 -0
  60. package/src/vite/plugins/build.ts +73 -0
  61. package/src/vite/plugins/index.ts +2 -0
  62. package/src/vite/plugins/server.ts +267 -0
  63. package/src/vite/server/build.ts +124 -0
  64. package/src/vite/server/index.ts +1 -0
  65. package/src/vite/server/plugins/index.ts +3 -0
  66. package/src/vite/server/plugins/output-wrapper.ts +109 -0
  67. package/src/vite/server/plugins/package-json-generator.ts +203 -0
  68. package/src/vite/server/plugins/type-generator.ts +285 -0
  69. package/src/vite/types.ts +369 -0
  70. package/src/vite/utils.ts +103 -0
@@ -0,0 +1,209 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { getCredentialsFromSchema } from "./utils";
4
+ import { Node } from "./nodes";
5
+ import { type RED } from "./types";
6
+ import { initValidator } from "./validator";
7
+
8
+ const MIME: Record<string, string> = {
9
+ ".js": "application/javascript",
10
+ ".mjs": "application/javascript",
11
+ ".css": "text/css",
12
+ ".json": "application/json",
13
+ ".map": "application/json",
14
+ ".png": "image/png",
15
+ ".svg": "image/svg+xml",
16
+ };
17
+
18
+ let _nrgResourcesRegistered = false;
19
+
20
+ function serveNrgResources(RED: RED): void {
21
+ if (_nrgResourcesRegistered) return;
22
+ _nrgResourcesRegistered = true;
23
+
24
+ const clientDir = path.resolve(__dirname, "./resources");
25
+ if (!fs.existsSync(clientDir)) return;
26
+
27
+ const httpAdmin = (RED as any).httpAdmin;
28
+ if (!httpAdmin) return;
29
+
30
+ // /nrg/assets/ is not handled by Node-RED's editorApp, so our handler
31
+ // appended via use() is reached normally without any stack manipulation.
32
+ httpAdmin.use(function (req: any, res: any, next: any) {
33
+ const prefix = "/nrg/assets/";
34
+ if (!(req.path as string).startsWith(prefix)) return next();
35
+ const reqPath = (req.path as string)
36
+ .slice(prefix.length)
37
+ .replace(/\.\./g, "");
38
+ const filePath = path.resolve(clientDir, reqPath);
39
+ if (!filePath.startsWith(clientDir)) return next();
40
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
41
+ return next();
42
+ const ext = path.extname(filePath);
43
+ res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
44
+ fs.createReadStream(filePath).pipe(res);
45
+ });
46
+ }
47
+
48
+ type AnyNodeClass = (abstract new (...args: any[]) => Node) &
49
+ Partial<typeof Node>;
50
+
51
+ /**
52
+ * Registers a custom node with Node-RED.
53
+ *
54
+ * @param RED - The Node-RED runtime API object
55
+ * @param NodeClass - A node class extending Node, IONode, or ConfigNode
56
+ * @throws If NodeClass does not extend Node
57
+ * @throws If NodeClass.type is not defined
58
+ */
59
+ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
60
+ const NC = NodeClass as any;
61
+ RED.log.debug(`Registering Type: ${NC.type}`);
62
+ if (!(NC.prototype instanceof Node)) {
63
+ throw new Error(`${NC.name} must extend IONode or ConfigNode classes`);
64
+ }
65
+
66
+ if (!NC.type) {
67
+ throw new Error("type must be provided when registering the node");
68
+ }
69
+
70
+ if (NC.color && !/^#[0-9A-Fa-f]{6}$/.test(NC.color)) {
71
+ throw new Error(
72
+ `Invalid color for ${NodeClass.type}: ${NC.color} color must be in hex format`,
73
+ );
74
+ }
75
+
76
+ if (
77
+ NC.inputs !== undefined &&
78
+ (!Number.isInteger(NC.inputs) || (NC.inputs != 0 && NC.inputs != 1))
79
+ ) {
80
+ throw new Error(
81
+ `Invalid number of inputs for ${NodeClass.type}: inputs must be 0 or 1`,
82
+ );
83
+ }
84
+
85
+ if (
86
+ NC.outputs !== undefined &&
87
+ (!Number.isInteger(NC.outputs) || NC.outputs < 0)
88
+ ) {
89
+ throw new Error(
90
+ `Invalid number of outputs for ${NodeClass.type}: outputs must be a positive integer`,
91
+ );
92
+ }
93
+
94
+ RED.nodes.registerType(
95
+ NC.type,
96
+ function (this: any, config: any) {
97
+ RED.nodes.createNode(this, config);
98
+ const node = new NC(RED, this, config, this.credentials);
99
+ // NOTE: save node intance inside node-red's node so that the proxy can resolve it lazyly
100
+ this._node = node;
101
+
102
+ // NOTE: created promise must be here because we only want it to start after the whole object creation chain has been completed: child -> IONode -> Node -> IONode -> child -> done
103
+ const createdPromise = Promise.resolve(node.created?.()).catch(
104
+ (error: any) => {
105
+ this.error("Error during created hook: " + error.message);
106
+ throw error;
107
+ },
108
+ );
109
+
110
+ this.on(
111
+ "input",
112
+ async (
113
+ msg: unknown,
114
+ send: (msg: unknown) => void,
115
+ done: (err?: Error) => void,
116
+ ) => {
117
+ try {
118
+ await createdPromise;
119
+ } catch {
120
+ done(new Error("Node failed to initialize"));
121
+ return;
122
+ }
123
+
124
+ try {
125
+ this.log("Calling input");
126
+ await Promise.resolve(node._input(msg, send));
127
+ done();
128
+ this.log("Input processed");
129
+ } catch (error) {
130
+ if (error instanceof Error) {
131
+ this.error("Error while processing input: " + error.message, msg);
132
+ done(error);
133
+ } else {
134
+ this.error("Unknown error occurred during input handling", msg);
135
+ done(new Error("Unknown error during input handling"));
136
+ }
137
+ }
138
+ },
139
+ );
140
+
141
+ this.on(
142
+ "close",
143
+ async (removed: boolean, done: (err?: Error) => void) => {
144
+ try {
145
+ this.log("Calling closed");
146
+ await Promise.resolve(node._closed(removed));
147
+ this.log("Node was closed");
148
+ done();
149
+ } catch (error) {
150
+ if (error instanceof Error) {
151
+ this.error("Error while closing node: " + error.message);
152
+ done(error);
153
+ } else {
154
+ this.error("Unknown error occurred while closing node");
155
+ done(new Error("Unknown error occurred while closing node"));
156
+ }
157
+ }
158
+ },
159
+ );
160
+ },
161
+ {
162
+ credentials: NC.credentialsSchema
163
+ ? getCredentialsFromSchema(NC.credentialsSchema)
164
+ : {},
165
+ settings: NC._settings?.(),
166
+ },
167
+ );
168
+
169
+ await Promise.resolve(NC._registered?.(RED));
170
+
171
+ RED.log.debug(`Type registered: ${NC.type}`);
172
+ }
173
+
174
+ type NodeRedPackageFunction = ((RED: RED) => Promise<void>) & {
175
+ nodes: AnyNodeClass[];
176
+ };
177
+
178
+ /**
179
+ * Registers multiple node classes with Node-RED.
180
+ *
181
+ * Returns a Node-RED package function that Node-RED calls with the RED
182
+ * runtime object when loading the package.
183
+ *
184
+ * @param nodes - Array of node classes to register
185
+ */
186
+ function registerTypes(nodes: AnyNodeClass[]): NodeRedPackageFunction {
187
+ const fn = async function (RED: RED) {
188
+ initValidator(RED);
189
+ serveNrgResources(RED);
190
+ try {
191
+ RED.log.info("Registering node types in series");
192
+ for (const NodeClass of nodes) {
193
+ await registerType(RED, NodeClass);
194
+ }
195
+ RED.log.info("All node types registered in series");
196
+ } catch (error) {
197
+ RED.log.error("Error registering node types:", error);
198
+ throw error;
199
+ }
200
+ };
201
+ (fn as NodeRedPackageFunction).nodes = nodes;
202
+ return fn as NodeRedPackageFunction;
203
+ }
204
+
205
+ export { registerType, registerTypes };
206
+ export { Node, IONode, ConfigNode } from "./nodes";
207
+ export type { RED } from "./types";
208
+ export { SchemaType, defineSchema } from "./schemas";
209
+ export type { Schema, Infer } from "./schemas/types";
@@ -0,0 +1,67 @@
1
+ import type { RED } from "../../server/types";
2
+ import { Node } from "./node";
3
+ import type {
4
+ ConfigNodeConfig,
5
+ ConfigNodeContext,
6
+ ConfigNodeCredentials,
7
+ } from "./types";
8
+ import { setupContext } from "./utils";
9
+
10
+ abstract class ConfigNode<
11
+ TConfig = any,
12
+ TCredentials = any,
13
+ TSettings = any,
14
+ > extends Node<TConfig, TCredentials, TSettings> {
15
+ public static override readonly category: string = "config";
16
+ declare public readonly config: ConfigNodeConfig<TConfig>;
17
+
18
+ protected override readonly context: ConfigNodeContext;
19
+
20
+ // NOTE: used by the registered function. Had to be a different one to avoid calling the parent's input again
21
+ /** @internal */
22
+ public static override _registered(RED: RED): void | Promise<void> {
23
+ this.validateSettings(RED);
24
+ return this.registered?.(RED);
25
+ }
26
+
27
+ constructor(
28
+ RED: RED,
29
+ node: any,
30
+ config: ConfigNodeConfig<TConfig>,
31
+ credentials: ConfigNodeCredentials<TCredentials>,
32
+ ) {
33
+ super(RED, node, config, credentials);
34
+
35
+ const context = node.context();
36
+ const fn = (scope: "node" | "global", store?: string) => {
37
+ const target = scope === "global" ? context.global : context;
38
+ return setupContext(target, store);
39
+ };
40
+ fn.node = setupContext(context);
41
+ fn.global = setupContext(context.global);
42
+
43
+ this.context = fn as any;
44
+ }
45
+
46
+ get userIds(): string[] {
47
+ return this.config._users;
48
+ }
49
+
50
+ get users(): Node[] {
51
+ return this.userIds
52
+ .map((id) => this.RED.nodes.getNode(id)?._node)
53
+ .filter((node): node is Node => node != null);
54
+ }
55
+
56
+ getUser<T extends Node = Node>(index: number): T | undefined {
57
+ const id = this.userIds[index];
58
+ if (!id) return undefined;
59
+ return this.RED.nodes.getNode(id)?._node as T | undefined;
60
+ }
61
+
62
+ override get credentials(): ConfigNodeCredentials<TCredentials> | undefined {
63
+ return this.node.credentials;
64
+ }
65
+ }
66
+
67
+ export { ConfigNode };
@@ -0,0 +1,4 @@
1
+ export { Node } from "./node";
2
+ export { IONode } from "./io-node";
3
+ export { ConfigNode } from "./config-node";
4
+ export type * from "./types";
@@ -0,0 +1,178 @@
1
+ import type { Schema } from "../schemas/types";
2
+ import type { RED } from "../../server/types";
3
+ import { validator } from "../validator";
4
+ import { Node } from "./node";
5
+ import type {
6
+ IONodeContext,
7
+ IONodeContextScope,
8
+ IONodeStatus,
9
+ IONodeConfig,
10
+ IONodeCredentials,
11
+ } from "./types";
12
+ import { setupContext } from "./utils";
13
+
14
+ abstract class IONode<
15
+ TConfig = any,
16
+ TCredentials = any,
17
+ TInput = any,
18
+ TOutput = any,
19
+ TSettings = any,
20
+ > extends Node<TConfig, TCredentials, TSettings> {
21
+ public static readonly align?: "left" | "right";
22
+ public static readonly color: `#${string}`;
23
+ public static readonly labelStyle?:
24
+ | "node_label"
25
+ | "node_label_italic"
26
+ | string;
27
+ public static readonly paletteLabel?: string;
28
+ public static readonly inputs?: number = 0;
29
+ public static readonly outputs?: number = 0;
30
+ public static readonly inputLabels?: string | string[];
31
+ public static readonly outputLabels?: string | string[];
32
+ public static readonly inputSchema?: Schema;
33
+ public static readonly outputsSchema?: Schema | Schema[];
34
+ public static readonly validateInput: boolean = false;
35
+ public static readonly validateOutput: boolean = false;
36
+
37
+ private _send: ((msg: any) => void) | undefined;
38
+
39
+ declare public readonly config: IONodeConfig<TConfig>;
40
+ protected override readonly context: IONodeContext;
41
+
42
+ // NOTE: used by the registered function. Had to be a different one to avoid calling the parent's input again
43
+ /** @internal */
44
+ public static override _registered(RED: RED): void | Promise<void> {
45
+ this.validateSettings(RED);
46
+ return this.registered?.(RED);
47
+ }
48
+
49
+ constructor(
50
+ RED: RED,
51
+ node: any,
52
+ config: IONodeConfig<TConfig>,
53
+ credentials: IONodeCredentials<TCredentials>,
54
+ ) {
55
+ super(RED, node, config, credentials);
56
+
57
+ const context = node.context();
58
+ const fn = (scope: IONodeContextScope, store?: string) => {
59
+ const target =
60
+ scope === "global"
61
+ ? context.global
62
+ : scope === "flow"
63
+ ? context.flow
64
+ : context;
65
+ return setupContext(target, store);
66
+ };
67
+
68
+ fn.node = setupContext(context);
69
+ fn.flow = setupContext(context.flow);
70
+ fn.global = setupContext(context.global);
71
+
72
+ this.context = fn as any;
73
+ }
74
+
75
+ public abstract input(msg: TInput): void | Promise<void>;
76
+
77
+ // NOTE: used by the registered function. Had to be a different one to avoid calling the parent's input again
78
+ /** @internal */
79
+ public async _input(msg: TInput, send: (msg: any) => void) {
80
+ const NodeClass = this.constructor as typeof IONode;
81
+ const shouldValidateInput =
82
+ this.config.validateInput ?? NodeClass.validateInput;
83
+ if (shouldValidateInput && NodeClass.inputSchema) {
84
+ this.log("Validating input");
85
+ validator.validate(msg, NodeClass.inputSchema, {
86
+ cacheKey: NodeClass.inputSchema.$id || `${NodeClass.type}:input-schema`,
87
+ throwOnError: true,
88
+ });
89
+ this.log("Input is valid");
90
+ }
91
+ this._send = send;
92
+ try {
93
+ await Promise.resolve(this.input(msg));
94
+ } finally {
95
+ this._send = undefined;
96
+ }
97
+ }
98
+
99
+ public send(msg: TOutput) {
100
+ const NodeClass = this.constructor as typeof IONode;
101
+ const shouldValidateOutput =
102
+ this.config.validateOutput ?? NodeClass.validateOutput;
103
+ if (shouldValidateOutput && NodeClass.outputsSchema) {
104
+ this.log("Validating output");
105
+ const schemas = NodeClass.outputsSchema;
106
+
107
+ if (Array.isArray(schemas)) {
108
+ // Per-port validation: schemas[i] validates msg[i]
109
+ const msgs = msg as any[];
110
+ for (let i = 0; i < schemas.length; i++) {
111
+ if (msgs[i] == null) continue;
112
+ validator.validate(msgs[i], schemas[i], {
113
+ cacheKey: schemas[i].$id || `${NodeClass.type}:output-schema:${i}`,
114
+ throwOnError: true,
115
+ });
116
+ }
117
+ } else if (Array.isArray(msg)) {
118
+ // Single schema, array of messages: validate each non-null element
119
+ for (let i = 0; i < (msg as any[]).length; i++) {
120
+ if ((msg as any[])[i] == null) continue;
121
+ validator.validate((msg as any[])[i], schemas, {
122
+ cacheKey: schemas.$id || `${NodeClass.type}:output-schema`,
123
+ throwOnError: true,
124
+ });
125
+ }
126
+ } else {
127
+ // Single schema, single message
128
+ validator.validate(msg, schemas, {
129
+ cacheKey: schemas.$id || `${NodeClass.type}:output-schema`,
130
+ throwOnError: true,
131
+ });
132
+ }
133
+ this.log("Output is valid");
134
+ }
135
+
136
+ if (this._send) {
137
+ this._send(msg);
138
+ } else {
139
+ this.node.send(msg);
140
+ }
141
+ }
142
+
143
+ public status(status: IONodeStatus) {
144
+ this.node.status(status);
145
+ }
146
+
147
+ public updateWires(wires: string[][]) {
148
+ this.node.updateWires(wires);
149
+ }
150
+
151
+ public receive(msg: TInput) {
152
+ this.node.receive(msg);
153
+ }
154
+
155
+ public get x(): number {
156
+ return this.node.x;
157
+ }
158
+
159
+ public get y(): number {
160
+ return this.node.y;
161
+ }
162
+
163
+ public get g(): string | undefined {
164
+ return this.node.g;
165
+ }
166
+
167
+ public get wires(): string[][] {
168
+ return this.node.wires;
169
+ }
170
+
171
+ public override get credentials():
172
+ | IONodeCredentials<TCredentials>
173
+ | undefined {
174
+ return this.node.credentials;
175
+ }
176
+ }
177
+
178
+ export { IONode };