RFD 128: Workspace Source Explorer in jp-vite
Summary
This document extends jp-vite with a hosted workspace source explorer at /source/. Relative links from RFDs, tickets, and wiki pages to repository files and line ranges (such as ../../crates/jp_llm/src/lib.rs#L12-L24) resolve inside the site rather than bouncing out to GitHub, highlight the linked lines with Shiki, and display every RFD, ticket, and wiki page that references that file.
Motivation
RFDs, tickets, and wiki pages frequently link to source files and directories in the workspace using relative paths (for example, ../../crates/plugins/command/rfd/src/links.rs#L88-L125). Today, rewriteRepositoryLinks in jp-vite/config.mjs rewrites any relative link that climbs out of the VitePress source directory (docs/) into an external GitHub URL (${repository}/blob/HEAD/<path>#L88-L125).
That approach has four limitations:
- Broken or stale links on active branches. During local work under
jp vite serve, clicking a link to a file added on the current branch returns a 404 on GitHubHEAD, and clicking a line-range link on a modified file highlights the wrong lines becauseHEADonorigindoes not reflect the local checkout. - No offline or forge-free browsing. Projects without a configured remote, or contributors working offline, cannot follow source links from the site.
- Silent dead links. Because
rewriteRepositoryLinksturns workspace links into external URLs at parse time, VitePress skips them during dead-link checking. Deleting or moving a source file never failsvitepress build. - One-way navigation. Clicking a link to GitHub leaves the site. A contributor inspecting a source file cannot see which RFDs, tickets, or wiki pages discuss or cite that file.
Hosting a lightweight source explorer inside jp-vite solves all four without changing how links are written in markdown.
Design
Overview
When the source explorer is enabled (the default), jp-vite adds a /source/ route that renders workspace files and directories inside the VitePress theme. The dev server always serves it; a static build includes it only when the project chooses to publish its source (see Configuration).
Existing markdown links work unchanged. A document still writes:
markdown[`Links::by_rfd`](../../crates/plugins/command/rfd/src/links.rs#L88-L125)In
$EDITORand on a Git forge, that relative path resolves directly to the file. Injp-vite,rewriteRepositoryLinksrewrites it to/source/?path=crates/plugins/command/rfd/src/links.rs#L88-L125.Line-range highlighting and scrolling. Fragments of the form
#L12,#L12-L24,#L12-24, and#12-24highlight the specified lines and scroll the first highlighted line into view on load. Clicking a line number in the gutter updates the URL hash (#L12), and Shift-clicking selects a range (#L12-L24) for copying into a document.Reverse document backlinks (
Referenced by). Above the file content, the explorer renders<RfdReferences />showing every RFD, ticket, and wiki page that links to this file (or to a parent directory), with the specific line ranges each document cites.External forge link preserved. When
repositoryis configured, the file header includes a secondaryGitHub ↗link pointing to${repository}/blob/HEAD/<path>#L....
Configuration
plugins.command.vite.options gains a source table:
[plugins.command.vite.options]
dir = "docs"
repository = "https://github.com/dcdpr/jp"
# Serve eligible workspace files at `/source/` on the dev server, and route
# source links there. When `false`, source links point at `repository`.
source.enable = true
# Also write eligible files into the static build, publishing them with the
# site. Off by default.
source.publish = true
# Files the explorer never serves or publishes, as globs relative to the
# workspace root.
source.exclude = ["crates/**/fixtures/**"]enable defaults to true, publish to false, and exclude to none. JP sets publish = true, as above.
The two switches split along what each costs. The dev server answers only the contributor running it, so serving source there publishes nothing. A static build is deployed, and git tracking a file says it belongs to the repository, not that it may be read by everyone the documentation reaches. A project with a private repository and public documentation turns on publish deliberately or not at all. With publish off, a static build rewrites source links to repository, as it does today, so following one still needs access to the repository.
jp vite sync records the three settings in docs/.vitepress/jp-vite/settings.json.
Eligible Files
One rule decides which files the explorer can show, and the tree endpoint, the file endpoint, the static emitter, and link rewriting all apply it. A file is eligible when it is:
- Tracked by git (
git ls-files --cached), so untracked secrets,.envfiles, andtarget/artifacts are never candidates, whatever the ignore files say. - Not matched by
source.exclude. The explorer takes no rules from.ignore, whichrgand JP's own file walkers also read: hiding a fixture from the site shouldn't hide it from search. - A regular file, not a symlink, so a tracked link can't serve content from outside the workspace.
- Text: a file with a null byte in its first 8,192 bytes shows a binary-file notice with its size instead.
On the dev server, an eligible file's content is read from the working tree, so local edits show. A file created on a branch appears once it is added to the index.
A directory is eligible when it holds at least one eligible file. A link to a file or directory that exists but isn't eligible is validated like any other source link and points at repository, never at an explorer page the build didn't write.
Architecture: Dev Server vs. Static Build
Generating a separate Vue SSR page per source file would add thousands of pages to vitepress build in medium-sized repositories. Instead, jp-vite uses a single /source/ viewer component (<SourceExplorer />) backed by on-demand loading in dev and static JSON/text chunks in production builds:
Markdown link: ../../crates/jp_llm/src/lib.rs#L12-L24
│
▼
rewriteRepositoryLinks (config.mjs)
├── Verify target path & line range on disk (fails build if missing)
└── Rewrite href -> /source/?path=crates/jp_llm/src/lib.rs#L12-L24
│
▼
<SourceExplorer />
├── Directory tree sidebar (collapsible workspace tree)
├── Header: breadcrumb path + "Referenced by" badges + "GitHub ↗"
└── Code view: line numbers + Shiki syntax highlighting + active #L rangeIn jp vite serve (Dev Server)
middleware.mjs registers /__source:
GET /__source?tree=1returns the tree of eligible files and the map of workspace paths to the RFDs, tickets, and wiki pages that link to them.GET /__source?path=<workspace-relative-path>reads an eligible file from the working tree and highlights it with VitePress's Shiki highlighter, and refuses any other path. Edits made in$EDITORappear immediately on refresh, with current line numbers.
In vitepress build (Static Build)
When source.publish is on, during buildEnd (alongside raw .md copying and RFD redirect generation):
jp-vitewritesoutDir/source-index.json(the file tree and reverse reference map) and copies or pre-highlights eligible files intooutDir/source-files/<path>.json(containing the Shiki HTML or raw lines).<SourceExplorer />fetchessource-index.jsonand the requested file's chunk on navigation, sovitepress buildcompiles only one Vue page (source/index.md) regardless of repository size.
Build-Time Link and Line-Range Validation
Inside rewriteRepositoryLinks (which already runs on every markdown token across the entire VitePress site, including RFDs, tickets, wiki pages, and user docs), a link is classified by its target, not by whether it leaves the site directory:
- A target that is a markdown page of the site, or a file in its
public/directory, is the site's own and stays with VitePress. - Any other relative target that resolves inside the workspace is a source link, whether or not it sits inside
settings.site. A site at the workspace root (dir = ".") has source links like any other.
For every source link:
- If the file or directory does not exist on disk, the build fails, naming the markdown page and the missing target.
- If the link carries a
#L<start>[-L<end>]fragment, the target must be a file and1 <= start <= end <= line_countmust hold, or the build fails. - An eligible target is rewritten to the explorer; any other points at
repository.
Validation runs whatever source.enable and source.publish say: whether a file exists is a different question from whether the site may show it.
Reverse Reference Index
The data loaders for RFDs, tickets, and wiki pages already scan document bodies for cross-document citations (referencedLabels in rfd-shared.mjs). We add a companion pass, referencedFiles(content, pageDir, settings), that extracts every workspace-relative link and its optional #L<start>-L<end> range:
{
"crates/plugins/command/rfd/src/links.rs": [
{ "label": "RFD 116", "path": "/rfd/116-shareable-rfd-ticket-and-site-tooling", "lines": null },
{ "label": "W-0b42fs5", "path": "/wiki/0b42fs5-query-stream-pipeline", "lines": "L88-L125" }
]
}When viewing crates/plugins/command/rfd/src/links.rs in <SourceExplorer />, the header displays these badges using the same status-colored pill style as <RfdReferences />. Clicking a badge with "lines": "L88-L125" or hovering over it shows which lines that document references.
Drawbacks
- Publishing publishes everything eligible. With
source.publishon, the deployed site carries every eligible file, including files no document links to.source.excludeis the only way to keep a tracked file off it. - Static build output size. Copying or pre-highlighting every eligible file into
outDir/source-files/grows the built site with the repository's tracked text (a few megabytes for JP). Projects with large tracked fixtures list them insource.exclude. - New files need
git add. A file created on a branch is missing from the dev explorer until it is added to the index, and a link to it points atrepositoryuntil then. - Query-parameter or hash routing on static hosts. Static hosts like GitHub Pages cannot serve arbitrary
/source/crates/foo/bar.rspaths without a HTML stub per file. Using/source/?path=crates/foo/bar.rs#L12-L24compiles a single/source/index.htmlpage and works on every static host without generating thousands of HTML files.
Alternatives
Generate one VitePress markdown page per source file
Using VitePress dynamic routes (source/[...file].paths.js) gives clean path URLs (/source/crates/jp_llm/src/lib.rs) without query strings. Rejected because VitePress runs full Vue SSR compilation and bundle chunking for every dynamic route, turning a 1,500-file repository into a multi-minute docs build.
Publish by default
One source.enable switch covering both the dev server and the static build is simpler, but makes publishing source the default. A project with a private repository and public documentation would publish its source on its first deploy after upgrading jp-vite, and jp vite sync's confirmation lists the files it writes into the project, not what the next deploy publishes. A disclosure like that can't be taken back, so publishing is opt-in.
Never publish
Serving /source/ only on the dev server avoids the question entirely, but the deployed site loses in-site browsing and Referenced by badges. JP wants both on its own site, so publishing stays available.
Non-Goals
- Editing source code in the browser. The explorer is read-only.
- Git blame or commit history views. The
GitHub ↗header link delegates blame and commit history to the repository's forge. - Semantic code navigation (go-to-definition). Highlighting is lexical via Shiki, not LSP-backed.
- Past revisions. The explorer shows the working tree on the dev server and the built revision on the deployed site. Permalinks to a commit, which RFD 127
Snapshotpages cite, are external links and open on the forge.
Risks and Open Questions
- Links to non-markdown files inside the site. Classifying a target as the site's own assumes VitePress serves what sits in
public/and resolves markdown pages. How it treats a plain link (not an image) to another file inside the source directory, such as[diagram](./x.png), needs checking before that case is left to it or routed to the explorer.
Implementation Plan
Phase 1: Workspace link & line-range validation in jp-vite
Update rewriteRepositoryLinks in crates/plugins/command/vite/site/jp-vite/config.mjs to classify links by target and to verify that source links and their #L<start>-L<end> ranges exist on disk during build and dev rendering.
Source files become inputs to the site build, so a pull request that changes, renames, or deletes a workspace file a document links to must run that build. JP's docs.yml builds a pull request only when it touches docs/, the justfile, or the workflow, so a deleted source file passes review and fails the deployment build on main. The workflow's trigger grows to cover those files: each changed path outside docs/ is looked up in docs/, which finds every relative link ending in it.
Mergeable independently.
Phase 2: <SourceExplorer /> and dev-server /__source endpoint
Add source/index.md to jp vite sync, implement <SourceExplorer /> with the file tree, Shiki highlighting, #L<start>-L<end> highlighting/selection, and reverse Referenced by badges, and add /__source to middleware.mjs.
Depends on Phase 1.
Phase 3: Static build emission (buildEnd)
Emit source-index.json and source-files/ in withJpVite's buildEnd hook when source.publish is on, and point a static build's eligible source links at /source/?path=<path>#L.... Set source.publish = true in JP's .jp/config.toml.
Depends on Phase 2.