├── .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 |
10 |
11 | Status & Log
12 | Configuration
13 |
14 |
15 |
16 | Status
17 |
18 |
19 |
20 | Activate for this site
21 | Try again
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Configure flo
30 |
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 | 
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 |
--------------------------------------------------------------------------------