diff --git a/.eslintrc.json b/.eslintrc.json index 7cde01d..6612481 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,7 @@ { - "extends": "airbnb-base" + "extends": "airbnb-base", + "globals": { + "document": false, + "window": false + } } diff --git a/.gitignore b/.gitignore index b6a2f7f..342fd5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ /node_modules/ -/browser/ -/components/*/ +/dist/ flowhub.json fbp.json package-lock.json *~ -gh-pages/ diff --git a/.travis.yml b/.travis.yml index e40cd1d..388c190 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ node_js: deploy: provider: pages skip_cleanup: true - local-dir: browser + local-dir: dist keep-history: true skip_cleanup: true committer-from-gh: true diff --git a/app.js b/app.js new file mode 100644 index 0000000..f80e370 --- /dev/null +++ b/app.js @@ -0,0 +1,24 @@ +const runtime = require('./runtime'); +const pkg = require('./package.json'); +const graph = require('./graphs/main.json'); + +function main() { + return runtime(graph, { + runtimeOptions: { + baseDir: pkg.name, + label: pkg.name, + namespace: pkg.name, + repository: pkg.repository ? pkg.repository.url : null, + }, + debugButton: document.getElementById('flowhub_debug_url'), + }); +} + +document.addEventListener('DOMContentLoaded', () => { + main() + .catch((err) => { + const message = document.querySelector('body p'); + message.innerHTML = `ERROR: ${err.message}`; + message.parentElement.classList.add('error'); + }); +}); diff --git a/assets/app.css b/assets/app.css new file mode 100644 index 0000000..87a1d02 --- /dev/null +++ b/assets/app.css @@ -0,0 +1,22 @@ +#flowhub_debug_url.nodebug { + display: none; +} +#flowhub_debug_url.debug { + display: block; + position: fixed; + z-index: 999; + width: 20em; + height: 1.5em; + right: -6.5em; + top: 3em; + text-align: center; + background-color: hsla(210, 98%, 46%, .8) !important; + border-color: hsl(210, 98%, 46%) !important; + color: white !important; + text-decoration: none; + padding: 1em; + box-sizing: content-box; + cursor: pointer; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + transform: rotate(45deg); +} diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..cfee2f1 --- /dev/null +++ b/assets/index.html @@ -0,0 +1,14 @@ + + + + + noflo-browser-app + + + + + Debug in Flowhub + +

+ + diff --git a/package.json b/package.json index baf8e3d..16c8f1f 100644 --- a/package.json +++ b/package.json @@ -12,22 +12,34 @@ }, "license": "MIT", "scripts": { - "pretest": "eslint *.js components/*.js", - "test": "fbp-spec --address ws://localhost:3569 --command \"noflo-runtime-headless -f browser/noflo-browser-app.js\" spec/*.yaml" + "build": "webpack", + "lint": "eslint *.js components/*.js", + "lint-fix": "eslint --fix *.js components/*.js", + "pretest": "npm run lint && npm run build", + "test": "fbp-spec --address ws://localhost:3569 --command \"noflo-runtime-headless -f dist/test.js\" spec/*.yaml", + "postinstall": "noflo-cache-preheat" }, "dependencies": { "noflo": "^1.0.0", "noflo-core": "^0.5.0", "noflo-dom": "^0.3.0", - "noflo-interaction": "^0.3.0" + "noflo-interaction": "^0.3.0", + "noflo-runtime-postmessage": "^0.10.0" }, "devDependencies": { + "@babel/core": "^7.1.6", + "@babel/preset-env": "^7.1.6", + "babel-loader": "^8.0.4", + "coffee-loader": "^0.9.0", + "copy-webpack-plugin": "^4.6.0", "eslint": "^5.6.0", "eslint-config-airbnb-base": "^13.1.0", "eslint-plugin-import": "^2.14.0", "fbp-spec": "^0.6.6", + "noflo-component-loader": "^0.3.2", "noflo-runtime-headless": "^0.1.0", - "noflo-runtime-postmessage": "^0.10.0" + "webpack": "^4.26.1", + "webpack-cli": "^3.1.2" }, "keywords": [ "noflo" diff --git a/runtime.js b/runtime.js new file mode 100644 index 0000000..b8003a7 --- /dev/null +++ b/runtime.js @@ -0,0 +1,95 @@ +const noflo = require('noflo'); +const postMessageRuntime = require('noflo-runtime-postmessage'); +const qs = require('querystring'); + +const defaultPermissions = [ + 'protocol:graph', + 'protocol:component', + 'protocol:network', + 'protocol:runtime', + 'component:getsource', + 'component:setsource', +]; + +function getParameterByName(name) { + const params = (new URL(document.location)).searchParams; + return params.get(name); +} + +function getIdeUrl(ide = 'https://app.flowhub.io') { + const query = qs.stringify({ + protocol: 'opener', + address: window.location.href, + }); + return `${ide}#runtime/endpoint?${query}`; +} + +function loadGraph(json) { + return new Promise((resolve, reject) => { + noflo.graph.loadJSON(json, (err, graphInstance) => { + if (err) { + reject(err); + return; + } + resolve(graphInstance); + }); + }); +} + +function startRuntime(graph, options = {}) { + return new Promise((resolve, reject) => { + const protocol = options.protocol || getParameterByName('fbp_protocol') || 'opener'; + + const runtimeOptions = { + ...options.runtimeOptions, + }; + if (!runtimeOptions.defaultPermissions) { + runtimeOptions.defaultPermissions = defaultPermissions; + } + + switch (protocol) { + case 'opener': { + if (!options.debugButton) { + reject(new Error('No debug button defined')); + return; + } + const button = options.debugButton; + button.classList.replace('nodebug', 'debug'); + button.href = getIdeUrl(options.ide); + resolve(postMessageRuntime.opener(runtimeOptions, button)); + return; + } + case 'iframe': { + resolve(postMessageRuntime.iframe(runtimeOptions)); + return; + } + default: { + reject(new Error(`Unknown FBP protocol ${protocol}`)); + } + } + }); +} + +function startNetwork(runtime, graph, options) { + const noLoad = options.noLoad || (getParameterByName('fbp_noload') === 'true'); + if (noLoad) { + return Promise.resolve(runtime); + } + return new Promise((resolve, reject) => { + const graphName = `${runtime.options.namespace || 'default'}/${graph.name || 'main'}`; + runtime.graph.registerGraph(graphName, graph); + // eslint-disable-next-line no-underscore-dangle + runtime.network._startNetwork(graph, graphName, 'none', (err) => { + if (err) { + reject(err); + return; + } + runtime.runtime.setMainGraph(graphName, graph); + resolve(runtime); + }); + }); +} + +module.exports = (graph, options) => loadGraph(graph) + .then(graphInstance => startRuntime(graphInstance, options) + .then(runtime => startNetwork(runtime, graphInstance, options))); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..367e923 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,91 @@ +const path = require('path'); + +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +module.exports = { + entry: { + app: './app.js', + test: './node_modules/noflo-runtime-headless/spec/build/webpack.entry.js', + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + }, + mode: 'production', + devtool: 'source-map', + module: { + rules: [ + { + test: /noflo([\\]+|\/)lib([\\]+|\/)loader([\\]+|\/)register.js$/, + use: [ + { + loader: 'noflo-component-loader', + options: { + graph: null, + debug: true, + baseDir: __dirname, + manifest: { + runtimes: ['noflo'], + discover: true, + }, + runtimes: [ + 'noflo', + 'noflo-browser', + ], + }, + }, + ], + }, + { + test: /\.s$/, + // exclude: /(node_modules|bower_components)/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'], + }, + }, + }, + { + test: /\.coffee$/, + use: [ + { + loader: 'coffee-loader', + options: { + transpile: { + presets: ['@babel/preset-env'], + }, + }, + }, + ], + }, + { + test: /\.fbp$/, + use: [ + { + loader: 'fbp-loader', + }, + ], + }, + ], + }, + plugins: [ + new CopyWebpackPlugin([ + { + from: 'assets/*.html', + flatten: true, + }, + { + from: 'assets/*.css', + flatten: true, + }, + ]), + ], + resolve: { + extensions: ['.coffee', '.js'], + }, + node: { + child_process: 'empty', + fs: 'empty', + }, +};