├── src ├── badpackages.json ├── index.js ├── libs │ ├── npmget.js │ ├── checkdeps.js │ ├── checkpackage.js │ └── checknodes.js ├── officalnodes.json └── commands │ └── validate.js ├── .eslintrc ├── bin ├── run.cmd └── run ├── test ├── mocha.opts └── commands │ ├── hello.test.js │ └── validate.test.js ├── .gitignore ├── .editorconfig ├── docs ├── N01.md ├── P03.md ├── D02.md ├── P08.md ├── P06.md ├── index.md ├── N02.md ├── P01.md ├── P05.md ├── P07.md ├── D01.md ├── D03.md ├── P02.md └── P04.md ├── package.json ├── README.md └── LICENSE /src/badpackages.json: -------------------------------------------------------------------------------- 1 | {"agent-base":"<6.0.0"} -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "oclif" 3 | } 4 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --reporter spec 3 | --timeout 5000 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@oclif/command') 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .then(require('@oclif/command/flush')) 5 | .catch(require('@oclif/errors/handle')) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /tmp 6 | /yarn.lock 7 | node_modules 8 | .DS_Store 9 | .gitconfig 10 | package-lock.json 11 | test.js 12 | oclif.manifest.json -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /docs/N01.md: -------------------------------------------------------------------------------- 1 | # Node Names 2 | 3 | ## Requirements 4 | Nodes SHOULD use unique names 5 | The tool will check the names for each node in your package against other packages already published to the catalog and highlight any duplicates. 6 | 7 | ## Reason 8 | A user cannot install 2 different nodes that use the same name. 9 | 10 | ## Reference 11 | https://nodered.org/docs/creating-nodes/packaging#packagejson -------------------------------------------------------------------------------- /test/commands/hello.test.js: -------------------------------------------------------------------------------- 1 | const {expect, test} = require('@oclif/test') 2 | 3 | describe('hello', () => { 4 | test 5 | .stdout() 6 | .command(['hello']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['hello', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /docs/P03.md: -------------------------------------------------------------------------------- 1 | # Package.json must contain a Repository or Bugs URL or Email 2 | 3 | ## Requirement 4 | The package.json file MUST contain either a repository object or a bugs object. 5 | If `bugs` is specified then either a URL or an email address can by supplied. 6 | 7 | ## Reason 8 | This is so that users can report bugs and request support for using the package. 9 | 10 | ## Reference 11 | https://docs.npmjs.com/cli/v7/configuring-npm/package-json#bugs 12 | -------------------------------------------------------------------------------- /test/commands/validate.test.js: -------------------------------------------------------------------------------- 1 | const {expect, test} = require('@oclif/test') 2 | 3 | describe('validate', () => { 4 | test 5 | .stdout() 6 | .command(['validate']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['validate', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /docs/D02.md: -------------------------------------------------------------------------------- 1 | 2 | # Bad Packages 3 | 4 | ## Requirements 5 | The tool will check that any packages with known incompatibilities to Node-RED core are not used in the dependency tree. 6 | 7 | ## Reason 8 | As of Oct 2021 there has only been one package that caused an issue, (agent-base <6.0.0). However this facility allows us to expand this list should the need arise in the future. 9 | 10 | ## Reference 11 | https://github.com/node-red/node-red-dev-cli/blob/main/src/badpackages.json 12 | -------------------------------------------------------------------------------- /docs/P08.md: -------------------------------------------------------------------------------- 1 | # Similar Names 2 | 3 | ## Requirements 4 | The package should use a unique name. 5 | The tool will check for packages of the same name with a different scope and these may be highlighted to the user. 6 | 7 | ## Reason 8 | Packages should use unique names, where a package is abandoned and another developer updates it and publishes a fork under their own scope this will help users to identify that they are similar packages, the README should me clear where a package is a fork and what the changes are. 9 | 10 | ## Reference 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/P06.md: -------------------------------------------------------------------------------- 1 | # Min node-red version 2 | 3 | ## Requirements 4 | Within the `node-red` section of the package.json file the developer SHOULD indicate the minimum supported version. This must satisfy at least one of the current `latest` or `maintenance` tagged releases. 5 | 6 | e.g. 7 | ```js 8 | "node-red" : { 9 | "version": ">=2.0.0", 10 | } 11 | ``` 12 | 13 | ## Reason 14 | This is so that users can identify nodes that rely on new features in latest versions of node-red. 15 | 16 | ## Reference 17 | 18 | https://nodered.org/docs/creating-nodes/packaging#packagejson 19 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | ## Package.json Checks 3 | 4 | - [P01 License](P01.md) 5 | - [P02 Check package name in repository](P02.md) 6 | - [P03 Repository or Bugs url/email](P03.md) 7 | - [P04 Naming](P04.md) 8 | - [P05 Node-RED Keyword](P05.md) 9 | - [P06 Min node-red version](P06.md) 10 | - [P07 Min node version](P07.md) 11 | - [P08 Similar Names](P08.md) 12 | 13 | ## Node Checks 14 | 15 | - [N01 Node Names](N01.md) 16 | - [N02 Examples](N02.md) 17 | 18 | ## Dependencies 19 | - [D01 Number of Dependencies](D01.md) 20 | - [D02 Bad Packages](D02.md) 21 | - [D03 Out of Date Dependencies](D03.md) -------------------------------------------------------------------------------- /docs/N02.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Requirements 4 | The package SHOULD include example flows that demonstrate each node. 5 | The tool will check the examples folder (and any nested folders) for json flow files, it will then iterate over these files and look at the nodes used in the flows to ensure that all nodes declared in the package are used in an example at least once. Config nodes are excluded from the check. 6 | 7 | 8 | ## Reason 9 | Examples are an excellent way for users to understand how to use a node 10 | 11 | ## Reference 12 | https://nodered.org/docs/creating-nodes/examples 13 | -------------------------------------------------------------------------------- /docs/P01.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | ## Requirement 4 | The package.json MUST contain a key of `license` that specifies the type of license used, this can be either an open source licence or your own proprietary license. The value should be either a recognised [SPDX ID](https://spdx.org/licenses/) Or refer to a filename within the package for a custom license. 5 | 6 | ## Reason 7 | The package must specify a license so that users can have confidence they are permitted to use it and understand any restrictions on its use. 8 | 9 | ## Reference 10 | https://docs.npmjs.com/cli/v7/configuring-npm/package-json#license 11 | -------------------------------------------------------------------------------- /docs/P05.md: -------------------------------------------------------------------------------- 1 | # Node-RED Keyword 2 | 3 | ## Requirements 4 | The package.json must contain the keyword `node-red` e.g. 5 | 6 | ```js 7 | "keywords": [ "node-red" ], 8 | ``` 9 | 10 | ## Reason 11 | This identifies the package as a Node-RED module rather than a more generic node package. 12 | 13 | Without this keyword set, it will not be listed on the Flow Library, but it will still be npm installable. 14 | 15 | If the module is not yet ready for wider use by the Node-RED community, you should not include the keyword. 16 | 17 | 18 | ## Reference 19 | https://nodered.org/docs/creating-nodes/packaging#packagejson 20 | -------------------------------------------------------------------------------- /docs/P07.md: -------------------------------------------------------------------------------- 1 | # Min node version 2 | 3 | ## Requirements 4 | Within the `engines` section of the package.json file you SHOULD declare the minimum version of Node that your package works on. 5 | This SHOULD satisfy the current minimum supported version of the latest node-red release. 6 | ```json 7 | { 8 | "engines": { 9 | "node": ">=12.0.0" 10 | } 11 | } 12 | ``` 13 | ## Reason 14 | Node-RED has supported multiple versions of Node in its history and some of these have become end of life, this helps users identify if a node will run on their installation. 15 | ## Reference 16 | https://docs.npmjs.com/cli/v7/configuring-npm/package-json#engines 17 | -------------------------------------------------------------------------------- /docs/D01.md: -------------------------------------------------------------------------------- 1 | # Number of Dependencies 2 | 3 | ## Requirements 4 | The tool will check the number of dependencies in the package.json and warn if there are more than 6. 5 | devDependencies are not counted. 6 | 7 | ## Reason 8 | 6 was chosen as 95% of all published Node-RED modules had less than 6 dependencies when tested (Oct 2021) 9 | We do see occasionally packages including a large number of unused dependencies including the `node-red` package itself! 10 | This isn't meant to be a hard limit just a warning to check that everything in your dependencies list is indeed needed, it is recognised that some packages may have a legitimate reason to have a lot of dependencies and this can be noted in the README 11 | 12 | ## Reference 13 | https://docs.npmjs.com/cli/v7/configuring-npm/package-json#dependencies -------------------------------------------------------------------------------- /docs/D03.md: -------------------------------------------------------------------------------- 1 | 2 | # Out of Date Dependencies 3 | 4 | ## Requirements 5 | The tool will check the dependencies listed in the package.json and if there is a newer version that does not satisfy the stated version it will flag a warning. 6 | For example if your dependencies are: 7 | ```json 8 | { "acme" : "~1.0.0"} 9 | ``` 10 | And the latest version of acme is 1.1.3 then this will not warn as the latest version will be installed when the package is added to node-red. 11 | However if acme is at 2.0.0 it would flag a warning. 12 | 13 | ## Reason 14 | Packages are often updated to include security fixes so users should be able to use the newest version available. 15 | Where your package needs to use an older version this should be noted in the README 16 | 17 | 18 | 19 | ## Reference 20 | https://docs.npmjs.com/cli/v7/configuring-npm/package-json#dependencies 21 | -------------------------------------------------------------------------------- /docs/P02.md: -------------------------------------------------------------------------------- 1 | # Check package name in repository is the same name as the package.json 2 | 3 | ## Requirement 4 | 5 | The name field in the package.json SHOULD match the name field in the package.json hosted within the respository specified in the package.json. 6 | 7 | ## Reason 8 | This is to check for forked nodes where the publisher has forked an original repo and published the node under a new name but not updated the repo to point to their own new copy. This can result in authors of the original node getting issues raised against their repository for a downstream fork. 9 | 10 | ## Note 11 | This test may fail where you are validating a local copy of the package and it has not yet been published, or where the code has not been published in a repository. 12 | 13 | Where code is part of a mono-repo you should use the `directory` key within `repository` to point to the path where the package.json is location 14 | 15 | ## Reference 16 | 17 | https://docs.npmjs.com/cli/v7/configuring-npm/package-json#name 18 | -------------------------------------------------------------------------------- /docs/P04.md: -------------------------------------------------------------------------------- 1 | # Naming 2 | 3 | ## Requirements 4 | New packages published to npm after 1st December 2021 should use a scoped name, they may then use any value in their package name eg `@devname/acme-node-red` 5 | 6 | Packages first published before this date can use unscoped names, however where the package name starts with `node-red` (or `nodered` then it MUST use `node-red-contrib` 7 | 8 | The following names would be allowed: 9 | `node-red-contrib-acme` 10 | `nodered-contrib-acme` 11 | `acme-nodered` 12 | 13 | The following would not be allowed: 14 | `node-red-acme` 15 | `nodered-acme` 16 | 17 | ## Reason 18 | Initially the contrib formatting was used to distinguish 3rd party nodes from core nodes created by the project. 19 | As the number of nodes has grown the namespace has become more cluttered, a move to scoped names will allow for cleaner naming of nodes by service provides. 20 | Scoped names also allow for a developer to fork an abandoned node and publish a new version with the same package name but within their scope. This makes it clearer to users where multiple packages exist. 21 | 22 | ## Reference 23 | 24 | 25 | https://nodered.org/docs/creating-nodes/packaging#naming 26 | -------------------------------------------------------------------------------- /src/libs/npmget.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const os = require('os') 3 | const fs = require('fs'); 4 | const tar = require('tar') 5 | 6 | function getFromNPM(package, version) { 7 | const path = os.tmpdir()+'/'+package 8 | let npm_metadata = false 9 | if (!fs.existsSync(path)){ 10 | if (package.substr(0,1) === '@'){ 11 | fs.mkdirSync(os.tmpdir()+'/'+package.split('/')[0]); 12 | fs.mkdirSync(os.tmpdir()+'/'+package); 13 | } else { 14 | fs.mkdirSync(path); 15 | } 16 | } 17 | const tarball = path+'/package.tgz'; 18 | return axios.get('https://registry.npmjs.org/'+package) 19 | .then(response => { 20 | npm_metadata = response.data 21 | if (!version){ 22 | version = response.data['dist-tags'].latest 23 | } 24 | var tarballUrl = response.data.versions[version].dist.tarball; 25 | return axios({ 26 | method: 'get', 27 | url: tarballUrl, 28 | responseType: 'arraybuffer', 29 | }) 30 | .then(response => { 31 | return new Promise(function(resolve, reject) { 32 | fs.writeFile(tarball, response.data, function(err) { 33 | if (err) reject(err); 34 | else { 35 | return tar.x({ file: tarball, cwd: path, sync: false, strip: 1 }) 36 | .then(r => { 37 | resolve([path, npm_metadata]) 38 | }) 39 | } 40 | }); 41 | }) 42 | 43 | }); 44 | }) 45 | .catch(error => { 46 | console.log(error); 47 | }); 48 | } 49 | 50 | module.exports = getFromNPM 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-dev", 3 | "description": "Node-RED Node Developer Tools", 4 | "version": "0.1.6", 5 | "author": "Sam Machin @sammachin", 6 | "bin": { 7 | "node-red-dev": "./bin/run" 8 | }, 9 | "bugs": "https://github.com/node-red/node-red-dev/issues", 10 | "dependencies": { 11 | "@oclif/command": "^1.8.0", 12 | "@oclif/config": "^1.17.0", 13 | "@oclif/plugin-help": "^3.2.3", 14 | "npm-check": "^5.9.2", 15 | "npm-remote-ls": "^1.3.2", 16 | "acorn": "^8.4.1", 17 | "acorn-walk": "^8.1.1", 18 | "axios": "^0.21.4", 19 | "semver": "^7.3.5", 20 | "tar": "^6.1.11" 21 | }, 22 | "devDependencies": { 23 | "@oclif/dev-cli": "^1.26.0", 24 | "@oclif/test": "^1.2.8", 25 | "chai": "^4.3.4", 26 | "eslint": "^5.16.0", 27 | "eslint-config-oclif": "^3.1.0", 28 | "globby": "^10.0.2", 29 | "mocha": "^5.2.0", 30 | "nyc": "^14.1.1" 31 | }, 32 | "engines": { 33 | "node": ">=14.0.0" 34 | }, 35 | "files": [ 36 | "/bin", 37 | "/npm-shrinkwrap.json", 38 | "/oclif.manifest.json", 39 | "/src" 40 | ], 41 | "homepage": "https://github.com/node-red/node-red-dev-cli", 42 | "keywords": [ 43 | "oclif" 44 | ], 45 | "license": "Apache-2.0", 46 | "main": "src/index.js", 47 | "oclif": { 48 | "commands": "./src/commands", 49 | "bin": "node-red-dev", 50 | "plugins": [ 51 | "@oclif/plugin-help" 52 | ] 53 | }, 54 | "repository": "node-red/node-red-dev-cli", 55 | "scripts": { 56 | "postpack": "rm -f oclif.manifest.json", 57 | "posttest": "eslint .", 58 | "prepack": "oclif-dev manifest", 59 | "test": "nyc mocha --forbid-only \"test/**/*.test.js\"" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-red-dev 2 | ============ 3 | 4 | Node-RED Node Developer Tools 5 | 6 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 7 | [![Version](https://img.shields.io/npm/v/node-red-dev.svg)](https://npmjs.org/package/node-red-dev) 8 | [![Downloads/week](https://img.shields.io/npm/dw/node-red-dev.svg)](https://npmjs.org/package/node-red-dev) 9 | [![License](https://img.shields.io/npm/l/node-red-dev.svg)](https://github.com/node-red/node-red-dev/blob/master/package.json) 10 | 11 | 12 | * [Usage](#usage) 13 | * [Commands](#commands) 14 | 15 | 16 | # Usage 17 | 18 | ```sh-session 19 | $ npm install -g node-red-dev 20 | $ node-red-dev COMMAND 21 | running command... 22 | $ node-red-dev (-v|--version|version) 23 | node-red-dev/0.1.3 darwin-arm64 node-v16.13.0 24 | $ node-red-dev --help [COMMAND] 25 | USAGE 26 | $ node-red-dev COMMAND 27 | ... 28 | ``` 29 | 30 | # Commands 31 | 32 | * [`node-red-dev help [COMMAND]`](#node-red-dev-help-command) 33 | * [`node-red-dev validate`](#node-red-dev-validate) 34 | 35 | ## `node-red-dev help [COMMAND]` 36 | 37 | display help for node-red-dev 38 | 39 | ``` 40 | USAGE 41 | $ node-red-dev help [COMMAND] 42 | 43 | ARGUMENTS 44 | COMMAND command to show help for 45 | 46 | OPTIONS 47 | --all see all commands in CLI 48 | ``` 49 | 50 | 51 | ## `node-red-dev validate` 52 | 53 | Run the full suite of Validation tests 54 | 55 | ``` 56 | USAGE 57 | $ node-red-dev validate 58 | 59 | OPTIONS 60 | -e, --embed=embed use when running embedded in anothe node app 61 | -n, --npm=npm Name of package on npm to validate 62 | -o, --output=output Path to write scorecard file to 63 | -p, --path=path Path of package to validate 64 | 65 | DESCRIPTION 66 | ... 67 | By default the tool will look in the current folder for a package, 68 | you can also specify a path with --path or a published npm package with --npm. 69 | ``` 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/officalnodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": ["node-red-node-pi-neopixel", "node-red-node-pisrf", "node-red-node-ui-vega", "node-red-node-pi-unicorn-hat", "node-red-node-pilcd", "node-red-node-geohash", "node-red-node-pi-gpio", "node-red-node-serialport", "node-red-node-pi-mcp3008", "node-red-dashboard", "node-red-node-aws", "node-red-node-annotate-image", "node-red-node-data-generator", "node-red-node-exif", "node-red-node-ldap", "node-red-node-daemon", "node-red-node-xmpp", "node-red-node-mysql", "node-red-debugger", "node-red-node-geofence", "node-red-node-sqlite", "node-red-node-pushover", "node-red-node-email", "node-red-node-ui-list", "node-red-node-ui-table", "node-red-node-openweathermap", "node-red-node-discovery", "node-red-node-wol", "node-red-node-piliter", "node-red-node-pi-sense-hat", "node-red-node-pibrella", "node-red-node-blink1", "node-red-node-ui-microphone", "node-red-node-watson", "node-red-node-ping", "node-red-node-physical-web", "node-red-node-ledborg", "node-red-node-tail", "node-red-node-wemo", "node-red-node-feedparser", "node-red-node-rbe", "node-red-node-ui-iframe", "node-red-node-pi-gpiod", "node-red-node-ui-webcam", "node-red-node-irc", "node-red-node-twitter", "node-red-node-base64", "node-red-node-random", "node-red-node-timeswitch", "node-red-node-emoncms", "node-red-node-mongodb", "node-red-node-pushbullet", "node-red-node-snmp", "node-red-node-pi-sense-hat-simulator", "node-red-node-group", "node-red-node-msgpack", "node-red-node-suncalc", "node-red-node-google", "node-red-node-markdown", "node-red-node-leveldb", "node-red-node-prowl", "node-red-node-sentiment", "node-red-node-darksky", "node-red-node-swagger", "node-red-node-arduino", "node-red-node-smaz", "node-red-node-smooth", "node-red-node-what3words", "node-red-node-ui-lineargauge", "node-red-bluemix-nodes", "node-red-node-multilang-sentiment", "node-red-node-badwords", "node-red-node-blinkstick", "node-red-node-stomp", "node-red-node-intel-gpio", "node-red-node-forecastio", "node-red-node-notify", "node-red-node-pinboard", "node-red-node-dropbox", "node-red-node-twilio", "node-red-node-pusher", "node-red-node-redis", "node-red-node-openwhisk", "node-red-node-dweetio", "node-red-node-sensortag", "node-red-node-makeymakey", "node-red-node-hbgpio", "node-red-node-box", "node-red-node-piface", "node-red-node-digirgb", "node-red-node-beaglebone", "node-red-node-pidcontrol", "node-red-node-wordpos", "node-red-node-strava", "node-red-node-tfl", "node-red-node-foursquare", "node-red-node-flickr", "node-red-node-jawboneup", "node-red-node-instagram", "node-red-node-mqlight", "node-red-node-fitbit", "node-red-node-delicious", "node-red-node-web-nodes"] 3 | } -------------------------------------------------------------------------------- /src/commands/validate.js: -------------------------------------------------------------------------------- 1 | const {Command, flags} = require('@oclif/command') 2 | const getFromNPM = require('../libs/npmget') 3 | const checkpackage = require('../libs/checkpackage') 4 | const checknodes = require('../libs/checknodes') 5 | const fs = require('fs'); 6 | const checkdeps = require('../libs/checkdeps'); 7 | const p = require('path'); 8 | 9 | let path = '' 10 | 11 | 12 | function parsePackage(i){ 13 | 14 | return [n,v] 15 | } 16 | 17 | 18 | class ValidateCommand extends Command { 19 | async run() { 20 | const {flags} = this.parse(ValidateCommand) 21 | const cli = this 22 | const npmstring = flags.npm 23 | let scorecard = {} 24 | let packagename 25 | let version 26 | let npm_metadata 27 | if (npmstring){ 28 | if (npmstring.includes('@')){ 29 | if (npmstring.indexOf('@') == 0) { 30 | packagename = '@'+npmstring.split('@')[1] 31 | version = npmstring.split('@')[2] 32 | } else{ 33 | packagename = npmstring.split('@')[0] 34 | version = npmstring.split('@')[1] 35 | } 36 | } else { 37 | packagename = npmstring 38 | version = false 39 | } 40 | [path, npm_metadata] = await getFromNPM(packagename, version) 41 | } else if (flags.path){ 42 | if(p.isAbsolute(flags.path)) { 43 | npm_metadata = false 44 | path = flags.path 45 | } else { 46 | npm_metadata = false 47 | path = process.cwd()+'/'+flags.path 48 | } 49 | } 50 | else { 51 | path = process.cwd() 52 | } 53 | await checkpackage(path, cli, scorecard, npm_metadata) 54 | .then(scorecard => { 55 | let pkg = require(path+'/package.json') 56 | // Remove the file from the module-cache so if we're being run 57 | // programmatically we don't cache previous versions 58 | delete require.cache[require.resolve(path+'/package.json')] 59 | 60 | if (!pkg['node-red'].hasOwnProperty('nodes')){ //If no nodes declared skip node validation (could be a plugin) 61 | cli.warn('No nodes declared in package.json') 62 | return(scorecard) 63 | } else{ 64 | return checknodes(path, cli, scorecard, npm_metadata) 65 | } 66 | }) 67 | .then(scorecard => { 68 | return checkdeps(path, cli, scorecard, npm_metadata) 69 | }) 70 | .then(() => { 71 | if (flags.output){ 72 | try { 73 | fs.writeFileSync(flags.output, JSON.stringify(scorecard)) 74 | } catch (err) { 75 | cli.error(err) 76 | } 77 | } 78 | }) 79 | 80 | .catch((e)=> { 81 | cli.error(e) 82 | }) 83 | .then(() =>{ 84 | cli.log('Complete') 85 | }) 86 | if (!flags.embed){ 87 | cli.exit() 88 | } 89 | } 90 | } 91 | 92 | 93 | 94 | ValidateCommand.description = `Run the full suite of Validation tests 95 | ... 96 | By default the tool will look in the current folder for a package, 97 | you can also specify a path with --path or a published npm package with --npm. 98 | ` 99 | 100 | ValidateCommand.flags = { 101 | npm: flags.string({char: 'n', description: 'Name of package on npm to validate'}), 102 | path: flags.string({char: 'p', description: 'Path of package to validate'}), 103 | output: flags.string({char: 'o', description: 'Path to write scorecard file to'}), 104 | embed: flags.string({char: 'e', description: 'use when running embedded in anothe node app'}), 105 | } 106 | 107 | 108 | module.exports = ValidateCommand 109 | -------------------------------------------------------------------------------- /src/libs/checkdeps.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const fs = require('fs'); 3 | const p = require('path') 4 | const axios = require('axios') 5 | const readline = require('readline').createInterface({ 6 | input: process.stdin, 7 | output: process.stdout 8 | }); 9 | const npmls = require('npm-remote-ls').ls; 10 | const npmls_config = require('npm-remote-ls').config 11 | npmls_config({ 12 | development: false, 13 | optional: false 14 | }) 15 | 16 | const { resolve } = require('path'); 17 | const util = require('util'); 18 | const npmCheck = require('npm-check'); 19 | const { consumers } = require('stream'); 20 | const semver = require('semver'); 21 | 22 | function checkdeps(path, cli, scorecard, npm_metadata) { 23 | const package = require(path+'/package.json'); 24 | // Remove the file from the module-cache so if we're being run 25 | // programmatically we don't cache previous versions 26 | delete require.cache[require.resolve(path+'/package.json')] 27 | 28 | return new Promise((resolve, reject) => { 29 | cli.log(' ---Validating Dependencies---') 30 | resolve(); 31 | }) 32 | .then(() => { 33 | // Should have 6 or less dependencies D01 34 | // 6 is based on the 95th percentile of all packages in catalog at Oct 2021, use https://flows.nodered.org/flow/df33d0171d3d095d7c7b70169b9aa759 to recalculate 35 | let depcount 36 | if (package.hasOwnProperty('dependencies')){ 37 | depcount = Object.keys(package.dependencies).length 38 | } else { 39 | depcount = 0 40 | } 41 | 42 | if (depcount <= 6) { 43 | cli.log(`✅ Package has ${depcount} dependencies`) 44 | scorecard.D01 = {'test' : true} 45 | scorecard.D01.total = depcount 46 | } else { 47 | cli.warn(`D01 Package has a large number of dependencies (${depcount})`) 48 | scorecard.D01 = {'test' : false} 49 | scorecard.D01.total = depcount 50 | } 51 | }) 52 | .then(() => { 53 | //Check dependency tree doesn't contain known incompatible packages 54 | scorecard.D02 = {'test' : true, packages : []} 55 | if (!package.hasOwnProperty('dependencies')){ 56 | package.dependencies = [] 57 | } 58 | //return axios.get('https://s3.sammachin.com/badpackages.json') // TODO Move to a node-red domain 59 | // for now use a local badpacakges file 60 | return new Promise((resolve, reject) => { 61 | let response = {} 62 | response = JSON.parse(fs.readFileSync(__dirname+'/../badpackages.json')); 63 | resolve(response); 64 | }) 65 | // end 66 | .then(response => { 67 | const badpackages = response 68 | for (const [name, version] of Object.entries(package.dependencies)) { 69 | return new Promise((resolve, reject) => { 70 | npmls(name, version, true, function(list) { 71 | list.forEach(i => { 72 | if (i.indexOf('@') == 0) { 73 | n = '@'+i.split('@')[1] 74 | v = i.split('@')[2] 75 | } else{ 76 | n = i.split('@')[0] 77 | v = i.split('@')[1] 78 | } 79 | if (Object.keys(badpackages).includes(n) && semver.satisfies(v, badpackages[n])){ 80 | cli.warn(`D02 Incompatible package ${i} found as dependency of ${name}`) 81 | scorecard.D02.test = false 82 | scorecard.D02.packages.push(i) 83 | //return 84 | } 85 | }); 86 | resolve() 87 | }); 88 | }) 89 | } 90 | 91 | }) 92 | .then(() => { 93 | if (scorecard.D02.test) { 94 | cli.log((`✅ No incompatible packages found in dependency tree`)) 95 | delete scorecard.D02.packages 96 | } 97 | return 98 | }) 99 | }) 100 | .then(() => { 101 | // Check if dependencies are out of date 102 | scorecard.D03 = {'test' : true, packages : []} 103 | return npmCheck({cwd: path, skipUnused: true, ignoreDev: true}) 104 | .then(currentState => { 105 | currentState.get('packages').forEach((dep) => { 106 | if (!dep.easyUpgrade){ 107 | cli.warn(`D03 ${dep.moduleName} is not at latest version, package.json specifies: ${dep.packageJson}, latest is: ${dep.latest}`) 108 | scorecard.D03.test = false 109 | scorecard.D03.packages.push(dep.moduleName) 110 | } 111 | }) 112 | if (scorecard.D03.test) { 113 | cli.log((`✅ All prod dependencies using latest versions`)) 114 | delete scorecard.D03.packages 115 | } 116 | }) 117 | }) 118 | .then(() => { 119 | return scorecard 120 | }) 121 | .catch((e) => { 122 | cli.error(e); 123 | }); 124 | 125 | } 126 | 127 | module.exports = checkdeps 128 | -------------------------------------------------------------------------------- /src/libs/checkpackage.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const fs = require('fs'); 3 | const { t } = require('tar'); 4 | const semver = require('semver'); 5 | const p = require('path') 6 | //const nodegit = require('nodegit'); 7 | const axios = require('axios') 8 | const readline = require('readline').createInterface({ 9 | input: process.stdin, 10 | output: process.stdout 11 | }); 12 | const officialnodes = require('../officalnodes.json') 13 | 14 | function isGitUrl(str) { 15 | //var regex = /(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|\#[-\d\w._]+?)$/; 16 | var regex = /(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\/?|\#[-\d\w._]+?)$/; 17 | return regex.test(str); 18 | }; 19 | 20 | 21 | function checkpackage(path, cli, scorecard, npm_metadata) { 22 | const package = require(path+'/package.json'); 23 | // Remove the file from the module-cache so if we're being run 24 | // programmatically we don't cache previous versions 25 | delete require.cache[require.resolve(path+'/package.json')] 26 | 27 | return new Promise((resolve, reject) => { 28 | cli.log(' ---Validating Package---') 29 | cli.log(` ${package.name}@${package.version}`) 30 | scorecard.package = {'name' : package.name, 'version' : package.version} 31 | resolve(); 32 | }) 33 | .then(() => { 34 | //MUST Have a License P01 35 | if (package.hasOwnProperty('license')){ 36 | cli.log(`✅ Package is ${package.license} licensed`) 37 | scorecard.P01 = {'test' : true, 'license' : package.license} 38 | } else { 39 | cli.warn('No License Specified') 40 | scorecard.P01 = {'test' : false} 41 | } 42 | }) 43 | .then(() => { 44 | //MUST Have Repository or Bugs url/email P03 45 | if (package.hasOwnProperty('repository') || package.hasOwnProperty('bugs')){ 46 | cli.log(`✅ Repository/Bugs Link Supplied`) 47 | scorecard.P03 = {'test' : true} 48 | } else { 49 | cli.warn('P03 Please provide either a repository URL or a Bugs URL/Email') 50 | scorecard.P03 = {'test' : false} 51 | } 52 | }) 53 | // Test P02 Disabled pending a more effieint method than nodegit 54 | //.then(() => { 55 | // // Check package in repository is the same name as the package.json (check for forks) - This may need more thought and testing P02 56 | // if (package.hasOwnProperty('repository')){ 57 | // let repourl = package.repository.url 58 | // let repo = p.basename(repourl) 59 | // const repopath = os.tmpdir()+'/'+repo 60 | // if (!fs.existsSync(repopath)){ 61 | // fs.mkdirSync(repopath); 62 | // } else{ 63 | // fs.rmSync(repopath+'/', { recursive:true }) 64 | // } 65 | // //Fix for repourls that contain credentials eg 'git@github.com:username/project.git' 66 | // if (repourl.indexOf('@') != -1){ repourl = 'https://'+repourl.replace(':', '/').split('@')[1]} 67 | // if (!isGitUrl(repourl)){ 68 | // cli.error('Invalid Repository URL: '+ repourl) 69 | // } 70 | // return nodegit.Clone(repourl, repopath) 71 | // .then(function (r){ 72 | // if (package.repository.hasOwnProperty('directory')){ 73 | // let path = r.workdir()+package.repository.directory 74 | // } else{ 75 | // let path = r.workdir() 76 | // } 77 | // let repopackage = require(path+'/package.json') 78 | // delete require.cache[require.resolve(path+'/package.json')] 79 | // if (package.name != repopackage.name){ 80 | // cli.warn('P02 Package name does not match package.json in repository') 81 | // scorecard.P02 = {'test' : false} 82 | // } else { 83 | // cli.log('✅ Package Name Matches Repository') 84 | // scorecard.P02 = {'test' : true} 85 | // } 86 | // }) 87 | // .catch((e) =>{ 88 | // cli.error('P02 Failed to clone git repository '+e) 89 | // scorecard.P02 = {'test' : false} 90 | // }) 91 | // } else { 92 | // cli.warn('P02 No Repository listed in package.json') 93 | // scorecard.P02 = {'test' : false} 94 | // } 95 | //}) 96 | .then(() => { 97 | // P04 Naming 98 | let legacy = false 99 | const scopedRegex = new RegExp('@[a-z\\d][\\w-.]+/[a-z\\d][\\w-.]*'); 100 | if (npm_metadata){ 101 | //New packages should Use a Scoped name 102 | let scoped_start= Date.parse('2022-02-01T00:00:00.000Z') //New Packages should use scoped names from 1st Feb 2021 103 | let created = Date.parse(npm_metadata.time.created) 104 | if (created -1) { 123 | cli.log('✅ Package is on the official nodes list') 124 | scorecard.P04 = { 'test' : true} 125 | } else { 126 | cli.warn('P04 Packages using the node-red prefix in their name must use node-red-contrib') 127 | scorecard.P04 = { 'test' : false} 128 | } 129 | } else { 130 | cli.log('✅ Package uses a Scoped Name') 131 | scorecard.P04 = { 'test' : true} 132 | } 133 | }) 134 | .then(() => { 135 | //Check for other package of same name in different scope, ask about fork? P08 136 | scorecard.P08 = {test : true} 137 | const name = package.name.split('/').slice(-1) // Package name without scope 138 | let similar = false 139 | let similarlist = [] 140 | return axios.get('https://catalogue.nodered.org/catalogue.json') 141 | .then(response => { 142 | response.data.modules.forEach((m) => { 143 | if (name.includes(m.id.split('/').slice(-1))){ 144 | cli.warn(`P08 Similar named package found at ${m.id}`) 145 | similar = true 146 | similarlist.push(m.id) 147 | } 148 | }) 149 | if (similar){ 150 | scorecard.P08 = { 'test' : false, 'similar': similarlist} 151 | } else { 152 | cli.log('✅ No similar named packages found') 153 | scorecard.P08 = { 'test' : true} 154 | } 155 | return similar 156 | }) 157 | scorecard.P08.test = !test 158 | }) 159 | .then(() => { 160 | //MUST have Node-RED in keywords P05 161 | if (package.hasOwnProperty('keywords') && package.keywords.includes('node-red')){ 162 | cli.log(`✅ Node-RED Keyword Found`) 163 | scorecard.P05 = { 'test' : true} 164 | } else { 165 | cli.warn('P05 Package.json keywords MUST contain node-red') 166 | scorecard.P05 = { 'test' : true} 167 | } 168 | }) 169 | .then(() => { 170 | //SHOULD declare min node-red version in node-red P06 171 | if (package['node-red'].hasOwnProperty('version')){ 172 | return axios.get('https://registry.npmjs.org/node-red') 173 | .then(response => { 174 | let tags = response.data['dist-tags'] 175 | let supportedRegex = new RegExp('.*maintenance.*|^latest$') // check which versions have a latest or maintenance tag 176 | let versions = [] 177 | Object.keys(tags).forEach(t => { 178 | if (supportedRegex.test(t)) { 179 | versions.push(tags[t]) 180 | } 181 | }); 182 | //Test version against current versions 183 | let compatible = false 184 | let cv = [] 185 | versions.forEach(v => { 186 | if (semver.satisfies(v, package['node-red'].version)){ 187 | cli.log(`✅ Compatible with Node-RED v${v}`) 188 | compatible = true 189 | scorecard.P06 = { 'test' : true} 190 | cv.push(v) 191 | } else { 192 | cli.warn(`P06 NOT Compatible with Node-RED v${v}`) 193 | } 194 | if (!compatible){ 195 | cli.warn('P06 Not Compatible with any current Node-RED versions') 196 | scorecard.P06 = { 'test' : false} 197 | } else { 198 | scorecard.P06.versions = cv 199 | } 200 | }) 201 | }) 202 | } else { 203 | cli.warn('P06 Node-RED version compatibility not declared') 204 | scorecard.P06 = { 'test' : false} 205 | } 206 | }) 207 | .then(() => { 208 | //SHOULD declare min node version in engines P07 209 | if ( package.hasOwnProperty('engines') && package.engines.hasOwnProperty('node')){ 210 | return axios.get('https://registry.npmjs.org/node-red') 211 | .then(response => { 212 | nminversion = semver.minVersion(response.data.versions[response.data["dist-tags"].latest].engines.node) 213 | if (semver.satisfies(nminversion, package.engines.node)){ 214 | cli.log(`✅ Compatible NodeJS Version found ${nminversion}`) 215 | scorecard.P07 = { 'test' : true} 216 | scorecard.P07.version = package.engines.node 217 | } else { 218 | cli.warn('P07 Minimum Node version is not compatible with minimum supported Node-RED Version Node v'+nminversion) 219 | scorecard.P07 = { 'test' : false} 220 | scorecard.P07.version = package.engines.node 221 | } 222 | }) 223 | } else { 224 | cli.warn('P07 Node version not declared in engines') 225 | scorecard.P07 = { 'test' :false} 226 | } 227 | }) 228 | .then(() => { 229 | return scorecard 230 | }) 231 | .catch((e) => { 232 | cli.error(e); 233 | }); 234 | 235 | } 236 | 237 | module.exports = checkpackage 238 | -------------------------------------------------------------------------------- /src/libs/checknodes.js: -------------------------------------------------------------------------------- 1 | const acorn = require('acorn'); 2 | const walk = require("acorn-walk"); 3 | const fs = require("fs"); 4 | const axios = require('axios') 5 | const pth = require("path"); 6 | const { pathToFileURL } = require('url'); 7 | const util = require('util'); 8 | 9 | 10 | function getNodeDefinitions(filename) { 11 | var regExp = /([\S\s]*?)<\/script>/ig; 12 | var content = fs.readFileSync(filename,'utf8'); 13 | // console.error(filename); 14 | var parts = []; 15 | var match; 16 | while((match = regExp.exec(content)) !== null) { 17 | var block = match[1]; 18 | parts.push(match[1]); 19 | } 20 | if (parts.length === 0) { 21 | throw new Error("No