Rebuild Sphinx documentation on changes, with hot reloading in the browser.
ASGI middleware for injecting WebSocket client code into HTML responses to enable hot reloading functionality. The middleware automatically adds JavaScript code to HTML pages that establishes a WebSocket connection for real-time browser refresh notifications.
ASGI middleware that injects WebSocket client JavaScript into HTML responses.
class JavascriptInjectorMiddleware:
def __init__(self, app: ASGIApp, ws_url: str) -> None:
"""
Initialize middleware with ASGI app and WebSocket URL.
Parameters:
- app: ASGIApp - The ASGI application to wrap
- ws_url: str - WebSocket URL in format "host:port" for client connections
Processing:
- Pre-generates WebSocket client script using web_socket_script()
- Encodes script to UTF-8 bytes for efficient injection
- Stores references for use in request handling
"""
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""
ASGI middleware callable for HTTP requests.
Automatically injects WebSocket JavaScript into HTML responses:
- Only processes HTTP requests (ignores WebSocket, etc.)
- Detects HTML responses by Content-Type header
- Adjusts Content-Length header when script is injected
- Appends script to response body on final chunk
Parameters:
- scope: Scope - ASGI request scope with connection info
- receive: Receive - ASGI receive callable for request messages
- send: Send - ASGI send callable for response messages
Returns:
- None
Behavior:
- Non-HTTP requests: Pass through unchanged
- Non-HTML responses: Pass through unchanged
- HTML responses: Inject WebSocket script at end of body
- Streaming responses: Only inject on final body chunk
"""Generate the JavaScript code that browsers execute to establish WebSocket connections.
def web_socket_script(ws_url: str) -> str:
"""
Generate WebSocket JavaScript code for browser auto-reload.
Creates a complete HTML script block with WebSocket client code that:
- Establishes WebSocket connection to specified URL
- Listens for messages from server
- Triggers browser reload on any message received
Parameters:
- ws_url: str - WebSocket URL in format "host:port"
Returns:
- str - Complete HTML script element with WebSocket client code
Generated Script Behavior:
- Creates WebSocket connection to "ws://{ws_url}/websocket-reload"
- Automatically reloads page when server sends any message
- Handles connection errors gracefully (no error handling shown to user)
"""from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.staticfiles import StaticFiles
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware
# Create ASGI application with middleware
app = Starlette(
middleware=[
Middleware(JavascriptInjectorMiddleware, ws_url="127.0.0.1:8000")
]
)from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount, WebSocketRoute
from starlette.staticfiles import StaticFiles
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware
from sphinx_autobuild.server import RebuildServer
# Setup components
server = RebuildServer(watch_dirs, ignore_filter, builder)
url_host = "127.0.0.1:8000"
# Create app with middleware and routes
app = Starlette(
routes=[
WebSocketRoute("/websocket-reload", server, name="reload"),
Mount("/", app=StaticFiles(directory="_build/html", html=True), name="static"),
],
middleware=[
Middleware(JavascriptInjectorMiddleware, ws_url=url_host)
],
lifespan=server.lifespan,
)from sphinx_autobuild.middleware import web_socket_script
# Generate script for different URLs
local_script = web_socket_script("127.0.0.1:8000")
network_script = web_socket_script("192.168.1.100:8080")
print(local_script)
# Output:
# <script>
# const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
# ws.onmessage = () => window.location.reload();
# </script>
# Use in custom middleware or manual injection
custom_html = f"""
<!DOCTYPE html>
<html>
<head><title>My Docs</title></head>
<body>
<h1>Documentation</h1>
{network_script}
</body>
</html>
"""The middleware intercepts HTTP responses and modifies them:
# The generated script is minimal and efficient:
script_template = """
<script>
const ws = new WebSocket("ws://{ws_url}/websocket-reload");
ws.onmessage = () => window.location.reload();
</script>
"""
# Key characteristics:
# - No error handling (fails silently if WebSocket unavailable)
# - Automatic reconnection on page reload
# - Triggers on any message (server sends "refresh")
# - Minimal performance impactThe middleware properly handles HTTP Content-Length headers:
# When HTML is detected:
if "Content-Length" in headers:
original_length = int(headers["Content-Length"])
new_length = original_length + len(script_bytes)
headers["Content-Length"] = str(new_length)
# This ensures:
# - HTTP/1.1 clients receive correct content length
# - Proxy servers handle responses correctly
# - No truncation of response bodyThe injected JavaScript requires WebSocket support:
// Browser-side connection handling:
const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
// Automatic behaviors:
ws.onopen = () => {
// Connection established - ready for reload messages
};
ws.onmessage = () => {
window.location.reload(); // Refresh entire page
};
ws.onclose = () => {
// Connection lost - will reconnect on next page load
};
ws.onerror = () => {
// Connection error - fails silently, no user notification
};WebSocket connections respect browser security policies:
This middleware is designed for development only:
Production Deployment: Remove middleware from production deployments:
# Development configuration
if DEBUG:
middleware.append(Middleware(JavascriptInjectorMiddleware, ws_url=url_host))
# Or use environment-based configuration
import os
if os.getenv("SPHINX_AUTOBUILD_DEV"):
middleware.append(Middleware(JavascriptInjectorMiddleware, ws_url=url_host))Middleware optimizes by only processing relevant requests:
Install with Tessl CLI
npx tessl i tessl/pypi-sphinx-autobuild