Skip to content

Commit 3c6f5f6

Browse files
st0012kou
andauthored
Add server mode with live reload (rdoc --server) (ruby#1620)
A better attempt of ruby#1151 Implement `rdoc --server[=PORT]` for previewing documentation with automatic browser refresh on source file changes. The server parses all sources on startup, serves pages from memory via the Aliki generator, and watches for file modifications, additions, and deletions — re-parsing only what changed. https://github.com/user-attachments/assets/06d5ae7b-cc54-4cc5-a8b0-1cc30177cadb ## Changes - **New `RDoc::Server`** (`lib/rdoc/server.rb`) — minimal HTTP server using Ruby's built-in `TCPServer` (no WEBrick or external dependencies) - Thread-per-connection with `Connection: close` - Persistent Aliki generator instance rendering pages to strings - In-memory page cache with full invalidation on changes - Live reload via inline JS polling `/__status` endpoint every 1 second - Background file watcher polling source file mtimes every 1 second - Incremental re-parse: only changed files are re-parsed, old data removed first - Detection of new and deleted source files - **`--server[=PORT]`** CLI option (default port 4000) and `rdoc:server` Rake task - **`RDoc::Store#remove_file`** — removes a file's entries from the store hashes - **`RDoc::Store#clear_file_contributions`** — surgically removes a file's methods, constants, comments, includes, extends, and aliases from its classes/modules, preserving classes that span multiple files. Supports `keep_position:` for server re-parse to preserve comment ordering. - **`RDoc::RDoc#relative_path_for`** — extracted path normalization (against `options.root` and `options.page_dir`) shared by `parse_file` and the server - **`Darkfish#refresh_store_data`** — extracted for reuse by the server after re-parsing - **`RDoc::Servlet` → `RDoc::RI::Servlet`** — moved to clarify RI-specific usage ## Security & robustness - Binds to `127.0.0.1` only (localhost) - Path traversal protection in asset serving (`File.expand_path` containment check) - Proper HTTP error responses: 400, 404, 405, 500 - 5-second `IO.select` read timeout on client sockets - Mutex protects all store mutations, generator refresh, and cache invalidation atomically - Individual `parse_file` errors rescued so one failure doesn't block remaining files - Watcher thread uses `@running` flag with clean shutdown via `Thread#join` ## Known limitations - **Full cache invalidation:** any file change clears all cached pages. Rendering is fast (~ms per page); parsing is the expensive part and is done incrementally. - **Template/CSS changes:** require server restart (only source files are watched). --------- Co-authored-by: Sutou Kouhei <kou@clear-code.com>
1 parent e2e563b commit 3c6f5f6

File tree

16 files changed

+1080
-98
lines changed

16 files changed

+1080
-98
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
name: test-server
3+
description: E2E testing workflow for the RDoc live-reload server (rdoc --server)
4+
---
5+
6+
# Test Server
7+
8+
End-to-end testing workflow for the RDoc live-reload server. Use after modifying server code, templates, generators, or routing.
9+
10+
## Steps
11+
12+
### 1. Start the server
13+
14+
```bash
15+
bundle exec rdoc --server &
16+
SERVER_PID=$!
17+
sleep 2 # wait for TCP server to bind
18+
```
19+
20+
Or on a custom port:
21+
22+
```bash
23+
bundle exec rdoc --server=8080 &
24+
```
25+
26+
Default port is 4000.
27+
28+
### 2. Verify core endpoints
29+
30+
Run these curl checks against the running server:
31+
32+
```bash
33+
# Root → 200, HTML index page
34+
curl -s -o /dev/null -w '%{http_code}' http://localhost:4000/
35+
# Expected: 200
36+
37+
# Status endpoint → 200, JSON with last_change float
38+
curl -s http://localhost:4000/__status
39+
# Expected: {"last_change":1234567890.123}
40+
41+
# Class page → 200, HTML with live-reload script
42+
curl -s http://localhost:4000/RDoc.html | head -5
43+
# Expected: HTML containing class documentation
44+
45+
# CSS asset → 200, stylesheet
46+
curl -s -o /dev/null -w '%{http_code}' http://localhost:4000/css/rdoc.css
47+
# Expected: 200
48+
49+
# JS search index → 200, search data
50+
curl -s -o /dev/null -w '%{http_code}' http://localhost:4000/js/search_data.js
51+
# Expected: 200
52+
53+
# Missing page → 404, still has live-reload script
54+
curl -s -w '\n%{http_code}' http://localhost:4000/Missing.html | tail -1
55+
# Expected: 404
56+
57+
# Path traversal via asset route → 404 (blocked by expand_path check)
58+
curl -s -o /dev/null -w '%{http_code}' 'http://localhost:4000/css/../../etc/passwd'
59+
# Expected: 404
60+
```
61+
62+
### 3. Verify live-reload
63+
64+
HTML pages should contain the live-reload polling script:
65+
66+
```bash
67+
# Check for live-reload script in a class page
68+
curl -s http://localhost:4000/RDoc.html | grep 'var lastChange'
69+
# Expected: var lastChange = <float>;
70+
71+
# Check that 404 pages also get live-reload
72+
curl -s http://localhost:4000/Missing.html | grep 'var lastChange'
73+
# Expected: var lastChange = <float>;
74+
```
75+
76+
The script polls `/__status` and reloads when `data.last_change > lastChange`.
77+
78+
### 4. Verify file change detection
79+
80+
Confirm the server detects source file changes and invalidates its cache:
81+
82+
```bash
83+
# Record the current last_change timestamp
84+
BEFORE=$(curl -s http://localhost:4000/__status | grep -o '"last_change":[0-9.]*' | cut -d: -f2)
85+
86+
# Touch a source file to trigger the file watcher
87+
touch lib/rdoc.rb
88+
sleep 2 # watcher polls every 1 second
89+
90+
# Check that last_change has advanced
91+
AFTER=$(curl -s http://localhost:4000/__status | grep -o '"last_change":[0-9.]*' | cut -d: -f2)
92+
echo "before=$BEFORE after=$AFTER"
93+
# Expected: AFTER > BEFORE
94+
```
95+
96+
### 5. (Optional) Visual testing with Playwright CLI
97+
98+
For visual inspection of rendered pages, use Playwright CLI commands directly:
99+
100+
```bash
101+
# Install browsers (one-time)
102+
npx playwright install chromium
103+
104+
# Take a screenshot of the index page
105+
npx playwright screenshot http://localhost:4000/ /tmp/rdoc-index.png
106+
107+
# Take a screenshot of a specific class page
108+
npx playwright screenshot http://localhost:4000/RDoc.html /tmp/rdoc-class.png
109+
110+
# Full-page screenshot
111+
npx playwright screenshot --full-page http://localhost:4000/RDoc.html /tmp/rdoc-full.png
112+
```
113+
114+
Review the screenshots to verify layout, styling, and content rendering.
115+
116+
### 6. Stop the server
117+
118+
```bash
119+
kill $SERVER_PID 2>/dev/null
120+
```

AGENTS.md

Lines changed: 58 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ bundle exec rake rerdoc
125125
# Show documentation coverage
126126
bundle exec rake rdoc:coverage
127127
bundle exec rake coverage
128+
129+
# Start live-reloading preview server (port 4000)
130+
bundle exec rake rdoc:server
131+
132+
# Or via CLI with custom port
133+
bundle exec rdoc --server=8080
128134
```
129135

130136
**Output Directory:** `_site/` (GitHub Pages compatible)
@@ -176,6 +182,7 @@ lib/rdoc/
176182
│ ├── c.rb # C extension parser
177183
│ ├── prism_ruby.rb # Prism-based Ruby parser
178184
│ └── ...
185+
├── server.rb # Live-reloading preview server (rdoc --server)
179186
├── generator/ # Documentation generators
180187
│ ├── aliki.rb # HTML generator (default theme)
181188
│ ├── darkfish.rb # HTML generator (deprecated, will be removed in v8.0)
@@ -232,6 +239,30 @@ exe/
232239
- **Parsers:** Ruby, C, Markdown, RD, Prism-based Ruby (experimental)
233240
- **Generators:** HTML/Aliki (default), HTML/Darkfish (deprecated), RI, POT (gettext), JSON, Markup
234241

242+
### Live Preview Server (`RDoc::Server`)
243+
244+
The server (`lib/rdoc/server.rb`) provides `rdoc --server` for live documentation preview.
245+
246+
**Architecture:**
247+
- Uses Ruby's built-in `TCPServer` (`socket` stdlib) — no WEBrick or external dependencies
248+
- Creates a persistent `RDoc::Generator::Aliki` instance with `file_output = false` (renders to strings)
249+
- Thread-per-connection HTTP handling with `Connection: close` (no keep-alive)
250+
- Background watcher thread polls file mtimes every 1 second
251+
- Live reload via inline JS polling `/__status` endpoint
252+
253+
**Key files:**
254+
- `lib/rdoc/server.rb` — HTTP server, routing, caching, file watcher
255+
- `lib/rdoc/rdoc.rb``start_server` method, server branch in `document`
256+
- `lib/rdoc/options.rb``--server[=PORT]` option
257+
- `lib/rdoc/generator/darkfish.rb``refresh_store_data` (extracted for server reuse)
258+
- `lib/rdoc/store.rb``remove_file` (for deleted file handling)
259+
- `lib/rdoc/task.rb``rdoc:server` Rake task
260+
261+
**Known limitations:**
262+
- Reopened classes: deleting a file that partially defines a class removes the entire class from the store (save the other file to restore)
263+
- Template/CSS changes require server restart (only source files are watched)
264+
- Full page cache invalidation on any change (rendering is fast, so this is acceptable)
265+
235266
## Common Workflows
236267

237268
Do NOT commit anything. Ask the developer to review the changes after tasks are finished.
@@ -319,20 +350,36 @@ When editing markup reference documentation, such as `doc/markup_reference/markd
319350

320351
When making changes to theme CSS or templates (e.g., Darkfish or Aliki themes):
321352

322-
1. **Generate documentation**: Run `bundle exec rake rerdoc` to create baseline
323-
2. **Start HTTP server**: Run `cd _site && python3 -m http.server 8000` (use different port if 8000 is in use)
324-
3. **Investigate with Playwright**: Ask the AI assistant to take screenshots and inspect the documentation visually
325-
- Example: "Navigate to the docs at localhost:8000 and screenshot the RDoc module page"
326-
- See "Playwright MCP for Testing Generated Documentation" section below for details
327-
4. **Make changes**: Edit files in `lib/rdoc/generator/template/<theme>/` as needed
328-
5. **Regenerate**: Run `bundle exec rake rerdoc` to rebuild documentation with changes
329-
6. **Verify with Playwright**: Take new screenshots and compare to original issues
330-
7. **Lint changes** (if modified):
353+
1. **Start the live-reloading server**: Run `bundle exec rdoc --server` (or `bundle exec rake rdoc:server`)
354+
2. **Make changes**: Edit files in `lib/rdoc/generator/template/<theme>/` or source code
355+
3. **Browser auto-refreshes**: The server detects file changes and refreshes the browser automatically
356+
4. **Verify with `/test-server`**: Use the test-server skill for endpoint checks, live-reload verification, and optional Playwright screenshots
357+
5. **Lint changes** (if modified):
331358
- ERB templates: `npx @herb-tools/linter "lib/rdoc/generator/template/**/*.rhtml"`
332359
- CSS files: `npm run lint:css -- --fix`
333-
8. **Stop server**: Kill the HTTP server process when done
334360

335-
**Tip:** Keep HTTP server running during iteration. Just regenerate with `bundle exec rake rerdoc` between changes.
361+
**Note:** The server watches source files, not template files. If you modify `.rhtml` templates or CSS in the template directory, restart the server to pick up those changes.
362+
363+
## Visual Testing with Playwright CLI
364+
365+
Use `npx playwright` to take screenshots of generated documentation — works with both the live-reload server and static `_site/` output.
366+
367+
```bash
368+
# Install browsers (one-time)
369+
npx playwright install chromium
370+
371+
# Screenshot a live server page
372+
npx playwright screenshot http://localhost:4000/RDoc.html /tmp/rdoc-class.png
373+
374+
# Screenshot static output (start a file server first)
375+
cd _site && python3 -m http.server 8000 &
376+
npx playwright screenshot http://localhost:8000/index.html /tmp/rdoc-index.png
377+
378+
# Full-page screenshot
379+
npx playwright screenshot --full-page http://localhost:4000/RDoc.html /tmp/rdoc-full.png
380+
```
381+
382+
For server-specific E2E testing (endpoint checks, live-reload verification, file change detection), use the `/test-server` skill.
336383

337384
## Notes for AI Agents
338385

@@ -345,64 +392,3 @@ When making changes to theme CSS or templates (e.g., Darkfish or Aliki themes):
345392
4. **Use `rake rerdoc`** to regenerate documentation (not just `rdoc`)
346393
5. **Verify generated files** with `rake verify_generated`
347394
6. **Don't edit generated files** directly (in `lib/rdoc/markdown/` and `lib/rdoc/rd/`)
348-
349-
## Playwright MCP for Testing Generated Documentation
350-
351-
The Playwright MCP server enables visual inspection and interaction with generated HTML documentation. This is useful for verifying CSS styling, layout issues, and overall appearance.
352-
353-
**MCP Server:** `@playwright/mcp` (Microsoft's official browser automation server)
354-
355-
### Setup
356-
357-
The Playwright MCP server can be used with any MCP-compatible AI tool (Claude Code, Cursor, GitHub Copilot, OpenAI Agents, etc.).
358-
359-
**Claude Code:**
360-
361-
```bash
362-
/plugin playwright
363-
```
364-
365-
**Other MCP-compatible tools:**
366-
367-
```bash
368-
npx @playwright/mcp@latest
369-
```
370-
371-
Configure your tool to connect to this MCP server. Playwright launches its own browser instance automatically - no manual browser setup or extensions required.
372-
373-
### Troubleshooting: Chrome Remote Debugging Blocked
374-
375-
If you encounter `DevTools remote debugging is disallowed by the system admin`, Chrome's debugging is blocked by the machine's policy. Use Firefox instead:
376-
377-
```bash
378-
# Install Firefox for Playwright
379-
npx playwright install firefox
380-
381-
# Add Playwright MCP with Firefox to your project (creates/updates .mcp.json)
382-
claude mcp add playwright --scope project -- npx -y @playwright/mcp@latest --browser firefox
383-
```
384-
385-
Restart Claude Code after running these commands.
386-
387-
### Testing Generated Documentation
388-
389-
To test the generated documentation:
390-
391-
```bash
392-
# Generate documentation
393-
bundle exec rake rerdoc
394-
395-
# Start a simple HTTP server in the _site directory (use an available port)
396-
cd _site && python3 -m http.server 8000
397-
```
398-
399-
If port 8000 is already in use, try another port (e.g., `python3 -m http.server 9000`).
400-
401-
Then ask the AI assistant to inspect the documentation. It will use the appropriate Playwright tools (`browser_navigate`, `browser_snapshot`, `browser_take_screenshot`, etc.) based on your request.
402-
403-
**Example requests:**
404-
405-
- "Navigate to `http://localhost:8000` and take a screenshot"
406-
- "Take a screenshot of the RDoc module page"
407-
- "Check if code blocks are rendering properly on the Markup page"
408-
- "Compare the index page before and after my CSS changes"

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,41 @@ There are also a few community-maintained themes for RDoc:
180180
181181
Please follow the theme's README for usage instructions.
182182
183+
## Live Preview Server
184+
185+
RDoc includes a built-in server for previewing documentation while you edit source files. It parses your code once on startup, then watches for changes and auto-refreshes the browser.
186+
187+
```shell
188+
rdoc --server
189+
```
190+
191+
This starts a server at `http://localhost:4000`. You can specify a different port:
192+
193+
```shell
194+
rdoc --server=8080
195+
```
196+
197+
Or use the Rake task:
198+
199+
```shell
200+
rake rdoc:server
201+
```
202+
203+
### How It Works
204+
205+
- Parses all source files on startup and serves pages from memory using the Aliki theme
206+
- A background thread polls file mtimes every second
207+
- When a file changes, only that file is re-parsed — the browser refreshes automatically
208+
- New files are detected and added; deleted files are removed
209+
210+
**No external dependencies.** The server uses Ruby's built-in `TCPServer` (`socket` stdlib) — no WEBrick or other gems required.
211+
212+
### Limitations
213+
214+
- **Reopened classes and file deletion.** If a class is defined across multiple files (e.g. `Foo` in both `a.rb` and `b.rb`), deleting one file removes the entire class from the store, including parts from the other file. Saving the remaining file triggers a re-parse that restores it.
215+
- **Full cache invalidation.** Any file change clears all cached pages. This is simple and correct — rendering is fast (~ms per page), parsing is the expensive part and is done incrementally.
216+
- **No HTTPS or HTTP/2.** The server is intended for local development preview only.
217+
183218
## Bugs
184219

185220
See [CONTRIBUTING.md](CONTRIBUTING.md) for information on filing a bug report. It's OK to file a bug report for anything you're having a problem with. If you can't figure out how to make RDoc produce the output you like that is probably a documentation bug.

lib/rdoc.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def self.home
160160
autoload :Generator, "#{__dir__}/rdoc/generator"
161161
autoload :Options, "#{__dir__}/rdoc/options"
162162
autoload :Parser, "#{__dir__}/rdoc/parser"
163-
autoload :Servlet, "#{__dir__}/rdoc/servlet"
163+
autoload :Server, "#{__dir__}/rdoc/server"
164164
autoload :RI, "#{__dir__}/rdoc/ri"
165165
autoload :Stats, "#{__dir__}/rdoc/stats"
166166
autoload :Store, "#{__dir__}/rdoc/store"

lib/rdoc/code_object/class_module.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,18 @@ def search_snippet
747747
snippet(first_comment)
748748
end
749749

750+
##
751+
# Rebuilds +@comment+ from the current +@comment_location+ entries,
752+
# skipping any empty placeholders.
753+
754+
def rebuild_comment_from_location
755+
texts = @comment_location.each_value.flat_map { |comments|
756+
comments.filter_map { |c| c.to_s unless c.empty? }
757+
}
758+
merged = texts.join("\n---\n")
759+
@comment = merged.empty? ? '' : RDoc::Comment.new(merged)
760+
end
761+
750762
##
751763
# Sets the store for this class or module and its contained code objects.
752764

lib/rdoc/generator/darkfish.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,15 @@ def setup
577577

578578
return unless @store
579579

580+
refresh_store_data
581+
end
582+
583+
##
584+
# Refreshes the generator's data from the store. Called by #setup and
585+
# can be called again after the store has been updated (e.g. in server
586+
# mode after re-parsing changed files).
587+
588+
def refresh_store_data
580589
@classes = @store.all_classes_and_modules.sort
581590
@files = @store.all_files.sort
582591
@methods = @classes.flat_map { |m| m.method_list }.sort

0 commit comments

Comments
 (0)