rack-livereload 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/README.md +47 -0
- data/Rakefile +11 -0
- data/config.ru +13 -0
- data/js/livereload.js +804 -0
- data/lib/rack-livereload.rb +6 -0
- data/lib/rack/livereload.rb +55 -0
- data/rack-livereload.gemspec +34 -0
- data/spec/rack/livereload_spec.rb +74 -0
- data/spec/spec_helper.rb +7 -0
- metadata +169 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
Hey, you've got [LiveReload](http://www.livereload.com/) in my [Rack](http://rack.rubyforge.org/)!
|
2
|
+
No need for browser extensions anymore! Just plug it in your middleware stack and go!
|
3
|
+
|
4
|
+
Use this with [guard-livereload](http://github.com/guard/guard-livereload) for maximum fun!
|
5
|
+
|
6
|
+
## Using in...
|
7
|
+
|
8
|
+
### Rails
|
9
|
+
|
10
|
+
In `config/environments/development.rb`:
|
11
|
+
|
12
|
+
``` ruby
|
13
|
+
MyApp::Application.configure do
|
14
|
+
config.middleware.insert_before(Rack::Lock, Rack::LiveReload)
|
15
|
+
|
16
|
+
# ...or, change some options...
|
17
|
+
|
18
|
+
config.middleware.insert_before(
|
19
|
+
Rack::Lock, Rack::LiveReload,
|
20
|
+
:min_delay => 500,
|
21
|
+
:max_delay => 10000,
|
22
|
+
:port => 56789,
|
23
|
+
:host => 'myhost.cool.wow'
|
24
|
+
)
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
### config.ru/Sinatra
|
29
|
+
|
30
|
+
``` ruby
|
31
|
+
require 'rack-livereload'
|
32
|
+
|
33
|
+
use Rack::LiveReload
|
34
|
+
# ...or...
|
35
|
+
use Rack::LiveReload, :min_delay => 500, ...
|
36
|
+
```
|
37
|
+
|
38
|
+
## How it works
|
39
|
+
|
40
|
+
The necessary `script` tag to bring in a vendored copy of [livereload.js](https://github.com/livereload/livereload-js) is
|
41
|
+
injected right before the closing `head` tag in any `text/html` pages that come through. The `script` tag is built in
|
42
|
+
such a way that the `HTTP_HOST` is used as the LiveReload host, so you can connect from external machines (say, to
|
43
|
+
`mycomputer:3000` instead of `localhost:3000`) and as long as the LiveReload port is accessible from the external machine,
|
44
|
+
you'll connect and be LiveReloading away!
|
45
|
+
|
46
|
+
As usual, super-alpha!
|
47
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
desc 'Update livereload.js'
|
4
|
+
task :update_livereload_js do
|
5
|
+
require 'httparty'
|
6
|
+
|
7
|
+
File.open('js/livereload.js', 'wb') { |fh|
|
8
|
+
fh.print HTTParty.get('https://raw.github.com/livereload/livereload-js/master/dist/livereload.js').body
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
data/config.ru
ADDED
data/js/livereload.js
ADDED
@@ -0,0 +1,804 @@
|
|
1
|
+
(function() {
|
2
|
+
var __customevents = {}, __protocol = {}, __connector = {}, __timer = {}, __options = {}, __reloader = {}, __livereload = {}, __startup = {};
|
3
|
+
|
4
|
+
// customevents
|
5
|
+
(function() {
|
6
|
+
var CustomEvents;
|
7
|
+
CustomEvents = {
|
8
|
+
bind: function(element, eventName, handler) {
|
9
|
+
if (element.addEventListener) {
|
10
|
+
return element.addEventListener(eventName, handler, false);
|
11
|
+
} else if (element.attachEvent) {
|
12
|
+
element[eventName] = 1;
|
13
|
+
return element.attachEvent('onpropertychange', function(event) {
|
14
|
+
if (event.propertyName === eventName) {
|
15
|
+
return handler();
|
16
|
+
}
|
17
|
+
});
|
18
|
+
} else {
|
19
|
+
throw new Error("Attempt to attach custom event " + eventName + " to something which isn't a DOMElement");
|
20
|
+
}
|
21
|
+
},
|
22
|
+
fire: function(element, eventName) {
|
23
|
+
var event;
|
24
|
+
if (element.addEventListener) {
|
25
|
+
event = document.createEvent('HTMLEvents');
|
26
|
+
event.initEvent(eventName, true, true);
|
27
|
+
return document.dispatchEvent(event);
|
28
|
+
} else if (element.attachEvent) {
|
29
|
+
if (element[eventName]) {
|
30
|
+
return element[eventName]++;
|
31
|
+
}
|
32
|
+
} else {
|
33
|
+
throw new Error("Attempt to fire custom event " + eventName + " on something which isn't a DOMElement");
|
34
|
+
}
|
35
|
+
}
|
36
|
+
};
|
37
|
+
__customevents.bind = CustomEvents.bind;
|
38
|
+
__customevents.fire = CustomEvents.fire;
|
39
|
+
}).call(this);
|
40
|
+
|
41
|
+
// protocol
|
42
|
+
(function() {
|
43
|
+
var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError;
|
44
|
+
var __indexOf = Array.prototype.indexOf || function(item) {
|
45
|
+
for (var i = 0, l = this.length; i < l; i++) {
|
46
|
+
if (this[i] === item) return i;
|
47
|
+
}
|
48
|
+
return -1;
|
49
|
+
};
|
50
|
+
__protocol.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6';
|
51
|
+
__protocol.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7';
|
52
|
+
__protocol.ProtocolError = ProtocolError = (function() {
|
53
|
+
function ProtocolError(reason, data) {
|
54
|
+
this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\".";
|
55
|
+
}
|
56
|
+
return ProtocolError;
|
57
|
+
})();
|
58
|
+
__protocol.Parser = Parser = (function() {
|
59
|
+
function Parser(handlers) {
|
60
|
+
this.handlers = handlers;
|
61
|
+
this.reset();
|
62
|
+
}
|
63
|
+
Parser.prototype.reset = function() {
|
64
|
+
return this.protocol = null;
|
65
|
+
};
|
66
|
+
Parser.prototype.process = function(data) {
|
67
|
+
var command, message, options, _ref;
|
68
|
+
try {
|
69
|
+
if (!(this.protocol != null)) {
|
70
|
+
if (data.match(/^!!ver:([\d.]+)$/)) {
|
71
|
+
this.protocol = 6;
|
72
|
+
} else if (message = this._parseMessage(data, ['hello'])) {
|
73
|
+
if (!message.protocols.length) {
|
74
|
+
throw new ProtocolError("no protocols specified in handshake message");
|
75
|
+
} else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) {
|
76
|
+
this.protocol = 7;
|
77
|
+
} else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) {
|
78
|
+
this.protocol = 6;
|
79
|
+
} else {
|
80
|
+
throw new ProtocolError("no supported protocols found");
|
81
|
+
}
|
82
|
+
}
|
83
|
+
return this.handlers.connected(this.protocol);
|
84
|
+
} else if (this.protocol === 6) {
|
85
|
+
message = JSON.parse(data);
|
86
|
+
if (!message.length) {
|
87
|
+
throw new ProtocolError("protocol 6 messages must be arrays");
|
88
|
+
}
|
89
|
+
command = message[0], options = message[1];
|
90
|
+
if (command !== 'refresh') {
|
91
|
+
throw new ProtocolError("unknown protocol 6 command");
|
92
|
+
}
|
93
|
+
return this.handlers.message({
|
94
|
+
command: 'reload',
|
95
|
+
path: options.path,
|
96
|
+
liveCSS: (_ref = options.apply_css_live) != null ? _ref : true
|
97
|
+
});
|
98
|
+
} else {
|
99
|
+
message = this._parseMessage(data, ['reload', 'alert']);
|
100
|
+
return this.handlers.message(message);
|
101
|
+
}
|
102
|
+
} catch (e) {
|
103
|
+
if (e instanceof ProtocolError) {
|
104
|
+
return this.handlers.error(e);
|
105
|
+
} else {
|
106
|
+
throw e;
|
107
|
+
}
|
108
|
+
}
|
109
|
+
};
|
110
|
+
Parser.prototype._parseMessage = function(data, validCommands) {
|
111
|
+
var message, _ref;
|
112
|
+
try {
|
113
|
+
message = JSON.parse(data);
|
114
|
+
} catch (e) {
|
115
|
+
throw new ProtocolError('unparsable JSON', data);
|
116
|
+
}
|
117
|
+
if (!message.command) {
|
118
|
+
throw new ProtocolError('missing "command" key', data);
|
119
|
+
}
|
120
|
+
if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) {
|
121
|
+
throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data);
|
122
|
+
}
|
123
|
+
return message;
|
124
|
+
};
|
125
|
+
return Parser;
|
126
|
+
})();
|
127
|
+
}).call(this);
|
128
|
+
|
129
|
+
// connector
|
130
|
+
(function() {
|
131
|
+
var Connector, PROTOCOL_6, PROTOCOL_7, Parser, _ref;
|
132
|
+
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
133
|
+
_ref = __protocol, Parser = _ref.Parser, PROTOCOL_6 = _ref.PROTOCOL_6, PROTOCOL_7 = _ref.PROTOCOL_7;
|
134
|
+
__connector.Connector = Connector = (function() {
|
135
|
+
function Connector(options, WebSocket, Timer, handlers) {
|
136
|
+
this.options = options;
|
137
|
+
this.WebSocket = WebSocket;
|
138
|
+
this.Timer = Timer;
|
139
|
+
this.handlers = handlers;
|
140
|
+
this._uri = "ws://" + this.options.host + ":" + this.options.port + "/livereload";
|
141
|
+
this._nextDelay = this.options.mindelay;
|
142
|
+
this._connectionDesired = false;
|
143
|
+
this.protocolParser = new Parser({
|
144
|
+
connected: __bind(function(protocol) {
|
145
|
+
this._handshakeTimeout.stop();
|
146
|
+
this._nextDelay = this.options.mindelay;
|
147
|
+
this._disconnectionReason = 'broken';
|
148
|
+
return this.handlers.connected(protocol);
|
149
|
+
}, this),
|
150
|
+
error: __bind(function(e) {
|
151
|
+
this.handlers.error(e);
|
152
|
+
return this._closeOnError();
|
153
|
+
}, this),
|
154
|
+
message: __bind(function(message) {
|
155
|
+
return this.handlers.message(message);
|
156
|
+
}, this)
|
157
|
+
});
|
158
|
+
this._handshakeTimeout = new Timer(__bind(function() {
|
159
|
+
if (!this._isSocketConnected()) {
|
160
|
+
return;
|
161
|
+
}
|
162
|
+
this._disconnectionReason = 'handshake-timeout';
|
163
|
+
return this.socket.close();
|
164
|
+
}, this));
|
165
|
+
this._reconnectTimer = new Timer(__bind(function() {
|
166
|
+
if (!this._connectionDesired) {
|
167
|
+
return;
|
168
|
+
}
|
169
|
+
return this.connect();
|
170
|
+
}, this));
|
171
|
+
this.connect();
|
172
|
+
}
|
173
|
+
Connector.prototype._isSocketConnected = function() {
|
174
|
+
return this.socket && this.socket.readyState === this.WebSocket.OPEN;
|
175
|
+
};
|
176
|
+
Connector.prototype.connect = function() {
|
177
|
+
this._connectionDesired = true;
|
178
|
+
if (this._isSocketConnected()) {
|
179
|
+
return;
|
180
|
+
}
|
181
|
+
if (this._reconnectTimer) {
|
182
|
+
clearTimeout(this._reconnectTimer);
|
183
|
+
}
|
184
|
+
this._disconnectionReason = 'cannot-connect';
|
185
|
+
this.protocolParser.reset();
|
186
|
+
this.handlers.connecting();
|
187
|
+
this.socket = new this.WebSocket(this._uri);
|
188
|
+
this.socket.onopen = __bind(function(e) {
|
189
|
+
return this._onopen(e);
|
190
|
+
}, this);
|
191
|
+
this.socket.onclose = __bind(function(e) {
|
192
|
+
return this._onclose(e);
|
193
|
+
}, this);
|
194
|
+
this.socket.onmessage = __bind(function(e) {
|
195
|
+
return this._onmessage(e);
|
196
|
+
}, this);
|
197
|
+
return this.socket.onerror = __bind(function(e) {
|
198
|
+
return this._onerror(e);
|
199
|
+
}, this);
|
200
|
+
};
|
201
|
+
Connector.prototype.disconnect = function() {
|
202
|
+
this._connectionDesired = false;
|
203
|
+
this._reconnectTimer.stop();
|
204
|
+
if (!this._isSocketConnected()) {
|
205
|
+
return;
|
206
|
+
}
|
207
|
+
this._disconnectionReason = 'manual';
|
208
|
+
return this.socket.close();
|
209
|
+
};
|
210
|
+
Connector.prototype._scheduleReconnection = function() {
|
211
|
+
if (!this._connectionDesired) {
|
212
|
+
return;
|
213
|
+
}
|
214
|
+
if (!this._reconnectTimer.running) {
|
215
|
+
this._reconnectTimer.start(this._nextDelay);
|
216
|
+
return this._nextDelay = Math.min(this.options.maxdelay, this._nextDelay * 2);
|
217
|
+
}
|
218
|
+
};
|
219
|
+
Connector.prototype.sendCommand = function(command) {
|
220
|
+
if (this.protocol == null) {
|
221
|
+
return;
|
222
|
+
}
|
223
|
+
return this._sendCommand(command);
|
224
|
+
};
|
225
|
+
Connector.prototype._sendCommand = function(command) {
|
226
|
+
return this.socket.send(JSON.stringify(command));
|
227
|
+
};
|
228
|
+
Connector.prototype._closeOnError = function() {
|
229
|
+
this._handshakeTimeout.stop();
|
230
|
+
this._disconnectionReason = 'error';
|
231
|
+
return this.socket.close();
|
232
|
+
};
|
233
|
+
Connector.prototype._onopen = function(e) {
|
234
|
+
this.handlers.socketConnected();
|
235
|
+
this._disconnectionReason = 'handshake-failed';
|
236
|
+
this._sendCommand({
|
237
|
+
command: 'hello',
|
238
|
+
protocols: [PROTOCOL_6, PROTOCOL_7]
|
239
|
+
});
|
240
|
+
return this._handshakeTimeout.start(this.options.handshake_timeout);
|
241
|
+
};
|
242
|
+
Connector.prototype._onclose = function(e) {
|
243
|
+
this.handlers.disconnected(this._disconnectionReason, this._nextDelay);
|
244
|
+
return this._scheduleReconnection();
|
245
|
+
};
|
246
|
+
Connector.prototype._onerror = function(e) {};
|
247
|
+
Connector.prototype._onmessage = function(e) {
|
248
|
+
return this.protocolParser.process(e.data);
|
249
|
+
};
|
250
|
+
return Connector;
|
251
|
+
})();
|
252
|
+
}).call(this);
|
253
|
+
|
254
|
+
// timer
|
255
|
+
(function() {
|
256
|
+
var Timer;
|
257
|
+
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
258
|
+
__timer.Timer = Timer = (function() {
|
259
|
+
function Timer(func) {
|
260
|
+
this.func = func;
|
261
|
+
this.running = false;
|
262
|
+
this.id = null;
|
263
|
+
this._handler = __bind(function() {
|
264
|
+
this.running = false;
|
265
|
+
this.id = null;
|
266
|
+
return this.func();
|
267
|
+
}, this);
|
268
|
+
}
|
269
|
+
Timer.prototype.start = function(timeout) {
|
270
|
+
if (this.running) {
|
271
|
+
clearTimeout(this.id);
|
272
|
+
}
|
273
|
+
this.id = setTimeout(this._handler, timeout);
|
274
|
+
return this.running = true;
|
275
|
+
};
|
276
|
+
Timer.prototype.stop = function() {
|
277
|
+
if (this.running) {
|
278
|
+
clearTimeout(this.id);
|
279
|
+
this.running = false;
|
280
|
+
return this.id = null;
|
281
|
+
}
|
282
|
+
};
|
283
|
+
return Timer;
|
284
|
+
})();
|
285
|
+
Timer.start = function(timeout, func) {
|
286
|
+
return setTimeout(func, timeout);
|
287
|
+
};
|
288
|
+
}).call(this);
|
289
|
+
|
290
|
+
// options
|
291
|
+
(function() {
|
292
|
+
var Options;
|
293
|
+
__options.Options = Options = (function() {
|
294
|
+
function Options() {
|
295
|
+
this.host = null;
|
296
|
+
this.port = 35729;
|
297
|
+
this.snipver = null;
|
298
|
+
this.ext = null;
|
299
|
+
this.extver = null;
|
300
|
+
this.mindelay = 1000;
|
301
|
+
this.maxdelay = 60000;
|
302
|
+
this.handshake_timeout = 5000;
|
303
|
+
}
|
304
|
+
Options.prototype.set = function(name, value) {
|
305
|
+
switch (typeof this[name]) {
|
306
|
+
case 'undefined':
|
307
|
+
break;
|
308
|
+
case 'number':
|
309
|
+
return this[name] = +value;
|
310
|
+
default:
|
311
|
+
return this[name] = value;
|
312
|
+
}
|
313
|
+
};
|
314
|
+
return Options;
|
315
|
+
})();
|
316
|
+
Options.extract = function(document) {
|
317
|
+
var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len2, _ref, _ref2;
|
318
|
+
_ref = document.getElementsByTagName('script');
|
319
|
+
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
320
|
+
element = _ref[_i];
|
321
|
+
if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) {
|
322
|
+
options = new Options();
|
323
|
+
if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) {
|
324
|
+
options.host = mm[1];
|
325
|
+
if (mm[2]) {
|
326
|
+
options.port = parseInt(mm[2], 10);
|
327
|
+
}
|
328
|
+
}
|
329
|
+
if (m[2]) {
|
330
|
+
_ref2 = m[2].split('&');
|
331
|
+
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
|
332
|
+
pair = _ref2[_j];
|
333
|
+
if ((keyAndValue = pair.split('=')).length > 1) {
|
334
|
+
options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('='));
|
335
|
+
}
|
336
|
+
}
|
337
|
+
}
|
338
|
+
return options;
|
339
|
+
}
|
340
|
+
}
|
341
|
+
return null;
|
342
|
+
};
|
343
|
+
}).call(this);
|
344
|
+
|
345
|
+
// reloader
|
346
|
+
(function() {
|
347
|
+
var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl;
|
348
|
+
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
349
|
+
splitUrl = function(url) {
|
350
|
+
var hash, index, params;
|
351
|
+
if ((index = url.indexOf('#')) >= 0) {
|
352
|
+
hash = url.slice(index);
|
353
|
+
url = url.slice(0, index);
|
354
|
+
} else {
|
355
|
+
hash = '';
|
356
|
+
}
|
357
|
+
if ((index = url.indexOf('?')) >= 0) {
|
358
|
+
params = url.slice(index);
|
359
|
+
url = url.slice(0, index);
|
360
|
+
} else {
|
361
|
+
params = '';
|
362
|
+
}
|
363
|
+
return {
|
364
|
+
url: url,
|
365
|
+
params: params,
|
366
|
+
hash: hash
|
367
|
+
};
|
368
|
+
};
|
369
|
+
pathFromUrl = function(url) {
|
370
|
+
var path;
|
371
|
+
url = splitUrl(url).url;
|
372
|
+
if (url.indexOf('file://') === 0) {
|
373
|
+
path = url.replace(/^file:\/\/(localhost)?/, '');
|
374
|
+
} else {
|
375
|
+
path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/');
|
376
|
+
}
|
377
|
+
return decodeURIComponent(path);
|
378
|
+
};
|
379
|
+
pickBestMatch = function(path, objects, pathFunc) {
|
380
|
+
var bestMatch, object, score, _i, _len;
|
381
|
+
bestMatch = {
|
382
|
+
score: 0
|
383
|
+
};
|
384
|
+
for (_i = 0, _len = objects.length; _i < _len; _i++) {
|
385
|
+
object = objects[_i];
|
386
|
+
score = numberOfMatchingSegments(path, pathFunc(object));
|
387
|
+
if (score > bestMatch.score) {
|
388
|
+
bestMatch = {
|
389
|
+
object: object,
|
390
|
+
score: score
|
391
|
+
};
|
392
|
+
}
|
393
|
+
}
|
394
|
+
if (bestMatch.score > 0) {
|
395
|
+
return bestMatch;
|
396
|
+
} else {
|
397
|
+
return null;
|
398
|
+
}
|
399
|
+
};
|
400
|
+
numberOfMatchingSegments = function(path1, path2) {
|
401
|
+
var comps1, comps2, eqCount, len;
|
402
|
+
path1 = path1.replace(/^\/+/, '').toLowerCase();
|
403
|
+
path2 = path2.replace(/^\/+/, '').toLowerCase();
|
404
|
+
if (path1 === path2) {
|
405
|
+
return 10000;
|
406
|
+
}
|
407
|
+
comps1 = path1.split('/').reverse();
|
408
|
+
comps2 = path2.split('/').reverse();
|
409
|
+
len = Math.min(comps1.length, comps2.length);
|
410
|
+
eqCount = 0;
|
411
|
+
while (eqCount < len && comps1[eqCount] === comps2[eqCount]) {
|
412
|
+
++eqCount;
|
413
|
+
}
|
414
|
+
return eqCount;
|
415
|
+
};
|
416
|
+
pathsMatch = function(path1, path2) {
|
417
|
+
return numberOfMatchingSegments(path1, path2) > 0;
|
418
|
+
};
|
419
|
+
IMAGE_STYLES = [
|
420
|
+
{
|
421
|
+
selector: 'background',
|
422
|
+
styleNames: ['backgroundImage']
|
423
|
+
}, {
|
424
|
+
selector: 'border',
|
425
|
+
styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage']
|
426
|
+
}
|
427
|
+
];
|
428
|
+
__reloader.Reloader = Reloader = (function() {
|
429
|
+
function Reloader(window, console, Timer) {
|
430
|
+
this.window = window;
|
431
|
+
this.console = console;
|
432
|
+
this.Timer = Timer;
|
433
|
+
this.document = this.window.document;
|
434
|
+
this.stylesheetGracePeriod = 200;
|
435
|
+
this.importCacheWaitPeriod = 200;
|
436
|
+
}
|
437
|
+
Reloader.prototype.reload = function(path, options) {
|
438
|
+
if (options.liveCSS) {
|
439
|
+
if (path.match(/\.css$/i)) {
|
440
|
+
if (this.reloadStylesheet(path)) {
|
441
|
+
return;
|
442
|
+
}
|
443
|
+
}
|
444
|
+
if (path.match(/\.less$/i) && this.window.less && this.window.less.refresh) {
|
445
|
+
this.window.less.refresh(true);
|
446
|
+
return;
|
447
|
+
}
|
448
|
+
}
|
449
|
+
if (options.liveImg) {
|
450
|
+
if (path.match(/\.(jpe?g|png|gif)$/i)) {
|
451
|
+
this.reloadImages(path);
|
452
|
+
return;
|
453
|
+
}
|
454
|
+
}
|
455
|
+
return this.reloadPage();
|
456
|
+
};
|
457
|
+
Reloader.prototype.reloadPage = function() {
|
458
|
+
return this.window.document.location.reload();
|
459
|
+
};
|
460
|
+
Reloader.prototype.reloadImages = function(path) {
|
461
|
+
var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len2, _len3, _len4, _ref, _ref2, _ref3, _ref4, _results;
|
462
|
+
expando = this.generateUniqueString();
|
463
|
+
_ref = this.document.images;
|
464
|
+
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
465
|
+
img = _ref[_i];
|
466
|
+
if (pathsMatch(path, pathFromUrl(img.src))) {
|
467
|
+
img.src = this.generateCacheBustUrl(img.src, expando);
|
468
|
+
}
|
469
|
+
}
|
470
|
+
if (this.document.querySelectorAll) {
|
471
|
+
for (_j = 0, _len2 = IMAGE_STYLES.length; _j < _len2; _j++) {
|
472
|
+
_ref2 = IMAGE_STYLES[_j], selector = _ref2.selector, styleNames = _ref2.styleNames;
|
473
|
+
_ref3 = this.document.querySelectorAll("[style*=" + selector + "]");
|
474
|
+
for (_k = 0, _len3 = _ref3.length; _k < _len3; _k++) {
|
475
|
+
img = _ref3[_k];
|
476
|
+
this.reloadStyleImages(img.style, styleNames, path, expando);
|
477
|
+
}
|
478
|
+
}
|
479
|
+
}
|
480
|
+
if (this.document.styleSheets) {
|
481
|
+
_ref4 = this.document.styleSheets;
|
482
|
+
_results = [];
|
483
|
+
for (_l = 0, _len4 = _ref4.length; _l < _len4; _l++) {
|
484
|
+
styleSheet = _ref4[_l];
|
485
|
+
_results.push(this.reloadStylesheetImages(styleSheet, path, expando));
|
486
|
+
}
|
487
|
+
return _results;
|
488
|
+
}
|
489
|
+
};
|
490
|
+
Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) {
|
491
|
+
var rule, rules, styleNames, _i, _j, _len, _len2;
|
492
|
+
try {
|
493
|
+
rules = styleSheet != null ? styleSheet.cssRules : void 0;
|
494
|
+
} catch (e) {
|
495
|
+
|
496
|
+
}
|
497
|
+
if (!rules) {
|
498
|
+
return;
|
499
|
+
}
|
500
|
+
for (_i = 0, _len = rules.length; _i < _len; _i++) {
|
501
|
+
rule = rules[_i];
|
502
|
+
switch (rule.type) {
|
503
|
+
case CSSRule.IMPORT_RULE:
|
504
|
+
this.reloadStylesheetImages(rule.styleSheet, path, expando);
|
505
|
+
break;
|
506
|
+
case CSSRule.STYLE_RULE:
|
507
|
+
for (_j = 0, _len2 = IMAGE_STYLES.length; _j < _len2; _j++) {
|
508
|
+
styleNames = IMAGE_STYLES[_j].styleNames;
|
509
|
+
this.reloadStyleImages(rule.style, styleNames, path, expando);
|
510
|
+
}
|
511
|
+
break;
|
512
|
+
case CSSRule.MEDIA_RULE:
|
513
|
+
this.reloadStylesheetImages(rule, path, expando);
|
514
|
+
}
|
515
|
+
}
|
516
|
+
};
|
517
|
+
Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) {
|
518
|
+
var newValue, styleName, value, _i, _len;
|
519
|
+
for (_i = 0, _len = styleNames.length; _i < _len; _i++) {
|
520
|
+
styleName = styleNames[_i];
|
521
|
+
value = style[styleName];
|
522
|
+
if (typeof value === 'string') {
|
523
|
+
newValue = value.replace(/\burl\s*\(([^)]*)\)/, __bind(function(match, src) {
|
524
|
+
if (pathsMatch(path, pathFromUrl(src))) {
|
525
|
+
return "url(" + (this.generateCacheBustUrl(src, expando)) + ")";
|
526
|
+
} else {
|
527
|
+
return match;
|
528
|
+
}
|
529
|
+
}, this));
|
530
|
+
if (newValue !== value) {
|
531
|
+
style[styleName] = newValue;
|
532
|
+
}
|
533
|
+
}
|
534
|
+
}
|
535
|
+
};
|
536
|
+
Reloader.prototype.reloadStylesheet = function(path) {
|
537
|
+
var imported, link, links, match, _i, _j, _len, _len2;
|
538
|
+
links = (function() {
|
539
|
+
var _i, _len, _ref, _results;
|
540
|
+
_ref = this.document.getElementsByTagName('link');
|
541
|
+
_results = [];
|
542
|
+
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
543
|
+
link = _ref[_i];
|
544
|
+
if (link.rel === 'stylesheet' && !link.__LiveReload_pendingRemoval) {
|
545
|
+
_results.push(link);
|
546
|
+
}
|
547
|
+
}
|
548
|
+
return _results;
|
549
|
+
}).call(this);
|
550
|
+
imported = [];
|
551
|
+
for (_i = 0, _len = links.length; _i < _len; _i++) {
|
552
|
+
link = links[_i];
|
553
|
+
this.collectImportedStylesheets(link, link.sheet, imported);
|
554
|
+
}
|
555
|
+
this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets");
|
556
|
+
match = pickBestMatch(path, links.concat(imported), function(l) {
|
557
|
+
return pathFromUrl(l.href);
|
558
|
+
});
|
559
|
+
if (match) {
|
560
|
+
if (match.object.rule) {
|
561
|
+
this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href);
|
562
|
+
this.reattachImportedRule(match.object);
|
563
|
+
} else {
|
564
|
+
this.console.log("LiveReload is reloading stylesheet: " + match.object.href);
|
565
|
+
this.reattachStylesheetLink(match.object);
|
566
|
+
}
|
567
|
+
} else {
|
568
|
+
this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one");
|
569
|
+
for (_j = 0, _len2 = links.length; _j < _len2; _j++) {
|
570
|
+
link = links[_j];
|
571
|
+
this.reattachStylesheetLink(link);
|
572
|
+
}
|
573
|
+
}
|
574
|
+
return true;
|
575
|
+
};
|
576
|
+
Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) {
|
577
|
+
var index, rule, rules, _len;
|
578
|
+
try {
|
579
|
+
rules = styleSheet != null ? styleSheet.cssRules : void 0;
|
580
|
+
} catch (e) {
|
581
|
+
|
582
|
+
}
|
583
|
+
if (rules && rules.length) {
|
584
|
+
for (index = 0, _len = rules.length; index < _len; index++) {
|
585
|
+
rule = rules[index];
|
586
|
+
switch (rule.type) {
|
587
|
+
case CSSRule.CHARSET_RULE:
|
588
|
+
continue;
|
589
|
+
case CSSRule.IMPORT_RULE:
|
590
|
+
result.push({
|
591
|
+
link: link,
|
592
|
+
rule: rule,
|
593
|
+
index: index,
|
594
|
+
href: rule.href
|
595
|
+
});
|
596
|
+
this.collectImportedStylesheets(link, rule.styleSheet, result);
|
597
|
+
break;
|
598
|
+
default:
|
599
|
+
break;
|
600
|
+
}
|
601
|
+
}
|
602
|
+
}
|
603
|
+
};
|
604
|
+
Reloader.prototype.reattachStylesheetLink = function(link) {
|
605
|
+
var clone, parent, timer;
|
606
|
+
if (link.__LiveReload_pendingRemoval) {
|
607
|
+
return;
|
608
|
+
}
|
609
|
+
link.__LiveReload_pendingRemoval = true;
|
610
|
+
clone = link.cloneNode(false);
|
611
|
+
clone.href = this.generateCacheBustUrl(link.href);
|
612
|
+
parent = link.parentNode;
|
613
|
+
if (parent.lastChild === link) {
|
614
|
+
parent.appendChild(clone);
|
615
|
+
} else {
|
616
|
+
parent.insertBefore(clone, link.nextSibling);
|
617
|
+
}
|
618
|
+
timer = new this.Timer(function() {
|
619
|
+
if (link.parentNode) {
|
620
|
+
return link.parentNode.removeChild(link);
|
621
|
+
}
|
622
|
+
});
|
623
|
+
return timer.start(this.stylesheetGracePeriod);
|
624
|
+
};
|
625
|
+
Reloader.prototype.reattachImportedRule = function(_arg) {
|
626
|
+
var href, index, link, media, newRule, parent, rule, tempLink;
|
627
|
+
rule = _arg.rule, index = _arg.index, link = _arg.link;
|
628
|
+
parent = rule.parentStyleSheet;
|
629
|
+
href = this.generateCacheBustUrl(rule.href);
|
630
|
+
media = rule.media.length ? [].join.call(rule.media, ', ') : '';
|
631
|
+
newRule = "@import url(\"" + href + "\") " + media + ";";
|
632
|
+
rule.__LiveReload_newHref = href;
|
633
|
+
tempLink = this.document.createElement("link");
|
634
|
+
tempLink.rel = 'stylesheet';
|
635
|
+
tempLink.href = href;
|
636
|
+
tempLink.__LiveReload_pendingRemoval = true;
|
637
|
+
if (link.parentNode) {
|
638
|
+
link.parentNode.insertBefore(tempLink, link);
|
639
|
+
}
|
640
|
+
return this.Timer.start(this.importCacheWaitPeriod, __bind(function() {
|
641
|
+
if (tempLink.parentNode) {
|
642
|
+
tempLink.parentNode.removeChild(tempLink);
|
643
|
+
}
|
644
|
+
if (rule.__LiveReload_newHref !== href) {
|
645
|
+
return;
|
646
|
+
}
|
647
|
+
parent.insertRule(newRule, index);
|
648
|
+
parent.deleteRule(index + 1);
|
649
|
+
rule = parent.cssRules[index];
|
650
|
+
rule.__LiveReload_newHref = href;
|
651
|
+
return this.Timer.start(this.importCacheWaitPeriod, __bind(function() {
|
652
|
+
if (rule.__LiveReload_newHref !== href) {
|
653
|
+
return;
|
654
|
+
}
|
655
|
+
parent.insertRule(newRule, index);
|
656
|
+
return parent.deleteRule(index + 1);
|
657
|
+
}, this));
|
658
|
+
}, this));
|
659
|
+
};
|
660
|
+
Reloader.prototype.generateUniqueString = function() {
|
661
|
+
return 'livereload=' + Date.now();
|
662
|
+
};
|
663
|
+
Reloader.prototype.generateCacheBustUrl = function(url, expando) {
|
664
|
+
var hash, oldParams, params, _ref;
|
665
|
+
if (expando == null) {
|
666
|
+
expando = this.generateUniqueString();
|
667
|
+
}
|
668
|
+
_ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params;
|
669
|
+
params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) {
|
670
|
+
return "" + sep + expando;
|
671
|
+
});
|
672
|
+
if (params === oldParams) {
|
673
|
+
if (oldParams.length === 0) {
|
674
|
+
params = "?" + expando;
|
675
|
+
} else {
|
676
|
+
params = "" + oldParams + "&" + expando;
|
677
|
+
}
|
678
|
+
}
|
679
|
+
return url + params + hash;
|
680
|
+
};
|
681
|
+
return Reloader;
|
682
|
+
})();
|
683
|
+
}).call(this);
|
684
|
+
|
685
|
+
// livereload
|
686
|
+
(function() {
|
687
|
+
var Connector, LiveReload, Options, Reloader, Timer;
|
688
|
+
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
689
|
+
Connector = __connector.Connector;
|
690
|
+
Timer = __timer.Timer;
|
691
|
+
Options = __options.Options;
|
692
|
+
Reloader = __reloader.Reloader;
|
693
|
+
__livereload.LiveReload = LiveReload = (function() {
|
694
|
+
function LiveReload(window) {
|
695
|
+
this.window = window;
|
696
|
+
this.listeners = {};
|
697
|
+
this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.console : {
|
698
|
+
log: function() {},
|
699
|
+
error: function() {}
|
700
|
+
};
|
701
|
+
if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) {
|
702
|
+
console.error("LiveReload disabled because the browser does not seem to support web sockets");
|
703
|
+
return;
|
704
|
+
}
|
705
|
+
if (!(this.options = Options.extract(this.window.document))) {
|
706
|
+
console.error("LiveReload disabled because it could not find its own <SCRIPT> tag");
|
707
|
+
return;
|
708
|
+
}
|
709
|
+
this.reloader = new Reloader(this.window, this.console, Timer);
|
710
|
+
this.connector = new Connector(this.options, this.WebSocket, Timer, {
|
711
|
+
connecting: __bind(function() {}, this),
|
712
|
+
socketConnected: __bind(function() {}, this),
|
713
|
+
connected: __bind(function(protocol) {
|
714
|
+
var _base;
|
715
|
+
if (typeof (_base = this.listeners).connect === "function") {
|
716
|
+
_base.connect();
|
717
|
+
}
|
718
|
+
return this.log("LiveReload is connected to " + this.options.host + ":" + this.options.port + " (protocol v" + protocol + ").");
|
719
|
+
}, this),
|
720
|
+
error: __bind(function(e) {
|
721
|
+
if (e instanceof ProtocolError) {
|
722
|
+
return console.log("" + e.message + ".");
|
723
|
+
} else {
|
724
|
+
return console.log("LiveReload internal error: " + e.message);
|
725
|
+
}
|
726
|
+
}, this),
|
727
|
+
disconnected: __bind(function(reason, nextDelay) {
|
728
|
+
var _base;
|
729
|
+
if (typeof (_base = this.listeners).disconnect === "function") {
|
730
|
+
_base.disconnect();
|
731
|
+
}
|
732
|
+
switch (reason) {
|
733
|
+
case 'cannot-connect':
|
734
|
+
return this.log("LiveReload cannot connect to " + this.options.host + ":" + this.options.port + ", will retry in " + nextDelay + " sec.");
|
735
|
+
case 'broken':
|
736
|
+
return this.log("LiveReload disconnected from " + this.options.host + ":" + this.options.port + ", reconnecting in " + nextDelay + " sec.");
|
737
|
+
case 'handshake-timeout':
|
738
|
+
return this.log("LiveReload cannot connect to " + this.options.host + ":" + this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec.");
|
739
|
+
case 'handshake-failed':
|
740
|
+
return this.log("LiveReload cannot connect to " + this.options.host + ":" + this.options.port + " (handshake failed), will retry in " + nextDelay + " sec.");
|
741
|
+
case 'manual':
|
742
|
+
break;
|
743
|
+
case 'error':
|
744
|
+
break;
|
745
|
+
default:
|
746
|
+
return this.log("LiveReload disconnected from " + this.options.host + ":" + this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec.");
|
747
|
+
}
|
748
|
+
}, this),
|
749
|
+
message: __bind(function(message) {
|
750
|
+
switch (message.command) {
|
751
|
+
case 'reload':
|
752
|
+
return this.performReload(message);
|
753
|
+
case 'alert':
|
754
|
+
return this.performAlert(message);
|
755
|
+
}
|
756
|
+
}, this)
|
757
|
+
});
|
758
|
+
}
|
759
|
+
LiveReload.prototype.on = function(eventName, handler) {
|
760
|
+
return this.listeners[eventName] = handler;
|
761
|
+
};
|
762
|
+
LiveReload.prototype.log = function(message) {
|
763
|
+
return this.console.log("" + message);
|
764
|
+
};
|
765
|
+
LiveReload.prototype.performReload = function(message) {
|
766
|
+
var _ref, _ref2;
|
767
|
+
this.log("LiveReload received reload request for " + message.path + ".");
|
768
|
+
return this.reloader.reload(message.path, {
|
769
|
+
liveCSS: (_ref = message.liveCSS) != null ? _ref : true,
|
770
|
+
liveImg: (_ref2 = message.liveImg) != null ? _ref2 : true
|
771
|
+
});
|
772
|
+
};
|
773
|
+
LiveReload.prototype.performAlert = function(message) {
|
774
|
+
return alert(message.message);
|
775
|
+
};
|
776
|
+
LiveReload.prototype.shutDown = function() {
|
777
|
+
var _base;
|
778
|
+
this.connector.disconnect();
|
779
|
+
this.log("LiveReload disconnected.");
|
780
|
+
return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0;
|
781
|
+
};
|
782
|
+
return LiveReload;
|
783
|
+
})();
|
784
|
+
}).call(this);
|
785
|
+
|
786
|
+
// startup
|
787
|
+
(function() {
|
788
|
+
var CustomEvents, LiveReload;
|
789
|
+
CustomEvents = __customevents;
|
790
|
+
LiveReload = window.LiveReload = new (__livereload.LiveReload)(window);
|
791
|
+
LiveReload.on('shutdown', function() {
|
792
|
+
return delete window.LiveReload;
|
793
|
+
});
|
794
|
+
LiveReload.on('connect', function() {
|
795
|
+
return CustomEvents.fire(document, 'LiveReloadConnect');
|
796
|
+
});
|
797
|
+
LiveReload.on('disconnect', function() {
|
798
|
+
return CustomEvents.fire(document, 'LiveReloadDisconnect');
|
799
|
+
});
|
800
|
+
CustomEvents.bind(document, 'LiveReloadShutDown', function() {
|
801
|
+
return LiveReload.shutDown();
|
802
|
+
});
|
803
|
+
}).call(this);
|
804
|
+
})();
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rack
|
2
|
+
class LiveReload
|
3
|
+
LIVERELOAD_JS_PATH = '/__rack/livereload.js'
|
4
|
+
|
5
|
+
attr_reader :app
|
6
|
+
|
7
|
+
def initialize(app, options = {})
|
8
|
+
@app = app
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
if env['PATH_INFO'] == LIVERELOAD_JS_PATH
|
14
|
+
deliver_file(::File.expand_path('../../../js/livereload.js', __FILE__))
|
15
|
+
else
|
16
|
+
status, headers, body = @app.call(env)
|
17
|
+
|
18
|
+
case headers['Content-Type']
|
19
|
+
when %r{text/html}
|
20
|
+
content_length = 0
|
21
|
+
|
22
|
+
body.each do |line|
|
23
|
+
if !headers['X-Rack-LiveReload'] && line['</head>']
|
24
|
+
src = LIVERELOAD_JS_PATH.dup
|
25
|
+
if @options[:host]
|
26
|
+
src << "?host=#{@options[:host]}"
|
27
|
+
else
|
28
|
+
src << "?host=#{env['HTTP_HOST'].gsub(%r{:.*}, '')}" if env['HTTP_HOST']
|
29
|
+
end
|
30
|
+
src << "&mindelay=#{@options[:min_delay]}" if @options[:min_delay]
|
31
|
+
src << "&maxdelay=#{@options[:max_delay]}" if @options[:max_delay]
|
32
|
+
src << "&port=#{@options[:port]}" if @options[:port]
|
33
|
+
|
34
|
+
line.gsub!('</head>', %{<script type="text/javascript" src="#{src}"></script></head>})
|
35
|
+
|
36
|
+
headers["X-Rack-LiveReload"] = '1'
|
37
|
+
end
|
38
|
+
|
39
|
+
content_length += line.length
|
40
|
+
end
|
41
|
+
|
42
|
+
headers['Content-Length'] = content_length.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
[ status, headers, body ]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def deliver_file(file)
|
51
|
+
[ 200, { 'Content-Type' => 'text/javascript', 'Content-Length' => ::File.size(file).to_s }, [ ::File.read(file) ] ]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rack-livereload"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rack-livereload"
|
7
|
+
s.version = Rack::LiveReload::VERSION
|
8
|
+
s.authors = ["John Bintz"]
|
9
|
+
s.email = ["john@coswellproductions.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Insert LiveReload into your app easily as Rack middleware}
|
12
|
+
s.description = %q{Insert LiveReload into your app easily as Rack middleware}
|
13
|
+
|
14
|
+
s.rubyforge_project = "rack-livereload"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "httparty"
|
24
|
+
s.add_development_dependency "sinatra"
|
25
|
+
s.add_development_dependency "shotgun"
|
26
|
+
s.add_development_dependency "thin"
|
27
|
+
s.add_development_dependency "rake"
|
28
|
+
s.add_development_dependency "mocha"
|
29
|
+
s.add_development_dependency "guard"
|
30
|
+
s.add_development_dependency "guard-rspec"
|
31
|
+
|
32
|
+
s.add_runtime_dependency "rack"
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rack::LiveReload do
|
4
|
+
let(:middleware) { described_class.new(app) }
|
5
|
+
let(:app) { stub }
|
6
|
+
|
7
|
+
subject { middleware }
|
8
|
+
|
9
|
+
its(:app) { should == app }
|
10
|
+
|
11
|
+
let(:env) { {} }
|
12
|
+
|
13
|
+
context 'not text/html' do
|
14
|
+
let(:ret) { [ 200, { 'Content-Type' => 'image/png' }, [ '<head></head>' ] ] }
|
15
|
+
|
16
|
+
before do
|
17
|
+
app.stubs(:call).with(env).returns(ret)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should pass through' do
|
21
|
+
middleware.call(env).should == ret
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'text/html' do
|
26
|
+
before do
|
27
|
+
app.stubs(:call).with(env).returns([ 200, { 'Content-Type' => 'text/html', 'Content-Length' => 0 }, [ '<head></head>' ] ])
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:host) { 'host' }
|
31
|
+
let(:env) { { 'HTTP_HOST' => host } }
|
32
|
+
|
33
|
+
let(:ret) { middleware.call(env) }
|
34
|
+
let(:body) { ret.last.join }
|
35
|
+
let(:length) { ret[1]['Content-Length'] }
|
36
|
+
|
37
|
+
it 'should add the livereload js script tag' do
|
38
|
+
body.should include("script")
|
39
|
+
body.should include(described_class::LIVERELOAD_JS_PATH)
|
40
|
+
|
41
|
+
length.should == body.length.to_s
|
42
|
+
|
43
|
+
described_class::LIVERELOAD_JS_PATH.should_not include(host)
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'set options' do
|
47
|
+
let(:middleware) { described_class.new(app, :host => new_host, :port => port, :min_delay => min_delay, :max_delay => max_delay) }
|
48
|
+
let(:min_delay) { 5 }
|
49
|
+
let(:max_delay) { 10 }
|
50
|
+
let(:port) { 23 }
|
51
|
+
let(:new_host) { 'myhost' }
|
52
|
+
|
53
|
+
it 'should add the livereload.js script tag' do
|
54
|
+
body.should include("mindelay=#{min_delay}")
|
55
|
+
body.should include("maxdelay=#{max_delay}")
|
56
|
+
body.should include("port=#{port}")
|
57
|
+
body.should include("host=#{new_host}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context '/__rack/livereload.js' do
|
63
|
+
let(:env) { { 'PATH_INFO' => described_class::LIVERELOAD_JS_PATH } }
|
64
|
+
|
65
|
+
before do
|
66
|
+
middleware.expects(:deliver_file).returns(true)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should return the js file' do
|
70
|
+
middleware.call(env).should be_true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-livereload
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Bintz
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-07 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &2153313940 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2153313940
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: httparty
|
27
|
+
requirement: &2153313340 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2153313340
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sinatra
|
38
|
+
requirement: &2153312520 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2153312520
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: shotgun
|
49
|
+
requirement: &2153311420 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2153311420
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: thin
|
60
|
+
requirement: &2153309720 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2153309720
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: &2153299320 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2153299320
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: mocha
|
82
|
+
requirement: &2153298900 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *2153298900
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: guard
|
93
|
+
requirement: &2153298480 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *2153298480
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: guard-rspec
|
104
|
+
requirement: &2153298000 !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: *2153298000
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: rack
|
115
|
+
requirement: &2153297500 !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ! '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
type: :runtime
|
122
|
+
prerelease: false
|
123
|
+
version_requirements: *2153297500
|
124
|
+
description: Insert LiveReload into your app easily as Rack middleware
|
125
|
+
email:
|
126
|
+
- john@coswellproductions.com
|
127
|
+
executables: []
|
128
|
+
extensions: []
|
129
|
+
extra_rdoc_files: []
|
130
|
+
files:
|
131
|
+
- .gitignore
|
132
|
+
- Gemfile
|
133
|
+
- Guardfile
|
134
|
+
- README.md
|
135
|
+
- Rakefile
|
136
|
+
- config.ru
|
137
|
+
- js/livereload.js
|
138
|
+
- lib/rack-livereload.rb
|
139
|
+
- lib/rack/livereload.rb
|
140
|
+
- rack-livereload.gemspec
|
141
|
+
- spec/rack/livereload_spec.rb
|
142
|
+
- spec/spec_helper.rb
|
143
|
+
homepage: ''
|
144
|
+
licenses: []
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
151
|
+
requirements:
|
152
|
+
- - ! '>='
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ! '>='
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
requirements: []
|
162
|
+
rubyforge_project: rack-livereload
|
163
|
+
rubygems_version: 1.8.11
|
164
|
+
signing_key:
|
165
|
+
specification_version: 3
|
166
|
+
summary: Insert LiveReload into your app easily as Rack middleware
|
167
|
+
test_files:
|
168
|
+
- spec/rack/livereload_spec.rb
|
169
|
+
- spec/spec_helper.rb
|