├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── rollup.config.editor.js ├── src ├── __tests__ │ └── transform-text.test.ts └── nodes │ ├── shared │ └── types.ts │ └── transform-text │ ├── icons │ └── transform-text.png │ ├── modules │ └── types.ts │ ├── shared │ └── types.ts │ ├── transform-text.html │ ├── editor.html │ ├── help.html │ ├── index.ts │ └── modules │ │ └── types.ts │ └── transform-text.ts ├── tsconfig.json ├── tsconfig.runtime.json ├── tsconfig.runtime.watch.json ├── utils ├── add-node.js └── templates │ ├── blank-0 │ ├── modules │ │ └── types.ts.mustache │ ├── node-type.html │ │ ├── editor.html.mustache │ │ ├── help.html.mustache │ │ ├── index.ts.mustache │ │ └── modules │ │ │ └── types.ts.mustache │ ├── node-type.ts.mustache │ └── shared │ │ └── types.ts.mustache │ ├── blank │ ├── modules │ │ └── types.ts.mustache │ ├── node-type.html │ │ ├── editor.html.mustache │ │ ├── help.html.mustache │ │ ├── index.ts.mustache │ │ └── modules │ │ │ └── types.ts.mustache │ ├── node-type.ts.mustache │ └── shared │ │ └── types.ts.mustache │ └── config │ ├── modules │ └── types.ts.mustache │ ├── node-type.html │ ├── editor.html.mustache │ ├── help.html.mustache │ ├── index.ts.mustache │ └── modules │ │ └── types.ts.mustache │ ├── node-type.ts.mustache │ └── shared │ └── types.ts.mustache └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | /rollup.config.editor.js 4 | /utils -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | extends: [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "prettier/@typescript-eslint", 7 | ], 8 | plugins: ["jest", "@typescript-eslint"], 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | sourceType: "module", 12 | }, 13 | env: { 14 | node: true, 15 | jest: true, 16 | es6: true, 17 | }, 18 | rules: { 19 | "@typescript-eslint/no-unused-vars": [ 20 | "error", 21 | { 22 | argsIgnorePattern: "^_", 23 | varsIgnorePattern: "^_", 24 | args: "after-used", 25 | ignoreRestSiblings: true, 26 | }, 27 | ], 28 | "no-unused-expressions": [ 29 | "error", 30 | { 31 | allowTernary: true, 32 | }, 33 | ], 34 | "no-console": 0, 35 | "no-confusing-arrow": 0, 36 | "no-else-return": 0, 37 | "no-return-assign": [2, "except-parens"], 38 | "no-underscore-dangle": 0, 39 | "jest/no-focused-tests": 2, 40 | "jest/no-identical-title": 2, 41 | camelcase: 0, 42 | "prefer-arrow-callback": [ 43 | "error", 44 | { 45 | allowNamedFunctions: true, 46 | }, 47 | ], 48 | "class-methods-use-this": 0, 49 | "no-restricted-syntax": 0, 50 | "no-param-reassign": [ 51 | "error", 52 | { 53 | props: false, 54 | }, 55 | ], 56 | 57 | "import/no-extraneous-dependencies": 0, 58 | 59 | "arrow-body-style": 0, 60 | "no-nested-ternary": 0, 61 | }, 62 | overrides: [ 63 | { 64 | files: ["src/**/*.d.ts"], 65 | rules: { 66 | "@typescript-eslint/triple-slash-reference": 0, 67 | }, 68 | }, 69 | ], 70 | }; 71 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [10.x, 12.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v2.3.4 15 | - name: Setup Node.js ${{ matrix.node-version }} environment 16 | uses: actions/setup-node@v2.1.2 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: yarn, lint, build and test 21 | run: | 22 | yarn 23 | yarn lint 24 | yarn build 25 | yarn test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | yarn-debug.log* 4 | yarn-error.log* 5 | node_modules/ 6 | *.tsbuildinfo 7 | dist 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/**/* 3 | !LICENSE 4 | !README.md 5 | !package.json 6 | !yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alex Kaul 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-RED Node TypeScript Starter 2 | 3 | This is a quick-start template repository for creating new Node-RED node sets in TypeScript. 4 | 5 | ## Project Structure 6 | 7 | ``` 8 | node-red-node-typescript-starter/ 9 | ├──src/ * source files of the node set 10 | │ ├──__tests__/ * tests for the node set (test file names should match *.test.ts glob pattern) 11 | │ │ └──transform-text.test.ts * tests for the transform-text node 12 | │ │ 13 | │ └──nodes/ * node set folder, where subfolder names = node types 14 | │ ├──shared/ * folder for .ts files shared across multiple nodes in the node set 15 | │ │ 16 | │ └──transform-text/ * source files of the transform-text node 17 | │ ├──icons/ * custom icons used by the node set in the editor 18 | │ │ 19 | │ ├──modules/ * .ts modules for the runtime side (transform-text.js file) of the node 20 | │ │ 21 | │ ├──shared/ * folder for .ts files shared between the runtime side (.js file) and the editor side (.html file) of the node 22 | │ │ 23 | │ ├──transform-text.html/ * files for compiling and bundling into the editor side (transform-text.html file) of the node 24 | │ │ ├──modules/ * .ts modules 25 | │ │ ├──editor.html * html template for the edit dialog 26 | │ │ ├──help.html * html template for the help in the info tab 27 | │ │ └──index.ts * entry file 28 | │ │ 29 | | └──transform-text.ts * entry file for the runtime side (transform-text.js file) of the node 30 | | 31 | ├──package.json * dependencies and node types for the Node-RED runtime to load 32 | | 33 | ├──rollup.config.editor.json * rollup config for building the editor side of the nodes 34 | | 35 | ├──tsconfig.json * base typescript config, for the code editor 36 | ├──tsconfig.runtime.json * config for creating a production build of the runtime side of the nodes 37 | └──tsconfig.runtime.watch.json * config for watching and incremental building the runtime side of the nodes 38 | ``` 39 | 40 | ## Getting Started 41 | 42 | 1. Generate a new GitHub repository by clicking the `Use this template` button at the top of the repository homepage, then clone your new repo. Or you might just clone this repo: `git clone https://github.com/alexk111/node-red-node-typescript-starter.git` and cd into it: `cd node-red-node-typescript-starter`. 43 | 2. This project is designed to work with `yarn`. If you don't have `yarn` installed, you can install it with `npm install -g yarn`. 44 | 3. Install dependencies: `yarn install`. 45 | 46 | ## Adding Nodes 47 | 48 | You can quickly scaffold a new node and add it to the node set. Use the following command to create `my-new-node-type` node: 49 | 50 | ``` 51 | yarn add-node my-new-node-type 52 | ``` 53 | 54 | The node generator is based on mustache templates. At the moment there are three templates available: 55 | 56 | - `blank` (used by default) - basic node for Node-RED >=1.0 57 | - `blank-0` - node with a backward compatibility for running on Node-RED <1.0 58 | - `config` - configuration node 59 | 60 | To generate a node using a template, specify it as the third argument: 61 | 62 | ``` 63 | yarn add-node my-new-node-type blank 64 | ``` 65 | 66 | or 67 | 68 | ``` 69 | yarn add-node my-new-node-config config 70 | ``` 71 | 72 | ### Adding Node Templates 73 | 74 | If you want to make your own template available, add it to `./utils/templates/`. 75 | 76 | ## Developing Nodes 77 | 78 | Build & Test in Watch mode: 79 | 80 | ``` 81 | yarn dev 82 | ``` 83 | 84 | ## Building Node Set 85 | 86 | Create a production build: 87 | 88 | ``` 89 | yarn build 90 | ``` 91 | 92 | ## Backers 💝 93 | 94 | [[Become a backer](https://mynode.alexkaul.com/gh-donate)] 95 | 96 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/0/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/0/profile) 97 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/1/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/1/profile) 98 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/2/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/2/profile) 99 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/3/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/3/profile) 100 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/4/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/4/profile) 101 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/5/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/5/profile) 102 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/6/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/6/profile) 103 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/7/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/7/profile) 104 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/8/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/8/profile) 105 | [![Backer](https://mynode.alexkaul.com/gh-backer/top/9/avatar/60)](https://mynode.alexkaul.com/gh-backer/top/9/profile) 106 | 107 | ## Testing Node Set in Node-RED 108 | 109 | [Read Node-RED docs](https://nodered.org/docs/creating-nodes/first-node#testing-your-node-in-node-red) on how to install the node set into your Node-RED runtime. 110 | 111 | ## License 112 | 113 | MIT © Alex Kaul 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-MY_NODESET_NAME", 3 | "version": "0.2.3", 4 | "description": "Description of the node set here", 5 | "scripts": { 6 | "add-node": "node ./utils/add-node.js", 7 | "copy": "copyfiles -u 2 \"./src/nodes/**/*.{png,svg}\" \"./dist/nodes/\"", 8 | "build:editor": "rollup -c rollup.config.editor.js", 9 | "build:editor:watch": "rollup -c rollup.config.editor.js -w", 10 | "build:runtime": "tsc -p tsconfig.runtime.json", 11 | "build:runtime:watch": "tsc -p tsconfig.runtime.watch.json --watch --preserveWatchOutput", 12 | "build": "rm -rf dist && yarn copy && yarn build:editor && yarn build:runtime", 13 | "test": "jest --forceExit --detectOpenHandles --colors", 14 | "test:watch": "jest --forceExit --detectOpenHandles --watchAll", 15 | "dev": "rm -rf dist && yarn copy && concurrently --kill-others --names 'COPY,EDITOR,RUNTIME,TEST' --prefix '({name})' --prefix-colors 'yellow.bold,cyan.bold,greenBright.bold,magenta.bold' 'onchange -v \"src/**/*.png\" \"src/**/*.svg\" -- yarn copy' 'yarn build:editor:watch' 'yarn build:runtime:watch' 'sleep 10; yarn test:watch'", 16 | "lint": "prettier --ignore-path .eslintignore --check '**/*.{js,ts,md}'; eslint --ext .js,.ts .", 17 | "lint:fix": "prettier --ignore-path .eslintignore --write '**/*.{js,ts,md}'; eslint --ext .js,.ts . --fix" 18 | }, 19 | "author": "Alex Kaul", 20 | "license": "MIT", 21 | "node-red": { 22 | "nodes": { 23 | "transform-text": "./dist/nodes/transform-text/transform-text.js" 24 | } 25 | }, 26 | "dependencies": {}, 27 | "devDependencies": { 28 | "@rollup/plugin-typescript": "^8.0.0", 29 | "@types/express": "^4.17.9", 30 | "@types/jest": "^26.0.15", 31 | "@types/node": "^14.14.10", 32 | "@types/node-red": "^1.1.1", 33 | "@types/node-red-node-test-helper": "^0.2.1", 34 | "@types/sinon": "^9.0.9", 35 | "@types/supertest": "^2.0.10", 36 | "@typescript-eslint/eslint-plugin": "^4.9.0", 37 | "@typescript-eslint/parser": "^4.9.0", 38 | "colorette": "^1.2.1", 39 | "concurrently": "^5.3.0", 40 | "copyfiles": "^2.4.1", 41 | "eslint": "^7.14.0", 42 | "eslint-config-prettier": "^7.1.0", 43 | "eslint-plugin-jest": "^24.1.3", 44 | "eslint-plugin-prettier": "^3.1.4", 45 | "glob": "^7.1.6", 46 | "jest": "^26.6.3", 47 | "mustache": "^4.0.1", 48 | "node-red": "^1.2.6", 49 | "node-red-node-test-helper": "^0.2.5", 50 | "onchange": "^7.0.2", 51 | "prettier": "^2.2.1", 52 | "rollup": "^2.23.0", 53 | "ts-jest": "^26.4.4", 54 | "typescript": "^4.1.2" 55 | }, 56 | "jest": { 57 | "testEnvironment": "node", 58 | "roots": [ 59 | "/src" 60 | ], 61 | "transform": { 62 | "^.+\\.ts$": "ts-jest" 63 | }, 64 | "testMatch": [ 65 | "**/__tests__/**/*.test.ts" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rollup.config.editor.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import glob from "glob"; 3 | import path from "path"; 4 | import typescript from "@rollup/plugin-typescript"; 5 | 6 | import packageJson from "./package.json"; 7 | 8 | const allNodeTypes = Object.keys(packageJson["node-red"].nodes); 9 | 10 | const htmlWatch = () => { 11 | return { 12 | name: "htmlWatch", 13 | load(id) { 14 | const editorDir = path.dirname(id); 15 | const htmlFiles = glob.sync(path.join(editorDir, "*.html")); 16 | htmlFiles.map((file) => this.addWatchFile(file)); 17 | }, 18 | }; 19 | }; 20 | 21 | const htmlBundle = () => { 22 | return { 23 | name: "htmlBundle", 24 | renderChunk(code, chunk, _options) { 25 | const editorDir = path.dirname(chunk.facadeModuleId); 26 | const htmlFiles = glob.sync(path.join(editorDir, "*.html")); 27 | const htmlContents = htmlFiles.map((fPath) => fs.readFileSync(fPath)); 28 | 29 | code = 30 | '\n" + 34 | htmlContents.join("\n"); 35 | 36 | return { 37 | code, 38 | map: { mappings: "" }, 39 | }; 40 | }, 41 | }; 42 | }; 43 | 44 | const makePlugins = (nodeType) => [ 45 | htmlWatch(), 46 | typescript({ 47 | lib: ["es5", "es6", "dom"], 48 | include: [ 49 | `src/nodes/${nodeType}/${nodeType}.html/**/*.ts`, 50 | `src/nodes/${nodeType}/shared/**/*.ts`, 51 | "src/nodes/shared/**/*.ts", 52 | ], 53 | target: "es5", 54 | tsconfig: false, 55 | noEmitOnError: process.env.ROLLUP_WATCH ? false : true, 56 | }), 57 | htmlBundle(), 58 | ]; 59 | 60 | const makeConfigItem = (nodeType) => ({ 61 | input: `src/nodes/${nodeType}/${nodeType}.html/index.ts`, 62 | output: { 63 | file: `dist/nodes/${nodeType}/${nodeType}.html`, 64 | format: "iife", 65 | }, 66 | plugins: makePlugins(nodeType), 67 | watch: { 68 | clearScreen: false, 69 | }, 70 | }); 71 | 72 | export default allNodeTypes.map((nodeType) => makeConfigItem(nodeType)); 73 | -------------------------------------------------------------------------------- /src/__tests__/transform-text.test.ts: -------------------------------------------------------------------------------- 1 | import testHelper, { TestFlowsItem } from "node-red-node-test-helper"; 2 | import transformTextNode from "../nodes/transform-text/transform-text"; 3 | import { TransformTextNodeDef } from "../nodes/transform-text/modules/types"; 4 | import { TransformTextOperation } from "../nodes/transform-text/shared/types"; 5 | 6 | type FlowsItem = TestFlowsItem; 7 | type Flows = Array; 8 | 9 | describe("transform-text node", () => { 10 | beforeEach((done) => { 11 | testHelper.startServer(done); 12 | }); 13 | 14 | afterEach((done) => { 15 | testHelper.unload().then(() => { 16 | testHelper.stopServer(done); 17 | }); 18 | }); 19 | 20 | it("should be loaded", (done) => { 21 | const flows: Flows = [ 22 | { id: "n1", type: "transform-text", name: "transform-text" }, 23 | ]; 24 | testHelper.load(transformTextNode, flows, () => { 25 | const n1 = testHelper.getNode("n1"); 26 | expect(n1).toBeTruthy(); 27 | expect(n1.name).toEqual("transform-text"); 28 | done(); 29 | }); 30 | }); 31 | 32 | describe("in upper-case mode", () => { 33 | let flows: Flows; 34 | beforeEach(() => { 35 | flows = [ 36 | { 37 | id: "n1", 38 | type: "transform-text", 39 | name: "transform-text", 40 | operation: TransformTextOperation.UpperCase, 41 | wires: [["n2"]], 42 | }, 43 | { id: "n2", type: "helper" }, 44 | ]; 45 | }); 46 | it("should make payload upper case, if it's a string", (done) => { 47 | testHelper.load(transformTextNode, flows, () => { 48 | const n2 = testHelper.getNode("n2"); 49 | const n1 = testHelper.getNode("n1"); 50 | n2.on("input", (msg: unknown) => { 51 | expect(msg).toBeTruthy(); 52 | expect(msg).toMatchObject({ payload: "UPPERCASE" }); 53 | done(); 54 | }); 55 | n1.receive({ payload: "UpperCase" }); 56 | }); 57 | }); 58 | 59 | it("should just pass a message, if payload is not a string", (done) => { 60 | testHelper.load(transformTextNode, flows, () => { 61 | const n2 = testHelper.getNode("n2"); 62 | const n1 = testHelper.getNode("n1"); 63 | n2.on("input", (msg: unknown) => { 64 | expect(msg).toBeTruthy(); 65 | expect(msg).toMatchObject({ payload: { str: "UpperCase" } }); 66 | done(); 67 | }); 68 | n1.receive({ payload: { str: "UpperCase" } }); 69 | }); 70 | }); 71 | }); 72 | describe("in lower-case mode", () => { 73 | let flows: Flows; 74 | beforeEach(() => { 75 | flows = [ 76 | { 77 | id: "n1", 78 | type: "transform-text", 79 | name: "transform-text", 80 | operation: TransformTextOperation.LowerCase, 81 | wires: [["n2"]], 82 | }, 83 | { id: "n2", type: "helper" }, 84 | ]; 85 | }); 86 | it("should make payload lower case, if it's a string", (done) => { 87 | testHelper.load(transformTextNode, flows, () => { 88 | const n2 = testHelper.getNode("n2"); 89 | const n1 = testHelper.getNode("n1"); 90 | n2.on("input", (msg: unknown) => { 91 | expect(msg).toBeTruthy(); 92 | expect(msg).toMatchObject({ payload: "lowercase" }); 93 | done(); 94 | }); 95 | n1.receive({ payload: "LowerCase" }); 96 | }); 97 | }); 98 | 99 | it("should just pass a message, if payload is not a string", (done) => { 100 | testHelper.load(transformTextNode, flows, () => { 101 | const n2 = testHelper.getNode("n2"); 102 | const n1 = testHelper.getNode("n1"); 103 | n2.on("input", (msg: unknown) => { 104 | expect(msg).toBeTruthy(); 105 | expect(msg).toMatchObject({ payload: { str: "LowerCase" } }); 106 | done(); 107 | }); 108 | n1.receive({ payload: { str: "LowerCase" } }); 109 | }); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/nodes/shared/types.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk111/node-red-node-typescript-starter/a33a3f9ae34aa4868d6cb4cf4b3957d2448a55cc/src/nodes/shared/types.ts -------------------------------------------------------------------------------- /src/nodes/transform-text/icons/transform-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexk111/node-red-node-typescript-starter/a33a3f9ae34aa4868d6cb4cf4b3957d2448a55cc/src/nodes/transform-text/icons/transform-text.png -------------------------------------------------------------------------------- /src/nodes/transform-text/modules/types.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef } from "node-red"; 2 | import { TransformTextOptions } from "../shared/types"; 3 | 4 | export interface TransformTextNodeDef extends NodeDef, TransformTextOptions {} 5 | export type TransformTextNode = Node; 6 | -------------------------------------------------------------------------------- /src/nodes/transform-text/shared/types.ts: -------------------------------------------------------------------------------- 1 | export enum TransformTextOperation { 2 | UpperCase = "upper", 3 | LowerCase = "lower", 4 | } 5 | 6 | export interface TransformTextOptions { 7 | operation: TransformTextOperation; 8 | } 9 | -------------------------------------------------------------------------------- /src/nodes/transform-text/transform-text.html/editor.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/nodes/transform-text/transform-text.html/help.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/nodes/transform-text/transform-text.html/index.ts: -------------------------------------------------------------------------------- 1 | import { EditorRED } from "node-red"; 2 | import { TransformTextEditorNodeProperties } from "./modules/types"; 3 | import { TransformTextOperation } from "../shared/types"; 4 | 5 | declare const RED: EditorRED; 6 | 7 | RED.nodes.registerType("transform-text", { 8 | category: "function", 9 | color: "#a6bbcf", 10 | defaults: { 11 | operation: { value: TransformTextOperation.UpperCase }, 12 | name: { value: "" }, 13 | }, 14 | inputs: 1, 15 | outputs: 1, 16 | icon: "transform-text.png", 17 | paletteLabel: "transform text", 18 | label: function () { 19 | if (this.name) { 20 | return this.name; 21 | } 22 | switch (this.operation) { 23 | case TransformTextOperation.UpperCase: { 24 | return "to upper case"; 25 | } 26 | case TransformTextOperation.LowerCase: { 27 | return "to lower case"; 28 | } 29 | } 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /src/nodes/transform-text/transform-text.html/modules/types.ts: -------------------------------------------------------------------------------- 1 | import { EditorNodeProperties } from "node-red"; 2 | import { TransformTextOptions } from "../../shared/types"; 3 | 4 | export interface TransformTextEditorNodeProperties 5 | extends EditorNodeProperties, 6 | TransformTextOptions {} 7 | -------------------------------------------------------------------------------- /src/nodes/transform-text/transform-text.ts: -------------------------------------------------------------------------------- 1 | import { NodeInitializer } from "node-red"; 2 | import { TransformTextNode, TransformTextNodeDef } from "./modules/types"; 3 | import { TransformTextOperation } from "./shared/types"; 4 | 5 | const nodeInit: NodeInitializer = (RED): void => { 6 | function TransformTextNodeConstructor( 7 | this: TransformTextNode, 8 | config: TransformTextNodeDef 9 | ): void { 10 | RED.nodes.createNode(this, config); 11 | 12 | switch (config.operation) { 13 | case TransformTextOperation.UpperCase: { 14 | this.on("input", (msg, send, done) => { 15 | if (typeof msg.payload === "string") { 16 | msg.payload = msg.payload.toUpperCase(); 17 | } 18 | send(msg); 19 | done(); 20 | }); 21 | break; 22 | } 23 | case TransformTextOperation.LowerCase: { 24 | this.on("input", (msg, send, done) => { 25 | if (typeof msg.payload === "string") { 26 | msg.payload = msg.payload.toLowerCase(); 27 | } 28 | send(msg); 29 | done(); 30 | }); 31 | break; 32 | } 33 | } 34 | } 35 | 36 | RED.nodes.registerType("transform-text", TransformTextNodeConstructor); 37 | }; 38 | 39 | export = nodeInit; 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018", "esnext.asynciterable", "dom"], 6 | "allowJs": false, 7 | "noEmitOnError": true, 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "rootDir": "src", 11 | "removeComments": false, 12 | "noEmit": false, 13 | "importHelpers": false, 14 | "preserveWatchOutput": true, 15 | 16 | "strict": true, 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noFallthroughCasesInSwitch": true, 23 | 24 | "moduleResolution": "node", 25 | "baseUrl": "./", 26 | "allowSyntheticDefaultImports": true, 27 | "esModuleInterop": true, 28 | "preserveConstEnums": true, 29 | "forceConsistentCasingInFileNames": true, 30 | 31 | "typeRoots": ["./src/types", "./node_modules/@types"] 32 | }, 33 | "include": ["src"], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.runtime.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules", "src/__tests__", "src/nodes/*/*.html"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.runtime.watch.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.runtime.json", 3 | "compilerOptions": { 4 | "incremental": true, 5 | "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /utils/add-node.js: -------------------------------------------------------------------------------- 1 | const { red, green, bold } = require("colorette"); 2 | const mustache = require("mustache"); 3 | const fs = require("fs"); 4 | const { COPYFILE_EXCL } = fs.constants; 5 | const { 6 | readdir, 7 | mkdir, 8 | copyFile, 9 | readFile, 10 | writeFile, 11 | } = require("fs").promises; 12 | const path = require("path"); 13 | 14 | // get args 15 | if (!process.argv[2]) { 16 | console.log(red(`Node type not specified`)); 17 | return; 18 | } 19 | const nodeTypeInKebabCase = process.argv[2].toLowerCase(); 20 | const nodeTemplate = process.argv[3] || "blank"; 21 | 22 | // convert node-type to all cases 23 | const nodeTypeSegs = nodeTypeInKebabCase.split("-"); 24 | const nodeTypeInSnakeCase = nodeTypeSegs.join("_"); 25 | const nodeTypeInPascalCase = nodeTypeSegs 26 | .map((val) => val.charAt(0).toUpperCase() + val.substring(1)) 27 | .join(""); 28 | const nodeTypeInCamelCase = nodeTypeSegs 29 | .map((val, idx) => 30 | idx === 0 ? val : val.charAt(0).toUpperCase() + val.substring(1) 31 | ) 32 | .join(""); 33 | const nodeLabel = nodeTypeSegs.join(" "); 34 | 35 | const mustacheData = { 36 | NodeTypeKebabCase: nodeTypeInKebabCase, 37 | NodeTypeSnakeCase: nodeTypeInSnakeCase, 38 | NodeTypePascalCase: nodeTypeInPascalCase, 39 | NodeTypeCamelCase: nodeTypeInCamelCase, 40 | NodeLabel: nodeLabel, 41 | }; 42 | 43 | // node files generator 44 | async function generateFiles(fromDir, toDir) { 45 | // always called for a nonexistent toDir 46 | await mkdir(toDir); 47 | console.log(green(`Created directory: ${bold(toDir)}`)); 48 | 49 | const fromDirents = await readdir(fromDir, { withFileTypes: true }); 50 | await Promise.all( 51 | fromDirents.map(async (fromDirent) => { 52 | const fromFilePath = path.join(fromDir, fromDirent.name); 53 | let toFilePath = path.join( 54 | toDir, 55 | fromDirent.name.replace("node-type", nodeTypeInKebabCase) 56 | ); 57 | 58 | if (fromDirent.isDirectory()) { 59 | // directory -> recursive call 60 | await generateFiles(fromFilePath, toFilePath); 61 | } else { 62 | // file -> generate 63 | const ext = path.extname(toFilePath); 64 | if (ext !== ".mustache") { 65 | // just copy as-is 66 | await copyFile(fromFilePath, toFilePath, COPYFILE_EXCL); 67 | console.log(green(`Copied file: ${bold(toFilePath)}`)); 68 | } else { 69 | // generate from mustache template 70 | toFilePath = path.join( 71 | path.dirname(toFilePath), 72 | path.basename(toFilePath, ext) 73 | ); 74 | const tpl = await readFile(fromFilePath, "utf8"); 75 | const renderedStr = mustache.render(tpl, mustacheData, {}, [ 76 | "<%", 77 | "%>", 78 | ]); 79 | await writeFile(toFilePath, renderedStr, "utf8"); 80 | console.log(green(`Generated file: ${bold(toFilePath)}`)); 81 | } 82 | } 83 | }) 84 | ); 85 | } 86 | 87 | async function addNodeToPackageJson() { 88 | const pkgJsonPath = path.join(__dirname, "..", "package.json"); 89 | const pkgJsonData = JSON.parse(await readFile(pkgJsonPath, "utf8")); 90 | pkgJsonData["node-red"].nodes[ 91 | nodeTypeInKebabCase 92 | ] = `./dist/nodes/${nodeTypeInKebabCase}/${nodeTypeInKebabCase}.js`; 93 | await writeFile(pkgJsonPath, JSON.stringify(pkgJsonData, null, 2), "utf8"); 94 | console.log(green(`Added ${bold(nodeTypeInKebabCase)} to package.json`)); 95 | } 96 | 97 | async function main() { 98 | // paths 99 | const templateDir = path.join(__dirname, "templates", nodeTemplate); 100 | const newNodeDir = path.join( 101 | __dirname, 102 | "..", 103 | "src", 104 | "nodes", 105 | nodeTypeInKebabCase 106 | ); 107 | 108 | // check if paths ok 109 | if (!fs.existsSync(templateDir)) { 110 | console.log(red(`Template ${bold(nodeTemplate)} does not exist`)); 111 | return; 112 | } 113 | if (fs.existsSync(newNodeDir)) { 114 | console.log(red(`Node ${bold(nodeTypeInKebabCase)} already exists`)); 115 | return; 116 | } 117 | 118 | // we can do that now 119 | console.log( 120 | green( 121 | `Generating ${bold(nodeTypeInKebabCase)} node using ${bold( 122 | nodeTemplate 123 | )} template` 124 | ) 125 | ); 126 | 127 | try { 128 | await generateFiles(templateDir, newNodeDir); 129 | await addNodeToPackageJson(); 130 | } catch (e) { 131 | console.log(red(`Error: ${bold(e)}`)); 132 | return; 133 | } 134 | } 135 | 136 | main(); 137 | -------------------------------------------------------------------------------- /utils/templates/blank-0/modules/types.ts.mustache: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef } from "node-red"; 2 | import { <%NodeTypePascalCase%>Options } from "../shared/types"; 3 | 4 | export interface <%NodeTypePascalCase%>NodeDef extends NodeDef, <%NodeTypePascalCase%>Options {} 5 | 6 | // export interface <%NodeTypePascalCase%>Node extends Node {} 7 | export type <%NodeTypePascalCase%>Node = Node; 8 | -------------------------------------------------------------------------------- /utils/templates/blank-0/node-type.html/editor.html.mustache: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /utils/templates/blank-0/node-type.html/help.html.mustache: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /utils/templates/blank-0/node-type.html/index.ts.mustache: -------------------------------------------------------------------------------- 1 | import { EditorRED } from "node-red"; 2 | import { <%NodeTypePascalCase%>EditorNodeProperties } from "./modules/types"; 3 | 4 | declare const RED: EditorRED; 5 | 6 | RED.nodes.registerType<<%NodeTypePascalCase%>EditorNodeProperties>("<%NodeTypeKebabCase%>", { 7 | category: "function", 8 | color: "#a6bbcf", 9 | defaults: { 10 | name: { value: "" }, 11 | }, 12 | inputs: 1, 13 | outputs: 1, 14 | icon: "file.png", 15 | paletteLabel: "<%NodeLabel%>", 16 | label: function () { 17 | return this.name || "<%NodeLabel%>"; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /utils/templates/blank-0/node-type.html/modules/types.ts.mustache: -------------------------------------------------------------------------------- 1 | import { EditorNodeProperties } from "node-red"; 2 | import { <%NodeTypePascalCase%>Options } from "../../shared/types"; 3 | 4 | export interface <%NodeTypePascalCase%>EditorNodeProperties 5 | extends EditorNodeProperties, 6 | <%NodeTypePascalCase%>Options {} 7 | -------------------------------------------------------------------------------- /utils/templates/blank-0/node-type.ts.mustache: -------------------------------------------------------------------------------- 1 | import { NodeInitializer } from "node-red"; 2 | import { <%NodeTypePascalCase%>Node, <%NodeTypePascalCase%>NodeDef } from "./modules/types"; 3 | 4 | const nodeInit: NodeInitializer = (RED): void => { 5 | function <%NodeTypePascalCase%>NodeConstructor( 6 | this: <%NodeTypePascalCase%>Node, 7 | config: <%NodeTypePascalCase%>NodeDef 8 | ): void { 9 | RED.nodes.createNode(this, config); 10 | 11 | this.on("input", (msg, send, done) => { 12 | // Node-RED <1.0: this.send(msg); 13 | // Node-RED >=1.0: send(msg); done(); 14 | // For more info: https://nodered.org/blog/2019/09/20/node-done 15 | const sendAnyNR = send || this.send; 16 | sendAnyNR(msg); 17 | if (done) { 18 | done(); 19 | } 20 | }); 21 | } 22 | 23 | RED.nodes.registerType("<%NodeTypeKebabCase%>", <%NodeTypePascalCase%>NodeConstructor); 24 | }; 25 | 26 | export = nodeInit; 27 | -------------------------------------------------------------------------------- /utils/templates/blank-0/shared/types.ts.mustache: -------------------------------------------------------------------------------- 1 | export interface <%NodeTypePascalCase%>Options { 2 | // node options 3 | } 4 | -------------------------------------------------------------------------------- /utils/templates/blank/modules/types.ts.mustache: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef } from "node-red"; 2 | import { <%NodeTypePascalCase%>Options } from "../shared/types"; 3 | 4 | export interface <%NodeTypePascalCase%>NodeDef extends NodeDef, <%NodeTypePascalCase%>Options {} 5 | 6 | // export interface <%NodeTypePascalCase%>Node extends Node {} 7 | export type <%NodeTypePascalCase%>Node = Node; 8 | -------------------------------------------------------------------------------- /utils/templates/blank/node-type.html/editor.html.mustache: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /utils/templates/blank/node-type.html/help.html.mustache: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /utils/templates/blank/node-type.html/index.ts.mustache: -------------------------------------------------------------------------------- 1 | import { EditorRED } from "node-red"; 2 | import { <%NodeTypePascalCase%>EditorNodeProperties } from "./modules/types"; 3 | 4 | declare const RED: EditorRED; 5 | 6 | RED.nodes.registerType<<%NodeTypePascalCase%>EditorNodeProperties>("<%NodeTypeKebabCase%>", { 7 | category: "function", 8 | color: "#a6bbcf", 9 | defaults: { 10 | name: { value: "" }, 11 | }, 12 | inputs: 1, 13 | outputs: 1, 14 | icon: "file.png", 15 | paletteLabel: "<%NodeLabel%>", 16 | label: function () { 17 | return this.name || "<%NodeLabel%>"; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /utils/templates/blank/node-type.html/modules/types.ts.mustache: -------------------------------------------------------------------------------- 1 | import { EditorNodeProperties } from "node-red"; 2 | import { <%NodeTypePascalCase%>Options } from "../../shared/types"; 3 | 4 | export interface <%NodeTypePascalCase%>EditorNodeProperties 5 | extends EditorNodeProperties, 6 | <%NodeTypePascalCase%>Options {} 7 | -------------------------------------------------------------------------------- /utils/templates/blank/node-type.ts.mustache: -------------------------------------------------------------------------------- 1 | import { NodeInitializer } from "node-red"; 2 | import { <%NodeTypePascalCase%>Node, <%NodeTypePascalCase%>NodeDef } from "./modules/types"; 3 | 4 | const nodeInit: NodeInitializer = (RED): void => { 5 | function <%NodeTypePascalCase%>NodeConstructor( 6 | this: <%NodeTypePascalCase%>Node, 7 | config: <%NodeTypePascalCase%>NodeDef 8 | ): void { 9 | RED.nodes.createNode(this, config); 10 | 11 | this.on("input", (msg, send, done) => { 12 | send(msg); 13 | done(); 14 | }); 15 | } 16 | 17 | RED.nodes.registerType("<%NodeTypeKebabCase%>", <%NodeTypePascalCase%>NodeConstructor); 18 | }; 19 | 20 | export = nodeInit; 21 | -------------------------------------------------------------------------------- /utils/templates/blank/shared/types.ts.mustache: -------------------------------------------------------------------------------- 1 | export interface <%NodeTypePascalCase%>Options { 2 | // node options 3 | } 4 | -------------------------------------------------------------------------------- /utils/templates/config/modules/types.ts.mustache: -------------------------------------------------------------------------------- 1 | import { Node, NodeDef } from "node-red"; 2 | import { <%NodeTypePascalCase%>Options } from "../shared/types"; 3 | 4 | export interface <%NodeTypePascalCase%>NodeDef extends NodeDef, <%NodeTypePascalCase%>Options {} 5 | 6 | // export interface <%NodeTypePascalCase%>Node extends Node {} 7 | export type <%NodeTypePascalCase%>Node = Node; 8 | -------------------------------------------------------------------------------- /utils/templates/config/node-type.html/editor.html.mustache: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /utils/templates/config/node-type.html/help.html.mustache: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /utils/templates/config/node-type.html/index.ts.mustache: -------------------------------------------------------------------------------- 1 | import { EditorRED } from "node-red"; 2 | import { <%NodeTypePascalCase%>EditorNodeProperties } from "./modules/types"; 3 | 4 | declare const RED: EditorRED; 5 | 6 | RED.nodes.registerType<<%NodeTypePascalCase%>EditorNodeProperties>("<%NodeTypeKebabCase%>", { 7 | category: "config", 8 | defaults: { 9 | name: { value: "" }, 10 | }, 11 | label: function () { 12 | return this.name || "<%NodeLabel%>"; 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /utils/templates/config/node-type.html/modules/types.ts.mustache: -------------------------------------------------------------------------------- 1 | import { EditorNodeProperties } from "node-red"; 2 | import { <%NodeTypePascalCase%>Options } from "../../shared/types"; 3 | 4 | export interface <%NodeTypePascalCase%>EditorNodeProperties 5 | extends EditorNodeProperties, 6 | <%NodeTypePascalCase%>Options {} 7 | -------------------------------------------------------------------------------- /utils/templates/config/node-type.ts.mustache: -------------------------------------------------------------------------------- 1 | import { NodeInitializer } from "node-red"; 2 | import { <%NodeTypePascalCase%>Node, <%NodeTypePascalCase%>NodeDef } from "./modules/types"; 3 | 4 | const nodeInit: NodeInitializer = (RED): void => { 5 | function <%NodeTypePascalCase%>NodeConstructor( 6 | this: <%NodeTypePascalCase%>Node, 7 | config: <%NodeTypePascalCase%>NodeDef 8 | ): void { 9 | RED.nodes.createNode(this, config); 10 | } 11 | 12 | RED.nodes.registerType("<%NodeTypeKebabCase%>", <%NodeTypePascalCase%>NodeConstructor); 13 | }; 14 | 15 | export = nodeInit; 16 | -------------------------------------------------------------------------------- /utils/templates/config/shared/types.ts.mustache: -------------------------------------------------------------------------------- 1 | export interface <%NodeTypePascalCase%>Options { 2 | // node options 3 | } 4 | --------------------------------------------------------------------------------