@bonsae/create-nrg 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -7,65 +7,99 @@ import { dashCase, scaffoldProject } from "./scaffold.js";
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
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
+ }
10
37
  async function main() {
11
38
  p.intro("Create a new NRG project");
12
39
  const args = process.argv.slice(2);
13
40
  const positionalName = args.find((a) => !a.startsWith("-"));
41
+ const parsed = parseArgs(args);
14
42
  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
- }),
43
+ projectName: () => parsed.name
44
+ ? Promise.resolve(parsed.name)
45
+ : p.text({
46
+ message: "What would you like to name your project?",
47
+ placeholder: "nrg-project",
48
+ defaultValue: positionalName || "nrg-project",
49
+ initialValue: positionalName,
50
+ validate: (value) => {
51
+ if (!value?.trim())
52
+ return "Project name is required.";
53
+ },
54
+ }),
55
+ nodeName: () => parsed.node
56
+ ? Promise.resolve(parsed.node)
57
+ : p.text({
58
+ message: "What would you like to name your first node?",
59
+ placeholder: "node-1",
60
+ defaultValue: "node-1",
61
+ validate: (value) => {
62
+ if (!value?.trim())
63
+ return "Node name is required.";
64
+ },
65
+ }),
66
+ nodeCategory: () => parsed.category
67
+ ? Promise.resolve(parsed.category)
68
+ : p.text({
69
+ message: "Which category should your node belong to?",
70
+ placeholder: "new category",
71
+ defaultValue: "new category",
72
+ }),
73
+ nodeColor: () => parsed.color
74
+ ? Promise.resolve(parsed.color)
75
+ : p.text({
76
+ message: "What color should represent your node? (hex)",
77
+ placeholder: "#1A1A1A",
78
+ defaultValue: "#1A1A1A",
79
+ validate: (value) => {
80
+ if (!value || !/^#([0-9A-F]{3}|[0-9A-F]{6})$/i.test(value)) {
81
+ return "Please enter a valid hex color code (e.g., #1A1A1A or #000)";
82
+ }
83
+ },
84
+ }),
85
+ hasInputs: () => parsed.hasInputs !== undefined
86
+ ? Promise.resolve(parsed.hasInputs)
87
+ : p.confirm({
88
+ message: "Does your node receive messages? (has an input port)",
89
+ initialValue: true,
90
+ }),
91
+ nodeOutputs: () => parsed.outputs
92
+ ? Promise.resolve(parsed.outputs)
93
+ : p.text({
94
+ message: "How many output ports should your node have?",
95
+ placeholder: "1",
96
+ defaultValue: "1",
97
+ validate: (value) => {
98
+ const n = parseInt(value || "", 10);
99
+ if (isNaN(n) || n < 0)
100
+ return "Outputs must be 0 or more.";
101
+ },
102
+ }),
69
103
  }, {
70
104
  onCancel: () => {
71
105
  p.cancel("Operation cancelled.");
@@ -76,8 +110,8 @@ async function main() {
76
110
  const nodeName = project.nodeName;
77
111
  const nodeCategory = project.nodeCategory;
78
112
  const nodeColor = project.nodeColor;
79
- const nodeInputs = project.nodeInputs;
80
- const nodeOutputs = project.nodeOutputs;
113
+ const hasInputs = project.hasInputs;
114
+ const nodeOutputs = parseInt(project.nodeOutputs, 10);
81
115
  const projectDir = path.resolve(process.cwd(), dashCase(projectName));
82
116
  if (fs.existsSync(projectDir)) {
83
117
  p.cancel(`Directory "${dashCase(projectName)}" already exists.`);
@@ -90,7 +124,7 @@ async function main() {
90
124
  nodeName,
91
125
  nodeCategory,
92
126
  nodeColor,
93
- nodeInputs,
127
+ hasInputs,
94
128
  nodeOutputs,
95
129
  });
96
130
  s.stop("Project scaffolded.");
package/dist/scaffold.js CHANGED
@@ -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
- return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? _);
17
+ const template = Handlebars.compile(content, { noEscape: true });
18
+ return template(vars);
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: dashCase(options.nodeName),
48
- pascalCaseNodeName: pascalCase(options.nodeName),
55
+ dashCaseNodeName,
56
+ pascalCaseNodeName,
49
57
  nodeCategory: options.nodeCategory,
50
58
  nodeColor: options.nodeColor,
51
- nodeInputs: options.nodeInputs,
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", `${vars.dashCaseNodeName}.ts`), vars);
61
- renderTemplateFile(path.join(nodeTemplatesDir, "schema.ts.hbs"), path.join(srcDir, "server", "schemas", `${vars.dashCaseNodeName}.ts`), vars);
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", vars.dashCaseNodeName, "en-US.json"), vars);
64
- renderTemplateFile(path.join(nodeTemplatesDir, "docs.md.hbs"), path.join(srcDir, "locales", "docs", vars.dashCaseNodeName, "en-US.md"), vars);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonsae/create-nrg",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a new NRG project for Node-RED",
5
5
  "author": "Allan Oricil <allanoricil@duck.com>",
6
6
  "license": "MIT",
@@ -41,7 +41,8 @@
41
41
  "prepare": "husky"
42
42
  },
43
43
  "dependencies": {
44
- "@clack/prompts": "^1.0.1"
44
+ "@clack/prompts": "^1.0.1",
45
+ "handlebars": "^4.7.9"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@commitlint/cli": "^20.5.0",
@@ -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,4 @@ const SettingsSchema = defineSchema(
37
55
  },
38
56
  );
39
57
 
40
- export { ConfigsSchema, CredentialsSchema, InputSchema, OutputSchema, SettingsSchema };
58
+ export { ConfigsSchema, CredentialsSchema, {{#if hasInputs}}InputSchema, {{/if}}{{#if hasSingleOutput}}OutputSchema, {{/if}}{{#if hasMultipleOutputs}}{{#each outputPorts}}OutputPort{{index}}Schema, {{/each}}{{/if}}SettingsSchema };
@@ -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
- public static override readonly settingsSchema: Schema = SettingsSchema;
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
- this.send(msg as unknown as Output);
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`);