@bonsae/nrg 0.18.5 → 0.19.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 (36) hide show
  1. package/package.json +1 -1
  2. package/server/index.cjs +86 -9
  3. package/server/resources/nrg-client.js +2020 -1987
  4. package/test/client/component/config.js +11 -0
  5. package/test/client/component/index.js +218 -235
  6. package/test/client/component/nrg.css +1 -0
  7. package/test/client/component/setup.js +1549 -140
  8. package/test/client/e2e/index.js +720 -369
  9. package/test/client/unit/index.js +204 -16
  10. package/test/client/unit/setup.js +209 -19
  11. package/test/server/unit/index.js +25 -4
  12. package/tsconfig/core/client.json +1 -1
  13. package/tsconfig/test/client/component.json +1 -1
  14. package/types/client.d.ts +98 -18
  15. package/types/server.d.ts +50 -12
  16. package/types/shims/brands.d.ts +32 -0
  17. package/types/shims/{form → client/form}/components/node-red-editor-input.vue.d.ts +1 -1
  18. package/types/shims/{form → client/form}/components/node-red-json-schema-form.vue.d.ts +21 -2
  19. package/types/shims/{form → client/form}/components/node-red-select-input.vue.d.ts +1 -0
  20. package/types/shims/{form → client/form}/components/node-red-typed-input.vue.d.ts +1 -0
  21. package/types/shims/client/types.d.ts +206 -0
  22. package/types/shims/components.d.ts +8 -8
  23. package/types/shims/constants.d.ts +4 -0
  24. package/types/shims/schema-options.d.ts +23 -10
  25. package/types/shims/typebox.d.ts +2 -2
  26. package/types/test-client-component.d.ts +170 -55
  27. package/types/test-client-e2e.d.ts +50 -0
  28. package/types/test-client-unit.d.ts +86 -22
  29. package/types/test-server-unit.d.ts +3 -1
  30. package/types/vite.d.ts +38 -9
  31. package/vite/index.js +732 -530
  32. /package/types/shims/{form → client/form}/components/node-red-config-input.vue.d.ts +0 -0
  33. /package/types/shims/{form → client/form}/components/node-red-input-label.vue.d.ts +0 -0
  34. /package/types/shims/{form → client/form}/components/node-red-input.vue.d.ts +0 -0
  35. /package/types/shims/{form → client/form}/components/node-red-toggle.vue.d.ts +0 -0
  36. /package/types/shims/{globals.d.ts → client/globals.d.ts} +0 -0
@@ -1,18 +1,67 @@
1
1
  // src/test/client/mocks.ts
