├── .npmignore
├── .gitignore
├── Makefile
├── package.json
├── Readme.md
├── client.js
├── History.md
├── browsers.json
└── repl.js
/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | static/build.js
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | # apps
3 | NODE ?= node
4 | NPM ?= npm
5 | BROWSERIFY ?= $(NODE) ./node_modules/.bin/browserify
6 |
7 | build: static/build.js
8 |
9 | node_modules: package.json
10 | $(NPM) install
11 |
12 | static/build.js: client.js node_modules
13 | mkdir -p static
14 | $(BROWSERIFY) $< > $@
15 |
16 | clean:
17 | rm static/build.js
18 |
19 | .PHONY: install build clean
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browser-repl",
3 | "version": "0.4.0",
4 | "description": "CLI utility to set up a remote browser repl",
5 | "dependencies": {
6 | "array-map": "0.0.0",
7 | "express": "3.4.8",
8 | "foreach": "2.0.4",
9 | "minimist": "0.0.7",
10 | "ngrok": "0.1.99",
11 | "socket.io": "1.3.5",
12 | "socket.io-client": "1.3.5",
13 | "to-array": "0.1.4",
14 | "util-inspect": "0.1.8",
15 | "wd": "0.3.11"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git://github.com/Automattic/browser-repl.git"
20 | },
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/Automattic/browser-repl/issues"
24 | },
25 | "bin": {
26 | "repl": "./repl.js"
27 | },
28 | "scripts": {
29 | "prepublish": "make build"
30 | },
31 | "devDependencies": {
32 | "browserify": "8.1.3"
33 | }
34 | }
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 |
2 | # browser-repl
3 |
4 | CLI utility to set up a remote browser repl.
5 |
6 | 
7 |
8 | ## How to use
9 |
10 | ```js
11 | $ npm install -g browser-repl
12 | $ export SAUCE_USERNAME="your username"
13 | $ export SAUCE_ACCESS_KEY="your key"
14 | $ repl ie6
15 | ```
16 |
17 | Sign up for a free OSS account on [SauceLabs](http://saucelabs.com).
18 |
19 | ## How it works
20 |
21 | `browser-repl` is built on top of the `wd` module, which is an
22 | implementation of the webdriver protocol.
23 |
24 | Once a browser session is established,
25 | [socket.io](http://github.com/learnboost/socket.io) is used to establish
26 | a persistent connection that works on all browsers as fast as possible.
27 |
28 | The socket.io server is hosted locally, and a reverse tunnel is set up
29 | with [localtunnel](https://github.com/defunctzombie/localtunnel)
30 | which gives your computer a temporary URL of the format
31 | `https://{uid}.localtunnel.me`.
32 |
33 | The lines you enter are subsequently `eval`d.
34 | A global `window.onerror` hook is also set to capture errors.
35 | Summoning `repl` with the `-n` argument disables this.
36 |
37 | ## Contributors
38 |
39 | - [Nathan Rajlich](https://github.com/tootallnate)
40 | - [Guillermo Rauch](https://github.com/guille)
41 |
42 | ## License
43 |
44 | MIT - Copyright © 2014 Automattic, Inc.
45 |
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | var io = require('socket.io-client');
2 | var map = require('array-map');
3 | var each = require('foreach');
4 | var toArray = require('to-array');
5 | var inspect = require('util-inspect');
6 | var socket = io();
7 |
8 | // make `console` remote
9 | if (!global.options.k) {
10 | global.console = {};
11 | each(['log', 'info', 'warn', 'error', 'debug'], function(m){
12 | global.console[m] = function(){
13 | var args = toArray(arguments);
14 | socket.emit('console', m, map(args, function(a){
15 | return inspect(a, { colors: true });
16 | }));
17 | };
18 | });
19 | }
20 |
21 | socket.on('run', function(js, fn){
22 | try {
23 | // eval in the global scope (http://stackoverflow.com/a/5776496/376773)
24 | var rtn = (function() { return eval.apply(this, arguments); })(js);
25 |
26 | // save the previous value as `_`. matches node's main REPL behavior
27 | global._ = rtn;
28 |
29 | fn(null, inspect(rtn, { colors: true }));
30 | } catch(e) {
31 | // we have to create a "flattened" version of the `e` Error object,
32 | // for JSON serialization purposes
33 | var err = {};
34 | for (var i in e) err[i] = e[i];
35 | err.message = e.message;
36 | err.stack = e.stack;
37 | // String() is needed here apparently for IE6-8 which throw an error deep in
38 | // socket.io that is hard to debug through SauceLabs remotely. For some
39 | // reason, toString() here bypasses the bug...
40 | err.name = String(e.name);
41 | fn(err);
42 | }
43 | });
44 |
45 | window.onerror = function(message, url, linenumber){
46 | socket.emit('global err', message, url, linenumber);
47 | };
48 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 |
2 | 0.4.0 / 2015-05-24
3 | ==================
4 |
5 | * repl: add a 'connected to' log with browser version.
6 | * repl: feature-detect the ansi readline bugfix.
7 | * repl: verbose listing of available browsers. color prompt.
8 | * browsers: update all to latest. explicit versions. fix OSX
9 | * browsers: add support for Android.
10 | * repl: support for android devices.
11 | * package: wd to 0.3
12 | * package: bump ngrok & socket.io
13 | * Makefile: fix `install` rule
14 | * ensure install prior to publish
15 |
16 | 0.3.2 / 2015-02-02
17 | ==================
18 |
19 | * repl: replace localtunnel with ngrok
20 | * package: bump socket.io and add ngrok
21 |
22 | 0.3.1 / 2015-02-02
23 | ==================
24 |
25 | * Makefile: ensure `static` dir is created
26 | * package: add "description" field
27 | * package: add "repository" and "license" fields
28 | * package: remove unused "debug" dependency
29 | * package: update "browserify" to v8.1.3
30 | * package: use npm's versions of "socket.io" and "socket.io-client"
31 |
32 | 0.3.0 / 2014-04-08
33 | ==================
34 |
35 | * repl: add HTML5 doctype tag
36 | * client, repl: use the correct window.onerror args
37 | * client: emit on the `socket`, not `io`
38 | * Readme: better gif
39 |
40 | 0.2.1 / 2014-02-17
41 | ==================
42 |
43 | * bump
44 |
45 | 0.2.0 / 2014-02-17
46 | ==================
47 |
48 | * repl: added sauce heartbeats
49 | * client: save the previous expression result as `_`
50 | * client: properly serialize the `message` and `stack`
51 | * repl: prepend the "name" and "message" to stack
52 | * repl: parse more syntax errors from more browsers
53 | * client, repl: better "error" handling
54 | * client: eval in the global scope
55 | * browsers: fix ie10
56 | * fix ie9, fix prompt, fix not specifying version
57 | * improve browserify build
58 | * client: remove redundant `color` option
59 | * Makefile: add a `build` rule
60 | * repl: don't use ansi escape codes in prompt
61 |
62 | 0.1.0 / 2014-02-13
63 | ==================
64 |
65 | * first release
66 |
--------------------------------------------------------------------------------
/browsers.json:
--------------------------------------------------------------------------------
1 | {
2 | "browsers": {
3 |
4 | "ie": { "name": "internet explorer", "platform": "win8.1" },
5 | "ie6": { "name": "internet explorer", "platform": "winxp" },
6 | "ie7": { "name": "internet explorer", "platform": "winxp" },
7 | "ie8": { "name": "internet explorer", "platform": "win7" },
8 | "ie9": { "name": "internet explorer", "platform": "win7" },
9 | "ie10": { "name": "internet explorer", "platform": "win8" },
10 | "ie11": { "name": "internet explorer", "platform": "win8" , "version": "11" },
11 |
12 | "opera": { "name": "opera", "platform": "win7" },
13 |
14 | "safari": { "name": "safari", "platform": "mac10.10", "version": "8" },
15 | "safari5": { "name": "safari", "platform": "mac10.6", "version": "5.1" },
16 | "safari6": { "name": "safari", "platform": "mac10.8", "version": "6" },
17 | "safari7": { "name": "safari", "platform": "mac10.9", "version": "7" },
18 | "safari8": { "name": "safari", "platform": "mac10.9", "version": "8" },
19 |
20 | "chrome": { "name": "chrome", "platform": "win8.1" },
21 | "chromedev": { "name": "chrome", "platform": "win8.1", "version": "dev" },
22 |
23 | "firefox": { "name": "firefox", "platform": "linux" },
24 | "firefoxdev": { "name": "firefox", "platform": "linux", "version": "dev" },
25 |
26 | "ipad": { "name": "ipad", "platform": "mac10.10", "version": "8" },
27 | "ipad4": { "name": "ipad", "platform": "mac10.6" },
28 | "ipad5": { "name": "ipad", "platform": "mac10.6" },
29 | "ipad5.1": { "name": "ipad", "platform": "mac10.6" },
30 | "ipad6": { "name": "ipad", "platform": "mac10.8" },
31 | "ipad6.1": { "name": "ipad", "platform": "mac10.8" },
32 | "iphone": { "name": "iphone", "platform": "mac10.10", "version": "8" },
33 | "iphone4": { "name": "iphone", "platform": "mac10.6" },
34 | "iphone5": { "name": "iphone", "platform": "mac10.6" },
35 | "iphone5.1": { "name": "iphone", "platform": "mac10.6" },
36 | "iphone6": { "name": "iphone", "platform": "mac10.8" },
37 | "iphone6.1": { "name": "iphone", "platform": "mac10.8" },
38 |
39 | "android": { "name": "android", "platform": "linux", "version": "5" },
40 | "android4.4": { "name": "android", "platform": "linux", "version": "4.4" },
41 | "android4.2": { "name": "android", "platform": "linux", "version": "4.2" },
42 | "android4.1": { "name": "android", "platform": "linux", "version": "4.1" }
43 |
44 | },
45 |
46 | "platforms": {
47 | "winxp": "Windows XP",
48 | "win7": "Windows 7",
49 | "win8": "Windows 8",
50 | "win8.1": "Windows 8.1",
51 | "mac10.6": "OS X 10.6",
52 | "mac10.8": "OS X 10.8",
53 | "mac10.9": "OS X 10.9",
54 | "mac10.10": "OS X 10.10",
55 | "linux": "Linux",
56 | "android": "Linux"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/repl.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | if (!process.stdout.isTTY) {
4 | console.error('Must run in a TTY');
5 | process.exit(1);
6 | }
7 |
8 | if (!process.env.SAUCE_ACCESS_KEY || !process.env.SAUCE_USERNAME) {
9 | console.error('Please configure $SAUCE_ACCESS_KEY and $SAUCE_USERNAME in your shell');
10 | console.error('Sign up at saucelabs.com');
11 | process.exit(1);
12 | }
13 |
14 | var wd = require('wd');
15 | var env = process.env;
16 | var repl = require('repl');
17 | var args = process.argv.slice(2);
18 | var argv = require('minimist')(args);
19 | var sio = require('socket.io');
20 | var ngrok = require('ngrok').connect;
21 | var join = require('path').join;
22 | var http = require('http').Server;
23 | var express = require('express');
24 |
25 | // config
26 | var config = require('./browsers');
27 | var browsers = config.browsers;
28 | var platforms = config.platforms;
29 |
30 | // parse args
31 | if (2 == argv._.length) platform = argv._.pop();
32 | var str = argv._.join('');
33 | var parts = str.match(/([a-z]+) *(\d+(\.\d+)?)?/);
34 | if (!parts) return usage();
35 |
36 | // locate browser
37 | var browser = browsers[str] || browsers[parts[1]];
38 | if (!browser) return usage();
39 | var version = parts[2] || browser.version;
40 | var platform = platforms[platform || browser.platform];
41 |
42 |
43 | // app
44 | var app = express();
45 | var srv = http(app);
46 | app.get('/', function(req, res){
47 | res.send([
48 | '',
49 | '',
50 | ''
51 | ].join('\n'));
52 | });
53 | app.use(express.static(join(__dirname, 'static')));
54 |
55 | var io = sio(srv);
56 | var socket;
57 |
58 | setup();
59 |
60 | function setup(){
61 | console.log('… setting up tunnel');
62 | srv.listen(function(){
63 | ngrok(srv.address().port, function(err, url){
64 | if (err) {
65 | console.error('… error setting up reverse tunnel');
66 | console.error(err.stack);
67 | return;
68 | }
69 |
70 | console.log('… booting up \033[96m'
71 | + browser.name + '\033[39m (' + (version || 'latest')
72 | + ') on ' + platform);
73 | spawn(url);
74 | });
75 | // let `error` throw
76 | });
77 | }
78 |
79 | function spawn(url){
80 | var user = env.SAUCE_USERNAME;
81 | var key = env.SAUCE_ACCESS_KEY;
82 | var vm = wd.remote('ondemand.saucelabs.com', 80, user, key);
83 |
84 | var isAndroid = browser.name == "android";
85 | var isiPhone = /^ip(hone|ad)$/.test(browser.name);
86 |
87 | var opts = {
88 | browserName: browser.name,
89 | platform : platform,
90 | version : version ? version : undefined,
91 | deviceName : isAndroid ? "Android Emulator" : isiPhone ? "iPhone Simulator" : undefined,
92 | 'device-orientation' : isAndroid || isiPhone ? 'portrait' : undefined,
93 | 'record-video' : false,
94 | 'record-screenshots' : false,
95 | };
96 |
97 | vm.init(opts, function(err, sessionid, client){
98 | if (err) throw err;
99 | if (client) console.log('… connected to', client.browserName, client.version);
100 | vm.get(url, function(err){
101 | if (err) throw err;
102 |
103 | // set up a heartbeat to keep session alive
104 | setInterval(function(){
105 | vm.eval('', function(err){
106 | if (err) throw err;
107 | });
108 | }, 30000);
109 |
110 | // socket io `connection` should fire now
111 | });
112 | });
113 |
114 | io.on('connection', function(s){
115 | socket = s;
116 | socket.on('disconnect', function(){
117 | console.log('socket disconnected');
118 | process.exit(1);
119 | });
120 | start();
121 | });
122 | }
123 |
124 | function usage(){
125 | console.error('');
126 | console.error('usage: repl [version] [platform]');
127 | console.error('');
128 | console.error('options:');
129 | console.error(' -h: this message');
130 | console.error(' -k: no remote `console` override');
131 | console.error('');
132 | console.error('examples:');
133 | console.error(' $ repl ie6 # ie 6');
134 | console.error(' $ repl chrome # chrome latest');
135 | console.error('');
136 | console.error('available browsers: ');
137 |
138 | var browsernames = {};
139 | Object.keys(browsers).map(function(k){ return browsers[k] }).forEach(function(k){ browsernames[k.name] = true; });
140 |
141 | Object.keys(browsernames).forEach(function(name){
142 | console.error(
143 | ' ' + name + ': ',
144 | Object.keys(browsers).filter(function(val){ return browsers[val].name == name }).join(' ')
145 | );
146 | });
147 |
148 | console.error('\navailable platforms: \n ' + Object.keys(platforms).join(' '));
149 | console.error('');
150 | process.exit(1);
151 | }
152 |
153 | function start(){
154 | console.log('… ready!');
155 | var isAnsiReadlineOK = 'stripVTControlCharacters' in require('readline');
156 |
157 | var cmd = repl.start({
158 | prompt: isAnsiReadlineOK ? '\u001b[96m' + str + ' › \u001b[39m' : str + ' › ',
159 | eval: function(cmd, ctx, file, fn){
160 | socket.emit('run', cmd, function(err, data){
161 | if (err) {
162 | // we have to create a synthetic SyntaxError if one occurred in the
163 | // browser because the REPL special-cases that error
164 | // to display the "more" prompt
165 | if (
166 | // most browsers set the `name` to "SyntaxError"
167 | ('SyntaxError' == err.name &&
168 | // firefox
169 | ('syntax error' == err.message ||
170 | 'function statement requires a name' == err.message ||
171 | // iOS
172 | 'Parse error' == err.message ||
173 | // opera
174 | /syntax error$/.test(err.message) ||
175 | /expected (.*), got (.*)$/.test(err.message) ||
176 | // safari
177 | /^Unexpected token (.*)$/.test(err.message)
178 | )
179 | ) ||
180 | // old IE doens't even have a "name" property :\
181 | ('Syntax error' == err.message || /^expected /i.test(err.message))
182 | ) {
183 | err = new SyntaxError('Unexpected end of input');
184 | } else {
185 | // any other `err` needs to be converted to an `Error` object
186 | // with the given `err`s properties copied over
187 | var e = new Error();
188 |
189 | // force an empty stack trace on the server-side... in the case where
190 | // the client-side didn't send us a `stack` property (old IE, safari),
191 | // it's confusing to see a server-side stack trace.
192 | e.stack = '';
193 |
194 | for (var i in err) {
195 | e[i] = err[i];
196 | }
197 |
198 | // firefox and opera, in particular, doesn't include the "name"
199 | // or "message" in the stack trace
200 | var prefix = e.name;
201 | if (e.message) prefix += ': ' + e.message;
202 | if (e.stack.substring(0, prefix.length) != prefix) {
203 | e.stack = prefix + '\n' + e.stack;
204 | }
205 |
206 | err = e;
207 | }
208 | }
209 | // We're intentionally passing the successful "data" response as the
210 | // `err` argument to the eval function. This is because the `data` is
211 | // actually a properly formatted String output from `util.inspect()` run
212 | // on the client-side, with proper coloring, etc. coincidentally, if we
213 | // pass that as the `err` argument then node's `repl` module will simply
214 | // console.log() the formatted string for us, which is what we want
215 | fn(err || data);
216 | });
217 | }
218 | });
219 |
220 | socket.on('global err', function(message, url, linenumber){
221 | console.log('Global error: ', message, url, linenumber);
222 | });
223 |
224 | socket.on('console', function(method, args){
225 | console[method].apply(console, args);
226 | });
227 |
228 | cmd.on('exit', function(){
229 | process.exit(0);
230 | });
231 | }
232 |
--------------------------------------------------------------------------------