@bonsae/nrg 0.18.5 → 0.19.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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/server/index.cjs +86 -9
  4. package/server/resources/nrg-client.js +2020 -1987
  5. package/test/client/component/config.js +11 -0
  6. package/test/client/component/index.js +218 -235
  7. package/test/client/component/nrg.css +1 -0
  8. package/test/client/component/setup.js +1549 -140
  9. package/test/client/e2e/index.js +706 -368
  10. package/test/client/unit/index.js +204 -16
  11. package/test/client/unit/setup.js +209 -19
  12. package/test/server/unit/index.js +25 -4
  13. package/tsconfig/core/client.json +1 -1
  14. package/tsconfig/test/client/component.json +1 -1
  15. package/types/client.d.ts +98 -18
  16. package/types/server.d.ts +50 -12
  17. package/types/shims/brands.d.ts +32 -0
  18. package/types/shims/{form → client/form}/components/node-red-editor-input.vue.d.ts +1 -1
  19. package/types/shims/{form → client/form}/components/node-red-json-schema-form.vue.d.ts +21 -2
  20. package/types/shims/{form → client/form}/components/node-red-select-input.vue.d.ts +1 -0
  21. package/types/shims/{form → client/form}/components/node-red-typed-input.vue.d.ts +1 -0
  22. package/types/shims/client/types.d.ts +206 -0
  23. package/types/shims/components.d.ts +8 -8
  24. package/types/shims/constants.d.ts +4 -0
  25. package/types/shims/schema-options.d.ts +23 -10
  26. package/types/shims/typebox.d.ts +2 -2
  27. package/types/test-client-component.d.ts +170 -55
  28. package/types/test-client-e2e.d.ts +50 -0
  29. package/types/test-client-unit.d.ts +86 -22
  30. package/types/test-server-unit.d.ts +3 -1
  31. package/types/vite.d.ts +25 -9
  32. package/vite/index.js +648 -499
  33. /package/types/shims/{form → client/form}/components/node-red-config-input.vue.d.ts +0 -0
  34. /package/types/shims/{form → client/form}/components/node-red-input-label.vue.d.ts +0 -0
  35. /package/types/shims/{form → client/form}/components/node-red-input.vue.d.ts +0 -0
  36. /package/types/shims/{form → client/form}/components/node-red-toggle.vue.d.ts +0 -0
  37. /package/types/shims/{globals.d.ts → client/globals.d.ts} +0 -0
@@ -18,6 +18,17 @@ var defaultConfig = {
18
18
  allow: [".."]
19
19
  }
20
20
  },
