remix-run/react-router · error · Error

Could not resolve module ID for ${url}

Error message

Could not resolve module ID for ${url}

What it means

Thrown inside the child compiler's route-transform pipeline (`getRouteModuleExports`-style flow). React Router resolves each route file to a URL, calls `pluginContainer.resolveId(url, undefined, { ssr: true })`, and requires a non-null result. A null result means no plugin in the child Vite container recognizes the file as a loadable module, so React Router cannot transform it or read its exports.

Source

Thrown at packages/react-router-dev/vite/plugin.ts:542

): Promise<string> => {
  if (!viteChildCompiler) {
    throw new Error("Vite child compiler not found");
  }

  // We transform the route module code with the Vite child compiler so that we
  // can parse the exports from non-JS files like MDX. This ensures that we can
  // understand the exports from anything that Vite can compile to JS, not just
  // the route file formats that the Remix compiler historically supported.

  let ssr = true;
  let { pluginContainer, moduleGraph } = viteChildCompiler;

  let routePath = path.resolve(ctx.reactRouterConfig.appDirectory, routeFile);
  let url = resolveFileUrl(ctx, routePath);

  let resolveId = async () => {
    let result = await pluginContainer.resolveId(url, undefined, { ssr });
    if (!result) throw new Error(`Could not resolve module ID for ${url}`);
    return result.id;
  };

  let [id, code] = await Promise.all([
    resolveId(),
    readRouteFile?.() ?? readFile(routePath, "utf-8"),
    // pluginContainer.transform(...) fails if we don't do this first:
    moduleGraph.ensureEntryFromUrl(url, ssr),
  ]);

  let transformed = await pluginContainer.transform(code, id, { ssr });
  return transformed.code;
};

const getRouteModuleExports = async (
  viteChildCompiler: Vite.ViteDevServer | null,
  ctx: ReactRouterPluginContext,
  routeFile: string,

View on GitHub (pinned to 1fd704a7da)

Solutions

  1. Verify the route file exists at the printed absolute path with `ls <routePath>`.
  2. Confirm the file extension is supported by a plugin in the Vite config (e.g. add `mdx()` for `.mdx`).
  3. Check `appDirectory` in your config matches where `routes.ts`/route files live.
  4. For custom extensions, add a Vite plugin that handles `resolveId`/`load` for that extension.
  5. Reproduce the resolved URL and run it through `vite.resolveId` in a REPL to see which plugin rejects it.

Example fix

// before: routes.ts references a non-existent file
route("/about", "./about-us.tsx");
// after
route("/about", "./about.tsx");
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync } from "node:fs";
import path from "node:path";
const assertRouteFileExists = (appDir: string, file: string) => {
  const abs = path.resolve(appDir, file);
  if (!existsSync(abs)) throw new Error(`Route file missing: ${abs}`);
};
// iterate your routes.ts entries and call this for each

Type guard

const isResolvable = (ext: string, supported: string[]): boolean =>
  supported.includes(ext);

Prevention

When it happens

Trigger: Route file path does not exist on disk (typo in `routes.ts` or a `flatRoutes()` glob mismatch); the file extension is not handled by any Vite plugin (e.g. `.mdx` without `@mdx-js/rollup` registered); the route sits outside `appDirectory`; an alias used in the route path is not resolvable; the file is git-LFS'd or unreadable.

Common situations: Renaming a route file but forgetting to update a manual `route()` entry in `routes.ts`; adding `.mdx` routes before installing the MDX plugin; symlinking routes from outside the app directory; case-sensitivity mismatches between macOS (case-insensitive) and Linux CI.

Related errors


AI-assisted analysis of remix-run/react-router@1fd704a7da (2026-08-12). Data as JSON: /api/errors/10b8b8088cc8db26. Report an issue: GitHub.