livereload/livereload-js

Custom "pathFromUrl" to remove digest/hash from asset filename

steffenweber opened this issue · 2 comments

First of all, thank you for implementing issue #17!

My asset hrefs contain a digest/hash, i.e. the main.css on disk is referenced as main.eec09356.css in the HTML (which is then rewritten to main.css by the webserver). The "changed" URL sent to livereload-js is just main.css. Therefore nothing matches with reloadMissingCSS = false.

I think I'd have to modify the pathFromUrl function to support this setup:

path = path.replace(/\.[0-9a-f]{8}\./, '.');

What would be the best way to make this feature optional / configurable such that it can be merged? Maybe a new pathFromUrl option that (if present) would be used instead of the default pathFromUrl function?

window.LiveReloadOptions = {
	pathFromUrl: function(url) {
		return url.replace(/\.[0-9a-f]{8}\./, '.');
	}
};
smhg commented

Sounds reasonable to me. There is no way the server can be aware of how to transform this, right?

The server (gulp + tiny-lr in my case) could compute the new digest/hash but it does not know the old digest/hash. However, it could provide the client with a regex to transform the path if you'd prefer making this configurable in the server instead of in window.LiveReloadOptions.

The following patch adds a client-side option pathRewriteFn that is handled inside pathFromUrl. I had to move this function into the Reloader class such that it has access to the @options object. I've furthermore renamed the old @options object to @reloadOptions (such that @options refers to the same object as in other source files).

diff --git a/src/livereload.coffee b/src/livereload.coffee
index 06f2579..0073400 100644
--- a/src/livereload.coffee
+++ b/src/livereload.coffee
@@ -40,7 +40,7 @@ exports.LiveReload = class LiveReload
         return
 
     # i can haz reloader?
-    @reloader = new Reloader(@window, @console, Timer)
+    @reloader = new Reloader(@options, @window, @console, Timer)
 
     # i can haz connection?
     @connector = new Connector @options, @WebSocket, Timer,
diff --git a/src/options.coffee b/src/options.coffee
index 057ba30..2cb7393 100644
--- a/src/options.coffee
+++ b/src/options.coffee
@@ -12,6 +12,8 @@ exports.Options = class Options
     @maxdelay = 60000
     @handshake_timeout = 5000
 
+    @pathRewriteFn = null
+
   set: (name, value) ->
     if typeof value is 'undefined'
       return
diff --git a/src/reloader.coffee b/src/reloader.coffee
index 4df0a52..56de2b0 100644
--- a/src/reloader.coffee
+++ b/src/reloader.coffee
@@ -20,17 +20,6 @@ splitUrl = (url) ->
 
   return { url, params, hash }
 
-pathFromUrl = (url) ->
-  url = splitUrl(url).url
-  if url.indexOf('file://') == 0
-    path = url.replace ///^ file:// (localhost)? ///, ''
-  else
-    #                        http  :   // hostname  :8080  /
-    path = url.replace ///^ ([^:]+ :)? // ([^:/]+) (:\d*)? / ///, '/'
-
-  # decodeURI has special handling of stuff like semicolons, so use decodeURIComponent
-  return decodeURIComponent(path)
-
 pickBestMatch = (path, objects, pathFunc) ->
   bestMatch = { score: 0 }
   for object in objects
@@ -68,7 +57,7 @@ IMAGE_STYLES = [
 
 exports.Reloader = class Reloader
 
-  constructor: (@window, @console, @Timer) ->
+  constructor: (@options, @window, @console, @Timer) ->
     @document = @window.document
     @importCacheWaitPeriod = 200
     @plugins = []
@@ -83,8 +72,8 @@ exports.Reloader = class Reloader
 
 
   reload: (path, options) ->
-    @options = options  # avoid passing it through all the funcs
-    @options.stylesheetReloadTimeout ?= 15000
+    @reloadOptions = options  # avoid passing it through all the funcs
+    @reloadOptions.stylesheetReloadTimeout ?= 15000
     for plugin in @plugins
       if plugin.reload && plugin.reload(path, options)
         return
@@ -109,7 +98,7 @@ exports.Reloader = class Reloader
     expando = @generateUniqueString()
 
     for img in this.document.images
-      if pathsMatch(path, pathFromUrl(img.src))
+      if pathsMatch(path, @pathFromUrl(img.src))
         img.src = @generateCacheBustUrl(img.src, expando)
 
     if @document.querySelectorAll
@@ -147,7 +136,7 @@ exports.Reloader = class Reloader
       value = style[styleName]
       if typeof value is 'string'
         newValue = value.replace ///\b url \s* \( ([^)]*) \) ///, (match, src) =>
-          if pathsMatch(path, pathFromUrl(src))
+          if pathsMatch(path, @pathFromUrl(src))
             "url(#{@generateCacheBustUrl(src, expando)})"
           else
             match
@@ -173,7 +162,7 @@ exports.Reloader = class Reloader
         links.push style
 
     @console.log "LiveReload found #{links.length} LINKed stylesheets, #{imported.length} @imported stylesheets"
-    match = pickBestMatch(path, links.concat(imported), (l) => pathFromUrl(@linkHref(l)))
+    match = pickBestMatch(path, links.concat(imported), (l) => @pathFromUrl(@linkHref(l)))
 
     if match
       if match.object.rule
@@ -241,7 +230,7 @@ exports.Reloader = class Reloader
           @Timer.start 50, poll
 
     # fail safe
-    @Timer.start @options.stylesheetReloadTimeout, executeCallback
+    @Timer.start @reloadOptions.stylesheetReloadTimeout, executeCallback
 
 
   linkHref: (link) ->
@@ -335,10 +324,10 @@ exports.Reloader = class Reloader
   generateCacheBustUrl: (url, expando=@generateUniqueString()) ->
     { url, hash, params: oldParams } = splitUrl(url)
 
-    if @options.overrideURL
-      if url.indexOf(@options.serverURL) < 0
+    if @reloadOptions.overrideURL
+      if url.indexOf(@reloadOptions.serverURL) < 0
         originalUrl = url
-        url = @options.serverURL + @options.overrideURL + "?url=" + encodeURIComponent(url)
+        url = @reloadOptions.serverURL + @reloadOptions.overrideURL + "?url=" + encodeURIComponent(url)
         @console.log "LiveReload is overriding source URL #{originalUrl} with #{url}"
 
     params = oldParams.replace /(\?|&)livereload=(\d+)/, (match, sep) -> "#{sep}#{expando}"
@@ -349,3 +338,19 @@ exports.Reloader = class Reloader
         params = "#{oldParams}&#{expando}"
 
     return url + params + hash
+
+  pathFromUrl: (url) ->
+    url = splitUrl(url).url
+    if url.indexOf('file://') == 0
+      path = url.replace ///^ file:// (localhost)? ///, ''
+    else
+      #                        http  :   // hostname  :8080  /
+      path = url.replace ///^ ([^:]+ :)? // ([^:/]+) (:\d*)? / ///, '/'
+
+    # decodeURI has special handling of stuff like semicolons, so use decodeURIComponent
+    path = decodeURIComponent(path)
+
+    if typeof @options.pathRewriteFn == 'function'
+      path = @options.pathRewriteFn(path)
+
+    return path

Example usage:

window.LiveReloadOptions = {
    host: 'localhost',
    pathRewriteFn: function(path) {
        return path.replace(/\.[0-9a-f]{8}\./, '.');
    }
};

What do you think?