Repository: mkdocs/mkdocs

Issue:
Custom subpath isn't applied for `livereload` feature
If using a `site_url` including a subpath for development (we're using it behind a proxy), this isn't applied when loading the `livereload.js` script, causing the whole auto-reload feature to break.

I expect it to be loaded from `https://test.local/my/subpath/js/livereload.js` if using `https://test.local/my/subpath` as `site_url`.


Comments:

Patch:
diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py
index deef55ba..37f4acc7 100644
--- a/mkdocs/livereload/__init__.py
+++ b/mkdocs/livereload/__init__.py
@@ -8,6 +8,7 @@ import pathlib
 import posixpath
 import re
 import socketserver
+import string
 import threading
 import time
 import warnings
@@ -16,6 +17,30 @@ import wsgiref.simple_server
 import watchdog.events
 import watchdog.observers.polling
 
+_SCRIPT_TEMPLATE = """
+var livereload = function(epoch, requestId) {
+    var req = new XMLHttpRequest();
+    req.onloadend = function() {
+        if (parseFloat(this.responseText) > epoch) {
+            location.reload();
+            return;
+        }
+        var launchNext = livereload.bind(this, epoch, requestId);
+        if (this.status === 200) {
+            launchNext();
+        } else {
+            setTimeout(launchNext, 3000);
+        }
+    };
+    req.open("GET", "${mount_path}livereload/" + epoch + "/" + requestId);
+    req.send();
+
+    console.log('Enabled live reload');
+}
+livereload(${epoch}, ${request_id});
+"""
+_SCRIPT_TEMPLATE = string.Template(_SCRIPT_TEMPLATE)
+
 
 class _LoggerAdapter(logging.LoggerAdapter):
     def process(self, msg, kwargs):
@@ -176,26 +201,25 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
         # https://github.com/bottlepy/bottle/blob/f9b1849db4/bottle.py#L984
         path = environ["PATH_INFO"].encode("latin-1").decode("utf-8", "ignore")
 
-        m = re.fullmatch(r"/livereload/([0-9]+)/[0-9]+", path)
-        if m:
-            epoch = int(m[1])
-            start_response("200 OK", [("Content-Type", "text/plain")])
+        if path.startswith(self.mount_path):
+            rel_file_path = path[len(self.mount_path):]
 
-            def condition():
-                return self._visible_epoch > epoch
+            m = re.fullmatch(r"livereload/([0-9]+)/[0-9]+", rel_file_path)
+            if m:
+                epoch = int(m[1])
+                start_response("200 OK", [("Content-Type", "text/plain")])
+
+                def condition():
+                    return self._visible_epoch > epoch
+
+                with self._epoch_cond:
+                    if not condition():
+                        # Stall the browser, respond as soon as there's something new.
+                        # If there's not, respond anyway after a minute.
+                        self._log_poll_request(environ.get("HTTP_REFERER"), request_id=path)
+                        self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout)
+                    return [b"%d" % self._visible_epoch]
 
-            with self._epoch_cond:
-                if not condition():
-                    # Stall the browser, respond as soon as there's something new.
-                    # If there's not, respond anyway after a minute.
-                    self._log_poll_request(environ.get("HTTP_REFERER"), request_id=path)
-                    self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout)
-                return [b"%d" % self._visible_epoch]
-
-        if path == "/js/livereload.js":
-            file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "livereload.js")
-        elif path.startswith(self.mount_path):
-            rel_file_path = path[len(self.mount_path):]
             if path.endswith("/"):
                 rel_file_path += "index.html"
             # Prevent directory traversal - normalize the path.
@@ -235,17 +259,20 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe
         )
         return wsgiref.util.FileWrapper(file)
 
-    @classmethod
-    def _inject_js_into_html(cls, content, epoch):
+    def _inject_js_into_html(self, content, epoch):
         try:
             body_end = content.rindex(b"</body>")
         except ValueError:
             body_end = len(content)
         # The page will reload if the livereload poller returns a newer epoch than what it knows.
         # The other timestamp becomes just a unique identifier for the initiating page.
-        return (
-            b'%b<script src="/js/livereload.js"></script><script>livereload(%d, %d);</script>%b'
-            % (content[:body_end], epoch, _timestamp(), content[body_end:])
+        script = _SCRIPT_TEMPLATE.substitute(
+            mount_path=self.mount_path, epoch=epoch, request_id=_timestamp()
+        )
+        return b"%b<script>%b</script>%b" % (
+            content[:body_end],
+            script.encode(),
+            content[body_end:],
         )
 
     @classmethod
diff --git a/mkdocs/livereload/livereload.js b/mkdocs/livereload/livereload.js
deleted file mode 100644
index a0f74365..00000000
--- a/mkdocs/livereload/livereload.js
+++ /dev/null
@@ -1,19 +0,0 @@
-function livereload(epoch, requestId) {
-    var req = new XMLHttpRequest();
-    req.onloadend = function() {
-        if (parseFloat(this.responseText) > epoch) {
-            location.reload();
-            return;
-        }
-        var launchNext = livereload.bind(this, epoch, requestId);
-        if (this.status === 200) {
-            launchNext();
-        } else {
-            setTimeout(launchNext, 3000);
-        }
-    };
-    req.open("GET", "/livereload/" + epoch + "/" + requestId);
-    req.send();
-
-    console.log('Enabled live reload');
-}


Test patch:
diff --git a/mkdocs/tests/livereload_tests.py b/mkdocs/tests/livereload_tests.py
index 6ec81fda..7fca958d 100644
--- a/mkdocs/tests/livereload_tests.py
+++ b/mkdocs/tests/livereload_tests.py
@@ -65,7 +65,7 @@ def do_request(server, content):
 
 
 SCRIPT_REGEX = (
-    r'<script src="/js/livereload.js"></script><script>livereload\([0-9]+, [0-9]+\);</script>'
+    r'<script>[\S\s]+?livereload\([0-9]+, [0-9]+\);\s*</script>'
 )
 
 
@@ -334,23 +334,18 @@ class BuildTests(unittest.TestCase):
             _, output = do_request(server, "GET /%E6%B5%8B%E8%AF%952/index.html")
             self.assertRegex(output, fr"^<body>bbb{SCRIPT_REGEX}</body>$")
 
-    @tempdir()
-    def test_serves_js(self, site_dir):
-        with testing_server(site_dir) as server:
-            for mount_path in "/", "/sub/":
-                server.mount_path = mount_path
-
-                headers, output = do_request(server, "GET /js/livereload.js")
-                self.assertIn("function livereload", output)
-                self.assertEqual(headers["_status"], "200 OK")
-                self.assertEqual(headers.get("content-type"), "application/javascript")
-
     @tempdir()
     def test_serves_polling_instantly(self, site_dir):
         with testing_server(site_dir) as server:
             _, output = do_request(server, "GET /livereload/0/0")
             self.assertTrue(output.isdigit())
 
+    @tempdir()
+    def test_serves_polling_from_mount_path(self, site_dir):
+        with testing_server(site_dir, mount_path="/test/f*o") as server:
+            _, output = do_request(server, "GET /test/f*o/livereload/0/0")
+            self.assertTrue(output.isdigit())
+
     @tempdir()
     @tempdir()
     def test_serves_polling_after_event(self, site_dir, docs_dir):