From fbfbd1955933f5732f7c24ce2b6a858ea81a3e56 Mon Sep 17 00:00:00 2001 From: Mark Lopez Date: Sat, 28 Jan 2017 00:26:25 -0600 Subject: [PATCH 1/7] Added basic support for LiveReload when watching + previewing. --- src/clients/Wyam/Commands/BuildCommand.cs | 30 +- src/clients/Wyam/Commands/PreviewCommand.cs | 2 +- .../Wyam/LiveReload/LiveReloadServer.cs | 56 + .../Wyam/LiveReload/Messages/BasicMessage.cs | 7 + .../Wyam/LiveReload/Messages/HelloMessage.cs | 13 + .../LiveReload/Messages/ILiveReloadMessage.cs | 7 + .../Wyam/LiveReload/Messages/InfoMessage.cs | 9 + .../Wyam/LiveReload/Messages/ReloadMessage.cs | 11 + src/clients/Wyam/LiveReload/ReloadClient.cs | 135 ++ .../LiveReload/ReloadClientServiceLocator.cs | 63 + src/clients/Wyam/LiveReload/livereload.js | 1205 +++++++++++++++++ src/clients/Wyam/PreviewServer.cs | 13 +- src/clients/Wyam/Wyam.csproj | 20 + src/clients/Wyam/packages.config | 5 +- 14 files changed, 1562 insertions(+), 14 deletions(-) create mode 100644 src/clients/Wyam/LiveReload/LiveReloadServer.cs create mode 100644 src/clients/Wyam/LiveReload/Messages/BasicMessage.cs create mode 100644 src/clients/Wyam/LiveReload/Messages/HelloMessage.cs create mode 100644 src/clients/Wyam/LiveReload/Messages/ILiveReloadMessage.cs create mode 100644 src/clients/Wyam/LiveReload/Messages/InfoMessage.cs create mode 100644 src/clients/Wyam/LiveReload/Messages/ReloadMessage.cs create mode 100644 src/clients/Wyam/LiveReload/ReloadClient.cs create mode 100644 src/clients/Wyam/LiveReload/ReloadClientServiceLocator.cs create mode 100644 src/clients/Wyam/LiveReload/livereload.js diff --git a/src/clients/Wyam/Commands/BuildCommand.cs b/src/clients/Wyam/Commands/BuildCommand.cs index 18b43dc48..d351f57c5 100644 --- a/src/clients/Wyam/Commands/BuildCommand.cs +++ b/src/clients/Wyam/Commands/BuildCommand.cs @@ -2,12 +2,13 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.CommandLine; -using System.IO; using System.Linq; using System.Threading; + using Wyam.Common.IO; +using Wyam.Common.Tracing; using Wyam.Configuration.Preprocessing; -using Trace = Wyam.Common.Tracing.Trace; +using Wyam.LiveReload; namespace Wyam.Commands { @@ -18,7 +19,7 @@ internal class BuildCommand : Command private readonly InterlockedBool _exit = new InterlockedBool(false); private readonly InterlockedBool _newEngine = new InterlockedBool(false); private readonly ConfigOptions _configOptions = new ConfigOptions(); - + private bool _preview = false; private int _previewPort = 5080; private DirectoryPath _previewVirtualDirectory = null; @@ -27,7 +28,7 @@ internal class BuildCommand : Command private bool _verifyConfig = false; private DirectoryPath _previewRoot = null; private bool _watch = false; - + public override string Description => "Runs the build process (this is the default command)."; public override string[] SupportedDirectives => new[] @@ -106,7 +107,7 @@ private static void AddSettings(IDictionary settings, IReadOnlyL { foreach (KeyValuePair kvp in MetadataParser.Parse(value)) { - settings[kvp.Key] = kvp.Value; + settings[kvp.Key] = kvp.Value; } } @@ -119,7 +120,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) { // Get the standard input stream _configOptions.Stdin = StandardInputReader.Read(); - + // Fix the root folder and other files DirectoryPath currentDirectory = Environment.CurrentDirectory; _configOptions.RootPath = _configOptions.RootPath == null ? currentDirectory : currentDirectory.Combine(_configOptions.RootPath); @@ -162,6 +163,14 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) bool messagePump = false; + // Start the LiveReload server. + bool runLiveReloadServer = _watch; + LiveReloadServer liveReloadServer = null; + if (runLiveReloadServer) + { + liveReloadServer = new LiveReloadServer(); + } + // Start the preview server IDisposable previewServer = null; if (_preview) @@ -170,7 +179,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) DirectoryPath previewPath = _previewRoot == null ? engineManager.Engine.FileSystem.GetOutputDirectory().Path : engineManager.Engine.FileSystem.GetOutputDirectory(_previewRoot).Path; - previewServer = PreviewServer.Start(previewPath, _previewPort, _previewForceExtension, _previewVirtualDirectory); + previewServer = PreviewServer.Start(previewPath, _previewPort, _previewForceExtension, _previewVirtualDirectory, liveReloadServer); } // Start the watchers @@ -192,7 +201,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) { Trace.Information("Watching configuration file {0}", _configOptions.ConfigFilePath); configFileWatcher = new ActionFileSystemWatcher(engineManager.Engine.FileSystem.GetOutputDirectory().Path, - new[] { _configOptions.ConfigFilePath.Directory }, false, _configOptions.ConfigFilePath.FileName.FullPath, path => + new[] {_configOptions.ConfigFilePath.Directory}, false, _configOptions.ConfigFilePath.FileName.FullPath, path => { FilePath filePath = new FilePath(path); if (_configOptions.ConfigFilePath.Equals(filePath)) @@ -228,7 +237,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) // Wait for activity while (true) { - _messageEvent.WaitOne(); // Blocks the current thread until a signal + _messageEvent.WaitOne(); // Blocks the current thread until a signal if (_exit) { break; @@ -283,6 +292,8 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) { exitCode = ExitCode.ExecutionError; } + + liveReloadServer?.RebuildCompleted(changedFiles); } } @@ -301,6 +312,7 @@ protected override ExitCode RunCommand(Preprocessor preprocessor) inputFolderWatcher?.Dispose(); configFileWatcher?.Dispose(); previewServer?.Dispose(); + liveReloadServer?.Dispose(); } return exitCode; diff --git a/src/clients/Wyam/Commands/PreviewCommand.cs b/src/clients/Wyam/Commands/PreviewCommand.cs index c1df39c49..c88510c9a 100644 --- a/src/clients/Wyam/Commands/PreviewCommand.cs +++ b/src/clients/Wyam/Commands/PreviewCommand.cs @@ -30,7 +30,7 @@ protected override void ParseParameters(ArgumentSyntax syntax) protected override ExitCode RunCommand(Preprocessor preprocessor) { _path = new DirectoryPath(Environment.CurrentDirectory).Combine(_path ?? "output"); - using (PreviewServer.Start(_path, _port, _forceExtension, _virtualDirectory)) + using (PreviewServer.Start(_path, _port, _forceExtension, _virtualDirectory, null)) { Trace.Information("Hit any key to exit"); Console.ReadKey(); diff --git a/src/clients/Wyam/LiveReload/LiveReloadServer.cs b/src/clients/Wyam/LiveReload/LiveReloadServer.cs new file mode 100644 index 000000000..1d82d8b5e --- /dev/null +++ b/src/clients/Wyam/LiveReload/LiveReloadServer.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Owin; +using Microsoft.Owin.FileSystems; +using Microsoft.Owin.StaticFiles; + +using Owin; +using Owin.WebSocket.Extensions; + +namespace Wyam.LiveReload +{ + internal class LiveReloadServer : IDisposable + { + private readonly ReloadClientServiceLocator _clientServiceLocator; + + public LiveReloadServer() + { + _clientServiceLocator = new ReloadClientServiceLocator(); + } + + public void InjectOwinMiddleware(IAppBuilder app) + { + // Host livereload.js + var liveReloadAssembly = typeof(LiveReloadServer).Assembly; + var rootNamespace = typeof(LiveReloadServer).Namespace; + var reloadFilesystem = new EmbeddedResourceFileSystem(liveReloadAssembly, $"{rootNamespace}"); + app.UseStaticFiles(new StaticFileOptions + { + RequestPath = PathString.Empty, + FileSystem = reloadFilesystem, + ServeUnknownFileTypes = true + }); + + // Host ws:// + app.MapWebSocketRoute("/livereload", _clientServiceLocator); + } + + public void RebuildCompleted(ICollection filesChanged) + { + var clientsToNotify = _clientServiceLocator?.ReloadClients ?? Enumerable.Empty(); + foreach (var client in clientsToNotify.Where(x => x.IsConnected)) + { + foreach (var modifiedFile in filesChanged) + { + client.NotifyOfChanges(modifiedFile); + } + } + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/BasicMessage.cs b/src/clients/Wyam/LiveReload/Messages/BasicMessage.cs new file mode 100644 index 000000000..695b8e870 --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/BasicMessage.cs @@ -0,0 +1,7 @@ +namespace Wyam.LiveReload.Messages +{ + internal class BasicMessage : ILiveReloadMessage + { + public string Command { get; set; } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/HelloMessage.cs b/src/clients/Wyam/LiveReload/Messages/HelloMessage.cs new file mode 100644 index 000000000..f4e4142ac --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/HelloMessage.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Wyam.LiveReload.Messages +{ + internal class HelloMessage : ILiveReloadMessage + { + public ICollection Protocols { get; set; } + + public string ServerName { get; set; } = "Wyam"; + + public string Command { get; set; } = "hello"; + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/ILiveReloadMessage.cs b/src/clients/Wyam/LiveReload/Messages/ILiveReloadMessage.cs new file mode 100644 index 000000000..0ebdd0a34 --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/ILiveReloadMessage.cs @@ -0,0 +1,7 @@ +namespace Wyam.LiveReload.Messages +{ + internal interface ILiveReloadMessage + { + string Command { get; set; } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/InfoMessage.cs b/src/clients/Wyam/LiveReload/Messages/InfoMessage.cs new file mode 100644 index 000000000..82d4ba95c --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/InfoMessage.cs @@ -0,0 +1,9 @@ +namespace Wyam.LiveReload.Messages +{ + internal class InfoMessage : ILiveReloadMessage + { + public string Command { get; set; } = "info"; + + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/Messages/ReloadMessage.cs b/src/clients/Wyam/LiveReload/Messages/ReloadMessage.cs new file mode 100644 index 000000000..1f414334e --- /dev/null +++ b/src/clients/Wyam/LiveReload/Messages/ReloadMessage.cs @@ -0,0 +1,11 @@ +namespace Wyam.LiveReload.Messages +{ + internal class ReloadMessage : ILiveReloadMessage + { + public string Path { get; set; } + + public bool LiveCss { get; set; } + + public string Command { get; set; } = "reload"; + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/ReloadClient.cs b/src/clients/Wyam/LiveReload/ReloadClient.cs new file mode 100644 index 000000000..104d44185 --- /dev/null +++ b/src/clients/Wyam/LiveReload/ReloadClient.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +using Owin.WebSocket; + +using Wyam.Common.Tracing; +using Wyam.LiveReload.Messages; + +namespace Wyam.LiveReload +{ + public class ReloadClient : WebSocketConnection + { + // Attempt to support the Livereload protocol v7. + // http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol + + private readonly JsonSerializerSettings _defaultSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Formatting = Formatting.Indented + }; + private readonly HashSet _supportedVersion = new HashSet + { + "http://livereload.com/protocols/official-7" // Only supporting v7 right now + }; + private readonly Guid _clientId = Guid.NewGuid(); + + public bool IsConnected { get; private set; } + + public override Task OnMessageReceived(ArraySegment message, WebSocketMessageType type) + { + var json = Encoding.UTF8.GetString(message.Array, message.Offset, message.Count); + HandleClientMessage(json); + + return Task.CompletedTask; + } + + public override void OnOpen() + { + SayHello(); + base.OnOpen(); + } + + public override void OnClose(WebSocketCloseStatus? closeStatus, string closeStatusDescription) + { + IsConnected = false; + LogVerbose($"Lost connection with LiveReload client. Status=({closeStatus}) Description=({closeStatusDescription})"); + base.OnClose(closeStatus, closeStatusDescription); + } + + public void NotifyOfChanges(string modifiedFile, bool supportCssReload = true) + { + // Asume changes have been rebuilt by this time + var reloadMessage = new ReloadMessage + { + Path = modifiedFile, + LiveCss = supportCssReload + }; + + LogVerbose($"Sending reload for {modifiedFile}."); + SendObject(reloadMessage); + } + + private ILiveReloadMessage HandleClientMessage(string json) + { + var parsedMessage = JsonConvert.DeserializeObject(json, _defaultSettings); + switch (parsedMessage.Command) + { + case "info": + var info = JsonConvert.DeserializeObject(json, _defaultSettings); + LogVerbose($"LiveReload sent info ({info.Url})."); + break; + case "hello": + var hello = JsonConvert.DeserializeObject(json, _defaultSettings); + HandleHello(hello); + break; + default: + LogVerbose($"Unknown command recieved from LiveReload client = {parsedMessage.Command}."); + break; + } + + return parsedMessage; + } + + private void HandleHello(HelloMessage message) + { + var negotiatedVersion = message.Protocols + .Intersect(_supportedVersion) + .OrderByDescending(x => x) + .FirstOrDefault(); + + if (negotiatedVersion == null) + { + var incompatibleMessage = "LiveReload client is not compatible with this server, aborting connection. " + + $"Client=({string.Join(",", message.Protocols)}) " + + $"Server=({string.Join(",", _supportedVersion)})"; + LogVerbose(incompatibleMessage); + Abort(); + } + else + { + LogVerbose($"LiveReload client hello. Negotiated=({negotiatedVersion})"); + IsConnected = true; + } + } + + private void SayHello() + { + var helloMessage = new HelloMessage + { + Protocols = _supportedVersion + }; + + SendObject(helloMessage); + } + + private void SendObject(T obj) + { + var json = JsonConvert.SerializeObject(obj, _defaultSettings); + var bytes = Encoding.UTF8.GetBytes(json); // UTF-8 by spec + SendText(bytes, true); + } + + private void LogVerbose(string message) + { + Trace.Verbose($"{message} Client=({_clientId})"); + } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/ReloadClientServiceLocator.cs b/src/clients/Wyam/LiveReload/ReloadClientServiceLocator.cs new file mode 100644 index 000000000..1535be0f7 --- /dev/null +++ b/src/clients/Wyam/LiveReload/ReloadClientServiceLocator.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +using Microsoft.Practices.ServiceLocation; + +namespace Wyam.LiveReload +{ + internal class ReloadClientServiceLocator : IServiceLocator + { + readonly ConcurrentBag _clients = new ConcurrentBag(); + + public IEnumerable ReloadClients => _clients.ToArray(); + + public object GetService(Type serviceType) + { + throw new NotImplementedException(); + } + + public object GetInstance(Type serviceType) + { + throw new NotImplementedException(); + } + + public object GetInstance(Type serviceType, string key) + { + throw new NotImplementedException(); + } + + public IEnumerable GetAllInstances(Type serviceType) + { + throw new NotImplementedException(); + } + + public TService GetInstance() + { + if (typeof(TService) == typeof(ReloadClient)) + { + var client = CreateClient(); + return (TService) client; + } + + throw new NotImplementedException(); + } + + public TService GetInstance(string key) + { + throw new NotImplementedException(); + } + + public IEnumerable GetAllInstances() + { + throw new NotImplementedException(); + } + + private object CreateClient() + { + var client = new ReloadClient(); + _clients.Add(client); + return client; + } + } +} \ No newline at end of file diff --git a/src/clients/Wyam/LiveReload/livereload.js b/src/clients/Wyam/LiveReload/livereload.js new file mode 100644 index 000000000..6bb479c1e --- /dev/null +++ b/src/clients/Wyam/LiveReload/livereload.js @@ -0,0 +1,1205 @@ +// Copyright (c) 2010-2012 Andrey Tarantsov + +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +(function e(t, n, r) { function s(o, u) { if (!n[o]) { if (!t[o]) { var a = typeof require == "function" && require; if (!u && a) return a(o, !0); if (i) return i(o, !0); var f = new Error("Cannot find module '" + o + "'"); throw f.code = "MODULE_NOT_FOUND", f } var l = n[o] = { exports: {} }; t[o][0].call(l.exports, function (e) { var n = t[o][1][e]; return s(n ? n : e) }, l, l.exports, e, t, n, r) } return n[o].exports } var i = typeof require == "function" && require; for (var o = 0; o < r.length; o++)s(r[o]); return s })({ + 1: [function (require, module, exports) { +(function() { + var Connector, PROTOCOL_6, PROTOCOL_7, Parser, Version, _ref; + + _ref = require('./protocol'), Parser = _ref.Parser, PROTOCOL_6 = _ref.PROTOCOL_6, PROTOCOL_7 = _ref.PROTOCOL_7; + + Version = '2.2.2'; + + exports.Connector = Connector = (function() { + function Connector(options, WebSocket, Timer, handlers) { + this.options = options; + this.WebSocket = WebSocket; + this.Timer = Timer; + this.handlers = handlers; + this._uri = "ws" + (this.options.https ? "s" : "") + "://" + this.options.host + ":" + this.options.port + "/livereload"; + this._nextDelay = this.options.mindelay; + this._connectionDesired = false; + this.protocol = 0; + this.protocolParser = new Parser({ + connected: (function(_this) { + return function(protocol) { + _this.protocol = protocol; + _this._handshakeTimeout.stop(); + _this._nextDelay = _this.options.mindelay; + _this._disconnectionReason = 'broken'; + return _this.handlers.connected(protocol); + }; + })(this), + error: (function(_this) { + return function(e) { + _this.handlers.error(e); + return _this._closeOnError(); + }; + })(this), + message: (function(_this) { + return function(message) { + return _this.handlers.message(message); + }; + })(this) + }); + this._handshakeTimeout = new Timer((function(_this) { + return function() { + if (!_this._isSocketConnected()) { + return; + } + _this._disconnectionReason = 'handshake-timeout'; + return _this.socket.close(); + }; + })(this)); + this._reconnectTimer = new Timer((function(_this) { + return function() { + if (!_this._connectionDesired) { + return; + } + return _this.connect(); + }; + })(this)); + this.connect(); + } + + Connector.prototype._isSocketConnected = function() { + return this.socket && this.socket.readyState === this.WebSocket.OPEN; + }; + + Connector.prototype.connect = function() { + this._connectionDesired = true; + if (this._isSocketConnected()) { + return; + } + this._reconnectTimer.stop(); + this._disconnectionReason = 'cannot-connect'; + this.protocolParser.reset(); + this.handlers.connecting(); + this.socket = new this.WebSocket(this._uri); + this.socket.onopen = (function(_this) { + return function(e) { + return _this._onopen(e); + }; + })(this); + this.socket.onclose = (function(_this) { + return function(e) { + return _this._onclose(e); + }; + })(this); + this.socket.onmessage = (function(_this) { + return function(e) { + return _this._onmessage(e); + }; + })(this); + return this.socket.onerror = (function(_this) { + return function(e) { + return _this._onerror(e); + }; + })(this); + }; + + Connector.prototype.disconnect = function() { + this._connectionDesired = false; + this._reconnectTimer.stop(); + if (!this._isSocketConnected()) { + return; + } + this._disconnectionReason = 'manual'; + return this.socket.close(); + }; + + Connector.prototype._scheduleReconnection = function() { + if (!this._connectionDesired) { + return; + } + if (!this._reconnectTimer.running) { + this._reconnectTimer.start(this._nextDelay); + return this._nextDelay = Math.min(this.options.maxdelay, this._nextDelay * 2); + } + }; + + Connector.prototype.sendCommand = function(command) { + if (this.protocol == null) { + return; + } + return this._sendCommand(command); + }; + + Connector.prototype._sendCommand = function(command) { + return this.socket.send(JSON.stringify(command)); + }; + + Connector.prototype._closeOnError = function() { + this._handshakeTimeout.stop(); + this._disconnectionReason = 'error'; + return this.socket.close(); + }; + + Connector.prototype._onopen = function(e) { + var hello; + this.handlers.socketConnected(); + this._disconnectionReason = 'handshake-failed'; + hello = { + command: 'hello', + protocols: [PROTOCOL_6, PROTOCOL_7] + }; + hello.ver = Version; + if (this.options.ext) { + hello.ext = this.options.ext; + } + if (this.options.extver) { + hello.extver = this.options.extver; + } + if (this.options.snipver) { + hello.snipver = this.options.snipver; + } + this._sendCommand(hello); + return this._handshakeTimeout.start(this.options.handshake_timeout); + }; + + Connector.prototype._onclose = function(e) { + this.protocol = 0; + this.handlers.disconnected(this._disconnectionReason, this._nextDelay); + return this._scheduleReconnection(); + }; + + Connector.prototype._onerror = function(e) {}; + + Connector.prototype._onmessage = function(e) { + return this.protocolParser.process(e.data); + }; + + return Connector; + + })(); + +}).call(this); + +},{"./protocol":6}],2:[function(require,module,exports){ +(function() { + var CustomEvents; + + CustomEvents = { + bind: function(element, eventName, handler) { + if (element.addEventListener) { + return element.addEventListener(eventName, handler, false); + } else if (element.attachEvent) { + element[eventName] = 1; + return element.attachEvent('onpropertychange', function(event) { + if (event.propertyName === eventName) { + return handler(); + } + }); + } else { + throw new Error("Attempt to attach custom event " + eventName + " to something which isn't a DOMElement"); + } + }, + fire: function(element, eventName) { + var event; + if (element.addEventListener) { + event = document.createEvent('HTMLEvents'); + event.initEvent(eventName, true, true); + return document.dispatchEvent(event); + } else if (element.attachEvent) { + if (element[eventName]) { + return element[eventName]++; + } + } else { + throw new Error("Attempt to fire custom event " + eventName + " on something which isn't a DOMElement"); + } + } + }; + + exports.bind = CustomEvents.bind; + + exports.fire = CustomEvents.fire; + +}).call(this); + +},{}],3:[function(require,module,exports){ +(function() { + var LessPlugin; + + module.exports = LessPlugin = (function() { + LessPlugin.identifier = 'less'; + + LessPlugin.version = '1.0'; + + function LessPlugin(window, host) { + this.window = window; + this.host = host; + } + + LessPlugin.prototype.reload = function(path, options) { + if (this.window.less && this.window.less.refresh) { + if (path.match(/\.less$/i)) { + return this.reloadLess(path); + } + if (options.originalPath.match(/\.less$/i)) { + return this.reloadLess(options.originalPath); + } + } + return false; + }; + + LessPlugin.prototype.reloadLess = function(path) { + var link, links, _i, _len; + links = (function() { + var _i, _len, _ref, _results; + _ref = document.getElementsByTagName('link'); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + link = _ref[_i]; + if (link.href && link.rel.match(/^stylesheet\/less$/i) || (link.rel.match(/stylesheet/i) && link.type.match(/^text\/(x-)?less$/i))) { + _results.push(link); + } + } + return _results; + })(); + if (links.length === 0) { + return false; + } + for (_i = 0, _len = links.length; _i < _len; _i++) { + link = links[_i]; + link.href = this.host.generateCacheBustUrl(link.href); + } + this.host.console.log("LiveReload is asking LESS to recompile all stylesheets"); + this.window.less.refresh(true); + return true; + }; + + LessPlugin.prototype.analyze = function() { + return { + disable: !!(this.window.less && this.window.less.refresh) + }; + }; + + return LessPlugin; + + })(); + +}).call(this); + +},{}],4:[function(require,module,exports){ +(function() { + var Connector, LiveReload, Options, Reloader, Timer, + __hasProp = {}.hasOwnProperty; + + Connector = require('./connector').Connector; + + Timer = require('./timer').Timer; + + Options = require('./options').Options; + + Reloader = require('./reloader').Reloader; + + exports.LiveReload = LiveReload = (function() { + function LiveReload(window) { + var k, v, _ref; + this.window = window; + this.listeners = {}; + this.plugins = []; + this.pluginIdentifiers = {}; + this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.location.href.match(/LR-verbose/) ? this.window.console : { + log: function() {}, + error: this.window.console.error.bind(this.window.console) + } : { + log: function() {}, + error: function() {} + }; + if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) { + this.console.error("LiveReload disabled because the browser does not seem to support web sockets"); + return; + } + if ('LiveReloadOptions' in window) { + this.options = new Options(); + _ref = window['LiveReloadOptions']; + for (k in _ref) { + if (!__hasProp.call(_ref, k)) continue; + v = _ref[k]; + this.options.set(k, v); + } + } else { + this.options = Options.extract(this.window.document); + if (!this.options) { + this.console.error("LiveReload disabled because it could not find its own