Add bin/djot-watch live-preview CLI#178
Merged
Merged
Conversation
Document wrapper provides title, stylesheet link, and livereload script hook. Bin entry stub registered in composer.json so vendor/bin/djot-watch becomes available once the watcher class is implemented.
SseChannel uses a shared file as the IPC channel between the long-lived watcher process and the short-lived PHP -S router handler. FileWatcher detects mtime changes via polling; the inotify optimisation lands later.
- PHP's filemtime() is second-resolution; rapid same-second re-saves with different content would not register. Adding filesize to the fingerprint catches the common workflow case. - Watcher stub so bin/djot-watch loads cleanly before the full controller lands in the next commit; printing a helpful 'not yet implemented' message beats a fatal Class-not-found.
Adds Server, router, livereload client, default stylesheet, and the real Watcher controller (replacing the stub). The Watcher polls the target file via FileWatcher, bumps SseChannel on change, and the router's /__sse handler pushes 'reload' events to connected browsers which then call location.reload(). Notes: - router.php's SSE loop checks connection_aborted() each iteration so it exits as soon as the browser disconnects; otherwise php -S would hold the worker for the full 25s long-poll window. - The browser is launched via OS-aware command (xdg-open/open/start). - Cross-platform: inotify is not used yet; polling-only is portable enough for v1. - Docs added under docs/reference/cli.md and a README line item.
- Server: redirect php -S stdio to /dev/null. The dev server writes a request log line per hit; on a pipe whose reader nobody owns, the buffer fills (~64KB) after a few thousand requests and php -S blocks on write. Discard the stream since we never surface it anyway. - FileWatcher: add a content-hash component to the fingerprint. A typo fix that preserves file length and lands within the same one-second mtime bucket would have been invisible to the prior (mtime, size) tuple. xxh32 if available, md5 fallback.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #178 +/- ##
============================================
- Coverage 93.51% 91.07% -2.45%
- Complexity 3242 3301 +59
============================================
Files 93 99 +6
Lines 8207 8472 +265
============================================
+ Hits 7675 7716 +41
- Misses 532 756 +224 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Master's CI has been red since 615c640 with two phpcs sniff failures on the new anonymous-class spacing rule (PSR12 + Universal). Without this one-character fix every PR inherits the red status — even though the violation is unrelated to the change under review. `new class('heading-ref')` -> `new class ('heading-ref')`.
Two changes to the SSE handler in router.php: 1. Strip all output buffers at entry and force implicit flush. php -S tends to hold bytes in PHP's own buffers; with output_buffering on the SSE stream surfaces only when the script ends, missing the live-reload window entirely. Now every echo is wire-immediate. 2. Send 2 KiB of comment padding before the first event. Chrome and its forks defer EventSource parsing until they have ~1 KiB+ of body bytes — without padding, the very first `event: reload` is often swallowed because the browser hasn't started parsing yet. Also swapped the post-reload `: tick` for a periodic `: ping` in the no-change branch. That keeps the connection warm AND gives PHP a chance to update connection_aborted() each iteration (per docs: status is only refreshed on write attempts).
Without explicit Cache-Control, Chrome can heuristically cache /__assets/livereload.js across sessions. After the script is updated server-side, a returning browser keeps using its cached copy — listeners that didn't exist in the old version (like 'reload') never fire, and the live-reload appears broken even though the server is emitting events correctly. Same fix for the rendered HTML at `/`: live preview regenerates the body each request, so any cache hit would defeat the loop. style.css gets the same treatment for consistency, since the default stylesheet does evolve and a --css override can change per session.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
bin/djot-watch, a long-running CLI that serves live-reloading HTML preview of a djot file in the browser. Companion to the existingbin/djotone-shot converter:bin/djot— render-once for build pipelines and scriptsbin/djot-watch— iterate while authoring; the browser tab reloads on every saveEditor-agnostic — works the same from Vim, Helix, VS Code, Sublime, Zed, or just a terminal. Designed to pair with
php-collective/zed-djotfor in-editor authoring.What's new
vendor/bin/djot-watch path/to/file.djot(flags:--port,--host,--no-open,--css,--version,--help).Djot\Watch\Renderer— wrapsDjotConverterand emits a full HTML document with a default stylesheet and a livereload<script>tag.Djot\Watch\FileWatcher— fingerprints tracked files via(mtime, size, content-hash)so same-second / same-size re-saves still trigger refresh.Djot\Watch\SseChannel— shared-file IPC between the long-lived watcher process and the short-livedphp -Srouter worker handling each request.Djot\Watch\Server— spawnsphp -Swith stdio redirected to/dev/null(so the dev-server's request log doesn't fill its pipe buffer) and a port-bump-on-collision fallback (8765 → 8774).src/Watch/router.php— runs insidephp -S; serves/(rendered HTML),/__sse(Server-Sent Events withconnection_aborted()checked each iteration so workers free up when browsers disconnect), and the bundled assets.src/Watch/assets/livereload.js+style.css— minimal browser-side client and a dark-mode-aware default stylesheet.Djot\Watch\Watcher— controller that ties it all together: arg parsing, PCNTL-based clean SIGINT/SIGTERM handling, OS-aware browser launch (xdg-open / open / start).How it works
php -Son the chosen port with a tiny router script and a few env vars (target path, SSE state file, optional CSS override).(mtime, size, hash)change, bumps the SSE sequence file.reloadevent,location.reload().Ctrl+Cshuts the server down cleanly.No daemon, no config files, no globals — just the binary and your file. Single-user localhost preview by design.
Tests
Renderer,SseChannel,FileWatcher(3 + 3 + 4 = 10 unit tests).tests/Watch/IntegrationTest.php): spawns the binary, hits/with curl, modifies the watched file, asserts an SSEreloadevent arrives within 5 s. Adds 1 test.ownsuite green: 1889 tests, 5069 assertions.src/Watch/andtests/Watch/.Docs
docs/reference/cli.mdgains a "Live Preview:djot-watch" section covering install, usage, flags, default styling, editor integration, and how it works internally.README.mdmentions both CLIs in the Features list and links to the CLI reference.