├── .gitignore ├── client ├── logo.png ├── devtools.html ├── manifest.json ├── logger.js ├── configure │ ├── configure.html │ ├── configure.js │ └── style.css ├── connection.js ├── index.js └── session.js ├── index.js ├── package.json ├── test ├── client │ ├── logger_mock.js │ ├── connection_test.js │ ├── browser_websocket.js │ └── session_test.js └── server │ └── flo_test.js ├── bin └── flo ├── PATENTS ├── LICENSE ├── lib ├── server.js └── flo.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /client/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/fb-flo/master/client/logo.png -------------------------------------------------------------------------------- /client/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fb-flo", 3 | "version": "0.11", 4 | "description": "Modify running web apps without reloading", 5 | "devtools_page": "devtools.html", 6 | "manifest_version": 2, 7 | "permissions": [ 8 | "*://*/" 9 | ], 10 | "icons": { 11 | "128": "logo.png" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | module.exports = require('./lib/flo'); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fb-flo", 3 | "version": "0.1.0", 4 | "description": "Modify running web apps without reloading", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/mocha/bin/mocha ./test/**/*_test.js --bail" 8 | }, 9 | "author": "", 10 | "license": "BSD-2-Clause", 11 | "dependencies": { 12 | "websocket": "~1.0.8", 13 | "sane": "~0.4.0" 14 | }, 15 | "devDependencies": { 16 | "mocha": "~1.17.1" 17 | }, 18 | "bin": "./bin/flo" 19 | } 20 | -------------------------------------------------------------------------------- /test/client/logger_mock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | module.exports = function loggerMock() { 11 | return { 12 | error: function(){}, 13 | log: function() {} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bin/flo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var flo = require('../'); 4 | var path = require('path'); 5 | 6 | var file = path.join( 7 | process.cwd(), 8 | process.argv[2] || 'flofile.js' 9 | ); 10 | 11 | var config; 12 | try { 13 | config = require(file); 14 | } catch (e) { 15 | config = {}; 16 | } 17 | 18 | config.options = config.options || {}; 19 | 20 | var f = flo( 21 | config.options.dir || process.cwd(), 22 | config.options, 23 | config.resolver 24 | ); 25 | 26 | f.once('ready', config.ready || function() {}); 27 | -------------------------------------------------------------------------------- /client/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | /* jshint evil:true */ 11 | 12 | this.Logger = (function() { 13 | 'use strict'; 14 | 15 | function Logger(log) { 16 | return function(namespace) { 17 | return { 18 | error: createLogLevel('error'), 19 | log: createLogLevel('log') 20 | }; 21 | 22 | function createLogLevel(level) { 23 | return function () { 24 | var args = [].slice.call(arguments); 25 | args[0] = '[' + namespace + '] ' + args[0]; 26 | return log([level, args]); 27 | }; 28 | } 29 | }; 30 | } 31 | 32 | Logger.logInContext = function(arg, method) { 33 | if (!method) { 34 | method = 'log'; 35 | } 36 | chrome.devtools.inspectedWindow['eval']( 37 | 'console.' + method + '("' + arg.toString() + '")' 38 | ); 39 | }; 40 | 41 | return Logger; 42 | })(); 43 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights 2 | 3 | "Software" means the "fb-flo" software distributed by Facebook, Inc. 4 | 5 | Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive, 6 | irrevocable (subject to the termination provision below) license under any 7 | rights in any patent claims owned by Facebook, to make, have made, use, sell, 8 | offer to sell, import, and otherwise transfer the Software. For avoidance of 9 | doubt, no license is granted under Facebook’s rights in any patent claims that 10 | are infringed by (i) modifications to the Software made by you or a third party, 11 | or (ii) the Software in combination with any software or other technology 12 | provided by you or a third party. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | for anyone that makes any claim (including by filing any lawsuit, assertion or 16 | other action) alleging (a) direct, indirect, or contributory infringement or 17 | inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or 18 | affiliates, whether or not such claim is related to the Software, (ii) by any 19 | party if such claim arises in whole or in part from any software, product or 20 | service of Facebook or any of its subsidiaries or affiliates, whether or not 21 | such claim is related to the Software, or (iii) by any party relating to the 22 | Software; or (b) that any right in any patent claim of Facebook is invalid or 23 | unenforceable. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For "fb-flo" software 4 | 5 | Copyright (c) 2014, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /test/client/connection_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | var assert = require('assert'); 11 | var Connection = require('../../client/connection'); 12 | var Server = require('../../lib/server'); 13 | var WebSocket = require('./browser_websocket'); 14 | var mockLogger = require('./logger_mock'); 15 | global.WebSocket = WebSocket; 16 | 17 | describe('Connection', function() { 18 | var port = 8543; 19 | var server; 20 | afterEach(function() { 21 | server.close(); 22 | con.disconnect(); 23 | }); 24 | 25 | it('should connect to server', function(done) { 26 | server = new Server({ 27 | port: port 28 | }); 29 | con = new Connection('localhost', port, mockLogger) 30 | .onopen(function() { 31 | server.broadcast({hi: 1}); 32 | }) 33 | .onmessage(function(msg) { 34 | assert.deepEqual(msg, { 35 | hi: 1 36 | }); 37 | done(); 38 | }) 39 | .onerror(done) 40 | .connect(); 41 | }); 42 | 43 | it('should retry to connect', function(done) { 44 | con = new Connection('localhost', port, mockLogger) 45 | .onopen(function() { 46 | done(); 47 | }) 48 | .onretry(function(){ 49 | server = new Server({ 50 | port: port 51 | }); 52 | }) 53 | .onerror(done) 54 | .connect(); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/client/browser_websocket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | var WebSocketClient = require('websocket').client; 11 | 12 | function WebSocket(url) { 13 | this.readyState = 0; 14 | var client = new WebSocketClient(); 15 | this.onconnect = this.onconnect.bind(this); 16 | client.once('connect', this.onconnect); 17 | client.once('connectFailed', this.emit.bind(this, 'close')); 18 | client.connect(url); 19 | } 20 | 21 | WebSocket.prototype.onconnect = function(connection) { 22 | this.readyState = 1; 23 | this.socket = connection; 24 | this.socket.on('error', this.emit.bind(this, 'close')); 25 | this.socket.on('close', function() { 26 | this.readyState = 3; 27 | this.emit('close', {}); 28 | }.bind(this)); 29 | this.socket.on('message', function(msg) { 30 | var data = msg.utf8Data; 31 | this.emit('message', {data: data}); 32 | }.bind(this)); 33 | this.emit('open'); 34 | }; 35 | 36 | WebSocket.prototype.send = function(arg) { 37 | this.socket.sendUTF(arg); 38 | }; 39 | 40 | WebSocket.prototype.close = function() { 41 | this.readyState = 2; 42 | this.socket.close(); 43 | }; 44 | 45 | WebSocket.prototype.emit = function(event) { 46 | var handler = this['on' + event]; 47 | if (typeof handler === 'function') { 48 | handler.apply(this, [].slice.call(arguments, 1)); 49 | } 50 | }; 51 | 52 | var states = { 53 | CONNECTING: 0, 54 | OPEN: 1, 55 | CLOSING: 2, 56 | CLOSED: 3 57 | }; 58 | 59 | Object.keys(states).forEach(function(state) { 60 | WebSocket.prototype[state] = states[state]; 61 | }); 62 | 63 | module.exports = WebSocket; 64 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var http = require('http'); 13 | var WSS = require('websocket').server; 14 | 15 | module.exports = Server; 16 | 17 | /** 18 | * Starts an http server with the given options and attaches a websocket server 19 | * to it. 20 | * 21 | * @class Server 22 | * @param {object} options 23 | */ 24 | 25 | function Server(options) { 26 | this.log = options.log || function() {}; 27 | this.httpServer = http.createServer(function(req, res) { 28 | res.writeHead(404); 29 | res.end(); 30 | }); 31 | this.httpServer.listen(options.port); 32 | this.wsServer = new WSS({ 33 | httpServer: this.httpServer, 34 | autoAcceptConnections: false 35 | }); 36 | this.wsServer.on('request', this.onRequest.bind(this)); 37 | this.connections = []; 38 | } 39 | 40 | /** 41 | * Request handler. 42 | * 43 | * @param {object} req 44 | * @private 45 | */ 46 | 47 | Server.prototype.onRequest = function(req) { 48 | this.log('Client connected', req.socket.address()); 49 | var ws = req.accept(); 50 | this.connections.push(ws); 51 | ws.on('close', this.onClose.bind(this, ws)); 52 | }; 53 | 54 | /** 55 | * Websocket connection close handler. 56 | * 57 | * @param {object} ws 58 | * @private 59 | */ 60 | 61 | Server.prototype.onClose = function(ws) { 62 | this.log('Client disconnected', ws.remoteAddress); 63 | if (this.connections) { 64 | this.connections.splice(this.connections.indexOf(ws), 1); 65 | } 66 | }; 67 | 68 | /** 69 | * Message handler. 70 | * 71 | * @param {object} msg 72 | * @public 73 | */ 74 | 75 | Server.prototype.broadcast = function(msg) { 76 | this.log('Broadcasting', msg); 77 | msg = JSON.stringify(msg); 78 | this.connections.forEach(function(ws) { 79 | ws.send(msg); 80 | }); 81 | }; 82 | 83 | /** 84 | * Close the server. 85 | * 86 | * @public 87 | */ 88 | 89 | Server.prototype.close = function() { 90 | this.log('Shutting down WebSocket server'); 91 | this.connections = null; 92 | this.wsServer.shutDown(); 93 | this.httpServer.close(); 94 | }; 95 | -------------------------------------------------------------------------------- /client/configure/configure.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | flo configuration 5 | 6 | 7 | 8 |
9 | 15 |
16 |

