pochade-node-red 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.
- package/README.md +71 -0
- package/bin/create-pochade-js.js +286 -0
- package/package.json +34 -0
- package/template/AGENTS.md +140 -0
- package/template/README.md +39 -0
- package/template/cursor.md +4 -0
- package/template/gemini.md +4 -0
- package/template/package-lock.json +9379 -0
- package/template/package.json +24 -0
- package/template/src/sample.html +26 -0
- package/template/src/sample.js +11 -0
- package/template/warp.md +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Pochade Node-RED
|
|
4
|
+
## For Writing Node-RED Plugins with Passion
|
|
5
|
+
|
|
6
|
+
Pochade Node-RED is a lightweight, opinionated generator for building Node-RED nodes (plugins) with modern standards but without the bloat. It sets up a clean, vanilla JavaScript environment so you can focus on writing the logic for your node.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Instant Scaffolding** - Generates a complete, ready-to-deploy Node-RED node project.
|
|
11
|
+
- **Auto Reload and Install** - Work in the nodes home directory, our watch script auto-installs and auto-reloads node-red when you make changes in the folder.
|
|
12
|
+
- **Interactive Setup** - easy-to-use CLI questionnaire to configure your node.
|
|
13
|
+
- **Vanilla JS** - No complex build pipelines by default, just standard Node.js and HTML.
|
|
14
|
+
- **Developer Experience** - Includes helper scripts to easily install your plugin into your local Node-RED instance for testing.
|
|
15
|
+
- **AI Ready** - Includes documentation to help AI assistants (`LLM.md`, `AGENTS.md`) understand your project structure.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
Create a new Node-RED node project with a single command:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx pochade-node-red my-node-red-plugin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
cd into the new directory.
|
|
28
|
+
|
|
29
|
+
### Installing to Local Node-RED
|
|
30
|
+
|
|
31
|
+
To test your node, you need to install it into your local Node-RED environment. We've included a handy script for this:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm run install-plugin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Watch Mode
|
|
38
|
+
|
|
39
|
+
If you are making frequent changes, you can use:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm run watch
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This will start node-red and on every text change will re-install the plugin to the ~/.node-red node_modules directory and restart node-red.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Project Structure
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
my-node-red-plugin/
|
|
53
|
+
├── src/
|
|
54
|
+
│ ├── my-node.js # The Node.js runtime logic
|
|
55
|
+
│ └── my-node.html # The Editor UI and help text
|
|
56
|
+
├── bin/ # Utility scripts
|
|
57
|
+
├── package.json # Node-RED module metadata
|
|
58
|
+
└── README.md # Documentation for your user
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
Unlicense (Public Domain)
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Contributing
|
|
70
|
+
|
|
71
|
+
Issues and pull requests welcome at [github.com/lnsy-dev/pochade-node-red](https://github.com/lnsy-dev/pochade-node-red)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pochade-node-red
|
|
5
|
+
*
|
|
6
|
+
* Creates a new Pochade Node-RED project from the template.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx pochade-node-red my-node
|
|
9
|
+
*
|
|
10
|
+
* @module pochade-node-red
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const spawn = require('cross-spawn');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const readline = require('readline');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a readline interface for user input
|
|
20
|
+
*
|
|
21
|
+
* @returns {readline.Interface} The readline interface
|
|
22
|
+
*/
|
|
23
|
+
function createReadlineInterface() {
|
|
24
|
+
return readline.createInterface({
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
output: process.stdout
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Prompts the user with a question and returns their answer
|
|
32
|
+
*
|
|
33
|
+
* @param {readline.Interface} rl - The readline interface
|
|
34
|
+
* @param {string} question - The question to ask
|
|
35
|
+
* @param {string} defaultValue - Optional default value
|
|
36
|
+
* @returns {Promise<string>} The user's answer
|
|
37
|
+
*/
|
|
38
|
+
function ask(rl, question, defaultValue = '') {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const prompt = defaultValue
|
|
41
|
+
? `${question} (${defaultValue}): `
|
|
42
|
+
: `${question}: `;
|
|
43
|
+
|
|
44
|
+
rl.question(prompt, (answer) => {
|
|
45
|
+
resolve(answer.trim() || defaultValue);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prompts the user with a question and validates the answer
|
|
52
|
+
*
|
|
53
|
+
* @param {readline.Interface} rl - The readline interface
|
|
54
|
+
* @param {string} question - The question to ask
|
|
55
|
+
* @param {function} validator - Function that returns true if valid, or a string error message
|
|
56
|
+
* @param {string} defaultValue - Optional default value
|
|
57
|
+
* @returns {Promise<string>} The user's answer
|
|
58
|
+
*/
|
|
59
|
+
async function askWithValidation(rl, question, validator, defaultValue = '') {
|
|
60
|
+
while (true) {
|
|
61
|
+
const answer = await ask(rl, question, defaultValue);
|
|
62
|
+
const validation = validator(answer);
|
|
63
|
+
if (validation === true) {
|
|
64
|
+
return answer;
|
|
65
|
+
}
|
|
66
|
+
console.log(`❌ ${validation}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validates a package/project name (URL-friendly, no spaces)
|
|
72
|
+
* @param {string} name
|
|
73
|
+
* @returns {boolean|string}
|
|
74
|
+
*/
|
|
75
|
+
function validatePackageName(name) {
|
|
76
|
+
if (!name) return "Name cannot be empty";
|
|
77
|
+
if (!/^[a-z0-9-_]+$/.test(name)) {
|
|
78
|
+
return "Name must only contain lowercase letters, numbers, hyphens, and underscores";
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validates a node name (safe for filenames)
|
|
85
|
+
* @param {string} name
|
|
86
|
+
* @returns {boolean|string}
|
|
87
|
+
*/
|
|
88
|
+
function validateNodeName(name) {
|
|
89
|
+
if (!name) return "Name cannot be empty";
|
|
90
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
91
|
+
return "Name must only contain lowercase letters, numbers, and hyphens";
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Collects project configuration from user input
|
|
100
|
+
*
|
|
101
|
+
* @param {string} projectName - The project name
|
|
102
|
+
* @returns {Promise<object>} Configuration object with all project details
|
|
103
|
+
*/
|
|
104
|
+
async function collectProjectInfo(projectName) {
|
|
105
|
+
const rl = createReadlineInterface();
|
|
106
|
+
|
|
107
|
+
const logo = ".-. .-. .-. . . .-. .-. .-. . .-.\r\n|-\' | | | |-| |-| | )|- | `-.\r\n\' `-\' `-\' \' ` ` \' `-\' `-\' `-\' `-\'\r\n Node-RED Plugins with Passion\r\n By LNSY\r\n"
|
|
108
|
+
console.log(logo);
|
|
109
|
+
|
|
110
|
+
console.log('\n📝 Let\'s set up your Node-RED Plugin project!\n');
|
|
111
|
+
|
|
112
|
+
// Default node name derived from project name
|
|
113
|
+
const defaultNodeName = projectName.replace(/^node-red-contrib-/, '');
|
|
114
|
+
|
|
115
|
+
const config = {
|
|
116
|
+
project_name: projectName,
|
|
117
|
+
project_description: await ask(rl, 'Project description', 'A Node-RED node'),
|
|
118
|
+
node_sidebar_title: await ask(rl, 'Sidebar title (category)', 'function'),
|
|
119
|
+
node_name: await askWithValidation(rl, 'Node name', validateNodeName, defaultNodeName),
|
|
120
|
+
node_purpose: await ask(rl, 'Node purpose', 'Processes messages'),
|
|
121
|
+
author_name: await ask(rl, 'Author name', ''),
|
|
122
|
+
license: await ask(rl, 'License', 'MIT')
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
rl.close();
|
|
128
|
+
|
|
129
|
+
return config;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Replaces template variables in a file content
|
|
134
|
+
*
|
|
135
|
+
* @param {string} content - The content with template variables
|
|
136
|
+
* @param {object} config - Configuration object with values
|
|
137
|
+
* @returns {string} Content with variables replaced
|
|
138
|
+
*/
|
|
139
|
+
function replaceTemplateVariables(content, config) {
|
|
140
|
+
return content
|
|
141
|
+
.replace(/\$\{\s*project_description\s*\}/g, config.project_description)
|
|
142
|
+
.replace(/\$\{\s*node_sidebar_title\s*\}/g, config.node_sidebar_title)
|
|
143
|
+
.replace(/\$\{\s*node_name\s*\}/g, config.node_name)
|
|
144
|
+
.replace(/\$\{\s*node_purpose\s*\}/g, config.node_purpose);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Recursively updates files in the project directory
|
|
149
|
+
*
|
|
150
|
+
* @param {string} dir - Directory to process
|
|
151
|
+
* @param {object} config - Configuration object
|
|
152
|
+
*/
|
|
153
|
+
function updateFiles(dir, config) {
|
|
154
|
+
const files = fs.readdirSync(dir);
|
|
155
|
+
|
|
156
|
+
for (const file of files) {
|
|
157
|
+
const filePath = path.join(dir, file);
|
|
158
|
+
const stats = fs.statSync(filePath);
|
|
159
|
+
|
|
160
|
+
if (stats.isDirectory()) {
|
|
161
|
+
updateFiles(filePath, config);
|
|
162
|
+
} else {
|
|
163
|
+
// Only process text files
|
|
164
|
+
if (['.js', '.html', '.json', '.md'].includes(path.extname(file))) {
|
|
165
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
166
|
+
content = replaceTemplateVariables(content, config);
|
|
167
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Main function to create a new Pochade Node-RED project
|
|
175
|
+
*
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
async function createProject() {
|
|
179
|
+
const projectName = process.argv[2];
|
|
180
|
+
|
|
181
|
+
// Validate project name
|
|
182
|
+
if (!projectName) {
|
|
183
|
+
console.error('Error: Please specify the project name.');
|
|
184
|
+
console.log('Usage: npx pochade-node-red <project-name>');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const nameValidation = validatePackageName(projectName);
|
|
189
|
+
if (nameValidation !== true) {
|
|
190
|
+
console.error(`Error: Invalid project name "${projectName}".`);
|
|
191
|
+
console.error(nameValidation);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const currentDir = process.cwd();
|
|
196
|
+
const projectDir = path.resolve(currentDir, projectName);
|
|
197
|
+
|
|
198
|
+
if (fs.existsSync(projectDir)) {
|
|
199
|
+
console.error(`Error: Directory "${projectName}" already exists.`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const config = await collectProjectInfo(projectName);
|
|
204
|
+
|
|
205
|
+
console.log(`\n🚀 Creating a new Node-RED node in ${projectDir}...`);
|
|
206
|
+
|
|
207
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
208
|
+
|
|
209
|
+
const templateDir = path.resolve(__dirname, '..', 'template');
|
|
210
|
+
|
|
211
|
+
if (!fs.existsSync(templateDir)) {
|
|
212
|
+
console.error('Error: Template directory not found.');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Copy template
|
|
217
|
+
fs.cpSync(templateDir, projectDir, { recursive: true });
|
|
218
|
+
|
|
219
|
+
// Rename dotfiles
|
|
220
|
+
const dotfiles = [
|
|
221
|
+
{ from: 'gitignore', to: '.gitignore' },
|
|
222
|
+
{ from: 'npmignore', to: '.npmignore' }
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
dotfiles.forEach(({ from, to }) => {
|
|
226
|
+
const fromPath = path.join(projectDir, from);
|
|
227
|
+
const toPath = path.join(projectDir, to);
|
|
228
|
+
if (fs.existsSync(fromPath)) {
|
|
229
|
+
fs.renameSync(fromPath, toPath);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Rename node files
|
|
234
|
+
const srcDir = path.join(projectDir, 'src');
|
|
235
|
+
if (fs.existsSync(srcDir)) {
|
|
236
|
+
const nodeJsPath = path.join(srcDir, 'sample.js');
|
|
237
|
+
const nodeHtmlPath = path.join(srcDir, 'sample.html');
|
|
238
|
+
|
|
239
|
+
if (fs.existsSync(nodeJsPath)) {
|
|
240
|
+
fs.renameSync(nodeJsPath, path.join(srcDir, `${config.node_name}.js`));
|
|
241
|
+
}
|
|
242
|
+
if (fs.existsSync(nodeHtmlPath)) {
|
|
243
|
+
fs.renameSync(nodeHtmlPath, path.join(srcDir, `${config.node_name}.html`));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Update file contents
|
|
248
|
+
updateFiles(projectDir, config);
|
|
249
|
+
|
|
250
|
+
// Update package.json specifically
|
|
251
|
+
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
252
|
+
const projectPackageJson = require(packageJsonPath);
|
|
253
|
+
projectPackageJson.name = config.project_name;
|
|
254
|
+
projectPackageJson.author = config.author_name;
|
|
255
|
+
projectPackageJson.license = config.license;
|
|
256
|
+
|
|
257
|
+
// Ensure the node-red section points to the correct file
|
|
258
|
+
if (projectPackageJson['node-red'] && projectPackageJson['node-red'].nodes) {
|
|
259
|
+
// Reconstruct the nodes object with the new key and value
|
|
260
|
+
projectPackageJson['node-red'].nodes = {};
|
|
261
|
+
projectPackageJson['node-red'].nodes[config.node_name] = `src/${config.node_name}.js`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(projectPackageJson, null, 2));
|
|
265
|
+
|
|
266
|
+
console.log('\n📦 Installing dependencies...');
|
|
267
|
+
|
|
268
|
+
const installResult = spawn.sync('npm', ['install'], {
|
|
269
|
+
cwd: projectDir,
|
|
270
|
+
stdio: 'inherit'
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (installResult.status !== 0) {
|
|
274
|
+
console.error('\n❌ Error: npm install failed.');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log('\n✨ Success!');
|
|
279
|
+
console.log(`\nTo get started:\n`);
|
|
280
|
+
console.log(` cd ${projectName}`);
|
|
281
|
+
console.log(` npm run install-plugin # Installs this node to your local Node-RED`);
|
|
282
|
+
console.log(` npm run watch # Develop with auto-restart`);
|
|
283
|
+
console.log('\n🎨 Happy coding!');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
createProject();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pochade-node-red",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight generator for Node-RED nodes and plugins.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"pochade-node-red": "bin/create-pochade-js.js"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/lnsy-dev/pochade-node-red.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"node-red",
|
|
14
|
+
"template",
|
|
15
|
+
"starter",
|
|
16
|
+
"vanilla-js",
|
|
17
|
+
"rspack",
|
|
18
|
+
"dataroom-js",
|
|
19
|
+
"custom-elements"
|
|
20
|
+
],
|
|
21
|
+
"author": "LNSY",
|
|
22
|
+
"license": "Unlicense",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/lnsy-dev/pochade-node-red/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/lnsy-dev/pochade-node-red#readme",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"cross-spawn": "^7.0.6"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin/",
|
|
32
|
+
"template/"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Node-RED Plugin Implementation Guide for AI Agents
|
|
2
|
+
|
|
3
|
+
This document provides a comprehensive guide for Large Language Models (LLMs) on how to implement Node-RED plugins (nodes).
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
A Node-RED node module typically has the following structure:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
package.json
|
|
11
|
+
nodes/
|
|
12
|
+
<node-name>.js # Runtime behavior
|
|
13
|
+
<node-name>.html # Editor UI and configuration
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### package.json
|
|
17
|
+
|
|
18
|
+
The `package.json` must include a `node-red` section that maps node names to their JavaScript files.
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"name": "node-red-contrib-example",
|
|
23
|
+
"version": "1.0.0",
|
|
24
|
+
"node-red": {
|
|
25
|
+
"nodes": {
|
|
26
|
+
"example-node": "nodes/example-node.js"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## JavaScript File (.js)
|
|
33
|
+
|
|
34
|
+
The JavaScript file defines the runtime behavior of the node. It must export a function that registers the node with the Node-RED runtime.
|
|
35
|
+
|
|
36
|
+
### Template
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
module.exports = function(RED) {
|
|
40
|
+
function ExampleNode(config) {
|
|
41
|
+
RED.nodes.createNode(this, config);
|
|
42
|
+
var node = this;
|
|
43
|
+
|
|
44
|
+
// Retrieve configuration values
|
|
45
|
+
node.name = config.name;
|
|
46
|
+
|
|
47
|
+
// Handle incoming messages
|
|
48
|
+
node.on('input', function(msg, send, done) {
|
|
49
|
+
// Process the message
|
|
50
|
+
msg.payload = msg.payload.toLowerCase();
|
|
51
|
+
|
|
52
|
+
// Send the message
|
|
53
|
+
send(msg);
|
|
54
|
+
|
|
55
|
+
// Signal completion
|
|
56
|
+
if (done) {
|
|
57
|
+
done();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Clean up on close
|
|
62
|
+
node.on('close', function() {
|
|
63
|
+
// Cleanup logic
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Register the node type
|
|
68
|
+
RED.nodes.registerType("example-node", ExampleNode);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Key Concepts
|
|
73
|
+
|
|
74
|
+
- **RED.nodes.createNode(this, config)**: Initializes the node instance.
|
|
75
|
+
- **this.on('input', ...)**: Listener for incoming messages.
|
|
76
|
+
- **send(msg)**: Sends a message to the next node(s). Configurable to support multiple outputs.
|
|
77
|
+
- **done()**: Callback to signal that processing is complete (for pre-1.0 compatibility loops).
|
|
78
|
+
|
|
79
|
+
## HTML File (.html)
|
|
80
|
+
|
|
81
|
+
The HTML file defines the node's appearance in the Node-RED editor, its configuration dialog, and its help text. It contains three script blocks.
|
|
82
|
+
|
|
83
|
+
### Template
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<!-- Registration and Configuration -->
|
|
87
|
+
<script type="text/javascript">
|
|
88
|
+
RED.nodes.registerType('example-node', {
|
|
89
|
+
category: 'function',
|
|
90
|
+
color: '#a6bbcf',
|
|
91
|
+
defaults: {
|
|
92
|
+
name: { value: "" },
|
|
93
|
+
property: { value: "payload", required: true }
|
|
94
|
+
},
|
|
95
|
+
inputs: 1,
|
|
96
|
+
outputs: 1,
|
|
97
|
+
icon: "file.png",
|
|
98
|
+
label: function() {
|
|
99
|
+
return this.name || "example node";
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<!-- Edit Dialog -->
|
|
105
|
+
<script type="text/html" data-template-name="example-node">
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
108
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
109
|
+
</div>
|
|
110
|
+
<div class="form-row">
|
|
111
|
+
<label for="node-input-property"><i class="fa fa-edit"></i> Property</label>
|
|
112
|
+
<input type="text" id="node-input-property">
|
|
113
|
+
</div>
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<!-- Help Text -->
|
|
117
|
+
<script type="text/html" data-help-name="example-node">
|
|
118
|
+
<p>A simple node that converts payload to lowercase.</p>
|
|
119
|
+
</script>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Key Concepts
|
|
123
|
+
|
|
124
|
+
- **category**: The palette category (e.g., 'input', 'output', 'function', or custom).
|
|
125
|
+
- **defaults**: Defines the configuration properties that are saved and restored.
|
|
126
|
+
- **data-template-name**: Must match the registered type name.
|
|
127
|
+
- **data-help-name**: Must match the registered type name.
|
|
128
|
+
|
|
129
|
+
## Best Practices
|
|
130
|
+
|
|
131
|
+
1. **Error Handling**: Use `node.error("message", msg)` to log errors.
|
|
132
|
+
2. **Status**: Use `node.status({fill:"red",shape:"ring",text:"disconnected"})` to show status in the editor.
|
|
133
|
+
3. **Context**: Use `node.context()`, `flow.context()`, or `global.context()` to store state.
|
|
134
|
+
4. **Dependencies**: Declare any external libraries in `package.json`.
|
|
135
|
+
|
|
136
|
+
## Development Cycle
|
|
137
|
+
|
|
138
|
+
1. **Install**: `npm install` in your node directory.
|
|
139
|
+
2. **Link**: `npm link` (or `npm install . -g`) to make it available to Node-RED.
|
|
140
|
+
3. **Restart**: Restart Node-RED to load changes.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ${node_name}
|
|
2
|
+
|
|
3
|
+
${node_purpose}
|
|
4
|
+
|
|
5
|
+
A Node-RED node component.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
You can install this node directly from your Node-RED user directory (typically `~/.node-red`):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd ~/.node-red
|
|
13
|
+
npm install ${project_name}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or if you are developing locally, run `npm run install-plugin` from the project directory.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
After installing and restarting Node-RED, you will find the **${node_name}** node in the **${node_sidebar_title}** category.
|
|
21
|
+
|
|
22
|
+
Drag the node onto the canvas and configure it.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Watch Mode
|
|
26
|
+
|
|
27
|
+
If you are making frequent changes, you can use:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm run watch
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This will start node-red and on every text change will re-install the plugin to the ~/.node-red node_modules directory and restart node-red.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
${license}
|