diff --git a/.changeset/popular-lobsters-agree.md b/.changeset/popular-lobsters-agree.md
new file mode 100644
index 0000000000..35e4c5c8fe
--- /dev/null
+++ b/.changeset/popular-lobsters-agree.md
@@ -0,0 +1,5 @@
+---
+"@react-router/dev": patch
+---
+
+Fix `reveal` and `routes` CLI commands
diff --git a/packages/react-router-dev/__tests__/cli-reveal-test.ts b/packages/react-router-dev/__tests__/cli-reveal-test.ts
new file mode 100644
index 0000000000..5aa40f4f81
--- /dev/null
+++ b/packages/react-router-dev/__tests__/cli-reveal-test.ts
@@ -0,0 +1,77 @@
+import path from "node:path";
+import fse from "fs-extra";
+import execa from "execa";
+
+function getProjectDir() {
+ let projectDir = path.join(
+ __dirname,
+ ".tmp",
+ `reveal-test-${Math.random().toString(32).slice(2)}`
+ );
+ fse.copySync(path.join(__dirname, "fixtures", "basic"), projectDir);
+ return projectDir;
+}
+
+async function runCli(cwd: string, args: string[]) {
+ return await execa(
+ "node",
+ [
+ "--require",
+ require.resolve("esbuild-register"),
+ path.resolve(__dirname, "../cli/index.ts"),
+ ...args,
+ ],
+ { cwd }
+ );
+}
+
+describe("the reveal command", () => {
+ it("generates an entry.server.tsx file in the app directory", async () => {
+ let projectDir = getProjectDir();
+
+ let entryClientFile = path.join(projectDir, "app", "entry.client.tsx");
+ let entryServerFile = path.join(projectDir, "app", "entry.server.tsx");
+
+ expect(fse.existsSync(entryServerFile)).toBeFalsy();
+ expect(fse.existsSync(entryClientFile)).toBeFalsy();
+
+ await runCli(projectDir, ["reveal"]);
+
+ expect(fse.existsSync(entryServerFile)).toBeTruthy();
+ expect(fse.existsSync(entryClientFile)).toBeTruthy();
+ });
+
+ it("generates an entry.server.tsx file in the app directory when specific entries are provided", async () => {
+ let projectDir = getProjectDir();
+
+ let entryClientFile = path.join(projectDir, "app", "entry.client.tsx");
+ let entryServerFile = path.join(projectDir, "app", "entry.server.tsx");
+
+ expect(fse.existsSync(entryServerFile)).toBeFalsy();
+ expect(fse.existsSync(entryClientFile)).toBeFalsy();
+
+ await runCli(projectDir, ["reveal", "entry.server"]);
+ expect(fse.existsSync(entryServerFile)).toBeTruthy();
+ expect(fse.existsSync(entryClientFile)).toBeFalsy();
+ fse.removeSync(entryServerFile);
+
+ await runCli(projectDir, ["reveal", "entry.client"]);
+ expect(fse.existsSync(entryClientFile)).toBeTruthy();
+ expect(fse.existsSync(entryServerFile)).toBeFalsy();
+ });
+
+ it("generates an entry.server.jsx file in the app directory", async () => {
+ let projectDir = getProjectDir();
+
+ let entryClientFile = path.join(projectDir, "app", "entry.client.jsx");
+ let entryServerFile = path.join(projectDir, "app", "entry.server.jsx");
+
+ expect(fse.existsSync(entryServerFile)).toBeFalsy();
+ expect(fse.existsSync(entryClientFile)).toBeFalsy();
+
+ await runCli(projectDir, ["reveal", "--no-typescript"]);
+
+ expect(fse.existsSync(entryServerFile)).toBeTruthy();
+ expect(fse.existsSync(entryClientFile)).toBeTruthy();
+ });
+});
diff --git a/packages/react-router-dev/__tests__/cli-routes-test.ts b/packages/react-router-dev/__tests__/cli-routes-test.ts
new file mode 100644
index 0000000000..be7c1b5abd
--- /dev/null
+++ b/packages/react-router-dev/__tests__/cli-routes-test.ts
@@ -0,0 +1,31 @@
+import path from "node:path";
+import execa from "execa";
+
+async function runCli(cwd: string, args: string[]) {
+ return await execa(
+ "node",
+ [
+ "--require",
+ require.resolve("esbuild-register"),
+ path.resolve(__dirname, "../cli/index.ts"),
+ ...args,
+ ],
+ { cwd }
+ );
+}
+
+describe("the routes command", () => {
+ it("displays routes", async () => {
+ let projectDir = path.join(__dirname, "fixtures", "basic");
+
+ let result = await runCli(projectDir, ["routes"]);
+
+ expect(result.stdout).toMatchInlineSnapshot(`
+ "
+
+
+
+ "
+ `);
+ });
+});
diff --git a/packages/react-router-dev/__tests__/fixtures/node/.gitignore b/packages/react-router-dev/__tests__/fixtures/basic/.gitignore
similarity index 54%
rename from packages/react-router-dev/__tests__/fixtures/node/.gitignore
rename to packages/react-router-dev/__tests__/fixtures/basic/.gitignore
index 3f7bf98da3..752e5fe866 100644
--- a/packages/react-router-dev/__tests__/fixtures/node/.gitignore
+++ b/packages/react-router-dev/__tests__/fixtures/basic/.gitignore
@@ -1,6 +1,6 @@
node_modules
-/.cache
/build
-/public/build
.env
+
+.react-router/
diff --git a/packages/react-router-dev/__tests__/fixtures/node/app/root.tsx b/packages/react-router-dev/__tests__/fixtures/basic/app/root.tsx
similarity index 100%
rename from packages/react-router-dev/__tests__/fixtures/node/app/root.tsx
rename to packages/react-router-dev/__tests__/fixtures/basic/app/root.tsx
diff --git a/packages/react-router-dev/__tests__/fixtures/basic/app/routes.ts b/packages/react-router-dev/__tests__/fixtures/basic/app/routes.ts
new file mode 100644
index 0000000000..49b6a0482f
--- /dev/null
+++ b/packages/react-router-dev/__tests__/fixtures/basic/app/routes.ts
@@ -0,0 +1,5 @@
+// Note that since this is used in a unit test context, we don't have access to
+// the `dev` build yet, so we can't import from `@react-router/dev/routes`.
+const routes = [{ file: "routes/_index.tsx", index: true }];
+
+export default routes;
diff --git a/packages/react-router-dev/__tests__/fixtures/node/app/routes/_index.tsx b/packages/react-router-dev/__tests__/fixtures/basic/app/routes/_index.tsx
similarity index 100%
rename from packages/react-router-dev/__tests__/fixtures/node/app/routes/_index.tsx
rename to packages/react-router-dev/__tests__/fixtures/basic/app/routes/_index.tsx
diff --git a/packages/react-router-dev/__tests__/fixtures/node/package.json b/packages/react-router-dev/__tests__/fixtures/basic/package.json
similarity index 100%
rename from packages/react-router-dev/__tests__/fixtures/node/package.json
rename to packages/react-router-dev/__tests__/fixtures/basic/package.json
diff --git a/packages/react-router-dev/__tests__/fixtures/basic/public/favicon.ico b/packages/react-router-dev/__tests__/fixtures/basic/public/favicon.ico
new file mode 100644
index 0000000000..5dbdfcddcb
Binary files /dev/null and b/packages/react-router-dev/__tests__/fixtures/basic/public/favicon.ico differ
diff --git a/packages/react-router-dev/__tests__/fixtures/node/tsconfig.json b/packages/react-router-dev/__tests__/fixtures/basic/tsconfig.json
similarity index 83%
rename from packages/react-router-dev/__tests__/fixtures/node/tsconfig.json
rename to packages/react-router-dev/__tests__/fixtures/basic/tsconfig.json
index 80f487a8a3..fccecf035b 100644
--- a/packages/react-router-dev/__tests__/fixtures/node/tsconfig.json
+++ b/packages/react-router-dev/__tests__/fixtures/basic/tsconfig.json
@@ -1,7 +1,8 @@
{
- "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@react-router/node", "vite/client"],
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"jsx": "react-jsx",
diff --git a/packages/react-router-dev/__tests__/fixtures/node/README.md b/packages/react-router-dev/__tests__/fixtures/node/README.md
deleted file mode 100644
index da8d02ad77..0000000000
--- a/packages/react-router-dev/__tests__/fixtures/node/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Welcome to Remix!
-
-- [Remix Docs](https://remix.run/docs)
-
-## Development
-
-From your terminal:
-
-```sh
-npm run dev
-```
-
-This starts your app in development mode, rebuilding assets on file changes.
-
-## Deployment
-
-First, build your app for production:
-
-```sh
-npm run build
-```
-
-Then run the app in production mode:
-
-```sh
-npm start
-```
-
-Now you'll need to pick a host to deploy it to.
-
-### DIY
-
-If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
-
-Make sure to deploy the output of `remix build`
-
-- `build/`
-- `public/build/`
diff --git a/packages/react-router-dev/__tests__/fixtures/node/public/favicon.ico b/packages/react-router-dev/__tests__/fixtures/node/public/favicon.ico
deleted file mode 100644
index 8830cf6821..0000000000
Binary files a/packages/react-router-dev/__tests__/fixtures/node/public/favicon.ico and /dev/null differ
diff --git a/packages/react-router-dev/__tests__/fixtures/node/remix.env.d.ts b/packages/react-router-dev/__tests__/fixtures/node/remix.env.d.ts
deleted file mode 100644
index fc1bbb27ff..0000000000
--- a/packages/react-router-dev/__tests__/fixtures/node/remix.env.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-///
-///
diff --git a/packages/react-router-dev/__tests__/reveal-test.ts b/packages/react-router-dev/__tests__/reveal-test.ts
deleted file mode 100644
index dd5d2880fd..0000000000
--- a/packages/react-router-dev/__tests__/reveal-test.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import os from "node:os";
-import path from "node:path";
-import fse from "fs-extra";
-
-import { run } from "../cli/run";
-
-const TEMP_DIR = path.join(
- fse.realpathSync(os.tmpdir()),
- `remix-tests-${Math.random().toString(32).slice(2)}`
-);
-
-beforeAll(async () => {
- await fse.remove(TEMP_DIR);
- await fse.ensureDir(TEMP_DIR);
-});
-
-afterAll(async () => {
- await fse.remove(TEMP_DIR);
-});
-
-let originalLog = console.log;
-let originalWarn = console.warn;
-let originalError = console.error;
-
-beforeEach(async () => {
- console.log = jest.fn();
- console.warn = jest.fn();
- console.error = jest.fn();
-});
-
-afterEach(() => {
- console.log = originalLog;
- console.warn = originalWarn;
- console.error = originalError;
-});
-
-// TODO: Migrate this to an integration test now that it relies on Vite to resolve config
-describe.skip("the reveal command", () => {
- let tempDirs = new Set();
- let originalCwd = process.cwd();
-
- beforeEach(() => {
- process.chdir(TEMP_DIR);
- });
-
- afterEach(async () => {
- process.chdir(originalCwd);
- for (let dir of tempDirs) {
- await fse.remove(dir);
- }
- tempDirs = new Set();
- });
-
- async function getProjectDir(name: string) {
- let tmpDir = path.join(TEMP_DIR, name);
- tempDirs.add(tmpDir);
- return tmpDir;
- }
-
- let runtimes = ["node", "cloudflare", "deno"] as const;
-
- for (let runtime of runtimes) {
- it(`generates a "${runtime}" specific entry.server.tsx file in the app directory`, async () => {
- let projectDir = await getProjectDir(`entry.server.${runtime}`);
- fse.copySync(path.join(__dirname, "fixtures", runtime), projectDir);
-
- let entryClientFile = path.join(projectDir, "app", "entry.client.tsx");
- let entryServerFile = path.join(projectDir, "app", "entry.server.tsx");
-
- expect(fse.existsSync(entryServerFile)).toBeFalsy();
- expect(fse.existsSync(entryClientFile)).toBeFalsy();
-
- await run(["reveal", "entry.server", projectDir]);
- await run(["reveal", "entry.client", projectDir]);
-
- expect(fse.existsSync(entryServerFile)).toBeTruthy();
- expect(fse.existsSync(entryClientFile)).toBeTruthy();
- });
-
- it(`generates a "${runtime}" specific entry.server.jsx file in the app directory`, async () => {
- let projectDir = await getProjectDir(`entry.server.${runtime}-js`);
- fse.copySync(path.join(__dirname, "fixtures", runtime), projectDir);
-
- let entryClientFile = path.join(projectDir, "app", "entry.client.jsx");
- let entryServerFile = path.join(projectDir, "app", "entry.server.jsx");
-
- expect(fse.existsSync(entryServerFile)).toBeFalsy();
- expect(fse.existsSync(entryClientFile)).toBeFalsy();
-
- await run(["reveal", "entry.server", projectDir, "--no-typescript"]);
- await run(["reveal", "entry.client", projectDir, "--no-typescript"]);
-
- expect(fse.existsSync(entryServerFile)).toBeTruthy();
- expect(fse.existsSync(entryClientFile)).toBeTruthy();
- });
- }
-});
diff --git a/packages/react-router-dev/cli/commands.ts b/packages/react-router-dev/cli/commands.ts
index 2ee8e02874..62005266eb 100644
--- a/packages/react-router-dev/cli/commands.ts
+++ b/packages/react-router-dev/cli/commands.ts
@@ -3,12 +3,14 @@ import fse from "fs-extra";
import PackageJson from "@npmcli/package-json";
import exitHook from "exit-hook";
import colors from "picocolors";
+// Workaround for "ERR_REQUIRE_CYCLE_MODULE" in Node 22.10.0+
+import "react-router";
import type { ViteDevOptions } from "../vite/dev";
import type { ViteBuildOptions } from "../vite/build";
+import { loadConfig } from "../config/config";
import { formatRoutes } from "../config/format";
import type { RoutesFormat } from "../config/format";
-import { loadPluginContext } from "../vite/plugin";
import { transpile as convertFileToJS } from "./useJavascript";
import * as profiler from "../vite/profiler";
import * as Typegen from "../typegen";
@@ -21,20 +23,16 @@ export async function routes(
json?: boolean;
} = {}
): Promise {
- let ctx = await loadPluginContext({
- root: reactRouterRoot,
- configFile: flags.config,
- });
+ let rootDirectory = reactRouterRoot ?? process.cwd();
+ let configResult = await loadConfig({ rootDirectory });
- if (!ctx) {
- console.error(
- colors.red("React Router Vite plugin not found in Vite config")
- );
+ if (!configResult.ok) {
+ console.error(colors.red(configResult.error));
process.exit(1);
}
let format: RoutesFormat = flags.json ? "json" : "jsx";
- console.log(formatRoutes(ctx.reactRouterConfig.routes, format));
+ console.log(formatRoutes(configResult.value.routes, format));
}
export async function build(
@@ -78,21 +76,13 @@ let conjunctionListFormat = new Intl.ListFormat("en", {
});
export async function generateEntry(
- entry: string,
- reactRouterRoot: string,
+ entry?: string,
+ reactRouterRoot?: string,
flags: {
typescript?: boolean;
config?: string;
} = {}
) {
- let ctx = await loadPluginContext({
- root: reactRouterRoot,
- configFile: flags.config,
- });
-
- let rootDirectory = ctx.rootDirectory;
- let appDirectory = ctx.reactRouterConfig.appDirectory;
-
// if no entry passed, attempt to create both
if (!entry) {
await generateEntry("entry.client", reactRouterRoot, flags);
@@ -100,6 +90,16 @@ export async function generateEntry(
return;
}
+ let rootDirectory = reactRouterRoot ?? process.cwd();
+ let configResult = await loadConfig({ rootDirectory });
+
+ if (!configResult.ok) {
+ console.error(colors.red(configResult.error));
+ return;
+ }
+
+ let appDirectory = configResult.value.appDirectory;
+
if (!entries.includes(entry)) {
let entriesArray = Array.from(entries);
let list = conjunctionListFormat.format(entriesArray);
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 4e605621b0..ca0a4a95e8 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -76,46 +76,6 @@ export async function extractPluginContext(viteConfig: Vite.ResolvedConfig) {
| undefined;
}
-export async function loadPluginContext({
- configFile,
- root,
-}: {
- configFile?: string;
- root?: string;
-}) {
- if (!root) {
- root = process.env.REACT_ROUTER_ROOT || process.cwd();
- }
-
- configFile =
- configFile ??
- findConfig(root, "vite.config", [
- ".ts",
- ".cts",
- ".mts",
- ".js",
- ".cjs",
- ".mjs",
- ]);
-
- if (!configFile) {
- console.error(colors.red("Vite config file not found"));
- process.exit(1);
- }
-
- let viteConfig = await resolveViteConfig({ configFile, root });
- let ctx = await extractPluginContext(viteConfig);
-
- if (!ctx) {
- console.error(
- colors.red("React Router Vite plugin not found in Vite config")
- );
- process.exit(1);
- }
-
- return ctx;
-}
-
const SERVER_ONLY_ROUTE_EXPORTS = ["loader", "action", "headers"];
const CLIENT_ROUTE_EXPORTS = [
"clientAction",