Status

17 |
18 | 19 | 20 | 21 | 22 |
23 |

Log  Report a bug

24 |
25 | 26 |
27 |
28 |
29 |

Configure flo

30 |
31 |
32 |
33 | 34 |

Sites to start flo for.

35 |
36 |
    37 | 38 |
    39 | 45 |
    46 |
    47 | 48 |

    The default port number your flo server would be listening on (you can set port number per site).

    49 | 50 |
    51 |
    52 |
    53 |
    54 |
    55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fb-flo 2 | --- 3 | 4 | fb-flo is a Chrome extension that lets you modify running apps without reloading. It's easy to integrate with your build system, dev environment, and can be used with your favorite editor. 5 | 6 | ## Usage 7 | 8 | fb-flo is made up of a server and client component. This will guide through configuring your server for your project and installing the Chrome extension. 9 | 10 | ### 1. Configure fb-flo server 11 | 12 | ``` 13 | $ npm install fb-flo 14 | ``` 15 | 16 | fb-flo exports a single `fb-flo` function to start the server. Here is an example where you have your source JavaScript and CSS files in the root directory and your build step involves bundling both into a respective `bundle.js`, `bundle.css`. 17 | 18 | ```js 19 | var flo = require('fb-flo'), 20 | path = require('path'), 21 | fs = require('fs'); 22 | 23 | var server = flo( 24 | sourceDirToWatch, 25 | { 26 | port: 8888, 27 | host: 'localhost', 28 | verbose: false, 29 | glob: [ 30 | // All JS files in `sourceDirToWatch` and subdirectories 31 | '**/*.js', 32 | // All CSS files in `sourceDirToWatch` and subdirectories 33 | '**/*.css' 34 | ] 35 | }, 36 | function resolver(filepath, callback) { 37 | // 1. Call into your compiler / bundler. 38 | // 2. Assuming that `bundle.js` is your output file, update `bundle.js` 39 | // and `bundle.css` when a JS or CSS file changes. 40 | callback({ 41 | resourceURL: 'bundle.js' + path.extname(filepath), 42 | // any string-ish value is acceptable. i.e. strings, Buffers etc. 43 | contents: fs.readFileSync(filepath) 44 | }); 45 | } 46 | ); 47 | ``` 48 | 49 | `flo` takes the following arguments. 50 | 51 | * `sourceDirToWatch`: absolute or relative path to the directory to watch that contains the source code that will be built. 52 | * `options` hash of options: 53 | * `port` port to start the server on (defaults to 8888). 54 | * `host` to listen on. 55 | * `verbose` `true` or `false` value indicating if flo should be noisy. 56 | * `glob` a glob string or array of globs to match against the files to watch. 57 | * `useFilePolling` some platforms that do not support native file watching, you can force the file watcher to work in polling mode. 58 | * `pollingInterval` if in polling mode (useFilePolling) then you can set the interval at which to poll for file changes. 59 | * `resolver` a function to map between files and resources. 60 | 61 | The resolver callback is called with two arguments: 62 | 63 | * `filepath` path to the file that changed relative to the watched directory. 64 | * `callback` called to update a resource file in the browser. Should be called with an object with the following properties: 65 | * `resourceURL` used as the resource identifier in the browser. 66 | * `contents` any string-ish value representing the source of the updated file. i.e. strings, Buffers etc. 67 | * `reload` (optional) forces a full page reload. Use this if you're sure the changed code cannot be hotswapped. 68 | * `match` (optional, defaults to: indexOf) identifies the matching function to be performed on the resource URL in the browser. Could be one of the following: 69 | * `"equal"` test the updated resource `resourceURL` against existing browser resources using an equality check. 70 | * `"indexOf"` use `String.prototype.indexOf` check 71 | * `/regexp/` a regexp object to exec. 72 | 73 | ### 2. Install the Chrome Extension 74 | 75 | Grab the [fb-flo Chrome extension](https://chrome.google.com/webstore/detail/ahkfhobdidabddlalamkkiafpipdfchp). This will add a new tab in your Chrome DevTools called 'flo'. 76 | 77 | ### 3. Activate fb-flo 78 | 79 | To activate fb-flo from the browser: 80 | 81 | * Open Chrome DevTools. 82 | * Click on the new 'fb-flo' pane. 83 | * Click on 'Activate for this site' 84 | 85 | See screenshot: 86 | 87 | ![](http://i.imgur.com/SamY32i.png) 88 | 89 | ### Example 90 | 91 | Say you have a Makefile program that builds your JavaScript and CSS into `build/build.js` and `build/build.css` respectively, this how you'd configure your fb-flo server: 92 | 93 | ```js 94 | var flo = require('fb-flo'); 95 | var fs = require('fs'); 96 | var exec = require('child_process').exec; 97 | 98 | var server = flo('./lib/', { 99 | port: 8888, 100 | dir: './lib/', 101 | glob: ['./lib/**/*.js', './lib/**/*.css'] 102 | }, resolver); 103 | 104 | server.once('ready', function() { 105 | console.log('Ready!'); 106 | }); 107 | 108 | function resolver(filepath, callback) { 109 | exec('make', function (err) { 110 | if (err) throw err; 111 | if (filepath.match(/\.js$/)) { 112 | callback({ 113 | resourceURL: 'build/build.js', 114 | contents: fs.readFileSync('build/build.js').toString() 115 | }) 116 | } else { 117 | callback({ 118 | resourceURL: 'build/build.css', 119 | contents: fs.readFileSync('build/build.css').toString() 120 | }) 121 | } 122 | }); 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /lib/flo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var fs = require('fs'); 13 | var path = require('path'); 14 | var sane = require('sane'); 15 | var assert = require('assert'); 16 | var Server = require('./server'); 17 | var EventEmitter = require('events').EventEmitter; 18 | 19 | module.exports = flo; 20 | 21 | /** 22 | * Top-level API for flo. Defaults params and instantiates `Flo`. 23 | * 24 | * @param {string} dir 25 | * @param {object} options 26 | * @param {function} callback 27 | * @return {Flo} 28 | * @public 29 | */ 30 | 31 | function flo(dir, options, callback) { 32 | if (typeof options === 'function') { 33 | callback = options; 34 | options = null; 35 | } 36 | 37 | dir = path.resolve(dir); 38 | 39 | options = options || {}; 40 | options = { 41 | port: options.port || 8888, 42 | host: options.host || 'localhost', 43 | verbose: options.verbose || false, 44 | glob: options.glob || [], 45 | useFilePolling: options.useFilePolling || false, 46 | pollingInterval: options.pollingInterval 47 | }; 48 | 49 | callback = callback || noBuildCallback(dir); 50 | 51 | return new Flo(dir, options, callback); 52 | } 53 | 54 | /** 55 | * Time before we emit the ready event. 56 | */ 57 | 58 | var DELAY = 200; 59 | 60 | /** 61 | * Starts the server and the watcher and handles the piping between both and the 62 | * resolver callback. 63 | * 64 | * @param {string} dir 65 | * @param {object} options 66 | * @param {function} callback 67 | * @class Flo 68 | * @private 69 | */ 70 | 71 | function Flo(dir, options, callback) { 72 | this.log = logger(options.verbose, 'Flo'); 73 | this.dir = dir; 74 | this.realpathdir = fs.realpathSync(dir); 75 | this.resolver = callback; 76 | this.server = new Server({ 77 | port: options.port, 78 | host: options.host, 79 | log: logger(options.verbose, 'Server') 80 | }); 81 | 82 | this.watcher = new sane.Watcher(dir, { 83 | glob: options.glob, 84 | poll: options.useFilePolling, 85 | pollingInterval: options.pollingInterval 86 | }); 87 | this.watcher.on('change', this.onFileChange.bind(this)); 88 | this.watcher.on( 89 | 'ready', 90 | this.emit.bind(this, 'ready') 91 | ); 92 | } 93 | 94 | Flo.prototype.__proto__ = EventEmitter.prototype; 95 | 96 | /** 97 | * Handles file changes. 98 | * 99 | * @param {string} filepath 100 | * @private 101 | */ 102 | 103 | Flo.prototype.onFileChange = function(filepath) { 104 | this.log('File changed', filepath); 105 | var server = this.server; 106 | this.resolver(filepath, function(resource) { 107 | resource = normalizeResource(resource); 108 | server.broadcast(resource); 109 | }); 110 | }; 111 | 112 | /** 113 | * Closes the server and the watcher. 114 | * 115 | * @public 116 | */ 117 | 118 | Flo.prototype.close = function() { 119 | this.log('Shutting down flo'); 120 | this.watcher.close(); 121 | this.server.close(); 122 | }; 123 | 124 | /** 125 | * Creates a logger for a given module. 126 | * 127 | * @param {boolean} verbose 128 | * @param {string} moduleName 129 | * @private 130 | */ 131 | 132 | function logger(verbose, moduleName) { 133 | var slice = [].slice; 134 | return function() { 135 | var args = slice.call(arguments); 136 | args[0] = '[' + moduleName + '] ' + args[0]; 137 | if (verbose) { 138 | console.log.apply(console, args); 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Check if an object is of type RegExp. 145 | * 146 | * @param {*} o 147 | * @return {boolean} 148 | * @private 149 | */ 150 | 151 | function isRegExp(o) { 152 | return o && typeof o === 'object' && 153 | Object.prototype.toString.call(o) === '[object RegExp]'; 154 | } 155 | 156 | /** 157 | * Given a resource, enforce type checks, default params, and prepare for 158 | * serialization. 159 | * 160 | * @param {*} o 161 | * @return {boolean} 162 | * @private 163 | */ 164 | 165 | function normalizeResource(resource) { 166 | if (resource.reload) { 167 | return resource; 168 | } 169 | 170 | assert(resource.resourceURL, 'expecting resourceURL'); 171 | assert(resource.contents, 'expecting contents'); 172 | 173 | var ret = {}; 174 | 175 | ret.match = resource.match || 'indexOf'; 176 | ret.contents = resource.contents.toString(); 177 | ret.resourceURL = resource.resourceURL; 178 | 179 | assert.ok( 180 | isRegExp(ret.match) || 181 | ['indexOf', 'equal'].indexOf(ret.match) > -1, 182 | 'resource.match must be of type function, or regexp, ' + 183 | 'or a string either "indexOf" or "equal' 184 | ); 185 | 186 | if (isRegExp(ret.match)) { 187 | var r = ret.match; 188 | ret.match = { 189 | type: 'regexp', 190 | source: r.source, 191 | global: r.global, 192 | multiline: r.multiline, 193 | ignoreCase: r.ignoreCase 194 | }; 195 | } 196 | 197 | return ret; 198 | } 199 | 200 | /** 201 | * Default resolver callback that will simply read the file and pass back the 202 | * filename relative to the watched dir as the url. 203 | * 204 | * @param {string} dir 205 | * @return {function} 206 | * @private 207 | */ 208 | 209 | function noBuildCallback(dir) { 210 | return function(filepath, callback) { 211 | fs.readFile(path.join(dir, filepath), function(err, data) { 212 | // Todo better error handling. 213 | if (err) { 214 | throw err; 215 | } 216 | callback({ 217 | resourceURL: filepath, 218 | contents: data.toString() 219 | }); 220 | }); 221 | }; 222 | } 223 | -------------------------------------------------------------------------------- /client/configure/configure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | (function () { 11 | 12 | /** 13 | * Utils. 14 | */ 15 | 16 | var $ = document.querySelector.bind(document); 17 | 18 | function $$() { 19 | var els = document.querySelectorAll.apply(document, arguments); 20 | return [].slice.call(els); 21 | } 22 | 23 | function triggerEvent(type, data) { 24 | var event = new Event('flo_' + type); 25 | event.data = data; 26 | window.dispatchEvent(event); 27 | return event; 28 | }; 29 | 30 | function listenToEvent(type, callback) { 31 | window.addEventListener('flo_' + type, callback); 32 | } 33 | 34 | /** 35 | * Navigation. 36 | */ 37 | 38 | $('nav').onclick = function(e) { 39 | if (e.target.nodeName !== 'LI') return; 40 | $$('.selected').forEach(function(el) { 41 | el.classList.remove('selected'); 42 | }); 43 | e.target.classList.add('selected'); 44 | var tabClass = e.target.getAttribute('data-tab'); 45 | var tabEl = $('.' + tabClass); 46 | tabEl && tabEl.classList.add('selected'); 47 | }; 48 | 49 | /** 50 | * Storage. 51 | */ 52 | 53 | function save() { 54 | var sites = $$('.hostnames .item span').map(function(el) { 55 | return { 56 | pattern: el.dataset.pattern, 57 | server: el.dataset.server, 58 | port: el.dataset.port 59 | }; 60 | }); 61 | var port = $('input[name="default-port"').value.trim(); 62 | triggerEvent('config_changed',{ 63 | port: port, 64 | sites: sites 65 | }); 66 | } 67 | 68 | function load(config) { 69 | $('.hostnames').innerHTML = ''; 70 | config.sites.forEach(function(site) { 71 | $('.hostnames').appendChild(createHostnameOption(site)); 72 | }); 73 | $('input[name="default-port"]').value = config.port; 74 | } 75 | 76 | /** 77 | * Templates. 78 | */ 79 | 80 | function createHostnameOption(item) { 81 | var option = document.createElement('li'); 82 | var text = document.createElement('span'); 83 | var remove = document.createElement('a'); 84 | remove.textContent = 'x'; 85 | remove.classList.add('remove'); 86 | text.textContent = item.pattern; 87 | text.dataset.pattern = item.pattern; 88 | text.dataset.port = item.port; 89 | text.dataset.server = item.server; 90 | option.appendChild(text); 91 | option.appendChild(remove); 92 | option.classList.add('item'); 93 | return option; 94 | } 95 | 96 | function createLogItem(level, str) { 97 | var div = document.createElement('div'); 98 | div.classList.add('loglevel-' + level); 99 | div.textContent = str; 100 | return div; 101 | } 102 | 103 | function showHostnameForm(callback) { 104 | var form = $('.hostname-form'); 105 | form.classList.remove('hidden'); 106 | form.querySelector('button').onclick = function() { 107 | var pattern = form.querySelector('[name=pattern]').value.trim(); 108 | if (!pattern) { 109 | form.querySelector('[name=pattern]').classList.add('red'); 110 | return; 111 | } 112 | 113 | var item = { 114 | pattern: pattern, 115 | server: form.querySelector('[name=server]').value.trim(), 116 | port: form.querySelector('[name=port]').value.trim() 117 | }; 118 | 119 | this.onclick = null; 120 | form.classList.add('hidden'); 121 | $$('.hostname-form input').forEach(function(input) { 122 | input.classList.remove('red'); 123 | input.value = ''; 124 | }); 125 | 126 | callback(item); 127 | } 128 | } 129 | 130 | /** 131 | * Event handlers. 132 | */ 133 | 134 | $('form').onsubmit = function (e) { 135 | e.preventDefault(); 136 | }; 137 | 138 | $('button.add').onclick = function () { 139 | showHostnameForm(function(item) { 140 | var option = createHostnameOption(item); 141 | $('.hostnames').appendChild(option); 142 | save(); 143 | }); 144 | }; 145 | 146 | $('.hostnames').onclick = function (e) { 147 | if (e.target.classList.contains('remove')) { 148 | e.target.parentNode.parentNode.removeChild(e.target.parentNode); 149 | save(); 150 | } 151 | }; 152 | 153 | $('form').onchange = save; 154 | 155 | var prevStatus = 'disabled'; 156 | listenToEvent('status_change', function(e) { 157 | var data = e.data; 158 | var indicator = $('.status .indicator') 159 | indicator.classList.remove(prevStatus); 160 | indicator.classList.add(data.type); 161 | prevStatus = data.type; 162 | $('.status .text').textContent = data.text; 163 | $$('.status .action').forEach(function(el) { 164 | el.classList.add('hidden'); 165 | }); 166 | if (data.action) { 167 | $('.status .' + data.action).classList.remove('hidden'); 168 | } 169 | }); 170 | 171 | $('.action.retry').onclick = function() { 172 | triggerEvent('retry'); 173 | }; 174 | 175 | $('.action.enable').onclick = function() { 176 | triggerEvent('enable_for_host'); 177 | }; 178 | 179 | listenToEvent('load', function(e) { 180 | load(e.data); 181 | }); 182 | 183 | var logs = ''; 184 | 185 | listenToEvent('log', function(e) { 186 | var level = e.data[0]; 187 | var args = e.data[1]; 188 | var message = args.map(function(a) { 189 | return a.toString() 190 | }).join(' '); 191 | 192 | var item = createLogItem( 193 | level, 194 | message 195 | ); 196 | var box = $('.log-box'); 197 | box.appendChild(item); 198 | box.scrollTop = box.scrollHeight; 199 | 200 | logs += '\n' + '[' + level + ']' + message; 201 | var report = $('.report-bug'); 202 | report.search = 'title=' + 'Report from extension' 203 | + '&body=```' + logs + '\n```'; 204 | }); 205 | })(); 206 | -------------------------------------------------------------------------------- /client/connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | (function() { 11 | 'use strict'; 12 | 13 | /** 14 | * Export to node for testing and to global for production. 15 | */ 16 | 17 | if (typeof module === 'object' && typeof exports === 'object') { 18 | module.exports = Connection; 19 | } else { 20 | this.Connection = Connection; 21 | } 22 | 23 | /** 24 | * Constants. 25 | */ 26 | 27 | var DELAY = 500; 28 | var RETRIES = 5; 29 | var NOP = function () {}; 30 | 31 | /** 32 | * Takes care of connecting, messaging, handling connection retries with the 33 | * flo server. 34 | * 35 | * @param {string} host 36 | * @param {string} port 37 | * @param {object} logger 38 | * @class Connection 39 | * @public 40 | */ 41 | 42 | function Connection(host, port, createLogger) { 43 | this.retries = RETRIES; 44 | this.host = host; 45 | this.port = port; 46 | this.logger = createLogger('connection'); 47 | this.openHandler = this.openHandler.bind(this); 48 | this.messageHandler = this.messageHandler.bind(this); 49 | this.closeHandler = this.closeHandler.bind(this); 50 | } 51 | 52 | /** 53 | * Callbacks. 54 | */ 55 | 56 | Connection.prototype.callbacks = { 57 | connecting: NOP, 58 | message: NOP, 59 | error: NOP, 60 | retry: NOP, 61 | open: NOP 62 | }; 63 | 64 | /** 65 | * Connect to host. 66 | * 67 | * @public 68 | * @returns {Connection} this 69 | */ 70 | 71 | Connection.prototype.connect = function() { 72 | var url = 'ws://' + this.host + ':' + this.port + '/'; 73 | var ws = new WebSocket(url); 74 | 75 | this.callbacks.connecting(); 76 | this.logger.log('Connecting to', url); 77 | 78 | ws.onopen = this.openHandler; 79 | ws.onmessage = this.messageHandler; 80 | ws.onclose = this.closeHandler; 81 | 82 | this.ws = ws; 83 | return this; 84 | }; 85 | 86 | /** 87 | * Registers a message handler. 88 | * 89 | * @param {function} callback 90 | * @param {object} thisObj 91 | * @return {Connection} this 92 | * @public 93 | */ 94 | 95 | Connection.prototype.onmessage = makeCallbackRegistrar('message'); 96 | 97 | /** 98 | * Registers an error handler. 99 | * 100 | * @param {function} callback 101 | * @param {object} thisObj 102 | * @return {Connection} this 103 | * @public 104 | */ 105 | 106 | Connection.prototype.onerror = makeCallbackRegistrar('error'); 107 | 108 | /** 109 | * Registers a connection handler. 110 | * 111 | * @param {function} callback 112 | * @param {object} thisObj 113 | * @return {Connection} this 114 | * @public 115 | */ 116 | 117 | Connection.prototype.onopen = makeCallbackRegistrar('open'); 118 | 119 | /** 120 | * Registers a retry handler. 121 | * 122 | * @param {function} callback 123 | * @param {object} thisObj 124 | * @return {Connection} this 125 | * @public 126 | */ 127 | 128 | Connection.prototype.onretry = makeCallbackRegistrar('retry'); 129 | 130 | /** 131 | * Connecting callback 132 | * 133 | * @param {function} callback 134 | * @param {object} thisObj 135 | * @return {Connection} this 136 | * @public 137 | */ 138 | 139 | Connection.prototype.onconnecting = makeCallbackRegistrar('connecting'); 140 | 141 | /** 142 | * Disconnects from the server 143 | * 144 | * @param {function} callback 145 | * @return {Connection} this 146 | * @public 147 | */ 148 | 149 | Connection.prototype.disconnect = function (callback) { 150 | callback = callback || NOP; 151 | if (this.connected()) { 152 | this.ws.onclose = callback; 153 | this.ws.close(); 154 | } else { 155 | callback(); 156 | } 157 | return this; 158 | }; 159 | 160 | /** 161 | * Are we connected? 162 | * 163 | * @public 164 | * @return {boolean} 165 | */ 166 | 167 | Connection.prototype.connected = function() { 168 | return this.ws && this.ws.readyState === this.ws.OPEN; 169 | }; 170 | 171 | /** 172 | * Message handler. 173 | * 174 | * @param {object} evt 175 | * @private 176 | */ 177 | 178 | Connection.prototype.messageHandler = function(evt) { 179 | var msg = JSON.parse(evt.data); 180 | this.callbacks.message(msg); 181 | }; 182 | 183 | 184 | /** 185 | * Open handler. 186 | * 187 | * @private 188 | */ 189 | 190 | Connection.prototype.openHandler = function() { 191 | this.logger.log('Connected'); 192 | this.callbacks.open(); 193 | this.retries = RETRIES; 194 | }; 195 | 196 | 197 | /** 198 | * Retries to connect or emits error. 199 | * 200 | * @param {object} evt The event that caused the retry. 201 | * @private 202 | */ 203 | 204 | Connection.prototype.closeHandler = function(evt) { 205 | this.logger.error('Failed to connect with', evt.reason, evt.code); 206 | this.retries -= 1; 207 | if (this.retries < 1) { 208 | var err = new Error(evt.reason || 'Error connecting.'); 209 | this.callbacks.error(err); 210 | } else { 211 | var delay = (RETRIES - this.retries) * DELAY; 212 | this.logger.log('Reconnecting in ', delay); 213 | this.callbacks.retry(delay); 214 | setTimeout(function () { 215 | this.connect(); 216 | }.bind(this), delay); 217 | } 218 | }; 219 | 220 | /** 221 | * Creates a function that registers an event listener when called. 222 | * 223 | * @param {string} name 224 | * @return {function} 225 | * @private 226 | */ 227 | 228 | function makeCallbackRegistrar(name) { 229 | return function(cb, context) { 230 | this.callbacks[name] = cb.bind(context || null); 231 | return this; 232 | }; 233 | } 234 | 235 | }).call(this); 236 | -------------------------------------------------------------------------------- /test/client/session_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | var assert = require('assert'); 11 | var Server = require('../../lib/server'); 12 | var Session = require('../../client/session'); 13 | var WebSocket = require('./browser_websocket') 14 | var Connection = require('../../client/connection'); 15 | var mockLogger = require('./logger_mock'); 16 | 17 | global.Connection = Connection; 18 | global.WebSocket = WebSocket; 19 | 20 | var nop = function() {}; 21 | 22 | describe('Session', function() { 23 | 24 | beforeEach(function() { 25 | this.server = new Server({ port: port }); 26 | global.chrome = { 27 | devtools: { 28 | inspectedWindow: { 29 | getResources: function(callback) { 30 | callback(); 31 | }, 32 | onResourceAdded: { 33 | addListener: nop, 34 | removeListener: nop 35 | } 36 | }, 37 | network: { 38 | onNavigated: { 39 | addListener: nop, 40 | removeListener: nop 41 | } 42 | } 43 | }, 44 | }; 45 | }); 46 | afterEach(function() { 47 | this.server.close(); 48 | this.session.destroy(); 49 | }); 50 | 51 | var port = 8543; 52 | it('should start', function(done) { 53 | var i = 0; 54 | function status(state) { 55 | if (++i == 1) { 56 | assert.equal(state, 'connecting'); 57 | } else if (i == 2) { 58 | assert.equal(state, 'connected'); 59 | } else { 60 | assert.equal(state, 'started'); 61 | done(); 62 | } 63 | } 64 | this.session = new Session('localhost', port, status, mockLogger); 65 | this.session.start(); 66 | }); 67 | 68 | it('should clean properly', function() { 69 | var status = function(state) { 70 | if (state !== 'started') return; 71 | var called = 0; 72 | chrome.devtools.inspectedWindow.onResourceAdded.removeListener = 73 | chrome.devtools.network.onNavigated.removeListener = 74 | function() { called++; }; 75 | 76 | this.session.destroy(function() { 77 | assert(!this.server.connections.length); 78 | assert.equal(called, 2); 79 | }.bind(this)); 80 | }.bind(this); 81 | 82 | this.session = new Session('localhost', port, status, mockLogger); 83 | this.session.start(); 84 | }); 85 | 86 | describe('resource manipulation', function() { 87 | beforeEach(function() { 88 | chrome.devtools.inspectedWindow.getResources = function(callback) { 89 | callback([{ 90 | url: 'http://wat', 91 | setContent: function() { 92 | this.setContent.apply(this, arguments) 93 | }.bind(this) 94 | }]); 95 | }.bind(this); 96 | this.session = new Session('localhost', port, function(state) { 97 | if (state === 'started') this.started(); 98 | }.bind(this), mockLogger); 99 | this.session.start(); 100 | }); 101 | 102 | it('should change resources using equal matcher', function(done) { 103 | this.started = function() { 104 | this.server.broadcast({ 105 | resourceURL: 'http://wat', 106 | match: 'equal', 107 | contents: 'foo' 108 | }); 109 | }.bind(this); 110 | this.setContent = function(contents, callback) { 111 | assert.equal(contents, 'foo'); 112 | done(); 113 | }; 114 | }); 115 | 116 | it('should change resource using regexp matcher', function(done) { 117 | this.started = function() { 118 | this.server.broadcast({ 119 | resourceURL: 'http://wat', 120 | match: { 121 | type: 'regexp', 122 | source: 'waT', 123 | ignoreCase: true 124 | }, 125 | contents: 'foo' 126 | }); 127 | }.bind(this); 128 | 129 | this.setContent = function(contents, callback) { 130 | assert.equal(contents, 'foo'); 131 | done(); 132 | }; 133 | }); 134 | 135 | it('should change resource using regexp matcher', function(done) { 136 | this.started = function() { 137 | this.server.broadcast({ 138 | resourceURL: 'http://wat', 139 | match: { 140 | type: 'regexp', 141 | source: 'waT', 142 | ignoreCase: true 143 | }, 144 | contents: 'foo' 145 | }); 146 | }.bind(this); 147 | 148 | this.setContent = function(contents, callback) { 149 | assert.equal(contents, 'foo'); 150 | done(); 151 | }; 152 | }); 153 | 154 | it('should change resource using indexOf matcher', function(done) { 155 | this.started = function() { 156 | this.server.broadcast({ 157 | resourceURL: 'http://w', 158 | match: 'indexOf', 159 | contents: 'foo' 160 | }); 161 | }.bind(this); 162 | 163 | this.setContent = function(contents, callback) { 164 | assert.equal(contents, 'foo'); 165 | done(); 166 | }; 167 | }); 168 | 169 | it('should trigger an event when matched', function(done) { 170 | chrome.devtools.inspectedWindow.eval = function(expr) { 171 | assert(expr.match(/var event = new Event\('fb-flo-reload'\);/), 172 | "event creation"); 173 | assert(expr.match(/event.data = \{"url":"http:\/\/w","contents":"foo"}/), 174 | "event data"); 175 | assert(expr.match(/window.dispatchEvent\(event\);/), 176 | "event dispatch"); 177 | done(); 178 | }; 179 | 180 | this.setContent = function(contents, bool, callback) { 181 | callback({ code: 'OK' }); 182 | }; 183 | 184 | this.started = function() { 185 | this.server.broadcast({ 186 | resourceURL: 'http://w', 187 | match: 'indexOf', 188 | contents: 'foo' 189 | }); 190 | }.bind(this); 191 | }); 192 | }); 193 | 194 | }); 195 | -------------------------------------------------------------------------------- /test/server/flo_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | var fs = require('fs'); 11 | var flo = require('../../'); 12 | var assert = require('assert'); 13 | var WebSocketClient = require('websocket').client; 14 | 15 | function client(connectFailed, connect, message) { 16 | var client = new WebSocketClient(); 17 | client.connect('ws://localhost:8888/'); 18 | client.on('connectFailed', connectFailed); 19 | client.on('connect', function (connection) { 20 | if (connect) connect(connection); 21 | connection.on('message', function (msg) { 22 | var data = msg.utf8Data; 23 | data = JSON.parse(data); 24 | if (message) message(data); 25 | }); 26 | }); 27 | return client; 28 | } 29 | 30 | describe('flo(dir)', function() { 31 | var f; 32 | 33 | beforeEach(function(done) { 34 | try { 35 | fs.mkdirSync('/tmp/flo_test'); 36 | } catch (e) { 37 | // don't care 38 | } 39 | 40 | fs.writeFileSync('/tmp/flo_test/foo.js', 'alert("wow")'); 41 | setTimeout(done, 300); 42 | }); 43 | 44 | afterEach(function() { 45 | f.close(); 46 | }); 47 | 48 | it('should start a server', function(done) { 49 | f = flo('/tmp/flo_test'); 50 | f.on('added', console.log) 51 | f.on('ready', function () { 52 | var c = client( 53 | done.bind(null, new Error('Failed to connect')), 54 | done.bind(null, null) 55 | ); 56 | }); 57 | }); 58 | 59 | it('should send resource to the client when changed', function(done) { 60 | f = flo('/tmp/flo_test'); 61 | f.on('ready', function () { 62 | var c = client( 63 | done.bind(null, new Error('Failed to connect')), 64 | null, 65 | function (msg) { 66 | assert.deepEqual(msg, { 67 | contents: 'alert("hi")', 68 | resourceURL: 'foo.js', 69 | match: 'indexOf' 70 | }); 71 | done(); 72 | } 73 | ); 74 | fs.writeFileSync('/tmp/flo_test/foo.js', 'alert("hi")'); 75 | }); 76 | }); 77 | 78 | it('should work with css', function(done) { 79 | fs.writeFileSync('/tmp/flo_test/bar.css', 'bar {color: red}'); 80 | f = flo('/tmp/flo_test'); 81 | f.on('ready', function () { 82 | var c = client( 83 | done.bind(null, new Error('Failed to connect')), 84 | null, 85 | function(msg) { 86 | assert.deepEqual(msg, { 87 | contents: 'bar {color: blue}', 88 | resourceURL: 'bar.css', 89 | match: 'indexOf' 90 | }); 91 | done(); 92 | } 93 | ); 94 | fs.writeFileSync('/tmp/flo_test/bar.css', 'bar {color: blue}'); 95 | }); 96 | }); 97 | 98 | it('should send resource to multiple clients when changed', function(done) { 99 | f = flo('/tmp/flo_test'); 100 | f.on('ready', function () { 101 | var i = 0; 102 | function handler(connection) { 103 | connection.on('message', function(msg) { 104 | var data = msg.utf8Data; 105 | var msg = JSON.parse(data); 106 | assert.deepEqual(msg, { 107 | contents: 'alert("hi")', 108 | resourceURL: 'foo.js', 109 | match: 'indexOf' 110 | }); 111 | if (++i == 2) { 112 | done(); 113 | } else { 114 | // make sure we handle disconnects. 115 | connection.close(); 116 | } 117 | }); 118 | } 119 | 120 | var c1 = client( 121 | done.bind(null, new Error('Failed to connect')), 122 | handler 123 | ); 124 | var c2 = client( 125 | done.bind(null, new Error('Failed to connect')), 126 | handler 127 | ); 128 | 129 | fs.writeFileSync('/tmp/flo_test/foo.js', 'alert("hi")'); 130 | }); 131 | }); 132 | }); 133 | 134 | describe('flo(dir, resolver)', function() { 135 | var f; 136 | 137 | beforeEach(function() { 138 | try { 139 | fs.mkdirSync('/tmp/flo_test'); 140 | } catch (e) { 141 | // don't care 142 | } 143 | 144 | fs.writeFileSync('/tmp/flo_test/foo.js', 'alert("wow")'); 145 | }); 146 | 147 | afterEach(function() { 148 | f.close(); 149 | }); 150 | 151 | it('should work with a custom resolver', function(done) { 152 | f = flo('/tmp/flo_test', function (filepath, callback) { 153 | assert.equal(filepath, 'foo.js'); 154 | callback({ 155 | contents: 'foobar', 156 | resourceURL: 'customurl' 157 | }); 158 | }); 159 | 160 | f.on('ready', function() { 161 | var c = client( 162 | done.bind(null, new Error('Failed to connect')), 163 | null, 164 | function(msg) { 165 | assert.deepEqual(msg, { 166 | contents: 'foobar', 167 | resourceURL: 'customurl', 168 | match: 'indexOf' 169 | }); 170 | done(); 171 | } 172 | ); 173 | 174 | fs.writeFileSync('/tmp/flo_test/foo.js', 'hmmmm'); 175 | }); 176 | }); 177 | 178 | describe('resolver match property', function() { 179 | it('should serialize regexp objects', function(done) { 180 | f = flo('/tmp/flo_test', function (filepath, callback) { 181 | assert.equal(filepath, 'foo.js'); 182 | callback({ 183 | contents: 'foobar', 184 | resourceURL: 'customurl', 185 | match: /a(b)+/gi 186 | }); 187 | }); 188 | 189 | f.on('ready', function() { 190 | var c = client( 191 | done.bind(null, new Error('Failed to connect')), 192 | null, 193 | function(msg) { 194 | assert.deepEqual(msg, { 195 | contents: 'foobar', 196 | resourceURL: 'customurl', 197 | match: { 198 | type: 'regexp', 199 | global: true, 200 | ignoreCase: true, 201 | multiline: false, 202 | source: 'a(b)+' 203 | } 204 | }); 205 | done(); 206 | } 207 | ); 208 | 209 | fs.writeFileSync('/tmp/flo_test/foo.js', 'hmmmm'); 210 | }); 211 | }); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /client/configure/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | cursor: default; 7 | position: absolute; 8 | top: 0; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | overflow: hidden; 13 | font-family: Lucida Grande, sans-serif; 14 | font-size: 12px; 15 | margin: 0; 16 | tab-size: 4; 17 | -webkit-user-select: none; 18 | color: #222; 19 | } 20 | 21 | body.platform-linux { 22 | color: rgb(48, 57, 66); 23 | font-family: Ubuntu, Arial, sans-serif; 24 | } 25 | 26 | body.platform-mac { 27 | color: rgb(48, 57, 66); 28 | font-family: 'Lucida Grande', sans-serif; 29 | } 30 | 31 | body.dock-to-right:not(.undocked):not(.overlay-contents) { 32 | border-left: 1px solid rgb(80, 80, 80); 33 | } 34 | 35 | body.dock-to-right.inactive:not(.undocked):not(.overlay-contents) { 36 | border-left: 1px solid rgb(64%, 64%, 64%); 37 | } 38 | 39 | body.platform-windows { 40 | font-family: 'Segoe UI', Tahoma, sans-serif; 41 | } 42 | 43 | * { 44 | box-sizing: border-box; 45 | } 46 | 47 | :focus { 48 | outline: none; 49 | } 50 | 51 | img { 52 | -webkit-user-drag: none; 53 | } 54 | 55 | iframe, 56 | a img { 57 | border: none; 58 | } 59 | 60 | iframe.view { 61 | position: absolute; 62 | width: 100%; 63 | height: 100%; 64 | left: 0; 65 | right: 0; 66 | top: 0; 67 | bottom: 0; 68 | } 69 | 70 | .hidden { 71 | display: none !important; 72 | } 73 | 74 | main { 75 | display: flex; 76 | flex-direction: row; 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | right: 0; 81 | bottom: 0; 82 | overflow: hidden; 83 | } 84 | 85 | nav { 86 | flex: none; 87 | flex-basis: 200px; 88 | flex-direction: column; 89 | display: flex !important; 90 | border-right: 1px solid rgb(64%, 64%, 64%); 91 | background-color: rgb(214, 221, 229); 92 | overflow: auto; 93 | overflow-x: hidden; 94 | cursor: default; 95 | position: relative; 96 | } 97 | 98 | ol { 99 | position: relative; 100 | padding: 0; 101 | margin: 0; 102 | list-style: none; 103 | } 104 | 105 | nav ol li.selected { 106 | color: white; 107 | text-shadow: rgba(0, 0, 0, 0.33) 1px 1px 0; 108 | background-color: rgb(180,180,180); 109 | background-color: rgb(56, 121, 217); 110 | } 111 | 112 | nav ol li { 113 | position: relative; 114 | height: 36px; 115 | padding: 0 5px 0 7px; 116 | white-space: nowrap; 117 | overflow-x: hidden; 118 | overflow-y: hidden; 119 | margin-top: 1px; 120 | line-height: 34px; 121 | } 122 | 123 | nav ol li:first-child { 124 | margin-top: 2px; 125 | } 126 | 127 | section.tab { 128 | display: flex; 129 | flex-direction: column; 130 | flex: auto; 131 | overflow: auto; 132 | cursor: default; 133 | position: relative; 134 | padding: 0 16px; 135 | font-size: 13px; 136 | display: none; 137 | } 138 | 139 | section.tab.selected { 140 | display: flex; 141 | } 142 | 143 | section.tab > *{ 144 | flex-shrink: 0; 145 | } 146 | 147 | h1 { 148 | color: rgb(110, 116, 128); 149 | font-size: 16px; 150 | line-height: 20px; 151 | font-weight: normal; 152 | margin-top: 0; 153 | padding: 15px 0 10px; 154 | } 155 | 156 | .desc { 157 | color: #808080; 158 | padding: 0; 159 | margin: 3px 0 15px; 160 | } 161 | 162 | select.hostnames { 163 | min-width: 300px; 164 | height: 100px; 165 | font-size: 13px; 166 | } 167 | 168 | button { 169 | font-size: 12px; 170 | padding: 2px 20px; 171 | height: 24px; 172 | color: rgb(6, 6, 6); 173 | border: 1px solid rgb(165, 165, 165); 174 | background-color: rgb(237, 237, 237); 175 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(252, 252, 252)), to(rgb(223, 223, 223))); 176 | border-radius: 12px; 177 | -webkit-appearance: none; 178 | } 179 | 180 | button:active { 181 | background-color: rgb(215, 215, 215); 182 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(194, 194, 194)), to(rgb(239, 239, 239))); 183 | } 184 | 185 | .hostnames-container { 186 | margin-bottom: 14px; 187 | overflow: hidden; 188 | } 189 | 190 | .hostnames-options { 191 | overflow: hidden; 192 | } 193 | 194 | .hostnames-container button.add { 195 | margin-left: 5px; 196 | display:block; 197 | margin-top: 10px; 198 | } 199 | .hostnames { 200 | min-height: 115px; 201 | max-height: 180px; 202 | min-width: 300px; 203 | max-width: 80%; 204 | border: 1px; 205 | border: 1px solid #ddd; 206 | padding: 8px; 207 | margin: 0; 208 | display: inline-block; 209 | overflow-y: scroll; 210 | } 211 | 212 | .hostnames .item { 213 | padding: 4px; 214 | position: relative; 215 | height: 30px; 216 | margin-bottom: 5px; 217 | line-height: 19px; 218 | background: #FAFAFA; 219 | border-radius: 4px; 220 | } 221 | 222 | .hostnames .item:last-child { 223 | margin-bottom: 0; 224 | } 225 | 226 | .hostnames .item a { 227 | position: absolute; 228 | right: 0; 229 | bottom: 0; 230 | top: 0; 231 | text-align: center; 232 | line-height: 20px; 233 | width: 30px; 234 | line-height: 24px; 235 | width: 30px; 236 | color: #ddd; 237 | cursor: pointer; 238 | font-size: 15px; 239 | } 240 | 241 | .hostnames .item a:hover { 242 | color: black; 243 | } 244 | 245 | .port-container { 246 | clear: both; 247 | } 248 | 249 | input { 250 | border: 1px solid #ddd; 251 | font-size: 13px; 252 | padding: 4px; 253 | } 254 | 255 | .status .indicator { 256 | display: inline-block; 257 | width: 16px; 258 | height: 16px; 259 | border-radius: 10px; 260 | background-color: red; 261 | margin: 0 10px -3px 10px; 262 | } 263 | 264 | .status .indicator.connected, 265 | .status .indicator.started { 266 | background-color: green; 267 | } 268 | 269 | .status .indicator.connecting { 270 | background-color: orange; 271 | } 272 | 273 | .status .indicator.disabled { 274 | background-color: gray; 275 | } 276 | 277 | .status .text { 278 | line-height: 19px; 279 | } 280 | 281 | .status .action { 282 | display: block; 283 | margin-top: 13px; 284 | } 285 | 286 | .log-box { 287 | min-height: 100px; 288 | max-height: 180px; 289 | min-width: 300px; 290 | max-width: 80%; 291 | border: 1px; 292 | border: 1px solid #ddd; 293 | padding: 8px; 294 | margin: 0; 295 | display: inline-block; 296 | overflow-y: scroll; 297 | margin-bottom: 15px; 298 | } 299 | 300 | .log-box .loglevel-error { 301 | color: #FF0000; 302 | } 303 | 304 | .hostnames-options, .hostname-form { 305 | float: left; 306 | } 307 | 308 | .hostname-form { 309 | padding-left: 15px 310 | } 311 | 312 | .hostname-form input { 313 | width: 165px; 314 | } 315 | 316 | .hostname-form div:nth-child(2) { 317 | margin-top: 10px; 318 | } 319 | 320 | ::-webkit-input-placeholder { 321 | color: #6E7487; 322 | } 323 | 324 | input.red::-webkit-input-placeholder { 325 | color: red; 326 | } 327 | 328 | .hidden { 329 | display: none; 330 | } 331 | 332 | .report-bug { 333 | font-size: 11px; 334 | } -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | /*global Session:false */ 11 | /* jshint evil:true */ 12 | 13 | (function() { 14 | 'use strict'; 15 | 16 | /** 17 | * Flo client controller. 18 | * 19 | * @class FloClient 20 | * @private 21 | */ 22 | 23 | function FloClient() { 24 | this.config = loadConfig(); 25 | this.session = null; 26 | this.panelWindow = null; 27 | this.panelEventBuffer = []; 28 | this.status = this.status.bind(this); 29 | this.startNewSession = this.startNewSession.bind(this); 30 | this.createLogger = Logger(this.triggerEvent.bind(this, 'log')); 31 | this.loggger = this.createLogger('flo'); 32 | this.createPanel(); 33 | this.start(); 34 | } 35 | 36 | /** 37 | * Save and optionally set new config. 38 | * 39 | * @param {object} config 40 | * @private 41 | */ 42 | 43 | FloClient.prototype.saveConfig = function() { 44 | localStorage.setItem('flo-config', JSON.stringify(this.config)); 45 | }; 46 | 47 | /** 48 | * Listen on the panel window for an event `type`, i.e. receive a message 49 | * from the panel. 50 | * 51 | * @param {string} type 52 | * @param {function} callback 53 | * @private 54 | */ 55 | 56 | FloClient.prototype.listenToPanel = function(type, callback) { 57 | if (!this.panelWindow) { 58 | throw new Error('Panel not found'); 59 | } 60 | this.panelWindow.addEventListener('flo_' + type, callback.bind(this)); 61 | }; 62 | 63 | /** 64 | * Trigger an event on the panel window, i.e. send a message to the panel. 65 | * If the panel wasn't instantiated yet, the event is buffered. 66 | * 67 | * @param {string} type 68 | * @param {object} data 69 | * @private 70 | */ 71 | 72 | FloClient.prototype.triggerEvent = function(type, data) { 73 | var event = new Event('flo_' + type); 74 | event.data = data; 75 | // Save events for anytime we need to reinit the panel with prev state. 76 | this.panelEventBuffer.push(event); 77 | if (this.panelWindow) { 78 | this.panelWindow.dispatchEvent(event); 79 | } 80 | return event; 81 | }; 82 | 83 | /** 84 | * Create a new panel. 85 | * 86 | * @param {function} callback 87 | * @private 88 | */ 89 | 90 | FloClient.prototype.createPanel = function(callback) { 91 | var self = this; 92 | chrome.devtools.panels.create( 93 | 'flo', 94 | '', 95 | 'configure/configure.html', 96 | function (panel) { 97 | panel.onShown.addListener(function(panelWindow) { 98 | if (!panelWindow.wasShown) { 99 | self.panelWindow = panelWindow; 100 | self.initPanel(); 101 | panelWindow.wasShown = true; 102 | } 103 | }); 104 | } 105 | ); 106 | }; 107 | 108 | /** 109 | * Called after the panel is first created to listen on it's events. 110 | * Will also trigger all buffered events on the panel. 111 | * 112 | * @param {object} config 113 | * @private 114 | */ 115 | 116 | FloClient.prototype.initPanel = function() { 117 | this.listenToPanel('config_changed', function(e) { 118 | this.config = e.data; 119 | this.saveConfig(); 120 | this.startNewSession(); 121 | }); 122 | this.listenToPanel('retry', this.startNewSession); 123 | this.listenToPanel('enable_for_host', this.enableForHost); 124 | this.panelEventBuffer.forEach(function(event) { 125 | this.panelWindow.dispatchEvent(event); 126 | }, this); 127 | this.triggerEvent('load', this.config); 128 | }; 129 | 130 | /** 131 | * Starts the flo client. 132 | * 133 | * @private 134 | */ 135 | 136 | FloClient.prototype.start = function() { 137 | this.status('starting'); 138 | this.startNewSession(); 139 | }; 140 | 141 | 142 | /** 143 | * Stops flo client. 144 | * 145 | * @private 146 | */ 147 | 148 | FloClient.prototype.stop = function() { 149 | this.session.destroy(); 150 | this.session = null; 151 | }; 152 | 153 | /** 154 | * Get the url location of the inspected window. 155 | * 156 | * @param {function} callback 157 | * @private 158 | */ 159 | 160 | FloClient.prototype.getLocation = function(callback) { 161 | chrome.devtools.inspectedWindow['eval']( 162 | 'location.hostname', 163 | callback.bind(this) 164 | ); 165 | }; 166 | 167 | /** 168 | * Match config patterns against `host` and returns the matched site record. 169 | * 170 | * @param {string} host 171 | * @return {object|null} 172 | * @private 173 | */ 174 | 175 | FloClient.prototype.getSite = function(host) { 176 | var config = this.config; 177 | for (var i = 0; i < config.sites.length; i++) { 178 | var site = config.sites[i]; 179 | var pattern = parsePattern(site.pattern); 180 | var matched = false; 181 | if (pattern instanceof RegExp) { 182 | matched = pattern.exec(host); 183 | } else { 184 | matched = pattern === host; 185 | } 186 | if (matched) return site; 187 | } 188 | return null; 189 | }; 190 | 191 | /** 192 | * Instantiates a new `session`. 193 | * 194 | * @private 195 | */ 196 | 197 | FloClient.prototype.startNewSession = function() { 198 | if (this.session) { 199 | this.stop(); 200 | } 201 | 202 | this.getLocation( 203 | function (host) { 204 | var site = this.getSite(host); 205 | if (site) { 206 | this.session = new Session( 207 | site.server || host, 208 | site.port || this.config.port, 209 | this.status, 210 | this.createLogger 211 | ); 212 | this.session.start(); 213 | } else { 214 | this.status('disabled'); 215 | } 216 | } 217 | ); 218 | }; 219 | 220 | /** 221 | * Enables flo for the current inspected window host. 222 | * 223 | * @private 224 | */ 225 | 226 | FloClient.prototype.enableForHost = function() { 227 | this.getLocation(function(host) { 228 | if (!this.getSite(host)) { 229 | this.config.sites.push({ 230 | pattern: host, 231 | server: host 232 | }); 233 | this.saveConfig(); 234 | this.triggerEvent('load', this.config); 235 | this.startNewSession(); 236 | } 237 | }); 238 | }; 239 | 240 | /** 241 | * Reports status changes to panel. 242 | * 243 | * @param {string} status 244 | * @param {object} aux 245 | * @private 246 | */ 247 | 248 | FloClient.prototype.status = function(status, aux) { 249 | var text, action; 250 | switch (status) { 251 | case 'starting': 252 | text = 'Starting'; 253 | break; 254 | case 'disabled': 255 | text = 'Disabled for this site'; 256 | action = 'enable'; 257 | break; 258 | case 'connecting': 259 | text = 'Connecting'; 260 | break; 261 | case 'connected': 262 | text = 'Connected'; 263 | break; 264 | case 'started': 265 | text = 'Started'; 266 | break; 267 | case 'retry': 268 | text = 'Failed to connect, retrying in ' + (aux / 1000) + 's'; 269 | break; 270 | case 'error': 271 | text = 'Error connecting'; 272 | action = 'retry'; 273 | break; 274 | default: 275 | throw new Error('Unknown session status.'); 276 | } 277 | 278 | this.triggerEvent('status_change', { 279 | type: status, 280 | text: text, 281 | action: action 282 | }); 283 | }; 284 | 285 | /** 286 | * Loads config from localstorage. 287 | * 288 | * @private 289 | */ 290 | 291 | function loadConfig() { 292 | var config = localStorage.getItem('flo-config'); 293 | try { 294 | config = JSON.parse(config); 295 | } catch (e) {} finally { 296 | return config || { 297 | sites: [], 298 | port: 8888 299 | }; 300 | } 301 | } 302 | 303 | /** 304 | * Optionally parses config from JSON to an object. 305 | * Also, parses patterns into regexp. 306 | * 307 | * @private 308 | * @return {object} 309 | */ 310 | 311 | function parsePattern(pattern) { 312 | if (!pattern) return null; 313 | var m = pattern.match(/^\/(.+)\/([gim]{0,3})$/); 314 | if (m && m[1]) { 315 | return new RegExp(m[1], m[2]); 316 | } 317 | return pattern; 318 | } 319 | 320 | // Start the app. 321 | new FloClient(); 322 | 323 | })(); 324 | -------------------------------------------------------------------------------- /client/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | /*global Connection:false */ 11 | 12 | (function () { 13 | 'use strict'; 14 | 15 | /** 16 | * Export to Node for testing and to global for production. 17 | */ 18 | 19 | if (typeof module === 'object' && typeof exports === 'object') { 20 | module.exports = Session; 21 | } else { 22 | this.Session = Session; 23 | } 24 | 25 | /** 26 | * Manages a user sessios. 27 | * 28 | * @param {string} host 29 | * @param {number} port 30 | * @param {function} status 31 | * @param {function} logger 32 | * @class Session 33 | * @public 34 | */ 35 | 36 | function Session(host, port, status, createLogger) { 37 | this.host = host; 38 | this.port = port; 39 | this.status = status; 40 | this.createLogger = createLogger; 41 | this.logger = createLogger('session'); 42 | this.resources = null; 43 | this.conn = null; 44 | this.listeners = {}; 45 | this.messageHandler = this.messageHandler.bind(this); 46 | this.started = this.started.bind(this); 47 | } 48 | 49 | /** 50 | * Registers the resources, connects to server and listens to events. 51 | * 52 | * @public 53 | */ 54 | 55 | Session.prototype.start = function() { 56 | this.logger.log('Starting flo for host', this.host); 57 | this.getResources(this.connect.bind(this, this.started)); 58 | }; 59 | 60 | /** 61 | * Similar to restart but does only what's needed to get flo started. 62 | * 63 | * @public 64 | */ 65 | 66 | Session.prototype.restart = function() { 67 | this.logger.log('Restarting'); 68 | this.removeEventListeners(); 69 | if (this.conn.connected()) { 70 | // No need to reconnect. We just refetch the resources. 71 | this.getResources(this.started.bind(this)); 72 | } else { 73 | this.start(); 74 | } 75 | }; 76 | 77 | /** 78 | * This method takes care of listening to events defined by the chrome api 79 | * @see http://developer.chrome.com/extensions/events 80 | * We also keep an internal map of events we're listening to so we can 81 | * unsubscribe in the future. 82 | * 83 | * @param {object} object 84 | * @param {string} event 85 | * @param {function} listener 86 | * @private 87 | */ 88 | 89 | Session.prototype.listen = function(obj, event, listener) { 90 | listener = listener.bind(this); 91 | obj[event].addListener(listener); 92 | this.listeners[event] = { 93 | obj: obj, 94 | listener: listener 95 | }; 96 | }; 97 | 98 | 99 | /** 100 | * Remove all event listeners. 101 | * 102 | * @private 103 | */ 104 | 105 | Session.prototype.removeEventListeners = function() { 106 | Object.keys(this.listeners).forEach(function(event) { 107 | var desc = this.listeners[event]; 108 | desc.obj[event].removeListener(desc.listener); 109 | }, this); 110 | }; 111 | 112 | /** 113 | * Registers the resources and listens to onResourceAdded events. 114 | * 115 | * @param {function} callback 116 | * @private 117 | */ 118 | 119 | Session.prototype.getResources = function(callback) { 120 | var self = this; 121 | chrome.devtools.inspectedWindow.getResources(function (resources) { 122 | self.resources = resources; 123 | // After we register the current resources, we listen to the 124 | // onResourceAdded event to push on more resources lazily fetched 125 | // to our array. 126 | self.listen( 127 | chrome.devtools.inspectedWindow, 128 | 'onResourceAdded', 129 | function (res) { 130 | self.resources.push(res); 131 | } 132 | ); 133 | callback(); 134 | }); 135 | }; 136 | 137 | /** 138 | * Connect to server. 139 | * 140 | * @param {function} callback 141 | * @private 142 | */ 143 | 144 | Session.prototype.connect = function(callback) { 145 | callback = once(callback); 146 | var self = this; 147 | this.conn = new Connection(this.host, this.port, this.createLogger) 148 | .onmessage(this.messageHandler) 149 | .onerror(function () { 150 | self.status('error'); 151 | }) 152 | .onopen(function () { 153 | self.status('connected'); 154 | callback(); 155 | }) 156 | .onretry(function(delay) { 157 | self.status('retry', delay); 158 | }) 159 | .onconnecting(function() { 160 | self.status('connecting'); 161 | }) 162 | .connect(); 163 | }; 164 | 165 | /** 166 | * Does whatever needs to be done after the session is started. Currenlty 167 | * just listening to page refresh events. 168 | * 169 | * @param {function} callback 170 | */ 171 | 172 | Session.prototype.started = function() { 173 | this.logger.log('Started'); 174 | this.status('started'); 175 | this.listen( 176 | chrome.devtools.network, 177 | 'onNavigated', 178 | this.restart 179 | ); 180 | }; 181 | 182 | /** 183 | * Handler for messages from the server. 184 | * 185 | * @param {object} updatedResource 186 | * @private 187 | */ 188 | 189 | Session.prototype.messageHandler = function(updatedResource) { 190 | this.logger.log('Requested resource update', updatedResource.resourceURL); 191 | 192 | if (updatedResource.reload) { 193 | chrome.devtools.inspectedWindow.reload(); 194 | return; 195 | } 196 | 197 | var match = updatedResource.match; 198 | var matcher; 199 | 200 | if (typeof match === 'string') { 201 | if (match === 'indexOf') { 202 | matcher = indexOfMatcher; 203 | } else if (match === 'equal') { 204 | matcher = equalMatcher; 205 | } else { 206 | this.logger.error('Unknown match string option', match); 207 | return; 208 | } 209 | } else if (match && typeof match === 'object') { 210 | if (match.type === 'regexp') { 211 | var flags = ''; 212 | if (match.ignoreCase) { 213 | flags += 'i'; 214 | } 215 | if (match.multiline) { 216 | flags += 'm'; 217 | } 218 | if (match.global) { 219 | flags += 'g'; 220 | } 221 | var r = new RegExp(match.source, flags); 222 | matcher = r.exec.bind(r); 223 | } else { 224 | this.logger.error('Unknown matcher object:', match); 225 | return; 226 | } 227 | } 228 | 229 | var resource = this.resources.filter(function (res) { 230 | return matcher(res.url, updatedResource.resourceURL); 231 | })[0]; 232 | 233 | if (!resource) { 234 | this.logger.error( 235 | 'Resource with the following URL is not on the page:', 236 | updatedResource.resourceURL 237 | ); 238 | return; 239 | } 240 | 241 | resource.setContent(updatedResource.contents, true, function (status) { 242 | if (status.code === 'OK') { 243 | this.logger.log('Resource update successful'); 244 | triggerReloadEvent(updatedResource); 245 | } else { 246 | this.logger.error( 247 | 'flo failed to update, this shouldn\'t happen please report it: ' + 248 | JSON.stringify(status) 249 | ); 250 | } 251 | }.bind(this)); 252 | }; 253 | 254 | /** 255 | * Destroys session. 256 | * 257 | * @public 258 | */ 259 | 260 | Session.prototype.destroy = function() { 261 | this.removeEventListeners(); 262 | if (this.conn) this.conn.disconnect(); 263 | }; 264 | 265 | /** 266 | * Utility to ensure's a function is called only once. 267 | * 268 | * @param {function} cb 269 | * @private 270 | */ 271 | 272 | function once(cb) { 273 | var called = false; 274 | return function() { 275 | if (!called) { 276 | called = true; 277 | return cb.apply(this, arguments); 278 | } 279 | }; 280 | } 281 | 282 | function triggerReloadEvent(resource) { 283 | var data = { 284 | url: resource.resourceURL, 285 | contents: resource.contents 286 | }; 287 | 288 | var script = '(function() {' + 289 | 'var event = new Event(\'fb-flo-reload\');' + 290 | 'event.data = ' + JSON.stringify(data) + ';' + 291 | 'window.dispatchEvent(event);' + 292 | '})()'; 293 | 294 | chrome.devtools.inspectedWindow.eval(script); 295 | } 296 | 297 | function indexOfMatcher(val, resourceURL) { 298 | return val.indexOf(resourceURL) > -1; 299 | } 300 | 301 | function equalMatcher(val, resourceURL) { 302 | return resourceURL === val; 303 | } 304 | 305 | }).call(this); 306 | --------------------------------------------------------------------------------