2
+ function createSettings() {
3
+ const settings = {
4
+ get(key) {
5
+ return settings[key];
6
+ },
7
+ set(key, value) {
8
+ settings[key] = value;
9
+ return value;
10
+ },
11
+ remove(key) {
12
+ const value = settings[key];
13
+ delete settings[key];
14
+ return value;
15
+ }
16
+ };
17
+ return settings;
18
+ }
19
+ function topicMatches(pattern, topic) {
20
+ if (pattern === topic) return true;
21
+ const p = pattern.split("/");
22
+ const t = topic.split("/");
23
+ for (let i = 0; i < p.length; i++) {
24
+ if (p[i] === "#") return true;
25
+ if (i >= t.length) return false;
26
+ if (p[i] !== "+" && p[i] !== t[i]) return false;
27
+ }
28
+ return p.length === t.length;
29
+ }
2
30
  function createRED() {
3
- return {
31
+ const nodeStore = /* @__PURE__ */ new Map();
32
+ const typeRegistry = /* @__PURE__ */ new Map();
33
+ const links = [];
34
+ const eventListeners = {};
35
+ const commsSubscriptions = [];
36
+ let dirtyState = false;
37
+ let idCounter = 0;
38
+ const popoverInstance = () => {
39
+ const instance = {
40
+ element: null,
41
+ open: () => instance,
42
+ close: () => instance,
43
+ setContent: () => instance,
44
+ move: () => {
45
+ }
46
+ };
47
+ return instance;
48
+ };
49
+ const red = {
4
50
  _: (key) => key,
5
51
  editor: {
6
52
  createEditor(options) {
7
- let currentValue = options.value || "";
53
+ let currentValue = options?.value || "";
54
+ const sessionListeners = {};
8
55
  const session = {
9
- on(_event, _cb) {
56
+ on(event, cb) {
57
+ (sessionListeners[event] ??= []).push(cb);
10
58
  }
11
59
  };
12
60
  return {
13
61
  getValue: () => currentValue,
14
62
  setValue: (val) => {
15
63
  currentValue = val;
64
+ (sessionListeners["change"] ?? []).forEach((cb) => cb());
16
65
  },
17
66
  getSession: () => session,
18
67
  focus: () => {
@@ -36,28 +85,133 @@ function createRED() {
36
85
  }
37
86
  },
38
87
  popover: {
39
- tooltip: () => ({ delete: () => {
40
- }, setAction: () => {
41
- } })
88
+ create: () => popoverInstance(),
89
+ tooltip: () => {
90
+ const instance = {
91
+ element: null,
92
+ open: () => instance,
93
+ close: () => instance,
94
+ setContent: () => instance,
95
+ move: () => {
96
+ },
97
+ delete: () => {
98
+ },
99
+ setAction: () => {
100
+ }
101
+ };
102
+ return instance;
103
+ }
42
104
  },
43
105
  nodes: {
44
- registerType: () => {
106
+ registerType(type, definition) {
107
+ typeRegistry.set(type, definition);
108
+ },
109
+ getType(type) {
110
+ return typeRegistry.get(type) ?? null;
111
+ },
112
+ node(id) {
113
+ return nodeStore.get(id) ?? null;
114
+ },
115
+ add(node) {
116
+ nodeStore.set(node.id, node);
117
+ return node;
118
+ },
119
+ remove(id) {
120
+ const node = nodeStore.get(id);
121
+ nodeStore.delete(id);
122
+ return { links: [], nodes: node ? [node] : [] };
123
+ },
124
+ clear() {
125
+ nodeStore.clear();
126
+ },
127
+ // The mock keeps a single registry: eachNode and eachConfig iterate the
128
+ // same entries. Register whatever your component expects to find.
129
+ eachNode(callback) {
130
+ for (const node of nodeStore.values()) {
131
+ if (callback(node) === false) break;
132
+ }
133
+ },
134
+ eachConfig(callback) {
135
+ for (const node of nodeStore.values()) {
136
+ if (callback(node) === false) break;
137
+ }
45
138
  },
46
- node: () => null,
47
- dirty: () => false
139
+ filterNodes(filter) {
140
+ return [...nodeStore.values()].filter(
141
+ (n) => (filter.type === void 0 || n.type === filter.type) && (filter.z === void 0 || n.z === filter.z)
142
+ );
143
+ },
144
+ filterLinks(filter) {
145
+ return links.filter(
146
+ (l) => (filter.source === void 0 || l.source === filter.source || l.source?.id === filter.source?.id) && (filter.target === void 0 || l.target === filter.target || l.target?.id === filter.target?.id)
147
+ );
148
+ },
149
+ addLink(link) {
150
+ links.push(link);
151
+ },
152
+ dirty(state) {
153
+ if (state === void 0) return dirtyState;
154
+ dirtyState = state;
155
+ },
156
+ id() {
157
+ return (++idCounter).toString(16).padStart(16, "0");
158
+ }
48
159
  },
49
160
  events: {
50
- on: () => {
161
+ on(event, listener) {
162
+ (eventListeners[event] ??= []).push(listener);
51
163
  },
52
- off: () => {
164
+ off(event, listener) {
165
+ if (!eventListeners[event]) return;
166
+ if (listener) {
167
+ eventListeners[event] = eventListeners[event].filter(
168
+ (l) => l !== listener
169
+ );
170
+ } else {
171
+ delete eventListeners[event];
172
+ }
53
173
  },
54
- emit: () => {
174
+ emit(event, ...args) {
175
+ [...eventListeners[event] ?? []].forEach((cb) => cb(...args));
55
176
  }
56
177
  },
57
- settings: {},
58
- notify: () => {
59
- }
178
+ comms: {
179
+ subscribe(topic, callback) {
180
+ commsSubscriptions.push({ topic, callback });
181
+ },
182
+ unsubscribe(topic, callback) {
183
+ const index = commsSubscriptions.findIndex(
184
+ (s) => s.topic === topic && s.callback === callback
185
+ );
186
+ if (index !== -1) commsSubscriptions.splice(index, 1);
187
+ },
188
+ publish(topic, msg) {
189
+ [...commsSubscriptions].filter((s) => topicMatches(s.topic, topic)).forEach((s) => s.callback(topic, msg));
190
+ }
191
+ },
192
+ settings: createSettings(),
193
+ notify: () => ({
194
+ update: () => {
195
+ },
196
+ close: () => {
197
+ }
198
+ })
60
199
  };
200
+ Object.defineProperty(red, "__reset", {
201
+ enumerable: false,
202
+ value: () => {
203
+ nodeStore.clear();
204
+ typeRegistry.clear();
205
+ links.length = 0;
206
+ for (const key of Object.keys(eventListeners)) {
207
+ delete eventListeners[key];
208
+ }
209
+ commsSubscriptions.length = 0;
210
+ dirtyState = false;
211
+ red.settings = createSettings();
212
+ }
213
+ });
214
+ return red;
61
215
  }
62
216
  function ensureState(el) {
63
217
  if (el && !el.__jqState) {
@@ -75,7 +229,11 @@ function createJQ(el) {
75
229
  length: el ? 1 : 0,
76
230
  typedInput(action, value) {
77
231
  if (typeof action === "object") {
78
- state.typedInput = { value: "", type: action.default || "" };
232
+ state.typedInput = {
233
+ value: "",
234
+ type: action.default || "",
235
+ types: action.types
236
+ };
79
237
  return jq;
80
238
  }
81
239
  if (action === "value") {
@@ -93,6 +251,36 @@ function createJQ(el) {
93
251
  }
94
252
  return state.typedInput.type;
95
253
  }
254
+ if (action === "types") {
255
+ state.typedInput.types = value;
256
+ return void 0;
257
+ }
258
+ if (action === "validate") {
259
+ return true;
260
+ }
261
+ if (action === "disable") {
262
+ state.typedInput.disabled = value !== false;
263
+ return void 0;
264
+ }
265
+ if (action === "enable") {
266
+ state.typedInput.disabled = false;
267
+ return void 0;
268
+ }
269
+ if (action === "hide") {
270
+ state.typedInput.hidden = true;
271
+ return void 0;
272
+ }
273
+ if (action === "show") {
274
+ state.typedInput.hidden = false;
275
+ return void 0;
276
+ }
277
+ if (action === "width") {
278
+ state.typedInput.width = value;
279
+ return void 0;
280
+ }
281
+ if (action === "focus") {
282
+ return void 0;
283
+ }
96
284
  return jq;
97
285
  },
98
286
  on(event, cb) {
@@ -2,20 +2,72 @@
2
2
  import { beforeEach } from "vitest";
3
3
 
4
4
  // src/test/client/mocks.ts
5
+ function createSettings() {
6
+ const settings = {
7
+ get(key) {
8
+ return settings[key];
9
+ },
10
+ set(key, value) {
11
+ settings[key] = value;
12
+ return value;
13
+ },
14
+ remove(key) {
15
+ const value = settings[key];
16
+ delete settings[key];
17
+ return value;
18
+ }
19
+ };
20
+ return settings;
21
+ }
22
+ function topicMatches(pattern, topic) {
23
+ if (pattern === topic) return true;
24
+ const p = pattern.split("/");
25
+ const t = topic.split("/");
26
+ for (let i = 0; i < p.length; i++) {
27
+ if (p[i] === "#") return true;
28
+ if (i >= t.length) return false;
29
+ if (p[i] !== "+" && p[i] !== t[i]) return false;
30
+ }
31
+ return p.length === t.length;
32
+ }
33
+ function resetRED(red) {
34
+ red.__reset?.();
35
+ }
5
36
  function createRED() {
6
- return {
37
+ const nodeStore = /* @__PURE__ */ new Map();
38
+ const typeRegistry = /* @__PURE__ */ new Map();
39
+ const links = [];
40
+ const eventListeners = {};
41
+ const commsSubscriptions = [];
42
+ let dirtyState = false;
43
+ let idCounter = 0;
44
+ const popoverInstance = () => {
45
+ const instance = {
46
+ element: null,
47
+ open: () => instance,
48
+ close: () => instance,
49
+ setContent: () => instance,
50
+ move: () => {
51
+ }
52
+ };
53
+ return instance;
54
+ };
55
+ const red = {
7
56
  _: (key) => key,
8
57
  editor: {
9
58
  createEditor(options) {
10
- let currentValue = options.value || "";
59
+ let currentValue = options?.value || "";
60
+ const sessionListeners = {};
11
61
  const session = {
12
- on(_event, _cb) {
62
+ on(event, cb) {
63
+ (sessionListeners[event] ??= []).push(cb);
13
64
  }
14
65
  };
15
66
  return {
16
67
  getValue: () => currentValue,
17
68
  setValue: (val) => {
18
69
  currentValue = val;
70
+ (sessionListeners["change"] ?? []).forEach((cb) => cb());
19
71
  },
20
72
  getSession: () => session,
21
73
  focus: () => {
@@ -39,28 +91,133 @@ function createRED() {
39
91
  }
40
92
  },
41
93
  popover: {
42
- tooltip: () => ({ delete: () => {
43
- }, setAction: () => {
44
- } })
94
+ create: () => popoverInstance(),
95
+ tooltip: () => {
96
+ const instance = {
97
+ element: null,
98
+ open: () => instance,
99
+ close: () => instance,
100
+ setContent: () => instance,
101
+ move: () => {
102
+ },
103
+ delete: () => {
104
+ },
105
+ setAction: () => {
106
+ }
107
+ };
108
+ return instance;
109
+ }
45
110
  },
46
111
  nodes: {
47
- registerType: () => {
112
+ registerType(type, definition) {
113
+ typeRegistry.set(type, definition);
114
+ },
115
+ getType(type) {
116
+ return typeRegistry.get(type) ?? null;
117
+ },
118
+ node(id) {
119
+ return nodeStore.get(id) ?? null;
120
+ },
121
+ add(node) {
122
+ nodeStore.set(node.id, node);
123
+ return node;
124
+ },
125
+ remove(id) {
126
+ const node = nodeStore.get(id);
127
+ nodeStore.delete(id);
128
+ return { links: [], nodes: node ? [node] : [] };
129
+ },
130
+ clear() {
131
+ nodeStore.clear();
132
+ },
133
+ // The mock keeps a single registry: eachNode and eachConfig iterate the
134
+ // same entries. Register whatever your component expects to find.
135
+ eachNode(callback) {
136
+ for (const node of nodeStore.values()) {
137
+ if (callback(node) === false) break;
138
+ }
139
+ },
140
+ eachConfig(callback) {
141
+ for (const node of nodeStore.values()) {
142
+ if (callback(node) === false) break;
143
+ }
48
144
  },
49
- node: () => null,
50
- dirty: () => false
145
+ filterNodes(filter) {
146
+ return [...nodeStore.values()].filter(
147
+ (n) => (filter.type === void 0 || n.type === filter.type) && (filter.z === void 0 || n.z === filter.z)
148
+ );
149
+ },
150
+ filterLinks(filter) {
151
+ return links.filter(
152
+ (l) => (filter.source === void 0 || l.source === filter.source || l.source?.id === filter.source?.id) && (filter.target === void 0 || l.target === filter.target || l.target?.id === filter.target?.id)
153
+ );
154
+ },
155
+ addLink(link) {
156
+ links.push(link);
157
+ },
158
+ dirty(state) {
159
+ if (state === void 0) return dirtyState;
160
+ dirtyState = state;
161
+ },
162
+ id() {
163
+ return (++idCounter).toString(16).padStart(16, "0");
164
+ }
51
165
  },
52
166
  events: {
53
- on: () => {
167
+ on(event, listener) {
168
+ (eventListeners[event] ??= []).push(listener);
54
169
  },
55
- off: () => {
170
+ off(event, listener) {
171
+ if (!eventListeners[event]) return;
172
+ if (listener) {
173
+ eventListeners[event] = eventListeners[event].filter(
174
+ (l) => l !== listener
175
+ );
176
+ } else {
177
+ delete eventListeners[event];
178
+ }
56
179
  },
57
- emit: () => {
180
+ emit(event, ...args) {
181
+ [...eventListeners[event] ?? []].forEach((cb) => cb(...args));
58
182
  }
59
183
  },
60
- settings: {},
61
- notify: () => {
62
- }
184
+ comms: {
185
+ subscribe(topic, callback) {
186
+ commsSubscriptions.push({ topic, callback });
187
+ },
188
+ unsubscribe(topic, callback) {
189
+ const index = commsSubscriptions.findIndex(
190
+ (s) => s.topic === topic && s.callback === callback
191
+ );
192
+ if (index !== -1) commsSubscriptions.splice(index, 1);
193
+ },
194
+ publish(topic, msg) {
195
+ [...commsSubscriptions].filter((s) => topicMatches(s.topic, topic)).forEach((s) => s.callback(topic, msg));
196
+ }
197
+ },
198
+ settings: createSettings(),
199
+ notify: () => ({
200
+ update: () => {
201
+ },
202
+ close: () => {
203
+ }
204
+ })
63
205
  };
206
+ Object.defineProperty(red, "__reset", {
207
+ enumerable: false,
208
+ value: () => {
209
+ nodeStore.clear();
210
+ typeRegistry.clear();
211
+ links.length = 0;
212
+ for (const key of Object.keys(eventListeners)) {
213
+ delete eventListeners[key];
214
+ }
215
+ commsSubscriptions.length = 0;
216
+ dirtyState = false;
217
+ red.settings = createSettings();
218
+ }
219
+ });
220
+ return red;
64
221
  }
65
222
  function ensureState(el) {
66
223
  if (el && !el.__jqState) {
@@ -78,7 +235,11 @@ function createJQ(el) {
78
235
  length: el ? 1 : 0,
79
236
  typedInput(action, value) {
80
237
  if (typeof action === "object") {
81
- state.typedInput = { value: "", type: action.default || "" };
238
+ state.typedInput = {
239
+ value: "",
240
+ type: action.default || "",
241
+ types: action.types
242
+ };
82
243
  return jq;
83
244
  }
84
245
  if (action === "value") {
@@ -96,6 +257,36 @@ function createJQ(el) {
96
257
  }
97
258
  return state.typedInput.type;
98
259
  }
260
+ if (action === "types") {
261
+ state.typedInput.types = value;
262
+ return void 0;
263
+ }
264
+ if (action === "validate") {
265
+ return true;
266
+ }
267
+ if (action === "disable") {
268
+ state.typedInput.disabled = value !== false;
269
+ return void 0;
270
+ }
271
+ if (action === "enable") {
272
+ state.typedInput.disabled = false;
273
+ return void 0;
274
+ }
275
+ if (action === "hide") {
276
+ state.typedInput.hidden = true;
277
+ return void 0;
278
+ }
279
+ if (action === "show") {
280
+ state.typedInput.hidden = false;
281
+ return void 0;
282
+ }
283
+ if (action === "width") {
284
+ state.typedInput.width = value;
285
+ return void 0;
286
+ }
287
+ if (action === "focus") {
288
+ return void 0;
289
+ }
99
290
  return jq;
100
291
  },
101
292
  on(event, cb) {
@@ -191,9 +382,8 @@ function createJQuery() {
191
382
  }
192
383
 
193
384
  // src/test/client/unit/setup.ts
194
- var RED = createRED();
195
385
  window.$ = createJQuery();
196
- window.RED = RED;
386
+ window.RED = createRED();
197
387
  beforeEach(() => {
198
- RED.settings = {};
388
+ resetRED(window.RED);
199
389
  });
@@ -6,6 +6,7 @@ import { vi } from "vitest";
6
6
  function createRED(options = {}) {
7
7
  const { settings = {} } = options;
8
8
  const nodes = {};
9
+ const credentials = {};
9
10
  const red = {
10
11
  log: {
11
12
  info: vi.fn(),
@@ -31,7 +32,10 @@ function createRED(options = {}) {
31
32
  getNode: vi.fn((id) => nodes[id]),
32
33
  registerType: vi.fn(),
33
34
  createNode: vi.fn(),
34
- getCredentials: vi.fn(),
35
+ getCredentials: vi.fn((id) => credentials[id]),
36
+ addCredentials: vi.fn((id, creds) => {
37
+ credentials[id] = { ...credentials[id], ...creds };
38
+ }),
35
39
  eachNode: vi.fn(),
36
40
  getType: vi.fn(),
37
41
  getNodeInfo: vi.fn(),
@@ -126,12 +130,16 @@ function createRED(options = {}) {
126
130
  }
127
131
  ),
128
132
  generateId: vi.fn(() => "mock-id"),
129
- cloneMessage: vi.fn((msg) => ({ ...msg })),
133
+ cloneMessage: vi.fn((msg) => structuredClone(msg)),
130
134
  ensureString: vi.fn((o) => String(o)),
131
135
  ensureBuffer: vi.fn(),
132
136
  compareObjects: vi.fn(),
133
- getMessageProperty: vi.fn(),
134
- setMessageProperty: vi.fn(),
137
+ getMessageProperty: vi.fn(
138
+ (msg, prop) => getProperty(msg, prop)
139
+ ),
140
+ setMessageProperty: vi.fn(
141
+ (msg, prop, value, createMissing) => setProperty(msg, prop, value, createMissing ?? false)
142
+ ),
135
143
  getObjectProperty: vi.fn(),
136
144
  setObjectProperty: vi.fn(),
137
145
  normalisePropertyExpression: vi.fn(),
@@ -156,6 +164,19 @@ function createRED(options = {}) {
156
164
  function getProperty(obj, path) {
157
165
  return path.split(".").reduce((acc, key) => acc?.[key], obj);
158
166
  }
167
+ function setProperty(obj, path, value, createMissing) {
168
+ const keys = path.split(".");
169
+ let target = obj;
170
+ for (const key of keys.slice(0, -1)) {
171
+ if (target[key] == null || typeof target[key] !== "object") {
172
+ if (!createMissing) return false;
173
+ target[key] = {};
174
+ }
175
+ target = target[key];
176
+ }
177
+ target[keys[keys.length - 1]] = value;
178
+ return true;
179
+ }
159
180
  function createContextStore() {
160
181
  const store = {};
161
182
  return {
@@ -6,6 +6,6 @@
6
6
  "files": [
7
7
  "../../types/shims/shims-vue.d.ts",
8
8
  "../../types/shims/components.d.ts",
9
- "../../types/shims/globals.d.ts"
9
+ "../../types/shims/client/globals.d.ts"
10
10
  ]
11
11
  }
@@ -6,6 +6,6 @@
6
6
  "files": [
7
7
  "../../../types/shims/shims-vue.d.ts",
8
8
  "../../../types/shims/components.d.ts",
9
- "../../../types/shims/globals.d.ts"
9
+ "../../../types/shims/client/globals.d.ts"
10
10
  ]
11
11
  }