Skip to content

fix: faster dev startup on Windows with directory-level symlinks#160

Open
rsbh wants to merge 8 commits into
mainfrom
fix/faster-dev-windows
Open

fix: faster dev startup on Windows with directory-level symlinks#160
rsbh wants to merge 8 commits into
mainfrom
fix/faster-dev-windows

Conversation

@rsbh

@rsbh rsbh commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

On Windows, chronicle dev takes a very long time to start on docs sites with thousands of MDX files. Chronicle mirrors project content by creating a symlink for every single file, and per-file symlinks are slow on Windows — so startup drags and iteration feels sluggish.

Solution

  • Link whole content directories instead of individual files (~1000+ filesystem ops down to a handful). On Windows this uses NTFS junctions, which need no admin rights.
  • Upgrade Vite to 8.1.5 — the older bundler didn't pick up content through directory symlinks during builds — and bump Nitro alongside it.
  • Allow the dev server to load its runtime files under Vite 8.1's stricter file-access rules.

Testing

  • CI now runs the test suite on Linux, macOS, and Windows
  • Smoke tests now assert real page content is served (previously only a health check, which missed empty-content regressions)
  • All 200 tests pass locally; dev/build/start verified with the example site

🤖 Generated with Claude Code

rsbh and others added 3 commits July 16, 2026 11:25
…p on Windows

Replace recursive per-file symlink mirroring with single directory-level
symlinks (junctions on Windows). Reduces ~1000+ filesystem ops to ~1-3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chronicle Ready Ready Preview, Comment Jul 17, 2026 3:12am

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Content mirroring now links complete content directories instead of individual files, with symlink-aware cleanup. Vite discovers SSR runtime dependency directories for filesystem access, package tooling is updated, and tests and CI add mirror, server-content, and package-test coverage.

Changes

Content mirror and SSR runtime support

Layer / File(s) Summary
Directory symlink mirroring
packages/chronicle/src/cli/utils/scaffold.ts
Configured latest and versioned content roots are linked directly using platform-appropriate directory symlinks, with symlink-aware cleanup.
Mirror behavior validation
packages/chronicle/src/cli/utils/scaffold.test.ts, .github/workflows/ci.yml
Tests verify directory targets, content accessibility, versioned mirrors, idempotency, legacy replacement, and missing sources; CI runs package tests and checks server content responses.
SSR runtime dependency access
packages/chronicle/src/server/vite-config.ts
Runtime dependency directories are resolved recursively and added to Vite’s filesystem allowlist, while SSR exclusions use a shared constant.
Package tooling alignment
packages/chronicle/package.json
Nitro and Vite are updated, Rolldown is added, and the @base-ui/react dependency entry is relocated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: rohilsurana

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: directory-level symlinks to speed up Windows dev startup.
Description check ✅ Passed The description accurately covers the symlink change, Vite/Nitro bumps, stricter file access, and CI testing updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/faster-dev-windows

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/chronicle/src/cli/utils/scaffold.ts (1)

54-92: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Fix Windows EPERM crash by using native fs.rm.

The manual recursive symlink cleanup will fail on Windows because Node.js's fs.unlink() throws an EPERM error when attempting to delete a directory symlink or junction. Additionally, calling fs.lstat() in the loop is redundant since fs.readdir with withFileTypes: true returns Dirent objects that natively provide isSymbolicLink() and isDirectory().

In modern Node.js, fs.rm(path, { recursive: true, force: true }) natively and safely removes directory trees, including symlinks and junctions, without traversing into their targets. It also implicitly handles missing paths (ENOENT). You can replace this entire block with a single native call to prevent the Windows crash and simplify the code.

