diff --git a/htmd/cli.py b/htmd/cli.py index 445f267..5c06305 100644 --- a/htmd/cli.py +++ b/htmd/cli.py @@ -2,11 +2,14 @@ import importlib from pathlib import Path import sys +import threading import warnings import click from flask import Flask from flask_flatpages import FlatPages, Page +from watchdog.observers import Observer +from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEventHandler from .utils import ( combine_and_minify_css, @@ -202,6 +205,24 @@ def build( click.echo(click.style(msg, fg='green')) +class StaticHandler(FileSystemEventHandler): + def __init__(self, static_directory: Path) -> None: + super().__init__() + self.static_directory = static_directory + + def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None: + dst_css = 'combined.min.css' + dst_js = 'combined.min.js' + if event.is_directory or dst_css in event.src_path or dst_js in event.src_path: + return + if event.event_type == 'modified' and event.src_path.endswith('.css'): + click.echo(f'Changes detected in {event.src_path}. Recreating {dst_css}...') + combine_and_minify_css(self.static_directory) + elif event.event_type == 'modified' and event.src_path.endswith('.js'): + click.echo(f'Changes detected in {event.src_path}. Recreating {dst_js}...') + combine_and_minify_js(self.static_directory) + + @cli.command('preview', short_help='Serve files to preview site.') @click.pass_context @click.option( @@ -245,13 +266,34 @@ def preview( assert app.static_folder is not None combine_and_minify_js(Path(app.static_folder)) + def watch_static() -> None: + static_directory = Path(app.static_folder) + + event_handler = StaticHandler(static_directory) + observer = Observer() + observer.schedule(event_handler, path=static_directory, recursive=True) + observer.start() + + try: + while not exit_event.is_set(): + observer.join(1) + finally: + observer.stop() + observer.join() + + watch_thread = threading.Thread(target=watch_static) + watch_thread.start() + # reload when static files change # werkzeug will re-run the terminal command # Which causes the above combine_and_minify_*() to run # and recreate combined.min.css/combined.min.js files static_path = site.project_dir / 'static' extra_files = static_path.iterdir() + exit_event = threading.Event() app.run(debug=True, host=host, port=port, extra_files=extra_files) + # After Flask has been stopped stop watchdog + exit_event.set() @cli.command('templates', short_help='Create any missing templates') diff --git a/pyproject.toml b/pyproject.toml index 54e599c..1f303de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "jsmin", "Pygments", "requests", + "watchdog", ] description = "Write Markdown and Jinja2 templates to create a website" maintainers = [ @@ -67,6 +68,7 @@ order-by-type = false section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] [tool.ruff.lint.per-file-ignores] +"htmd/cli.py" = ["I001"] "tests/test_app.py" = ["ARG001"] "tests/test_build.py" = ["I001"] "tests/test_drafts.py" = ["ARG001", "I001"] diff --git a/tests/test_preview.py b/tests/test_preview.py index 48db40f..e0ae0b5 100644 --- a/tests/test_preview.py +++ b/tests/test_preview.py @@ -1,6 +1,7 @@ from pathlib import Path import time from types import TracebackType +import urllib3 from click.testing import CliRunner from htmd.cli import preview @@ -182,7 +183,7 @@ def test_preview_reload_js(run_start: CliRunner) -> None: # noqa: ARG001 while after == before: try: response = requests.get(url, timeout=0.1) - except requests.exceptions.ReadTimeout: + except (requests.exceptions.ReadTimeout, urllib3.exceptions.IncompleteRead): # happens during restart read_timeout = True else: @@ -191,3 +192,34 @@ def test_preview_reload_js(run_start: CliRunner) -> None: # noqa: ARG001 assert read_timeout assert before != after assert expected in after + + +def test_preview_reload_js_new_file(run_start: CliRunner) -> None: # noqa: ARG001 + url = 'http://localhost:9090/static/combined.min.js' + new_js = 'document.getElementByTagName("body");' + expected = new_js + + with run_preview(): + response = requests.get(url, timeout=0.01) + assert response.status_code == 404 + before = response.text + # Need to create before running preview since no .js files exist + js_path = Path('static') / 'script.js' + with js_path.open('w') as js_file: + js_file.write(new_js) + + # Ensure new style is available after reload + read_timeout = False + after = before + while after == before: + try: + response = requests.get(url, timeout=0.1) + except requests.exceptions.ReadTimeout: + # happens during restart + read_timeout = True + else: + after = response.text + + assert read_timeout is False + assert before != after + assert expected in after