@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,223 @@
1
+ import type { Plugin, InlineConfig } from "vite";
2
+ import { build as viteBuild } from "vite";
3
+ import vue from "@vitejs/plugin-vue";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { BuildError } from "../errors";
7
+ import { logger } from "../logger";
8
+ import type { ClientBuildOptions, BuildContext, CopyTarget } from "../types";
9
+ import {
10
+ htmlGenerator,
11
+ localesGenerator,
12
+ minifier,
13
+ nodeDefinitionsInliner,
14
+ staticCopy,
15
+ } from "./plugins";
16
+
17
+ async function build(
18
+ clientBuildOptions: ClientBuildOptions,
19
+ buildContext: BuildContext,
20
+ ): Promise<void> {
21
+ const {
22
+ srcDir = "./client",
23
+ entry = "index.ts",
24
+ name = "NodeRedNodes",
25
+ format = "es",
26
+ licensePath = "./LICENSE",
27
+ locales,
28
+ staticDirs = {},
29
+ external = ["jquery", "node-red", "vue"],
30
+ globals = { jquery: "$", "node-red": "RED", vue: "Vue" },
31
+ manualChunks,
32
+ plugins: userPlugins = [],
33
+ } = clientBuildOptions;
34
+
35
+ const physicalEntryPath = path.resolve(srcDir, entry);
36
+ let entryPath: string;
37
+ let generatedEntry = false;
38
+
39
+ if (fs.existsSync(physicalEntryPath)) {
40
+ entryPath = physicalEntryPath;
41
+ } else {
42
+ // No physical entry — create a minimal empty file that the inliner
43
+ // will prepend auto-registration code into.
44
+ if (!fs.existsSync(path.dirname(physicalEntryPath))) {
45
+ fs.mkdirSync(path.dirname(physicalEntryPath), { recursive: true });
46
+ }
47
+ fs.writeFileSync(physicalEntryPath, "// auto-generated entry\n");
48
+ entryPath = physicalEntryPath;
49
+ generatedEntry = true;
50
+ }
51
+
52
+ const iconsDir = path.resolve(
53
+ staticDirs.icons ?? path.join(path.dirname(path.resolve(srcDir)), "icons"),
54
+ );
55
+
56
+ const plugins: Plugin[] = [
57
+ vue(),
58
+ nodeDefinitionsInliner(
59
+ buildContext.outDir,
60
+ entryPath,
61
+ fs.existsSync(iconsDir) ? iconsDir : undefined,
62
+ path.resolve(srcDir, "components"),
63
+ path.resolve(srcDir, "nodes"),
64
+ !generatedEntry,
65
+ ),
66
+ ...userPlugins,
67
+ ];
68
+
69
+ plugins.push(
70
+ htmlGenerator({
71
+ packageName: buildContext.packageName,
72
+ licensePath: licensePath ? path.resolve(licensePath) : undefined,
73
+ }),
74
+ );
75
+
76
+ if (locales) {
77
+ const {
78
+ docsDir = "./locales/docs",
79
+ labelsDir = "./locales/labels",
80
+ languages = [
81
+ "en-US",
82
+ "de",
83
+ "es-ES",
84
+ "fr",
85
+ "ko",
86
+ "pt-BR",
87
+ "ru",
88
+ "ja",
89
+ "zh-CN",
90
+ "zh-TW",
91
+ ],
92
+ } = locales;
93
+
94
+ plugins.push(
95
+ localesGenerator({
96
+ outDir: path.join(buildContext.outDir, "locales"),
97
+ docsDir: path.resolve(docsDir),
98
+ labelsDir: path.resolve(labelsDir),
99
+ languages,
100
+ }),
101
+ );
102
+ }
103
+
104
+ const copyTargets: CopyTarget[] = [];
105
+
106
+ const publicDir = path.resolve(
107
+ staticDirs.public ?? path.join(srcDir, "public"),
108
+ );
109
+ if (fs.existsSync(publicDir)) {
110
+ copyTargets.push({
111
+ src: publicDir,
112
+ dest: path.join(buildContext.outDir, "resources"),
113
+ });
114
+ }
115
+
116
+ if (fs.existsSync(iconsDir)) {
117
+ copyTargets.push({
118
+ src: iconsDir,
119
+ dest: path.join(buildContext.outDir, "icons"),
120
+ });
121
+ }
122
+
123
+ if (copyTargets.length > 0) {
124
+ plugins.push(staticCopy({ targets: copyTargets }));
125
+ }
126
+
127
+ if (!buildContext.isDev && format === "es") {
128
+ plugins.push(minifier());
129
+ }
130
+
131
+ // Intercept '@bonsae/nrg/client' before Vite's resolver so it stays external.
132
+ // Rollup's string-array external check runs against the *resolved* file path,
133
+ // which wouldn't match the original specifier. 'vue' is handled by the
134
+ // external array directly (Vite keeps the bare specifier for known packages).
135
+ plugins.unshift({
136
+ name: "nrg-client-external",
137
+ enforce: "pre",
138
+ resolveId(id) {
139
+ if (id === "@bonsae/nrg/client")
140
+ return { id: "@bonsae/nrg/client", external: true };
141
+ },
142
+ } as Plugin);
143
+
144
+ const defaultManualChunks = (id: string): string | undefined => {
145
+ if (!id.includes("node_modules")) return undefined;
146
+
147
+ const parts = id
148
+ .substring(id.lastIndexOf("node_modules/") + "node_modules/".length)
149
+ .split("/");
150
+
151
+ const pkgName = parts[0].startsWith("@")
152
+ ? `${parts[0]}/${parts[1]}`
153
+ : parts[0];
154
+
155
+ if (["jsonpointer", "es-toolkit"].includes(pkgName)) return "vendor-utils";
156
+ return "vendor";
157
+ };
158
+
159
+ const config: InlineConfig = {
160
+ configFile: false,
161
+ logLevel: "warn",
162
+ base: `/${path.join("resources", buildContext.packageName)}`,
163
+ publicDir: path.resolve(srcDir, "public"),
164
+ resolve: {
165
+ alias: {
166
+ "@": path.resolve(srcDir),
167
+ },
168
+ },
169
+ plugins,
170
+ css: {
171
+ devSourcemap: buildContext.isDev,
172
+ },
173
+ build: {
174
+ outDir: buildContext.outDir,
175
+ emptyOutDir: false,
176
+ sourcemap: buildContext.isDev ? "inline" : true,
177
+ minify: !buildContext.isDev && format !== "es",
178
+ copyPublicDir: false,
179
+ lib: {
180
+ entry: entryPath,
181
+ name,
182
+ fileName: "index",
183
+ formats: [format],
184
+ },
185
+ rollupOptions: {
186
+ external,
187
+ treeshake: false,
188
+ output: {
189
+ entryFileNames: path.join("resources", "index.[hash].js"),
190
+ chunkFileNames: path.join("resources", "vendor.[hash].js"),
191
+ assetFileNames: path.join("resources", "[name].[hash].[ext]"),
192
+ globals,
193
+ paths: {
194
+ vue: "/nrg/assets/vue.esm-browser.prod.js",
195
+ "@bonsae/nrg/client": "/nrg/assets/nrg-client.js",
196
+ },
197
+ sourcemapPathTransform: (relativeSourcePath) => {
198
+ return relativeSourcePath.replace(/\/client\//g, "/");
199
+ },
200
+ manualChunks: manualChunks ?? defaultManualChunks,
201
+ },
202
+ },
203
+ },
204
+ define: {
205
+ "process.env.NODE_ENV": JSON.stringify(
206
+ buildContext.isDev ? "development" : "production",
207
+ ),
208
+ "process.env": {},
209
+ },
210
+ };
211
+
212
+ try {
213
+ await viteBuild(config);
214
+ } catch (error) {
215
+ throw new BuildError("client", error as Error);
216
+ } finally {
217
+ if (generatedEntry) {
218
+ fs.unlinkSync(physicalEntryPath);
219
+ }
220
+ }
221
+ }
222
+
223
+ export { build };
@@ -0,0 +1 @@
1
+ export { build } from "./build";
@@ -0,0 +1,75 @@
1
+ import type { Plugin } from "vite";
2
+ import mime from "mime-types";
3
+ import fs from "fs";
4
+ import path from "path";
5
+
6
+ function htmlGenerator(options: {
7
+ packageName: string;
8
+ licensePath?: string;
9
+ }): Plugin {
10
+ const { packageName, licensePath } = options;
11
+
12
+ return {
13
+ name: "vite-plugin-node-red:client:html-generator",
14
+ apply: "build",
15
+ enforce: "post",
16
+
17
+ generateBundle(_, bundle) {
18
+ const resourcesTags = Object.keys(bundle)
19
+ .map((fileName) => {
20
+ const asset = bundle[fileName];
21
+ const srcPath = path.join(
22
+ "resources",
23
+ packageName,
24
+ fileName.replace(/^resources\/?/, ""),
25
+ );
26
+
27
+ const content =
28
+ asset.type === "asset"
29
+ ? asset.source
30
+ : asset.type === "chunk"
31
+ ? asset.code
32
+ : null;
33
+
34
+ if (typeof content !== "string" && !(content instanceof Uint8Array))
35
+ return null;
36
+
37
+ const mimeType = mime.lookup(fileName);
38
+
39
+ switch (mimeType) {
40
+ case "application/javascript":
41
+ case "text/javascript":
42
+ return `<script type="module" src="${srcPath}" defer></script>`;
43
+ case "text/css":
44
+ return `<link rel="stylesheet" href="${srcPath}">`;
45
+ case "font/woff":
46
+ case "font/woff2":
47
+ case "application/font-woff":
48
+ case "application/font-woff2":
49
+ case "application/x-font-ttf":
50
+ case "application/x-font-opentype":
51
+ case "font/ttf":
52
+ case "font/otf":
53
+ return `<link rel="preload" as="font" href="${srcPath}" type="${mimeType}">`;
54
+ default:
55
+ return null;
56
+ }
57
+ })
58
+ .filter(Boolean)
59
+ .join("\n");
60
+
61
+ const licenseBanner =
62
+ licensePath && fs.existsSync(licensePath)
63
+ ? `<!--\n${fs.readFileSync(licensePath, "utf-8")}\n-->`
64
+ : "";
65
+
66
+ this.emitFile({
67
+ type: "asset",
68
+ fileName: "index.html",
69
+ source: `${licenseBanner}\n${resourcesTags}`,
70
+ });
71
+ },
72
+ };
73
+ }
74
+
75
+ export { htmlGenerator };
@@ -0,0 +1,5 @@
1
+ export { htmlGenerator } from "./html-generator";
2
+ export { localesGenerator } from "./locales-generator";
3
+ export { minifier } from "./minifier";
4
+ export { nodeDefinitionsInliner } from "./node-definitions-inliner";
5
+ export { staticCopy } from "./static-copy";
@@ -0,0 +1,126 @@
1
+ import type { Plugin } from "vite";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ function localesGenerator(options: {
6
+ outDir: string;
7
+ docsDir: string;
8
+ labelsDir: string;
9
+ languages: string[];
10
+ }): Plugin {
11
+ const { outDir, docsDir, labelsDir, languages } = options;
12
+
13
+ return {
14
+ name: "vite-plugin-node-red:client:locales-generator",
15
+ apply: "build",
16
+ enforce: "post",
17
+
18
+ closeBundle() {
19
+ function validateLanguage(lang: string, filePath: string) {
20
+ if (!languages.includes(lang)) {
21
+ throw new Error(
22
+ `[locales] Invalid language "${lang}" in "${filePath}".\n` +
23
+ `Supported: ${languages.join(", ")}`,
24
+ );
25
+ }
26
+ }
27
+
28
+ function forEachFile<T>(
29
+ baseDir: string,
30
+ fileExtensions: string[],
31
+ processFile: (params: {
32
+ ext: string;
33
+ filePath: string;
34
+ nodeType: string;
35
+ }) => T | null,
36
+ ): Map<string, T extends unknown[] ? T : Record<string, T>> {
37
+ const langMap = new Map();
38
+
39
+ if (!fs.existsSync(baseDir)) return langMap;
40
+
41
+ const nodeDirs = fs
42
+ .readdirSync(baseDir, { withFileTypes: true })
43
+ .filter((d) => d.isDirectory());
44
+
45
+ for (const nodeDir of nodeDirs) {
46
+ const nodeType = nodeDir.name;
47
+ const nodePath = path.join(baseDir, nodeType);
48
+ const files = fs.readdirSync(nodePath);
49
+
50
+ for (const file of files) {
51
+ const ext = path.extname(file);
52
+ if (!fileExtensions.includes(ext)) continue;
53
+
54
+ const lang = path.basename(file, ext);
55
+ const filePath = path.join(nodePath, file);
56
+ validateLanguage(lang, filePath);
57
+
58
+ const value = processFile({ ext, filePath, nodeType });
59
+ if (value == null) continue;
60
+
61
+ if (!langMap.has(lang)) {
62
+ langMap.set(lang, Array.isArray(value) ? [] : {});
63
+ }
64
+
65
+ if (Array.isArray(value)) {
66
+ langMap.get(lang).push(...value);
67
+ } else {
68
+ langMap.get(lang)[nodeType] = value;
69
+ }
70
+ }
71
+ }
72
+
73
+ return langMap;
74
+ }
75
+
76
+ function writeOutput<T>(
77
+ langMap: Map<string, T>,
78
+ fileName: string,
79
+ serialize: (value: T) => string,
80
+ ) {
81
+ for (const [lang, data] of langMap.entries()) {
82
+ const langOutDir = path.join(outDir, lang);
83
+ fs.mkdirSync(langOutDir, { recursive: true });
84
+ fs.writeFileSync(
85
+ path.join(langOutDir, fileName),
86
+ serialize(data),
87
+ "utf-8",
88
+ );
89
+ }
90
+ }
91
+
92
+ const docLangs = forEachFile(
93
+ docsDir,
94
+ [".html", ".md"],
95
+ ({ ext, filePath, nodeType }) => {
96
+ const type =
97
+ ext === ".html"
98
+ ? "text/html"
99
+ : ext === ".md"
100
+ ? "text/markdown"
101
+ : null;
102
+ if (!type) return null;
103
+
104
+ const content = fs.readFileSync(filePath, "utf-8");
105
+ return [
106
+ `<script type="${type}" data-help-name="${nodeType}">\n${content}\n</script>`,
107
+ ];
108
+ },
109
+ );
110
+
111
+ writeOutput(docLangs, "index.html", (value: string[]) =>
112
+ value.join("\n"),
113
+ );
114
+
115
+ const labelLangs = forEachFile(labelsDir, [".json"], ({ filePath }) =>
116
+ JSON.parse(fs.readFileSync(filePath, "utf-8")),
117
+ );
118
+
119
+ writeOutput(labelLangs, "index.json", (value) =>
120
+ JSON.stringify(value, null, 2),
121
+ );
122
+ },
123
+ };
124
+ }
125
+
126
+ export { localesGenerator };
@@ -0,0 +1,22 @@
1
+ import type { Plugin } from "vite";
2
+ import { transform } from "esbuild";
3
+
4
+ function minifier(): Plugin {
5
+ return {
6
+ name: "vite-plugin-node-red:client:minifier",
7
+ apply: "build",
8
+
9
+ renderChunk: {
10
+ order: "post",
11
+ async handler(code, chunk, outputOptions) {
12
+ if (outputOptions.format === "es" && chunk.fileName.endsWith(".js")) {
13
+ const result = await transform(code, { minify: true });
14
+ return result.code;
15
+ }
16
+ return code;
17
+ },
18
+ },
19
+ };
20
+ }
21
+
22
+ export { minifier };
@@ -0,0 +1,224 @@
1
+ import type { Plugin } from "vite";
2
+ import { createRequire } from "module";
3
+ import { pathToFileURL } from "url";
4
+ import path from "path";
5
+ import fs from "fs";
6
+ import mime from "mime-types";
7
+
8
+ const VIRTUAL_ID = "virtual:nrg/node-definitions";
9
+ const RESOLVED_ID = "\0" + VIRTUAL_ID;
10
+
11
+ const VIRTUAL_ENTRY_ID = "virtual:nrg/client-entry";
12
+ const RESOLVED_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID;
13
+
14
+ const SKIP_DEFAULTS = new Set(["x", "y", "z", "g", "wires", "type", "id"]);
15
+
16
+ function getDefaultsFromSchema(
17
+ schema: any,
18
+ ):
19
+ | Record<string, { type?: string; required: boolean; value: any }>
20
+ | undefined {
21
+ if (!schema?.properties) return undefined;
22
+ const result: Record<
23
+ string,
24
+ { type?: string; required: boolean; value: any }
25
+ > = {};
26
+ for (const [key, prop] of Object.entries(schema.properties) as [
27
+ string,
28
+ any,
29
+ ][]) {
30
+ if (SKIP_DEFAULTS.has(key)) continue;
31
+ result[key] = {
32
+ required: false,
33
+ value: prop.default ?? undefined,
34
+ type: prop["node-type"],
35
+ };
36
+ }
37
+ return result;
38
+ }
39
+
40
+ function getCredentialsFromSchema(
41
+ schema: any,
42
+ ): Record<string, { type: string; required: boolean; value: any }> | undefined {
43
+ if (!schema?.properties) return undefined;
44
+ const result: Record<
45
+ string,
46
+ { type: string; required: boolean; value: any }
47
+ > = {};
48
+ for (const [key, prop] of Object.entries(schema.properties) as [
49
+ string,
50
+ any,
51
+ ][]) {
52
+ result[key] = {
53
+ required: false,
54
+ type: prop.format === "password" ? "password" : "text",
55
+ value: prop.default ?? undefined,
56
+ };
57
+ }
58
+ return result;
59
+ }
60
+
61
+ function resolveIcon(iconsDir: string, type: string): string | undefined {
62
+ if (!fs.existsSync(iconsDir)) return undefined;
63
+ return fs.readdirSync(iconsDir).find((f) => {
64
+ if (path.basename(f, path.extname(f)) !== type) return false;
65
+ const mimeType = mime.lookup(f);
66
+ return mimeType !== false && mimeType.startsWith("image/");
67
+ });
68
+ }
69
+
70
+ function nodeDefinitionsInliner(
71
+ serverOutDir: string,
72
+ entryPath: string,
73
+ iconsDir?: string,
74
+ componentsDir?: string,
75
+ nodesDir?: string,
76
+ hasUserEntry: boolean = true,
77
+ ): Plugin {
78
+ let _nodeTypes: string[] = [];
79
+ let _definitions: Record<string, any> = {};
80
+
81
+ return {
82
+ name: "vite-plugin-node-red:client:node-definitions-inliner",
83
+ enforce: "pre",
84
+
85
+ // Load the server bundle in buildStart so _nodeTypes is populated
86
+ // before any load/transform hooks run.
87
+ async buildStart() {
88
+ _nodeTypes = [];
89
+ _definitions = {};
90
+
91
+ const esmEntryPath = path.resolve(serverOutDir, "index.mjs");
92
+ const cjsEntryPath = path.resolve(serverOutDir, "index.js");
93
+
94
+ let packageFn: any;
95
+ if (fs.existsSync(esmEntryPath)) {
96
+ const fileUrl = pathToFileURL(esmEntryPath).href + `?t=${Date.now()}`;
97
+ const mod = await import(fileUrl);
98
+ packageFn = mod?.default ?? mod;
99
+ } else if (fs.existsSync(cjsEntryPath)) {
100
+ const require = createRequire(import.meta.url);
101
+ delete require.cache[cjsEntryPath];
102
+ const rawMod = require(cjsEntryPath);
103
+ packageFn = rawMod?.default ?? rawMod;
104
+ }
105
+
106
+ const nodeClasses: any[] = packageFn?.nodes ?? [];
107
+
108
+ for (const NodeClass of nodeClasses) {
109
+ const type = NodeClass.type;
110
+ if (!type) continue;
111
+ _nodeTypes.push(type);
112
+ const configSchema = NodeClass.configSchema ?? null;
113
+ const credentialsSchema = NodeClass.credentialsSchema ?? null;
114
+ const inputSchema = NodeClass.inputSchema ?? null;
115
+ const outputsSchema = NodeClass.outputsSchema ?? null;
116
+
117
+ // Pre-compute defaults and credentials at build time
118
+ const defaults = getDefaultsFromSchema(configSchema);
119
+ if (defaults && inputSchema) {
120
+ defaults.validateInput = { required: false, value: false };
121
+ }
122
+ if (defaults && outputsSchema) {
123
+ defaults.validateOutput = { required: false, value: false };
124
+ }
125
+ const credentials = getCredentialsFromSchema(credentialsSchema);
126
+
127
+ _definitions[type] = {
128
+ type,
129
+ category: NodeClass.category,
130
+ configSchema,
131
+ credentialsSchema,
132
+ settingsSchema: NodeClass.settingsSchema ?? null,
133
+ defaults: defaults ?? undefined,
134
+ credentials: credentials ?? undefined,
135
+ align: NodeClass.align,
136
+ color: NodeClass.color,
137
+ icon: iconsDir ? resolveIcon(iconsDir, type) : undefined,
138
+ labelStyle: NodeClass.labelStyle,
139
+ paletteLabel: NodeClass.paletteLabel,
140
+ inputs: NodeClass.inputs,
141
+ outputs: NodeClass.outputs,
142
+ inputLabels: NodeClass.inputLabels,
143
+ outputLabels: NodeClass.outputLabels,
144
+ inputSchema,
145
+ outputsSchema,
146
+ };
147
+ }
148
+ },
149
+
150
+ resolveId(id) {
151
+ if (id === VIRTUAL_ID) return RESOLVED_ID;
152
+ if (id === VIRTUAL_ENTRY_ID) return RESOLVED_ENTRY_ID;
153
+ },
154
+
155
+ load(id) {
156
+ if (id === RESOLVED_ENTRY_ID) return "";
157
+ if (id !== RESOLVED_ID) return;
158
+ return `export default ${JSON.stringify(_definitions)};`;
159
+ },
160
+
161
+ transform(code, id) {
162
+ if (id !== entryPath && id !== RESOLVED_ENTRY_ID) return;
163
+
164
+ const nrgImports = new Set<string>(["__setSchemas"]);
165
+ const lines = [`import __nrgSchemas from "${VIRTUAL_ID}";`];
166
+ const postLines: string[] = [`__setSchemas(__nrgSchemas);`];
167
+
168
+ // Auto-detect form components by convention: {componentsDir}/{type}.vue
169
+ if (componentsDir && fs.existsSync(componentsDir)) {
170
+ const formImports: string[] = [];
171
+ const formEntries: string[] = [];
172
+
173
+ for (const type of _nodeTypes) {
174
+ const componentPath = path.resolve(componentsDir, `${type}.vue`);
175
+ if (fs.existsSync(componentPath)) {
176
+ const varName = `__nrgForm_${type.replace(/-/g, "_")}`;
177
+ formImports.push(
178
+ `import ${varName} from ${JSON.stringify(componentPath)};`,
179
+ );
180
+ formEntries.push(`${JSON.stringify(type)}: ${varName}`);
181
+ }
182
+ }
183
+
184
+ if (formImports.length > 0) {
185
+ lines.push(...formImports);
186
+ nrgImports.add("__setForms");
187
+ postLines.push(`__setForms({ ${formEntries.join(", ")} });`);
188
+ }
189
+ }
190
+
191
+ // Auto-register only when no user entry was provided.
192
+ // When the user provides client/index.ts, they control registration.
193
+ if (!hasUserEntry) {
194
+ const defVarNames: string[] = [];
195
+ for (const type of _nodeTypes) {
196
+ const varName = `__nrgNodeDef_${type.replace(/-/g, "_")}`;
197
+ const tsPath = nodesDir ? path.resolve(nodesDir, `${type}.ts`) : null;
198
+
199
+ if (tsPath && fs.existsSync(tsPath)) {
200
+ lines.push(`import ${varName} from ${JSON.stringify(tsPath)};`);
201
+ } else {
202
+ lines.push(`const ${varName} = { type: ${JSON.stringify(type)} };`);
203
+ }
204
+ defVarNames.push(varName);
205
+ }
206
+
207
+ if (defVarNames.length > 0) {
208
+ nrgImports.add("registerTypes");
209
+ postLines.push(`registerTypes([${defVarNames.join(", ")}]);`);
210
+ }
211
+ }
212
+
213
+ // Build the @bonsae/nrg/client import line
214
+ const importLine = `import { ${[...nrgImports].join(", ")} } from "@bonsae/nrg/client";`;
215
+ lines.splice(1, 0, importLine);
216
+
217
+ lines.push(...postLines);
218
+ lines.push("");
219
+ return { code: lines.join("\n") + code, map: null };
220
+ },
221
+ };
222
+ }
223
+
224
+ export { nodeDefinitionsInliner };