Skip to content

react-router dev crashes with ENOTEMPTY when typegen runs in parallel #15283

Description

@borngraced

Description

react-router dev can crash when another process runs react-router typegen at the same time. The failure is a filesystem race on .react-router/types:

Error: ENOTEMPTY: directory not empty, rmdir '.react-router/types/app'

This happens because the dev-server typegen watcher and a standalone typegen invocation both try to remove and rewrite the same generated directories concurrently.

Reproduction

  1. Start the dev server in a Framework Mode app:

    react-router dev
  2. While dev is running, trigger typegen from another process (common in real projects):

    react-router typegen

    Or run a script that shells out to typegen, e.g.:

    "tsc": "react-router typegen && tsgo"

    with a pre-commit hook that also runs react-router typegen before tsc.

  3. Repeat a few times or trigger a route-config change in dev while typegen is running.

Expected: Both processes complete; generated types stay consistent.

Actual: Dev server or typegen crashes with ENOTEMPTY when one process rmdirs .react-router/types/app (or the whole types/ tree) while the other is still writing.

Root cause

In @react-router/dev typegen/index.ts:

  • run() and watch() call fs.rm(typesDirectory, { recursive: true }) before writing
  • watch() also calls clearRouteModuleAnnotations() on route-config changes, which removes types/<appDirectory>/
  • There is no cross-process coordination between the Vite plugin watcher and CLI typegen

Proposed fix

  1. Serialize typegen writes with a cross-process lock outside the generated tree (e.g. .react-router/.typegen.lock), so dev watcher and CLI typegen cannot overlap destructive writes
  2. Retry fs.rm on retryable errors (ENOTEMPTY, EBUSY, EPERM) with short backoff before surfacing the error

Environment

  • @react-router/dev: 8.1.0
  • Node: >= 22
  • OS: macOS (also reported on other platforms with parallel file operations)

Happy to open a PR with lock + retry if this approach looks good.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions