@bonsae/nrg 0.5.2 → 0.5.4
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/build/server/index.cjs +65 -33
- package/build/server/resources/nrg-client.js +344 -344
- package/build/vite/index.js +3 -1
- package/package.json +7 -1
- package/src/core/client/app.vue +1 -1
- package/src/core/client/components/node-red-json-schema-form.vue +2 -1
- package/src/core/client/components/node-red-typed-input.vue +6 -0
- package/src/core/client/index.ts +17 -25
- package/src/core/errors.ts +9 -0
- package/src/core/server/index.ts +19 -12
- package/src/core/server/nodes/node.ts +5 -1
- package/src/core/server/nodes/utils.ts +50 -27
- package/src/vite/client/build.ts +3 -1
package/build/vite/index.js
CHANGED
|
@@ -1594,7 +1594,9 @@ async function build2(clientBuildOptions, buildContext) {
|
|
|
1594
1594
|
throw new BuildError("client", error);
|
|
1595
1595
|
} finally {
|
|
1596
1596
|
if (generatedEntry) {
|
|
1597
|
-
fs10.
|
|
1597
|
+
if (fs10.existsSync(entryPath)) {
|
|
1598
|
+
fs10.unlinkSync(entryPath);
|
|
1599
|
+
}
|
|
1598
1600
|
}
|
|
1599
1601
|
}
|
|
1600
1602
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonsae/nrg",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "NRG framework — build Node-RED nodes with Vue 3, TypeScript, and JSON Schema",
|
|
5
5
|
"author": "Allan Oricil <allanoricil@duck.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"pnpm": ">=10.11.0"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
+
"test": "vitest run && vitest run --config vitest.e2e.config.ts",
|
|
21
|
+
"test:unit": "vitest run",
|
|
22
|
+
"test:unit:watch": "vitest",
|
|
23
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
20
24
|
"build": "node build.mjs",
|
|
21
25
|
"typecheck": "tsc -p src/core/server/tsconfig.json --noEmit && tsc -p src/core/client/tsconfig.json --noEmit",
|
|
22
26
|
"lint": "eslint src/",
|
|
@@ -103,6 +107,7 @@
|
|
|
103
107
|
"@types/node": "^22.15.18",
|
|
104
108
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
105
109
|
"@typescript-eslint/parser": "^8.32.1",
|
|
110
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
106
111
|
"eslint": "^9.27.0",
|
|
107
112
|
"eslint-config-prettier": "^10.1.8",
|
|
108
113
|
"eslint-plugin-vue": "^10.1.0",
|
|
@@ -113,6 +118,7 @@
|
|
|
113
118
|
"semantic-release": "^24.2.4",
|
|
114
119
|
"typescript-eslint": "^8.32.1",
|
|
115
120
|
"vite": "^6.3.4",
|
|
121
|
+
"vitest": "^4.1.5",
|
|
116
122
|
"vue": "^3.5.14"
|
|
117
123
|
}
|
|
118
124
|
}
|
package/src/core/client/app.vue
CHANGED
|
@@ -136,6 +136,12 @@ export default defineComponent({
|
|
|
136
136
|
this.onChange();
|
|
137
137
|
});
|
|
138
138
|
},
|
|
139
|
+
beforeUnmount() {
|
|
140
|
+
if (this._observer) {
|
|
141
|
+
this._observer.disconnect();
|
|
142
|
+
this._observer = null;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
139
145
|
methods: {
|
|
140
146
|
onChange() {
|
|
141
147
|
const newValue = this.$input.typedInput("value");
|
package/src/core/client/index.ts
CHANGED
|
@@ -203,7 +203,7 @@ function getNodeState(node: Node): NodeState {
|
|
|
203
203
|
const state: NodeState = {
|
|
204
204
|
credentials: {},
|
|
205
205
|
};
|
|
206
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
206
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
207
207
|
state[prop] = node[prop];
|
|
208
208
|
});
|
|
209
209
|
if (node._def.credentials) {
|
|
@@ -300,8 +300,6 @@ function defineNode<T extends NodeDefinition>(options: T): T {
|
|
|
300
300
|
async function registerType(definition: NodeDefinition): Promise<void> {
|
|
301
301
|
const { type } = definition;
|
|
302
302
|
try {
|
|
303
|
-
console.log(`Registering node type: ${type}`);
|
|
304
|
-
|
|
305
303
|
const nodeDefinition = {
|
|
306
304
|
...(_schemas[type] ?? {}),
|
|
307
305
|
...definition,
|
|
@@ -311,9 +309,6 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
311
309
|
const defaults = nodeDefinition.defaults ?? undefined;
|
|
312
310
|
const credentials = nodeDefinition.credentials ?? undefined;
|
|
313
311
|
|
|
314
|
-
console.log("defaults", defaults);
|
|
315
|
-
console.log("credentials", credentials);
|
|
316
|
-
|
|
317
312
|
const appContainerId = `nrg-app-${type}`;
|
|
318
313
|
|
|
319
314
|
$("<script>", {
|
|
@@ -323,21 +318,20 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
323
318
|
}).appendTo("body");
|
|
324
319
|
|
|
325
320
|
function oneditprepare(this: Node) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
321
|
+
const validationSchema =
|
|
322
|
+
nodeDefinition.configSchema &&
|
|
323
|
+
nodeDefinition.credentialsSchema?.properties
|
|
324
|
+
? {
|
|
325
|
+
...nodeDefinition.configSchema,
|
|
326
|
+
properties: {
|
|
327
|
+
...nodeDefinition.configSchema.properties,
|
|
328
|
+
credentials: {
|
|
329
|
+
type: "object",
|
|
330
|
+
properties: nodeDefinition.credentialsSchema.properties,
|
|
331
|
+
},
|
|
337
332
|
},
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
: nodeDefinition.configSchema;
|
|
333
|
+
}
|
|
334
|
+
: nodeDefinition.configSchema;
|
|
341
335
|
|
|
342
336
|
const form =
|
|
343
337
|
definition.form ??
|
|
@@ -360,7 +354,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
360
354
|
const changed = !!Object.keys(changes)?.length;
|
|
361
355
|
if (!changed) return false;
|
|
362
356
|
|
|
363
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
357
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
364
358
|
if (!node._def.defaults?.[prop]?.type) return;
|
|
365
359
|
const oldConfigNodeId: string = node[prop] as string;
|
|
366
360
|
const newConfigNodeId: string = node._newState![prop] as string;
|
|
@@ -376,7 +370,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
376
370
|
}
|
|
377
371
|
});
|
|
378
372
|
|
|
379
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
373
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
380
374
|
if (!node._def.defaults?.[prop]?.type) return;
|
|
381
375
|
const newConfigNodeId: string = node._newState![prop] as string;
|
|
382
376
|
if (!newConfigNodeId) return;
|
|
@@ -400,7 +394,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
400
394
|
// overwriting the correctly-typed values already set by merge() above.
|
|
401
395
|
const isConfigNode = definition.category === "config";
|
|
402
396
|
if (isConfigNode) {
|
|
403
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
397
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
404
398
|
if (node._def.defaults[prop].type) return; // config-node refs handled separately
|
|
405
399
|
const inputId = `node-config-input-${prop}`;
|
|
406
400
|
let input = $(`#${inputId}`);
|
|
@@ -484,9 +478,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
484
478
|
*/
|
|
485
479
|
async function registerTypes(nodes: NodeDefinition[]): Promise<void> {
|
|
486
480
|
try {
|
|
487
|
-
console.log("Registering node types in parallel");
|
|
488
481
|
await Promise.all(nodes.map((definition) => registerType(definition)));
|
|
489
|
-
console.log("All node types registered in parallel");
|
|
490
482
|
} catch (error) {
|
|
491
483
|
console.error("Error registering node types:", error);
|
|
492
484
|
throw error;
|
package/src/core/server/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getCredentialsFromSchema } from "./utils";
|
|
|
4
4
|
import { Node } from "./nodes";
|
|
5
5
|
import { type RED } from "./types";
|
|
6
6
|
import { initValidator } from "./validator";
|
|
7
|
+
import { NrgError } from "../errors";
|
|
7
8
|
|
|
8
9
|
const MIME: Record<string, string> = {
|
|
9
10
|
".js": "application/javascript",
|
|
@@ -32,9 +33,7 @@ function serveNrgResources(RED: RED): void {
|
|
|
32
33
|
httpAdmin.use(function (req: any, res: any, next: any) {
|
|
33
34
|
const prefix = "/nrg/assets/";
|
|
34
35
|
if (!(req.path as string).startsWith(prefix)) return next();
|
|
35
|
-
let reqPath = (req.path as string)
|
|
36
|
-
.slice(prefix.length)
|
|
37
|
-
.replace(/\.\./g, "");
|
|
36
|
+
let reqPath = (req.path as string).slice(prefix.length);
|
|
38
37
|
// Serve the Vue dev build in development for devtools support
|
|
39
38
|
if (
|
|
40
39
|
reqPath === "vue.esm-browser.prod.js" &&
|
|
@@ -46,7 +45,8 @@ function serveNrgResources(RED: RED): void {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
const filePath = path.resolve(clientDir, reqPath);
|
|
49
|
-
|
|
48
|
+
const rel = path.relative(clientDir, filePath);
|
|
49
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return next();
|
|
50
50
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
51
51
|
return next();
|
|
52
52
|
const ext = path.extname(filePath);
|
|
@@ -70,24 +70,24 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
|
|
|
70
70
|
const NC = NodeClass as any;
|
|
71
71
|
RED.log.debug(`Registering Type: ${NC.type}`);
|
|
72
72
|
if (!(NC.prototype instanceof Node)) {
|
|
73
|
-
throw new
|
|
73
|
+
throw new NrgError(`${NC.name} must extend IONode or ConfigNode classes`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (!NC.type) {
|
|
77
|
-
throw new
|
|
77
|
+
throw new NrgError("type must be provided when registering the node");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (NC.color && !/^#[0-9A-Fa-f]{6}$/.test(NC.color)) {
|
|
81
|
-
throw new
|
|
81
|
+
throw new NrgError(
|
|
82
82
|
`Invalid color for ${NodeClass.type}: ${NC.color} color must be in hex format`,
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
if (
|
|
87
87
|
NC.inputs !== undefined &&
|
|
88
|
-
(!Number.isInteger(NC.inputs) || (NC.inputs
|
|
88
|
+
(!Number.isInteger(NC.inputs) || (NC.inputs !== 0 && NC.inputs !== 1))
|
|
89
89
|
) {
|
|
90
|
-
throw new
|
|
90
|
+
throw new NrgError(
|
|
91
91
|
`Invalid number of inputs for ${NodeClass.type}: inputs must be 0 or 1`,
|
|
92
92
|
);
|
|
93
93
|
}
|
|
@@ -96,7 +96,7 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
|
|
|
96
96
|
NC.outputs !== undefined &&
|
|
97
97
|
(!Number.isInteger(NC.outputs) || NC.outputs < 0)
|
|
98
98
|
) {
|
|
99
|
-
throw new
|
|
99
|
+
throw new NrgError(
|
|
100
100
|
`Invalid number of outputs for ${NodeClass.type}: outputs must be a positive integer`,
|
|
101
101
|
);
|
|
102
102
|
}
|
|
@@ -106,8 +106,14 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
|
|
|
106
106
|
function (this: any, config: any) {
|
|
107
107
|
RED.nodes.createNode(this, config);
|
|
108
108
|
const node = new NC(RED, this, config, this.credentials);
|
|
109
|
-
// NOTE: save node
|
|
110
|
-
|
|
109
|
+
// NOTE: save node instance inside node-red's node so that the proxy can resolve it lazily.
|
|
110
|
+
// Non-writable to prevent accidental clobbering by other code in the process.
|
|
111
|
+
Object.defineProperty(this, "_node", {
|
|
112
|
+
value: node,
|
|
113
|
+
writable: false,
|
|
114
|
+
configurable: false,
|
|
115
|
+
enumerable: false,
|
|
116
|
+
});
|
|
111
117
|
|
|
112
118
|
// 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
|
|
113
119
|
const createdPromise = Promise.resolve(node.created?.()).catch(
|
|
@@ -214,6 +220,7 @@ function registerTypes(nodes: AnyNodeClass[]): NodeRedPackageFunction {
|
|
|
214
220
|
|
|
215
221
|
export { registerType, registerTypes };
|
|
216
222
|
export { Node, IONode, ConfigNode } from "./nodes";
|
|
223
|
+
export { NrgError } from "../errors";
|
|
217
224
|
export type { RED } from "./types";
|
|
218
225
|
export { SchemaType, defineSchema } from "./schemas";
|
|
219
226
|
export type { Schema, Infer } from "./schemas/types";
|
|
@@ -115,7 +115,11 @@ abstract class Node<TConfig = any, TCredentials = any, TSettings = any> {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
(this as any).config = setupConfigProxy(
|
|
118
|
+
(this as any).config = setupConfigProxy(
|
|
119
|
+
RED,
|
|
120
|
+
config,
|
|
121
|
+
constructor.configSchema,
|
|
122
|
+
);
|
|
119
123
|
|
|
120
124
|
if (constructor.credentialsSchema && credentials) {
|
|
121
125
|
this.log("Validating credentials");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResolveNodeRefs } from "../schemas/types";
|
|
2
2
|
import type { RED, NodeRedContextStore } from "../types";
|
|
3
3
|
import type { NodeContextStore } from "./types";
|
|
4
|
+
import { NrgError } from "../../errors";
|
|
4
5
|
|
|
5
6
|
function setupContext(
|
|
6
7
|
context: NodeRedContextStore,
|
|
@@ -31,50 +32,72 @@ function setupContext(
|
|
|
31
32
|
function setupConfigProxy<T extends object>(
|
|
32
33
|
RED: RED,
|
|
33
34
|
config: T,
|
|
35
|
+
schema?: any,
|
|
34
36
|
): ResolveNodeRefs<T> {
|
|
35
|
-
// NOTE: must not proxy its own id or parents ids
|
|
36
37
|
const SKIP_PROPS = new Set(["id", "_id", "_users"]);
|
|
37
38
|
|
|
39
|
+
// Build a set of property names that are node references based on the schema.
|
|
40
|
+
// Only these properties will have their string values resolved via RED.nodes.getNode().
|
|
41
|
+
const nodeRefProps = new Set<string>();
|
|
42
|
+
if (schema?.properties) {
|
|
43
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
44
|
+
if ((propSchema as any)?.["x-nrg-node-type"]) {
|
|
45
|
+
nodeRefProps.add(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Per-node-instance cache: original object/array -> proxy or mapped array.
|
|
51
|
+
// This preserves reference equality: config.server === config.server
|
|
52
|
+
const cache = new WeakMap<object, any>();
|
|
53
|
+
|
|
38
54
|
const createProxy = <O extends object>(obj: O): any => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (typeof prop === "symbol") {
|
|
42
|
-
return target[prop];
|
|
43
|
-
}
|
|
55
|
+
const cached = cache.get(obj);
|
|
56
|
+
if (cached) return cached;
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
if (Array.isArray(obj)) {
|
|
59
|
+
// Map once, cache the result array so identity is stable across reads
|
|
60
|
+
const mapped = obj.map((item) => {
|
|
61
|
+
if (item && typeof item === "object") {
|
|
62
|
+
return createProxy(item);
|
|
47
63
|
}
|
|
64
|
+
return item;
|
|
65
|
+
});
|
|
66
|
+
cache.set(obj, mapped);
|
|
67
|
+
return mapped;
|
|
68
|
+
}
|
|
48
69
|
|
|
49
|
-
|
|
70
|
+
const proxy = new Proxy(obj, {
|
|
71
|
+
get(target: any, prop: string | symbol): any {
|
|
72
|
+
if (typeof prop === "symbol") return target[prop];
|
|
73
|
+
if (SKIP_PROPS.has(prop)) return target[prop];
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
// NOTE: using the instance provided by the user instead of node-red's internal one
|
|
53
|
-
const node = RED.nodes.getNode(value)?._node;
|
|
54
|
-
return node || value;
|
|
55
|
-
}
|
|
75
|
+
const value = target[prop];
|
|
56
76
|
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (item && typeof item === "object") {
|
|
65
|
-
return createProxy(item);
|
|
66
|
-
}
|
|
67
|
-
return item;
|
|
68
|
-
});
|
|
77
|
+
// Only resolve strings as node references if the schema marks the property
|
|
78
|
+
if (
|
|
79
|
+
typeof value === "string" &&
|
|
80
|
+
value.length > 0 &&
|
|
81
|
+
nodeRefProps.has(prop)
|
|
82
|
+
) {
|
|
83
|
+
return RED.nodes.getNode(value)?._node ?? value;
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
if (value && typeof value === "object") {
|
|
72
|
-
return createProxy(value);
|
|
87
|
+
return createProxy(value); // hits the cache on repeat access
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
return value;
|
|
76
91
|
},
|
|
92
|
+
set(_target: any, prop: string | symbol): boolean {
|
|
93
|
+
throw new NrgError(
|
|
94
|
+
`Cannot set property '${String(prop)}' on read-only node config`,
|
|
95
|
+
);
|
|
96
|
+
},
|
|
77
97
|
});
|
|
98
|
+
|
|
99
|
+
cache.set(obj, proxy);
|
|
100
|
+
return proxy;
|
|
78
101
|
};
|
|
79
102
|
|
|
80
103
|
return createProxy(config) as ResolveNodeRefs<T>;
|
package/src/vite/client/build.ts
CHANGED