├── src
├── nodes
│ ├── shared
│ │ └── types.ts
│ └── transform-text
│ │ ├── icons
│ │ └── transform-text.png
│ │ ├── transform-text.html
│ │ ├── help.html
│ │ ├── modules
│ │ │ └── types.ts
│ │ ├── editor.html
│ │ └── index.ts
│ │ ├── shared
│ │ └── types.ts
│ │ ├── modules
│ │ └── types.ts
│ │ └── transform-text.ts
└── __tests__
│ └── transform-text.test.ts
├── .eslintignore
├── .npmignore
├── .gitignore
├── utils
├── templates
│ ├── blank
│ │ ├── shared
│ │ │ └── types.ts.mustache
│ │ ├── node-type.html
│ │ │ ├── help.html.mustache
│ │ │ ├── editor.html.mustache
│ │ │ ├── modules
│ │ │ │ └── types.ts.mustache
│ │ │ └── index.ts.mustache
│ │ ├── modules
│ │ │ └── types.ts.mustache
│ │ └── node-type.ts.mustache
│ ├── config
│ │ ├── shared
│ │ │ └── types.ts.mustache
│ │ ├── node-type.html
│ │ │ ├── help.html.mustache
│ │ │ ├── editor.html.mustache
│ │ │ ├── modules
│ │ │ │ └── types.ts.mustache
│ │ │ └── index.ts.mustache
│ │ ├── modules
│ │ │ └── types.ts.mustache
│ │ └── node-type.ts.mustache
│ └── blank-0
│ │ ├── shared
│ │ └── types.ts.mustache
│ │ ├── node-type.html
│ │ ├── help.html.mustache
│ │ ├── editor.html.mustache
│ │ ├── modules
│ │ │ └── types.ts.mustache
│ │ └── index.ts.mustache
│ │ ├── modules
│ │ └── types.ts.mustache
│ │ └── node-type.ts.mustache
└── add-node.js
├── tsconfig.runtime.json
├── tsconfig.runtime.watch.json
├── .github
└── workflows
│ └── ci.yml
├── tsconfig.json
├── LICENSE
├── .eslintrc.js
├── rollup.config.editor.js
├── package.json
└── README.md
/src/nodes/shared/types.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | /rollup.config.editor.js
4 | /utils
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/**/*
3 | !LICENSE
4 | !README.md
5 | !package.json
6 | !yarn.lock
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | yarn-debug.log*
4 | yarn-error.log*
5 | node_modules/
6 | *.tsbuildinfo
7 | dist
8 |
--------------------------------------------------------------------------------
/utils/templates/blank/shared/types.ts.mustache:
--------------------------------------------------------------------------------
1 | export interface <%NodeTypePascalCase%>Options {
2 | // node options
3 | }
4 |
--------------------------------------------------------------------------------
/utils/templates/config/shared/types.ts.mustache:
--------------------------------------------------------------------------------
1 | export interface <%NodeTypePascalCase%>Options {
2 | // node options
3 | }
4 |
--------------------------------------------------------------------------------
/utils/templates/blank-0/shared/types.ts.mustache:
--------------------------------------------------------------------------------
1 | export interface <%NodeTypePascalCase%>Options {
2 | // node options
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.runtime.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src"],
4 | "exclude": ["node_modules", "src/__tests__", "src/nodes/*/*.html"]
5 | }
6 |
--------------------------------------------------------------------------------
/src/nodes/transform-text/icons/transform-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexk111/node-red-node-typescript-starter/HEAD/src/nodes/transform-text/icons/transform-text.png
--------------------------------------------------------------------------------
/utils/templates/blank-0/node-type.html/help.html.mustache:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/utils/templates/blank/node-type.html/help.html.mustache:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/utils/templates/config/node-type.html/help.html.mustache:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tsconfig.runtime.watch.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.runtime.json",
3 | "compilerOptions": {
4 | "incremental": true,
5 | "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/nodes/transform-text/transform-text.html/help.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/utils/templates/blank-0/node-type.html/editor.html.mustache:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/utils/templates/blank/node-type.html/editor.html.mustache:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/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/config/node-type.html/editor.html.mustache:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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.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/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-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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/nodes/transform-text/transform-text.html/editor.html:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/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-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | [](https://mynode.alexkaul.com/gh-backer/top/0/profile)
97 | [](https://mynode.alexkaul.com/gh-backer/top/1/profile)
98 | [](https://mynode.alexkaul.com/gh-backer/top/2/profile)
99 | [](https://mynode.alexkaul.com/gh-backer/top/3/profile)
100 | [](https://mynode.alexkaul.com/gh-backer/top/4/profile)
101 | [](https://mynode.alexkaul.com/gh-backer/top/5/profile)
102 | [](https://mynode.alexkaul.com/gh-backer/top/6/profile)
103 | [](https://mynode.alexkaul.com/gh-backer/top/7/profile)
104 | [](https://mynode.alexkaul.com/gh-backer/top/8/profile)
105 | [](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 |
--------------------------------------------------------------------------------