@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,60 @@
1
+ // src/vite/utils.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ function cleanDir(dir) {
5
+ if (fs.existsSync(dir)) {
6
+ fs.rmSync(dir, { recursive: true });
7
+ }
8
+ fs.mkdirSync(dir, { recursive: true });
9
+ }
10
+ function copyFiles(targets, outDir) {
11
+ for (const { src, dest } of targets) {
12
+ const srcPath = path.resolve(src);
13
+ const destPath = path.join(outDir, dest);
14
+ if (!fs.existsSync(srcPath)) {
15
+ continue;
16
+ }
17
+ const stat = fs.statSync(srcPath);
18
+ if (stat.isDirectory()) {
19
+ fs.cpSync(srcPath, destPath, { recursive: true });
20
+ } else {
21
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
22
+ fs.copyFileSync(srcPath, destPath);
23
+ }
24
+ }
25
+ }
26
+ function getPackageName() {
27
+ const pkgPath = path.resolve("./package.json");
28
+ if (fs.existsSync(pkgPath)) {
29
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
30
+ return pkg.name;
31
+ }
32
+ return "node-red-nodes";
33
+ }
34
+ function mergeOptions(defaults, overrides) {
35
+ if (!overrides) return { ...defaults };
36
+ const result = { ...defaults };
37
+ for (const key of Object.keys(overrides)) {
38
+ const overrideVal = overrides[key];
39
+ const defaultVal = defaults[key];
40
+ if (overrideVal !== void 0 && !Array.isArray(overrideVal) && !Array.isArray(defaultVal) && typeof overrideVal === "object" && typeof defaultVal === "object" && overrideVal !== null && defaultVal !== null) {
41
+ result[key] = mergeOptions(
42
+ defaultVal,
43
+ overrideVal
44
+ );
45
+ } else if (overrideVal !== void 0) {
46
+ result[key] = overrideVal;
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ function defineRuntimeSettings(settings) {
52
+ return settings;
53
+ }
54
+ export {
55
+ cleanDir,
56
+ copyFiles,
57
+ defineRuntimeSettings,
58
+ getPackageName,
59
+ mergeOptions
60
+ };
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "@bonsae/nrg",
3
+ "version": "0.1.0",
4
+ "description": "NRG framework — build Node-RED nodes with Vue 3, TypeScript, and JSON Schema",
5
+ "author": "Allan Oricil <allanoricil@duck.com>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "repository": {
9
+ "url": "https://github.com/bonsaedev/nrg",
10
+ "type": "git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "engines": {
16
+ "node": ">=22",
17
+ "pnpm": ">=10.11.0"
18
+ },
19
+ "scripts": {
20
+ "build": "node build.mjs",
21
+ "typecheck": "tsc -p src/core/server/tsconfig.json --noEmit && tsc -p src/core/client/tsconfig.json --noEmit",
22
+ "lint": "eslint src/",
23
+ "lint:fix": "eslint src/ --fix",
24
+ "format": "prettier --write \"src/**/*.{ts,vue,json}\"",
25
+ "format:check": "prettier --check \"src/**/*.{ts,vue,json}\"",
26
+ "prepare": "husky"
27
+ },
28
+ "files": [
29
+ "src/",
30
+ "build/"
31
+ ],
32
+ "exports": {
33
+ "./server": {
34
+ "types": "./src/core/server/index.ts",
35
+ "require": "./build/server/index.cjs",
36
+ "default": "./build/server/index.cjs"
37
+ },
38
+ "./client": "./src/core/client/index.ts",
39
+ "./schemas": "./src/core/server/schemas/index.ts",
40
+ "./schemas/types": "./src/core/server/schemas/types/index.ts",
41
+ "./vite": {
42
+ "types": "./src/vite/index.ts",
43
+ "default": "./build/vite/index.js"
44
+ },
45
+ "./vite/utils": {
46
+ "types": "./src/vite/utils.ts",
47
+ "default": "./build/vite/utils.js"
48
+ },
49
+ "./tsconfig/base.json": "./src/tsconfig/base.json",
50
+ "./tsconfig/client.json": "./src/tsconfig/client.json",
51
+ "./tsconfig/server.json": "./src/tsconfig/server.json"
52
+ },
53
+ "peerDependencies": {
54
+ "@sinclair/typebox": "^0.34.33",
55
+ "vite": "^6.0.0",
56
+ "vue": "^3.5.14"
57
+ },
58
+ "dependencies": {
59
+ "@clack/prompts": "^1.0.1",
60
+ "@sinclair/typebox": "^0.34.33",
61
+ "@vitejs/plugin-vue": "^5.2.3",
62
+ "ajv": "^8.17.1",
63
+ "ajv-errors": "^3.0.0",
64
+ "ajv-formats": "^3.0.1",
65
+ "chokidar": "^5.0.0",
66
+ "detect-port": "^2.1.0",
67
+ "es-toolkit": "^1.37.2",
68
+ "esbuild": "^0.25.4",
69
+ "get-port": "^7.1.0",
70
+ "jsonpointer": "^5.0.1",
71
+ "mime-types": "^3.0.1",
72
+ "tree-kill": "^1.2.2",
73
+ "typescript": "^5.8.3",
74
+ "vite-plugin-dts": "^4.5.4",
75
+ "vite-plugin-static-copy": "^3.1.0"
76
+ },
77
+ "lint-staged": {
78
+ "src/**/*.{ts,vue}": [
79
+ "eslint --fix",
80
+ "prettier --write"
81
+ ],
82
+ "src/**/*.json": [
83
+ "prettier --write"
84
+ ]
85
+ },
86
+ "devDependencies": {
87
+ "@commitlint/cli": "^20.5.0",
88
+ "@commitlint/config-conventional": "^20.5.0",
89
+ "@eslint/js": "^9.27.0",
90
+ "@semantic-release/changelog": "^6.0.3",
91
+ "@semantic-release/exec": "^7.1.0",
92
+ "@semantic-release/git": "^10.0.1",
93
+ "@types/express": "^5.0.1",
94
+ "@types/mime-types": "^2.1.4",
95
+ "@types/node": "^22.15.18",
96
+ "@typescript-eslint/eslint-plugin": "^8.32.1",
97
+ "@typescript-eslint/parser": "^8.32.1",
98
+ "eslint": "^9.27.0",
99
+ "eslint-config-prettier": "^10.1.8",
100
+ "eslint-plugin-vue": "^10.1.0",
101
+ "globals": "^16.1.0",
102
+ "husky": "^9.1.7",
103
+ "lint-staged": "^16.4.0",
104
+ "prettier": "^3.5.3",
105
+ "semantic-release": "^24.2.4",
106
+ "typescript-eslint": "^8.32.1",
107
+ "vite": "^6.3.4",
108
+ "vue": "^3.5.14"
109
+ }
110
+ }
@@ -0,0 +1,17 @@
1
+ import { type NodeDefinitionApiResponse } from "../../server/types";
2
+
3
+ async function fetchNodeDefinition(
4
+ type: string,
5
+ ): Promise<NodeDefinitionApiResponse> {
6
+ const response = await fetch(`/nrg/nodes/${type}`);
7
+
8
+ if (!response.ok) {
9
+ throw new Error(
10
+ `Failed to fetch node definition for "${type}": ${response.status}`,
11
+ );
12
+ }
13
+
14
+ return response.json() as Promise<NodeDefinitionApiResponse>;
15
+ }
16
+
17
+ export { fetchNodeDefinition };
@@ -0,0 +1,201 @@
1
+ <template>
2
+ <div
3
+ v-if="features.hasInputSchema || features.hasOutputSchema"
4
+ class="form-row"
5
+ style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap"
6
+ >
7
+ <template v-if="features.hasInputSchema">
8
+ <input
9
+ :id="`node-input-validateInput-${localNode.id}`"
10
+ type="checkbox"
11
+ :checked="localNode.validateInput"
12
+ style="width: auto; margin: 0"
13
+ @change="
14
+ localNode.validateInput = ($event.target as HTMLInputElement).checked
15
+ "
16
+ />
17
+ <span class="nrg-label" style="width: auto">Validate Input</span>
18
+ </template>
19
+ <template v-if="features.hasOutputSchema">
20
+ <input
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
+ <span class="nrg-label" style="width: auto">Validate Output</span>
30
+ </template>
31
+ </div>
32
+ <div style="width: 100%; padding-bottom: 12px">
33
+ <NodeRedNodeForm
34
+ :node="localNode"
35
+ :schema="schema"
36
+ :errors="errors"
37
+ style="width: 100%"
38
+ />
39
+ </div>
40
+ </template>
41
+
42
+ <script lang="ts">
43
+ import jsonpointer from "jsonpointer";
44
+ import { type JSONSchemaType } from "ajv";
45
+ import type { PropType } from "vue";
46
+ import { defineComponent } from "vue";
47
+ import { validator } from "../validator";
48
+
49
+ export default defineComponent({
50
+ name: "NodeRedVueApp",
51
+ props: {
52
+ node: {
53
+ type: Object,
54
+ required: true,
55
+ },
56
+ schema: {
57
+ type: Object as PropType<JSONSchemaType<any>>,
58
+ required: true,
59
+ },
60
+ features: {
61
+ type: Object as PropType<{
62
+ hasInputSchema: boolean;
63
+ hasOutputSchema: boolean;
64
+ }>,
65
+ required: true,
66
+ },
67
+ },
68
+ data() {
69
+ return {
70
+ localNode: this.node,
71
+ errors: {},
72
+ };
73
+ },
74
+ beforeMount() {
75
+ // Normalize array-typed properties to actual arrays. Nodes saved with an
76
+ // older version of the code may have stored array values as comma-separated
77
+ // strings; this ensures validation and future saves always see real arrays.
78
+ if (this.schema?.properties) {
79
+ for (const [prop, propSchema] of Object.entries(this.schema.properties)) {
80
+ if (
81
+ (propSchema as any).type === "array" &&
82
+ !Array.isArray(this.localNode[prop])
83
+ ) {
84
+ const val = this.localNode[prop];
85
+ this.localNode[prop] = val
86
+ ? String(val).split(",").filter(Boolean)
87
+ : [];
88
+ }
89
+ }
90
+ }
91
+
92
+ // Set __PWD__ for existing password fields before the first validation so
93
+ // the password-skip logic in validate() sees the sentinel value correctly.
94
+ if (this.localNode._def.credentials) {
95
+ Object.keys(this.localNode._def.credentials).forEach((prop) => {
96
+ if (
97
+ this.localNode._def.credentials[prop].type === "password" &&
98
+ this.localNode.credentials[`has_${prop}`]
99
+ ) {
100
+ this.localNode.credentials[prop] = "__PWD__";
101
+ }
102
+ });
103
+ }
104
+
105
+ this.validate();
106
+
107
+ if (this.localNode._def.defaults) {
108
+ Object.keys(this.localNode._def.defaults).forEach((prop) => {
109
+ this.$watch(
110
+ () => this.localNode[prop],
111
+ () => {
112
+ this.validate();
113
+ },
114
+ { deep: true },
115
+ );
116
+ });
117
+ }
118
+
119
+ if (this.localNode._def.credentials) {
120
+ Object.keys(this.localNode._def.credentials).forEach((prop) => {
121
+ this.$watch(
122
+ () => this.localNode.credentials[prop],
123
+ (newVal, oldVal) => {
124
+ this.validate();
125
+
126
+ if (
127
+ this.localNode._def.credentials[prop].type === "password" &&
128
+ newVal !== oldVal
129
+ ) {
130
+ this.localNode.credentials[`has_${prop}`] = !!newVal;
131
+ }
132
+ },
133
+ { deep: true },
134
+ );
135
+ });
136
+ }
137
+ },
138
+ beforeUnmount() {
139
+ // NOTE: must set credentials prop to undefined to avoid updating it to __PWD__ in the server
140
+ if (this.localNode._def.credentials) {
141
+ Object.keys(this.localNode._def.credentials).forEach((prop) => {
142
+ if (
143
+ this.localNode._def.credentials[prop].type === "password" &&
144
+ this.localNode.credentials?.[`has_${prop}`] &&
145
+ this.localNode.credentials?.[prop] === "__PWD__"
146
+ ) {
147
+ this.localNode.credentials[prop] = undefined;
148
+ }
149
+ });
150
+ }
151
+ },
152
+ methods: {
153
+ validate() {
154
+ const result = validator.validate(this.localNode, this.schema, {
155
+ cacheKey: `node-schema-${this.node.type}`,
156
+ });
157
+
158
+ if (!result.valid) {
159
+ this.errors = result.errors.reduce((acc, error) => {
160
+ const errorValue = jsonpointer.get(
161
+ this.localNode,
162
+ error.instancePath,
163
+ );
164
+ if (
165
+ error.parentSchema.format === "password" &&
166
+ errorValue === "__PWD__"
167
+ ) {
168
+ return acc;
169
+ } else {
170
+ const key = `node${error.instancePath.replaceAll("/", ".")}`;
171
+ acc[key] = error.message;
172
+ return acc;
173
+ }
174
+ }, {});
175
+ } else {
176
+ this.errors = {};
177
+ }
178
+ },
179
+ },
180
+ });
181
+ </script>
182
+
183
+ <style scoped>
184
+ :deep(.node-red-vue-input-error-message) {
185
+ color: var(--red-ui-text-color-error);
186
+ }
187
+
188
+ :deep(.nrg-label) {
189
+ display: inline-block;
190
+ width: 100%;
191
+ cursor: default;
192
+ }
193
+
194
+ :deep(.form-row input[type="text"]),
195
+ :deep(.form-row input[type="number"]),
196
+ :deep(.form-row input[type="password"]) {
197
+ height: 34px;
198
+ padding: 0 8px;
199
+ box-sizing: border-box;
200
+ }
201
+ </style>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div style="display: flex; flex-direction: column; width: 100%">
3
+ <input :id="inputId" type="text" style="width: 100%" />
4
+ <div v-if="error" class="node-red-vue-input-error-message">
5
+ {{ error }}
6
+ </div>
7
+ </div>
8
+ </template>
9
+
10
+ <script lang="ts">
11
+ import { defineComponent } from "vue";
12
+ export default defineComponent({
13
+ props: {
14
+ value: {
15
+ type: String,
16
+ default: "",
17
+ },
18
+ type: {
19
+ type: String,
20
+ required: true,
21
+ },
22
+ node: {
23
+ type: Object,
24
+ required: true,
25
+ },
26
+ propName: {
27
+ type: String,
28
+ required: true,
29
+ },
30
+ error: {
31
+ type: String,
32
+ default: "",
33
+ },
34
+ },
35
+ emits: ["update:value"],
36
+ computed: {
37
+ inputId() {
38
+ return "node-input-" + this.propName;
39
+ },
40
+ },
41
+ mounted() {
42
+ RED.editor.prepareConfigNodeSelect(
43
+ this.node,
44
+ this.propName,
45
+ this.type,
46
+ "node-input",
47
+ );
48
+
49
+ const input = $("#" + this.inputId);
50
+ input.on("change", () => {
51
+ this.$emit("update:value", input.val());
52
+ });
53
+
54
+ input.val(this.value || "_ADD_");
55
+ },
56
+ });
57
+ </script>