Skip to content

Commit 2eac59c

Browse files
refactored server code as sub package
1 parent c958287 commit 2eac59c

File tree

7 files changed

+261
-217
lines changed

7 files changed

+261
-217
lines changed

syrinx/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def main():
5757
config = configure(args)
5858

5959
if args.command == 'serve':
60-
from syrinx.dev_server import DevServer
60+
from syrinx.server.dev_server import DevServer
6161
server = DevServer(root_dir, port=args.port)
6262
server.start()
6363
return

syrinx/dev_server.py

Lines changed: 0 additions & 216 deletions
This file was deleted.

syrinx/server/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Syrinx development server package.
2+
3+
This subpackage provides development server functionality including
4+
file watching, live reload, and HTTP serving for Syrinx projects.
5+
"""

syrinx/server/dev_server.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Development server module for Syrinx with file watching and live reload.
2+
3+
This module provides a development server that watches for file changes in the
4+
parent directory hierarchy and automatically rebuilds the site when changes are
5+
detected. It includes an HTTP server to serve the built files.
6+
"""
7+
8+
import os
9+
from socketserver import TCPServer
10+
from watchdog.observers import Observer
11+
from syrinx.build import build
12+
from syrinx.read import read
13+
from syrinx.preprocess import preprocess
14+
from syrinx.server.hot_reload_handler import HotReloadHandler
15+
from syrinx.server.rebuild_handler import RebuildHandler
16+
17+
18+
class DevServer:
19+
"""Development server with file watching and HTTP serving.
20+
21+
Provides a complete development environment for Syrinx projects by:
22+
- Watching parent directories for file changes
23+
- Automatically rebuilding on changes
24+
- Serving the built files via HTTP
25+
26+
Attributes:
27+
root_dir: The absolute path to the Syrinx project root.
28+
port: The port number for the HTTP server.
29+
reload_version: Version counter for triggering reloads.
30+
"""
31+
def __init__(self, root_dir, port=8000):
32+
self.root_dir = os.path.abspath(root_dir)
33+
self.port = port
34+
self.reload_version = 0
35+
36+
def trigger_reload(self):
37+
"""Increment reload version to trigger browser reloads."""
38+
self.reload_version += 1
39+
40+
def start(self):
41+
"""Start the development server.
42+
43+
Performs an initial build, sets up file watching on the parent
44+
directory, and starts an HTTP server to serve the dist directory.
45+
The server runs until interrupted with Ctrl+C.
46+
"""
47+
# Determine parent directory to watch
48+
watch_dir = os.path.dirname(self.root_dir)
49+
print(f"[DEV] Watching for changes in: {watch_dir}")
50+
print(f"[DEV] Building from: {self.root_dir}")
51+
52+
# Initial build
53+
print("[DEV] Initial build...")
54+
preprocess(self.root_dir, clean=False)
55+
root = read(self.root_dir)
56+
build(root, self.root_dir)
57+
print("[DEV] Initial build complete!")
58+
59+
# Setup file watcher with reload callback
60+
event_handler = RebuildHandler(self.root_dir, watch_dir, self.trigger_reload)
61+
observer = Observer()
62+
observer.schedule(event_handler, watch_dir, recursive=True)
63+
observer.start()
64+
65+
# Setup HTTP server
66+
dist_dir = os.path.join(self.root_dir, 'dist')
67+
68+
# Store original directory to restore later
69+
original_dir = os.getcwd()
70+
71+
# Configure the handler class attributes
72+
HotReloadHandler.dev_server = self
73+
HotReloadHandler.dist_dir = dist_dir
74+
75+
try:
76+
with TCPServer(("", self.port), HotReloadHandler) as httpd:
77+
print(f"\n[DEV] Server running at http://localhost:{self.port}")
78+
print("[DEV] Press Ctrl+C to stop\n")
79+
httpd.serve_forever()
80+
except KeyboardInterrupt:
81+
print("\n[DEV] Shutting down...")
82+
observer.stop()
83+
finally:
84+
observer.join()
85+
os.chdir(original_dir)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""HTTP handler for development server with live reload functionality."""
2+
3+
import os
4+
import json
5+
from http.server import SimpleHTTPRequestHandler
6+
7+
8+
class HotReloadHandler(SimpleHTTPRequestHandler):
9+
"""Custom HTTP handler that injects live reload script into HTML pages.
10+
11+
This handler serves files from a specified directory and automatically
12+
injects a live reload script into HTML pages to enable automatic page
13+
refreshes during development.
14+
15+
Attributes:
16+
dev_server: Reference to the DevServer instance for reload version.
17+
dist_dir: Directory to serve files from.
18+
"""
19+
20+
dev_server: 'DevServer' = None # type: ignore
21+
dist_dir: str = None # type: ignore
22+
reload_script_content: str = None # type: ignore
23+
24+
def __init__(self, *args, **kwargs):
25+
super().__init__(*args, directory=self.dist_dir, **kwargs)
26+
27+
# Read reload script once during initialization
28+
if self.reload_script_content is None:
29+
reload_js_path = os.path.join(os.path.dirname(__file__), 'reload_outdated.js')
30+
with open(reload_js_path, 'r', encoding='utf-8') as f:
31+
HotReloadHandler.reload_script_content = f.read()
32+
33+
def do_GET(self):
34+
"""Handle GET requests with reload script injection for HTML files."""
35+
# Handle reload check endpoint
36+
if self.path == '/__dev_reload_check__':
37+
self.send_response(200)
38+
self.send_header('Content-Type', 'application/json')
39+
self.send_header('Cache-Control', 'no-cache')
40+
self.end_headers()
41+
self.wfile.write(json.dumps({'version': self.dev_server.reload_version}).encode())
42+
return
43+
44+
# For HTML files, inject reload script
45+
if self.path.endswith('.html') or self.path == '/':
46+
file_path = os.path.join(self.dist_dir, self.path.lstrip('/'))
47+
if self.path == '/':
48+
file_path = os.path.join(self.dist_dir, 'index.html')
49+
50+
if os.path.exists(file_path):
51+
with open(file_path, 'r', encoding='utf-8') as f:
52+
content = f.read()
53+
54+
# Use cached reload script and replace version placeholder
55+
reload_script_content = self.reload_script_content.replace(
56+
'__CURRENT_VERSION__',
57+
str(self.dev_server.reload_version)
58+
)
59+
60+
reload_script = f'<script>\n{reload_script_content}\n</script>\n</body>'
61+
content = content.replace('</body>', reload_script)
62+
63+
self.send_response(200)
64+
self.send_header('Content-Type', 'text/html')
65+
self.send_header('Content-Length', str(len(content.encode())))
66+
self.end_headers()
67+
self.wfile.write(content.encode())
68+
return
69+
70+
# Default behavior for other files
71+
super().do_GET()

0 commit comments

Comments
 (0)