├── .editorconfig ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CHANGES.md ├── README.md ├── bin └── noflo-nodejs ├── fbp-config.json ├── package.json ├── spec ├── .eslintrc.json ├── Repeat.yaml ├── cli.js ├── fbpSpec.js ├── fixtures │ ├── auto-save │ │ └── package.json │ ├── graph-as-component │ │ ├── components │ │ │ ├── Output.js │ │ │ └── Repeat.js │ │ ├── graphs │ │ │ └── helloin.fbp │ │ └── package.json │ ├── helloworld.fbp │ ├── library │ │ ├── components │ │ │ └── Output.js │ │ ├── graphs │ │ │ └── main.fbp │ │ └── package.json │ └── missingcomponent.fbp ├── library.js ├── mdns.js └── protocol.js └── src ├── autoSave.js ├── debug.js ├── library.js ├── noflo-nodejs.js ├── permissions.js ├── runtime.js ├── server.js ├── settings.js └── trace.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "no-console": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | versioning-strategy: increase-if-necessary 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Node.js Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2.3.4 13 | - uses: actions/setup-node@v2.1.5 14 | with: 15 | node-version: 12 16 | - run: npm install 17 | - run: npm test 18 | 19 | publish-npm: 20 | needs: build 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2.3.4 24 | - uses: actions/setup-node@v2.1.5 25 | with: 26 | node-version: 12 27 | registry-url: https://registry.npmjs.org/ 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Run test suite 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [12.x] 12 | steps: 13 | - uses: actions/checkout@v2.3.4 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v2.1.5 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - run: npm install 19 | - run: npm test 20 | env: 21 | CI: true 22 | merge-me: 23 | name: Auto-merge dependency updates 24 | needs: test 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: ridedott/merge-me-action@v2.10.43 28 | with: 29 | GITHUB_LOGIN: 'dependabot[bot]' 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | flowhub.json 3 | package-lock.json 4 | /*.pem 5 | /*.key 6 | /*.cert 7 | .flowtrace 8 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## noflo-nodejs 0.15.3 (12-01-2024) 2 | 3 | * Now using the community version of NoFlo UI by default 4 | 5 | ## noflo-nodejs 0.15.2 (29-12-2022) 6 | 7 | * Options are now passed to the preStart callback when used as a library 8 | 9 | ## noflo-nodejs 0.15.1 (14-01-2021) 10 | 11 | * Fixed graph properties when auto-saving 12 | 13 | ## noflo-nodejs 0.15.0 (14-12-2020) 14 | 15 | * Updated to the new Promise-based NoFlo 1.4 APIs 16 | 17 | ## noflo-nodejs 0.14.0 (25-11-2020) 18 | 19 | * Updated to the new built-in Flowtrace functionality in NoFlo 1.3.0 20 | 21 | ## noflo-nodejs 0.13.1 (16-11-2020) 22 | 23 | * Compatibility with the TypeScript version of fbp-graph 24 | 25 | ## noflo-nodejs 0.13.0 (02-10-2020) 26 | 27 | * Added support for WebRTC protocol as an alternative to WebSockets 28 | 29 | ## noflo-nodejs 0.12.5 (24-09-2020) 30 | 31 | * Graphs created by fbp-spec for test execution purposes no longer get auto-saved 32 | 33 | ## noflo-nodejs 0.12.4 (23-09-2020) 34 | 35 | * Specs sent by IDE via `setSource` are no also auto-saved 36 | 37 | ## noflo-nodejs 0.12.3 (23-09-2020) 38 | 39 | * Graph names are used without namespace when auto-saving 40 | 41 | ## noflo-nodejs 0.12.2 (17-09-2020) 42 | 43 | * Added auto-saving support for components written in TypeScript 44 | 45 | ## noflo-nodejs 0.12.1 (02-09-2020) 46 | 47 | * Modified SSL warnings to be written to STDOUT instead of STDERR to improve operation with fbp-spec 48 | 49 | ## noflo-nodejs 0.12.0 (02-09-2020) 50 | 51 | * Switched to the NoFlo 1.2 "network drives graph" mode for more accurate error handling 52 | * Added support for discovering the noflo-nodejs runtime via mDNS 53 | 54 | ## noflo-nodejs 0.11.1 (26-02-2019) 55 | 56 | Bugfixes: 57 | 58 | * noflo-nodejs library mode also subscribes to runtime now, so that `autoSave` mode works 59 | 60 | ## noflo-nodejs 0.11.0 (25-02-2019) 61 | 62 | New features: 63 | 64 | * noflo-nodejs is now much easier to embed in existing Node.js applications as a library 65 | * Added `--auto-save` option to enable saving edited graphs and components to disk automatically 66 | 67 | ## noflo-nodejs 0.10.1 (23-03-2018) 68 | 69 | New features: 70 | 71 | * Added `--open` option to control whether to open the runtime in user's IDE when started. Defaults to `true`. 72 | 73 | ## noflo-nodejs 0.10.0 (22-03-2018) 74 | 75 | New features: 76 | 77 | * Changed configuration file `flowhub.json` to be automatically saved from command-line arguments, persisting settings like runtime ID and secret between runs 78 | * Added support for encrypted WebSockets with `--tls-key` and `--tls-cert` options 79 | * When Flowtracing is enabled with the `--trace` option, uncaught exceptions also trigger the trace to be saved 80 | * Uncaught exceptions are now printed in a more readable format, with a truncated stack trace 81 | * The runtime registry URL can now be configured with the `--registry` option 82 | * Runtime registry pinging can be disabled with `--registry-ping=0` 83 | * If running on a desktop machine, the runtime will be automatically opened in user's default browser 84 | * Added compatibility with FBP Protocol version 0.7 85 | 86 | Removal of deprecated features: 87 | 88 | * Removed deprecated `noflo-nodejs-init` tool 89 | * Removed deprecated `--register` option. Your runtime can be registered with Flowhub by opening it there 90 | 91 | Internal changes: 92 | 93 | * Ported noflo-nodejs from CoffeeScript to ES6 94 | 95 | ## noflo-nodejs 0.9.0 (05-11-2017) 96 | 97 | * Compatibility with NoFlo 1.x 98 | 99 | ## noflo-nodejs 0.8.3 (12-05-2017) 100 | 101 | Deprecations 102 | 103 | * `noflo-nodejs --register` is **deprecated**, in favor of `flowhub-registry-register` or accessing the live URL. 104 | Support will be removed in 0.9.x. 105 | 106 | Breaking changes 107 | 108 | * Registration with Flowhub is no longer done by default. Must be enabled using `--register true` 109 | 110 | Additions 111 | 112 | * All options/features are now available on `noflo-nodejs`, so `noflo-nodejs-init` can be skipped 113 | * Support pinging Flowhub registry using option `--ping true` or envvar `NOFLO_RUNTIME_PING=true` 114 | * Support specifying runtime id using `NOFLO_RUNTIME_ID` envvar 115 | * Support passing runtime `id` in live URLs 116 | * Now gives a warning when no `secret` is passed, since will not be able to connect over WebSocket (no permissions) 117 | * Supports Flowhub "Edit as Project" feature, by passing component namespace via FBP protocol 118 | 119 | Bugfixes: 120 | 121 | * Fixed --version being called --0.8.x 122 | * Fixed --permissions not having effect on commandline 123 | 124 | ## noflo-nodejs 0.8.1 (2017-03-01) 125 | 126 | * Updated to **NoFlo 0.8.x** 127 | 128 | ## noflo-nodejs 0.7.0 (2016-03-31) 129 | 130 | * Updated to **NoFlo 0.7.x** 131 | 132 | ## noflo-nodejs 0.6.1 133 | 134 | * Support for [Flowtrace](https://github.com/flowbased/flowtrace) when using `--trace` option. 135 | * For long-running programs, flowtrace dumps can be triggered via `SIGUSR2` Unix signal 136 | 137 | ## noflo-nodejs 0.5.0 138 | 139 | * Implements FBP protocol version 0.5. Use of a runtime `secret` is now enforced 140 | * Support for `--debug`, `--verbose` and `--capture-exception` flags to tune logging behavior. 141 | * Support for `--host` and `--port` options, with defaults. Port also respects PORT envvar. 142 | * Support for multiple levels of `permissions` based on provided secret and configured 143 | * Support for a `--batch` mode, where process exists when graph stops. Intended to replace the `noflo` executable 144 | * Several FBP protocol fixes from noflo-runtime-base 0.5.x. Should now work with [fbp-spec](https://github.com/flowbased/fbp-spec) 145 | * Registration with Flowhub registry is optional, just warning 146 | 147 | ## noflo-nodejs 0.0.6 148 | 149 | * ... 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Command-line tool for running NoFlo programs on Node.js 2 | ================================= 3 | 4 | This tool is designed to be used together with the [NoFlo UI](https://app.noflojs.org/) development environment 5 | for running [NoFlo](http://noflojs.org/) networks on [Node.js](http://nodejs.org/). This tool runs your 6 | NoFlo programs, and provides a [FBP Protocol](https://flowbased.github.io/fbp-protocol/) interface over 7 | either WebSockets or WebRTC to tools like NoFlo UI and fbp-spec. 8 | 9 | This enables inspection of the state of the running NoFlo program, as well as live editing of the graph 10 | and components of your project. 11 | 12 | ## Prepare a project folder 13 | 14 | Start by setting up a local NoFlo Node.js project. For example: 15 | 16 | ```shell 17 | $ mkdir my-project 18 | $ cd my-project 19 | $ npm init 20 | $ npm install noflo --save 21 | $ npm install noflo-nodejs --save 22 | ``` 23 | 24 | Continue by installing whatever [NoFlo component libraries](https://www.npmjs.com/browse/keyword/noflo) you need, for example: 25 | 26 | ```shell 27 | $ npm install noflo-core --save 28 | ``` 29 | 30 | If you want, this is a great time to push your project to [GitHub](https://github.com/). 31 | 32 | ## Starting the runtime 33 | 34 | Once you have installed the runtime, it is time to start it: 35 | 36 | ```shell 37 | $ npx noflo-nodejs 38 | ``` 39 | 40 | This will start a WebSocket-based NoFlo Runtime server. When started, it will output an URL with the connection details needed by NoFlo UI. 41 | 42 | Copy paste this URL into the browser. The NoFlo UI IDE will open, and automatically connect to your runtime. 43 | To make changes hit 'Edit as Project'. You should be able to see available components and build up your system. 44 | 45 | ## Starting an existing graph 46 | 47 | If you want to run an existing graph, you can use the `--graph` option. 48 | 49 | ```shell 50 | noflo-nodejs --graph graphs/MyMainGraph.json 51 | ``` 52 | 53 | If you want the process to exit when the network stops, you can pass `--batch`. 54 | 55 | ## Typical project setup 56 | 57 | In most Node.js projects there will be three different setups you might want to have with NoFlo: development, testing, and production. Each of these can be easily expressed via NPM scripts in `package.json`: 58 | 59 | ```json 60 | { 61 | "name": "my-project", 62 | "scripts": { 63 | "dev": "noflo-nodejs --host localhost --auto-save --graph ./graphs/MyGraph.json", 64 | "test": "fbp-spec --secret test --address ws://localhost:3333 --command 'noflo-nodejs --port 3333 --capture-output --secret test --open false' spec/", 65 | "start": "noflo-nodejs --protocol webrtc --graph ./graphs/MyGraph.json" 66 | }, 67 | "dependencies": { 68 | ... 69 | }, 70 | ... 71 | } 72 | ``` 73 | 74 | With this setup you get the following: 75 | 76 | * By running `npm run dev`, noflo-nodejs will start your projects' main graph and open the NoFlo UI IDE in your browser. Any changes you make in NoFlo UI will be persisted on your local hard drive 77 | * By running `npm test`, [fbp-spec](https://github.com/flowbased/fbp-spec) will start a noflo-nodejs instance, connect to it, and run all of your local fbp-spec tests 78 | * By running `npm start`, noflo-nodejs starts your program, enabling remote debugging via the WebRTC protocol 79 | 80 | ## Host address autodetection for WebSockets 81 | 82 | By default `noflo-nodejs` will attempt to autodetect the public hostname/IP of your system. 83 | If this fails, you can specify `--host myhostname` manually. 84 | 85 | ## Securing the WebSocket connection 86 | 87 | The noflo-nodejs runtime can be secured using TLS. Place the key and certificate files somewhere that noflo-nodejs can read, and then start the runtime with the `--tls-key` and --tls-cert` options. 88 | 89 | For example, to use self-signed keys, you could do the following: 90 | 91 | ```shell 92 | $ openssl genrsa -out localhost.key 2048 93 | $ openssl req -new -x509 -key localhost.key -out localhost.cert -days 3650 -subj /CN=localhost 94 | $ noflo-nodejs --tls-key=localhost.key --tls-cert=localhost.cert 95 | ``` 96 | 97 | Note: browsers may refuse to connect to a WebSocket with a self-signed certificate by default. You can visit the runtime URL with your browser first to accept the certificate before connecting to it in the IDE. 98 | 99 | ## Peer-to-peer WebRTC connections 100 | 101 | If you want to use a peer-to-peer WebRTC connection instead of WebSockets, start `noflo-nodejs` with the argument `--protocol webrtc`. 102 | 103 | While slightly more complex and slower to start, WebRTC has some advantages over WebSockets: 104 | 105 | * Peer-to-peer connections can (sometimes) work through firewalls 106 | * No need for setting up TLS to secure communications between the runtime and the client 107 | 108 | By default noflo-nodejs uses [Flowhub's](https://flowhub.io) signalling server for negotiating the connection details between runtime and clients. You can supply a different [RTC Switchboard](https://github.com/rtc-io/rtc-switchboard) instance with the `--signaller` option. 109 | 110 | ## Debugging 111 | 112 | noflo-nodejs supports [flowtrace](https://github.com/flowbased/flowtrace) allows to trace & store the execution of the FBP program, 113 | so you can debug any issues that would occur. Specify `--trace` to enable tracing. 114 | 115 | ```shell 116 | $ noflo-nodejs --graph graphs/MyMainGraph.json --trace 117 | ``` 118 | 119 | If you are running in `--batch` mode, the file will be dumped to disk when the program terminates. 120 | Otherwise you can send the `SIGUSR2` to trigger dumping the file to disk. 121 | 122 | ```shell 123 | $ kill -SIGUSR2 $PID_OF_PROCESS 124 | ... Wrote flowtrace to: .flowtrace/1151020-12063-ami5vq.json 125 | ``` 126 | 127 | You can now use various flowtrace tools to introspect the data. 128 | For instance, you can get a human readable log using `flowtrace-show` 129 | 130 | ```shell 131 | $ npx flowtrace-show .flowtrace/1151020-12063-ami5vq.json 132 | 133 | -> IN repeat CONN 134 | -> IN repeat DATA hello world 135 | -> IN stdout CONN 136 | -> IN stdout DATA hello world 137 | -> IN repeat DISC 138 | -> IN stdout DISC 139 | ``` 140 | 141 | ## Signalling aliveness 142 | 143 | `noflo-nodejs` will ping Flowhub registry periodically to signal aliveness to IDE users. To disable this behavior, set `--registry-ping 0`. 144 | 145 | ## Persistent runtime configuration 146 | 147 | Settings can be loaded from a `flowhub.json` file. 148 | By default the configuration will be read from the current working directory, 149 | but you can change this by setting the `PROJECT_HOME` environment variable. 150 | 151 | This file will be automatically saved when you run noflo-nodejs, meaning that settings like runtime ID and secret will be persisted between runs. 152 | 153 | Environment variables and command-line options will override settings specified in config file. 154 | 155 | Since the values are often machine and/or user specific, you usually don't want to add this file to version control. 156 | 157 | ## Embedding runtime in an existing service 158 | 159 | In addition to running noflo-nodejs as a command-line program that starts and runs your NoFlo graphs, you can embed it into an existing Node.js application. Here is a quick example how to do it: 160 | 161 | ```javascript 162 | const runtime = require('noflo-nodejs'); 163 | 164 | // This function returns a Promise that resolves when the NoFlo runtime has started up 165 | startRuntime(graphPath, options = {}) { 166 | // Configure noflo-nodejs. Options here map roughly to the standard command-line arguments 167 | const settings = { 168 | id: '9f1432b1-a259-454a-bb67-e9d91525cc63', // Set an unique UUID for your application instance 169 | label: 'My cool app', 170 | baseDir: __dirname, 171 | host: 'localhost', 172 | port: 3569, 173 | ...options, 174 | }; 175 | return runtime(graphPath, settings); 176 | } 177 | ``` 178 | -------------------------------------------------------------------------------- /bin/noflo-nodejs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("../src/noflo-nodejs.js").main(); 3 | -------------------------------------------------------------------------------- /fbp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noflo-nodejs", 3 | "command": null, 4 | "host": "localhost", 5 | "port": 8080, 6 | "collection": "core", 7 | "version": "0.7" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noflo-nodejs", 3 | "version": "0.15.3", 4 | "description": "Command-line tool for running NoFlo programs on Node.js", 5 | "main": "src/library.js", 6 | "scripts": { 7 | "pretest": "eslint src/*.js spec/*.js", 8 | "test": "mocha --exit spec/*.js" 9 | }, 10 | "bin": { 11 | "noflo-nodejs": "./bin/noflo-nodejs" 12 | }, 13 | "author": "Henri Bergius ", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/noflo/noflo-nodejs.git" 18 | }, 19 | "dependencies": { 20 | "cli-color": "^2.0.0", 21 | "clone": "^2.1.1", 22 | "commander": "^6.1.0", 23 | "debounce-promise": "^3.1.2", 24 | "fbp-graph": "^0.7.0", 25 | "flowhub-registry": "^0.2.0", 26 | "mdns-js": "^1.0.3", 27 | "noflo": "^1.4.0", 28 | "noflo-runtime-base": "^0.13.1", 29 | "noflo-runtime-webrtc": "^0.13.0", 30 | "noflo-runtime-websocket": "^0.13.0", 31 | "open": "^8.0.1", 32 | "password-generator": "^2.2.0", 33 | "slug": "^4.0.2", 34 | "uuid": "^8.1.0" 35 | }, 36 | "devDependencies": { 37 | "chai": "^4.0.0", 38 | "eslint": "^8.14.0", 39 | "eslint-config-airbnb-base": "^15.0.0", 40 | "eslint-plugin-import": "^2.9.0", 41 | "eslint-plugin-mocha": "^10.1.0", 42 | "fbp-client": "^0.4.1", 43 | "fbp-protocol": "^0.9.8", 44 | "fbp-protocol-healthcheck": "^1.1.0", 45 | "fbp-spec": "^0.8.0", 46 | "mocha": "^10.0.0", 47 | "noflo-core": ">= 0.6.0" 48 | }, 49 | "keywords": [ 50 | "noflo", 51 | "ecosystem:noflo" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /spec/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "mocha" 5 | ], 6 | "env": { 7 | "mocha": true 8 | }, 9 | "rules": { 10 | "func-names": 0, 11 | "no-console": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spec/Repeat.yaml: -------------------------------------------------------------------------------- 1 | topic: "core/Repeat" 2 | cases: 3 | - 4 | name: 'sending a boolean' 5 | assertion: 'should repeat the same' 6 | inputs: 7 | in: true 8 | expect: 9 | out: 10 | equals: true 11 | - 12 | name: 'sending a number' 13 | assertion: 'should repeat the same' 14 | inputs: 15 | in: 1000 16 | expect: 17 | out: 18 | equals: 1000 19 | - 20 | name: 'sending a string' 21 | assertion: 'should repeat the same' 22 | inputs: 23 | in: "my string" 24 | expect: 25 | out: 26 | equals: "my string" 27 | -------------------------------------------------------------------------------- /spec/cli.js: -------------------------------------------------------------------------------- 1 | const { exec, spawn } = require('child_process'); 2 | const { expect } = require('chai'); 3 | const { promisify } = require('util'); 4 | const { v4: uuid } = require('uuid'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const fbpHealthCheck = require('fbp-protocol-healthcheck'); 8 | const fbpClient = require('fbp-client'); 9 | const fbpGraph = require('fbp-graph'); 10 | 11 | function healthCheck(address, callback) { 12 | fbpHealthCheck(address) 13 | .then(() => callback(), () => healthCheck(address, callback)); 14 | } 15 | 16 | function waitFor(time) { 17 | return new Promise((resolve) => { 18 | setTimeout(resolve, time); 19 | }); 20 | } 21 | 22 | describe('noflo-nodejs CLI', () => { 23 | const prog = path.resolve(__dirname, '../bin/noflo-nodejs'); 24 | const runtimeSecret = process.env.FBP_PROTOCOL_SECRET || 'noflo-nodejs'; 25 | describe('--graph=helloworld.fbp --batch --trace', () => { 26 | let stdout = ''; 27 | let stderr = ''; 28 | const graph = path.resolve(__dirname, './fixtures/helloworld.fbp'); 29 | it('should execute graph and exit', (done) => { 30 | const cmd = `${prog} --graph=${graph} --batch --trace --open=false`; 31 | exec(cmd, (err, o, e) => { 32 | if (err) { 33 | done(err); 34 | return; 35 | } 36 | stdout = o; 37 | stderr = e; 38 | done(); 39 | }); 40 | }).timeout(10 * 1000); 41 | it('should have written the expected output', () => { 42 | expect(stdout).to.contain('hello world'); 43 | }); 44 | it('should not have written any errors', () => { 45 | expect(stderr).to.eql(''); 46 | }); 47 | it('should have produced a flowtrace', () => { 48 | expect(stdout.toLowerCase()).to.include('wrote flowtrace to:'); 49 | }); 50 | }); 51 | describe('--graph=missingcomponent.fbp', () => { 52 | const graph = path.resolve(__dirname, './fixtures/missingcomponent.fbp'); 53 | it('should fail with an error telling about the missing component', (done) => { 54 | const cmd = `${prog} --graph=${graph} --open=false`; 55 | exec(cmd, (err) => { 56 | expect(err.message).to.contain('Component foo/Bar not available'); 57 | done(); 58 | }); 59 | }).timeout(10 * 1000); 60 | }); 61 | describe('--graph=helloin.fbp', () => { 62 | const baseDir = path.resolve(__dirname, './fixtures/graph-as-component'); 63 | const graph = path.resolve(baseDir, './graphs/helloin.fbp'); 64 | let runtimeProcess; 65 | let runtimeClient; 66 | before('start runtime', (done) => { 67 | runtimeProcess = spawn(prog, [ 68 | '--host=localhost', 69 | '--port=3470', 70 | '--open=false', 71 | `--secret=${runtimeSecret}`, 72 | `--base-dir=${baseDir}`, 73 | `--graph=${graph}`, 74 | ]); 75 | runtimeProcess.stdout.pipe(process.stdout); 76 | runtimeProcess.stderr.pipe(process.stderr); 77 | healthCheck('ws://localhost:3470', done); 78 | }); 79 | after('stop runtime', (done) => { 80 | if (!runtimeProcess) { 81 | done(); 82 | return; 83 | } 84 | process.kill(runtimeProcess.pid); 85 | done(); 86 | }); 87 | it('should be possible to connect', () => fbpClient({ 88 | address: 'ws://localhost:3470', 89 | protocol: 'websocket', 90 | secret: runtimeSecret, 91 | }) 92 | .then((c) => { 93 | runtimeClient = c; 94 | return c.connect(); 95 | })); 96 | it('should have marked the graph as the main', () => { 97 | expect(runtimeClient.definition.graph).to.equal('graph-as-component/HelloIn'); 98 | }); 99 | it('should be possible to get graph sources', () => runtimeClient 100 | .protocol.component.getsource({ 101 | name: 'graph-as-component/HelloIn', 102 | })); 103 | it('should be possible to get the component list', () => runtimeClient 104 | .protocol.component.list() 105 | .then((components) => { 106 | const expectedNames = [ 107 | 'graph-as-component/Repeat', 108 | 'graph-as-component/Output', 109 | 'Graph', 110 | 'graph-as-component/HelloIn', 111 | ]; 112 | const names = components.map((c) => c.name); 113 | names.sort(); 114 | expectedNames.sort(); 115 | expect(names).to.eql(expectedNames); 116 | })); 117 | it('should be possible to get status of the running network', () => runtimeClient 118 | .protocol.network.getstatus({ 119 | graph: 'graph-as-component/HelloIn', 120 | })); 121 | }); 122 | describe('--auto-save', () => { 123 | const baseDir = path.resolve(__dirname, './fixtures/auto-save'); 124 | const readFile = promisify(fs.readFile); 125 | const unlink = promisify(fs.unlink); 126 | let runtimeProcess; 127 | let runtimeClient; 128 | before('start runtime', (done) => { 129 | runtimeProcess = spawn(prog, [ 130 | '--host=localhost', 131 | '--port=3471', 132 | '--open=false', 133 | `--base-dir=${baseDir}`, 134 | `--secret=${runtimeSecret}`, 135 | '--auto-save=true', 136 | ]); 137 | runtimeProcess.stdout.pipe(process.stdout); 138 | runtimeProcess.stderr.pipe(process.stderr); 139 | healthCheck('ws://localhost:3471', done); 140 | }); 141 | after('stop runtime', (done) => { 142 | if (!runtimeProcess) { 143 | done(); 144 | return; 145 | } 146 | process.kill(runtimeProcess.pid); 147 | done(); 148 | }); 149 | it('should be possible to connect', () => fbpClient({ 150 | address: 'ws://localhost:3471', 151 | protocol: 'websocket', 152 | secret: runtimeSecret, 153 | }) 154 | .then((c) => { 155 | runtimeClient = c; 156 | return c.connect(); 157 | })); 158 | describe('setting component sources', () => { 159 | const source = `const noflo = require('noflo'); 160 | exports.getComponent = () => { 161 | const c = new noflo.Component(); 162 | c.inPorts.add('in'); 163 | c.outPorts.add('out'); 164 | c.process((input, output) => { 165 | output.sendDone(input.getData() + 2); 166 | }); 167 | return c; 168 | };`; 169 | const spec = `topic: auto-save/Plusser 170 | cases: 171 | - 172 | name: 'sending a boolean' 173 | assertion: 'should repeat the same' 174 | inputs: 175 | in: true 176 | expect: 177 | out: 178 | equals: true`; 179 | const componentPath = path.resolve(__dirname, './fixtures/auto-save/components/Plusser.js'); 180 | const specPath = path.resolve(__dirname, './fixtures/auto-save/spec/Plusser.yaml'); 181 | let plusserFound = false; 182 | after('clean up file', () => { 183 | if (!plusserFound) { 184 | return Promise.resolve(); 185 | } 186 | return unlink(componentPath) 187 | .then(() => unlink(specPath)); 188 | }); 189 | it('should be possible to send the source code to the runtime', () => runtimeClient 190 | .protocol.component.source({ 191 | name: 'Plusser', 192 | library: 'auto-save', 193 | language: 'javascript', 194 | tests: spec, 195 | code: source, 196 | }) 197 | .then(() => new Promise((resolve) => { 198 | setTimeout(() => { 199 | resolve(); 200 | }, 200); 201 | }))); 202 | it('should have saved the source code to the fixture folder', () => readFile( 203 | componentPath, 204 | 'utf-8', 205 | ) 206 | .then((contents) => { 207 | plusserFound = true; 208 | expect(contents).to.eql(source); 209 | })); 210 | it('should have saved the fbp-spec file to the fixture folder', () => readFile( 211 | specPath, 212 | 'utf-8', 213 | ) 214 | .then((contents) => { 215 | expect(contents).to.eql(spec); 216 | })); 217 | }); 218 | describe('setting component sources outside of project', () => { 219 | let source; 220 | const componentPath = path.resolve(__dirname, './fixtures/auto-save/components/Output.js'); 221 | before('read source code', () => readFile( 222 | path.resolve(__dirname, '../node_modules/noflo-core/components/Output.js'), 223 | 'utf-8', 224 | ) 225 | .then((contents) => { 226 | source = contents; 227 | })); 228 | it('should be possible to send the source code to the runtime', () => runtimeClient 229 | .protocol.component.source({ 230 | name: 'Output', 231 | library: 'core', 232 | language: 'javascript', 233 | code: source, 234 | })); 235 | it('should not have saved the source code to the fixture folder', () => readFile( 236 | componentPath, 237 | 'utf-8', 238 | ) 239 | .then( 240 | () => Promise.reject(new Error('core/Output was saved unexpectedly')), 241 | () => Promise.resolve('No Output.js found, as expected'), 242 | )); 243 | }); 244 | describe('editing a graph without namespaced name', () => { 245 | const graphName = 'Test'; 246 | const graphPath = path.resolve(__dirname, `./fixtures/auto-save/graphs/${graphName}.json`); 247 | const graphInstance = new fbpGraph.Graph(graphName); 248 | let graphFound = false; 249 | before('set up graph', () => { 250 | graphInstance.setProperties({ 251 | ...graphInstance.properties, 252 | library: 'auto-save', 253 | id: graphName, 254 | main: false, 255 | environment: { 256 | type: 'noflo-nodejs', 257 | }, 258 | }); 259 | graphInstance.addNode('one', 'auto-save/Plusser'); 260 | graphInstance.addNode('two', 'core/Output'); 261 | graphInstance.addEdge('one', 'out', 'two', 'in'); 262 | graphInstance.addInitial(1, 'one', 'in'); 263 | }); 264 | after('clean up file', () => { 265 | if (!graphFound) { 266 | return Promise.resolve(); 267 | } 268 | return unlink(graphPath); 269 | }); 270 | it('should be possible to send a graph to the runtime', () => runtimeClient 271 | .protocol.graph.send(graphInstance, false)); 272 | it('should have saved the graph JSON to the fixture folder', () => waitFor(200) 273 | .then(() => readFile( 274 | graphPath, 275 | 'utf-8', 276 | )) 277 | .then((contents) => { 278 | graphFound = true; 279 | const originalGraphJson = JSON.parse(JSON.stringify(graphInstance.toJSON())); 280 | delete originalGraphJson.properties.id; 281 | const graphJson = JSON.parse(contents); 282 | expect(graphJson).to.eql(originalGraphJson); 283 | })); 284 | }); 285 | describe('editing a graph with namespaced name', () => { 286 | const graphName = 'main'; 287 | const graphPath = path.resolve(__dirname, './fixtures/auto-save/graphs/main.json'); 288 | const graphInstance = new fbpGraph.Graph(graphName); 289 | let graphFound = false; 290 | before('set up graph', () => { 291 | graphInstance.setProperties({ 292 | ...graphInstance.properties, 293 | library: 'auto-save', 294 | id: `default/${graphName}`, 295 | main: true, 296 | environment: { 297 | type: 'noflo-nodejs', 298 | }, 299 | }); 300 | graphInstance.addNode('one', 'auto-save/Plusser'); 301 | graphInstance.addNode('two', 'core/Output'); 302 | graphInstance.addEdge('one', 'out', 'two', 'in'); 303 | graphInstance.addInitial(1, 'one', 'in'); 304 | }); 305 | after('clean up file', () => { 306 | if (!graphFound) { 307 | return Promise.resolve(); 308 | } 309 | return unlink(graphPath); 310 | }); 311 | it('should be possible to send a graph to the runtime', () => runtimeClient 312 | .protocol.graph.send({ 313 | ...graphInstance, 314 | name: 'default/main', 315 | }, true)); 316 | it('should have saved the graph JSON to the fixture folder', () => waitFor(200) 317 | .then(() => readFile( 318 | graphPath, 319 | 'utf-8', 320 | )) 321 | .then((contents) => { 322 | graphFound = true; 323 | const originalGraphJson = JSON.parse(JSON.stringify(graphInstance.toJSON())); 324 | delete originalGraphJson.properties.id; 325 | const graphJson = JSON.parse(contents); 326 | expect(graphJson).to.eql(originalGraphJson); 327 | })); 328 | }); 329 | }); 330 | describe('--protocol=webrtc', () => { 331 | const baseDir = path.resolve(__dirname, './fixtures/graph-as-component'); 332 | const graph = path.resolve(baseDir, './graphs/helloin.fbp'); 333 | let runtimeProcess; 334 | let runtimeClient; 335 | const runtimeId = uuid(); 336 | before('start runtime', (done) => { 337 | runtimeProcess = spawn(prog, [ 338 | '--open=false', 339 | `--id=${runtimeId}`, 340 | '--protocol=webrtc', 341 | `--secret=${runtimeSecret}`, 342 | `--base-dir=${baseDir}`, 343 | `--graph=${graph}`, 344 | ]); 345 | runtimeProcess.stdout.pipe(process.stdout); 346 | runtimeProcess.stderr.pipe(process.stderr); 347 | done(); 348 | }); 349 | it('should be possible to connect', function () { 350 | this.timeout(6000); 351 | return fbpClient({ 352 | address: runtimeId, 353 | protocol: 'webrtc', 354 | secret: runtimeSecret, 355 | }, { 356 | connectionTimeout: 5000, 357 | }) 358 | .then((c) => { 359 | runtimeClient = c; 360 | return c.connect(); 361 | }); 362 | }); 363 | it('should have marked the graph as the main', () => { 364 | expect(runtimeClient.definition.graph).to.equal('graph-as-component/HelloIn'); 365 | }); 366 | it('should be possible to get graph sources', () => runtimeClient 367 | .protocol.component.getsource({ 368 | name: 'graph-as-component/HelloIn', 369 | })); 370 | after('stop runtime', (done) => { 371 | if (!runtimeProcess) { 372 | done(); 373 | return; 374 | } 375 | process.kill(runtimeProcess.pid); 376 | done(); 377 | }); 378 | }); 379 | }); 380 | -------------------------------------------------------------------------------- /spec/fbpSpec.js: -------------------------------------------------------------------------------- 1 | const { spawn, exec } = require('child_process'); 2 | const path = require('path'); 3 | const fbpHealthCheck = require('fbp-protocol-healthcheck'); 4 | 5 | function healthCheck(callback) { 6 | fbpHealthCheck('ws://localhost:8081') 7 | .then(() => callback(), () => healthCheck(callback)); 8 | } 9 | 10 | describe('FBP Spec Compatibility', () => { 11 | const prog = path.resolve(__dirname, '../bin/noflo-nodejs'); 12 | const tester = path.resolve(__dirname, '../node_modules/.bin/fbp-spec'); 13 | const runtimeSecret = process.env.FBP_PROTOCOL_SECRET || 'noflo-nodejs'; 14 | let progProcess; 15 | before('start runtime', (done) => { 16 | progProcess = spawn(prog, [ 17 | '--host=localhost', 18 | '--port=8081', 19 | '--open=false', 20 | '--trace=false', 21 | `--secret=${runtimeSecret}`, 22 | ]); 23 | healthCheck(done); 24 | }); 25 | after('stop runtime', (done) => { 26 | if (!progProcess) { 27 | done(); 28 | return; 29 | } 30 | process.kill(progProcess.pid); 31 | done(); 32 | }); 33 | it('should pass the test suite', (done) => { 34 | exec( 35 | `${tester} --secret ${runtimeSecret} --address ws://localhost:8081 spec/*.yaml`, 36 | (err, stdout, stderr) => { 37 | console.log(stdout); 38 | if (stderr) { 39 | console.error(stderr); 40 | } 41 | done(err); 42 | }, 43 | ); 44 | }).timeout(60000); 45 | }); 46 | -------------------------------------------------------------------------------- /spec/fixtures/auto-save/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-save" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/graph-as-component/components/Output.js: -------------------------------------------------------------------------------- 1 | const noflo = require('noflo'); 2 | 3 | exports.getComponent = () => { 4 | const c = new noflo.Component(); 5 | c.inPorts.add('in'); 6 | c.outPorts.add('out'); 7 | return c.process((input, output) => { 8 | console.log(input.getData('in')); 9 | output.done(); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /spec/fixtures/graph-as-component/components/Repeat.js: -------------------------------------------------------------------------------- 1 | const noflo = require('noflo'); 2 | 3 | exports.getComponent = () => { 4 | const c = new noflo.Component(); 5 | c.inPorts.add('in'); 6 | c.outPorts.add('out'); 7 | return c.process((input, output) => { 8 | output.sendDone(input.getData('in')); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /spec/fixtures/graph-as-component/graphs/helloin.fbp: -------------------------------------------------------------------------------- 1 | # @name HelloIn 2 | INPORT=repeat.IN:IN 3 | repeat(graph-as-component/Repeat) OUT -> IN stdout(graph-as-component/Output) 4 | -------------------------------------------------------------------------------- /spec/fixtures/graph-as-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-as-component" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/helloworld.fbp: -------------------------------------------------------------------------------- 1 | 2 | 'hello world' -> IN repeat(core/Repeat) OUT -> IN stdout(core/Output) 3 | -------------------------------------------------------------------------------- /spec/fixtures/library/components/Output.js: -------------------------------------------------------------------------------- 1 | const noflo = require('noflo'); 2 | 3 | function output(value) { 4 | console.log('Got value', value); 5 | } 6 | 7 | exports.getComponent = () => noflo.asComponent(output); 8 | -------------------------------------------------------------------------------- /spec/fixtures/library/graphs/main.fbp: -------------------------------------------------------------------------------- 1 | 1 -> VALUE Show(library/Output) 2 | -------------------------------------------------------------------------------- /spec/fixtures/library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "library" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/missingcomponent.fbp: -------------------------------------------------------------------------------- 1 | 'hello world' -> IN repeat(foo/Bar) OUT -> IN stdout(core/Output) 2 | -------------------------------------------------------------------------------- /spec/library.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fbpHealthCheck = require('fbp-protocol-healthcheck'); 3 | const fbpClient = require('fbp-client'); 4 | const { expect } = require('chai'); 5 | const library = require('../src/library'); 6 | const server = require('../src/server'); 7 | 8 | describe('noflo-nodejs as library', () => { 9 | let rt; 10 | let rtClient; 11 | after('stop the runtime', () => rtClient.disconnect() 12 | .then(() => server.stop(rt))); 13 | it('should be able to start a fixture project', () => library( 14 | path.resolve(__dirname, './fixtures/library/graphs/main.fbp'), 15 | { 16 | hostname: 'localhost', 17 | port: 3571, 18 | secret: 'foo', 19 | baseDir: path.resolve(__dirname, './fixtures/library'), 20 | }, 21 | ) 22 | .then((runtime) => { 23 | rt = runtime; 24 | })); 25 | it('should have started a WebSocket runtime', () => fbpHealthCheck( 26 | 'ws://localhost:3571', 27 | )); 28 | it('should be possible to connect to the runtime', () => fbpClient({ 29 | address: 'ws://localhost:3571', 30 | protocol: 'websocket', 31 | secret: 'foo', 32 | }) 33 | .then((c) => { 34 | rtClient = c; 35 | return c.connect(); 36 | })); 37 | it('runtime should have declared its main graph', () => { 38 | expect(rtClient.definition.graph).to.equal('library/main'); 39 | }); 40 | it('should return graph sources when requested', () => rtClient 41 | .protocol.component.getsource({ 42 | name: rtClient.definition.graph, 43 | })); 44 | it('should return network status when requested', () => rtClient 45 | .protocol.network.getstatus({ 46 | graph: rtClient.definition.graph, 47 | })); 48 | }); 49 | -------------------------------------------------------------------------------- /spec/mdns.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const mdns = require('mdns-js'); 3 | const { v4: uuid } = require('uuid'); 4 | const fbpHealthCheck = require('fbp-protocol-healthcheck'); 5 | const { expect } = require('chai'); 6 | const library = require('../src/library'); 7 | const server = require('../src/server'); 8 | 9 | describe('noflo-nodejs mDNS discovery', () => { 10 | let rt; 11 | let browser; 12 | before('prepare mDNS', (done) => { 13 | browser = mdns.createBrowser(); 14 | browser.on('ready', () => { 15 | done(); 16 | }); 17 | }); 18 | after('stop mDNS', () => { 19 | browser.stop(); 20 | }); 21 | after('stop the runtime', () => server.stop(rt)); 22 | it('should be able to start a fixture project', () => library( 23 | path.resolve(__dirname, './fixtures/library/graphs/main.fbp'), 24 | { 25 | id: uuid(), 26 | host: 'localhost', 27 | port: 3571, 28 | secret: 'foo', 29 | baseDir: path.resolve(__dirname, './fixtures/library'), 30 | mdns: true, 31 | }, 32 | ) 33 | .then((runtime) => { 34 | rt = runtime; 35 | })); 36 | it('should have started a WebSocket runtime', () => fbpHealthCheck( 37 | 'ws://localhost:3571', 38 | )); 39 | it('should be discoverable via mDNS', (done) => { 40 | browser.discover(); 41 | browser.on('update', (data) => { 42 | if (!data.fullname || data.fullname.indexOf('fbp-ws') === -1) { 43 | // Unrelated service 44 | return; 45 | } 46 | expect(data.txt).to.include('type=noflo-nodejs'); 47 | if (data.txt.indexOf(`id=${rt.options.id}`) === -1) { 48 | // Different runtime instance 49 | return; 50 | } 51 | expect(data.txt).to.include(`id=${rt.options.id}`); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /spec/protocol.js: -------------------------------------------------------------------------------- 1 | const { spawn, exec } = require('child_process'); 2 | const path = require('path'); 3 | const fbpHealthCheck = require('fbp-protocol-healthcheck'); 4 | 5 | function healthCheck(callback) { 6 | fbpHealthCheck('ws://localhost:8080') 7 | .then(() => callback(), () => healthCheck(callback)); 8 | } 9 | 10 | describe('FBP Protocol Compatibility', () => { 11 | const prog = path.resolve(__dirname, '../bin/noflo-nodejs'); 12 | const tester = path.resolve(__dirname, '../node_modules/.bin/fbp-test --colors'); 13 | const runtimeSecret = process.env.FBP_PROTOCOL_SECRET || 'noflo-nodejs'; 14 | let progProcess; 15 | before('start runtime', (done) => { 16 | progProcess = spawn(prog, [ 17 | '--host=localhost', 18 | '--port=8080', 19 | '--open=false', 20 | `--secret=${runtimeSecret}`, 21 | ]); 22 | healthCheck(done); 23 | }); 24 | after('stop runtime', (done) => { 25 | if (!progProcess) { 26 | done(); 27 | return; 28 | } 29 | process.kill(progProcess.pid); 30 | done(); 31 | }); 32 | it('should pass the test suite', (done) => { 33 | exec(tester, { 34 | env: { 35 | ...process.env, 36 | FBP_PROTOCOL_SECRET: runtimeSecret, 37 | }, 38 | }, (err, stdout, stderr) => { 39 | console.log(stdout); 40 | console.error(stderr); 41 | done(err); 42 | }); 43 | }).timeout(60000); 44 | }); 45 | -------------------------------------------------------------------------------- /src/autoSave.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const debounce = require('debounce-promise'); 5 | 6 | const stat = promisify(fs.stat); 7 | const mkdir = promisify(fs.mkdir); 8 | const writeFile = promisify(fs.writeFile); 9 | 10 | function ensureDir(dirName, rt) { 11 | const directoryPath = path.resolve(rt.options.baseDir, `./${dirName}`); 12 | return stat(directoryPath) 13 | .catch(() => mkdir(directoryPath, { 14 | recursive: true, 15 | }) 16 | .then(() => stat(directoryPath))) 17 | .then((stats) => { 18 | if (!stats.isDirectory()) { 19 | return Promise.reject(new Error(`${directoryPath} is not a directory`)); 20 | } 21 | return Promise.resolve(directoryPath); 22 | }); 23 | } 24 | 25 | function getComponentPath(component, directoryPath) { 26 | const componentName = path.basename(component.name); 27 | return new Promise((resolve, reject) => { 28 | let suffix; 29 | switch (component.language) { 30 | case 'yaml': 31 | suffix = 'yaml'; 32 | break; 33 | case 'coffeescript': 34 | suffix = 'coffee'; 35 | break; 36 | case 'typescript': 37 | suffix = 'ts'; 38 | break; 39 | case 'javascript': 40 | case 'es2015': 41 | suffix = 'js'; 42 | break; 43 | default: 44 | reject(new Error(`Unsupported component language ${component.language}`)); 45 | } 46 | resolve(path.resolve(directoryPath, `${componentName}.${suffix}`)); 47 | }); 48 | } 49 | 50 | function getGraphPath(name, graph, directoryPath) { 51 | const graphName = path.basename(name); 52 | return Promise.resolve(path.resolve(directoryPath, `${graphName}.json`)); 53 | } 54 | 55 | function fileDisplayPath(filePath, rt) { 56 | return path.relative(rt.options.baseDir, filePath); 57 | } 58 | 59 | function saveSpec(component, rt) { 60 | if (!component.tests) { 61 | return Promise.resolve(); 62 | } 63 | // Default assumption is that specs are in the same language as the source 64 | let { language } = component; 65 | if (component.tests.indexOf('topic: ') !== -1 && component.tests.indexOf('cases:') !== -1) { 66 | // Reasonable guess is that this is an fbp-spec file. 67 | // Should probably try parsing YAML to be sure. 68 | language = 'yaml'; 69 | } 70 | return ensureDir('spec', rt) 71 | .then((directoryPath) => getComponentPath({ 72 | ...component, 73 | language, 74 | }, directoryPath)) 75 | .then((filePath) => writeFile(filePath, component.tests) 76 | .then(() => { 77 | console.log(`Saved ${fileDisplayPath(filePath, rt)}`); 78 | })); 79 | } 80 | 81 | function saveComponent(component, rt) { 82 | if (component.library !== rt.options.namespace) { 83 | // Skip saving components outside of project namespace 84 | return Promise.resolve(); 85 | } 86 | return ensureDir('components', rt) 87 | .then((directoryPath) => getComponentPath(component, directoryPath)) 88 | .then((filePath) => writeFile(filePath, component.code) 89 | .then(() => { 90 | console.log(`Saved ${fileDisplayPath(filePath, rt)}`); 91 | return saveSpec(component, rt); 92 | })); 93 | } 94 | 95 | function saveGraph(name, graph, rt) { 96 | if (graph.properties.id && graph.properties.id.indexOf('fixture.') === 0) { 97 | // fbp-spec graph, should not be saved 98 | return Promise.resolve(); 99 | } 100 | if (graph.properties.library && graph.properties.library !== rt.options.namespace) { 101 | // Skip saving graphs outside of project namespace 102 | return Promise.resolve(); 103 | } 104 | return ensureDir('graphs', rt) 105 | .then((directoryPath) => getGraphPath(name, graph, directoryPath)) 106 | .then((filePath) => { 107 | const graphJSON = graph.toJSON(); 108 | if (name && graphJSON.properties) { 109 | graphJSON.properties.name = path.basename(name); 110 | } 111 | if (graphJSON.properties && graphJSON.properties.id) { 112 | delete graphJSON.properties.id; 113 | } 114 | if (graphJSON.properties && !graphJSON.properties.environment) { 115 | graphJSON.properties.environment = { 116 | type: 'noflo-nodejs', 117 | }; 118 | } 119 | return writeFile(filePath, JSON.stringify(graphJSON, null, 4)) 120 | .then(() => { 121 | console.log(`Saved ${fileDisplayPath(filePath, rt)}`); 122 | }); 123 | }); 124 | } 125 | 126 | const saveComponentDebounced = debounce(saveComponent, 100); 127 | const saveGraphDebounced = debounce(saveGraph, 100); 128 | 129 | exports.subscribe = (rt) => { 130 | if (typeof rt.component.on !== 'function' || typeof rt.graph.on !== 'function') { 131 | console.log('Skipping auto-save due to noflo-runtime-base being too old'); 132 | return; 133 | } 134 | 135 | rt.component.on('updated', (component) => { 136 | saveComponentDebounced(component, rt) 137 | .catch((e) => { 138 | console.error(e); 139 | process.exit(1); 140 | }); 141 | }); 142 | rt.graph.on('updated', ({ name, graph }) => { 143 | saveGraphDebounced(name, graph, rt) 144 | .catch((e) => { 145 | console.error(e); 146 | process.exit(1); 147 | }); 148 | }); 149 | }; 150 | -------------------------------------------------------------------------------- /src/debug.js: -------------------------------------------------------------------------------- 1 | const clc = require('cli-color'); 2 | const path = require('path'); 3 | 4 | function networkIdentifier(network, options) { 5 | if (!network.graph || !options.graph) { 6 | return 'Unknown network'; 7 | } 8 | if (!network.graph.name) { 9 | return path.basename(options.graph); 10 | } 11 | return network.graph.name; 12 | } 13 | 14 | function packetIdentifier(ip) { 15 | let result = ''; 16 | if (ip.subgraph) { 17 | result += `${clc.magenta.italic(ip.subgraph.join(':'))} `; 18 | } 19 | // TODO: Would be nice to utilize graph edge colors 20 | result += clc.blue.italic(ip.id); 21 | return result; 22 | } 23 | 24 | function formatTime(ms) { 25 | const seconds = ms / 1000; 26 | if (seconds < 60) { 27 | return `${seconds} seconds`; 28 | } 29 | const minutes = Math.floor(seconds / 60); 30 | const remaining = Math.floor(seconds % 60); 31 | if (remaining < 1) { 32 | return `${minutes} minutes`; 33 | } 34 | return `${minutes} minutes, ${remaining} seconds`; 35 | } 36 | 37 | exports.add = (network, options) => { 38 | network.on('start', ({ start }) => { 39 | console.log(`${clc.green(networkIdentifier(network, options))} started on ${start}`); 40 | }); 41 | network.on('end', ({ end, uptime }) => { 42 | console.log(`${clc.green(networkIdentifier(network, options))} ended on ${end} (uptime ${formatTime(uptime)})`); 43 | }); 44 | network.on('ip', (ip) => { 45 | if (ip.subGraph && !options.verbose) { 46 | return; 47 | } 48 | switch (ip.type) { 49 | case 'openbracket': { 50 | console.log(`${packetIdentifier(ip)} ${clc.cyan(`< ${ip.data}`)}`); 51 | return; 52 | } 53 | case 'closebracket': { 54 | console.log(`${packetIdentifier(ip)} ${clc.cyan(`> ${ip.data}`)}`); 55 | return; 56 | } 57 | case 'data': { 58 | if (options.verbose) { 59 | console.log(`${packetIdentifier(ip)} ${clc.green('DATA')}`, ip.data); 60 | return; 61 | } 62 | console.log(`${packetIdentifier(ip)} ${clc.green('DATA')}`); 63 | return; 64 | } 65 | default: { 66 | console.log(`${packetIdentifier(ip)} ${clc.cyan(`${ip.type} ${ip.data}`)}`); 67 | } 68 | } 69 | }); 70 | // TODO: Log other network events? 71 | // - process-error 72 | // - icon 73 | }; 74 | 75 | exports.showError = (err) => { 76 | let stack = err.stack.split('\n'); 77 | console.error(clc.red(err.message)); 78 | if (stack.length > 10) { 79 | stack = stack.slice(1, 9); 80 | stack.push(' ...'); 81 | } 82 | console.error(clc.cyan(stack.join('\n'))); 83 | }; 84 | -------------------------------------------------------------------------------- /src/library.js: -------------------------------------------------------------------------------- 1 | const server = require('./server'); 2 | const runtime = require('./runtime'); 3 | const settings = require('./settings'); 4 | 5 | module.exports = (mainGraph, options = {}, preStart = () => Promise.resolve()) => settings 6 | .loadForLibrary(options) 7 | .then((config) => server.create(config) 8 | // Subscribe runtime to signals 9 | .then((rt) => runtime.subscribe(rt, config)) 10 | // Execute pre-start hook, like for example custom component loader 11 | .then((rt) => preStart(rt, config).then(() => rt)) 12 | // Load and set up a main graph 13 | .then((rt) => runtime.startGraph(mainGraph, rt, config)) 14 | // Start the WebSocket server 15 | .then((rt) => server.start(rt, config)) 16 | // Register service 17 | .then((rt) => runtime.advertiseMdns(rt, options)) 18 | .then((rt) => runtime.ping(rt, config))); 19 | -------------------------------------------------------------------------------- /src/noflo-nodejs.js: -------------------------------------------------------------------------------- 1 | const open = require('open'); 2 | const settings = require('./settings'); 3 | const server = require('./server'); 4 | const runtime = require('./runtime'); 5 | const debug = require('./debug'); 6 | 7 | exports.main = () => { 8 | settings.load() 9 | .then((options) => server.create(options) 10 | .then((rt) => runtime.subscribe(rt, options)) 11 | .then((rt) => runtime.startGraph(options.graph, rt, options)) 12 | .then((rt) => server.start(rt, options)) 13 | .then((rt) => runtime.advertiseMdns(rt, options)) 14 | .then((rt) => runtime.ping(rt, options)) 15 | .then(() => { 16 | if (options.batch) { 17 | return; 18 | } 19 | setTimeout(() => { 20 | if (options.protocol === 'webrtc') { 21 | console.log(`NoFlo runtime is now listening at WebRTC channel #${options.id}`); 22 | } else { 23 | console.log(`NoFlo runtime is now listening at ${server.getUrl(options)}`); 24 | } 25 | if (options.secret) { 26 | console.log(`Live IDE URL: ${server.liveUrl(options)}`); 27 | } 28 | if (!options.open) { 29 | return; 30 | } 31 | open(server.liveUrl(options, true)) 32 | .catch(() => {}); 33 | }, 10); 34 | })) 35 | .catch((err) => { 36 | debug.showError(err); 37 | process.exit(1); 38 | }); 39 | }; 40 | 41 | if (!module.parent) { 42 | exports.main(); 43 | } 44 | -------------------------------------------------------------------------------- /src/permissions.js: -------------------------------------------------------------------------------- 1 | const allPermissions = [ 2 | 'protocol:component', 3 | 'protocol:runtime', 4 | 'protocol:graph', 5 | 'protocol:network', 6 | 'component:getsource', 7 | 'component:setsource', 8 | ]; 9 | 10 | exports.all = () => allPermissions.slice(0); 11 | -------------------------------------------------------------------------------- /src/runtime.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fbpGraph = require('fbp-graph'); 3 | const { Runtime: FlowhubRuntime } = require('flowhub-registry'); 4 | const os = require('os'); 5 | const mdns = require('mdns-js'); 6 | const debug = require('./debug'); 7 | const server = require('./server'); 8 | const autoSave = require('./autoSave'); 9 | const { writeTrace } = require('./trace'); 10 | 11 | exports.loadGraph = (options) => { 12 | if (typeof options.graph === 'object') { 13 | // Graph instance provided, return as-is 14 | return Promise.resolve(options.graph); 15 | } 16 | return fbpGraph.graph.loadFile(options.graph); 17 | }; 18 | 19 | exports.startGraph = (graphPath, runtime, settings) => exports 20 | .loadGraph({ 21 | graph: graphPath, 22 | }) 23 | .then((graphInstance) => { 24 | const graph = graphInstance; 25 | graph.name = graph.name || path.basename(graphPath, path.extname(graphPath)); 26 | const graphName = `${settings.namespace}/${graph.name}`; 27 | return runtime.graph.registerGraph(graphName, graph) 28 | .then(() => runtime // eslint-disable-line no-underscore-dangle 29 | .network._startNetwork(graph, graphName, 'none')) 30 | .then(() => { 31 | runtime.runtime.setMainGraph(graphName); 32 | return runtime; 33 | }); 34 | }); 35 | 36 | function stopNetwork(network) { 37 | return network.stop(); 38 | } 39 | 40 | function stopRuntime(rt, options, tracer) { 41 | writeTrace(options, tracer) 42 | .then(() => server.stop(rt)) 43 | .then(() => { 44 | process.exit(0); 45 | }) 46 | .catch((err) => { 47 | console.error(err); 48 | process.exit(1); 49 | }); 50 | } 51 | 52 | exports.subscribe = (rt, options) => new Promise((resolve) => { 53 | const networks = []; 54 | const tracers = []; 55 | 56 | process.on('SIGUSR2', () => { 57 | Promise.all(tracers.map((tracer) => writeTrace(options, tracer))) 58 | .catch((err) => { 59 | console.error(err); 60 | }); 61 | }); 62 | 63 | process.on('SIGTERM', () => { 64 | Promise.all(networks.map(stopNetwork)) 65 | .then(() => Promise.all(tracers.map((tracer) => writeTrace(options, tracer)))) 66 | .then(() => process.exit(0)) 67 | .catch((err) => { 68 | debug.showError(err); 69 | process.exit(1); 70 | }); 71 | }); 72 | 73 | if (!options.catchExceptions) { 74 | process.on('uncaughtException', (err) => { 75 | debug.showError(err); 76 | Promise.all(tracers.map((tracer) => writeTrace(options, tracer))) 77 | .then(() => { 78 | process.exit(1); 79 | }, (e) => { 80 | debug.showError(e); 81 | process.exit(1); 82 | }); 83 | }); 84 | } 85 | 86 | rt.network.on('addnetwork', (network, graphName) => { 87 | let tracer; 88 | if (options.trace) { 89 | tracer = rt.trace.startTrace(graphName, network); 90 | } 91 | if (options.debug) { 92 | debug.add(network, options); 93 | } 94 | if (options.batch && options.graph) { 95 | network.on('end', () => { 96 | stopRuntime(rt, options, tracer); 97 | }); 98 | } 99 | networks.push(network); 100 | }); 101 | rt.network.on('removenetwork', (network, graphName) => { 102 | if (networks.indexOf(network) === -1) { 103 | return; 104 | } 105 | if (options.trace && rt.trace.traces[graphName]) { 106 | writeTrace(options, rt.trace.traces[graphName]) 107 | .catch((e) => { 108 | console.error(e); 109 | }); 110 | } 111 | networks.splice(networks.indexOf(network), 1); 112 | }); 113 | 114 | if (options.autoSave) { 115 | autoSave.subscribe(rt); 116 | } 117 | resolve(rt); 118 | }); 119 | 120 | function getDefinition(options) { 121 | return { 122 | id: options.id, 123 | label: options.label || options.host, 124 | protocol: 'websocket', 125 | type: 'noflo-nodejs', 126 | }; 127 | } 128 | 129 | exports.advertiseMdns = (rt, options) => new Promise((resolve) => { 130 | if (!options.mdns) { 131 | resolve(rt); 132 | return; 133 | } 134 | const definition = getDefinition(options); 135 | const service = mdns.createAdvertisement(mdns.tcp('_fbp-ws'), options.port, { 136 | name: os.hostname().split('.').shift(), 137 | txt: { 138 | txtvers: '1', 139 | ...definition, 140 | }, 141 | }); 142 | service.start(); 143 | resolve(rt); 144 | }); 145 | 146 | function doPing(flowhubRt) { 147 | flowhubRt.ping(() => {}); 148 | } 149 | 150 | exports.ping = (rt, options) => new Promise((resolve) => { 151 | if (!options.id || !options.registryPing) { 152 | resolve(rt); 153 | return; 154 | } 155 | const definition = getDefinition(options); 156 | const flowhubRt = new FlowhubRuntime({ 157 | address: server.getUrl(options), 158 | ...definition, 159 | }, { 160 | host: options.registry, 161 | }); 162 | doPing(flowhubRt); 163 | setInterval(() => doPing(flowhubRt), options.registryPing); 164 | resolve(rt); 165 | }); 166 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const url = require('url'); 4 | const fs = require('fs'); 5 | const querystring = require('querystring'); 6 | const websocket = require('noflo-runtime-websocket'); 7 | const WebRTCRuntime = require('noflo-runtime-webrtc'); 8 | 9 | exports.getUrl = (options) => { 10 | if (options.protocol === 'webrtc') { 11 | const signalUrl = url.parse(options.signaller); 12 | signalUrl.hash = options.id; 13 | return url.format(signalUrl); 14 | } 15 | let protocol = 'ws:'; 16 | if (options.tlsKey && options.tlsCert) { 17 | protocol = 'wss:'; 18 | } 19 | const rtUrl = { 20 | protocol, 21 | slashes: true, 22 | hostname: options.host, 23 | port: `${options.port}`, 24 | pathname: '', 25 | }; 26 | return url.format(rtUrl); 27 | }; 28 | 29 | exports.liveUrl = (options, silent = false) => { 30 | const liveUrl = url.parse(options.ide); 31 | const rtUrl = url.parse(exports.getUrl(options)); 32 | liveUrl.pathname = liveUrl.pathname || '/'; 33 | if (rtUrl.protocol === 'ws:' && liveUrl.protocol === 'https:') { 34 | if (!silent) { 35 | console.log('Browsers will reject connections from HTTPS pages to unsecured WebSockets'); 36 | console.log('You can use insecure version of the IDE, or enable secure WebSockets with --tls-key and --tls-cert options'); 37 | } 38 | liveUrl.protocol = 'http:'; 39 | } 40 | const query = [ 41 | `protocol=${options.protocol}`, 42 | `address=${url.format(rtUrl)}`, 43 | `id=${options.id}`, 44 | `secret=${options.secret}`, 45 | ].join('&'); 46 | liveUrl.hash = `#runtime/endpoint?${querystring.escape(query)}`; 47 | return url.format(liveUrl); 48 | }; 49 | 50 | function createRuntime(options, server, runtimeOptions) { 51 | switch (options.protocol) { 52 | case 'webrtc': { 53 | return new WebRTCRuntime(exports.getUrl(options), runtimeOptions); 54 | } 55 | case 'websocket': { 56 | return websocket(server, runtimeOptions); 57 | } 58 | default: { 59 | throw new Error(`Unknown protocol ${options.protocol}. Use "websocket" or "webrtc"`); 60 | } 61 | } 62 | } 63 | 64 | exports.create = (options) => new Promise((resolve, reject) => { 65 | const handleRequest = (req, res) => { 66 | res.writeHead(302, { 67 | Location: exports.liveUrl(options), 68 | }); 69 | res.end(); 70 | }; 71 | 72 | let server = null; 73 | if (options.tlsKey && options.tlsCert) { 74 | server = https.createServer({ 75 | key: fs.readFileSync(options.tlsKey), 76 | cert: fs.readFileSync(options.tlsCert), 77 | }, handleRequest); 78 | } else { 79 | server = http.createServer(handleRequest); 80 | } 81 | const rt = createRuntime(options, server, { 82 | baseDir: options.baseDir, 83 | captureOutput: options.captureOutput, 84 | catchExceptions: options.catchExceptions, 85 | defaultPermissions: [], 86 | permissions: options.permissions, 87 | cache: options.cache, 88 | id: options.id, 89 | label: options.label, 90 | namespace: options.namespace, 91 | repository: options.repository, 92 | }); 93 | rt.webServer = server; 94 | rt.once('ready', () => { 95 | resolve(rt); 96 | }); 97 | rt.once('error', (err) => { 98 | reject(err); 99 | }); 100 | }); 101 | 102 | exports.start = (rt, options) => new Promise((resolve, reject) => { 103 | rt.webServer.listen(options.port, (err) => { 104 | if (err) { 105 | reject(err); 106 | return; 107 | } 108 | resolve(rt); 109 | }); 110 | }); 111 | 112 | exports.stop = (rt) => new Promise((resolve, reject) => { 113 | rt.webServer.close((err) => { 114 | if (err) { 115 | reject(err); 116 | return; 117 | } 118 | resolve(null); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | const commander = require('commander'); 2 | const { v4: uuid } = require('uuid'); 3 | const generatePassword = require('password-generator'); 4 | const os = require('os'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const fbpGraph = require('fbp-graph'); 8 | const clone = require('clone'); 9 | const nofloNodejs = require('../package.json'); 10 | const permissions = require('./permissions'); 11 | 12 | const config = { 13 | protocol: { 14 | description: 'Which protocol to use: "webrtc" or "websocket"', 15 | default: 'websocket', 16 | }, 17 | id: { 18 | description: 'Unique identifier (UUID) for the runtime', 19 | env: 'NOFLO_RUNTIME_ID', 20 | generate: () => uuid(), 21 | }, 22 | label: { 23 | description: 'Human-readable label for the runtime', 24 | generate: (project) => `${project.name} NoFlo runtime`, 25 | }, 26 | graph: { 27 | description: 'Path to graph file to run', 28 | skipSave: true, 29 | }, 30 | baseDir: { 31 | cli: 'base-dir', 32 | env: 'PROJECT_HOME', 33 | description: 'Project base directory used for component loading', 34 | default: process.cwd(), 35 | skipSave: true, 36 | }, 37 | batch: { 38 | description: 'Exit program when graph finishes', 39 | boolean: true, 40 | skipSave: true, 41 | }, 42 | host: { 43 | description: 'Hostname or IP for the runtime. Use "autodetect" for dynamic detection', 44 | default: 'autodetect', 45 | }, 46 | port: { 47 | description: 'Port for the runtime', 48 | default: 3569, 49 | }, 50 | tlsKey: { 51 | cli: 'tls-key', 52 | description: 'Path to TLS key file', 53 | }, 54 | tlsCert: { 55 | cli: 'tls-cert', 56 | description: 'Path to TLS cert file', 57 | }, 58 | secret: { 59 | description: 'Password to be used by FBP protocol clients', 60 | generate: () => generatePassword(), 61 | }, 62 | permissions: { 63 | description: 'Permissions for the FBP protocol clients', 64 | convert: (val) => val.split(','), 65 | default: permissions.all(), 66 | }, 67 | captureOutput: { 68 | cli: 'capture-output', 69 | boolean: true, 70 | description: 'Catch writes to STDOUT and send to FBP protocol client', 71 | skipSave: true, 72 | }, 73 | catchExceptions: { 74 | cli: 'catch-exceptions', 75 | boolean: true, 76 | description: 'Catch exceptions and send to FBP protocol client', 77 | skipSave: true, 78 | }, 79 | debug: { 80 | boolean: true, 81 | description: 'Log NoFlo packet events to STDOUT', 82 | skipSave: true, 83 | }, 84 | verbose: { 85 | boolean: true, 86 | description: 'Log NoFlo packet contents to STDOUT', 87 | skipSave: true, 88 | }, 89 | cache: { 90 | boolean: true, 91 | description: 'Enable NoFlo component loader cache', 92 | }, 93 | trace: { 94 | boolean: true, 95 | description: 'Record flowtrace from graph execution', 96 | }, 97 | open: { 98 | boolean: true, 99 | description: 'Open the runtime in IDE in user\'s default browser', 100 | skipSave: true, 101 | default: true, 102 | }, 103 | mdns: { 104 | boolean: true, 105 | description: 'Advertise runtime via mDNS', 106 | default: true, 107 | }, 108 | ide: { 109 | description: 'URL for the FBP protocol client', 110 | default: 'https://app.noflojs.org', 111 | }, 112 | signaller: { 113 | description: 'URL for the WebRTC signalling server', 114 | default: 'wss://api.flowhub.io', 115 | }, 116 | registry: { 117 | description: 'URL for the runtime registry', 118 | default: 'https://api.flowhub.io', 119 | }, 120 | registryPing: { 121 | cli: 'registry-ping', 122 | description: 'How often to ping the runtime registry', 123 | convert: (val) => parseInt(val, 10), 124 | default: 10 * 60 * 1000, 125 | }, 126 | autoSave: { 127 | cli: 'auto-save', 128 | boolean: true, 129 | description: 'Save edited graphs and components to disk automatically', 130 | default: false, 131 | }, 132 | }; 133 | 134 | function discoverIp(preferred) { 135 | const ifaces = os.networkInterfaces(); 136 | let externalAddress = ''; 137 | let internalAddress = ''; 138 | 139 | const findInterface = (connection) => { 140 | if (connection.family !== 'IPv4') { 141 | return; 142 | } 143 | if (connection.internal) { 144 | internalAddress = connection.address; 145 | return; 146 | } 147 | externalAddress = connection.address; 148 | }; 149 | 150 | if (typeof preferred === 'string' && ifaces[preferred]) { 151 | // Only look at the preferred network interface 152 | ifaces[preferred].forEach(findInterface); 153 | } else { 154 | // Cycle through all network interfaces 155 | Object.keys(ifaces).forEach((iface) => { 156 | ifaces[iface].forEach(findInterface); 157 | }); 158 | } 159 | 160 | return externalAddress || internalAddress; 161 | } 162 | 163 | const readPackage = (baseDir) => new Promise((resolve, reject) => { 164 | const packagePath = path.resolve(baseDir, './package.json'); 165 | fs.readFile(packagePath, 'utf8', (err, contents) => { 166 | if (err) { 167 | reject(err); 168 | return; 169 | } 170 | try { 171 | const packageFile = JSON.parse(contents); 172 | resolve(packageFile); 173 | } catch (e) { 174 | reject(e); 175 | } 176 | }); 177 | }); 178 | 179 | const applyEnv = () => new Promise((resolve) => { 180 | const applied = {}; 181 | Object.keys(config).forEach((key) => { 182 | if (!config[key].env) { 183 | return; 184 | } 185 | if (process.env[config[key].env]) { 186 | applied[key] = process.env[config[key].env]; 187 | } 188 | }); 189 | resolve(applied); 190 | }); 191 | 192 | const parseArguments = () => { 193 | const options = commander.version(nofloNodejs.version, '-v --version'); 194 | const convertBoolean = (val) => String(val) === 'true'; 195 | Object.keys(config).forEach((key) => { 196 | const conf = config[key]; 197 | const optionKey = conf.cli || key; 198 | let { description } = conf; 199 | if (conf.skipSave) { 200 | description = `${description} [not saved to flowhub.json]`; 201 | } 202 | if (config[key].boolean) { 203 | options.option(`--${optionKey} [true]`, description, convertBoolean, conf.default); 204 | return; 205 | } 206 | if (config[key].convert) { 207 | options.option(`--${optionKey} <${optionKey}>`, description, conf.convert, conf.default); 208 | return; 209 | } 210 | options.option(`--${optionKey} <${optionKey}>`, description, conf.default); 211 | }); 212 | options.parse(process.argv); 213 | if (typeof options.register !== 'undefined') { 214 | console.warn('noflo-nodejs --register is deprecated and has no effect'); 215 | delete options.register; 216 | } 217 | return options; 218 | }; 219 | 220 | const applyOptions = (settings, options) => new Promise((resolve) => { 221 | const applied = clone(settings); 222 | Object.keys(config).forEach((key) => { 223 | if (typeof options[key] === 'undefined') { 224 | return; 225 | } 226 | applied[key] = options[key]; 227 | }); 228 | resolve(applied); 229 | }); 230 | 231 | const applyDefaults = (settings) => new Promise((resolve) => { 232 | const applied = clone(settings); 233 | Object.keys(config).forEach((key) => { 234 | if (typeof config[key].default === 'undefined') { 235 | return; 236 | } 237 | if (typeof applied[key] !== 'undefined') { 238 | return; 239 | } 240 | applied[key] = config[key].default; 241 | }); 242 | resolve(applied); 243 | }); 244 | 245 | const applyArguments = (settings) => { 246 | const options = parseArguments(); 247 | return applyOptions(settings, options); 248 | }; 249 | 250 | const convertNamespace = (name) => { 251 | if (!name) { 252 | return ''; 253 | } 254 | if (name === 'noflo') { 255 | return ''; 256 | } 257 | let cleanedName = name; 258 | if (cleanedName[0] === '@') { 259 | cleanedName = cleanedName.replace(/@[a-z-]+\//, ''); 260 | } 261 | return cleanedName.replace(/^noflo-/, ''); 262 | }; 263 | 264 | const generateValues = (settings) => { 265 | const applied = clone(settings); 266 | return readPackage(applied.baseDir) 267 | .then((packageData) => { 268 | Object.keys(config).forEach((key) => { 269 | if (typeof applied[key] !== 'undefined') { 270 | return; 271 | } 272 | if (!config[key].generate) { 273 | return; 274 | } 275 | applied[key] = config[key].generate(packageData, applied); 276 | }); 277 | // Ensure permissions is in the correct format 278 | if (Array.isArray(applied.permissions)) { 279 | if (applied.secret) { 280 | const perms = {}; 281 | perms[applied.secret] = applied.permissions; 282 | applied.permissions = perms; 283 | } else { 284 | delete applied.permissions; 285 | } 286 | } 287 | if (packageData.repository && packageData.repository.url) { 288 | applied.repository = packageData.repository.url; 289 | } 290 | if (packageData.name) { 291 | applied.namespace = convertNamespace(packageData.name); 292 | } 293 | return applied; 294 | }); 295 | }; 296 | 297 | const loadSettings = (settings) => new Promise((resolve, reject) => { 298 | const applied = clone(settings); 299 | const settingsPath = path.resolve(applied.baseDir, 'flowhub.json'); 300 | fs.readFile(settingsPath, 'utf8', (err, contents) => { 301 | if (err) { 302 | // Not having a persisted settings file is OK 303 | resolve(applied); 304 | return; 305 | } 306 | try { 307 | const savedSettings = JSON.parse(contents); 308 | Object.keys(savedSettings).forEach((key) => { 309 | if (typeof applied[key] !== 'undefined') { 310 | return; 311 | } 312 | if (config[key].skipSave) { 313 | return; 314 | } 315 | applied[key] = savedSettings[key]; 316 | }); 317 | resolve(applied); 318 | } catch (e) { 319 | // However, if settings file is corrupted, this is a problem 320 | reject(e); 321 | } 322 | }); 323 | }); 324 | 325 | const saveSettings = (settings) => new Promise((resolve, reject) => { 326 | const saveables = {}; 327 | Object.keys(config).forEach((key) => { 328 | if (typeof settings[key] === 'undefined') { 329 | return; 330 | } 331 | if (settings[key] === config[key].default) { 332 | return; 333 | } 334 | if (config[key].skipSave) { 335 | return; 336 | } 337 | saveables[key] = settings[key]; 338 | }); 339 | const settingsPath = path.resolve(settings.baseDir, 'flowhub.json'); 340 | fs.writeFile(settingsPath, JSON.stringify(saveables, null, 2), (err) => { 341 | if (err) { 342 | reject(err); 343 | return; 344 | } 345 | resolve(settings); 346 | }); 347 | }); 348 | 349 | // These settings may change for each execution so they're done after saving 350 | const autodetect = (settings) => new Promise((resolve) => { 351 | const applied = clone(settings); 352 | if (applied.host === 'autodetect') { 353 | applied.host = discoverIp(); 354 | } 355 | if (!applied.graph) { 356 | const graph = fbpGraph.graph.createGraph('main'); 357 | graph.setProperties({ 358 | environment: { 359 | type: 'noflo-nodejs', 360 | }, 361 | }); 362 | applied.graph = graph; 363 | } 364 | resolve(applied); 365 | }); 366 | 367 | // Layered config loading, each level overrides previous 368 | // - Defaults 369 | // - ~/.flowhub.json 370 | // - .flowhub.json 371 | // - env vars 372 | // - CLI arguments 373 | // - Generated, as needed 374 | 375 | exports.load = () => applyEnv() 376 | .then((settings) => applyArguments(settings)) 377 | .then((settings) => loadSettings(settings)) 378 | .then((settings) => generateValues(settings)) 379 | .then((settings) => saveSettings(settings)) 380 | .then((settings) => autodetect(settings)); 381 | 382 | exports.loadForLibrary = (options) => applyEnv() 383 | .then((settings) => applyOptions(settings, options)) 384 | .then((settings) => applyDefaults(settings)) 385 | .then((settings) => generateValues(settings)) 386 | .then((settings) => autodetect(settings)); 387 | -------------------------------------------------------------------------------- /src/trace.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { promisify } = require('util'); 3 | const { resolve } = require('path'); 4 | const slug = require('slug'); 5 | 6 | const stat = promisify(fs.stat); 7 | const mkdir = promisify(fs.mkdir); 8 | const writeFile = promisify(fs.writeFile); 9 | 10 | function ensureTracedir(options) { 11 | if (!options.trace) { 12 | return Promise.resolve(); 13 | } 14 | const tracePath = resolve(options.baseDir, './.flowtrace'); 15 | return stat(tracePath) 16 | .catch(() => mkdir(tracePath, { 17 | recursive: true, 18 | }) 19 | .then(() => stat(tracePath))) 20 | .then((stats) => { 21 | if (!stats.isDirectory()) { 22 | return Promise.reject(new Error(`${tracePath} is not a directory`)); 23 | } 24 | return Promise.resolve(tracePath); 25 | }); 26 | } 27 | 28 | function writeTrace(options, tracer) { 29 | if (!options.trace) { 30 | return Promise.resolve(); 31 | } 32 | const date = new Date().toISOString().substr(0, 10); 33 | const fileName = slug(`${date}-noflo-nodejs-${options.id}-${tracer.mainGraph}`); 34 | return ensureTracedir(options) 35 | .then((traceDir) => { 36 | const tracePath = resolve(traceDir, `./${fileName}.json`); 37 | return writeFile(tracePath, JSON.stringify(tracer, null, 2)) 38 | .then(() => tracePath); 39 | }) 40 | .then((filename) => { 41 | console.log(`Wrote flowtrace to: ${filename}`); 42 | return null; 43 | }); 44 | } 45 | 46 | module.exports = { 47 | ensureTracedir, 48 | writeTrace, 49 | }; 50 | --------------------------------------------------------------------------------