@bonsae/nrg 0.5.3 → 0.6.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.
- package/README.md +56 -7
- package/build/server/index.cjs +120 -39
- package/build/server/resources/nrg-client.js +1653 -1590
- package/build/vite/index.js +3 -1
- package/package.json +7 -1
- package/src/core/client/app.vue +16 -26
- package/src/core/client/components/node-red-json-schema-form.vue +23 -3
- package/src/core/client/components/node-red-toggle.vue +115 -0
- package/src/core/client/components/node-red-typed-input.vue +6 -0
- package/src/core/client/index.ts +19 -25
- package/src/core/server/api/index.ts +1 -0
- package/src/core/server/api/serve-nrg-resources.ts +54 -0
- package/src/core/server/index.ts +21 -57
- package/src/core/server/nodes/factories.ts +133 -0
- package/src/core/server/nodes/index.ts +1 -0
- package/src/core/server/nodes/io-node.ts +2 -1
- package/src/core/server/nodes/types/factories.ts +115 -0
- package/src/core/server/nodes/types/index.ts +1 -0
- package/src/core/server/nodes/types/io-node.ts +3 -0
- package/src/core/server/schemas/types/index.ts +1 -0
- 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.
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
@@ -2,32 +2,22 @@
|
|
|
2
2
|
<div
|
|
3
3
|
v-if="features.hasInputSchema || features.hasOutputSchema"
|
|
4
4
|
class="form-row"
|
|
5
|
-
style="display: flex; align-items: center; gap:
|
|
5
|
+
style="display: flex; align-items: center; gap: 12px"
|
|
6
6
|
>
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
:id="`node-input-validateOutput-${localNode.id}`"
|
|
22
|
-
type="checkbox"
|
|
23
|
-
:checked="localNode.validateOutput"
|
|
24
|
-
style="width: auto; margin: 0"
|
|
25
|
-
@change="
|
|
26
|
-
localNode.validateOutput = ($event.target as HTMLInputElement).checked
|
|
27
|
-
"
|
|
28
|
-
/>
|
|
29
|
-
<NodeRedInputLabel label="Validate Output" style="width: auto" />
|
|
30
|
-
</template>
|
|
7
|
+
<NodeRedToggle
|
|
8
|
+
v-if="features.hasInputSchema"
|
|
9
|
+
:model-value="localNode.validateInput"
|
|
10
|
+
label="Validate Input"
|
|
11
|
+
style="flex: 1"
|
|
12
|
+
@update:model-value="localNode.validateInput = $event"
|
|
13
|
+
/>
|
|
14
|
+
<NodeRedToggle
|
|
15
|
+
v-if="features.hasOutputSchema"
|
|
16
|
+
:model-value="localNode.validateOutput"
|
|
17
|
+
label="Validate Output"
|
|
18
|
+
style="flex: 1"
|
|
19
|
+
@update:model-value="localNode.validateOutput = $event"
|
|
20
|
+
/>
|
|
31
21
|
</div>
|
|
32
22
|
<div style="width: 100%; padding-bottom: 12px">
|
|
33
23
|
<NodeRedNodeForm
|
|
@@ -162,7 +152,7 @@ export default defineComponent({
|
|
|
162
152
|
error.instancePath,
|
|
163
153
|
);
|
|
164
154
|
if (
|
|
165
|
-
error.parentSchema
|
|
155
|
+
error.parentSchema?.format === "password" &&
|
|
166
156
|
errorValue === "__PWD__"
|
|
167
157
|
) {
|
|
168
158
|
return acc;
|
|
@@ -12,12 +12,20 @@
|
|
|
12
12
|
@update:value="node[field.key] = $event"
|
|
13
13
|
/>
|
|
14
14
|
|
|
15
|
+
<div v-else-if="field.inputType === 'boolean' && field.toggle">
|
|
16
|
+
<NodeRedToggle
|
|
17
|
+
:model-value="node[field.key]"
|
|
18
|
+
:label="field.label"
|
|
19
|
+
:icon="field.icon"
|
|
20
|
+
@update:model-value="node[field.key] = $event"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
15
24
|
<div v-else-if="field.inputType === 'boolean'">
|
|
16
25
|
<NodeRedInputLabel
|
|
17
26
|
:label="field.label"
|
|
18
27
|
:icon="field.icon"
|
|
19
28
|
:required="field.required"
|
|
20
|
-
style="width: auto"
|
|
21
29
|
/>
|
|
22
30
|
<input
|
|
23
31
|
type="checkbox"
|
|
@@ -144,6 +152,7 @@
|
|
|
144
152
|
import type { PropType } from "vue";
|
|
145
153
|
import { defineComponent } from "vue";
|
|
146
154
|
import NodeRedInputLabel from "./node-red-input-label.vue";
|
|
155
|
+
import NodeRedToggle from "./node-red-toggle.vue";
|
|
147
156
|
import NodeRedInput from "./node-red-input.vue";
|
|
148
157
|
import NodeRedSelectInput from "./node-red-select-input.vue";
|
|
149
158
|
import NodeRedTypedInput from "./node-red-typed-input.vue";
|
|
@@ -169,6 +178,7 @@ interface NrgFormOptions {
|
|
|
169
178
|
icon?: string;
|
|
170
179
|
typedInputTypes?: string[];
|
|
171
180
|
editorLanguage?: string;
|
|
181
|
+
toggle?: boolean;
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
interface FieldSchema {
|
|
@@ -197,7 +207,8 @@ interface FormField {
|
|
|
197
207
|
| "select"
|
|
198
208
|
| "typed"
|
|
199
209
|
| "config"
|
|
200
|
-
| "editor"
|
|
210
|
+
| "editor"
|
|
211
|
+
| "array-text";
|
|
201
212
|
required: boolean;
|
|
202
213
|
htmlType?: "text" | "number" | "password";
|
|
203
214
|
options?: Array<{ value: string; label: string }>;
|
|
@@ -205,6 +216,7 @@ interface FormField {
|
|
|
205
216
|
types?: string[];
|
|
206
217
|
configType?: string;
|
|
207
218
|
language?: string;
|
|
219
|
+
toggle?: boolean;
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
function formatLabel(key: string): string {
|
|
@@ -291,7 +303,14 @@ function buildField(
|
|
|
291
303
|
|
|
292
304
|
switch (rawType) {
|
|
293
305
|
case "boolean":
|
|
294
|
-
return {
|
|
306
|
+
return {
|
|
307
|
+
key,
|
|
308
|
+
label,
|
|
309
|
+
icon,
|
|
310
|
+
inputType: "boolean",
|
|
311
|
+
required,
|
|
312
|
+
toggle: form.toggle,
|
|
313
|
+
};
|
|
295
314
|
|
|
296
315
|
case "number":
|
|
297
316
|
case "integer":
|
|
@@ -367,6 +386,7 @@ export default defineComponent({
|
|
|
367
386
|
name: "NodeRedJsonSchemaForm",
|
|
368
387
|
components: {
|
|
369
388
|
NodeRedInputLabel,
|
|
389
|
+
NodeRedToggle,
|
|
370
390
|
NodeRedInput,
|
|
371
391
|
NodeRedSelectInput,
|
|
372
392
|
NodeRedTypedInput,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="nrg-toggle-wrapper">
|
|
3
|
+
<label class="nrg-toggle" :class="{ 'nrg-toggle--checked': modelValue }">
|
|
4
|
+
<input
|
|
5
|
+
type="checkbox"
|
|
6
|
+
:checked="modelValue"
|
|
7
|
+
class="nrg-toggle__input"
|
|
8
|
+
@change="
|
|
9
|
+
$emit(
|
|
10
|
+
'update:modelValue',
|
|
11
|
+
($event.target as HTMLInputElement).checked,
|
|
12
|
+
)
|
|
13
|
+
"
|
|
14
|
+
/>
|
|
15
|
+
<span v-if="icon || label" class="nrg-toggle__label">
|
|
16
|
+
<i v-if="icon" :class="iconClass"></i>
|
|
17
|
+
{{ label }}
|
|
18
|
+
</span>
|
|
19
|
+
<span class="nrg-toggle__slider"></span>
|
|
20
|
+
</label>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
import { defineComponent } from "vue";
|
|
26
|
+
|
|
27
|
+
export default defineComponent({
|
|
28
|
+
name: "NodeRedToggle",
|
|
29
|
+
props: {
|
|
30
|
+
modelValue: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
label: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: "",
|
|
37
|
+
},
|
|
38
|
+
icon: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: "",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
emits: ["update:modelValue"],
|
|
44
|
+
computed: {
|
|
45
|
+
iconClass(): string {
|
|
46
|
+
if (!this.icon) return "";
|
|
47
|
+
const name = this.icon.startsWith("fa-") ? this.icon : `fa-${this.icon}`;
|
|
48
|
+
return `fa ${name}`;
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<style scoped>
|
|
55
|
+
.nrg-toggle-wrapper {
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.nrg-toggle {
|
|
61
|
+
position: relative;
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
gap: 8px;
|
|
66
|
+
user-select: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.nrg-toggle__input {
|
|
70
|
+
position: absolute;
|
|
71
|
+
opacity: 0;
|
|
72
|
+
width: 0;
|
|
73
|
+
height: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.nrg-toggle__slider {
|
|
77
|
+
position: relative;
|
|
78
|
+
display: inline-block;
|
|
79
|
+
width: 36px;
|
|
80
|
+
min-width: 36px;
|
|
81
|
+
height: 20px;
|
|
82
|
+
background-color: var(--red-ui-secondary-border-color, #ccc);
|
|
83
|
+
border-radius: 10px;
|
|
84
|
+
transition: background-color 0.2s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.nrg-toggle__slider::after {
|
|
88
|
+
content: "";
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: 2px;
|
|
91
|
+
left: 2px;
|
|
92
|
+
width: 16px;
|
|
93
|
+
height: 16px;
|
|
94
|
+
background-color: white;
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
transition: transform 0.2s ease;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.nrg-toggle--checked .nrg-toggle__slider {
|
|
100
|
+
background-color: var(--red-ui-text-color-link, #0070d2);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.nrg-toggle--checked .nrg-toggle__slider::after {
|
|
104
|
+
transform: translateX(16px);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.nrg-toggle__label {
|
|
108
|
+
cursor: default;
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.nrg-toggle__label i {
|
|
113
|
+
margin-right: 2px;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
@@ -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
|
@@ -9,6 +9,7 @@ import NodeRedConfigInput from "./components/node-red-config-input.vue";
|
|
|
9
9
|
import NodeRedSelectInput from "./components/node-red-select-input.vue";
|
|
10
10
|
import NodeRedEditorInput from "./components/node-red-editor-input.vue";
|
|
11
11
|
import NodeRedInputLabel from "./components/node-red-input-label.vue";
|
|
12
|
+
import NodeRedToggle from "./components/node-red-toggle.vue";
|
|
12
13
|
import NodeRedJsonSchemaForm from "./components/node-red-json-schema-form.vue";
|
|
13
14
|
|
|
14
15
|
const _schemas: Record<string, any> = {};
|
|
@@ -165,6 +166,7 @@ function createNodeRedVueApp(
|
|
|
165
166
|
});
|
|
166
167
|
|
|
167
168
|
app.component("NodeRedInputLabel", NodeRedInputLabel);
|
|
169
|
+
app.component("NodeRedToggle", NodeRedToggle);
|
|
168
170
|
app.component("NodeRedInput", NodeRedInput);
|
|
169
171
|
app.component("NodeRedTypedInput", NodeRedTypedInput);
|
|
170
172
|
app.component("NodeRedConfigInput", NodeRedConfigInput);
|
|
@@ -203,7 +205,7 @@ function getNodeState(node: Node): NodeState {
|
|
|
203
205
|
const state: NodeState = {
|
|
204
206
|
credentials: {},
|
|
205
207
|
};
|
|
206
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
208
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
207
209
|
state[prop] = node[prop];
|
|
208
210
|
});
|
|
209
211
|
if (node._def.credentials) {
|
|
@@ -300,8 +302,6 @@ function defineNode<T extends NodeDefinition>(options: T): T {
|
|
|
300
302
|
async function registerType(definition: NodeDefinition): Promise<void> {
|
|
301
303
|
const { type } = definition;
|
|
302
304
|
try {
|
|
303
|
-
console.log(`Registering node type: ${type}`);
|
|
304
|
-
|
|
305
305
|
const nodeDefinition = {
|
|
306
306
|
...(_schemas[type] ?? {}),
|
|
307
307
|
...definition,
|
|
@@ -311,9 +311,6 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
311
311
|
const defaults = nodeDefinition.defaults ?? undefined;
|
|
312
312
|
const credentials = nodeDefinition.credentials ?? undefined;
|
|
313
313
|
|
|
314
|
-
console.log("defaults", defaults);
|
|
315
|
-
console.log("credentials", credentials);
|
|
316
|
-
|
|
317
314
|
const appContainerId = `nrg-app-${type}`;
|
|
318
315
|
|
|
319
316
|
$("<script>", {
|
|
@@ -323,21 +320,20 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
323
320
|
}).appendTo("body");
|
|
324
321
|
|
|
325
322
|
function oneditprepare(this: Node) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
323
|
+
const validationSchema =
|
|
324
|
+
nodeDefinition.configSchema &&
|
|
325
|
+
nodeDefinition.credentialsSchema?.properties
|
|
326
|
+
? {
|
|
327
|
+
...nodeDefinition.configSchema,
|
|
328
|
+
properties: {
|
|
329
|
+
...nodeDefinition.configSchema.properties,
|
|
330
|
+
credentials: {
|
|
331
|
+
type: "object",
|
|
332
|
+
properties: nodeDefinition.credentialsSchema.properties,
|
|
333
|
+
},
|
|
337
334
|
},
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
: nodeDefinition.configSchema;
|
|
335
|
+
}
|
|
336
|
+
: nodeDefinition.configSchema;
|
|
341
337
|
|
|
342
338
|
const form =
|
|
343
339
|
definition.form ??
|
|
@@ -360,7 +356,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
360
356
|
const changed = !!Object.keys(changes)?.length;
|
|
361
357
|
if (!changed) return false;
|
|
362
358
|
|
|
363
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
359
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
364
360
|
if (!node._def.defaults?.[prop]?.type) return;
|
|
365
361
|
const oldConfigNodeId: string = node[prop] as string;
|
|
366
362
|
const newConfigNodeId: string = node._newState![prop] as string;
|
|
@@ -376,7 +372,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
376
372
|
}
|
|
377
373
|
});
|
|
378
374
|
|
|
379
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
375
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
380
376
|
if (!node._def.defaults?.[prop]?.type) return;
|
|
381
377
|
const newConfigNodeId: string = node._newState![prop] as string;
|
|
382
378
|
if (!newConfigNodeId) return;
|
|
@@ -400,7 +396,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
400
396
|
// overwriting the correctly-typed values already set by merge() above.
|
|
401
397
|
const isConfigNode = definition.category === "config";
|
|
402
398
|
if (isConfigNode) {
|
|
403
|
-
Object.keys(node._def.defaults).forEach((prop) => {
|
|
399
|
+
Object.keys(node._def.defaults ?? {}).forEach((prop) => {
|
|
404
400
|
if (node._def.defaults[prop].type) return; // config-node refs handled separately
|
|
405
401
|
const inputId = `node-config-input-${prop}`;
|
|
406
402
|
let input = $(`#${inputId}`);
|
|
@@ -484,9 +480,7 @@ async function registerType(definition: NodeDefinition): Promise<void> {
|
|
|
484
480
|
*/
|
|
485
481
|
async function registerTypes(nodes: NodeDefinition[]): Promise<void> {
|
|
486
482
|
try {
|
|
487
|
-
console.log("Registering node types in parallel");
|
|
488
483
|
await Promise.all(nodes.map((definition) => registerType(definition)));
|
|
489
|
-
console.log("All node types registered in parallel");
|
|
490
484
|
} catch (error) {
|
|
491
485
|
console.error("Error registering node types:", error);
|
|
492
486
|
throw error;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { serveNrgResources } from "./serve-nrg-resources";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import type { RED } from "../types";
|
|
4
|
+
|
|
5
|
+
const MIME: Record<string, string> = {
|
|
6
|
+
".js": "application/javascript",
|
|
7
|
+
".mjs": "application/javascript",
|
|
8
|
+
".css": "text/css",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".map": "application/json",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".svg": "image/svg+xml",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let _registered = false;
|
|
16
|
+
|
|
17
|
+
function serveNrgResources(RED: RED): void {
|
|
18
|
+
if (_registered) return;
|
|
19
|
+
_registered = true;
|
|
20
|
+
|
|
21
|
+
const clientDir = path.resolve(__dirname, "./resources");
|
|
22
|
+
if (!fs.existsSync(clientDir)) return;
|
|
23
|
+
|
|
24
|
+
const httpAdmin = (RED as any).httpAdmin;
|
|
25
|
+
if (!httpAdmin) return;
|
|
26
|
+
|
|
27
|
+
// /nrg/assets/ is not handled by Node-RED's editorApp, so our handler
|
|
28
|
+
// appended via use() is reached normally without any stack manipulation.
|
|
29
|
+
httpAdmin.use(function (req: any, res: any, next: any) {
|
|
30
|
+
const prefix = "/nrg/assets/";
|
|
31
|
+
if (!(req.path as string).startsWith(prefix)) return next();
|
|
32
|
+
let reqPath = (req.path as string).slice(prefix.length);
|
|
33
|
+
// Serve the Vue dev build in development for devtools support
|
|
34
|
+
if (
|
|
35
|
+
reqPath === "vue.esm-browser.prod.js" &&
|
|
36
|
+
process.env.NODE_ENV !== "production"
|
|
37
|
+
) {
|
|
38
|
+
const devPath = path.resolve(clientDir, "vue.esm-browser.js");
|
|
39
|
+
if (fs.existsSync(devPath)) {
|
|
40
|
+
reqPath = "vue.esm-browser.js";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const filePath = path.resolve(clientDir, reqPath);
|
|
44
|
+
const rel = path.relative(clientDir, filePath);
|
|
45
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return next();
|
|
46
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
47
|
+
return next();
|
|
48
|
+
const ext = path.extname(filePath);
|
|
49
|
+
res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
50
|
+
fs.createReadStream(filePath).pipe(res);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { serveNrgResources };
|
package/src/core/server/index.ts
CHANGED
|
@@ -1,60 +1,10 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
1
|
import { getCredentialsFromSchema } from "./utils";
|
|
4
2
|
import { Node } from "./nodes";
|
|
5
3
|
import { type RED } from "./types";
|
|
6
4
|
import { initValidator } from "./validator";
|
|
5
|
+
import { serveNrgResources } from "./api";
|
|
7
6
|
import { NrgError } from "../errors";
|
|
8
7
|
|
|
9
|
-
const MIME: Record<string, string> = {
|
|
10
|
-
".js": "application/javascript",
|
|
11
|
-
".mjs": "application/javascript",
|
|
12
|
-
".css": "text/css",
|
|
13
|
-
".json": "application/json",
|
|
14
|
-
".map": "application/json",
|
|
15
|
-
".png": "image/png",
|
|
16
|
-
".svg": "image/svg+xml",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
let _nrgResourcesRegistered = false;
|
|
20
|
-
|
|
21
|
-
function serveNrgResources(RED: RED): void {
|
|
22
|
-
if (_nrgResourcesRegistered) return;
|
|
23
|
-
_nrgResourcesRegistered = true;
|
|
24
|
-
|
|
25
|
-
const clientDir = path.resolve(__dirname, "./resources");
|
|
26
|
-
if (!fs.existsSync(clientDir)) return;
|
|
27
|
-
|
|
28
|
-
const httpAdmin = (RED as any).httpAdmin;
|
|
29
|
-
if (!httpAdmin) return;
|
|
30
|
-
|
|
31
|
-
// /nrg/assets/ is not handled by Node-RED's editorApp, so our handler
|
|
32
|
-
// appended via use() is reached normally without any stack manipulation.
|
|
33
|
-
httpAdmin.use(function (req: any, res: any, next: any) {
|
|
34
|
-
const prefix = "/nrg/assets/";
|
|
35
|
-
if (!(req.path as string).startsWith(prefix)) return next();
|
|
36
|
-
let reqPath = (req.path as string).slice(prefix.length);
|
|
37
|
-
// Serve the Vue dev build in development for devtools support
|
|
38
|
-
if (
|
|
39
|
-
reqPath === "vue.esm-browser.prod.js" &&
|
|
40
|
-
process.env.NODE_ENV !== "production"
|
|
41
|
-
) {
|
|
42
|
-
const devPath = path.resolve(clientDir, "vue.esm-browser.js");
|
|
43
|
-
if (fs.existsSync(devPath)) {
|
|
44
|
-
reqPath = "vue.esm-browser.js";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const filePath = path.resolve(clientDir, reqPath);
|
|
48
|
-
const rel = path.relative(clientDir, filePath);
|
|
49
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) return next();
|
|
50
|
-
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
51
|
-
return next();
|
|
52
|
-
const ext = path.extname(filePath);
|
|
53
|
-
res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
54
|
-
fs.createReadStream(filePath).pipe(res);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
8
|
type AnyNodeClass = (abstract new (...args: any[]) => Node) &
|
|
59
9
|
Partial<typeof Node>;
|
|
60
10
|
|
|
@@ -187,7 +137,7 @@ async function registerType(RED: RED, NodeClass: AnyNodeClass) {
|
|
|
187
137
|
RED.log.debug(`Type registered: ${NC.type}`);
|
|
188
138
|
}
|
|
189
139
|
|
|
190
|
-
type
|
|
140
|
+
type RegistrationFunction = ((RED: RED) => Promise<void>) & {
|
|
191
141
|
nodes: AnyNodeClass[];
|
|
192
142
|
};
|
|
193
143
|
|
|
@@ -199,7 +149,7 @@ type NodeRedPackageFunction = ((RED: RED) => Promise<void>) & {
|
|
|
199
149
|
*
|
|
200
150
|
* @param nodes - Array of node classes to register
|
|
201
151
|
*/
|
|
202
|
-
function registerTypes(nodes: AnyNodeClass[]):
|
|
152
|
+
function registerTypes(nodes: AnyNodeClass[]): RegistrationFunction {
|
|
203
153
|
const fn = async function (RED: RED) {
|
|
204
154
|
initValidator(RED);
|
|
205
155
|
serveNrgResources(RED);
|
|
@@ -214,12 +164,26 @@ function registerTypes(nodes: AnyNodeClass[]): NodeRedPackageFunction {
|
|
|
214
164
|
throw error;
|
|
215
165
|
}
|
|
216
166
|
};
|
|
217
|
-
(fn as
|
|
218
|
-
return fn as
|
|
167
|
+
(fn as RegistrationFunction).nodes = nodes;
|
|
168
|
+
return fn as RegistrationFunction;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface ModuleDefinition {
|
|
172
|
+
nodes: AnyNodeClass[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function defineModule(definition: ModuleDefinition): ModuleDefinition {
|
|
176
|
+
return definition;
|
|
219
177
|
}
|
|
220
178
|
|
|
221
|
-
export { registerType, registerTypes };
|
|
222
|
-
export {
|
|
179
|
+
export { registerType, registerTypes, defineModule };
|
|
180
|
+
export {
|
|
181
|
+
Node,
|
|
182
|
+
IONode,
|
|
183
|
+
ConfigNode,
|
|
184
|
+
defineIONode,
|
|
185
|
+
defineConfigNode,
|
|
186
|
+
} from "./nodes";
|
|
223
187
|
export { NrgError } from "../errors";
|
|
224
188
|
export type { RED } from "./types";
|
|
225
189
|
export { SchemaType, defineSchema } from "./schemas";
|