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",