Skip to content

fix(dev): avoid app reloads for colocated files#1014

Merged
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:nathan/app-watcher-route-files
May 2, 2026
Merged

fix(dev): avoid app reloads for colocated files#1014
james-elicx merged 1 commit intocloudflare:mainfrom
NathanDrake2406:nathan/app-watcher-route-files

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

What this changes

Stops the App Router dev watcher from invalidating the generated route entry and sending a full reload when ordinary colocated files are added or removed under app/, such as app/components/Button.tsx.

The watcher now invalidates only files that can affect App Router structure or generated metadata route entries: route conventions such as page, route, layout, template, loading, error, access fallback files, root global-error, and vinext metadata route files.

Fixes #1013.

Why

Next.js explicitly treats arbitrary project files in app/ as safely colocated and non-routable. The source backs that up in the route discovery matcher:

vinext was using only the broad page extension regex for app/ watcher add/unlink events, so any .ts, .tsx, .js, or .jsx file under app/ looked route-affecting and triggered a full reload.

Approach

Add a small pure predicate, shouldInvalidateAppRouteFile, that mirrors vinext's current App Router and metadata discovery boundaries:

  • rejects files outside the exact app/ directory, fixing sibling-prefix cases like app-utils/
  • rejects private _ folders
  • accepts App Router structure conventions that change the generated route tree or RSC entry wiring
  • accepts vinext's dynamic and static metadata route files using the existing metadata route map

The watcher still keeps the broad extension behavior for the Pages Router, since every matching file under pages/ is routable by design unless filtered elsewhere.

Validation

  • vp test run tests/file-matcher.test.ts
  • vp test run tests/file-matcher.test.ts tests/page-extensions-routing.test.ts
  • vp check tests/file-matcher.test.ts packages/vinext/src/server/dev-route-files.ts packages/vinext/src/index.ts packages/vinext/src/server/metadata-routes.ts
  • vp run vinext#build

The package build completed successfully with the existing virtual-module externalization warnings for private-next-instrumentation-client, virtual:vinext-rsc-entry, and virtual:vite-rsc/client-references.

Risks / follow-ups

The metadata side intentionally follows vinext's current scanMetadataFiles behavior, not every future Next.js metadata convention. If vinext expands metadata route support later, this predicate should be updated with the same source of truth.

Adding or removing any JavaScript or TypeScript file under app currently invalidates the App Router route cache and sends a full reload. That is wrong for colocated components and helpers because the App Router only treats specific file conventions and metadata files as route structure.

The watcher was using the broad page extension matcher for app files, which confused ordinary project files with route-affecting files. Add a dev route-file predicate aligned with vinext route and metadata discovery, then use it before invalidating the generated RSC entry and root params modules.

Tests cover route convention files, metadata files, private folders, sibling directory prefix safety, and colocated non-route files.
Copilot AI review requested due to automatic review settings May 2, 2026 11:45
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1014

commit: 1d72b85

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Clean, well-scoped fix. The predicate accurately mirrors the App Router conventions vinext currently supports, the test coverage is good, and extracting invalidateAppRoutingModules() is a nice cleanup. A few minor notes below, none blocking.

const routePrefix = visibleRoutePrefix(parts);
for (const [metaType, config] of Object.entries(METADATA_FILE_MAP)) {
if (!matchMetadataFileBaseName(metaType, baseName)) continue;
if (!config.nestable && routePrefix !== "") return false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: isMetadataRouteFile correctly returns false when !config.nestable && routePrefix !== "", but it does so only for the first matching metaType. If a future METADATA_FILE_MAP entry happened to share a base name with an existing non-nestable entry, the early return false would prevent the loop from checking subsequent entries.

This can't happen today because all base names in the map are unique, but making the intent explicit would be a minor hardening:

Suggested change
if (!config.nestable && routePrefix !== "") return false;
if (!config.nestable && routePrefix !== "") continue;

This way, a non-nestable mismatch just moves on to the next entry instead of short-circuiting the entire function.

expect(shouldInvalidateAppRouteFile(appDir, "/project/app/@modal/default.tsx", matcher)).toBe(
true,
);
expect(shouldInvalidateAppRouteFile(appDir, "/project/app/robots.ts", matcher)).toBe(true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice that you cover the non-nestable robots.ts at root. Consider also adding a test for a nestable metadata route in a nested directory (e.g. app/blog/sitemap.xml -> true) to confirm the nestable path works end-to-end. Currently the only nested metadata test is opengraph-image.png, which covers static extensions but not dynamic nestable files.

Also worth adding a case for app/global-error.tsx -> true since that's a separate code path (isRootGlobalError) that isn't currently exercised in the test.


function visibleRoutePrefix(parts: readonly string[]): string {
const visibleParts = parts
.slice(0, -1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: intercepting route directories like (.)shop and (..)settings use the ( prefix but don't end with ), so they pass through the route-group filter here and contribute to the visible prefix. That seems correct — intercepting routes are real route segments — just noting it for future readers.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 2, 2026

Review submitted. Here's a summary of what I reviewed and the feedback:

Verdict: Approved

The PR correctly narrows the App Router dev watcher to only invalidate on files that actually affect routing — structure files (page, layout, route, template, loading, error, not-found, forbidden, unauthorized, default), root global-error, and metadata route files. This prevents unnecessary full reloads when colocated files like app/components/Button.tsx are added or removed.

Three minor comments posted:

  1. isMetadataRouteFile early return (line 68) — The return false for non-nestable metadata types short-circuits the entire loop. Today this is safe because base names are unique, but changing it to continue would be a small hardening against future map additions.

  2. Test coverage gaps — Suggested adding cases for app/blog/sitemap.xml (nestable metadata in nested dir) and app/global-error.tsx (exercises the isRootGlobalError path which currently has no direct test assertion).

  3. Intercepting routes in visibleRoutePrefix — Noted that intercepting route dirs like (.)shop correctly pass through the route-group filter since they aren't (group) patterns, which is the right behavior.

github run

@james-elicx james-elicx merged commit 257a5de into cloudflare:main May 2, 2026
29 of 30 checks passed
@NathanDrake2406 NathanDrake2406 deleted the nathan/app-watcher-route-files branch May 6, 2026 04:30
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.

adding any .ts/.tsx file inside app/ triggers a full page reload — watcher uses broad extension regex instead of route-file regex

3 participants