21
+ optimizeDeps: {
22
+ // @bonsae/nrg/server is a CJS bundle — prebundling converts it so browser
23
+ // tests can import TypeBox schemas straight from server schema modules.
24
+ include: [
25
+ "@bonsae/nrg/server",
26
+ "jsonpointer",
27
+ "ajv",
28
+ "ajv-formats",
29
+ "ajv-errors"
30
+ ]
31
+ },
21
32
  test: {
22
33
  testTimeout: 3e4,
23
34
  setupFiles: ["@bonsae/nrg/test/client/component/setup"],
@@ -1,259 +1,242 @@
1
- // src/test/client/component/index.ts
2
- import { vi } from "vitest";
3
-
4
- // src/test/client/mocks.ts
5
- function createRED() {
6
- return {
7
- _: (key) => key,
8
- editor: {
9
- createEditor(options) {
10
- let currentValue = options.value || "";
11
- const session = {
12
- on(_event, _cb) {
13
- }
14
- };
15
- return {
16
- getValue: () => currentValue,
17
- setValue: (val) => {
18
- currentValue = val;
19
- },
20
- getSession: () => session,
21
- focus: () => {
22
- },
23
- destroy: () => {
24
- },
25
- saveView: () => {
26
- },
27
- restoreView: () => {
28
- }
29
- };
30
- },
31
- prepareConfigNodeSelect: () => {
32
- },
33
- validateNode: () => true
34
- },
35
- tray: {
36
- show: () => {
37
- },
38
- close: () => {
39
- }
40
- },
41
- popover: {
42
- tooltip: () => ({ delete: () => {
43
- }, setAction: () => {
44
- } })
45
- },
46
- nodes: {
47
- registerType: () => {
48
- },
49
- node: () => null,
50
- dirty: () => false
51
- },
52
- events: {
53
- on: () => {
54
- },
55
- off: () => {
1
+ var y = Object.defineProperty;
2
+ var _ = (r, e, t) => e in r ? y(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t;
3
+ var u = (r, e, t) => _(r, typeof e != "symbol" ? e + "" : e, t);
4
+ import { vi as l } from "vitest";
5
+ import { inject as m, reactive as p, watch as g } from "vue";
6
+ import w from "jsonpointer";
7
+ import j from "ajv";
8
+ import $ from "ajv-formats";
9
+ import F from "ajv-errors";
10
+ class b {
11
+ constructor(e) {
12
+ u(this, "ajv");
13
+ const { customKeywords: t, customFormats: o, ...s } = e || {};
14
+ this.ajv = new j({
15
+ allErrors: !0,
16
+ code: {
17
+ source: !1
56
18
  },
57
- emit: () => {
58
- }
59
- },
60
- settings: {},
61
- notify: () => {
19
+ coerceTypes: !0,
20
+ removeAdditional: !1,
21
+ strict: !1,
22
+ strictSchema: !1,
23
+ useDefaults: !0,
24
+ validateFormats: !0,
25
+ // NOTE: typebox handles validation via typescript
26
+ // NOTE: if true, types that are not serializable JSON, like Function, would not work
27
+ validateSchema: !1,
28
+ verbose: !0,
29
+ ...s
30
+ }), $(this.ajv), F(this.ajv), this.addCustomKeywords(t || []), this.addCustomFormats(o || {});
31
+ }
32
+ /**
33
+ * Add custom keywords to the validator
34
+ */
35
+ addCustomKeywords(e) {
36
+ e && e.forEach((t) => {
37
+ this.ajv.addKeyword(t);
38
+ });
39
+ }
40
+ /**
41
+ * Add custom formats to the validator
42
+ */
43
+ addCustomFormats(e) {
44
+ e && Object.entries(e).forEach(([t, o]) => {
45
+ o instanceof RegExp ? this.ajv.addFormat(t, o) : this.ajv.addFormat(t, { validate: o });
46
+ });
47
+ }
48
+ /**
49
+ * Create a validator function with caching
50
+ * @param schema - JSON Schema to validate against
51
+ * @param cacheKey - Optional cache key for reusing validators
52
+ */
53
+ createValidator(e, t) {
54
+ if (t && !e.$id && (e.$id = t), e.$id) {
55
+ const s = this.ajv.getSchema(e.$id);
56
+ if (s) return s;
62
57
  }
63
- };
64
- }
65
- function ensureState(el) {
66
- if (el && !el.__jqState) {
67
- el.__jqState = {
68
- typedInput: { value: "", type: "" },
69
- listeners: {}
58
+ return this.ajv.compile(e);
59
+ }
60
+ /**
61
+ * Validate data against a schema and return a structured result
62
+ */
63
+ validate(e, t, o) {
64
+ const s = this.createValidator(t, o == null ? void 0 : o.cacheKey);
65
+ if (!s(e)) {
66
+ const n = this.formatErrors(s.errors);
67
+ if (o != null && o.throwOnError)
68
+ throw new f(n, s.errors || []);
69
+ return {
70
+ valid: !1,
71
+ errors: s.errors || void 0,
72
+ errorMessage: n
73
+ };
74
+ }
75
+ return {
76
+ valid: !0,
77
+ data: e
70
78
  };
71
79
  }
72
- return el ? el.__jqState : { typedInput: { value: "", type: "" }, listeners: {} };
80
+ /**
81
+ * Format errors into a human-readable string
82
+ */
83
+ formatErrors(e, t) {
84
+ return !e || e.length === 0 ? "No errors" : this.ajv.errorsText(e, {
85
+ separator: "; ",
86
+ dataVar: "data",
87
+ ...t
88
+ });
89
+ }
90
+ /**
91
+ * Get detailed error information
92
+ */
93
+ getDetailedErrors(e) {
94
+ return !e || e.length === 0 ? [] : e.map((t) => ({
95
+ field: t.instancePath || "/",
96
+ message: t.message || "Validation failed",
97
+ keyword: t.keyword,
98
+ params: t.params,
99
+ schemaPath: t.schemaPath
100
+ }));
101
+ }
102
+ /**
103
+ * Add a schema to the validator for reference
104
+ */
105
+ addSchema(e, t) {
106
+ return this.ajv.addSchema(e, t), this;
107
+ }
108
+ /**
109
+ * Remove a schema from the validator
110
+ */
111
+ removeSchema(e) {
112
+ return this.ajv.removeSchema(e), this;
113
+ }
73
114
  }
74
- function createJQ(el) {
75
- const state = ensureState(el);
76
- const jq = {
77
- 0: el,
78
- length: el ? 1 : 0,
79
- typedInput(action, value) {
80
- if (typeof action === "object") {
81
- state.typedInput = { value: "", type: action.default || "" };
82
- return jq;
83
- }
84
- if (action === "value") {
85
- if (value !== void 0) {
86
- state.typedInput.value = String(value);
87
- if (el) el.setAttribute("value", String(value));
88
- return void 0;
89
- }
90
- return state.typedInput.value;
91
- }
92
- if (action === "type") {
93
- if (value !== void 0) {
94
- state.typedInput.type = String(value);
95
- return void 0;
96
- }
97
- return state.typedInput.type;
98
- }
99
- return jq;
100
- },
101
- on(event, cb) {
102
- if (!state.listeners[event]) state.listeners[event] = [];
103
- state.listeners[event].push(cb);
104
- return jq;
105
- },
106
- off(event) {
107
- if (event) {
108
- delete state.listeners[event];
109
- } else {
110
- for (const key of Object.keys(state.listeners)) {
111
- delete state.listeners[key];
112
- }
113
- }
114
- return jq;
115
- },
116
- val(value) {
117
- if (value !== void 0) {
118
- if (el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) {
119
- el.value = String(value);
120
- }
121
- return jq;
122
- }
123
- if (el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) {
124
- return el.value;
115
+ class f extends Error {
116
+ constructor(e, t) {
117
+ super(e), this.errors = t, this.name = "ValidationError", Object.setPrototypeOf(this, f.prototype);
118
+ }
119
+ }
120
+ const k = new b({
121
+ customKeywords: [
122
+ {
123
+ keyword: "x-nrg-skip-validation",
124
+ schemaType: "boolean",
125
+ valid: !0
126
+ },
127
+ {
128
+ keyword: "x-nrg-node-type",
129
+ type: "string",
130
+ validate: (r, e) => {
131
+ if (!e) return !0;
132
+ const t = RED.nodes.node(e);
133
+ return !!t && t.type === r;
125
134
  }
126
- return "";
127
- },
128
- find(selector) {
129
- return createJQ(el?.querySelector(selector) ?? null);
130
- },
131
- append(child) {
132
- const childEl = child?.[0] || child;
133
- if (el && childEl instanceof Element) el.appendChild(childEl);
134
- return jq;
135
- },
136
- appendTo(target) {
137
- const t = target?.[0] || target;
138
- if (t instanceof Element && el) t.appendChild(el);
139
- return jq;
140
- },
141
- html(content) {
142
- if (el) el.innerHTML = content;
143
- return jq;
144
- },
145
- empty() {
146
- if (el) el.innerHTML = "";
147
- return jq;
148
- },
149
- i18n() {
150
- return jq;
151
- },
152
- addClass(cls) {
153
- el?.classList.add(cls);
154
- return jq;
155
- },
156
- removeClass(cls) {
157
- el?.classList.remove(cls);
158
- return jq;
159
- },
160
- __trigger(event) {
161
- (state.listeners[event] || []).forEach((cb) => cb());
162
135
  }
163
- };
164
- return jq;
165
- }
166
- function createJQuery() {
167
- return function $(selector, attrs) {
168
- if (typeof selector === "string") {
169
- if (selector.trim().startsWith("<")) {
170
- const tpl = document.createElement("template");
171
- tpl.innerHTML = selector.trim();
172
- const el = tpl.content.firstElementChild;
173
- if (el && attrs) {
174
- for (const [key, value] of Object.entries(attrs)) {
175
- if (key === "html") {
176
- el.innerHTML = String(value);
177
- } else {
178
- el.setAttribute(key, String(value));
179
- }
180
- }
181
- }
182
- return createJQ(el);
136
+ ],
137
+ customFormats: {
138
+ "node-id": /^[a-zA-Z0-9-_]+$/,
139
+ "flow-id": /^[a-f0-9]{16}$/,
140
+ "topic-path": (r) => /^[a-zA-Z0-9/_-]+$/.test(r)
141
+ }
142
+ });
143
+ function O(r, e) {
144
+ return r && (e != null && e.properties) ? {
145
+ ...r,
146
+ properties: {
147
+ ...r.properties,
148
+ credentials: {
149
+ type: "object",
150
+ properties: e.properties
183
151
  }
184
- return createJQ(document.querySelector(selector));
185
152
  }
186
- if (selector instanceof Element) return createJQ(selector);
187
- if (selector && typeof selector === "object" && selector.nodeType)
188
- return createJQ(selector);
189
- return createJQ(null);
190
- };
153
+ } : r;
154
+ }
155
+ function P(r, e) {
156
+ const t = k.validate(r, e, {
157
+ cacheKey: `node-schema-${r.type}`
158
+ });
159
+ return t.valid ? [] : t.errors ?? [];
191
160
  }
192
-
193
- // src/core/client/use-form-node.ts
194
- import { inject } from "vue";
195
- function useFormNode() {
196
- const node = inject("__nrg_form_node");
197
- const schema = inject("__nrg_form_schema");
198
- const errors = inject("__nrg_form_errors");
199
- if (!node) {
161
+ function v(r, e) {
162
+ return P(r, e).filter((t) => {
163
+ var s;
164
+ return ((s = t.parentSchema) == null ? void 0 : s.format) !== "password" ? !0 : w.get(r, t.instancePath) !== "__PWD__";
165
+ }).reduce(
166
+ (t, o) => {
167
+ var n;
168
+ let s = o.instancePath;
169
+ o.keyword === "required" && ((n = o.params) != null && n.missingProperty) && (s = `${s}/${o.params.missingProperty}`);
170
+ const i = `node${s.replaceAll("/", ".")}`;
171
+ return t[i] = o.message ?? "Invalid", t;
172
+ },
173
+ {}
174
+ );
175
+ }
176
+ function I() {
177
+ const r = m("__nrg_form_node"), e = m("__nrg_form_schema"), t = m("__nrg_form_errors");
178
+ if (!r)
200
179
  throw new Error(
201
180
  "useFormNode() must be called inside a form component mounted by NRG."
202
181
  );
203
- }
204
182
  return {
205
- node,
206
- schema,
207
- errors
183
+ node: r,
184
+ schema: e,
185
+ errors: t
208
186
  };
209
187
  }
210
-
211
- // src/test/client/component/index.ts
212
- function createNode(overrides = {}) {
213
- const node = {
214
- id: `test-${Math.random().toString(36).slice(2, 10)}`,
215
- type: "test-node",
216
- changed: false,
188
+ let h = 0;
189
+ function Z(r = {}) {
190
+ const e = "configs" in r || "configSchema" in r || "credentialsSchema" in r || "nodes" in r ? r : { configs: r }, t = p({
191
+ id: `test-${h}`,
192
+ // Unique type per node: validateForm caches compiled schemas by
193
+ // `node-schema-${subject.type}`, so a shared type would silently reuse
194
+ // the first schema for every later createNode call in the same file.
195
+ type: `test-node-${h++}`,
196
+ changed: !1,
217
197
  _def: { outputs: 1 },
218
- _: (key) => key,
219
- ...overrides
220
- };
221
- const RED = getMockRED();
222
- spyOnRED(RED);
223
- const provide = {
224
- __nrg_form_node: node,
225
- __nrg_form_schema: {},
226
- __nrg_form_errors: {}
227
- };
228
- return { node, RED, provide };
229
- }
230
- function spyIfNeeded(obj, method) {
231
- if (!vi.isMockFunction(obj[method])) {
232
- vi.spyOn(obj, method);
198
+ _: (d) => d,
199
+ ...e.configs,
200
+ credentials: { ...e.credentials }
201
+ }), o = O(
202
+ e.configSchema,
203
+ e.credentialsSchema
204
+ );
205
+ let s;
206
+ if (o) {
207
+ const { $id: d, ...c } = o;
208
+ s = c;
233
209
  }
210
+ const i = S();
211
+ N(i), i.nodes.clear();
212
+ for (const d of e.nodes ?? [])
213
+ i.nodes.add(d);
214
+ const n = p(
215
+ s ? v(t, s) : {}
216
+ );
217
+ return s && g(
218
+ t,
219
+ () => {
220
+ const d = v(t, s);
221
+ Object.keys(n).forEach((c) => delete n[c]), Object.assign(n, d);
222
+ },
223
+ { deep: !0 }
224
+ ), { node: t, errors: n, RED: i, provide: {
225
+ __nrg_form_node: t,
226
+ __nrg_form_schema: s ?? {},
227
+ __nrg_form_errors: n
228
+ } };
229
+ }
230
+ function a(r, e) {
231
+ l.isMockFunction(r[e]) || l.spyOn(r, e);
234
232
  }
235
- function spyOnRED(RED) {
236
- spyIfNeeded(RED, "_");
237
- spyIfNeeded(RED, "notify");
238
- spyIfNeeded(RED.editor, "createEditor");
239
- spyIfNeeded(RED.editor, "prepareConfigNodeSelect");
240
- spyIfNeeded(RED.editor, "validateNode");
241
- spyIfNeeded(RED.tray, "show");
242
- spyIfNeeded(RED.tray, "close");
243
- spyIfNeeded(RED.popover, "tooltip");
244
- spyIfNeeded(RED.nodes, "registerType");
245
- spyIfNeeded(RED.nodes, "node");
246
- spyIfNeeded(RED.nodes, "dirty");
247
- spyIfNeeded(RED.events, "on");
248
- spyIfNeeded(RED.events, "off");
249
- spyIfNeeded(RED.events, "emit");
233
+ function N(r) {
234
+ a(r, "_"), a(r, "notify"), a(r.editor, "createEditor"), a(r.editor, "prepareConfigNodeSelect"), a(r.editor, "validateNode"), a(r.tray, "show"), a(r.tray, "close"), a(r.popover, "tooltip"), a(r.popover, "create"), a(r.nodes, "registerType"), a(r.nodes, "node"), a(r.nodes, "add"), a(r.nodes, "remove"), a(r.nodes, "getType"), a(r.nodes, "dirty"), a(r.events, "on"), a(r.events, "off"), a(r.events, "emit"), a(r.comms, "subscribe"), a(r.comms, "unsubscribe");
250
235
  }
251
- function getMockRED() {
236
+ function S() {
252
237
  return window.RED;
253
238
  }
254
239
  export {
255
- createJQuery,
256
- createNode,
257
- createRED,
258
- useFormNode
240
+ Z as createNode,
241
+ I as useFormNode
259
242
  };
@@ -0,0 +1 @@
1
+ .nrg-label[data-v-864b02f2]{display:inline-block;width:100%;margin-bottom:4px;cursor:default}.nrg-label i[data-v-864b02f2]{margin-right:5px}.nrg-required[data-v-864b02f2]{color:var(--red-ui-text-color-error);margin-left:2px}.editor-wrapper[data-v-ea93e881]{position:relative}.expand-button[data-v-ea93e881]{position:absolute;top:-23px;right:0;z-index:10;transition:color .3s ease;cursor:pointer}.nrg-toggle-wrapper[data-v-724dcae4]{display:inline-flex;align-items:center}.nrg-toggle[data-v-724dcae4]{position:relative;display:inline-flex!important;flex-direction:column;align-items:flex-start;cursor:pointer;gap:4px;-webkit-user-select:none;user-select:none}.nrg-toggle__input[data-v-724dcae4]{position:absolute;opacity:0;width:0;height:0}.nrg-toggle__slider[data-v-724dcae4]{position:relative;display:inline-block;width:36px;min-width:36px;height:20px;background-color:var(--red-ui-secondary-border-color, #ccc);border-radius:10px;transition:background-color .2s ease}.nrg-toggle__slider[data-v-724dcae4]:after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;background-color:#fff;border-radius:50%;transition:transform .2s ease}.nrg-toggle--checked .nrg-toggle__slider[data-v-724dcae4]{background-color:var(--red-ui-text-color-link, #0070d2)}.nrg-toggle--checked .nrg-toggle__slider[data-v-724dcae4]:after{transform:translate(16px)}.nrg-toggle__label[data-v-724dcae4]{cursor:default;white-space:nowrap}.nrg-toggle__label i[data-v-724dcae4]{margin-right:2px}