@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,203 @@
1
+ import type { Plugin } from "vite";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { builtinModules } from "node:module";
5
+ import { logger } from "../../logger";
6
+ import type { PackageJson } from "../../types";
7
+
8
+ // NOTE: don't want to recreate it every time resolveId runs
9
+ const nodeBuiltins = new Set([
10
+ ...builtinModules,
11
+ ...builtinModules.map((m) => `node:${m}`),
12
+ ]);
13
+
14
+ function buildTypesPath(entryName: string): string {
15
+ return `./${entryName}.d.ts`;
16
+ }
17
+
18
+ function buildOutputPath(entryName: string): string {
19
+ return `./${entryName}.js`;
20
+ }
21
+
22
+ function buildExportKey(entryName: string): string {
23
+ return entryName === "index" ? "." : `./${entryName}`;
24
+ }
25
+
26
+ function buildEsmOutputPath(entryName: string): string {
27
+ return `./${entryName}.mjs`;
28
+ }
29
+
30
+ function generateExports(
31
+ entryNames: string[],
32
+ format: "cjs" | "esm" = "cjs",
33
+ ): Record<string, unknown> {
34
+ const exports: Record<string, unknown> = {};
35
+ for (const name of entryNames) {
36
+ const key = buildExportKey(name);
37
+ if (format === "esm") {
38
+ exports[key] = {
39
+ types: buildTypesPath(name),
40
+ import: buildEsmOutputPath(name),
41
+ require: buildOutputPath(name),
42
+ default: buildOutputPath(name),
43
+ };
44
+ } else {
45
+ exports[key] = {
46
+ types: buildTypesPath(name),
47
+ require: buildOutputPath(name),
48
+ default: buildOutputPath(name),
49
+ };
50
+ }
51
+ }
52
+ return exports;
53
+ }
54
+
55
+ function patchExportsWithTypes(
56
+ existingExports: Record<string, unknown>,
57
+ entryNames: string[],
58
+ ): Record<string, unknown> {
59
+ const patched: Record<string, unknown> = { ...existingExports };
60
+ for (const [key, value] of Object.entries(patched)) {
61
+ const entryName = key === "." ? "index" : key.replace(/^\.\//, "");
62
+ if (!entryNames.includes(entryName)) continue;
63
+ const typesPath = buildTypesPath(entryName);
64
+ if (typeof value === "string") {
65
+ patched[key] = { types: typesPath, require: value, default: value };
66
+ } else if (typeof value === "object" && value !== null) {
67
+ const condition = value as Record<string, unknown>;
68
+ if (!condition.types) {
69
+ patched[key] = { types: typesPath, ...condition };
70
+ }
71
+ }
72
+ }
73
+ return patched;
74
+ }
75
+
76
+ export function packageJsonGenerator(options: {
77
+ outDir: string;
78
+ bundled?: string[];
79
+ types?: boolean;
80
+ entryNames?: string[];
81
+ format?: "cjs" | "esm";
82
+ }): Plugin {
83
+ const {
84
+ outDir,
85
+ bundled = [],
86
+ types = false,
87
+ entryNames = [],
88
+ format = "cjs",
89
+ } = options;
90
+
91
+ const trackedDependencies = new Set<string>();
92
+ return {
93
+ name: "vite-plugin-node-red:server:package-json-generator",
94
+ enforce: "pre",
95
+
96
+ buildStart() {
97
+ trackedDependencies.clear();
98
+ },
99
+
100
+ resolveId: {
101
+ order: "pre",
102
+ handler(source, importer) {
103
+ if (!importer || source.startsWith(".") || source.startsWith("/")) {
104
+ return null;
105
+ }
106
+
107
+ if (nodeBuiltins.has(source)) {
108
+ return { id: source, external: true };
109
+ }
110
+
111
+ const packageName = source.startsWith("@")
112
+ ? source.split("/").slice(0, 2).join("/")
113
+ : source.split("/")[0];
114
+
115
+ if (bundled.includes(packageName)) {
116
+ return null;
117
+ }
118
+
119
+ trackedDependencies.add(packageName);
120
+ return { id: source, external: true };
121
+ },
122
+ },
123
+
124
+ closeBundle() {
125
+ const rootPackageJsonPath = path.resolve("./package.json");
126
+ if (!fs.existsSync(rootPackageJsonPath)) {
127
+ logger.warn(`package.json not found: ${rootPackageJsonPath}`);
128
+ return;
129
+ }
130
+
131
+ const rootPackageJson = JSON.parse(
132
+ fs.readFileSync(rootPackageJsonPath, "utf-8"),
133
+ ) as PackageJson;
134
+
135
+ const sourceDeps: Record<string, string> =
136
+ rootPackageJson.dependencies ?? {};
137
+ const peerDeps: Record<string, string> =
138
+ rootPackageJson.peerDependencies ?? {};
139
+ let distDependencies: Record<string, string> | undefined = {};
140
+ for (const dep of trackedDependencies) {
141
+ if (peerDeps[dep]) {
142
+ continue;
143
+ }
144
+ if (sourceDeps[dep]) {
145
+ distDependencies[dep] = sourceDeps[dep];
146
+ } else {
147
+ const dependencyPackageJsonPath = path.resolve(
148
+ `./node_modules/${dep}/package.json`,
149
+ );
150
+ if (fs.existsSync(dependencyPackageJsonPath)) {
151
+ const dependencyPackageJson = JSON.parse(
152
+ fs.readFileSync(dependencyPackageJsonPath, "utf-8"),
153
+ ) as PackageJson;
154
+ distDependencies[dep] = `^${dependencyPackageJson.version}`;
155
+ }
156
+ }
157
+ }
158
+
159
+ if (Object.keys(distDependencies).length === 0) {
160
+ distDependencies = undefined;
161
+ }
162
+
163
+ const distPackageJson: PackageJson = {
164
+ ...rootPackageJson,
165
+ main: "index.js",
166
+ type: "commonjs",
167
+ devDependencies: undefined,
168
+ scripts: undefined,
169
+ dependencies: distDependencies,
170
+ keywords: [
171
+ ...new Set([...(rootPackageJson.keywords ?? []), "node-red"]),
172
+ ],
173
+ "node-red": {
174
+ nodes: { nodes: "index.js" },
175
+ },
176
+ };
177
+
178
+ if (types && entryNames.length > 0) {
179
+ const userExports = rootPackageJson.exports;
180
+ if (userExports && Object.keys(userExports).length > 0) {
181
+ distPackageJson.exports = patchExportsWithTypes(
182
+ userExports,
183
+ entryNames,
184
+ );
185
+ } else {
186
+ distPackageJson.exports = generateExports(entryNames, format);
187
+ if (entryNames.includes("index")) {
188
+ distPackageJson.types = "index.d.ts";
189
+ }
190
+ }
191
+ }
192
+
193
+ if (!fs.existsSync(outDir)) {
194
+ fs.mkdirSync(outDir, { recursive: true });
195
+ }
196
+
197
+ fs.writeFileSync(
198
+ path.join(outDir, "package.json"),
199
+ JSON.stringify(distPackageJson, null, 2),
200
+ );
201
+ },
202
+ };
203
+ }
@@ -0,0 +1,285 @@
1
+ import type { Plugin } from "vite";
2
+ import dts from "vite-plugin-dts";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import ts from "typescript";
6
+
7
+ /**
8
+ * Recursively collect all .ts files under a directory, excluding .d.ts files.
9
+ */
10
+ function collectTsFiles(dir: string): string[] {
11
+ if (!fs.existsSync(dir)) return [];
12
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((dirent) => {
13
+ const full = path.join(dir, dirent.name);
14
+ if (dirent.isDirectory()) return collectTsFiles(full);
15
+ if (
16
+ dirent.isFile() &&
17
+ dirent.name.endsWith(".ts") &&
18
+ !dirent.name.endsWith(".d.ts")
19
+ )
20
+ return [full];
21
+ return [];
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Convert a file basename (without extension) to PascalCase.
27
+ * E.g. "your-node" → "YourNode", "remote_server" → "RemoteServer"
28
+ */
29
+ function toPascalCase(name: string): string {
30
+ return name
31
+ .split(/[-_]/)
32
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
33
+ .join("");
34
+ }
35
+
36
+ /**
37
+ * Positional semantic names for each framework base class.
38
+ * Position i in the generic corresponds to the i-th semantic slot.
39
+ * Users can name their types anything — the position determines meaning.
40
+ */
41
+ const BASE_CLASS_SLOTS: Record<string, string[]> = {
42
+ IONode: ["Config", "Credentials", "Input", "Output", "Settings"],
43
+ ConfigNode: ["Config", "Credentials", "Settings"],
44
+ };
45
+
46
+ /**
47
+ * Parse a node source file and return `{ localName, semanticName }` pairs for
48
+ * types that should be exported from the package.
49
+ *
50
+ * The semantic name is derived from the generic POSITION in the base class, so
51
+ * a user can name their types anything:
52
+ *
53
+ * export default class MyNode extends IONode<Bananas, MyCreds, …>
54
+ * export type Bananas = { … } → exported as {Ns}Config (position 0)
55
+ * export type MyCreds = { … } → exported as {Ns}Credentials (position 1)
56
+ *
57
+ * Only types that appear as generic args AND are exported from the file are
58
+ * included, so unrelated helper types are never leaked.
59
+ */
60
+ function getNodeTypeExports(
61
+ filePath: string,
62
+ ): Array<{ localName: string; semanticName: string }> {
63
+ const content = fs.readFileSync(filePath, "utf-8");
64
+ const source = ts.createSourceFile(
65
+ filePath,
66
+ content,
67
+ ts.ScriptTarget.ESNext,
68
+ /* setParentNodes */ true,
69
+ );
70
+
71
+ const hasModifier = (node: ts.HasModifiers, kind: ts.SyntaxKind) =>
72
+ (node.modifiers ?? []).some((m) => m.kind === kind);
73
+
74
+ // Collect names of exported type aliases and interfaces.
75
+ const exportedTypeNames = new Set<string>();
76
+ for (const stmt of source.statements) {
77
+ if (
78
+ ts.isTypeAliasDeclaration(stmt) &&
79
+ hasModifier(stmt, ts.SyntaxKind.ExportKeyword)
80
+ ) {
81
+ exportedTypeNames.add(stmt.name.text);
82
+ }
83
+ if (
84
+ ts.isInterfaceDeclaration(stmt) &&
85
+ hasModifier(stmt, ts.SyntaxKind.ExportKeyword)
86
+ ) {
87
+ exportedTypeNames.add(stmt.name.text);
88
+ }
89
+ }
90
+
91
+ if (exportedTypeNames.size === 0) return [];
92
+
93
+ // Find `export default class … extends BaseClass<T0, T1, …>` and extract
94
+ // the base class name and its generic type argument names by position.
95
+ let baseClassName = "";
96
+ const genericArgNames: string[] = [];
97
+
98
+ for (const stmt of source.statements) {
99
+ if (
100
+ !ts.isClassDeclaration(stmt) ||
101
+ !hasModifier(stmt, ts.SyntaxKind.DefaultKeyword)
102
+ ) {
103
+ continue;
104
+ }
105
+ for (const clause of stmt.heritageClauses ?? []) {
106
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
107
+ for (const type of clause.types) {
108
+ if (ts.isIdentifier(type.expression)) {
109
+ baseClassName = type.expression.text;
110
+ }
111
+ for (const arg of type.typeArguments ?? []) {
112
+ if (ts.isTypeReferenceNode(arg) && ts.isIdentifier(arg.typeName)) {
113
+ genericArgNames.push(arg.typeName.text);
114
+ } else {
115
+ // Unknown / complex type arg — push empty string to preserve positions.
116
+ genericArgNames.push("");
117
+ }
118
+ }
119
+ }
120
+ }
121
+ break;
122
+ }
123
+
124
+ const slots = BASE_CLASS_SLOTS[baseClassName];
125
+ if (!slots) return [];
126
+
127
+ // Build pairs: for each position, if the arg is exported AND we have a
128
+ // semantic name for that slot, include it.
129
+ const result: Array<{ localName: string; semanticName: string }> = [];
130
+ for (let i = 0; i < genericArgNames.length; i++) {
131
+ const localName = genericArgNames[i];
132
+ const semanticName = slots[i];
133
+ if (localName && semanticName && exportedTypeNames.has(localName)) {
134
+ result.push({ localName, semanticName });
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+
140
+ /**
141
+ * Build re-export statements for every file in {srcDir}/nodes/, relative to
142
+ * the given entry file path.
143
+ *
144
+ * Per node file two kinds of statements are generated:
145
+ *
146
+ * // 1. Class re-export — so api-extractor includes the class declaration
147
+ * // in the bundled .d.ts, enabling `import { YourNode }`, `instanceof`,
148
+ * // type assertions `as YourNode`, and `resolveTypedInput<YourNode>()`.
149
+ * export { default as YourNode } from "./nodes/your-node";
150
+ *
151
+ * // 2. Type-only exports — one per exported type alias/interface, prefixed
152
+ * // with the namespace name to avoid collisions across node files.
153
+ * // Enables `import type { YourNodeConfig, YourNodeCredentials }`.
154
+ * export type { Config as YourNodeConfig, Credentials as YourNodeCredentials,
155
+ * Input as YourNodeInput, Output as YourNodeOutput,
156
+ * Settings as YourNodeSettings } from "./nodes/your-node";
157
+ *
158
+ * These statements are appended in memory to the entry file content served to
159
+ * the TypeScript compiler via a `ts.sys.readFile` patch, so api-extractor
160
+ * includes them in the final bundled `.d.ts` without touching any source file.
161
+ * Runtime class values are exposed by `cjsDefaultExportPlugin` dynamically
162
+ * via the `nodes` array on the package function.
163
+ */
164
+ function buildNodeReexports(srcDir: string, entryFile: string): string {
165
+ const nodesDir = path.join(srcDir, "nodes");
166
+ const nodeFiles = collectTsFiles(nodesDir);
167
+ return nodeFiles
168
+ .map((file) => {
169
+ const rel = path
170
+ .relative(path.dirname(entryFile), file)
171
+ .replace(/\\/g, "/");
172
+ const relPath = rel.startsWith(".") ? rel : `./${rel}`;
173
+ const specifier = relPath.replace(/\.ts$/, "");
174
+ const ns = toPascalCase(path.basename(file, ".ts"));
175
+
176
+ const lines = [`export { default as ${ns} } from "${specifier}";`];
177
+
178
+ const typePairs = getNodeTypeExports(file);
179
+ if (typePairs.length > 0) {
180
+ const prefixed = typePairs
181
+ .map(
182
+ ({ localName, semanticName }) =>
183
+ `${localName} as ${ns}${semanticName}`,
184
+ )
185
+ .join(", ");
186
+ lines.push(`export type { ${prefixed} } from "${specifier}";`);
187
+ }
188
+
189
+ return lines.join("\n");
190
+ })
191
+ .join("\n");
192
+ }
193
+
194
+ function typeGenerator(options: {
195
+ srcDir: string;
196
+ outDir: string;
197
+ /** Absolute paths of entry files (values from the entryPoints record). */
198
+ entryFiles: string[];
199
+ }): Plugin[] {
200
+ const { srcDir, outDir, entryFiles } = options;
201
+
202
+ // Prefer the server-specific tsconfig (inside srcDir) over the root one.
203
+ // The root tsconfig typically only includes vite.config.ts, while the server
204
+ // tsconfig includes the actual source files needed for declaration emit.
205
+ const serverTsconfig = path.resolve(srcDir, "tsconfig.json");
206
+ const rootTsconfig = path.resolve("tsconfig.json");
207
+ const userTsconfig = fs.existsSync(serverTsconfig)
208
+ ? serverTsconfig
209
+ : rootTsconfig;
210
+ // Exclude the user's client dir (sibling of srcDir), any nested client dirs,
211
+ // and the vite/ build-tooling dir — none of these should appear in the
212
+ // published type declarations.
213
+ const clientDir = path.relative(
214
+ process.cwd(),
215
+ path.join(path.dirname(srcDir), "client"),
216
+ );
217
+
218
+ // Absolute resolved path → augmented source content (in-memory only).
219
+ const augmentedContents = new Map<string, string>();
220
+ // Original ts.sys.readFile — restored in closeBundle.
221
+ let origReadFile: typeof ts.sys.readFile | null = null;
222
+
223
+ const nodeTypeExporter: Plugin = {
224
+ name: "vite-plugin-node-red:server:type-exporter",
225
+ enforce: "pre",
226
+ buildStart() {
227
+ augmentedContents.clear();
228
+ for (const entryFile of entryFiles) {
229
+ const reexports = buildNodeReexports(srcDir, entryFile);
230
+ if (!reexports) continue;
231
+ const original = fs.readFileSync(entryFile, "utf-8");
232
+ augmentedContents.set(
233
+ path.resolve(entryFile),
234
+ `${original}\n${reexports}\n`,
235
+ );
236
+ }
237
+ if (augmentedContents.size === 0) return;
238
+ origReadFile = ts.sys.readFile.bind(ts.sys);
239
+ ts.sys.readFile = (fileName, encoding) => {
240
+ const resolved = path.resolve(fileName);
241
+ return (
242
+ augmentedContents.get(resolved) ?? origReadFile!(fileName, encoding)
243
+ );
244
+ };
245
+ },
246
+ closeBundle() {
247
+ if (origReadFile) {
248
+ ts.sys.readFile = origReadFile;
249
+ origReadFile = null;
250
+ }
251
+ augmentedContents.clear();
252
+ },
253
+ };
254
+
255
+ return [
256
+ nodeTypeExporter,
257
+ dts({
258
+ rollupTypes: true,
259
+ rollupOptions: {
260
+ messageCallback(message) {
261
+ if (message.messageId === "console-preamble") {
262
+ message.logLevel = "none";
263
+ }
264
+ },
265
+ },
266
+ outDir,
267
+ ...(fs.existsSync(userTsconfig) && { tsconfigPath: userTsconfig }),
268
+ compilerOptions: {
269
+ noEmit: false,
270
+ declaration: true,
271
+ emitDeclarationOnly: true,
272
+ noCheck: true,
273
+ },
274
+ exclude: [
275
+ `${clientDir}/**`,
276
+ "**/client/**",
277
+ "vite/**",
278
+ "node_modules/**",
279
+ "dist/**",
280
+ ],
281
+ }),
282
+ ].flat();
283
+ }
284
+
285
+ export { typeGenerator };