@bonsae/create-nrg 0.4.0 → 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/LICENSE +21 -0
- package/README.md +4 -2
- package/index.js +139 -0
- package/package.json +18 -10
- package/{dist/scaffold.js → scaffold.js} +20 -8
- package/templates/node/schema.ts.hbs +34 -1
- package/templates/node/server-index.ts.hbs +3 -2
- package/templates/node/server-node.ts.hbs +36 -4
- package/templates/project/.gitignore.hbs +1 -0
- package/templates/project/.husky/pre-push.hbs +1 -0
- package/templates/project/package.json.hbs +18 -11
- package/templates/project/src/server/tsconfig.json.hbs +1 -1
- package/templates/project/vitest.server.unit.config.ts.hbs +11 -0
- package/dist/index.js +0 -104
- package/templates/project/vitest.config.ts.hbs +0 -7
- /package/templates/project/{commitlint.config.js.hbs → commitlint.config.ts.hbs} +0 -0
- /package/templates/project/{eslint.config.js.hbs → eslint.config.ts.hbs} +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Allan Oricil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://www.npmjs.com/package/@bonsae/create-nrg"><img src="https://img.shields.io/npm/v/@bonsae/create-nrg.svg" alt="npm package"></a>
|
|
7
7
|
<a href="https://github.com/bonsaedev/create-nrg/actions/workflows/ci.yaml"><img src="https://github.com/bonsaedev/create-nrg/actions/workflows/ci.yaml/badge.svg?branch=main" alt="build status"></a>
|
|
8
|
+
<a href="https://codecov.io/gh/bonsaedev/create-nrg"><img src="https://codecov.io/gh/bonsaedev/create-nrg/graph/badge.svg" alt="codecov"/></a>
|
|
9
|
+
<a href="https://socket.dev/npm/package/@bonsae/create-nrg"><img src="https://badge.socket.dev/npm/package/@bonsae/create-nrg" alt="Socket Badge"></a>
|
|
8
10
|
</p>
|
|
9
11
|
|
|
10
12
|
# create-nrg
|
|
@@ -42,8 +44,8 @@ my-project/
|
|
|
42
44
|
├── .husky/
|
|
43
45
|
├── .gitignore
|
|
44
46
|
├── .prettierrc.json
|
|
45
|
-
├── eslint.config.
|
|
46
|
-
├── commitlint.config.
|
|
47
|
+
├── eslint.config.ts
|
|
48
|
+
├── commitlint.config.ts
|
|
47
49
|
├── package.json
|
|
48
50
|
├── tsconfig.json
|
|
49
51
|
├── vite.config.ts
|
package/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dashCase, scaffoldProject } from "./scaffold.js";
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const TEMPLATES_DIR = path.resolve(__dirname, "templates");
|
|
10
|
+
export function parseArgs(argv) {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const arg = argv[i];
|
|
14
|
+
if (arg === "--inputs") {
|
|
15
|
+
result.hasInputs = true;
|
|
16
|
+
}
|
|
17
|
+
else if (arg === "--no-inputs") {
|
|
18
|
+
result.hasInputs = false;
|
|
19
|
+
}
|
|
20
|
+
else if (arg === "--no-outputs") {
|
|
21
|
+
result.outputs = "0";
|
|
22
|
+
}
|
|
23
|
+
else if (arg.startsWith("--")) {
|
|
24
|
+
const key = arg.slice(2);
|
|
25
|
+
const next = argv[i + 1];
|
|
26
|
+
if (next && !next.startsWith("-")) {
|
|
27
|
+
result[key] = next;
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
result[key] = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/* v8 ignore start -- interactive CLI entry point, tested via cli.test.ts subprocess */
|
|
38
|
+
async function main() {
|
|
39
|
+
p.intro("Create a new NRG project");
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const positionalName = args.find((a) => !a.startsWith("-"));
|
|
42
|
+
const parsed = parseArgs(args);
|
|
43
|
+
const project = await p.group({
|
|
44
|
+
projectName: () => parsed.name
|
|
45
|
+
? Promise.resolve(parsed.name)
|
|
46
|
+
: p.text({
|
|
47
|
+
message: "What would you like to name your project?",
|
|
48
|
+
placeholder: "nrg-project",
|
|
49
|
+
defaultValue: positionalName || "nrg-project",
|
|
50
|
+
initialValue: positionalName,
|
|
51
|
+
validate: (value) => {
|
|
52
|
+
if (!value?.trim())
|
|
53
|
+
return "Project name is required.";
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
nodeName: () => parsed.node
|
|
57
|
+
? Promise.resolve(parsed.node)
|
|
58
|
+
: p.text({
|
|
59
|
+
message: "What would you like to name your first node?",
|
|
60
|
+
placeholder: "node-1",
|
|
61
|
+
defaultValue: "node-1",
|
|
62
|
+
validate: (value) => {
|
|
63
|
+
if (!value?.trim())
|
|
64
|
+
return "Node name is required.";
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
nodeCategory: () => parsed.category
|
|
68
|
+
? Promise.resolve(parsed.category)
|
|
69
|
+
: p.text({
|
|
70
|
+
message: "Which category should your node belong to?",
|
|
71
|
+
placeholder: "new category",
|
|
72
|
+
defaultValue: "new category",
|
|
73
|
+
}),
|
|
74
|
+
nodeColor: () => parsed.color
|
|
75
|
+
? Promise.resolve(parsed.color)
|
|
76
|
+
: p.text({
|
|
77
|
+
message: "What color should represent your node? (hex)",
|
|
78
|
+
placeholder: "#1A1A1A",
|
|
79
|
+
defaultValue: "#1A1A1A",
|
|
80
|
+
validate: (value) => {
|
|
81
|
+
if (!value || !/^#([0-9A-F]{3}|[0-9A-F]{6})$/i.test(value)) {
|
|
82
|
+
return "Please enter a valid hex color code (e.g., #1A1A1A or #000)";
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
hasInputs: () => parsed.hasInputs !== undefined
|
|
87
|
+
? Promise.resolve(parsed.hasInputs)
|
|
88
|
+
: p.confirm({
|
|
89
|
+
message: "Does your node receive messages? (has an input port)",
|
|
90
|
+
initialValue: true,
|
|
91
|
+
}),
|
|
92
|
+
nodeOutputs: () => parsed.outputs
|
|
93
|
+
? Promise.resolve(parsed.outputs)
|
|
94
|
+
: p.text({
|
|
95
|
+
message: "How many output ports should your node have?",
|
|
96
|
+
placeholder: "1",
|
|
97
|
+
defaultValue: "1",
|
|
98
|
+
validate: (value) => {
|
|
99
|
+
const n = parseInt(value || "", 10);
|
|
100
|
+
if (isNaN(n) || n < 0)
|
|
101
|
+
return "Outputs must be 0 or more.";
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
}, {
|
|
105
|
+
onCancel: () => {
|
|
106
|
+
p.cancel("Operation cancelled.");
|
|
107
|
+
process.exit(0);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const projectName = project.projectName;
|
|
111
|
+
const nodeName = project.nodeName;
|
|
112
|
+
const nodeCategory = project.nodeCategory;
|
|
113
|
+
const nodeColor = project.nodeColor;
|
|
114
|
+
const hasInputs = project.hasInputs;
|
|
115
|
+
const nodeOutputs = parseInt(project.nodeOutputs, 10);
|
|
116
|
+
const projectDir = path.resolve(process.cwd(), dashCase(projectName));
|
|
117
|
+
if (fs.existsSync(projectDir)) {
|
|
118
|
+
p.cancel(`Directory "${dashCase(projectName)}" already exists.`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const s = p.spinner();
|
|
122
|
+
s.start("Scaffolding project...");
|
|
123
|
+
scaffoldProject(projectDir, TEMPLATES_DIR, {
|
|
124
|
+
projectName,
|
|
125
|
+
nodeName,
|
|
126
|
+
nodeCategory,
|
|
127
|
+
nodeColor,
|
|
128
|
+
hasInputs,
|
|
129
|
+
nodeOutputs,
|
|
130
|
+
});
|
|
131
|
+
s.stop("Project scaffolded.");
|
|
132
|
+
p.note([`cd ${dashCase(projectName)}`, `pnpm install`, `pnpm run dev`].join("\n"), "Next steps");
|
|
133
|
+
p.outro("Happy coding!");
|
|
134
|
+
}
|
|
135
|
+
main().catch((err) => {
|
|
136
|
+
p.cancel("Something went wrong.");
|
|
137
|
+
console.error(err);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonsae/create-nrg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Scaffold a new NRG project for Node-RED",
|
|
5
5
|
"author": "Allan Oricil <allanoricil@duck.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"create-nrg": "./
|
|
9
|
+
"create-nrg": "./index.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
12
|
+
"index.js",
|
|
13
|
+
"scaffold.js",
|
|
13
14
|
"templates/"
|
|
14
15
|
],
|
|
15
16
|
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
+
"access": "public",
|
|
18
|
+
"directory": "dist"
|
|
17
19
|
},
|
|
18
20
|
"engines": {
|
|
19
21
|
"node": ">=22"
|
|
@@ -30,18 +32,23 @@
|
|
|
30
32
|
"bonsae"
|
|
31
33
|
],
|
|
32
34
|
"scripts": {
|
|
33
|
-
"build": "tsc",
|
|
35
|
+
"build": "tsc && node -e \"const fs=require('fs');fs.cpSync('src/templates','dist/templates',{recursive:true});for(const f of['package.json','README.md','LICENSE'])fs.copyFileSync(f,'dist/'+f)\"",
|
|
34
36
|
"dev": "tsc --watch",
|
|
35
|
-
"
|
|
37
|
+
"validate": "pnpm validate:lint && pnpm validate:format",
|
|
38
|
+
"validate:lint": "eslint src/",
|
|
39
|
+
"validate:format": "prettier --check \"src/**/*.ts\"",
|
|
36
40
|
"lint:fix": "eslint src/ --fix",
|
|
37
41
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
38
|
-
"
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"test:watch": "vitest",
|
|
42
|
+
"test": "pnpm test:unit",
|
|
43
|
+
"test:unit": "vitest run --config vitest.unit.config.ts",
|
|
44
|
+
"test:unit:watch": "vitest --config vitest.unit.config.ts",
|
|
45
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
46
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
41
47
|
"prepare": "husky"
|
|
42
48
|
},
|
|
43
49
|
"dependencies": {
|
|
44
|
-
"@clack/prompts": "^1.0.1"
|
|
50
|
+
"@clack/prompts": "^1.0.1",
|
|
51
|
+
"handlebars": "^4.7.9"
|
|
45
52
|
},
|
|
46
53
|
"devDependencies": {
|
|
47
54
|
"@commitlint/cli": "^20.5.0",
|
|
@@ -53,6 +60,7 @@
|
|
|
53
60
|
"@types/node": "^22.15.18",
|
|
54
61
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
55
62
|
"@typescript-eslint/parser": "^8.32.1",
|
|
63
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
56
64
|
"eslint": "^9.27.0",
|
|
57
65
|
"eslint-config-prettier": "^10.1.8",
|
|
58
66
|
"husky": "^9.1.7",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import Handlebars from "handlebars";
|
|
3
4
|
export function dashCase(str) {
|
|
4
5
|
return str
|
|
5
6
|
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
@@ -13,7 +14,8 @@ export function pascalCase(str) {
|
|
|
13
14
|
.join("");
|
|
14
15
|
}
|
|
15
16
|
export function renderTemplate(content, vars) {
|
|
16
|
-
|
|
17
|
+
const template = Handlebars.compile(content, { noEscape: true });
|
|
18
|
+
return template(vars).replace(/\r\n/g, "\n");
|
|
17
19
|
}
|
|
18
20
|
export function copyTemplateDir(srcDir, destDir, vars) {
|
|
19
21
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
@@ -41,15 +43,25 @@ export function renderTemplateFile(templatePath, destPath, vars) {
|
|
|
41
43
|
fs.writeFileSync(destPath, renderTemplate(content, vars));
|
|
42
44
|
}
|
|
43
45
|
export function scaffoldProject(destDir, templatesDir, options) {
|
|
46
|
+
const dashCaseNodeName = dashCase(options.nodeName);
|
|
47
|
+
const pascalCaseNodeName = pascalCase(options.nodeName);
|
|
48
|
+
const outputPorts = Array.from({ length: options.nodeOutputs }, (_, i) => ({
|
|
49
|
+
index: i + 1,
|
|
50
|
+
schemaName: `OutputPort${i + 1}Schema`,
|
|
51
|
+
}));
|
|
44
52
|
const vars = {
|
|
45
53
|
projectName: dashCase(options.projectName),
|
|
46
54
|
nodeName: options.nodeName,
|
|
47
|
-
dashCaseNodeName
|
|
48
|
-
pascalCaseNodeName
|
|
55
|
+
dashCaseNodeName,
|
|
56
|
+
pascalCaseNodeName,
|
|
49
57
|
nodeCategory: options.nodeCategory,
|
|
50
58
|
nodeColor: options.nodeColor,
|
|
51
|
-
|
|
59
|
+
hasInputs: options.hasInputs,
|
|
52
60
|
nodeOutputs: options.nodeOutputs,
|
|
61
|
+
hasSingleOutput: options.nodeOutputs === 1,
|
|
62
|
+
hasMultipleOutputs: options.nodeOutputs > 1,
|
|
63
|
+
hasOutputs: options.nodeOutputs > 0,
|
|
64
|
+
outputPorts,
|
|
53
65
|
};
|
|
54
66
|
fs.mkdirSync(destDir, { recursive: true });
|
|
55
67
|
// Copy project-level templates (includes directory structure with READMEs)
|
|
@@ -57,9 +69,9 @@ export function scaffoldProject(destDir, templatesDir, options) {
|
|
|
57
69
|
// Place node-specific files into the correct locations
|
|
58
70
|
const nodeTemplatesDir = path.join(templatesDir, "node");
|
|
59
71
|
const srcDir = path.join(destDir, "src");
|
|
60
|
-
renderTemplateFile(path.join(nodeTemplatesDir, "server-node.ts.hbs"), path.join(srcDir, "server", "nodes", `${
|
|
61
|
-
renderTemplateFile(path.join(nodeTemplatesDir, "schema.ts.hbs"), path.join(srcDir, "server", "schemas", `${
|
|
72
|
+
renderTemplateFile(path.join(nodeTemplatesDir, "server-node.ts.hbs"), path.join(srcDir, "server", "nodes", `${dashCaseNodeName}.ts`), vars);
|
|
73
|
+
renderTemplateFile(path.join(nodeTemplatesDir, "schema.ts.hbs"), path.join(srcDir, "server", "schemas", `${dashCaseNodeName}.ts`), vars);
|
|
62
74
|
renderTemplateFile(path.join(nodeTemplatesDir, "server-index.ts.hbs"), path.join(srcDir, "server", "index.ts"), vars);
|
|
63
|
-
renderTemplateFile(path.join(nodeTemplatesDir, "labels.json.hbs"), path.join(srcDir, "locales", "labels",
|
|
64
|
-
renderTemplateFile(path.join(nodeTemplatesDir, "docs.md.hbs"), path.join(srcDir, "locales", "docs",
|
|
75
|
+
renderTemplateFile(path.join(nodeTemplatesDir, "labels.json.hbs"), path.join(srcDir, "locales", "labels", dashCaseNodeName, "en-US.json"), vars);
|
|
76
|
+
renderTemplateFile(path.join(nodeTemplatesDir, "docs.md.hbs"), path.join(srcDir, "locales", "docs", dashCaseNodeName, "en-US.md"), vars);
|
|
65
77
|
}
|
|
@@ -15,20 +15,38 @@ const CredentialsSchema = defineSchema(
|
|
|
15
15
|
$id: "{{pascalCaseNodeName}}CredentialsSchema",
|
|
16
16
|
},
|
|
17
17
|
);
|
|
18
|
+
{{#if hasInputs}}
|
|
18
19
|
|
|
20
|
+
// TODO: define the properties your node expects to receive
|
|
19
21
|
const InputSchema = defineSchema(
|
|
20
22
|
{},
|
|
21
23
|
{
|
|
22
24
|
$id: "{{pascalCaseNodeName}}InputSchema",
|
|
23
25
|
},
|
|
24
26
|
);
|
|
27
|
+
{{/if}}
|
|
28
|
+
{{#if hasSingleOutput}}
|
|
25
29
|
|
|
30
|
+
// TODO: define the properties your node sends
|
|
26
31
|
const OutputSchema = defineSchema(
|
|
27
32
|
{},
|
|
28
33
|
{
|
|
29
34
|
$id: "{{pascalCaseNodeName}}OutputSchema",
|
|
30
35
|
},
|
|
31
36
|
);
|
|
37
|
+
{{/if}}
|
|
38
|
+
{{#if hasMultipleOutputs}}
|
|
39
|
+
{{#each outputPorts}}
|
|
40
|
+
|
|
41
|
+
// TODO: define the properties for output port {{index}}
|
|
42
|
+
const OutputPort{{index}}Schema = defineSchema(
|
|
43
|
+
{},
|
|
44
|
+
{
|
|
45
|
+
$id: "{{../pascalCaseNodeName}}OutputPort{{index}}Schema",
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
{{/each}}
|
|
49
|
+
{{/if}}
|
|
32
50
|
|
|
33
51
|
const SettingsSchema = defineSchema(
|
|
34
52
|
{},
|
|
@@ -37,4 +55,19 @@ const SettingsSchema = defineSchema(
|
|
|
37
55
|
},
|
|
38
56
|
);
|
|
39
57
|
|
|
40
|
-
export {
|
|
58
|
+
export {
|
|
59
|
+
ConfigsSchema,
|
|
60
|
+
CredentialsSchema,
|
|
61
|
+
{{#if hasInputs}}
|
|
62
|
+
InputSchema,
|
|
63
|
+
{{/if}}
|
|
64
|
+
{{#if hasSingleOutput}}
|
|
65
|
+
OutputSchema,
|
|
66
|
+
{{/if}}
|
|
67
|
+
{{#if hasMultipleOutputs}}
|
|
68
|
+
{{#each outputPorts}}
|
|
69
|
+
OutputPort{{index}}Schema,
|
|
70
|
+
{{/each}}
|
|
71
|
+
{{/if}}
|
|
72
|
+
SettingsSchema,
|
|
73
|
+
};
|
|
@@ -3,35 +3,64 @@ import { IONode } from "@bonsae/nrg/server";
|
|
|
3
3
|
import {
|
|
4
4
|
ConfigsSchema,
|
|
5
5
|
CredentialsSchema,
|
|
6
|
+
{{#if hasInputs}}
|
|
6
7
|
InputSchema,
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{#if hasSingleOutput}}
|
|
7
10
|
OutputSchema,
|
|
11
|
+
{{/if}}
|
|
12
|
+
{{#if hasMultipleOutputs}}
|
|
13
|
+
{{#each outputPorts}}
|
|
14
|
+
OutputPort{{index}}Schema,
|
|
15
|
+
{{/each}}
|
|
16
|
+
{{/if}}
|
|
8
17
|
SettingsSchema,
|
|
9
18
|
} from "../schemas/{{dashCaseNodeName}}";
|
|
10
19
|
|
|
11
20
|
export type Config = Infer<typeof ConfigsSchema>;
|
|
12
21
|
export type Credentials = Infer<typeof CredentialsSchema>;
|
|
22
|
+
{{#if hasInputs}}
|
|
13
23
|
export type Input = Infer<typeof InputSchema>;
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{#if hasSingleOutput}}
|
|
14
26
|
export type Output = Infer<typeof OutputSchema>;
|
|
27
|
+
{{/if}}
|
|
15
28
|
export type Settings = Infer<typeof SettingsSchema>;
|
|
16
29
|
|
|
17
30
|
export default class {{pascalCaseNodeName}} extends IONode<
|
|
18
31
|
Config,
|
|
19
32
|
Credentials,
|
|
33
|
+
{{#if hasInputs}}
|
|
20
34
|
Input,
|
|
35
|
+
{{else}}
|
|
36
|
+
any,
|
|
37
|
+
{{/if}}
|
|
38
|
+
{{#if hasSingleOutput}}
|
|
21
39
|
Output,
|
|
40
|
+
{{else}}
|
|
41
|
+
any,
|
|
42
|
+
{{/if}}
|
|
22
43
|
Settings
|
|
23
44
|
> {
|
|
24
45
|
public static override readonly type: string = "{{dashCaseNodeName}}";
|
|
25
46
|
public static override readonly category: string = "{{nodeCategory}}";
|
|
26
47
|
public static override readonly color: `#${string}` = "{{nodeColor}}";
|
|
27
|
-
public static override readonly inputs: number = {{nodeInputs}};
|
|
28
|
-
public static override readonly outputs: number = {{nodeOutputs}};
|
|
29
48
|
|
|
30
49
|
public static override readonly configSchema: Schema = ConfigsSchema;
|
|
31
50
|
public static override readonly credentialsSchema: Schema = CredentialsSchema;
|
|
51
|
+
public static override readonly settingsSchema: Schema = SettingsSchema;
|
|
52
|
+
{{#if hasInputs}}
|
|
53
|
+
// TODO: define the schema for incoming messages
|
|
32
54
|
public static override readonly inputSchema: Schema = InputSchema;
|
|
55
|
+
{{/if}}
|
|
56
|
+
{{#if hasSingleOutput}}
|
|
57
|
+
// TODO: define the schema for outgoing messages
|
|
33
58
|
public static override readonly outputsSchema: Schema = OutputSchema;
|
|
34
|
-
|
|
59
|
+
{{/if}}
|
|
60
|
+
{{#if hasMultipleOutputs}}
|
|
61
|
+
// TODO: define the schema for each output port
|
|
62
|
+
public static override readonly outputsSchema: Schema[] = [{{#each outputPorts}}OutputPort{{index}}Schema{{#unless @last}}, {{/unless}}{{/each}}];
|
|
63
|
+
{{/if}}
|
|
35
64
|
|
|
36
65
|
public static override async registered(RED: RED): Promise<void> {
|
|
37
66
|
RED.log.info(`${this.type} registered`);
|
|
@@ -40,10 +69,13 @@ export default class {{pascalCaseNodeName}} extends IONode<
|
|
|
40
69
|
public override created(): void {
|
|
41
70
|
this.log(`{{pascalCaseNodeName}} ${this.id} created`);
|
|
42
71
|
}
|
|
72
|
+
{{#if hasInputs}}
|
|
43
73
|
|
|
44
74
|
public override async input(msg: Input): Promise<void> {
|
|
45
|
-
|
|
75
|
+
// TODO: implement your node logic here
|
|
76
|
+
this.send(msg as any);
|
|
46
77
|
}
|
|
78
|
+
{{/if}}
|
|
47
79
|
|
|
48
80
|
public override async closed(): Promise<void> {
|
|
49
81
|
this.log(`{{pascalCaseNodeName}} ${this.id} closed`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pnpm build && pnpm validate && pnpm test
|
|
@@ -14,18 +14,20 @@
|
|
|
14
14
|
"nrg"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"debug": "
|
|
18
|
-
"dev": "
|
|
19
|
-
"build:dev": "
|
|
20
|
-
"build": "
|
|
21
|
-
"
|
|
17
|
+
"debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode development",
|
|
18
|
+
"dev": "vite --mode development",
|
|
19
|
+
"build:dev": "vite build --mode development",
|
|
20
|
+
"build": "vite build --mode production",
|
|
21
|
+
"validate": "pnpm validate:tsc && pnpm validate:lint && pnpm validate:format",
|
|
22
|
+
"validate:tsc": "tsc -p src/server/tsconfig.json --noEmit",
|
|
23
|
+
"validate:lint": "eslint src/",
|
|
24
|
+
"validate:format": "prettier --check \"src/**/*.{ts,vue,json}\"",
|
|
22
25
|
"lint:fix": "eslint src/ --fix",
|
|
23
26
|
"format": "prettier --write \"src/**/*.{ts,vue,json}\"",
|
|
24
|
-
"
|
|
25
|
-
"test": "
|
|
26
|
-
"test:
|
|
27
|
-
"
|
|
28
|
-
"tsc:server": "tsc -p src/server/tsconfig.json",
|
|
27
|
+
"test": "pnpm test:server",
|
|
28
|
+
"test:server": "pnpm test:server:unit",
|
|
29
|
+
"test:server:unit": "vitest run --config vitest.server.unit.config.ts",
|
|
30
|
+
"test:server:unit:watch": "vitest --config vitest.server.unit.config.ts",
|
|
29
31
|
"prepare": "husky"
|
|
30
32
|
},
|
|
31
33
|
"lint-staged": {
|
|
@@ -33,6 +35,9 @@
|
|
|
33
35
|
"eslint --fix",
|
|
34
36
|
"prettier --write"
|
|
35
37
|
],
|
|
38
|
+
"tests/**/*.ts": [
|
|
39
|
+
"prettier --write"
|
|
40
|
+
],
|
|
36
41
|
"src/**/*.json": [
|
|
37
42
|
"prettier --write"
|
|
38
43
|
]
|
|
@@ -53,11 +58,13 @@
|
|
|
53
58
|
"globals": "^16.1.0",
|
|
54
59
|
"husky": "^9.1.7",
|
|
55
60
|
"lint-staged": "^16.4.0",
|
|
61
|
+
"node-red": "latest",
|
|
56
62
|
"prettier": "^3.5.3",
|
|
63
|
+
"tsx": "^4.22.4",
|
|
57
64
|
"typescript": "^5.8.3",
|
|
58
65
|
"typescript-eslint": "^8.32.1",
|
|
59
66
|
"vite": "^6.3.4",
|
|
60
67
|
"vitest": "^4.1.5",
|
|
61
|
-
"vue": "^3.5.
|
|
68
|
+
"vue": "^3.5.34"
|
|
62
69
|
}
|
|
63
70
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig, mergeConfig } from "vitest/config";
|
|
2
|
+
import { defaultConfig } from "@bonsae/nrg/test/server/unit/config";
|
|
3
|
+
|
|
4
|
+
export default mergeConfig(
|
|
5
|
+
defaultConfig,
|
|
6
|
+
defineConfig({
|
|
7
|
+
test: {
|
|
8
|
+
include: ["tests/server/**/*.test.ts"],
|
|
9
|
+
},
|
|
10
|
+
}),
|
|
11
|
+
);
|
package/dist/index.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as p from "@clack/prompts";
|
|
3
|
-
import * as fs from "node:fs";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { dashCase, scaffoldProject } from "./scaffold.js";
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
const TEMPLATES_DIR = path.resolve(__dirname, "../templates");
|
|
10
|
-
async function main() {
|
|
11
|
-
p.intro("Create a new NRG project");
|
|
12
|
-
const args = process.argv.slice(2);
|
|
13
|
-
const positionalName = args.find((a) => !a.startsWith("-"));
|
|
14
|
-
const project = await p.group({
|
|
15
|
-
projectName: () => p.text({
|
|
16
|
-
message: "What would you like to name your project?",
|
|
17
|
-
placeholder: "nrg-project",
|
|
18
|
-
defaultValue: positionalName || "nrg-project",
|
|
19
|
-
initialValue: positionalName,
|
|
20
|
-
validate: (value) => {
|
|
21
|
-
if (!value?.trim())
|
|
22
|
-
return "Project name is required.";
|
|
23
|
-
},
|
|
24
|
-
}),
|
|
25
|
-
nodeName: () => p.text({
|
|
26
|
-
message: "What would you like to name your first node?",
|
|
27
|
-
placeholder: "node-1",
|
|
28
|
-
defaultValue: "node-1",
|
|
29
|
-
validate: (value) => {
|
|
30
|
-
if (!value?.trim())
|
|
31
|
-
return "Node name is required.";
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
nodeCategory: () => p.text({
|
|
35
|
-
message: "Which category should your node belong to?",
|
|
36
|
-
placeholder: "new category",
|
|
37
|
-
defaultValue: "new category",
|
|
38
|
-
}),
|
|
39
|
-
nodeColor: () => p.text({
|
|
40
|
-
message: "What color should represent your node? (hex)",
|
|
41
|
-
placeholder: "#1A1A1A",
|
|
42
|
-
defaultValue: "#1A1A1A",
|
|
43
|
-
validate: (value) => {
|
|
44
|
-
if (!value || !/^#([0-9A-F]{3}|[0-9A-F]{6})$/i.test(value)) {
|
|
45
|
-
return "Please enter a valid hex color code (e.g., #1A1A1A or #000)";
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
}),
|
|
49
|
-
nodeInputs: () => p.text({
|
|
50
|
-
message: "How many inputs should your node have? (0 or 1)",
|
|
51
|
-
placeholder: "1",
|
|
52
|
-
defaultValue: "1",
|
|
53
|
-
validate: (value) => {
|
|
54
|
-
const n = parseInt(value || "", 10);
|
|
55
|
-
if (n !== 0 && n !== 1)
|
|
56
|
-
return "Inputs must be either 0 or 1.";
|
|
57
|
-
},
|
|
58
|
-
}),
|
|
59
|
-
nodeOutputs: () => p.text({
|
|
60
|
-
message: "How many outputs should your node have?",
|
|
61
|
-
placeholder: "1",
|
|
62
|
-
defaultValue: "1",
|
|
63
|
-
validate: (value) => {
|
|
64
|
-
const n = parseInt(value || "", 10);
|
|
65
|
-
if (isNaN(n) || n < 0)
|
|
66
|
-
return "Outputs must be 0 or more.";
|
|
67
|
-
},
|
|
68
|
-
}),
|
|
69
|
-
}, {
|
|
70
|
-
onCancel: () => {
|
|
71
|
-
p.cancel("Operation cancelled.");
|
|
72
|
-
process.exit(0);
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
const projectName = project.projectName;
|
|
76
|
-
const nodeName = project.nodeName;
|
|
77
|
-
const nodeCategory = project.nodeCategory;
|
|
78
|
-
const nodeColor = project.nodeColor;
|
|
79
|
-
const nodeInputs = project.nodeInputs;
|
|
80
|
-
const nodeOutputs = project.nodeOutputs;
|
|
81
|
-
const projectDir = path.resolve(process.cwd(), dashCase(projectName));
|
|
82
|
-
if (fs.existsSync(projectDir)) {
|
|
83
|
-
p.cancel(`Directory "${dashCase(projectName)}" already exists.`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
const s = p.spinner();
|
|
87
|
-
s.start("Scaffolding project...");
|
|
88
|
-
scaffoldProject(projectDir, TEMPLATES_DIR, {
|
|
89
|
-
projectName,
|
|
90
|
-
nodeName,
|
|
91
|
-
nodeCategory,
|
|
92
|
-
nodeColor,
|
|
93
|
-
nodeInputs,
|
|
94
|
-
nodeOutputs,
|
|
95
|
-
});
|
|
96
|
-
s.stop("Project scaffolded.");
|
|
97
|
-
p.note([`cd ${dashCase(projectName)}`, `pnpm install`, `pnpm run dev`].join("\n"), "Next steps");
|
|
98
|
-
p.outro("Happy coding!");
|
|
99
|
-
}
|
|
100
|
-
main().catch((err) => {
|
|
101
|
-
p.cancel("Something went wrong.");
|
|
102
|
-
console.error(err);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
});
|
|
File without changes
|
|
File without changes
|