♻️ Proposed fix to simplify and fix cleanup
-async function removeMirror(mirrorRoot: string): Promise<void> {
-  try {
-    const stat = await fs.lstat(mirrorRoot);
-    if (stat.isSymbolicLink() || stat.isFile()) {
-      await fs.unlink(mirrorRoot);
-    } else if (stat.isDirectory()) {
-      const entries = await fs.readdir(mirrorRoot, { withFileTypes: true });
-      for (const entry of entries) {
-        const entryPath = path.join(mirrorRoot, entry.name);
-        const entryStat = await fs.lstat(entryPath);
-        if (entryStat.isSymbolicLink()) {
-          await fs.unlink(entryPath);
-        } else if (entryStat.isDirectory()) {
-          await cleanDirSymlinks(entryPath);
-          await fs.rm(entryPath, { recursive: true, force: true });
-        } else {
-          await fs.unlink(entryPath);
-        }
-      }
-      await fs.rm(mirrorRoot, { recursive: true, force: true });
-    }
-  } catch (error) {
-    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
-    throw error;
-  }
+async function removeMirror(mirrorRoot: string): Promise<void> {
+  await fs.rm(mirrorRoot, { recursive: true, force: true });
 }
-
-async function cleanDirSymlinks(dir: string): Promise<void> {
-  const entries = await fs.readdir(dir, { withFileTypes: true });
-  for (const entry of entries) {
-    const entryPath = path.join(dir, entry.name);
-    const entryStat = await fs.lstat(entryPath);
-    if (entryStat.isSymbolicLink()) {
-      await fs.unlink(entryPath);
-    } else if (entryStat.isDirectory()) {
-      await cleanDirSymlinks(entryPath);
-    }
-  }
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chronicle/src/cli/utils/scaffold.ts` around lines 54 - 92, Replace
the manual cleanup logic in removeMirror, including cleanDirSymlinks, with a
single fs.rm call using recursive and force options. Remove the redundant lstat,
readdir, and unlink traversal so directory symlinks and junctions are handled
safely on Windows while missing paths remain ignored.
🧹 Nitpick comments (1)
packages/chronicle/src/cli/utils/scaffold.ts (1)

44-51: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Ensure parent directories exist before symlinking.

If a content configuration specifies a nested output directory (e.g., dir: 'category/docs'), fs.symlink will fail with an ENOENT error because the parent directory has not been created yet in the mirror root.

Consider defensively creating the parent directory of dest before linking to ensure robust behavior for all configurations.

🛠️ Proposed defensive fix
 async function linkDir(source: string, dest: string): Promise<void> {
   try {
     await fs.access(source);
   } catch {
     throw new Error(`Content directory not found: ${source}`);
   }
+  await fs.mkdir(path.dirname(dest), { recursive: true });
   const type = process.platform === 'win32' ? 'junction' : 'dir';
   await fs.symlink(source, dest, type);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chronicle/src/cli/utils/scaffold.ts` around lines 44 - 51, Update
linkDir to create the parent directory of dest before calling fs.symlink, using
recursive directory creation so nested output paths such as category/docs work
reliably. Preserve the existing source validation and symlink type selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/chronicle/src/cli/utils/scaffold.ts`:
- Around line 54-92: Replace the manual cleanup logic in removeMirror, including
cleanDirSymlinks, with a single fs.rm call using recursive and force options.
Remove the redundant lstat, readdir, and unlink traversal so directory symlinks
and junctions are handled safely on Windows while missing paths remain ignored.

---

Nitpick comments:
In `@packages/chronicle/src/cli/utils/scaffold.ts`:
- Around line 44-51: Update linkDir to create the parent directory of dest
before calling fs.symlink, using recursive directory creation so nested output
paths such as category/docs work reliably. Preserve the existing source
validation and symlink type selection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0cc84674-9fc0-4634-8342-813eb9da2987

📥 Commits

Reviewing files that changed from the base of the PR and between cc7882b and ff403e0.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • packages/chronicle/src/cli/utils/scaffold.test.ts
  • packages/chronicle/src/cli/utils/scaffold.ts

rsbh and others added 2 commits July 16, 2026 21:09
…nt dirs

Rolldown's native import.meta.glob (vite 8.0.3 / rolldown 1.0.0-rc.12) does
not crawl through directory symlinks at build time, so the directory-level
.content mirror produced builds with zero content pages. Fixed upstream in
rolldown 1.1.5 (vite 8.1.5). The dev server's JS glob was unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Health-only checks stayed green while builds shipped zero content pages;
only the Docker smoke test caught it. Assert the index redirect serves a
page containing content links and that the page API returns frontmatter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rsbh and others added 2 commits July 17, 2026 08:28
Vite 8.1 enforces server.fs.allow for SSR module-runner imports. Nitro's
dev runtime and ssr.noExternal packages load by absolute path from wherever
the package manager hoisted them, which the previous allow list blocked,
failing every dev SSR request with "Failed to load url ... dev-entry.mjs".
Resolve those packages and their transitive runtime dependencies instead of
allowing all of node_modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ange

Nitro accepts rolldown ^1.1.0 while vite 8.1.5 requires ~1.1.5; fresh
installs resolve the just-released rolldown 1.2.0 for nitro, mixing two
rolldown copies in one dev pipeline and crashing every SSR request in
vite:css-post. A direct ~1.1.5 dependency makes flat installs hoist a
single compatible copy. Drop once nitro and vite agree on a range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/chronicle/src/server/vite-config.ts`:
- Around line 59-84: Update the package traversal loop to track visited packages
by resolved directory rather than the package name. Move the duplicate check
until after resolution succeeds, use the existing dirs Set to skip directories
already processed, and remove or replace the seen name-based tracking so
alternate versions of the same dependency are traversed and added.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa82ae19-293a-4310-9b67-963fec4be2a4

📥 Commits

Reviewing files that changed from the base of the PR and between 0098304 and 7a92804.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • packages/chronicle/package.json
  • packages/chronicle/src/server/vite-config.ts

Comment thread packages/chronicle/src/server/vite-config.ts Outdated
- removeMirror: replace manual symlink walk with fs.rm (doesn't follow
  symlinks, handles junctions and missing paths)
- linkDir: create parent dirs so nested content dirs work
- resolveRuntimeDepDirs: dedupe by resolved dir instead of package name
  so isolated layouts with multiple versions of a dep are all allowed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant